Rust on iOS
You may have heard of rust, it’s a systems programming language designed for memory safety and speed. Built by Mozilla to power the next generation high performance cross platform software. If you haven’t already I suggest having a look at the great learning material, but keep in mind that can take a while to get into and appreciate so I suggest writing more than just a hello world.
If you’re an iOS developer you may be asking yourself how and why you would make use of rust on iOS. This article will mostly cover the how. As to why, the most compelling reason for us at Visly is that it enables us to share code between Android and iOS in a performant and safe manner, in a language much easier to work with than C++.
Getting set up
Before we get started we need to make sure we have the rust toolchain set up. We will assume you already have a working iOS toolchain, if not you should download Xcode and set it up according to any other iOS guide. To make sure you have the xcode command line tools installed run the following command. This all assumes you are running macOS as that is a requirement to build for iOS.
Next we have to make sure rust is installed on our system. Rustup makes this a simple one-liner.
You can validate rust was successfully installed and located in your
PATH by running
rustc --version. Once rust is installed on your system we need to make rust aware of how to build for the supported iOS architectures. Rust can build binaries for all sorts of architectures but not by default. To add the appropriate architectures run the following command.
rustc we also installed
cargo onto our system.
cargo is the rust package manager, much like Cocoapods, homebrew, or npm it allows us to add dependencies as well as global binaries to our system. We’ll use
cargo to install
cargo-lipo, a tool for building universal iOS binaries, as well as
cbindgen a tool for generating C headers from rust. These will help manage all the intricacies of building and linking iOS libraries for us.
That’s it! If you can successfully run
cargo lipo --help and
cbindgen --help you know everything was installed correctly.
Let’s build a small hello world app using rust! We’ll start by creating our rust library and later move on to creating our Xcode project.
This will create a basic rust library managed by
cargo which we will later make use of in our Xcode project. The
--lib flag instructs
cargo that we want to create a library, not an executable binary. Within the newly created project folder you will find
Cargo.toml which much like a
Podfile defines metadata for your library as well as any dependencies. You’ll also find a
src folder which contains our rust source code. The
src folder only contains
lib.rs which itself only contains a sample test function. We can start by removing everything in this file and replacing it with the following.
These are two basic functions, one which contructs a new string given an input string and the other which de-allocates the provided string. Because rust, much like C or C++, manages its own memory we must do manual memory management when passing objects between Swift and Rust. Later on we will link to some patterns we use in Shard to make this easier. There are a couple other things to notice here, because we are interfacing with swift we have to make use of C calling conventions, this means we have to tell rust not to mangle any names (with
#[no_mangle]), we also have to convert between rust strings and C strings. In a larger application this is not much of an issue as the glue code between Swift and Rust can be kept fairly small.
Before we can start trying to use this code in a Swift project we have to generate a C header so Swift can understand this code. We do this using
cbindgen which we installed earlier.
One last thing we have to do before moving on to Xcode is building our rust library. Open
Cargo.toml and add the following to the end of the file.
cargo-lipo what kind of binary to produce as well as what name it should be given. Now we are ready to run
cargo lipo --release. If all goes well you should have a
librust.a file located in
Time to start a new Xcode project and test this out in a simulator. Start by going through the standard Xcode project setup, we’ll be using Swift but you can use Objective-C if you want as well. We will name the project
hello-rust saving it next to our rust library at the root of
rust-ios-example. Open the generated
ViewController.swift and replace its contents with the following.
If you try to build and run this Xcode should give you an error saying that it cannot find
rust_hello_free. Nothing weird here, we still need to add the library and header we created earlier!
Start by copying over the rust binary and header file into our Xcode project.
Now open the Xcode build settings panel and add the added library as a linked library, set the header as the bridging header, and set the correct library and header search path.
At this point your project should build and run. Running it should produce a “Hello World” message in your logs. Remember that at this point we haven’t automated the process of compiling the rust code or placing the resulting binary in the correct location so you will need to redo the import of the header file and binary any time you re-compile the rust code.
Automating the process
Automating this process of copying over binaries and headers is actually pretty easy with a simple bash script.
Now save this as
rust-ios-example/install.sh and just run the script after any updates to your rust code to compile and install it into your Xcode project. If you want to get fancy you can add this as a build step in Xcode so it is automatically run any time you build your Xcode project.
While the code above works, it isn’t super easy to work with. In a larger project we want a way to encapsulate the ugly bits of interacting with rust from swift and vice versa. Take a look at some of the patterns we have adopted when building Shard. All the code is available on GitHub.