Big Nerd Ranch Logo • • •
  • work
  • team training
  • bootcamps
  • books
  • screencasts
  • resources
  • blog
  • work
  • team training
  • bootcamps
  • books
  • screencasts
  • resources
  • blog

Building an iOS App in Rust, Part 3: Passing Owned Objects between Rust and iOS

John Gallagher
John Gallagher
Dec 3, 2015 • iOS
  • Bird
  • Facebook
  • LinkedIn__x28_alt_x29_
  • Shape
  • Combined Shape

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

Welcome to Part 3 of our “Building an iOS App in Rust” series. This post continues to build on the tools and details we set up in Part 1 and Part 2, so please refer back to those as needed. Part 1 walks you through setting up a cross-compiler built from the latest “unstable” Rust compiler. The code in in this post uses a feature that was only recently stabilized, so if you are following along with your own compiler toolchain, note that you’ll need Rust 1.4 or newer.

Another major change is that we’re now using Swift 2, which added the capability to pass Swift functions to C APIs that wanted function pointers. We no longer need to drop down into Objective-C to wrap our Rust code.

In Part 2, we ended with passing strings between Rust and Swift. Our implementation worked but was not ideal in either direction:

  • When passing a Swift String into Rust via the c_string_to_rust function, the pointer we give to Rust is only valid for the duration of the function call. If we want to keep the string to do something with it later, we’d need to make a copy of the string data.
  • When passing a Rust string to Swift as we did in the get_string_from_rust function, the pointer we returned was valid only because it pointed to static constant data. We don’t currently have a way to pass a dynamically allocated string from Rust to Swift.

Both of these issues would be complete showstoppers for a real application. We need a way to pass much more complex data than strings, and we’ll need to be able to correctly and safely manage the ownership of that data.

As in Part 2, we will not be going over every line of code in detail, but all of the code for this post is available on GitHub.

Ownership in Rust

