One of the advantages of writing code in Rust is that it can be re-used in other places. Both iOS and Android allow using native libraries within your apps, and Rust compiles to native. Web pages can now use WebAssembly (WASM), and Rust can compile to WASM.
So, it should be easy, right?
Well, in practice it seems a little tricky, so I created a small example project to explain it to myself, so maybe it’s helpful to you too.
The full code is at gitlab.com/andybalaam/example-rust-bindings, but here is the general idea:
- crates/example-rust-bindings – the real Rust code
- bindings/ffi – uniffi code to build shared objects for Android and iOS
- bindings/wasm – wasm_bingen code to build WASM for Web
- examples/example-android – an Android app that generates a Kotlin wrapper, and runs the code in the shared object
- examples/example-ios – an iOS XCode project where we generate Swift bindings, so we can call the code in the shared object
- examples/example-web – a web page that imports the WASM and runs it
Steps for WASM
- Write normal Rust code e.g. crates/example-rust-bindings/src/lib.rs (and Cargo.toml etc. to go with it, with nothing special in it)
- Write wasm-bindgen wrappers for all interfaces you want to expose e.g. bindings/wasm/src/lib.rs (and Cargo.toml to go with it, depending on wasm-bindgen). This basically involves creating structs and functions annotated with #[wasm_bindgen] that call through to the real underlying code from the previous step.
- Write a package.json with a build step that calls wasm-pack e.g. bindings/wasm/package.json
- Build the bindings with npm (see bindings/wasm/README.md)
- Copy the generated .js and .wasm files into a web project
- Include the generated .js file into an HTML file using an import statement in a module e.g. examples/example-web/index.html
Variation: if you modify the build script in package.json to call wasm-pack with --target node instead of --target web you can generate code suitable for using from a NodeJS module.
Steps for Android
- Write normal Rust code e.g. crates/example-rust-bindings/src/lib.rs (and Cargo.toml etc. to go with it, with nothing special in it)
- Write a UDL file to describe your interfaces e.g. bindings/ffi/src/my_rust_code.udl
- Implement those interfaces in some Rust code, wrapping returned values in Arc e.g. bindings/ffi/src/lib.rs.
- Write a Cargo.toml that builds the bindings e.g. bindings/ffi/Cargo.toml
- Generate uniffi bindings scaffolding during the Cargo build by writing a build.rs file e.g. bindings/ffi/build.rs
- Get the Android NDK and set up Cargo’s config to use it to build – see bindings/ffi/README.md
- Cross-compile for the Android platforms you need by adding the targets using rustup and then building using cargo build --target=blah – see bindings/ffi/README.md
- Copy the built .so shared object files into your Android project under `jniLibs/INSERT_PLATFORM` – see bindings/ffi/README.md
- In your Android project, add a android.applicationVariants.all section to your app/build.gradle that generates Kotlin wrappers around the shared objects e.g. examples/example-android/app/build.gradle
- Now you can write normal Kotlin that accesses your Rust code via a namespace like uniffi.my_rust_code e.g. MainActivity.kt
Steps for iOS
- Write normal Rust code e.g. crates/example-rust-bindings/src/lib.rs (and Cargo.toml etc. to go with it, with nothing special in it)
- Write a UDL file to describe your interfaces e.g. bindings/ffi/src/my_rust_code.udl
- Implement those interfaces in some Rust code, wrapping returned values in Arc e.g. bindings/ffi/src/lib.rs.
- Write a Cargo.toml that builds the bindings e.g. bindings/ffi/Cargo.toml
- Generate uniffi bindings – see bindings/ffi/README.md
- Cross-compile for the iOS platforms you need by adding the targets using rustup and then building using cargo build --target=blah – see bindings/ffi/README.md
- Combine together the built libraries using lipo – see bindings/ffi/README.md
- Copy the combined .a library file into your XCode project – see bindings/ffi/README.md
- Generate an XCode project with xcodebuild – see bindings/ffi/README.md
- Now you can write normal Swift that accesses your Rust code [TODO: details]
Hi Andy,
Thanks for the great intro/reference. We’re currently looking at building a cross platform rust app targeting Android and Web. Being new to Rust, we’re in the process of getting our heads around the idea that we must choose an async runtime to underpin async language features like futures. We don’t have a need for multi-threading, but DX-friendly concurrency abstractions like futures and event-based/reactive programming will help us a lot in our particular domain which is heavily async.
Do you have any recommendation for how to solve this cross platform? We’ve seen that WASM bindgen can convert futures code to promises, but this ultimately ends up in the JS layer. I guess what we’re looking for is the ability for our rust module to use things like futures and events internally, and expose related methods/events/callbacks externally to JavaScript or Java/Kotlin code.
A concrete example would be the ability for the rust code to schedule an HTTP request and await a response, where the making of request is delegated to fetch or OkHttp respectively, and the response returned.
Any help much appreciated!
Hi Patrick, no good advice except it’s definitely worth looking at IniFFY: https://mozilla.github.io/uniffi-rs/ . Good luck!