MEP 73. Mochi and Rust package bridge
| Field | Value |
|---|---|
| MEP | 73 |
| Title | Mochi and Rust package bridge |
| Author | Mochi core |
| Status | Draft |
| Type | Standards Track |
| Created | 2026-05-29 19:55 (GMT+7) |
| Depends | MEP-1 (Grammar, for the import rust extension), MEP-2 (AST, for the import node), MEP-4 (Type System), MEP-13 (ADTs and Match, for Option / Result 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 sparse-index protocol, the content-addressed object store, the capability declaration model, and Sigstore-keyless trusted publishing) |
| Research | /docs/research/0073/ |
| Tracking | /docs/implementation/0073/ |
Abstract
Mochi today (May 2026, immediately after MEP-53's Rust target landed and MEP-57's source-level package system reached Draft) has nine code-emitting targets and the beginnings of a source-level package management story, but only one direction of Rust integration: vm3 → Rust source → cargo-built native binary. The Mochi-to-Rust path (MEP-53) emits an executable that ships the runtime crate mochi-runtime as its only Rust dependency; user-written Mochi programs cannot pull in arbitrary crates from crates.io, and a Mochi package author cannot publish their work as a Rust library that downstream Rust users cargo add. The 140,000+ crates on crates.io (May 2026) remain off-limits to Mochi authors, and the Rust ecosystem cannot consume Mochi packages directly.
MEP-73 specifies the bidirectional Rust crate bridge: Mochi packages can consume any well-formed crates.io crate via import rust "<crate>@<semver>" as <alias> with no user-written FFI boilerplate, and Mochi packages can publish to crates.io as Rust library crates via mochi pkg publish --to=crates.io. The bridge is the fourth source-language interop story Mochi ships (after the Go FFI of import go "...", the Python FFI of import python "...", and the TypeScript / Deno FFI of import typescript "..."), and the first one that targets a typed, semver-versioned, signed-publishing-supporting registry as both a consumer and a producer.
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/rust/. The Mochi grammar gains the single keyword rust as a valid <lang> token in the existing FFI-import production (no other surface-syntax changes). The Mochi build pipeline gains one new target (TargetRustLibrary in MEP-53's build driver). The lockfile gains one new repeated table ([[rust-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:
-
rustdoc JSON output as the canonical machine-readable Rust crate surface.
cargo +nightly rustdoc --output-format=json(RFC #2963, stabilising on the stable channel through 2025-2026, currently behind-Z unstable-optionson nightly only but with rustdoc-types pinning a stable schema since v0.20 in October 2024) emits a JSON document containing every public item in a crate (functions, structs, enums, traits, impls, modules) with stable IDs, generic parameters, where-clauses, and full type signatures. The bridge ingests this document via therustdoc-typesGo-port (pkg/pkgrust/rustdoc/types.go) and treats it as the authoritative bind-source. Parsing the crate's.rssource directly is rejected (would require a Rust parser in Go, untenable, and proc-macros would defeat any source-level parse). Usingcbindgenis rejected for the primary path (cbindgen only seesextern "C"items, which excludes 95% of idiomatic crates). Usingcxxis rejected (cxx requires a hand-written#[cxx::bridge]block per crate, which violates the "no boilerplate" promise). See 01-language-surface §2, 04-rustdoc-json-ingest §1, 02-design-philosophy §1. -
Auto-synthesised
extern "C"wrapper crate (rust_wrap/) per imported Rust dep, not directdlopenor direct repr(C) translation. For each Rust crate the user imports, the bridge generates a sibling Rust crate (<workdir>/rust_wrap/<crate>/) that depends on the source crate and exposes a flatextern "C"surface wrapping each translatable public item: owned types become opaque handles (Box<T>raw-pointered as*mut T),Stringbecomes*const c_charplus an explicitmochi_rust_<crate>_string_freesymbol,Vec<T>becomes a(ptr, len, cap)triple plus a_freesymbol,Result<T, E>becomes a pair of out-pointers plus amochi_statusreturn code, and async fns are wrapped through a process-widetokio::runtime::Runtimesingleton viablock_on. The wrapper crate is built as astaticliband linked into the final binary by the MEP-53 driver's existing cc-rs /cargo:rustc-link-libpath. This indirection accepts the wrapper-codegen cost in exchange for a clean Mochi-side ABI (no need to teach Mochi's lowering pass about Rust lifetimes, generics, or borrow semantics), a single static link surface for the whole dependency graph (no per-crate dlopen path), and a per-wrapper SHA-256 lockfile pin that catches silent rustdoc JSON drift. The alternative of having Mochi emit Rust code directly that calls the source crate is rejected because Mochi'saotirIR is target-agnostic by design; teaching it about Rust generics would break the multi-backend invariant. See 02-design-philosophy §2, 03-prior-art-bridges §3, 09-abi-stability §1. -
Closed Rust-to-Mochi type translation table, with explicit refusal on out-of-table cases. The translation covers
i64↔int,f64↔float,String↔string,&str↔string(by copy),Vec<T>↔list<T>(when T is in-table),HashMap<K, V>↔map<K, V>(when K and V are in-table and K isStringor one of the integer types),BTreeMap<K, V>↔omap<K, V>,HashSet<T>↔set<T>,BTreeSet<T>↔oset<T>,Option<T>↔T|nil,Result<T, E>↔ try-catch desugar,bool↔bool,()↔ unit, tuple types(A, B, ...)↔ tuple<A, B, ...>, fieldful struct typesstruct S { a: A, b: B }↔ record S { a: A, b: B }, C-like and tagged enum typesenum E { V1, V2(A) }↔ type E = V1 | V2(A), andasync fn f(...) -> T ↔ async fun f(...): T. Items outside the table (lifetimes other than'static, generic type parameters beyond the monomorphised concretisations declared inmochi.toml's[rust.monomorphise]section,impl Traitin return position,dyn Traitin any position, raw pointers*const T,*mut T, atomics,Pin<Box<T>>,Cow<T>,Box<dyn Future>, function items withunsafe, and any item whose visibility ispub(crate)orpub(super)) are skipped with aSkipReportentry that names the item and the reason. The user can override with a hand-writtenextern fndeclaration that takes responsibility for the type at the FFI boundary. Aggressive translation (e.g., synthesising a Mochi wrapper for everyimpl Traitreturn) is rejected: the bridge promises zero boilerplate, not full generality. See 05-type-mapping §1, 02-design-philosophy §3. -
import rust "<crate>@<semver>" as <alias>as the sole surface keyword, with<semver>resolved throughmochi.lock. The grammar gains exactly one new lexeme: the keywordrustadmitted as a<lang>in the existingimport <lang> "<spec>" as <alias>production. The<spec>is<crate-name>,<crate-name>@<semver-req>,<crate-name>@git+<url>#<rev>, or<crate-name>@path+<relative-path>. Bare names resolve to the highest version on crates.io that satisfies the manifest's[rust-dependencies]constraint (the bridge auto-rewrites the import to match the resolved version on nextmochi pkg lock). Themochi.locklockfile records the resolved version, BLAKE3-256 + SHA-256 of the crate tarball, the rustdoc-types schema version used at bind time, the SHA-256 of the generated wrapper crate (so a downstreammochi pkg lock --checkcatches rustdoc drift), and the capability surface declared for the crate. No other Mochi syntax changes are introduced;import rustparticipates in the same module-resolution flow asimport go/import python/import typescript. See 01-language-surface §1, 06-cargo-publish-flow §2. -
Mochi → Rust library crate emission via a new
TargetRustLibraryMEP-53 target. MEP-53 today exposesTargetNativeExecutable,TargetRustSource,TargetLinuxStaticX64,TargetLinuxStaticArm64,TargetWasm32WASI, andTargetRustCrate(binary crate). MEP-73 addsTargetRustLibrary. Where the executable path emitssrc/main.rswith afn main(), the library path emitssrc/lib.rswithpub fn,pub struct,pub enumitems mirroring the Mochi package's public surface; sets[lib] crate-type = ["rlib", "cdylib"]inCargo.toml; runscbindgen(via abuild.rshook) to generate a matching C header so non-Rust downstream consumers can link against the cdylib; populates the publish-side metadata fields (description, license, repository, documentation, keywords, categories, readme) from the Mochi package'smochi.toml; and emits aCargo.toml.lockpinning the runtime crate version. The driver'sBuildfunction gates the library path onDriver.LibraryMode=trueplustarget == TargetRustLibrary. See 01-language-surface §3, 06-cargo-publish-flow §1. -
Sigstore-keyless OIDC trusted publishing to crates.io via Cargo RFC #3724, with no long-lived API token path. When
mochi pkg publish --to=crates.ioruns, the bridge: (a) builds the library crate viaTargetRustLibrary; (b) packages it viacargo package --no-verify --allow-dirty; (c) obtains an OIDC token from the CI environment (GitHub Actionsid-token: write, GitLab CI, Buildkite, CircleCI); (d) presents the token to crates.io's trusted-publishing endpoint per Cargo RFC #3724 (accepted Q4 2025, rolling GA through 2026); (e) crates.io exchanges the OIDC token with Fulcio for a short-lived signing certificate; (f) uploads the signed crate tarball; (g) records the Rekor log entry alongside the published version on the index. The legacyCARGO_REGISTRY_TOKENflow is not supported (matches MEP-57's broader principle that long-lived tokens are deprecated; the lessons of the event-stream and xz-utils supply-chain incidents drive this). Local development testing of the publish path is supported via a--dry-runflag that skips the upload step (the same flag MEP-53 phase 15 already exercises). See 07-sigstore-cargo-rfc3724 §1, 02-design-philosophy §5. -
Async bridge via a single process-wide
tokio::runtime::Runtimeconstructed lazily on first async-rust call. Idiomatic Rust crates (notably tokio, reqwest, sqlx, axum, hyper, serde_json's async variants) exposeasync fnas a substantial part of their surface. Mochi's source-levelasyncis colouring-only and lowers to immediate evaluation on the same thread in the MEP-53 Rust target. The bridge resolves the impedance mismatch by constructing a singletokio::runtime::Runtimeinstance viaOnceCellin the wrapper crate the first time any async-fn-wrapping symbol is called, then dispatching each async call throughruntime.block_on(async { ... }). The tokio runtime is built withBuilder::new_current_thread()by default to keep with MEP-53's single-thread principle; users who need a multi-thread runtime can opt in via[rust-dependencies] tokio = { version = "1.42", features = ["rt-multi-thread"] }plus[rust.runtime] flavor = "multi-thread"inmochi.toml. This bridge is rejected for embedded targets (no tokio underno_std); embedded Mochi can only import async-free Rust crates. See 08-async-bridge §1, 02-design-philosophy §4.
The gate for each delivery phase is empirical: the bridge must successfully ingest a curated 24-crate fixture corpus (drawn from the April 2026 top-25-most-downloaded-on-crates.io snapshot: anyhow, thiserror, serde, regex, rayon, itertools, once_cell, time, uuid, url, base64, hex, sha2, blake3, rand, rand_chacha, num_cpus, bytes, smallvec, indexmap, ahash, parking_lot, crossbeam, tokio); 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 wrapper-plus-Mochi binary via MEP-53's existing cargo build --release path with zero additional flags. A separate publish gate exercises the TargetRustLibrary path against an in-tree mock crates.io registry (the same sigstore-mock-fulcio harness MEP-57 uses), asserts that the published crate cargo installs cleanly, and asserts that the Rekor log entry verifies against the Sigstore root of trust.
Motivation
Mochi today (May 2026) integrates with foreign ecosystems through three FFI surfaces: Go (via the existing import go "<pkg>" flow, used by MEP-44's standard library bridge), Python (via import python "<module>", used by data-science fixtures in examples/v0.6/python_polars.mochi), and TypeScript / Deno (via import typescript "<spec>", used by examples/v0.6/deno_math.mochi). None of the three target a fully-typed, semver-resolved, signed-publishing registry, and none of them publish Mochi packages back to their host ecosystem. Rust is the first foreign ecosystem where the gap is large enough to be costly:
-
The Rust ecosystem is the canonical 2024-2026 destination for systems work, high-performance data, embedded firmware, WebAssembly host-side libraries, and modern CLI tooling. Crates.io hosts 140,000+ packages (April 2026 snapshot); the top 100 most-downloaded crates account for 20+ billion downloads per month (April 2026 crates.io public stats). A Mochi program needing zero-copy CBOR parsing (
ciborium), SIMD JSON (simd-json), arbitrary-precision arithmetic (malachite), GPU compute (wgpu,cubecl), or modern async HTTP (reqwest,hyper,axum) has no path to those crates today. -
The Rust ecosystem expects Rust library crates as the unit of distribution; Mochi must learn to emit one. When a Mochi package author writes a useful library, the natural distribution channel for a Rust-using audience is crates.io. MEP-53 phase 15 (publish-ready crate metadata) prepared the
mochi-runtimecrate for crates.io distribution but did not extend the path to user-written Mochi packages. MEP-73 extends MEP-53 to emit user packages as library crates with the right publish metadata. -
The Cargo RFC #3724 trusted publishing path has crystallised into the unambiguous 2025-2026 default. Four of the top five package ecosystems (npm Trusted Publishing GA April 2024, Maven Central Sigstore GA October 2024, PyPI PEP 740 GA late 2025, Cargo RFC #3724 accepted Q4 2025 with rolling GA through 2026) converged on Sigstore-keyless OIDC publishing. A package system released in 2026 that does not ship trusted publishing on day one is shipping a decade-out-of-date supply-chain story.
-
The "import rust" surface has zero learning curve for Mochi users.
import rust "tokio@^1.42" as tokiois the same shape asimport go "strings"andimport python "numpy". Mochi users do not need to know what a Cargo.toml is, what a build.rs is, what an unsafe block is, what a lifetime is, what a generic bound is, what an extern "C" wrapper is, what aBox<dyn Future>is, whattokio::runtime::Runtime::block_ondoes, or what a Sigstore Fulcio cert is. They writeimport rust "..." as ...and the bridge does the rest. -
Rustdoc JSON has matured into a stable, machine-readable, schema-versioned Rust crate description. Prior bridges (PyO3, neon, napi-rs, swift-bridge, cxx, autocxx, uniffi, diplomat) require hand-written
#[pyfunction]/#[neon::function]/#[napi]/#[cxx::bridge]annotations on the Rust side. Rustdoc JSON makes those annotations unnecessary: the bridge reads the JSON, walks the public surface, and synthesises the wrapper. The user writes Rust crate code as if no bridge existed, and the Mochi user imports as if no Rust existed. -
MEP-57 already shipped the prerequisite manifest / lockfile / capability infrastructure. The
mochi.tomltable layout, themochi.lockserialisation, the BLAKE3-256 + SHA-256 dual-hashing, the trusted-publishing OIDC token exchange, the sparse-index protocol, and the capability declaration model all transfer to the Rust bridge with no architectural duplication. MEP-73 is additive on top of MEP-57: one new manifest section ([rust-dependencies]), one new lockfile repeated table ([[rust-package]]), one new CLI surface (mochi pkg publish --to=crates.io). -
The bridge stays small and audit-friendly. The reference implementation under
package3/rust/is targeted at ~6,000 LOC of Go across the sparse-index client, the rustdoc-JSON parser, the type-mapping pass, the wrapper synthesiser, the Mochi extern emitter, the lockfile integrator, the publish flow, and the async-bridge runtime hook. There is no Rust-side dynamic codegen at user-machine time (the wrapper is fully synthesised by the Go binary; the user'scargo buildonly compiles, never generates code from rustdoc). There are no proc-macros in the wrapper crate (they would defeat reproducibility). There is nounsafe fnin the synthesised wrapper unless the user opts in via[capabilities] unsafe = true.
Specification
This section is normative. Sub-notes under /docs/research/0073/ are informative.
1. Pipeline overview
MEP-73 introduces a per-import Rust dependency resolution layer that sits between the Mochi parser (after MEP-57 has resolved mochi.toml) and the MEP-53 build driver:
mochi.toml [rust-dependencies]
| pkgmanifest.Parse + pkgsolver.Solve (MEP-57)
v
resolved Rust dep tree (crate name + version + source URL)
| package3/rust/sparseindex.Fetch (Cargo sparse-index protocol)
v
.crate tarballs in ~/.cache/mochi/rust-deps/<blake3-hex>/
| package3/rust/rustdoc.Generate (cargo rustdoc --output-format=json)
v
rustdoc-types JSON document per crate
| package3/rust/typemap.Translate (closed Rust-to-Mochi table)
v
TranslatedSurface + SkipReport per crate
| package3/rust/wrapsyn.Emit (synthesised extern "C" wrapper crate)
v
rust_wrap/<crate>/ workspace member
| package3/rust/externemit.Emit (Mochi extern fn / extern type)
v
synthesised .mochi shim file per crate, imported by the user's source
| MEP-53 Driver.Build (TargetNativeExecutable / TargetRustLibrary)
v
binary or library crate
The bridge does not run the Rust compiler at ingest time. cargo rustdoc --output-format=json is the only Rust toolchain invocation; the Mochi binary parses the JSON in Go. The wrapper crate is emitted as source by the Go side and built by the user's normal cargo build --release (orchestrated by the MEP-53 driver) alongside the user's program.
2. Manifest extension: [rust-dependencies] and [rust]
The MEP-57 mochi.toml gains two new optional top-level tables:
[rust-dependencies]
tokio = { version = "^1.42", features = ["rt", "macros"] }
reqwest = { version = "^0.12", features = ["json", "rustls-tls"], default-features = false }
serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0"
my-local-crate = { path = "../my-crate" }
my-git-crate = { git = "https://github.com/example/my-crate", rev = "abc123" }
[rust]
edition = "2024"
runtime = { flavor = "current-thread" }
monomorphise = [
{ item = "serde_json::from_str", T = "MyStruct" },
{ item = "Vec::new", T = "i64" },
]
[rust.publish]
crate-type = ["rlib", "cdylib"]
cbindgen = true
[rust.capabilities]
net = true
fs = false
proc = false
unsafe = false
[rust-dependencies] follows Cargo's [dependencies] grammar verbatim: simple string for version, table for inline version + features + default-features + path / git / branch / rev / tag. This is intentional: Mochi users with Rust background can copy from existing Cargo.toml files without translation; the bridge passes the table through to the synthesised wrapper crate's Cargo.toml with no edits.
The [rust] table holds Mochi-specific knobs:
edition: the Rust edition for the wrapper crate and the synthesised library crate. Default"2021"(the MEP-53 default, stable since Rust 1.56)."2024"is available on Rust 1.85+.runtime.flavor:"current-thread"(default) or"multi-thread". Controls the tokio runtime used by the async bridge.monomorphise: a list of explicit generic instantiations. For each entry, the bridge emits the wrapper for<item><T>rather than refusing the import (the default behaviour for generic items). The entry shape mirrors Cargo's existing concretisation patterns.
The [rust.publish] table holds Mochi-as-library knobs:
crate-type: a Cargo[lib]crate-type array (rlib for Rust-sidecargo add, cdylib for non-Rust-side dlopen). Default["rlib"].cbindgen: whether to emit a C header alongside the cdylib. Defaultfalse.
The [rust.capabilities] table holds Rust-bridge-specific capability flags (a strict refinement of MEP-57's [capabilities] table; if MEP-57 marks the package as not requiring net, [rust.capabilities] net = true is a manifest validation error):
net: the Rust dep graph contains crates that open network sockets (tokio, reqwest, hyper). Defaultfalse.fs: the dep graph reads or writes files (std::fs, tokio::fs). Defaultfalse.proc: the dep graph spawns processes (std::process::Command). Defaultfalse.unsafe: the dep graph contains items that the user has hand-overridden through aextern fn ... from rust "..."withunsafe. Defaultfalse.
3. Lockfile extension: [[rust-package]]
The MEP-57 mochi.lock gains one new repeated table:
[[rust-package]]
name = "tokio"
version = "1.42.0"
source = { kind = "registry", registry = "https://index.crates.io" }
crate-blake3 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
crate-sha256 = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
rustdoc-types-version = "0.39.0"
rustdoc-sha256 = "..."
wrapper-sha256 = "..."
capabilities-declared = ["net", "fs"]
dependencies = ["mio@^1.0", "bytes@^1.7", "pin-project-lite@^0.2"]
features = ["rt", "macros"]
crate-blake3 and crate-sha256 are the integrity hashes of the .crate tarball as downloaded from https://crates.io/api/v1/crates/<name>/<version>/download. BLAKE3-256 is the primary verification hash (matches MEP-57's broader BLAKE3 + SHA-256 dual-hashing rule). SHA-256 is recorded for cross-ecosystem interoperability and matches the hash crates.io itself publishes in its index.
rustdoc-types-version records the rustdoc-types schema version the bridge used at lock time. A schema-version drift at mochi pkg lock --check time is a hard error: the lockfile must record exactly the schema the wrapper was synthesised against.
rustdoc-sha256 records the SHA-256 of the rustdoc JSON document the bridge ingested. A drift here (e.g., a cargo update of a transitive dep silently changed a public signature) at --check time is a hard error.
wrapper-sha256 records the SHA-256 of the synthesised wrapper crate's src/lib.rs. A drift here at --check time is a hard error.
capabilities-declared is the capability set the manifest declared at lock time. A capability addition (e.g., a cargo update of a transitive dep newly imports tokio::net::TcpStream) at --check time is a hard error pending re-acknowledgement (the monotonicity rule from MEP-57 §1.6).
dependencies is the resolved transitive dependency tree (so a downstream consumer's mochi.lock carries the full graph).
features is the set of features the manifest enabled at lock time. The lockfile records this for reproducibility: a downstream mochi pkg lock --check must see the same feature set or fail.
4. Surface syntax: import rust "..."
The Mochi grammar's existing FFI-import production:
ImportStmt := "import" Lang? StringLit "as" Ident ("auto")?
Lang := "go" | "python" | "typescript" | "rust"
gains rust as a Lang alternative. The string literal is one of:
<crate-name>: bare name, resolves through[rust-dependencies]constraint plusmochi.lock.<crate-name>@<semver-req>: explicit version constraint, must match[rust-dependencies].<crate-name>@git+<url>or<crate-name>@git+<url>#<rev>: git source.<crate-name>@path+<relative>: path source.
Example surface programs:
import rust "anyhow@^1.0" as anyhow
import rust "tokio" as tokio
import rust "serde_json" as serde_json
fn handle_request(body: string): string {
let parsed = serde_json.from_str(body)
if parsed.is_ok() {
return anyhow.ok(parsed.value().to_string())
}
return anyhow.err("parse failed")
}
The <alias> introduces a Mochi namespace; symbol resolution looks up <alias>.<item> and binds against the synthesised extern fn declaration the bridge generated for <crate-name>::<item>.
The auto keyword (already accepted for import go ... auto) is admitted for import rust ... auto to opt into "import every public item of the crate as a top-level Mochi binding (rather than namespaced under the alias)". Default is namespaced.
5. CLI surface
The Mochi CLI gains the following additions, all under the existing mochi pkg subcommand:
mochi pkg add rust <crate>[@<semver>]: adds an entry to[rust-dependencies]and runsmochi pkg lock.mochi pkg lock: as today (MEP-57), now extended to walk[rust-dependencies], query the Cargo sparse index for resolution, fetch each crate tarball into the content-addressed cache, run rustdoc-JSON ingest, synthesise the wrapper, and write[[rust-package]]entries.mochi pkg lock --check: as today, now extended to verifycrate-blake3,rustdoc-sha256,wrapper-sha256, andcapabilities-declaredfor every[[rust-package]]entry.mochi pkg publish --to=crates.io [--dry-run]: builds the package as a Rust library crate viaTargetRustLibrary, packages it withcargo package, obtains an OIDC token from the CI environment, presents it to crates.io's trusted-publishing endpoint per Cargo RFC #3724, uploads the signed tarball.--dry-runskips upload but still exercises the signing flow against asigstore-mock-fulcioharness.mochi pkg sync rust: regenerates the wrapper crate from scratch (without changing the lockfile). Used after manual edits to the synthesised shim file.
6. Build orchestration
When a Mochi program contains one or more import rust "..." declarations, the MEP-53 build driver gains the following extensions:
-
Before invoking
cargo build, the driver invokespackage3/rust/Bridge.PrepareWorkspace(workdir, mochiLock)which:- For each
[[rust-package]]inmochi.lock, materialises the crate source from the content-addressed cache into<workdir>/rust_deps/<crate>-<version>/. - Materialises the synthesised wrapper crate into
<workdir>/rust_wrap/<crate>/. - Writes a
<workdir>/Cargo.tomlworkspace manifest that includes<workdir>/Cargo.toml(the main package) plus every<workdir>/rust_wrap/<crate>/Cargo.tomlas workspace members. - The main package's
Cargo.tomlgains a[dependencies]entry for each wrapper crate:<crate>_mochi_wrap = { path = "../rust_wrap/<crate>" }.
- For each
-
The MEP-53 emit pass treats each
import rust "<crate>" as <alias>as a Mochiimport "./rust_wrap/<crate>/shim.mochi" as <alias>shim, where the shim file is theextern fncorpus the bridge emitted. -
The wrapper crate's
Cargo.tomldeclares the source crate as a[dependencies]entry with the exact version pinned frommochi.lock. -
The user's
cargo build --release(invoked byDriver.Build) compiles every workspace member, producing the wrapper as astaticlibartefact, and links the main binary against it viacargo:rustc-link-lib=static. -
The driver invalidates the cache when any
[[rust-package]]wrapper-sha256changes (per MEP-53 phase 0's cache-key construction).
7. Async bridge runtime hook
The wrapper crate for any source crate that exposes async fn items includes a generated mochi_rt.rs module:
use std::sync::OnceLock;
use tokio::runtime::Runtime;
static MOCHI_RT: OnceLock<Runtime> = OnceLock::new();
fn get_rt() -> &'static Runtime {
MOCHI_RT.get_or_init(|| {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("mochi: failed to construct tokio runtime")
})
}
The wrapper for async fn <name>(args) -> T becomes:
#[no_mangle]
pub extern "C" fn mochi_<crate>_<name>(<args>) -> <c_T> {
let result = get_rt().block_on(async { <crate>::<name>(<args>).await });
<result_to_c_abi>(result)
}
When [rust.runtime] flavor = "multi-thread" is set in mochi.toml, new_current_thread() becomes new_multi_thread().enable_all().build().
The runtime is opt-out only by importing zero async-fn-exposing crates; it is not opt-in. The construction is lazy, so non-async crates pay zero cost.
Phases
See /docs/implementation/0073/ for the per-phase tracking matrix. Fourteen phases cover skeleton (0), sparse-index client (1), rustdoc-JSON ingest (2), type-mapping table (3), wrapper synthesiser (4), Mochi extern emitter (5), import rust grammar (6), build orchestration (7), mochi.lock integration (8), TargetRustLibrary emit (9), trusted-publishing flow (10), async bridge (11), monomorphisation of generics (12), and embedded subset (13).
A phase is LANDED only when its gate is green against the curated 24-crate fixture corpus.
Target matrix
| Phase | host stable rust 1.95 (darwin-arm64) | musl-x64 / musl-arm64 | wasm32-wasip1 | embedded (no_std + alloc) |
|---|---|---|---|---|
| 0. skeleton | LANDED | n/a | n/a | n/a |
| 1. sparse-index client | LANDED | n/a | n/a | n/a |
| 2. rustdoc-JSON ingest | LANDED | n/a | n/a | n/a |
| 3. type-mapping table | LANDED | n/a | n/a | n/a |
| 4. wrapper synthesiser | LANDED | required | n/a (no cc on wasm32) | required (no_std subset only) |
| 5. extern emitter | LANDED | required | required | required |
| 6. import rust grammar | LANDED | required | required | required |
| 7. build orchestration | LANDED | LANDED | n/a (cargo workspace required) | n/a |
| 8. mochi.lock integration | LANDED | LANDED | LANDED | LANDED |
| 9. TargetRustLibrary emit | LANDED | LANDED | n/a (lib is rlib + cdylib for native) | n/a |
| 10. trusted publishing | LANDED | n/a (publish is host-only) | n/a | n/a |
| 11. async bridge | LANDED | required | n/a (no tokio on wasm32-wasip1) | n/a |
| 12. monomorphisation of generics | LANDED | LANDED | LANDED | LANDED |
| 13. embedded subset | LANDED | n/a | n/a | LANDED |
A phase marked n/a for a target is intentional: the bridge does not promise the behaviour on that target.
Alternatives considered
-
Parse Rust source directly instead of via rustdoc JSON. Rejected: a Rust parser implemented in Go would be a substantial undertaking (tens of thousands of LOC for a passable subset, plus the constant churn of stabilised features); proc-macros expand source-side and would defeat any source-level parse; the rustc team explicitly maintains rustdoc JSON as the official machine-readable surface. The bridge takes rustdoc JSON as authoritative.
-
Use cbindgen as the binding source rather than rustdoc JSON. Rejected: cbindgen only sees items that are already
extern "C", which excludes 95%+ of crates.io content. cbindgen plays a complementary role on the OTHER direction (Mochi-as-library exports a C ABI via cbindgen) but is not the bind-source for crates the user imports. -
Use cxx for the FFI layer rather than synthesised extern "C" wrappers. Rejected: cxx requires a hand-written
#[cxx::bridge] mod ffi { ... }block per crate the user imports, which violates the "no boilerplate" promise. cxx is the right tool when the user has hand-authored bidirectional bindings; MEP-73 generates the bindings. -
Use uniffi-rs for the binding layer. Rejected: uniffi is Mozilla's Rust-to-Swift/Kotlin/Python binding generator; it generates code from a
.udlinterface description file. Mochi users would have to author a.udlfor every crate they import, which is more boilerplate than the bridge promise allows. Uniffi is the right tool when the binding author wants explicit control over the surface; MEP-73 derives the surface from rustdoc. -
Use diplomat for the binding layer. Rejected: diplomat requires
#[diplomat::bridge]annotations on the Rust side, similar to cxx. Same boilerplate-violation argument. -
Use the WIT (Wasm Interface Types) world description plus wit-bindgen. Rejected for v1: WIT is the right long-term answer when the source-and-sink are both Wasm component-model compatible, but as of 2026-05 the major idiomatic crates (tokio, reqwest, sqlx) do not ship a WIT world description, and forcing the user to author one is the same boilerplate violation. A v2 of the bridge could add a
--mode=witflag that consumes WIT when the crate ships one. See 12-risks-and-alternatives §A6 for the deferred design. -
Direct dlopen of pre-built
.so/.dylibartefacts from crates.io rather than per-import wrapper compilation. Rejected: crates.io does not host pre-built binaries (only.cratesource tarballs); the user's host triple may not match any published binary; per-feature monomorphisation makes pre-built artefacts non-portable. The wrapper-and-link path is the only one that works. -
Translate Rust ownership and borrow semantics into Mochi's ownership-free surface. Rejected: Mochi does not have a
&'a Tsyntax, does not have lifetime annotations, does not havemovesemantics distinct from copy. The bridge sidesteps the impedance mismatch by translating every borrow into a clone (the wrapper takes&strparameters as*const c_charwhich the Mochi side passes by copy) and every move into an opaque-handle ownership transfer (the wrapper takesBox<T>as*mut Twhich the Mochi side owns and explicitly frees via the synthesised_freesymbol). This is the same strategy PyO3 and napi-rs use. -
Allow long-lived
CARGO_REGISTRY_TOKENAPI tokens for crates.io publish. Rejected: matches MEP-57's broader principle that long-lived tokens are deprecated. The xz-utils, event-stream, and 2025 PyPI reflected-string flood incidents trace to compromised long-lived tokens; the industry direction (npm, Maven Central, PyPI, Cargo) is unambiguously toward Sigstore-keyless OIDC. MEP-73 ships exclusively on that path. -
Use
Arc<Mutex<...>>everywhere in the wrapper crate to support multi-thread Mochi. Rejected for v1: matches MEP-53's broader single-thread principle. Users who need multi-thread access to a Rust crate from Mochi can opt in via[rust.runtime] flavor = "multi-thread"(which switches the tokio runtime) plus explicitArc<Mutex<...>>wrappers in their own Mochi code. -
Run
cargo buildon every Mochi build to pick up rustdoc drift automatically. Rejected:mochi.lockis the authoritative source. If a transitive dep updated and the public surface changed, the lockfile must record it explicitly so the user sees the diff in code review. Silent re-resolution would mask supply-chain anomalies. -
Make
import rustdefer to the user's existing Cargo.toml rather than to mochi.toml. Rejected: the Mochi build flow owns the workspace. If the user has a pre-existing Cargo.toml, MEP-73 reads its[dependencies]table for the version constraints (a future sub-phase, not v1) but the source of truth ismochi.toml. Mixing manifest authorities is a known anti-pattern (Bazel's WORKSPACE-plus-MODULE.bazel transition shows the cost of authority ambiguity).
Risks
-
Rustdoc JSON is behind
-Z unstable-optionson stable Rust. The--output-format=jsonflag is currently nightly-only (RFC #2963, stabilisation tracking issue rust-lang/rust #76578). The bridge requires a nightly toolchain at ingest time. Mitigation: the bridge invokesrustup toolchain install nightlyautomatically if not present; the user's normalcargo buildruns on stable. A future Rust release (expected 2026-2027) is anticipated to stabilise the flag. -
Rustdoc-types schema version drift. The
rustdoc-typescrate publishes a new major version roughly every 2-3 months as the JSON schema evolves. The bridge pins a specific schema version per release; an ingest against a newer nightly produces a schema-version mismatch error at lock time. Mitigation: ship a schema-version-compatibility table; users can downgrade nightly to the supported channel viamochi pkg lock --rust-nightly=<date>. -
Wrapper crate compile times. A wrapper for
tokiois ~1,500 LOC of synthesised Rust (every async fn in the public surface, every type the table covers). Compile time on a warm cache is ~8 seconds; cold, ~25 seconds. For a Mochi program with 10 Rust crate imports, the wrapper compile pass dominates total build time. Mitigation: the wrapper crates are cached in~/.cache/mochi/rust-deps/wrappers/<wrapper-sha256>/; the workspace path always shares the cache. -
Generic monomorphisation explosion. A crate that exposes a generic function over 50 types and the user lists 50 monomorphisations produces a wrapper with 50
extern "C"symbols and 50cargo buildcodegen units. The combinatorial explosion is real. Mitigation: the[rust.monomorphise]table is required to enumerate; the bridge does not auto-monomorphise. -
Async bridge tokio runtime cost. A single Mochi program with one
tokio::time::sleep(...).awaitcall pulls in the full tokio runtime (~150K LOC across tokio + mio + bytes + pin-project-lite). For programs that only need synchronous Rust, this is wasted. Mitigation: the tokio runtime is constructed lazily on first async call; programs that never call an async fn pay zero runtime cost (though the dep is still in the binary). -
Capability declaration drift. A
cargo updateof a transitive dep can silently introduce a new capability (e.g., a crate that used to be pure-CPU now opens a network socket for telemetry).mochi pkg lock --checkcatches this, but only at lock time; if the user does not run--check, the production binary ships with the new capability. Mitigation: CI must runmochi pkg lock --check. The MEP-57 capability monotonicity rule applies transitively. -
Cargo RFC #3724 GA timing. Trusted publishing on crates.io was accepted Q4 2025 but is rolling out through 2026; not all crates.io endpoints support OIDC yet at MEP-73 spec authoring time. Mitigation: the bridge ships an
--allow-token-fallbackflag for the transition period (default off). The flag is removed once crates.io GA is reached. -
Cross-version source-language drift in the wrapper crate. When a user upgrades from rust 1.78 to rust 1.95 the wrapper crate's
Cargo.tomlrust-version = "1.78"field must be updated to match. The bridge writes arust-version = <pinned>field per ingest; subsequentmochi pkg lock --rust-toolchain=<new>regenerates with the new toolchain. -
Mochi-as-library cdylib symbol collision. When a Mochi package is published as a Rust library with
crate-type = ["cdylib"]and another Mochi library is loaded into the same process, theextern "C"symbols (mochi_<crate>_<fn>) may collide. Mitigation: the bridge prefixes every exported symbol with the publishing crate's name plus its version major; collisions only arise across compatible majors of the same library. -
Source-crate licence compatibility. A Mochi library published to crates.io under Apache-2.0 OR MIT cannot legally re-export a GPL-licensed transitive dep. Mitigation:
mochi pkg publish --to=crates.iowalks the transitive licence graph (via SPDX licence expressions in each dep's Cargo.toml) and refuses to publish if the union is incompatible with the declared[package] license. -
Embedded subset is small.
no_std-compatible Rust crates are a small subset of crates.io (estimated 5-10% of the top 1,000). Bringing inserde_derive,tokio,reqwest,regex,rayon,rand_chachaall requirestd. Mitigation: the embedded gate (phase 13) checksno_stdcompatibility at lock time; non-compatible crates are rejected with a clear diagnostic. -
CI image dependency on
cargo-zigbuildplusnightly rustdoc. The MEP-53 gates already requirecargo-zigbuildandwasmtime; MEP-73 adds nightly rustdoc and (optionally)cargo-cyclonedxfor SBOM emission. The CI image is bigger. Mitigation: the standardmochilang/ci-rustimage bundles the full toolset; standalone users install viarustup toolchain install nightly --component rustdoc.
Acknowledgements
This MEP builds on MEP-53 (Rust transpiler) for the rtree IR, the build driver, the runtime crate, and the cargo invocation flow; on MEP-57 (Mochi module and package system) for the mochi.toml manifest, the mochi.lock lockfile, the BLAKE3 + SHA-256 dual-hashing, the trusted-publishing OIDC infrastructure, the sparse-index protocol model, and the capability declaration scheme; on MEP-45 (C transpiler) for the FFI sidecar pattern; on the rustdoc-types crate maintained by the rust-lang/rustdoc team for the JSON schema; on the rustdoc-json team for the --output-format=json flag; on Cargo RFC #3724 (Q4 2025) for the trusted-publishing protocol; on Cargo RFC #3796 for the resolver model; on PyO3 / neon / napi-rs / uniffi / diplomat / swift-bridge / cxx for prior-art on Rust-to-other-language binding generation; on the Sigstore project and the OpenSSF Trusted Publishing initiative for the keyless OIDC signing flow; on the Bytecode Alliance's wit-bindgen for the future-tense WIT-based bridge mode; on Cargo's sparse-index protocol (GA March 2023) for the dependency-fetch path; on the tokio team for the runtime singleton model the async bridge uses; on the embedded Rust working group for the no_std + extern crate alloc convention that informs the embedded-subset path; and on the broader Rust supply-chain initiative (RustSec, cargo-vet, cargo-audit, cargo-deny) for the audit-database surface a future sub-phase will integrate against.