Unable to bridge function that receives &str and returns result
When given the following bridge definition:
#[swift_bridge::bridge]
mod ffi {
extern "Rust" {
type RustApp;
#[swift_bridge(init)]
fn new() -> RustApp;
#[swift_bridge(swift_name = "resultTest")]
fn result_test(&self, receives: &str) -> Result<(), String>;
}
}
The generated swift code produces a compiler error:
Invalid conversion from throwing function of type '(RustStr) throws -> ()' to non-throwing function type '(RustStr) -> ()'
extension RustAppRef {
public func resultTest<GenericToRustStr: ToRustStr>(_ receives: GenericToRustStr) throws {
return receives.toRustStr({ receivesAsRustStr in // <-------------- compiler error here
try {
let val = __swift_bridge__$RustApp$resultTest(ptr, receivesAsRustStr)
if val != nil { throw RustString(ptr: val!) } else { return }
}()
})
}
}
I have included the following in my project:
extension RustString: @unchecked Sendable {}
extension RustString: Error {}
extension RustStr: @unchecked Sendable {}
extension RustStr: Error {}
If instead my bridge function is defined as follows, then the project compiles fine:
#[swift_bridge(swift_name = "resultTest")]
fn result_test(&self, receives: String) -> Result<(), String>;
That generates the following Swift:
public func resultTest<GenericIntoRustString: IntoRustString>(_ receives: GenericIntoRustString) throws {
try {
let val = __swift_bridge__$RustApp$result_test(
ptr,
{
let rustString = receives.intoRustString()
rustString.isOwned = false
return rustString.ptr
}())
if val != nil { throw RustString(ptr: val!) } else { return }
}()
}
I'm not sure if this is expected behavior or not. So far I've been able to send Swift String values into Rust as &str fine for any function that does not return a result.
For my project, it's fine for it to use owned strings, but I did lose a bit of time before I tried switching the inputs. Even if there is no resulting change to swift-bridge, though, maybe at least having it documented in an issue will save others some heartache!
Thanks for taking the time to make a detailed and easy to follow issue.
Glad you have a workaround. I'll explain the problem and solution in case someone needs the signature to work in the future.
Looks like the problem is that we're using ToRustStr which takes a closure of type (RustStr) -> T.
public protocol ToRustStr {
func toRustStr<T> (_ withUnsafeRustStr: (RustStr) -> T) -> T;
}
https://github.com/chinedufn/swift-bridge/blob/7fc3d3ccca618f3f231fc6f8a7678ad119ca99b0/crates/swift-bridge-build/src/generate_core/string.swift#L71-L82
But the codegen is using a closure of that can throw, meaning it has type (RustStr) throws -> T.
// The closure that is being passed into `toRustStr` can throw an exception,
// but `toRustStr` is defined to take a closure that does not throw an exception.
return receives.toRustStr({ receivesAsRustStr in
try {
let val = __swift_bridge__$RustApp$resultTest(ptr, receivesAsRustStr)
if val != nil { throw RustString(ptr: val!) } else { return }
}()
})
Potential Solution
Here's a potential solution in case someone in the future needs this signature to work.
It boils down to making a toRustStrThrows function and calling that instead of toRustStr.
-
Review the documentation for supporting a new signature https://github.com/chinedufn/swift-bridge/blob/master/book/src/contributing/adding-support-for-a-signature/README.md
-
Add a
func toRustStrThrows<T> (_ withUnsafeRustStr: (RustStr) throws -> T)to theToRustStrprotocol -
Add a
fn rust_fn_accepts_ref_str_returns_result(str: &str) -> Result<String, String>. https://github.com/chinedufn/swift-bridge/blob/27246447b4ac741b8bf086d3dfca15b47558e44f/crates/swift-integration-tests/src/string.rs#L6 -
Add an implementation for
rust_fn_accepts_ref_str_returns_resultfn rust_fn_accepts_ref_str_returns_result(str: &str) -> Result<String, String> { if str == "should succeed" { return Ok("ok".to_string()) } else { return Err("error".to_string()) } } -
Add an integration test to
StringTests.swiftthat callsrust_fn_accepts_ref_str_returns_resultwith "should_succeed" and confirms that it does not throw, then calls it with "fail" and confirms that it throws
- Tests to use as inspiration:
- Calling
rust_fn_accepts_ref_str_returns_resulthttps://github.com/chinedufn/swift-bridge/blob/27246447b4ac741b8bf086d3dfca15b47558e44f/SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/StringTests.swift#L24-L30 - Testing that a function throws: https://github.com/chinedufn/swift-bridge/blob/de059024732fa2f8135a9fe1232b727d96794af6/SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/ResultTests.swift#L56-L66
- Testing that a functiond does not throw: https://github.com/chinedufn/swift-bridge/blob/de059024732fa2f8135a9fe1232b727d96794af6/SwiftRustIntegrationTestRunner/SwiftRustIntegrationTestRunnerTests/ResultTests.swift#L68-L77
- Calling
- Add a codegen test to
string_codegen_tests.rswith a function that accepts a string arg and returns a result. For example,fn some_function (arg: &str) -> Result<RustType, RustType>.
- The generated Swift code should call
toRustStrThrowsinstead oftoRustStr - inspiration:
- https://github.com/chinedufn/swift-bridge/blob/58f4a40f96bb050607c746376422ab3c62e0e771/crates/swift-bridge-ir/src/codegen/codegen_tests/string_codegen_tests.rs#L60-L115
- https://github.com/chinedufn/swift-bridge/blob/852ece728d3f362f3ada4368e037fe9ebb2a913f/crates/swift-bridge-ir/src/codegen/codegen_tests/result_codegen_tests.rs#L129-L200
- Get tests passing
I also ran into this. The change from having a &str to String on the arguments of the function that returns the Result<> did the trick for me.