on
Bridging Rust to Swift, pt. 4: Swift Packages
In installments prior, we looked at
- using a C FFI to publish Rust code externally
- using cbindgen to automatically generate C style header files
- and, targeting multiple platforms using Binary frameworks.
While this works, using the library in this fashion feels a bit ‘unswifty’ (for lack of a better word). Consider this C header file.
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.
- The C functions to interact with the struct are freestanding, whereas in
Swift, we’d expect them to be encapsulated in the
struct
definition. - We have to manually free the memory allocated to the struct on the heap - an activity that is notoriously error-prone.
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 anOpaquePointer
in Swift. Unfortunately, using anOpaquePointer
is a bit more involved. For example, to usefree_person
on anOpaquePointer
namedfrank
we’d have to do something likewithUnsafePointer(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).
Once, we have the .xcframework.zip
hosted in an appropriate location,
edit the projects Package.swift
file to look like the following
The key edits here are to
- declare support for both macOS and iOS platforms - this can be modified as needed.
- create a binary target for the package. This binary target is declared with
the URL hosting the
.xcframework.zip
along with its checksum. - 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.