Skip to content

Commit

Permalink
Merge pull request #1573 from bendk/proc-macro-callbacks
Browse files Browse the repository at this point in the history
Proc macro callbacks
  • Loading branch information
bendk authored Jun 14, 2023
2 parents 42e8f56 + f1100f0 commit 09a4646
Show file tree
Hide file tree
Showing 54 changed files with 1,399 additions and 598 deletions.
4 changes: 1 addition & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,13 @@
- The `include_scaffolding!()` macro must now either be called from your crate root or you must have `use the_mod_that_calls_include_scaffolding::*` in your crate root. This was always the expectation, but wasn't required before. This will now start failing with errors that say `crate::UniFfiTag` does not exist.
- proc-macros now work with many more types including type aliases, type paths, etc.
- The `uniffi_types` module is no longer needed when using proc-macros.

- Traits can be exposed as a UniFFI `interface` by using a `[Trait]` attribute in the UDL.
See [the documentation](https://mozilla.github.io/uniffi-rs/udl/interfaces.html#exposing-traits-as-interfaces).

- The `bytes` primitive type was added, it represents an array of bytes. It maps to `ByteArray` in Kotlin, `bytes` in Python, `String` with `Encoding::BINARY` in Ruby and `Data` in Swift.
- Shortened `str()` representations of errors in Python to align with other exceptions in Python. Use `repr()` or the `{!r}` format to get the old representation back.

- Methods implemented by standard Rust traits, such as `Debug`, `Display`, `Eq` and `Hash` can now be exposed over the FFI and bindings may implement special methods for them.
See [the documentation](https://mozilla.github.io/uniffi-rs/udl/interfaces.html#exposing-methods-from-standard-rust-traits).
- Implemented proc-macro callback interface support

### Guidance for external bindings

Expand Down
49 changes: 49 additions & 0 deletions docs/manual/src/proc_macro/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,55 @@ fn do_http_request() -> Result<(), MyApiError> {
}
```

## The `#[uniffi::export(callback_interface)]` attribute

`#[uniffi::export(callback_interface)]` can be used to export a [callback interface](../udl/callback_interfaces.html) definition.
This allows the foreign bindings to implement the interface and pass an instance to the Rust code.

```rust
#[uniffi::export(callback_interface)]
pub trait Person {
fn name() -> String;
fn age() -> u32;
}

// Corresponding UDL:
// callback interface Person {
// string name();
// u32 age();
// }
```

### Exception handling in callback interfaces

Most languages allow arbitrary exceptions to be thrown, which presents issues for callback
interfaces. If a callback interface function returns a non-Result type, then any exception will
result in a panic on the Rust side.

To avoid panics, callback interfaces can use `Result<T, E>` types for all return values. If the callback
interface implementation throws the exception that corresponds to the `E` parameter, `Err(E)` will
be returned to the Rust code. However, in most languages it's still possible for the implementation
to throw other exceptions. To avoid panics in those cases, the error type must be wrapped
with the `#[uniffi(handle_unknown_callback_error)]` attribute and
`From<UnexpectedUniFFICallbackError>` must be implemented:

```rust
#[derive(uniffi::Error)]
#[uniffi(handle_unknown_callback_error)]
pub enum MyApiError {
IOError,
ValueError,
UnexpectedError { reason: String },
}

impl From<UnexpectedUniFFICallbackError> for MyApiError {
fn from(e: UnexpectedUniFFICallbackError) -> Self {
Self::UnexpectedError { reason: e.reason }
}
}
```


## Other limitations

In addition to the per-item limitations of the macros presented above, there is also currently a
Expand Down
42 changes: 36 additions & 6 deletions fixtures/metadata/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ mod calc {
pub struct Calculator {}
}

#[uniffi::export(callback_interface)]
pub trait Logger {
fn log(&self, message: String);
}

pub use calc::Calculator;
pub use error::{ComplexError, FlatError};
pub use person::Person;
Expand Down Expand Up @@ -476,7 +481,6 @@ mod test_function_metadata {
MethodMetadata {
module_path: "uniffi_fixture_metadata".into(),
self_name: "Calculator".into(),
self_is_trait: false,
name: "add".into(),
is_async: false,
inputs: vec![
Expand Down Expand Up @@ -555,7 +559,6 @@ mod test_function_metadata {
MethodMetadata {
module_path: "uniffi_fixture_metadata".into(),
self_name: "Calculator".into(),
self_is_trait: false,
name: "async_sub".into(),
is_async: true,
inputs: vec![
Expand Down Expand Up @@ -583,7 +586,6 @@ mod test_function_metadata {
MethodMetadata {
module_path: "uniffi_fixture_metadata".into(),
self_name: "Calculator".into(),
self_is_trait: false,
name: "get_display".into(),
is_async: false,
inputs: vec![],
Expand All @@ -602,10 +604,10 @@ mod test_function_metadata {
fn test_trait_method() {
check_metadata(
&UNIFFI_META_UNIFFI_FIXTURE_METADATA_METHOD_CALCULATORDISPLAY_DISPLAY_RESULT,
MethodMetadata {
TraitMethodMetadata {
module_path: "uniffi_fixture_metadata".into(),
self_name: "CalculatorDisplay".into(),
self_is_trait: true,
trait_name: "CalculatorDisplay".into(),
index: 0,
name: "display_result".into(),
is_async: false,
inputs: vec![
Expand All @@ -621,4 +623,32 @@ mod test_function_metadata {
},
);
}

#[test]
fn test_callback_interface() {
check_metadata(
&UNIFFI_META_UNIFFI_FIXTURE_METADATA_CALLBACK_INTERFACE_LOGGER,
CallbackInterfaceMetadata {
module_path: "uniffi_fixture_metadata".into(),
name: "Logger".into(),
},
);
check_metadata(
&UNIFFI_META_UNIFFI_FIXTURE_METADATA_METHOD_LOGGER_LOG,
TraitMethodMetadata {
module_path: "uniffi_fixture_metadata".into(),
trait_name: "Logger".into(),
index: 0,
name: "log".into(),
is_async: false,
inputs: vec![FnParamMetadata {
name: "message".into(),
ty: Type::String,
}],
return_type: None,
throws: None,
checksum: UNIFFI_META_CONST_UNIFFI_FIXTURE_METADATA_METHOD_LOGGER_LOG.checksum(),
},
);
}
}
12 changes: 12 additions & 0 deletions fixtures/proc-macro/src/callback_interface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* 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 crate::BasicError;

#[uniffi::export(callback_interface)]
pub trait TestCallbackInterface {
fn do_nothing(&self);
fn add(&self, a: u32, b: u32) -> u32;
fn try_parse_int(&self, value: String) -> Result<u32, BasicError>;
}
29 changes: 28 additions & 1 deletion fixtures/proc-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

use std::sync::Arc;

mod callback_interface;

use callback_interface::TestCallbackInterface;

#[derive(uniffi::Record)]
pub struct One {
inner: i32,
Expand Down Expand Up @@ -87,6 +91,21 @@ fn take_two(two: Two) -> String {
two.a
}

#[uniffi::export]
fn test_callback_interface(cb: Box<dyn TestCallbackInterface>) {
cb.do_nothing();
assert_eq!(cb.add(1, 1), 2);
assert_eq!(Ok(10), cb.try_parse_int("10".to_string()));
assert_eq!(
Err(BasicError::InvalidInput),
cb.try_parse_int("ten".to_string())
);
assert!(matches!(
cb.try_parse_int("force-unexpected-error".to_string()),
Err(BasicError::UnexpectedError { .. }),
));
}

// Type that's defined in the UDL and not wrapped with #[uniffi::export]
pub struct Zero {
inner: String,
Expand All @@ -111,10 +130,18 @@ fn enum_identity(value: MaybeBool) -> MaybeBool {
value
}

#[derive(uniffi::Error)]
#[derive(uniffi::Error, Debug, PartialEq, Eq)]
#[uniffi(handle_unknown_callback_error)]
pub enum BasicError {
InvalidInput,
OsError,
UnexpectedError { reason: String },
}

impl From<uniffi::UnexpectedUniFFICallbackError> for BasicError {
fn from(e: uniffi::UnexpectedUniFFICallbackError) -> Self {
Self::UnexpectedError { reason: e.reason }
}
}

#[uniffi::export]
Expand Down
21 changes: 21 additions & 0 deletions fixtures/proc-macro/tests/bindings/test_proc_macro.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,24 @@ try {
throw RuntimeException("doStuff should throw if its argument is 0")
} catch (e: FlatException) {
}


class KtTestCallbackInterface : TestCallbackInterface {
override fun doNothing() { }

override fun add(a: UInt, b: UInt) = a + b

override fun tryParseInt(value: String): UInt {
if (value == "force-unexpected-error") {
// raise an error that's not expected
throw RuntimeException(value)
}
try {
return value.toUInt()
} catch(e: NumberFormatException) {
throw BasicException.InvalidInput()
}
}
}

testCallbackInterface(KtTestCallbackInterface())
18 changes: 18 additions & 0 deletions fixtures/proc-macro/tests/bindings/test_proc_macro.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,21 @@
pass
else:
raise Exception("do_stuff should throw if its argument is 0")

class PyTestCallbackInterface(TestCallbackInterface):
def do_nothing(self):
pass

def add(self, a, b):
return a + b

def try_parse_int(self, value):
if value == "force-unexpected-error":
# raise an error that's not expected
raise KeyError(value)
try:
return int(value)
except BaseException:
raise BasicError.InvalidInput()

test_callback_interface(PyTestCallbackInterface())
25 changes: 25 additions & 0 deletions fixtures/proc-macro/tests/bindings/test_proc_macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,28 @@ do {
fatalError("doStuff should throw if its argument is 0")
} catch FlatError.InvalidInput {
}

struct SomeOtherError: Error { }

class SwiftTestCallbackInterface : TestCallbackInterface {
func doNothing() { }

func add(a: UInt32, b: UInt32) -> UInt32 {
return a + b;
}

func tryParseInt(value: String) throws -> UInt32 {
if (value == "force-unexpected-error") {
// raise an error that's not expected
throw SomeOtherError()
}
let parsed = UInt32(value)
if parsed != nil {
return parsed!
} else {
throw BasicError.InvalidInput
}
}
}

testCallbackInterface(cb: SwiftTestCallbackInterface())
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ error[E0533]: expected value, found struct variant `Self::DivisionByZero`
| | tag = crate::UniFfiTag,
| | flat_error,
| | with_try_read,
| | handle_unknown_callback_error,
| | )]
| |__^ not a value
|
Expand Down
69 changes: 69 additions & 0 deletions uniffi_bindgen/src/backend/filters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* 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/. */

//! Backend-agnostic askama filters
use crate::interface::{CallbackInterface, ComponentInterface, Enum, Function, Object, Record};
use askama::Result;
use std::fmt;

// Need to define an error that implements std::error::Error, which neither String nor
// anyhow::Error do.
#[derive(Debug)]
struct UniFFIError {
message: String,
}

impl UniFFIError {
fn new(message: String) -> Self {
Self { message }
}
}

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

impl std::error::Error for UniFFIError {}

macro_rules! lookup_error {
($($args:tt)*) => {
askama::Error::Custom(Box::new(UniFFIError::new(format!($($args)*))))
}
}

/// Get an Enum definition by name
pub fn get_enum_definition<'a>(ci: &'a ComponentInterface, name: &str) -> Result<&'a Enum> {
ci.get_enum_definition(name)
.ok_or_else(|| lookup_error!("enum {name} not found"))
}

/// Get a Record definition by name
pub fn get_record_definition<'a>(ci: &'a ComponentInterface, name: &str) -> Result<&'a Record> {
ci.get_record_definition(name)
.ok_or_else(|| lookup_error!("record {name} not found"))
}

/// Get a Function definition by name
pub fn get_function_definition<'a>(ci: &'a ComponentInterface, name: &str) -> Result<&'a Function> {
ci.get_function_definition(name)
.ok_or_else(|| lookup_error!("function {name} not found"))
}

/// Get an Object definition by name
pub fn get_object_definition<'a>(ci: &'a ComponentInterface, name: &str) -> Result<&'a Object> {
ci.get_object_definition(name)
.ok_or_else(|| lookup_error!("object {name} not found"))
}

/// Get an Callback Interface definition by name
pub fn get_callback_interface_definition<'a>(
ci: &'a ComponentInterface,
name: &str,
) -> Result<&'a CallbackInterface> {
ci.get_callback_interface_definition(name)
.ok_or_else(|| lookup_error!("callback interface {name} not found"))
}
1 change: 1 addition & 0 deletions uniffi_bindgen/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

mod config;
pub mod filters;
mod types;

pub use crate::interface::{Literal, Type};
Expand Down
1 change: 1 addition & 0 deletions uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ impl<T: AsType> AsCodeType for T {

pub mod filters {
use super::*;
pub use crate::backend::filters::*;

pub fn type_name(as_ct: &impl AsCodeType) -> Result<String, askama::Error> {
Ok(as_ct.as_codetype().type_label())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{%- let cbi = ci.get_callback_interface_definition(name).unwrap() %}
{%- let cbi = ci|get_callback_interface_definition(name) %}
{%- let type_name = cbi|type_name %}
{%- let foreign_callback = format!("ForeignCallback{}", canonical_type_name) %}

Expand Down
Loading

0 comments on commit 09a4646

Please sign in to comment.