1
Fork 0

Address feedback

This commit is contained in:
Jan-Erik Rediger 2022-01-31 10:41:35 +01:00 committed by Jan-Erik Rediger
parent eb86f20875
commit d15bea31e9

View file

@ -15,7 +15,8 @@ excerpt: |
into an iOS application.
---
(“This Week in Glean” is a series of blog posts that the Glean Team at Mozilla is using to try to communicate better about our work. They could be release notes, documentation, hopes, dreams, or whatever: so long as it is inspired by Glean.)
(“This Week in Glean” is a series of blog posts that the Glean Team at Mozilla is using to try to communicate better about our work.
They could be release notes, documentation, hopes, dreams, or whatever: so long as it is inspired by Glean.)
All "This Week in Glean" blog posts are listed in the [TWiG index](https://mozilla.github.io/glean/book/appendix/twig.html)
(and on the [Mozilla Data blog](https://blog.mozilla.org/data/category/glean/)).
This article is [cross-posted on the Mozilla Data blog][datablog].
@ -26,7 +27,7 @@ This article is [cross-posted on the Mozilla Data blog][datablog].
We ship the Glean SDK for multiple platforms, one of them being iOS applications.
Previously I talked about [how we got it to build on the Apple ARM machines](/2021/04/16/rustc-ios-and-an-m1/).
Today we will take a closer look at how we bundle a Rust static library into an iOS application.
Today we will take a closer look at how to bundle a Rust static library into an iOS application.
The Glean SDK project was set up in 2019 and we have evolved its project configuration over time.
A lot has changed in Xcode since then, so for this article we're starting with a fresh Xcode project, a fresh Rust library and put it all together step by step.
@ -116,18 +117,19 @@ pub extern "C" fn shipping_rust_addition(a: c_int, b: c_int) -> c_int {
}
```
The `no_mangle` ensures the name lands in the compiled library as is
The `no_mangle` ensures the name lands in the compiled library as-is
and the `extern "C"` makes sure it uses the right ABI.
We now got a Rust library, exporting a C-ABI compatible interface.
We now have a Rust library exporting a C-ABI compatible interface.
We can now consume this in our iOS application.
## The Xcode parts
Before we can use the code we need a bit more setup.
Strap in, there's a lot of fiddly manual steps now[^1].
We start by linking against the `libshipping_rust_ffi.a` library.
In your Xcode project open your target configuration[^1],
In your Xcode project open your target configuration[^2],
go to "Build Phases", then look for "Link Binary with Libraries".
Add a new one, in the popup select "Add files" on the bottom left
and look for the `target/debug/libshipping_rust_ffi.a` file.
@ -135,16 +137,17 @@ Yes, that's actually for the wrong target. This is just for the name, we'll fix
Go to "Build Settings" and search for "Library Search Paths".
It probably has the path to file in there right now for both `Debug` and `Release` builds.
Remove that one for `Debug`, then add a new row by clicking the small `+` symbol.
Select any matcher, e.g. `Any Driverkit`. It doesn't matter what value you give it.
We will manually modify that entry next.
Select the `Any Driverkit` matcher.
It doesn't matter which matcher you choose or what value you give it,
but when we overwrite this manually in the next step I'll assume you chose `Any Driverkit`.
Do the same for the `Release` configuration.
Once that's done, save your project and go back to your project directory.
We will modify the project configuration to have Xcode look for the library based on the target it is building for[^2].
We will modify the project configuration to have Xcode look for the library based on the target it is building for[^3].
Open up `ShippingRust.xcodeproj/project.pbxproj` in a text editor,
then search for the first line with `"LIBRARY_SEARCH_PATHS[sdk=driverkit*]"`.
It should be in a section saying `/* Debug */`.
Remove that line and add 3 new ones:
Remove the `LIBRARY_SEARCH_PATHS` line and add 3 new ones:
```
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/target/aarch64-apple-ios/debug";
@ -160,14 +163,16 @@ Look for the next line with `"LIBRARY_SEARCH_PATHS[sdk=driverkit*]"`, now in a `
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/target/x86_64-apple-ios/release";
```
Save the file and return back to Xcode.
If you didn't make any typos Xcode should still be able to open your project.
Save the file and return focus back to Xcode.
If you didn't make any typos Xcode should still have your project open.
In the settings you will find the library search paths as we've just defined them.
If you messed something up Xcode will complain that it cannot read the project file if you try to go to the settings.
Next we need to teach Xcode how to compile Rust code.
Once again go to your target settings, selecting the "Build Phases" tab again.
There add a new "Run Script" phase, give it a name (double-click the "Run Script" section header),
There add a new "Run Script" phase, give it the name "Build Rust library"
(double-click the "Run Script" section header),
and set the command to:
```
@ -181,19 +186,26 @@ This is not yet defined, so let's do it first.
Go to the "Build Settings" tab and click the `+` button to add a new "User-Defined Setting".
Give it the name `buildvariant` and choose a value based on the build variant: `debug` for `Debug` and `release` for `Release`.
Now we're ready to write the `compile-library.sh` script.
Now we need the actual script to build the Rust library for the right targets.
It's a bit long to write out, but the logic is not too complex:
First we select the Cargo profile to use based on our `buildvariant` (that is whether to pass `--release` or not),
then we set up `LIBRARY_PATH` if necessary and finally compile the Rust library for the selected target.
Xcode passes the architectures to build in `ARCHS`.
It's either `x86_64` for simulator builds on Intel Mac hardware or `arm64`.
If it's `arm64` it can be either the simulator or an actual hardware target.
Those differ, but we can know which is which from what's in `LLVM_TARGET_TRIPLE_SUFFIX` and select the right Rust target.
Let's put all of that into a `compile-library.sh` script.
Create a new directory `bin` in your project directory.
In there create the file with the following content:
```bash
#!/usr/bin/env bash
# This should be invoked from inside xcode, not manually
if [ "$#" -ne 2 ]
then
echo "Usage (note: only call inside xcode!):"
echo "Args: $*"
echo "path/to/build-scripts/xc-universal-binary.sh <FFI_TARGET> <buildvariant>"
echo "compile-library.sh <FFI_TARGET> <buildvariant>"
exit 1
fi
@ -204,7 +216,7 @@ BUILDVARIANT=$2
RELFLAG=
if [[ "$BUILDVARIANT" != "debug" ]]; then
RELFLAG=--release
RELFLAG=--release
fi
set -euvx
@ -246,13 +258,7 @@ for arch in $ARCHS; do
done
```
It's a bit long, but not too complex:
It selects the cargo profile to use (that is whether to pass `--release` or not),
sets up the `LIBRARY_PATH` if necessary and finally compiles the Rust library for the selected target.
Xcode passes the architectures to build in `ARCHS`.
It's either `x86_64` for simulator builds on Intel Mac hardware or `arm64`.
If it's `arm64` it can be either the simulator or an actual hardware target.
Those differ, but we can know which is which from what's in `LLVM_TARGET_TRIPLE_SUFFIX` and select the right Rust target.
And now we're done with the setup for compiling the Rust library automatically as part of the Xcode project build.
## The code parts
@ -307,7 +313,7 @@ struct ContentView: View {
We simply interpolate the result of `shipping_rust_addition(30, 1)` into the string displayed.
Once we compile and run it in the simulator we succeeded:
Once we compile and run it in the simulator we see we've succeeded at satisfying our premise:
> Show a non-interactive message to the user with data from a Rust library.
@ -321,18 +327,27 @@ This was a lot of setup for calling one simple function.
Luckily this is a one-time setup. From here on you can extend your Rust library, define them in the header file and call them from Swift.
If you go that route you should really start using [cbindgen] to generate that header file automatically for you.
This time we looked at building an iOS application directly calling a Rust library.
That's not actually how Glean works. The Glean Swift SDK itself wraps the Rust library and exposes a Swift library.
In a next blog post I'll showcase how we ship that as a Swift package.
For Glean we're stepping away from manually writing our FFI functions.
We're instead migrating our code base to use [UniFFI].
UniFFI will generate the C API from an API definitions file and also comes with a bit of runtime code to handle conversion between Rust, C and Swift data types for us.
We're not there yet for Glean, but you can try it on your own. [Read the UniFFI documentation][uniffi-docs] and integrate it into your project.
It should be possible to extent the setup we done her to also run the necessary steps for UniFFI.
Eventually I'll document how we did it as well.
[cbindgen]: https://github.com/eqrion/cbindgen
[uniffi]: https://github.com/mozilla/uniffi-rs/
[uniffi-docs]: https://mozilla.github.io/uniffi-rs/
---
_Footnotes:_
[^1]: Click your project name in the tree view on the left. This gets you to the project configuration (backed by the `ShippingRust.xcodeproj/project.pbxproj` file). You should then see the Targets, including your `ShippingRust` target and probably `ShippingRustTests` as well. We need the former.
[^2]: Previously we would have build a universal library containing the library of multiple targets. That doesn't work anymore now that `arm64` can stand for both the simulator and hardware targets. Thus linking to the individual libraries is the way to go, as the now-deprecated [`cargo-lipo`][cargo-lipo] also points out.
[^1]: And most of these steps are user-interface-dependent and might be different in future Xcode version. :(
[^2]: Click your project name in the tree view on the left. This gets you to the project configuration (backed by the `ShippingRust.xcodeproj/project.pbxproj` file). You should then see the Targets, including your `ShippingRust` target and probably `ShippingRustTests` as well. We need the former.
[^3]: Previously we would have built a universal library containing the library of multiple targets. That doesn't work anymore now that `arm64` can stand for both the simulator and hardware targets. Thus linking to the individual libraries is the way to go, as the now-deprecated [`cargo-lipo`][cargo-lipo] also points out.
[cargo-lipo]: https://github.com/TimNN/cargo-lipo