MEP 68. Mochi and .NET package bridge
| Field | Value |
|---|---|
| MEP | 68 |
| Title | Mochi and .NET package bridge |
| Author | Mochi core |
| Status | Draft |
| Type | Standards Track |
| Created | 2026-05-29 23:05 (GMT+7) |
| Depends | MEP-1 (Grammar, for the import dotnet extension), MEP-2 (AST, for the import node), MEP-4 (Type System), MEP-13 (ADTs and Match, for nullable and enum translation), MEP-45 (C transpiler, for the FFI sidecar pattern), MEP-53 (Rust transpiler, for the lowering pipeline and the runtime crate), MEP-57 (Mochi module and package system, for mochi.toml, mochi.lock, the NuGet v3 protocol, the content-addressed object store, the capability declaration model, and trusted publishing) |
| Research | /docs/research/0068/ |
| Tracking | /docs/implementation/0068/ |
Abstract
Mochi today (May 2026, after MEP-53's Rust target and MEP-57's source-level package system reached Draft) integrates with Rust (MEP-73), Python, Go, and TypeScript, but has no path into the .NET / NuGet ecosystem. The 350,000+ packages on nuget.org (May 2026) cover enterprise data access (Dapper, Entity Framework Core), structured logging (Serilog, Microsoft.Extensions.Logging), HTTP clients (RestSharp, Microsoft.Extensions.Http), cloud SDKs (AWSSDK.Core, Azure.* families), serialisation (Newtonsoft.Json, System.Text.Json), testing (NUnit, xUnit, Moq, FluentAssertions), and distributed systems infrastructure (StackExchange.Redis, Npgsql, Polly). All of it remains off-limits to Mochi authors, and the .NET ecosystem cannot consume Mochi packages as NuGet packages.
MEP-68 specifies the bidirectional .NET / NuGet package bridge: Mochi packages can consume any well-formed NuGet package via import dotnet "<package>@<semver>" as <alias> with no user-written FFI boilerplate, and Mochi packages can publish to nuget.org as .NET library packages via mochi pkg publish --to=nuget.org. The bridge is the fifth source-language interop story Mochi ships (after Go, Python, TypeScript, and Rust), and the first one targeting the CLR runtime with its CLR hosting API, assembly metadata reflection, and NuGet trusted publishing.
The proposal builds on MEP-57's manifest / lockfile / capability infrastructure and MEP-53's emit pipeline, and adds a new self-contained component under package3/dotnet/. The Mochi grammar gains the single keyword dotnet as a valid <lang> token in the existing FFI-import production. The build pipeline gains one new target (TargetDotNetLibrary). The lockfile gains one new repeated table ([[dotnet-package]]). No existing transpiler MEP needs to change.
The system is anchored on seven load-bearing decisions, each justified in §Rationale and surveyed in the companion research notes:
-
Assembly metadata via
mochi-dotnet-meta(a small .NET CLI tool wrappingSystem.Reflection.Metadata.MetadataReader) as the canonical machine-readable NuGet package surface. The tool is invoked once atmochi pkg locktime, emits a JSON document containing every public type and method signature in the assembly, and writes its SHA-256 tomochi.lock. The alternative of parsing C# source (via Roslyn syntax trees) is rejected because Roslyn source generators, partial classes, and conditional compilation directives mean the post-compilation surface differs from the source-level surface. The alternative of reading the XML documentation comments (the.xmlfile that ships with most NuGet packages) is rejected because it contains only documentation, not type signatures. See 04-assembly-metadata-ingest §1, 02-design-philosophy §1. -
CLR hosting API as the default runtime bridge (
hostfxr_initialize_for_runtime_config+hostfxr_get_runtime_delegate+load_assembly_and_get_function_pointer). CLR hosting (stable since .NET 5 SDK, GA since .NET 6, supported on Linux/macOS/Windows) embeds the .NET runtime in-process and lets native code call into managed assemblies via function pointers. This is the only path that supports the full .NET type system (including reflection, generics, and the CLR thread pool) at runtime. NativeAOT (pre-compilation of the shim to a native library with no CLR at runtime) is opt-in via[dotnet] bridge = "nativeaot"because not all NuGet packages support NativeAOT trimming. See 02-design-philosophy §2, 09-abi-stability §1. -
C# shim assembly with
[UnmanagedCallersOnly]exports (the analogue of MEP-73's synthesisedextern "C"wrapper crate). For each imported NuGet package, the bridge generates a C# project (dotnet_shim/<pkg>/) that depends on the source package and exposes a flat P/Invoke-compatible surface: every translatable public method becomes a static method annotated[UnmanagedCallersOnly(EntryPoint = "mochi_<pkg>_<method>")]; strings pass asbyte*+int(UTF-8 pointers); structs pass asrepr-compatible C structs; opaque types pass asnint(native-sized integer) handles. The shim project is built as a shared library consumed by the CLR hosting API at runtime. See 02-design-philosophy §3, 09-abi-stability §2. -
Closed CLR-to-Mochi type translation table, with explicit refusal on out-of-table cases. .NET generics are reified at runtime (
List<int>andList<string>are distinct CLR types), which means monomorphisation must be explicit. The translation coverslong/int32↔int,double/float↔double,string↔string,bool↔bool,List<T>↔list<T>,Dictionary<K,V>↔map<K,V>,T?↔T|nil,Task<T>↔async desugar, struct types, record types, interface types (as opaque handles), and enum types. Items outside the table are skipped with aSkipReport. Generic types beyond the monomorphised concretisations declared inmochi.toml's[dotnet.monomorphise]are refused. See 05-type-mapping §1, 10-generics-and-reification §1. -
Task<T>→ synchronous Mochi viatask.GetAwaiter().GetResult()as the v1 async bridge. The C# shim wraps eachasync Task<T>method by calling.GetAwaiter().GetResult()from the[UnmanagedCallersOnly]entry point. This marshals the async call through the CLR thread pool and returns synchronously to the Mochi side. A fully async Mochi bridge (exposed as Mochi's own async colour) is opt-in via[dotnet.runtime] async-mode = "task-parallel"for high-throughput cases. See 08-async-bridge §1. -
NuGet trusted publishing (GitHub Actions OIDC via nuget.org's trusted publishing feature, GA March 2024). When
mochi pkg publish --to=nuget.orgruns, the bridge builds the .nupkg, obtains a GitHub Actions OIDC token, and presents it to nuget.org's trusted-publishing endpoint. No long-lived NuGet API keys are stored; the OIDC token is ephemeral and tied to the specific CI workflow and repository. See 07-nuget-trusted-publishing §1, 02-design-philosophy §5. -
NativeAOT as opt-in alternative for packages that support AOT trimming. When
[dotnet] bridge = "nativeaot"is set, the shim project is built withPublishAot=trueviadotnet publish, producing a native.dylib/.so/.dllwith a generated C header and no CLR startup overhead. The gate at lock time checks AOT-compatibility via the package'sIsAotCompatibleNuGet metadata or by running adotnet publish --dry-runanalysis. See 11-nativeaot-and-trimming §1.
The gate for each delivery phase is empirical: the bridge must successfully ingest a curated 20-package fixture corpus drawn from the April 2026 top-downloaded-on-nuget.org snapshot (Newtonsoft.Json, Serilog, Microsoft.Extensions.DependencyInjection, System.Text.Json, Dapper, NUnit, xUnit, FluentAssertions, AutoMapper, MediatR, FluentValidation, Polly, Bogus, Moq, RestSharp, StackExchange.Redis, Npgsql, EntityFramework Core, Microsoft.Extensions.Http, AWSSDK.Core); generate a translatable surface for every public item the closed type-table covers; emit a SkipReport for every item out of table; produce a Mochi extern fn corpus that parses cleanly; and build the resulting shim-plus-Mochi binary via the CLR hosting API with zero additional configuration flags. A separate publish gate exercises the TargetDotNetLibrary path against an in-tree mock nuget.org registry, asserts that the published package installs cleanly via dotnet add package, and asserts that the trusted-publishing OIDC log entry verifies.
Motivation
Mochi today (May 2026) integrates with Rust (MEP-73), Python, Go, and TypeScript. None of the four targets the .NET / NuGet ecosystem, which is large and growing:
-
The NuGet ecosystem covers enterprise, cloud, and data work that no other Mochi target reaches. nuget.org hosts 350,000+ packages (May 2026). The Azure SDK (.NET-first), the AWS SDK (.NET tier-one), ADO.NET and Entity Framework Core (canonical data access), ASP.NET Core (dominant .NET web framework), and the Microsoft.Extensions.* hosting infrastructure are all NuGet-only. A Mochi program doing cloud-native data access has no path to those libraries today.
-
The .NET ecosystem expects NuGet packages as the unit of distribution; Mochi must learn to emit one. When a Mochi package author writes a library intended for enterprise consumption, the natural distribution channel for a .NET-using audience is nuget.org. MEP-53's emit pipeline does not extend to .NET library packages; MEP-68 adds
TargetDotNetLibraryto fill this gap. -
NuGet trusted publishing has been GA since March 2024. NuGet trusted publishing (GitHub Actions OIDC, GA March 2024) predates every other major ecosystem's trusted publishing GA. A package system released in 2026 that does not support nuget.org trusted publishing from day one is shipping a year-old supply-chain story.
-
The CLR hosting API is a stable, documented, cross-platform bridge surface.
hostfxrandhostfxr_get_runtime_delegatehave been stable since .NET 5 (November 2020, now EOL) and are fully supported on .NET 6 / 8 LTS. The API is documented by Microsoft and is the canonical way for native code to call into managed assemblies. It is not an undocumented or version-fragile surface. -
Assembly metadata reflection is the canonical machine-readable .NET surface.
System.Reflection.Metadata.MetadataReader(part of the .NET Base Class Library since .NET Core 2.1) reads ECMA-335 assembly metadata deterministically from the.dllfile. It is unaffected by source generators, partial classes, or conditional compilation; the assembly is the post-compilation truth. The analogy is exact: assembly metadata is to .NET what rustdoc JSON is to Rust (MEP-73 §Motivation point 5). -
MEP-57 already shipped the prerequisite manifest / lockfile / capability infrastructure. The
mochi.tomltable layout, themochi.lockserialisation, the content-addressed object store, the trusted-publishing OIDC token exchange, the capability declaration model, and the NuGet v3 protocol all transfer to the .NET bridge. MEP-68 is additive on top of MEP-57: one new manifest section ([dotnet-dependencies]), one new lockfile repeated table ([[dotnet-package]]), one new CLI surface (mochi pkg publish --to=nuget.org). -
The bridge stays small and audit-friendly. The reference implementation under
package3/dotnet/is targeted at approximately 5,500 LOC of Go across the NuGet v3 client, the metadata JSON parser, the type-mapping pass, the shim generator, the Mochi extern emitter, the lockfile integrator, the CLR hosting runtime hook, the publish flow, and the async bridge dispatch. There is no CLR-side dynamic code generation at user-machine time (the shim is fully generated by the Go binary; the user'sdotnet buildonly compiles, never generates code from metadata). There are no Roslyn source generators in the shim project.
Specification
This section is normative. Sub-notes under /docs/research/0068/ are informative.
1. Pipeline overview
MEP-68 introduces a per-import .NET dependency resolution layer that sits between the Mochi parser (after MEP-57 has resolved mochi.toml) and the MEP-53 build driver:
mochi.toml [dotnet-dependencies]
| pkgmanifest.Parse + pkgsolver.Solve (MEP-57)
v
resolved .NET dep tree (package id + version + source URL)
| package3/dotnet/nugetindex.Fetch (NuGet v3 API)
v
.nupkg files in ~/.cache/mochi/dotnet-deps/<sha512-hex>/
| package3/dotnet/metacli.Extract (mochi-dotnet-meta JSON)
v
metadata JSON document per package
| package3/dotnet/typemap.Translate (closed CLR-to-Mochi table)
v
TranslatedSurface + SkipReport per package
| package3/dotnet/shimgen.Emit (C# shim with [UnmanagedCallersOnly])
v
dotnet_shim/<pkg>/ directory (C# project)
| package3/dotnet/externemit.Emit (Mochi extern fn / extern type)
v
synthesised .mochi shim file per package
| MEP-53 Driver.Build + CLR hosting API
v
binary or .nupkg
The bridge does not run the .NET compiler at ingest time. mochi-dotnet-meta is the only .NET toolchain invocation at lock time; the Mochi binary parses the JSON in Go. The shim project is emitted as C# source by the Go side and built by the user's normal dotnet build (orchestrated by the MEP-53 driver) alongside the user's program.
2. Manifest extension: [dotnet-dependencies] and [dotnet]
The MEP-57 mochi.toml gains two new optional top-level tables:
[dotnet-dependencies]
Newtonsoft.Json = "^13.0"
Serilog = "^3.1"
Microsoft.Extensions.Http = { version = "^8.0", framework = "net8.0" }
MyLocalPackage = { path = "../MyPackage" }
[dotnet]
framework = "net8.0"
runtime = "win-x64" # or "linux-x64", "osx-arm64"
bridge = "clr-hosting" # or "nativeaot"
monomorphise = [
{ item = "System.Linq.Enumerable.Select", T = "string" },
]
[dotnet.publish]
package-id = "MyMochiLib"
version = "1.0.0"
authors = ["tamnd"]
description = "A Mochi library published as a NuGet package."
target-framework = "net8.0"
aot = false
[dotnet.capabilities]
net = true
fs = false
[dotnet-dependencies] follows NuGet's package reference grammar: a bare string for a version constraint, or a table for inline version + framework + path. The grammar maps to the NuGet package reference format; the user can copy dependency declarations from a .csproj file without translation.
The [dotnet] table holds Mochi-specific knobs:
framework: the target framework moniker (TFM) for resolution. Default"net8.0"(the .NET 8 LTS GA, November 2023)."net9.0"is available for .NET 9 users.runtime: the runtime identifier (RID) for native AOT compilation and platform-specific packages. Default auto-detected from the host triple. Recognised values:"win-x64","linux-x64","linux-arm64","osx-x64","osx-arm64".bridge:"clr-hosting"(default) or"nativeaot". Controls whether the shim is loaded via the CLR hosting API or pre-compiled to native code.monomorphise: a list of explicit generic instantiations for the type-mapping pass.
The [dotnet.publish] table holds Mochi-as-library knobs. All fields map directly to the .nuspec manifest:
package-id: the NuGet package identifier (matches[package].namefrommochi.tomlby default).version: semantic version string. Must match themochi.toml[package].versionfield.authors: a list of author names.description: package description for nuget.org.target-framework: the<TargetFramework>value in the emitted.csproj. Default"net8.0".aot: whether to publish an AOT-compiled variant. Defaultfalse.
The [dotnet.capabilities] table holds .NET-bridge-specific capability flags:
net: the package graph contains types that open network sockets. Defaultfalse.fs: the package graph reads or writes files. Defaultfalse.
3. Lockfile extension: [[dotnet-package]]
The MEP-57 mochi.lock gains one new repeated table:
[[dotnet-package]]
id = "Newtonsoft.Json"
version = "13.0.3"
source = { kind = "registry", registry = "https://api.nuget.org/v3/index.json" }
nupkg-sha512 = "abc123..."
metadata-sha256 = "def456..."
shim-sha256 = "789abc..."
capabilities-declared = ["net"]
target-framework = "net8.0"
dependencies = ["Microsoft.CSharp@^4.7"]
nupkg-sha512 is the SHA-512 hash of the .nupkg file as downloaded from the NuGet v3 content URL. SHA-512 is the hash NuGet uses natively in its nupkg.metadata files.
metadata-sha256 records the SHA-256 of the mochi-dotnet-meta JSON document the bridge ingested. A drift here (e.g., a package update that silently changed a public type signature) at mochi pkg lock --check time is a hard error.
shim-sha256 records the SHA-256 of the generated C# shim project's primary source file. A drift at --check time is a hard error.
capabilities-declared is the capability set the manifest declared at lock time. A capability addition at --check time is a hard error pending re-acknowledgement (the monotonicity rule from MEP-57 §1.6 applies).
target-framework records the TFM the bridge resolved against at lock time. A TFM change between lock runs is a hard error pending explicit user acknowledgement.
dependencies is the resolved transitive dependency list, recording the immediate upstream packages the bridge walked when constructing the dep tree.
target-framework is also used by the mochi pkg lock --check command to detect TFM drift: if the user changes [dotnet] framework between lock runs, the check fails with a mismatch error requiring an explicit re-lock.
4. Surface syntax: import dotnet "..."
The Mochi grammar's existing FFI-import production:
ImportStmt := "import" Lang? StringLit "as" Ident ("auto")?
Lang := "go" | "python" | "typescript" | "rust" | "dotnet"
gains dotnet as a Lang alternative. The string literal is one of:
<PackageName>: bare name, resolves through[dotnet-dependencies]constraint plusmochi.lock.<PackageName>@<semver-req>: explicit version constraint, must match[dotnet-dependencies].<PackageName>@path+<relative-path>: path source, relative to the manifest.
Example surface programs:
import dotnet "Newtonsoft.Json" as json
import dotnet "Serilog@^3.1" as log
fn process(data: string): string {
let parsed = json.JsonConvert.DeserializeObject(data)
log.Log.Information("Processing {Count} items", parsed.Count)
return parsed.ToString()
}
The <alias> introduces a Mochi namespace; symbol resolution looks up <alias>.<item> and binds against the synthesised extern fn declaration the bridge generated for <PackageName>.<TypeName>.<MethodName>. Member access chains follow the CLR namespace hierarchy: json.Newtonsoft.Json.JsonConvert.DeserializeObject or (with namespace flattening via auto) JsonConvert.DeserializeObject.
The auto keyword (accepted for import go ... auto and import rust ... auto) is admitted for import dotnet ... auto to opt into flat namespace binding.
5. CLI surface
The Mochi CLI gains the following additions, all under the existing mochi pkg subcommand:
mochi pkg add dotnet <PackageName>[@<version>]: adds an entry to[dotnet-dependencies]and runsmochi pkg lock.mochi pkg lock: extended to walk[dotnet-dependencies], query NuGet v3 API for resolution, fetch each.nupkginto the content-addressed cache, runmochi-dotnet-meta, generate the C# shim, and write[[dotnet-package]]entries.mochi pkg lock --check: extended to verifynupkg-sha512,metadata-sha256,shim-sha256, andcapabilities-declaredfor every[[dotnet-package]]entry.mochi pkg lock --accept-capability=<cap>: re-locks after explicitly acknowledging a capability addition detected by--check. Records the new capability in[dotnet.capabilities]automatically.mochi pkg publish --to=nuget.org [--dry-run]: builds the package viaTargetDotNetLibrary, packages it as a.nupkg, obtains an OIDC token from the CI environment, presents it to nuget.org's trusted-publishing endpoint.--dry-runskips upload.mochi pkg publish --to=nuget.org --emit-ci: generates a.github/workflows/release.ymlworkflow template for trusted publishing, without performing the publish.mochi pkg sync dotnet: regenerates the shim from scratch without changing the lockfile. Used after manual edits to the synthesised shim file or after a bridge upgrade.
6. Build orchestration
When a Mochi program contains one or more import dotnet "..." declarations, the MEP-53 build driver gains the following extensions:
-
Before invoking the native build step, the driver invokes
package3/dotnet/Bridge.PrepareWorkspace(workdir, mochiLock)which:- For each
[[dotnet-package]]inmochi.lock, materialises the package from the content-addressed cache. - Materialises the synthesised C# shim project into
<workdir>/dotnet_shim/<pkg>/. - Writes a
<workdir>/dotnet_shim/<pkg>/<pkg>.csprojthat references the source package and sets[UnmanagedCallersOnly]for each entry point. - Builds each shim via
dotnet build -c Release --no-restoreinto<workdir>/dotnet_shim/<pkg>/bin/Release/<framework>/.
- For each
-
The MEP-53 emit pass treats each
import dotnet "<pkg>" as <alias>as a Mochiimport "./dotnet_shim/<pkg>/shim.mochi" as <alias>shim, where the shim file is theextern fncorpus the bridge emitted. -
The CLR hosting runtime hook is injected into the Mochi binary's startup sequence:
package3/dotnet/hosting.InitCLR(workdir, netRuntimeVersion)callshostfxr_initialize_for_runtime_configand sets up ahostfxr_get_runtime_delegate-obtainedload_assembly_and_get_function_pointerdelegate for each shim assembly. -
The driver invalidates the cache when any
[[dotnet-package]]shim-sha256changes.
7. Async bridge runtime hook
The C# shim for each async Task<T> method uses synchronous dispatch as the v1 default:
[UnmanagedCallersOnly(EntryPoint = "mochi_pkg_FetchAsync")]
public static unsafe IntPtr FetchAsync(byte* url_ptr, int url_len) {
var url = Marshal.PtrToStringUTF8((IntPtr)url_ptr, url_len);
var result = SomePackage.FetchAsync(url).GetAwaiter().GetResult();
return MochiMarshal.StringToCoTaskMem(result);
}
The .GetAwaiter().GetResult() call blocks the calling thread while the CLR thread pool runs the continuation. The result is marshalled back through the native boundary using MochiMarshal helpers (defined in dotnet_shim/shared/MochiMarshal.cs).
For high-throughput cases the user can opt in to a fully async Mochi bridge via [dotnet.runtime] async-mode = "task-parallel", which exposes the tasks via Mochi's own async colour system. This mode is post-v1 (phase 11 delivers the synchronous bridge first).
The CLR thread pool is shared across all shim assemblies in the process. Each [UnmanagedCallersOnly] entry point is thread-safe by construction (CLR managed code is GC-safe by default).
8. Package capability audit
The bridge walks the resolved .NET dependency graph at lock time and computes the union of capability marks across every reachable package. The capability audit follows MEP-57's capability monotonicity rule:
- For each
[[dotnet-package]]entry, the bridge consults thepackage3/dotnet/capdb/capability database (a Go-side map from NuGet package ID + version range to capability set, updated quarterly from community analysis). - The union of capabilities across all reachable packages is computed.
- If the union exceeds the
[dotnet.capabilities]declaration inmochi.toml, lock fails with a diagnostic:ERROR: capability gate: [dotnet.capabilities] net = falsePackage: [email protected] requires net (opens TCP sockets)Resolution: add `net = true` to [dotnet.capabilities] - A capability addition discovered by a later
mochi pkg lock --check(e.g., a minor package update adds network calls) is a hard error pending explicit re-acknowledgement viamochi pkg lock --accept-capability=net.
The capability database is seeded from the NuGet package catalogue analysis (scanning .nuspec and known API patterns) and supplemented by community reports. Packages not in the database default to declaring all capabilities (conservative over-count), requiring the user to explicitly list what they accept.
Phases
See /docs/implementation/0068/ for the per-phase tracking matrix. Fourteen phases cover skeleton (0), NuGet v3 index client (1), assembly metadata ingest (2), type-mapping table (3), C# shim generator (4), Mochi extern emitter (5), import dotnet grammar (6), CLR hosting integration (7), mochi.lock integration (8), NuGet package emit (9), NuGet trusted publishing (10), async bridge (11), generics monomorphisation (12), and NativeAOT opt-in path (13).
A phase is LANDED only when its gate is green against the curated 20-package fixture corpus.
Target matrix
| Phase | host .NET 8 (darwin-arm64) | linux-x64 | win-x64 | NativeAOT |
|---|---|---|---|---|
| 0. skeleton | LANDED | n/a | n/a | n/a |
| 1. NuGet v3 index client | LANDED | n/a | n/a | n/a |
| 2. assembly metadata ingest | LANDED | n/a | n/a | n/a |
| 3. type-mapping table | LANDED | n/a | n/a | n/a |
| 4. C# shim generator | LANDED | required | required | required (AOT-compatible packages only) |
| 5. extern emitter | LANDED | required | required | required |
| 6. import dotnet grammar | LANDED | required | required | required |
| 7. CLR hosting integration | NOT STARTED | required | required | n/a (NativeAOT has no CLR) |
| 8. mochi.lock integration | NOT STARTED | required | required | required |
| 9. NuGet package emit | NOT STARTED | required | required | n/a (publish is host-orchestrated) |
| 10. trusted publishing | NOT STARTED | n/a (publish is host-only) | n/a | n/a |
| 11. async bridge | NOT STARTED | required | required | n/a (NativeAOT: no CLR thread pool) |
| 12. generics monomorphisation | NOT STARTED | required | required | required |
| 13. NativeAOT opt-in | NOT STARTED | n/a | n/a | required |
A phase marked n/a for a target is intentional: the bridge does not promise the behaviour on that target.
The fixture corpus gate is the only definition of "LANDED" for each phase; internal implementation milestones (code written, tests passing in isolation) do not count.
Alternatives considered
-
Parse C# source via Roslyn instead of reading assembly metadata. Rejected: Roslyn source generators, partial classes, conditional compilation directives, and
T4templates all produce a post-compilation surface that differs from the source-level parse.System.Reflection.Metadata.MetadataReaderreads the ECMA-335 binary directly and is unaffected by source-level constructs. Using source-level parsing would also require shipping a C# parser in Go, a substantial undertaking. See 04-assembly-metadata-ingest §3. -
Use the
.xmldocumentation file as the binding source. Rejected: the.xmlfile contains documentation comments (<summary>,<param>,<returns>) attached to member names, but does not contain type signatures. Reconstructing type signatures from XML documentation is not possible without a separate assembly file. -
Use NativeAOT as the default runtime mode instead of CLR hosting. Rejected: NativeAOT requires every type in the dependency graph to be AOT-trim-compatible. As of May 2026, many widely-used NuGet packages (Entity Framework Core, Serilog with reflection-based sinks, RestSharp) are not fully NativeAOT-compatible. CLR hosting works with any package regardless of AOT compatibility. NativeAOT is opt-in for users whose dep graph supports it and who want the lower startup cost.
-
Use P/Invoke directly (platform invocation services) from the Mochi binary instead of a C# shim. Rejected: P/Invoke targets native (
unmanaged) entry points; calling into managed .NET assemblies via P/Invoke requires the assembly to already export[UnmanagedCallersOnly]methods. Raw NuGet packages do not. The C# shim generates those exports. -
Use COM interop to call into .NET objects. Rejected: COM interop is Windows-only (the CrossPlatformCOM shim is experimental as of .NET 8 and not production-supported on Linux/macOS). MEP-68 targets Linux, macOS, and Windows; a Windows-only bridge is not acceptable for the primary path.
-
Use GraalVM polyglot (shared JVM/CLR) as the bridge mechanism. Rejected: GraalVM polyglot supports JVM and JavaScript (GraalJS); it does not ship a .NET CLR. The CLR hosting API is the correct native bridge for .NET.
-
Use IKVM.NET (JVM-on-CLR) to bridge Java ↔ .NET ↔ Mochi. Rejected: IKVM.NET translates JVM bytecode to .NET MSIL; it is not a Mochi↔.NET bridge. The indirection through JVM would add startup cost and a complex dependency graph.
-
Use the
dotnet-embedapproach (embed the entire .NET runtime as a static archive). Rejected: the .NET runtime is not available as a static archive for general use. The CLR hosting API is the supported embedding mechanism. -
Allow long-lived NuGet API keys for nuget.org publish. Rejected: NuGet trusted publishing has been GA since March 2024. Long-lived API keys stored as CI secrets are the historical attack vector for package registry compromise. MEP-68 follows MEP-57's principle that long-lived tokens are deprecated and ships exclusively on the trusted-publishing path.
-
Use WIT (WebAssembly Interface Types) as the bridge protocol. Rejected for v1: the .NET Wasm Component Model support is experimental as of May 2026 (the
dotnet-wasipreview). MEP-68 targets native binaries, not Wasm; the native path via CLR hosting is more mature and covers the full NuGet ecosystem. -
Auto-generate monomorphisations for all generic instantiations in a package. Rejected: .NET generics are reified at the CLR level;
List<int>andList<string>are distinct types. Auto-generating bindings for all possible instantiations would be combinatorially explosive for packages with many generic types (e.g.,System.Linq.Enumerablehas dozens of generic methods). The explicit[dotnet.monomorphise]table makes the surface bounded and user-visible. -
Mirror the transitive dep graph verbatim from each package's
.nuspec. Rejected: NuGet's[netX.0]dependency groups can vary by target framework; the bridge resolves against the declared[dotnet] frameworkTFM and records only the TFM-scoped transitive deps inmochi.lock.
Risks
-
CLR hosting API versioning. The
hostfxrAPI surface is stable since .NET 5 but the exact function signatures and runtime config file format have evolved. Mochi pins to the .NET 8 LTS hosting API contract (documented innethost.h). A future .NET SDK that changeshostfxrsemantics requires a bridge update. Mitigation: the bridge reads thehostfxrAPI version from the SDK's header at lock time and errors if the version is outside the supported range. -
Assembly metadata schema differences across TFMs. Some packages ship different assembly surfaces for different target framework monikers (e.g.,
net6.0vsnet8.0vsnetstandard2.0). The bridge resolves against the declared[dotnet] frameworkTFM and ingest the assembly for that TFM only. Amochi.lockbuilt againstnet8.0is invalid fornet6.0and the bridge errors on TFM mismatch. Mitigation: the lockfile records the TFM; a TFM change forces a full re-lock. -
C# shim compile time. A shim for
Newtonsoft.Jsoncovers approximately 150 public methods;dotnet buildtakes ~4 seconds warm, ~15 seconds cold. For a Mochi program with 10 .NET package imports, cold shim builds add ~150 seconds to the first build. Mitigation: shim build artefacts are cached in~/.cache/mochi/dotnet-deps/shims/<shim-sha256>/; subsequent builds with the same shim SHA-256 are cache hits. -
CLR startup overhead. The CLR hosting initialisation (
hostfxr_initialize_for_runtime_config) takes 50-250 ms on first call, depending on the host machine and the .NET runtime version. For short-lived CLI tools that make few .NET calls, this overhead is disproportionate. Mitigation: NativeAOT (phase 13) eliminates this cost for compatible packages. CLR hosting is the default because it works universally; NativeAOT is opt-in. -
NuGet package restoration at build time. The C# shim project references NuGet packages via
<PackageReference>and requiresdotnet restoreto download them. In hermetic CI environments (no outbound NuGet access), this restore step fails. Mitigation: the bridge copies the.nupkgfrom the content-addressed cache into the shim project's<RestorePackagesPath>before invokingdotnet restore, using the NuGet offline restore path. -
NativeAOT compatibility detection is imperfect. The
IsAotCompatibleNuGet metadata field is advisory, not enforced. A package may declare itself AOT-compatible but fail at trim time. Mitigation: phase 13 gate exercises AOT trim against the full 20-package corpus; packages that fail trim are markedaot-compatible = falsein the bridge's advisory database. -
Windows-only packages in a cross-platform bridge. Some NuGet packages expose Windows-only P/Invoke surfaces (e.g.,
System.Drawing.Commonbefore .NET 6's platform-specific redesign). On Linux/macOS, such packages fail at runtime. Mitigation:mochi pkg lockchecks the package's[runtime-guard]metadata and warns when a Windows-only package is locked against a non-Windows runtime identifier. -
Trusted publishing OIDC configuration on nuget.org. The crate owner must configure a trusted publisher on nuget.org before the first publish. A first-time publisher who runs
mochi pkg publish --to=nuget.orgwithout configuring the trusted publisher on the nuget.org web UI will receive a 403. Mitigation: the bridge detects the 403 and emits a step-by-step configuration guide directing the user to the nuget.org trusted publishing settings page. -
Generic type explosion via
[dotnet.monomorphise]. A user who lists 50 monomorphisations for a generic method generates 50 C# shim methods plus 50 Mochi extern declarations. Each monomorphisation is a distinct CLR type at runtime. Compile time and binary size grow linearly. Mitigation: the bridge warns when[dotnet.monomorphise]exceeds 20 entries per item. -
GetAwaiter().GetResult()deadlock risk. Calling.GetAwaiter().GetResult()on aTask<T>from a thread that already holds an async context (e.g., a synchronisation context like ASP.NET Core's) can deadlock. In the CLR hosting context, the calling thread is the Mochi main thread (no synchronisation context), so deadlock does not occur for v1 patterns. A user who manually calls Mochi from a .NET managed thread that has an existing sync context can hit this. Mitigation: the shim includes aConfigureAwait(false)before.GetAwaiter().GetResult()to prevent sync-context capture. -
Package signature verification. NuGet packages are optionally signed with NuGet.org repository signatures. The bridge verifies the NuGet package signature when present, using the NuGet.org signing root certificate. Unsigned packages (which are the majority as of May 2026) are accepted with a warning; a future
[dotnet.capabilities] require-signed = trueoption will reject unsigned packages. -
Version resolution conflicts across packages. The NuGet v3 resolver uses a lowest-matching-version strategy (unlike npm's or Cargo's highest-matching-version). Two packages that both depend on
Newtonsoft.Jsonat^12.0and^13.0respectively cannot be unified. Mitigation: the bridge surfaces the version conflict as a lock error with a diagnostic suggesting a manual version override in[dotnet-dependencies].
Acknowledgements
This MEP builds on MEP-53 (Rust transpiler) for the rtree IR, the build driver, and the cargo invocation flow analogue; on MEP-57 (Mochi module and package system) for the mochi.toml manifest, the mochi.lock lockfile, the content-addressed object store, the trusted-publishing OIDC infrastructure, and the capability declaration scheme; on MEP-45 (C transpiler) for the FFI sidecar pattern; on MEP-73 (Rust package bridge) for the architectural template this MEP follows; on the System.Reflection.Metadata package and the ECMA-335 specification for the assembly metadata reading strategy; on the .NET CLR hosting documentation (Microsoft Learn, nethost.h header) for the runtime embedding API; on the NuGet v3 API specification for the package index and download protocol; on the NuGet trusted publishing documentation (GA March 2024) for the OIDC publish flow; on pythonnet for prior art on CLR hosting from a non-.NET language; on IKVM.NET for prior art on bidirectional .NET / JVM bridges; on CsWin32 and ClangSharp for prior art on auto-generated P/Invoke shims from metadata; on uniffi and diplomat for prior art on multi-language binding generators; on the Sigstore project and the OpenSSF Trusted Publishing initiative for the keyless OIDC signing model; and on the .NET NativeAOT team for the AOT publishing path whose trim-compatibility model informed the bridge's opt-in design.