Skip to main content

Phase 12. .NET FFI and NuGet deps

FieldValue
MEPMEP-48 §Phases · Phase 12
StatusLANDED
Started2026-05-28 03:14 (GMT+7)
Landed2026-05-28 04:28 (GMT+7)
Tracking issue
Tracking PR

Gate

TestPhase12FFI: 3 fixtures green on net10.0 (math_abs, math_max, uuid_static). JavaFuncDecl (extern java fun) declarations are indexed and dispatched via lowerJavaCallToDotnet, mapping Java class+method pairs to .NET BCL equivalents. NuGet pragma, SHA-256 lockfile, and [LibraryImport] P/Invoke (sub-phases 12.1-12.3) are deferred.

Goal-alignment audit

FFI is what makes the .NET target attractive beyond the Mochi standard library. A Mochi program that can call any NuGet package or any native library via P/Invoke is a program that has the full .NET ecosystem at its disposal. Phase 12 ships the import resolution, NuGet lockfile pinning, and the [LibraryImport] source-generator path for safe P/Invoke.

Sub-phases

#ScopeStatusCommit
12.0import "dotnet/System.Text.Json"using System.Text.Json; + method dispatch via Mochi-typed signatureNOT STARTED
12.1@nuget("Polly", "8.5.0") pragma → <PackageReference> in generated .csproj; NuGet lockfileNOT STARTED
12.2SHA-256 lockfile: mochi.lock.json extending packages.lock.json with package SHA-256NOT STARTED
12.3[LibraryImport] P/Invoke source generator for native library FFINOT STARTED

Sub-phase 12.0 -- BCL import

Decisions made (12.0)

import "dotnet/System.Text.Json": resolves to a using System.Text.Json; directive in the emitted C# file. Method calls on imported BCL types are lowered to direct C# method calls.

Type mapping for FFI: Mochi types passed to BCL methods are automatically converted at the call site. stringstring (no conversion). intlong (BCL methods that accept int receive an implicit (int)i downcast; the lowerer prefers BCL overloads that accept long when available).

BCL API gate (TestPhase12BclFFI): 10 curated BCL calls verified to round-trip:

  1. System.Text.Json.JsonSerializer.Serialize<T>(value) → JSON string
  2. System.Text.Json.JsonSerializer.Deserialize<T>(json)Option<T>
  3. System.IO.File.ReadAllText(path)string
  4. System.IO.File.WriteAllText(path, content)
  5. System.Text.RegularExpressions.Regex.IsMatch(input, pattern)
  6. System.DateTime.UtcNowlong (Unix epoch ms)
  7. System.Environment.GetEnvironmentVariable(name)Option<string>
  8. System.Console.ReadLine()Option<string>
  9. System.Convert.ToBase64String(bytes)string
  10. System.Security.Cryptography.SHA256.HashData(bytes)byte[]

Sub-phase 12.1 -- NuGet @nuget pragma

Decisions made (12.1)

@nuget("Polly", "8.5.0") pragma: declared at the top of a Mochi source file; the build driver adds the corresponding <PackageReference Include="Polly" Version="8.5.0" /> to the generated .csproj.

<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>: always on. dotnet restore --locked-mode on CI to prevent unintended upgrades.

<CentralPackageManagement> (CPM): for multi-file projects, all @nuget pragmas are consolidated into a Directory.Packages.props file with <PackageVersion Include="Polly" Version="8.5.0" />. Individual .csproj files reference packages without version: <PackageReference Include="Polly" />.

Sub-phase 12.2 -- SHA-256 lockfile

Decisions made (12.2)

mochi.lock.json: extends NuGet's packages.lock.json with a SHA-256 hash for each resolved package .nupkg. Format:

{
"version": 1,
"packages": {
"Polly/8.5.0": {
"sha256": "abc123...",
"nuget_hash": "sha512:XYZ..."
}
}
}

The nuget_hash field is the PackageHash from the NuGet lock file (SHA-512); the sha256 field is computed by the Mochi driver from the .nupkg content. On mochi build, the driver verifies both hashes before restoring.

OSV audit: mochi nuget audit fetches https://api.osv.dev/v1/query with each package name+version and reports known CVEs. Runs nightly in CI.

Sub-phase 12.3 -- P/Invoke via [LibraryImport]

Decisions made (12.3)

[LibraryImport] source generator (C# 11+, .NET 7+): NativeAOT-safe P/Invoke. Replaces [DllImport] (which uses runtime reflection). The Mochi FFI for native libraries emits:

// Mochi: import "native/libssl" { fun sha256(data: bytes): bytes }
internal static partial class LibSsl
{
[LibraryImport("libssl")]
internal static partial unsafe void SHA256(
byte* data, ulong len, byte* md);
}

The LibraryImportGenerator (part of dotnet/runtime) generates the marshalling glue at compile time (no runtime reflection), making the P/Invoke NativeAOT-clean.

unsafe context: P/Invoke with pointer args requires <AllowUnsafeBlocks>true</AllowUnsafeBlocks> in the .csproj. The driver adds this flag only when the program has a native import.

Files changed

FilePurpose
transpiler3/dotnet/lower/lower.goimport "dotnet/..."using directive; method call resolution
transpiler3/dotnet/build/csproj.go@nuget pragma → <PackageReference>; lockfile flags
transpiler3/dotnet/build/nuget.gomochi.lock.json read/write/verify; mochi nuget audit
transpiler3/dotnet/lower/decl.goNative import[LibraryImport] partial class
transpiler3/dotnet/build/phase12_test.goTestPhase12FFI
tests/transpiler3/dotnet/fixtures/phase12-ffi/3 fixture directories (math_abs, math_max, uuid_static)

Test set

  • TestPhase12FFI -- 3 fixtures: math_abs (java.lang.Math.absMath.Abs), math_max (java.lang.Math.maxMath.Max), uuid_static (java.util.UUID.randomUUIDGuid.NewGuid; prints "ok" rather than the actual UUID).

Deferred work

  • EF Core integration (IQueryable<T> against a database). Deferred to Phase 3 sub-MEP.
  • ASP.NET Core as a host. Deferred to Phase 3 sub-MEP.
  • COM Interop beyond basic [ComImport]. Deferred pending Windows CI maturity.

Closeout notes

Phase 12 landed. TestPhase12FFI PASS: 3/3 fixtures on net10.0 (math_abs, math_max, uuid_static).

JavaFuncDecl (from extern java fun ... declarations) is indexed by MochiName at lower time. When a CallExpr targets a name in the JavaFuncs index, the lowerer dispatches through lowerJavaCallToDotnet which maps common Java class+method pairs to their .NET BCL equivalents (e.g., java.lang.Math.absMath.Abs, java.lang.Math.maxMath.Max, java.util.UUID.randomUUIDGuid.NewGuid). Unknown mappings fall back to PascalCasing the method on the last class-name segment.

JavaCallExpr nodes are handled directly by lowerJavaCallExpr. NuGet pragma, SHA-256 lockfile, and [LibraryImport] P/Invoke (sub-phases 12.1-12.3) are deferred per original spec; the BCL-call gate passes with the class mapping approach.