Search

Building an iOS App in Rust, Part 1: Getting Started with Rust

John Gallagher

11 min read

Jun 7, 2015

iOS

Building an iOS App in Rust, Part 1: Getting Started with Rust

Editor’s note: This is the first post in our series on building an iOS app in Rust.

The vast majority of apps that get developed for iOS and Android are written in
the native languages provided by the platform: Swift or Objective-C on iOS, and
Java on Android. I don’t expect that to change any time soon; however,
sometimes there’s a need for something more.

If you’re developing an app on multiple platforms more or less independently,
you’ll face certain challenges. Functionality will be duplicated (obviously), which means
you have two different codebases that need to be maintained. Bugs can crop up
on one platform or the other or both, and new features have to be added to
each. An alternative approach, which Dropbox talked about at last year’s
UIKonf and CppCon (video 1, video
2
), is to develop a library that can be shared by both
platforms.

Developing a cross-platform library is challenging for a number of reasons, not
the least of which that the choice of language is pretty limited. There are
some tools, like Google’s J2ObjC, which allow you to write in one
platform’s language (Java, in this case) and have it automatically translated
into another platform’s language (Objective-C). A more traditional approach is
to develop in C or C++, languages that are portable to both platforms and that
can be called by both platforms.

I’m not going to try and sell you on the merits of going down this road—there
are big pros and big cons. I suspect that this approach is probably the wrong
one for most applications, but it’s still a very interesting area to explore.
C++ is the reigning king of the hill for portable, native library development,
but there’s a new challenger with an exciting amount of development behind it.

Rust describes itself as “a systems programming language that runs
blazingly fast, prevents almost all crashes and eliminates data races.” It’s
been in development for quite a while (about eight years, at the time of this
writing), and the Rust team released version 1.0 on May 15 of this year.

Rust is often compared with Go (probably because they entered the public eye around the same
time and both described themselves as systems programming languages), but the
comparison isn’t really fair to either: they have very different aims in mind.
Rust’s goal is to be a safer alternative to C++ without giving up the control
and performance that C++ provides.

Roadmap

This post is the first in a (long) series. We’re going to end up with a simple but nontrivial app that can ask Flickr for its recent photos, display
thumbnails in a UICollectionView and show the full image when a thumbnail
is tapped:

Rustorama demo

The trick is that we’re going to put all the smarts in the Rust layer. We’ll
roughly follow an MVVM (Model—View—View Model) architecture where the Model
and View Model layers are implemented in Rust, and the iOS side is just the
View layer. (This app is a variant of one that you’ll build while going through the
next edition of our iOS Programming Guide, to be
published in the second half of 2015.)

While the app is simple, we’ll touch on a lot of advanced topics getting Rust
and iOS to play nicely together. Here’s the plan for this blog series:

  1. Getting Started with Rust on iOS (that’s this post)
  2. Passing Data between Rust and iOS
  3. Sharing a View Model between Rust and iOS
  4. Writing a Flickr Client in Rust
  5. Tying it All Together: Rustorama

I’ll cover some basic Rust syntax as we go through the post, but if this is
your first experience with the language, consider reading through the Rust
book
. I’ll be glossing over some fairly advanced things in the
later posts out of necessity.

Installing Rust with multirust

This section assumes you’re running Mac OS X and have not installed Rust. If
either of those isn’t true, you’ll need to tweak these instructions.

There are three different versions of the Rust compiler available at any given
time: stable, beta and nightly. Every six weeks, the current nightly becomes
the new beta and the current beta becomes the new stable; this is called the
six-week train model.

A slick tool for managing multiple Rust installations is
multirust. We’ll use it to manage a version of the Rust compiler
for targeting iOS.

Go ahead and install multirust and set the nightly as your default Rust
compiler. (I’ve omitted copying the instructions from the multirust repository
in case they change in the future, but at the moment, there’s a
one-liner
you can run to set everything up.) You
should be able to replicate the following, although your build dates and
version hashes will be different:

$ multirust show-default
multirust: default toolchain: nightly
multirust: default location: /Users/john/.multirust/toolchains/nightly

rustc 1.1.0-nightly (c42c1e7a6 2015-05-02) (built 2015-05-01)
cargo 0.2.0-nightly (efb482d 2015-04-30) (built 2015-05-01)

$ rustc -V
rustc 1.1.0-nightly (c42c1e7a6 2015-05-02) (built 2015-05-01)

