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

Support returning Result<OpaqueRust, OpaqueRust> from async function #158

Conversation

NiwakaDev
Copy link
Collaborator

This PR addresses #157 and adds support for returning a Result<OpaqueRust, OpaqueRust> from async functions.

Here's an example of using this.

//Rust
#[swift_bridge::bridge]
mod ffi {
    extern "Rust" {
        type OpaqueType1;
        type OpaqueType2;
        async fn some_function() -> Result<OpaqueType1, OpaqueType2>;
    }
}
//Swift
do {
    let opaqueType1 = try await some_function()
} catch {
    //...
}

Copy link
Owner

@chinedufn chinedufn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NiwakaDev this is truly outstanding work. AWESOME.

I left some minor feedback, but overall this is excellent. Really impressive stuff. Thanks a lot.

@@ -18,3 +18,19 @@ async fn some_async_function(list: Vec<u8>) -> ffi::MyStruct {
ffi::MyStruct
}
```

## Return `Result<OpaqueType, OpaqueType>` from async functions
Copy link
Owner

@chinedufn chinedufn Feb 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that we need this documentation since it doesn't really communicate anything other than "results can be used as return types", but we already have a chapter that describes all supported types.


But, it would be nice to highlight a Result in the example since async functions often return results.

Maybe just change the bridge module above to:

#[swift_bridge::bridge]
mod ffi {    
    extern "Rust" {
        type User;
        type ApiError;

        async fn user_count() -> u32;
        async fn load_user(url: &str) -> Result<User, ApiError>;
    }
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As well as a Swift block below it:

// Swift

let totalUsers = await user_count()

do {
    let user = try await load_user("https://example.com/users/5")
} catch let error as ApiError {
    // ... error handling ...
}

}
}

return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<SomeType1, Error>) in
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be SomeType2 instead of Error?

Copy link
Collaborator Author

@NiwakaDev NiwakaDev Feb 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to do this, but I got the following error.

Testing failed:
	Cannot convert value of type '(CheckedContinuation<AsyncResultOpaqueRustType1, AsyncResultOpaqueRustType2>) -> Void' to expected argument type '(CheckedContinuation<AsyncResultOpaqueRustType1, any Error>) -> Void'
	Testing cancelled because the build failed.

Copy link
Collaborator Author

@NiwakaDev NiwakaDev Feb 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the minimal code to reproduce this error.

struct ZeroError: Error{}

func hoge(value: Int) async throws -> Int {
    try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Int, ZeroError>) in
        if value == 0{
            continuation.resume(throwing: ZeroError())
        }else{
            continuation.resume(returning: value*2)
        }
    }
}

error message:

Cannot convert value of type '(CheckedContinuation<Int, ZeroError>) -> Void' to expected argument type '(CheckedContinuation<Int, any Error>) -> Void'

Copy link
Owner

@chinedufn chinedufn Feb 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the minimal example.

This compiles:

struct ZeroError: Error{}

func hoge(value: Int) async throws -> Int {
    try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Int, _>) in
        if value == 0{
            continuation.resume(throwing: ZeroError())
        }else{
            continuation.resume(returning: value*2)
        }
    }
}

Since ZeroError doesn't compile leaving it as Error is fine.

Would just leave a comment somewhere in the test.

Maybe:

TODO: Replace `Error` with the concrete error type `SomeType2`. 
 As of Feb 2023 using the concrete error type leads to a compile time error.
 This seems like a bug in the Swift compiler.

Doesn't really matter since it doesn't impact the user experience... I just don't want to look at this in a year and forget why we're using Error instead of the concrete error type.

@@ -54,6 +54,13 @@ pub(crate) trait BridgeableType: Debug {
/// Whether or not this is a `Result<T,E>`.
fn is_result(&self) -> bool;

/// Extract the Swift representation of each variant from Rust's Result<T,E>.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exact Swift results variant feels a bit too specific to belong in the BridgeableType trait.


Here's what I'm thinking instead:


We add an as_result method to the BridgeableType trait

trait BridgeableType {
    // ...

    fn is_result(&self) -> bool {
        self.as_result().is_some()
    }

    fn as_result(&self) -> Option<&BuiltInResult>;
}

Then we can use .as_result() to get back the BuiltInResult, and use a method(s) on the built in result to generate whatever information we need when generating the async support code on the Swift side.

do {
let result = try await rust_async_func_reflect_result_opaque_rust(.Ok(AsyncResultOpaqueRustType1(10)))
} catch {
XCTFail()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice idea with the XCTFail()

Copy link
Collaborator Author

@NiwakaDev NiwakaDev Feb 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about replacing fatalError() with XCTFail() in other tests?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea, please go for it!

XCTFail()
} catch let error as AsyncResultOpaqueRustType2 {
XCTAssertEqual(error.val(), 100)
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add another catch block that has XCTFail(), which proves that we're definitely hitting the catch let error as AsyncResultOpaqueRustType2 block?

#[swift_bridge::bridge]
mod ffi {
extern "Rust" {
type SomeType1;
Copy link
Owner

@chinedufn chinedufn Feb 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice idea having two different types.

What about naming them OkType and ErrorType?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

class CbWrapper$some_function {
var cb: (Result<SomeType1, Error>) -> ()

public init(cb: @escaping (Result<SomeType1, Error>) -> ()) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same idea here. Should all of these Errors be SomeType2?

types,
),
)
if func_ret_ty.is_result() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't be hard coding types.

Since we have to support a lot of types in a lot of different places, it gets really hard to maintain and refactor our code generation if types are hard coded everywhere.

Instead, the convert_ffi_value_to_swift_value should be tuned to give us the proper value here.

I think we can accomplish this by changing this:

match type_pos {
TypePosition::FnReturn(_) => self.ok_ty.to_swift_type(type_pos, types),
TypePosition::FnArg(_, _)
| TypePosition::SharedStructField
| TypePosition::SwiftCallsRustAsyncOnCompleteReturnTy => {
format!(
"RustResult<{}, {}>",
self.ok_ty.to_swift_type(type_pos, types),
self.err_ty.to_swift_type(type_pos, types),
)
}
}

to have the TypePosition::SwiftCallsRustAsyncOnCompleteReturnTy match block return __private__ResultPtrAndPtr.


This is better because it means that anything that calls to_swift_type will get the proper value given this TypePosition::SwiftCallsRustAsyncOnCompleteReturnTy.

@@ -533,3 +533,107 @@ void __swift_bridge__$SomeType$some_method(void* callback_wrapper, void __swift_
.test();
}
}

mod extern_rust_async_function_returns_result_opaque {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind adding a Verify that ... comment like the other bridge modules?

@chinedufn
Copy link
Owner

Just wanted to say again that this is a really great PR. Thanks for implementing this!!

@NiwakaDev
Copy link
Collaborator Author

Addressed your review! Please let me know if there are any omissions.

Copy link
Owner

@chinedufn chinedufn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minor bit of cleanup than looks good to me!

do {
let result = try await rust_async_func_reflect_result_opaque_rust(.Ok(AsyncResultOpaqueRustType1(10)))
} catch {
XCTFail()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea, please go for it!

let (maybe_on_complete_sig_ret_val, on_complete_ret_val) = if func_ret_ty.is_null() {
("".to_string(), "()".to_string())
} else {
(
format!(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can undo this change and go back to the original code.

The if and else blocks that were added are identical.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants