From 2e4e2ae53e83c832cdff80cb4c8779038789f7aa Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Mon, 26 Feb 2024 21:03:03 -0500 Subject: [PATCH] Allow interfaces to be errors. (#1963) --- CHANGELOG.md | 5 + Cargo.lock | 9 + Cargo.toml | 1 + docs/manual/src/udl/errors.md | 65 ++++++++ fixtures/error-types/Cargo.toml | 22 +++ fixtures/error-types/README.md | 55 +++++++ fixtures/error-types/build.rs | 7 + fixtures/error-types/src/error_types.udl | 31 ++++ fixtures/error-types/src/lib.rs | 155 ++++++++++++++++++ fixtures/error-types/tests/bindings/test.kts | 42 +++++ fixtures/error-types/tests/bindings/test.py | 77 +++++++++ .../error-types/tests/bindings/test.swift | 18 ++ .../tests/test_generated_bindings.rs | 5 + .../kotlin/templates/ObjectTemplate.kt | 24 ++- .../python/templates/ObjectTemplate.py | 36 +++- .../templates/TopLevelFunctionTemplate.py | 8 +- .../src/bindings/python/templates/macros.py | 59 ++++--- .../src/bindings/swift/gen_swift/mod.rs | 9 + .../swift/templates/ErrorTemplate.swift | 4 - .../swift/templates/ObjectTemplate.swift | 30 +++- .../templates/TopLevelFunctionTemplate.swift | 2 +- .../src/bindings/swift/templates/macros.swift | 4 +- uniffi_bindgen/src/interface/mod.rs | 2 +- 23 files changed, 623 insertions(+), 47 deletions(-) create mode 100644 fixtures/error-types/Cargo.toml create mode 100644 fixtures/error-types/README.md create mode 100644 fixtures/error-types/build.rs create mode 100644 fixtures/error-types/src/error_types.udl create mode 100644 fixtures/error-types/src/lib.rs create mode 100644 fixtures/error-types/tests/bindings/test.kts create mode 100644 fixtures/error-types/tests/bindings/test.py create mode 100644 fixtures/error-types/tests/bindings/test.swift create mode 100644 fixtures/error-types/tests/test_generated_bindings.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b280bbd360..eb1bedb904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` - 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? diff --git a/Cargo.lock b/Cargo.lock index 2c4892b790..af73be5d4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1568,6 +1568,15 @@ dependencies = [ "uniffi_testing", ] +[[package]] +name = "uniffi-fixture-error-types" +version = "0.22.0" +dependencies = [ + "anyhow", + "thiserror", + "uniffi", +] + [[package]] name = "uniffi-fixture-ext-types" version = "0.22.0" diff --git a/Cargo.toml b/Cargo.toml index 1493b23fb1..bcd751d612 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ members = [ "fixtures/benchmarks", "fixtures/coverall", "fixtures/callbacks", + "fixtures/error-types", "fixtures/ext-types/guid", "fixtures/ext-types/http-headermap", diff --git a/docs/manual/src/udl/errors.md b/docs/manual/src/udl/errors.md index 5f65601514..3b64628272 100644 --- a/docs/manual/src/udl/errors.md +++ b/docs/manual/src/udl/errors.md @@ -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 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> { + 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> { ... } +``` + +[See our tests this feature.](https://github.com/mozilla/uniffi-rs/tree/main/fixtures/error-types) diff --git a/fixtures/error-types/Cargo.toml b/fixtures/error-types/Cargo.toml new file mode 100644 index 0000000000..0b7cee93d6 --- /dev/null +++ b/fixtures/error-types/Cargo.toml @@ -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"] } + diff --git a/fixtures/error-types/README.md b/fixtures/error-types/README.md new file mode 100644 index 0000000000..b2f129013c --- /dev/null +++ b/fixtures/error-types/README.md @@ -0,0 +1,55 @@ +Tests for objects as errors. + +This works well with explicit type wrangling: + +```rust +fn oops() -> Result<(), Arc> { + 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, +> as ::uniffi::LowerReturn>::ReturnType { +... + ::uniffi::rust_call( + call_status, + || { + <::std::result::Result<(), std::sync::Arc> 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`, `::into` can't do that. + +This works for enum because all `Arc`s above are `ErrorEnum`. So above call is more like: +map_err has `CrateInternalError`, we want `CratePublicError`, `::into` can do that. diff --git a/fixtures/error-types/build.rs b/fixtures/error-types/build.rs new file mode 100644 index 0000000000..bdd47f59a1 --- /dev/null +++ b/fixtures/error-types/build.rs @@ -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(); +} diff --git a/fixtures/error-types/src/error_types.udl b/fixtures/error-types/src/error_types.udl new file mode 100644 index 0000000000..3356a7e0a0 --- /dev/null +++ b/fixtures/error-types/src/error_types.udl @@ -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 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 { +}; diff --git a/fixtures/error-types/src/lib.rs b/fixtures/error-types/src/lib.rs new file mode 100644 index 0000000000..eba628afe3 --- /dev/null +++ b/fixtures/error-types/src/lib.rs @@ -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 { + self.e.chain().map(ToString::to_string).collect() + } + fn link(&self, ndx: u64) -> Option { + 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 for ErrorInterface { + fn from(e: anyhow::Error) -> Self { + Self { e } + } +} + +// must do explicit conversion... +fn oops() -> Result<(), Arc> { + Err(Arc::new( + anyhow::Error::msg("oops") + .context("because uniffi told me so") + .into(), + )) +} + +#[uniffi::export] +fn toops() -> Result<(), Arc> { + Err(Arc::new(ErrorTraitImpl { + m: "trait-oops".to_string(), + })) +} + +#[uniffi::export] +async fn aoops() -> Result<(), Arc> { + Err(Arc::new(anyhow::Error::msg("async-oops").into())) +} + +fn get_error(message: String) -> std::sync::Arc { + 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> { + Err(Arc::new(anyhow::Error::msg("fallible_new").into())) + } + + fn oops(&self) -> Result<(), Arc> { + 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> { + 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> { + Err(Arc::new(ProcErrorInterface { e })) +} + +#[uniffi::export] +fn return_proc_error(e: String) -> Arc { + 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"); diff --git a/fixtures/error-types/tests/bindings/test.kts b/fixtures/error-types/tests/bindings/test.kts new file mode 100644 index 0000000000..b70998f2f5 --- /dev/null +++ b/fixtures/error-types/tests/bindings/test.kts @@ -0,0 +1,42 @@ +/* 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/. */ + +import uniffi.error_types.*; +import kotlinx.coroutines.* + +try { + oops() + throw RuntimeException("Should have failed") +} catch (e: ErrorInterface) { + assert(e.toString() == "because uniffi told me so\n\nCaused by:\n oops") + assert(e.chain().size == 2) + assert(e.link(0U) == "because uniffi told me so") +} + +try { + toops() + throw RuntimeException("Should have failed") +} catch (e: ErrorTrait) { + assert(e.msg() == "trait-oops") +} + +val e = getError("the error") +assert(e.toString() == "the error") +assert(e.link(0U) == "the error") + +try { + throwRich("oh no") + throw RuntimeException("Should have failed") +} catch (e: RichException) { + assert(e.toString() == "RichError: \"oh no\"") +} + +runBlocking { + try { + aoops() + throw RuntimeException("Should have failed") + } catch (e: ErrorInterface) { + assert(e.toString() == "async-oops") + } +} diff --git a/fixtures/error-types/tests/bindings/test.py b/fixtures/error-types/tests/bindings/test.py new file mode 100644 index 0000000000..efb95c609a --- /dev/null +++ b/fixtures/error-types/tests/bindings/test.py @@ -0,0 +1,77 @@ +# 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/. */ + +import asyncio +import unittest +from error_types import * + +class TestErrorTypes(unittest.TestCase): + def test_normal_catch(self): + try: + oops() + self.fail("must fail") + except ErrorInterface as e: + self.assertEqual(str(e), "because uniffi told me so\n\nCaused by:\n oops") + + def test_error_interface(self): + with self.assertRaises(ErrorInterface) as cm: + oops() + self.assertEqual(cm.exception.chain(), ["because uniffi told me so", "oops"]) + self.assertEqual(cm.exception.link(0), "because uniffi told me so") + self.assertEqual(repr(cm.exception), "ErrorInterface { e: because uniffi told me so\n\nCaused by:\n oops }") + self.assertEqual(str(cm.exception), "because uniffi told me so\n\nCaused by:\n oops") + + def test_async_error_interface(self): + try: + asyncio.run(aoops()) + self.fail("must fail") + except ErrorInterface as e: + self.assertEqual(str(e), "async-oops") + + def test_error_trait(self): + with self.assertRaises(ErrorTrait) as cm: + toops() + self.assertEqual(cm.exception.msg(), "trait-oops") + + # Check we can still call a function which returns an error (as opposed to one which throws it) + def test_error_return(self): + e = get_error("the error") + self.assertEqual(e.chain(), ["the error"]) + self.assertEqual(repr(e), "ErrorInterface { e: the error }") + self.assertEqual(str(e), "the error") + + def test_rich_error(self): + try: + throw_rich("oh no") + self.fail("must fail") + except RichError as e: + self.assertEqual(repr(e), 'RichError { e: "oh no" }') + self.assertEqual(str(e), 'RichError: "oh no"') + + # TestInterface also throws. + def test_interface_errors(self): + with self.assertRaises(ErrorInterface) as cm: + TestInterface.fallible_new() + self.assertEqual(str(cm.exception), "fallible_new") + + interface = TestInterface() + with self.assertRaises(ErrorInterface) as cm: + interface.oops() + self.assertEqual(str(cm.exception), "because the interface told me so\n\nCaused by:\n oops") + + try: + asyncio.run(interface.aoops()) + self.fail("must fail") + except ErrorInterface as e: + self.assertEqual(str(e), "async-oops") + + # TestInterface also throws. + def test_procmacro_interface_errors(self): + with self.assertRaises(ProcErrorInterface) as cm: + throw_proc_error("eek") + self.assertEqual(cm.exception.message(), "eek") + self.assertEqual(str(cm.exception), "ProcErrorInterface(eek)") + +if __name__=='__main__': + unittest.main() diff --git a/fixtures/error-types/tests/bindings/test.swift b/fixtures/error-types/tests/bindings/test.swift new file mode 100644 index 0000000000..5370565d11 --- /dev/null +++ b/fixtures/error-types/tests/bindings/test.swift @@ -0,0 +1,18 @@ +import error_types +do { + try oops() + fatalError("Should have thrown") +} catch let e as ErrorInterface { + assert(String(describing: e) == "because uniffi told me so\n\nCaused by:\n oops") +} + +do { + try toops() + fatalError("Should have thrown") +} catch let e as ErrorTrait { + assert(e.msg() == "trait-oops") +} + +let e = getError(message: "the error") +assert(String(describing: e) == "the error") +assert(String(reflecting: e) == "ErrorInterface { e: the error }") diff --git a/fixtures/error-types/tests/test_generated_bindings.rs b/fixtures/error-types/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..a6c3de1fd3 --- /dev/null +++ b/fixtures/error-types/tests/test_generated_bindings.rs @@ -0,0 +1,5 @@ +uniffi::build_foreign_language_testcases!( + "tests/bindings/test.py", + "tests/bindings/test.kts", + "tests/bindings/test.swift" +); diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt index d91c0eab31..156215fb84 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt @@ -104,11 +104,17 @@ {%- let (interface_name, impl_class_name) = obj|object_names(ci) %} {%- let methods = obj.methods() %} {%- let interface_docstring = obj.docstring() %} +{%- let is_error = ci.is_name_used_as_error(name) %} +{%- let ffi_converter_name = obj|ffi_converter_name %} {%- include "Interface.kt" %} {%- call kt::docstring(obj, 0) %} +{% if (is_error) %} +open class {{ impl_class_name }} : Exception, Disposable, AutoCloseable, {{ interface_name }} { +{% else -%} open class {{ impl_class_name }}: Disposable, AutoCloseable, {{ interface_name }} { +{%- endif %} constructor(pointer: Pointer) { this.pointer = pointer @@ -127,7 +133,7 @@ open class {{ impl_class_name }}: Disposable, AutoCloseable, {{ interface_name } } {%- match obj.primary_constructor() %} - {%- when Some with (cons) %} + {%- when Some(cons) %} {%- call kt::docstring(cons, 4) %} constructor({% call kt::arg_list_decl(cons) -%}) : this({% call kt::to_ffi_call(cons) %}) @@ -200,7 +206,7 @@ open class {{ impl_class_name }}: Disposable, AutoCloseable, {{ interface_name } {% for meth in obj.methods() -%} {%- call kt::docstring(meth, 4) %} {%- match meth.throws_type() -%} - {%- when Some with (throwable) %} + {%- when Some(throwable) %} @Throws({{ throwable|type_name(ci) }}::class) {%- else -%} {%- endmatch -%} @@ -289,6 +295,7 @@ open class {{ impl_class_name }}: Disposable, AutoCloseable, {{ interface_name } {%- endmatch %} {%- endfor %} + {# XXX - "companion object" confusion? How to have alternate constructors *and* be an error? #} {% if !obj.alternate_constructors().is_empty() -%} companion object { {% for cons in obj.alternate_constructors() -%} @@ -297,6 +304,17 @@ open class {{ impl_class_name }}: Disposable, AutoCloseable, {{ interface_name } {{ impl_class_name }}({% call kt::to_ffi_call(cons) %}) {% endfor %} } + {% else if is_error %} + companion object ErrorHandler : UniffiRustCallStatusErrorHandler<{{ impl_class_name }}> { + override fun lift(error_buf: RustBuffer.ByValue): {{ impl_class_name }} { + // Due to some mismatches in the ffi converter mechanisms, errors are a RustBuffer. + val bb = error_buf.asByteBuffer() + if (bb == null) { + throw InternalException("?") + } + return {{ ffi_converter_name }}.read(bb) + } + } {% else %} companion object {% endif %} @@ -309,7 +327,7 @@ open class {{ impl_class_name }}: Disposable, AutoCloseable, {{ interface_name } {% include "CallbackInterfaceImpl.kt" %} {%- endif %} -public object {{ obj|ffi_converter_name }}: FfiConverter<{{ type_name }}, Pointer> { +public object {{ ffi_converter_name }}: FfiConverter<{{ type_name }}, Pointer> { {%- if obj.has_callback_interface() %} internal val handleMap = UniffiHandleMap<{{ type_name }}>() {%- endif %} diff --git a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py index 042ac504bf..d9c0f0d868 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py @@ -5,9 +5,12 @@ {% include "Protocol.py" %} +{% if ci.is_name_used_as_error(name) %} +class {{ impl_name }}(Exception): +{%- else %} class {{ impl_name }}: +{%- endif %} {%- call py::docstring(obj, 4) %} - _pointer: ctypes.c_void_p {%- match obj.primary_constructor() %} @@ -53,8 +56,7 @@ def {{ cons.name()|fn_name }}(cls, {% call py::arg_list_decl(cons) %}): {%- for meth in obj.methods() -%} {%- call py::method_decl(meth.name()|fn_name, meth) %} -{% endfor %} - +{%- endfor %} {%- for tm in obj.uniffi_traits() -%} {%- match tm %} {%- when UniffiTrait::Debug { fmt } %} @@ -75,8 +77,8 @@ def __ne__(self, other: object) -> {{ ne.return_type().unwrap()|type_name }}: return {{ ne.return_type().unwrap()|lift_fn }}({% call py::to_ffi_call_with_prefix("self._uniffi_clone_pointer()", ne) %}) {%- when UniffiTrait::Hash { hash } %} {%- call py::method_decl("__hash__", hash) %} -{% endmatch %} -{% endfor %} +{%- endmatch %} +{%- endfor %} {%- if obj.has_callback_interface() %} {%- let ffi_init_callback = obj.ffi_init_callback() %} @@ -85,6 +87,30 @@ def __ne__(self, other: object) -> {{ ne.return_type().unwrap()|type_name }}: {% include "CallbackInterfaceImpl.py" %} {%- endif %} +{# Objects as error #} +{%- if ci.is_name_used_as_error(name) %} +{# Due to some mismatches in the ffi converter mechanisms, errors are forced to be a RustBuffer #} +class {{ ffi_converter_name }}__as_error(_UniffiConverterRustBuffer): + @classmethod + def read(cls, buf): + raise NotImplementedError() + + @classmethod + def write(cls, value, buf): + raise NotImplementedError() + + @staticmethod + def lift(value): + # Errors are always a rust buffer holding a pointer - which is a "read" + with value.consume_with_stream() as stream: + return {{ ffi_converter_name }}.read(stream) + + @staticmethod + def lower(value): + raise NotImplementedError() + +{%- endif %} + class {{ ffi_converter_name }}: {%- if obj.has_callback_interface() %} _handle_map = _UniffiHandleMap() diff --git a/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py b/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py index be99e92e85..230b9e853f 100644 --- a/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py @@ -21,13 +21,7 @@ async def {{ func.name()|fn_name }}({%- call py::arg_list_decl(func) -%}) -> Non {%- when None %} lambda val: None, {% endmatch %} - # Error FFI converter - {%- match func.throws_type() %} - {%- when Some(e) %} - {{ e|ffi_converter_name }}, - {%- when None %} - None, - {%- endmatch %} + {% call py::error_ffi_converter(func) %} ) {%- else %} diff --git a/uniffi_bindgen/src/bindings/python/templates/macros.py b/uniffi_bindgen/src/bindings/python/templates/macros.py index 015ef75b44..c2f7cbaf30 100644 --- a/uniffi_bindgen/src/bindings/python/templates/macros.py +++ b/uniffi_bindgen/src/bindings/python/templates/macros.py @@ -5,27 +5,29 @@ #} {%- macro to_ffi_call(func) -%} - {%- match func.throws_type() -%} - {%- when Some with (e) -%} -_rust_call_with_error({{ e|ffi_converter_name }}, - {%- else -%} -_rust_call( - {%- endmatch -%} - _UniffiLib.{{ func.ffi_func().name() }}, - {%- call arg_list_lowered(func) -%} -) +{%- call _to_ffi_call_with_prefix_arg("", func) %} {%- endmacro -%} {%- macro to_ffi_call_with_prefix(prefix, func) -%} - {%- match func.throws_type() -%} - {%- when Some with (e) -%} -_rust_call_with_error( - {{ e|ffi_converter_name }}, - {%- else -%} +{%- call _to_ffi_call_with_prefix_arg(format!("{},", prefix), func) %} +{%- endmacro -%} + +{%- macro _to_ffi_call_with_prefix_arg(prefix, func) -%} +{%- match func.throws_type() -%} +{%- when Some with (e) -%} +{%- match e -%} +{%- when Type::Enum { name, module_path } -%} +_rust_call_with_error({{ e|ffi_converter_name }}, +{%- when Type::Object { name, module_path, imp } -%} +_rust_call_with_error({{ e|ffi_converter_name }}__as_error, +{%- else %} +# unsupported error type! +{%- endmatch %} +{%- else -%} _rust_call( - {%- endmatch -%} +{%- endmatch -%} _UniffiLib.{{ func.ffi_func().name() }}, - {{- prefix }}, + {{- prefix }} {%- call arg_list_lowered(func) -%} ) {%- endmacro -%} @@ -138,13 +140,7 @@ async def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}) -> None: {%- when None %} lambda val: None, {% endmatch %} - # Error FFI converter - {%- match meth.throws_type() %} - {%- when Some(e) %} - {{ e|ffi_converter_name }}, - {%- when None %} - None, - {%- endmatch %} + {% call error_ffi_converter(meth) %} ) {%- else -%} @@ -169,3 +165,20 @@ def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}) -> None: {% endif %} {% endmacro %} + +{%- macro error_ffi_converter(func) %} + # Error FFI converter +{% match func.throws_type() %} +{%- when Some(e) %} +{%- match e -%} +{%- when Type::Enum { name, module_path } -%} + {{ e|ffi_converter_name }}, +{%- when Type::Object { name, module_path, imp } -%} + {{ e|ffi_converter_name }}__as_error, +{%- else %} + # unsupported error type! +{%- endmatch %} +{%- when None %} + None, +{%- endmatch %} +{% endmacro %} diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs index 3969f6094e..e8933b7b39 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs @@ -585,6 +585,15 @@ pub mod filters { Ok(oracle().find(&as_type.as_type()).ffi_converter_name()) } + pub fn ffi_error_converter_name(as_type: &impl AsType) -> Result { + // special handling for types used as errors. + let mut name = oracle().find(&as_type.as_type()).ffi_converter_name(); + if matches!(&as_type.as_type(), Type::Object { .. }) { + name.push_str("__as_error") + } + Ok(name) + } + pub fn lower_fn(as_type: &impl AsType) -> Result { Ok(oracle().find(&as_type.as_type()).lower()) } diff --git a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift index 202d1974e4..317b538313 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift @@ -16,10 +16,6 @@ public enum {{ type_name }} { {% endfor %} {%- endif %} - - fileprivate static func uniffiErrorHandler(_ error: RustBuffer) throws -> Error { - return try {{ ffi_converter_name }}.lift(error) - } } diff --git a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift index 2a5db462ac..c42c50803e 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift @@ -3,6 +3,8 @@ {%- let methods = obj.methods() %} {%- let protocol_docstring = obj.docstring() %} +{%- let is_error = ci.is_name_used_as_error(name) %} + {% include "Protocol.swift" %} {%- call swift::docstring(obj, 0) %} @@ -20,6 +22,9 @@ open class {{ impl_class_name }}: {%- else %} {%- endmatch %} {%- endfor %} + {%- if is_error %} + Error, + {% endif %} {{ protocol_name }} { fileprivate let pointer: UnsafeMutableRawPointer! @@ -95,7 +100,7 @@ open class {{ impl_class_name }}: {%- endmatch %} {%- match meth.throws_type() %} {%- when Some with (e) %} - errorHandler: {{ e|ffi_converter_name }}.lift + errorHandler: {{ e|ffi_error_converter_name }}.lift {%- else %} errorHandler: nil {% endmatch %} @@ -207,6 +212,29 @@ public struct {{ ffi_converter_name }}: FfiConverter { } } +{# Objects as error #} +{%- if is_error %} +{# Due to some mismatches in the ffi converter mechanisms, errors are a RustBuffer holding a pointer #} +public struct {{ ffi_converter_name }}__as_error: FfiConverterRustBuffer { + public static func lift(_ buf: RustBuffer) throws -> {{ type_name }} { + var reader = createReader(data: Data(rustBuffer: buf)) + return try {{ ffi_converter_name }}.read(from: &reader) + } + + public static func lower(_ value: {{ type_name }}) -> RustBuffer { + fatalError("not implemented") + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { + fatalError("not implemented") + } + + public static func write(_ value: {{ type_name }}, into buf: inout [UInt8]) { + fatalError("not implemented") + } +} +{%- endif %} + {# We always write these public functions just in case the enum is used as an external type by another crate. diff --git a/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift index a258aa7bb6..2e6870cacb 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift @@ -21,7 +21,7 @@ public func {{ func.name()|fn_name }}({%- call swift::arg_list_decl(func) -%}) a {%- endmatch %} {%- match func.throws_type() %} {%- when Some with (e) %} - errorHandler: {{ e|ffi_converter_name }}.lift + errorHandler: {{ e|ffi_error_converter_name }}.lift {%- else %} errorHandler: nil {% endmatch %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/macros.swift b/uniffi_bindgen/src/bindings/swift/templates/macros.swift index fa7385e1ae..2da9398c26 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/macros.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/macros.swift @@ -8,7 +8,7 @@ {%- call try(func) -%} {%- match func.throws_type() -%} {%- when Some with (e) -%} - rustCallWithError({{ e|ffi_converter_name }}.lift) { + rustCallWithError({{ e|ffi_error_converter_name }}.lift) { {%- else -%} rustCall() { {%- endmatch %} @@ -20,7 +20,7 @@ {% call try(func) %} {%- match func.throws_type() %} {%- when Some with (e) %} - rustCallWithError({{ e|ffi_converter_name }}.lift) { + rustCallWithError({{ e|ffi_error_converter_name }}.lift) { {%- else %} rustCall() { {% endmatch %} diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index fe90fd5a14..c7e083df9d 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -1066,7 +1066,7 @@ fn throws_name(throws: &Option) -> Option<&str> { // Type has no `name()` method, just `canonical_name()` which isn't what we want. match throws { None => None, - Some(Type::Enum { name, .. }) => Some(name), + Some(Type::Enum { name, .. }) | Some(Type::Object { name, .. }) => Some(name), _ => panic!("unknown throw type: {throws:?}"), } }