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
RustBufferthat Kotlin lowers is allocated by the runtime's own Rust library, so any crate can receive it, - every module uses the same
RustBuffer,FfiConverter,UniffiHandleMapandInternalExceptionclasses, 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.