-
Notifications
You must be signed in to change notification settings - Fork 65
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
Support returning Result<OpaqueRust, OpaqueRust>
from async function
#158
Conversation
There was a problem hiding this 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 |
There was a problem hiding this comment.
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>;
}
}
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'
There was a problem hiding this comment.
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>. |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) | ||
} |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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>) -> ()) { |
There was a problem hiding this comment.
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 Error
s be SomeType2
?
types, | ||
), | ||
) | ||
if func_ret_ty.is_result() { |
There was a problem hiding this comment.
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:
swift-bridge/crates/swift-bridge-ir/src/bridged_type/bridgeable_result.rs
Lines 108 to 119 in 2ab37cb
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 { |
There was a problem hiding this comment.
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?
Just wanted to say again that this is a really great PR. Thanks for implementing this!! |
Addressed your review! Please let me know if there are any omissions. |
There was a problem hiding this 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() |
There was a problem hiding this comment.
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!( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
This PR addresses #157 and adds support for returning a
Result<OpaqueRust, OpaqueRust>
from async functions.Here's an example of using this.