Skip to content

Commit ea55b2a

Browse files
authored
Add Swift 6 compatibility for async function code generation (#362)
Add Swift 6 strict concurrency compatibility for generated async function code Add `@MainActor` to test classes using XCTContext.runActivity Add CI matrix to test with both Swift 5.0 and Swift 6.0
1 parent 82b0885 commit ea55b2a

9 files changed

Lines changed: 128 additions & 19 deletions

File tree

.github/workflows/test.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ jobs:
5353
integration-test:
5454
runs-on: macos-14
5555
timeout-minutes: 30
56+
strategy:
57+
matrix:
58+
# NOTE: As explained in `README.md`, the minimum supported Swift version is currently `6.0`.
59+
# If supporting `5.0` ever becomes difficult we can consider removing it from the test matrix.
60+
swift_version: ['5.0', '6.0']
5661

5762
steps:
5863
- uses: actions/checkout@v2
@@ -61,15 +66,16 @@ jobs:
6166
with:
6267
toolchain: stable
6368

64-
# Xcode 16.2 provides Swift 5.9+ which is required for typed throws.
69+
# Xcode 16.2 provides Swift 5.9+ and Swift 6.0, which we make use of in the test matrix above.
70+
# If we need to support other versions in the future we can change this Xcode version.
6571
- name: Set Xcode version
6672
run: sudo xcode-select -s /Applications/Xcode_16.2.app
6773

6874
- name: Add rust targets
6975
run: rustup target add aarch64-apple-darwin x86_64-apple-darwin
7076

71-
- name: Run integration tests
72-
run: ./test-swift-rust-integration.sh
77+
- name: Run integration tests (Swift ${{ matrix.swift_version }})
78+
run: SWIFT_VERSION=${{ matrix.swift_version }} ./test-swift-rust-integration.sh
7379

7480
build-examples:
7581
runs-on: macos-14
@@ -95,4 +101,4 @@ jobs:
95101
run: cargo build -p rust-binary-calls-swift-package
96102

97103
- name: Build without-a-bridge-module example
98-
run: cargo build -p without-a-bridge-module
104+
run: cargo build -p without-a-bridge-module

SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/OpaqueRustStructTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import XCTest
99
@testable import SwiftRustIntegrationTestRunner
1010

11+
@MainActor
1112
class OpaqueRustStructTests: XCTestCase {
1213

1314
override func setUpWithError() throws {

SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/ResultTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import XCTest
88
@testable import SwiftRustIntegrationTestRunner
99

10+
@MainActor
1011
class ResultTests: XCTestCase {
1112
/// Verify that we can pass a Result<String, String> from Swift -> Rust
1213
func testSwiftCallRustResultString() throws {

SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/StringTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import XCTest
99
@testable import SwiftRustIntegrationTestRunner
1010

11+
@MainActor
1112
class StringTests: XCTestCase {
1213
override func setUpWithError() throws {
1314
// Put setup code here. This method is called before the invocation of each test method in the class.

SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/TupleTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import XCTest
1111
@testable import SwiftRustIntegrationTestRunner
1212

1313
/// Tests tuples
14+
@MainActor
1415
final class TupleTest: XCTestCase {
1516
/// Verify that we can pass and return Rust tuples.
1617
func testSwiftCallsRustTuples() throws {

crates/swift-bridge-build/src/generate_core.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ fn core_swift() -> String {
7272

7373
core_swift += &generic_freer();
7474
core_swift += &generic_copy_type_ffi_repr();
75+
core_swift += &unchecked_sendable_wrapper();
7576

7677
core_swift
7778
}
@@ -212,3 +213,14 @@ fn generic_copy_type_ffi_repr() -> &'static str {
212213
protocol SwiftBridgeGenericCopyTypeFfiRepr {}
213214
"#
214215
}
216+
217+
/// A wrapper type that makes any value Sendable for use in Task closures.
218+
/// This is used for FFI callbacks that we know are safe to use across Task boundaries.
219+
fn unchecked_sendable_wrapper() -> &'static str {
220+
r#"
221+
public struct __private__UncheckedSendable<T>: @unchecked Sendable {
222+
public let value: T
223+
@inlinable public init(_ value: T) { self.value = value }
224+
}
225+
"#
226+
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,7 +1100,9 @@ mod extern_swift_async_function_no_return {
11001100
r#"
11011101
@_cdecl("__swift_bridge__$some_function")
11021102
func __swift_bridge__some_function (_ callbackWrapper: UnsafeMutableRawPointer, _ callback: @escaping @convention(c) (UnsafeMutableRawPointer) -> Void) {
1103+
let __callbacks = __private__UncheckedSendable((callbackWrapper, callback))
11031104
Task {
1105+
let (callbackWrapper, callback) = __callbacks.value
11041106
let _ = await some_function()
11051107
callback(callbackWrapper)
11061108
}
@@ -1176,7 +1178,9 @@ mod extern_swift_async_function_returns_u8 {
11761178
r#"
11771179
@_cdecl("__swift_bridge__$some_function")
11781180
func __swift_bridge__some_function (_ callbackWrapper: UnsafeMutableRawPointer, _ callback: @escaping @convention(c) (UnsafeMutableRawPointer, UInt8) -> Void) {
1181+
let __callbacks = __private__UncheckedSendable((callbackWrapper, callback))
11791182
Task {
1183+
let (callbackWrapper, callback) = __callbacks.value
11801184
let result = await some_function()
11811185
callback(callbackWrapper, result)
11821186
}
@@ -1252,7 +1256,9 @@ mod extern_swift_async_function_with_args {
12521256
r#"
12531257
@_cdecl("__swift_bridge__$some_function")
12541258
func __swift_bridge__some_function (_ callbackWrapper: UnsafeMutableRawPointer, _ callback: @escaping @convention(c) (UnsafeMutableRawPointer, UInt8) -> Void, _ arg: UInt32) {
1259+
let __callbacks = __private__UncheckedSendable((callbackWrapper, callback))
12551260
Task {
1261+
let (callbackWrapper, callback) = __callbacks.value
12561262
let result = await some_function(arg: arg)
12571263
callback(callbackWrapper, result)
12581264
}
@@ -1338,7 +1344,9 @@ mod extern_swift_async_function_returns_result {
13381344
r#"
13391345
@_cdecl("__swift_bridge__$some_function")
13401346
func __swift_bridge__some_function (_ callbackWrapper: UnsafeMutableRawPointer, _ onSuccess: @escaping @convention(c) (UnsafeMutableRawPointer, UInt32) -> Void, _ onError: @escaping @convention(c) (UnsafeMutableRawPointer, UnsafeMutableRawPointer) -> Void) {
1347+
let __callbacks = __private__UncheckedSendable((callbackWrapper, onSuccess, onError))
13411348
Task {
1349+
let (callbackWrapper, onSuccess, onError) = __callbacks.value
13421350
do {
13431351
let result = try await some_function()
13441352
onSuccess(callbackWrapper, result)
@@ -1432,7 +1440,9 @@ mod extern_swift_async_function_returns_result_with_args {
14321440
r#"
14331441
@_cdecl("__swift_bridge__$some_function")
14341442
func __swift_bridge__some_function (_ callbackWrapper: UnsafeMutableRawPointer, _ onSuccess: @escaping @convention(c) (UnsafeMutableRawPointer, UInt32) -> Void, _ onError: @escaping @convention(c) (UnsafeMutableRawPointer, UnsafeMutableRawPointer) -> Void, _ arg: UInt32) {
1443+
let __callbacks = __private__UncheckedSendable((callbackWrapper, onSuccess, onError))
14351444
Task {
1445+
let (callbackWrapper, onSuccess, onError) = __callbacks.value
14361446
do {
14371447
let result = try await some_function(arg: arg)
14381448
onSuccess(callbackWrapper, result)
@@ -1524,7 +1534,9 @@ mod extern_swift_async_function_returns_result_void_ok {
15241534
r#"
15251535
@_cdecl("__swift_bridge__$some_function")
15261536
func __swift_bridge__some_function (_ callbackWrapper: UnsafeMutableRawPointer, _ onSuccess: @escaping @convention(c) (UnsafeMutableRawPointer) -> Void, _ onError: @escaping @convention(c) (UnsafeMutableRawPointer, UnsafeMutableRawPointer) -> Void) {
1537+
let __callbacks = __private__UncheckedSendable((callbackWrapper, onSuccess, onError))
15271538
Task {
1539+
let (callbackWrapper, onSuccess, onError) = __callbacks.value
15281540
do {
15291541
_ = try await some_function()
15301542
onSuccess(callbackWrapper)
@@ -1617,7 +1629,9 @@ mod extern_swift_async_function_returns_result_void_ok_with_args {
16171629
r#"
16181630
@_cdecl("__swift_bridge__$some_function")
16191631
func __swift_bridge__some_function (_ callbackWrapper: UnsafeMutableRawPointer, _ onSuccess: @escaping @convention(c) (UnsafeMutableRawPointer) -> Void, _ onError: @escaping @convention(c) (UnsafeMutableRawPointer, UnsafeMutableRawPointer) -> Void, _ arg: UInt32) {
1632+
let __callbacks = __private__UncheckedSendable((callbackWrapper, onSuccess, onError))
16201633
Task {
1634+
let (callbackWrapper, onSuccess, onError) = __callbacks.value
16211635
do {
16221636
_ = try await some_function(arg: arg)
16231637
onSuccess(callbackWrapper)
@@ -1699,7 +1713,9 @@ mod extern_swift_async_method_with_self {
16991713
r#"
17001714
@_cdecl("__swift_bridge__$SomeType$some_method")
17011715
func __swift_bridge__SomeType_some_method (_ callbackWrapper: UnsafeMutableRawPointer, _ callback: @escaping @convention(c) (UnsafeMutableRawPointer, UInt32) -> Void, _ this: UnsafeMutableRawPointer) {
1716+
let __captures = __private__UncheckedSendable((callbackWrapper, callback, this))
17021717
Task {
1718+
let (callbackWrapper, callback, this) = __captures.value
17031719
let result = await Unmanaged<SomeType>.fromOpaque(this).takeUnretainedValue().some_method()
17041720
callback(callbackWrapper, result)
17051721
}
@@ -1776,7 +1792,9 @@ mod extern_swift_async_method_with_self_and_args {
17761792
r#"
17771793
@_cdecl("__swift_bridge__$SomeType$some_method")
17781794
func __swift_bridge__SomeType_some_method (_ callbackWrapper: UnsafeMutableRawPointer, _ callback: @escaping @convention(c) (UnsafeMutableRawPointer, UInt8) -> Void, _ this: UnsafeMutableRawPointer, _ arg1: UInt32, _ arg2: UnsafeMutableRawPointer) {
1795+
let __captures = __private__UncheckedSendable((callbackWrapper, callback, this))
17791796
Task {
1797+
let (callbackWrapper, callback, this) = __captures.value
17801798
let result = await Unmanaged<SomeType>.fromOpaque(this).takeUnretainedValue().some_method(arg1: arg1, arg2: RustString(ptr: arg2))
17811799
callback(callbackWrapper, result)
17821800
}
@@ -1863,7 +1881,9 @@ mod extern_swift_async_method_with_self_returns_result {
18631881
r#"
18641882
@_cdecl("__swift_bridge__$SomeType$some_method")
18651883
func __swift_bridge__SomeType_some_method (_ callbackWrapper: UnsafeMutableRawPointer, _ onSuccess: @escaping @convention(c) (UnsafeMutableRawPointer, UInt32) -> Void, _ onError: @escaping @convention(c) (UnsafeMutableRawPointer, UnsafeMutableRawPointer) -> Void, _ this: UnsafeMutableRawPointer, _ arg: UInt32) {
1884+
let __captures = __private__UncheckedSendable((callbackWrapper, onSuccess, onError, this))
18661885
Task {
1886+
let (callbackWrapper, onSuccess, onError, this) = __captures.value
18671887
do {
18681888
let result = try await Unmanaged<SomeType>.fromOpaque(this).takeUnretainedValue().some_method(arg: arg)
18691889
onSuccess(callbackWrapper, result)
@@ -1952,7 +1972,9 @@ mod extern_swift_async_method_with_self_returns_result_void_ok {
19521972
r#"
19531973
@_cdecl("__swift_bridge__$SomeType$some_method")
19541974
func __swift_bridge__SomeType_some_method (_ callbackWrapper: UnsafeMutableRawPointer, _ onSuccess: @escaping @convention(c) (UnsafeMutableRawPointer) -> Void, _ onError: @escaping @convention(c) (UnsafeMutableRawPointer, UnsafeMutableRawPointer) -> Void, _ this: UnsafeMutableRawPointer) {
1975+
let __captures = __private__UncheckedSendable((callbackWrapper, onSuccess, onError, this))
19551976
Task {
1977+
let (callbackWrapper, onSuccess, onError, this) = __captures.value
19561978
do {
19571979
_ = try await Unmanaged<SomeType>.fromOpaque(this).takeUnretainedValue().some_method()
19581980
onSuccess(callbackWrapper)

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

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,10 @@ fn gen_async_function_exposes_swift_to_rust(
301301
// Build the async call expression
302302
let call_expression = build_swift_call_expression(func, fn_name, &args);
303303

304-
// Build params_str, task_body, and optional typed_throws_check based on whether this is a Result type or not
305-
let (params_str, task_body, typed_throws_check) = if let Some(result) = maybe_result {
304+
// Build params_str, task_body, optional typed_throws_check, and pre_task_bindings based on whether this is a Result type or not
305+
let (params_str, task_body, typed_throws_check, pre_task_bindings) = if let Some(result) =
306+
maybe_result
307+
{
306308
// Result type: generate two callbacks (on_success and on_error)
307309

308310
// For the catch clause, we need the actual Swift wrapper type name (e.g., "ErrorType"),
@@ -372,15 +374,6 @@ fn gen_async_function_exposes_swift_to_rust(
372374
all_params.push(original_params.clone());
373375
}
374376

375-
let task_body = format!(
376-
r#"do {{
377-
{result_binding}try await {call_expression}
378-
{on_success_call}
379-
}} catch let error as {err_swift_ty} {{
380-
onError(callbackWrapper, {err_ffi_convert})
381-
}}"#
382-
);
383-
384377
// Generate a typed throw checker function that verifies at compile-time
385378
// that the Swift function only throws the expected error type.
386379
// This uses Swift's typed throws feature (Swift 5.9+).
@@ -396,7 +389,26 @@ func {prefixed_fn_name}__TypedThrowsCheck({checker_params}) async throws({err_sw
396389
}}"#
397390
);
398391

399-
(all_params.join(", "), task_body, Some(typed_throws_check))
392+
// Pre-task wrapper for Swift 6 sendability - wrap callbacks in UncheckedSendable
393+
let pre_task_bindings =
394+
"let __callbacks = __private__UncheckedSendable((callbackWrapper, onSuccess, onError))";
395+
// Destructure at start of Task body
396+
let task_body = format!(
397+
r#"let (callbackWrapper, onSuccess, onError) = __callbacks.value
398+
do {{
399+
{result_binding}try await {call_expression}
400+
{on_success_call}
401+
}} catch let error as {err_swift_ty} {{
402+
onError(callbackWrapper, {err_ffi_convert})
403+
}}"#
404+
);
405+
406+
(
407+
all_params.join(", "),
408+
task_body,
409+
Some(typed_throws_check),
410+
pre_task_bindings.to_string(),
411+
)
400412
} else {
401413
// Non-Result type: single callback
402414
let return_ty_ref = return_ty.as_ref();
@@ -445,16 +457,61 @@ func {prefixed_fn_name}__TypedThrowsCheck({checker_params}) async throws({err_sw
445457
"let _ = "
446458
};
447459

448-
let task_body = format!("{result_binding}await {call_expression}\n {callback_call}");
460+
// Pre-task wrapper for Swift 6 sendability
461+
let pre_task_bindings =
462+
"let __callbacks = __private__UncheckedSendable((callbackWrapper, callback))";
463+
// Destructure at start of Task body
464+
let task_body = format!("let (callbackWrapper, callback) = __callbacks.value\n {result_binding}await {call_expression}\n {callback_call}");
449465

450-
(all_params.join(", "), task_body, None)
466+
(
467+
all_params.join(", "),
468+
task_body,
469+
None,
470+
pre_task_bindings.to_string(),
471+
)
451472
};
452473

453474
let maybe_typed_throws_check = typed_throws_check.unwrap_or_default();
454475

476+
// Check if this is a method (has associated_type) - if so, we need to include 'this' in the wrapper
477+
let (pre_task_bindings, task_body) = if func.associated_type.is_some() {
478+
// For methods, we need to wrap 'this' as well
479+
let is_result = maybe_result.is_some();
480+
if is_result {
481+
// Result case: wrap callbackWrapper, onSuccess, onError, this
482+
let wrapper = "let __captures = __private__UncheckedSendable((callbackWrapper, onSuccess, onError, this))";
483+
let destructure =
484+
"let (callbackWrapper, onSuccess, onError, this) = __captures.value\n ";
485+
(
486+
wrapper.to_string(),
487+
destructure.to_string()
488+
+ &task_body.trim_start().replace(
489+
"let (callbackWrapper, onSuccess, onError) = __callbacks.value\n ",
490+
"",
491+
),
492+
)
493+
} else {
494+
// Non-result case: wrap callbackWrapper, callback, this
495+
let wrapper =
496+
"let __captures = __private__UncheckedSendable((callbackWrapper, callback, this))";
497+
let destructure = "let (callbackWrapper, callback, this) = __captures.value\n ";
498+
(
499+
wrapper.to_string(),
500+
destructure.to_string()
501+
+ &task_body.trim_start().replace(
502+
"let (callbackWrapper, callback) = __callbacks.value\n ",
503+
"",
504+
),
505+
)
506+
}
507+
} else {
508+
(pre_task_bindings.clone(), task_body.clone())
509+
};
510+
455511
format!(
456512
r#"@_cdecl("{link_name}")
457513
func {prefixed_fn_name} ({params_str}) {{
514+
{pre_task_bindings}
458515
Task {{
459516
{task_body}
460517
}}

test-swift-rust-integration.sh

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,15 @@ touch ./Generated/SwiftBridgeCore.{h,swift}
2121
mkdir -p ./Generated/swift-integration-tests
2222
touch ./Generated/swift-integration-tests/swift-integration-tests.{h,swift}
2323

24+
# Build settings override (e.g., SWIFT_VERSION=6.0)
25+
EXTRA_BUILD_SETTINGS=""
26+
if [ -n "$SWIFT_VERSION" ]; then
27+
EXTRA_BUILD_SETTINGS="SWIFT_VERSION=$SWIFT_VERSION"
28+
echo "Using Swift version: $SWIFT_VERSION"
29+
fi
30+
2431
xcodebuild \
2532
-project SwiftRustIntegrationTestRunner.xcodeproj \
2633
-scheme SwiftRustIntegrationTestRunner \
27-
clean test
34+
clean test \
35+
$EXTRA_BUILD_SETTINGS

0 commit comments

Comments
 (0)