Skip to content

Commit

Permalink
Support returning Result<(), OpaqueRust> from Rust functions (#180)
Browse files Browse the repository at this point in the history
This commit adds support for returning `Result<(), OpaqueRust>` but not passing as an arg.

The change to `examples/rust-binary-calls-swift-package/build.rs` was to support xcode not existing in the default location (I use the `xcodes` tool).

This commit also supports empty types, such as `struct UnitStruct()`.

This enables the following:

```rust
#[swift_bridge::bridge]
mod ffi {
    struct UnitType;
    extern "Rust" {
        type OpaqueType;
        fn null_result() -> Result<(), OpaqueType>;
        fn unit_result() -> Result<UnitType, OpaqueType>;
    }
}

fn null_result() -> Result<(), OpaqueType> {
    Ok(())
}

fn unit_result() -> Result<UnitType, OpaqueType> {
    Ok(UnitType {})
}
```
  • Loading branch information
jfaust authored Feb 26, 2023
1 parent b13c56a commit 27d7be9
Show file tree
Hide file tree
Showing 13 changed files with 323 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,28 @@ class ResultTests: XCTestCase {
.Err(ResultTestOpaqueSwiftType(val: 666))
)
}

/// Verify that we can receive a Result<(), OpaqueRust> from Rust
func testSwiftCallRustResultNullOpaqueRust() throws {
try! rust_func_return_result_null_opaque_rust(true)

do {
try rust_func_return_result_null_opaque_rust(false)
XCTFail("The function should have returned an error.")
} catch let error as ResultTestOpaqueRustType {
XCTAssertEqual(error.val(), 222)
}
}

/// Verify that we can receive a Result<UnitStruct, OpaqueRust> from Rust
func testSwiftCallRustResultUnitStructOpaqueRust() throws {
try! rust_func_return_result_unit_struct_opaque_rust(true)

do {
try rust_func_return_result_unit_struct_opaque_rust(false)
XCTFail("The function should have returned an error.")
} catch let error as ResultTestOpaqueRustType {
XCTAssertEqual(error.val(), 222)
}
}
}
6 changes: 5 additions & 1 deletion crates/swift-bridge-build/src/generate_core.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::generate_core::boxed_fn_support::{
C_CALLBACK_SUPPORT_NO_ARGS_NO_RETURN, SWIFT_CALLBACK_SUPPORT_NO_ARGS_NO_RETURN,
};
use crate::generate_core::result_support::{C_RESULT_SUPPORT, SWIFT_RUST_RESULT};
use crate::generate_core::result_support::{
C_RESULT_SUPPORT, C_RESULT_VOID_SUPPORT, SWIFT_RUST_RESULT,
};
use std::path::Path;

