Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
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.
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:
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:
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.
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!
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)
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:
#[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.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.println!("...");
will print the string on stdout. It’s analogous toprintln
function. (The !
means that println!
in Rust is actually a macro, but that’s not important for our purposes.)10
, as the last line of the function without a semicolon, is the valuereturn
keyword, so we could havereturn 10;
instead, but that isn’t idiomatic. The Rust book’sAt 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.
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!
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.
Our introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
The Combine framework in Swift is a powerful declarative API for the asynchronous processing of values over time. It takes full advantage of Swift...
SwiftUI has changed a great many things about how developers create applications for iOS, and not just in the way we lay out our...