From f0041800c48293d631a2f425861726a5440e245b Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Thu, 15 Jun 2023 10:54:30 -0400 Subject: [PATCH] Added suport for using external types in proc-macros Moved the FfiConverter implementation for callback interfaces and traits from a declarative macro in `uniffi_core` to a proc-macro in `uniffi_macros`. This offers better `UniFfiTag` handling. Now `FfiConverter` is implemented for all tags for callback interfaces/traits that are wrapped with the proc-macro attributes. This matches the behavior for other types and will help fix #1531. Changed the object FfiConverter to be inside a proc-macro, rather than using the `Interface` trait. This more flexibility and avoids conflicting impls. For example, this allows for both the objects and trait interface `FfiConverters`s to be implemented on `Arc`. I also think it's nicer with less indirection. One drawback is that libraries can't implement `FfiConverter` on Arc because of the orphan rules. To get around this, I added the `FfiConverterArc` trait. Other changes: - Adding `module_path` to the user type metadata - Added an proc-macro -> proc-macro external type test by copying the ext-types fixture into another one that uses proc macros. --- CHANGELOG.md | 1 + Cargo.toml | 1 + docs/manual/src/proc_macro/index.md | 23 ++ fixtures/ext-types/README.md | 6 + fixtures/ext-types/proc-macro-lib/Cargo.toml | 37 +++ fixtures/ext-types/proc-macro-lib/build.rs | 7 + .../proc-macro-lib/src/ext-types-lib.udl | 1 + fixtures/ext-types/proc-macro-lib/src/lib.rs | 139 ++++++++ .../tests/bindings/test_imported_types.kts | 38 +++ .../tests/bindings/test_imported_types.py | 52 +++ .../tests/bindings/test_imported_types.swift | 36 ++ .../tests/test_generated_bindings.rs | 5 + fixtures/ext-types/uniffi-one/src/lib.rs | 5 + fixtures/metadata/src/tests.rs | 15 + .../ui/interface_not_sync_and_send.stderr | 13 +- uniffi/tests/ui/proc_macro_arc.stderr | 8 +- .../templates/ForeignExecutorTemplate.swift | 8 +- uniffi_bindgen/src/interface/types/mod.rs | 25 +- .../templates/CallbackInterfaceTemplate.rs | 2 +- .../templates/ExternalTypesTemplate.rs | 1 + .../scaffolding/templates/ObjectTemplate.rs | 2 +- uniffi_core/src/ffi_converter_impls.rs | 68 +--- uniffi_core/src/ffi_converter_traits.rs | 255 +++++++++++++++ uniffi_core/src/lib.rs | 309 +++--------------- uniffi_macros/src/enum_.rs | 5 + uniffi_macros/src/error.rs | 5 + uniffi_macros/src/export.rs | 74 ++++- .../src/export/callback_interface.rs | 68 +++- uniffi_macros/src/lib.rs | 88 ++++- uniffi_macros/src/object.rs | 89 ++++- uniffi_macros/src/record.rs | 14 +- uniffi_macros/src/util.rs | 19 +- uniffi_meta/src/group.rs | 177 +++++++++- uniffi_meta/src/lib.rs | 16 + uniffi_meta/src/reader.rs | 13 +- 35 files changed, 1252 insertions(+), 373 deletions(-) create mode 100644 fixtures/ext-types/README.md create mode 100644 fixtures/ext-types/proc-macro-lib/Cargo.toml create mode 100644 fixtures/ext-types/proc-macro-lib/build.rs create mode 100644 fixtures/ext-types/proc-macro-lib/src/ext-types-lib.udl create mode 100644 fixtures/ext-types/proc-macro-lib/src/lib.rs create mode 100644 fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.kts create mode 100644 fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.py create mode 100644 fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.swift create mode 100644 fixtures/ext-types/proc-macro-lib/tests/test_generated_bindings.rs create mode 100644 uniffi_core/src/ffi_converter_traits.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a46f08f972..3a0c134314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ - No more implicit conversion to integers/floats in Ruby ([#1596](https://github.com/mozilla/uniffi-rs/pull/1596)) - Updated Rust dependencies ([#1495](https://github.com/mozilla/uniffi-rs/pull/1495), [#1583](https://github.com/mozilla/uniffi-rs/pull/1583), [#1569](https://github.com/mozilla/uniffi-rs/pull/1569)) - Added type checking to strings/bytes for Python/Ruby ([#1597](https://github.com/mozilla/uniffi-rs/pull/1597#)) +- Implemented proc-macro external type support. This allows proc-macros to use types defined in UDL files from other crates, [#1600](https://github.com/mozilla/uniffi-rs/pull/1600) ### Guidance for external bindings diff --git a/Cargo.toml b/Cargo.toml index 3f06b0ddd2..cd7ddd593d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "fixtures/ext-types/guid", "fixtures/ext-types/uniffi-one", "fixtures/ext-types/lib", + "fixtures/ext-types/proc-macro-lib", # we should roll the above and below up somehow that makes sense... "fixtures/external-types/crate-one", diff --git a/docs/manual/src/proc_macro/index.md b/docs/manual/src/proc_macro/index.md index a62dc9a85a..0e609cdd42 100644 --- a/docs/manual/src/proc_macro/index.md +++ b/docs/manual/src/proc_macro/index.md @@ -276,6 +276,29 @@ impl From for MyApiError { } ``` +## Types from dependent crates + +When using proc-macros, you can use types from dependent crates in your exported library, as long as +the dependent crate annotates the type with one of the UniFFI derives. However, there are a couple +exceptions: + +### Types from UDL-based dependent crates + +If the dependent crate uses a UDL file to define their types, then you must invoke one of the +`uniffi::use_udl_*!` macros, for example: + +```rust +uniffi::use_udl_record!(dependent_crate, RecordType); +uniffi::use_udl_enum!(dependent_crate, EnumType); +uniffi::use_udl_error!(dependent_crate, ErrorType); +uniffi::use_udl_object!(dependent_crate, ObjectType); +``` + +### Non-UniFFI types from dependent crates + +If the dependent crate doesn't define the type in a UDL file or use one of the UniFFI derive macros, +then it's currently not possible to use them in an proc-macro exported interface. However, we hope +to fix this limitation soon. ## Other limitations diff --git a/fixtures/ext-types/README.md b/fixtures/ext-types/README.md new file mode 100644 index 0000000000..91f9ba421a --- /dev/null +++ b/fixtures/ext-types/README.md @@ -0,0 +1,6 @@ +This directory contains the tests for external types -- types defined in one crate and used in a +different one. + +- `guid` and `uniffi-one` are dependent crates that define types exported by UniFFI +- `lib` is a library crate that depends on `guid` and `uniffi-one` +- `proc-macro-lib` is another library crate, but this one uses proc-macros rather than UDL files diff --git a/fixtures/ext-types/proc-macro-lib/Cargo.toml b/fixtures/ext-types/proc-macro-lib/Cargo.toml new file mode 100644 index 0000000000..3286ddaa34 --- /dev/null +++ b/fixtures/ext-types/proc-macro-lib/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "uniffi-fixture-ext-types-proc-macro" +edition = "2021" +version = "0.22.0" +authors = ["Firefox Sync Team "] +license = "MPL-2.0" +publish = false + +[package.metadata.uniffi.testing] +external-crates = [ + "uniffi-fixture-ext-types-guid", + "uniffi-fixture-ext-types-lib-one", + "uniffi-example-custom-types", +] + +[lib] +crate-type = ["lib", "cdylib"] +name = "uniffi_ext_types_proc_macro_lib" + +[dependencies] +anyhow = "1" +bytes = "1.3" +uniffi = {path = "../../../uniffi"} + +uniffi-fixture-ext-types-lib-one = {path = "../uniffi-one"} +uniffi-fixture-ext-types-guid = {path = "../guid"} + +# Reuse one of our examples. +uniffi-example-custom-types = {path = "../../../examples/custom-types"} + +url = "2.2" + +[build-dependencies] +uniffi = {path = "../../../uniffi", features = ["build"] } + +[dev-dependencies] +uniffi = {path = "../../../uniffi", features = ["bindgen-tests"] } diff --git a/fixtures/ext-types/proc-macro-lib/build.rs b/fixtures/ext-types/proc-macro-lib/build.rs new file mode 100644 index 0000000000..376a44c55a --- /dev/null +++ b/fixtures/ext-types/proc-macro-lib/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/ext-types-lib.udl").unwrap(); +} diff --git a/fixtures/ext-types/proc-macro-lib/src/ext-types-lib.udl b/fixtures/ext-types/proc-macro-lib/src/ext-types-lib.udl new file mode 100644 index 0000000000..dbaf61ae64 --- /dev/null +++ b/fixtures/ext-types/proc-macro-lib/src/ext-types-lib.udl @@ -0,0 +1 @@ +namespace imported_types_lib { }; diff --git a/fixtures/ext-types/proc-macro-lib/src/lib.rs b/fixtures/ext-types/proc-macro-lib/src/lib.rs new file mode 100644 index 0000000000..def13212e1 --- /dev/null +++ b/fixtures/ext-types/proc-macro-lib/src/lib.rs @@ -0,0 +1,139 @@ +use custom_types::Handle; +use ext_types_guid::Guid; +use std::sync::Arc; +use uniffi_one::{UniffiOneEnum, UniffiOneInterface, UniffiOneProcMacroType, UniffiOneType}; +use url::Url; + +uniffi::use_udl_record!(uniffi_one, UniffiOneType); +uniffi::use_udl_enum!(uniffi_one, UniffiOneEnum); +uniffi::use_udl_object!(uniffi_one, UniffiOneInterface); +uniffi::use_udl_record!(ext_types_guid, Guid); +uniffi::use_udl_record!(custom_types, Url); +uniffi::use_udl_record!(custom_types, Handle); + +#[derive(uniffi::Record)] +pub struct CombinedType { + pub uoe: UniffiOneEnum, + pub uot: UniffiOneType, + pub uots: Vec, + pub maybe_uot: Option, + + pub guid: Guid, + pub guids: Vec, + pub maybe_guid: Option, + + pub url: Url, + pub urls: Vec, + pub maybe_url: Option, + + pub handle: Handle, + pub handles: Vec, + pub maybe_handle: Option, +} + +#[uniffi::export] +fn get_combined_type(value: Option) -> CombinedType { + value.unwrap_or_else(|| CombinedType { + uoe: UniffiOneEnum::One, + uot: UniffiOneType { + sval: "hello".to_string(), + }, + uots: vec![ + UniffiOneType { + sval: "first of many".to_string(), + }, + UniffiOneType { + sval: "second of many".to_string(), + }, + ], + maybe_uot: None, + + guid: Guid("a-guid".into()), + guids: vec![Guid("b-guid".into()), Guid("c-guid".into())], + maybe_guid: None, + + url: Url::parse("http://example.com/").unwrap(), + urls: vec![], + maybe_url: None, + + handle: Handle(123), + handles: vec![Handle(1), Handle(2), Handle(3)], + maybe_handle: Some(Handle(4)), + }) +} + +// A Custom type +#[uniffi::export] +fn get_url(url: Url) -> Url { + url +} + +#[uniffi::export] +fn get_urls(urls: Vec) -> Vec { + urls +} + +#[uniffi::export] +fn get_maybe_url(url: Option) -> Option { + url +} + +#[uniffi::export] +fn get_maybe_urls(urls: Vec>) -> Vec> { + urls +} + +// A struct +#[uniffi::export] +fn get_uniffi_one_type(t: UniffiOneType) -> UniffiOneType { + t +} + +// Test using a type defined in a proc-macro in another crate +#[uniffi::export] +fn get_uniffi_one_proc_macro_type(t: UniffiOneProcMacroType) -> UniffiOneProcMacroType { + t +} + +#[uniffi::export] +fn get_uniffi_one_types(ts: Vec) -> Vec { + ts +} + +#[uniffi::export] +fn get_maybe_uniffi_one_type(t: Option) -> Option { + t +} + +#[uniffi::export] +fn get_maybe_uniffi_one_types(ts: Vec>) -> Vec> { + ts +} + +// An enum +#[uniffi::export] +fn get_uniffi_one_enum(e: UniffiOneEnum) -> UniffiOneEnum { + e +} + +#[uniffi::export] +fn get_uniffi_one_enums(es: Vec) -> Vec { + es +} + +#[uniffi::export] +fn get_maybe_uniffi_one_enum(e: Option) -> Option { + e +} + +#[uniffi::export] +fn get_maybe_uniffi_one_enums(es: Vec>) -> Vec> { + es +} + +#[uniffi::export] +fn get_uniffi_one_interface() -> Arc { + Arc::new(UniffiOneInterface::new()) +} + +uniffi::include_scaffolding!("ext-types-lib"); diff --git a/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.kts b/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.kts new file mode 100644 index 0000000000..1338eb27cd --- /dev/null +++ b/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.kts @@ -0,0 +1,38 @@ +/* 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.imported_types_lib.* +import uniffi.uniffi_one.* + +val ct = getCombinedType(null) +assert(ct.uot.sval == "hello") +assert(ct.guid == "a-guid") +assert(ct.url == java.net.URL("http://example.com/")) + +val ct2 = getCombinedType(ct) +assert(ct == ct2) + +val url = java.net.URL("http://example.com/") +assert(getUrl(url) == url) +assert(getMaybeUrl(url)!! == url) +assert(getMaybeUrl(null) == null) +assert(getUrls(listOf(url)) == listOf(url)) +assert(getMaybeUrls(listOf(url, null)) == listOf(url, null)) + +val uot = UniffiOneType("hello") +assert(getUniffiOneType(uot) == uot) +assert(getMaybeUniffiOneType(uot)!! == uot) +assert(getMaybeUniffiOneType(null) == null) +assert(getUniffiOneTypes(listOf(uot)) == listOf(uot)) +assert(getMaybeUniffiOneTypes(listOf(uot, null)) == listOf(uot, null)) + +val uopmt = UniffiOneProcMacroType("hello from proc-macro world") +assert(getUniffiOneProcMacroType(uopmt) == uopmt) + +val uoe = UniffiOneEnum.ONE +assert(getUniffiOneEnum(uoe) == uoe) +assert(getMaybeUniffiOneEnum(uoe)!! == uoe) +assert(getMaybeUniffiOneEnum(null) == null) +assert(getUniffiOneEnums(listOf(uoe)) == listOf(uoe)) +assert(getMaybeUniffiOneEnums(listOf(uoe, null)) == listOf(uoe, null)) diff --git a/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.py b/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.py new file mode 100644 index 0000000000..d36b233782 --- /dev/null +++ b/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.py @@ -0,0 +1,52 @@ +# 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 unittest +import urllib +from imported_types_lib import * +from uniffi_one import * + +class TestIt(unittest.TestCase): + def test_it(self): + ct = get_combined_type(None) + self.assertEqual(ct.uot.sval, "hello") + self.assertEqual(ct.guid, "a-guid") + self.assertEqual(ct.url.scheme, 'http') + self.assertEqual(ct.url.netloc, 'example.com') + self.assertEqual(ct.url.path, '/') + + ct2 = get_combined_type(ct) + self.assertEqual(ct, ct2) + + def test_get_url(self): + url = urllib.parse.urlparse("http://example.com/") + self.assertEqual(get_url(url), url) + self.assertEqual(get_urls([url]), [url]) + self.assertEqual(get_maybe_url(url), url) + self.assertEqual(get_maybe_url(None), None) + self.assertEqual(get_maybe_urls([url, None]), [url, None]) + + def test_get_uniffi_one_type(self): + t1 = UniffiOneType("hello") + self.assertEqual(t1, get_uniffi_one_type(t1)) + self.assertEqual(t1, get_maybe_uniffi_one_type(t1)) + self.assertEqual(None, get_maybe_uniffi_one_type(None)) + self.assertEqual([t1], get_uniffi_one_types([t1])) + self.assertEqual([t1, None], get_maybe_uniffi_one_types([t1, None])) + + def test_get_uniffi_one_proc_macro_type(self): + t1 = UniffiOneProcMacroType("hello") + self.assertEqual(t1, get_uniffi_one_proc_macro_type(t1)) + + def test_get_uniffi_one_enum(self): + e = UniffiOneEnum.ONE + self.assertEqual(e, get_uniffi_one_enum(e)) + self.assertEqual(e, get_maybe_uniffi_one_enum(e)) + self.assertEqual(None, get_maybe_uniffi_one_enum(None)) + self.assertEqual([e], get_uniffi_one_enums([e])) + self.assertEqual([e, None], get_maybe_uniffi_one_enums([e, None])) + + +if __name__=='__main__': + unittest.main() diff --git a/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.swift b/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.swift new file mode 100644 index 0000000000..09b76d30ca --- /dev/null +++ b/fixtures/ext-types/proc-macro-lib/tests/bindings/test_imported_types.swift @@ -0,0 +1,36 @@ +/* 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 imported_types_lib +//import uniffi_one +import Foundation + +let ct = getCombinedType(value: nil) +assert(ct.uot.sval == "hello") +assert(ct.guid == "a-guid") +assert(ct.url == URL(string: "http://example.com/")) + +let ct2 = getCombinedType(value: ct) +assert(ct == ct2) + +let url = URL(string: "http://example.com/")!; +assert(getUrl(url: url) == url) +assert(getMaybeUrl(url: url)! == url) +assert(getMaybeUrl(url: nil) == nil) +assert(getUrls(urls: [url]) == [url]) +assert(getMaybeUrls(urls: [url, nil]) == [url, nil]) + +assert(getUniffiOneType(t: UniffiOneType(sval: "hello")).sval == "hello") +assert(getMaybeUniffiOneType(t: UniffiOneType(sval: "hello"))!.sval == "hello") +assert(getMaybeUniffiOneType(t: nil) == nil) +assert(getUniffiOneTypes(ts: [UniffiOneType(sval: "hello")]) == [UniffiOneType(sval: "hello")]) +assert(getMaybeUniffiOneTypes(ts: [UniffiOneType(sval: "hello"), nil]) == [UniffiOneType(sval: "hello"), nil]) + +assert(getUniffiOneProcMacroType(t: UniffiOneProcMacroType(sval: "hello from proc-macro world")).sval == "hello from proc-macro world") + +assert(getUniffiOneEnum(e: UniffiOneEnum.one) == UniffiOneEnum.one) +assert(getMaybeUniffiOneEnum(e: UniffiOneEnum.one)! == UniffiOneEnum.one) +assert(getMaybeUniffiOneEnum(e: nil) == nil) +assert(getUniffiOneEnums(es: [UniffiOneEnum.one]) == [UniffiOneEnum.one]) +assert(getMaybeUniffiOneEnums(es: [UniffiOneEnum.one, nil]) == [UniffiOneEnum.one, nil]) diff --git a/fixtures/ext-types/proc-macro-lib/tests/test_generated_bindings.rs b/fixtures/ext-types/proc-macro-lib/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..0ca9c68d64 --- /dev/null +++ b/fixtures/ext-types/proc-macro-lib/tests/test_generated_bindings.rs @@ -0,0 +1,5 @@ +uniffi::build_foreign_language_testcases!( + "tests/bindings/test_imported_types.kts", + "tests/bindings/test_imported_types.py", + "tests/bindings/test_imported_types.swift", +); diff --git a/fixtures/ext-types/uniffi-one/src/lib.rs b/fixtures/ext-types/uniffi-one/src/lib.rs index d80965b025..41e0388572 100644 --- a/fixtures/ext-types/uniffi-one/src/lib.rs +++ b/fixtures/ext-types/uniffi-one/src/lib.rs @@ -9,6 +9,11 @@ pub enum UniffiOneEnum { Two, } +#[derive(uniffi::Record)] +pub struct UniffiOneProcMacroType { + pub sval: String, +} + #[derive(Default)] pub struct UniffiOneInterface { current: AtomicI32, diff --git a/fixtures/metadata/src/tests.rs b/fixtures/metadata/src/tests.rs index 411dfc2abb..b802dc24ec 100644 --- a/fixtures/metadata/src/tests.rs +++ b/fixtures/metadata/src/tests.rs @@ -132,12 +132,15 @@ mod test_type_ids { #[test] fn test_user_types() { check_type_id::(Type::Record { + module_path: "uniffi_fixture_metadata".into(), name: "Person".into(), }); check_type_id::(Type::Enum { + module_path: "uniffi_fixture_metadata".into(), name: "Weapon".into(), }); check_type_id::>(Type::ArcObject { + module_path: "uniffi_fixture_metadata".into(), object_name: "Calculator".into(), is_trait: false, }); @@ -243,6 +246,7 @@ mod test_metadata { fields: vec![FieldMetadata { name: "result".into(), ty: Type::Record { + module_path: "uniffi_fixture_metadata".into(), name: "Person".into(), }, default: None, @@ -303,6 +307,7 @@ mod test_metadata { fields: vec![FieldMetadata { name: "weapon".into(), ty: Type::Enum { + module_path: "uniffi_fixture_metadata".into(), name: "Weapon".into(), }, default: None, @@ -399,12 +404,14 @@ mod test_function_metadata { FnParamMetadata { name: "person".into(), ty: Type::Record { + module_path: "uniffi_fixture_metadata".into(), name: "Person".into(), }, }, FnParamMetadata { name: "weapon".into(), ty: Type::Enum { + module_path: "uniffi_fixture_metadata".into(), name: "Weapon".into(), }, }, @@ -443,9 +450,11 @@ mod test_function_metadata { is_async: false, inputs: vec![], return_type: Some(Type::Enum { + module_path: "uniffi_fixture_metadata".into(), name: "State".into(), }), throws: Some(Type::Enum { + module_path: "uniffi_fixture_metadata".into(), name: "FlatError".into(), }), checksum: UNIFFI_META_CONST_UNIFFI_FIXTURE_METADATA_FUNC_TEST_FUNC_THAT_THROWS @@ -465,6 +474,7 @@ mod test_function_metadata { inputs: vec![], return_type: None, throws: Some(Type::Enum { + module_path: "uniffi_fixture_metadata".into(), name: "FlatError".into(), }), checksum: @@ -513,12 +523,14 @@ mod test_function_metadata { FnParamMetadata { name: "person".into(), ty: Type::Record { + module_path: "uniffi_fixture_metadata".into(), name: "Person".into(), }, }, FnParamMetadata { name: "weapon".into(), ty: Type::Enum { + module_path: "uniffi_fixture_metadata".into(), name: "Weapon".into(), }, }, @@ -540,9 +552,11 @@ mod test_function_metadata { is_async: true, inputs: vec![], return_type: Some(Type::Enum { + module_path: "uniffi_fixture_metadata".into(), name: "State".into(), }), throws: Some(Type::Enum { + module_path: "uniffi_fixture_metadata".into(), name: "FlatError".into(), }), checksum: @@ -590,6 +604,7 @@ mod test_function_metadata { is_async: false, inputs: vec![], return_type: Some(Type::ArcObject { + module_path: "uniffi_fixture_metadata".into(), object_name: "CalculatorDisplay".into(), is_trait: true, }), diff --git a/fixtures/uitests/tests/ui/interface_not_sync_and_send.stderr b/fixtures/uitests/tests/ui/interface_not_sync_and_send.stderr index bcc8c73755..1c47a23a73 100644 --- a/fixtures/uitests/tests/ui/interface_not_sync_and_send.stderr +++ b/fixtures/uitests/tests/ui/interface_not_sync_and_send.stderr @@ -1,8 +1,8 @@ error[E0277]: `Cell` cannot be shared between threads safely --> $OUT_DIR[uniffi_uitests]/counter.uniffi.rs | - | struct r#Counter { } - | ^^^^^^^^^ `Cell` cannot be shared between threads safely + | uniffi::deps::static_assertions::assert_impl_all!(r#Counter: Sync, Send); + | ^^^^^^^^^ `Cell` cannot be shared between threads safely | = help: within `Counter`, the trait `Sync` is not implemented for `Cell` = note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicU32` instead @@ -11,8 +11,9 @@ note: required because it appears within the type `Counter` | 9 | pub struct Counter { | ^^^^^^^ -note: required by a bound in `Interface` - --> $WORKSPACE/uniffi_core/src/lib.rs +note: required by a bound in `assert_impl_all` + --> $OUT_DIR[uniffi_uitests]/counter.uniffi.rs | - | pub trait Interface: Send + Sync + Sized { - | ^^^^ required by this bound in `Interface` + | uniffi::deps::static_assertions::assert_impl_all!(r#Counter: Sync, Send); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_impl_all` + = note: this error originates in the macro `uniffi::deps::static_assertions::assert_impl_all` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/uniffi/tests/ui/proc_macro_arc.stderr b/uniffi/tests/ui/proc_macro_arc.stderr index acbf36f232..e12ebbe671 100644 --- a/uniffi/tests/ui/proc_macro_arc.stderr +++ b/uniffi/tests/ui/proc_macro_arc.stderr @@ -1,8 +1,8 @@ -error[E0277]: the trait bound `Foo: Interface` is not satisfied +error[E0277]: the trait bound `Foo: FfiConverterArc` is not satisfied --> tests/ui/proc_macro_arc.rs:10:1 | 10 | #[uniffi::export] - | ^^^^^^^^^^^^^^^^^ the trait `Interface` is not implemented for `Foo` + | ^^^^^^^^^^^^^^^^^ the trait `FfiConverterArc` is not implemented for `Foo` | = help: the trait `FfiConverter` is implemented for `Arc` = note: required for `Arc` to implement `FfiConverter` @@ -16,11 +16,11 @@ note: erroneous constant used | = note: this note originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0277]: the trait bound `child::Foo: Interface` is not satisfied +error[E0277]: the trait bound `child::Foo: FfiConverterArc` is not satisfied --> tests/ui/proc_macro_arc.rs:20:5 | 20 | #[uniffi::export] - | ^^^^^^^^^^^^^^^^^ the trait `Interface` is not implemented for `child::Foo` + | ^^^^^^^^^^^^^^^^^ the trait `FfiConverterArc` is not implemented for `child::Foo` | = help: the trait `FfiConverter` is implemented for `Arc` = note: required for `Arc` to implement `FfiConverter` diff --git a/uniffi_bindgen/src/bindings/swift/templates/ForeignExecutorTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ForeignExecutorTemplate.swift index 92276a4737..d56510ae70 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ForeignExecutorTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ForeignExecutorTemplate.swift @@ -19,17 +19,17 @@ fileprivate struct FfiConverterForeignExecutor: FfiConverter { // let's use `Int`, which is equivalent to `size_t` typealias FfiType = Int - static func lift(_ value: FfiType) throws -> SwiftType { + public static func lift(_ value: FfiType) throws -> SwiftType { UniFfiForeignExecutor(priority: TaskPriority(rawValue: numericCast(value))) } - static func lower(_ value: SwiftType) -> FfiType { + public static func lower(_ value: SwiftType) -> FfiType { numericCast(value.priority.rawValue) } - static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { fatalError("FfiConverterForeignExecutor.read not implemented yet") } - static func write(_ value: SwiftType, into buf: inout [UInt8]) { + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { fatalError("FfiConverterForeignExecutor.read not implemented yet") } } diff --git a/uniffi_bindgen/src/interface/types/mod.rs b/uniffi_bindgen/src/interface/types/mod.rs index cdefb68db9..ba518c979d 100644 --- a/uniffi_bindgen/src/interface/types/mod.rs +++ b/uniffi_bindgen/src/interface/types/mod.rs @@ -238,17 +238,18 @@ impl From for Type { Ty::SystemTime => Type::Timestamp, Ty::Duration => Type::Duration, Ty::ForeignExecutor => Type::ForeignExecutor, - Ty::Record { name } => Type::Record(name), + Ty::Record { name, .. } => Type::Record(name), Ty::Enum { name, .. } => Type::Enum(name), Ty::ArcObject { object_name, is_trait, + .. } => Type::Object { name: object_name, imp: ObjectImpl::from_is_trait(is_trait), }, - Ty::CallbackInterface { name } => Type::CallbackInterface(name), - Ty::Custom { name, builtin } => Type::Custom { + Ty::CallbackInterface { name, .. } => Type::CallbackInterface(name), + Ty::Custom { name, builtin, .. } => Type::Custom { name, builtin: builtin.into(), }, @@ -258,6 +259,15 @@ impl From for Type { key_type, value_type, } => Type::Map(key_type.into(), value_type.into()), + Ty::External { + module_path, + name, + kind, + } => Type::External { + crate_name: module_path.split(':').next().unwrap().to_string(), + name, + kind: kind.into(), + }, } } } @@ -274,6 +284,15 @@ impl From> for Box { } } +impl From for ExternalKind { + fn from(kind: uniffi_meta::ExternalKind) -> Self { + match kind { + uniffi_meta::ExternalKind::DataClass => Self::DataClass, + uniffi_meta::ExternalKind::Interface => Self::Interface, + } + } +} + /// The set of all possible types used in a particular component interface. /// /// Every component API uses a finite number of types, including primitive types, API-defined diff --git a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs index a4cbe05073..a196f76a9b 100644 --- a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs @@ -79,4 +79,4 @@ impl r#{{ trait_name }} for {{ trait_impl }} { {%- endfor %} } -::uniffi::ffi_converter_callback_interface!(r#{{ trait_name }}, {{ trait_impl }}, "{{ cbi.name() }}", crate::UniFfiTag); +::uniffi::scaffolding_ffi_converter_callback_interface!(r#{{ trait_name }}, {{ trait_impl }}); diff --git a/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs index 746f2d29ac..b09778de02 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs @@ -50,6 +50,7 @@ unsafe impl uniffi::FfiConverter for r#{{ name }} { ::uniffi::ffi_converter_default_return!(crate::UniFfiTag); const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TYPE_CUSTOM) + .concat_str("{{ ci.namespace() }}") .concat_str("{{ name }}") .concat({{ builtin|ffi_converter }}::TYPE_ID_META); } diff --git a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs index c37b284f14..afe498e799 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs @@ -12,7 +12,7 @@ {%- match obj.imp() -%} {%- when ObjectImpl::Trait %} -::uniffi::ffi_converter_trait_decl!({{ obj.rust_name() }}, "{{ obj.name() }}", crate::UniFfiTag); +::uniffi::scaffolding_ffi_converter_trait_interface!(r#{{ obj.name() }}); {% else %} #[::uniffi::ffi_converter_interface(tag = crate::UniFfiTag)] struct {{ obj.rust_name() }} { } diff --git a/uniffi_core/src/ffi_converter_impls.rs b/uniffi_core/src/ffi_converter_impls.rs index 003facdf21..789014c9f3 100644 --- a/uniffi_core/src/ffi_converter_impls.rs +++ b/uniffi_core/src/ffi_converter_impls.rs @@ -24,7 +24,7 @@ use crate::{ check_remaining, ffi_converter_default_return, ffi_converter_rust_buffer_lift_and_lower, lower_into_rust_buffer, metadata, try_lift_from_rust_buffer, FfiConverter, FutureCallback, - Interface, MetadataBuffer, Result, RustBuffer, RustCallStatus, UnexpectedUniFFICallbackError, + MetadataBuffer, Result, RustBuffer, RustCallStatus, UnexpectedUniFFICallbackError, }; use anyhow::bail; use bytes::buf::{Buf, BufMut}; @@ -427,72 +427,6 @@ where .concat(V::TYPE_ID_META); } -/// Support for passing reference-counted shared objects via the FFI. -/// -/// To avoid dealing with complex lifetime semantics over the FFI, any data passed -/// by reference must be encapsulated in an `Arc`, and must be safe to share -/// across threads. -unsafe impl> FfiConverter for std::sync::Arc { - // Don't use a pointer to as that requires a `pub ` - type FfiType = *const std::os::raw::c_void; - - /// When lowering, we have an owned `Arc` and we transfer that ownership - /// to the foreign-language code, "leaking" it out of Rust's ownership system - /// as a raw pointer. This works safely because we have unique ownership of `self`. - /// The foreign-language code is responsible for freeing this by calling the - /// `ffi_object_free` FFI function provided by the corresponding UniFFI type. - /// - /// Safety: when freeing the resulting pointer, the foreign-language code must - /// call the destructor function specific to the type `T`. Calling the destructor - /// function for other types may lead to undefined behaviour. - fn lower(obj: std::sync::Arc) -> Self::FfiType { - std::sync::Arc::into_raw(obj) as Self::FfiType - } - - /// When lifting, we receive a "borrow" of the `Arc` that is owned by - /// the foreign-language code, and make a clone of it for our own use. - /// - /// Safety: the provided value must be a pointer previously obtained by calling - /// the `lower()` or `write()` method of this impl. - fn try_lift(v: Self::FfiType) -> Result> { - let v = v as *const T; - // We musn't drop the `Arc` that is owned by the foreign-language code. - let foreign_arc = std::mem::ManuallyDrop::new(unsafe { std::sync::Arc::::from_raw(v) }); - // Take a clone for our own use. - Ok(std::sync::Arc::clone(&*foreign_arc)) - } - - /// When writing as a field of a complex structure, make a clone and transfer ownership - /// of it to the foreign-language code by writing its pointer into the buffer. - /// The foreign-language code is responsible for freeing this by calling the - /// `ffi_object_free` FFI function provided by the corresponding UniFFI type. - /// - /// Safety: when freeing the resulting pointer, the foreign-language code must - /// call the destructor function specific to the type `T`. Calling the destructor - /// function for other types may lead to undefined behaviour. - fn write(obj: std::sync::Arc, buf: &mut Vec) { - static_assertions::const_assert!(std::mem::size_of::<*const std::ffi::c_void>() <= 8); - buf.put_u64(>::lower(obj) as u64); - } - - /// When reading as a field of a complex structure, we receive a "borrow" of the `Arc` - /// that is owned by the foreign-language code, and make a clone for our own use. - /// - /// Safety: the buffer must contain a pointer previously obtained by calling - /// the `lower()` or `write()` method of this impl. - fn try_read(buf: &mut &[u8]) -> Result> { - static_assertions::const_assert!(std::mem::size_of::<*const std::ffi::c_void>() <= 8); - check_remaining(buf, 8)?; - >::try_lift(buf.get_u64() as Self::FfiType) - } - - ffi_converter_default_return!(UT); - - const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_INTERFACE) - .concat_str(T::NAME) - .concat_bool(false); -} - /// FFI support for ForeignSchedulers /// /// These are passed over the FFI as opaque pointer-sized types representing the foreign executor. diff --git a/uniffi_core/src/ffi_converter_traits.rs b/uniffi_core/src/ffi_converter_traits.rs new file mode 100644 index 0000000000..024222f8a0 --- /dev/null +++ b/uniffi_core/src/ffi_converter_traits.rs @@ -0,0 +1,255 @@ +/* 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; + +use crate::{ + try_lift_from_rust_buffer, FfiDefault, MetadataBuffer, Result, RustBuffer, RustCallStatus, + UnexpectedUniFFICallbackError, +}; + +/// Trait defining how to transfer values via the FFI layer. +/// +/// The `FfiConverter` trait defines how to pass values of a particular type back-and-forth over +/// the uniffi generated FFI layer, both as standalone argument or return values, and as +/// part of serialized compound data structures. `FfiConverter` is mainly used in generated code. +/// The goal is to make it easy for the code generator to name the correct FFI-related function for +/// a given type. +/// +/// FfiConverter has a generic parameter, that's filled in with a type local to the UniFFI consumer crate. +/// This allows us to work around the Rust orphan rules for remote types. See +/// `https://mozilla.github.io/uniffi-rs/internals/lifting_and_lowering.html#code-generation-and-the-fficonverter-trait` +/// for details. +/// +/// ## Scope +/// +/// It could be argued that FfiConverter handles too many concerns and should be split into +/// separate traits (like `FfiLiftAndLower`, `FfiSerialize`, `FfiReturn`). However, there are good +/// reasons to keep all the functionality in one trait. +/// +/// The main reason is that splitting the traits requires fairly complex blanket implementations, +/// for example `impl FfiReturn for T: FfiLiftAndLower`. Writing these impls often +/// means fighting with `rustc` over what counts as a conflicting implementation. In fact, as of +/// Rust 1.66, that previous example conflicts with `impl FfiReturn for ()`, since other +/// crates can implement `impl FfiReturn for ()`. In general, this path gets +/// complicated very quickly and that distracts from the logic that we want to define, which is +/// fairly simple. +/// +/// The main downside of having a single `FfiConverter` trait is that we need to implement it for +/// types that only support some of the functionality. For example, `Result<>` supports returning +/// values, but not lifting/lowering/serializing them. This is a bit weird, but works okay -- +/// especially since `FfiConverter` is rarely used outside of generated code. +/// +/// ## Safety +/// +/// This is an unsafe trait (implementing it requires `unsafe impl`) because we can't guarantee +/// that it's safe to pass your type out to foreign-language code and back again. Buggy +/// implementations of this trait might violate some assumptions made by the generated code, +/// or might not match with the corresponding code in the generated foreign-language bindings. +/// +/// In general, you should not need to implement this trait by hand, and should instead rely on +/// implementations generated from your component UDL via the `uniffi-bindgen scaffolding` command. +pub unsafe trait FfiConverter: Sized { + /// The low-level type used for passing values of this type over the FFI. + /// + /// This must be a C-compatible type (e.g. a numeric primitive, a `#[repr(C)]` struct) into + /// which values of the target rust type can be converted. + /// + /// For complex data types, we currently recommend using `RustBuffer` and serializing + /// the data for transfer. In theory it could be possible to build a matching + /// `#[repr(C)]` struct for a complex data type and pass that instead, but explicit + /// serialization is simpler and safer as a starting point. + type FfiType; + + /// The type that should be returned by scaffolding functions for this type. + /// + /// This is usually the same as `FfiType`, but `Result<>` has specialized handling. + type ReturnType: FfiDefault; + + /// The `FutureCallback` type used for async functions + /// + /// This is almost always `FutureCallback`. The one exception is the + /// unit type, see that `FfiConverter` impl for details. + type FutureCallback: Copy; + + /// Lower a rust value of the target type, into an FFI value of type Self::FfiType. + /// + /// This trait method is used for sending data from rust to the foreign language code, + /// by (hopefully cheaply!) converting it into something that can be passed over the FFI + /// and reconstructed on the other side. + /// + /// Note that this method takes an owned value; this allows it to transfer ownership in turn to + /// the foreign language code, e.g. by boxing the value and passing a pointer. + fn lower(obj: Self) -> Self::FfiType; + + /// Lower this value for scaffolding function return + /// + /// This method converts values into the `Result<>` type that [rust_call] expects. For + /// successful calls, return `Ok(lower_return)`. For errors that should be translated into + /// thrown exceptions on the foreign code, serialize the error into a RustBuffer and return + /// `Err(buf)` + fn lower_return(obj: Self) -> Result; + + /// Lift a rust value of the target type, from an FFI value of type Self::FfiType. + /// + /// This trait method is used for receiving data from the foreign language code in rust, + /// by (hopefully cheaply!) converting it from a low-level FFI value of type Self::FfiType + /// into a high-level rust value of the target type. + /// + /// Since we cannot statically guarantee that the foreign-language code will send valid + /// values of type Self::FfiType, this method is fallible. + fn try_lift(v: Self::FfiType) -> Result; + + /// Lift a Rust value for a callback interface method result + fn lift_callback_return(buf: RustBuffer) -> Self { + try_lift_from_rust_buffer(buf).expect("Error reading callback interface result") + } + + /// Lift a Rust value for a callback interface method error result + /// + /// This is called for "expected errors" -- the callback method returns a Result<> type and the + /// foreign code throws an exception that corresponds to the error type. + fn lift_callback_error(_buf: RustBuffer) -> Self { + panic!("Callback interface method returned unexpected error") + } + + /// Lift a Rust value for an unexpected callback interface error + /// + /// The main reason this is called is when the callback interface throws an error type that + /// doesn't match the Rust trait definition. It's also called for corner cases, like when the + /// foreign code doesn't follow the FFI contract. + /// + /// The default implementation panics unconditionally. Errors used in callback interfaces + /// handle this using the `From` impl that the library author + /// must provide. + fn handle_callback_unexpected_error(_e: UnexpectedUniFFICallbackError) -> Self { + panic!("Callback interface method returned unexpected error") + } + + /// Write a rust value into a buffer, to send over the FFI in serialized form. + /// + /// This trait method can be used for sending data from rust to the foreign language code, + /// in cases where we're not able to use a special-purpose FFI type and must fall back to + /// sending serialized bytes. + /// + /// Note that this method takes an owned value because it's transferring ownership + /// to the foreign language code via the RustBuffer. + fn write(obj: Self, buf: &mut Vec); + + /// Read a rust value from a buffer, received over the FFI in serialized form. + /// + /// This trait method can be used for receiving data from the foreign language code in rust, + /// in cases where we're not able to use a special-purpose FFI type and must fall back to + /// receiving serialized bytes. + /// + /// Since we cannot statically guarantee that the foreign-language code will send valid + /// serialized bytes for the target type, this method is fallible. + /// + /// Note the slightly unusual type here - we want a mutable reference to a slice of bytes, + /// because we want to be able to advance the start of the slice after reading an item + /// from it (but will not mutate the actual contents of the slice). + fn try_read(buf: &mut &[u8]) -> Result; + + /// Invoke a `FutureCallback` to complete a async call + fn invoke_future_callback( + callback: Self::FutureCallback, + callback_data: *const (), + return_value: Self::ReturnType, + call_status: RustCallStatus, + ); + + /// Type ID metadata, serialized into a [MetadataBuffer] + const TYPE_ID_META: MetadataBuffer; +} + +/// FfiConverter for Arc-types +/// +/// This trait gets around the orphan rule limitations, which prevent library crates from +/// implementing `FfiConverter` on an Arc. When this is implemented for T, we generate an +/// `FfiConverter` impl for Arc. +/// +/// Note: There's no need for `FfiConverterBox`, since Box is a fundamental type. +/// +/// ## Safety +/// +/// This has the same safety considerations as FfiConverter +pub unsafe trait FfiConverterArc { + type FfiType; + type ReturnType: FfiDefault; + type FutureCallback: Copy; + + fn lower(obj: Arc) -> Self::FfiType; + fn lower_return(obj: Arc) -> Result; + fn try_lift(v: Self::FfiType) -> Result>; + fn lift_callback_return(buf: RustBuffer) -> Arc { + try_lift_from_rust_buffer(buf).expect("Error reading callback interface result") + } + fn lift_callback_error(_buf: RustBuffer) -> Arc { + panic!("Callback interface method returned unexpected error") + } + fn handle_callback_unexpected_error(_e: UnexpectedUniFFICallbackError) -> Arc { + panic!("Callback interface method returned unexpected error") + } + fn write(obj: Arc, buf: &mut Vec); + fn try_read(buf: &mut &[u8]) -> Result>; + fn invoke_future_callback( + callback: Self::FutureCallback, + callback_data: *const (), + return_value: Self::ReturnType, + call_status: RustCallStatus, + ); + const TYPE_ID_META: MetadataBuffer; +} + +unsafe impl FfiConverter for Arc +where + T: FfiConverterArc + ?Sized, +{ + type FfiType = T::FfiType; + type ReturnType = T::ReturnType; + type FutureCallback = T::FutureCallback; + + fn lower(obj: Self) -> Self::FfiType { + T::lower(obj) + } + + fn lower_return(obj: Self) -> Result { + T::lower_return(obj) + } + + fn try_lift(v: Self::FfiType) -> Result { + T::try_lift(v) + } + + fn lift_callback_return(buf: RustBuffer) -> Self { + T::lift_callback_error(buf) + } + + fn lift_callback_error(buf: RustBuffer) -> Self { + T::lift_callback_error(buf) + } + + fn handle_callback_unexpected_error(e: UnexpectedUniFFICallbackError) -> Self { + T::handle_callback_unexpected_error(e) + } + + fn write(obj: Self, buf: &mut Vec) { + T::write(obj, buf) + } + + fn try_read(buf: &mut &[u8]) -> Result { + T::try_read(buf) + } + + fn invoke_future_callback( + callback: Self::FutureCallback, + callback_data: *const (), + return_value: Self::ReturnType, + call_status: RustCallStatus, + ) { + T::invoke_future_callback(callback, callback_data, return_value, call_status) + } + + const TYPE_ID_META: MetadataBuffer = T::TYPE_ID_META; +} diff --git a/uniffi_core/src/lib.rs b/uniffi_core/src/lib.rs index ae7e4d381a..f5e4a26cbb 100644 --- a/uniffi_core/src/lib.rs +++ b/uniffi_core/src/lib.rs @@ -40,9 +40,11 @@ pub use anyhow::Result; pub mod ffi; mod ffi_converter_impls; +mod ffi_converter_traits; pub mod metadata; pub use ffi::*; +pub use ffi_converter_traits::{FfiConverter, FfiConverterArc}; pub use metadata::*; // Re-export the libs that we use in the generated code, @@ -127,168 +129,6 @@ macro_rules! assert_compatible_version { }; } -/// Trait defining how to transfer values via the FFI layer. -/// -/// The `FfiConverter` trait defines how to pass values of a particular type back-and-forth over -/// the uniffi generated FFI layer, both as standalone argument or return values, and as -/// part of serialized compound data structures. `FfiConverter` is mainly used in generated code. -/// The goal is to make it easy for the code generator to name the correct FFI-related function for -/// a given type. -/// -/// FfiConverter has a generic parameter, that's filled in with a type local to the UniFFI consumer crate. -/// This allows us to work around the Rust orphan rules for remote types. See -/// `https://mozilla.github.io/uniffi-rs/internals/lifting_and_lowering.html#code-generation-and-the-fficonverter-trait` -/// for details. -/// -/// ## Scope -/// -/// It could be argued that FfiConverter handles too many concerns and should be split into -/// separate traits (like `FfiLiftAndLower`, `FfiSerialize`, `FfiReturn`). However, there are good -/// reasons to keep all the functionality in one trait. -/// -/// The main reason is that splitting the traits requires fairly complex blanket implementations, -/// for example `impl FfiReturn for T: FfiLiftAndLower`. Writing these impls often -/// means fighting with `rustc` over what counts as a conflicting implementation. In fact, as of -/// Rust 1.66, that previous example conflicts with `impl FfiReturn for ()`, since other -/// crates can implement `impl FfiReturn for ()`. In general, this path gets -/// complicated very quickly and that distracts from the logic that we want to define, which is -/// fairly simple. -/// -/// The main downside of having a single `FfiConverter` trait is that we need to implement it for -/// types that only support some of the functionality. For example, `Result<>` supports returning -/// values, but not lifting/lowering/serializing them. This is a bit weird, but works okay -- -/// especially since `FfiConverter` is rarely used outside of generated code. -/// -/// ## Safety -/// -/// This is an unsafe trait (implementing it requires `unsafe impl`) because we can't guarantee -/// that it's safe to pass your type out to foreign-language code and back again. Buggy -/// implementations of this trait might violate some assumptions made by the generated code, -/// or might not match with the corresponding code in the generated foreign-language bindings. -/// -/// In general, you should not need to implement this trait by hand, and should instead rely on -/// implementations generated from your component UDL via the `uniffi-bindgen scaffolding` command. -pub unsafe trait FfiConverter: Sized { - /// The low-level type used for passing values of this type over the FFI. - /// - /// This must be a C-compatible type (e.g. a numeric primitive, a `#[repr(C)]` struct) into - /// which values of the target rust type can be converted. - /// - /// For complex data types, we currently recommend using `RustBuffer` and serializing - /// the data for transfer. In theory it could be possible to build a matching - /// `#[repr(C)]` struct for a complex data type and pass that instead, but explicit - /// serialization is simpler and safer as a starting point. - type FfiType; - - /// The type that should be returned by scaffolding functions for this type. - /// - /// This is usually the same as `FfiType`, but `Result<>` has specialized handling. - type ReturnType: FfiDefault; - - /// The `FutureCallback` type used for async functions - /// - /// This is almost always `FutureCallback`. The one exception is the - /// unit type, see that `FfiConverter` impl for details. - type FutureCallback: Copy; - - /// Lower a rust value of the target type, into an FFI value of type Self::FfiType. - /// - /// This trait method is used for sending data from rust to the foreign language code, - /// by (hopefully cheaply!) converting it into something that can be passed over the FFI - /// and reconstructed on the other side. - /// - /// Note that this method takes an owned value; this allows it to transfer ownership in turn to - /// the foreign language code, e.g. by boxing the value and passing a pointer. - fn lower(obj: Self) -> Self::FfiType; - - /// Lower this value for scaffolding function return - /// - /// This method converts values into the `Result<>` type that [rust_call] expects. For - /// successful calls, return `Ok(lower_return)`. For errors that should be translated into - /// thrown exceptions on the foreign code, serialize the error into a RustBuffer and return - /// `Err(buf)` - fn lower_return(obj: Self) -> Result; - - /// Lift a rust value of the target type, from an FFI value of type Self::FfiType. - /// - /// This trait method is used for receiving data from the foreign language code in rust, - /// by (hopefully cheaply!) converting it from a low-level FFI value of type Self::FfiType - /// into a high-level rust value of the target type. - /// - /// Since we cannot statically guarantee that the foreign-language code will send valid - /// values of type Self::FfiType, this method is fallible. - fn try_lift(v: Self::FfiType) -> Result; - - /// Lift a Rust value for a callback interface method result - fn lift_callback_return(buf: RustBuffer) -> Self { - try_lift_from_rust_buffer(buf).expect("Error reading callback interface result") - } - - /// Lift a Rust value for a callback interface method error result - /// - /// This is called for "expected errors" -- the callback method returns a Result<> type and the - /// foreign code throws an exception that corresponds to the error type. - fn lift_callback_error(_buf: RustBuffer) -> Self { - panic!("Callback interface method returned unexpected error") - } - - /// Lift a Rust value for an unexpected callback interface error - /// - /// The main reason this is called is when the callback interface throws an error type that - /// doesn't match the Rust trait definition. It's also called for corner cases, like when the - /// foreign code doesn't follow the FFI contract. - /// - /// The default implementation panics unconditionally. Errors used in callback interfaces - /// handle this using the `From` impl that the library author - /// must provide. - fn handle_callback_unexpected_error(_e: UnexpectedUniFFICallbackError) -> Self { - panic!("Callback interface method returned unexpected error") - } - - /// Write a rust value into a buffer, to send over the FFI in serialized form. - /// - /// This trait method can be used for sending data from rust to the foreign language code, - /// in cases where we're not able to use a special-purpose FFI type and must fall back to - /// sending serialized bytes. - /// - /// Note that this method takes an owned value because it's transferring ownership - /// to the foreign language code via the RustBuffer. - fn write(obj: Self, buf: &mut Vec); - - /// Read a rust value from a buffer, received over the FFI in serialized form. - /// - /// This trait method can be used for receiving data from the foreign language code in rust, - /// in cases where we're not able to use a special-purpose FFI type and must fall back to - /// receiving serialized bytes. - /// - /// Since we cannot statically guarantee that the foreign-language code will send valid - /// serialized bytes for the target type, this method is fallible. - /// - /// Note the slightly unusual type here - we want a mutable reference to a slice of bytes, - /// because we want to be able to advance the start of the slice after reading an item - /// from it (but will not mutate the actual contents of the slice). - fn try_read(buf: &mut &[u8]) -> Result; - - /// Invoke a `FutureCallback` to complete a async call - fn invoke_future_callback( - callback: Self::FutureCallback, - callback_data: *const (), - return_value: Self::ReturnType, - call_status: RustCallStatus, - ); - - /// Type ID metadata, serialized into a [MetadataBuffer] - const TYPE_ID_META: MetadataBuffer; -} - -/// Implemented for exported interface types -/// -/// Like, FfiConverter this has a generic parameter, that's filled in with a type local to the -/// UniFFI consumer crate. -pub trait Interface: Send + Sync + Sized { - const NAME: &'static str; -} - /// Struct to use when we want to lift/lower/serialize types inside the `uniffi` crate. struct UniFfiTag; @@ -383,37 +223,69 @@ macro_rules! ffi_converter_rust_buffer_lift_and_lower { }; } -/// Macro to implement `FfiConverter` for a type by forwording all calls to another type +/// Macro to implement `FfiConverter` for a UniFfiTag using a different UniFfiTag /// /// This is used for external types #[macro_export] macro_rules! ffi_converter_forward { + // Forward a `FfiConverter` implementation + ($T:ty, $existing_impl_tag:ty, $new_impl_tag:ty) => { + ::uniffi::do_ffi_converter_forward!( + FfiConverter, + Self, + $T, + $existing_impl_tag, + $new_impl_tag + ); + }; +} + +/// Macro to implement `FfiConverterArc` for a UniFfiTag using a different UniFfiTag +/// +/// This is used for external types +#[macro_export] +macro_rules! ffi_converter_arc_forward { ($T:ty, $existing_impl_tag:ty, $new_impl_tag:ty) => { - unsafe impl $crate::FfiConverter<$new_impl_tag> for $T { - type FfiType = <$T as $crate::FfiConverter<$existing_impl_tag>>::FfiType; - type ReturnType = <$T as $crate::FfiConverter<$existing_impl_tag>>::FfiType; - type FutureCallback = <$T as $crate::FfiConverter<$existing_impl_tag>>::FutureCallback; + ::uniffi::do_ffi_converter_forward!( + FfiConverterArc, + ::std::sync::Arc, + $T, + $existing_impl_tag, + $new_impl_tag + ); + }; +} - fn lower(obj: $T) -> Self::FfiType { - <$T as $crate::FfiConverter<$existing_impl_tag>>::lower(obj) +// Generic code between the two macros above +#[doc(hidden)] +#[macro_export] +macro_rules! do_ffi_converter_forward { + ($trait:ident, $rust_type:ty, $T:ty, $existing_impl_tag:ty, $new_impl_tag:ty) => { + unsafe impl $crate::$trait<$new_impl_tag> for $T { + type FfiType = <$T as $crate::$trait<$existing_impl_tag>>::FfiType; + type ReturnType = <$T as $crate::$trait<$existing_impl_tag>>::FfiType; + type FutureCallback = <$T as $crate::$trait<$existing_impl_tag>>::FutureCallback; + + fn lower(obj: $rust_type) -> Self::FfiType { + <$T as $crate::$trait<$existing_impl_tag>>::lower(obj) } fn lower_return( - v: Self, + v: $rust_type, ) -> ::std::result::Result { - <$T as $crate::FfiConverter<$existing_impl_tag>>::lower_return(v) + <$T as $crate::$trait<$existing_impl_tag>>::lower_return(v) } - fn try_lift(v: Self::FfiType) -> $crate::Result<$T> { - <$T as $crate::FfiConverter<$existing_impl_tag>>::try_lift(v) + fn try_lift(v: Self::FfiType) -> $crate::Result<$rust_type> { + <$T as $crate::$trait<$existing_impl_tag>>::try_lift(v) } - fn write(obj: $T, buf: &mut Vec) { - <$T as $crate::FfiConverter<$existing_impl_tag>>::write(obj, buf) + fn write(obj: $rust_type, buf: &mut Vec) { + <$T as $crate::$trait<$existing_impl_tag>>::write(obj, buf) } - fn try_read(buf: &mut &[u8]) -> $crate::Result<$T> { - <$T as $crate::FfiConverter<$existing_impl_tag>>::try_read(buf) + fn try_read(buf: &mut &[u8]) -> $crate::Result<$rust_type> { + <$T as $crate::$trait<$existing_impl_tag>>::try_read(buf) } fn invoke_future_callback( @@ -422,7 +294,7 @@ macro_rules! ffi_converter_forward { return_value: Self::ReturnType, call_status: $crate::RustCallStatus, ) { - <$T as $crate::FfiConverter<$existing_impl_tag>>::invoke_future_callback( + <$T as $crate::$trait<$existing_impl_tag>>::invoke_future_callback( callback, callback_data, return_value, @@ -431,86 +303,7 @@ macro_rules! ffi_converter_forward { } const TYPE_ID_META: ::uniffi::MetadataBuffer = - <$T as $crate::FfiConverter<$existing_impl_tag>>::TYPE_ID_META; - } - }; -} - -/// Macro to implement `FfiConverter` for a trait -#[macro_export] -macro_rules! ffi_converter_trait_decl { - ($T:ty, $name:expr, $uniffi_tag:ty) => { - use $crate::deps::bytes::{Buf, BufMut}; - unsafe impl $crate::FfiConverter<$uniffi_tag> for std::sync::Arc<$T> { - type FfiType = *const std::os::raw::c_void; - $crate::ffi_converter_default_return!($uniffi_tag); - //type ReturnType = *const std::os::raw::c_void; - - fn lower(obj: std::sync::Arc<$T>) -> Self::FfiType { - Box::into_raw(Box::new(obj)) as *const std::os::raw::c_void - } - - fn try_lift(v: Self::FfiType) -> $crate::Result> { - let foreign_arc = Box::leak(unsafe { Box::from_raw(v as *mut std::sync::Arc<$T>) }); - // Take a clone for our own use. - Ok(std::sync::Arc::clone(foreign_arc)) - } - - fn write(obj: std::sync::Arc<$T>, buf: &mut Vec) { - $crate::deps::static_assertions::const_assert!(std::mem::size_of::<*const std::ffi::c_void>() <= 8); - buf.put_u64(>::lower(obj) as u64); - } - - fn try_read(buf: &mut &[u8]) -> $crate::Result> { - $crate::deps::static_assertions::const_assert!(std::mem::size_of::<*const std::ffi::c_void>() <= 8); - $crate::check_remaining(buf, 8)?; - >::try_lift(buf.get_u64() as Self::FfiType) - } - const TYPE_ID_META: $crate::MetadataBuffer = $crate::MetadataBuffer::from_code($crate::metadata::codes::TYPE_INTERFACE).concat_str($name).concat_bool(true); - } - } -} - -/// Macro to implement `FfiConverter` for a callback interface -#[macro_export] -macro_rules! ffi_converter_callback_interface { - ($trait:ident, $T:ty, $name:expr, $uniffi_tag:ty) => { - unsafe impl ::uniffi::FfiConverter<$uniffi_tag> for Box { - type FfiType = u64; - - // Lower and write are tricky to implement because we have a dyn trait as our type. There's - // probably a way to, but this carries lots of thread safety risks, down to impedance - // mismatches between Rust and foreign languages, and our uncertainty around implementations of - // concurrent handlemaps. - // - // The use case for them is also quite exotic: it's passing a foreign callback back to the foreign - // language. - // - // Until we have some certainty, and use cases, we shouldn't use them. - fn lower(_obj: Box) -> Self::FfiType { - panic!("Lowering CallbackInterface not supported") - } - - fn write(_obj: Box, _buf: &mut std::vec::Vec) { - panic!("Writing CallbackInterface not supported") - } - - fn try_lift(v: Self::FfiType) -> uniffi::deps::anyhow::Result> { - Ok(Box::new(<$T>::new(v))) - } - - fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result> { - use uniffi::deps::bytes::Buf; - uniffi::check_remaining(buf, 8)?; - Self::try_lift(buf.get_u64()) - } - - ::uniffi::ffi_converter_default_return!($uniffi_tag); - - const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code( - ::uniffi::metadata::codes::TYPE_CALLBACK_INTERFACE, - ) - .concat_str($name); + <$T as $crate::$trait<$existing_impl_tag>>::TYPE_ID_META; } }; } diff --git a/uniffi_macros/src/enum_.rs b/uniffi_macros/src/enum_.rs index 9dc28dbf55..919a48f270 100644 --- a/uniffi_macros/src/enum_.rs +++ b/uniffi_macros/src/enum_.rs @@ -84,6 +84,10 @@ fn enum_or_error_ffi_converter_impl( ) -> TokenStream { let name = ident_to_string(ident); let impl_spec = tagged_impl_header("FfiConverter", ident, tag); + let mod_path = match mod_path() { + Ok(p) => p, + Err(e) => return e.into_compile_error(), + }; let write_match_arms = enum_.variants.iter().enumerate().map(|(i, v)| { let v_ident = &v.ident; let fields = v.fields.iter().map(|f| &f.ident); @@ -140,6 +144,7 @@ fn enum_or_error_ffi_converter_impl( #handle_callback_unexpected_error const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(#metadata_type_code) + .concat_str(#mod_path) .concat_str(#name); } } diff --git a/uniffi_macros/src/error.rs b/uniffi_macros/src/error.rs index cf090db312..ad4f65c444 100644 --- a/uniffi_macros/src/error.rs +++ b/uniffi_macros/src/error.rs @@ -96,6 +96,10 @@ fn flat_error_ffi_converter_impl( ) -> TokenStream { let name = ident_to_string(ident); let impl_spec = tagged_impl_header("FfiConverter", ident, tag); + let mod_path = match mod_path() { + Ok(p) => p, + Err(e) => return e.into_compile_error(), + }; let write_impl = { let match_arms = enum_.variants.iter().enumerate().map(|(i, v)| { @@ -155,6 +159,7 @@ fn flat_error_ffi_converter_impl( #handle_callback_unexpected_error const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TYPE_ENUM) + .concat_str(#mod_path) .concat_str(#name); } } diff --git a/uniffi_macros/src/export.rs b/uniffi_macros/src/export.rs index 3813b83c30..0b94afa12c 100644 --- a/uniffi_macros/src/export.rs +++ b/uniffi_macros/src/export.rs @@ -4,7 +4,7 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, quote_spanned}; -use syn::{visit_mut::VisitMut, Item, Type}; +use syn::{visit_mut::VisitMut, Item, Path, Type}; mod attributes; mod callback_interface; @@ -16,7 +16,11 @@ use self::{ item::{ExportItem, ImplItem}, scaffolding::{gen_constructor_scaffolding, gen_fn_scaffolding, gen_method_scaffolding}, }; -use crate::{object::interface_meta_static_var, util::ident_to_string}; +use crate::{ + object::interface_meta_static_var, + util::{ident_to_string, mod_path, tagged_impl_header}, +}; +pub use callback_interface::ffi_converter_callback_interface_impl; use uniffi_meta::free_fn_symbol_name; // TODO(jplatte): Ensure no generics, … @@ -81,14 +85,12 @@ pub(crate) fn expand_export( let meta_static_var = interface_meta_static_var(&self_ident, true, &mod_path) .unwrap_or_else(syn::Error::into_compile_error); - let macro_tokens = quote! { - ::uniffi::ffi_converter_trait_decl!(dyn #self_ident, stringify!(#self_ident), crate::UniFfiTag); - }; + let ffi_converter_tokens = ffi_converter_trait_impl(&self_ident, None); Ok(quote_spanned! { self_ident.span() => #meta_static_var #free_tokens - #macro_tokens + #ffi_converter_tokens #impl_tokens }) } @@ -154,6 +156,66 @@ pub(crate) fn expand_export( } } +pub(crate) fn ffi_converter_trait_impl(trait_ident: &Ident, tag: Option<&Path>) -> TokenStream { + let impl_spec = tagged_impl_header("FfiConverterArc", "e! { dyn #trait_ident }, tag); + let name = ident_to_string(trait_ident); + let mod_path = match mod_path() { + Ok(p) => p, + Err(e) => return e.into_compile_error(), + }; + + quote! { + unsafe #impl_spec { + type FfiType = *const ::std::os::raw::c_void; + type ReturnType = Self::FfiType; + type FutureCallback = ::uniffi::FutureCallback; + + fn lower(obj: ::std::sync::Arc) -> Self::FfiType { + ::std::boxed::Box::into_raw(::std::boxed::Box::new(obj)) as *const ::std::os::raw::c_void + } + + fn try_lift(v: Self::FfiType) -> ::uniffi::Result<::std::sync::Arc> { + let foreign_arc = ::std::boxed::Box::leak(unsafe { Box::from_raw(v as *mut ::std::sync::Arc) }); + // Take a clone for our own use. + Ok(::std::sync::Arc::clone(foreign_arc)) + } + + fn write(obj: ::std::sync::Arc, buf: &mut Vec) { + ::uniffi::deps::static_assertions::const_assert!(::std::mem::size_of::<*const ::std::ffi::c_void>() <= 8); + ::uniffi::deps::bytes::BufMut::put_u64( + buf, + >::lower(obj) as u64, + ); + } + + fn try_read(buf: &mut &[u8]) -> ::uniffi::Result<::std::sync::Arc> { + ::uniffi::deps::static_assertions::const_assert!(::std::mem::size_of::<*const ::std::ffi::c_void>() <= 8); + ::uniffi::check_remaining(buf, 8)?; + >::try_lift( + ::uniffi::deps::bytes::Buf::get_u64(buf) as Self::FfiType) + } + + fn lower_return(v: ::std::sync::Arc) -> ::std::result::Result { + Ok(>::lower(v)) + } + + fn invoke_future_callback( + callback: Self::FutureCallback, + callback_data: *const (), + return_value: Self::ReturnType, + call_status: ::uniffi::RustCallStatus, + ) { + callback(callback_data, return_value, call_status); + } + + const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TYPE_INTERFACE) + .concat_str(#mod_path) + .concat_str(#name) + .concat_bool(true); + } + } +} + /// Rewrite Self type alias usage in an impl block to the type itself. /// /// For example, diff --git a/uniffi_macros/src/export/callback_interface.rs b/uniffi_macros/src/export/callback_interface.rs index 232231719e..67007d7c89 100644 --- a/uniffi_macros/src/export/callback_interface.rs +++ b/uniffi_macros/src/export/callback_interface.rs @@ -5,12 +5,12 @@ use crate::{ export::ImplItem, fnsig::{FnKind, FnSignature}, - util::{create_metadata_items, ident_to_string}, + util::{create_metadata_items, ident_to_string, mod_path, tagged_impl_header}, }; use proc_macro2::{Span, TokenStream}; use quote::quote; use std::iter; -use syn::Ident; +use syn::{Ident, Path}; pub(super) fn trait_impl( ident: &Ident, @@ -18,7 +18,6 @@ pub(super) fn trait_impl( internals_ident: &Ident, items: &[ImplItem], ) -> syn::Result { - let trait_name = ident_to_string(trait_ident); let trait_impl_methods = items .iter() .map(|item| match item { @@ -26,6 +25,7 @@ pub(super) fn trait_impl( _ => unreachable!("traits have no constructors"), }) .collect::>()?; + let ffi_converter_tokens = ffi_converter_callback_interface_impl(trait_ident, ident, None); Ok(quote! { #[doc(hidden)] @@ -54,10 +54,70 @@ pub(super) fn trait_impl( #trait_impl_methods } - ::uniffi::ffi_converter_callback_interface!(#trait_ident, #ident, #trait_name, crate::UniFfiTag); + #ffi_converter_tokens }) } +pub fn ffi_converter_callback_interface_impl( + trait_ident: &Ident, + trait_impl_ident: &Ident, + tag: Option<&Path>, +) -> TokenStream { + let name = ident_to_string(trait_ident); + let impl_spec = tagged_impl_header("FfiConverter", "e! { Box }, tag); + let tag = match tag { + Some(t) => quote! { #t }, + None => quote! { T }, + }; + let mod_path = match mod_path() { + Ok(p) => p, + Err(e) => return e.into_compile_error(), + }; + + quote! { + #[doc(hidden)] + #[automatically_derived] + unsafe #impl_spec { + type FfiType = u64; + + // Lower and write are tricky to implement because we have a dyn trait as our type. There's + // probably a way to, but this carries lots of thread safety risks, down to impedance + // mismatches between Rust and foreign languages, and our uncertainty around implementations of + // concurrent handlemaps. + // + // The use case for them is also quite exotic: it's passing a foreign callback back to the foreign + // language. + // + // Until we have some certainty, and use cases, we shouldn't use them. + fn lower(_obj: Self) -> Self::FfiType { + panic!("Lowering CallbackInterface not supported") + } + + fn write(_obj: Self, _buf: &mut std::vec::Vec) { + panic!("Writing CallbackInterface not supported") + } + + fn try_lift(v: Self::FfiType) -> uniffi::deps::anyhow::Result { + Ok(Box::new(<#trait_impl_ident>::new(v))) + } + + fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { + use uniffi::deps::bytes::Buf; + uniffi::check_remaining(buf, 8)?; + >::try_lift(buf.get_u64()) + } + + ::uniffi::ffi_converter_default_return!(#tag); + + const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code( + ::uniffi::metadata::codes::TYPE_CALLBACK_INTERFACE, + ) + .concat_str(#mod_path) + .concat_str(#name); + } + } +} + fn gen_method_impl(sig: &FnSignature, internals_ident: &Ident) -> syn::Result { let FnSignature { ident, diff --git a/uniffi_macros/src/lib.rs b/uniffi_macros/src/lib.rs index ee4f600939..a3d1d02c58 100644 --- a/uniffi_macros/src/lib.rs +++ b/uniffi_macros/src/lib.rs @@ -12,7 +12,10 @@ use camino::Utf8Path; use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, LitStr}; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, Ident, LitStr, Token, +}; mod enum_; mod error; @@ -150,6 +153,46 @@ pub fn ffi_converter_interface(attrs: TokenStream, input: TokenStream) -> TokenS .into() } +/// Generate the FfiConverter implementation for an trait interface for the scaffolding code +#[doc(hidden)] +#[proc_macro] +pub fn scaffolding_ffi_converter_trait_interface(tokens: TokenStream) -> TokenStream { + export::ffi_converter_trait_impl( + &syn::parse_macro_input!(tokens), + Some(&syn::parse_quote!(crate::UniFfiTag)), + ) + .into() +} + +/// Generate the FfiConverter implementation for an trait interface for the scaffolding code +#[doc(hidden)] +#[proc_macro] +pub fn scaffolding_ffi_converter_callback_interface(tokens: TokenStream) -> TokenStream { + struct Input { + trait_ident: Ident, + impl_ident: Ident, + } + + impl Parse for Input { + fn parse(input: ParseStream<'_>) -> syn::Result { + let trait_ident = input.parse()?; + input.parse::()?; + let impl_ident = input.parse()?; + Ok(Self { + trait_ident, + impl_ident, + }) + } + } + let input: Input = syn::parse_macro_input!(tokens); + export::ffi_converter_callback_interface_impl( + &input.trait_ident, + &input.impl_ident, + Some(&syn::parse_quote!(crate::UniFfiTag)), + ) + .into() +} + /// A helper macro to include generated component scaffolding. /// /// This is a simple convenience macro to include the UniFFI component @@ -194,6 +237,49 @@ pub fn include_scaffolding(component_name: TokenStream) -> TokenStream { }.into() } +// Use a UniFFI types from dependent crates that uses UDL files +// +// See [util::CommonAttr] for a discussion of why this is needed. + +#[proc_macro] +pub fn use_udl_record(tokens: TokenStream) -> TokenStream { + use_udl_simple_type(tokens) +} + +#[proc_macro] +pub fn use_udl_enum(tokens: TokenStream) -> TokenStream { + use_udl_simple_type(tokens) +} + +#[proc_macro] +pub fn use_udl_error(tokens: TokenStream) -> TokenStream { + use_udl_simple_type(tokens) +} + +fn use_udl_simple_type(tokens: TokenStream) -> TokenStream { + let util::ExternalTypeItem { + crate_ident, + type_ident, + .. + } = parse_macro_input!(tokens); + quote! { + ::uniffi::ffi_converter_forward!(#type_ident, #crate_ident::UniFfiTag, crate::UniFfiTag); + } + .into() +} + +#[proc_macro] +pub fn use_udl_object(tokens: TokenStream) -> TokenStream { + let util::ExternalTypeItem { + crate_ident, + type_ident, + .. + } = parse_macro_input!(tokens); + quote! { + ::uniffi::ffi_converter_arc_forward!(#type_ident, #crate_ident::UniFfiTag, crate::UniFfiTag); + }.into() +} + /// A helper macro to generate and include component scaffolding. /// /// This is a convenience macro designed for writing `trybuild`-style tests and diff --git a/uniffi_macros/src/object.rs b/uniffi_macros/src/object.rs index dde8e70020..439505b646 100644 --- a/uniffi_macros/src/object.rs +++ b/uniffi_macros/src/object.rs @@ -4,7 +4,7 @@ use syn::{DeriveInput, Path}; use uniffi_meta::free_fn_symbol_name; use crate::util::{ - create_metadata_items, ident_to_string, tagged_impl_header, ArgumentNotAllowedHere, + create_metadata_items, ident_to_string, mod_path, tagged_impl_header, ArgumentNotAllowedHere, AttributeSliceExt, CommonAttr, }; @@ -50,12 +50,93 @@ pub(crate) fn expand_ffi_converter_interface(attr: CommonAttr, input: DeriveInpu pub(crate) fn interface_impl(ident: &Ident, tag: Option<&Path>) -> TokenStream { let name = ident_to_string(ident); - let impl_spec = tagged_impl_header("Interface", ident, tag); + let impl_spec = tagged_impl_header("FfiConverterArc", ident, tag); + let mod_path = match mod_path() { + Ok(p) => p, + Err(e) => return e.into_compile_error(), + }; + quote! { #[doc(hidden)] #[automatically_derived] - #impl_spec { - const NAME: &'static str = #name; + /// Support for passing reference-counted shared objects via the FFI. + /// + /// To avoid dealing with complex lifetime semantics over the FFI, any data passed + /// by reference must be encapsulated in an `Arc`, and must be safe to share + /// across threads. + unsafe #impl_spec { + // Don't use a pointer to as that requires a `pub ` + type FfiType = *const ::std::os::raw::c_void; + type ReturnType = *const ::std::os::raw::c_void; + type FutureCallback = ::uniffi::FutureCallback; + + /// When lowering, we have an owned `Arc` and we transfer that ownership + /// to the foreign-language code, "leaking" it out of Rust's ownership system + /// as a raw pointer. This works safely because we have unique ownership of `self`. + /// The foreign-language code is responsible for freeing this by calling the + /// `ffi_object_free` FFI function provided by the corresponding UniFFI type. + /// + /// Safety: when freeing the resulting pointer, the foreign-language code must + /// call the destructor function specific to the type `T`. Calling the destructor + /// function for other types may lead to undefined behaviour. + fn lower(obj: ::std::sync::Arc) -> Self::FfiType { + ::std::sync::Arc::into_raw(obj) as Self::FfiType + } + + /// When lifting, we receive a "borrow" of the `Arc` that is owned by + /// the foreign-language code, and make a clone of it for our own use. + /// + /// Safety: the provided value must be a pointer previously obtained by calling + /// the `lower()` or `write()` method of this impl. + fn try_lift(v: Self::FfiType) -> ::uniffi::Result<::std::sync::Arc> { + let v = v as *const #ident; + // We musn't drop the `Arc` that is owned by the foreign-language code. + let foreign_arc = ::std::mem::ManuallyDrop::new(unsafe { ::std::sync::Arc::::from_raw(v) }); + // Take a clone for our own use. + Ok(::std::sync::Arc::clone(&*foreign_arc)) + } + + /// When writing as a field of a complex structure, make a clone and transfer ownership + /// of it to the foreign-language code by writing its pointer into the buffer. + /// The foreign-language code is responsible for freeing this by calling the + /// `ffi_object_free` FFI function provided by the corresponding UniFFI type. + /// + /// Safety: when freeing the resulting pointer, the foreign-language code must + /// call the destructor function specific to the type `T`. Calling the destructor + /// function for other types may lead to undefined behaviour. + fn write(obj: ::std::sync::Arc, buf: &mut Vec) { + ::uniffi::deps::static_assertions::const_assert!(::std::mem::size_of::<*const ::std::ffi::c_void>() <= 8); + ::uniffi::deps::bytes::BufMut::put_u64(buf, >::lower(obj) as u64); + } + + /// When reading as a field of a complex structure, we receive a "borrow" of the `Arc` + /// that is owned by the foreign-language code, and make a clone for our own use. + /// + /// Safety: the buffer must contain a pointer previously obtained by calling + /// the `lower()` or `write()` method of this impl. + fn try_read(buf: &mut &[u8]) -> ::uniffi::Result<::std::sync::Arc> { + ::uniffi::deps::static_assertions::const_assert!(::std::mem::size_of::<*const ::std::ffi::c_void>() <= 8); + ::uniffi::check_remaining(buf, 8)?; + >::try_lift(::uniffi::deps::bytes::Buf::get_u64(buf) as Self::FfiType) + } + + fn lower_return(v: ::std::sync::Arc) -> ::std::result::Result { + Ok(>::lower(v)) + } + + fn invoke_future_callback( + callback: Self::FutureCallback, + callback_data: *const (), + return_value: Self::ReturnType, + call_status: ::uniffi::RustCallStatus, + ) { + callback(callback_data, return_value, call_status); + } + + const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TYPE_INTERFACE) + .concat_str(#mod_path) + .concat_str(#name) + .concat_bool(false); } } } diff --git a/uniffi_macros/src/record.rs b/uniffi_macros/src/record.rs index 2c80a1b2a0..f93186421a 100644 --- a/uniffi_macros/src/record.rs +++ b/uniffi_macros/src/record.rs @@ -26,7 +26,8 @@ pub fn expand_record(input: DeriveInput) -> TokenStream { .parse_uniffi_attr_args::() .err() .map(syn::Error::into_compile_error); - let ffi_converter = record_ffi_converter_impl(ident, &record, None); + let ffi_converter = record_ffi_converter_impl(ident, &record, None) + .unwrap_or_else(syn::Error::into_compile_error); let meta_static_var = record_meta_static_var(ident, &record).unwrap_or_else(syn::Error::into_compile_error); @@ -39,7 +40,8 @@ pub fn expand_record(input: DeriveInput) -> TokenStream { pub(crate) fn expand_record_ffi_converter(attr: CommonAttr, input: DeriveInput) -> TokenStream { match input.data { - Data::Struct(s) => record_ffi_converter_impl(&input.ident, &s, attr.tag.as_ref()), + Data::Struct(s) => record_ffi_converter_impl(&input.ident, &s, attr.tag.as_ref()) + .unwrap_or_else(syn::Error::into_compile_error), _ => syn::Error::new( proc_macro2::Span::call_site(), "This attribute must only be used on structs", @@ -52,13 +54,14 @@ pub(crate) fn record_ffi_converter_impl( ident: &Ident, record: &DataStruct, tag: Option<&Path>, -) -> TokenStream { +) -> syn::Result { let impl_spec = tagged_impl_header("FfiConverter", ident, tag); let name = ident_to_string(ident); + let mod_path = mod_path()?; let write_impl: TokenStream = record.fields.iter().map(write_field).collect(); let try_read_fields: TokenStream = record.fields.iter().map(try_read_field).collect(); - quote! { + Ok(quote! { #[automatically_derived] unsafe #impl_spec { ::uniffi::ffi_converter_rust_buffer_lift_and_lower!(crate::UniFfiTag); @@ -73,9 +76,10 @@ pub(crate) fn record_ffi_converter_impl( } const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TYPE_RECORD) + .concat_str(#mod_path) .concat_str(#name); } - } + }) } fn write_field(f: &Field) -> TokenStream { diff --git a/uniffi_macros/src/util.rs b/uniffi_macros/src/util.rs index b516666dac..f7842f0f56 100644 --- a/uniffi_macros/src/util.rs +++ b/uniffi_macros/src/util.rs @@ -201,7 +201,7 @@ pub fn either_attribute_arg(a: Option, b: Option) -> syn::Res pub(crate) fn tagged_impl_header( trait_name: &str, - ident: &Ident, + ident: &impl ToTokens, tag: Option<&Path>, ) -> TokenStream { let trait_name = Ident::new(trait_name, Span::call_site()); @@ -267,3 +267,20 @@ impl Parse for CommonAttr { parse_comma_separated(input) } } + +/// Specifies a type from a dependent crate +pub struct ExternalTypeItem { + pub crate_ident: Ident, + pub sep: Token![,], + pub type_ident: Ident, +} + +impl Parse for ExternalTypeItem { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + crate_ident: input.parse()?, + sep: input.parse()?, + type_ident: input.parse()?, + }) + } +} diff --git a/uniffi_meta/src/group.rs b/uniffi_meta/src/group.rs index 3c96c10e80..67096e1079 100644 --- a/uniffi_meta/src/group.rs +++ b/uniffi_meta/src/group.rs @@ -2,10 +2,11 @@ * 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 super::{Metadata, NamespaceMetadata}; -use anyhow::{bail, Result}; use std::collections::{BTreeSet, HashMap}; +use crate::*; +use anyhow::{bail, Result}; + /// Group metadata by namespace pub fn group_metadata(items: Vec) -> Result> { // Map crate names to MetadataGroup instances @@ -49,7 +50,7 @@ pub fn group_metadata(items: Vec) -> Result> { Metadata::Error(meta) => (format!("error `{}`", meta.name()), meta.module_path()), }; - let crate_name = module_path.split("::").next().unwrap(); + let crate_name = calc_crate_name(module_path); let group = match group_map.get_mut(crate_name) { Some(ns) => ns, None => bail!("Unknown namespace for {item_desc} ({crate_name})"), @@ -57,7 +58,7 @@ pub fn group_metadata(items: Vec) -> Result> { if group.items.contains(&item) { bail!("Duplicate metadata item: {item:?}"); } - group.items.insert(item); + group.add_item(item); } Ok(group_map.into_values().collect()) } @@ -67,3 +68,171 @@ pub struct MetadataGroup { pub namespace: NamespaceMetadata, pub items: BTreeSet, } + +impl MetadataGroup { + pub fn add_item(&mut self, item: Metadata) { + let converter = ExternalTypeConverter { + crate_name: &self.namespace.crate_name, + }; + self.items.insert(converter.convert_item(item)); + } +} + +/// Convert metadata items by replacing types from external crates with Type::External +struct ExternalTypeConverter<'a> { + crate_name: &'a str, +} + +impl<'a> ExternalTypeConverter<'a> { + fn convert_item(&self, item: Metadata) -> Metadata { + match item { + Metadata::Func(meta) => Metadata::Func(FnMetadata { + inputs: self.convert_params(meta.inputs), + return_type: self.convert_optional(meta.return_type), + throws: self.convert_optional(meta.throws), + ..meta + }), + Metadata::Method(meta) => Metadata::Method(MethodMetadata { + inputs: self.convert_params(meta.inputs), + return_type: self.convert_optional(meta.return_type), + throws: self.convert_optional(meta.throws), + ..meta + }), + Metadata::TraitMethod(meta) => Metadata::TraitMethod(TraitMethodMetadata { + inputs: self.convert_params(meta.inputs), + return_type: self.convert_optional(meta.return_type), + throws: self.convert_optional(meta.throws), + ..meta + }), + Metadata::Constructor(meta) => Metadata::Constructor(ConstructorMetadata { + inputs: self.convert_params(meta.inputs), + throws: self.convert_optional(meta.throws), + ..meta + }), + Metadata::Record(meta) => Metadata::Record(RecordMetadata { + fields: self.convert_fields(meta.fields), + ..meta + }), + Metadata::Enum(meta) => Metadata::Enum(self.convert_enum(meta)), + Metadata::Error(meta) => Metadata::Error(match meta { + ErrorMetadata::Enum { enum_, is_flat } => ErrorMetadata::Enum { + enum_: self.convert_enum(enum_), + is_flat, + }, + }), + _ => item, + } + } + + fn convert_params(&self, params: Vec) -> Vec { + params + .into_iter() + .map(|param| FnParamMetadata { + ty: self.convert_type(param.ty), + ..param + }) + .collect() + } + + fn convert_fields(&self, fields: Vec) -> Vec { + fields + .into_iter() + .map(|field| FieldMetadata { + ty: self.convert_type(field.ty), + ..field + }) + .collect() + } + + fn convert_enum(&self, enum_: EnumMetadata) -> EnumMetadata { + EnumMetadata { + variants: enum_ + .variants + .into_iter() + .map(|variant| VariantMetadata { + fields: self.convert_fields(variant.fields), + ..variant + }) + .collect(), + ..enum_ + } + } + + fn convert_optional(&self, ty: Option) -> Option { + ty.map(|ty| self.convert_type(ty)) + } + + fn convert_type(&self, ty: Type) -> Type { + match ty { + // Convert `ty` if it's external + Type::Enum { module_path, name } | Type::Record { module_path, name } + if self.is_module_path_external(&module_path) => + { + Type::External { + module_path, + name, + kind: ExternalKind::DataClass, + } + } + Type::Custom { + module_path, name, .. + } if self.is_module_path_external(&module_path) => { + // For now, it's safe to assume that all custom types are data classes. + // There's no reason to use a custom type with an interface. + Type::External { + module_path, + name, + kind: ExternalKind::DataClass, + } + } + Type::ArcObject { + module_path, + object_name, + .. + } if self.is_module_path_external(&module_path) => Type::External { + module_path, + name: object_name, + kind: ExternalKind::Interface, + }, + Type::CallbackInterface { module_path, name } + if self.is_module_path_external(&module_path) => + { + panic!("External callback interfaces not supported ({name})") + } + // Convert child types + Type::Custom { + module_path, + name, + builtin, + .. + } => Type::Custom { + module_path, + name, + builtin: Box::new(self.convert_type(*builtin)), + }, + Type::Option { inner_type } => Type::Option { + inner_type: Box::new(self.convert_type(*inner_type)), + }, + Type::Vec { inner_type } => Type::Vec { + inner_type: Box::new(self.convert_type(*inner_type)), + }, + Type::HashMap { + key_type, + value_type, + } => Type::HashMap { + key_type: Box::new(self.convert_type(*key_type)), + value_type: Box::new(self.convert_type(*value_type)), + }, + // Otherwise, just return the type unchanged + _ => ty, + } + } + + fn is_module_path_external(&self, module_path: &str) -> bool { + calc_crate_name(module_path) != self.crate_name + } +} + +fn calc_crate_name(module_path: &str) -> &str { + module_path.split("::").next().unwrap() +} diff --git a/uniffi_meta/src/lib.rs b/uniffi_meta/src/lib.rs index 074fdb2098..f3c1feb719 100644 --- a/uniffi_meta/src/lib.rs +++ b/uniffi_meta/src/lib.rs @@ -235,19 +235,24 @@ pub enum Type { ForeignExecutor, SystemTime, Enum { + module_path: String, name: String, }, Record { + module_path: String, name: String, }, ArcObject { + module_path: String, object_name: String, is_trait: bool, }, CallbackInterface { + module_path: String, name: String, }, Custom { + module_path: String, name: String, builtin: Box, }, @@ -261,6 +266,17 @@ pub enum Type { key_type: Box, value_type: Box, }, + External { + module_path: String, + name: String, + kind: ExternalKind, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)] +pub enum ExternalKind { + DataClass, + Interface, } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)] diff --git a/uniffi_meta/src/reader.rs b/uniffi_meta/src/reader.rs index 037d1d010c..40b0333c2e 100644 --- a/uniffi_meta/src/reader.rs +++ b/uniffi_meta/src/reader.rs @@ -122,19 +122,24 @@ impl<'a> MetadataReader<'a> { codes::TYPE_SYSTEM_TIME => Type::SystemTime, codes::TYPE_FOREIGN_EXECUTOR => Type::ForeignExecutor, codes::TYPE_RECORD => Type::Record { + module_path: self.read_string()?, name: self.read_string()?, }, codes::TYPE_ENUM => Type::Enum { + module_path: self.read_string()?, name: self.read_string()?, }, codes::TYPE_INTERFACE => Type::ArcObject { + module_path: self.read_string()?, object_name: self.read_string()?, is_trait: self.read_bool()?, }, codes::TYPE_CALLBACK_INTERFACE => Type::CallbackInterface { + module_path: self.read_string()?, name: self.read_string()?, }, codes::TYPE_CUSTOM => Type::Custom { + module_path: self.read_string()?, name: self.read_string()?, builtin: Box::new(self.read_type()?), }, @@ -204,10 +209,10 @@ impl<'a> MetadataReader<'a> { return_type .filter(|t| { - *t == Type::ArcObject { - object_name: self_name.clone(), - is_trait: false, - } + matches!( + t, + Type::ArcObject { object_name, is_trait: false, .. } if object_name == &self_name + ) }) .context("Constructor return type must be Arc")?;