const RUST_STRING_SWIFT: &'static str = include_str!("./generate_core/rust_string.swift");
Expand Down Expand Up @@ -33,6 +35,8 @@ pub(super) fn write_core_swift_and_c(out_dir: &Path) {
c_header += &C_CALLBACK_SUPPORT_NO_ARGS_NO_RETURN;
c_header += "\n";
c_header += &C_RESULT_SUPPORT;
c_header += "\n";
c_header += &C_RESULT_VOID_SUPPORT;

std::fs::write(core_c_header_out, c_header).unwrap();
}
Expand Down
4 changes: 4 additions & 0 deletions crates/swift-bridge-build/src/generate_core/result_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ extension RustResult {
pub const C_RESULT_SUPPORT: &'static str = r#"
struct __private__ResultPtrAndPtr { bool is_ok; void* ok_or_err; };
"#;

pub const C_RESULT_VOID_SUPPORT: &'static str = r#"
struct __private__ResultVoidAndPtr { bool is_ok; void* err; };
"#;
5 changes: 3 additions & 2 deletions crates/swift-bridge-ir/src/bridged_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ pub(crate) trait BridgeableType: Debug {
!self.is_built_in_type()
}

/// Whether or not this type can be encoded to exactly one representation.
/// Whether or not this type can be encoded to exactly one representation,
/// and therefore can be encoded with zero bytes.
/// For example `()` and `struct Foo;` can have exactly one representation,
/// but `u8` cannot since there are 255 possible `u8`s.
fn has_exactly_one_encoding(&self) -> bool {
fn can_be_encoded_with_zero_bytes(&self) -> bool {
self.only_encoding().is_some()
}

Expand Down
112 changes: 87 additions & 25 deletions crates/swift-bridge-ir/src/bridged_type/bridgeable_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ impl BuiltInResult {
// TODO: Choose the kind of Result representation based on whether or not the ok and error
// types are primitives.
// See `swift-bridge/src/std_bridge/result`
let result_kind = quote! {
ResultPtrAndPtr
let result_kind = if self.ok_ty.can_be_encoded_with_zero_bytes() {
quote! {
ResultVoidAndPtr
}
} else {
quote! {
ResultPtrAndPtr
}
};

quote! {
Expand Down Expand Up @@ -54,18 +60,37 @@ impl BuiltInResult {
span,
);

quote! {
match #expression {
Ok(ok) => {
#swift_bridge_path::result::ResultPtrAndPtr {
is_ok: true,
ok_or_err: #convert_ok as *mut std::ffi::c_void
if self.ok_ty.can_be_encoded_with_zero_bytes() {
quote! {
match #expression {
Ok(ok) => {
#swift_bridge_path::result::ResultVoidAndPtr {
is_ok: true,
err: std::ptr::null_mut::<std::ffi::c_void>()
}
}
Err(err) => {
#swift_bridge_path::result::ResultVoidAndPtr {
is_ok: false,
err: #convert_err as *mut std::ffi::c_void
}
}
}
Err(err) => {
#swift_bridge_path::result::ResultPtrAndPtr {
is_ok: false,
ok_or_err: #convert_err as *mut std::ffi::c_void
}
} else {
quote! {
match #expression {
Ok(ok) => {
#swift_bridge_path::result::ResultPtrAndPtr {
is_ok: true,
ok_or_err: #convert_ok as *mut std::ffi::c_void
}
}
Err(err) => {
#swift_bridge_path::result::ResultPtrAndPtr {
is_ok: false,
ok_or_err: #convert_err as *mut std::ffi::c_void
}
}
}
}
Expand Down Expand Up @@ -129,18 +154,44 @@ impl BuiltInResult {
type_pos: TypePosition,
types: &TypeDeclarations,
) -> String {
let convert_ok =
self.ok_ty
.convert_ffi_expression_to_swift_type("val.ok_or_err!", type_pos, types);
let convert_err =
self.err_ty
.convert_ffi_expression_to_swift_type("val.ok_or_err!", type_pos, types);
let (mut ok, err) = if let Some(zero_byte_encoding) = self.ok_ty.only_encoding() {
let ok = zero_byte_encoding.swift;
let convert_err = self
.err_ty
.convert_ffi_expression_to_swift_type("val.err!", type_pos, types);

(ok, convert_err)
} else {
let convert_ok =
self.ok_ty
.convert_ffi_expression_to_swift_type("val.ok_or_err!", type_pos, types);
let convert_err =
self.err_ty
.convert_ffi_expression_to_swift_type("val.ok_or_err!", type_pos, types);

(convert_ok, convert_err)
};

// There is a Swift compiler bug in Xcode 13 where using an explicit `()` here somehow leads
// the Swift compiler to a compile time error:
// "Unable to infer complex closure return type; add explicit type to disambiguate"
//
// It's asking us to add a `{ () -> () in .. }` explicit type to the beginning of our closure.
//
// To solve this bug we can either add that explicit closure type, or remove the explicit
// `return ()` in favor of a `return`.. Not sure why making the return type less explicit
// solves the compile time error.. But it does..
//
// As mentioned, this doesn't seem to happen in Xcode 14.
// So, we can remove this if statement whenever we stop supporting Xcode 13.
if self.ok_ty.is_null() {
ok = "".to_string();
}

format!(
"try {{ let val = {expression}; if val.is_ok {{ return {ok} }} else {{ throw {err} }} }}()",
expression = expression,
ok = convert_ok,
err = convert_err
err = err
)
}

Expand All @@ -156,17 +207,28 @@ impl BuiltInResult {
.err_ty
.convert_swift_expression_to_ffi_type("err", type_pos);

format!(
"{{ switch {val} {{ case .Ok(let ok): return __private__ResultPtrAndPtr(is_ok: true, ok_or_err: {convert_ok}) case .Err(let err): return __private__ResultPtrAndPtr(is_ok: false, ok_or_err: {convert_err}) }} }}()",
val = expression
)
if self.ok_ty.can_be_encoded_with_zero_bytes() {
format!(
"{{ switch {val} {{ case .Ok(let ok): return __private__ResultVoidAndPtr(is_ok: true, err: nil) case .Err(let err): return __private__ResultVoidAndPtr(is_ok: false, err: {convert_err}) }} }}()",
val = expression
)
} else {
format!(
"{{ switch {val} {{ case .Ok(let ok): return __private__ResultPtrAndPtr(is_ok: true, ok_or_err: {convert_ok}) case .Err(let err): return __private__ResultPtrAndPtr(is_ok: false, ok_or_err: {convert_err}) }} }}()",
val = expression
)
}
}

pub fn to_c(&self) -> &'static str {
// TODO: Choose the kind of Result representation based on whether or not the ok and error
// types are primitives.
// See `swift-bridge/src/std_bridge/result`
"struct __private__ResultPtrAndPtr"
if self.ok_ty.can_be_encoded_with_zero_bytes() {
"struct __private__ResultVoidAndPtr"
} else {
"struct __private__ResultPtrAndPtr"
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,144 @@ void __swift_bridge__$some_function(struct __private__ResultPtrAndPtr arg);
.test();
}
}

/// Test code generation for Rust function that accepts a Result<(), E> where E is an
/// opaque Rust type.
mod extern_rust_fn_return_result_null_and_opaque_rust {
use super::*;

fn bridge_module_tokens() -> TokenStream {
quote! {
mod ffi {
extern "Rust" {
type SomeType;

fn some_function () -> Result<(), 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::ResultVoidAndPtr {
match super::some_function() {
Ok(ok) => {
swift_bridge::result::ResultVoidAndPtr {
is_ok: true,
err: std::ptr::null_mut::<std::ffi::c_void>()
}
}
Err(err) => {
swift_bridge::result::ResultVoidAndPtr {
is_ok: false,
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 -> () {
try { let val = __swift_bridge__$some_function(); if val.is_ok { return } else { throw SomeType(ptr: val.err!) } }()
}
"#,
)
}

const EXPECTED_C_HEADER: ExpectedCHeader = ExpectedCHeader::ContainsAfterTrim(
r#"
struct __private__ResultVoidAndPtr __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();
}
}

// Test code generation for Rust function that accepts a Result<T, E> where T is a UnitStruct and E is an
/// opaque Rust type.
mod extern_rust_fn_return_result_unit_and_opaque_rust {
use super::*;

fn bridge_module_tokens() -> TokenStream {
quote! {
mod ffi {
struct UnitType;
extern "Rust" {
type SomeType;

fn some_function () -> Result<UnitType, 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::ResultVoidAndPtr {
match super::some_function() {
Ok(ok) => {
swift_bridge::result::ResultVoidAndPtr {
is_ok: true,
err: std::ptr::null_mut::<std::ffi::c_void>()
}
}
Err(err) => {
swift_bridge::result::ResultVoidAndPtr {
is_ok: false,
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 -> UnitType {
try { let val = __swift_bridge__$some_function(); if val.is_ok { return UnitType() } else { throw SomeType(ptr: val.err!) } }()
}
"#,
)
}

const EXPECTED_C_HEADER: ExpectedCHeader = ExpectedCHeader::ContainsAfterTrim(
r#"
struct __private__ResultVoidAndPtr __swift_bridge__$some_function(void);
"#,
);

#[test]
fn extern_rust_fn_return_result_unit_and_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();
}
}
Loading

0 comments on commit 27d7be9

Please sign in to comment.