Skip to content

Commit b80238f

Browse files
authored
Support Option<String> fields in shared structs (#354)
Adds support for Option<String> fields in shared structs. ## Example ```rust // rust #[swift_bridge::bridge] mod ffi { #[swift_bridge(swift_repr = "struct")] struct UserProfile { name: String, bio: Option<String>, // Now supported! } extern "Rust" { fn create_profile(name: String, bio: Option<String>) -> UserProfile; } } ``` ```swift // swift // Create with Some value let profileWithBio = UserProfile( name: "Alice".intoRustString(), bio: "Hello world!".intoRustString() ) // Create with None value let profileWithoutBio = UserProfile( name: "Bob".intoRustString(), bio: nil ) // Access optional field if let bio = profileWithBio.bio { print("Bio: \(bio.toString())") } ```
1 parent 69bda0e commit b80238f

4 files changed

Lines changed: 122 additions & 13 deletions

File tree

SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/OptionTests.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ class OptionTests: XCTestCase {
157157
u8: 123, i8: 123, u16: 123, i16: 123,
158158
u32: 123, i32: 123, u64: 123, i64: 123,
159159
usize: 123, isize: 123, f32: 123.4, f64: 123.4,
160-
boolean: true
160+
boolean: true, string: "hello".intoRustString()
161161
)
162162
let reflected = rust_reflect_struct_with_option_fields(val)
163163
XCTAssertEqual(reflected.u8, 123)
@@ -173,14 +173,15 @@ class OptionTests: XCTestCase {
173173
XCTAssertEqual(reflected.f32, 123.4)
174174
XCTAssertEqual(reflected.f64, 123.4)
175175
XCTAssertEqual(reflected.boolean, true)
176+
XCTAssertEqual(reflected.string!.toString(), "hello")
176177
}
177178

178179
func testStructWithOptionFieldsNone() {
179180
let val = StructWithOptionFields(
180181
u8: nil, i8: nil, u16: nil, i16: nil,
181182
u32: nil, i32: nil, u64: nil, i64: nil,
182183
usize: nil, isize: nil, f32: nil, f64: nil,
183-
boolean: nil
184+
boolean: nil, string: nil
184185
)
185186
let reflected = rust_reflect_struct_with_option_fields(val)
186187
XCTAssertEqual(reflected.i8, nil)
@@ -195,6 +196,7 @@ class OptionTests: XCTestCase {
195196
XCTAssertEqual(reflected.f32, nil)
196197
XCTAssertEqual(reflected.f64, nil)
197198
XCTAssertEqual(reflected.boolean, nil)
199+
XCTAssertNil(reflected.string)
198200
}
199201

200202
func testEnumWhereVariantsHaveNoData() {

crates/swift-bridge-ir/src/bridged_type/bridgeable_string.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ impl BridgeableType for BridgedString {
2727
}
2828

2929
fn as_option(&self) -> Option<&super::bridged_option::BridgedOption> {
30-
todo!()
30+
None
3131
}
3232

3333
fn is_passed_via_pointer(&self) -> bool {
@@ -123,9 +123,7 @@ impl BridgeableType for BridgedString {
123123
"UnsafeMutableRawPointer?".to_string()
124124
}
125125
}
126-
TypePosition::SharedStructField => {
127-
todo!()
128-
}
126+
TypePosition::SharedStructField => "UnsafeMutableRawPointer?".to_string(),
129127
TypePosition::ResultFfiReturnType => {
130128
todo!()
131129
}
@@ -200,7 +198,10 @@ impl BridgeableType for BridgedString {
200198
}
201199
}
202200
TypePosition::SharedStructField => {
203-
todo!("Option<String> fields in structs are not yet supported.")
201+
format!(
202+
"{{ if let rustString = optionalStringIntoRustString({expression}) {{ rustString.isOwned = false; return rustString.ptr }} else {{ return nil }} }}()",
203+
expression = expression
204+
)
204205
}
205206
TypePosition::ResultFfiReturnType => {
206207
unimplemented!()
@@ -290,10 +291,7 @@ impl BridgeableType for BridgedString {
290291
rust: quote! {
291292
std::ptr::null::<#swift_bridge_path::string::RustString>() as *mut #swift_bridge_path::string::RustString
292293
},
293-
// TODO: Add integration tests:
294-
// Rust: crates/swift-integration-tests/src/option.rs
295-
// Swift: OptionTests.swift
296-
swift: "TODO_SWIFT_OPTIONAL_STRING_SUPPORT".to_string(),
294+
swift: "nil".to_string(),
297295
}
298296
}
299297

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

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,3 +1475,112 @@ typedef struct __swift_bridge__$SomeStruct { struct __private__OptionU8 field; }
14751475
.test();
14761476
}
14771477
}
1478+
1479+
/// Test conversion to and from the FFI representation of a struct that contains Option<String> fields.
1480+
mod shared_struct_with_option_string_field_ffi_repr {
1481+
use super::*;
1482+
1483+
fn bridge_module_tokens() -> TokenStream {
1484+
quote! {
1485+
mod ffi {
1486+
struct SomeStruct {
1487+
field: Option<String>
1488+
}
1489+
}
1490+
}
1491+
}
1492+
1493+
fn expected_rust_tokens() -> ExpectedRustTokens {
1494+
ExpectedRustTokens::ContainsMany(vec![
1495+
quote! {
1496+
pub struct __swift_bridge__SomeStruct {
1497+
field: *mut swift_bridge::string::RustString
1498+
}
1499+
},
1500+
quote! {
1501+
impl SomeStruct {
1502+
#[doc(hidden)]
1503+
#[inline(always)]
1504+
pub fn into_ffi_repr(self) -> __swift_bridge__SomeStruct {
1505+
{
1506+
let val = self;
1507+
__swift_bridge__SomeStruct {
1508+
field: if let Some(val) = val.field {
1509+
swift_bridge::string::RustString(val).box_into_raw()
1510+
} else {
1511+
std::ptr::null::<swift_bridge::string::RustString>() as *mut swift_bridge::string::RustString
1512+
}
1513+
}
1514+
}
1515+
}
1516+
}
1517+
},
1518+
quote! {
1519+
impl __swift_bridge__SomeStruct {
1520+
#[doc(hidden)]
1521+
#[inline(always)]
1522+
pub fn into_rust_repr(self) -> SomeStruct {
1523+
{
1524+
let val = self;
1525+
SomeStruct {
1526+
field: {
1527+
let val = val.field;
1528+
1529+
if val.is_null() {
1530+
None
1531+
} else {
1532+
Some( unsafe { Box::from_raw(val).0 } )
1533+
}
1534+
}
1535+
}
1536+
}
1537+
}
1538+
}
1539+
},
1540+
])
1541+
}
1542+
1543+
fn expected_swift_code() -> ExpectedSwiftCode {
1544+
ExpectedSwiftCode::ContainsAfterTrim(
1545+
r#"
1546+
public struct SomeStruct {
1547+
public var field: Optional<RustString>
1548+
1549+
public init(field: Optional<RustString>) {
1550+
self.field = field
1551+
}
1552+
1553+
@inline(__always)
1554+
func intoFfiRepr() -> __swift_bridge__$SomeStruct {
1555+
{ let val = self; return __swift_bridge__$SomeStruct(field: { if let rustString = optionalStringIntoRustString(val.field) { rustString.isOwned = false; return rustString.ptr } else { return nil } }()); }()
1556+
}
1557+
}
1558+
extension __swift_bridge__$SomeStruct {
1559+
@inline(__always)
1560+
func intoSwiftRepr() -> SomeStruct {
1561+
{ let val = self; return SomeStruct(field: { let val = val.field; if val != nil { return RustString(ptr: val!) } else { return nil } }()); }()
1562+
}
1563+
}
1564+
"#,
1565+
)
1566+
}
1567+
1568+
fn expected_c_header() -> ExpectedCHeader {
1569+
ExpectedCHeader::ContainsAfterTrim(
1570+
r#"
1571+
typedef struct __swift_bridge__$SomeStruct { void* field; } __swift_bridge__$SomeStruct;
1572+
"#,
1573+
)
1574+
}
1575+
1576+
#[test]
1577+
fn shared_struct_with_option_string_field_ffi_repr() {
1578+
CodegenTest {
1579+
bridge_module: bridge_module_tokens().into(),
1580+
expected_rust_tokens: expected_rust_tokens(),
1581+
expected_swift_code: expected_swift_code(),
1582+
expected_c_header: expected_c_header(),
1583+
}
1584+
.test();
1585+
}
1586+
}

crates/swift-integration-tests/src/option.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ mod ffi {
1919
f32: Option<f32>,
2020
f64: Option<f64>,
2121
boolean: Option<bool>,
22-
// TODO: Support test more types:
23-
// string: Option<String>,
22+
string: Option<String>,
23+
// TODO: Support more types:
2424
// str: Option<&'static str>,
2525
}
2626

0 commit comments

Comments
 (0)