Runtime

Source: runtime/. Published as ch.ubique.uniffi:runtime, with the same version as the plugin.

The runtime holds everything the generated code needs that doesn't depend on a particular crate. Generated files start with import uniffi.runtime.*.

Why a separate library

Upstream UniFFI generators copy all helper code into every generated file, and each copy allocates RustBuffers through its own crate's rustbuffer_alloc. That works for one crate per app, but not for several modules that share types: the converter for a shared type can't know which crate a buffer will be passed to. With a shared runtime:

  • every RustBuffer that Kotlin lowers is allocated by the runtime's own Rust library, so any crate can receive it,
  • every module uses the same RustBuffer, FfiConverter, UniffiHandleMap and InternalException classes, so values can pass between modules.

This is the basis of multi-module support. In addition, the helper code exists once per app, not once per module.

Contents

Source set Contents
commonMain UniffiHandleMap, InternalException, expect declarations for Pointer
jvmMain, androidMain, nativeMain FfiConverter, ByteBuffer, RustBuffer and RustBufferHelper, ForeignBytes / withForeignBytes, uniffiRustCall, converters for every primitive type, String, ByteArray, Instant, Duration, the async helpers, FfiConverterCallbackInterface, UniffiCleaner
src/commonMain/rust/lib.rs a Rust crate uniffi_runtime that exports nothing but setup_scaffolding!()

The three platform source sets are separate hand-written copies. JVM and Android are nearly identical. Native differs where JNA and cinterop differ: structures, pointers, callbacks (staticCFunction) and the cleaner. A fix in one usually has to be applied to all three.

The runtime's own Rust library

The runtime is a UniFFI crate itself, with an empty interface. setup_scaffolding!() still exports the standard FFI functions, in particular ffi_uniffi_runtime_rustbuffer_alloc and _free. The module applies this repository's plugin with bindgenFromPath(..., features = listOf("runtime")), and addRuntime = false. The runtime feature makes the bindgen emit only the UniffiLib declarations for this crate (see Bindgen). The Rust library is packaged like any other: JVM resources, Android jniLibs, cinterop. Its cinterop klib also carries the shared common.h, which every generated crate reuses (see Bindgen).

RustBufferHelper.allocValue calls ffi_uniffi_runtime_rustbuffer_alloc. Every RustBuffer that Kotlin lowers is allocated by the runtime's library, and freed by whichever crate receives it. This only works because all of them use the same allocator, which is true for Rust's default system allocator. A crate that installs its own #[global_allocator] breaks it. There is no simple fix, because the allocation has to happen before Kotlin knows which crate will receive the buffer. The user guide warns about this in Requirements.

Relation to the templates

Much of the runtime started out as templates in bindgen/src/templates/generic/ffi/. Those templates have been removed: everything that doesn't depend on the crate (primitive, String, ByteArray, Instant and Duration converters, RustBuffer handling, the async helpers and the object cleaner) comes from the runtime on every platform. What is left in generic/ffi/ is code that is specific to a crate's types.

If generated code needs a new crate-independent helper, add it to the runtime, in all three platform source sets, rather than to the templates.

Tests

runtime/src/commonTest, androidHostTest and androidDeviceTest test the runtime directly. tests/runtime is a separate fixture that uses the runtime through generated bindings.