Bridging Rust to Swift, pt. 4: Swift Packages

In installments prior, we looked at

While this works, using the library in this fashion feels a bit ‘unswifty’ (for lack of a better word). Consider this C header file.

// person.h
#ifndef RMPERSON_H
#define RMPERSON_H

typedef struct Person Person;

Person* create_person(unsigned int age);
void free_person(Person* p);
unsigned int get_person_age(Person *p);

#endif

The header file declares the interface to a struct of type Person, while its implementation details are hidden away. A few items of note jump out here.

  1. The C functions to interact with the struct are freestanding, whereas in Swift, we’d expect them to be encapsulated in the struct definition.
  2. We have to manually free the memory allocated to the struct on the heap - an activity that is notoriously error-prone.
  3. We are dealing with C pointers. In this admittedly contrived example, the header file does not fully define the struct Person. A pointer to such a C struct is mapped to an OpaquePointer in Swift. Unfortunately, using an OpaquePointer is a bit more involved. For example, to use free_person on an OpaquePointer named frank we’d have to do something like

    withUnsafePointer(to: frank) { unsafe_frank in
      let age = get_person_age(unsafe_frank.pointee)
    }
    

That last bit around working with OpaquePointer is especially cumbersome. One way to ease our interactions with the C library is to write Swift code that wraps around the C library. We are going to do this via a Swift package, which have the added advantage of making distribution easier.

Let’s start by creating a package SwiftPerson in Xcode. This should generate a Package.swift file that allows us to define the various attributes of the package. We are going to add our .xcframework as a binary target of the package. However, we are not going to ship the .xcframework along with our package. Instead, we are going to zip it up and host that someplace public. When our users add this package as a dependency to their projects, Xcode will automatically download the .xcframework as needed. For security reasons, we need to specify the checksum for the .zip as well. This checksum is computed with the following command on the command line (from the root of our Swift package folder).

swift package compute-checksum /path/to/Person.xcframework.zip

Once, we have the .xcframework.zip hosted in an appropriate location, edit the projects Package.swift file to look like the following

// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "SwiftPerson",
    platforms: [.macOS(.v11), .iOS(.v13)],
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "SwiftPerson",
            targets: ["SwiftPerson"]),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "SwiftPerson",
            dependencies: ["Person"]),
        .binaryTarget(name: "Person", url: "https://www.branchout.dev/assets/posts/rust_to_swift_pt4/Person.xcframework.zip", checksum: "ff0467fb34cc823dd05834a1c0511794320432195d6ed1151624d6453299feeb"),
        .testTarget(
            name: "SwiftPersonTests",
            dependencies: ["SwiftPerson"]),
    ]
)

The key edits here are to

  1. declare support for both macOS and iOS platforms - this can be modified as needed.
  2. create a binary target for the package. This binary target is declared with the URL hosting the .xcframework.zip along with its checksum.
  3. make the binary target a dependency of our package SwiftPerson.

At this point, users of our package can directly import person to use the C API. But, we can do one better.

P.S. Cross-compiling a C library for macOS and iPhoneOS involve a few subtleties. I have some notes on that here.