Skip to content

Commit

Permalink
Allow interfaces to be errors. (#1963)
Browse files Browse the repository at this point in the history
  • Loading branch information
mhammond authored Feb 27, 2024
1 parent afbc6f1 commit 2e4e2ae
Show file tree
Hide file tree
Showing 23 changed files with 623 additions and 47 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

## [[UnreleasedUniFFIVersion]] (backend crates: [[UnreleasedBackendVersion]]) - (_[[ReleaseDate]]_)

### What's new?

- Objects can be errors - anywhere you can specify an enum error object you can specify
an `Arc<Object>` - see [the manual](https://mozilla.github.io/uniffi-rs/udl/errors.html).

[All changes in [[UnreleasedUniFFIVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.26.0...HEAD).

### What's new?
Expand Down
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ members = [
"fixtures/benchmarks",
"fixtures/coverall",
"fixtures/callbacks",
"fixtures/error-types",

"fixtures/ext-types/guid",
"fixtures/ext-types/http-headermap",
Expand Down
65 changes: 65 additions & 0 deletions docs/manual/src/udl/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,68 @@ interface ArithmeticError {
IntegerOverflow(u64 a, u64 b);
};
```

## Interfaces as errors

It's possible to use an `interface` (ie, a rust struct impl or a `dyn Trait`) as an error;
the thrown object will have methods instead of fields.
This can be particularly useful when working with `anyhow` style errors, where
an enum can't easily represent certain errors.

In your UDL:
```
namespace error {
[Throws=MyError]
void bail(string message);
}
[Traits=(Debug)]
interface MyError {
string message();
};
```
and Rust:
```rs
#[derive(Debug, thiserror::Error)]
#[error("{e:?}")] // default message is from anyhow.
pub struct MyError {
e: anyhow::Error,
}

impl MyError {
fn message(&self) -> String> { self.to_string() }
}

impl From<anyhow::Error> for MyError {
fn from(e: anyhow::Error) -> Self {
Self { e }
}
}
```
You can't yet use `anyhow` directly in your exposed functions - you need a wrapper:

```rs
fn oops() -> Result<(), Arc<MyError>> {
let e = anyhow::Error::msg("oops");
Err(Arc::new(e.into()))
}
```
then in Python:
```py
try:
oops()
except MyError as e:
print("oops", e.message())
```

This works for procmacros too - just derive or export the types.
```rs
#[derive(Debug, uniffi::Error)]
pub struct MyError { ... }
#[uniffi::export]
impl MyError { ... }
#[uniffi::export]
fn oops(e: String) -> Result<(), Arc<MyError>> { ... }
```

[See our tests this feature.](https://github.com/mozilla/uniffi-rs/tree/main/fixtures/error-types)
22 changes: 22 additions & 0 deletions fixtures/error-types/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "uniffi-fixture-error-types"
version = "0.22.0"
edition = "2021"
license = "MPL-2.0"
publish = false

[lib]
crate-type = ["lib", "cdylib"]
name = "uniffi_error_types"

[dependencies]
uniffi = {path = "../../uniffi"}
anyhow = "1"
thiserror = "1.0"

[build-dependencies]
uniffi = {path = "../../uniffi", features = ["build"] }

[dev-dependencies]
uniffi = {path = "../../uniffi", features = ["bindgen-tests"] }

55 changes: 55 additions & 0 deletions fixtures/error-types/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
Tests for objects as errors.

This works well with explicit type wrangling:

```rust
fn oops() -> Result<(), Arc<MyError>> {
let e = anyhow::Error::msg("oops");
Err(Arc::new(e.into()))
}
```

But a goal is to allow:

```rust
// doesn't work
fn oops() -> anyhow::Result<()> {
anyhow::bail!("oops");
}
```

# Stuck!

the above uniffi expands to:

```rust
extern "C" fn uniffi_uniffi_error_types_fn_func_oops(
call_status: &mut ::uniffi::RustCallStatus,
) -> <::std::result::Result<
(),
std::sync::Arc<ErrorInterface>,
> as ::uniffi::LowerReturn<crate::UniFfiTag>>::ReturnType {
...
::uniffi::rust_call(
call_status,
|| {
<::std::result::Result<(), std::sync::Arc<ErrorInterface>> as ::uniffi::LowerReturn ...>::lower_return(
match uniffi_lift_args() {
Ok(uniffi_args) => oops().map_err(::std::convert::Into::into),
Err((arg_name, anyhow_error)) => ...
},
)
},
)
}
```

# Problem is:
```rust
Ok(uniffi_args) => oops().map_err(::std::convert::Into::into),
```

map_err has `anyhow::Error<>`, we want `Arc<ErrorInterface>`, `::into` can't do that.

This works for enum because all `Arc<ErrorInterface>`s above are `ErrorEnum`. So above call is more like:
map_err has `CrateInternalError`, we want `CratePublicError`, `::into` can do that.
7 changes: 7 additions & 0 deletions fixtures/error-types/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

fn main() {
uniffi::generate_scaffolding("./src/error_types.udl").unwrap();
}
31 changes: 31 additions & 0 deletions fixtures/error-types/src/error_types.udl
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace error_types {
[Throws=ErrorInterface]
void oops();

ErrorInterface get_error(string message);

[Throws=RichError]
void throw_rich(string message);
};

interface TestInterface {
constructor();

[Throws=ErrorInterface, Name="fallible_new"]
constructor();

[Throws=ErrorInterface]
void oops();
};

[Traits=(Debug, Display)]
interface ErrorInterface {
sequence<string> chain();
string? link(u64 index);
};

// Kotlin replaces a trailing "Error" with "Exception"
// for enums, so we should check it does for objects too.
[Traits=(Debug, Display)]
interface RichError {
};
155 changes: 155 additions & 0 deletions fixtures/error-types/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use std::sync::Arc;

#[derive(Debug, thiserror::Error)]
#[error("{e:?}")]
pub struct ErrorInterface {
e: anyhow::Error,
}

impl ErrorInterface {
fn chain(&self) -> Vec<String> {
self.e.chain().map(ToString::to_string).collect()
}
fn link(&self, ndx: u64) -> Option<String> {
self.e.chain().nth(ndx as usize).map(ToString::to_string)
}
}

// A conversion into our ErrorInterface from anyhow::Error.
// We can't use this implicitly yet, but it still helps.
impl From<anyhow::Error> for ErrorInterface {
fn from(e: anyhow::Error) -> Self {
Self { e }
}
}

// must do explicit conversion...
fn oops() -> Result<(), Arc<ErrorInterface>> {
Err(Arc::new(
anyhow::Error::msg("oops")
.context("because uniffi told me so")
.into(),
))
}

#[uniffi::export]
fn toops() -> Result<(), Arc<dyn ErrorTrait>> {
Err(Arc::new(ErrorTraitImpl {
m: "trait-oops".to_string(),
}))
}

#[uniffi::export]
async fn aoops() -> Result<(), Arc<ErrorInterface>> {
Err(Arc::new(anyhow::Error::msg("async-oops").into()))
}

fn get_error(message: String) -> std::sync::Arc<ErrorInterface> {
Arc::new(anyhow::Error::msg(message).into())
}

#[uniffi::export]
pub trait ErrorTrait: Send + Sync + std::fmt::Debug + std::error::Error {
fn msg(&self) -> String;
}

#[derive(Debug, thiserror::Error)]
#[error("{m:?}")]
struct ErrorTraitImpl {
m: String,
}

impl ErrorTrait for ErrorTraitImpl {
fn msg(&self) -> String {
self.m.clone()
}
}

fn throw_rich(e: String) -> Result<(), RichError> {
Err(RichError { e })
}

// Exists to test trailing "Error" mapping in bindings
#[derive(Debug, thiserror::Error)]
#[error("RichError: {e:?}")]
pub struct RichError {
e: String,
}

impl RichError {}

pub struct TestInterface {}

impl TestInterface {
fn new() -> Self {
TestInterface {}
}

fn fallible_new() -> Result<Self, Arc<ErrorInterface>> {
Err(Arc::new(anyhow::Error::msg("fallible_new").into()))
}

fn oops(&self) -> Result<(), Arc<ErrorInterface>> {
Err(Arc::new(
anyhow::Error::msg("oops")
.context("because the interface told me so")
.into(),
))
}
}

#[uniffi::export]
impl TestInterface {
// can't define this in UDL due to #1915
async fn aoops(&self) -> Result<(), Arc<ErrorInterface>> {
Err(Arc::new(anyhow::Error::msg("async-oops").into()))
}
}

// A procmacro as an error
#[derive(Debug, uniffi::Object, thiserror::Error)]
#[uniffi::export(Debug, Display)]
pub struct ProcErrorInterface {
e: String,
}

#[uniffi::export]
impl ProcErrorInterface {
fn message(&self) -> String {
self.e.clone()
}
}

impl std::fmt::Display for ProcErrorInterface {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ProcErrorInterface({})", self.e)
}
}

#[uniffi::export]
fn throw_proc_error(e: String) -> Result<(), Arc<ProcErrorInterface>> {
Err(Arc::new(ProcErrorInterface { e }))
}

#[uniffi::export]
fn return_proc_error(e: String) -> Arc<ProcErrorInterface> {
Arc::new(ProcErrorInterface { e })
}

// Enums have good coverage elsewhere, but simple coverage here is good.
#[derive(thiserror::Error, uniffi::Error, Debug)]
pub enum EnumError {
#[error("Oops")]
Oops,
}

#[uniffi::export]
fn oops_enum() -> Result<(), EnumError> {
Err(EnumError::Oops)
}

uniffi::include_scaffolding!("error_types");
Loading

0 comments on commit 2e4e2ae

Please sign in to comment.