chinedufn / swift-bridge

swift-bridge facilitates Rust and Swift interop.
https://chinedufn.github.io/swift-bridge
Apache License 2.0
842 stars 62 forks source link

Unable to bridge function that receives &str and returns result #265

Open sax opened 7 months ago

sax commented 7 months ago

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!

chinedufn commented 7 months ago

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.

  1. 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

  2. Add a func toRustStrThrows<T> (_ withUnsafeRustStr: (RustStr) throws -> T) to the ToRustStr protocol

  3. 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

  4. Add an implementation for rust_fn_accepts_ref_str_returns_result

    fn 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())
        }
    }
  5. Add an integration test to StringTests.swift that calls rust_fn_accepts_ref_str_returns_result with "should_succeed" and confirms that it does not throw, then calls it with "fail" and confirms that it throws

  6. Add a codegen test to string_codegen_tests.rs with a function that accepts a string arg and returns a result. For example, fn some_function (arg: &str) -> Result<RustType, RustType>.

  7. Get tests passing