This is a part of the post Mobile: A different way using Rust?.
There are some great resources out there on UniFFI already such as this post, but it doesn’t cover watchOS, so let’s take a quick tour through what I’ve set up in the example repository https://github.com/Tehnix/template-mobile-wasm.
We've set up four crates:
appy
: Our Leptos App, Capacitor, and the XCode projectcapacitor-rs
: Bridging code between the Capacitor JS library and our Rust codeshared
: Our shared code that we might use in appy
, and also want to expose in Swift to use in our Widgets or watchOS Appmobile
: Where we will generate the Swift bindings from via UniFFI, reexporting everything from shared
that’s made available to UniFFI via the macrosI won’t go over the details to get these to play nicely with Cargo and Workspaces, check out the repository for that. Let’s instead focus on a simplified version of what mobile
does (the rest assumes you’re in the mobile/
directory).
If starting from scratch, create a new cargo project:
Update your Cargo.toml
:
1 []
2 = "mobile"
3 = "0.1.0"
4 = "2021"
5
6 []
7 = ["staticlib"]
8 = "mobile"
9
10 [[]]
11 = "uniffi-bindgen"
12 = "src/bin/uniffi-bindgen.rs"
13
14 []
15 = 'z' # Optimize for size.
16 = true # Enable Link Time Optimization.
17 = true # Automatically strip symbols from the binary.
18 = "abort"
19 = false
20 # Optional:
21 # codegen-units = 1 # Reduce Parallel Code Generation Units to Increase Optimization.
22
23 []
24 # UniFFI dependencies for generating Swift bindings.
25 = { = "0.28.0", = ["cli"] }
Update your src/lib.rs
:
1 !;
setup_scaffolding 2
3
4 5 6 7 8
9
10
11 12 13 14 15 16 17
We’re using setup_scaffolding
to avoid needing to manually construct headers, modulemaps, and the UDL files (check out the docs here).
And finally, create a new file src/bin/uniffi-bindgen.rs
:
1 2 3
We’re now ready to build the binary for generating our bindings, and then use that to generate the actual bindings:
# Build the uniffi-bindgen binary and our initial library file.
We also need to rename the FFI file to module.modulemap
so that XCFramework will find it:
Now, let's add support for iOS, the Simulator and macOS via rustup:
and then build the library for all of our targets:
We'll combine x86_64-apple-ios
and aarch64-apple-ios-sim
into a single binary later on, but for now we keep them separate.
If we want watchOS we need to handle things a bit differently, since these are Tier 3 targets (i.e. rustup won't have their stdlib):
That's a lot of targets, which represent all the various Watch models, as well as the simulators (we always need both ARM and x86).
xcodebuild
won't be happy if we just drop them in individually, so we need to create a fat binary:
# Combine the watchOS simulator libraries into a single file using lipo.
# Confirm the architectures.
# Combine the watchOS libraries into a single file using lipo.
# Confirm the architectures.
We can then create our XCFramework:
And finally, we'll combine x86_64-apple-ios
and aarch64-apple-ios-sim
into a single binary. If we included both of these in the XCFramework, xcodebuild
would complain that these are the same, and not generate our XCFramework file. Oddly enough, it will not be able to build the project without both, so we let xcodebuild
generate the XCFramework first, and then replace the binary with the fat binary:
# We need to combine the architectures for the iOS Simulator libraries after we've
# constructed the XCFramework, otherwise it will complain about them being the same,
# while also failing because of missing x86_64 if we omit it.
# Confirm the architectures.
# Move it into place.
Done!
As the final step we drag-n-drop ./ios/Shared.xcframework
and ./bindings/shared.swift
into the XCode project whereever you want them. I personally like to create a new group (folder) called Generated
for them (the build-ios.sh
script assumes that's the case).