MEP 70. Mochi and Kotlin package bridge
| Field | Value |
|---|---|
| MEP | 70 |
| Title | Mochi and Kotlin package bridge |
| Author | Mochi core |
| Status | Draft |
| Type | Standards Track |
| Created | 2026-05-29 23:12 (GMT+7) |
| Depends | MEP-1 (Grammar), MEP-2 (AST), MEP-4 (Type System), MEP-13 (ADTs and Match), MEP-45 (C transpiler, FFI sidecar pattern), MEP-53 (Rust transpiler, lowering pipeline), MEP-57 (module and package system, mochi.toml, mochi.lock, content-addressed store, Sigstore/trusted publishing) |
| Research | /docs/research/0070/ |
| Tracking | /docs/implementation/0070/ |
Abstract
Mochi today (May 2026) targets nine code-emitting backends and has a growing source-level package story via MEP-57, but has no connection to the Kotlin/JVM ecosystem. The Kotlin language (May 2026) commands the Android platform, has strong penetration in server-side JVM development via Spring Boot and Ktor, and ships over 70,000 artifacts on Maven Central under Kotlin-specific metadata annotations. Mochi authors cannot consume any of this ecosystem, and Kotlin developers cannot add a Mochi library via Gradle.
MEP-70 specifies the bidirectional Kotlin package bridge: a Mochi program can consume any well-formed Maven Central artifact whose JAR carries kotlin.Metadata annotations via import kotlin "<group>:<artifact>@<semver>" as <alias> with no user-written boilerplate, and a Mochi package can publish to Maven Central as a Kotlin library JAR via mochi pkg publish --to=maven-central. The bridge is the fifth source-language interop story Mochi ships (after Go, Python, TypeScript, and Rust from MEP-73), and the first one that targets the JVM platform, which requires a fundamentally different native interop strategy from the C-ABI-grounded bridges of prior MEPs.
The proposal adds a self-contained component under package3/kotlin/, extends MEP-57's manifest/lockfile/capability infrastructure, and adds one new build target (TargetKotlinLibrary) to the MEP-53 emit driver. The Mochi grammar gains the single keyword kotlin as a valid <lang> token in the existing FFI-import production. No existing transpiler MEP needs to change.
1. Motivation
1.1 The Kotlin ecosystem is large and Kotlin-native in metadata
Maven Central hosts over 70,000 artifacts with kotlin.Metadata class-file annotations (May 2026 snapshot). These include the full Jetbrains standard library (kotlin-stdlib, kotlinx-coroutines-core, kotlinx-serialization-json, kotlinx-datetime), all major server frameworks (Ktor, Spring Boot Kotlin extensions, Micronaut Kotlin), the Arrow functional-programming library, Exposed ORM, Koin and Kodein DI, Kotest, MockK, and the full suite of Android Jetpack libraries as AAR. None of these are reachable from Mochi without manual JNI boilerplate that would span dozens of files per artifact.
1.2 Kotlin metadata is stable and machine-readable
Since Kotlin 1.4 (September 2020), every Kotlin .class file carries a @kotlin.Metadata annotation whose data1/data2 binary fields encode the full public Kotlin API surface (function signatures, property types, nullability annotations, sealed class hierarchies, value classes, inline reified type parameters, default argument masks, and coroutine suspend status) in a versioned protobuf schema via the kotlinx-metadata-jvm library. This is the stable, officially-supported machine-readable surface; it supersedes reflection and javap output. The bridge ingests this annotation at lock time, requiring no Kotlin toolchain binary beyond the JAR on disk.
1.3 The JVM interop problem is solved by GraalVM Native Image
The fundamental challenge of calling JVM code from a native binary is JVM startup cost and the need for an embedded JVM. GraalVM Native Image (GraalVM CE 21+, available as a free download) solves both problems: it AOT-compiles a JVM program (including all transitive dependencies) into a platform-native shared library (libwrap.so / libwrap.dylib / libwrap.dll) with a plain C ABI that Mochi's existing FFI machinery can link against. The result has sub-millisecond initialization (no JVM startup), predictable memory usage (no GC pauses visible at the C boundary), and a stable versioned ABI. The bridge uses this path as its primary compilation target; the traditional libjvm.so embedding path is retained as a fallback for artifacts that defeat native-image (dynamic class loading, reflection-heavy frameworks).
1.4 Mochi as a Kotlin library producer opens the Android/JVM market
A Mochi package that targets TargetKotlinLibrary compiles its public API to a JVM bytecode JAR with the correct kotlin.Metadata annotations, so kotlinc and IntelliJ see it as an idiomatic Kotlin library: functions have their Mochi-declared types reflected as Kotlin types, nullable returns are annotated ?, unit-return functions emit Unit, and KDoc comments are extracted from Mochi doc-comments. The result is publishable to Maven Central and consumable with a plain implementation("group:artifact:version") Gradle line.
2. Design decisions
2.1 kotlinx-metadata-jvm over reflection
Runtime reflection (java.lang.reflect.*) sees only JVM types after type erasure, loses all Kotlin-specific information (nullability, inline classes, sealed hierarchies, suspend status), and requires a live JVM. kotlinx-metadata-jvm reads the @kotlin.Metadata binary annotation from .class bytes on disk, reconstructing the full pre-erasure Kotlin type surface including all extensions. The bridge uses only kotlinx-metadata-jvm; it never spawns a JVM for API surface extraction.
2.2 GraalVM Native Image as the primary runtime bridge
Three options exist for calling JVM code from a native binary: (a) embed a JVM via libjvm.so (JNI classic), (b) AOT-compile with GraalVM Native Image to a native shared library, (c) compile with Kotlin/Native to a platform-native static/dynamic library. Option (a) requires a JVM on the end-user machine and adds 200-400 ms of cold-start latency. Option (c) requires all transitive dependencies to be Kotlin/Native-compatible, which today excludes roughly 80% of Maven Central artifacts. Option (b) (GraalVM) requires only the GraalVM CE toolchain at build time (not at run time), produces a plain extern "C" shared library, adds under 5 ms cold-start, and supports all standard JVM artifacts including reflection with explicit reflect-config.json hints that the bridge synthesises automatically. MEP-70 uses (b) as the primary path and (a) as the [kotlin] runtime = "jvm-embed" override.
2.3 JNI-style C ABI wrapper over Panama FFI
Java 22+ Project Panama (java.lang.foreign) provides a safer, more ergonomic native interop model than JNI. However, GraalVM Native Image's shared-library mode exposes a JNI-compatible C ABI (the graal_create_isolate / graal_tear_down_isolate lifecycle plus standard jni.h function signatures) as its stable public interface. Mochi's existing C-ABI FFI machinery (from MEP-45 and MEP-53) already understands this shape. Using Panama would require a Panama-aware callsite generator entirely separate from the existing FFI pipeline. The bridge uses the JNI-compatible C ABI exposed by GraalVM; this is a private implementation detail hidden behind the synthesised extern fn declarations.
2.4 Maven Central via Sonatype Central Portal for publishing
As of February 2024, Sonatype retired the legacy Nexus-based OSSRH and replaced it with the Central Portal (central.sonatype.com), which exposes a REST API (/api/v1/publisher/upload) for deployment bundles. The bundle format is a ZIP containing the JARs (compiled classes, sources, Javadoc), POM, GPG .asc signatures, and SHA-1/MD5 checksums. The bridge synthesises this bundle and POSTs it via the API using a short-lived portal token obtained via Sonatype's OIDC-compatible CI integration (analogous to how MEP-73 uses Cargo's RFC #3724 trusted publishing). Long-lived API tokens are not accepted by the bridge.
2.5 No boilerplate for the user
The user writes one line: import kotlin "org.jetbrains.kotlinx:[email protected]" as json. The bridge resolves, fetches, compiles the wrapper, and synthesises the Mochi extern declarations. The user never writes @JvmStatic, @JvmName, JNIEnv*, jobject, jstring, or a Gradle build file.
3. Scope and non-goals
In scope:
- Consuming JVM-targeting Kotlin artifacts from Maven Central, JitPack, and Google Maven via
import kotlin. - Publishing Mochi packages as Kotlin/JVM JARs to Maven Central.
- Kotlin standard types:
Int,Long,Double,Boolean,String,List<T>,Map<K,V>,Set<T>,Pair<A,B>,Triple<A,B,C>,data class,enum class,sealed class(finite hierarchy),object(singleton),companion objectfunctions, nullableT?via MochiOption<T>. suspendfunctions via the coroutines bridge (Phase 13).- Kotlin Multiplatform libraries that have a JVM target.
- Android AAR consumer path (Phase 14 subset).
Out of scope:
- Kotlin/Native artifacts (
.klib): these have no JVM bytecode and no Maven Central presence at scale. A future MEP may add direct LLVM-IR interop. - Kotlin/JS artifacts.
- Dynamic class loading (
Class.forName,URLClassLoader) inside consumed artifacts: these defeat GraalVM Native Image and are rejected at lock time with a diagnostic. @JvmFieldwith non-final backing fields accessed by reflection: the bridge reads metadata, not runtime state.- Android-specific APIs on non-Android hosts: the bridge can consume Android library JARs and stubs them as
extern typeon non-Android targets; actual Android runtime is only available on Android targets. - Incremental compilation of the GraalVM wrapper: each
mochi pkg lockrebuilds the full wrapper for changed artifacts.
4. Surface syntax: import kotlin
The Mochi grammar's ImportStmt production gains kotlin as the fifth <lang> token:
ImportStmt := "import" Lang? StringLit "as" Ident ("auto")?
Lang := "go" | "python" | "typescript" | "rust" | "kotlin"
The string literal is one of:
| Form | Example | Resolution |
|---|---|---|
<group>:<artifact> | "org.jetbrains.kotlinx:kotlinx-coroutines-core" | Bare coordinates. Version resolved from [kotlin-dependencies] + mochi.lock. |
<group>:<artifact>@<version> | "org.jetbrains.kotlinx:[email protected]" | Explicit version constraint. |
<group>:<artifact>@<version>@<classifier> | "org.jetbrains.kotlin:[email protected]@sources" | Explicit classifier (rare; used for sources or test JARs). |
Example surface:
fn fetch_user(url: string): User {
let client = okhttp.OkHttpClient()
let request = okhttp.Request.Builder().url(url).build()
let response = client.newCall(request).execute()
let body = response.body().string()
return json.Json.decodeFromString(body)
}
The auto modifier is supported: import kotlin "..." as k auto binds every public top-level declaration of the artifact at file scope.