diff --git a/SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/OptionTests.swift b/SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/OptionTests.swift index 7a64f602..34cf859a 100644 --- a/SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/OptionTests.swift +++ b/SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/OptionTests.swift @@ -220,4 +220,11 @@ class OptionTests: XCTestCase { XCTAssertEqual(reflectedSome!.field, 123) XCTAssertNil(reflectedNone) } + + /// Verify that we can use failable initializers defined on the Rust side. + func testFailableInitializer() { + XCTAssertEqual(FailableInitType(false), nil) + let failableInitType = FailableInitType(true) + XCTAssertEqual(failableInitType!.count(), 132) + } } diff --git a/crates/swift-bridge-ir/src/bridged_type.rs b/crates/swift-bridge-ir/src/bridged_type.rs index 3e58309b..6b423f85 100644 --- a/crates/swift-bridge-ir/src/bridged_type.rs +++ b/crates/swift-bridge-ir/src/bridged_type.rs @@ -84,6 +84,8 @@ pub(crate) trait BridgeableType: Debug { fn as_result(&self) -> Option<&BuiltInResult>; + fn as_option(&self) -> Option<&BridgedOption>; + /// True if the type's FFI representation is a pointer fn is_passed_via_pointer(&self) -> bool; @@ -494,6 +496,13 @@ impl BridgeableType for BridgedType { } } + fn as_option(&self) -> Option<&BridgedOption> { + match self { + BridgedType::StdLib(StdLibType::Option(ty)) => Some(ty), + _ => None, + } + } + fn is_passed_via_pointer(&self) -> bool { match self { BridgedType::StdLib(StdLibType::Vec(_)) => true, diff --git a/crates/swift-bridge-ir/src/bridged_type/bridgeable_pointer.rs b/crates/swift-bridge-ir/src/bridged_type/bridgeable_pointer.rs index a8e17cbb..4c90cb9c 100644 --- a/crates/swift-bridge-ir/src/bridged_type/bridgeable_pointer.rs +++ b/crates/swift-bridge-ir/src/bridged_type/bridgeable_pointer.rs @@ -41,6 +41,10 @@ impl BridgeableType for BuiltInPointer { todo!() } + fn as_option(&self) -> Option<&super::bridged_option::BridgedOption> { + todo!(); + } + fn is_passed_via_pointer(&self) -> bool { todo!() } diff --git a/crates/swift-bridge-ir/src/bridged_type/bridgeable_string.rs b/crates/swift-bridge-ir/src/bridged_type/bridgeable_string.rs index f151115b..b5bca782 100644 --- a/crates/swift-bridge-ir/src/bridged_type/bridgeable_string.rs +++ b/crates/swift-bridge-ir/src/bridged_type/bridgeable_string.rs @@ -26,6 +26,10 @@ impl BridgeableType for BridgedString { None } + fn as_option(&self) -> Option<&super::bridged_option::BridgedOption> { + todo!() + } + fn is_passed_via_pointer(&self) -> bool { true } diff --git a/crates/swift-bridge-ir/src/bridged_type/bridged_opaque_type.rs b/crates/swift-bridge-ir/src/bridged_type/bridged_opaque_type.rs index ea979fa5..053fe6fe 100644 --- a/crates/swift-bridge-ir/src/bridged_type/bridged_opaque_type.rs +++ b/crates/swift-bridge-ir/src/bridged_type/bridged_opaque_type.rs @@ -36,6 +36,10 @@ impl BridgeableType for OpaqueForeignType { None } + fn as_option(&self) -> Option<&super::bridged_option::BridgedOption> { + None + } + fn is_passed_via_pointer(&self) -> bool { true } diff --git a/crates/swift-bridge-ir/src/bridged_type/built_in_tuple.rs b/crates/swift-bridge-ir/src/bridged_type/built_in_tuple.rs index 301049a7..bc28dbff 100644 --- a/crates/swift-bridge-ir/src/bridged_type/built_in_tuple.rs +++ b/crates/swift-bridge-ir/src/bridged_type/built_in_tuple.rs @@ -65,6 +65,10 @@ impl BridgeableType for BuiltInTuple { todo!(); } + fn as_option(&self) -> Option<&super::bridged_option::BridgedOption> { + todo!() + } + fn is_passed_via_pointer(&self) -> bool { todo!(); } diff --git a/crates/swift-bridge-ir/src/codegen/codegen_tests/opaque_rust_type_codegen_tests.rs b/crates/swift-bridge-ir/src/codegen/codegen_tests/opaque_rust_type_codegen_tests.rs index fcb4420c..2d2a6f03 100644 --- a/crates/swift-bridge-ir/src/codegen/codegen_tests/opaque_rust_type_codegen_tests.rs +++ b/crates/swift-bridge-ir/src/codegen/codegen_tests/opaque_rust_type_codegen_tests.rs @@ -456,3 +456,74 @@ typedef struct MyType MyType; .test(); } } + +/// Verify that we generated a Swift class with a failable init method. +mod extern_rust_class_with_failable_init { + use super::*; + + fn bridge_module_tokens() -> TokenStream { + quote! { + mod foo { + extern "Rust" { + type Foo; + + #[swift_bridge(init)] + fn new() -> Option; + } + } + } + } + + fn expected_rust_tokens() -> ExpectedRustTokens { + ExpectedRustTokens::Contains(quote! { + # [export_name = "__swift_bridge__$Foo$new"] + pub extern "C" fn __swift_bridge__Foo_new () -> * mut super :: Foo { + if let Some (val) = super :: Foo :: new () { + Box :: into_raw (Box :: new (val)) + } else { + std :: ptr :: null_mut () + } + } + }) + } + + const EXPECTED_SWIFT: ExpectedSwiftCode = ExpectedSwiftCode::ContainsAfterTrim( + r#" +public class Foo: FooRefMut { + var isOwned: Bool = true + + public override init(ptr: UnsafeMutableRawPointer) { + super.init(ptr: ptr) + } + + deinit { + if isOwned { + __swift_bridge__$Foo$_free(ptr) + } + } +} +extension Foo { + public convenience init?() { + guard let val = __swift_bridge__$Foo$new() else { return nil }; self.init(ptr: val) + } +} +"#, + ); + + const EXPECTED_C_HEADER: ExpectedCHeader = ExpectedCHeader::ContainsAfterTrim( + r#" +void* __swift_bridge__$Foo$new(void); +"#, + ); + + #[test] + fn extern_rust_class_with_failable_init() { + CodegenTest { + bridge_module: bridge_module_tokens().into(), + expected_rust_tokens: expected_rust_tokens(), + expected_swift_code: EXPECTED_SWIFT, + expected_c_header: EXPECTED_C_HEADER, + } + .test(); + } +} diff --git a/crates/swift-bridge-ir/src/codegen/generate_swift/generate_function_swift_calls_rust.rs b/crates/swift-bridge-ir/src/codegen/generate_swift/generate_function_swift_calls_rust.rs index fe698efd..a52a9933 100644 --- a/crates/swift-bridge-ir/src/codegen/generate_swift/generate_function_swift_calls_rust.rs +++ b/crates/swift-bridge-ir/src/codegen/generate_swift/generate_function_swift_calls_rust.rs @@ -55,7 +55,11 @@ pub(super) fn gen_func_swift_calls_rust( if function.is_copy_method_on_opaque_type() { "public init".to_string() } else { - "public convenience init".to_string() + if function.is_swift_failable_initializer { + "public convenience init?".to_string() + } else { + "public convenience init".to_string() + } } } else { if let Some(swift_name) = &function.swift_name_override { @@ -179,7 +183,14 @@ pub(super) fn gen_func_swift_calls_rust( if function.is_copy_method_on_opaque_type() { call_rust = format!("self.bytes = {}", call_rust) } else { - call_rust = format!("self.init(ptr: {})", call_rust) + if function.is_swift_failable_initializer { + call_rust = format!( + "guard let val = {} else {{ return nil }}; self.init(ptr: val)", + call_rust + ) + } else { + call_rust = format!("self.init(ptr: {})", call_rust) + } } } @@ -316,5 +327,6 @@ return{maybe_try}await {with_checked_continuation_function_name}({{ (continuatio call_rust = call_rust, ) }; + func_definition } diff --git a/crates/swift-bridge-ir/src/codegen/generate_swift/swift_class.rs b/crates/swift-bridge-ir/src/codegen/generate_swift/swift_class.rs index 3f083fbb..b2a30d51 100644 --- a/crates/swift-bridge-ir/src/codegen/generate_swift/swift_class.rs +++ b/crates/swift-bridge-ir/src/codegen/generate_swift/swift_class.rs @@ -233,6 +233,7 @@ extension {ty_name}Ref: Hashable{{ "".to_string() } }; + let class = format!( r#" {class_decl}{initializers}{owned_instance_methods}{class_ref_decl}{ref_mut_instance_methods}{class_ref_mut_decl}{ref_instance_methods}{generic_freer}{equatable_method}{hashable_method}"#, diff --git a/crates/swift-bridge-ir/src/parse/parse_extern_mod.rs b/crates/swift-bridge-ir/src/parse/parse_extern_mod.rs index 6e7c8148..b15c1cfd 100644 --- a/crates/swift-bridge-ir/src/parse/parse_extern_mod.rs +++ b/crates/swift-bridge-ir/src/parse/parse_extern_mod.rs @@ -111,14 +111,19 @@ impl<'a> ForeignModParser<'a> { } let return_type = &func.sig.output; + let mut is_swift_failable_initializer = false; if let ReturnType::Type(_, return_ty) = return_type { - if BridgedType::new_with_type(return_ty.deref(), &self.type_declarations) - .is_none() - { + let bridged_return_type = + BridgedType::new_with_type(return_ty.deref(), &self.type_declarations); + if let Some(ty) = &bridged_return_type { + if ty.as_option().is_some() && attributes.is_swift_initializer { + is_swift_failable_initializer = true; + } + } + if bridged_return_type.is_none() { self.unresolved_types.push(return_ty.deref().clone()); } } - let first_input = func.sig.inputs.iter().next(); let associated_type = self.get_associated_type( @@ -126,6 +131,7 @@ impl<'a> ForeignModParser<'a> { func.clone(), &attributes, &mut local_type_declarations, + is_swift_failable_initializer, )?; if attributes.is_swift_identifiable { @@ -225,10 +231,12 @@ impl<'a> ForeignModParser<'a> { } } } + let func = ParsedExternFn { func, associated_type, is_swift_initializer: attributes.is_swift_initializer, + is_swift_failable_initializer: is_swift_failable_initializer, is_swift_identifiable: attributes.is_swift_identifiable, host_lang, rust_name_override: attributes.rust_name, @@ -294,6 +302,7 @@ impl<'a> ForeignModParser<'a> { func: ForeignItemFn, attributes: &FunctionAttributes, local_type_declarations: &mut HashMap, + is_swift_failable_initializer: bool, ) -> syn::Result> { let associated_type = match first { Some(FnArg::Receiver(recv)) => { @@ -337,6 +346,7 @@ impl<'a> ForeignModParser<'a> { func.clone(), attributes, local_type_declarations, + is_swift_failable_initializer, )?; associated_type } @@ -373,10 +383,19 @@ Otherwise we use a more general error that says that your argument is invalid. ty_string } }; + if is_swift_failable_initializer { + // Safety: since we've already checked ty_string is formatted as "Option<~>" before calling this function. + let last_bracket = ty_string.rfind(">").unwrap(); + + let inner = &ty_string[0..last_bracket]; + let inner = inner.trim_start_matches("Option < ").trim_end_matches(" "); + let ty = self.type_declarations.get(inner); + ty.map(|ty| ty.clone()) + } else { + let ty = self.type_declarations.get(&ty_string); - let ty = self.type_declarations.get(&ty_string); - - ty.map(|ty| ty.clone()) + ty.map(|ty| ty.clone()) + } } else { None }; diff --git a/crates/swift-bridge-ir/src/parse/parse_extern_mod/function_attributes.rs b/crates/swift-bridge-ir/src/parse/parse_extern_mod/function_attributes.rs index 6d40e6d3..16b85b1f 100644 --- a/crates/swift-bridge-ir/src/parse/parse_extern_mod/function_attributes.rs +++ b/crates/swift-bridge-ir/src/parse/parse_extern_mod/function_attributes.rs @@ -277,6 +277,27 @@ mod tests { assert!(func.is_swift_initializer); } + /// Verify that we can parse an failable init function. + #[test] + fn failable_initializer() { + let tokens = quote! { + mod foo { + extern "Rust" { + type Foo; + + #[swift_bridge(init)] + fn bar () -> Option; + } + } + }; + + let module = parse_ok(tokens); + + let func = &module.functions[0]; + assert!(func.is_swift_initializer); + assert!(func.is_swift_failable_initializer); + } + /// Verify that we can parse an init function that takes inputs. #[test] fn initializer_with_inputs() { diff --git a/crates/swift-bridge-ir/src/parsed_extern_fn.rs b/crates/swift-bridge-ir/src/parsed_extern_fn.rs index f01c0d44..2faa67b5 100644 --- a/crates/swift-bridge-ir/src/parsed_extern_fn.rs +++ b/crates/swift-bridge-ir/src/parsed_extern_fn.rs @@ -59,6 +59,10 @@ pub(crate) struct ParsedExternFn { pub host_lang: HostLang, /// Whether or not this function is a Swift initializer. pub is_swift_initializer: bool, + /// Whether or not this function is a Swift failable initializer. + /// For more details, see: + /// [Swift Documentation - Failable Initializers](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/initialization/#Failable-Initializers) + pub is_swift_failable_initializer: bool, /// Whether or not this function should be used for the associated type's Swift /// `Identifiable` protocol implementation. pub is_swift_identifiable: bool, diff --git a/crates/swift-integration-tests/src/option.rs b/crates/swift-integration-tests/src/option.rs index 8a8bd4c4..8ad058b5 100644 --- a/crates/swift-integration-tests/src/option.rs +++ b/crates/swift-integration-tests/src/option.rs @@ -148,6 +148,15 @@ mod ffi { fn swift_arg_option_str(arg: Option<&str>) -> bool; // fn swift_reflect_option_str(arg: Option<&str>) -> Option<&str>; } + + extern "Rust" { + #[swift_bridge(Equatable)] + type FailableInitType; + + #[swift_bridge(init)] + fn new(success: bool) -> Option; + fn count(&self) -> i32; + } } fn test_rust_calls_swift_option_primitive() { @@ -340,3 +349,20 @@ fn rust_reflect_option_struct_with_no_data( ) -> Option { arg } + +#[derive(PartialEq)] +struct FailableInitType; + +impl FailableInitType { + fn new(success: bool) -> Option { + if success { + Some(FailableInitType) + } else { + None + } + } + + fn count(&self) -> i32 { + 132 + } +}