Skip to content

Commit 4827500

Browse files
authored
Add #[swift_bridge(Sendable)] attribute (#317)
This commit introduces a new `#[swift_bridge(Sendable)]` attribute that emits Swift `Sendable` implementations for opaque Rust types, and Rust `Send + Sync` implementations for opaque Swift types. These implementations are checked at compile time. Example usage: ```rust mod ffi { extern "Rust" { #[swift_bridge(Sendable)] type MyRustType; } extern "Swift" { #[swift_bridge(Sendable)] type MySwiftType; } } ```
1 parent c15279c commit 4827500

File tree

16 files changed

+420
-50
lines changed

16 files changed

+420
-50
lines changed

SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunner.xcodeproj/project.pbxproj

+19-11
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
220432EC27530AFC00BAE645 /* RustFnUsesOpaqueSwiftTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 220432EB27530AFC00BAE645 /* RustFnUsesOpaqueSwiftTypeTests.swift */; };
2222
22046383282B4E3F00A09119 /* FunctionAttributeGetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22046382282B4E3F00A09119 /* FunctionAttributeGetTests.swift */; };
2323
221E16B42786233600F94AC0 /* ConditionalCompilationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 221E16B32786233600F94AC0 /* ConditionalCompilationTests.swift */; };
24-
221E16B62786F9FF00F94AC0 /* OpaqueTypeAttributeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 221E16B52786F9FF00F94AC0 /* OpaqueTypeAttributeTests.swift */; };
24+
221E16B62786F9FF00F94AC0 /* AlreadyDeclaredAttributeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 221E16B52786F9FF00F94AC0 /* AlreadyDeclaredAttributeTests.swift */; };
2525
222A81E928EB5BB100D4A412 /* Primitive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222A81E828EB5BB100D4A412 /* Primitive.swift */; };
2626
222A81EB28EB5DF800D4A412 /* PrimitiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222A81EA28EB5DF800D4A412 /* PrimitiveTests.swift */; };
2727
22553324281DB5FC008A3121 /* GenericTests.rs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22553323281DB5FC008A3121 /* GenericTests.rs.swift */; };
@@ -57,6 +57,8 @@
5757
22FD1C562753CB3F00F64281 /* SwiftFnUsesOpaqueRustTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22FD1C552753CB3F00F64281 /* SwiftFnUsesOpaqueRustTypeTests.swift */; };
5858
C926E4DE294F07AA0027E7E2 /* FunctionAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = C926E4DD294F07AA0027E7E2 /* FunctionAttributes.swift */; };
5959
C926E4E0294F18C50027E7E2 /* FunctionAttributeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C926E4DF294F18C50027E7E2 /* FunctionAttributeTests.swift */; };
60+
DE9444DC2D5241AD007A83A4 /* SendableAttributeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE9444DB2D5241A8007A83A4 /* SendableAttributeTests.swift */; };
61+
DE9444DE2D527C3B007A83A4 /* SendableAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE9444DD2D527C31007A83A4 /* SendableAttribute.swift */; };
6062
/* End PBXBuildFile section */
6163

