Skip to main content

MEP 68. Mochi and .NET package bridge

FieldValue
MEP68
TitleMochi and .NET package bridge
AuthorMochi core
StatusDraft
TypeStandards Track
Created2026-05-29 23:05 (GMT+7)
DependsMEP-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:

  1. Assembly metadata via mochi-dotnet-meta (a small .NET CLI tool wrapping System.Reflection.Metadata.MetadataReader) as the canonical machine-readable NuGet package surface. The tool is invoked once at mochi pkg lock time, emits a JSON document containing every public type and method signature in the assembly, and writes its SHA-256 to mochi.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 .xml file 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.

  2. 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.

  3. C# shim assembly with [UnmanagedCallersOnly] exports (the analogue of MEP-73's synthesised extern "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 as byte* + int (UTF-8 pointers); structs pass as repr-compatible C structs; opaque types pass as nint (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.

  4. Closed CLR-to-Mochi type translation table, with explicit refusal on out-of-table cases. .NET generics are reified at runtime (List<int> and List<string> are distinct CLR types), which means monomorphisation must be explicit. The translation covers long/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 a SkipReport. Generic types beyond the monomorphised concretisations declared in mochi.toml's [dotnet.monomorphise] are refused. See 05-type-mapping §1, 10-generics-and-reification §1.

  5. Task<T> → synchronous Mochi via task.GetAwaiter().GetResult() as the v1 async bridge. The C# shim wraps each async 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.

  6. NuGet trusted publishing (GitHub Actions OIDC via nuget.org's trusted publishing feature, GA March 2024). When mochi pkg publish --to=nuget.org runs, 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.

  7. NativeAOT as opt-in alternative for packages that support AOT trimming. When [dotnet] bridge = "nativeaot" is set, the shim project is built with PublishAot=true via dotnet publish, producing a native .dylib/.so/.dll with a generated C header and no CLR startup overhead. The gate at lock time checks AOT-compatibility via the package's IsAotCompatible NuGet metadata or by running a dotnet publish --dry-run analysis. 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:

  1. 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.

  2. 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 TargetDotNetLibrary to fill this gap.

  3. 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.

  4. The CLR hosting API is a stable, documented, cross-platform bridge surface. hostfxr and hostfxr_get_runtime_delegate have 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.

  5. 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 .dll file. 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).

  6. MEP-57 already shipped the prerequisite manifest / lockfile / capability infrastructure. The mochi.toml table layout, the mochi.lock serialisation, 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).

  7. 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's dotnet build only 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].name from mochi.toml by default).
  • version: semantic version string. Must match the mochi.toml [package].version field.
  • 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. Default false.

The [dotnet.capabilities] table holds .NET-bridge-specific capability flags:

  • net: the package graph contains types that open network sockets. Default false.
  • fs: the package graph reads or writes files. Default false.

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 plus mochi.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 runs mochi pkg lock.
  • mochi pkg lock: extended to walk [dotnet-dependencies], query NuGet v3 API for resolution, fetch each .nupkg into the content-addressed cache, run mochi-dotnet-meta, generate the C# shim, and write [[dotnet-package]] entries.
  • mochi pkg lock --check: extended to verify nupkg-sha512, metadata-sha256, shim-sha256, and capabilities-declared for 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 via TargetDotNetLibrary, packages it as a .nupkg, obtains an OIDC token from the CI environment, presents it to nuget.org's trusted-publishing endpoint. --dry-run skips upload.
  • mochi pkg publish --to=nuget.org --emit-ci: generates a .github/workflows/release.yml workflow 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:

  1. Before invoking the native build step, the driver invokes package3/dotnet/Bridge.PrepareWorkspace(workdir, mochiLock) which:

    • For each [[dotnet-package]] in mochi.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>.csproj that references the source package and sets [UnmanagedCallersOnly] for each entry point.
    • Builds each shim via dotnet build -c Release --no-restore into <workdir>/dotnet_shim/<pkg>/bin/Release/<framework>/.
  2. The MEP-53 emit pass treats each import dotnet "<pkg>" as <alias> as a Mochi import "./dotnet_shim/<pkg>/shim.mochi" as <alias> shim, where the shim file is the extern fn corpus the bridge emitted.

  3. The CLR hosting runtime hook is injected into the Mochi binary's startup sequence: package3/dotnet/hosting.InitCLR(workdir, netRuntimeVersion) calls hostfxr_initialize_for_runtime_config and sets up a hostfxr_get_runtime_delegate-obtained load_assembly_and_get_function_pointer delegate for each shim assembly.

  4. The driver invalidates the cache when any [[dotnet-package]] shim-sha256 changes.

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:

  1. For each [[dotnet-package]] entry, the bridge consults the package3/dotnet/capdb/ capability database (a Go-side map from NuGet package ID + version range to capability set, updated quarterly from community analysis).
  2. The union of capabilities across all reachable packages is computed.
  3. If the union exceeds the [dotnet.capabilities] declaration in mochi.toml, lock fails with a diagnostic:
    ERROR: capability gate: [dotnet.capabilities] net = false
    Package: [email protected] requires net (opens TCP sockets)
    Resolution: add `net = true` to [dotnet.capabilities]
  4. 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 via mochi 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

