Bridging Rust to Swift, pt. 5: Swift Packages, the final wrap

In our last post, we built a Swift package that made distributing and using our .xcframework a lot easier. We will now tackle making our interaction with the C library more in line with Swift principles.

The major sticking points we found while using our C API in the last post were

  1. Structs and their associated functions were decoupled.
  2. Working with OpaquePointers was a bit cumbersome.
  3. We had to manually manage memory.

One class to rule them all

All the aforementioned issues can be tackled by writing a single Swift class SwiftPerson. Our Swift package already has a file SwiftPerson.swift with the necessary boilerplate code. By default, the generated SwiftPerson declaration will be of type struct - we need to change that to class. We make this change as we can’t have a deinitializer in a struct.

We will have the class manage a single OpaquePointer to a C struct Person. When the class is deinited in Swift, it will automatically free the memory allocated in C. Finally, it will encapsulate all the C functions that interact with the struct.

public class SwiftPerson {
  var person_op: OpaquePointer

  public init(age: UInt32) {
    person_op = create_person(age)
  }

  deinit {
    withUnsafePointer(to: person) { unsafe_person in
      free_person(unsafe_person.pointee)
    }
  }
}

Let’s break this code down

  1. We first declare a variable to reference our OpaquePointer.
  2. In our init, we create a C struct Person and assign the returned OpaquePointer to person_op.
  3. Finally, in the deinit, we make sure that we always free the memory that was allocated in C during init.

To complete our API encapsulation, we need a way to access the age on Person. We can do this by either an associated function in Swift or a computed property. Computed properties feel a bit more natural in this instance.

var age: UInt32 {
  withUnsafePointer(to: person) { unsafe_person in
    return get_person_age(unsafe_person.pointee)
  }
}

The computed property hides the mechanics behind the working of the OpaquePointer. We now have a perfectly usable Swift class SwiftPerson that encapsulates our C struct Person. Our final class definition reads as

public class SwiftPerson {
  var person: OpaquePointer
    
  public init(age: UInt32) {
    person = create_person(age)
  }
    
  deinit {
    withUnsafePointer(to: person) { unsafe_person in
      free_person(unsafe_person.pointee)
    }
  }
    
  var age: UInt32 {
    withUnsafePointer(to: person) { unsafe_person in
      return get_person_age(unsafe_person.pointee)
    }
  }
}

The C API (and it’s existence) is completely hidden from the users of our package SwiftPerson, who can now do

let swifty_frank = SwiftPerson(age: 40)
let age = swifty_frank.age

Much simpler (and safer). This concludes our simplistic introduction to bridging rust/C to swift. However, once we get down to it, there are other topics that would require us to put our thinking caps back on - essentially around handling more complicated data structures and strings. Perhaps, another post for another day…