Skip to main content

MEP 73. Mochi and Rust package bridge

FieldValue
MEP73
TitleMochi and Rust package bridge
AuthorMochi core
StatusDraft
TypeStandards Track
Created2026-05-29 19:55 (GMT+7)
DependsMEP-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:

  1. 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-options on 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 the rustdoc-types Go-port (pkg/pkgrust/rustdoc/types.go) and treats it as the authoritative bind-source. Parsing the crate's .rs source directly is rejected (would require a Rust parser in Go, untenable, and proc-macros would defeat any source-level parse). Using cbindgen is rejected for the primary path (cbindgen only sees extern "C" items, which excludes 95% of idiomatic crates). Using cxx is 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.

  2. Auto-synthesised extern "C" wrapper crate (rust_wrap/) per imported Rust dep, not direct dlopen or 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 flat extern "C" surface wrapping each translatable public item: owned types become opaque handles (Box<T> raw-pointered as *mut T), String becomes *const c_char plus an explicit mochi_rust_<crate>_string_free symbol, Vec<T> becomes a (ptr, len, cap) triple plus a _free symbol, Result<T, E> becomes a pair of out-pointers plus a mochi_status return code, and async fns are wrapped through a process-wide tokio::runtime::Runtime singleton via block_on. The wrapper crate is built as a staticlib and linked into the final binary by the MEP-53 driver's existing cc-rs / cargo:rustc-link-lib path. 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's aotir IR 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.

  3. 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 is String or 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 types struct S { a: A, b: B }↔ record S { a: A, b: B }, C-like and tagged enum types enum E { V1, V2(A) }↔ type E = V1 | V2(A), and async fn f(...) -> T ↔ async fun f(...): T. Items outside the table (lifetimes other than 'static, generic type parameters beyond the monomorphised concretisations declared in mochi.toml's [rust.monomorphise] section, impl Trait in return position, dyn Trait in any position, raw pointers *const T, *mut T, atomics, Pin<Box<T>>, Cow<T>, Box<dyn Future>, function items with unsafe, and any item whose visibility is pub(crate) or pub(super)) are skipped with a SkipReport entry that names the item and the reason. The user can override with a hand-written extern fn declaration that takes responsibility for the type at the FFI boundary. Aggressive translation (e.g., synthesising a Mochi wrapper for every impl Trait return) is rejected: the bridge promises zero boilerplate, not full generality. See 05-type-mapping §1, 02-design-philosophy §3.

  4. import rust "<crate>@<semver>" as <alias> as the sole surface keyword, with <semver> resolved through mochi.lock. The grammar gains exactly one new lexeme: the keyword rust admitted as a <lang> in the existing import <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 next mochi pkg lock). The mochi.lock lockfile 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 downstream mochi pkg lock --check catches rustdoc drift), and the capability surface declared for the crate. No other Mochi syntax changes are introduced; import rust participates in the same module-resolution flow as import go / import python / import typescript. See 01-language-surface §1, 06-cargo-publish-flow §2.

  5. Mochi → Rust library crate emission via a new TargetRustLibrary MEP-53 target. MEP-53 today exposes TargetNativeExecutable, TargetRustSource, TargetLinuxStaticX64, TargetLinuxStaticArm64, TargetWasm32WASI, and TargetRustCrate (binary crate). MEP-73 adds TargetRustLibrary. Where the executable path emits src/main.rs with a fn main(), the library path emits src/lib.rs with pub fn, pub struct, pub enum items mirroring the Mochi package's public surface; sets [lib] crate-type = ["rlib", "cdylib"] in Cargo.toml; runs cbindgen (via a build.rs hook) 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's mochi.toml; and emits a Cargo.toml.lock pinning the runtime crate version. The driver's Build function gates the library path on Driver.LibraryMode=true plus target == TargetRustLibrary. See 01-language-surface §3, 06-cargo-publish-flow §1.

  6. 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.io runs, the bridge: (a) builds the library crate via TargetRustLibrary; (b) packages it via cargo package --no-verify --allow-dirty; (c) obtains an OIDC token from the CI environment (GitHub Actions id-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 legacy CARGO_REGISTRY_TOKEN flow 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-run flag that skips the upload step (the same flag MEP-53 phase 15 already exercises). See 07-sigstore-cargo-rfc3724 §1, 02-design-philosophy §5.

  7. Async bridge via a single process-wide tokio::runtime::Runtime constructed lazily on first async-rust call. Idiomatic Rust crates (notably tokio, reqwest, sqlx, axum, hyper, serde_json's async variants) expose async fn as a substantial part of their surface. Mochi's source-level async is 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 single tokio::runtime::Runtime instance via OnceCell in the wrapper crate the first time any async-fn-wrapping symbol is called, then dispatching each async call through runtime.block_on(async { ... }). The tokio runtime is built with Builder::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" in mochi.toml. This bridge is rejected for embedded targets (no tokio under no_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:

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

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

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

  4. The "import rust" surface has zero learning curve for Mochi users. import rust "tokio@^1.42" as tokio is the same shape as import go "strings" and import 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 a Box<dyn Future> is, what tokio::runtime::Runtime::block_on does, or what a Sigstore Fulcio cert is. They write import rust "..." as ... and the bridge does the rest.

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

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

  7. 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's cargo build only compiles, never generates code from rustdoc). There are no proc-macros in the wrapper crate (they would defeat reproducibility). There is no unsafe fn in 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-side cargo add, cdylib for non-Rust-side dlopen). Default ["rlib"].
  • cbindgen: whether to emit a C header alongside the cdylib. Default false.

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). Default false.
  • fs: the dep graph reads or writes files (std::fs, tokio::fs). Default false.
  • proc: the dep graph spawns processes (std::process::Command). Default false.
  • unsafe: the dep graph contains items that the user has hand-overridden through a extern fn ... from rust "..." with unsafe. Default false.

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 plus mochi.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 runs mochi 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 verify crate-blake3, rustdoc-sha256, wrapper-sha256, and capabilities-declared for every [[rust-package]] entry.
  • mochi pkg publish --to=crates.io [--dry-run]: builds the package as a Rust library crate via TargetRustLibrary, packages it with cargo 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-run skips upload but still exercises the signing flow against a sigstore-mock-fulcio harness.
  • 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:

  1. Before invoking cargo build, the driver invokes package3/rust/Bridge.PrepareWorkspace(workdir, mochiLock) which:

    • For each [[rust-package]] in mochi.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.toml workspace manifest that includes <workdir>/Cargo.toml (the main package) plus every <workdir>/rust_wrap/<crate>/Cargo.toml as workspace members.
    • The main package's Cargo.toml gains a [dependencies] entry for each wrapper crate: <crate>_mochi_wrap = { path = "../rust_wrap/<crate>" }.
  2. The MEP-53 emit pass treats each import rust "<crate>" as <alias> as a Mochi import "./rust_wrap/<crate>/shim.mochi" as <alias> shim, where the shim file is the extern fn corpus the bridge emitted.

  3. The wrapper crate's Cargo.toml declares the source crate as a [dependencies] entry with the exact version pinned from mochi.lock.

  4. The user's cargo build --release (invoked by Driver.Build) compiles every workspace member, producing the wrapper as a staticlib artefact, and links the main binary against it via cargo:rustc-link-lib=static.

  5. The driver invalidates the cache when any [[rust-package]] wrapper-sha256 changes (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

Phasehost stable rust 1.95 (darwin-arm64)musl-x64 / musl-arm64wasm32-wasip1embedded (no_std + alloc)
0. skeletonLANDEDn/an/an/a
1. sparse-index clientLANDEDn/an/an/a
2. rustdoc-JSON ingestLANDEDn/an/an/a
3. type-mapping tableLANDEDn/an/an/a
4. wrapper synthesiserLANDEDrequiredn/a (no cc on wasm32)required (no_std subset only)
5. extern emitterLANDEDrequiredrequiredrequired
6. import rust grammarLANDEDrequiredrequiredrequired
7. build orchestrationLANDEDLANDEDn/a (cargo workspace required)n/a
8. mochi.lock integrationLANDEDLANDEDLANDEDLANDED
9. TargetRustLibrary emitLANDEDLANDEDn/a (lib is rlib + cdylib for native)n/a
10. trusted publishingLANDEDn/a (publish is host-only)n/an/a
11. async bridgeLANDEDrequiredn/a (no tokio on wasm32-wasip1)n/a
12. monomorphisation of genericsLANDEDLANDEDLANDEDLANDED
13. embedded subsetLANDEDn/an/aLANDED

A phase marked n/a for a target is intentional: the bridge does not promise the behaviour on that target.

Alternatives considered

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

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

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

  4. Use uniffi-rs for the binding layer. Rejected: uniffi is Mozilla's Rust-to-Swift/Kotlin/Python binding generator; it generates code from a .udl interface description file. Mochi users would have to author a .udl for 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.

  5. Use diplomat for the binding layer. Rejected: diplomat requires #[diplomat::bridge] annotations on the Rust side, similar to cxx. Same boilerplate-violation argument.

  6. 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=wit flag that consumes WIT when the crate ships one. See 12-risks-and-alternatives §A6 for the deferred design.

  7. Direct dlopen of pre-built .so / .dylib artefacts from crates.io rather than per-import wrapper compilation. Rejected: crates.io does not host pre-built binaries (only .crate source 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.

  8. Translate Rust ownership and borrow semantics into Mochi's ownership-free surface. Rejected: Mochi does not have a &'a T syntax, does not have lifetime annotations, does not have move semantics distinct from copy. The bridge sidesteps the impedance mismatch by translating every borrow into a clone (the wrapper takes &str parameters as *const c_char which the Mochi side passes by copy) and every move into an opaque-handle ownership transfer (the wrapper takes Box<T> as *mut T which the Mochi side owns and explicitly frees via the synthesised _free symbol). This is the same strategy PyO3 and napi-rs use.

  9. Allow long-lived CARGO_REGISTRY_TOKEN API 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.

  10. 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 explicit Arc<Mutex<...>> wrappers in their own Mochi code.

  11. Run cargo build on every Mochi build to pick up rustdoc drift automatically. Rejected: mochi.lock is 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.

  12. Make import rust defer 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 is mochi.toml. Mixing manifest authorities is a known anti-pattern (Bazel's WORKSPACE-plus-MODULE.bazel transition shows the cost of authority ambiguity).

Risks

  1. Rustdoc JSON is behind -Z unstable-options on stable Rust. The --output-format=json flag 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 invokes rustup toolchain install nightly automatically if not present; the user's normal cargo build runs on stable. A future Rust release (expected 2026-2027) is anticipated to stabilise the flag.

  2. Rustdoc-types schema version drift. The rustdoc-types crate 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 via mochi pkg lock --rust-nightly=<date>.

  3. Wrapper crate compile times. A wrapper for tokio is ~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.

  4. 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 50 cargo build codegen units. The combinatorial explosion is real. Mitigation: the [rust.monomorphise] table is required to enumerate; the bridge does not auto-monomorphise.

  5. Async bridge tokio runtime cost. A single Mochi program with one tokio::time::sleep(...).await call 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).

  6. Capability declaration drift. A cargo update of 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 --check catches this, but only at lock time; if the user does not run --check, the production binary ships with the new capability. Mitigation: CI must run mochi pkg lock --check. The MEP-57 capability monotonicity rule applies transitively.

  7. 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-fallback flag for the transition period (default off). The flag is removed once crates.io GA is reached.

  8. 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.toml rust-version = "1.78" field must be updated to match. The bridge writes a rust-version = <pinned> field per ingest; subsequent mochi pkg lock --rust-toolchain=<new> regenerates with the new toolchain.

  9. 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, the extern "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.

  10. 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.io walks 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.

  11. 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 in serde_derive, tokio, reqwest, regex, rayon, rand_chacha all require std. Mitigation: the embedded gate (phase 13) checks no_std compatibility at lock time; non-compatible crates are rejected with a clear diagnostic.

  12. CI image dependency on cargo-zigbuild plus nightly rustdoc. The MEP-53 gates already require cargo-zigbuild and wasmtime; MEP-73 adds nightly rustdoc and (optionally) cargo-cyclonedx for SBOM emission. The CI image is bigger. Mitigation: the standard mochilang/ci-rust image bundles the full toolset; standalone users install via rustup 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.