1 unstable release
new 0.1.0 | Mar 3, 2025 |
---|
#602 in Development tools
245KB
4.5K
SLoC
Gobley
Kotlin Multiplatform bindings generation for UniFFI. This project was forked from UniFFI Kotlin Multiplatform bindings. Since the original project is no longer maintained, active development now continues here.
Currently, Android, Kotlin/JVM, and Kotlin/Native are supported. WASM is not supported yet.
Features
- Bindings Generation for Kotlin Multiplatform (Android, JVM, Kotlin/Native)
- KotlinX Serialization Support
- Automatic building and linking of Rust libraries to Kotlin projects
Getting started
We recommend to first read the UniFFI user guide. Then, please read this documentation in the following order.
- Development environment and common development practices
- Using the Gradle plugin
- The Bindgen
- Bindgen configuration
- Cross-compilation tips
Development environment and common development practices
There are only one option to code both in Kotlin and Rust in a single IDE: IntelliJ IDEA Ultimate, which needs a paid subscription. Fleet was another option, but JetBrains announced that they are dropping the KMP support in Fleet.
Therefore, most users may use different IDEs for Kotlin and Rust when developing with this project. For Kotlin, you
can use Android Studio or IntelliJ IDEA, and for Rust, you can use rust-analyzer
with Visual Studio Code or RustRover.
In normal cases, Kotlin handles the part interacting with users such as UI while Rust handles the core business logic,
so using two IDEs won't harm the developer experience that much.
Since building Rust takes much time than compiling Kotlin, try separating the Kotlin part that uses Rust directly as a
core library. You can build and publish the core library using the maven-publish
plugin and the other Kotlin part can
download it from a maven repository.
The more platforms you target, the larger the build result will be. Ensure your CI has enough space to build your project.
Gradle caches files from the build in ~/.gradle/caches
. If you encounter much more No space left on device
errors after
using this project, try removing ~/.gradle/caches
. Since Gradle still tries to find cached files in ~/.gradle/caches
after you remove it, remove all .gradle
and build
directories in your project as well. On macOS & Linux:
find . -name .gradle | xargs rm -rf
find . -name "build" | grep -v '^./target' | xargs -r rm -rf
In PowerShell on Windows:
Get-ChildItem . -Attributes Directory -Recurse |
Where-Object { $_.Name -eq ".gradle" } |
ForEach-Object { Remove-Item -Recurse -Force $_ }
Get-ChildItem . -Attributes Directory -Recurse |
Where-Object { $_.Name -eq "build" } |
Where-Object { -not $_.FullName.Contains("\target\") } |
ForEach-Object { Remove-Item -Recurse -Force $_ }
When you build iOS apps, Xcode generates files in /private/var/folders/zz
, which are removed automatically after every
reboot. Try restart your Mac if you still have the disk space issue after removing the Gradle caches.
Using the Gradle plugin
This project contains three Gradle plugins:
- The Cargo plugin (
dev.gobley.cargo
) - The UniFFI plugin (
dev.gobley.uniffi
) - The Rust plugin (
dev.gobley.rust
)
The Cargo plugin
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
dev.gobley.cargo
the plugin.
plugins {
kotlin("multiplatform")
id("dev.gobley.cargo") version "0.1.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 gobley.gradle.cargo.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 gobley.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 gobley.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 embedRustLibrary
property. For example, to
build a shared library for the current build host only, set this property to
rustTarget == GobleyHost.current.rustTarget
.
import gobley.gradle.GobleyHost
import gobley.gradle.cargo.dsl.*
cargo {
builds.jvm {
embedRustLibrary = (rustTarget == GobleyHost.current.rustTarget)
}
}
On Windows, both MinGW and Visual C++ can generate DLLs. By default, the Cargo plugin doesn't invoke the MinGW build
for JVM when Visual C++ is available. To override this behavior, use the embedRustLibrary
property like the
following. Note that Windows on ARM is not available with MinGW.
import gobley.gradle.GobleyHost
import gobley.gradle.cargo.dsl.*
import gobley.gradle.rust.targets.RustWindowsTarget
cargo {
builds.jvm {
if (GobleyHost.Platform.Windows.isCurrent) {
when (rustTarget) {
RustWindowsTarget.X64 -> embedRustLibrary = false
RustPosixTarget.MinGWX64 -> embedRustLibrary = true
else -> {}
}
}
}
}
embedRustLibrary
is also used when you use the external types feature
in your project. Rust statically links all the crates unless you specify the library crate's kind as dylib
. So, the
final Kotlin library does not have to include shared libraries built from every crate. Suppose you have two crates, foo
, and
bar
, where foo
exposes the external types and bar
uses types in foo
. Since when building bar.dll
, libbar.dylib
, or
libbar.so
, the foo
crate is also included in bar
, you don't have to put foo.dll
, libfoo.dylib
, or libfoo.so
inside your Kotlin library. So, to configure that, put the followings in foo/build.gradle.kts
:
cargo {
builds.android {
embedRustLibrary = false
}
builds.jvm {
embedRustLibrary = false
}
}
and in foo/uniffi.toml
:
# The cdylib_name used in `bar/uniffi.toml`
cdylib_name = "bar"
The JVM loadIndirect()
function in the bindings allow users to override the cdylib_name
value using the
uniffi.component.<namespace name>.libraryOverride
system property as well. See the
:tests:uniffi:ext-types:ext-types
test to see how this works.
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 gobley.gradle.cargo.dsl.*
import gobley.gradle.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:desktopJar
The 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:publishToMavenLocal
In 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
}
}
}
}
If your Rust library is dependent on other shared libraries, you have to ensure that they are also available during
runtime. For JVM and Android builds, you can use the dynamicLibraries
and the dynamicLibrarySearchPaths
properties.
The specified libraries will be embedded into the resulting JAR or the Android bundle.
cargo {
builds.android {
// Copies libaaudio.so and libc++_shared.so from NDK
dynamicLibraries.addAll("aaudio", "c++_shared")
}
builds.jvm {
// Copies libmyaudio.so or myaudio.dll
dynamicLibraries.addAll("myaudio")
}
}
Some directories like the NDK installation directory or the Cargo build output directory are already registered in
dynamicLibrarySearchPaths
. If your build system uses another directory, add that to this property.
The UniFFI plugin
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 gobley.gradle.Variant
import gobley.gradle.rust.targets.RustAndroidTarget
plugins {
kotlin("multiplatform")
id("dev.gobley.cargo") version "0.1.0"
id("dev.gobley.uniffi") 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 Rust target of the build to use to generate the bindings. If unspecified, one of the available builds
// will be automatically selected.
build = RustAndroidTarget.Arm64
// 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
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("dev.gobley.rust") 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 gobley.gradle.rust.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 gobley.gradle.rust.dsl.*
kotlin {
iosArm64().compilations.getByName("main") {
useRustUpLinker()
}
}
The Bindgen
The bindings generator (the "bindgen") is the program that generates Kotlin source codes connecting your Kotlin code to your Rust code. In most cases, the UniFFI Gradle plugin handles the bindings generation, so you don't have to know all the details of the bindgen. Still, you can directly use this bindgen if you have more complicated build system.
The minimum Rust version required to install gobley-uniffi-bindgen
is 1.72
. Newer Rust versions should
also work fine. The source code of the bindgen for Kotlin Multiplatform is in bindgen
. See comments in
bindgen/src/main.rs
or
BuildBindingsTask.kt
to see how to use the bindgen from the command line.
To install the bindgen, run:
cargo install --bin gobley-uniffi-bindgen gobley-uniffi-bindgen@0.1.0
to invoke the bindgen, run:
gobley-uniffi-bindgen --lib-file <path-to-library-file> --out-dir <output-directory> --crate <crate-name> <path-to-udl-file>
If you want to use the bindgen in your own Crago build script, please read the "Generating foreign-language bindings" part in the official UniFFI documentation.
When the bindings are generated correctly, it has a directory structure like the following.
<output directory>
├── androidMain
│ └── kotlin
│ └── <namespace name>
│ └── <namespace name>.android.kt
├── commonMain
│ └── kotlin
│ └── <namespace name>
│ └── <namespace name>.common.kt
├── jvmMain
│ └── kotlin
│ └── <namespace name>
│ └── <namespace name>.jvm.kt
├── nativeInterop
│ └── headers
│ └── <namespace name>
│ └── <namespace name>.h
└── nativeMain
└── kotlin
└── <namespace name>
└── <namespace name>.native.kt
Bindgen configuration
Various settings used by the bindgen can be configured in <manifest dir>/uniffi.toml
. For more
details, see bindgen/src/gen_kotlin_multiplatform/mod.rs
or Config.kt
.
Configuration Name | Type | Description |
---|---|---|
package_name |
String | The Kotlin package name to use. Defaults to uniffi.<namespace name> . |
cdylib_name |
String | The name of the resulting dynamic library without the prefix (e.g. lib ) and the file extension. Defaults to the library's name when bindings are generated from it, or uniffi_<namespace> when generated from a UDL file. |
generate_immutable_records |
Boolean | When true , generated data classes has val properties instead of var . |
custom_types |
See the documentation | |
kotlin_target_version |
String | The Kotlin version used by your project. Newer syntax will be used (e.g. data object or Enum.entries ) when the compiler of the specified version supports. This is automatically set to the Kotlin Gradle plugin version by the UniFFI Gradle plugin. |
disable_java_cleaner |
Boolean | When true , com.sun.jna.internal.Cleaner will be used instead of android.system.SystemCleaner or java.lang.ref.Cleaner . Defaults to false . Consider changing this option when your project targets JVM 1.8. |
generate_serializable_types |
Boolean | When true , data classes will be annotated with @kotlinx.serialization.Serializable when possible. This is automatically set to true by the UniFFI Gradle plugin when your Kotlin project uses KotlinX Serialization. |
jvm_dynamic_library_dependencies |
String Array | The list of dynamic libraries required by your Rust library on Desktop JVM targets without the prefix and the file extension. Use this if your project depends on an external dynamic library. |
android_dynamic_library_dependencies |
String Array | The list of dynamic libraries required by your Rust library on Android without the prefix and the file extension. |
dynamic_library_dependencies |
String Array | The list of dynamic libraries required by your Rust library on both Desktop JVM targets and Android targets. |
Cross-compilation tips
Linux cross-compilation on Windows or macOS
If this is your first time cross-compiling a Linux binary on macOS, you may encounter a linker error reporting that the linker has received some unknown command-line arguments. That happens because you tried to link a Linux binary using the linker for Apple platforms.
error linking with `cc` failed: exit status 1
|
= note: LC_ALL="C" PATH="..." "<linker path>" <arguments ...>
= note: ld: unknown options: --version-script=... --no-undefined-version ...
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Similarly, if you try to cross-compile a Linux binary on Windows, you may encounter a different
error by Cargo that it couldn't find cc
.
error: linker `cc` not found
|
= note: program not found
Install Zig
When you want to build your application or library for Linux on Windows or macOS, you have to use a dedicated cross-compilation linker for Linux. There are two available options: GCC and Zig. The latter is much easier to install.
You can manually download Zig here. If you're using a package manager, you can also install Zig as follows:
Package Manager | Command | Zig Installation Path |
---|---|---|
WinGet (Windows) | winget install -e --id zig.zig |
%LOCALAPPDATA%\Microsoft\WinGet\Links\zig.exe |
Chocolatey (Windows) | choco install zig |
C:\ProgramData\chocolatey\bin\zig.exe |
Homebrew (macOS) | brew install zig |
/opt/homebrew/bin/zig (Apple Silicon) or /usr/local/bin/zig (Intel) |
Next, we have to make Cargo use Zig when building libraries for Linux. First, find where zig
is
installed. If you installed Zig using a package manager, the installation path is mentioned above.
Make sure Zig is installed correctly.
On Windows using PowerShell:
> & "${env:LOCALAPPDATA}\Microsoft\WinGet\Links\zig.exe" version
0.13.0
> C:\ProgramData\chocolatey\bin\zig.exe version
0.13.0
or CMD:
> %LOCALAPPDATA%\Microsoft\WinGet\Links\zig.exe version
0.13.0
On macOS:
> /opt/homebrew/bin/zig version
0.13.0
Make Cargo use Zig (Windows)
We make two batch scripts that uses the zig
command. You can use any name, but we'll use the
following names.
%USERPROFILE
isC:\Users\<user name>
.
%USERPROFILE%\.cargo\x86_64-unknown-linux-gnu-cc.bat
%USERPROFILE%\.cargo\aarch64-unknown-linux-gnu-cc.bat
In x86_64-unknown-linux-gnu-cc.bat
, put the following:
@echo off
<zig path> cc -target x86_64-linux-gnu %*
If you installed Zig with WinGet, the content is:
@echo off
%LOCALAPPDATA%\Microsoft\WinGet\Links\zig.exe cc -target x86_64-linux-gnu %*
Similarly, in aarch64-unknown-linux-gnu-cc.sh
, put as follows:
@echo off
<zig path> cc -target aarch64-linux-gnu %*
This is the final step. Put the paths to the script files in
the Cargo configuration file. The easiest
option is to modify %USERPROFILE%\.cargo\config.toml
as follows.
[target.x86_64-unknown-linux-gnu]
linker = "C:\\Users\\<user name>\\.cargo\\x86_64-unknown-linux-gnu.bat"
[target.aarch64-unknown-linux-gnu]
linker = "C:\\Users\\<user name>\\.cargo\\aarch64-unknown-linux-gnu.bat"
You're ready to start building your library and application for Linux.
Make Cargo use Zig (macOS)
We make two shell scripts that uses the zig
command. You can use any name, but we'll use the
following names.
~/.cargo/x86_64-unknown-linux-gnu-cc.sh
~/.cargo/aarch64-unknown-linux-gnu-cc.sh
In x86_64-unknown-linux-gnu-cc.sh
, put the following:
#! /bin/sh
<zig path> cc -target x86_64-linux-gnu "$@"
If you installed Zig with Homebrew on a Apple Silicon Mac, the content is:
#! /bin/sh
/opt/homebrew/bin/zig -target x86_64-linux-gnu "$@"
Similarly, in aarch64-unknown-linux-gnu-cc.sh
, put as follows:
#! /bin/sh
<zig path> cc -target aarch64-linux-gnu "$@"
After making two script files, ensure that these files are executable.
chmod 555 ~/.cargo/x86_64-unknown-linux-gnu-cc.sh
chmod 555 ~/.cargo/aarch64-unknown-linux-gnu-cc.sh
This is the final step. Put the paths to the script files in
the Cargo configuration file. The easiest
option is to modify ~/.cargo/config.toml
as follows.
[target.x86_64-unknown-linux-gnu]
linker = "/Users/<user name>/.cargo/x86_64-unknown-linux-gnu-cc.sh"
[target.aarch64-unknown-linux-gnu]
linker = "/Users/<user name>/.cargo/aarch64-unknown-linux-gnu-cc.sh"
You're ready to start building your library and application for Linux.
LLVM version compatibility on Apple Platforms
If you encounter an undefined symbols linker error like the following when building your Rust library that has a dependency on a C library for iOS, you may have an LLVM version compatibility issue.
Undefined symbols for architecture arm64:
"___chkstk_darwin", referenced from:
<function name> in <library file name or object file name>
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
To check your Rust toolchain's LLVM version, use rustc --version --verbose
. For example,
> rustc --version --verbose
rustc 1.82.0 (f6e511eec 2024-10-15)
binary: rustc
commit-hash: f6e511eec7342f59a25f7c0534f1dbea00d01b14
commit-date: 2024-10-15
host: aarch64-apple-darwin
release: 1.82.0
LLVM version: 19.1.1
You can see that Rust 1.82 uses LLVM 19. To check the LLVM version used by Xcode, use
/usr/bin/gcc --version
(Yeah, Apple puts Clang in that path.)
> /usr/bin/xcodebuild -version
Xcode 16.2
Build version 16C5032a
> /usr/bin/gcc --version
Apple clang version 16.0.0 (clang-1600.0.26.6)
Target: arm64-apple-darwin24.3.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
You can see Xcode 16.2 uses LLVM 16. So, the linker in Apple LLVM 16 tried to link object files that targets LLVM 19, which resulted in a linker error.
To resolve this issue, try downgrading your Rust toolchain to a version that uses lower LLVM version. For example, Rust 1.81 uses LLVM 18, so downgrading to 1.81 might help.
> rustup target add 1.81
> rustup default 1.81
> rustc --version --verbose
rustc 1.81.0 (eeb90cda1 2024-09-04)
binary: rustc
commit-hash: eeb90cda1969383f56a2637cbd3037bdf598841c
commit-date: 2024-09-04
host: aarch64-apple-darwin
release: 1.81.0
LLVM version: 18.1.7
You can also set the toolchain directory via the toolchainDirectory
property in the rust {}
block, so
consider using this if you don't want to rustup default 1.81
.
To see which Rust version uses which LLVM version, see the Rust compiler
CHANGELOG. You can see
LLVM version upgrade notes in Internal Changes
sections.
C++ Runtime on Android NDK
Android NDK has multiple kinds of C++ runtime libraries, so it is important to check which one you are using now.
If you encounter a linker error (whether it's dynamic or static) mentioning functions like __cxa_pure_virtual
,
you may have not linked C++ runtime to your library properly.
Such error can be a dynamic library load error in runtime like the following:
dlopen failed: cannot locate symbol "__cxa_pure_virtual" referenced by "/data/app/.../libyourlibrary.so"
or a linking error occurred during build as below.
<file name>: undefined reference to `__cxa_pure_virtual'
Currently, two C++ runtime libraries are available: libc++_static.a
and libc++_shared.so
. The criteria
for choosing which one to use is explained in detail in
the official documentation. If you are
embedding your Rust library to an application, use libc++_shared.so
.
To link C++ runtime, use Cargo build scripts.
Configure the path to the directory containing the runtime libraries via the cargo::rustc-link-search
command. Use cargo::rustc-link-lib
to control which runtime library to link. For example in
<manifest dir>/build.rs
:
use std::env;
use std::path::PathBuf;
fn main() {
// The ANDROID_NDK_ROOT variable is automatically set to <SDK root>/ndk/<NDK version> by the
// Cargo Gradle plugin. You may want to implement your own logic finding the path to NDK if
// you are not invoking Cargo from Gradle.
let android_ndk_root = env::var("ANDROID_NDK_ROOT").unwrap();
// set this to false if you want to use libc++_static.a.
let use_shared = true;
let host = if cfg!(target_os = "windows") {
"windows-x86_64"
} else if cfg!(target_os = "macos") {
// Apple Sillion Macs also use x86_64.
"darwin-x86_64"
} else if cfg!(target_os = "linux") {
"linux-x86_64"
} else {
panic!("unsupported host")
};
let ndk_triplet = match env::var("CARGO_CFG_TARGET_ARCH").unwrap().as_str() {
"aarch64" => "aarch64-linux-android",
"arm" => "arm-linux-androideabi",
"x86_64" => "x86_64-linux-android",
"x86" => "i686-linux-android",
/* RISC-V is not supported by this project yet */
_ => panic!("unsupported architecture"),
};
// `libc++_shared.so` and `libc++_static.a` are in
// toolchains/llvm/prebuilt/<host>/sysroot/usr/lib/<NDK triplet>.
let library_dir = PathBuf::from(android_ndk_root)
.join("toolchains")
.join("llvm")
.join("prebuilt")
.join(host)
.join("sysroot")
.join("usr")
.join("lib")
.join(ndk_triplet);
// Configure the library directory path.
println!("cargo::rustc-link-search={}", library_dir.display());
// Configure the library name.
println!(
"cargo::rustc-link-lib={}={}",
if use_shared { "dylib" } else { "static" },
if use_shared { "c++_shared" } else { "c++_static" },
);
}
Some Rust libraries automatically find libc++_static.a
or libc++_shared.so
from NDK, and they usually
allow users to control this using Cargo features. If some of your dependency uses libc++_static.a
while
others use libc++_shared.so
, you may encounter another linker error like the following.
ld: error: <HOME>/.rustup/toolchains/.../lib/rustlib/armv7-linux-androideabi/lib/libcompiler_builtins-...(compiler_builtins-... .o): symbol __aeabi_memcpy8@@LIBC_N has undefined version LIBC_N
Before such error is printed, Cargo shows the entire linker invocation arguments. For example, you may be able to see something like:
error: linking with `<linker path>` failed: exit status: 1
|
= note: LC_ALL="C" PATH="..." "<linker path>" <arguments ...>
Copy this error and see if both -lc++_static
and -lc++_shared
are in the invocation. If this is the
case, inspect the output emitted by build scripts of dependencies. You can read it from
target[/<Cargo triplet>]/<profile>/build/<package name>-<hash>/output
. For example in
target/aarch64-linux-android/debug/build/blake3-<hash>/output
, you can see something like the following.
cargo:rerun-if-env-changed=CARGO_FEATURE_PURE
cargo:rerun-if-env-changed=CARGO_FEATURE_NO_NEON
cargo:rerun-if-env-changed=CARGO_FEATURE_NEON
cargo:rerun-if-env-changed=CARGO_FEATURE_NEON
cargo:rerun-if-env-changed=CARGO_FEATURE_NO_NEON
cargo:rerun-if-env-changed=CARGO_FEATURE_PURE
cargo:rustc-cfg=blake3_neon
TARGET = Some("aarch64-linux-android")
OPT_LEVEL = Some("0")
HOST = Some("aarch64-apple-darwin")
...
Check whether there is something like cargo::rustc-link-lib=c++_static
in it.
When you use libc++_shared.so
, it should be embedded into the application. Use the dynamicLibraries
property in the builds.android {}
block to ensure libc++_shared.so
is included in the resulting
Android application/library.
cargo {
builds.android {
dynamicLibraries.addAll("c++_shared")
}
}
Building for Windows on ARM
By default on an x64 machine, Visual Studio installs MSVC for x64/x86 only. If you try to link a
program for ARM64 without the MSVC ARM64 toolchain, you may see an error that Cargo couldn't find
link.exe
.
> cargo build --target aarch64-pc-windows-msvc
error: linker `link.exe` not found
|
= note: program not found
note: the msvc targets depend on the msvc linker but `link.exe` was not found
note: please ensure that Visual Studio 2017 or later, or Build Tools for Visual Studio were installed with the Visual C++ option.
note: VS Code is a different product, and is not sufficient.
Make sure you installed the ARM64/ARM64EC compilers and linkers via Visual Studio Installer. Double-check whether you installed the ARM64 toolchain instead of the 32-bit ARM toolchain. This project does not support building for 32-bit ARM Windows.
Versioning
gobley-uniffi-bindgen
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. gobley-uniffi-bindgen
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, gobley-uniffi-bindgen
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 gobley-uniffi-bindgen
versions are tied to uniffi-rs
are tied:
gobley-uniffi-bindgen version | uniffi-rs version |
---|---|
v0.1.0 | v0.25.2 |
Build and use locally
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. Since this project contains many unit tests and examples, you may want to opt out of building them. Use the following Gradle properties to choose tests and examples to turn on and off.
Gradle property name | Projects |
---|---|
gobley.projects.gradleTests |
:tests:gradle |
gobley.projects.uniffiTests |
:tests:uniffi & :examples:custom-types |
gobley.projects.examples |
:examples |
These following properties are already in gradle.properties
. Simply replace =true
to =false
to turn
them off.
If you want to run projects in :tests:gradle
or :examples
, install the following dependencies.
- Rust
- Zig 0.13
- MinGW (GCC 13)
- OpenJDK 17
- Android SDK 35 with CMake (CMake is used by
:tests:gradle:android-linking
) - Android NDK
- Perl (Used to build OpenSSL by
:examples:tokio-blake3-app
) - Visual C++ x64 & ARM64 (Windows)
- Xcode (macOS)
See .meta/build-image/Dockerfile
for more details.
Option 1 - Dynamically include the plugins in your project
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("dev.gobley.uniffi") if you have it here
}
}
// ...
includeBuild("../uniffi-kotlin-multiplatform-bindings/build-logic") {
dependencySubstitution {
substitute(module("dev.gobley.uniffi:gradle-plugin"))
.using(project(":gradle-plugin"))
}
}
Add the Gradle plugin to the Gradle build file.
// build.gradle.kts
plugins {
kotlin("multiplatform")
id("dev.gobley.uniffi")
// ...
}
Optionally, configure the uniffi
extension with the exact path to the bindgen of this repository.
uniffi {
// ...
bindgenFromPath("<path-to-our-bindgen>")
}
Option 2 - Publish the plugins locally
Clone the repository and build it.
Run the following to publish the plugins:
./gradlew :build-logic:gobley-gradle:publishToMavenLocal
./gradlew :build-logic:gobley-gradle-cargo:publishToMavenLocal
./gradlew :build-logic:gobley-gradle-rust:publishToMavenLocal
./gradlew :build-logic:gobley-gradle-uniffi:publishToMavenLocal
Add 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://github.com/gobley/gobley", "v0.1.0")
}
Dependencies
~8.5MB
~157K SLoC