Why a Bridge Module?
The swift-bridge
project provides direct support for expressing the Rust+Swift FFI boundary using one or more bridge modules such as:
#![allow(unused)] fn main() { #[swift_bridge::bridge] mod ffi { extern "Rust" { fn generate_random_number() -> u32; } } fn generate_random_number() -> u32 { rand::random() } }
swift-bridge
's original maintainer wrote swift-bridge
for use in a cross platform application where he preferred to keep his FFI code separate from his application code.
He believed that this separation would reduce the likelihood of him biasing his core application's design towards types that were easier to bridge to Swift.
While in the future swift-bridge
may decide to directly support other approaches to defining FFI boundaries, at present only the bridge module approach is directly supported.
Users with other needs can write wrappers around swift-bridge
to expose alternative frontends.
The examples/without-a-bridge-macro
example demonstrates how to reuse swift-bridge
's code generation facilities without using a bridge module.
Inline Annotations
The main alternative to the bridge module design would be to support inline annotations where one could describe their FFI boundary by annotating their Rust types.
For instance a user might wish to expose their Rust banking code to Swift using an approach such as:
#![allow(unused)] fn main() { // IMAGINARY CODE. WE DO NOT PROVIDE A WAY TO DO THIS. #[derive(Swift)] pub struct BankAccount { balance: u32 } #[swift_bridge::bridge] pub fn create_bank_account() -> BankAccount { BankAccount { balance: 0 } } }
swift-bridge
aims to be a low-level library that generates far more efficient FFI code than a human would write and maintain themselves.
The more information that swift-bridge
has at compile time, the more efficient code it can generate.
Let's explore an example of bridging a UserId
type, along with a function that returns the latest UserId
in the system.
#![allow(unused)] fn main() { type Uuid = [u8; 16]; #[derive(Copy)] struct UserId(Uuid); pub fn get_latest_user() -> Result<UserId, ()> { Ok(UserId([123; 16])) } }
In our example, the UserId
is a wrapper around a 16 byte UUID.
Exposing this as a bridge module might look like:
#![allow(unused)] fn main() { #[swift_bridge::bridge] mod ffi { extern "Rust" { #[swift_bridge(Copy(16))] type UserId; fn get_latest_user() -> UserId; } } }
Exposing the UserId
using inlined annotation might look something like:
#![allow(unused)] fn main() { // WE DO NOT SUPPORT THIS type Uuid = [u8; 16]; #[derive(Copy, ExposeToSwift)] struct UserId(Uuid); #[swift_bridge::bridge] pub fn get_latest_user() -> Result<UserId, ()> { UserId([123; 16]) } }
In the bridge module example, swift-bridge
knows at compile time that the UserId
implements Copy
and has a size of 16
bytes.
In the inlined annotation example, however, swift-bridge
does not know the UserId
implements Copy
.
While it would be possible to inline this information, it would mean that users would need to remember to inline this information
on every function that used the UserId
.
#![allow(unused)] fn main() { // WE DO NOT SUPPORT THIS #[swift_bridge::bridge] #[swift_bridge(UserId impl Copy(16))] pub fn get_latest_user() -> Result<UserId, ()> { UserId([123; 16]) } }
We expect that users would find it difficult to remember to repeat such annotations, meaning users would tend to expose less efficient bridges than they otherwise could have.
If swift-bridge
does not know that the UserId
implements Copy
, it will need to generate code like:
#![allow(unused)] fn main() { pub extern "C" fn __swift_bridge__get_latest_user() -> *mut UserId { let user = get_latest_user(); match user { Ok(user) => Box::new(Box::into_raw(user)), Err(()) => std::ptr::null_mut() as *mut UserId, } } }
Whereas if swift-bridge
knows that the UserId
implements Copy
, it might be able to avoid an allocation by generating code such as:
#![allow(unused)] fn main() { /// `swift-bridge` could conceivably generate code like this to bridge /// a `Result<UserId, ()>`. /// Here we use a 17 byte array where the first byte indicates `Ok` or `Err` /// and, then `Ok`, the last 16 bytes hold the `UserId`. /// We expect this to be more performant than the boxing in the previous /// example codegen. pub extern "C" fn __swift_bridge__get_latest_user() -> [u8; 17] { let mut bytes: [u8; 17] = [0; 17]; let user = get_latest_user(); match user { Ok(user) => { let user_bytes: [u8; 16] = unsafe { std::mem::transmute(user) }; (&mut bytes[1..]).copy_from_slice(&user_bytes); bytes[0] = 255; bytes } Err(()) => { bytes } } } }
More generally, the more information that swift-bridge
has about the FFI interface, the more optimized code it can generate.
The bridge module design steers users towards providing more information to swift-bridge
, which we expect to lead to more efficient
applications.
Users that do not need such efficiency can explore reusing swift-bridge
in alternative projects that better meet their needs.