Rust is a great language for a number of reasons, but the one feature where it really shines is how it deals with ownership (who is responsible for allocating and deallocating resources held by an instance) and borrowing (loaning out temporary access to an instance without giving away ownership. Much has been written about this, with more detail than I can offer here. If you want to learn more, check out the excellent Fearless Concurrency with Rust blog post as well as the Rust book’s sections on Ownership, References and Borrowing and Lifetimes.

In pure Rust, the compiler makes sure you obey ownership rules that ensure all resources will be cleaned up at the proper time. The same ownership rules provide thread safety guarantees that we’ll discuss in a later post. When we’re working with the foreign function interface (FFI) layer, we have to be much more careful, as we’re interacting with a system outside of the Rust compiler’s knowledge, so we don’t get nearly as much help.

The most basic things we want to be able to understand are (a) how long will an object we’re passing between languages be valid (e.g., in c_string_to_rust, the pointer is valid only for the function call), and (b) who is responsible for cleaning it up (e.g., in c_string_to_rust, Swift will free the string’s memory). For the rest of this post, we’re going to look at how to pass ownership of an object across the boundary: creating an object in Rust and giving responsibility for its cleanup to Swift and vice versa.

Giving Ownership of a Rust Instance to Swift

Most of the Rust types we’re going to create will not boil down to a primitive or even a simple structure like RustByteSlice. For example, we could build a Rust structure like this, which represents a label and some associated data:

// Automatically derive an implementation of the `Debug` trait so we can
// print instances of NamedData for debugging purposes.
#[derive(Debug)]
pub struct NamedData {
    name: String,
    data: Vec<i32>,
}

When we wrote the C header file for RustByteSlice, we put the C-compatible definition of RustByteSlice directly in the header. We can’t do that here, though: we don’t know what the in-memory layout of NamedData is, because we don’t know what the internal data layouts of String or Vec<i32> are (nor should we need to). If we want to create instances of this type and give them to Swift, we can create a C interface for creation and destruction like this:

// Forward declare a struct but never specify its fields.
// We will only work with pointers to named_data.
struct named_data;

// Create a new instance of `named_data`.
// The caller is responsible for passing the returned pointer to
// named_data_destroy, or memory will be leaked.
struct named_data *named_data_new(void);

// Free a `named_data` instance returned by `named_data_new`.
void named_data_destroy(struct named_data *data);

The Swift side can’t use a struct named_data * directly; its bridged type, COpaquePointer, makes that obvious. We’ll need to add functions to access the properties we want to expose:

// Get the name of a `named_data`. The returned byte slice is valid until
// the `named_data` instance is destroyed.
struct RustByteSlice named_data_get_name(const struct named_data *data);

// Get the number of elements stored in `data`.
size_t named_data_count(const struct named_data *data);

On the Rust side, let’s start by implementing the Drop trait for NamedInit. The drop function is called when a value goes out of scope; it’s analogous to a destructor in C++ or deinit in Swift (although any value in Rust can implement Drop, whereas there’s no way to have a deinit for Swift structs or enums). We don’t actually need to do anything, but we can add a print statement for our own understanding:

impl Drop for NamedData {
    fn drop(&mut self) {
        println!("{:?} is being deallocated", self);
    }
}

To implement named_data_new, we need to create an instance of NamedData on the heap (if we created it on the stack, it would be destroyed as soon as named_data_new returns), then return a raw pointer to the instance. Rust’s standard library provides the Box type for heap-allocating instances. Typically Rust’s RAII semantics would cause a Box-allocated instance to be deallocated when the box goes out of scope, but we can use Box::into_raw to cause the instance to be forgotten by the RAII system.

#[no_mangle]
pub extern fn named_data_new() -> *mut NamedData {
    // Create an instance of NamedData.
    let named_data = NamedData{
        name: "some data".to_string(),
        data: vec![1, 2, 3, 4, 5],
    };

    // Put named_data into a Box, which moves it onto the heap.
    let boxed_data = Box::new(named_data);

    // Convert our Box<NamedData> into a *mut NamedData. Rust is no longer
    // managing the destruction of boxed_data; we must (at some point in the
    // future) convert this pointer back into a Box<NamedData> so it can be
    // deallocated.
    Box::into_raw(boxed_data)
}

This will leak memory if we don’t later give the instance back to a Rust Box for deallocation, but that’s exactly what we want! We created a heap-allocated NamedData, got a pointer to it that we can give to Swift, and Rust will not deallocate it out from under us. We can implement named_data_destroy that will take a raw pointer to a NamedData, put it back into a Box, and let that Box fall out of scope, causing the instance to be deallocated.

#[no_mangle]
pub unsafe extern fn named_data_destroy(data: *mut NamedData) {
    // Convert a *mut NamedData back into a Box<NamedData>.
    // This function is unsafe because the Rust compiler can't know
    // whether data is actually pointing to a boxed NamedData.
    //
    // Note that we don't actually have to do anything else or even
    // give the new Box a name - when we convert it back to a Box
    // and then don't use it, the Rust compiler will insert the
    // necessary code to drop it (deallocating the memory).
    let _ = Box::from_raw(data);
}

Implementing the two accessor functions is simpler; the only new bit here is converting a raw *const NamedData into a Rust reference (&NamedData), which requires an unsafe block (because we have to dereference the pointer, and the Rust compiler can’t know whether the pointer is actually valid):

#[no_mangle]
pub extern fn named_data_get_name(named_data: *const NamedData) -> RustByteSlice {
    let named_data = unsafe { &*named_data };
    RustByteSlice::from(named_data.name.as_ref())
}

#[no_mangle]
pub extern fn named_data_count(named_data: *const NamedData) -> size_t {
    let named_data = unsafe { &*named_data };
    named_data.data.len() as size_t
}

There is an interesting point to make here about the use of unsafe. The Rust compiler is extremely strict about safety, but it is sometimes necessary to use unsafe to implement particular details. We should be careful what kind of API we present, though. These functions are primarily intended to be used from outside of Rust, but they could still be called by other Rust code. These functions appear to the Rust compiler to be safe - it should be memory safe to call these with any possible *const NamedData. But our implementation does not check for NULL, in particular. This is very bad - we have an unsafe function masquerading as a safe one. To fix this, we can move unsafe up from just a block around the pointer dereference to a marker on the entire function:

#[no_mangle]
pub unsafe extern fn named_data_get_name(named_data: *const NamedData) -> RustByteSlice {
    let named_data = &*named_data;
    RustByteSlice::from(named_data.name.as_ref())
}

#[no_mangle]
pub unsafe extern fn named_data_count(named_data: *const NamedData) -> size_t {
    let named_data = &*named_data;
    named_data.data.len() as size_t
}

Now our functions are correctly marked for other Rust consumers - if they want to call these functions, they can only do so from other unsafe code. We also make use of a fairly common Rust idiom: let named_data = &*named_data; creates a new binding for the named_data name which shadows the old named_data. The new named_data has type &NamedData; the shadowed one that we no longer can (or need to) access had type *const NamedData.

On the Swift side, we want to guarantee we’re not going to forget to pair every call to named_data_new with a call to named_data_destroy, so we’ll create a RustNamedData wrapper class with an appropriate deinit. This is also a convenient place to put calls to the other accessor functions, as well:

class RustNamedData {
    private let raw: COpaquePointer

    init() {
        raw = named_data_new()
    }

    deinit {
        named_data_destroy(raw)
    }

    var name: String {
        let byteSlice = named_data_get_name(raw)
        return byteSlice.asString()!
    }

    var count: Int {
        return named_data_count(raw)
    }
}

We can create an instance of this class, print some properties, and see the destruction happen as we expect:

let namedData = RustNamedData()
print("namedData.name = \(namedData.name)")
print("namedData.count = \(namedData.count)")

// Output from running the above snippet:
// namedData.name = some data
// namedData.count = 5
// NamedData { name: "some data", data: [1, 2, 3, 4, 5] } is being deallocated

Embedding Rust into another language is relatively straightforward. There are no concerns about any runtime support like a garbage collector, so we just have to give the host language (Swift) a way to create and destroy instances. Going the other direction is a little trickier thanks to ARC.

Giving Ownership of a Swift Instance to Rust

Passing ownership of Swift objects down to Rust is problematic. We saw earlier how to pass an ephemeral pointer to a String; we could easily do that with other Swift types by making use of the withUnsafePointer Swift function. However, pointers created by withUnsafePointer are only valid for the duration of that function call, and we’re going to need a way to give Rust a more permanent handle on Swift objects. In Swift 1, we would need to drop down to Objective-C to solve this problem. Since Swift 2 added the ability to pass Swift functions to APIs expecting C function pointers, we no longer need to do that. We will still get our hands a little dirty, though.

Let’s start with the C interface each side is going to implement:

struct swift_object {
    void *user;
    void (*destroy)(void *user);
    void (*callback_with_int_arg)(void *user, int32_t arg);
};

void give_object_to_rust(struct swift_object object);

The swift_object struct has three fields:

  1. user is a void *; it will be a pointer to an instance of our Swift object.
  2. destroy is a C function pointer that will be called when Rust wants to destroy user.
  3. callback_with_int_arg is a C function pointer that Rust can call with a 32-bit signed integer argument.

Let’s create the Swift side of our Swift object:

class SwiftObject {
    deinit {
        print("SwiftObject being deallocated")
    }

    private func callbackWithArg(arg: Int) {
        print("SwiftObject: received callback with arg \(arg)")
    }

    func sendToRust() {
        let ownedPointer = UnsafeMutablePointer<Void>(Unmanaged.passRetained(self).toOpaque())
        let wrapper = swift_object(
            user: ownedPointer,
            destroy: destroy,
            callback_with_int_arg: callback_with_int_arg)
        give_object_to_rust(wrapper)
    }
}

private func callback_with_int_arg(user: UnsafeMutablePointer<Void>, arg: Int32) {
    let obj: SwiftObject = Unmanaged.fromOpaque(COpaquePointer(user)).takeUnretainedValue()
    obj.callbackWithArg(Int(arg))
}

private func destroy(user: UnsafeMutablePointer<Void>) {
    let _ = Unmanaged<SwiftObject>.fromOpaque(COpaquePointer(user)).takeRetainedValue()
}

deinit and callbackWithArg are straightforward: we just want to see output when they’re called. Most of the magic happens in sendToRust, so let’s break that down:

let ownedPointer = UnsafeMutablePointer<Void>(Unmanaged.passRetained(self).toOpaque())

We have an instance of a Swift object (self), and the bridged form of our swift_object struct is expecting an UnsafeMutablePointer<Void> for its user field. We have to pass through two intermediate states to get there:

  • Unmanaged is Swift’s window into you taking more control over memory management. When you call Unmanaged.passRetained(self), you get back “an unmanaged reference with an unbalanced retain”. This is exactly what we want: the reference has now been retained, and we are responsible for releasing the object when we (or Rust) is finished with it.
  • Given an Unmanaged<SwiftObject>, we can call toOpaque() on it to get a COpaquePointer.
  • Given a COpaquePointer, we can finally create an UnsafeMutablePointer, the type we need to supply for user.

Now that we’ve converted self into an UnsafeMutablePointer<Void>, we can build up an instance of the swift_object struct and call our give_object_to_rust function:

let wrapper = swift_object(
    user: ownedPointer,
    destroy: destroy,
    callback_with_int_arg: callback_with_int_arg)
give_object_to_rust(wrapper)

The destroy and callback_with_int_arg arguments are private functions; let’s look at those now.

callback_with_int_arg is given a user (the exact same UnsafeMutablePointer<Void> we just created and an integer argument. The tricky bit here is converting user back into a usable SwiftObject; we have to repeat the process we did above but in reverse:

// UnsafeMutablePointer<Void> -> COpaquePointer
COpaquePointer(user)

// COpaquePointer -> Unmanaged<SwiftObject>
Unmanaged.fromOpaque(COpaquePointer(user))

// Unmanaged<SwiftObject> -> SwiftObject
Unmanaged.fromOpaque(COpaquePointer(user)).takeUnretainedValue()

Note that we call takeUnretainedValue(), not takeRetainedValue(), because we do not want to modify the reference count of the underlying SwiftObject. Now that we have a SwiftObject, we can call methods on it just like normal:

obj.callbackWithArg(Int(arg))

Finally, destroy is a one-liner that is almost identical to the first line of callback_with_int_arg. The difference, as you probably expect, is that here we do call takeRetainedValue(). This will decrement the reference count on the underlying object, causing it to be deallocated (assuming Rust was holding the only or last reference to it).

Now that the Swift side is ready, what does the Rust side look like? First, let’s define the SwiftObject struct:

use libc::c_void;

#[repr(C)]
pub struct SwiftObject {
    user: *mut c_void,
    destroy: extern fn(user: *mut c_void),
    callback_with_int_arg: extern fn(user: *mut c_void, arg: i32),
}

This shouldn’t be too bad, if you’ve made it this far. C function pointers come in as extern fn types, and we need to make sure the argument and return types match. (If we wanted to allow these function pointers to be NULL, we would use Option<extern fn(…)> instead, but we don’t need to do that for this example.)

Now for give_object_to_rust. To make things interesting, we’ll start up a thread, move the SwiftObject onto that thread, sleep for 1 second, and then issue the callback into iOS. To tell Rust that it is safe for instances of SwiftObject to be sent across threads, we’ll also need to add an implementation of the (empty) Send trait. (The explanation for this is a little long, and this post is already too long by half, so I’ll refer you to the Rust book’s Concurrency chapter if you’re curious.)

use std::thread;

unsafe impl Send for SwiftObject {}

#[no_mangle]
pub extern fn give_object_to_rust(obj: SwiftObject) {
    println!("moving SwiftObject onto a new thread created by Rust");
    thread::spawn(move||{
        thread::sleep_ms(1000);
        (obj.callback_with_int_arg)(obj.user, 10);
        (obj.destroy)(obj.user);
    });
}

If we run this, we’ll find everything working; we get the following log, with a 1 second delay between the 2nd and 3rd lines:

moving SwiftObject onto a new thread created by Rust
SwiftObject: received callback with arg 10
SwiftObject being deallocated

You can also set a breakpoint in SwiftObject.callbackWithArg and see that the callback is happening off of the main thread.

However, there is the unsightly bit of having to manually call obj.destroy. This is Rust - we should not need to do manual resource management! Earlier, we implemented the Drop trait on NamedData. Let’s try to do the same thing here. We can move the call to destroy into drop, which means Rust will put the call at exactly the right place (whenever our SwiftObject falls out of scope):

impl Drop for SwiftObject {
    fn drop(&mut self) {
        (self.destroy)(self.user);
    }
}

#[no_mangle]
pub extern fn give_object_to_rust(obj: SwiftObject) {
    println!("moving SwiftObject onto a new thread created by Rust");
    thread::spawn(move||{
        thread::sleep_ms(1000);
        (obj.callback_with_int_arg)(obj.user, 10);
        /* (obj.destroy)(obj.user); */
    });
}

This compiles, but with a pretty scary warning:

src/swift_ownership_to_rust.rs:13:1: 17:2 warning: implementing Drop adds hidden state to types, possibly conflicting with `#[repr(C)]`, #[warn(drop_with_repr_extern)] on by default
src/swift_ownership_to_rust.rs:13 impl Drop for SwiftObject {
src/swift_ownership_to_rust.rs:14     fn drop(&mut self) {
src/swift_ownership_to_rust.rs:15         (self.destroy)(self.user);
src/swift_ownership_to_rust.rs:16     }
src/swift_ownership_to_rust.rs:17 }

It turns out the warning is scary-sounding for a reason: running this code now crashes!

Explaining what’s going on is a little involved, but I’ll try to summarize. Currently, when you implement Drop on a struct, the Rust compiler inserts a hidden field into that struct that it uses to track whether or not it needs to call drop. This is just a limitation of the current implementation; there are an accepted RFC and an open issue that will address this. For now, however, this hidden field changes the size of SwiftObject. The Rust compiler is warning us that we said we wanted an in-memory representation compatible with C, but that isn’t happening because of this hidden field.

Luckily, fixing this is easy. We can wrap SwiftObject up in a new Rust type and implement Drop on that type instead. We’ll use Rust’s newtype syntax for a struct-that-only-exists-to-wrap-another-type:

use std::ops::Deref;

struct SwiftObjectWrapper(SwiftObject);

impl Deref for SwiftObjectWrapper {
    type Target = SwiftObject;

    fn deref(&self) -> &SwiftObject {
        &self.0
    }
}

impl Drop for SwiftObjectWrapper {
    fn drop(&mut self) {
        (self.destroy)(self.user);
    }
}

There’s another new thing here: we implemented the Deref trait. This lets us freely access the fields (and methods, if there were any) on a SwiftObjectWrapper’s inner SwiftObject. Finally, we need to update our give_object_to_rust implementation to wrap the struct Swift gives us into a SwiftObjectWrapper, and move that wrapper onto the background thread:

#[no_mangle]
pub extern fn give_object_to_rust(obj: SwiftObject) {
    println!("moving SwiftObject onto a new thread created by Rust");
    let obj = SwiftObjectWrapper(obj);
    thread::spawn(move||{
        thread::sleep_ms(1000);
        (obj.callback_with_int_arg)(obj.user, 10);
    });
}

This compiles (and runs) without warning or error, and we no long have to worry about calling destroy: Rust will insert the call for us at precisely the right moment.

Next Steps

We’ve covered a lot in this post! We now know how to pass all kinds of things from Rust to Swift and from Swift to Rust. In the next couple of posts, we’ll start to take steps towards a more realistic example and explore two different ways to implement view models in Rust that can be used from Swift.

All the code, plus the extra stuff necessary to get it running (like an Xcode project file), is available on GitHub.

  • Bird
  • Facebook
  • LinkedIn__x28_alt_x29_
  • Shape
  • Combined Shape
John Gallagher
more by
John Gallagher

Big Nerd Ranch iOS Courses

Interested in learning more about our basic and advanced iOS Courses?
Learn from the experts at a Big Nerd Ranch Bootcamp!

LEARN MORE

Big Nerd Ranch Screencasts

Interested in leveling up your coding skills from the same authors of the Big Nerd Ranch Guide? Subscribe to The Frontier today!

LEARN MORE

Related Posts:

Pro Pattern-Matching in Swift

Nick Teissler

Life in Technicolor

Dan Berry

Core Graphics, Part 3: Lines

Mark Dalrymple

Core Graphics, Part 2: Contextually Speaking

Mark Dalrymple

Producing a CircleCI Test Summary with Fastlane

Jeremy Sherman

Recent Comments

comments powered by Disqus
  • Twitter
  • Facebook
  • Instagram
  • Github
  • LinkedI
  • Youtube
Subscribe to Our Newsletter

App Development

  • Work

Books

  • All books
  • Front-end
  • Swift
  • iOS
  • Android

Team Training

  • Team Training Overview
  • iOS
  • Android
  • Web
  • Design

Bootcamps

  • Bootcamp Overview
  • iOS
  • Android
  • Web
  • Schedule
  • Georgia Ranch
  • California Ranch
  • Sign In

Company

  • Who We Are
  • News
  • Resources
  • Careers
  • Blog
  • Contact

Copyright© 1998 - 2018 Big Nerd Ranch, LLC. All Rights Reserved. | Privacy Policy