This project contains Kotlin Multiplatform bindings generation for UniFFI.
Currently only the Kotlin targets JVM and Native are supported. JS support would be awesome, but needs WASM support within uniffi.
You can find examples on how to use the bindings in the tests directory.
We recommend to first read the UniFFI user guide. If you follow their tutorial, then you can use the Kotlin Multiplatform bindings as explained bellow during the "Generating foreign-language bindings" part.
For instructions on how to install the plugin and runtime please refer to the instructions here.
Add the following lines to the build.gradle.kts file of your project:
plugins {
kotlin("multiplatform")
id("io.gitlab.trixnity.uniffi.kotlin.multiplatform") version "0.6.0"
id("io.gitlab.trixnity.cargo.kotlin.multiplatform") version "0.6.0"
// ...
}
// ...
cargo {
// the directory where your rust crate is located
packageDirectory = layout.projectDirectory.dir("rust")
}
// ...
uniffi {
bindgenFromGitTag("https://gitlab.com/trixnity/uniffi-kotlin-multiplatform-bindings", "v0.6.0")
generateFromLibrary()
}
// ...
kotlin {
// ...
sourceSets {
commonMain {
dependencies {
// Currently necessairy to add the runtime manually
implementation("ch.ubique.uniffi.runtime:0.1.0")
}
}
}
}Now you're good to go!
This project contains three Gradle plugins:
- The Cargo plugin (
io.gitlab.trixnity.cargo.kotlin.multiplatform) - The UniFFI plugin (
io.gitlab.trixnity.uniffi.kotlin.multiplatform) - The Rust plugin (
io.gitlab.trixnity.rust.kotlin.multiplatform)
The Cargo plugin is responsible for building and linking the Rust library to your Kotlin project. You can use it even
when you are not using UniFFI. If the Cargo.toml is located in the project root, you can simply apply the
io.gitlab.trixnity.cargo.kotlin.multiplatform the plugin.
plugins {
kotlin("multiplatform")
id("io.gitlab.trixnity.cargo.kotlin.multiplatform") version "0.6.0"
}If the Cargo package is located in another directory, you can configure the path in the cargo {} block.
cargo {
// The Cargo package is located in a `rust` subdirectory.
packageDirectory = layout.projectDirectory.dir("rust")
}Since searching Cargo.toml is done
by cargo locate-project,
it still works even if you set packageDirectory to a subdirectory, but it is not recommended.
cargo {
// This works
packageDirectory = layout.projectDirectory.dir("rust/src")
}If you want to use Cargo features or
customized Cargo profiles,
you can configure them in the cargo {} block as well.
import io.gitlab.trixnity.gradle.cargo.rust.profiles.CargoProfile
cargo {
features.addAll("foo", "bar")
debug.profile = CargoProfile("my-debug")
release.profile = CargoProfile.Bench
}If you want to use different features for each variant (debug or release), you can configure them in the debug {} or
release {} blocks.
cargo {
features.addAll("foo")
debug {
// Use "foo", "logging" for debug builds
features.addAll("logging")
}
release {
// Use "foo", "app-integrity-checks" for release builds
features.addAll("app-integrity-checks")
}
}features are inherited from the outer block to the inner block. To override this behavior in the inner block,
use .set() or the = operator overloading.
cargo {
features.addAll("foo")
debug {
// Use "foo", "logging" for debug builds
features.addAll("logging")
}
release {
// Use "app-integrity-checks" (not "foo"!) for release builds
features.set(setOf("app-integrity-checks"))
}
}For configurations applied to all variants, you can use the variants {} block.
cargo {
variants {
features.addAll("another-feature")
}
}For Android and Apple platform builds invoked by Xcode, the plugin automatically decides which profile to use. For other
targets, you can configure it with the jvmVariant or nativeVariant properties. When undecidable, these values
default to Variant.Debug.
import io.gitlab.trixnity.gradle.Variant
cargo {
jvmVariant = Variant.Release
nativeVariant = Variant.Debug
}Cargo build tasks are configured as the corresponding Kotlin target is added in the kotlin {} block. For example, if
you don't invoke androidTarget() in kotlin {}, the Cargo plugin won't configure the Android build task as well.
cargo {
builds.android {
println("foo") // not executed
}
}
kotlin {
// The plugin will react to the targets definition
jvm()
linuxX64()
}The Cargo plugin scans all the Rust dependencies
using cargo metadata. If you modify Rust source files
including those in dependencies defined in the Cargo manifest, the Cargo plugin will rebuild the Cargo project.
For Android builds, the Cargo plugin automatically determines the SDK and the NDK to use based on the property values of
the android {} block. To use different a NDK version, set ndkVersion to that version.
android {
ndkVersion = "26.2.11394342"
}The Cargo plugin also automatically determines the ABI to build based on the value
of android.defaultConfig.ndk.abiFilters. If you don't want to build for x86 or x86_64, set this
to ["arm64-v8a", "armeabi-v7a"].
android {
defaultConfig {
ndk.abiFilters += setOf("arm64-v8a", "armeabi-v7a")
}
}The Cargo plugin automatically configures environment variables like ANDROID_HOME or CC_<target> for you, but if you
need finer control, you can directly configure the properties of the build task. The build task is accessible in the
builds {} block.
import io.gitlab.trixnity.gradle.cargo.dsl.*
cargo {
builds {
// Configure Android builds
android {
debug.buildTaskProvider.configure {
additionalEnvironment.put("CLANG", "/path/to/clang")
}
}
// You can configure for other targets as well
appleMobile {}
desktop {}
jvm {}
mobile {}
native {}
posix {}
mingw {}
linux {}
macos {}
windows {}
}
}For JVM builds, the Cargo plugin tries to build all the targets, whether the required toolchains are installed on the current system or not. The list of such targets by the build host is as follows.
| Targets | Windows | macOS | Linux |
|---|---|---|---|
| Android | ✅ | ✅ | ✅ |
| Apple Mobile | ❌ | ✅ | ❌ |
| MinGW | ✅ | ✅ | ✅ |
| macOS | ❌ | ✅ | ❌ |
| Linux | ✅ | ✅ | ✅ |
| Visual C++ | ✅ | ❌ | ❌ |
To build for specific targets only, you can configure that using the jvm property. For example, to build a shared
library for the current build host only, set this property to rustTarget == RustHost.current.rustTarget.
import io.gitlab.trixnity.gradle.RustHost
import io.gitlab.trixnity.gradle.cargo.dsl.*
cargo {
builds.jvm {
jvm = (rustTarget == RustHost.current.rustTarget)
}
}Android local unit tests requires JVM targets to be built, as they run in the host machine's JVM. The Cargo plugin
automatically copies the Rust shared library targeting the host machine into Android local unit tests. It also finds
projects that depend on the project using the Cargo plugin, and the Rust library will be copied to all projects that
directly or indirectly use the Cargo project. If you want to include shared library built for a different platform, you
can control that using the androidUnitTest property.
import io.gitlab.trixnity.gradle.cargo.dsl.*
import io.gitlab.trixnity.gradle.cargo.rust.targets.RustWindowsTarget
cargo {
builds.jvm {
// Use Visual C++ X64 for Android local unit tests
androidUnitTest = (rustTarget == RustWindowsTarget.X64)
}
}
kotlin {
jvm()
androidTarget()
}Local unit tests are successfully built even if there are no builds with androidUnitTest enabled, but you will
encounter a runtime error when you invoke a Rust function from Kotlin.
When you build or publish your Rust Android library separately and run Android local unit tests in another build, you also have to reference the JVM version of your library from the Android unit tests.
To build the JVM version, run the <JVM target name>Jar task. The name of the JVM target can be configured with the
jvm() function, which defaults to "jvm". For example, when the name of the JVM target is "desktop":
kotlin {
jvm("desktop")
}the name of the task will be desktopJar.
# ./gradlew :your:library:<JVM target name>Jar
./gradlew :your:library:desktopJarThe build output will be located in build/libs/<project name>-<JVM target name>.jar. In the above case, the name of
the JAR file will be <project name>-desktop.jar. The JAR file then can be referenced using the files or the
fileTree functions.
kotlin {
sourceSets {
getByName("androidUnitTest") {
dependencies {
// implementation(files("<project name>-<JVM target name>.jar"))
implementation(files("library-desktop.jar"))
implementation("net.java.dev.jna:jna:5.13.0") // required to run
}
}
}
}The above process can be automated using the maven-publish Gradle plugin. It publishes the JVM version of your library
separately. For more details about using maven-publish with Kotlin Multiplatform, please refer
here.
To publish your library to the local Maven repository on your system, run the publishToMavenLocal task.
./gradlew :your:project:publishToMavenLocalIn the local repository which is located in ~/.m2, you will see that multiple artifacts including <project name> and
<project name>-<JVM target name> are generated. To reference it, register the mavenLocal() repository and put the
artifact name to implementation().
repositories {
mavenLocal()
// ...
}
kotlin {
sourceSets {
getByName("androidUnitTest") {
dependencies {
// implementation("<group name>:<project name>-<JVM target name>:<version>")
implementation("your.library:library-desktop:0.1.0")
implementation("net.java.dev.jna:jna:5.13.0") // required to run
}
}
}
}The UniFFI plugin is responsible for generating Kotlin bindings from your Rust package. Here is an example of using the UniFFI plugin to build bindings from the resulting library binary.
import io.gitlab.trixnity.gradle.Variant
plugins {
kotlin("multiplatform")
id("io.gitlab.trixnity.cargo.kotlin.multiplatform") version "0.1.0"
id("io.gitlab.trixnity.uniffi.kotlin.multiplatform") version "0.1.0"
}
uniffi {
// Generate the bindings using library mode.
generateFromLibrary {
// The UDL namespace as in the UDL file. Defaults to the library crate name.
namespace = "my_crate"
// The name of the build that makes the library to use to generate the bindings. The list of the names can be
// retrieved with `cargo.builds.names`. If not specified, the UniFFI plugin automatically selects a build.
build = "AndroidArm64"
// The variant of the build that makes the library to use. If unspecified, the UniFFI plugin automatically picks
// one.
variant = Variant.Debug
}
}If you want to generate bindings from a UDL file as well, you can specify the path using the generateFromUdl {} block.
uniffi {
generateFromUdl {
namespace = "..."
build = "..."
variant = Variant.Debug
// The UDL file. Defaults to "${crateDirectory}/src/${crateName}.udl".
udlFile = layout.projectDirectory.file("rust/src/my_crate.udl")
}
}If you want to run ktlint on the generated bindings set formatCode to true.
uniffi {
formatCode = true
}The Rust plugin is for configuring the Rust toolchain you want to use or linking your Rust library to your Kotlin
project. By default, the plugins think cargo and rustup are installed in ~/.cargo/bin or a directory registered in
the PATH environment variable, which is okay for almost everyone.
However, if you have installed cargo or rustup in another directory, you can provide that information to the plugin
via the rust {} block. The information in the rust {} block is automatically passed to the Cargo or the UniFFI
plugins.
plugins {
id("io.gitlab.trixnity.rust.kotlin.multiplatform") version "0.1.0"
}
rust {
toolchainDirectory = File("/path/to/my/Rust/toolchain")
}The Rust plugin also defines two extension functions KotlinMultiplatformExtension.hostNativeTarget
and KotlinNativeCompilation.useRustUpLinker.
hostNativeTarget can be invoked in kotlin {} and adds the Kotlin Native target for the build host; it invokes
mingwX64 on Windows, macosX64 or macosArm64 on macOS, and linuxX64 or linuxArm64 on Linux, though Linux Arm64
build host is not supported yet.
import io.gitlab.trixnity.gradle.cargo.dsl.*
kotlin {
hostNativeTarget()
}useRustUpLinker is for Kotlin Native projects referencing a Rust library but not directly using Rust. Since Kotlin
Native is shipped with an LLVM older than the one shipped with the Rust toolchain, you may encounter a linker error
when building that Kotlin Native project. useRustUpLinker automatically finds the LLVM linker distributed
with rustup, so you can use this when your Rust project emits a linker flag that is not supported by the Kotlin Native
LLVM linker.
import io.gitlab.trixnity.gradle.cargo.dsl.*
kotlin {
iosArm64().compilations.getByName("main") {
useRustUpLinker()
}
}Minimum Rust version required to install uniffi_bindgen_kotlin_multiplatform is 1.72.
Newer Rust versions should also work fine.
Install the bindgen:
cargo install --bin uniffi-bindgen-kotlin-multiplatform uniffi_bindgen_kotlin_multiplatform@0.1.0Invoke the bindgen:
uniffi-bindgen-kotlin-multiplatform --lib-file <path-to-library-file> --out-dir <output-directory> --crate <crate-name> <path-to-udl-file>uniffi_bindgen_kotlin_multiplatform is versioned separately from uniffi-rs. UniFFI follows the
SemVer rules from the Cargo Book
which states "Versions are considered compatible if their left-most non-zero major/minor/patch
component is the same". A breaking change is any modification to the Kotlin Multiplatform bindings
that demands the consumer of the bindings to make corresponding changes to their code to ensure that
the bindings continue to function properly. uniffi_bindgen_kotlin_multiplatform is young, and it's
unclear how stable the generated bindings are going to be between versions. For this reason, major
version is currently 0, and most changes are probably going to bump minor version.
To ensure consistent feature set across external binding generators, uniffi_bindgen_kotlin_multiplatform
targets a specific uniffi-rs version. A consumer using these bindings or any other external
bindings (for example, Go bindings or
C# bindings) expects the same features to be
available across multiple bindings generators. This means that the consumer should choose external
binding generator versions such that each generator targets the same uniffi-rs version.
Here is how uniffi_bindgen_kotlin_multiplatform versions are tied to uniffi-rs are tied:
| uniffi_bindgen_kotlin_multiplatform version | uniffi-rs version |
|---|---|
| v0.1.0 | v0.25.2 |
| v0.6.0 | v0.28.3 |
If you want to work on the bindgen or the Gradle plugin locally, you will have to do some additional Gradle configuration in order to use these local versions in your projects.
Clone this repository and reference it from your project. Configure dependencySubstitution to use the local plugin
version.
// settings.gradle.kts
pluginManagement {
// ..
includeBuild("../uniffi-kotlin-multiplatform-bindings/build-logic")
// ...
plugins {
// comment out id("io.gitlab.trixnity.uniffi.kotlin.multiplatform") if you have it here
}
}
// ...
includeBuild("../uniffi-kotlin-multiplatform-bindings/build-logic") {
dependencySubstitution {
substitute(module("io.gitlab.trixnity.uniffi.kotlin.multiplatform:gradle-plugin"))
.using(project(":gradle-plugin"))
substitute(module("ch.ubique.uniffi.runtime"))
.using(project(":runtime"))
}
}
includeBuild("../uniffi-kotlin-multiplatform-bindings/runtime") {
dependencySubstitution {
substitute(module("ch.ubique.uniffi.runtime"))
.using(project(":runtime"))
}
}
Add the Gradle plugin to the Gradle build file.
// build.gradle.kts
plugins {
kotlin("multiplatform")
id("io.gitlab.trixnity.uniffi.kotlin.multiplatform")
// ...
}Optionally, configure the uniffi extension with the exact path to the bindgen of this repository.
uniffi {
// ...
bindgenFromPath("<path-to-our-bindgen>")
}Clone the repository and build it.
Then invoke:
./gradlew :build-logic:gradle-plugin:publishToMavenLocal
./gradlew :runtime:publishToMavenLocalAdd the local repository in your project's settings.gradle.kts:
pluginManagement {
repositories {
mavenLocal()
// ...
}
}Optionally, configure the uniffi extension with the exact path to the bindgen of this repository.
uniffi {
// ...
bindgenFromPath("<path-to-our-bindgen>")
}You can also install the bindgen from a git remote as well. Use this method if you don't want to keep the source code of this repository on your computer.
uniffi {
bindgenFromGitTag("https://gitlab.com/trixnity/uniffi-kotlin-multiplatform-bindings", "v0.6.0")
}