Skip to content

Commit d64db64

Browse files
authored
Document motivation for bridge module (#301)
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.
1 parent c45a38c commit d64db64

File tree

7 files changed

+255
-49
lines changed

7 files changed

+255
-49
lines changed

.github/workflows/test.yml

+52-49
Original file line numberDiff line numberDiff line change
@@ -12,80 +12,83 @@ jobs:
1212
timeout-minutes: 15
1313

1414
steps:
15-
- uses: actions/checkout@v2
16-
17-
- uses: actions-rs/toolchain@v1
18-
with:
19-
toolchain: stable
20-
21-
- name: Rust Version Info
22-
run: rustc --version && cargo --version
23-
24-
- name: Cargo format
25-
run: cargo fmt --all -- --check
26-
27-
- name: Run tests
28-
run: |
29-
RUSTFLAGS="-D warnings" cargo test -p swift-bridge \
30-
-p swift-bridge-build \
31-
-p swift-bridge-cli \
32-
-p swift-bridge-ir \
33-
-p swift-bridge-macro \
34-
-p swift-integration-tests
35-
15+
- uses: actions/checkout@v2
16+
17+
- uses: actions-rs/toolchain@v1
18+
with:
19+
toolchain: stable
20+
21+
- name: Rust Version Info
22+
run: rustc --version && cargo --version
23+
24+
- name: Cargo format
25+
run: cargo fmt --all -- --check
26+
27+
- name: Run tests
28+
run: |
29+
RUSTFLAGS="-D warnings" cargo test -p swift-bridge \
30+
-p swift-bridge-build \
31+
-p swift-bridge-cli \
32+
-p swift-bridge-ir \
33+
-p swift-bridge-macro \
34+
-p swift-integration-tests
35+
3636
swift-package-test:
3737
runs-on: macos-14
3838
timeout-minutes: 30
3939

4040
steps:
41-
- uses: actions/checkout@v2
41+
- uses: actions/checkout@v2
4242

43-
- uses: actions-rs/toolchain@v1
44-
with:
45-
toolchain: stable
43+
- uses: actions-rs/toolchain@v1
44+
with:
45+
toolchain: stable
4646

47-
- name: Add rust targets
48-
run: rustup target add aarch64-apple-darwin x86_64-apple-darwin
47+
- name: Add rust targets
48+
run: rustup target add aarch64-apple-darwin x86_64-apple-darwin
4949

50-
- name: Run swift package tests
51-
run: ./test-swift-packages.sh
50+
- name: Run swift package tests
51+
run: ./test-swift-packages.sh
5252

5353
integration-test:
5454
runs-on: macos-14
5555
timeout-minutes: 30
5656

5757
steps:
58-
- uses: actions/checkout@v2
58+
- uses: actions/checkout@v2
5959

60-
- uses: actions-rs/toolchain@v1
61-
with:
62-
toolchain: stable
60+
- uses: actions-rs/toolchain@v1
61+
with:
62+
toolchain: stable
6363

64-
- name: Add rust targets
65-
run: rustup target add aarch64-apple-darwin x86_64-apple-darwin
64+
- name: Add rust targets
65+
run: rustup target add aarch64-apple-darwin x86_64-apple-darwin
6666

67-
- name: Run integration tests
68-
run: ./test-swift-rust-integration.sh
67+
- name: Run integration tests
68+
run: ./test-swift-rust-integration.sh
6969

7070
build-examples:
7171
runs-on: macos-14
7272
timeout-minutes: 15
7373

7474
steps:
75-
- uses: actions/checkout@v2
75+
- uses: actions/checkout@v2
76+
77+
- uses: actions-rs/toolchain@v1
78+
with:
79+
toolchain: stable
7680

77-
- uses: actions-rs/toolchain@v1
78-
with:
79-
toolchain: stable
81+
- name: Add rust targets
82+
run: rustup target add aarch64-apple-darwin x86_64-apple-darwin
8083

81-
- name: Add rust targets
82-
run: rustup target add aarch64-apple-darwin x86_64-apple-darwin
84+
- name: Build codegen-visualizer example
85+
run: xcodebuild -project examples/codegen-visualizer/CodegenVisualizer/CodegenVisualizer.xcodeproj -scheme CodegenVisualizer
8386

84-
- name: Build codegen-visualizer example
85-
run: xcodebuild -project examples/codegen-visualizer/CodegenVisualizer/CodegenVisualizer.xcodeproj -scheme CodegenVisualizer
87+
- name: Build async function example
88+
run: ./examples/async-functions/build.sh
8689

87-
- name: Build async function example
88-
run: ./examples/async-functions/build.sh
90+
- name: Build Rust binary calls Swift Package examaple
91+
run: cargo build -p rust-binary-calls-swift-package
8992

90-
- name: Build Rust binary calls Swift Package examaple
91-
run: cargo build -p rust-binary-calls-swift-package
93+
- name: Build without-a-bridge-module example
94+
run: cargo build -p without-a-bridge-module

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ members = [
3939
"examples/async-functions",
4040
"examples/codegen-visualizer",
4141
"examples/rust-binary-calls-swift-package",
42+
"examples/without-a-bridge-module",
4243
]

book/src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- [Transparent Enums](./bridge-module/transparent-types/enums/README.md)
1818
- [Generics](./bridge-module/generics/README.md)
1919
- [Conditional Compilation](./bridge-module/conditional-compilation/README.md)
20+
- [Why a Bridge Module](./bridge-module/why-a-bridge-module/README.md)
2021

2122
- [Built In Types](./built-in/README.md)
2223
- [String <---> String](./built-in/string/README.md)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Why a Bridge Module?
2+
3+
The `swift-bridge` project provides direct support for expressing the Rust+Swift FFI boundary using one or more bridge modules such as:
4+
```rust
5+
#[swift_bridge::bridge]
6+
mod ffi {
7+
extern "Rust" {
8+
fn generate_random_number() -> u32;
9+
}
10+
}
11+
12+
fn generate_random_number() -> u32 {
13+
rand::random()
14+
}
15+
```
16+
17+
`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.
18+
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.
19+
20+
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.
21+
22+
Users with other needs can write wrappers around `swift-bridge` to expose alternative frontends.
23+
24+
The `examples/without-a-bridge-macro` example demonstrates how to reuse `swift-bridge`'s code generation facilities without using a bridge module.
25+
26+
## Inline Annotations
27+
28+
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.
29+
30+
For instance a user might wish to expose their Rust banking code to Swift using an approach such as:
31+
```rust
32+
// IMAGINARY CODE. WE DO NOT PROVIDE A WAY TO DO THIS.
33+
34+
#[derive(Swift)]
35+
pub struct BankAccount {
36+
balance: u32
37+
}
38+
39+
#[swift_bridge::bridge]
40+
pub fn create_bank_account() -> BankAccount {
41+
BankAccount {
42+
balance: 0
43+
}
44+
}
45+
```
46+
47+
`swift-bridge` aims to be a low-level library that generates far more efficient FFI code than a human would write and maintain themselves.
48+
49+
The more information that `swift-bridge` has at compile time, the more efficient code it can generate.
50+
51+
Let's explore an example of bridging a `UserId` type, along with a function that returns the latest `UserId` in the system.
52+
53+
```rust
54+
type Uuid = [u8; 16];
55+
56+
#[derive(Copy)]
57+
struct UserId(Uuid);
58+
59+
pub fn get_latest_user() -> Result<UserId, ()> {
60+
Ok(UserId([123; 16]))
61+
}
62+
```
63+
64+
In our example, the `UserId` is a wrapper around a 16 byte UUID.
65+
66+
Exposing this as a bridge module might look like:
67+
68+
```rust
69+
#[swift_bridge::bridge]
70+
mod ffi {
71+
extern "Rust" {
72+
#[swift_bridge(Copy(16))]
73+
type UserId;
74+
75+
fn get_latest_user() -> UserId;
76+
}
77+
}
78+
```
79+
80+
Exposing the `UserId` using inlined annotation might look something like:
81+
82+
```rust
83+
// WE DO NOT SUPPORT THIS
84+
85+
type Uuid = [u8; 16];
86+
87+
#[derive(Copy, ExposeToSwift)]
88+
struct UserId(Uuid);
89+
90+
#[swift_bridge::bridge]
91+
pub fn get_latest_user() -> Result<UserId, ()> {
92+
UserId([123; 16])
93+
}
94+
```
95+
96+
In the bridge module example, `swift-bridge` knows at compile time that the `UserId` implements `Copy` and has a size of `16` bytes.
97+
98+
In the inlined annotation example, however, `swift-bridge` does not know the `UserId` implements `Copy`.
99+
100+
While it would be possible to inline this information, it would mean that users would need to remember to inline this information
101+
on every function that used the `UserId`.
102+
```rust
103+
// WE DO NOT SUPPORT THIS
104+
105+
#[swift_bridge::bridge]
106+
#[swift_bridge(UserId impl Copy(16))]
107+
pub fn get_latest_user() -> Result<UserId, ()> {
108+
UserId([123; 16])
109+
}
110+
```
111+
112+
We expect that users would find it difficult to remember to repeat such annotations, meaning users would tend to expose less efficient bridges
113+
than they otherwise could have.
114+
115+
If `swift-bridge` does not know that the `UserId` implements `Copy`, it will need to generate code like:
116+
```rust
117+
pub extern "C" fn __swift_bridge__get_latest_user() -> *mut UserId {
118+
let user = get_latest_user();
119+
match user {
120+
Ok(user) => Box::new(Box::into_raw(user)),
121+
Err(()) => std::ptr::null_mut() as *mut UserId,
122+
}
123+
}
124+
```
125+
126+
Whereas if `swift-bridge` knows that the `UserId` implements `Copy`, it might be able to avoid an allocation by generating code such as:
127+
```rust
128+
/// `swift-bridge` could conceivably generate code like this to bridge
129+
/// a `Result<UserId, ()>`.
130+
/// Here we use a 17 byte array where the first byte indicates `Ok` or `Err`
131+
/// and, then `Ok`, the last 16 bytes hold the `UserId`.
132+
/// We expect this to be more performant than the boxing in the previous
133+
/// example codegen.
134+
pub extern "C" fn __swift_bridge__get_latest_user() -> [u8; 17] {
135+
let mut bytes: [u8; 17] = [0; 17];
136+
137+
let user = get_latest_user();
138+
139+
match user {
140+
Ok(user) => {
141+
let user_bytes: [u8; 16] = unsafe { std::mem::transmute(user) };
142+
(&mut bytes[1..]).copy_from_slice(&user_bytes);
143+
144+
bytes[0] = 255;
145+
bytes
146+
}
147+
Err(()) => {
148+
bytes
149+
}
150+
}
151+
}
152+
```
153+
154+
More generally, the more information that `swift-bridge` has about the FFI interface, the more optimized code it can generate.
155+
The bridge module design steers users towards providing more information to `swift-bridge`, which we expect to lead to more efficient
156+
applications.
157+
158+
Users that do not need such efficiency can explore reusing `swift-bridge` in alternative projects that better meet their needs.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[package]
2+
name = "without-a-bridge-module"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
swift-bridge-ir = { path = "../../crates/swift-bridge-ir" }
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# without-a-bridge-module
2+
3+
`swift-bridge`'s code generators live in `crates/swift-bridge-ir`.
4+
5+
This example demonstrates how one might use the `swift-bridge-ir` crate directly in order to generate a Rust+Swift FFI boundary.
6+
7+
This is mainly useful for library authors who might wish to expose an alternative frontend, such as being able to annotate types:
8+
```rust
9+
use some_third_party_lib;
10+
11+
/// An imaginary third-party library that wraps `swift-bridge-ir`
12+
/// in a proc macro attribute that users can annotate their types
13+
/// with.
14+
#[some_third_party_lib::ExposeToSwift]
15+
pub struct User {
16+
name: String
17+
}
18+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
fn main() {
2+
// TODO: Use the `swift-bridge-ir` crate to generate a representation of the FFI
3+
// boundary.
4+
// Then use that representation to generate Rust, Swift and C code.
5+
// Then write that code to a temporary directory and spawn a process to compile and run
6+
// the generated code.
7+
// Today, `swift-bridge-ir` has the `SwiftBridgeModule` type which represents a bridge module.
8+
// One solution would be to create a new `RustSwiftFfiDefinition` type that holds the minimum
9+
// information required to define an FFI boundary, and then change `swift-bridge-ir` from:
10+
// - TODAY -> `SwiftBridgeModule` gets converted into Rust+Swift+C Code
11+
// - FUTURE -> `SwiftBridgeModule` gets converted into `RustSwiftFfiDefinition` which gets
12+
// converted into Rust+Swift+C Code
13+
// After that we can make this `without-a-bridge-module` example make use of the
14+
// `RustSwiftFfiDefinition` to generate some Rust+Swift+C FFI glue code.
15+
// ---
16+
// If you are reading this and would like to wrap `swift-bridge-ir` in your own library please
17+
// open an issue so that we know when and how to prioritize this work.
18+
}

0 commit comments

Comments
 (0)