Phasehost .NET 8 (darwin-arm64)linux-x64win-x64NativeAOT
0. skeletonLANDEDn/an/an/a
1. NuGet v3 index clientLANDEDn/an/an/a
2. assembly metadata ingestLANDEDn/an/an/a
3. type-mapping tableLANDEDn/an/an/a
4. C# shim generatorLANDEDrequiredrequiredrequired (AOT-compatible packages only)
5. extern emitterLANDEDrequiredrequiredrequired
6. import dotnet grammarLANDEDrequiredrequiredrequired
7. CLR hosting integrationNOT STARTEDrequiredrequiredn/a (NativeAOT has no CLR)
8. mochi.lock integrationNOT STARTEDrequiredrequiredrequired
9. NuGet package emitNOT STARTEDrequiredrequiredn/a (publish is host-orchestrated)
10. trusted publishingNOT STARTEDn/a (publish is host-only)n/an/a
11. async bridgeNOT STARTEDrequiredrequiredn/a (NativeAOT: no CLR thread pool)
12. generics monomorphisationNOT STARTEDrequiredrequiredrequired
13. NativeAOT opt-inNOT STARTEDn/an/arequired

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

  1. Parse C# source via Roslyn instead of reading assembly metadata. Rejected: Roslyn source generators, partial classes, conditional compilation directives, and T4 templates all produce a post-compilation surface that differs from the source-level parse. System.Reflection.Metadata.MetadataReader reads 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.

  2. Use the .xml documentation file as the binding source. Rejected: the .xml file 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. Use the dotnet-embed approach (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.

  9. 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.

  10. 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-wasi preview). MEP-68 targets native binaries, not Wasm; the native path via CLR hosting is more mature and covers the full NuGet ecosystem.

  11. Auto-generate monomorphisations for all generic instantiations in a package. Rejected: .NET generics are reified at the CLR level; List<int> and List<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.Enumerable has dozens of generic methods). The explicit [dotnet.monomorphise] table makes the surface bounded and user-visible.

  12. 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] framework TFM and records only the TFM-scoped transitive deps in mochi.lock.

Risks

  1. CLR hosting API versioning. The hostfxr API 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 in nethost.h). A future .NET SDK that changes hostfxr semantics requires a bridge update. Mitigation: the bridge reads the hostfxr API version from the SDK's header at lock time and errors if the version is outside the supported range.

  2. Assembly metadata schema differences across TFMs. Some packages ship different assembly surfaces for different target framework monikers (e.g., net6.0 vs net8.0 vs netstandard2.0). The bridge resolves against the declared [dotnet] framework TFM and ingest the assembly for that TFM only. A mochi.lock built against net8.0 is invalid for net6.0 and the bridge errors on TFM mismatch. Mitigation: the lockfile records the TFM; a TFM change forces a full re-lock.

  3. C# shim compile time. A shim for Newtonsoft.Json covers approximately 150 public methods; dotnet build takes ~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.

  4. 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.

  5. NuGet package restoration at build time. The C# shim project references NuGet packages via <PackageReference> and requires dotnet restore to download them. In hermetic CI environments (no outbound NuGet access), this restore step fails. Mitigation: the bridge copies the .nupkg from the content-addressed cache into the shim project's <RestorePackagesPath> before invoking dotnet restore, using the NuGet offline restore path.

  6. NativeAOT compatibility detection is imperfect. The IsAotCompatible NuGet 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 marked aot-compatible = false in the bridge's advisory database.

  7. Windows-only packages in a cross-platform bridge. Some NuGet packages expose Windows-only P/Invoke surfaces (e.g., System.Drawing.Common before .NET 6's platform-specific redesign). On Linux/macOS, such packages fail at runtime. Mitigation: mochi pkg lock checks the package's [runtime-guard] metadata and warns when a Windows-only package is locked against a non-Windows runtime identifier.

  8. 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.org without 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.

  9. 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.

  10. GetAwaiter().GetResult() deadlock risk. Calling .GetAwaiter().GetResult() on a Task<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 a ConfigureAwait(false) before .GetAwaiter().GetResult() to prevent sync-context capture.

  11. 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 = true option will reject unsigned packages.

  12. 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.Json at ^12.0 and ^13.0 respectively 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.