Try a “Hello, World” program:

$ cat > hello-world.rs
fn main() {
    println!("Hello, world!");
}
<Ctrl-D>
$ rustc hello-world.rs
$ ./hello-world
Hello, world!

Building a Cross Compiler

This part is not for the faint of time: this will take at least an hour, maybe
a few. We need to build a Rust toolchain that can create executables for all
five iOS platforms: armv7, armv7s, arm64 and the 32- and 64-bit simulators.
We’re going to build off of the master branch, the same as the nightly
releases.

First, clone the Rust compiler’s repository and get its
submodules (this assumes you have SSH set up with Github; feel free to clone
however you normally would):

$ git clone git@github.com:rust-lang/rust.git
$ cd rust
$ git submodule update --init --recursive

Next, create a subdirectory for all the build artifacts and cd into it:

$ mkdir build
$ cd build

Finally, configure the build to target all five architectures, and set up an
appropriate installation prefix. In the following, we’ll install to
our home directory:

$ ../configure --target=armv7-apple-ios,armv7s-apple-ios,i386-apple-ios,aarch64-apple-ios,x86_64-apple-ios --prefix=$HOME/rustc-ios
... snipping lots of output ...

At last, start the build:

$ make -j8 && make install

Go watch a movie or something; come back when your laptop fans stop spinning.

All done? Let’s tell multirust about your brand new toolchain, naming it ios:

$ multirust update ios --link-local $HOME/rustc-ios
multirust: creating link from /Users/john/rustc-ios

One final cleanup step. multirust expects to be able to find
Cargo, Rust’s package manager and build tool extraordinaire, but
we’ve only installed the Rust compiler itself. We don’t really need to go and
build Cargo, because the Rust nightly you installed in the previous section
also installed Cargo. Instead, we can create a symlink in the right place:

$ ln -s $HOME/.multirust/toolchains/nightly/bin/cargo $HOME/rustc-ios/bin

Ask multirust to run your ios version of rustc and Cargo, just to make sure
all is well:

$ multirust run ios rustc -V
rustc 1.1.0-dev (ce1150b9f 2015-05-04) (built 2015-05-03)
$ multirust run ios cargo -V
cargo 0.2.0-nightly (efb482d 2015-04-30) (built 2015-05-01)

Hello, World: Building the Rust Library

Now that all the painful waiting is done, let’s get to the fun part: writing a
Rust library and calling it from an iOS app. Create a clean working space
somewhere, and create directories to hold the Rust component and the iOS
component:

$ mkdir -p rust-ios-part-1/{ios,rust}
$ cd rust-ios-part-1

You probably noticed above that we were able to use multirust run ios ... to
run commands from the ios toolchain we installed. It would be awfully tedious
to type that every time, so multirust provides a directory-level override. Set
that up now, so that any Rust commands you issue in this directory (or any
descendent directory) will use your ios toolchain:

$ multirust override ios
multirust: using existing install for 'ios'
multirust: override toolchain for '/Users/john/rust-ios-app-part-1' set to 'ios'

We’ll build the Rust library first. Change into your rust directory and tell
Cargo to create a new library called hello for you:

$ cd rust
$ cargo new hello
$ cd hello

If you dig around under hello, you’ll find two files:

  • Cargo.toml is the manifest file describing your library. Cargo.toml is like a Rust-specific Makefile. It contains the names of your input files and any library or executable targets your project defines, as well as any dependencies your project uses.

  • src/lib.rs is the placeholder file created for you. This is where we’ll put whatever Rust code we write. (In later posts we’ll use more files, but this one is fine for “Hello, world.”)

Let’s start by updating src/lib.rs. There are Rust plugins for many popular editors; Google around for yours if you’d like. Delete the default code in src/lib.rs and replace it with this:

#[no_mangle]
pub extern fn rust_hello_world() -> i32 {
    println!("Hello, I'm in Rust code! I'm about to return 10.");
    10
}

