From 201f17d432af777d7dcc0e985116c721b3c8dcf9 Mon Sep 17 00:00:00 2001 From: Chinedu Francis Nwafili Date: Sat, 16 Nov 2024 14:38:10 -0500 Subject: [PATCH] Document motivation for bridge module This commit introduces documentation explaining why `swift-bridge` uses a bridge module design, as opposed to, say, a design where users annotate their types with proc macro attributes. --- .github/workflows/test.yml | 101 +++++------ Cargo.toml | 1 + book/src/SUMMARY.md | 1 + .../why-a-bridge-module/README.md | 158 ++++++++++++++++++ examples/without-a-bridge-module/Cargo.toml | 7 + examples/without-a-bridge-module/README.md | 18 ++ examples/without-a-bridge-module/src/main.rs | 18 ++ 7 files changed, 255 insertions(+), 49 deletions(-) create mode 100644 book/src/bridge-module/why-a-bridge-module/README.md create mode 100644 examples/without-a-bridge-module/Cargo.toml create mode 100644 examples/without-a-bridge-module/README.md create mode 100644 examples/without-a-bridge-module/src/main.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 88bc643f..8511c802 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,80 +12,83 @@ jobs: timeout-minutes: 15 steps: - - uses: actions/checkout@v2 - - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - - name: Rust Version Info - run: rustc --version && cargo --version - - - name: Cargo format - run: cargo fmt --all -- --check - - - name: Run tests - run: | - RUSTFLAGS="-D warnings" cargo test -p swift-bridge \ - -p swift-bridge-build \ - -p swift-bridge-cli \ - -p swift-bridge-ir \ - -p swift-bridge-macro \ - -p swift-integration-tests - + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Rust Version Info + run: rustc --version && cargo --version + + - name: Cargo format + run: cargo fmt --all -- --check + + - name: Run tests + run: | + RUSTFLAGS="-D warnings" cargo test -p swift-bridge \ + -p swift-bridge-build \ + -p swift-bridge-cli \ + -p swift-bridge-ir \ + -p swift-bridge-macro \ + -p swift-integration-tests + swift-package-test: runs-on: macos-14 timeout-minutes: 30 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable - - name: Add rust targets - run: rustup target add aarch64-apple-darwin x86_64-apple-darwin + - name: Add rust targets + run: rustup target add aarch64-apple-darwin x86_64-apple-darwin - - name: Run swift package tests - run: ./test-swift-packages.sh + - name: Run swift package tests + run: ./test-swift-packages.sh integration-test: runs-on: macos-14 timeout-minutes: 30 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable - - name: Add rust targets - run: rustup target add aarch64-apple-darwin x86_64-apple-darwin + - name: Add rust targets + run: rustup target add aarch64-apple-darwin x86_64-apple-darwin - - name: Run integration tests - run: ./test-swift-rust-integration.sh + - name: Run integration tests + run: ./test-swift-rust-integration.sh build-examples: runs-on: macos-14 timeout-minutes: 15 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable + - name: Add rust targets + run: rustup target add aarch64-apple-darwin x86_64-apple-darwin - - name: Add rust targets - run: rustup target add aarch64-apple-darwin x86_64-apple-darwin + - name: Build codegen-visualizer example + run: xcodebuild -project examples/codegen-visualizer/CodegenVisualizer/CodegenVisualizer.xcodeproj -scheme CodegenVisualizer - - name: Build codegen-visualizer example - run: xcodebuild -project examples/codegen-visualizer/CodegenVisualizer/CodegenVisualizer.xcodeproj -scheme CodegenVisualizer + - name: Build async function example + run: ./examples/async-functions/build.sh - - name: Build async function example - run: ./examples/async-functions/build.sh + - name: Build Rust binary calls Swift Package examaple + run: cargo build -p rust-binary-calls-swift-package - - name: Build Rust binary calls Swift Package examaple - run: cargo build -p rust-binary-calls-swift-package + - name: Build without-a-bridge-module example + run: cargo build -p without-a-bridge-module \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index d0b2cf40..73e8f0d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,4 +39,5 @@ members = [ "examples/async-functions", "examples/codegen-visualizer", "examples/rust-binary-calls-swift-package", + "examples/without-a-bridge-module", ] diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 64937628..b16c6144 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -17,6 +17,7 @@ - [Transparent Enums](./bridge-module/transparent-types/enums/README.md) - [Generics](./bridge-module/generics/README.md) - [Conditional Compilation](./bridge-module/conditional-compilation/README.md) + - [Why a Bridge Module](./bridge-module/why-a-bridge-module/README.md) - [Built In Types](./built-in/README.md) - [String <---> String](./built-in/string/README.md) diff --git a/book/src/bridge-module/why-a-bridge-module/README.md b/book/src/bridge-module/why-a-bridge-module/README.md new file mode 100644 index 00000000..725c6805 --- /dev/null +++ b/book/src/bridge-module/why-a-bridge-module/README.md @@ -0,0 +1,158 @@ +# 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: +```rust +#[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: +```rust +// 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. + +```rust +type Uuid = [u8; 16]; + +#[derive(Copy)] +struct UserId(Uuid); + +pub fn get_latest_user() -> Result { + 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: + +```rust +#[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: + +```rust +// 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([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`. +```rust +// WE DO NOT SUPPORT THIS + +#[swift_bridge::bridge] +#[swift_bridge(UserId impl Copy(16))] +pub fn get_latest_user() -> Result { + 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: +```rust +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: +```rust +/// `swift-bridge` could conceivably generate code like this to bridge +/// a `Result`. +/// 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. diff --git a/examples/without-a-bridge-module/Cargo.toml b/examples/without-a-bridge-module/Cargo.toml new file mode 100644 index 00000000..23c29c25 --- /dev/null +++ b/examples/without-a-bridge-module/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "without-a-bridge-module" +version = "0.1.0" +edition = "2021" + +[dependencies] +swift-bridge-ir = { path = "../../crates/swift-bridge-ir" } \ No newline at end of file diff --git a/examples/without-a-bridge-module/README.md b/examples/without-a-bridge-module/README.md new file mode 100644 index 00000000..2d8bedea --- /dev/null +++ b/examples/without-a-bridge-module/README.md @@ -0,0 +1,18 @@ +# without-a-bridge-module + +`swift-bridge`'s code generators live in `crates/swift-bridge-ir`. + +This example demonstrates how one might use the `swift-bridge-ir` crate directly in order to generate a Rust+Swift FFI boundary. + +This is mainly useful for library authors who might wish to expose an alternative frontend, such as being able to annotate types: +```rust +use some_third_party_lib; + +/// An imaginary third-party library that wraps `swift-bridge-ir` +/// in a proc macro attribute that users can annotate their types +/// with. +#[some_third_party_lib::ExposeToSwift] +pub struct User { + name: String +} +``` diff --git a/examples/without-a-bridge-module/src/main.rs b/examples/without-a-bridge-module/src/main.rs new file mode 100644 index 00000000..ff4c4410 --- /dev/null +++ b/examples/without-a-bridge-module/src/main.rs @@ -0,0 +1,18 @@ +fn main() { + // TODO: Use the `swift-bridge-ir` crate to generate a representation of the FFI + // boundary. + // Then use that representation to generate Rust, Swift and C code. + // Then write that code to a temporary directory and spawn a process to compile and run + // the generated code. + // Today, `swift-bridge-ir` has the `SwiftBridgeModule` type which represents a bridge module. + // One solution would be to create a new `RustSwiftFfiDefinition` type that holds the minimum + // information required to define an FFI boundary, and then change `swift-bridge-ir` from: + // - TODAY -> `SwiftBridgeModule` gets converted into Rust+Swift+C Code + // - FUTURE -> `SwiftBridgeModule` gets converted into `RustSwiftFfiDefinition` which gets + // converted into Rust+Swift+C Code + // After that we can make this `without-a-bridge-module` example make use of the + // `RustSwiftFfiDefinition` to generate some Rust+Swift+C FFI glue code. + // --- + // If you are reading this and would like to wrap `swift-bridge-ir` in your own library please + // open an issue so that we know when and how to prioritize this work. +}