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

proc-macro: Add support for fallible functions #1408

Closed
wants to merge 11 commits into from
53 changes: 53 additions & 0 deletions docs/manual/src/proc_macro/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,59 @@ impl Foo {
}
```

## The `uniffi::Error` derive

The `Error` derive registers a type as an error and can be used on any enum that the `Enum` derive also accepts.
By default, it exposes any variant fields to the foreign code.
This type can then be used as the `E` in a `Result<T, E>` return type of an exported function or method.
The generated foreign function for an exported function with a `Result<T, E>` return type
will have the result's `T` as its return type and throw the error in case the Rust call returns `Err(e)`.

```rust
#[derive(uniffi::Error)]
pub enum MyError {
MissingInput,
IndexOutOfBounds {
index: u32,
size: u32,
}
Generic {
message: String,
}
}

#[uniffi::export]
fn do_thing() -> Result<(), MyError> {
// ...
}
```

You can also use the helper attribute `#[uniffi(flat_error)]` to expose just the variants but none of the fields.
In this case the error will be serialized using Rust's `ToString` trait
and will be accessible as the only field on each of the variants.
For flat errors your variants can have unnamed fields,
and the types of the fields don't need to implement any special traits.

```rust
#[derive(uniffi::Error)]
#[uniffi(flat_error)]
pub enum MyApiError {
Http(reqwest::Error),
Json(serde_json::Error),
}

// ToString is not usually implemented directly, but you get it for free by implementing Display.
// This impl could also be generated by a proc-macro, for example thiserror::Error.
impl std::fmt::Display for MyApiError {
jplatte marked this conversation as resolved.
Show resolved Hide resolved
// ...
}

#[uniffi::export]
fn do_http_request() -> Result<(), MyApiError> {
// ...
}
```

## Other limitations

In addition to the per-item limitations of the macros presented above, there is also currently a
Expand Down
39 changes: 38 additions & 1 deletion fixtures/proc-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,45 @@ fn enum_identity(value: MaybeBool) -> MaybeBool {
value
}

#[derive(uniffi::Error)]
pub enum BasicError {
InvalidInput,
OsError,
}