Walking through each line:

  1. #[no_mangle] tells the Rust compiler not to perform its default name mangling on the function name. This will result in the rust_hello_world symbol being exported just like it would if it had been written in C.
  2. pub marks the function as public, i.e., callable by code outside of this library. extern fn tells the Rust compiler to use C calling conventions for this function, meaning any language that knows how to call C functions can now call this Rust function. rust_hello_world() is the name of the function; the empty parentheses indicate it takes no arguments. -> i32 states that the return value of this function is an i32, i.e., a 32-bit signed integer.
  3. println!("..."); will print the string on stdout. It’s analogous to
    Swift’s println function. (The ! means that println! in Rust is actually a macro, but that’s not important for our purposes.)
  4. 10, as the last line of the function without a semicolon, is the value
    returned by the function. Rust does have a return keyword, so we could have
    written return 10; instead, but that isn’t idiomatic. The Rust book’s
    functions chapter discusses this in more detail.

At this point, if you were developing a normal Rust library, you could build it
via cargo build and go about your merry way. We have a few more steps to
take to build a library suitable for iOS, though. By default, Cargo will create
libhello.rlib, where rlib stands for “Rust library.” We need a traditional
static library, so update Cargo.toml, adding the [lib] section below:

[package]
name = "hello"
version = "0.1.0"
authors = ["John Gallagher <jgallagher@bignerdranch.com>"]

[lib]
name = "hello"
crate-type = ["staticlib"]

Now we can tell Cargo to build a static library, and we’ll specify that we want
one for the 64-bit iOS simulator:

$ cargo build --target x86_64-apple-ios
   Compiling hello v0.1.0 (file:///Users/john/github/bignerdranch/rust-ios-app-part-1/rust/hello)
note: link against the following native artifacts when linking against this static library
note: the order and any duplication can be significant on some platforms, and so may need to be preserved
note: library: System
note: library: objc
note: framework: Foundation
note: framework: Security
note: library: pthread
note: library: c
note: library: m
$ ls target/x86_64-apple-ios/debug/
build/      deps/       examples/   libhello.a  native/

For real development, we’ll actually want to use lipo to create a fat library
for all five iOS architectures. That isn’t something currently supported by
Cargo, so there is a Makefile in the repo for this blog post that
will tell Cargo to build all five architectures and then combine them into a
fat library.

There’s one last thing we need: a C header file that we can import on the iOS
side. There isn’t a tool (yet) for creating C headers for a
Rust library, so we will create a header manually. Still in your rust
directory, create hello.h and give it the following contents:

#pragma once

#include <stdint.h>

int32_t rust_hello_world(void);

This type signature matches our Rust function above: it takes no arguments and
returns a 32-bit signed integer.

Hello, World: Building the iOS App

Hop into Xcode and create a new Single-View project. Put it into the
rust-ios-part-1/ios directory you created above. I’ll assume you want to use
Swift; if you’re using Objective-C, things are actually simpler, so you can
probably manage just fine.

Find the hello.h and libhello.a files you created in the previous section,
and drag them both into your Xcode project. (Make sure you grab the
libhello.a under target/x86_64-apple-ios/debug, or the one you created
using the Makefile, if you did that.) In order for your Swift code to be able
to see hello.h, you need to include it in your app’s bridging header. By
default, Swift projects don’t have one. You can either create one
manually
or add a new class to your project, select
Objective-C as the language, click “Yes” when Xcode asks if you want a bridging
header, then delete the Objective-C files.

Once you have a bridging header, add hello.h to it:

//
//  Use this file to import your target's public headers that you would like
//  to expose to Swift.
//
#import "hello.h"

Open up AppDelegate.swift, and try calling your Rust function:

func application(application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?)
    -> Bool
{
    let result = rust_hello_world()
    println("I called Rust and got (result)")
    return true
}

Try to build and run your app. You’ll need to have a 64-bit simulator selected,
such as the iPhone 6 simulator. If you get a linker error about missing the
symbol _rust_hello_world, make sure you added the correct libhello.a to
your app target.

You should see the following in the Xcode console:

Hello, I'm in Rust code! I'm about to return 10.
I called Rust and got 10

Congratulations! You’ve written a Rust library and used it on iOS!

Next Steps

In the next post, we’ll build on all the setup you’ve done. We’ll talk about
how to pass non-primitive data types like strings to and from Rust, as well as
how to pass more complicated data structures and objects. Stay tuned!

All the code, both Rust and Swift, from this post is on GitHub.

Editor’s note: Be sure to check out the other posts in this series: Part 2, Part 3.

Zack Simon

Reviewer Big Nerd Ranch

Zack is an Experience Director on the Big Nerd Ranch design team and has worked on products for companies ranging from startups to Fortune 100s. Zack is passionate about customer experience strategy, helping designers grow in their career, and sharpening consulting and delivery practices.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News