on
Bridging Rust to Swift, pt. 1: Hello, C
C Anyone?
Swift has great interoperability with C. Given a C header and linked library, it can call into C code quite easily. If we were to build a C wrapper to our Rust code, we should be good to go - with using the C library’s functionality. As it turns out, doing so is relatively painless. Let’s start by creating a sample rust library project.
cargo new --lib swift_bridgedThis should create a new rust project with a Cargo.toml and src under folder
swift_bridged. Cargo.toml is currently quite basic with the following
contents.
[package]
name = "swift_bridged"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]Targets
By default, building our project with Cargo will produce libraries that are
compatible with rustc. This can be examined by running cargo build --release
in the root of our project. We will find libswift_bridged.d and
libswift_bridged.rlib inside release/target. Since, we are looking to access
the library from C, we will need to tell rustc to build a C compatible target.
Amongst the different rust targets
available, we need to tell
Cargo to build a cdylib as well.
Let’s add a new lib section to our Cargo.toml with the following content.
This gets Cargo to build a rustc compatible staticlib along with a C
compatible dynamically shared cdylib.
[lib]
crate_type = ["lib", "staticlib", "cdylib"]Re-running cargo build --release, shows two additional targets
libswift_bridged.a and libswift_bridged.dylib. While we can directly access
these libraries from C, they are currently empty. Let’s add some functionality
to the library in rust. For illustrative purposes, it’s going to be a simple
function that
- Accepts two integers as input
- Returns an integer that is the sum of the two integer inputs as output.
Modify src/lib.rs to be the following
pub fn add_integers(a: u32, b: u32) -> u32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add_integers(3, 4);
assert_eq!(result, 7);
}
}Simple enough. Let’s examine the content of our generated libraries. On macOS,
we can use the nm tool in conjunction with rustfilt. Generally, the function
names are mangled in the generated rust libraries. rustfilt unmangles them for
easier readability, and can be easily installed via cargo install rustfilt.
$ cargo install rustfilt
$ nm -gU target/release/libswift_bridged.rlib | rustfilt | grep -i integers
target/release/libswift_bridged.rlib:lib.rmeta: no symbols
0000000000000000 T _swift_bridged::add_integers
$ nm -gU target/release/libswift_bridged.dylib | rustfilt | grep -i integers
target/release/libswift_bridged.dylib: no symbols
Interestingly, while our native rust rlib contains the add_integers
function, it is missing in our cdylib. For a function to be made available in
the cdylib, it needs to be marked for export in our rust code. rustc strips
out all functions that are not exported or used by an exported function while
generating the cdylib.
#[allow(non_snake_case)]
#[no_mangle]
pub unsafe extern "C" fn bodevAddIntegers(a: u32, b: u32) -> u32 {
add_integers(a, b)
}There is a fair amount going on with the above snippet. Instead of directly
exporting fn add_integers() we are going to write a C compatible fn
bodevAddIntegers() that will wrap it. Additionally, we are instructing rustc
to
- Allow non snake case function names and suppress warnings about it.
- Not mangle the function name in the exported library. If the function name were to be mangled, we wouldn’t know how to call it from our downstream programs.
- Since, the function is going to be accessed from outside
rust, we have no real control over what it does or how it is called. The calling program could theoretically pass in invalid values. We mark the function asunsafeto denote this - the rust compiler is unable to check the usage of this function.
Let’s build and examine our libraries again.
# nm -gU target/release/libswift_bridged.dylib | rustfilt | grep -i integers
0000000000003fb0 T _bodevAddIntegers
We now find our C wrapper function in the dynamic library. Coupled with a header file, we could now call into this library from C/Swift. And now, let’s move onto automating the generation of this header file.