6264
/* Begin PBXContainerItemProxy section */
@@ -84,7 +86,7 @@
8486
220432EB27530AFC00BAE645 /* RustFnUsesOpaqueSwiftTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RustFnUsesOpaqueSwiftTypeTests.swift; sourceTree = "<group>"; };
8587
22046382282B4E3F00A09119 /* FunctionAttributeGetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FunctionAttributeGetTests.swift; sourceTree = "<group>"; };
8688
221E16B32786233600F94AC0 /* ConditionalCompilationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalCompilationTests.swift; sourceTree = "<group>"; };
87-
221E16B52786F9FF00F94AC0 /* OpaqueTypeAttributeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpaqueTypeAttributeTests.swift; sourceTree = "<group>"; };
89+
221E16B52786F9FF00F94AC0 /* AlreadyDeclaredAttributeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlreadyDeclaredAttributeTests.swift; sourceTree = "<group>"; };
8890
222A81E828EB5BB100D4A412 /* Primitive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Primitive.swift; sourceTree = "<group>"; };
8991
222A81EA28EB5DF800D4A412 /* PrimitiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimitiveTests.swift; sourceTree = "<group>"; };
9092
22553323281DB5FC008A3121 /* GenericTests.rs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericTests.rs.swift; sourceTree = "<group>"; };
@@ -126,6 +128,8 @@
126128
22FD1C552753CB3F00F64281 /* SwiftFnUsesOpaqueRustTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftFnUsesOpaqueRustTypeTests.swift; sourceTree = "<group>"; };
127129
C926E4DD294F07AA0027E7E2 /* FunctionAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionAttributes.swift; sourceTree = "<group>"; };
128130
C926E4DF294F18C50027E7E2 /* FunctionAttributeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionAttributeTests.swift; sourceTree = "<group>"; };
131+
DE9444DB2D5241A8007A83A4 /* SendableAttributeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendableAttributeTests.swift; sourceTree = "<group>"; };
132+
DE9444DD2D527C31007A83A4 /* SendableAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendableAttribute.swift; sourceTree = "<group>"; };
129133
/* End PBXFileReference section */
130134