#[uniffi::export]
fn always_fails() -> Result<(), BasicError> {
Err(BasicError::OsError)
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
#[non_exhaustive]
pub enum FlatError {
#[error("Invalid input")]
InvalidInput,

// Inner types that aren't FFI-convertible, as well as unnamed fields,
// are allowed for flat errors
#[error("OS error: {0}")]
OsError(std::io::Error),
}

#[uniffi::export]
impl Object {
fn do_stuff(&self, times: u32) -> Result<(), FlatError> {
match times {
0 => Err(FlatError::InvalidInput),
_ => {
// do stuff
Ok(())
}
}
}
}

include!(concat!(env!("OUT_DIR"), "/proc-macro.uniffi.rs"));

mod uniffi_types {
pub use crate::{MaybeBool, NestedRecord, Object, One, Three, Two};
pub use crate::{BasicError, FlatError, MaybeBool, NestedRecord, Object, One, Three, Two};
}
14 changes: 14 additions & 0 deletions fixtures/proc-macro/tests/bindings/test_proc_macro.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,17 @@ assert(enumIdentity(MaybeBool.TRUE) == MaybeBool.TRUE)

// just make sure this works / doesn't crash
val three = Three(obj)

try {
alwaysFails()
throw RuntimeException("alwaysFails should have thrown")
} catch (e: BasicException) {
}

obj.doStuff(5u)

try {
obj.doStuff(0u)
throw RuntimeException("doStuff should throw if its argument is 0")
} catch (e: FlatException) {
}
16 changes: 16 additions & 0 deletions fixtures/proc-macro/tests/bindings/test_proc_macro.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,19 @@

# just make sure this works / doesn't crash
three = Three(obj)

try:
always_fails()
except BasicError.OsError:
pass
else:
raise Exception("always_fails should have thrown")

obj.do_stuff(5)

try:
obj.do_stuff(0)
except FlatError.InvalidInput:
pass
else:
raise Exception("do_stuff should throw if its argument is 0")
14 changes: 14 additions & 0 deletions fixtures/proc-macro/tests/bindings/test_proc_macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,17 @@ assert(enumIdentity(value: .true) == .true)

// just make sure this works / doesn't crash
let three = Three(obj: obj)

do {
try alwaysFails()
fatalError("alwaysFails should have thrown")
} catch BasicError.OsError {
}

try! obj.doStuff(times: 5)

do {
try obj.doStuff(times: 0)
fatalError("doStuff should throw if its argument is 0")
} catch FlatError.InvalidInput {
}
40 changes: 37 additions & 3 deletions uniffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub mod deps {
pub use static_assertions;
}

pub use uniffi_macros::{export, Enum, Object, Record};
pub use uniffi_macros::{export, Enum, Error, Object, Record};

mod panichook;

Expand Down Expand Up @@ -211,6 +211,38 @@ pub unsafe trait FfiConverter: Sized {
fn try_read(buf: &mut &[u8]) -> Result<Self::RustType>;
}

/// Types that can be returned from exported functions.
///
/// Blanket-implemented by any FfiConverter with RustType = Self, but additionally implemented by
/// the unit type.
///
/// This helper trait is currently only used to simplify the code generated by the export macro and
/// is not part of the public interface of the library, hence its documentation is hidden.
#[doc(hidden)]
pub unsafe trait FfiReturn: Sized {
type FfiType;
fn lower(self) -> Self::FfiType;
}

unsafe impl<T> FfiReturn for T
where
T: FfiConverter<RustType = T>,
{
type FfiType = <Self as FfiConverter>::FfiType;

fn lower(self) -> Self::FfiType {
<Self as FfiConverter>::lower(self)
}
}

unsafe impl FfiReturn for () {
type FfiType = ();

fn lower(self) -> Self::FfiType {
self
}
}

/// A helper function to ensure we don't read past the end of a buffer.
///
/// Rust won't actually let us read past the end of a buffer, but the `Buf` trait does not support
Expand Down Expand Up @@ -608,7 +640,7 @@ unsafe impl<T: Sync + Send> FfiConverter for std::sync::Arc<T> {
/// function for other types may lead to undefined behaviour.
fn write(obj: Self::RustType, buf: &mut Vec<u8>) {
static_assertions::const_assert!(std::mem::size_of::<*const std::ffi::c_void>() <= 8);
buf.put_u64(Self::lower(obj) as u64);
buf.put_u64(<Self as FfiConverter>::lower(obj) as u64);
}

/// When reading as a field of a complex structure, we receive a "borrow" of the `Arc<T>`
Expand Down Expand Up @@ -639,7 +671,9 @@ where

#[cfg(test)]
mod test {
use super::*;
use std::time::{Duration, SystemTime};

use super::FfiConverter as _;

#[test]
fn trybuild_ui_tests() {
Expand Down
2 changes: 1 addition & 1 deletion uniffi_bindgen/src/bindings/python/templates/Helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def rust_call_with_error(error_ffi_converter, fn, *args):
return result
elif call_status.code == RustCallStatus.CALL_ERROR:
if error_ffi_converter is None:
call_status.err_buf.contents.free()
call_status.error_buf.free()
raise InternalError("rust_call_with_error: CALL_ERROR, but error_ffi_converter is None")
else:
raise error_ffi_converter.lift(call_status.error_buf)
Expand Down
12 changes: 12 additions & 0 deletions uniffi_bindgen/src/interface/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ impl FunctionAttributes {
}
}

impl FromIterator<Attribute> for FunctionAttributes {
fn from_iter<T: IntoIterator<Item = Attribute>>(iter: T) -> Self {
Self(Vec::from_iter(iter))
}
}

impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for FunctionAttributes {
type Error = anyhow::Error;
fn try_from(
Expand Down Expand Up @@ -346,6 +352,12 @@ impl MethodAttributes {
}
}

impl FromIterator<Attribute> for MethodAttributes {
fn from_iter<T: IntoIterator<Item = Attribute>>(iter: T) -> Self {
Self(Vec::from_iter(iter))
}
}

impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for MethodAttributes {
type Error = anyhow::Error;
fn try_from(
Expand Down
21 changes: 17 additions & 4 deletions uniffi_bindgen/src/interface/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ use super::{APIConverter, ComponentInterface};
/// they're handled in the FFI very differently. We create them in `uniffi::call_with_result()` if
/// the wrapped function returns an `Err` value
/// struct and assign an integer error code to each variant.
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Error {
pub name: String,
enum_: Enum,
Expand Down Expand Up @@ -135,6 +135,19 @@ impl Error {
}
}

impl From<uniffi_meta::ErrorMetadata> for Error {
fn from(meta: uniffi_meta::ErrorMetadata) -> Self {
Self {
name: meta.name.clone(),
enum_: Enum {
name: meta.name,
variants: meta.variants.into_iter().map(Into::into).collect(),
flat: meta.flat,
},
}
}
}

impl APIConverter<Error> for weedle::EnumDefinition<'_> {
fn convert(&self, ci: &mut ComponentInterface) -> Result<Error> {
Ok(Error::from_enum(APIConverter::<Enum>::convert(self, ci)?))
Expand All @@ -159,7 +172,7 @@ mod test {
enum Testing { "one", "two", "three" };
"#;
let ci = ComponentInterface::from_webidl(UDL).unwrap();
assert_eq!(ci.error_definitions().len(), 1);
assert_eq!(ci.error_definitions().count(), 1);
let error = ci.get_error_definition("Testing").unwrap();
assert_eq!(
error
Expand All @@ -182,7 +195,7 @@ mod test {
enum Testing { "one", "two", "one" };
"#;
let ci = ComponentInterface::from_webidl(UDL).unwrap();
assert_eq!(ci.error_definitions().len(), 1);
assert_eq!(ci.error_definitions().count(), 1);
assert_eq!(
ci.get_error_definition("Testing").unwrap().variants().len(),
3
Expand All @@ -201,7 +214,7 @@ mod test {
};
"#;
let ci = ComponentInterface::from_webidl(UDL).unwrap();
assert_eq!(ci.error_definitions().len(), 1);
assert_eq!(ci.error_definitions().count(), 1);
let error: &Error = ci.get_error_definition("Testing").unwrap();
assert_eq!(
error
Expand Down
9 changes: 3 additions & 6 deletions uniffi_bindgen/src/interface/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,11 @@ use std::hash::{Hash, Hasher};

use anyhow::{bail, Result};

use super::attributes::{ArgumentAttributes, Attribute, FunctionAttributes};
use super::ffi::{FfiArgument, FfiFunction};
use super::literal::{convert_default_value, Literal};
use super::types::{Type, TypeIterator};
use super::{
attributes::{ArgumentAttributes, FunctionAttributes},
convert_type,
};
use super::{APIConverter, ComponentInterface};
use super::{convert_type, APIConverter, ComponentInterface};

/// Represents a standalone function.
///
Expand Down Expand Up @@ -137,7 +134,7 @@ impl From<uniffi_meta::FnMetadata> for Function {
arguments,
return_type,
ffi_func,
attributes: Default::default(),
attributes: meta.throws.map(Attribute::Throws).into_iter().collect(),
}
}
}
Expand Down
Loading