Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
sax opened this issue Apr 2, 2024 · 1 comment
Open

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

sax opened this issue Apr 2, 2024 · 1 comment
Labels
good first issue Good for newcomers

Comments

@sax
Copy link

sax commented Apr 2, 2024

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
Copy link
Owner

chinedufn commented Apr 9, 2024

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;
}

/// Used to safely get a pointer to a sequence of utf8 bytes, represented as a `RustStr`.
///
/// For example, the Swift `String` implementation of the `ToRustStr` protocol does the following:
/// 1. Use Swift's `String.utf8.withUnsafeBufferPointer` to get a pointer to the strings underlying
/// utf8 bytes.
/// 2. Construct a `RustStr` that points to these utf8 bytes. This is safe because `withUnsafeBufferPointer`
/// guarantees that the buffer pointer will be valid for the duration of the `withUnsafeBufferPointer`
/// callback.
/// 3. Pass the `RustStr` to the closure that was passed into `RustStr.toRustStr`.
public protocol ToRustStr {
func toRustStr<T> (_ withUnsafeRustStr: (RustStr) -> T) -> T;
}

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

    fn create_string(str: &str) -> String;

  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

  1. 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>.
  • The generated Swift code should call toRustStrThrows instead of toRustStr
  • inspiration:
    • /// Test code generation for Rust function that takes an &str argument.
      mod extern_rust_fn_with_str_argument {
      use super::*;
      fn bridge_module_tokens() -> TokenStream {
      quote! {
      mod foo {
      extern "Rust" {
      fn some_function (arg: &str);
      }
      }
      }
      }
      fn expected_rust_tokens() -> ExpectedRustTokens {
      ExpectedRustTokens::Contains(quote! {
      #[export_name = "__swift_bridge__$some_function"]
      pub extern "C" fn __swift_bridge__some_function(
      arg: swift_bridge::string::RustStr
      ) {
      super::some_function(arg.to_str())
      }
      })
      }
      fn expected_swift_code() -> ExpectedSwiftCode {
      ExpectedSwiftCode::ContainsAfterTrim(
      r#"
      func some_function<GenericToRustStr: ToRustStr>(_ arg: GenericToRustStr) {
      arg.toRustStr({ argAsRustStr in
      __swift_bridge__$some_function(argAsRustStr)
      })
      }
      "#,
      )
      }
      fn expected_c_header() -> ExpectedCHeader {
      ExpectedCHeader::ExactAfterTrim(
      r#"
      void __swift_bridge__$some_function(struct RustStr arg);
      "#,
      )
      }
      #[test]
      fn extern_rust_fn_with_str_argument() {
      CodegenTest {
      bridge_module: bridge_module_tokens().into(),
      expected_rust_tokens: expected_rust_tokens(),
      expected_swift_code: expected_swift_code(),
      expected_c_header: expected_c_header(),
      }
      .test();
      }
      }
    • /// Test code generation for Rust function that accepts a Result<T, E> where T and E are
      /// opaque Rust types.
      mod extern_rust_fn_return_result_opaque_rust {
      use super::*;
      fn bridge_module_tokens() -> TokenStream {
      quote! {
      mod ffi {
      extern "Rust" {
      type SomeType;
      fn some_function () -> Result<SomeType, SomeType>;
      }
      }
      }
      }
      fn expected_rust_tokens() -> ExpectedRustTokens {
      ExpectedRustTokens::Contains(quote! {
      #[export_name = "__swift_bridge__$some_function"]
      pub extern "C" fn __swift_bridge__some_function() -> swift_bridge::result::ResultPtrAndPtr {
      match super::some_function() {
      Ok(ok) => {
      swift_bridge::result::ResultPtrAndPtr {
      is_ok: true,
      ok_or_err: Box::into_raw(Box::new({
      let val: super::SomeType = ok;
      val
      })) as *mut super::SomeType as *mut std::ffi::c_void
      }
      }
      Err(err) => {
      swift_bridge::result::ResultPtrAndPtr {
      is_ok: false,
      ok_or_err: Box::into_raw(Box::new({
      let val: super::SomeType = err;
      val
      })) as *mut super::SomeType as *mut std::ffi::c_void
      }
      }
      }
      }
      })
      }
      fn expected_swift_code() -> ExpectedSwiftCode {
      ExpectedSwiftCode::ContainsAfterTrim(
      r#"
      public func some_function() throws -> SomeType {
      try { let val = __swift_bridge__$some_function(); if val.is_ok { return SomeType(ptr: val.ok_or_err!) } else { throw SomeType(ptr: val.ok_or_err!) } }()
      }
      "#,
      )
      }
      const EXPECTED_C_HEADER: ExpectedCHeader = ExpectedCHeader::ContainsAfterTrim(
      r#"
      struct __private__ResultPtrAndPtr __swift_bridge__$some_function(void);
      "#,
      );
      #[test]
      fn extern_rust_fn_return_result_opaque_rust() {
      CodegenTest {
      bridge_module: bridge_module_tokens().into(),
      expected_rust_tokens: expected_rust_tokens(),
      expected_swift_code: expected_swift_code(),
      expected_c_header: EXPECTED_C_HEADER,
      }
      .test();
      }
      }
  1. Get tests passing

@chinedufn chinedufn added the good first issue Good for newcomers label Apr 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
good first issue Good for newcomers
Projects
None yet
Development

No branches or pull requests

2 participants