131135
/* Begin PBXFrameworksBuildPhase section */
@@ -189,6 +193,7 @@
189193
22EE4E0828B5388000FEC83C /* SwiftFnUsesOpaqueSwiftType.swift */,
190194
22C0625228CE699D007A6F67 /* Callbacks.swift */,
191195
225908FD28DA0F9F0080C737 /* Result.swift */,
196+
DE9444DD2D527C31007A83A4 /* SendableAttribute.swift */,
192197
22BC4BBB294BA0EC0032B8A8 /* SharedEnumAttributes.swift */,
193198
C926E4DD294F07AA0027E7E2 /* FunctionAttributes.swift */,
194199
1784BE2729CE86D600AE5A4A /* Tuple.swift */,
@@ -208,32 +213,33 @@
208213
228FE5E52740DB6D00805D9E /* SwiftRustIntegrationTestRunnerTests */ = {
209214
isa = PBXGroup;
210215
children = (
216+
221E16B52786F9FF00F94AC0 /* AlreadyDeclaredAttributeTests.swift */,
217+
178F1CD2298E97FB00335AA0 /* ArgumentAttributesTest.swift */,
211218
22D092A227B7E865009A4C2B /* AsyncTests.swift */,
219+
22C0625428CE6C9A007A6F67 /* CallbackTests.swift */,
212220
221E16B32786233600F94AC0 /* ConditionalCompilationTests.swift */,
221+
C926E4DF294F18C50027E7E2 /* FunctionAttributeTests.swift */,
213222
22046382282B4E3F00A09119 /* FunctionAttributeGetTests.swift */,
214223
22BCAAB827A2607700686A21 /* FunctionAttributeIdentifiableTests.swift */,
224+
22553323281DB5FC008A3121 /* GenericTests.rs.swift */,
215225
228FE60F27416C0300805D9E /* OpaqueRustStructTests.swift */,
216226
228FE61127428A8D00805D9E /* OpaqueSwiftStructTests.swift */,
217-
221E16B52786F9FF00F94AC0 /* OpaqueTypeAttributeTests.swift */,
227+
220432A6274C953E00BAE645 /* PointerTests.swift */,
218228
22043294274ADA7A00BAE645 /* OptionTests.swift */,
219229
225908FB28DA0E320080C737 /* ResultTests.swift */,
220-
220432A6274C953E00BAE645 /* PointerTests.swift */,
221230
222A81EA28EB5DF800D4A412 /* PrimitiveTests.swift */,
222231
220432EB27530AFC00BAE645 /* RustFnUsesOpaqueSwiftTypeTests.swift */,
232+
DE9444DB2D5241A8007A83A4 /* SendableAttributeTests.swift */,
223233
2202BC0727B2DD1700D43CC4 /* SharedEnumTests.swift */,
224234
22BC4BB9294B8CCD0032B8A8 /* SharedEnumAttributeTests.swift */,
225235
22C0AD50278ECA9E00A96469 /* SharedStructAttributeTests.swift */,
226236
220432AE274E7BF800BAE645 /* SharedStructTests.swift */,
237+
2289E82B29A879A7009D89D7 /* SingleRepresentationTypeElisionTests.swift */,
227238
228FE5E62740DB6D00805D9E /* StringTests.swift */,
228239
22FD1C552753CB3F00F64281 /* SwiftFnUsesOpaqueRustTypeTests.swift */,
229-
22043292274A8FDF00BAE645 /* VecTests.swift */,
230-
22553323281DB5FC008A3121 /* GenericTests.rs.swift */,
231-
2289E82B29A879A7009D89D7 /* SingleRepresentationTypeElisionTests.swift */,
232240
22EE4E0A28B538A700FEC83C /* SwiftFnUsesOpaqueSwiftTypeTests.swift */,
233-
22C0625428CE6C9A007A6F67 /* CallbackTests.swift */,
234-
C926E4DF294F18C50027E7E2 /* FunctionAttributeTests.swift */,
235-
178F1CD2298E97FB00335AA0 /* ArgumentAttributesTest.swift */,
236241
1745111429BE189B00B96A1A /* TupleTests.swift */,
242+
22043292274A8FDF00BAE645 /* VecTests.swift */,
237243
);
238244
path = SwiftRustIntegrationTestRunnerTests;
239245
sourceTree = "<group>";
@@ -407,6 +413,7 @@
407413
C926E4DE294F07AA0027E7E2 /* FunctionAttributes.swift in Sources */,
408414
228FE64A274919C600805D9E /* swift-integration-tests.swift in Sources */,
409415
22C0625328CE699D007A6F67 /* Callbacks.swift in Sources */,
416+
DE9444DE2D527C3B007A83A4 /* SendableAttribute.swift in Sources */,
410417
22BC10F82799A3A000A0D046 /* SharedStructAttributes.swift in Sources */,
411418
22EE4E0928B5388000FEC83C /* SwiftFnUsesOpaqueSwiftType.swift in Sources */,
412419
228FE60C2740F42000805D9E /* ASwiftStack.swift in Sources */,
@@ -418,12 +425,13 @@
418425
buildActionMask = 2147483647;
419426
files = (
420427
22043293274A8FDF00BAE645 /* VecTests.swift in Sources */,
421-
221E16B62786F9FF00F94AC0 /* OpaqueTypeAttributeTests.swift in Sources */,
428+
221E16B62786F9FF00F94AC0 /* AlreadyDeclaredAttributeTests.swift in Sources */,
422429
220432A7274C953E00BAE645 /* PointerTests.swift in Sources */,
423430
C926E4E0294F18C50027E7E2 /* FunctionAttributeTests.swift in Sources */,
424431
220432AF274E7BF800BAE645 /* SharedStructTests.swift in Sources */,
425432
178F1CD3298E97FB00335AA0 /* ArgumentAttributesTest.swift in Sources */,
426433
2289E82C29A879A7009D89D7 /* SingleRepresentationTypeElisionTests.swift in Sources */,
434+
DE9444DC2D5241AD007A83A4 /* SendableAttributeTests.swift in Sources */,
427435
220432EC27530AFC00BAE645 /* RustFnUsesOpaqueSwiftTypeTests.swift in Sources */,
428436
222A81EB28EB5DF800D4A412 /* PrimitiveTests.swift in Sources */,
429437
22553324281DB5FC008A3121 /* GenericTests.rs.swift in Sources */,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// SendableAttribute.swift
3+
// SwiftRustIntegrationTestRunner
4+
//
5+
// Created by Frankie Nwafili on 2/4/25.
6+
//
7+
8+
final class SendableSwiftType {}
9+
extension SendableSwiftType: Sendable {}
10+
11+
final class AnotherSendableSwiftType {}
12+
extension AnotherSendableSwiftType: Sendable {}

SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/OpaqueTypeAttributeTests.swift SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/AlreadyDeclaredAttributeTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// OpaqueTypeAttributeTests.swift
2+
// AlreadyDeclaredAttributeTests.swift
33
// SwiftRustIntegrationTestRunnerTests
44
//
55
// Created by Frankie Nwafili on 1/6/22.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// SendableAttributeTests.swift
3+
// SwiftRustIntegrationTestRunner
4+
//
5+
// Created by Frankie Nwafili on 2/4/25.
6+
//
7+
8+
import XCTest
9+
@testable import SwiftRustIntegrationTestRunner
10+
11+
/// Tests for the `#[swift_bridge(Sendable)]` attribute.
12+
///
13+
/// For the corresponding Rust code, see `crates/swift-integration-tests/src/sendable_attribute.rs`.
14+
/// For the corresponding codegen tests, see `crates/swift-bridge-ir/src/codegen/codegen_tests/sendable_attribute.rs`.
15+
class SendableAttributeTests: XCTestCase {
16+
/// Verify that we can a Rust type that has the `#[swift_bridge(Sendable)]` attribute gets a `Sendable` protocol implementation.
17+
func testSendableExternRustType() throws {
18+
let sendableRustType = SendableRustType()
19+
20+
// Move the type to another thread.
21+
Thread.detachNewThread {
22+
let _ = sendableRustType;
23+
}
24+
}
25+
}
26+
27+
28+
protocol AssertIsSendable: Sendable {}
29+
extension SendableRustType: AssertIsSendable{}
30+

book/src/bridge-module/opaque-types/README.md

+25
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,31 @@ table[val] = "world"
266266
print(table[val])
267267
```
268268

269+
#### #[swift_bridge(Sendable)]
270+
271+
The `Sendable` attribute can be added to both opaque Rust and opaque Swift types.
272+
273+
When applied to an opaque Rust type, the generated Swift type will implement Swift's `Sendable` protocol.
274+
`swift-bridge` will emit code that, at compile time, confirms that the Rust type implements `Send + Sync`.
275+
276+
When applied to an opaque Swift type, the generated Rust type will implement Rust's `Send + Sync` traits.
277+
`swift-bridge` will emit code that, at compile time, confirms that the Swift type implements `Sendable`.
278+
279+
```rust
280+
#[swift_bridge::bridge]
281+
mod ffi {
282+
extern "Rust" {
283+
#[swift_bridge(Sendable)]
284+
type MyRustType;
285+
}
286+
287+
extern "Swift" {
288+
#[swift_bridge(Sendable)]
289+
type MySwiftType;
290+
}
291+
}
292+
```
293+
269294
#### #[swift_bridge(__experimental_ownership)]
270295

271296
The `__experimental_ownership` attribute instructs `swift-bridge` to emit code that takes advantage of Swift 6's

crates/swift-bridge-ir/src/codegen/codegen_tests.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ mod boxed_fnonce;
3434
mod built_in_tuple;
3535
mod c_header_declaration_order;
3636
mod conditional_compilation;
37-
mod derive_attribute;
38-
mod derive_struct_attribute;
37+
mod derive_copy_clone;
38+
mod derive_debug;
3939
mod extern_rust_function_opaque_rust_type_argument;
4040
mod extern_rust_function_opaque_rust_type_return;
4141
mod extern_rust_method_swift_class_placement;
@@ -46,6 +46,7 @@ mod opaque_swift_type;
4646
mod option;
4747
mod result;
4848
mod return_into_attribute;
49+
mod sendable_attribute;
4950
mod single_representation_type_elision;
5051
mod string;
5152
mod transparent_enum;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use super::{ExpectedCHeader, ExpectedRustTokens, ExpectedSwiftCode};
2+
use proc_macro2::TokenStream;
3+
use quote::quote;
4+
5+
/// Verify that we can add `#[swift_bridge(Sendable)]` to an extern Rust type.
6+
mod extern_rust_sendable_attribute {
7+
use super::*;
8+
use crate::codegen::codegen_tests::CodegenTest;
9+
10+
fn bridge_module_tokens() -> TokenStream {
11+
quote! {
12+
mod ffi {
13+
extern "Rust" {
14+
#[swift_bridge(Sendable)]
15+
type SomeType;
16+
}
17+
}
18+
}
19+
}
20+
21+
/// Verify that we generate a function that frees the memory behind an opaque pointer to a Rust
22+
/// type.
23+
fn expected_rust_tokens() -> ExpectedRustTokens {
24+
ExpectedRustTokens::Contains(quote! {
25+
const fn __swift_bridge__assert_send_sync<T: Send + Sync>() {}
26+
const _: () = { __swift_bridge__assert_send_sync::<super::SomeType>() };
27+
})
28+
}
29+
30+
fn expected_swift_code() -> ExpectedSwiftCode {
31+
ExpectedSwiftCode::ContainsAfterTrim(
32+
r#"
33+
extension SomeType: @unchecked Sendable {}
34+
"#,
35+
)
36+
}
37+
38+
const EXPECTED_C_HEADER: ExpectedCHeader = ExpectedCHeader::ContainsAfterTrim(
39+
r#"
40+
typedef struct SomeType SomeType;
41+
void __swift_bridge__$SomeType$_free(void* self);
42+
"#,
43+
);
44+
45+
#[test]
46+
fn extern_rust_type() {
47+
CodegenTest {
48+
bridge_module: bridge_module_tokens().into(),
49+
expected_rust_tokens: expected_rust_tokens(),
50+
expected_swift_code: expected_swift_code(),
51+
expected_c_header: EXPECTED_C_HEADER,
52+
}
53+
.test();
54+
}
55+
}
56+
57+
/// Verify that we can add `#[swift_bridge(Sendable)]` to an extern Swift type.
58+
mod extern_swift_sendable_attribute {
59+
use super::*;
60+
use crate::codegen::codegen_tests::CodegenTest;
61+
62+
fn bridge_module_tokens() -> TokenStream {
63+
quote! {
64+
mod foo {
65+
extern "Swift" {
66+
#[swift_bridge(Sendable)]
67+
type SomeSwiftType;
68+
}
69+
}
70+
}
71+
}
72+
73+
fn expected_rust_tokens() -> ExpectedRustTokens {
74+
ExpectedRustTokens::Contains(quote! {
75+
#[repr(C)]
76+
pub struct SomeSwiftType(*mut std::ffi::c_void);
77+
78+
impl Drop for SomeSwiftType {
79+
fn drop (&mut self) {
80+
unsafe { __swift_bridge__SomeSwiftType__free(self.0) }
81+
}
82+
}
83+
84+
unsafe impl Send for SomeSwiftType {}
85+
unsafe impl Sync for SomeSwiftType {}
86+
87+
})
88+
}
89+
90+
const EXPECTED_SWIFT_CODE: ExpectedSwiftCode = ExpectedSwiftCode::ContainsAfterTrim(
91+
r#"
92+
protocol __swift_bridge__IsSendable: Sendable {}
93+
extension SomeSwiftType: __swift_bridge__IsSendable {}
94+
"#,
95+
);
96+
97+
const EXPECTED_C_HEADER: ExpectedCHeader = ExpectedCHeader::ExactAfterTrim(r#""#);
98+
99+
#[test]
100+
fn extern_swift_type_derive_sendable() {
101+
CodegenTest {
102+
bridge_module: bridge_module_tokens().into(),
103+
expected_rust_tokens: expected_rust_tokens(),
104+
expected_swift_code: EXPECTED_SWIFT_CODE,
105+
expected_c_header: EXPECTED_C_HEADER,
106+
}
107+
.test();
108+
}
109+
}

crates/swift-bridge-ir/src/codegen/generate_c_header.rs

+6
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,12 @@ fn declare_func(
523523
#[cfg(test)]
524524
mod tests {
525525
//! More tests can be found in src/codegen/codegen_tests.rs and its submodules.
526+
//!
527+
//! TODO: Gradually delete these tests and replace them with tests in the existing
528+
//! `mod codegen_tests`.
529+
//! This way we have one place to analyze the related Rust+Swift+C generated code
530+
//! vs. currently needing to look at `generate_swift.rs` `generate_c.rs` and `generate_rust.rs`
531+
//! to get a full picture of the codegen.
526532
527533
use proc_macro2::TokenStream;
528534
use quote::quote;

0 commit comments

Comments
 (0)