From 887d014c27b9b8a5860e80e529c25e86c6ecb565 Mon Sep 17 00:00:00 2001 From: Dmitry Borodin Date: Tue, 22 Aug 2023 14:12:06 +0200 Subject: [PATCH 01/24] implemented buildMap todo for kotlin --- .../src/bindings/kotlin/templates/MapTemplate.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt index c8cbecb68b..2fe02fd85b 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt @@ -2,15 +2,14 @@ {%- let value_type_name = value_type|type_name %} public object {{ ffi_converter_name }}: FfiConverterRustBuffer> { override fun read(buf: ByteBuffer): Map<{{ key_type_name }}, {{ value_type_name }}> { - // TODO: Once Kotlin's `buildMap` API is stabilized we should use it here. - val items : MutableMap<{{ key_type_name }}, {{ value_type_name }}> = mutableMapOf() - val len = buf.getInt() - repeat(len) { - val k = {{ key_type|read_fn }}(buf) - val v = {{ value_type|read_fn }}(buf) - items[k] = v + return buildMap<{{ key_type_name }}, {{ value_type_name }}> { + val len = buf.getInt() + repeat(len) { + val k = { { key_type|read_fn } }(buf) + val v = { { value_type|read_fn } }(buf) + this[k] = v + } } - return items } override fun allocationSize(value: Map<{{ key_type_name }}, {{ value_type_name }}>): Int { From c7c3c576e22ad655e8a2a26ebfacd49352ac53b9 Mon Sep 17 00:00:00 2001 From: Dmitry Borodin Date: Tue, 22 Aug 2023 15:10:03 +0200 Subject: [PATCH 02/24] removed added space between } --- uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt index 2fe02fd85b..e8b48925de 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt @@ -5,8 +5,8 @@ public object {{ ffi_converter_name }}: FfiConverterRustBuffer { val len = buf.getInt() repeat(len) { - val k = { { key_type|read_fn } }(buf) - val v = { { value_type|read_fn } }(buf) + val k = {{ key_type|read_fn }}(buf) + val v = {{ value_type|read_fn }}(buf) this[k] = v } } From 331098fa7049a33feee9e90d23d3cd1f50344675 Mon Sep 17 00:00:00 2001 From: Dmitry Borodin Date: Sun, 27 Aug 2023 14:23:25 +0200 Subject: [PATCH 03/24] optimised buildMap - create map of required capaticy to avoid expansion --- uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt index e8b48925de..5fb635342d 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt @@ -2,8 +2,8 @@ {%- let value_type_name = value_type|type_name %} public object {{ ffi_converter_name }}: FfiConverterRustBuffer> { override fun read(buf: ByteBuffer): Map<{{ key_type_name }}, {{ value_type_name }}> { - return buildMap<{{ key_type_name }}, {{ value_type_name }}> { - val len = buf.getInt() + val len = buf.getInt() + return buildMap<{{ key_type_name }}, {{ value_type_name }}>(len) { repeat(len) { val k = {{ key_type|read_fn }}(buf) val v = {{ value_type|read_fn }}(buf) From 86c2e26272cfd5054d78d9e8c8ed225d20ab89bc Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Fri, 22 Sep 2023 21:50:55 +0200 Subject: [PATCH 04/24] test(fixtures): remove potential duplicate in proc-macro fixture (#1763) Closes #1759 --- fixtures/proc-macro/src/lib.rs | 2 +- fixtures/proc-macro/tests/bindings/test_proc_macro.kts | 2 +- fixtures/proc-macro/tests/bindings/test_proc_macro.py | 2 +- fixtures/proc-macro/tests/bindings/test_proc_macro.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fixtures/proc-macro/src/lib.rs b/fixtures/proc-macro/src/lib.rs index 56666d32bc..0dc62ad972 100644 --- a/fixtures/proc-macro/src/lib.rs +++ b/fixtures/proc-macro/src/lib.rs @@ -122,7 +122,7 @@ fn take_record_with_bytes(rwb: RecordWithBytes) -> Vec { } #[uniffi::export] -fn test_callback_interface(cb: Box) { +fn call_callback_interface(cb: Box) { cb.do_nothing(); assert_eq!(cb.add(1, 1), 2); assert_eq!(cb.optional(Some(1)), 1); diff --git a/fixtures/proc-macro/tests/bindings/test_proc_macro.kts b/fixtures/proc-macro/tests/bindings/test_proc_macro.kts index 1a5493731a..8d5199a001 100644 --- a/fixtures/proc-macro/tests/bindings/test_proc_macro.kts +++ b/fixtures/proc-macro/tests/bindings/test_proc_macro.kts @@ -75,4 +75,4 @@ class KtTestCallbackInterface : TestCallbackInterface { } } -testCallbackInterface(KtTestCallbackInterface()) +callCallbackInterface(KtTestCallbackInterface()) diff --git a/fixtures/proc-macro/tests/bindings/test_proc_macro.py b/fixtures/proc-macro/tests/bindings/test_proc_macro.py index 087c1caa93..009d7cdb4b 100644 --- a/fixtures/proc-macro/tests/bindings/test_proc_macro.py +++ b/fixtures/proc-macro/tests/bindings/test_proc_macro.py @@ -77,4 +77,4 @@ def callback_handler(self, h): v = h.take_error(BasicError.InvalidInput()) return v -test_callback_interface(PyTestCallbackInterface()) +call_callback_interface(PyTestCallbackInterface()) diff --git a/fixtures/proc-macro/tests/bindings/test_proc_macro.swift b/fixtures/proc-macro/tests/bindings/test_proc_macro.swift index 388324cde3..1232b406bd 100644 --- a/fixtures/proc-macro/tests/bindings/test_proc_macro.swift +++ b/fixtures/proc-macro/tests/bindings/test_proc_macro.swift @@ -84,4 +84,4 @@ class SwiftTestCallbackInterface : TestCallbackInterface { } } -testCallbackInterface(cb: SwiftTestCallbackInterface()) +callCallbackInterface(cb: SwiftTestCallbackInterface()) From 856f40207e9a736f79241246c42bdb90d5c8e894 Mon Sep 17 00:00:00 2001 From: Akazm <37962180+Akazm@users.noreply.github.com> Date: Mon, 25 Sep 2023 18:05:39 +0200 Subject: [PATCH 05/24] Provide Kotlin companion objects for generated types (#1761) Co-authored-by: Andre Kazmierczak --- .../bindings/kotlin/templates/CallbackInterfaceTemplate.kt | 1 + .../src/bindings/kotlin/templates/EnumTemplate.kt | 6 +++++- .../src/bindings/kotlin/templates/ObjectTemplate.kt | 3 +++ .../src/bindings/kotlin/templates/RecordTemplate.kt | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt index 445252a9ca..56ae558544 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt @@ -17,6 +17,7 @@ public interface {{ type_name }} { {%- else -%} {%- endmatch %} {% endfor %} + companion object } // The ForeignCallback that is passed to Rust. diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt index 798abf2b21..a84ab50875 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt @@ -11,6 +11,7 @@ enum class {{ type_name }} { {% for variant in e.variants() -%} {{ variant|variant_name }}{% if loop.last %};{% else %},{% endif %} {%- endfor %} + companion object } public object {{ e|ffi_converter_name }}: FfiConverterRustBuffer<{{ type_name }}> { @@ -38,7 +39,9 @@ sealed class {{ type_name }}{% if contains_object_references %}: Disposable {% e {% for field in variant.fields() -%} val {{ field.name()|var_name }}: {{ field|type_name}}{% if loop.last %}{% else %}, {% endif %} {% endfor -%} - ) : {{ type_name }}() + ) : {{ type_name }}() { + companion object + } {%- endif %} {% endfor %} @@ -58,6 +61,7 @@ sealed class {{ type_name }}{% if contains_object_references %}: Disposable {% e }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } {% endif %} + companion object } public object {{ e|ffi_converter_name }} : FfiConverterRustBuffer<{{ type_name }}>{ diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt index f581361323..02e85c980e 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt @@ -21,6 +21,7 @@ public interface {{ type_name }}Interface { {%- endmatch -%} {% endfor %} + companion object } class {{ type_name }}( @@ -110,6 +111,8 @@ class {{ type_name }}( {{ type_name }}({% call kt::to_ffi_call(cons) %}) {% endfor %} } + {% else %} + companion object {% endif %} } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt index 9b5fa3362d..ac558511b0 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt @@ -16,6 +16,7 @@ data class {{ type_name }} ( {% call kt::destroy_fields(rec) %} } {% endif %} + companion object } public object {{ rec|ffi_converter_name }}: FfiConverterRustBuffer<{{ type_name }}> { From 8c828a93999d7154f241f35f99e6b3adf18aae43 Mon Sep 17 00:00:00 2001 From: Antonius Naumann Date: Thu, 28 Sep 2023 15:55:57 +0200 Subject: [PATCH 06/24] Fix inconsistent example code in proc-macro docs (#1769) The previous example code for the custom_type macro used url::Url even though the rest of the code features a custom Uuid type. --- docs/manual/src/proc_macro/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/manual/src/proc_macro/index.md b/docs/manual/src/proc_macro/index.md index cafc1b4c1e..6fc5b9081e 100644 --- a/docs/manual/src/proc_macro/index.md +++ b/docs/manual/src/proc_macro/index.md @@ -231,8 +231,8 @@ pub struct Uuid { val: String, } -// Use `url::Url` as a custom type, with `String` as the Builtin -uniffi::custom_type!(Url, String); +// Use `Uuid` as a custom type, with `String` as the Builtin +uniffi::custom_type!(Uuid, String); impl UniffiCustomTypeConverter for Uuid { type Builtin = String; From 7acfce022c1c394d9f91c390874557e611b47385 Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Thu, 28 Sep 2023 20:47:12 -0400 Subject: [PATCH 07/24] Fix external types namespace/crate_name confusion. (#1765) --- docs/manual/src/udl/ext_types_external.md | 3 + fixtures/ext-types/lib/src/ext-types-lib.udl | 5 + fixtures/ext-types/lib/src/lib.rs | 6 +- .../tests/bindings/test_imported_types.kts | 6 +- .../lib/tests/bindings/test_imported_types.py | 7 +- .../tests/bindings/test_imported_types.swift | 2 +- 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 | 2 +- .../tests/bindings/test_imported_types.kts | 3 +- .../tests/bindings/test_imported_types.py | 2 +- .../tests/bindings/test_imported_types.swift | 2 +- fixtures/ext-types/uniffi-one/Cargo.toml | 1 + fixtures/ext-types/uniffi-one/src/lib.rs | 5 + .../ext-types/uniffi-one/src/uniffi-one.udl | 2 +- .../src/bindings/kotlin/gen_kotlin/mod.rs | 6 +- .../kotlin/templates/ExternalTypeTemplate.kt | 2 +- .../src/bindings/kotlin/templates/Types.kt | 2 +- .../python/templates/ExternalTemplate.py | 9 +- .../src/bindings/python/templates/Types.py | 2 +- uniffi_bindgen/src/interface/mod.rs | 7 +- uniffi_bindgen/src/library_mode.rs | 34 +++++- uniffi_bindgen/src/macro_metadata/ci.rs | 8 +- .../templates/ExternalTypesTemplate.rs | 5 +- uniffi_checksum_derive/src/lib.rs | 9 +- uniffi_meta/src/group.rs | 101 +++++++++++------- uniffi_meta/src/lib.rs | 19 +++- uniffi_meta/src/types.rs | 3 + uniffi_udl/src/attributes.rs | 21 ++++ uniffi_udl/src/finder.rs | 9 +- 30 files changed, 211 insertions(+), 80 deletions(-) delete mode 100644 fixtures/ext-types/proc-macro-lib/build.rs delete mode 100644 fixtures/ext-types/proc-macro-lib/src/ext-types-lib.udl diff --git a/docs/manual/src/udl/ext_types_external.md b/docs/manual/src/udl/ext_types_external.md index 305e5f5eaa..ad0b4834cd 100644 --- a/docs/manual/src/udl/ext_types_external.md +++ b/docs/manual/src/udl/ext_types_external.md @@ -29,6 +29,9 @@ dictionary ConsumingDict { ``` +If in the above example, `DemoDict` was actually "exported" via a procmacro +you should instead use `ExternExport` + (Note the special type `extern` used on the `typedef`. It is not currently enforced that the literal `extern` is used, but we hope to enforce this later, so please use it!) diff --git a/fixtures/ext-types/lib/src/ext-types-lib.udl b/fixtures/ext-types/lib/src/ext-types-lib.udl index 8f62b23be5..38f5bf2d06 100644 --- a/fixtures/ext-types/lib/src/ext-types-lib.udl +++ b/fixtures/ext-types/lib/src/ext-types-lib.udl @@ -18,6 +18,7 @@ namespace imported_types_lib { UniffiOneInterface get_uniffi_one_interface(); ExternalCrateInterface get_external_crate_interface(string val); + UniffiOneProcMacroType get_uniffi_one_proc_macro_type(UniffiOneProcMacroType t); }; // A type defined in a .udl file in the `uniffi-one` crate (ie, in @@ -33,6 +34,10 @@ typedef extern UniffiOneEnum; [ExternalInterface="uniffi_one"] typedef extern UniffiOneInterface; +// A type defined via procmacros in an external crate +[ExternalExport="uniffi_one"] +typedef extern UniffiOneProcMacroType; + // A "wrapped" type defined in the guid crate (ie, defined in `../../guid/src/lib.rs` and // "declared" in `../../guid/src/guid.udl`). But it's still "external" from our POV, // So same as the `.udl` type above! diff --git a/fixtures/ext-types/lib/src/lib.rs b/fixtures/ext-types/lib/src/lib.rs index 52628e86f9..c9dc4fe02a 100644 --- a/fixtures/ext-types/lib/src/lib.rs +++ b/fixtures/ext-types/lib/src/lib.rs @@ -2,7 +2,7 @@ use custom_types::Handle; use ext_types_external_crate::{ExternalCrateDictionary, ExternalCrateInterface}; use ext_types_guid::Guid; use std::sync::Arc; -use uniffi_one::{UniffiOneEnum, UniffiOneInterface, UniffiOneType}; +use uniffi_one::{UniffiOneEnum, UniffiOneInterface, UniffiOneProcMacroType, UniffiOneType}; use url::Url; pub struct CombinedType { @@ -113,6 +113,10 @@ fn get_uniffi_one_interface() -> Arc { Arc::new(UniffiOneInterface::new()) } +fn get_uniffi_one_proc_macro_type(t: UniffiOneProcMacroType) -> UniffiOneProcMacroType { + t +} + fn get_external_crate_interface(val: String) -> Arc { Arc::new(ExternalCrateInterface::new(val)) } diff --git a/fixtures/ext-types/lib/tests/bindings/test_imported_types.kts b/fixtures/ext-types/lib/tests/bindings/test_imported_types.kts index 78cc8d033f..49937fe1ce 100644 --- a/fixtures/ext-types/lib/tests/bindings/test_imported_types.kts +++ b/fixtures/ext-types/lib/tests/bindings/test_imported_types.kts @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import uniffi.imported_types_lib.* -import uniffi.uniffi_one.* +import uniffi.uniffi_one_ns.* val ct = getCombinedType(null) assert(ct.uot.sval == "hello") @@ -27,6 +27,10 @@ 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) +assert(getMyProcMacroType(uopmt) == uopmt) + val uoe = UniffiOneEnum.ONE assert(getUniffiOneEnum(uoe) == uoe) assert(getMaybeUniffiOneEnum(uoe)!! == uoe) diff --git a/fixtures/ext-types/lib/tests/bindings/test_imported_types.py b/fixtures/ext-types/lib/tests/bindings/test_imported_types.py index 1cf60d8059..bf3b5cfe45 100644 --- a/fixtures/ext-types/lib/tests/bindings/test_imported_types.py +++ b/fixtures/ext-types/lib/tests/bindings/test_imported_types.py @@ -5,7 +5,7 @@ import unittest import urllib from imported_types_lib import * -from uniffi_one import * +from uniffi_one_ns import * class TestIt(unittest.TestCase): def test_it(self): @@ -48,5 +48,10 @@ def test_external_crate_types(self): self.assertEqual(ct.ecd.sval, "ecd"); self.assertEqual(get_external_crate_interface("foo").value(), "foo") + def test_procmacro_types(self): + t1 = UniffiOneProcMacroType("hello") + self.assertEqual(t1, get_uniffi_one_proc_macro_type(t1)) + self.assertEqual(t1, get_my_proc_macro_type(t1)) + if __name__=='__main__': unittest.main() diff --git a/fixtures/ext-types/lib/tests/bindings/test_imported_types.swift b/fixtures/ext-types/lib/tests/bindings/test_imported_types.swift index cec96613e7..833d0dbd4b 100644 --- a/fixtures/ext-types/lib/tests/bindings/test_imported_types.swift +++ b/fixtures/ext-types/lib/tests/bindings/test_imported_types.swift @@ -3,7 +3,6 @@ * 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) @@ -26,6 +25,7 @@ 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(getMyProcMacroType(t: UniffiOneProcMacroType(sval: "proc-macros")).sval == "proc-macros") assert(getUniffiOneEnum(e: UniffiOneEnum.one) == UniffiOneEnum.one) assert(getMaybeUniffiOneEnum(e: UniffiOneEnum.one)! == UniffiOneEnum.one) diff --git a/fixtures/ext-types/proc-macro-lib/build.rs b/fixtures/ext-types/proc-macro-lib/build.rs deleted file mode 100644 index 376a44c55a..0000000000 --- a/fixtures/ext-types/proc-macro-lib/build.rs +++ /dev/null @@ -1,7 +0,0 @@ -/* 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 deleted file mode 100644 index dbaf61ae64..0000000000 --- a/fixtures/ext-types/proc-macro-lib/src/ext-types-lib.udl +++ /dev/null @@ -1 +0,0 @@ -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 index 0a535b32e4..e6ad524d02 100644 --- a/fixtures/ext-types/proc-macro-lib/src/lib.rs +++ b/fixtures/ext-types/proc-macro-lib/src/lib.rs @@ -196,4 +196,4 @@ fn get_guid_procmacro(g: Option) -> Guid { ext_types_guid::get_guid(g) } -uniffi::include_scaffolding!("ext-types-lib"); +uniffi::setup_scaffolding!("imported_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 index 4d405f33ab..9dc98372e7 100644 --- 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 @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import uniffi.imported_types_lib.* -import uniffi.uniffi_one.* +import uniffi.uniffi_one_ns.* val ct = getCombinedType(null) assert(ct.uot.sval == "hello") @@ -29,6 +29,7 @@ assert(getMaybeUniffiOneTypes(listOf(uot, null)) == listOf(uot, null)) val uopmt = UniffiOneProcMacroType("hello from proc-macro world") assert(getUniffiOneProcMacroType(uopmt) == uopmt) +assert(getMyProcMacroType(uopmt) == uopmt) val uoe = UniffiOneEnum.ONE assert(getUniffiOneEnum(uoe) == uoe) 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 index 2f6aa2d376..37d046086f 100644 --- 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 @@ -6,7 +6,7 @@ import urllib from ext_types_guid import * from imported_types_lib import * -from uniffi_one import * +from uniffi_one_ns import * class TestIt(unittest.TestCase): def test_it(self): 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 index 891777d6ae..2bf9302e9a 100644 --- 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 @@ -3,7 +3,6 @@ * 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) @@ -28,6 +27,7 @@ assert(getUniffiOneTypes(ts: [UniffiOneType(sval: "hello")]) == [UniffiOneType(s 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(getMyProcMacroType(t: UniffiOneProcMacroType(sval: "proc-macros all the way down")).sval == "proc-macros all the way down") assert(getUniffiOneEnum(e: UniffiOneEnum.one) == UniffiOneEnum.one) assert(getMaybeUniffiOneEnum(e: UniffiOneEnum.one)! == UniffiOneEnum.one) diff --git a/fixtures/ext-types/uniffi-one/Cargo.toml b/fixtures/ext-types/uniffi-one/Cargo.toml index de1dbf9807..493db47288 100644 --- a/fixtures/ext-types/uniffi-one/Cargo.toml +++ b/fixtures/ext-types/uniffi-one/Cargo.toml @@ -8,6 +8,7 @@ publish = false [lib] crate-type = ["lib", "cdylib"] +# Note the crate name is different from the namespace used by this crate. name = "uniffi_one" [dependencies] diff --git a/fixtures/ext-types/uniffi-one/src/lib.rs b/fixtures/ext-types/uniffi-one/src/lib.rs index 41e0388572..cf905f48b4 100644 --- a/fixtures/ext-types/uniffi-one/src/lib.rs +++ b/fixtures/ext-types/uniffi-one/src/lib.rs @@ -29,4 +29,9 @@ impl UniffiOneInterface { } } +#[uniffi::export] +fn get_my_proc_macro_type(t: UniffiOneProcMacroType) -> UniffiOneProcMacroType { + t +} + uniffi::include_scaffolding!("uniffi-one"); diff --git a/fixtures/ext-types/uniffi-one/src/uniffi-one.udl b/fixtures/ext-types/uniffi-one/src/uniffi-one.udl index 900833dad7..fed862c614 100644 --- a/fixtures/ext-types/uniffi-one/src/uniffi-one.udl +++ b/fixtures/ext-types/uniffi-one/src/uniffi-one.udl @@ -1,4 +1,4 @@ -namespace uniffi_one {}; +namespace uniffi_one_ns {}; dictionary UniffiOneType { string sval; diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs index 2c1a3e468d..5749245a1c 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs @@ -144,11 +144,13 @@ impl<'a> TypeRenderer<'a> { } // Get the package name for an external type - fn external_type_package_name(&self, module_path: &str) -> String { + fn external_type_package_name(&self, module_path: &str, namespace: &str) -> String { + // config overrides are keyed by the crate name, default fallback is the namespace. let crate_name = module_path.split("::").next().unwrap(); match self.kotlin_config.external_packages.get(crate_name) { Some(name) => name.clone(), - None => crate_name.to_string(), + // unreachable in library mode - all deps are in our config with correct namespace. + None => format!("uniffi.{namespace}"), } } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ExternalTypeTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ExternalTypeTemplate.kt index 1e08de3b9e..0fade7a0bc 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ExternalTypeTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ExternalTypeTemplate.kt @@ -1,4 +1,4 @@ -{%- let package_name=self.external_type_package_name(module_path) %} +{%- let package_name=self.external_type_package_name(module_path, namespace) %} {%- let fully_qualified_type_name = "{}.{}"|format(package_name, name) %} {%- let fully_qualified_ffi_converter_name = "{}.FfiConverterType{}"|format(package_name, name) %} {%- let fully_qualified_rustbuffer_name = "{}.RustBuffer"|format(package_name) %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt index a9f75a0c3b..2359679911 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt @@ -94,7 +94,7 @@ {%- when Type::Custom { module_path, name, builtin } %} {% include "CustomTypeTemplate.kt" %} -{%- when Type::External { module_path, name, kind } %} +{%- when Type::External { module_path, name, namespace, kind, tagged } %} {% include "ExternalTypeTemplate.kt" %} {%- else %} diff --git a/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py index 651431db58..71e05e8b06 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py @@ -1,8 +1,9 @@ -{%- let mod_name = module_path|fn_name %} +{%- let ns = namespace|fn_name %} +# External type {{name}} is in namespace "{{namespace}}", crate {{module_path}} {%- let ffi_converter_name = "_UniffiConverterType{}"|format(name) %} -{{ self.add_import_of(mod_name, ffi_converter_name) }} -{{ self.add_import_of(mod_name, name) }} {#- import the type alias itself -#} +{{ self.add_import_of(ns, ffi_converter_name) }} +{{ self.add_import_of(ns, name) }} {#- import the type alias itself -#} {%- let rustbuffer_local_name = "_UniffiRustBuffer{}"|format(name) %} -{{ self.add_import_of_as(mod_name, "_UniffiRustBuffer", rustbuffer_local_name) }} +{{ self.add_import_of_as(ns, "_UniffiRustBuffer", rustbuffer_local_name) }} diff --git a/uniffi_bindgen/src/bindings/python/templates/Types.py b/uniffi_bindgen/src/bindings/python/templates/Types.py index a2b29f799e..aef2996438 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Types.py +++ b/uniffi_bindgen/src/bindings/python/templates/Types.py @@ -91,7 +91,7 @@ {%- when Type::Custom { name, module_path, builtin } %} {%- include "CustomType.py" %} -{%- when Type::External { name, module_path, kind } %} +{%- when Type::External { name, module_path, namespace, kind, tagged } %} {%- include "ExternalTemplate.py" %} {%- when Type::ForeignExecutor %} diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index 7fdcb25ebd..e43e652bb3 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -252,16 +252,21 @@ impl ComponentInterface { /// Get details about all `Type::External` types. /// Returns an iterator of (name, crate_name, kind) - pub fn iter_external_types(&self) -> impl Iterator { + pub fn iter_external_types( + &self, + ) -> impl Iterator { self.types.iter_known_types().filter_map(|t| match t { Type::External { name, module_path, kind, + tagged, + .. } => Some(( name, module_path.split("::").next().unwrap().to_string(), *kind, + *tagged, )), _ => None, }) diff --git a/uniffi_bindgen/src/library_mode.rs b/uniffi_bindgen/src/library_mode.rs index a60909c07f..26ca522482 100644 --- a/uniffi_bindgen/src/library_mode.rs +++ b/uniffi_bindgen/src/library_mode.rs @@ -26,7 +26,7 @@ use std::{ collections::{HashMap, HashSet}, fs, }; -use uniffi_meta::{group_metadata, MetadataGroup}; +use uniffi_meta::{create_metadata_groups, fixup_external_type, group_metadata, MetadataGroup}; /// Generate foreign bindings /// @@ -121,8 +121,34 @@ fn find_sources( library_path: &Utf8Path, cdylib_name: Option<&str>, ) -> Result> { - group_metadata(macro_metadata::extract_from_library(library_path)?)? - .into_iter() + let items = macro_metadata::extract_from_library(library_path)?; + let mut metadata_groups = create_metadata_groups(&items); + group_metadata(&mut metadata_groups, items)?; + + // Collect and process all UDL from all groups at the start - the fixups + // of external types makes this tricky to do as we finalize the group. + let mut udl_items: HashMap = HashMap::new(); + + for group in metadata_groups.values() { + let package = find_package_by_crate_name(cargo_metadata, &group.namespace.crate_name)?; + let crate_root = package + .manifest_path + .parent() + .context("manifest path has no parent")?; + let crate_name = group.namespace.crate_name.clone(); + if let Some(mut metadata_group) = load_udl_metadata(group, crate_root, &crate_name)? { + // fixup the items. + metadata_group.items = metadata_group + .items + .into_iter() + .map(|item| fixup_external_type(item, &metadata_groups)) + .collect(); + udl_items.insert(crate_name, metadata_group); + }; + } + + metadata_groups + .into_values() .map(|group| { let package = find_package_by_crate_name(cargo_metadata, &group.namespace.crate_name)?; let crate_root = package @@ -131,7 +157,7 @@ fn find_sources( .context("manifest path has no parent")?; let crate_name = group.namespace.crate_name.clone(); let mut ci = ComponentInterface::new(&crate_name); - if let Some(metadata) = load_udl_metadata(&group, crate_root, &crate_name)? { + if let Some(metadata) = udl_items.remove(&crate_name) { ci.add_metadata(metadata)?; }; ci.add_metadata(group)?; diff --git a/uniffi_bindgen/src/macro_metadata/ci.rs b/uniffi_bindgen/src/macro_metadata/ci.rs index 351443f66c..c5642c08ba 100644 --- a/uniffi_bindgen/src/macro_metadata/ci.rs +++ b/uniffi_bindgen/src/macro_metadata/ci.rs @@ -4,7 +4,9 @@ use crate::interface::{CallbackInterface, ComponentInterface, Enum, Record, Type}; use anyhow::{bail, Context}; -use uniffi_meta::{group_metadata, EnumMetadata, ErrorMetadata, Metadata, MetadataGroup}; +use uniffi_meta::{ + create_metadata_groups, group_metadata, EnumMetadata, ErrorMetadata, Metadata, MetadataGroup, +}; /// Add Metadata items to the ComponentInterface /// @@ -18,7 +20,9 @@ pub fn add_to_ci( iface: &mut ComponentInterface, metadata_items: Vec, ) -> anyhow::Result<()> { - for group in group_metadata(metadata_items)? { + let mut group_map = create_metadata_groups(&metadata_items); + group_metadata(&mut group_map, metadata_items)?; + for group in group_map.into_values() { if group.items.is_empty() { continue; } diff --git a/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs index 0a851be04e..ade1578897 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs @@ -1,14 +1,17 @@ // Support for external types. // Types with an external `FfiConverter`... -{% for (name, crate_name, kind) in ci.iter_external_types() %} +{% for (name, crate_name, kind, tagged) in ci.iter_external_types() %} // The FfiConverter for `{{ name }}` is defined in `{{ crate_name }}` +// If it has its existing FfiConverter defined with a UniFFITag, it needs forwarding. +{% if tagged %} {%- match kind %} {%- when ExternalKind::DataClass %} ::uniffi::ffi_converter_forward!(r#{{ name }}, ::{{ crate_name|crate_name_rs }}::UniFfiTag, crate::UniFfiTag); {%- when ExternalKind::Interface %} ::uniffi::ffi_converter_arc_forward!(r#{{ name }}, ::{{ crate_name|crate_name_rs }}::UniFfiTag, crate::UniFfiTag); {%- endmatch %} +{% endif %} {%- endfor %} // We generate support for each Custom Type and the builtin type it uses. diff --git a/uniffi_checksum_derive/src/lib.rs b/uniffi_checksum_derive/src/lib.rs index bf1a42fc15..e600982d08 100644 --- a/uniffi_checksum_derive/src/lib.rs +++ b/uniffi_checksum_derive/src/lib.rs @@ -81,9 +81,12 @@ pub fn checksum_derive(input: TokenStream) -> TokenStream { .named .iter() .map(|field| field.ident.as_ref().unwrap()); - let field_stmts = field_idents - .clone() - .map(|ident| quote! { Checksum::checksum(#ident, state); }); + let field_stmts = fields.named.iter() + .filter(|field| !has_ignore_attribute(&field.attrs)) + .map(|field| { + let ident = field.ident.as_ref().unwrap(); + quote! { Checksum::checksum(#ident, state); } + }); quote! { Self::#ident { #(#field_idents,)* } => { #discriminant; diff --git a/uniffi_meta/src/group.rs b/uniffi_meta/src/group.rs index 4f39466579..f0be2e5a98 100644 --- a/uniffi_meta/src/group.rs +++ b/uniffi_meta/src/group.rs @@ -7,10 +7,12 @@ use std::collections::{BTreeSet, HashMap}; use crate::*; use anyhow::{bail, Result}; -/// Group metadata by namespace -pub fn group_metadata(items: Vec) -> Result> { +type MetadataGroupMap = HashMap; + +// Create empty metadata groups based on the metadata items. +pub fn create_metadata_groups(items: &[Metadata]) -> MetadataGroupMap { // Map crate names to MetadataGroup instances - let mut group_map = items + items .iter() .filter_map(|i| match i { Metadata::Namespace(namespace) => { @@ -33,48 +35,29 @@ pub fn group_metadata(items: Vec) -> Result> { } _ => None, }) - .collect::>(); + .collect::>() +} +/// Consume the items into the previously created metadata groups. +pub fn group_metadata(group_map: &mut MetadataGroupMap, items: Vec) -> Result<()> { for item in items { - let (item_desc, module_path) = match &item { - Metadata::Namespace(_) => continue, - Metadata::UdlFile(meta) => { - (format!("udl_file `{}`", meta.namespace), &meta.module_path) - } - Metadata::Func(meta) => (format!("function `{}`", meta.name), &meta.module_path), - Metadata::Constructor(meta) => ( - format!("constructor `{}.{}`", meta.self_name, meta.name), - &meta.module_path, - ), - Metadata::Method(meta) => ( - format!("method `{}.{}`", meta.self_name, meta.name), - &meta.module_path, - ), - Metadata::Record(meta) => (format!("record `{}`", meta.name), &meta.module_path), - Metadata::Enum(meta) => (format!("enum `{}`", meta.name), &meta.module_path), - Metadata::Object(meta) => (format!("object `{}`", meta.name), &meta.module_path), - Metadata::CallbackInterface(meta) => ( - format!("callback interface `{}`", meta.name), - &meta.module_path, - ), - Metadata::TraitMethod(meta) => { - (format!("trait method`{}`", meta.name), &meta.module_path) - } - Metadata::Error(meta) => (format!("error `{}`", meta.name()), meta.module_path()), - Metadata::CustomType(meta) => (format!("custom `{}`", meta.name), &meta.module_path), - }; + if matches!(&item, Metadata::Namespace(_)) { + continue; + } + + let crate_name = calc_crate_name(item.module_path()).to_owned(); // XXX - kill clone? - let crate_name = calc_crate_name(module_path); - let group = match group_map.get_mut(crate_name) { + let item = fixup_external_type(item, group_map); + let group = match group_map.get_mut(&crate_name) { Some(ns) => ns, - None => bail!("Unknown namespace for {item_desc} ({crate_name})"), + None => bail!("Unknown namespace for {item:?} ({crate_name})"), }; if group.items.contains(&item) { bail!("Duplicate metadata item: {item:?}"); } group.add_item(item); } - Ok(group_map.into_values().collect()) + Ok(()) } #[derive(Debug)] @@ -85,19 +68,35 @@ pub struct MetadataGroup { 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)); + self.items.insert(item); } } +pub fn fixup_external_type(item: Metadata, group_map: &MetadataGroupMap) -> Metadata { + let crate_name = calc_crate_name(item.module_path()).to_owned(); + let converter = ExternalTypeConverter { + crate_name: &crate_name, + crate_to_namespace: group_map, + }; + converter.convert_item(item) +} + /// Convert metadata items by replacing types from external crates with Type::External struct ExternalTypeConverter<'a> { crate_name: &'a str, + crate_to_namespace: &'a MetadataGroupMap, } impl<'a> ExternalTypeConverter<'a> { + fn crate_to_namespace(&self, crate_name: &str) -> String { + self.crate_to_namespace + .get(crate_name) + .unwrap_or_else(|| panic!("Can't find namespace for module {crate_name}")) + .namespace + .name + .clone() + } + fn convert_item(&self, item: Metadata) -> Metadata { match item { Metadata::Func(meta) => Metadata::Func(FnMetadata { @@ -183,9 +182,11 @@ impl<'a> ExternalTypeConverter<'a> { if self.is_module_path_external(&module_path) => { Type::External { + namespace: self.crate_to_namespace(&module_path), module_path, name, kind: ExternalKind::DataClass, + tagged: false, } } Type::Custom { @@ -194,17 +195,21 @@ impl<'a> ExternalTypeConverter<'a> { // 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 { + namespace: self.crate_to_namespace(&module_path), module_path, name, kind: ExternalKind::DataClass, + tagged: false, } } Type::Object { module_path, name, .. } if self.is_module_path_external(&module_path) => Type::External { + namespace: self.crate_to_namespace(&module_path), module_path, name, kind: ExternalKind::Interface, + tagged: false, }, Type::CallbackInterface { module_path, name } if self.is_module_path_external(&module_path) => @@ -235,6 +240,24 @@ impl<'a> ExternalTypeConverter<'a> { key_type: Box::new(self.convert_type(*key_type)), value_type: Box::new(self.convert_type(*value_type)), }, + // Existing External types probably need namespace fixed. + Type::External { + namespace, + module_path, + name, + kind, + tagged, + } => { + assert!(namespace.is_empty()); + Type::External { + namespace: self.crate_to_namespace(&module_path), + module_path, + name, + kind, + tagged, + } + } + // Otherwise, just return the type unchanged _ => ty, } diff --git a/uniffi_meta/src/lib.rs b/uniffi_meta/src/lib.rs index c62c092623..5cfdee9607 100644 --- a/uniffi_meta/src/lib.rs +++ b/uniffi_meta/src/lib.rs @@ -9,7 +9,7 @@ mod ffi_names; pub use ffi_names::*; mod group; -pub use group::{group_metadata, MetadataGroup}; +pub use group::{create_metadata_groups, fixup_external_type, group_metadata, MetadataGroup}; mod reader; pub use reader::{read_metadata, read_metadata_type}; @@ -403,6 +403,23 @@ impl Metadata { pub fn read(data: &[u8]) -> anyhow::Result { read_metadata(data) } + + pub(crate) fn module_path(&self) -> &String { + match self { + Metadata::Namespace(meta) => &meta.crate_name, + Metadata::UdlFile(meta) => &meta.module_path, + Metadata::Func(meta) => &meta.module_path, + Metadata::Constructor(meta) => &meta.module_path, + Metadata::Method(meta) => &meta.module_path, + Metadata::Record(meta) => &meta.module_path, + Metadata::Enum(meta) => &meta.module_path, + Metadata::Object(meta) => &meta.module_path, + Metadata::CallbackInterface(meta) => &meta.module_path, + Metadata::TraitMethod(meta) => &meta.module_path, + Metadata::Error(meta) => meta.module_path(), + Metadata::CustomType(meta) => &meta.module_path, + } + } } impl From for Metadata { diff --git a/uniffi_meta/src/types.rs b/uniffi_meta/src/types.rs index 4129a0735c..24f8a6f2a8 100644 --- a/uniffi_meta/src/types.rs +++ b/uniffi_meta/src/types.rs @@ -114,7 +114,10 @@ pub enum Type { External { module_path: String, name: String, + #[checksum_ignore] // The namespace is not known generating scaffolding. + namespace: String, kind: ExternalKind, + tagged: bool, // does its FfiConverter use ? }, // Custom type on the scaffolding side Custom { diff --git a/uniffi_udl/src/attributes.rs b/uniffi_udl/src/attributes.rs index 2f9c372390..a0430da3b2 100644 --- a/uniffi_udl/src/attributes.rs +++ b/uniffi_udl/src/attributes.rs @@ -35,6 +35,7 @@ pub(super) enum Attribute { External { crate_name: String, kind: ExternalKind, + export: bool, }, // Custom type on the scaffolding side Custom, @@ -77,10 +78,22 @@ impl TryFrom<&weedle::attribute::ExtendedAttribute<'_>> for Attribute { "External" => Ok(Attribute::External { crate_name: name_from_id_or_string(&identity.rhs), kind: ExternalKind::DataClass, + export: false, + }), + "ExternalExport" => Ok(Attribute::External { + crate_name: name_from_id_or_string(&identity.rhs), + kind: ExternalKind::DataClass, + export: true, }), "ExternalInterface" => Ok(Attribute::External { crate_name: name_from_id_or_string(&identity.rhs), kind: ExternalKind::Interface, + export: false, + }), + "ExternalInterfaceExport" => Ok(Attribute::External { + crate_name: name_from_id_or_string(&identity.rhs), + kind: ExternalKind::Interface, + export: true, }), _ => anyhow::bail!( "Attribute identity Identifier not supported: {:?}", @@ -484,6 +497,14 @@ impl TypedefAttributes { _ => None, }) } + + pub(super) fn external_tagged(&self) -> Option { + // If it was "exported" via a proc-macro the FfiConverter was not tagged. + self.0.iter().find_map(|attr| match attr { + Attribute::External { export, .. } => Some(!*export), + _ => None, + }) + } } impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for TypedefAttributes { diff --git a/uniffi_udl/src/finder.rs b/uniffi_udl/src/finder.rs index f0f76547ad..35daabd5a9 100644 --- a/uniffi_udl/src/finder.rs +++ b/uniffi_udl/src/finder.rs @@ -130,7 +130,8 @@ impl TypeFinder for weedle::TypedefDefinition<'_> { }, ) } else { - let kind = attrs.external_kind().expect("ExternalKind missing"); + let kind = attrs.external_kind().expect("External missing"); + let tagged = attrs.external_tagged().expect("External missing"); // A crate which can supply an `FfiConverter`. // We don't reference `self._type`, so ideally we could insist on it being // the literal 'extern' but that's tricky @@ -138,8 +139,10 @@ impl TypeFinder for weedle::TypedefDefinition<'_> { name, Type::External { name: name.to_string(), + namespace: "".to_string(), // we don't know this yet module_path: attrs.get_crate_name(), kind, + tagged, }, ) } @@ -249,11 +252,11 @@ mod test { "#, |types| { assert!( - matches!(types.get_type_definition("ExternalType").unwrap(), Type::External { name, module_path, kind: ExternalKind::DataClass } + matches!(types.get_type_definition("ExternalType").unwrap(), Type::External { name, module_path, kind: ExternalKind::DataClass, .. } if name == "ExternalType" && module_path == "crate-name") ); assert!( - matches!(types.get_type_definition("ExternalInterfaceType").unwrap(), Type::External { name, module_path, kind: ExternalKind::Interface } + matches!(types.get_type_definition("ExternalInterfaceType").unwrap(), Type::External { name, module_path, kind: ExternalKind::Interface, .. } if name == "ExternalInterfaceType" && module_path == "crate-name") ); assert!( From 2a8213d03c73b1a720f7ca3251b0439a7f3ee897 Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Mon, 2 Oct 2023 16:00:55 -0400 Subject: [PATCH 08/24] Update docs and tests for HashMap<>/record<> (#1775) --- docs/manual/src/udl/builtin_types.md | 2 +- fixtures/proc-macro/src/lib.rs | 13 ++++++++++++- .../proc-macro/tests/bindings/test_proc_macro.py | 5 +++++ uniffi_core/src/ffi_converter_impls.rs | 5 +---- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/manual/src/udl/builtin_types.md b/docs/manual/src/udl/builtin_types.md index 9930e9fb2b..4c1e618c78 100644 --- a/docs/manual/src/udl/builtin_types.md +++ b/docs/manual/src/udl/builtin_types.md @@ -15,7 +15,7 @@ The following built-in types can be passed as arguments/returned by Rust methods | `&T` | `[ByRef] T` | This works for `&str` and `&[T]` | | `Option` | `T?` | | | `Vec` | `sequence` | | -| `HashMap` | `record` | Only string keys are supported | +| `HashMap` | `record` | | | `()` | `void` | Empty return | | `Result` | N/A | See [Errors](./errors.md) section | diff --git a/fixtures/proc-macro/src/lib.rs b/fixtures/proc-macro/src/lib.rs index 0dc62ad972..11af491694 100644 --- a/fixtures/proc-macro/src/lib.rs +++ b/fixtures/proc-macro/src/lib.rs @@ -2,7 +2,7 @@ * 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 std::{collections::HashMap, sync::Arc}; mod callback_interface; @@ -116,6 +116,17 @@ fn take_two(two: Two) -> String { two.a } +#[uniffi::export] +fn make_hashmap(k: i8, v: u64) -> HashMap { + HashMap::from([(k, v)]) +} + +// XXX - fails to call this from python - https://github.com/mozilla/uniffi-rs/issues/1774 +#[uniffi::export] +fn return_hashmap(h: HashMap) -> HashMap { + h +} + #[uniffi::export] fn take_record_with_bytes(rwb: RecordWithBytes) -> Vec { rwb.some_bytes diff --git a/fixtures/proc-macro/tests/bindings/test_proc_macro.py b/fixtures/proc-macro/tests/bindings/test_proc_macro.py index 009d7cdb4b..c80489fc1a 100644 --- a/fixtures/proc-macro/tests/bindings/test_proc_macro.py +++ b/fixtures/proc-macro/tests/bindings/test_proc_macro.py @@ -33,6 +33,11 @@ assert(make_zero().inner == "ZERO") assert(make_record_with_bytes().some_bytes == bytes([0, 1, 2, 3, 4])) +assert(make_hashmap(1, 2) == {1: 2}) +# fails with AttributeError!? - https://github.com/mozilla/uniffi-rs/issues/1774 +# d = {1, 2} +# assert(return_hashmap(d) == d) + try: always_fails() except BasicError.OsError: diff --git a/uniffi_core/src/ffi_converter_impls.rs b/uniffi_core/src/ffi_converter_impls.rs index 44707399bf..6eb29ba8a5 100644 --- a/uniffi_core/src/ffi_converter_impls.rs +++ b/uniffi_core/src/ffi_converter_impls.rs @@ -385,10 +385,7 @@ unsafe impl> FfiConverter for Vec { MetadataBuffer::from_code(metadata::codes::TYPE_VEC).concat(T::TYPE_ID_META); } -/// Support for associative arrays via the FFI. -/// Note that because of webidl limitations, -/// the key must always be of the String type. -/// +/// Support for associative arrays via the FFI - `record` in UDL. /// HashMaps are currently always passed by serializing to a buffer. /// We write a `i32` entries count followed by each entry (string /// key followed by the value) in turn. From 20d4d8ccab416bb7a8b87af299ffc38391050779 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Tue, 5 Sep 2023 17:14:57 -0400 Subject: [PATCH 09/24] Updated Future FFI The initial motivation for this was cancellation. PR#1697 made it so if an async function was cancelled we would eventually release the resources. However, it would be better if we could immedately release the resources. In order to implement that, the future FFI needed to change quite a bit and most of these changes are good. The new FFI is simpler overall and supports cancel and drop operations. It's actually quite close to the initial FFI that Hywan proposed. Cancel ensures that the foreign code will resume and break out of its async code. Drop ensures that all resources from the wrapped future are relased. Note: the new FFI has a cancel method, but no bindings use it yet. For Python/Kotlin we don't need to because they throw cancellation exceptions, which means cancellation support falls out from the new API without any extra work. This cancel method could be used for Swift, but we still need to think this through in a different PR. The new code does not use ForeignExecutor anymore, so that code is in a state of limbo. I hope to repurpose it for foreign dispatch queues (#1734). If that doesn't work out, we can just delete it. The FFI calls need some care since we pass a type-erased handle to the foreign code, while RustFuture is generic over an anonymous Future type: - The concrete RustFuture type implements the `RustFutureFFI` trait. - We give the foreign side a leaked `Box>>>`. - We hand-monomorphize, creating a scaffolding function for each RustFutureFFI method, for each possible FFI type. Updated proc macros lift arguments in 2 phases. First we call `try_lift` on all arguments, generating a `Result`. Then we call the rust function using that tuple or handle the error. This is needed because the new async code adds a `Send` bound futures. The `Send` bound is good because futures are going to be moving across threads as we poll/wake them. However, the lift code won't always be Send because it may deal with raw pointers. To deal with that, we perform the non-Send lifting outside of the future, then create a Send future from the result. This means that the lift phase is executed outside of the async context, but it should be very fast. This change also allows futures that return Results to attempt to downcast lift errors. More changes: - Updated the futures fixture tests for this to hold on to the mutex longer in the initial call. This makes it so they will fail unless the future is dropped while the mutex is still locked. Before they would only succeed as long as the mutex was dropped once the timeout expired. - Updated `RustCallStatus.code` field to be an enum. Added `Cancelled` as one of the variants. `Cancelled` is only used for async functions. - Removed the FutureCallback and invoke_future_callback from `FfiConverter`. - New syncronization handling code in RustFuture that's hopefully clearer, more correct, and more understandable than the old stuff. - Updated `UNIFFI_CONTRACT_VERSION` since this is an ABI change - Removed the `RustCallStatus` param from async scaffolding functions. These functions can't fail, so there's no need. - Added is_async() to the Callable trait. - Changed `handle_failed_lift` signature. Now, if a `Result<>` is able to downcast the error, it returns `Self::Err` directly instead of serializing the error into `RustBuffer`. Co-authored-by: Ivan Enderlin Co-authored-by: Jonas Platte --- Cargo.lock | 1 + docs/manual/src/futures.md | 96 +- fixtures/futures/src/lib.rs | 6 + .../futures/tests/bindings/test_futures.kts | 16 +- .../futures/tests/bindings/test_futures.py | 36 +- .../reexport-scaffolding-macro/src/lib.rs | 14 +- .../src/bindings/kotlin/gen_kotlin/mod.rs | 13 +- .../src/bindings/kotlin/templates/Async.kt | 44 + .../bindings/kotlin/templates/AsyncTypes.kt | 47 - .../src/bindings/kotlin/templates/Helpers.kt | 9 +- .../kotlin/templates/ObjectTemplate.kt | 51 +- .../templates/TopLevelFunctionTemplate.kt | 43 +- .../src/bindings/kotlin/templates/Types.kt | 5 +- .../src/bindings/kotlin/templates/wrapper.kt | 5 + .../src/bindings/python/gen_python/mod.rs | 29 +- .../src/bindings/python/templates/Async.py | 40 + .../bindings/python/templates/AsyncTypes.py | 36 - .../src/bindings/python/templates/Helpers.py | 23 +- .../templates/TopLevelFunctionTemplate.py | 26 +- .../src/bindings/python/templates/Types.py | 4 - .../src/bindings/python/templates/macros.py | 28 +- .../src/bindings/python/templates/wrapper.py | 5 + .../src/bindings/ruby/gen_ruby/mod.rs | 4 +- .../templates/NamespaceLibraryTemplate.rb | 2 +- .../src/bindings/swift/gen_swift/mod.rs | 37 +- .../src/bindings/swift/templates/Async.swift | 62 + .../bindings/swift/templates/AsyncTypes.swift | 29 - .../swift/templates/BridgingHeaderTemplate.h | 6 +- .../bindings/swift/templates/Helpers.swift | 4 + .../swift/templates/ObjectTemplate.swift | 38 +- .../templates/TopLevelFunctionTemplate.swift | 38 +- .../src/bindings/swift/templates/Types.swift | 4 - .../bindings/swift/templates/wrapper.swift | 4 + uniffi_bindgen/src/interface/ffi.rs | 37 +- uniffi_bindgen/src/interface/function.rs | 38 +- uniffi_bindgen/src/interface/mod.rs | 163 ++- uniffi_bindgen/src/interface/object.rs | 8 + uniffi_bindgen/src/scaffolding/mod.rs | 7 +- uniffi_core/Cargo.toml | 1 + uniffi_core/src/ffi/rustbuffer.rs | 1 + uniffi_core/src/ffi/rustcalls.rs | 78 +- uniffi_core/src/ffi/rustfuture.rs | 1005 +++++++++-------- uniffi_core/src/ffi_converter_impls.rs | 32 +- uniffi_core/src/ffi_converter_traits.rs | 36 +- uniffi_core/src/lib.rs | 25 - uniffi_macros/src/export.rs | 10 - uniffi_macros/src/export/scaffolding.rs | 106 +- uniffi_macros/src/fnsig.rs | 67 +- uniffi_macros/src/object.rs | 10 - uniffi_macros/src/setup_scaffolding.rs | 83 ++ uniffi_meta/src/lib.rs | 2 +- 51 files changed, 1413 insertions(+), 1101 deletions(-) create mode 100644 uniffi_bindgen/src/bindings/kotlin/templates/Async.kt delete mode 100644 uniffi_bindgen/src/bindings/kotlin/templates/AsyncTypes.kt create mode 100644 uniffi_bindgen/src/bindings/python/templates/Async.py delete mode 100644 uniffi_bindgen/src/bindings/python/templates/AsyncTypes.py create mode 100644 uniffi_bindgen/src/bindings/swift/templates/Async.swift delete mode 100644 uniffi_bindgen/src/bindings/swift/templates/AsyncTypes.swift diff --git a/Cargo.lock b/Cargo.lock index f34a129aa8..8a93d1334a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1971,6 +1971,7 @@ dependencies = [ "bytes", "camino", "log", + "once_cell", "oneshot", "paste", "static_assertions", diff --git a/docs/manual/src/futures.md b/docs/manual/src/futures.md index c127ea205e..d6279236ab 100644 --- a/docs/manual/src/futures.md +++ b/docs/manual/src/futures.md @@ -4,7 +4,7 @@ UniFFI supports exposing async Rust functions over the FFI. It can convert a Rus Check out the [examples](https://github.com/mozilla/uniffi-rs/tree/main/examples/futures) or the more terse and thorough [fixtures](https://github.com/mozilla/uniffi-rs/tree/main/fixtures/futures). -Note that currently async functions are only supported by proc-macros, if you require UDL support please file a bug. +Note that currently async functions are only supported by proc-macros, UDL support is being planned in https://github.com/mozilla/uniffi-rs/issues/1716. ## Example @@ -41,97 +41,3 @@ In Rust `Future` terminology this means the foreign bindings supply the "executo There are [some great API docs](https://docs.rs/uniffi_core/latest/uniffi_core/ffi/rustfuture/index.html) on the implementation that are well worth a read. See the [foreign-executor fixture](https://github.com/mozilla/uniffi-rs/tree/main/fixtures/foreign-executor) for more implementation details. - -## How it works - -As [described in the documentation](https://docs.rs/uniffi_core/latest/uniffi_core/ffi/rustfuture/index.html), -UniFFI generates code which uses callbacks from Rust futures back into that foreign "executor" to drive them to completion. -Fortunately, each of the bindings and Rust have similar models, so the discussion below is Python, but it's almost exactly the same in Kotlin and Swift. - -In the above example, the generated `say_after` function looks something like: - -```python - -# A helper to work with asyncio. -def _rust_say_after_executor(eventloop_handle, rust_task_handle): - event_loop = UniFFIMagic_GetExecutor(eventloop_handle) - - def callback(task_handle): - # The event-loop has called us - call back into Rust. - _uniffi_say_after_executor_callback(task_handle) - - # Now have the asyncio eventloop - ask it to schedule a call to help drive the Rust future. - eventloop.call_soon_threadsafe(callback, rust_task_handle) - -# A helper for say_after which creates a future and passes it Rust -def _rust_call_say_after(callback_fn): - # Handle to our executor. - eventloop = asyncio.get_running_loop() - eventloop_handle = UniFFIMagic_SetExecutor(eventloop) - - # Use asyncio to create a new Python future. - future = eventloop.create_future() - future_handle = UniFFIMagic_SetFuture(future) - - # This is a "normal" UniFFI call across the FFI to Rust scaffoloding, but - # because it is an async function it has a special signature which - # requires the handles and the callback. - _uniffi_call_say_after(executor_handle, callback_fun, future_handle) - - # and return the future to the caller. - return future - -def say_after_callback(future_handle, result) - future = UniFFIMagic_GetFuture(future_handle) - if future.cancelled(): - return - future.set_result(result)) - -def say_after(...): - return await _rust_call_say_after(say_after_callback) - -``` - -And the code generated for Rust is something like: - -```rust -struct SayAfterHelper { - rust_future: Future<>, - uniffi_executor_handle: ::uniffi::ForeignExecutorHandle, - uniffi_callback: ::uniffi::FfiConverter::FutureCallback, - uniffi_future_handle: ..., -} - -impl SayAfterHelper { - fn wake(&self) { - match self.rust_future.poll() { - Some(Poll::Pending) => { - // ... snip executor stuff - self.rust_future.wake() - }, - Some(Poll::Ready(v)) => { - // ready - tell the foreign executor - UniFFI_Magic_Invoke_Foreign_Callback(self.uniffi_callback, self.uniffi_future_handle) - }, - None => todo!("error handling"), - } - } -} - -pub extern "C" fn _uniffi_call_say_after( - uniffi_executor_handle: ::uniffi::ForeignExecutorHandle, - uniffi_callback: ::uniffi::FfiConverter::FutureCallback, - uniffi_future_handle: ..., -) { - // Call the async function to get the Rust future. - let rust_future = say_after(...) - let helper = SayAfterHelper { - rust_future, - uniffi_executor_handle, - uniffi_callback, - uniffi_future_handle, - ); - helper.wake(); - Ok(()) -} -``` \ No newline at end of file diff --git a/fixtures/futures/src/lib.rs b/fixtures/futures/src/lib.rs index f077ed4299..418f3d19a0 100644 --- a/fixtures/futures/src/lib.rs +++ b/fixtures/futures/src/lib.rs @@ -151,6 +151,12 @@ pub async fn async_maybe_new_megaphone(y: bool) -> Option> { } } +/// Async function that inputs `Megaphone`. +#[uniffi::export] +pub async fn say_after_with_megaphone(megaphone: Arc, ms: u16, who: String) -> String { + megaphone.say_after(ms, who).await +} + /// A megaphone. Be careful with the neighbours. #[derive(uniffi::Object)] pub struct Megaphone; diff --git a/fixtures/futures/tests/bindings/test_futures.kts b/fixtures/futures/tests/bindings/test_futures.kts index 0ac594bc0f..810bb40f41 100644 --- a/fixtures/futures/tests/bindings/test_futures.kts +++ b/fixtures/futures/tests/bindings/test_futures.kts @@ -92,6 +92,17 @@ runBlocking { assertApproximateTime(time, 200, "async methods") } +runBlocking { + val megaphone = newMegaphone() + val time = measureTimeMillis { + val resultAlice = sayAfterWithMegaphone(megaphone, 200U, "Alice") + + assert(resultAlice == "HELLO, ALICE!") + } + + assertApproximateTime(time, 200, "async methods") +} + // Test async method returning optional object runBlocking { val megaphone = asyncMaybeNewMegaphone(true) @@ -209,7 +220,7 @@ runBlocking { runBlocking { val time = measureTimeMillis { val job = launch { - useSharedResource(SharedResourceOptions(releaseAfterMs=100U, timeoutMs=1000U)) + useSharedResource(SharedResourceOptions(releaseAfterMs=5000U, timeoutMs=100U)) } // Wait some time to ensure the task has locked the shared resource @@ -233,6 +244,3 @@ runBlocking { } println("useSharedResource (not canceled): ${time}ms") } - -// Test that we properly cleaned up future callback references -assert(uniffiActiveFutureCallbacks.size == 0) diff --git a/fixtures/futures/tests/bindings/test_futures.py b/fixtures/futures/tests/bindings/test_futures.py index eab2fc0ac9..bfbeba86f8 100644 --- a/fixtures/futures/tests/bindings/test_futures.py +++ b/fixtures/futures/tests/bindings/test_futures.py @@ -74,6 +74,19 @@ async def test(): asyncio.run(test()) + def test_async_object_param(self): + async def test(): + megaphone = new_megaphone() + t0 = now() + result_alice = await say_after_with_megaphone(megaphone, 200, 'Alice') + t1 = now() + + t_delta = (t1 - t0).total_seconds() + self.assertGreater(t_delta, 0.2) + self.assertEqual(result_alice, 'HELLO, ALICE!') + + asyncio.run(test()) + def test_with_tokio_runtime(self): async def test(): t0 = now() @@ -150,21 +163,14 @@ async def test(): # Test a future that uses a lock and that is cancelled. def test_shared_resource_cancellation(self): - # Note: Python uses the event loop to schedule calls via the `call_soon_threadsafe()` - # method. This means that creating a task and cancelling it won't trigger the issue, we - # need to create an EventLoop and close it instead. - loop = asyncio.new_event_loop() - loop.create_task(use_shared_resource( - SharedResourceOptions(release_after_ms=100, timeout_ms=1000))) - # Wait some time to ensure the task has locked the shared resource - loop.call_later(0.05, loop.stop) - loop.run_forever() - # Close the EventLoop before the shared resource has been released. - loop.close() - - # Try accessing the shared resource again using the main event loop. The initial task - # should release the shared resource before the timeout expires. - asyncio.run(use_shared_resource(SharedResourceOptions(release_after_ms=0, timeout_ms=1000))) + async def test(): + task = asyncio.create_task(use_shared_resource( + SharedResourceOptions(release_after_ms=5000, timeout_ms=100))) + # Wait some time to ensure the task has locked the shared resource + await asyncio.sleep(0.05) + task.cancel() + await use_shared_resource(SharedResourceOptions(release_after_ms=0, timeout_ms=1000)) + asyncio.run(test()) def test_shared_resource_no_cancellation(self): async def test(): diff --git a/fixtures/reexport-scaffolding-macro/src/lib.rs b/fixtures/reexport-scaffolding-macro/src/lib.rs index 6262b431a7..6bd04f2ccd 100644 --- a/fixtures/reexport-scaffolding-macro/src/lib.rs +++ b/fixtures/reexport-scaffolding-macro/src/lib.rs @@ -8,7 +8,7 @@ mod tests { use std::ffi::CString; use std::os::raw::c_void; use std::process::{Command, Stdio}; - use uniffi::{FfiConverter, ForeignCallback, RustBuffer, RustCallStatus}; + use uniffi::{FfiConverter, ForeignCallback, RustBuffer, RustCallStatus, RustCallStatusCode}; use uniffi_bindgen::ComponentInterface; struct UniFfiTag; @@ -165,7 +165,7 @@ mod tests { get_symbol(&library, object_def.ffi_object_free().name()); let num_alive = unsafe { get_num_alive(&mut call_status) }; - assert_eq!(call_status.code, 0); + assert_eq!(call_status.code, RustCallStatusCode::Success); assert_eq!(num_alive, 0); let obj_id = unsafe { @@ -174,24 +174,24 @@ mod tests { &mut call_status, ) }; - assert_eq!(call_status.code, 0); + assert_eq!(call_status.code, RustCallStatusCode::Success); let name_buf = unsafe { coveralls_get_name(obj_id, &mut call_status) }; - assert_eq!(call_status.code, 0); + assert_eq!(call_status.code, RustCallStatusCode::Success); assert_eq!( >::try_lift(name_buf).unwrap(), "TestName" ); let num_alive = unsafe { get_num_alive(&mut call_status) }; - assert_eq!(call_status.code, 0); + assert_eq!(call_status.code, RustCallStatusCode::Success); assert_eq!(num_alive, 1); unsafe { coveralls_free(obj_id, &mut call_status) }; - assert_eq!(call_status.code, 0); + assert_eq!(call_status.code, RustCallStatusCode::Success); let num_alive = unsafe { get_num_alive(&mut call_status) }; - assert_eq!(call_status.code, 0); + assert_eq!(call_status.code, RustCallStatusCode::Success); assert_eq!(num_alive, 0); } } diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs index 5749245a1c..0008916b7d 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs @@ -198,6 +198,7 @@ pub struct KotlinWrapper<'a> { ci: &'a ComponentInterface, type_helper_code: String, type_imports: BTreeSet, + has_async_fns: bool, } impl<'a> KotlinWrapper<'a> { @@ -210,6 +211,7 @@ impl<'a> KotlinWrapper<'a> { ci, type_helper_code, type_imports, + has_async_fns: ci.has_async_fns(), } } @@ -218,6 +220,10 @@ impl<'a> KotlinWrapper<'a> { .iter_types() .map(|t| KotlinCodeOracle.find(t)) .filter_map(|ct| ct.initialization_fn()) + .chain( + self.has_async_fns + .then(|| "uniffiRustFutureContinuationCallback.register".into()), + ) .collect() } @@ -302,10 +308,11 @@ impl KotlinCodeOracle { FfiType::ForeignCallback => "ForeignCallback".to_string(), FfiType::ForeignExecutorHandle => "USize".to_string(), FfiType::ForeignExecutorCallback => "UniFfiForeignExecutorCallback".to_string(), - FfiType::FutureCallback { return_type } => { - format!("UniFfiFutureCallback{}", Self::ffi_type_label(return_type)) + FfiType::RustFutureHandle => "Pointer".to_string(), + FfiType::RustFutureContinuationCallback => { + "UniFffiRustFutureContinuationCallbackType".to_string() } - FfiType::FutureCallbackData => "USize".to_string(), + FfiType::RustFutureContinuationData => "USize".to_string(), } } } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Async.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Async.kt new file mode 100644 index 0000000000..c6a32655f2 --- /dev/null +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Async.kt @@ -0,0 +1,44 @@ +// Async return type handlers + +internal const val UNIFFI_RUST_FUTURE_POLL_READY = 0.toShort() +internal const val UNIFFI_RUST_FUTURE_POLL_MAYBE_READY = 1.toShort() + +internal val uniffiContinuationHandleMap = UniFfiHandleMap>() + +// FFI type for Rust future continuations +internal object uniffiRustFutureContinuationCallback: UniFffiRustFutureContinuationCallbackType { + override fun callback(continuationHandle: USize, pollResult: Short) { + uniffiContinuationHandleMap.remove(continuationHandle)?.resume(pollResult) + } + + internal fun register(lib: _UniFFILib) { + lib.{{ ci.ffi_rust_future_continuation_callback_set().name() }}(this) + } +} + +internal suspend fun uniffiRustCallAsync( + rustFuture: Pointer, + pollFunc: (Pointer, USize) -> Unit, + completeFunc: (Pointer, RustCallStatus) -> F, + freeFunc: (Pointer) -> Unit, + liftFunc: (F) -> T, + errorHandler: CallStatusErrorHandler +): T { + try { + do { + val pollResult = suspendCancellableCoroutine { continuation -> + pollFunc( + rustFuture, + uniffiContinuationHandleMap.insert(continuation) + ) + } + } while (pollResult != UNIFFI_RUST_FUTURE_POLL_READY); + + return liftFunc( + rustCallWithError(errorHandler, { status -> completeFunc(rustFuture, status) }) + ) + } finally { + freeFunc(rustFuture) + } +} + diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/AsyncTypes.kt b/uniffi_bindgen/src/bindings/kotlin/templates/AsyncTypes.kt deleted file mode 100644 index 51da2bf314..0000000000 --- a/uniffi_bindgen/src/bindings/kotlin/templates/AsyncTypes.kt +++ /dev/null @@ -1,47 +0,0 @@ -// Async return type handlers - -{# add imports that we use #} -{{ self.add_import("kotlin.coroutines.Continuation") }} -{{ self.add_import("kotlin.coroutines.resume") }} -{{ self.add_import("kotlin.coroutines.resumeWithException") }} - -{# We use these in the generated functions, which don't have access to add_import() -- might as well add it here #} -{{ self.add_import("kotlinx.coroutines.suspendCancellableCoroutine") }} -{{ self.add_import("kotlinx.coroutines.coroutineScope") }} - -// Stores all active future callbacks to ensure they're not GC'ed while waiting for the Rust code to -// complete the callback -val uniffiActiveFutureCallbacks = mutableSetOf() - -// FFI type for callback handlers -{%- for callback_param in ci.iter_future_callback_params()|unique_ffi_types %} -internal interface UniFfiFutureCallback{{ callback_param|ffi_type_name }} : com.sun.jna.Callback { - // Note: callbackData is always 0. We could pass Rust a pointer/usize to represent the - // continuation, but with JNA it's easier to just store it in the callback handler. - fun callback(_callbackData: USize, returnValue: {{ callback_param|ffi_type_name_by_value }}?, callStatus: RustCallStatus.ByValue); -} -{%- endfor %} - -// Callback handlers for an async call. These are invoked by Rust when the future is ready. They -// lift the return value or error and resume the suspended function. -{%- for result_type in ci.iter_async_result_types() %} -{%- let callback_param = result_type.future_callback_param() %} - -internal class {{ result_type|future_callback_handler }}(val continuation: {{ result_type|future_continuation_type }}) - : UniFfiFutureCallback{{ callback_param|ffi_type_name }} { - override fun callback(_callbackData: USize, returnValue: {{ callback_param|ffi_type_name_by_value }}?, callStatus: RustCallStatus.ByValue) { - uniffiActiveFutureCallbacks.remove(this) - try { - checkCallStatus({{ result_type|error_handler }}, callStatus) - {%- match result_type.return_type %} - {%- when Some(return_type) %} - continuation.resume({{ return_type|lift_fn }}(returnValue!!)) - {%- when None %} - continuation.resume(Unit) - {%- endmatch %} - } catch (e: Throwable) { - continuation.resumeWithException(e) - } - } -} -{%- endfor %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt index 26926a17a5..382a5f7413 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt @@ -150,7 +150,12 @@ internal class UniFfiHandleMap { return map.get(handle) } - fun remove(handle: USize) { - map.remove(handle) + fun remove(handle: USize): T? { + return map.remove(handle) } } + +// FFI type for Rust future continuations +internal interface UniFffiRustFutureContinuationCallbackType : com.sun.jna.Callback { + fun callback(continuationHandle: USize, pollResult: Short); +} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt index 02e85c980e..5cbb5d5d93 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt @@ -58,32 +58,31 @@ class {{ type_name }}( {%- if meth.is_async() %} @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") override suspend fun {{ meth.name()|fn_name }}({%- call kt::arg_list_decl(meth) -%}){% match meth.return_type() %}{% when Some with (return_type) %} : {{ return_type|type_name }}{% when None %}{%- endmatch %} { - // Create a new `CoroutineScope` for this operation, suspend the coroutine, and call the - // scaffolding function, passing it one of the callback handlers from `AsyncTypes.kt`. - return coroutineScope { - val scope = this - return@coroutineScope suspendCancellableCoroutine { continuation -> - try { - val callback = {{ meth.result_type().borrow()|future_callback_handler }}(continuation) - uniffiActiveFutureCallbacks.add(callback) - continuation.invokeOnCancellation { uniffiActiveFutureCallbacks.remove(callback) } - callWithPointer { thisPtr -> - rustCall { status -> - _UniFFILib.INSTANCE.{{ meth.ffi_func().name() }}( - thisPtr, - {% call kt::arg_list_lowered(meth) %} - FfiConverterForeignExecutor.lower(scope), - callback, - USize(0), - status, - ) - } - } - } catch (e: Exception) { - continuation.resumeWithException(e) - } - } - } + return uniffiRustCallAsync( + callWithPointer { thisPtr -> + _UniFFILib.INSTANCE.{{ meth.ffi_func().name() }}( + thisPtr, + {% call kt::arg_list_lowered(meth) %} + ) + }, + { future, continuation -> _UniFFILib.INSTANCE.{{ meth.ffi_rust_future_poll(ci) }}(future, continuation) }, + { future, status -> _UniFFILib.INSTANCE.{{ meth.ffi_rust_future_complete(ci) }}(future, status) }, + { future -> _UniFFILib.INSTANCE.{{ meth.ffi_rust_future_free(ci) }}(future) }, + // lift function + {%- match meth.return_type() %} + {%- when Some(return_type) %} + { {{ return_type|lift_fn }}(it) }, + {%- when None %} + { Unit }, + {% endmatch %} + // Error FFI converter + {%- match meth.throws_type() %} + {%- when Some(e) %} + {{ e|error_type_name }}.ErrorHandler, + {%- when None %} + NullCallStatusErrorHandler, + {%- endmatch %} + ) } {%- else -%} {%- match meth.return_type() -%} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt index dbcab05b81..4f5cd7f2b6 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt @@ -7,29 +7,26 @@ @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") suspend fun {{ func.name()|fn_name }}({%- call kt::arg_list_decl(func) -%}){% match func.return_type() %}{% when Some with (return_type) %} : {{ return_type|type_name }}{% when None %}{%- endmatch %} { - // Create a new `CoroutineScope` for this operation, suspend the coroutine, and call the - // scaffolding function, passing it one of the callback handlers from `AsyncTypes.kt`. - return coroutineScope { - val scope = this - return@coroutineScope suspendCancellableCoroutine { continuation -> - try { - val callback = {{ func.result_type().borrow()|future_callback_handler }}(continuation) - uniffiActiveFutureCallbacks.add(callback) - continuation.invokeOnCancellation { uniffiActiveFutureCallbacks.remove(callback) } - rustCall { status -> - _UniFFILib.INSTANCE.{{ func.ffi_func().name() }}( - {% call kt::arg_list_lowered(func) %} - FfiConverterForeignExecutor.lower(scope), - callback, - USize(0), - status, - ) - } - } catch (e: Exception) { - continuation.resumeWithException(e) - } - } - } + return uniffiRustCallAsync( + _UniFFILib.INSTANCE.{{ func.ffi_func().name() }}({% call kt::arg_list_lowered(func) %}), + { future, continuation -> _UniFFILib.INSTANCE.{{ func.ffi_rust_future_poll(ci) }}(future, continuation) }, + { future, status -> _UniFFILib.INSTANCE.{{ func.ffi_rust_future_complete(ci) }}(future, status) }, + { future -> _UniFFILib.INSTANCE.{{ func.ffi_rust_future_free(ci) }}(future) }, + // lift function + {%- match func.return_type() %} + {%- when Some(return_type) %} + { {{ return_type|lift_fn }}(it) }, + {%- when None %} + { Unit }, + {% endmatch %} + // Error FFI converter + {%- match func.throws_type() %} + {%- when Some(e) %} + {{ e|error_type_name }}.ErrorHandler, + {%- when None %} + NullCallStatusErrorHandler, + {%- endmatch %} + ) } {%- else %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt index 2359679911..89546eac92 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt @@ -102,5 +102,8 @@ {%- endfor %} {%- if ci.has_async_fns() %} -{% include "AsyncTypes.kt" %} +{# Import types needed for async support #} +{{ self.add_import("kotlin.coroutines.resume") }} +{{ self.add_import("kotlinx.coroutines.suspendCancellableCoroutine") }} +{{ self.add_import("kotlinx.coroutines.CancellableContinuation") }} {%- endif %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt b/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt index 22f9ca31de..9ee4229018 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt @@ -42,6 +42,11 @@ import java.util.concurrent.ConcurrentHashMap // and the FFI Function declarations in a com.sun.jna.Library. {% include "NamespaceLibraryTemplate.kt" %} +// Async support +{%- if ci.has_async_fns() %} +{% include "Async.kt" %} +{%- endif %} + // Public interface members begin here. {{ type_helper_code }} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs index 35b17e3009..0fd99cab02 100644 --- a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs +++ b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs @@ -311,13 +311,9 @@ impl PythonCodeOracle { // Pointer to an `asyncio.EventLoop` instance FfiType::ForeignExecutorHandle => "ctypes.c_size_t".to_string(), FfiType::ForeignExecutorCallback => "_UNIFFI_FOREIGN_EXECUTOR_CALLBACK_T".to_string(), - FfiType::FutureCallback { return_type } => { - format!( - "_uniffi_future_callback_t({})", - Self::ffi_type_label(return_type), - ) - } - FfiType::FutureCallbackData => "ctypes.c_size_t".to_string(), + FfiType::RustFutureHandle => "ctypes.c_void_p".to_string(), + FfiType::RustFutureContinuationCallback => "_UNIFFI_FUTURE_CONTINUATION_T".to_string(), + FfiType::RustFutureContinuationData => "ctypes.c_size_t".to_string(), } } } @@ -408,25 +404,14 @@ pub mod filters { Ok(format!("{}.write", ffi_converter_name(as_ct)?)) } - // Name of the callback function we pass to Rust to complete an async call - pub fn async_callback_fn(result_type: &ResultType) -> Result { - let return_string = match &result_type.return_type { - Some(t) => PythonCodeOracle.find(t).canonical_name().to_snake_case(), - None => "void".into(), - }; - let throws_string = match &result_type.throws_type { - Some(t) => PythonCodeOracle.find(t).canonical_name().to_snake_case(), - None => "void".into(), - }; - Ok(format!( - "_uniffi_async_callback_{return_string}__{throws_string}" - )) - } - pub fn literal_py(literal: &Literal, as_ct: &impl AsCodeType) -> Result { Ok(as_ct.as_codetype().literal(literal)) } + pub fn ffi_type(type_: &Type) -> Result { + Ok(type_.into()) + } + /// Get the Python syntax for representing a given low-level `FfiType`. pub fn ffi_type_name(type_: &FfiType) -> Result { Ok(PythonCodeOracle::ffi_type_label(type_)) diff --git a/uniffi_bindgen/src/bindings/python/templates/Async.py b/uniffi_bindgen/src/bindings/python/templates/Async.py new file mode 100644 index 0000000000..82aa534b46 --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/templates/Async.py @@ -0,0 +1,40 @@ +# RustFuturePoll values +_UNIFFI_RUST_FUTURE_POLL_READY = 0 +_UNIFFI_RUST_FUTURE_POLL_MAYBE_READY = 1 + +# Stores futures for _uniffi_continuation_callback +_UniffiContinuationPointerManager = _UniffiPointerManager() + +# Continuation callback for async functions +# lift the return value or error and resolve the future, causing the async function to resume. +@_UNIFFI_FUTURE_CONTINUATION_T +def _uniffi_continuation_callback(future_ptr, poll_code): + (eventloop, future) = _UniffiContinuationPointerManager.release_pointer(future_ptr) + eventloop.call_soon_threadsafe(_uniffi_set_future_result, future, poll_code) + +def _uniffi_set_future_result(future, poll_code): + if not future.cancelled(): + future.set_result(poll_code) + +async def _uniffi_rust_call_async(rust_future, ffi_poll, ffi_complete, ffi_free, lift_func, error_ffi_converter): + try: + eventloop = asyncio.get_running_loop() + + # Loop and poll until we see a _UNIFFI_RUST_FUTURE_POLL_READY value + while True: + future = eventloop.create_future() + ffi_poll( + rust_future, + _UniffiContinuationPointerManager.new_pointer((eventloop, future)), + ) + poll_code = await future + if poll_code == _UNIFFI_RUST_FUTURE_POLL_READY: + break + + return lift_func( + _rust_call_with_error(error_ffi_converter, ffi_complete, rust_future) + ) + finally: + ffi_free(rust_future) + +_UniffiLib.{{ ci.ffi_rust_future_continuation_callback_set().name() }}(_uniffi_continuation_callback) diff --git a/uniffi_bindgen/src/bindings/python/templates/AsyncTypes.py b/uniffi_bindgen/src/bindings/python/templates/AsyncTypes.py deleted file mode 100644 index c85891defe..0000000000 --- a/uniffi_bindgen/src/bindings/python/templates/AsyncTypes.py +++ /dev/null @@ -1,36 +0,0 @@ -# Callback handlers for async returns - -_UniffiPyFuturePointerManager = _UniffiPointerManager() - -# Callback handlers for an async calls. These are invoked by Rust when the future is ready. They -# lift the return value or error and resolve the future, causing the async function to resume. -{%- for result_type in ci.iter_async_result_types() %} -@_uniffi_future_callback_t( - {%- match result_type.return_type -%} - {%- when Some(return_type) -%} - {{ return_type|ffi_type|ffi_type_name }} - {%- when None -%} - ctypes.c_uint8 - {%- endmatch -%} -) -def {{ result_type|async_callback_fn }}(future_ptr, result, call_status): - future = _UniffiPyFuturePointerManager.release_pointer(future_ptr) - if future.cancelled(): - return - try: - {%- match result_type.throws_type %} - {%- when Some(throws_type) %} - _uniffi_check_call_status({{ throws_type|ffi_converter_name }}, call_status) - {%- when None %} - _uniffi_check_call_status(None, call_status) - {%- endmatch %} - - {%- match result_type.return_type %} - {%- when Some(return_type) %} - future.set_result({{ return_type|lift_fn }}(result)) - {%- when None %} - future.set_result(None) - {%- endmatch %} - except BaseException as e: - future.set_exception(e) -{%- endfor %} diff --git a/uniffi_bindgen/src/bindings/python/templates/Helpers.py b/uniffi_bindgen/src/bindings/python/templates/Helpers.py index 356fe9786b..dca962f176 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Helpers.py +++ b/uniffi_bindgen/src/bindings/python/templates/Helpers.py @@ -44,25 +44,6 @@ def _rust_call_with_error(error_ffi_converter, fn, *args): _uniffi_check_call_status(error_ffi_converter, call_status) return result -def _rust_call_async(scaffolding_fn, callback_fn, *args): - # Call the scaffolding function, passing it a callback handler for `AsyncTypes.py` and a pointer - # to a python Future object. The async function then awaits the Future. - uniffi_eventloop = asyncio.get_running_loop() - uniffi_py_future = uniffi_eventloop.create_future() - uniffi_call_status = _UniffiRustCallStatus(code=_UniffiRustCallStatus.CALL_SUCCESS, error_buf=_UniffiRustBuffer(0, 0, None)) - scaffolding_fn(*args, - _UniffiConverterForeignExecutor._pointer_manager.new_pointer(uniffi_eventloop), - callback_fn, - # Note: It's tempting to skip the pointer manager and just use a `py_object` pointing to a - # local variable like we do in Swift. However, Python doesn't use cooperative cancellation - # -- asyncio can cancel a task at anytime. This means if we use a local variable, the Rust - # callback could fire with a dangling pointer. - _UniffiPyFuturePointerManager.new_pointer(uniffi_py_future), - ctypes.byref(uniffi_call_status), - ) - _uniffi_check_call_status(None, uniffi_call_status) - return uniffi_py_future - def _uniffi_check_call_status(error_ffi_converter, call_status): if call_status.code == _UniffiRustCallStatus.CALL_SUCCESS: pass @@ -88,3 +69,7 @@ def _uniffi_check_call_status(error_ffi_converter, call_status): # A function pointer for a callback as defined by UniFFI. # Rust definition `fn(handle: u64, method: u32, args: _UniffiRustBuffer, buf_ptr: *mut _UniffiRustBuffer) -> int` _UNIFFI_FOREIGN_CALLBACK_T = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_ulonglong, ctypes.c_ulong, ctypes.POINTER(ctypes.c_char), ctypes.c_int, ctypes.POINTER(_UniffiRustBuffer)) + +# UniFFI future continuation +_UNIFFI_FUTURE_CONTINUATION_T = ctypes.CFUNCTYPE(None, ctypes.c_size_t, ctypes.c_int8) + diff --git a/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py b/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py index 89b1b5f137..f258b60a1c 100644 --- a/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py @@ -1,11 +1,25 @@ {%- if func.is_async() %} -async def {{ func.name()|fn_name }}({%- call py::arg_list_decl(func) -%}): - {%- call py::setup_args(func) %} - return await _rust_call_async( - _UniffiLib.{{ func.ffi_func().name() }}, - {{ func.result_type().borrow()|async_callback_fn }}, - {% call py::arg_list_lowered(func) %} +def {{ func.name()|fn_name }}({%- call py::arg_list_decl(func) -%}): + return _uniffi_rust_call_async( + _UniffiLib.{{ func.ffi_func().name() }}({% call py::arg_list_lowered(func) %}), + _UniffiLib.{{func.ffi_rust_future_poll(ci) }}, + _UniffiLib.{{func.ffi_rust_future_complete(ci) }}, + _UniffiLib.{{func.ffi_rust_future_free(ci) }}, + # lift function + {%- match func.return_type() %} + {%- when Some(return_type) %} + {{ return_type|lift_fn }}, + {%- when None %} + lambda val: None, + {% endmatch %} + # Error FFI converter + {%- match func.throws_type() %} + {%- when Some(e) %} + {{ e|ffi_converter_name }}, + {%- when None %} + None, + {%- endmatch %} ) {%- else %} diff --git a/uniffi_bindgen/src/bindings/python/templates/Types.py b/uniffi_bindgen/src/bindings/python/templates/Types.py index aef2996438..5e05314c37 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Types.py +++ b/uniffi_bindgen/src/bindings/python/templates/Types.py @@ -100,7 +100,3 @@ {%- else %} {%- endmatch %} {%- endfor %} - -{%- if ci.has_async_fns() %} -{%- include "AsyncTypes.py" %} -{%- endif %} diff --git a/uniffi_bindgen/src/bindings/python/templates/macros.py b/uniffi_bindgen/src/bindings/python/templates/macros.py index bc4f121653..ef3b1bb94d 100644 --- a/uniffi_bindgen/src/bindings/python/templates/macros.py +++ b/uniffi_bindgen/src/bindings/python/templates/macros.py @@ -100,13 +100,29 @@ {%- macro method_decl(py_method_name, meth) %} {% if meth.is_async() %} - async def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}): + def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}): {%- call setup_args_extra_indent(meth) %} - return await _rust_call_async( - _UniffiLib.{{ func.ffi_func().name() }}, - {{ func.result_type().borrow()|async_callback_fn }}, - self._pointer, - {% call arg_list_lowered(func) %} + return _uniffi_rust_call_async( + _UniffiLib.{{ meth.ffi_func().name() }}( + self._pointer, {% call arg_list_lowered(meth) %} + ), + _UniffiLib.{{ meth.ffi_rust_future_poll(ci) }}, + _UniffiLib.{{ meth.ffi_rust_future_complete(ci) }}, + _UniffiLib.{{ meth.ffi_rust_future_free(ci) }}, + # lift function + {%- match meth.return_type() %} + {%- when Some(return_type) %} + {{ return_type|lift_fn }}, + {%- when None %} + lambda val: None, + {% endmatch %} + # Error FFI converter + {%- match meth.throws_type() %} + {%- when Some(e) %} + {{ e|ffi_converter_name }}, + {%- when None %} + None, + {%- endmatch %} ) {%- else -%} diff --git a/uniffi_bindgen/src/bindings/python/templates/wrapper.py b/uniffi_bindgen/src/bindings/python/templates/wrapper.py index 6fb88dcaee..24c3290ff7 100644 --- a/uniffi_bindgen/src/bindings/python/templates/wrapper.py +++ b/uniffi_bindgen/src/bindings/python/templates/wrapper.py @@ -40,6 +40,11 @@ # Contains loading, initialization code, and the FFI Function declarations. {% include "NamespaceLibraryTemplate.py" %} +# Async support +{%- if ci.has_async_fns() %} +{%- include "Async.py" %} +{%- endif %} + # Public interface members begin here. {{ type_helper_code }} diff --git a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs index 4380cc378c..1f7260d00b 100644 --- a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs +++ b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs @@ -162,7 +162,9 @@ mod filters { FfiType::ForeignExecutorHandle => { unimplemented!("Foreign executors are not implemented") } - FfiType::FutureCallback { .. } | FfiType::FutureCallbackData => { + FfiType::RustFutureHandle + | FfiType::RustFutureContinuationCallback + | FfiType::RustFutureContinuationData => { unimplemented!("Async functions are not implemented") } }) diff --git a/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb b/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb index 858b42bf91..8536fc322e 100644 --- a/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb +++ b/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb @@ -9,7 +9,7 @@ module UniFFILib ffi_lib '{{ config.cdylib_name() }}' {% endif %} - {% for func in ci.iter_ffi_function_definitions() -%} + {% for func in ci.iter_ffi_function_definitions_non_async() -%} attach_function :{{ func.name() }}, {%- call rb::arg_list_ffi_decl(func) %}, {% match func.return_type() %}{% when Some with (type_) %}{{ type_|type_ffi }}{% when None %}:void{% endmatch %} diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs index f85453c9ff..0855bcb282 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs @@ -342,6 +342,7 @@ pub struct SwiftWrapper<'a> { ci: &'a ComponentInterface, type_helper_code: String, type_imports: BTreeSet, + has_async_fns: bool, } impl<'a> SwiftWrapper<'a> { pub fn new(config: Config, ci: &'a ComponentInterface) -> Self { @@ -353,6 +354,7 @@ impl<'a> SwiftWrapper<'a> { ci, type_helper_code, type_imports, + has_async_fns: ci.has_async_fns(), } } @@ -365,6 +367,10 @@ impl<'a> SwiftWrapper<'a> { .iter_types() .map(|t| SwiftCodeOracle.find(t)) .filter_map(|ct| ct.initialization_fn()) + .chain( + self.has_async_fns + .then(|| "uniffiInitContinuationCallback".into()), + ) .collect() } } @@ -463,10 +469,10 @@ impl SwiftCodeOracle { FfiType::ForeignCallback => "ForeignCallback".into(), FfiType::ForeignExecutorHandle => "Int".into(), FfiType::ForeignExecutorCallback => "ForeignExecutorCallback".into(), - FfiType::FutureCallback { return_type } => { - format!("UniFfiFutureCallback{}", self.ffi_type_label(return_type)) + FfiType::RustFutureContinuationCallback => "UniFfiRustFutureContinuation".into(), + FfiType::RustFutureHandle | FfiType::RustFutureContinuationData => { + "UnsafeMutableRawPointer".into() } - FfiType::FutureCallbackData => "UnsafeMutableRawPointer".into(), } } @@ -474,7 +480,9 @@ impl SwiftCodeOracle { match ffi_type { FfiType::ForeignCallback | FfiType::ForeignExecutorCallback - | FfiType::FutureCallback { .. } => { + | FfiType::RustFutureHandle + | FfiType::RustFutureContinuationCallback + | FfiType::RustFutureContinuationData => { format!("{} _Nonnull", self.ffi_type_label_raw(ffi_type)) } _ => self.ffi_type_label_raw(ffi_type), @@ -558,11 +566,12 @@ pub mod filters { FfiType::ForeignCallback => "ForeignCallback _Nonnull".into(), FfiType::ForeignExecutorCallback => "UniFfiForeignExecutorCallback _Nonnull".into(), FfiType::ForeignExecutorHandle => "size_t".into(), - FfiType::FutureCallback { return_type } => format!( - "UniFfiFutureCallback{} _Nonnull", - SwiftCodeOracle.ffi_type_label_raw(return_type) - ), - FfiType::FutureCallbackData => "void* _Nonnull".into(), + FfiType::RustFutureContinuationCallback => { + "UniFfiRustFutureContinuation _Nonnull".into() + } + FfiType::RustFutureHandle | FfiType::RustFutureContinuationData => { + "void* _Nonnull".into() + } }) } @@ -618,14 +627,4 @@ pub mod filters { } )) } - - pub fn future_continuation_type(result: &ResultType) -> Result { - Ok(format!( - "CheckedContinuation<{}, Error>", - match &result.return_type { - Some(return_type) => type_name(return_type)?, - None => "()".into(), - } - )) - } } diff --git a/uniffi_bindgen/src/bindings/swift/templates/Async.swift b/uniffi_bindgen/src/bindings/swift/templates/Async.swift new file mode 100644 index 0000000000..fee7ca6ea6 --- /dev/null +++ b/uniffi_bindgen/src/bindings/swift/templates/Async.swift @@ -0,0 +1,62 @@ +private let UNIFFI_RUST_FUTURE_POLL_READY: Int8 = 0 +private let UNIFFI_RUST_FUTURE_POLL_MAYBE_READY: Int8 = 1 + +internal func uniffiRustCallAsync( + rustFutureFunc: () -> UnsafeMutableRawPointer, + pollFunc: (UnsafeMutableRawPointer, UnsafeMutableRawPointer) -> (), + completeFunc: (UnsafeMutableRawPointer, UnsafeMutablePointer) -> F, + freeFunc: (UnsafeMutableRawPointer) -> (), + liftFunc: (F) throws -> T, + errorHandler: ((RustBuffer) throws -> Error)? +) async throws -> T { + // Make sure to call uniffiEnsureInitialized() since future creation doesn't have a + // RustCallStatus param, so doesn't use makeRustCall() + uniffiEnsureInitialized() + let rustFuture = rustFutureFunc() + defer { + freeFunc(rustFuture) + } + var pollResult: Int8; + repeat { + pollResult = await withUnsafeContinuation { + pollFunc(rustFuture, ContinuationHolder($0).toOpaque()) + } + } while pollResult != UNIFFI_RUST_FUTURE_POLL_READY + + return try liftFunc(makeRustCall( + { completeFunc(rustFuture, $0) }, + errorHandler: errorHandler + )) +} + +// Callback handlers for an async calls. These are invoked by Rust when the future is ready. They +// lift the return value or error and resume the suspended function. +fileprivate func uniffiFutureContinuationCallback(ptr: UnsafeMutableRawPointer, pollResult: Int8) { + ContinuationHolder.fromOpaque(ptr).resume(pollResult) +} + +// Wraps UnsafeContinuation in a class so that we can use reference counting when passing it across +// the FFI +class ContinuationHolder { + let continuation: UnsafeContinuation + + init(_ continuation: UnsafeContinuation) { + self.continuation = continuation + } + + func resume(_ pollResult: Int8) { + self.continuation.resume(returning: pollResult) + } + + func toOpaque() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + static func fromOpaque(_ ptr: UnsafeRawPointer) -> ContinuationHolder { + return Unmanaged.fromOpaque(ptr).takeRetainedValue() + } +} + +fileprivate func uniffiInitContinuationCallback() { + {{ ci.ffi_rust_future_continuation_callback_set().name() }}(uniffiFutureContinuationCallback) +} diff --git a/uniffi_bindgen/src/bindings/swift/templates/AsyncTypes.swift b/uniffi_bindgen/src/bindings/swift/templates/AsyncTypes.swift deleted file mode 100644 index b7dcff516b..0000000000 --- a/uniffi_bindgen/src/bindings/swift/templates/AsyncTypes.swift +++ /dev/null @@ -1,29 +0,0 @@ -// Callbacks for async functions - -// Callback handlers for an async calls. These are invoked by Rust when the future is ready. They -// lift the return value or error and resume the suspended function. -{%- for result_type in ci.iter_async_result_types() %} -fileprivate func {{ result_type|future_callback }}( - rawContinutation: UnsafeRawPointer, - returnValue: {{ result_type.future_callback_param().borrow()|ffi_type_name }}, - callStatus: RustCallStatus) { - - let continuation = rawContinutation.bindMemory( - to: {{ result_type|future_continuation_type }}.self, - capacity: 1 - ) - - do { - try uniffiCheckCallStatus(callStatus: callStatus, errorHandler: {{ result_type|error_handler }}) - {%- match result_type.return_type %} - {%- when Some(return_type) %} - continuation.pointee.resume(returning: try {{ return_type|lift_fn }}(returnValue)) - {%- when None %} - continuation.pointee.resume(returning: ()) - {%- endmatch %} - } catch let error { - continuation.pointee.resume(throwing: error) - } -} - -{%- endfor %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h index d87977670f..87698e359f 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h +++ b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h @@ -59,10 +59,8 @@ typedef struct RustCallStatus { // ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ #endif // def UNIFFI_SHARED_H -// Callbacks for UniFFI Futures -{%- for ffi_type in ci.iter_future_callback_params() %} -typedef void (*UniFfiFutureCallback{{ ffi_type|ffi_canonical_name }})(const void * _Nonnull, {{ ffi_type|header_ffi_type_name }}, RustCallStatus); -{%- endfor %} +// Continuation callback for UniFFI Futures +typedef void (*UniFfiRustFutureContinuation)(void * _Nonnull, int8_t); // Scaffolding functions {%- for func in ci.iter_ffi_function_definitions() %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift b/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift index feccd6551e..a34b128e23 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift @@ -29,6 +29,7 @@ fileprivate enum UniffiInternalError: LocalizedError { fileprivate let CALL_SUCCESS: Int8 = 0 fileprivate let CALL_ERROR: Int8 = 1 fileprivate let CALL_PANIC: Int8 = 2 +fileprivate let CALL_CANCELLED: Int8 = 3 fileprivate extension RustCallStatus { init() { @@ -91,6 +92,9 @@ private func uniffiCheckCallStatus( throw UniffiInternalError.rustPanic("Rust panic") } + case CALL_CANCELLED: + throw CancellationError() + default: throw UniffiInternalError.unexpectedRustCallStatusCode } diff --git a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift index 0de3118707..a137803bda 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift @@ -44,25 +44,31 @@ public class {{ type_name }}: {{ obj.name() }}Protocol { {%- if meth.is_async() %} public func {{ meth.name()|fn_name }}({%- call swift::arg_list_decl(meth) -%}) async {% call swift::throws(meth) %}{% match meth.return_type() %}{% when Some with (return_type) %} -> {{ return_type|type_name }}{% when None %}{% endmatch %} { - // Suspend the function and call the scaffolding function, passing it a callback handler from - // `AsyncTypes.swift` - // - // Make sure to hold on to a reference to the continuation in the top-level scope so that - // it's not freed before the callback is invoked. - var continuation: {{ meth.result_type().borrow()|future_continuation_type }}? = nil - return {% call swift::try(meth) %} await withCheckedThrowingContinuation { - continuation = $0 - try! rustCall() { + return {% call swift::try(meth) %} await uniffiRustCallAsync( + rustFutureFunc: { {{ meth.ffi_func().name() }}( self.pointer, - {% call swift::arg_list_lowered(meth) %} - FfiConverterForeignExecutor.lower(UniFfiForeignExecutor()), - {{ meth.result_type().borrow()|future_callback }}, - &continuation, - $0 + {%- for arg in meth.arguments() %} + {{ arg|lower_fn }}({{ arg.name()|var_name }}){% if !loop.last %},{% endif %} + {%- endfor %} ) - } - } + }, + pollFunc: {{ meth.ffi_rust_future_poll(ci) }}, + completeFunc: {{ meth.ffi_rust_future_complete(ci) }}, + freeFunc: {{ meth.ffi_rust_future_free(ci) }}, + {%- match meth.return_type() %} + {%- when Some(return_type) %} + liftFunc: {{ return_type|lift_fn }}, + {%- when None %} + liftFunc: { $0 }, + {%- endmatch %} + {%- match meth.throws_type() %} + {%- when Some with (e) %} + errorHandler: {{ e|ffi_converter_name }}.lift + {%- else %} + errorHandler: nil + {% endmatch %} + ) } {% else -%} diff --git a/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift index e3c87ca336..a2c6311931 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift @@ -1,24 +1,30 @@ {%- if func.is_async() %} public func {{ func.name()|fn_name }}({%- call swift::arg_list_decl(func) -%}) async {% call swift::throws(func) %}{% match func.return_type() %}{% when Some with (return_type) %} -> {{ return_type|type_name }}{% when None %}{% endmatch %} { - var continuation: {{ func.result_type().borrow()|future_continuation_type }}? = nil - // Suspend the function and call the scaffolding function, passing it a callback handler from - // `AsyncTypes.swift` - // - // Make sure to hold on to a reference to the continuation in the top-level scope so that - // it's not freed before the callback is invoked. - return {% call swift::try(func) %} await withCheckedThrowingContinuation { - continuation = $0 - try! rustCall() { + return {% call swift::try(func) %} await uniffiRustCallAsync( + rustFutureFunc: { {{ func.ffi_func().name() }}( - {% call swift::arg_list_lowered(func) %} - FfiConverterForeignExecutor.lower(UniFfiForeignExecutor()), - {{ func.result_type().borrow()|future_callback }}, - &continuation, - $0 + {%- for arg in func.arguments() %} + {{ arg|lower_fn }}({{ arg.name()|var_name }}){% if !loop.last %},{% endif %} + {%- endfor %} ) - } - } + }, + pollFunc: {{ func.ffi_rust_future_poll(ci) }}, + completeFunc: {{ func.ffi_rust_future_complete(ci) }}, + freeFunc: {{ func.ffi_rust_future_free(ci) }}, + {%- match func.return_type() %} + {%- when Some(return_type) %} + liftFunc: {{ return_type|lift_fn }}, + {%- when None %} + liftFunc: { $0 }, + {%- endmatch %} + {%- match func.throws_type() %} + {%- when Some with (e) %} + errorHandler: {{ e|ffi_converter_name }}.lift + {%- else %} + errorHandler: nil + {% endmatch %} + ) } {% else %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/Types.swift b/uniffi_bindgen/src/bindings/swift/templates/Types.swift index dbde9c0d4c..aba34f4b0b 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/Types.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/Types.swift @@ -96,7 +96,3 @@ {%- else %} {%- endmatch %} {%- endfor %} - -{%- if ci.has_async_fns() %} -{%- include "AsyncTypes.swift" %} -{%- endif %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift b/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift index 8aa85a9195..c34d348efb 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift @@ -19,6 +19,10 @@ import {{ config.ffi_module_name() }} // Public interface members begin here. {{ type_helper_code }} +{%- if ci.has_async_fns() %} +{% include "Async.swift" %} +{%- endif %} + {%- for func in ci.function_definitions() %} {%- include "TopLevelFunctionTemplate.swift" %} {%- endfor %} diff --git a/uniffi_bindgen/src/interface/ffi.rs b/uniffi_bindgen/src/interface/ffi.rs index 81ba0674ad..d18aaf8262 100644 --- a/uniffi_bindgen/src/interface/ffi.rs +++ b/uniffi_bindgen/src/interface/ffi.rs @@ -54,14 +54,11 @@ pub enum FfiType { ForeignExecutorHandle, /// Pointer to the callback function that's invoked to schedule calls with a ForeignExecutor ForeignExecutorCallback, - /// Pointer to a callback function to complete an async Rust function - FutureCallback { - /// Note: `return_type` is not optional because we have a void callback parameter like we - /// can have a void return. Instead, we use `UInt8` as a placeholder value. - return_type: Box, - }, - /// Opaque pointer passed to the FutureCallback - FutureCallbackData, + /// Pointer to a Rust future + RustFutureHandle, + /// Continuation function for a Rust future + RustFutureContinuationCallback, + RustFutureContinuationData, // TODO: you can imagine a richer structural typesystem here, e.g. `Ref` or something. // We don't need that yet and it's possible we never will, so it isn't here for now. } @@ -184,28 +181,8 @@ impl FfiFunction { ) { self.arguments = args.into_iter().collect(); if self.is_async() { - self.arguments.extend([ - // Used to schedule polls - FfiArgument { - name: "uniffi_executor".into(), - type_: FfiType::ForeignExecutorHandle, - }, - // Invoked when the future is ready - FfiArgument { - name: "uniffi_callback".into(), - type_: FfiType::FutureCallback { - return_type: Box::new(return_type.unwrap_or(FfiType::UInt8)), - }, - }, - // Data pointer passed to the callback - FfiArgument { - name: "uniffi_callback_data".into(), - type_: FfiType::FutureCallbackData, - }, - ]); - // Async scaffolding functions never return values. Instead, the callback is invoked - // when the Future is ready. - self.return_type = None; + self.return_type = Some(FfiType::RustFutureHandle); + self.has_rust_call_status_arg = false; } else { self.return_type = return_type; } diff --git a/uniffi_bindgen/src/interface/function.rs b/uniffi_bindgen/src/interface/function.rs index 767c525ef7..1b93bd1229 100644 --- a/uniffi_bindgen/src/interface/function.rs +++ b/uniffi_bindgen/src/interface/function.rs @@ -35,8 +35,7 @@ use anyhow::Result; use super::ffi::{FfiArgument, FfiFunction, FfiType}; -use super::Literal; -use super::{AsType, ObjectImpl, Type, TypeIterator}; +use super::{AsType, ComponentInterface, Literal, ObjectImpl, Type, TypeIterator}; use uniffi_meta::Checksum; /// Represents a standalone function. @@ -242,12 +241,39 @@ pub trait Callable { fn arguments(&self) -> Vec<&Argument>; fn return_type(&self) -> Option; fn throws_type(&self) -> Option; + fn is_async(&self) -> bool; fn result_type(&self) -> ResultType { ResultType { return_type: self.return_type(), throws_type: self.throws_type(), } } + + // Quick way to get the rust future scaffolding function that corresponds to our return type. + + fn ffi_rust_future_poll(&self, ci: &ComponentInterface) -> String { + ci.ffi_rust_future_poll(self.return_type().map(Into::into)) + .name() + .to_owned() + } + + fn ffi_rust_future_cancel(&self, ci: &ComponentInterface) -> String { + ci.ffi_rust_future_cancel(self.return_type().map(Into::into)) + .name() + .to_owned() + } + + fn ffi_rust_future_complete(&self, ci: &ComponentInterface) -> String { + ci.ffi_rust_future_complete(self.return_type().map(Into::into)) + .name() + .to_owned() + } + + fn ffi_rust_future_free(&self, ci: &ComponentInterface) -> String { + ci.ffi_rust_future_free(self.return_type().map(Into::into)) + .name() + .to_owned() + } } impl Callable for Function { @@ -262,6 +288,10 @@ impl Callable for Function { fn throws_type(&self) -> Option { self.throws_type().cloned() } + + fn is_async(&self) -> bool { + self.is_async + } } // Needed because Askama likes to add extra refs to variables @@ -277,6 +307,10 @@ impl Callable for &T { fn throws_type(&self) -> Option { (*self).throws_type() } + + fn is_async(&self) -> bool { + (*self).is_async() + } } #[cfg(test)] diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index e43e652bb3..f4b21e0606 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -433,6 +433,115 @@ impl ComponentInterface { } } + /// Builtin FFI function to set the Rust Future continuation callback + pub fn ffi_rust_future_continuation_callback_set(&self) -> FfiFunction { + FfiFunction { + name: format!( + "ffi_{}_rust_future_continuation_callback_set", + self.ffi_namespace() + ), + arguments: vec![FfiArgument { + name: "callback".to_owned(), + type_: FfiType::RustFutureContinuationCallback, + }], + return_type: None, + is_async: false, + has_rust_call_status_arg: false, + is_object_free_function: false, + } + } + + /// Builtin FFI function to poll a Rust future. + pub fn ffi_rust_future_poll(&self, return_ffi_type: Option) -> FfiFunction { + FfiFunction { + name: self.rust_future_ffi_fn_name("rust_future_poll", return_ffi_type), + is_async: false, + arguments: vec![ + FfiArgument { + name: "handle".to_owned(), + type_: FfiType::RustFutureHandle, + }, + // Data to pass to the continuation + FfiArgument { + name: "uniffi_callback".to_owned(), + type_: FfiType::RustFutureContinuationData, + }, + ], + return_type: None, + has_rust_call_status_arg: false, + is_object_free_function: false, + } + } + + /// Builtin FFI function to complete a Rust future and get it's result. + /// + /// We generate one of these for each FFI return type. + pub fn ffi_rust_future_complete(&self, return_ffi_type: Option) -> FfiFunction { + FfiFunction { + name: self.rust_future_ffi_fn_name("rust_future_complete", return_ffi_type.clone()), + is_async: false, + arguments: vec![FfiArgument { + name: "handle".to_owned(), + type_: FfiType::RustFutureHandle, + }], + return_type: return_ffi_type, + has_rust_call_status_arg: true, + is_object_free_function: false, + } + } + + /// Builtin FFI function for cancelling a Rust Future + pub fn ffi_rust_future_cancel(&self, return_ffi_type: Option) -> FfiFunction { + FfiFunction { + name: self.rust_future_ffi_fn_name("rust_future_cancel", return_ffi_type), + is_async: false, + arguments: vec![FfiArgument { + name: "handle".to_owned(), + type_: FfiType::RustFutureHandle, + }], + return_type: None, + has_rust_call_status_arg: false, + is_object_free_function: false, + } + } + + /// Builtin FFI function for freeing a Rust Future + pub fn ffi_rust_future_free(&self, return_ffi_type: Option) -> FfiFunction { + FfiFunction { + name: self.rust_future_ffi_fn_name("rust_future_free", return_ffi_type), + is_async: false, + arguments: vec![FfiArgument { + name: "handle".to_owned(), + type_: FfiType::RustFutureHandle, + }], + return_type: None, + has_rust_call_status_arg: false, + is_object_free_function: false, + } + } + + fn rust_future_ffi_fn_name(&self, base_name: &str, return_ffi_type: Option) -> String { + let namespace = self.ffi_namespace(); + match return_ffi_type { + Some(t) => match t { + FfiType::UInt8 => format!("ffi_{namespace}_{base_name}_u8"), + FfiType::Int8 => format!("ffi_{namespace}_{base_name}_i8"), + FfiType::UInt16 => format!("ffi_{namespace}_{base_name}_u16"), + FfiType::Int16 => format!("ffi_{namespace}_{base_name}_i16"), + FfiType::UInt32 => format!("ffi_{namespace}_{base_name}_u32"), + FfiType::Int32 => format!("ffi_{namespace}_{base_name}_i32"), + FfiType::UInt64 => format!("ffi_{namespace}_{base_name}_u64"), + FfiType::Int64 => format!("ffi_{namespace}_{base_name}_i64"), + FfiType::Float32 => format!("ffi_{namespace}_{base_name}_f32"), + FfiType::Float64 => format!("ffi_{namespace}_{base_name}_f64"), + FfiType::RustArcPtr(_) => format!("ffi_{namespace}_{base_name}_pointer"), + FfiType::RustBuffer(_) => format!("ffi_{namespace}_{base_name}_rust_buffer"), + _ => unimplemented!("FFI return type: {t:?}"), + }, + None => format!("ffi_{namespace}_{base_name}_void"), + } + } + /// Does this interface contain async functions? pub fn has_async_fns(&self) -> bool { self.iter_ffi_function_definitions().any(|f| f.is_async()) @@ -464,11 +573,23 @@ impl ComponentInterface { self.iter_user_ffi_function_definitions() .cloned() .chain(self.iter_rust_buffer_ffi_function_definitions()) + .chain(self.iter_futures_ffi_function_definitons()) .chain(self.iter_checksum_ffi_functions()) .chain(self.ffi_foreign_executor_callback_set()) .chain([self.ffi_uniffi_contract_version()]) } + /// Alternate version of iter_ffi_function_definitions for languages that don't support async + pub fn iter_ffi_function_definitions_non_async( + &self, + ) -> impl Iterator + '_ { + self.iter_user_ffi_function_definitions() + .cloned() + .chain(self.iter_rust_buffer_ffi_function_definitions()) + .chain(self.iter_checksum_ffi_functions()) + .chain([self.ffi_uniffi_contract_version()]) + } + /// List all FFI functions definitions for user-defined interfaces /// /// This includes FFI functions for: @@ -501,6 +622,40 @@ impl ComponentInterface { .into_iter() } + /// List all FFI functions definitions for async functionality. + pub fn iter_futures_ffi_function_definitons(&self) -> impl Iterator + '_ { + let all_possible_return_ffi_types = [ + Some(FfiType::UInt8), + Some(FfiType::Int8), + Some(FfiType::UInt16), + Some(FfiType::Int16), + Some(FfiType::UInt32), + Some(FfiType::Int32), + Some(FfiType::UInt64), + Some(FfiType::Int64), + Some(FfiType::Float32), + Some(FfiType::Float64), + // RustBuffer and RustArcPtr have an inner field which doesn't affect the rust future + // complete scaffolding function, so we just use a placeholder value here. + Some(FfiType::RustArcPtr("".to_owned())), + Some(FfiType::RustBuffer(None)), + None, + ]; + + iter::once(self.ffi_rust_future_continuation_callback_set()).chain( + all_possible_return_ffi_types + .into_iter() + .flat_map(|return_type| { + [ + self.ffi_rust_future_poll(return_type.clone()), + self.ffi_rust_future_cancel(return_type.clone()), + self.ffi_rust_future_free(return_type.clone()), + self.ffi_rust_future_complete(return_type), + ] + }), + ) + } + /// The ffi_foreign_executor_callback_set FFI function /// /// We only include this in the FFI if the `ForeignExecutor` type is actually used @@ -627,10 +782,6 @@ impl ComponentInterface { bail!("Conflicting type definition for \"{}\"", defn.name()); } self.types.add_known_types(defn.iter_types())?; - if defn.is_async() { - // Async functions depend on the foreign executor - self.types.add_known_type(&Type::ForeignExecutor)?; - } self.functions.push(defn); Ok(()) @@ -653,10 +804,6 @@ impl ComponentInterface { .ok_or_else(|| anyhow!("add_method_meta: object {} not found", &method.object_name))?; self.types.add_known_types(method.iter_types())?; - if method.is_async() { - // Async functions depend on the foreign executor - self.types.add_known_type(&Type::ForeignExecutor)?; - } method.object_impl = object.imp; object.methods.push(method); diff --git a/uniffi_bindgen/src/interface/object.rs b/uniffi_bindgen/src/interface/object.rs index cd61feae9c..4caedf54c5 100644 --- a/uniffi_bindgen/src/interface/object.rs +++ b/uniffi_bindgen/src/interface/object.rs @@ -581,6 +581,10 @@ impl Callable for Constructor { fn throws_type(&self) -> Option { self.throws_type().cloned() } + + fn is_async(&self) -> bool { + false + } } impl Callable for Method { @@ -595,6 +599,10 @@ impl Callable for Method { fn throws_type(&self) -> Option { self.throws_type().cloned() } + + fn is_async(&self) -> bool { + self.is_async + } } #[cfg(test)] diff --git a/uniffi_bindgen/src/scaffolding/mod.rs b/uniffi_bindgen/src/scaffolding/mod.rs index 4febc14d24..8271d70efe 100644 --- a/uniffi_bindgen/src/scaffolding/mod.rs +++ b/uniffi_bindgen/src/scaffolding/mod.rs @@ -84,11 +84,10 @@ mod filters { FfiType::RustBuffer(_) => "::uniffi::RustBuffer".into(), FfiType::ForeignBytes => "::uniffi::ForeignBytes".into(), FfiType::ForeignCallback => "::uniffi::ForeignCallback".into(), + FfiType::RustFutureHandle => "::uniffi::RustFutureHandle".into(), + FfiType::RustFutureContinuationCallback => "::uniffi::RustFutureContinuation".into(), + FfiType::RustFutureContinuationData => "*const ()".into(), FfiType::ForeignExecutorHandle => "::uniffi::ForeignExecutorHandle".into(), - FfiType::FutureCallback { return_type } => { - format!("::uniffi::FutureCallback<{}>", type_ffi(return_type)?) - } - FfiType::FutureCallbackData => "*const ()".into(), FfiType::ForeignExecutorCallback => "::uniffi::ForeignExecutorCallback".into(), }) } diff --git a/uniffi_core/Cargo.toml b/uniffi_core/Cargo.toml index 6a8b894927..7b9ab4fb72 100644 --- a/uniffi_core/Cargo.toml +++ b/uniffi_core/Cargo.toml @@ -17,6 +17,7 @@ async-compat = { version = "0.2.1", optional = true } bytes = "1.3" camino = "1.0.8" log = "0.4" +once_cell = "1.10.0" # Enable "async" so that receivers implement Future, no need for "std" since we don't block on them. oneshot = { version = "0.1", features = ["async"] } # Regular dependencies diff --git a/uniffi_core/src/ffi/rustbuffer.rs b/uniffi_core/src/ffi/rustbuffer.rs index 5344ab123c..e09e3be89a 100644 --- a/uniffi_core/src/ffi/rustbuffer.rs +++ b/uniffi_core/src/ffi/rustbuffer.rs @@ -49,6 +49,7 @@ use crate::ffi::{rust_call, ForeignBytes, RustCallStatus}; /// This struct is based on `ByteBuffer` from the `ffi-support` crate, but modified /// to retain unallocated capacity rather than truncating to the occupied length. #[repr(C)] +#[derive(Debug)] pub struct RustBuffer { /// The allocated capacity of the underlying `Vec`. /// In Rust this is a `usize`, but we use an `i32` for compatibility with JNA. diff --git a/uniffi_core/src/ffi/rustcalls.rs b/uniffi_core/src/ffi/rustcalls.rs index 5a84a05209..ff169d7f67 100644 --- a/uniffi_core/src/ffi/rustcalls.rs +++ b/uniffi_core/src/ffi/rustcalls.rs @@ -19,13 +19,14 @@ use std::panic; /// /// ## Usage /// -/// - The consumer code creates a `RustCallStatus` with an empty `RustBuffer` and `CALL_SUCCESS` -/// (0) as the status code +/// - The consumer code creates a [RustCallStatus] with an empty [RustBuffer] and +/// [RustCallStatusCode::Success] (0) as the status code /// - A pointer to this object is passed to the rust FFI function. This is an /// "out parameter" which will be updated with any error that occurred during the function's /// execution. -/// - After the call, if `code` is `CALL_ERROR` then `error_buf` will be updated to contain -/// the serialized error object. The consumer is responsible for freeing `error_buf`. +/// - After the call, if `code` is [RustCallStatusCode::Error] or [RustCallStatusCode::UnexpectedError] +/// then `error_buf` will be updated to contain a serialized error object. See +/// [RustCallStatusCode] for what gets serialized. The consumer is responsible for freeing `error_buf`. /// /// ## Layout/fields /// @@ -38,20 +39,9 @@ use std::panic; /// RustBuffer error_buf; /// }; /// ``` -/// -/// #### The `code` field. -/// -/// - `CALL_SUCCESS` (0) for successful calls -/// - `CALL_ERROR` (1) for calls that returned an `Err` value -/// - `CALL_PANIC` (2) for calls that panicked -/// -/// #### The `error_buf` field. -/// -/// - For `CALL_ERROR` this is a `RustBuffer` with the serialized error. The consumer code is -/// responsible for freeing this `RustBuffer`. #[repr(C)] pub struct RustCallStatus { - pub code: i8, + pub code: RustCallStatusCode, // code is signed because unsigned types are experimental in Kotlin pub error_buf: MaybeUninit, // error_buf is MaybeUninit to avoid dropping the value that the consumer code sends in: @@ -65,19 +55,49 @@ pub struct RustCallStatus { // leak the first `RustBuffer`. } +impl RustCallStatus { + pub fn cancelled() -> Self { + Self { + code: RustCallStatusCode::Cancelled, + error_buf: MaybeUninit::new(RustBuffer::new()), + } + } + + pub fn error(message: impl Into) -> Self { + Self { + code: RustCallStatusCode::UnexpectedError, + error_buf: MaybeUninit::new(>::lower(message.into())), + } + } +} + impl Default for RustCallStatus { fn default() -> Self { Self { - code: 0, + code: RustCallStatusCode::Success, error_buf: MaybeUninit::uninit(), } } } -#[allow(dead_code)] -const CALL_SUCCESS: i8 = 0; // CALL_SUCCESS is set by the calling code -const CALL_ERROR: i8 = 1; -const CALL_PANIC: i8 = 2; +/// Result of a FFI call to a Rust function +#[repr(i8)] +#[derive(Debug, PartialEq, Eq)] +pub enum RustCallStatusCode { + /// Successful call. + Success = 0, + /// Expected error, corresponding to the `Result::Err` variant. [RustCallStatus::error_buf] + /// will contain the serialized error. + Error = 1, + /// Unexpected error. [RustCallStatus::error_buf] will contain a serialized message string + UnexpectedError = 2, + /// Async function cancelled. [RustCallStatus::error_buf] will be empty and does not need to + /// be freed. + /// + /// This is only returned for async functions and only if the bindings code uses the + /// [rust_future_cancel] call. + Cancelled = 3, +} /// Handle a scaffolding calls /// @@ -89,7 +109,7 @@ const CALL_PANIC: i8 = 2; /// - `FfiConverter::lower_return` returns `Result<>` types that meet the above criteria> /// - If the function returns a `Ok` value it will be unwrapped and returned /// - If the function returns a `Err` value: -/// - `out_status.code` will be set to `CALL_ERROR` +/// - `out_status.code` will be set to [RustCallStatusCode::Error]. /// - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing the error. The calling /// code is responsible for freeing the `RustBuffer` /// - `FfiDefault::ffi_default()` is returned, although foreign code should ignore this value @@ -125,11 +145,11 @@ where }); match result { // Happy path. Note: no need to update out_status in this case because the calling code - // initializes it to CALL_SUCCESS + // initializes it to [RustCallStatusCode::Success] Ok(Ok(v)) => Some(v), // Callback returned an Err. Ok(Err(buf)) => { - out_status.code = CALL_ERROR; + out_status.code = RustCallStatusCode::Error; unsafe { // Unsafe because we're setting the `MaybeUninit` value, see above for safety // invariants. @@ -139,7 +159,7 @@ where } // Callback panicked Err(cause) => { - out_status.code = CALL_PANIC; + out_status.code = RustCallStatusCode::UnexpectedError; // Try to coerce the cause into a RustBuffer containing a String. Since this code can // panic, we need to use a second catch_unwind(). let message_result = panic::catch_unwind(panic::AssertUnwindSafe(move || { @@ -176,7 +196,7 @@ mod test { fn create_call_status() -> RustCallStatus { RustCallStatus { - code: 0, + code: RustCallStatusCode::Success, error_buf: MaybeUninit::new(RustBuffer::new()), } } @@ -196,13 +216,13 @@ mod test { as FfiConverter>::lower_return(test_callback(0)) }); - assert_eq!(status.code, CALL_SUCCESS); + assert_eq!(status.code, RustCallStatusCode::Success); assert_eq!(return_value, 100); rust_call(&mut status, || { as FfiConverter>::lower_return(test_callback(1)) }); - assert_eq!(status.code, CALL_ERROR); + assert_eq!(status.code, RustCallStatusCode::Error); unsafe { assert_eq!( >::try_lift(status.error_buf.assume_init()) @@ -215,7 +235,7 @@ mod test { rust_call(&mut status, || { as FfiConverter>::lower_return(test_callback(2)) }); - assert_eq!(status.code, CALL_PANIC); + assert_eq!(status.code, RustCallStatusCode::UnexpectedError); unsafe { assert_eq!( >::try_lift(status.error_buf.assume_init()) diff --git a/uniffi_core/src/ffi/rustfuture.rs b/uniffi_core/src/ffi/rustfuture.rs index 27e1b2a14b..6310a240ea 100644 --- a/uniffi_core/src/ffi/rustfuture.rs +++ b/uniffi_core/src/ffi/rustfuture.rs @@ -8,68 +8,28 @@ //! //! # The big picture //! -//! What happens when you call an async function exported from the Rust API? +//! We implement async foreign functions using a simplified version of the Future API: //! -//! 1. You make a call to a generated async function in the foreign bindings. -//! 2. That function suspends itself, then makes a scaffolding call. In addition to the normal -//! arguments, it passes a `ForeignExecutor` and callback. -//! 3. Rust uses the `ForeignExecutor` to schedules polls of the Future until it's ready. Then -//! invokes the callback. -//! 4. The callback resumes the suspended async function from (2), closing the loop. +//! 0. At startup, register a [RustFutureContinuationCallback] by calling +//! rust_future_continuation_callback_set. +//! 1. Call the scaffolding function to get a [RustFutureHandle] +//! 2a. In a loop: +//! - Call [rust_future_poll] +//! - Suspend the function until the [rust_future_poll] continuation function is called +//! - If the continuation was function was called with [RustFuturePoll::Ready], then break +//! otherwise continue. +//! 2b. If the async function is cancelled, then call [rust_future_cancel]. This causes the +//! continuation function to be called with [RustFuturePoll::Ready] and the [RustFuture] to +//! enter a cancelled state. +//! 3. Call [rust_future_complete] to get the result of the future. +//! 4. Call [rust_future_free] to free the future, ideally in a finally block. This: +//! - Releases any resources held by the future +//! - Calls any continuation callbacks that have not been called yet //! -//! # Anatomy of an async call -//! -//! Let's consider the following Rust function: -//! -//! ```rust,ignore -//! #[uniffi::export] -//! async fn hello() -> bool { -//! true -//! } -//! ``` -//! -//! In Rust, this `async fn` syntax is strictly equivalent to a normal function that returns a -//! `Future`: -//! -//! ```rust,ignore -//! #[uniffi::export] -//! fn hello() -> impl Future { /* … */ } -//! ``` -//! -//! `uniffi-bindgen` will generate a scaffolding function for each exported async function: -//! -//! ```rust,ignore -//! // The `hello` function, as seen from the outside. It inputs 3 extra arguments: -//! // - executor: used to schedule polls of the future -//! // - callback: invoked when the future is ready -//! // - callback_data: opaque pointer that's passed to the callback. It points to any state needed to -//! // resume the async function. -//! #[no_mangle] -//! pub extern "C" fn _uniffi_hello( -//! // ...If the function inputted arguments, the lowered versions would go here -//! uniffi_executor: ForeignExecutor, -//! uniffi_callback: >::FutureCallback, -//! uniffi_callback_data: *const (), -//! uniffi_call_status: &mut ::uniffi::RustCallStatus -//! ) { -//! ::uniffi::call_with_output(uniffi_call_status, || { -//! let uniffi_rust_future = RustFuture::<_, bool, crate::UniFFITag,>::new( -//! future: hello(), // the future! -//! uniffi_executor, -//! uniffi_callback, -//! uniffi_callback_data, -//! ); -//! uniffi_rust_future.wake(); -//! }) -//! } -//! ``` -//! -//! Rust will continue to poll the future until it's ready, after that. The callback will -//! eventually be invoked with these arguments: -//! - callback_data -//! - FfiConverter::ReturnType (the type that would be returned by a sync function) -//! - RustCallStatus (used to signal errors/panics when executing the future) -//! - Rust will stop polling the future, even if it's waker is invoked again. +//! Note: Technically, the foreign code calls the scaffolding versions of the `rust_future_*` +//! functions. These are generated by the scaffolding macro, specially prefixed, and extern "C", +//! and manually monomorphized in the case of [rust_future_complete]. See +//! `uniffi_macros/src/setup_scaffolding.rs` for details. //! //! ## How does `Future` work exactly? //! @@ -115,477 +75,638 @@ //! [`Waker`]: https://doc.rust-lang.org/std/task/struct.Waker.html //! [`RawWaker`]: https://doc.rust-lang.org/std/task/struct.RawWaker.html -use crate::{ - ffi::foreignexecutor::RustTaskCallbackCode, rust_call_with_out_status, schedule_raw, - FfiConverter, FfiDefault, ForeignExecutor, ForeignExecutorHandle, RustCallStatus, -}; use std::{ - cell::UnsafeCell, future::Future, + marker::PhantomData, + mem, + ops::Deref, panic, pin::Pin, - sync::{ - atomic::{AtomicU32, Ordering}, - Arc, - }, - task::{Context, Poll, RawWaker, RawWakerVTable, Waker}, + sync::{Arc, Mutex}, + task::{Context, Poll, Wake}, }; -/// Callback that we invoke when a `RustFuture` is ready. +use once_cell::sync::OnceCell; + +use crate::{rust_call_with_out_status, FfiConverter, FfiDefault, RustCallStatus}; + +/// Result code for [rust_future_poll]. This is passed to the continuation function. +#[repr(i8)] +#[derive(Debug, PartialEq, Eq)] +pub enum RustFuturePoll { + /// The future is ready and is waiting for [rust_future_complete] to be called + Ready = 0, + /// The future might be ready and [rust_future_poll] should be called again + MaybeReady = 1, +} + +/// Foreign callback that's passed to [rust_future_poll] /// -/// The foreign code passes a pointer to one of these callbacks along with an opaque data pointer. -/// When the future is ready, we invoke the callback. -pub type FutureCallback = - extern "C" fn(callback_data: *const (), result: T, status: RustCallStatus); +/// The Rust side of things calls this when the foreign side should call [rust_future_poll] again +/// to continue progress on the future. +pub type RustFutureContinuationCallback = extern "C" fn(callback_data: *const (), RustFuturePoll); + +/// Opaque handle for a Rust future that's stored by the foreign language code +#[repr(transparent)] +pub struct RustFutureHandle(*const ()); + +/// Stores the global continuation callback +static RUST_FUTURE_CONTINUATION_CALLBACK_CELL: OnceCell = + OnceCell::new(); + +/// Set the global RustFutureContinuationCallback. +pub fn rust_future_continuation_callback_set(callback: RustFutureContinuationCallback) { + if let Err(existing) = RUST_FUTURE_CONTINUATION_CALLBACK_CELL.set(callback) { + // Don't panic if this to be called multiple times with the same callback. + if existing != callback { + panic!("Attempt to set the RustFuture continuation callback twice"); + } + } +} -/// Future that the foreign code is awaiting +fn call_continuation(data: *const (), poll_code: RustFuturePoll) { + let callback = RUST_FUTURE_CONTINUATION_CALLBACK_CELL + .get() + .expect("RustFuture continuation callback not set. This is likely a uniffi bug."); + callback(data, poll_code) +} + +// === Public FFI API === + +/// Create a new [RustFutureHandle] /// -/// RustFuture is always stored inside a Pin>. The `Arc<>` allows it to be shared between -/// wakers and Pin<> signals that it must not move, since this would break any self-references in -/// the future. -pub struct RustFuture +/// For each exported async function, UniFFI will create a scaffolding function that uses this to +/// create the [RustFutureHandle] to pass to the foreign code. +pub fn rust_future_new(future: F, tag: UT) -> RustFutureHandle where - // The future needs to be `Send`, since it will move to whatever thread the foreign executor - // chooses. However, it doesn't need to be `Sync', since we don't share references between - // threads (see do_wake()). - F: Future + Send, - T: FfiConverter, + // F is the future type returned by the exported async function. It needs to be Send + `static + // since it will move between threads for an indeterminate amount of time as the foreign + // executor calls polls it and the Rust executor wakes it. It does not need to by `Sync`, + // since we synchronize all access to the values. + F: Future + Send + 'static, + // T is the output of the Future. It needs to implement FfiConverter. Also it must be Send + + // 'static for the same reason as F. + T: FfiConverter + Send + 'static, + // The UniFfiTag ZST. The Send + 'static bound is to keep rustc happy. + UT: Send + 'static, { - future: UnsafeCell, - executor: ForeignExecutor, - wake_counter: AtomicU32, - callback: T::FutureCallback, - callback_data: *const (), + // Create a RustFuture and coerce to `Arc`, which is what we use to + // implement the FFI + let future_ffi = RustFuture::new(future, tag) as Arc>; + // Box the Arc, to convert the wide pointer into a normal sized pointer so that we can pass it + // to the foreign code. + let boxed_ffi = Box::new(future_ffi); + // We can now create a RustFutureHandle + RustFutureHandle(Box::into_raw(boxed_ffi) as *mut ()) } -// Mark `RustFuture` as `Send` + `Sync`, since we will be sharing it between threads. -// This means we need to serialize access to any fields that aren't `Send` + `Sync` (`future`, `callback`, and `callback_data`). -// See `do_wake()` for details on this. +/// Poll a Rust future +/// +/// When the future is ready to progress the continuation will be called with the `data` value and +/// a [RustFuturePoll] value. For each [rust_future_poll] call the continuation will be called +/// exactly once. +/// +/// # Safety +/// +/// The [RustFutureHandle] must not previously have been passed to [rust_future_free] +pub unsafe fn rust_future_poll(handle: RustFutureHandle, data: *const ()) { + let future = &*(handle.0 as *mut Arc>); + future.clone().ffi_poll(data) +} -unsafe impl Send for RustFuture -where - F: Future + Send, - T: FfiConverter, -{ +/// Cancel a Rust future +/// +/// Any current and future continuations will be immediately called with RustFuturePoll::Ready. +/// +/// This is needed for languages like Swift, which continuation to wait for the continuation to be +/// called when tasks are cancelled. +/// +/// # Safety +/// +/// The [RustFutureHandle] must not previously have been passed to [rust_future_free] +pub unsafe fn rust_future_cancel(handle: RustFutureHandle) { + let future = &*(handle.0 as *mut Arc>); + future.clone().ffi_cancel() } -unsafe impl Sync for RustFuture +/// Complete a Rust future +/// +/// Note: the actually extern "C" scaffolding functions can't be generic, so we generate one for +/// each supported FFI type. +/// +/// # Safety +/// +/// - The [RustFutureHandle] must not previously have been passed to [rust_future_free] +/// - The `T` param must correctly correspond to the [rust_future_new] call. It must +/// be `>::ReturnType` +pub unsafe fn rust_future_complete( + handle: RustFutureHandle, + out_status: &mut RustCallStatus, +) -> ReturnType { + let future = &*(handle.0 as *mut Arc>); + future.ffi_complete(out_status) +} + +/// Free a Rust future, dropping the strong reference and releasing all references held by the +/// future. +/// +/// # Safety +/// +/// The [RustFutureHandle] must not previously have been passed to [rust_future_free] +pub unsafe fn rust_future_free(handle: RustFutureHandle) { + let future = Box::from_raw(handle.0 as *mut Arc>); + future.ffi_free() +} + +/// Thread-safe storage for [RustFutureContinuationCallback] data +/// +/// The basic guarantee is that all data pointers passed in are passed out exactly once to the +/// foreign continuation callback. This enables us to uphold the [rust_future_poll] guarantee. +/// +/// [ContinuationDataCell] also tracks cancellation, which is closely tied to continuation data. +enum ContinuationDataCell { + Empty, + Cancelled, + Set(*const ()), +} + +impl ContinuationDataCell { + fn new() -> Self { + Self::Empty + } + + /// Store new continuation data + fn store(&mut self, data: *const ()) { + // If we're cancelled, then call the continuation immediately rather than storing it + if matches!(self, Self::Cancelled) { + call_continuation(data, RustFuturePoll::Ready); + return; + } + + match mem::replace(self, Self::Set(data)) { + Self::Empty => (), + Self::Cancelled => unreachable!(), + Self::Set(old_data) => { + log::error!( + "store: observed Self::Set state, is poll() being called from multiple threads at once?" + ); + call_continuation(old_data, RustFuturePoll::Ready); + } + } + } + + fn send(&mut self) { + if matches!(self, Self::Cancelled) { + return; + } + + if let Self::Set(old_data) = mem::replace(self, Self::Empty) { + call_continuation(old_data, RustFuturePoll::MaybeReady); + } + } + + fn cancel(&mut self) { + if let Self::Set(old_data) = mem::replace(self, Self::Cancelled) { + call_continuation(old_data, RustFuturePoll::Ready); + } + } + + fn is_cancelled(&self) -> bool { + matches!(self, Self::Cancelled) + } +} + +// ContinuationDataCell is Send + Sync as long we handle the *const () pointer correctly + +unsafe impl Send for ContinuationDataCell {} +unsafe impl Sync for ContinuationDataCell {} + +/// Wraps the actual future we're polling +struct WrappedFuture where - F: Future + Send, - T: FfiConverter, + // See rust_future_new for an explanation of these trait bounds + F: Future + Send + 'static, + T: FfiConverter + Send + 'static, + UT: Send + 'static, { + // Note: this could be a single enum, but that would make it easy to mess up the future pinning + // guarantee. For example you might want to call `std::mem::take()` to try to get the result, + // but if the future happened to be stored that would move and break all internal references. + future: Option, + result: Option>, } -impl RustFuture +impl WrappedFuture where - F: Future + Send, - T: FfiConverter, + // See rust_future_new for an explanation of these trait bounds + F: Future + Send + 'static, + T: FfiConverter + Send + 'static, + UT: Send + 'static, { - pub fn new( - future: F, - executor_handle: ForeignExecutorHandle, - callback: T::FutureCallback, - callback_data: *const (), - ) -> Pin> { - let executor = - >::try_lift(executor_handle) - .expect("Error lifting ForeignExecutorHandle"); - Arc::pin(Self { - future: UnsafeCell::new(future), - wake_counter: AtomicU32::new(0), - executor, - callback, - callback_data, - }) - } - - /// Wake up soon and poll our future. - /// - /// This method ensures that a call to `do_wake()` is scheduled. Only one call will be scheduled - /// at any time, even if `wake_soon` called multiple times from multiple threads. - pub fn wake(self: Pin>) { - if self.wake_counter.fetch_add(1, Ordering::Relaxed) == 0 { - self.schedule_do_wake(); + fn new(future: F) -> Self { + Self { + future: Some(future), + result: None, } } - /// Schedule `do_wake`. - /// - /// `self` is consumed but _NOT_ dropped, it's purposely leaked via `into_raw()`. - /// `wake_callback()` will call `from_raw()` to reverse the leak. - fn schedule_do_wake(self: Pin>) { - unsafe { - let handle = self.executor.handle; - let raw_ptr = Arc::into_raw(Pin::into_inner_unchecked(self)); - // SAFETY: The `into_raw()` / `from_raw()` contract guarantees that our executor cannot - // be dropped before we call `from_raw()` on the raw pointer. This means we can safely - // use its handle to schedule a callback. - if !schedule_raw(handle, 0, Self::wake_callback, raw_ptr as *const ()) { - // There was an error scheduling the callback, drop the arc reference since - // `wake_callback()` will never be called - // - // Note: specifying the `` generic is a good safety measure. Things would go - // very bad if Rust inferred the wrong type. + // Poll the future and check if it's ready or not + fn poll(&mut self, context: &mut Context<'_>) -> bool { + if self.result.is_some() { + true + } else if let Some(future) = &mut self.future { + // SAFETY: We can call Pin::new_unchecked because: + // - This is the only time we get a &mut to `self.future` + // - We never poll the future after it's moved (for example by using take()) + // - We never move RustFuture, which contains us. + // - RustFuture is private to this module so no other code can move it. + let pinned = unsafe { Pin::new_unchecked(future) }; + // Run the poll and lift the result if it's ready + let mut out_status = RustCallStatus::default(); + let result: Option> = rust_call_with_out_status( + &mut out_status, + // This closure uses a `&mut F` value, which means it's not UnwindSafe by + // default. If the future panics, it may be in an invalid state. // - // However, the `Pin<>` part doesn't matter since its `repr(transparent)`. - Arc::::decrement_strong_count(raw_ptr); + // However, we can safely use `AssertUnwindSafe` since a panic will lead the `None` + // case below and we will never poll the future again. + panic::AssertUnwindSafe(|| match pinned.poll(context) { + Poll::Pending => Ok(Poll::Pending), + Poll::Ready(v) => T::lower_return(v).map(Poll::Ready), + }), + ); + match result { + Some(Poll::Pending) => false, + Some(Poll::Ready(v)) => { + self.future = None; + self.result = Some(Ok(v)); + true + } + None => { + self.future = None; + self.result = Some(Err(out_status)); + true + } } + } else { + log::error!("poll with neither future nor result set"); + true } } - extern "C" fn wake_callback(self_ptr: *const (), status_code: RustTaskCallbackCode) { - // No matter what, call `Arc::from_raw()` to balance the `Arc::into_raw()` call in - // `schedule_do_wake()`. - let task = unsafe { Pin::new_unchecked(Arc::from_raw(self_ptr as *const Self)) }; - if status_code == RustTaskCallbackCode::Success { - // Only drive the future forward on `RustTaskCallbackCode::Success`. - // `RUST_TASK_CALLBACK_CANCELED` indicates the foreign executor has been cancelled / - // shutdown and we should not continue. - task.do_wake(); + fn complete(&mut self, out_status: &mut RustCallStatus) -> T::ReturnType { + let mut return_value = T::ReturnType::ffi_default(); + match self.result.take() { + Some(Ok(v)) => return_value = v, + Some(Err(call_status)) => *out_status = call_status, + None => *out_status = RustCallStatus::cancelled(), } + self.free(); + return_value } - // Does the work for wake, we take care to ensure this always runs in a serialized fashion. - fn do_wake(self: Pin>) { - // Store 1 in `waker_counter`, which we'll use at the end of this call. - self.wake_counter.store(1, Ordering::Relaxed); - - // Pin<&mut> from our UnsafeCell. &mut is is safe, since this is the only reference we - // ever take to `self.future` and calls to this function are serialized. Pin<> is safe - // since we never move the future out of `self.future`. - let future = unsafe { Pin::new_unchecked(&mut *self.future.get()) }; - let waker = self.make_waker(); - - // Run the poll and lift the result if it's ready - let mut out_status = RustCallStatus::default(); - let result: Option> = rust_call_with_out_status( - &mut out_status, - // This closure uses a `&mut F` value, which means it's not UnwindSafe by default. If - // the closure panics, the future may be in an invalid state. - // - // However, we can safely use `AssertUnwindSafe` since a panic will lead the `Err()` - // case below. In that case, we will never run `do_wake()` again and will no longer - // access the future. - panic::AssertUnwindSafe(|| match future.poll(&mut Context::from_waker(&waker)) { - Poll::Pending => Ok(Poll::Pending), - Poll::Ready(v) => T::lower_return(v).map(Poll::Ready), - }), - ); + fn free(&mut self) { + self.future = None; + self.result = None; + } +} - // All the main work is done, time to finish up - match result { - Some(Poll::Pending) => { - // Since we set `wake_counter` to 1 at the start of this function... - // - If it's > 1 now, then some other code tried to wake us while we were polling - // the future. Schedule another poll in this case. - // - If it's still 1, then exit after decrementing it. The next call to `wake()` - // will schedule a poll. - if self.wake_counter.fetch_sub(1, Ordering::Relaxed) > 1 { - self.schedule_do_wake(); - } - } - // Success! Call our callback. - // - // Don't decrement `wake_counter'. This way, if wake() is called in the future, we - // will just ignore it - Some(Poll::Ready(v)) => { - T::invoke_future_callback(self.callback, self.callback_data, v, out_status); - } - // Error/panic polling the future. Call the callback with a default value. - // `out_status` contains the error code and serialized error. Again, don't decrement - // `wake_counter'. - None => { - T::invoke_future_callback( - self.callback, - self.callback_data, - T::ReturnType::ffi_default(), - out_status, - ); - } - }; +// If F and T are Send, then WrappedFuture is too +// +// Rust will not mark it Send by default when T::ReturnType is a raw pointer. This is promising +// that we will treat the raw pointer properly, for example by not returning it twice. +unsafe impl Send for WrappedFuture +where + // See rust_future_new for an explanation of these trait bounds + F: Future + Send + 'static, + T: FfiConverter + Send + 'static, + UT: Send + 'static, +{ +} + +/// Future that the foreign code is awaiting +struct RustFuture +where + // See rust_future_new for an explanation of these trait bounds + F: Future + Send + 'static, + T: FfiConverter + Send + 'static, + UT: Send + 'static, +{ + // This Mutex should never block if our code is working correctly, since there should not be + // multiple threads calling [Self::poll] and/or [Self::complete] at the same time. + future: Mutex>, + continuation_data: Mutex, + // UT is used as the generic parameter for FfiConverter. + // Let's model this with PhantomData as a function that inputs a UT value. + _phantom: PhantomData ()>, +} + +impl RustFuture +where + // See rust_future_new for an explanation of these trait bounds + F: Future + Send + 'static, + T: FfiConverter + Send + 'static, + UT: Send + 'static, +{ + fn new(future: F, _tag: UT) -> Arc { + Arc::new(Self { + future: Mutex::new(WrappedFuture::new(future)), + continuation_data: Mutex::new(ContinuationDataCell::new()), + _phantom: PhantomData, + }) } - fn make_waker(self: &Pin>) -> Waker { - // This is safe as long as we implement the waker interface correctly. - unsafe { - Waker::from_raw(RawWaker::new( - self.clone().into_raw(), - &Self::RAW_WAKER_VTABLE, - )) + fn poll(self: Arc, data: *const ()) { + let ready = self.is_cancelled() || { + let mut locked = self.future.lock().unwrap(); + let waker: std::task::Waker = Arc::clone(&self).into(); + locked.poll(&mut Context::from_waker(&waker)) + }; + if ready { + call_continuation(data, RustFuturePoll::Ready) + } else { + self.continuation_data.lock().unwrap().store(data); } } - /// SAFETY: Ensure that all calls to `into_raw()` are balanced with a call to `from_raw()` - fn into_raw(self: Pin>) -> *const () { - unsafe { Arc::into_raw(Pin::into_inner_unchecked(self)) as *const () } + fn is_cancelled(&self) -> bool { + self.continuation_data.lock().unwrap().is_cancelled() } - /// Consume a pointer to get an arc - /// - /// SAFETY: The pointer must have come from `into_raw()` or was cloned with `raw_clone()`. - /// Once a pointer is passed into this function, it should no longer be used. - fn from_raw(self_ptr: *const ()) -> Pin> { - unsafe { Pin::new_unchecked(Arc::from_raw(self_ptr as *const Self)) } + fn wake(&self) { + self.continuation_data.lock().unwrap().send(); } - // Implement the waker interface by defining a RawWakerVTable - // - // We could also handle this by implementing the `Wake` interface, but that uses an `Arc` - // instead of a `Pin>` and in theory it could try to move the `T` value out of the arc - // with something like `Arc::try_unwrap()` which would break the pinning contract with - // `Future`. Implementing this using `RawWakerVTable` allows us verify that this doesn't - // happen. - const RAW_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new( - Self::raw_clone, - Self::raw_wake, - Self::raw_wake_by_ref, - Self::raw_drop, - ); - - /// This function will be called when the `RawWaker` gets cloned, e.g. when - /// the `Waker` in which the `RawWaker` is stored gets cloned. - unsafe fn raw_clone(self_ptr: *const ()) -> RawWaker { - Arc::increment_strong_count(self_ptr as *const Self); - RawWaker::new(self_ptr, &Self::RAW_WAKER_VTABLE) - } - - /// This function will be called when `wake` is called on the `Waker`. It - /// must wake up the task associated with this `RawWaker`. - unsafe fn raw_wake(self_ptr: *const ()) { - Self::from_raw(self_ptr).wake() - } - - /// This function will be called when `wake_by_ref` is called on the - /// `Waker`. It must wake up the task associated with this `RawWaker`. - unsafe fn raw_wake_by_ref(self_ptr: *const ()) { - // This could be optimized by only incrementing the strong count if we end up calling - // `schedule_do_wake()`, but it's not clear that's worth the extra complexity - Arc::increment_strong_count(self_ptr as *const Self); - Self::from_raw(self_ptr).wake() - } - - /// This function gets called when a `RawWaker` gets dropped. - /// This function gets called when a `RawWaker` gets dropped. - unsafe fn raw_drop(self_ptr: *const ()) { - drop(Self::from_raw(self_ptr)) + fn cancel(&self) { + self.continuation_data.lock().unwrap().cancel(); + } + + fn complete(&self, call_status: &mut RustCallStatus) -> T::ReturnType { + self.future.lock().unwrap().complete(call_status) + } + + fn free(self: Arc) { + // Call cancel() to send any leftover data to the continuation callback + self.continuation_data.lock().unwrap().cancel(); + // Ensure we drop our inner future, releasing all held references + self.future.lock().unwrap().free(); } } -#[cfg(test)] -mod tests { - use super::*; - use crate::{test_util::TestError, try_lift_from_rust_buffer, MockEventLoop}; - use std::sync::Weak; +impl Wake for RustFuture +where + // See rust_future_new for an explanation of these trait bounds + F: Future + Send + 'static, + T: FfiConverter + Send + 'static, + UT: Send + 'static, +{ + fn wake(self: Arc) { + self.deref().wake() + } - // Mock future that we can manually control using an Option<> - struct MockFuture(Option>); + fn wake_by_ref(self: &Arc) { + self.deref().wake() + } +} - impl Future for MockFuture { - type Output = Result; +/// RustFuture FFI trait. This allows `Arc>` to be cast to +/// `Arc>`, which is needed to implement the public FFI API. In particular, this +/// allows you to use RustFuture functionality without knowing the concrete Future type, which is +/// unnamable. +/// +/// This is parametrized on the ReturnType rather than the `T` directly, to reduce the number of +/// scaffolding functions we need to generate. If it was parametrized on `T`, then we would need +/// to create a poll, cancel, complete, and free scaffolding function for each exported async +/// function. That would add ~1kb binary size per exported function based on a quick estimate on a +/// x86-64 machine . By parametrizing on `T::ReturnType` we can instead monomorphize by hand and +/// only create those functions for each of the 13 possible FFI return types. +#[doc(hidden)] +trait RustFutureFfi { + fn ffi_poll(self: Arc, data: *const ()); + fn ffi_cancel(&self); + fn ffi_complete(&self, call_status: &mut RustCallStatus) -> ReturnType; + fn ffi_free(self: Arc); +} - fn poll(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll { - match &self.0 { - Some(v) => Poll::Ready(v.clone()), - None => Poll::Pending, - } - } +impl RustFutureFfi for RustFuture +where + // See rust_future_new for an explanation of these trait bounds + F: Future + Send + 'static, + T: FfiConverter + Send + 'static, + UT: Send + 'static, +{ + fn ffi_poll(self: Arc, data: *const ()) { + self.poll(data) } - // Type alias for the RustFuture we'll use in our tests - type TestRustFuture = RustFuture, crate::UniFfiTag>; + fn ffi_cancel(&self) { + self.cancel() + } - // Stores the result that we send to the foreign code - #[derive(Default)] - struct MockForeignResult { - value: i8, - status: RustCallStatus, + fn ffi_complete(&self, call_status: &mut RustCallStatus) -> T::ReturnType { + self.complete(call_status) } - extern "C" fn mock_foreign_callback(data_ptr: *const (), value: i8, status: RustCallStatus) { - println!("mock_foreign_callback: {value} {data_ptr:?}"); - let result: &mut Option = - unsafe { &mut *(data_ptr as *mut Option) }; - *result = Some(MockForeignResult { value, status }); + fn ffi_free(self: Arc) { + self.free(); } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_util::TestError, try_lift_from_rust_buffer, RustBuffer, RustCallStatusCode}; + use std::task::Waker; - // Bundles everything together so that we can run some tests - struct TestFutureEnvironment { - rust_future: Pin>, - foreign_result: Pin>>, + // Sender/Receiver pair that we use for testing + struct Channel { + result: Option>, + waker: Option, } - impl TestFutureEnvironment { - fn new(eventloop: &Arc) -> Self { - let foreign_result = Box::pin(None); - let foreign_result_ptr = &*foreign_result as *const Option<_> as *const (); + struct Sender(Arc>); - let rust_future = TestRustFuture::new( - MockFuture(None), - eventloop.new_handle(), - mock_foreign_callback, - foreign_result_ptr, - ); - Self { - rust_future, - foreign_result, + impl Sender { + fn wake(&self) { + let inner = self.0.lock().unwrap(); + if let Some(waker) = &inner.waker { + waker.wake_by_ref(); } } - fn wake(&self) { - self.rust_future.clone().wake(); + fn send(&self, value: Result) { + let mut inner = self.0.lock().unwrap(); + if inner.result.replace(value).is_some() { + panic!("value already sent"); + } + if let Some(waker) = &inner.waker { + waker.wake_by_ref(); + } } + } - fn rust_future_weak(&self) -> Weak { - // It seems like there's not a great way to get an &Arc from a Pin, so we need to - // do a little dance here - Arc::downgrade(&Pin::into_inner(Clone::clone(&self.rust_future))) - } + struct Receiver(Arc>); - fn complete_future(&self, value: Result) { - unsafe { - (*self.rust_future.future.get()).0 = Some(value); + impl Future for Receiver { + type Output = Result; + + fn poll( + self: Pin<&mut Self>, + context: &mut Context<'_>, + ) -> Poll> { + let mut inner = self.0.lock().unwrap(); + match &inner.result { + Some(v) => Poll::Ready(v.clone()), + None => { + inner.waker = Some(context.waker().clone()); + Poll::Pending + } } } } + // Create a sender and rust future that we can use for testing + fn channel() -> (Sender, Arc>) { + let channel = Arc::new(Mutex::new(Channel { + result: None, + waker: None, + })); + let rust_future = RustFuture::new(Receiver(channel.clone()), crate::UniFfiTag); + (Sender(channel), rust_future) + } + + /// Poll a Rust future and get an OnceCell that's set when the continuation is called + fn poll(rust_future: &Arc>) -> Arc> { + let cell = Arc::new(OnceCell::new()); + let cell_ptr = Arc::into_raw(cell.clone()) as *const (); + rust_future.clone().ffi_poll(cell_ptr); + cell + } + + fn setup_continuation_callback() { + let _ = RUST_FUTURE_CONTINUATION_CALLBACK_CELL.set(poll_continuation); + } + + extern "C" fn poll_continuation(data: *const (), code: RustFuturePoll) { + let cell = unsafe { Arc::from_raw(data as *const OnceCell) }; + cell.set(code).expect("Error setting OnceCell"); + } + + fn complete(rust_future: Arc>) -> (RustBuffer, RustCallStatus) { + let mut out_status_code = RustCallStatus::default(); + let return_value = rust_future.ffi_complete(&mut out_status_code); + (return_value, out_status_code) + } + #[test] - fn test_wake() { - let eventloop = MockEventLoop::new(); - let mut test_env = TestFutureEnvironment::new(&eventloop); - // Initially, we shouldn't have a result and nothing should be scheduled - assert!(test_env.foreign_result.is_none()); - assert_eq!(eventloop.call_count(), 0); - - // wake() should schedule a call - test_env.wake(); - assert_eq!(eventloop.call_count(), 1); - - // When that call runs, we should still not have a result yet - eventloop.run_all_calls(); - assert!(test_env.foreign_result.is_none()); - assert_eq!(eventloop.call_count(), 0); - - // Multiple wakes should only result in 1 scheduled call - test_env.wake(); - test_env.wake(); - assert_eq!(eventloop.call_count(), 1); - - // Make the future ready, which should call mock_foreign_callback and set the result - test_env.complete_future(Ok(true)); - eventloop.run_all_calls(); - let result = test_env - .foreign_result - .take() - .expect("Expected result to be set"); - assert_eq!(result.value, 1); - assert_eq!(result.status.code, 0); - assert_eq!(eventloop.call_count(), 0); - - // Future wakes shouldn't schedule any calls - test_env.wake(); - assert_eq!(eventloop.call_count(), 0); + fn test_success() { + setup_continuation_callback(); + let (sender, rust_future) = channel(); + + // Test polling the rust future before it's ready + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), None); + sender.wake(); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::MaybeReady)); + + // Test polling the rust future when it's ready + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), None); + sender.send(Ok("All done".into())); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::MaybeReady)); + + // Future polls should immediately return ready + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready)); + + // Complete the future + let (return_buf, call_status) = complete(rust_future); + assert_eq!(call_status.code, RustCallStatusCode::Success); + assert_eq!( + >::try_lift(return_buf).unwrap(), + "All done" + ); } #[test] fn test_error() { - let eventloop = MockEventLoop::new(); - let mut test_env = TestFutureEnvironment::new(&eventloop); - test_env.complete_future(Err("Something went wrong".into())); - test_env.wake(); - eventloop.run_all_calls(); - let result = test_env - .foreign_result - .take() - .expect("Expected result to be set"); - assert_eq!(result.status.code, 1); + setup_continuation_callback(); + let (sender, rust_future) = channel(); + + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), None); + sender.send(Err("Something went wrong".into())); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::MaybeReady)); + + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready)); + + let (_, call_status) = complete(rust_future); + assert_eq!(call_status.code, RustCallStatusCode::Error); unsafe { assert_eq!( - try_lift_from_rust_buffer::( - result.status.error_buf.assume_init() + try_lift_from_rust_buffer::( + call_status.error_buf.assume_init() ) .unwrap(), - String::from("Something went wrong"), + TestError::from("Something went wrong"), ) } - assert_eq!(eventloop.call_count(), 0); } + // Once `complete` is called, the inner future should be released, even if wakers still hold a + // reference to the RustFuture #[test] - fn test_raw_clone_and_drop() { - let test_env = TestFutureEnvironment::new(&MockEventLoop::new()); - let waker = test_env.rust_future.make_waker(); - let weak_ref = test_env.rust_future_weak(); - assert_eq!(weak_ref.strong_count(), 2); - let waker2 = waker.clone(); - assert_eq!(weak_ref.strong_count(), 3); - drop(waker); - assert_eq!(weak_ref.strong_count(), 2); - drop(waker2); - assert_eq!(weak_ref.strong_count(), 1); - drop(test_env); - assert_eq!(weak_ref.strong_count(), 0); - assert!(weak_ref.upgrade().is_none()); + fn test_cancel() { + setup_continuation_callback(); + let (_sender, rust_future) = channel(); + + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), None); + rust_future.ffi_cancel(); + // Cancellation should immediately invoke the callback with RustFuturePoll::Ready + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready)); + + // Future polls should immediately invoke the callback with RustFuturePoll::Ready + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready)); + + let (_, call_status) = complete(rust_future); + assert_eq!(call_status.code, RustCallStatusCode::Cancelled); } + // Once `free` is called, the inner future should be released, even if wakers still hold a + // reference to the RustFuture #[test] - fn test_raw_wake() { - let eventloop = MockEventLoop::new(); - let test_env = TestFutureEnvironment::new(&eventloop); - let waker = test_env.rust_future.make_waker(); - let weak_ref = test_env.rust_future_weak(); - // `test_env` and `waker` both hold a strong reference to the `RustFuture` - assert_eq!(weak_ref.strong_count(), 2); - - // wake_by_ref() should schedule a wake - waker.wake_by_ref(); - assert_eq!(eventloop.call_count(), 1); - - // Once the wake runs, the strong could should not have changed - eventloop.run_all_calls(); - assert_eq!(weak_ref.strong_count(), 2); - - // wake() should schedule a wake - waker.wake(); - assert_eq!(eventloop.call_count(), 1); - - // Once the wake runs, the strong have decremented, since wake() consumes the waker - eventloop.run_all_calls(); - assert_eq!(weak_ref.strong_count(), 1); + fn test_release_future() { + setup_continuation_callback(); + let (sender, rust_future) = channel(); + // Create a weak reference to the channel to use to check if rust_future has dropped its + // future. + let channel_weak = Arc::downgrade(&sender.0); + drop(sender); + // Create an extra ref to rust_future, simulating a waker that still holds a reference to + // it + let rust_future2 = rust_future.clone(); + + // Complete the rust future + rust_future.ffi_free(); + // Even though rust_future is still alive, the channel shouldn't be + assert!(Arc::strong_count(&rust_future2) > 0); + assert_eq!(channel_weak.strong_count(), 0); + assert!(channel_weak.upgrade().is_none()); } - // Test trying to create a RustFuture before the executor is shutdown. + // If `free` is called with a continuation still stored, we should call it them then. // - // The main thing we're testing is that we correctly drop the Future in this case + // This shouldn't happen in practice, but it seems like good defensive programming #[test] - fn test_executor_shutdown() { - let eventloop = MockEventLoop::new(); - eventloop.shutdown(); - let test_env = TestFutureEnvironment::new(&eventloop); - let weak_ref = test_env.rust_future_weak(); - // When we wake the future, it should try to schedule a callback and fail. This should - // cause the future to be dropped - test_env.wake(); - drop(test_env); - assert!(weak_ref.upgrade().is_none()); - } - - // Similar run a similar test to the last, but simulate an executor shutdown after the future was - // scheduled, but before the callback is called. - #[test] - fn test_executor_shutdown_after_schedule() { - let eventloop = MockEventLoop::new(); - let test_env = TestFutureEnvironment::new(&eventloop); - let weak_ref = test_env.rust_future_weak(); - test_env.complete_future(Ok(true)); - test_env.wake(); - eventloop.shutdown(); - eventloop.run_all_calls(); - - // Test that the foreign async side wasn't completed. Even though we could have - // driven the future to completion, we shouldn't have since the executor was shutdown - assert!(test_env.foreign_result.is_none()); - // Also test that we've dropped all references to the future - drop(test_env); - assert!(weak_ref.upgrade().is_none()); + fn test_complete_with_stored_continuation() { + setup_continuation_callback(); + let (_sender, rust_future) = channel(); + + let continuation_result = poll(&rust_future); + rust_future.ffi_free(); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready)); } } diff --git a/uniffi_core/src/ffi_converter_impls.rs b/uniffi_core/src/ffi_converter_impls.rs index 6eb29ba8a5..20fd68ce90 100644 --- a/uniffi_core/src/ffi_converter_impls.rs +++ b/uniffi_core/src/ffi_converter_impls.rs @@ -23,8 +23,8 @@ /// "UT" means an abitrary `UniFfiTag` type. 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, - LiftRef, MetadataBuffer, Result, RustBuffer, RustCallStatus, UnexpectedUniFFICallbackError, + lower_into_rust_buffer, metadata, try_lift_from_rust_buffer, FfiConverter, LiftRef, + MetadataBuffer, Result, RustBuffer, UnexpectedUniFFICallbackError, }; use anyhow::bail; use bytes::buf::{Buf, BufMut}; @@ -121,9 +121,6 @@ unsafe impl FfiConverter for () { type FfiType = (); // Returning `()` is FFI-safe, since it gets translated into a void return type ReturnType = (); - // However, we can't use `FutureCallback<()>` since passing `()` as an argument is not - // FFI-safe. So we used an arbitrary non-ZST type instead. - type FutureCallback = FutureCallback; fn try_lift(_: Self::FfiType) -> Result<()> { Ok(()) @@ -141,15 +138,6 @@ unsafe impl FfiConverter for () { Ok(()) } - fn invoke_future_callback( - callback: Self::FutureCallback, - callback_data: *const (), - _return_value: (), - call_status: RustCallStatus, - ) { - callback(callback_data, 0, call_status) - } - const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_UNIT); } @@ -425,7 +413,7 @@ where .concat(V::TYPE_ID_META); } -/// FFI support for ForeignSchedulers +/// FFI support for [ForeignExecutor] /// /// These are passed over the FFI as opaque pointer-sized types representing the foreign executor. /// The foreign bindings may use an actual pointer to the executor object, or a usized integer @@ -479,7 +467,6 @@ where { type FfiType = (); // Placeholder while lower/lift/serializing is unimplemented type ReturnType = R::ReturnType; - type FutureCallback = R::FutureCallback; fn try_lift(_: Self::FfiType) -> Result { unimplemented!("try_lift"); @@ -504,9 +491,9 @@ where } } - fn handle_failed_lift(arg_name: &str, err: anyhow::Error) -> RustBuffer { + fn handle_failed_lift(arg_name: &str, err: anyhow::Error) -> Self { match err.downcast::() { - Ok(actual_error) => lower_into_rust_buffer(actual_error), + Ok(actual_error) => Err(actual_error), Err(ohno) => panic!("Failed to convert arg '{arg_name}': {ohno}"), } } @@ -525,15 +512,6 @@ where Err(E::handle_callback_unexpected_error(e)) } - fn invoke_future_callback( - callback: Self::FutureCallback, - callback_data: *const (), - return_value: Self::ReturnType, - call_status: RustCallStatus, - ) { - R::invoke_future_callback(callback, callback_data, return_value, call_status) - } - const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_RESULT) .concat(R::TYPE_ID_META) .concat(E::TYPE_ID_META); diff --git a/uniffi_core/src/ffi_converter_traits.rs b/uniffi_core/src/ffi_converter_traits.rs index a08d249c85..50474f22ef 100644 --- a/uniffi_core/src/ffi_converter_traits.rs +++ b/uniffi_core/src/ffi_converter_traits.rs @@ -5,7 +5,7 @@ use std::{borrow::Borrow, sync::Arc}; use crate::{ - try_lift_from_rust_buffer, FfiDefault, MetadataBuffer, Result, RustBuffer, RustCallStatus, + try_lift_from_rust_buffer, FfiDefault, MetadataBuffer, Result, RustBuffer, UnexpectedUniFFICallbackError, }; @@ -67,12 +67,6 @@ pub unsafe trait FfiConverter: Sized { /// 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, @@ -97,7 +91,7 @@ pub unsafe trait FfiConverter: Sized { /// returns, if the anyhow error can be downcast to `E`, then serialize that and return it. /// This results in the foreign code throwing a "normal" exception, rather than an unexpected /// exception. - fn handle_failed_lift(arg_name: &str, e: anyhow::Error) -> RustBuffer { + fn handle_failed_lift(arg_name: &str, e: anyhow::Error) -> Self { panic!("Failed to convert arg '{arg_name}': {e}") } @@ -161,14 +155,6 @@ pub unsafe trait FfiConverter: Sized { /// 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; } @@ -187,7 +173,6 @@ pub unsafe trait FfiConverter: Sized { pub unsafe trait FfiConverterArc: Send + Sync { type FfiType; type ReturnType: FfiDefault; - type FutureCallback: Copy; fn lower(obj: Arc) -> Self::FfiType; fn lower_return(obj: Arc) -> Result; @@ -203,12 +188,7 @@ pub unsafe trait FfiConverterArc: Send + Sync { } 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; } @@ -218,7 +198,6 @@ where { type FfiType = T::FfiType; type ReturnType = T::ReturnType; - type FutureCallback = T::FutureCallback; fn lower(obj: Self) -> Self::FfiType { T::lower(obj) @@ -252,15 +231,6 @@ where 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 7444ccca88..f08e37dbc2 100644 --- a/uniffi_core/src/lib.rs +++ b/uniffi_core/src/lib.rs @@ -184,20 +184,10 @@ pub fn try_lift_from_rust_buffer, UT>(v: RustBuffer) -> Resu macro_rules! ffi_converter_default_return { ($uniffi_tag:ty) => { type ReturnType = >::FfiType; - type FutureCallback = $crate::FutureCallback; fn lower_return(v: Self) -> ::std::result::Result { Ok(>::lower(v)) } - - fn invoke_future_callback( - callback: Self::FutureCallback, - callback_data: *const (), - return_value: Self::ReturnType, - call_status: $crate::RustCallStatus, - ) { - callback(callback_data, return_value, call_status); - } }; } @@ -264,7 +254,6 @@ macro_rules! do_ffi_converter_forward { 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) @@ -288,20 +277,6 @@ macro_rules! do_ffi_converter_forward { <$T as $crate::$trait<$existing_impl_tag>>::try_read(buf) } - fn invoke_future_callback( - callback: Self::FutureCallback, - callback_data: *const (), - return_value: Self::ReturnType, - call_status: $crate::RustCallStatus, - ) { - <$T as $crate::$trait<$existing_impl_tag>>::invoke_future_callback( - callback, - callback_data, - return_value, - call_status, - ) - } - const TYPE_ID_META: ::uniffi::MetadataBuffer = <$T as $crate::$trait<$existing_impl_tag>>::TYPE_ID_META; } diff --git a/uniffi_macros/src/export.rs b/uniffi_macros/src/export.rs index 9a77beb700..5c950f306f 100644 --- a/uniffi_macros/src/export.rs +++ b/uniffi_macros/src/export.rs @@ -191,7 +191,6 @@ pub(crate) fn ffi_converter_trait_impl(trait_ident: &Ident, udl_mode: bool) -> T 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 @@ -222,15 +221,6 @@ pub(crate) fn ffi_converter_trait_impl(trait_ident: &Ident, udl_mode: bool) -> T 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) diff --git a/uniffi_macros/src/export/scaffolding.rs b/uniffi_macros/src/export/scaffolding.rs index 3f25d4ed0d..5981a69b78 100644 --- a/uniffi_macros/src/export/scaffolding.rs +++ b/uniffi_macros/src/export/scaffolding.rs @@ -92,9 +92,9 @@ pub(super) fn gen_method_scaffolding( struct ScaffoldingBits { /// Parameters for the scaffolding function params: Vec, - /// Statements to execute before `rust_fn_call` - pre_fn_call: TokenStream, - /// Tokenstream for the call to the actual Rust function + /// Lift closure. See `FnSignature::lift_closure` for an explanation of this. + lift_closure: TokenStream, + /// Expression to call the Rust function after a successful lift. rust_fn_call: TokenStream, } @@ -102,17 +102,18 @@ impl ScaffoldingBits { fn new_for_function(sig: &FnSignature, udl_mode: bool) -> Self { let ident = &sig.ident; let params: Vec<_> = sig.args.iter().map(NamedArg::scaffolding_param).collect(); - let param_lifts = sig.lift_exprs(); - let simple_rust_fn_call = quote! { #ident(#(#param_lifts,)*) }; + let call_params = sig.rust_call_params(false); + let rust_fn_call = quote! { #ident(#call_params) }; + // UDL mode adds an extra conversion (#1749) let rust_fn_call = if udl_mode && sig.looks_like_result { - quote! { #simple_rust_fn_call.map_err(::std::convert::Into::into) } + quote! { #rust_fn_call.map_err(::std::convert::Into::into) } } else { - simple_rust_fn_call + rust_fn_call }; Self { params, - pre_fn_call: quote! {}, + lift_closure: sig.lift_closure(None), rust_fn_call, } } @@ -136,23 +137,24 @@ impl ScaffoldingBits { let params: Vec<_> = iter::once(quote! { uniffi_self_lowered: #ffi_converter::FfiType }) .chain(sig.scaffolding_params()) .collect(); - let param_lifts = sig.lift_exprs(); - let simple_rust_fn_call = quote! { uniffi_self.#ident(#(#param_lifts,)*) }; + let lift_closure = sig.lift_closure(Some(quote! { + match #ffi_converter::try_lift(uniffi_self_lowered) { + Ok(v) => v, + Err(e) => return Err(("self", e)) + } + })); + let call_params = sig.rust_call_params(true); + let rust_fn_call = quote! { uniffi_args.0.#ident(#call_params) }; + // UDL mode adds an extra conversion (#1749) let rust_fn_call = if udl_mode && sig.looks_like_result { - quote! { #simple_rust_fn_call.map_err(::std::convert::Into::into) } + quote! { #rust_fn_call.map_err(::std::convert::Into::into) } } else { - simple_rust_fn_call + rust_fn_call }; - let return_ffi_converter = sig.return_ffi_converter(); Self { params, - pre_fn_call: quote! { - let uniffi_self = match #ffi_converter::try_lift(uniffi_self_lowered) { - Ok(v) => v, - Err(e) => return Err(#return_ffi_converter::handle_failed_lift("self", e)), - }; - }, + lift_closure, rust_fn_call, } } @@ -160,20 +162,21 @@ impl ScaffoldingBits { fn new_for_constructor(sig: &FnSignature, self_ident: &Ident, udl_mode: bool) -> Self { let ident = &sig.ident; let params: Vec<_> = sig.args.iter().map(NamedArg::scaffolding_param).collect(); - let param_lifts = sig.lift_exprs(); - let simple_rust_fn_call = quote! { #self_ident::#ident(#(#param_lifts,)*) }; + let call_params = sig.rust_call_params(false); + let rust_fn_call = quote! { #self_ident::#ident(#call_params) }; + // UDL mode adds extra conversions (#1749) let rust_fn_call = match (udl_mode, sig.looks_like_result) { // For UDL - (true, false) => quote! { ::std::sync::Arc::new(#simple_rust_fn_call) }, + (true, false) => quote! { ::std::sync::Arc::new(#rust_fn_call) }, (true, true) => { - quote! { #simple_rust_fn_call.map(::std::sync::Arc::new).map_err(::std::convert::Into::into) } + quote! { #rust_fn_call.map(::std::sync::Arc::new).map_err(::std::convert::Into::into) } } - (false, _) => simple_rust_fn_call, + (false, _) => rust_fn_call, }; Self { params, - pre_fn_call: quote! {}, + lift_closure: sig.lift_closure(None), rust_fn_call, } } @@ -190,7 +193,7 @@ fn gen_ffi_function( ) -> syn::Result { let ScaffoldingBits { params, - pre_fn_call, + lift_closure, rust_fn_call, } = match &sig.kind { FnKind::Function => ScaffoldingBits::new_for_function(sig, udl_mode), @@ -213,7 +216,7 @@ fn gen_ffi_function( let ffi_ident = sig.scaffolding_fn_ident()?; let name = &sig.name; - let return_ty = &sig.return_ty; + let return_ffi_converter = &sig.return_ffi_converter(); Ok(if !sig.is_async { quote! { @@ -222,12 +225,17 @@ fn gen_ffi_function( #vis extern "C" fn #ffi_ident( #(#params,)* call_status: &mut ::uniffi::RustCallStatus, - ) -> <#return_ty as ::uniffi::FfiConverter>::ReturnType { + ) -> #return_ffi_converter::ReturnType { ::uniffi::deps::log::debug!(#name); + let uniffi_lift_args = #lift_closure; ::uniffi::rust_call(call_status, || { - #pre_fn_call - <#return_ty as ::uniffi::FfiConverter>::lower_return( - #rust_fn_call + #return_ffi_converter::lower_return( + match uniffi_lift_args() { + Ok(uniffi_args) => #rust_fn_call, + Err((arg_name, anyhow_error)) => { + #return_ffi_converter::handle_failed_lift(arg_name, anyhow_error) + }, + } ) }) } @@ -241,25 +249,25 @@ fn gen_ffi_function( quote! { #[doc(hidden)] #[no_mangle] - #vis extern "C" fn #ffi_ident( - #(#params,)* - uniffi_executor_handle: ::uniffi::ForeignExecutorHandle, - uniffi_callback: <#return_ty as ::uniffi::FfiConverter>::FutureCallback, - uniffi_callback_data: *const (), - uniffi_call_status: &mut ::uniffi::RustCallStatus, - ) { + pub extern "C" fn #ffi_ident(#(#params,)*) -> ::uniffi::RustFutureHandle { ::uniffi::deps::log::debug!(#name); - ::uniffi::rust_call(uniffi_call_status, || { - #pre_fn_call; - let uniffi_rust_future = ::uniffi::RustFuture::<_, #return_ty, crate::UniFfiTag>::new( - #future_expr, - uniffi_executor_handle, - uniffi_callback, - uniffi_callback_data - ); - uniffi_rust_future.wake(); - Ok(()) - }); + let uniffi_lift_args = #lift_closure; + match uniffi_lift_args() { + Ok(uniffi_args) => { + ::uniffi::rust_future_new( + async move { #future_expr.await }, + crate::UniFfiTag + ) + }, + Err((arg_name, anyhow_error)) => { + ::uniffi::rust_future_new( + async move { + #return_ffi_converter::handle_failed_lift(arg_name, anyhow_error) + }, + crate::UniFfiTag, + ) + }, + } } } }) diff --git a/uniffi_macros/src/fnsig.rs b/uniffi_macros/src/fnsig.rs index 5ee86866ab..d921545e5f 100644 --- a/uniffi_macros/src/fnsig.rs +++ b/uniffi_macros/src/fnsig.rs @@ -107,11 +107,48 @@ impl FnSignature { } } - /// Lift expressions for each of our arguments - pub fn lift_exprs(&self) -> impl Iterator + '_ { - self.args - .iter() - .map(|a| a.lift_expr(&self.return_ffi_converter())) + /// Generate a closure that tries to lift all arguments into a tuple. + /// + /// The closure moves all scaffolding arguments into itself and returns: + /// - The lifted argument tuple on success + /// - The field name and error on failure (`Err(&'static str, anyhow::Error>`) + pub fn lift_closure(&self, self_lift: Option) -> TokenStream { + let arg_lifts = self.args.iter().map(|arg| { + let ident = &arg.ident; + let ffi_converter = arg.ffi_converter(); + let name = &arg.name; + quote! { + match #ffi_converter::try_lift(#ident) { + Ok(v) => v, + Err(e) => return Err((#name, e)), + } + } + }); + let all_lifts = self_lift.into_iter().chain(arg_lifts); + quote! { + move || Ok(( + #(#all_lifts,)* + )) + } + } + + /// Call a Rust function from a [Self::lift_closure] success. + /// + /// This takes an Ok value returned by `lift_closure` with the name `uniffi_args` and generates + /// a series of parameters to pass to the Rust function. + pub fn rust_call_params(&self, self_lift: bool) -> TokenStream { + let start_idx = if self_lift { 1 } else { 0 }; + let args = self.args.iter().enumerate().map(|(i, arg)| { + let idx = syn::Index::from(i + start_idx); + let ty = &arg.ty; + match &arg.ref_type { + None => quote! { uniffi_args.#idx }, + Some(ref_type) => quote! { + <#ty as ::std::borrow::Borrow<#ref_type>>::borrow(&uniffi_args.#idx) + }, + } + }); + quote! { #(#args),* } } /// Write expressions for each of our arguments @@ -369,26 +406,6 @@ impl NamedArg { quote! { #ident: #ffi_type } } - /// Generate the expression to lift the scaffolding parameter for this arg - pub(crate) fn lift_expr(&self, return_ffi_converter: &TokenStream) -> TokenStream { - let ident = &self.ident; - let ty = &self.ty; - let ffi_converter = self.ffi_converter(); - let name = &self.name; - let lift = quote! { - match #ffi_converter::try_lift(#ident) { - Ok(v) => v, - Err(e) => return Err(#return_ffi_converter::handle_failed_lift(#name, e)) - } - }; - match &self.ref_type { - None => lift, - Some(ref_type) => quote! { - <#ty as ::std::borrow::Borrow<#ref_type>>::borrow(&#lift) - }, - } - } - /// Generate the expression to write the scaffolding parameter for this arg pub(crate) fn write_expr(&self, buf_ident: &Ident) -> TokenStream { let ident = &self.ident; diff --git a/uniffi_macros/src/object.rs b/uniffi_macros/src/object.rs index ee992eb074..b794d41b71 100644 --- a/uniffi_macros/src/object.rs +++ b/uniffi_macros/src/object.rs @@ -65,7 +65,6 @@ pub(crate) fn interface_impl(ident: &Ident, udl_mode: bool) -> TokenStream { // 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 @@ -121,15 +120,6 @@ pub(crate) fn interface_impl(ident: &Ident, udl_mode: bool) -> TokenStream { 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) diff --git a/uniffi_macros/src/setup_scaffolding.rs b/uniffi_macros/src/setup_scaffolding.rs index 7b5a46f10f..d8596c77dd 100644 --- a/uniffi_macros/src/setup_scaffolding.rs +++ b/uniffi_macros/src/setup_scaffolding.rs @@ -22,6 +22,9 @@ pub fn setup_scaffolding(namespace: String) -> Result { let reexport_hack_ident = format_ident!("{module_path}_uniffi_reexport_hack"); let ffi_foreign_executor_callback_set_ident = format_ident!("ffi_{module_path}_foreign_executor_callback_set"); + let ffi_rust_future_continuation_callback_set = + format_ident!("ffi_{module_path}_rust_future_continuation_callback_set"); + let ffi_rust_future_scaffolding_fns = rust_future_scaffolding_fns(&module_path); Ok(quote! { // Unit struct to parameterize the FfiConverter trait. @@ -93,6 +96,15 @@ pub fn setup_scaffolding(namespace: String) -> Result { uniffi::ffi::foreign_executor_callback_set(callback) } + #[allow(clippy::missing_safety_doc, missing_docs)] + #[doc(hidden)] + #[no_mangle] + pub unsafe extern "C" fn #ffi_rust_future_continuation_callback_set(callback: ::uniffi::RustFutureContinuationCallback) { + ::uniffi::ffi::rust_future_continuation_callback_set(callback); + } + + #ffi_rust_future_scaffolding_fns + // Code to re-export the UniFFI scaffolding functions. // // Rust won't always re-export the functions from dependencies @@ -132,3 +144,74 @@ pub fn setup_scaffolding(namespace: String) -> Result { } }) } + +/// Generates the rust_future_* functions +/// +/// The foreign side uses a type-erased `RustFutureHandle` to interact with futures, which presents +/// a problem when creating scaffolding functions. What is the `ReturnType` parameter of `RustFutureFfi`? +/// +/// Handle this by using some brute-force monomorphization. For each possible ffi type, we +/// generate a set of scaffolding functions. The bindings code is responsible for calling the one +/// corresponds the scaffolding function that created the `RustFutureHandle`. +/// +/// This introduces safety issues, but we do get some type checking. If the bindings code calls +/// the wrong rust_future_complete function, they should get an unexpected return type, which +/// hopefully will result in a compile-time error. +fn rust_future_scaffolding_fns(module_path: &str) -> TokenStream { + let fn_info = [ + (quote! { u8 }, "u8"), + (quote! { i8 }, "i8"), + (quote! { u16 }, "u16"), + (quote! { i16 }, "i16"), + (quote! { u32 }, "u32"), + (quote! { i32 }, "i32"), + (quote! { u64 }, "u64"), + (quote! { i64 }, "i64"), + (quote! { f32 }, "f32"), + (quote! { f64 }, "f64"), + (quote! { *const ::std::ffi::c_void }, "pointer"), + (quote! { ::uniffi::RustBuffer }, "rust_buffer"), + (quote! { () }, "void"), + ]; + fn_info.iter() + .map(|(return_type, fn_suffix)| { + let ffi_rust_future_poll = format_ident!("ffi_{module_path}_rust_future_poll_{fn_suffix}"); + let ffi_rust_future_cancel = format_ident!("ffi_{module_path}_rust_future_cancel_{fn_suffix}"); + let ffi_rust_future_complete = format_ident!("ffi_{module_path}_rust_future_complete_{fn_suffix}"); + let ffi_rust_future_free = format_ident!("ffi_{module_path}_rust_future_free_{fn_suffix}"); + + quote! { + #[allow(clippy::missing_safety_doc, missing_docs)] + #[doc(hidden)] + #[no_mangle] + pub unsafe extern "C" fn #ffi_rust_future_poll(handle: ::uniffi::RustFutureHandle, data: *const ()) { + ::uniffi::ffi::rust_future_poll::<#return_type>(handle, data); + } + + #[allow(clippy::missing_safety_doc, missing_docs)] + #[doc(hidden)] + #[no_mangle] + pub unsafe extern "C" fn #ffi_rust_future_cancel(handle: ::uniffi::RustFutureHandle) { + ::uniffi::ffi::rust_future_cancel::<#return_type>(handle) + } + + #[allow(clippy::missing_safety_doc, missing_docs)] + #[doc(hidden)] + #[no_mangle] + pub unsafe extern "C" fn #ffi_rust_future_complete( + handle: ::uniffi::RustFutureHandle, + out_status: &mut ::uniffi::RustCallStatus + ) -> #return_type { + ::uniffi::ffi::rust_future_complete::<#return_type>(handle, out_status) + } + + #[allow(clippy::missing_safety_doc, missing_docs)] + #[doc(hidden)] + #[no_mangle] + pub unsafe extern "C" fn #ffi_rust_future_free(handle: ::uniffi::RustFutureHandle) { + ::uniffi::ffi::rust_future_free::<#return_type>(handle) + } + } + }) + .collect() +} diff --git a/uniffi_meta/src/lib.rs b/uniffi_meta/src/lib.rs index 5cfdee9607..23d8fde418 100644 --- a/uniffi_meta/src/lib.rs +++ b/uniffi_meta/src/lib.rs @@ -23,7 +23,7 @@ mod metadata; // `docs/uniffi-versioning.md` for details. // // Once we get to 1.0, then we'll need to update the scheme to something like 100 + major_version -pub const UNIFFI_CONTRACT_VERSION: u32 = 23; +pub const UNIFFI_CONTRACT_VERSION: u32 = 24; /// Similar to std::hash::Hash. /// From 496ed732999fa52c7e70d227ba0392bc3ede4500 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Mon, 25 Sep 2023 09:38:45 -0400 Subject: [PATCH 10/24] Split up the FfiConverter trait Added new traits for specific operations (lift, lower, return, etc) and use those in the macro code. Added the `derive_ffi_traits!` macro to derive impls for those traits for types that define FfiConverter. The new system allows us have more fine grained control over what types implement. We now can implement Lower without Lift, Return without Lower, etc. This simplifies callback error handling. No more need for the handle_unknown_callback_error attribute. Errors that implement From work with callback interfaces and errors that don't will result in a compile-time error. Many exported functions that used to cause UniFFI panics when called, like functions that inputted errors or results will now fail to compile. This required some changes to the keywords fixtures. It also means that we don't need to implement `FfiConverter` methods with a body that just panics. --- CHANGELOG.md | 2 + docs/manual/src/proc_macro/index.md | 29 -- fixtures/keywords/kotlin/src/keywords.udl | 3 +- fixtures/keywords/kotlin/src/lib.rs | 5 +- fixtures/keywords/rust/src/keywords.udl | 7 +- fixtures/keywords/rust/src/lib.rs | 11 +- fixtures/metadata/src/tests.rs | 4 +- fixtures/proc-macro/src/lib.rs | 1 - ..._used_in_callbacks_cant_have_fields.stderr | 1 - .../tests/ui/invalid_types_in_signatures.rs | 25 + .../ui/invalid_types_in_signatures.stderr | 53 ++ .../tests/ui/non_hashable_record_key.stderr | 6 +- .../tests/ui/trait_methods_no_trait.stderr | 2 +- uniffi/tests/ui/proc_macro_arc.stderr | 7 +- uniffi_bindgen/src/interface/mod.rs | 4 - uniffi_bindgen/src/scaffolding/mod.rs | 18 +- .../templates/CallbackInterfaceTemplate.rs | 2 +- .../scaffolding/templates/ErrorTemplate.rs | 3 - .../scaffolding/templates/ObjectTemplate.rs | 6 +- .../src/scaffolding/templates/macros.rs | 6 +- uniffi_core/src/ffi/callbackinterface.rs | 6 +- uniffi_core/src/ffi/rustcalls.rs | 24 +- uniffi_core/src/ffi/rustfuture.rs | 31 +- uniffi_core/src/ffi_converter_impls.rs | 347 +++++++------- uniffi_core/src/ffi_converter_traits.rs | 453 +++++++++++++----- uniffi_core/src/lib.rs | 86 +--- uniffi_macros/src/custom.rs | 25 +- uniffi_macros/src/enum_.rs | 39 +- uniffi_macros/src/error.rs | 109 ++--- uniffi_macros/src/export.rs | 7 +- .../src/export/callback_interface.rs | 38 +- uniffi_macros/src/export/scaffolding.rs | 10 +- uniffi_macros/src/fnsig.rs | 44 +- uniffi_macros/src/object.rs | 7 +- uniffi_macros/src/record.rs | 18 +- uniffi_macros/src/util.rs | 13 +- 36 files changed, 793 insertions(+), 659 deletions(-) create mode 100644 fixtures/uitests/tests/ui/invalid_types_in_signatures.rs create mode 100644 fixtures/uitests/tests/ui/invalid_types_in_signatures.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index 713e2d4f54..ff357acb3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ - Proc-macros: Added support for ByRef arguments - Proc-macros: Implemented custom type conversion error handling (https://mozilla.github.io/uniffi-rs/udl/custom_types.html#error-handling-during-conversion) - Error types must now implement `Error + Send + Sync + 'static`. +- Proc-macros: The `handle_unknown_callback_error` attribute is no longer needed for callback + interface errors ### What's Fixed diff --git a/docs/manual/src/proc_macro/index.md b/docs/manual/src/proc_macro/index.md index 6fc5b9081e..e2a3b76ec6 100644 --- a/docs/manual/src/proc_macro/index.md +++ b/docs/manual/src/proc_macro/index.md @@ -331,35 +331,6 @@ pub trait Person { // } ``` -### Exception handling in callback interfaces - -Most languages allow arbitrary exceptions to be thrown, which presents issues for callback -interfaces. If a callback interface function returns a non-Result type, then any exception will -result in a panic on the Rust side. - -To avoid panics, callback interfaces can use `Result` types for all return values. If the callback -interface implementation throws the exception that corresponds to the `E` parameter, `Err(E)` will -be returned to the Rust code. However, in most languages it's still possible for the implementation -to throw other exceptions. To avoid panics in those cases, the error type must be wrapped -with the `#[uniffi(handle_unknown_callback_error)]` attribute and -`From` must be implemented: - -```rust -#[derive(uniffi::Error)] -#[uniffi(handle_unknown_callback_error)] -pub enum MyApiError { - IOError, - ValueError, - UnexpectedError { reason: String }, -} - -impl From for MyApiError { - fn from(e: UnexpectedUniFFICallbackError) -> Self { - Self::UnexpectedError { reason: e.reason } - } -} -``` - ## Types from dependent crates When using proc-macros, you can use types from dependent crates in your exported library, as long as diff --git a/fixtures/keywords/kotlin/src/keywords.udl b/fixtures/keywords/kotlin/src/keywords.udl index 72e1c1e8cc..6cdd7a6f07 100644 --- a/fixtures/keywords/kotlin/src/keywords.udl +++ b/fixtures/keywords/kotlin/src/keywords.udl @@ -9,7 +9,7 @@ interface break { callback interface continue { return return(return v); - continue? continue(sequence continue); + continue? continue(); record break(break? v); while while(sequence while); record>? class(record> v); @@ -32,7 +32,6 @@ interface false { true(u8 object); }; -[Error] enum class { "object", }; diff --git a/fixtures/keywords/kotlin/src/lib.rs b/fixtures/keywords/kotlin/src/lib.rs index c156a8037c..d5c63ab780 100644 --- a/fixtures/keywords/kotlin/src/lib.rs +++ b/fixtures/keywords/kotlin/src/lib.rs @@ -17,7 +17,7 @@ impl r#break { #[allow(non_camel_case_types)] trait r#continue { fn r#return(&self, v: r#return) -> r#return; - fn r#continue(&self, v: Vec>) -> Option>; + fn r#continue(&self) -> Option>; fn r#break(&self, _v: Option>) -> HashMap>; fn r#while(&self, _v: Vec) -> r#while; fn class(&self, _v: HashMap>) -> Option>>; @@ -44,9 +44,8 @@ pub enum r#false { } #[allow(non_camel_case_types)] -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] pub enum class { - #[error("object error")] object, } diff --git a/fixtures/keywords/rust/src/keywords.udl b/fixtures/keywords/rust/src/keywords.udl index fe51b1e315..1edc1cdb6b 100644 --- a/fixtures/keywords/rust/src/keywords.udl +++ b/fixtures/keywords/rust/src/keywords.udl @@ -7,7 +7,7 @@ namespace keywords_rust { interface break { return return(return v); - sequence? continue(sequence continue); + void continue(sequence continue); record? break(record v); void yield(u8 async); void async(u8? yield); @@ -15,7 +15,7 @@ interface break { callback interface continue { return return(return v); - continue? continue(sequence continue); + continue? continue(); record break(break? v); while while(sequence while); record>? yield(record> v); @@ -28,7 +28,7 @@ dictionary return { dictionary while { return return; - sequence continue; + sequence yield; record break; break? for; sequence async; @@ -39,7 +39,6 @@ interface async { as(u8 async); }; -[Error] enum yield { "async", }; diff --git a/fixtures/keywords/rust/src/lib.rs b/fixtures/keywords/rust/src/lib.rs index 09f9e6de24..0b93557307 100644 --- a/fixtures/keywords/rust/src/lib.rs +++ b/fixtures/keywords/rust/src/lib.rs @@ -16,9 +16,7 @@ impl r#break { pub fn r#break(&self, v: HashMap>) -> Option>> { Some(v) } - fn r#continue(&self, v: Vec>) -> Option>> { - Some(v) - } + fn r#continue(&self, _v: Vec>) {} pub fn r#yield(&self, _async: u8) {} pub fn r#async(&self, _yield: Option) {} } @@ -26,7 +24,7 @@ impl r#break { #[allow(non_camel_case_types)] pub trait r#continue { fn r#return(&self, v: r#return) -> r#return; - fn r#continue(&self, v: Vec>) -> Option>; + fn r#continue(&self) -> Option>; fn r#break(&self, _v: Option>) -> HashMap>; fn r#while(&self, _v: Vec) -> r#while; fn r#yield(&self, _v: HashMap>) -> Option>>; @@ -42,7 +40,7 @@ pub struct r#return { #[allow(non_camel_case_types)] pub struct r#while { r#return: r#return, - r#continue: Vec>, + r#yield: Vec, r#break: HashMap>, r#for: Option>, r#async: Vec, @@ -55,9 +53,8 @@ pub enum r#async { } #[allow(non_camel_case_types)] -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] pub enum r#yield { - #[error("async error")] r#async, } diff --git a/fixtures/metadata/src/tests.rs b/fixtures/metadata/src/tests.rs index 8833aa5360..2469f3c32d 100644 --- a/fixtures/metadata/src/tests.rs +++ b/fixtures/metadata/src/tests.rs @@ -81,9 +81,9 @@ mod test_type_ids { use super::*; use std::collections::HashMap; use std::sync::Arc; - use uniffi_core::FfiConverter; + use uniffi_core::Lower; - fn check_type_id>(correct_type: Type) { + fn check_type_id>(correct_type: Type) { let buf = &mut T::TYPE_ID_META.as_ref(); assert_eq!( uniffi_meta::read_metadata_type(buf).unwrap(), diff --git a/fixtures/proc-macro/src/lib.rs b/fixtures/proc-macro/src/lib.rs index 11af491694..351dbd8245 100644 --- a/fixtures/proc-macro/src/lib.rs +++ b/fixtures/proc-macro/src/lib.rs @@ -188,7 +188,6 @@ fn enum_identity(value: MaybeBool) -> MaybeBool { } #[derive(thiserror::Error, uniffi::Error, Debug, PartialEq, Eq)] -#[uniffi(handle_unknown_callback_error)] pub enum BasicError { #[error("InvalidInput")] InvalidInput, diff --git a/fixtures/uitests/tests/ui/fieldless_errors_used_in_callbacks_cant_have_fields.stderr b/fixtures/uitests/tests/ui/fieldless_errors_used_in_callbacks_cant_have_fields.stderr index 6b8f8c4271..fdb3b15fb3 100644 --- a/fixtures/uitests/tests/ui/fieldless_errors_used_in_callbacks_cant_have_fields.stderr +++ b/fixtures/uitests/tests/ui/fieldless_errors_used_in_callbacks_cant_have_fields.stderr @@ -4,7 +4,6 @@ error[E0533]: expected value, found struct variant `Self::DivisionByZero` | / #[::uniffi::derive_error_for_udl( | | flat_error, | | with_try_read, - | | handle_unknown_callback_error, | | )] | |__^ not a value | diff --git a/fixtures/uitests/tests/ui/invalid_types_in_signatures.rs b/fixtures/uitests/tests/ui/invalid_types_in_signatures.rs new file mode 100644 index 0000000000..a0f9e20cf3 --- /dev/null +++ b/fixtures/uitests/tests/ui/invalid_types_in_signatures.rs @@ -0,0 +1,25 @@ +fn main() { /* empty main required by `trybuild` */} + +#[derive(Debug, uniffi::Error, thiserror::Error)] +pub enum ErrorType { + #[error("Foo")] + Foo, +} + +#[uniffi::export] +pub fn input_error(_e: ErrorType) { } + +#[uniffi::export] +pub fn output_error() -> ErrorType { + ErrorType::Foo +} + +#[uniffi::export] +pub fn input_result(_r: Result<(), ErrorType>) { } + +#[uniffi::export] +pub fn return_option_result() -> Option> { + Some(Ok(())) +} + +uniffi_macros::setup_scaffolding!(); diff --git a/fixtures/uitests/tests/ui/invalid_types_in_signatures.stderr b/fixtures/uitests/tests/ui/invalid_types_in_signatures.stderr new file mode 100644 index 0000000000..ff32243a4f --- /dev/null +++ b/fixtures/uitests/tests/ui/invalid_types_in_signatures.stderr @@ -0,0 +1,53 @@ +error[E0277]: the trait bound `Result<(), ErrorType>: Lift` is not satisfied + --> tests/ui/invalid_types_in_signatures.rs:17:1 + | +17 | #[uniffi::export] + | ^^^^^^^^^^^^^^^^^ the trait `Lift` is not implemented for `Result<(), ErrorType>` + | + = help: the following other types implement trait `Lift`: + Arc + Duration + ErrorType + ForeignExecutor + HashMap + Option + String + SystemTime + and $N others + = note: this error originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) + +note: erroneous constant used + --> tests/ui/invalid_types_in_signatures.rs:17:1 + | +17 | #[uniffi::export] + | ^^^^^^^^^^^^^^^^^ + | + = 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 `Result<(), ErrorType>: Lower` is not satisfied + --> tests/ui/invalid_types_in_signatures.rs:20:1 + | +20 | #[uniffi::export] + | ^^^^^^^^^^^^^^^^^ the trait `Lower` is not implemented for `Result<(), ErrorType>` + | + = help: the following other types implement trait `Lower`: + Arc + Duration + ErrorType + ForeignExecutor + HashMap + Option + String + SystemTime + and $N others + = note: required for `Option>` to implement `Lower` + = note: required for `Option>` to implement `LowerReturn` + = note: this error originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) + +note: erroneous constant used + --> tests/ui/invalid_types_in_signatures.rs:20:1 + | +20 | #[uniffi::export] + | ^^^^^^^^^^^^^^^^^ + | + = note: this note originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/fixtures/uitests/tests/ui/non_hashable_record_key.stderr b/fixtures/uitests/tests/ui/non_hashable_record_key.stderr index 98a1f99d3e..04fcc2f24e 100644 --- a/fixtures/uitests/tests/ui/non_hashable_record_key.stderr +++ b/fixtures/uitests/tests/ui/non_hashable_record_key.stderr @@ -14,7 +14,8 @@ error[E0277]: the trait bound `f32: Hash` is not satisfied u128 u16 and $N others - = note: required for `HashMap` to implement `FfiConverter` + = note: required for `HashMap` to implement `Lower` + = note: required for `HashMap` to implement `LowerReturn` = note: this error originates in the attribute macro `::uniffi::export_for_udl` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `f32: std::cmp::Eq` is not satisfied @@ -33,7 +34,8 @@ error[E0277]: the trait bound `f32: std::cmp::Eq` is not satisfied u128 u16 and $N others - = note: required for `HashMap` to implement `FfiConverter` + = note: required for `HashMap` to implement `Lower` + = note: required for `HashMap` to implement `LowerReturn` = note: this error originates in the attribute macro `::uniffi::export_for_udl` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0425]: cannot find function `get_dict` in this scope diff --git a/fixtures/uitests/tests/ui/trait_methods_no_trait.stderr b/fixtures/uitests/tests/ui/trait_methods_no_trait.stderr index 8dde34ea49..7883f1ffd3 100644 --- a/fixtures/uitests/tests/ui/trait_methods_no_trait.stderr +++ b/fixtures/uitests/tests/ui/trait_methods_no_trait.stderr @@ -16,7 +16,7 @@ note: required by a bound in `uniffi_uniffi_trait_methods_fn_method_traitmethods error[E0277]: `TraitMethods` doesn't implement `std::fmt::Display` --> $OUT_DIR[uniffi_uitests]/trait_methods.uniffi.rs | - | / match as ::uniffi::FfiConverter>::try_lift(r#ptr) { + | / match as ::uniffi::Lift>::try_lift(r#ptr) { | | Ok(ref val) => val, | | Err(err) => panic!("Failed to convert arg '{}': {}", "ptr", err), | | } diff --git a/uniffi/tests/ui/proc_macro_arc.stderr b/uniffi/tests/ui/proc_macro_arc.stderr index e12ebbe671..349fc95f19 100644 --- a/uniffi/tests/ui/proc_macro_arc.stderr +++ b/uniffi/tests/ui/proc_macro_arc.stderr @@ -4,8 +4,10 @@ error[E0277]: the trait bound `Foo: FfiConverterArc` is not satisfied 10 | #[uniffi::export] | ^^^^^^^^^^^^^^^^^ the trait `FfiConverterArc` is not implemented for `Foo` | - = help: the trait `FfiConverter` is implemented for `Arc` + = help: the trait `LowerReturn` is implemented for `Arc` = note: required for `Arc` to implement `FfiConverter` + = note: required for `Arc` to implement `Lower` + = note: required for `Arc` to implement `LowerReturn` = note: this error originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) note: erroneous constant used @@ -22,8 +24,9 @@ error[E0277]: the trait bound `child::Foo: FfiConverterArc` is not sa 20 | #[uniffi::export] | ^^^^^^^^^^^^^^^^^ the trait `FfiConverterArc` is not implemented for `child::Foo` | - = help: the trait `FfiConverter` is implemented for `Arc` + = help: the trait `Lift` is implemented for `Arc` = note: required for `Arc` to implement `FfiConverter` + = note: required for `Arc` to implement `Lift` = note: this error originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) note: erroneous constant used diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index f4b21e0606..3298ce6420 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -290,10 +290,6 @@ impl ComponentInterface { self.types.get_type_definition(name) } - pub fn is_callback_interface_throws_type(&self, type_: Type) -> bool { - self.callback_interface_throws_types.contains(&type_) - } - /// Iterate over all types contained in the given item. /// /// This method uses `iter_types` to iterate over the types contained within the given type, diff --git a/uniffi_bindgen/src/scaffolding/mod.rs b/uniffi_bindgen/src/scaffolding/mod.rs index 8271d70efe..2679d5b357 100644 --- a/uniffi_bindgen/src/scaffolding/mod.rs +++ b/uniffi_bindgen/src/scaffolding/mod.rs @@ -92,20 +92,20 @@ mod filters { }) } - // Map a type to Rust code that specifies the FfiConverter implementation. + // Map a type a ffi converter trait impl // - // This outputs something like `` - pub fn ffi_converter(type_: &Type) -> Result { + // This outputs something like `>` + pub fn ffi_trait(type_: &Type, trait_name: &str) -> Result { Ok(match type_ { Type::External { name, kind: ExternalKind::Interface, .. } => { - format!("<::std::sync::Arc as ::uniffi::FfiConverter>") + format!("<::std::sync::Arc as ::uniffi::{trait_name}>") } _ => format!( - "<{} as ::uniffi::FfiConverter>", + "<{} as ::uniffi::{trait_name}>", type_rs(type_)? ), }) @@ -126,14 +126,6 @@ mod filters { }) } - // Map return types to their fully-qualified `FfiConverter` impl. - pub fn return_ffi_converter(callable: &T) -> Result { - let return_type = return_type(callable)?; - Ok(format!( - "<{return_type} as ::uniffi::FfiConverter>" - )) - } - // Turns a `crate-name` into the `crate_name` the .rs code needs to specify. pub fn crate_name_rs(nm: &str) -> Result { Ok(format!("r#{}", nm.to_string().to_snake_case())) diff --git a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs index a196f76a9b..64c69e4d8e 100644 --- a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs @@ -69,7 +69,7 @@ impl r#{{ trait_name }} for {{ trait_impl }} { let mut args_buf = Vec::new(); {% endif -%} {%- for arg in meth.arguments() %} - {{ arg.as_type().borrow()|ffi_converter }}::write(r#{{ arg.name() }}, &mut args_buf); + {{ arg.as_type().borrow()|ffi_trait("Lower") }}::write(r#{{ arg.name() }}, &mut args_buf); {%- endfor -%} let args_rbuf = uniffi::RustBuffer::from_vec(args_buf); diff --git a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs index 7cd9ba2ec5..94538ecaa8 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs @@ -14,9 +14,6 @@ with_try_read, {%- endif %} {%- endif %} - {%- if ci.is_callback_interface_throws_type(e.as_type()) %} - handle_unknown_callback_error, - {%- endif %} )] enum r#{{ e.name() }} { {%- for variant in e.variants() %} diff --git a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs index 1ad3d34bba..2af0493e5f 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs @@ -83,7 +83,7 @@ impl {{ obj.rust_name() }} { uniffi::deps::static_assertions::assert_impl_all!({{ obj.rust_name() }}: std::fmt::Debug); // This object has a trait method which requires `Debug` be implemented. format!( "{:?}", - match as ::uniffi::FfiConverter>::try_lift(r#ptr) { + match as ::uniffi::Lift>::try_lift(r#ptr) { Ok(ref val) => val, Err(err) => panic!("Failed to convert arg '{}': {}", "ptr", err), } @@ -96,7 +96,7 @@ impl {{ obj.rust_name() }} { uniffi::deps::static_assertions::assert_impl_all!({{ obj.rust_name() }}: std::fmt::Display); // This object has a trait method which requires `Display` be implemented. format!( "{}", - match as ::uniffi::FfiConverter>::try_lift(r#ptr) { + match as ::uniffi::Lift>::try_lift(r#ptr) { Ok(ref val) => val, Err(err) => panic!("Failed to convert arg '{}': {}", "ptr", err), } @@ -109,7 +109,7 @@ impl {{ obj.rust_name() }} { use ::std::hash::{Hash, Hasher}; uniffi::deps::static_assertions::assert_impl_all!({{ obj.rust_name() }}: Hash); // This object has a trait method which requires `Hash` be implemented. let mut s = ::std::collections::hash_map::DefaultHasher::new(); - Hash::hash(match as ::uniffi::FfiConverter>::try_lift(r#ptr) { + Hash::hash(match as ::uniffi::Lift>::try_lift(r#ptr) { Ok(ref val) => val, Err(err) => panic!("Failed to convert arg '{}': {}", "ptr", err), }, &mut s); diff --git a/uniffi_bindgen/src/scaffolding/templates/macros.rs b/uniffi_bindgen/src/scaffolding/templates/macros.rs index 67c19b0476..cc4d5a99a4 100644 --- a/uniffi_bindgen/src/scaffolding/templates/macros.rs +++ b/uniffi_bindgen/src/scaffolding/templates/macros.rs @@ -8,7 +8,7 @@ r#{{ func.name() }}({% call _arg_list_rs_call(func) -%}) {%- macro _arg_list_rs_call(func) %} {%- for arg in func.full_arguments() %} - match {{- arg.as_type().borrow()|ffi_converter }}::try_lift(r#{{ arg.name() }}) { + match {{- arg.as_type().borrow()|ffi_trait("Lift") }}::try_lift(r#{{ arg.name() }}) { {%- if arg.by_ref() %} {# args passed by reference get special treatment for traits and their Box<> #} {%- if arg.is_trait_ref() %} @@ -58,7 +58,7 @@ r#{{ func.name() }}({% call _arg_list_rs_call(func) -%}) {% macro return_signature(func) %} {%- match func.return_type() %} -{%- when Some with (return_type) %} -> {{ return_type|ffi_converter }}::ReturnType +{%- when Some with (return_type) %} -> {{ return_type|ffi_trait("LowerReturn") }}::ReturnType {%- else -%} {%- endmatch -%} {%- endmacro -%} @@ -72,7 +72,7 @@ pub extern "C" fn r#{{ meth.ffi_func().name() }}( ) {% call return_signature(meth) %} { uniffi::deps::log::debug!("{{ meth.ffi_func().name() }}"); uniffi::rust_call(call_status, || { - {{ meth|return_ffi_converter }}::lower_return( + <{{ meth|return_type }} as ::uniffi::LowerReturn>::lower_return( {%- endmacro %} {%- macro method_decl_postscript(meth) %} diff --git a/uniffi_core/src/ffi/callbackinterface.rs b/uniffi_core/src/ffi/callbackinterface.rs index 8379289122..d4c8b42e3e 100644 --- a/uniffi_core/src/ffi/callbackinterface.rs +++ b/uniffi_core/src/ffi/callbackinterface.rs @@ -113,7 +113,7 @@ //! type and then returns to client code. //! -use crate::{FfiConverter, ForeignCallback, ForeignCallbackCell, RustBuffer}; +use crate::{ForeignCallback, ForeignCallbackCell, Lift, LiftReturn, RustBuffer}; use std::fmt; /// The method index used by the Drop trait to communicate to the foreign language side that Rust has finished with it, @@ -169,7 +169,7 @@ impl ForeignCallbackInternals { /// Invoke a callback interface method on the foreign side and return the result pub fn invoke_callback(&self, handle: u64, method: u32, args: RustBuffer) -> R where - R: FfiConverter, + R: LiftReturn, { let mut ret_rbuf = RustBuffer::new(); let callback = self.callback_cell.get(); @@ -189,7 +189,7 @@ impl ForeignCallbackInternals { CallbackResult::Error => R::lift_callback_error(ret_rbuf), CallbackResult::UnexpectedError => { let reason = if !ret_rbuf.is_empty() { - match >::try_lift(ret_rbuf) { + match >::try_lift(ret_rbuf) { Ok(s) => s, Err(e) => { log::error!("{{ trait_name }} Error reading ret_buf: {e}"); diff --git a/uniffi_core/src/ffi/rustcalls.rs b/uniffi_core/src/ffi/rustcalls.rs index ff169d7f67..53265393c0 100644 --- a/uniffi_core/src/ffi/rustcalls.rs +++ b/uniffi_core/src/ffi/rustcalls.rs @@ -8,10 +8,10 @@ //! //! It handles: //! - Catching panics -//! - Adapting the result of `FfiConverter::lower_return()` into either a return value or an +//! - Adapting the result of `Return::lower_return()` into either a return value or an //! exception -use crate::{FfiConverter, FfiDefault, RustBuffer, UniFfiTag}; +use crate::{FfiDefault, Lower, RustBuffer, UniFfiTag}; use std::mem::MaybeUninit; use std::panic; @@ -66,7 +66,7 @@ impl RustCallStatus { pub fn error(message: impl Into) -> Self { Self { code: RustCallStatusCode::UnexpectedError, - error_buf: MaybeUninit::new(>::lower(message.into())), + error_buf: MaybeUninit::new(>::lower(message.into())), } } } @@ -106,7 +106,7 @@ pub enum RustCallStatusCode { /// - For errors that should be translated into thrown exceptions in the foreign code, serialize /// the error into a `RustBuffer`, then return `Ok(buf)` /// - The success type, must implement `FfiDefault`. -/// - `FfiConverter::lower_return` returns `Result<>` types that meet the above criteria> +/// - `Return::lower_return` returns `Result<>` types that meet the above criteria> /// - If the function returns a `Ok` value it will be unwrapped and returned /// - If the function returns a `Err` value: /// - `out_status.code` will be set to [RustCallStatusCode::Error]. @@ -172,7 +172,7 @@ where "Unknown panic!".to_string() }; log::error!("Caught a panic calling rust code: {:?}", message); - >::lower(message) + >::lower(message) })); if let Ok(buf) = message_result { unsafe { @@ -192,7 +192,7 @@ where #[cfg(test)] mod test { use super::*; - use crate::test_util::TestError; + use crate::{test_util::TestError, Lift, LowerReturn}; fn create_call_status() -> RustCallStatus { RustCallStatus { @@ -213,33 +213,31 @@ mod test { fn test_rust_call() { let mut status = create_call_status(); let return_value = rust_call(&mut status, || { - as FfiConverter>::lower_return(test_callback(0)) + as LowerReturn>::lower_return(test_callback(0)) }); assert_eq!(status.code, RustCallStatusCode::Success); assert_eq!(return_value, 100); rust_call(&mut status, || { - as FfiConverter>::lower_return(test_callback(1)) + as LowerReturn>::lower_return(test_callback(1)) }); assert_eq!(status.code, RustCallStatusCode::Error); unsafe { assert_eq!( - >::try_lift(status.error_buf.assume_init()) - .unwrap(), + >::try_lift(status.error_buf.assume_init()).unwrap(), TestError("Error".to_owned()) ); } let mut status = create_call_status(); rust_call(&mut status, || { - as FfiConverter>::lower_return(test_callback(2)) + as LowerReturn>::lower_return(test_callback(2)) }); assert_eq!(status.code, RustCallStatusCode::UnexpectedError); unsafe { assert_eq!( - >::try_lift(status.error_buf.assume_init()) - .unwrap(), + >::try_lift(status.error_buf.assume_init()).unwrap(), "Unexpected value: 2" ); } diff --git a/uniffi_core/src/ffi/rustfuture.rs b/uniffi_core/src/ffi/rustfuture.rs index 6310a240ea..d8b791c245 100644 --- a/uniffi_core/src/ffi/rustfuture.rs +++ b/uniffi_core/src/ffi/rustfuture.rs @@ -88,7 +88,7 @@ use std::{ use once_cell::sync::OnceCell; -use crate::{rust_call_with_out_status, FfiConverter, FfiDefault, RustCallStatus}; +use crate::{rust_call_with_out_status, FfiDefault, LowerReturn, RustCallStatus}; /// Result code for [rust_future_poll]. This is passed to the continuation function. #[repr(i8)] @@ -144,9 +144,9 @@ where // executor calls polls it and the Rust executor wakes it. It does not need to by `Sync`, // since we synchronize all access to the values. F: Future + Send + 'static, - // T is the output of the Future. It needs to implement FfiConverter. Also it must be Send + + // T is the output of the Future. It needs to implement [LowerReturn]. Also it must be Send + // 'static for the same reason as F. - T: FfiConverter + Send + 'static, + T: LowerReturn + Send + 'static, // The UniFfiTag ZST. The Send + 'static bound is to keep rustc happy. UT: Send + 'static, { @@ -198,7 +198,7 @@ pub unsafe fn rust_future_cancel(handle: RustFutureHandle) { /// /// - The [RustFutureHandle] must not previously have been passed to [rust_future_free] /// - The `T` param must correctly correspond to the [rust_future_new] call. It must -/// be `>::ReturnType` +/// be `>::ReturnType` pub unsafe fn rust_future_complete( handle: RustFutureHandle, out_status: &mut RustCallStatus, @@ -286,7 +286,7 @@ struct WrappedFuture where // See rust_future_new for an explanation of these trait bounds F: Future + Send + 'static, - T: FfiConverter + Send + 'static, + T: LowerReturn + Send + 'static, UT: Send + 'static, { // Note: this could be a single enum, but that would make it easy to mess up the future pinning @@ -300,7 +300,7 @@ impl WrappedFuture where // See rust_future_new for an explanation of these trait bounds F: Future + Send + 'static, - T: FfiConverter + Send + 'static, + T: LowerReturn + Send + 'static, UT: Send + 'static, { fn new(future: F) -> Self { @@ -379,7 +379,7 @@ unsafe impl Send for WrappedFuture where // See rust_future_new for an explanation of these trait bounds F: Future + Send + 'static, - T: FfiConverter + Send + 'static, + T: LowerReturn + Send + 'static, UT: Send + 'static, { } @@ -389,14 +389,14 @@ struct RustFuture where // See rust_future_new for an explanation of these trait bounds F: Future + Send + 'static, - T: FfiConverter + Send + 'static, + T: LowerReturn + Send + 'static, UT: Send + 'static, { // This Mutex should never block if our code is working correctly, since there should not be // multiple threads calling [Self::poll] and/or [Self::complete] at the same time. future: Mutex>, continuation_data: Mutex, - // UT is used as the generic parameter for FfiConverter. + // UT is used as the generic parameter for [LowerReturn]. // Let's model this with PhantomData as a function that inputs a UT value. _phantom: PhantomData ()>, } @@ -405,7 +405,7 @@ impl RustFuture where // See rust_future_new for an explanation of these trait bounds F: Future + Send + 'static, - T: FfiConverter + Send + 'static, + T: LowerReturn + Send + 'static, UT: Send + 'static, { fn new(future: F, _tag: UT) -> Arc { @@ -457,7 +457,7 @@ impl Wake for RustFuture where // See rust_future_new for an explanation of these trait bounds F: Future + Send + 'static, - T: FfiConverter + Send + 'static, + T: LowerReturn + Send + 'static, UT: Send + 'static, { fn wake(self: Arc) { @@ -492,7 +492,7 @@ impl RustFutureFfi for RustFuture where // See rust_future_new for an explanation of these trait bounds F: Future + Send + 'static, - T: FfiConverter + Send + 'static, + T: LowerReturn + Send + 'static, UT: Send + 'static, { fn ffi_poll(self: Arc, data: *const ()) { @@ -515,7 +515,8 @@ where #[cfg(test)] mod tests { use super::*; - use crate::{test_util::TestError, try_lift_from_rust_buffer, RustBuffer, RustCallStatusCode}; + use crate::{test_util::TestError, Lift, RustBuffer, RustCallStatusCode}; + use once_cell::sync::OnceCell; use std::task::Waker; // Sender/Receiver pair that we use for testing @@ -623,7 +624,7 @@ mod tests { let (return_buf, call_status) = complete(rust_future); assert_eq!(call_status.code, RustCallStatusCode::Success); assert_eq!( - >::try_lift(return_buf).unwrap(), + >::try_lift(return_buf).unwrap(), "All done" ); } @@ -645,7 +646,7 @@ mod tests { assert_eq!(call_status.code, RustCallStatusCode::Error); unsafe { assert_eq!( - try_lift_from_rust_buffer::( + >::try_lift_from_rust_buffer( call_status.error_buf.assume_init() ) .unwrap(), diff --git a/uniffi_core/src/ffi_converter_impls.rs b/uniffi_core/src/ffi_converter_impls.rs index 20fd68ce90..fc7824fafe 100644 --- a/uniffi_core/src/ffi_converter_impls.rs +++ b/uniffi_core/src/ffi_converter_impls.rs @@ -22,17 +22,18 @@ /// consumer crates. To do this, it defines blanket impls like `impl FFIConverter for u8`. /// "UT" means an abitrary `UniFfiTag` type. 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, LiftRef, - MetadataBuffer, Result, RustBuffer, UnexpectedUniFFICallbackError, + check_remaining, derive_ffi_traits, ffi_converter_rust_buffer_lift_and_lower, metadata, + FfiConverter, ForeignExecutor, Lift, LiftReturn, Lower, LowerReturn, MetadataBuffer, Result, + RustBuffer, UnexpectedUniFFICallbackError, }; use anyhow::bail; use bytes::buf::{Buf, BufMut}; use paste::paste; use std::{ collections::HashMap, - convert::{Infallible, TryFrom}, + convert::TryFrom, error::Error, + sync::Arc, time::{Duration, SystemTime}, }; @@ -44,8 +45,6 @@ macro_rules! impl_ffi_converter_for_num_primitive { ($T:ty, $type_code:expr) => { paste! { unsafe impl FfiConverter for $T { - ffi_converter_default_return!(UT); - type FfiType = $T; fn lower(obj: $T) -> Self::FfiType { @@ -87,8 +86,6 @@ impl_ffi_converter_for_num_primitive!(f64, metadata::codes::TYPE_F64); /// Booleans are passed as an `i8` in order to avoid problems with handling /// C-compatible boolean values on JVM-based languages. unsafe impl FfiConverter for bool { - ffi_converter_default_return!(UT); - type FfiType = i8; fn lower(obj: bool) -> Self::FfiType { @@ -115,56 +112,6 @@ unsafe impl FfiConverter for bool { const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_BOOL); } -/// Support unit-type returns via the FFI. -unsafe impl FfiConverter for () { - // This actually isn't used, but we need to specify something - type FfiType = (); - // Returning `()` is FFI-safe, since it gets translated into a void return - type ReturnType = (); - - fn try_lift(_: Self::FfiType) -> Result<()> { - Ok(()) - } - - fn lower(_: ()) -> Self::FfiType {} - - fn write(_: (), _: &mut Vec) {} - - fn try_read(_: &mut &[u8]) -> Result<()> { - Ok(()) - } - - fn lower_return(_: ()) -> Result { - Ok(()) - } - - const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_UNIT); -} - -unsafe impl FfiConverter for Infallible { - ffi_converter_default_return!(UT); - - type FfiType = RustBuffer; - - fn try_lift(_: Self::FfiType) -> Result { - unreachable!() - } - - fn lower(_: Infallible) -> Self::FfiType { - unreachable!() - } - - fn write(_: Infallible, _: &mut Vec) { - unreachable!() - } - - fn try_read(_: &mut &[u8]) -> Result { - unreachable!() - } - - const TYPE_ID_META: MetadataBuffer = MetadataBuffer::new(); -} - /// Support for passing Strings via the FFI. /// /// Unlike many other implementations of `FfiConverter`, this passes a struct containing @@ -177,8 +124,6 @@ unsafe impl FfiConverter for Infallible { /// followed by utf8-encoded bytes. (It's a signed integer because unsigned types are /// currently experimental in Kotlin). unsafe impl FfiConverter for String { - ffi_converter_default_return!(UT); - type FfiType = RustBuffer; // This returns a struct with a raw pointer to the underlying bytes, so it's very @@ -240,7 +185,6 @@ unsafe impl FfiConverter for String { /// if the total offset should be added to or subtracted from the unix epoch. unsafe impl FfiConverter for SystemTime { ffi_converter_rust_buffer_lift_and_lower!(UT); - ffi_converter_default_return!(UT); fn write(obj: SystemTime, buf: &mut Vec) { let mut sign = 1; @@ -286,7 +230,6 @@ unsafe impl FfiConverter for SystemTime { /// and 999,999,999. unsafe impl FfiConverter for Duration { ffi_converter_rust_buffer_lift_and_lower!(UT); - ffi_converter_default_return!(UT); fn write(obj: Duration, buf: &mut Vec) { buf.put_u64(obj.as_secs()); @@ -301,18 +244,18 @@ unsafe impl FfiConverter for Duration { const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_DURATION); } -/// Support for passing optional values via the FFI. -/// -/// Optional values are currently always passed by serializing to a buffer. -/// We write either a zero byte for `None`, or a one byte followed by the containing -/// item for `Some`. -/// -/// In future we could do the same optimization as rust uses internally, where the -/// `None` option is represented as a null pointer and the `Some` as a valid pointer, -/// but that seems more fiddly and less safe in the short term, so it can wait. -unsafe impl> FfiConverter for Option { - ffi_converter_rust_buffer_lift_and_lower!(UT); - ffi_converter_default_return!(UT); +// Support for passing optional values via the FFI. +// +// Optional values are currently always passed by serializing to a buffer. +// We write either a zero byte for `None`, or a one byte followed by the containing +// item for `Some`. +// +// In future we could do the same optimization as rust uses internally, where the +// `None` option is represented as a null pointer and the `Some` as a valid pointer, +// but that seems more fiddly and less safe in the short term, so it can wait. + +unsafe impl> Lower for Option { + type FfiType = RustBuffer; fn write(obj: Option, buf: &mut Vec) { match obj { @@ -324,6 +267,17 @@ unsafe impl> FfiConverter for Option { } } + fn lower(obj: Option) -> RustBuffer { + Self::lower_into_rust_buffer(obj) + } + + const TYPE_ID_META: MetadataBuffer = + MetadataBuffer::from_code(metadata::codes::TYPE_OPTION).concat(T::TYPE_ID_META); +} + +unsafe impl> Lift for Option { + type FfiType = RustBuffer; + fn try_read(buf: &mut &[u8]) -> Result> { check_remaining(buf, 1)?; Ok(match buf.get_i8() { @@ -333,81 +287,119 @@ unsafe impl> FfiConverter for Option { }) } + fn try_lift(buf: RustBuffer) -> Result> { + Self::try_lift_from_rust_buffer(buf) + } + const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_OPTION).concat(T::TYPE_ID_META); } -/// Support for passing vectors of values via the FFI. -/// -/// Vectors are currently always passed by serializing to a buffer. -/// We write a `i32` item count followed by each item in turn. -/// (It's a signed type due to limits of the JVM). -/// -/// Ideally we would pass `Vec` directly as a `RustBuffer` rather -/// than serializing, and perhaps even pass other vector types using a -/// similar struct. But that's for future work. -unsafe impl> FfiConverter for Vec { - ffi_converter_rust_buffer_lift_and_lower!(UT); - ffi_converter_default_return!(UT); +// Support for passing vectors of values via the FFI. +// +// Vectors are currently always passed by serializing to a buffer. +// We write a `i32` item count followed by each item in turn. +// (It's a signed type due to limits of the JVM). +// +// Ideally we would pass `Vec` directly as a `RustBuffer` rather +// than serializing, and perhaps even pass other vector types using a +// similar struct. But that's for future work. + +unsafe impl> Lower for Vec { + type FfiType = RustBuffer; fn write(obj: Vec, buf: &mut Vec) { // TODO: would be nice not to panic here :-/ let len = i32::try_from(obj.len()).unwrap(); buf.put_i32(len); // We limit arrays to i32::MAX items for item in obj { - >::write(item, buf); + >::write(item, buf); } } + fn lower(obj: Vec) -> RustBuffer { + Self::lower_into_rust_buffer(obj) + } + + const TYPE_ID_META: MetadataBuffer = + MetadataBuffer::from_code(metadata::codes::TYPE_VEC).concat(T::TYPE_ID_META); +} + +/// Support for associative arrays via the FFI - `record` in UDL. +/// HashMaps are currently always passed by serializing to a buffer. +/// We write a `i32` entries count followed by each entry (string +/// key followed by the value) in turn. +/// (It's a signed type due to limits of the JVM). +unsafe impl> Lift for Vec { + type FfiType = RustBuffer; + fn try_read(buf: &mut &[u8]) -> Result> { check_remaining(buf, 4)?; let len = usize::try_from(buf.get_i32())?; let mut vec = Vec::with_capacity(len); for _ in 0..len { - vec.push(>::try_read(buf)?) + vec.push(>::try_read(buf)?) } Ok(vec) } + fn try_lift(buf: RustBuffer) -> Result> { + Self::try_lift_from_rust_buffer(buf) + } + const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_VEC).concat(T::TYPE_ID_META); } -/// Support for associative arrays via the FFI - `record` in UDL. -/// HashMaps are currently always passed by serializing to a buffer. -/// We write a `i32` entries count followed by each entry (string -/// key followed by the value) in turn. -/// (It's a signed type due to limits of the JVM). -unsafe impl FfiConverter for HashMap +unsafe impl Lower for HashMap where - K: FfiConverter + std::hash::Hash + Eq, - V: FfiConverter, + K: Lower + std::hash::Hash + Eq, + V: Lower, { - ffi_converter_rust_buffer_lift_and_lower!(UT); - ffi_converter_default_return!(UT); + type FfiType = RustBuffer; fn write(obj: HashMap, buf: &mut Vec) { // TODO: would be nice not to panic here :-/ let len = i32::try_from(obj.len()).unwrap(); buf.put_i32(len); // We limit HashMaps to i32::MAX entries for (key, value) in obj { - >::write(key, buf); - >::write(value, buf); + >::write(key, buf); + >::write(value, buf); } } + fn lower(obj: HashMap) -> RustBuffer { + Self::lower_into_rust_buffer(obj) + } + + const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_HASH_MAP) + .concat(K::TYPE_ID_META) + .concat(V::TYPE_ID_META); +} + +unsafe impl Lift for HashMap +where + K: Lift + std::hash::Hash + Eq, + V: Lift, +{ + type FfiType = RustBuffer; + fn try_read(buf: &mut &[u8]) -> Result> { check_remaining(buf, 4)?; let len = usize::try_from(buf.get_i32())?; let mut map = HashMap::with_capacity(len); for _ in 0..len { - let key = >::try_read(buf)?; - let value = >::try_read(buf)?; + let key = >::try_read(buf)?; + let value = >::try_read(buf)?; map.insert(key, value); } Ok(map) } + fn try_lift(buf: RustBuffer) -> Result> { + Self::try_lift_from_rust_buffer(buf) + } + const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_HASH_MAP) .concat(K::TYPE_ID_META) .concat(V::TYPE_ID_META); @@ -418,7 +410,7 @@ where /// These are passed over the FFI as opaque pointer-sized types representing the foreign executor. /// The foreign bindings may use an actual pointer to the executor object, or a usized integer /// handle. -unsafe impl FfiConverter for crate::ForeignExecutor { +unsafe impl FfiConverter for ForeignExecutor { type FfiType = crate::ForeignExecutorHandle; // Passing these back to the foreign bindings is currently not supported @@ -437,7 +429,7 @@ unsafe impl FfiConverter for crate::ForeignExecutor { } fn try_lift(executor: Self::FfiType) -> Result { - Ok(crate::ForeignExecutor::new(executor)) + Ok(ForeignExecutor::new(executor)) } fn try_read(buf: &mut &[u8]) -> Result { @@ -450,44 +442,81 @@ unsafe impl FfiConverter for crate::ForeignExecutor { >::try_lift(crate::ForeignExecutorHandle(usize_val as *const ())) } - ffi_converter_default_return!(UT); - const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_FOREIGN_EXECUTOR); } -/// Support `Result<>` via the FFI. -/// -/// This is currently supported for function returns. Lifting/lowering Result<> arguments is not -/// implemented. -unsafe impl FfiConverter for Result -where - R: FfiConverter, - E: FfiConverter + Error + Send + Sync + 'static, -{ - type FfiType = (); // Placeholder while lower/lift/serializing is unimplemented - type ReturnType = R::ReturnType; +derive_ffi_traits!(blanket u8); +derive_ffi_traits!(blanket i8); +derive_ffi_traits!(blanket u16); +derive_ffi_traits!(blanket i16); +derive_ffi_traits!(blanket u32); +derive_ffi_traits!(blanket i32); +derive_ffi_traits!(blanket u64); +derive_ffi_traits!(blanket i64); +derive_ffi_traits!(blanket f32); +derive_ffi_traits!(blanket f64); +derive_ffi_traits!(blanket bool); +derive_ffi_traits!(blanket String); +derive_ffi_traits!(blanket Duration); +derive_ffi_traits!(blanket SystemTime); +derive_ffi_traits!(blanket ForeignExecutor); + +// For composite types, derive LowerReturn, LiftReturn, etc, from Lift/Lower. +// +// Note that this means we don't get specialized return handling. For example, if we could return +// an `Option>` we would always return that type directly and never throw. +derive_ffi_traits!(impl LowerReturn for Option where Option: Lower); +derive_ffi_traits!(impl LiftReturn for Option where Option: Lift); +derive_ffi_traits!(impl LiftRef for Option where Option: Lift); + +derive_ffi_traits!(impl LowerReturn for Vec where Vec: Lower); +derive_ffi_traits!(impl LiftReturn for Vec where Vec: Lift); +derive_ffi_traits!(impl LiftRef for Vec where Vec: Lift); + +derive_ffi_traits!(impl LowerReturn for HashMap where HashMap: Lower); +derive_ffi_traits!(impl LiftReturn for HashMap where HashMap: Lift); +derive_ffi_traits!(impl LiftRef for HashMap where HashMap: Lift); + +// For Arc we derive all the traits, but have to write it all out because we need an unsized T bound +derive_ffi_traits!(impl Lower for Arc where Arc: FfiConverter, T: ?Sized); +derive_ffi_traits!(impl Lift for Arc where Arc: FfiConverter, T: ?Sized); +derive_ffi_traits!(impl LowerReturn for Arc where Arc: Lower, T: ?Sized); +derive_ffi_traits!(impl LiftReturn for Arc where Arc: Lift, T: ?Sized); +derive_ffi_traits!(impl LiftRef for Arc where Arc: Lift, T: ?Sized); + +// Implement LowerReturn/LiftReturn for the unit type (void returns) + +unsafe impl LowerReturn for () { + type ReturnType = (); - fn try_lift(_: Self::FfiType) -> Result { - unimplemented!("try_lift"); + fn lower_return(_: ()) -> Result { + Ok(()) } - fn lower(_: Self) -> Self::FfiType { - unimplemented!("lower"); - } + const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_UNIT); +} - fn write(_: Self, _: &mut Vec) { - unimplemented!("write"); - } +unsafe impl LiftReturn for () { + fn lift_callback_return(_buf: RustBuffer) -> Self {} - fn try_read(_: &mut &[u8]) -> Result { - unimplemented!("try_read"); - } + const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_UNIT); +} + +// Implement LowerReturn/LiftReturn for `Result`. This is where we handle exceptions/Err +// results. + +unsafe impl LowerReturn for Result +where + R: LowerReturn, + E: Lower + Error + Send + Sync + 'static, +{ + type ReturnType = R::ReturnType; fn lower_return(v: Self) -> Result { match v { Ok(r) => R::lower_return(r), - Err(e) => Err(lower_into_rust_buffer(e)), + Err(e) => Err(E::lower_into_rust_buffer(e)), } } @@ -498,56 +527,36 @@ where } } + const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_RESULT) + .concat(R::TYPE_ID_META) + .concat(E::TYPE_ID_META); +} + +unsafe impl LiftReturn for Result +where + R: LiftReturn, + E: Lift + From, +{ fn lift_callback_return(buf: RustBuffer) -> Self { - Ok(try_lift_from_rust_buffer::(buf) - .expect("Error reading callback interface result")) + Ok(R::lift_callback_return(buf)) } fn lift_callback_error(buf: RustBuffer) -> Self { - Err(try_lift_from_rust_buffer::(buf) - .expect("Error reading callback interface Err result")) + match E::try_lift_from_rust_buffer(buf) { + Ok(lifted_error) => Err(lifted_error), + Err(anyhow_error) => { + Self::handle_callback_unexpected_error(UnexpectedUniFFICallbackError { + reason: format!("Error lifting from rust buffer: {anyhow_error}"), + }) + } + } } fn handle_callback_unexpected_error(e: UnexpectedUniFFICallbackError) -> Self { - Err(E::handle_callback_unexpected_error(e)) + Err(E::from(e)) } const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_RESULT) .concat(R::TYPE_ID_META) .concat(E::TYPE_ID_META); } - -macro_rules! simple_lift_ref_impl { - ($($t:ty),* $(,)?) => { - $( - impl LiftRef for $t { - type LiftType = $t; - } - )* - } -} - -simple_lift_ref_impl!( - u8, i8, u16, i16, u32, i32, u64, i64, f32, f64, bool, String, Duration, SystemTime -); - -impl LiftRef for Option -where - Option: FfiConverter, -{ - type LiftType = Self; -} - -impl LiftRef for Vec -where - Vec: FfiConverter, -{ - type LiftType = Self; -} - -impl LiftRef for HashMap -where - HashMap: FfiConverter, -{ - type LiftType = Self; -} diff --git a/uniffi_core/src/ffi_converter_traits.rs b/uniffi_core/src/ffi_converter_traits.rs index 50474f22ef..7846374270 100644 --- a/uniffi_core/src/ffi_converter_traits.rs +++ b/uniffi_core/src/ffi_converter_traits.rs @@ -2,54 +2,70 @@ * 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/. */ +//! Traits that define how to transfer values via the FFI layer. +//! +//! These traits define how to pass values over the FFI in various ways: as arguments or as return +//! values, from Rust to the foreign side and vice-versa. These traits are mainly used by the +//! proc-macro generated code. The goal is to allow the proc-macros to go from a type name to the +//! correct function for a given FFI operation. +//! +//! The traits form a sort-of tree structure from general to specific: +//! ```ignore +//! +//! [FfiConverter] +//! | +//! ----------------------------- +//! | | +//! [Lower] [Lift] +//! | | +//! | -------------- +//! | | | +//! [LowerReturn] [LiftRef] [LiftReturn] +//! ``` +//! +//! The `derive_ffi_traits` macro can be used to derive the specific traits from the general ones. +//! Here's the main ways we implement these traits: +//! +//! * For most types we implement [FfiConverter] and use [derive_ffi_traits] to implement the rest +//! * If a type can only be lifted/lowered, then we implement [Lift] or [Lower] and use +//! [derive_ffi_traits] to implement the rest +//! * If a type needs special-case handling, like `Result<>` and `()`, we implement the traits +//! directly. +//! +//! 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. +//! +//! ## Safety +//! +//! All traits are unsafe (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. +//! These traits should not be used directly, only in generated code, and the generated code should +//! have fixture tests to test that everything works correctly together. + use std::{borrow::Borrow, sync::Arc}; -use crate::{ - try_lift_from_rust_buffer, FfiDefault, MetadataBuffer, Result, RustBuffer, - UnexpectedUniFFICallbackError, -}; +use anyhow::bail; +use bytes::Buf; -/// 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. +use crate::{FfiDefault, MetadataBuffer, Result, RustBuffer, UnexpectedUniFFICallbackError}; + +/// Generalized FFI conversions /// -/// 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. +/// This trait is not used directly by the code generation, but implement this and calling +/// [derive_ffi_traits] is a simple way to implement all the traits that are. /// /// ## Safety /// -/// This is an unsafe trait (implementing it requires `unsafe impl`) because we can't guarantee +/// All traits are unsafe (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. +/// These traits should not be used directly, only in generated code, and the generated code should +/// have fixture tests to test that everything works correctly together. pub unsafe trait FfiConverter: Sized { /// The low-level type used for passing values of this type over the FFI. /// @@ -60,12 +76,9 @@ pub unsafe trait FfiConverter: Sized { /// 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; + /// If a type implements multiple FFI traits, `FfiType` must be the same for all of them. + type FfiType: FfiDefault; /// Lower a rust value of the target type, into an FFI value of type Self::FfiType. /// @@ -77,24 +90,6 @@ pub unsafe trait FfiConverter: Sized { /// 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; - - /// If possible, get a serialized error for failed argument lifts - /// - /// By default, we just panic and let `rust_call` handle things. However, for `Result<_, E>` - /// returns, if the anyhow error can be downcast to `E`, then serialize that and return it. - /// This results in the foreign code throwing a "normal" exception, rather than an unexpected - /// exception. - fn handle_failed_lift(arg_name: &str, e: anyhow::Error) -> Self { - panic!("Failed to convert arg '{arg_name}': {e}") - } - /// 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, @@ -105,32 +100,6 @@ pub unsafe trait FfiConverter: Sized { /// 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 failure: {e}") - } - /// 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, @@ -155,7 +124,9 @@ pub unsafe trait FfiConverter: Sized { /// from it (but will not mutate the actual contents of the slice). fn try_read(buf: &mut &[u8]) -> Result; - /// Type ID metadata, serialized into a [MetadataBuffer] + /// Type ID metadata, serialized into a [MetadataBuffer]. + /// + /// If a type implements multiple FFI traits, `TYPE_ID_META` must be the same for all of them. const TYPE_ID_META: MetadataBuffer; } @@ -169,23 +140,17 @@ pub unsafe trait FfiConverter: Sized { /// /// ## Safety /// -/// This has the same safety considerations as FfiConverter +/// All traits are unsafe (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. +/// These traits should not be used directly, only in generated code, and the generated code should +/// have fixture tests to test that everything works correctly together. pub unsafe trait FfiConverterArc: Send + Sync { - type FfiType; - type ReturnType: FfiDefault; + type FfiType: FfiDefault; 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 failure: {e}") - } fn write(obj: Arc, buf: &mut Vec); fn try_read(buf: &mut &[u8]) -> Result>; @@ -197,50 +162,290 @@ where T: FfiConverterArc + ?Sized, { type FfiType = T::FfiType; - type ReturnType = T::ReturnType; 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 write(obj: Self, buf: &mut Vec) { + T::write(obj, buf) } - fn lift_callback_error(buf: RustBuffer) -> Self { - T::lift_callback_error(buf) + fn try_read(buf: &mut &[u8]) -> Result { + T::try_read(buf) } - fn handle_callback_unexpected_error(e: UnexpectedUniFFICallbackError) -> Self { - T::handle_callback_unexpected_error(e) + const TYPE_ID_META: MetadataBuffer = T::TYPE_ID_META; +} + +/// Lift values passed by the foreign code over the FFI into Rust values +/// +/// This is used by the code generation to handle arguments. It's usually derived from +/// [FfiConverter], except for types that only support lifting but not lowering. +/// +/// See [FfiConverter] for a discussion of the methods +/// +/// ## Safety +/// +/// All traits are unsafe (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. +/// These traits should not be used directly, only in generated code, and the generated code should +/// have fixture tests to test that everything works correctly together. +pub unsafe trait Lift: Sized { + type FfiType; + + fn try_lift(v: Self::FfiType) -> Result; + + fn try_read(buf: &mut &[u8]) -> Result; + + /// Convenience method + fn try_lift_from_rust_buffer(v: RustBuffer) -> Result { + let vec = v.destroy_into_vec(); + let mut buf = vec.as_slice(); + let value = Self::try_read(&mut buf)?; + match Buf::remaining(&buf) { + 0 => Ok(value), + n => bail!("junk data left in buffer after lifting (count: {n})",), + } } - fn write(obj: Self, buf: &mut Vec) { - T::write(obj, buf) + const TYPE_ID_META: MetadataBuffer; +} + +/// Lower Rust values to pass them to the foreign code +/// +/// This is used to pass arguments to callback interfaces. It's usually derived from +/// [FfiConverter], except for types that only support lowering but not lifting. +/// +/// See [FfiConverter] for a discussion of the methods +/// +/// ## Safety +/// +/// All traits are unsafe (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. +/// These traits should not be used directly, only in generated code, and the generated code should +/// have fixture tests to test that everything works correctly together. +pub unsafe trait Lower: Sized { + type FfiType: FfiDefault; + + fn lower(obj: Self) -> Self::FfiType; + + fn write(obj: Self, buf: &mut Vec); + + /// Convenience method + fn lower_into_rust_buffer(obj: Self) -> RustBuffer { + let mut buf = ::std::vec::Vec::new(); + Self::write(obj, &mut buf); + RustBuffer::from_vec(buf) } - fn try_read(buf: &mut &[u8]) -> Result { - T::try_read(buf) + const TYPE_ID_META: MetadataBuffer; +} + +/// Return Rust values to the foreign code +/// +/// This is usually derived from [Lift], but we special case types like `Result<>` and `()`. +/// +/// ## Safety +/// +/// All traits are unsafe (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. +/// These traits should not be used directly, only in generated code, and the generated code should +/// have fixture tests to test that everything works correctly together. +pub unsafe trait LowerReturn: Sized { + /// The type that should be returned by scaffolding functions for this type. + /// + /// When derived, it's the same as `FfiType`. + type ReturnType: FfiDefault; + + /// 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; + + /// If possible, get a serialized error for failed argument lifts + /// + /// By default, we just panic and let `rust_call` handle things. However, for `Result<_, E>` + /// returns, if the anyhow error can be downcast to `E`, then serialize that and return it. + /// This results in the foreign code throwing a "normal" exception, rather than an unexpected + /// exception. + fn handle_failed_lift(arg_name: &str, e: anyhow::Error) -> Self { + panic!("Failed to convert arg '{arg_name}': {e}") } - const TYPE_ID_META: MetadataBuffer = T::TYPE_ID_META; + const TYPE_ID_META: MetadataBuffer; } -/// Trait used to lift references +/// Return foreign values to Rust /// -/// This is needed because if we see a `&T` parameter, it's not clear which FfiConverter to use to -/// lift it. Normally it's just `T`, but for interfaces it's `Arc` and for callback interfaces -/// it's `Box`. +/// This is usually derived from [Lower], but we special case types like `Result<>` and `()`. /// -/// This trait provides the proc-macros with a way to name the correct type. -pub trait LiftRef { - type LiftType: FfiConverter + Borrow; +/// ## Safety +/// +/// All traits are unsafe (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. +/// These traits should not be used directly, only in generated code, and the generated code should +/// have fixture tests to test that everything works correctly together. +pub unsafe trait LiftReturn: Sized { + /// Lift a Rust value for a callback interface method result + fn lift_callback_return(buf: RustBuffer) -> Self; + + /// 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 failure: {e}") + } + + const TYPE_ID_META: MetadataBuffer; +} + +/// Lift references +/// +/// This is usually derived from [Lift] and also implemented for the inner `T` value of smart +/// pointers. For example, if `Lift` is implemented for `Arc`, then we implement this to lift +/// +/// ## Safety +/// +/// All traits are unsafe (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. +/// These traits should not be used directly, only in generated code, and the generated code should +/// have fixture tests to test that everything works correctly together. +/// `&T` using the Arc. +pub unsafe trait LiftRef { + type LiftType: Lift + Borrow; +} + +/// Derive FFI traits +/// +/// This can be used to derive: +/// * [Lower] and [Lift] from [FfiConverter] +/// * [LowerReturn] from [Lower] +/// * [LiftReturn] and [LiftRef] from [Lift] +/// +/// Usage: +/// ```ignore +/// +/// // Derive everything from [FfiConverter] for all Uniffi tags +/// ::uniffi::derive_ffi_traits!(blanket Foo) +/// // Derive everything from [FfiConverter] for the local crate::UniFfiTag +/// ::uniffi::derive_ffi_traits!(local Foo) +/// // To derive a specific trait, write out the impl item minus the actual block +/// ::uniffi::derive_ffi_traits!(impl LowerReturn for Option) +/// ``` +#[macro_export] +#[allow(clippy::crate_in_macro_def)] +macro_rules! derive_ffi_traits { + (blanket $ty:ty) => { + $crate::derive_ffi_traits!(impl Lower for $ty); + $crate::derive_ffi_traits!(impl Lift for $ty); + $crate::derive_ffi_traits!(impl LowerReturn for $ty); + $crate::derive_ffi_traits!(impl LiftReturn for $ty); + $crate::derive_ffi_traits!(impl LiftRef for $ty); + }; + + (local $ty:ty) => { + $crate::derive_ffi_traits!(impl Lower for $ty); + $crate::derive_ffi_traits!(impl Lift for $ty); + $crate::derive_ffi_traits!(impl LowerReturn for $ty); + $crate::derive_ffi_traits!(impl LiftReturn for $ty); + $crate::derive_ffi_traits!(impl LiftRef for $ty); + }; + + (impl $(<$($generic:ident),*>)? $(::uniffi::)? Lower<$ut:path> for $ty:ty $(where $($where:tt)*)?) => { + unsafe impl $(<$($generic),*>)* $crate::Lower<$ut> for $ty $(where $($where)*)* + { + type FfiType = >::FfiType; + + fn lower(obj: Self) -> Self::FfiType { + >::lower(obj) + } + + fn write(obj: Self, buf: &mut ::std::vec::Vec) { + >::write(obj, buf) + } + + const TYPE_ID_META: $crate::MetadataBuffer = >::TYPE_ID_META; + } + }; + + (impl $(<$($generic:ident),*>)? $(::uniffi::)? Lift<$ut:path> for $ty:ty $(where $($where:tt)*)?) => { + unsafe impl $(<$($generic),*>)* $crate::Lift<$ut> for $ty $(where $($where)*)* + { + type FfiType = >::FfiType; + + fn try_lift(v: Self::FfiType) -> $crate::deps::anyhow::Result { + >::try_lift(v) + } + + fn try_read(buf: &mut &[u8]) -> $crate::deps::anyhow::Result { + >::try_read(buf) + } + + const TYPE_ID_META: $crate::MetadataBuffer = >::TYPE_ID_META; + } + }; + + (impl $(<$($generic:ident),*>)? $(::uniffi::)? LowerReturn<$ut:path> for $ty:ty $(where $($where:tt)*)?) => { + unsafe impl $(<$($generic),*>)* $crate::LowerReturn<$ut> for $ty $(where $($where)*)* + { + type ReturnType = >::FfiType; + + fn lower_return(obj: Self) -> $crate::deps::anyhow::Result { + Ok(>::lower(obj)) + } + + const TYPE_ID_META: $crate::MetadataBuffer =>::TYPE_ID_META; + } + }; + + (impl $(<$($generic:ident),*>)? $(::uniffi::)? LiftReturn<$ut:path> for $ty:ty $(where $($where:tt)*)?) => { + unsafe impl $(<$($generic),*>)* $crate::LiftReturn<$ut> for $ty $(where $($where)*)* + { + fn lift_callback_return(buf: $crate::RustBuffer) -> Self { + >::try_lift_from_rust_buffer(buf) + .expect("Error reading callback interface result") + } + + const TYPE_ID_META: $crate::MetadataBuffer = >::TYPE_ID_META; + } + }; + + (impl $(<$($generic:ident),*>)? $(::uniffi::)? LiftRef<$ut:path> for $ty:ty $(where $($where:tt)*)?) => { + unsafe impl $(<$($generic),*>)* $crate::LiftRef<$ut> for $ty $(where $($where)*)* + { + type LiftType = Self; + } + }; } diff --git a/uniffi_core/src/lib.rs b/uniffi_core/src/lib.rs index f08e37dbc2..4bc383d1f4 100644 --- a/uniffi_core/src/lib.rs +++ b/uniffi_core/src/lib.rs @@ -44,7 +44,9 @@ mod ffi_converter_traits; pub mod metadata; pub use ffi::*; -pub use ffi_converter_traits::{FfiConverter, FfiConverterArc, LiftRef}; +pub use ffi_converter_traits::{ + FfiConverter, FfiConverterArc, Lift, LiftRef, LiftReturn, Lower, LowerReturn, +}; pub use metadata::*; // Re-export the libs that we use in the generated code, @@ -147,50 +149,6 @@ pub fn check_remaining(buf: &[u8], num_bytes: usize) -> Result<()> { Ok(()) } -/// Helper function to lower an `anyhow::Error` that's wrapping an error type -pub fn lower_anyhow_error_or_panic(err: anyhow::Error, arg_name: &str) -> RustBuffer -where - E: 'static + FfiConverter + Sync + Send + std::fmt::Debug + std::fmt::Display, -{ - match err.downcast::() { - Ok(actual_error) => lower_into_rust_buffer(actual_error), - Err(ohno) => panic!("Failed to convert arg '{arg_name}': {ohno}"), - } -} - -/// Helper function to create a RustBuffer with a single value -pub fn lower_into_rust_buffer, UT>(obj: T) -> RustBuffer { - let mut buf = ::std::vec::Vec::new(); - T::write(obj, &mut buf); - RustBuffer::from_vec(buf) -} - -/// Helper function to deserialize a RustBuffer with a single value -pub fn try_lift_from_rust_buffer, UT>(v: RustBuffer) -> Result { - let vec = v.destroy_into_vec(); - let mut buf = vec.as_slice(); - let value = T::try_read(&mut buf)?; - match Buf::remaining(&buf) { - 0 => Ok(value), - n => bail!("junk data left in buffer after lifting (count: {n})",), - } -} - -/// Macro to implement returning values by simply lowering them and returning them -/// -/// This is what we use for all FfiConverters except for `Result`. This would be nicer as a -/// trait default, but Rust doesn't support defaults on associated types. -#[macro_export] -macro_rules! ffi_converter_default_return { - ($uniffi_tag:ty) => { - type ReturnType = >::FfiType; - - fn lower_return(v: Self) -> ::std::result::Result { - Ok(>::lower(v)) - } - }; -} - /// Macro to implement lowering/lifting using a `RustBuffer` /// /// For complex types where it's too fiddly or too unsafe to convert them into a special-purpose @@ -204,11 +162,21 @@ macro_rules! ffi_converter_rust_buffer_lift_and_lower { type FfiType = $crate::RustBuffer; fn lower(v: Self) -> $crate::RustBuffer { - $crate::lower_into_rust_buffer::(v) + let mut buf = ::std::vec::Vec::new(); + >::write(v, &mut buf); + $crate::RustBuffer::from_vec(buf) } fn try_lift(buf: $crate::RustBuffer) -> $crate::Result { - $crate::try_lift_from_rust_buffer::(buf) + let vec = buf.destroy_into_vec(); + let mut buf = vec.as_slice(); + let value = >::try_read(&mut buf)?; + match $crate::deps::bytes::Buf::remaining(&buf) { + 0 => Ok(value), + n => $crate::deps::anyhow::bail!( + "junk data left in buffer after lifting (count: {n})", + ), + } } }; } @@ -222,11 +190,13 @@ macro_rules! ffi_converter_forward { ($T:ty, $existing_impl_tag:ty, $new_impl_tag:ty) => { ::uniffi::do_ffi_converter_forward!( FfiConverter, - Self, + $T, $T, $existing_impl_tag, $new_impl_tag ); + + $crate::derive_ffi_traits!(local $T); }; } @@ -238,11 +208,13 @@ macro_rules! ffi_converter_arc_forward { ($T:ty, $existing_impl_tag:ty, $new_impl_tag:ty) => { ::uniffi::do_ffi_converter_forward!( FfiConverterArc, - ::std::sync::Arc, + ::std::sync::Arc<$T>, $T, $existing_impl_tag, $new_impl_tag ); + + // Note: no need to call derive_ffi_traits! because there is a blanket impl for all Arc }; } @@ -253,18 +225,11 @@ 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; fn lower(obj: $rust_type) -> Self::FfiType { <$T as $crate::$trait<$existing_impl_tag>>::lower(obj) } - fn lower_return( - v: $rust_type, - ) -> ::std::result::Result { - <$T as $crate::$trait<$existing_impl_tag>>::lower_return(v) - } - fn try_lift(v: Self::FfiType) -> $crate::Result<$rust_type> { <$T as $crate::$trait<$existing_impl_tag>>::try_lift(v) } @@ -280,10 +245,6 @@ macro_rules! do_ffi_converter_forward { const TYPE_ID_META: ::uniffi::MetadataBuffer = <$T as $crate::$trait<$existing_impl_tag>>::TYPE_ID_META; } - - impl $crate::LiftRef<$new_impl_tag> for $T { - type LiftType = <$T as $crate::LiftRef<$existing_impl_tag>>::LiftType; - } }; } @@ -328,9 +289,8 @@ pub mod test_util { pub struct TestError(pub String); // Use FfiConverter to simplify lifting TestError out of RustBuffer to check it - unsafe impl FfiConverter for TestError { + unsafe impl FfiConverter for TestError { ffi_converter_rust_buffer_lift_and_lower!(UniFfiTag); - ffi_converter_default_return!(UniFfiTag); fn write(obj: TestError, buf: &mut Vec) { >::write(obj.0, buf); @@ -357,4 +317,6 @@ pub mod test_util { Self(v.into()) } } + + derive_ffi_traits!(blanket TestError); } diff --git a/uniffi_macros/src/custom.rs b/uniffi_macros/src/custom.rs index d3274ca5ac..5fb590f3a1 100644 --- a/uniffi_macros/src/custom.rs +++ b/uniffi_macros/src/custom.rs @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use crate::util::{ident_to_string, mod_path, tagged_impl_header}; +use crate::util::{derive_ffi_traits, ident_to_string, mod_path, tagged_impl_header}; use proc_macro2::{Ident, TokenStream}; use quote::quote; use syn::Path; @@ -15,41 +15,40 @@ pub(crate) fn expand_ffi_converter_custom_type( udl_mode: bool, ) -> syn::Result { let impl_spec = tagged_impl_header("FfiConverter", ident, udl_mode); - let lift_ref_impl_spec = tagged_impl_header("LiftRef", ident, udl_mode); + let derive_ffi_traits = derive_ffi_traits(ident, udl_mode); let name = ident_to_string(ident); let mod_path = mod_path()?; Ok(quote! { #[automatically_derived] unsafe #impl_spec { - type FfiType = <#builtin as ::uniffi::FfiConverter>::FfiType; + // Note: the builtin type needs to implement both `Lower` and `Lift'. We use the + // `Lower` trait to get the associated type `FfiType` and const `TYPE_ID_META`. These + // can't differ between `Lower` and `Lift`. + type FfiType = <#builtin as ::uniffi::Lower>::FfiType; fn lower(obj: #ident ) -> Self::FfiType { - <#builtin as ::uniffi::FfiConverter>::lower(<#ident as crate::UniffiCustomTypeConverter>::from_custom(obj)) + <#builtin as ::uniffi::Lower>::lower(<#ident as crate::UniffiCustomTypeConverter>::from_custom(obj)) } fn try_lift(v: Self::FfiType) -> uniffi::Result<#ident> { - <#ident as crate::UniffiCustomTypeConverter>::into_custom(<#builtin as ::uniffi::FfiConverter>::try_lift(v)?) + <#ident as crate::UniffiCustomTypeConverter>::into_custom(<#builtin as ::uniffi::Lift>::try_lift(v)?) } fn write(obj: #ident, buf: &mut Vec) { - <#builtin as ::uniffi::FfiConverter>::write(<#ident as crate::UniffiCustomTypeConverter>::from_custom(obj), buf); + <#builtin as ::uniffi::Lower>::write(<#ident as crate::UniffiCustomTypeConverter>::from_custom(obj), buf); } fn try_read(buf: &mut &[u8]) -> uniffi::Result<#ident> { - <#ident as crate::UniffiCustomTypeConverter>::into_custom(<#builtin as ::uniffi::FfiConverter>::try_read(buf)?) + <#ident as crate::UniffiCustomTypeConverter>::into_custom(<#builtin as ::uniffi::Lift>::try_read(buf)?) } - ::uniffi::ffi_converter_default_return!(crate::UniFfiTag); - const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TYPE_CUSTOM) .concat_str(#mod_path) .concat_str(#name) - .concat(<#builtin as ::uniffi::FfiConverter>::TYPE_ID_META); + .concat(<#builtin as ::uniffi::Lower>::TYPE_ID_META); } - #lift_ref_impl_spec { - type LiftType = Self; - } + #derive_ffi_traits }) } diff --git a/uniffi_macros/src/enum_.rs b/uniffi_macros/src/enum_.rs index 9034a9540a..3ddccdd7c1 100644 --- a/uniffi_macros/src/enum_.rs +++ b/uniffi_macros/src/enum_.rs @@ -3,7 +3,7 @@ use quote::quote; use syn::{Data, DataEnum, DeriveInput, Field, Index}; use crate::util::{ - create_metadata_items, ident_to_string, mod_path, tagged_impl_header, + create_metadata_items, derive_ffi_traits, ident_to_string, mod_path, tagged_impl_header, try_metadata_value_from_usize, try_read_field, }; @@ -39,7 +39,6 @@ pub(crate) fn enum_ffi_converter_impl( ident, enum_, udl_mode, - false, quote! { ::uniffi::metadata::codes::TYPE_ENUM }, ) } @@ -48,13 +47,11 @@ pub(crate) fn rich_error_ffi_converter_impl( ident: &Ident, enum_: &DataEnum, udl_mode: bool, - handle_unknown_callback_error: bool, ) -> TokenStream { enum_or_error_ffi_converter_impl( ident, enum_, udl_mode, - handle_unknown_callback_error, quote! { ::uniffi::metadata::codes::TYPE_ENUM }, ) } @@ -63,12 +60,11 @@ fn enum_or_error_ffi_converter_impl( ident: &Ident, enum_: &DataEnum, udl_mode: bool, - handle_unknown_callback_error: bool, metadata_type_code: TokenStream, ) -> TokenStream { let name = ident_to_string(ident); let impl_spec = tagged_impl_header("FfiConverter", ident, udl_mode); - let lift_ref_impl_spec = tagged_impl_header("LiftRef", ident, udl_mode); + let derive_ffi_traits = derive_ffi_traits(ident, udl_mode); let mod_path = match mod_path() { Ok(p) => p, Err(e) => return e.into_compile_error(), @@ -109,14 +105,10 @@ fn enum_or_error_ffi_converter_impl( }) }; - let handle_callback_unexpected_error = - handle_callback_unexpected_error_fn(handle_unknown_callback_error); - quote! { #[automatically_derived] unsafe #impl_spec { ::uniffi::ffi_converter_rust_buffer_lift_and_lower!(crate::UniFfiTag); - ::uniffi::ffi_converter_default_return!(crate::UniFfiTag); fn write(obj: Self, buf: &mut ::std::vec::Vec) { #write_impl @@ -126,16 +118,12 @@ fn enum_or_error_ffi_converter_impl( #try_read_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); } - #lift_ref_impl_spec { - type LiftType = Self; - } + #derive_ffi_traits } } @@ -144,7 +132,7 @@ fn write_field(f: &Field) -> TokenStream { let ty = &f.ty; quote! { - <#ty as ::uniffi::FfiConverter>::write(#ident, buf); + <#ty as ::uniffi::Lower>::write(#ident, buf); } } @@ -196,7 +184,7 @@ pub fn variant_metadata(enum_: &DataEnum) -> syn::Result> { .concat_value(#fields_len) #( .concat_str(#field_names) - .concat(<#field_types as ::uniffi::FfiConverter>::TYPE_ID_META) + .concat(<#field_types as ::uniffi::Lower>::TYPE_ID_META) // field defaults not yet supported for enums .concat_bool(false) )* @@ -205,20 +193,3 @@ pub fn variant_metadata(enum_: &DataEnum) -> syn::Result> { ) .collect() } - -/// Generate the `handle_callback_unexpected_error()` implementation -/// -/// If handle_unknown_callback_error is true, this will use the `From` -/// implementation that the library author must provide. -/// -/// If handle_unknown_callback_error is false, then we won't generate any code, falling back to the default -/// implementation which panics. -pub(crate) fn handle_callback_unexpected_error_fn( - handle_unknown_callback_error: bool, -) -> Option { - handle_unknown_callback_error.then(|| quote! { - fn handle_callback_unexpected_error(e: ::uniffi::UnexpectedUniFFICallbackError) -> Self { - >::from(e) - } - }) -} diff --git a/uniffi_macros/src/error.rs b/uniffi_macros/src/error.rs index a60db41753..5ab6f4f424 100644 --- a/uniffi_macros/src/error.rs +++ b/uniffi_macros/src/error.rs @@ -6,7 +6,7 @@ use syn::{ }; use crate::{ - enum_::{handle_callback_unexpected_error_fn, rich_error_ffi_converter_impl, variant_metadata}, + enum_::{rich_error_ffi_converter_impl, variant_metadata}, util::{ chain, create_metadata_items, either_attribute_arg, ident_to_string, kw, mod_path, parse_comma_separated, tagged_impl_header, try_metadata_value_from_usize, @@ -69,20 +69,9 @@ fn error_ffi_converter_impl( udl_mode: bool, ) -> TokenStream { if attr.flat.is_some() { - flat_error_ffi_converter_impl( - ident, - enum_, - udl_mode, - attr.with_try_read.is_some(), - attr.handle_unknown_callback_error.is_some(), - ) + flat_error_ffi_converter_impl(ident, enum_, udl_mode, attr.with_try_read.is_some()) } else { - rich_error_ffi_converter_impl( - ident, - enum_, - udl_mode, - attr.handle_unknown_callback_error.is_some(), - ) + rich_error_ffi_converter_impl(ident, enum_, udl_mode) } } @@ -94,18 +83,16 @@ fn flat_error_ffi_converter_impl( ident: &Ident, enum_: &DataEnum, udl_mode: bool, - implement_try_read: bool, - handle_unknown_callback_error: bool, + implement_lift: bool, ) -> TokenStream { let name = ident_to_string(ident); - let impl_spec = tagged_impl_header("FfiConverter", ident, udl_mode); - let lift_ref_impl_spec = tagged_impl_header("LiftRef", ident, udl_mode); + let lower_impl_spec = tagged_impl_header("Lower", ident, udl_mode); let mod_path = match mod_path() { Ok(p) => p, Err(e) => return e.into_compile_error(), }; - let write_impl = { + let lower_impl = { let match_arms = enum_.variants.iter().enumerate().map(|(i, v)| { let v_ident = &v.ident; let idx = Index::from(i + 1); @@ -113,18 +100,34 @@ fn flat_error_ffi_converter_impl( quote! { Self::#v_ident { .. } => { ::uniffi::deps::bytes::BufMut::put_i32(buf, #idx); - <::std::string::String as ::uniffi::FfiConverter>::write(error_msg, buf); + <::std::string::String as ::uniffi::Lower>::write(error_msg, buf); } } }); quote! { - let error_msg = ::std::string::ToString::to_string(&obj); - match obj { #(#match_arms)* } + #[automatically_derived] + unsafe #lower_impl_spec { + type FfiType = ::uniffi::RustBuffer; + + fn write(obj: Self, buf: &mut ::std::vec::Vec) { + let error_msg = ::std::string::ToString::to_string(&obj); + match obj { #(#match_arms)* } + } + + fn lower(obj: Self) -> ::uniffi::RustBuffer { + >::lower_into_rust_buffer(obj) + } + + const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TYPE_ENUM) + .concat_str(#mod_path) + .concat_str(#name); + } } }; - let try_read_impl = if implement_try_read { + let lift_impl = implement_lift.then(|| { + let lift_impl_spec = tagged_impl_header("Lift", ident, udl_mode); let match_arms = enum_.variants.iter().enumerate().map(|(i, v)| { let v_ident = &v.ident; let idx = Index::from(i + 1); @@ -134,42 +137,30 @@ fn flat_error_ffi_converter_impl( } }); quote! { - Ok(match ::uniffi::deps::bytes::Buf::get_i32(buf) { - #(#match_arms)* - v => ::uniffi::deps::anyhow::bail!("Invalid #ident enum value: {}", v), - }) - } - } else { - quote! { ::std::panic!("try_read not supported for flat errors") } - }; - - let handle_callback_unexpected_error = - handle_callback_unexpected_error_fn(handle_unknown_callback_error); - - quote! { - #[automatically_derived] - unsafe #impl_spec { - ::uniffi::ffi_converter_rust_buffer_lift_and_lower!(crate::UniFfiTag); - ::uniffi::ffi_converter_default_return!(crate::UniFfiTag); + #[automatically_derived] + unsafe #lift_impl_spec { + type FfiType = ::uniffi::RustBuffer; + + fn try_read(buf: &mut &[::std::primitive::u8]) -> ::uniffi::deps::anyhow::Result { + Ok(match ::uniffi::deps::bytes::Buf::get_i32(buf) { + #(#match_arms)* + v => ::uniffi::deps::anyhow::bail!("Invalid #ident enum value: {}", v), + }) + } - fn write(obj: Self, buf: &mut ::std::vec::Vec) { - #write_impl - } + fn try_lift(v: ::uniffi::RustBuffer) -> ::uniffi::deps::anyhow::Result { + >::try_lift_from_rust_buffer(v) + } - fn try_read(buf: &mut &[::std::primitive::u8]) -> ::uniffi::deps::anyhow::Result { - #try_read_impl + const TYPE_ID_META: ::uniffi::MetadataBuffer = >::TYPE_ID_META; } - #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); } + }); - #lift_ref_impl_spec { - type LiftType = Self; - } + quote! { + #lower_impl + #lift_impl } } @@ -211,8 +202,6 @@ pub fn flat_error_variant_metadata(enum_: &DataEnum) -> syn::Result, with_try_read: Option, - /// Can this error be used in a callback interface? - handle_unknown_callback_error: Option, } impl UniffiAttributeArgs for ErrorAttr { @@ -229,10 +218,8 @@ impl UniffiAttributeArgs for ErrorAttr { ..Self::default() }) } else if lookahead.peek(kw::handle_unknown_callback_error) { - Ok(Self { - handle_unknown_callback_error: input.parse()?, - ..Self::default() - }) + // Not used anymore, but still lallowed + Ok(Self::default()) } else { Err(lookahead.error()) } @@ -242,10 +229,6 @@ impl UniffiAttributeArgs for ErrorAttr { Ok(Self { flat: either_attribute_arg(self.flat, other.flat)?, with_try_read: either_attribute_arg(self.with_try_read, other.with_try_read)?, - handle_unknown_callback_error: either_attribute_arg( - self.handle_unknown_callback_error, - other.handle_unknown_callback_error, - )?, }) } } diff --git a/uniffi_macros/src/export.rs b/uniffi_macros/src/export.rs index 5c950f306f..03fb2471ec 100644 --- a/uniffi_macros/src/export.rs +++ b/uniffi_macros/src/export.rs @@ -190,7 +190,6 @@ pub(crate) fn ffi_converter_trait_impl(trait_ident: &Ident, udl_mode: bool) -> T unsafe #impl_spec { type FfiType = *const ::std::os::raw::c_void; - type ReturnType = Self::FfiType; 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 @@ -217,17 +216,13 @@ pub(crate) fn ffi_converter_trait_impl(trait_ident: &Ident, udl_mode: bool) -> T ::uniffi::deps::bytes::Buf::get_u64(buf) as Self::FfiType) } - fn lower_return(v: ::std::sync::Arc) -> ::std::result::Result { - Ok(>::lower(v)) - } - 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); } - #lift_ref_impl_spec { + unsafe #lift_ref_impl_spec { type LiftType = ::std::sync::Arc; } } diff --git a/uniffi_macros/src/export/callback_interface.rs b/uniffi_macros/src/export/callback_interface.rs index 4c8fc360f3..2f2561bbc2 100644 --- a/uniffi_macros/src/export/callback_interface.rs +++ b/uniffi_macros/src/export/callback_interface.rs @@ -66,13 +66,8 @@ pub fn ffi_converter_callback_interface_impl( let name = ident_to_string(trait_ident); let dyn_trait = quote! { dyn #trait_ident }; let box_dyn_trait = quote! { ::std::boxed::Box<#dyn_trait> }; - let impl_spec = tagged_impl_header("FfiConverter", &box_dyn_trait, udl_mode); + let lift_impl_spec = tagged_impl_header("Lift", &box_dyn_trait, udl_mode); let lift_ref_impl_spec = tagged_impl_header("LiftRef", &dyn_trait, udl_mode); - let tag = if udl_mode { - quote! { crate::UniFfiTag } - } else { - quote! { T } - }; let mod_path = match mod_path() { Ok(p) => p, Err(e) => return e.into_compile_error(), @@ -81,38 +76,19 @@ pub fn ffi_converter_callback_interface_impl( quote! { #[doc(hidden)] #[automatically_derived] - unsafe #impl_spec { + unsafe #lift_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 { + fn try_lift(v: Self::FfiType) -> ::uniffi::deps::anyhow::Result { Ok(::std::boxed::Box::new(<#trait_impl_ident>::new(v))) } - fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { + 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::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, ) @@ -120,7 +96,7 @@ pub fn ffi_converter_callback_interface_impl( .concat_str(#name); } - #lift_ref_impl_spec { + unsafe #lift_ref_impl_spec { type LiftType = #box_dyn_trait; } } diff --git a/uniffi_macros/src/export/scaffolding.rs b/uniffi_macros/src/export/scaffolding.rs index 5981a69b78..acc8c44c4c 100644 --- a/uniffi_macros/src/export/scaffolding.rs +++ b/uniffi_macros/src/export/scaffolding.rs @@ -216,7 +216,7 @@ fn gen_ffi_function( let ffi_ident = sig.scaffolding_fn_ident()?; let name = &sig.name; - let return_ffi_converter = &sig.return_ffi_converter(); + let return_impl = &sig.return_impl(); Ok(if !sig.is_async { quote! { @@ -225,15 +225,15 @@ fn gen_ffi_function( #vis extern "C" fn #ffi_ident( #(#params,)* call_status: &mut ::uniffi::RustCallStatus, - ) -> #return_ffi_converter::ReturnType { + ) -> #return_impl::ReturnType { ::uniffi::deps::log::debug!(#name); let uniffi_lift_args = #lift_closure; ::uniffi::rust_call(call_status, || { - #return_ffi_converter::lower_return( + #return_impl::lower_return( match uniffi_lift_args() { Ok(uniffi_args) => #rust_fn_call, Err((arg_name, anyhow_error)) => { - #return_ffi_converter::handle_failed_lift(arg_name, anyhow_error) + #return_impl::handle_failed_lift(arg_name, anyhow_error) }, } ) @@ -262,7 +262,7 @@ fn gen_ffi_function( Err((arg_name, anyhow_error)) => { ::uniffi::rust_future_new( async move { - #return_ffi_converter::handle_failed_lift(arg_name, anyhow_error) + #return_impl::handle_failed_lift(arg_name, anyhow_error) }, crate::UniFfiTag, ) diff --git a/uniffi_macros/src/fnsig.rs b/uniffi_macros/src/fnsig.rs index d921545e5f..f0090f1256 100644 --- a/uniffi_macros/src/fnsig.rs +++ b/uniffi_macros/src/fnsig.rs @@ -100,10 +100,10 @@ impl FnSignature { }) } - pub fn return_ffi_converter(&self) -> TokenStream { + pub fn return_impl(&self) -> TokenStream { let return_ty = &self.return_ty; quote! { - <#return_ty as ::uniffi::FfiConverter> + <#return_ty as ::uniffi::LowerReturn> } } @@ -115,10 +115,10 @@ impl FnSignature { pub fn lift_closure(&self, self_lift: Option) -> TokenStream { let arg_lifts = self.args.iter().map(|arg| { let ident = &arg.ident; - let ffi_converter = arg.ffi_converter(); + let lift_impl = arg.lift_impl(); let name = &arg.name; quote! { - match #ffi_converter::try_lift(#ident) { + match #lift_impl::try_lift(#ident) { Ok(v) => v, Err(e) => return Err((#name, e)), } @@ -201,7 +201,7 @@ impl FnSignature { self.args.len(), "UniFFI limits functions to 256 arguments", )?; - let arg_metadata_calls = self.args.iter().map(NamedArg::metadata_calls); + let arg_metadata_calls = self.args.iter().map(NamedArg::arg_metadata); match &self.kind { FnKind::Function => Ok(create_metadata_items( @@ -214,7 +214,7 @@ impl FnSignature { .concat_bool(#is_async) .concat_value(#args_len) #(#arg_metadata_calls)* - .concat(<#return_ty as ::uniffi::FfiConverter>::TYPE_ID_META) + .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) }, Some(self.checksum_symbol_name()), )), @@ -232,7 +232,7 @@ impl FnSignature { .concat_bool(#is_async) .concat_value(#args_len) #(#arg_metadata_calls)* - .concat(<#return_ty as ::uniffi::FfiConverter>::TYPE_ID_META) + .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) }, Some(self.checksum_symbol_name()), )) @@ -252,7 +252,7 @@ impl FnSignature { .concat_bool(#is_async) .concat_value(#args_len) #(#arg_metadata_calls)* - .concat(<#return_ty as ::uniffi::FfiConverter>::TYPE_ID_META) + .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) }, Some(self.checksum_symbol_name()), )) @@ -270,7 +270,7 @@ impl FnSignature { .concat_str(#name) .concat_value(#args_len) #(#arg_metadata_calls)* - .concat(<#return_ty as ::uniffi::FfiConverter>::TYPE_ID_META) + .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) }, Some(self.checksum_symbol_name()), )) @@ -380,16 +380,14 @@ impl NamedArg { } } - /// Generate the expression for this argument's FfiConverter - pub(crate) fn ffi_converter(&self) -> TokenStream { + pub(crate) fn lift_impl(&self) -> TokenStream { let ty = &self.ty; - quote! { <#ty as ::uniffi::FfiConverter> } + quote! { <#ty as ::uniffi::Lift> } } - /// Generate the expression for this argument's FfiType - pub(crate) fn ffi_type(&self) -> TokenStream { - let ffi_converter = self.ffi_converter(); - quote! { #ffi_converter::FfiType } + pub(crate) fn lower_impl(&self) -> TokenStream { + let ty = &self.ty; + quote! { <#ty as ::uniffi::Lower> } } /// Generate the parameter for this Arg @@ -402,23 +400,23 @@ impl NamedArg { /// Generate the scaffolding parameter for this Arg pub(crate) fn scaffolding_param(&self) -> TokenStream { let ident = &self.ident; - let ffi_type = self.ffi_type(); - quote! { #ident: #ffi_type } + let lift_impl = self.lift_impl(); + quote! { #ident: #lift_impl::FfiType } } /// Generate the expression to write the scaffolding parameter for this arg pub(crate) fn write_expr(&self, buf_ident: &Ident) -> TokenStream { let ident = &self.ident; - let ffi_converter = self.ffi_converter(); - quote! { #ffi_converter::write(#ident, &mut #buf_ident) } + let lower_impl = self.lower_impl(); + quote! { #lower_impl::write(#ident, &mut #buf_ident) } } - pub(crate) fn metadata_calls(&self) -> TokenStream { + pub(crate) fn arg_metadata(&self) -> TokenStream { let name = &self.name; - let ffi_converter = self.ffi_converter(); + let lift_impl = self.lift_impl(); quote! { .concat_str(#name) - .concat(#ffi_converter::TYPE_ID_META) + .concat(#lift_impl::TYPE_ID_META) } } } diff --git a/uniffi_macros/src/object.rs b/uniffi_macros/src/object.rs index b794d41b71..573a1eaadd 100644 --- a/uniffi_macros/src/object.rs +++ b/uniffi_macros/src/object.rs @@ -64,7 +64,6 @@ pub(crate) fn interface_impl(ident: &Ident, udl_mode: bool) -> TokenStream { 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; /// 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 @@ -116,17 +115,13 @@ pub(crate) fn interface_impl(ident: &Ident, udl_mode: bool) -> TokenStream { >::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)) - } - 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); } - #lift_ref_impl_spec { + unsafe #lift_ref_impl_spec { type LiftType = ::std::sync::Arc; } } diff --git a/uniffi_macros/src/record.rs b/uniffi_macros/src/record.rs index aa7fa1eebc..453f67ce86 100644 --- a/uniffi_macros/src/record.rs +++ b/uniffi_macros/src/record.rs @@ -6,8 +6,9 @@ use syn::{ }; use crate::util::{ - create_metadata_items, either_attribute_arg, ident_to_string, kw, mod_path, tagged_impl_header, - try_metadata_value_from_usize, try_read_field, AttributeSliceExt, UniffiAttributeArgs, + create_metadata_items, derive_ffi_traits, either_attribute_arg, ident_to_string, kw, mod_path, + tagged_impl_header, try_metadata_value_from_usize, try_read_field, AttributeSliceExt, + UniffiAttributeArgs, }; pub fn expand_record(input: DeriveInput, udl_mode: bool) -> syn::Result { @@ -40,7 +41,7 @@ pub(crate) fn record_ffi_converter_impl( udl_mode: bool, ) -> syn::Result { let impl_spec = tagged_impl_header("FfiConverter", ident, udl_mode); - let lift_ref_impl_spec = tagged_impl_header("LiftRef", ident, udl_mode); + let derive_ffi_traits = derive_ffi_traits(ident, udl_mode); let name = ident_to_string(ident); let mod_path = mod_path()?; let write_impl: TokenStream = record.fields.iter().map(write_field).collect(); @@ -50,7 +51,6 @@ pub(crate) fn record_ffi_converter_impl( #[automatically_derived] unsafe #impl_spec { ::uniffi::ffi_converter_rust_buffer_lift_and_lower!(crate::UniFfiTag); - ::uniffi::ffi_converter_default_return!(crate::UniFfiTag); fn write(obj: Self, buf: &mut ::std::vec::Vec) { #write_impl @@ -65,9 +65,7 @@ pub(crate) fn record_ffi_converter_impl( .concat_str(#name); } - #lift_ref_impl_spec { - type LiftType = Self; - } + #derive_ffi_traits }) } @@ -76,7 +74,7 @@ fn write_field(f: &Field) -> TokenStream { let ty = &f.ty; quote! { - <#ty as ::uniffi::FfiConverter>::write(obj.#ident, buf); + <#ty as ::uniffi::Lower>::write(obj.#ident, buf); } } @@ -158,9 +156,11 @@ pub(crate) fn record_meta_static_var( None => quote! { .concat_bool(false) }, }; + // Note: fields need to implement both `Lower` and `Lift` to be used in a record. The + // TYPE_ID_META should be the same for both traits. Ok(quote! { .concat_str(#name) - .concat(<#ty as ::uniffi::FfiConverter>::TYPE_ID_META) + .concat(<#ty as ::uniffi::Lower>::TYPE_ID_META) #default }) }) diff --git a/uniffi_macros/src/util.rs b/uniffi_macros/src/util.rs index b4a5a96108..8159e7a88f 100644 --- a/uniffi_macros/src/util.rs +++ b/uniffi_macros/src/util.rs @@ -80,7 +80,7 @@ pub fn try_read_field(f: &syn::Field) -> TokenStream { let ty = &f.ty; quote! { - #ident: <#ty as ::uniffi::FfiConverter>::try_read(buf)?, + #ident: <#ty as ::uniffi::Lift>::try_read(buf)?, } } @@ -216,6 +216,14 @@ pub(crate) fn tagged_impl_header( } } +pub(crate) fn derive_ffi_traits(ty: &Ident, udl_mode: bool) -> TokenStream { + if udl_mode { + quote! { ::uniffi::derive_ffi_traits!(local #ty); } + } else { + quote! { ::uniffi::derive_ffi_traits!(blanket #ty); } + } +} + /// Custom keywords pub mod kw { syn::custom_keyword!(async_runtime); @@ -223,9 +231,10 @@ pub mod kw { syn::custom_keyword!(constructor); syn::custom_keyword!(default); syn::custom_keyword!(flat_error); - syn::custom_keyword!(handle_unknown_callback_error); syn::custom_keyword!(None); syn::custom_keyword!(with_try_read); + // Not used anymore + syn::custom_keyword!(handle_unknown_callback_error); } /// Specifies a type from a dependent crate From 055617027034c42eb4567bd3a50a3b25927e8e9d Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 4 Oct 2023 11:18:49 +0200 Subject: [PATCH 11/24] Fix Swift codegen for async object methods without extra parameters --- fixtures/futures/src/lib.rs | 7 ++++++- .../src/bindings/swift/templates/ObjectTemplate.swift | 7 ++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/fixtures/futures/src/lib.rs b/fixtures/futures/src/lib.rs index 418f3d19a0..63f6c46b5a 100644 --- a/fixtures/futures/src/lib.rs +++ b/fixtures/futures/src/lib.rs @@ -168,7 +168,12 @@ impl Megaphone { say_after(ms, who).await.to_uppercase() } - // An async method that can throw. + /// An async method without any extra arguments. + pub async fn silence(&self) -> String { + String::new() + } + + /// An async method that can throw. pub async fn fallible_me(self: Arc, do_fail: bool) -> Result { if do_fail { Err(MyError::Foo) diff --git a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift index a137803bda..57a77ca6df 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift @@ -47,9 +47,10 @@ public class {{ type_name }}: {{ obj.name() }}Protocol { return {% call swift::try(meth) %} await uniffiRustCallAsync( rustFutureFunc: { {{ meth.ffi_func().name() }}( - self.pointer, - {%- for arg in meth.arguments() %} - {{ arg|lower_fn }}({{ arg.name()|var_name }}){% if !loop.last %},{% endif %} + self.pointer + {%- for arg in meth.arguments() -%} + , + {{ arg|lower_fn }}({{ arg.name()|var_name }}) {%- endfor %} ) }, From b1ae5d3127f85d776d445f77c1957ccc6609507c Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Wed, 4 Oct 2023 10:22:46 -0400 Subject: [PATCH 12/24] Update rust to 1.72 (#1754) --- .circleci/config.yml | 8 ++-- examples/callbacks/src/lib.rs | 2 +- .../ui/invalid_types_in_signatures.stderr | 48 +++++++------------ .../tests/ui/non_hashable_record_key.stderr | 20 ++++---- rust-toolchain.toml | 2 +- uniffi/src/lib.rs | 10 ++-- uniffi/tests/ui/proc_macro_arc.stderr | 16 ------- uniffi_bindgen/src/interface/enum_.rs | 4 +- uniffi_bindgen/src/interface/function.rs | 4 +- uniffi_bindgen/src/interface/mod.rs | 4 +- uniffi_core/src/ffi/foreignbytes.rs | 4 +- uniffi_core/src/ffi/foreignexecutor.rs | 2 + uniffi_udl/src/attributes.rs | 6 +-- uniffi_udl/src/finder.rs | 2 +- 14 files changed, 51 insertions(+), 81 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 393b09ca10..f9110bc59f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,7 +39,7 @@ orbs: jobs: Check Rust formatting: docker: - - image: cimg/rust:1.69 + - image: cimg/rust:1.72 resource_class: small steps: - checkout @@ -49,7 +49,7 @@ jobs: - run: cargo fmt -- --check Lint Rust with clippy: docker: - - image: cimg/rust:1.69 + - image: cimg/rust:1.72 resource_class: small steps: - checkout @@ -59,7 +59,7 @@ jobs: - run: cargo clippy --all --all-targets -- -D warnings Lint Rust Docs: docker: - - image: cimg/rust:1.69 + - image: cimg/rust:1.72 resource_class: small steps: - checkout @@ -117,7 +117,7 @@ jobs: - run: cargo test -- --skip trybuild_ui_tests Deploy website: docker: - - image: cimg/rust:1.69 + - image: cimg/rust:1.72 resource_class: small steps: - checkout diff --git a/examples/callbacks/src/lib.rs b/examples/callbacks/src/lib.rs index 672e5bea9f..3268b80c9c 100644 --- a/examples/callbacks/src/lib.rs +++ b/examples/callbacks/src/lib.rs @@ -26,7 +26,7 @@ pub trait CallAnswerer { pub struct Telephone; impl Telephone { pub fn new() -> Self { - Self::default() + Self {} } pub fn call(&self, answerer: Box) -> Result { diff --git a/fixtures/uitests/tests/ui/invalid_types_in_signatures.stderr b/fixtures/uitests/tests/ui/invalid_types_in_signatures.stderr index ff32243a4f..697bb4437c 100644 --- a/fixtures/uitests/tests/ui/invalid_types_in_signatures.stderr +++ b/fixtures/uitests/tests/ui/invalid_types_in_signatures.stderr @@ -5,25 +5,17 @@ error[E0277]: the trait bound `Result<(), ErrorType>: Lift` is not sa | ^^^^^^^^^^^^^^^^^ the trait `Lift` is not implemented for `Result<(), ErrorType>` | = help: the following other types implement trait `Lift`: - Arc - Duration - ErrorType - ForeignExecutor - HashMap - Option - String - SystemTime + bool + i8 + i16 + i32 + i64 + u8 + u16 + u32 and $N others = note: this error originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) -note: erroneous constant used - --> tests/ui/invalid_types_in_signatures.rs:17:1 - | -17 | #[uniffi::export] - | ^^^^^^^^^^^^^^^^^ - | - = 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 `Result<(), ErrorType>: Lower` is not satisfied --> tests/ui/invalid_types_in_signatures.rs:20:1 | @@ -31,23 +23,15 @@ error[E0277]: the trait bound `Result<(), ErrorType>: Lower` is not s | ^^^^^^^^^^^^^^^^^ the trait `Lower` is not implemented for `Result<(), ErrorType>` | = help: the following other types implement trait `Lower`: - Arc - Duration - ErrorType - ForeignExecutor - HashMap - Option - String - SystemTime + bool + i8 + i16 + i32 + i64 + u8 + u16 + u32 and $N others = note: required for `Option>` to implement `Lower` = note: required for `Option>` to implement `LowerReturn` = note: this error originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) - -note: erroneous constant used - --> tests/ui/invalid_types_in_signatures.rs:20:1 - | -20 | #[uniffi::export] - | ^^^^^^^^^^^^^^^^^ - | - = note: this note originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/fixtures/uitests/tests/ui/non_hashable_record_key.stderr b/fixtures/uitests/tests/ui/non_hashable_record_key.stderr index 04fcc2f24e..2841a127d9 100644 --- a/fixtures/uitests/tests/ui/non_hashable_record_key.stderr +++ b/fixtures/uitests/tests/ui/non_hashable_record_key.stderr @@ -5,14 +5,14 @@ error[E0277]: the trait bound `f32: Hash` is not satisfied | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Hash` is not implemented for `f32` | = help: the following other types implement trait `Hash`: - i128 + isize + i8 i16 i32 i64 - i8 - isize - u128 - u16 + i128 + usize + u8 and $N others = note: required for `HashMap` to implement `Lower` = note: required for `HashMap` to implement `LowerReturn` @@ -25,14 +25,14 @@ error[E0277]: the trait bound `f32: std::cmp::Eq` is not satisfied | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::cmp::Eq` is not implemented for `f32` | = help: the following other types implement trait `std::cmp::Eq`: - i128 + isize + i8 i16 i32 i64 - i8 - isize - u128 - u16 + i128 + usize + u8 and $N others = note: required for `HashMap` to implement `Lower` = note: required for `HashMap` to implement `LowerReturn` diff --git a/rust-toolchain.toml b/rust-toolchain.toml index e0039971b1..3c573cf5ab 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -5,7 +5,7 @@ # * ./.circleci/config.yml which also specifies the rust versions used in CI. [toolchain] -channel = "1.69.0" +channel = "1.72.0" targets = [ "aarch64-linux-android", "armv7-linux-androideabi", diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index 758f1d4c2d..0625bd9c66 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -25,6 +25,11 @@ pub use uniffi_build::{generate_scaffolding, generate_scaffolding_for_crate}; #[cfg(feature = "bindgen-tests")] pub use uniffi_macros::build_foreign_language_testcases; +#[cfg(feature = "cli")] +pub fn uniffi_bindgen_main() { + cli::run_main().unwrap(); +} + #[cfg(test)] mod test { #[test] @@ -33,8 +38,3 @@ mod test { t.compile_fail("tests/ui/*.rs"); } } - -#[cfg(feature = "cli")] -pub fn uniffi_bindgen_main() { - cli::run_main().unwrap(); -} diff --git a/uniffi/tests/ui/proc_macro_arc.stderr b/uniffi/tests/ui/proc_macro_arc.stderr index 349fc95f19..0654a29e25 100644 --- a/uniffi/tests/ui/proc_macro_arc.stderr +++ b/uniffi/tests/ui/proc_macro_arc.stderr @@ -10,14 +10,6 @@ error[E0277]: the trait bound `Foo: FfiConverterArc` is not satisfied = note: required for `Arc` to implement `LowerReturn` = note: this error originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) -note: erroneous constant used - --> tests/ui/proc_macro_arc.rs:10:1 - | -10 | #[uniffi::export] - | ^^^^^^^^^^^^^^^^^ - | - = 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: FfiConverterArc` is not satisfied --> tests/ui/proc_macro_arc.rs:20:5 | @@ -28,11 +20,3 @@ error[E0277]: the trait bound `child::Foo: FfiConverterArc` is not sa = note: required for `Arc` to implement `FfiConverter` = note: required for `Arc` to implement `Lift` = note: this error originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) - -note: erroneous constant used - --> tests/ui/proc_macro_arc.rs:20:5 - | -20 | #[uniffi::export] - | ^^^^^^^^^^^^^^^^^ - | - = note: this note originates in the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/uniffi_bindgen/src/interface/enum_.rs b/uniffi_bindgen/src/interface/enum_.rs index 1dcfccc750..82baf1dd50 100644 --- a/uniffi_bindgen/src/interface/enum_.rs +++ b/uniffi_bindgen/src/interface/enum_.rs @@ -302,7 +302,7 @@ mod test { #[test] fn test_associated_data() { - const UDL: &str = r##" + const UDL: &str = r#" namespace test { void takes_an_enum(TestEnum e); void takes_an_enum_with_data(TestEnumWithData ed); @@ -324,7 +324,7 @@ mod test { One(); Two(); }; - "##; + "#; let ci = ComponentInterface::from_webidl(UDL, "crate_name").unwrap(); assert_eq!(ci.enum_definitions().count(), 3); assert_eq!(ci.function_definitions().len(), 4); diff --git a/uniffi_bindgen/src/interface/function.rs b/uniffi_bindgen/src/interface/function.rs index 1b93bd1229..2d18288c1c 100644 --- a/uniffi_bindgen/src/interface/function.rs +++ b/uniffi_bindgen/src/interface/function.rs @@ -321,7 +321,7 @@ mod test { #[test] fn test_minimal_and_rich_function() -> Result<()> { let ci = ComponentInterface::from_webidl( - r##" + r#" namespace test { void minimal(); [Throws=TestError] @@ -332,7 +332,7 @@ mod test { dictionary TestDict { u32 field; }; - "##, + "#, "crate_name", )?; diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index 3298ce6420..44a3ed6d70 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -774,7 +774,7 @@ impl ComponentInterface { if self.functions.iter().any(|f| f.name == defn.name) { bail!("duplicate function definition: \"{}\"", defn.name); } - if !matches!(self.types.get_type_definition(defn.name()), None) { + if self.types.get_type_definition(defn.name()).is_some() { bail!("Conflicting type definition for \"{}\"", defn.name()); } self.types.add_known_types(defn.iter_types())?; @@ -868,7 +868,7 @@ impl ComponentInterface { // Because functions aren't first class types, we need to check here that // a function name hasn't already been used as a type name. for f in self.functions.iter() { - if !matches!(self.types.get_type_definition(f.name()), None) { + if self.types.get_type_definition(f.name()).is_some() { bail!("Conflicting type definition for \"{}\"", f.name()); } } diff --git a/uniffi_core/src/ffi/foreignbytes.rs b/uniffi_core/src/ffi/foreignbytes.rs index 5ec93118ad..9516f61844 100644 --- a/uniffi_core/src/ffi/foreignbytes.rs +++ b/uniffi_core/src/ffi/foreignbytes.rs @@ -81,7 +81,7 @@ mod test { use super::*; #[test] fn test_foreignbytes_access() { - let v = vec![1u8, 2, 3]; + let v = [1u8, 2, 3]; let fbuf = unsafe { ForeignBytes::from_raw_parts(v.as_ptr(), 3) }; assert_eq!(fbuf.len(), 3); assert_eq!(fbuf.as_slice(), &[1u8, 2, 3]); @@ -111,7 +111,7 @@ mod test { #[test] #[should_panic] fn test_foreignbytes_provided_len_must_be_non_negative() { - let v = vec![0u8, 1, 2]; + let v = [0u8, 1, 2]; let fbuf = unsafe { ForeignBytes::from_raw_parts(v.as_ptr(), -1) }; fbuf.as_slice(); } diff --git a/uniffi_core/src/ffi/foreignexecutor.rs b/uniffi_core/src/ffi/foreignexecutor.rs index b02c053c2d..7b1cb9bd80 100644 --- a/uniffi_core/src/ffi/foreignexecutor.rs +++ b/uniffi_core/src/ffi/foreignexecutor.rs @@ -246,6 +246,8 @@ mod test { is_shutdown: bool, } + unsafe impl Send for MockEventLoopInner {} + static FOREIGN_EXECUTOR_CALLBACK_INIT: Once = Once::new(); impl MockEventLoop { diff --git a/uniffi_udl/src/attributes.rs b/uniffi_udl/src/attributes.rs index a0430da3b2..f06b4f29c1 100644 --- a/uniffi_udl/src/attributes.rs +++ b/uniffi_udl/src/attributes.rs @@ -648,7 +648,7 @@ mod test { let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[]").unwrap(); let attrs = FunctionAttributes::try_from(&node).unwrap(); - assert!(matches!(attrs.get_throws_err(), None)); + assert!(attrs.get_throws_err().is_none()); } #[test] @@ -696,12 +696,12 @@ mod test { let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Throws=Error]").unwrap(); let attrs = ConstructorAttributes::try_from(&node).unwrap(); assert!(matches!(attrs.get_throws_err(), Some("Error"))); - assert!(matches!(attrs.get_name(), None)); + assert!(attrs.get_name().is_none()); let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Name=MyFactory]").unwrap(); let attrs = ConstructorAttributes::try_from(&node).unwrap(); - assert!(matches!(attrs.get_throws_err(), None)); + assert!(attrs.get_throws_err().is_none()); assert!(matches!(attrs.get_name(), Some("MyFactory"))); let (_, node) = diff --git a/uniffi_udl/src/finder.rs b/uniffi_udl/src/finder.rs index 35daabd5a9..0c4c187dc0 100644 --- a/uniffi_udl/src/finder.rs +++ b/uniffi_udl/src/finder.rs @@ -261,7 +261,7 @@ mod test { ); assert!( matches!(types.get_type_definition("CustomType").unwrap(), Type::Custom { name, builtin, ..} - if name == "CustomType" && builtin == Box::new(Type::String)) + if name == "CustomType" && *builtin == Type::String) ); }, ); From b12c7ee145073a733a83bf9079b24f52b14aea22 Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Wed, 4 Oct 2023 10:38:29 -0400 Subject: [PATCH 13/24] Allow uniffi_traits to be used with procmacros. (#1773) --- CHANGELOG.md | 1 + Cargo.lock | 20 +-- docs/manual/src/udl/interfaces.md | 8 + fixtures/metadata/Cargo.toml | 2 +- fixtures/metadata/src/tests.rs | 26 ++- fixtures/trait-methods/src/lib.rs | 25 ++- fixtures/trait-methods/tests/bindings/test.py | 24 +++ .../tests/ui/trait_methods_no_trait.stderr | 22 +-- uniffi_bindgen/src/interface/mod.rs | 10 +- uniffi_bindgen/src/interface/object.rs | 2 +- uniffi_bindgen/src/library_mode.rs | 8 +- uniffi_bindgen/src/macro_metadata/ci.rs | 3 + uniffi_bindgen/src/scaffolding/mod.rs | 26 +-- .../scaffolding/templates/ObjectTemplate.rs | 74 ++------ .../src/scaffolding/templates/macros.rs | 55 ------ uniffi_core/src/metadata.rs | 1 + uniffi_macros/src/export.rs | 12 +- uniffi_macros/src/export/attributes.rs | 29 +++ uniffi_macros/src/export/item.rs | 26 +++ uniffi_macros/src/export/scaffolding.rs | 2 +- uniffi_macros/src/export/utrait.rs | 168 ++++++++++++++++++ uniffi_macros/src/fnsig.rs | 98 ++++++---- uniffi_macros/src/util.rs | 4 + uniffi_meta/src/lib.rs | 51 +++++- uniffi_meta/src/metadata.rs | 1 + uniffi_meta/src/reader.rs | 27 ++- uniffi_udl/src/converters/interface.rs | 4 +- 27 files changed, 515 insertions(+), 214 deletions(-) create mode 100644 uniffi_macros/src/export/utrait.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ff357acb3b..244953b29c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ [All changes in [[UnreleasedUniFFIVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.24.3...HEAD). ### What's new +- Proc-macros can now expose standard Rust traits (eg, `Display`, `Eq`, etc) - Fixed issues when trying to combine UDL and procmacros in the same crate when the "namespace" is different from the crate name. This meant that the "ffi namespace" has changed to consistently be the crate name, rather than either the crate name or the namespace name depending on whether the diff --git a/Cargo.lock b/Cargo.lock index 8a93d1334a..ef1d6bfe1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1786,6 +1786,16 @@ dependencies = [ "uniffi", ] +[[package]] +name = "uniffi-fixture-metadata" +version = "0.1.0" +dependencies = [ + "thiserror", + "uniffi", + "uniffi_core", + "uniffi_meta", +] + [[package]] name = "uniffi-fixture-proc-macro" version = "0.22.0" @@ -1977,16 +1987,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "uniffi_fixture_metadata" -version = "0.1.0" -dependencies = [ - "thiserror", - "uniffi", - "uniffi_core", - "uniffi_meta", -] - [[package]] name = "uniffi_macros" version = "0.24.3" diff --git a/docs/manual/src/udl/interfaces.md b/docs/manual/src/udl/interfaces.md index 474c82ef77..6041c9acfa 100644 --- a/docs/manual/src/udl/interfaces.md +++ b/docs/manual/src/udl/interfaces.md @@ -169,6 +169,14 @@ struct TodoList { ... } ``` +(or using proc-macros) +```rust +#[derive(Debug, uniffi::Object)] +#[uniffi::export(Debug)] +struct TodoList { + ... +} +``` This will cause the Python bindings to generate a `__repr__` method that returns the value implemented by the `Debug` trait. Not all bindings support generating special methods, so they may be ignored. diff --git a/fixtures/metadata/Cargo.toml b/fixtures/metadata/Cargo.toml index b4980799df..a95c447885 100644 --- a/fixtures/metadata/Cargo.toml +++ b/fixtures/metadata/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "uniffi_fixture_metadata" +name = "uniffi-fixture-metadata" version = "0.1.0" edition = "2021" license = "MPL-2.0" diff --git a/fixtures/metadata/src/tests.rs b/fixtures/metadata/src/tests.rs index 2469f3c32d..eac852cfea 100644 --- a/fixtures/metadata/src/tests.rs +++ b/fixtures/metadata/src/tests.rs @@ -66,6 +66,12 @@ mod calc { pub struct Calculator {} } +mod uniffi_traits { + #[derive(Debug, PartialEq, Eq, uniffi::Object)] + #[uniffi::export(Debug, Eq)] + pub struct Special {} +} + #[uniffi::export(callback_interface)] pub trait Logger { fn log(&self, message: String); @@ -75,6 +81,7 @@ pub use calc::Calculator; pub use error::{ComplexError, FlatError}; pub use person::Person; pub use state::State; +pub use uniffi_traits::Special; pub use weapon::Weapon; mod test_type_ids { @@ -308,10 +315,27 @@ mod test_metadata { module_path: "uniffi_fixture_metadata".into(), name: "Calculator".into(), imp: ObjectImpl::Struct, - uniffi_traits: vec![], }, ); } + + #[test] + fn test_uniffi_traits() { + assert!(matches!( + uniffi_meta::read_metadata(&uniffi_traits::UNIFFI_META_UNIFFI_FIXTURE_METADATA_UNIFFI_TRAIT_SPECIAL_DEBUG).unwrap(), + Metadata::UniffiTrait(UniffiTraitMetadata::Debug { fmt }) + if fmt.module_path == "uniffi_fixture_metadata" + && fmt.self_name == "Special" + )); + assert!(matches!( + uniffi_meta::read_metadata(&uniffi_traits::UNIFFI_META_UNIFFI_FIXTURE_METADATA_UNIFFI_TRAIT_SPECIAL_EQ).unwrap(), + Metadata::UniffiTrait(UniffiTraitMetadata::Eq { eq, ne }) + if eq.module_path == "uniffi_fixture_metadata" + && ne.module_path == "uniffi_fixture_metadata" + && eq.self_name == "Special" + && ne.self_name == "Special" + )); + } } mod test_function_metadata { diff --git a/fixtures/trait-methods/src/lib.rs b/fixtures/trait-methods/src/lib.rs index 81da1c640a..acc3683f35 100644 --- a/fixtures/trait-methods/src/lib.rs +++ b/fixtures/trait-methods/src/lib.rs @@ -1,8 +1,11 @@ /* 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, PartialEq, Eq, Hash)] -struct TraitMethods { +pub struct TraitMethods { val: String, } @@ -18,4 +21,24 @@ impl std::fmt::Display for TraitMethods { } } +#[derive(Debug, PartialEq, Eq, Hash, uniffi::Object)] +#[uniffi::export(Debug, Display, Eq, Hash)] +pub struct ProcTraitMethods { + val: String, +} + +#[uniffi::export] +impl ProcTraitMethods { + #[uniffi::constructor] + fn new(val: String) -> Arc { + Arc::new(Self { val }) + } +} + +impl std::fmt::Display for ProcTraitMethods { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ProcTraitMethods({})", self.val) + } +} + uniffi::include_scaffolding!("trait_methods"); diff --git a/fixtures/trait-methods/tests/bindings/test.py b/fixtures/trait-methods/tests/bindings/test.py index 1f5b9cf8e2..9ec51a4cb0 100644 --- a/fixtures/trait-methods/tests/bindings/test.py +++ b/fixtures/trait-methods/tests/bindings/test.py @@ -29,5 +29,29 @@ def test_hash(self): d[m] = "m" self.assertTrue(m in d) +class TestProcmacroTraitMethods(unittest.TestCase): + def test_str(self): + m = ProcTraitMethods("yo") + self.assertEqual(str(m), "ProcTraitMethods(yo)") + + def test_repr(self): + m = ProcTraitMethods("yo") + self.assertEqual(repr(m), 'ProcTraitMethods { val: "yo" }') + + def test_eq(self): + m = ProcTraitMethods("yo") + self.assertEqual(m, ProcTraitMethods("yo")) + self.assertNotEqual(m, ProcTraitMethods("yoyo")) + + def test_eq(self): + m = ProcTraitMethods("yo") + self.assertNotEqual(m, 17) + + def test_hash(self): + d = {} + m = ProcTraitMethods("m") + d[m] = "m" + self.assertTrue(m in d) + if __name__=='__main__': unittest.main() diff --git a/fixtures/uitests/tests/ui/trait_methods_no_trait.stderr b/fixtures/uitests/tests/ui/trait_methods_no_trait.stderr index 7883f1ffd3..c689aeca95 100644 --- a/fixtures/uitests/tests/ui/trait_methods_no_trait.stderr +++ b/fixtures/uitests/tests/ui/trait_methods_no_trait.stderr @@ -1,28 +1,24 @@ error[E0277]: `TraitMethods` doesn't implement `std::fmt::Display` --> $OUT_DIR[uniffi_uitests]/trait_methods.uniffi.rs | - | ... uniffi::deps::static_assertions::assert_impl_all!(r#TraitMethods: std::fmt::Display); // This object has a trait method which requi... - | ^^^^^^^^^^^^^^ `TraitMethods` cannot be formatted with the default formatter + | struct r#TraitMethods { } + | ^^^^^^^^^^^^^^ `TraitMethods` cannot be formatted with the default formatter | = help: the trait `std::fmt::Display` is not implemented for `TraitMethods` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead -note: required by a bound in `uniffi_uniffi_trait_methods_fn_method_traitmethods_uniffi_trait_display::{closure#0}::_::{closure#0}::assert_impl_all` +note: required by a bound in `TraitMethods::uniffi_trait_display::_::{closure#0}::assert_impl_all` --> $OUT_DIR[uniffi_uitests]/trait_methods.uniffi.rs | - | ... uniffi::deps::static_assertions::assert_impl_all!(r#TraitMethods: std::fmt::Display); // This object has a trait method which requi... - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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) + | #[uniffi::export(Display)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_impl_all` + = note: this error originates in the macro `::uniffi::deps::static_assertions::assert_impl_all` which comes from the expansion of the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: `TraitMethods` doesn't implement `std::fmt::Display` --> $OUT_DIR[uniffi_uitests]/trait_methods.uniffi.rs | - | / match as ::uniffi::Lift>::try_lift(r#ptr) { - | | Ok(ref val) => val, - | | Err(err) => panic!("Failed to convert arg '{}': {}", "ptr", err), - | | } - | |_________________^ `TraitMethods` cannot be formatted with the default formatter + | #[uniffi::export(Display)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ `TraitMethods` cannot be formatted with the default formatter | = help: the trait `std::fmt::Display` is not implemented for `TraitMethods` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead - = help: the trait `std::fmt::Display` is implemented for `Arc` - = note: this error originates in the macro `$crate::__export::format_args` which comes from the expansion of the macro `format` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `$crate::__export::format_args` which comes from the expansion of the attribute macro `uniffi::export` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index 44a3ed6d70..8e4df2149b 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -71,7 +71,7 @@ pub use ffi::{FfiArgument, FfiFunction, FfiType}; pub use uniffi_meta::Radix; use uniffi_meta::{ ConstructorMetadata, LiteralMetadata, NamespaceMetadata, ObjectMetadata, TraitMethodMetadata, - UNIFFI_CONTRACT_VERSION, + UniffiTraitMetadata, UNIFFI_CONTRACT_VERSION, }; pub type Literal = LiteralMetadata; @@ -802,7 +802,15 @@ impl ComponentInterface { self.types.add_known_types(method.iter_types())?; method.object_impl = object.imp; object.methods.push(method); + Ok(()) + } + pub(super) fn add_uniffitrait_meta(&mut self, meta: UniffiTraitMetadata) -> Result<()> { + let object = get_object(&mut self.objects, meta.self_name()) + .ok_or_else(|| anyhow!("add_uniffitrait_meta: object not found"))?; + let ut: UniffiTrait = meta.into(); + self.types.add_known_types(ut.iter_types())?; + object.uniffi_traits.push(ut); Ok(()) } diff --git a/uniffi_bindgen/src/interface/object.rs b/uniffi_bindgen/src/interface/object.rs index 4caedf54c5..942032b3c6 100644 --- a/uniffi_bindgen/src/interface/object.rs +++ b/uniffi_bindgen/src/interface/object.rs @@ -225,7 +225,7 @@ impl From for Object { imp: meta.imp, constructors: Default::default(), methods: Default::default(), - uniffi_traits: meta.uniffi_traits.into_iter().map(Into::into).collect(), + uniffi_traits: Default::default(), ffi_func_free: FfiFunction { name: ffi_free_name, ..Default::default() diff --git a/uniffi_bindgen/src/library_mode.rs b/uniffi_bindgen/src/library_mode.rs index 26ca522482..488d2e98fc 100644 --- a/uniffi_bindgen/src/library_mode.rs +++ b/uniffi_bindgen/src/library_mode.rs @@ -26,7 +26,9 @@ use std::{ collections::{HashMap, HashSet}, fs, }; -use uniffi_meta::{create_metadata_groups, fixup_external_type, group_metadata, MetadataGroup}; +use uniffi_meta::{ + create_metadata_groups, fixup_external_type, group_metadata, Metadata, MetadataGroup, +}; /// Generate foreign bindings /// @@ -142,6 +144,10 @@ fn find_sources( .items .into_iter() .map(|item| fixup_external_type(item, &metadata_groups)) + // some items are both in UDL and library metadata. For many that's fine but + // uniffi-traits aren't trivial to compare meaning we end up with dupes. + // We filter out such problematic items here. + .filter(|item| !matches!(item, Metadata::UniffiTrait { .. })) .collect(); udl_items.insert(crate_name, metadata_group); }; diff --git a/uniffi_bindgen/src/macro_metadata/ci.rs b/uniffi_bindgen/src/macro_metadata/ci.rs index c5642c08ba..7ce6c3a70b 100644 --- a/uniffi_bindgen/src/macro_metadata/ci.rs +++ b/uniffi_bindgen/src/macro_metadata/ci.rs @@ -109,6 +109,9 @@ fn add_item_to_ci(iface: &mut ComponentInterface, item: Metadata) -> anyhow::Res })?; iface.add_object_meta(meta)?; } + Metadata::UniffiTrait(meta) => { + iface.add_uniffitrait_meta(meta)?; + } Metadata::CallbackInterface(meta) => { iface.types.add_known_type(&Type::CallbackInterface { module_path: meta.module_path.clone(), diff --git a/uniffi_bindgen/src/scaffolding/mod.rs b/uniffi_bindgen/src/scaffolding/mod.rs index 2679d5b357..f3759cf6fa 100644 --- a/uniffi_bindgen/src/scaffolding/mod.rs +++ b/uniffi_bindgen/src/scaffolding/mod.rs @@ -68,31 +68,7 @@ mod filters { }) } - pub fn type_ffi(type_: &FfiType) -> Result { - Ok(match type_ { - FfiType::Int8 => "i8".into(), - FfiType::UInt8 => "u8".into(), - FfiType::Int16 => "i16".into(), - FfiType::UInt16 => "u16".into(), - FfiType::Int32 => "i32".into(), - FfiType::UInt32 => "u32".into(), - FfiType::Int64 => "i64".into(), - FfiType::UInt64 => "u64".into(), - FfiType::Float32 => "f32".into(), - FfiType::Float64 => "f64".into(), - FfiType::RustArcPtr(_) => "*const std::os::raw::c_void".into(), - FfiType::RustBuffer(_) => "::uniffi::RustBuffer".into(), - FfiType::ForeignBytes => "::uniffi::ForeignBytes".into(), - FfiType::ForeignCallback => "::uniffi::ForeignCallback".into(), - FfiType::RustFutureHandle => "::uniffi::RustFutureHandle".into(), - FfiType::RustFutureContinuationCallback => "::uniffi::RustFutureContinuation".into(), - FfiType::RustFutureContinuationData => "*const ()".into(), - FfiType::ForeignExecutorHandle => "::uniffi::ForeignExecutorHandle".into(), - FfiType::ForeignExecutorCallback => "::uniffi::ForeignExecutorCallback".into(), - }) - } - - // Map a type a ffi converter trait impl + // Map a type to Rust code that specifies the FfiConverter implementation. // // This outputs something like `>` pub fn ffi_trait(type_: &Type, trait_name: &str) -> Result { diff --git a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs index 2af0493e5f..3346d4fbfb 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs @@ -28,6 +28,18 @@ pub trait r#{{ obj.name() }} { {% endfor %} } {% when ObjectImpl::Struct %} +{%- for tm in obj.uniffi_traits() %} +{% match tm %} +{% when UniffiTrait::Debug { fmt }%} +#[uniffi::export(Debug)] +{% when UniffiTrait::Display { fmt }%} +#[uniffi::export(Display)] +{% when UniffiTrait::Hash { hash }%} +#[uniffi::export(Hash)] +{% when UniffiTrait::Eq { eq, ne }%} +#[uniffi::export(Eq)] +{% endmatch %} +{% endfor %} #[::uniffi::derive_object_for_udl] struct {{ obj.rust_name() }} { } @@ -73,65 +85,3 @@ impl {{ obj.rust_name() }} { {%- endfor %} {% endmatch %} - -{%- for tm in obj.uniffi_traits() %} -{# All magic methods get an explicit shim #} -{% match tm %} -{% when UniffiTrait::Debug { fmt }%} - {% call rs::method_decl_prelude(fmt) %} - { - uniffi::deps::static_assertions::assert_impl_all!({{ obj.rust_name() }}: std::fmt::Debug); // This object has a trait method which requires `Debug` be implemented. - format!( - "{:?}", - match as ::uniffi::Lift>::try_lift(r#ptr) { - Ok(ref val) => val, - Err(err) => panic!("Failed to convert arg '{}': {}", "ptr", err), - } - ) - } - {% call rs::method_decl_postscript(fmt) %} -{% when UniffiTrait::Display { fmt }%} - {% call rs::method_decl_prelude(fmt) %} - { - uniffi::deps::static_assertions::assert_impl_all!({{ obj.rust_name() }}: std::fmt::Display); // This object has a trait method which requires `Display` be implemented. - format!( - "{}", - match as ::uniffi::Lift>::try_lift(r#ptr) { - Ok(ref val) => val, - Err(err) => panic!("Failed to convert arg '{}': {}", "ptr", err), - } - ) - } - {% call rs::method_decl_postscript(fmt) %} -{% when UniffiTrait::Hash { hash }%} - {% call rs::method_decl_prelude(hash) %} - { - use ::std::hash::{Hash, Hasher}; - uniffi::deps::static_assertions::assert_impl_all!({{ obj.rust_name() }}: Hash); // This object has a trait method which requires `Hash` be implemented. - let mut s = ::std::collections::hash_map::DefaultHasher::new(); - Hash::hash(match as ::uniffi::Lift>::try_lift(r#ptr) { - Ok(ref val) => val, - Err(err) => panic!("Failed to convert arg '{}': {}", "ptr", err), - }, &mut s); - s.finish() - } - {% call rs::method_decl_postscript(hash) %} -{% when UniffiTrait::Eq { eq, ne }%} - {# PartialEq::Eq #} - {% call rs::method_decl_prelude(eq) %} - { - use ::std::cmp::PartialEq; - uniffi::deps::static_assertions::assert_impl_all!({{ obj.rust_name() }}: PartialEq); // This object has a trait method which requires `PartialEq` be implemented. - PartialEq::eq({% call rs::_arg_list_rs_call(eq) -%}) - } - {% call rs::method_decl_postscript(eq) %} - {# PartialEq::Ne #} - {% call rs::method_decl_prelude(ne) %} - { - use ::std::cmp::PartialEq; - uniffi::deps::static_assertions::assert_impl_all!({{ obj.rust_name() }}: PartialEq); // This object has a trait method which requires `PartialEq` be implemented. - PartialEq::ne({% call rs::_arg_list_rs_call(ne) -%}) - } - {% call rs::method_decl_postscript(ne) %} -{% endmatch %} -{% endfor %} diff --git a/uniffi_bindgen/src/scaffolding/templates/macros.rs b/uniffi_bindgen/src/scaffolding/templates/macros.rs index cc4d5a99a4..8b1f94cd1c 100644 --- a/uniffi_bindgen/src/scaffolding/templates/macros.rs +++ b/uniffi_bindgen/src/scaffolding/templates/macros.rs @@ -1,42 +1,6 @@ {# // Template to receive calls into rust. -#} -{%- macro to_rs_call(func) -%} -r#{{ func.name() }}({% call _arg_list_rs_call(func) -%}) -{%- endmacro -%} - -{%- macro _arg_list_rs_call(func) %} - {%- for arg in func.full_arguments() %} - match {{- arg.as_type().borrow()|ffi_trait("Lift") }}::try_lift(r#{{ arg.name() }}) { - {%- if arg.by_ref() %} - {# args passed by reference get special treatment for traits and their Box<> #} - {%- if arg.is_trait_ref() %} - Ok(ref val) => &**val, - {%- else %} - Ok(ref val) => val, - {%- endif %} - {%- else %} - {# args not passed by reference get passed directly #} - Ok(val) => val, - {%- endif %} - - {#- If this function returns an error, we attempt to downcast errors doing arg - conversions to this error. If the downcast fails or the function doesn't - return an error, we just panic. - -#} - {%- match func.throws_type() -%} - {%- when Some with (e) %} - Err(err) => return Err(uniffi::lower_anyhow_error_or_panic::(err, "{{ arg.name() }}")), - {%- else %} - Err(err) => panic!("Failed to convert arg '{}': {}", "{{ arg.name() }}", err), - {%- endmatch %} - } - {%- if !loop.last %},{% endif %} - {%- endfor %} -{%- endmacro -%} - -{#- // Arglist as used in the _UniFFILib function declarations. // Note unfiltered name but type_ffi filters. -#} @@ -62,22 +26,3 @@ r#{{ func.name() }}({% call _arg_list_rs_call(func) -%}) {%- else -%} {%- endmatch -%} {%- endmacro -%} - -{%- macro method_decl_prelude(meth) %} -#[doc(hidden)] -#[no_mangle] -#[allow(clippy::let_unit_value,clippy::unit_arg)] // The generated code uses the unit type like other types to keep things uniform -pub extern "C" fn r#{{ meth.ffi_func().name() }}( - {%- call arg_list_ffi_decl(meth.ffi_func()) %} -) {% call return_signature(meth) %} { - uniffi::deps::log::debug!("{{ meth.ffi_func().name() }}"); - uniffi::rust_call(call_status, || { - <{{ meth|return_type }} as ::uniffi::LowerReturn>::lower_return( -{%- endmacro %} - -{%- macro method_decl_postscript(meth) %} - {% if meth.throws() %}.map_err(Into::into){% endif %} - ) - }) -} -{% endmacro %} diff --git a/uniffi_core/src/metadata.rs b/uniffi_core/src/metadata.rs index e8af045a0c..770d2b36d5 100644 --- a/uniffi_core/src/metadata.rs +++ b/uniffi_core/src/metadata.rs @@ -38,6 +38,7 @@ pub mod codes { pub const UDL_FILE: u8 = 8; pub const CALLBACK_INTERFACE: u8 = 9; pub const TRAIT_METHOD: u8 = 10; + pub const UNIFFI_TRAIT: u8 = 11; pub const UNKNOWN: u8 = 255; // Type codes diff --git a/uniffi_macros/src/export.rs b/uniffi_macros/src/export.rs index 03fb2471ec..bbb16acf90 100644 --- a/uniffi_macros/src/export.rs +++ b/uniffi_macros/src/export.rs @@ -10,10 +10,13 @@ mod attributes; mod callback_interface; mod item; mod scaffolding; +mod utrait; use self::{ item::{ExportItem, ImplItem}, - scaffolding::{gen_constructor_scaffolding, gen_fn_scaffolding, gen_method_scaffolding}, + scaffolding::{ + gen_constructor_scaffolding, gen_ffi_function, gen_fn_scaffolding, gen_method_scaffolding, + }, }; use crate::{ object::interface_meta_static_var, @@ -169,6 +172,13 @@ pub(crate) fn expand_export( #(#metadata_items)* }) } + ExportItem::Struct { + self_ident, + uniffi_traits, + } => { + assert!(!udl_mode); + utrait::expand_uniffi_trait_export(self_ident, uniffi_traits) + } } } diff --git a/uniffi_macros/src/export/attributes.rs b/uniffi_macros/src/export/attributes.rs index 92b79cfe3b..c3edcd5920 100644 --- a/uniffi_macros/src/export/attributes.rs +++ b/uniffi_macros/src/export/attributes.rs @@ -12,6 +12,11 @@ pub struct ExportAttributeArguments { pub(crate) async_runtime: Option, pub(crate) callback_interface: Option, pub(crate) constructor: Option, + // tried to make this a vec but that got messy quickly... + pub(crate) trait_debug: Option, + pub(crate) trait_display: Option, + pub(crate) trait_hash: Option, + pub(crate) trait_eq: Option, } impl Parse for ExportAttributeArguments { @@ -40,6 +45,26 @@ impl UniffiAttributeArgs for ExportAttributeArguments { constructor: input.parse()?, ..Self::default() }) + } else if lookahead.peek(kw::Debug) { + Ok(Self { + trait_debug: input.parse()?, + ..Self::default() + }) + } else if lookahead.peek(kw::Display) { + Ok(Self { + trait_display: input.parse()?, + ..Self::default() + }) + } else if lookahead.peek(kw::Hash) { + Ok(Self { + trait_hash: input.parse()?, + ..Self::default() + }) + } else if lookahead.peek(kw::Eq) { + Ok(Self { + trait_eq: input.parse()?, + ..Self::default() + }) } else { Ok(Self::default()) } @@ -53,6 +78,10 @@ impl UniffiAttributeArgs for ExportAttributeArguments { other.callback_interface, )?, constructor: either_attribute_arg(self.constructor, other.constructor)?, + trait_debug: either_attribute_arg(self.trait_debug, other.trait_debug)?, + trait_display: either_attribute_arg(self.trait_display, other.trait_display)?, + trait_hash: either_attribute_arg(self.trait_hash, other.trait_hash)?, + trait_eq: either_attribute_arg(self.trait_eq, other.trait_eq)?, }) } } diff --git a/uniffi_macros/src/export/item.rs b/uniffi_macros/src/export/item.rs index f0d68048a2..98c7d0ebe2 100644 --- a/uniffi_macros/src/export/item.rs +++ b/uniffi_macros/src/export/item.rs @@ -7,6 +7,7 @@ use proc_macro2::{Ident, Span}; use quote::ToTokens; use super::attributes::{ExportAttributeArguments, ExportedImplFnAttributes}; +use uniffi_meta::UniffiTraitDiscriminants; pub(super) enum ExportItem { Function { @@ -21,6 +22,10 @@ pub(super) enum ExportItem { items: Vec, callback_interface: bool, }, + Struct { + self_ident: Ident, + uniffi_traits: Vec, + }, } impl ExportItem { @@ -32,6 +37,7 @@ impl ExportItem { } syn::Item::Impl(item) => Self::from_impl(item, args.constructor.is_some()), syn::Item::Trait(item) => Self::from_trait(item, args.callback_interface.is_some()), + syn::Item::Struct(item) => Self::from_struct(item, args), // FIXME: Support const / static? _ => Err(syn::Error::new( Span::call_site(), @@ -150,6 +156,26 @@ impl ExportItem { callback_interface, }) } + + fn from_struct(item: syn::ItemStruct, args: &ExportAttributeArguments) -> syn::Result { + let mut uniffi_traits = Vec::new(); + if args.trait_debug.is_some() { + uniffi_traits.push(UniffiTraitDiscriminants::Debug); + } + if args.trait_display.is_some() { + uniffi_traits.push(UniffiTraitDiscriminants::Display); + } + if args.trait_hash.is_some() { + uniffi_traits.push(UniffiTraitDiscriminants::Hash); + } + if args.trait_eq.is_some() { + uniffi_traits.push(UniffiTraitDiscriminants::Eq); + } + Ok(Self::Struct { + self_ident: item.ident, + uniffi_traits, + }) + } } pub(super) enum ImplItem { diff --git a/uniffi_macros/src/export/scaffolding.rs b/uniffi_macros/src/export/scaffolding.rs index acc8c44c4c..f120ccc880 100644 --- a/uniffi_macros/src/export/scaffolding.rs +++ b/uniffi_macros/src/export/scaffolding.rs @@ -186,7 +186,7 @@ impl ScaffoldingBits { /// /// `pre_fn_call` is the statements that we should execute before the rust call /// `rust_fn` is the Rust function to call. -fn gen_ffi_function( +pub(super) fn gen_ffi_function( sig: &FnSignature, arguments: &ExportAttributeArguments, udl_mode: bool, diff --git a/uniffi_macros/src/export/utrait.rs b/uniffi_macros/src/export/utrait.rs new file mode 100644 index 0000000000..3db09ea2b7 --- /dev/null +++ b/uniffi_macros/src/export/utrait.rs @@ -0,0 +1,168 @@ +/* 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 proc_macro2::{Ident, TokenStream}; +use quote::quote; +use syn::ext::IdentExt; + +use super::{attributes::ExportAttributeArguments, gen_ffi_function}; +use crate::fnsig::FnSignature; +use uniffi_meta::UniffiTraitDiscriminants; + +pub(crate) fn expand_uniffi_trait_export( + self_ident: Ident, + uniffi_traits: Vec, +) -> syn::Result { + let udl_mode = false; + let mut impl_items = Vec::new(); + let mut global_items = Vec::new(); + for trait_id in uniffi_traits { + match trait_id { + UniffiTraitDiscriminants::Debug => { + let method = quote! { + fn uniffi_trait_debug(&self) -> String { + ::uniffi::deps::static_assertions::assert_impl_all!(#self_ident: ::std::fmt::Debug); + format!("{:?}", self) + } + }; + let (ffi_func, method_meta) = + process_uniffi_trait_method(&method, &self_ident, udl_mode)?; + // metadata for the trait - which includes metadata for the method. + let discr = UniffiTraitDiscriminants::Debug as u8; + let trait_meta = crate::util::create_metadata_items( + "uniffi_trait", + &format!("{}_Debug", self_ident.unraw()), + quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::UNIFFI_TRAIT) + .concat_value(#discr) + .concat(#method_meta) + }, + None, + ); + impl_items.push(method); + global_items.push(ffi_func); + global_items.push(trait_meta); + } + UniffiTraitDiscriminants::Display => { + let method = quote! { + fn uniffi_trait_display(&self) -> String { + ::uniffi::deps::static_assertions::assert_impl_all!(#self_ident: ::std::fmt::Display); + format!("{}", self) + } + }; + let (ffi_func, method_meta) = + process_uniffi_trait_method(&method, &self_ident, udl_mode)?; + // metadata for the trait - which includes metadata for the method. + let discr = UniffiTraitDiscriminants::Display as u8; + let trait_meta = crate::util::create_metadata_items( + "uniffi_trait", + &format!("{}_Display", self_ident.unraw()), + quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::UNIFFI_TRAIT) + .concat_value(#discr) + .concat(#method_meta) + }, + None, + ); + impl_items.push(method); + global_items.push(ffi_func); + global_items.push(trait_meta); + } + UniffiTraitDiscriminants::Hash => { + let method = quote! { + fn uniffi_trait_hash(&self) -> u64 { + use ::std::hash::{Hash, Hasher}; + ::uniffi::deps::static_assertions::assert_impl_all!(#self_ident: Hash); + let mut s = ::std::collections::hash_map::DefaultHasher::new(); + Hash::hash(self, &mut s); + s.finish() + } + }; + let (ffi_func, method_meta) = + process_uniffi_trait_method(&method, &self_ident, udl_mode)?; + // metadata for the trait - which includes metadata for the hash method. + let discr = UniffiTraitDiscriminants::Hash as u8; + let trait_meta = crate::util::create_metadata_items( + "uniffi_trait", + &format!("{}_Hash", self_ident.unraw()), + quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::UNIFFI_TRAIT) + .concat_value(#discr) + .concat(#method_meta) + }, + None, + ); + impl_items.push(method); + global_items.push(ffi_func); + global_items.push(trait_meta); + } + UniffiTraitDiscriminants::Eq => { + let method_eq = quote! { + fn uniffi_trait_eq_eq(&self, other: &#self_ident) -> bool { + use ::std::cmp::PartialEq; + uniffi::deps::static_assertions::assert_impl_all!(#self_ident: PartialEq); // This object has a trait method which requires `PartialEq` be implemented. + PartialEq::eq(self, other) + } + }; + let method_ne = quote! { + fn uniffi_trait_eq_ne(&self, other: &#self_ident) -> bool { + use ::std::cmp::PartialEq; + uniffi::deps::static_assertions::assert_impl_all!(#self_ident: PartialEq); // This object has a trait method which requires `PartialEq` be implemented. + PartialEq::ne(self, other) + } + }; + let (ffi_func_eq, method_meta_eq) = + process_uniffi_trait_method(&method_eq, &self_ident, udl_mode)?; + let (ffi_func_ne, method_meta_ne) = + process_uniffi_trait_method(&method_ne, &self_ident, udl_mode)?; + // metadata for the trait itself. + let discr = UniffiTraitDiscriminants::Eq as u8; + let trait_meta = crate::util::create_metadata_items( + "uniffi_trait", + &format!("{}_Eq", self_ident.unraw()), + quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::UNIFFI_TRAIT) + .concat_value(#discr) + .concat(#method_meta_eq) + .concat(#method_meta_ne) + }, + None, + ); + impl_items.push(method_eq); + impl_items.push(method_ne); + global_items.push(ffi_func_eq); + global_items.push(ffi_func_ne); + global_items.push(trait_meta); + } + } + } + Ok(quote! { + #[doc(hidden)] + impl #self_ident { + #(#impl_items)* + } + #(#global_items)* + }) +} + +fn process_uniffi_trait_method( + method: &TokenStream, + self_ident: &Ident, + udl_mode: bool, +) -> syn::Result<(TokenStream, TokenStream)> { + let item = syn::parse(method.clone().into())?; + + let syn::Item::Fn(item) = item else { + unreachable!() + }; + + let ffi_func = gen_ffi_function( + &FnSignature::new_method(self_ident.clone(), item.sig.clone())?, + &ExportAttributeArguments::default(), + udl_mode, + )?; + // metadata for the method, which will be packed inside metadata for the trait. + let method_meta = FnSignature::new_method(self_ident.clone(), item.sig)?.metadata_expr()?; + Ok((ffi_func, method_meta)) +} diff --git a/uniffi_macros/src/fnsig.rs b/uniffi_macros/src/fnsig.rs index f0090f1256..69b6529d1e 100644 --- a/uniffi_macros/src/fnsig.rs +++ b/uniffi_macros/src/fnsig.rs @@ -187,7 +187,7 @@ impl FnSignature { } /// Generate metadata items for this function - pub(crate) fn metadata_items(&self) -> syn::Result { + pub(crate) fn metadata_expr(&self) -> syn::Result { let Self { name, return_ty, @@ -204,18 +204,67 @@ impl FnSignature { let arg_metadata_calls = self.args.iter().map(NamedArg::arg_metadata); match &self.kind { - FnKind::Function => Ok(create_metadata_items( - "func", - name, - quote! { - ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::FUNC) + FnKind::Function => Ok(quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::FUNC) + .concat_str(#mod_path) + .concat_str(#name) + .concat_bool(#is_async) + .concat_value(#args_len) + #(#arg_metadata_calls)* + .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) + }), + + FnKind::Method { self_ident } => { + let object_name = ident_to_string(self_ident); + Ok(quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::METHOD) .concat_str(#mod_path) + .concat_str(#object_name) .concat_str(#name) .concat_bool(#is_async) .concat_value(#args_len) #(#arg_metadata_calls)* .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) - }, + }) + } + + FnKind::TraitMethod { self_ident, index } => { + let object_name = ident_to_string(self_ident); + Ok(quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TRAIT_METHOD) + .concat_str(#mod_path) + .concat_str(#object_name) + .concat_u32(#index) + .concat_str(#name) + .concat_bool(#is_async) + .concat_value(#args_len) + #(#arg_metadata_calls)* + .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) + }) + } + + FnKind::Constructor { self_ident } => { + let object_name = ident_to_string(self_ident); + Ok(quote! { + ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::CONSTRUCTOR) + .concat_str(#mod_path) + .concat_str(#object_name) + .concat_str(#name) + .concat_value(#args_len) + #(#arg_metadata_calls)* + .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) + }) + } + } + } + + pub(crate) fn metadata_items(&self) -> syn::Result { + let Self { name, .. } = &self; + match &self.kind { + FnKind::Function => Ok(create_metadata_items( + "func", + name, + self.metadata_expr()?, Some(self.checksum_symbol_name()), )), @@ -224,36 +273,17 @@ impl FnSignature { Ok(create_metadata_items( "method", &format!("{object_name}_{name}"), - quote! { - ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::METHOD) - .concat_str(#mod_path) - .concat_str(#object_name) - .concat_str(#name) - .concat_bool(#is_async) - .concat_value(#args_len) - #(#arg_metadata_calls)* - .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) - }, + self.metadata_expr()?, Some(self.checksum_symbol_name()), )) } - FnKind::TraitMethod { self_ident, index } => { + FnKind::TraitMethod { self_ident, .. } => { let object_name = ident_to_string(self_ident); Ok(create_metadata_items( "method", &format!("{object_name}_{name}"), - quote! { - ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TRAIT_METHOD) - .concat_str(#mod_path) - .concat_str(#object_name) - .concat_u32(#index) - .concat_str(#name) - .concat_bool(#is_async) - .concat_value(#args_len) - #(#arg_metadata_calls)* - .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) - }, + self.metadata_expr()?, Some(self.checksum_symbol_name()), )) } @@ -263,15 +293,7 @@ impl FnSignature { Ok(create_metadata_items( "constructor", &format!("{object_name}_{name}"), - quote! { - ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::CONSTRUCTOR) - .concat_str(#mod_path) - .concat_str(#object_name) - .concat_str(#name) - .concat_value(#args_len) - #(#arg_metadata_calls)* - .concat(<#return_ty as ::uniffi::LowerReturn>::TYPE_ID_META) - }, + self.metadata_expr()?, Some(self.checksum_symbol_name()), )) } diff --git a/uniffi_macros/src/util.rs b/uniffi_macros/src/util.rs index 8159e7a88f..e33c4bc505 100644 --- a/uniffi_macros/src/util.rs +++ b/uniffi_macros/src/util.rs @@ -233,6 +233,10 @@ pub mod kw { syn::custom_keyword!(flat_error); syn::custom_keyword!(None); syn::custom_keyword!(with_try_read); + syn::custom_keyword!(Debug); + syn::custom_keyword!(Display); + syn::custom_keyword!(Eq); + syn::custom_keyword!(Hash); // Not used anymore syn::custom_keyword!(handle_unknown_callback_error); } diff --git a/uniffi_meta/src/lib.rs b/uniffi_meta/src/lib.rs index 23d8fde418..e486d84d89 100644 --- a/uniffi_meta/src/lib.rs +++ b/uniffi_meta/src/lib.rs @@ -310,7 +310,6 @@ pub struct ObjectMetadata { pub module_path: String, pub name: String, pub imp: types::ObjectImpl, - pub uniffi_traits: Vec, } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -346,6 +345,48 @@ pub enum UniffiTraitMetadata { }, } +impl UniffiTraitMetadata { + fn module_path(&self) -> &String { + &match self { + UniffiTraitMetadata::Debug { fmt } => fmt, + UniffiTraitMetadata::Display { fmt } => fmt, + UniffiTraitMetadata::Eq { eq, .. } => eq, + UniffiTraitMetadata::Hash { hash } => hash, + } + .module_path + } + + pub fn self_name(&self) -> &String { + &match self { + UniffiTraitMetadata::Debug { fmt } => fmt, + UniffiTraitMetadata::Display { fmt } => fmt, + UniffiTraitMetadata::Eq { eq, .. } => eq, + UniffiTraitMetadata::Hash { hash } => hash, + } + .self_name + } +} + +#[repr(u8)] +pub enum UniffiTraitDiscriminants { + Debug, + Display, + Eq, + Hash, +} + +impl UniffiTraitDiscriminants { + pub fn from(v: u8) -> anyhow::Result { + Ok(match v { + 0 => UniffiTraitDiscriminants::Debug, + 1 => UniffiTraitDiscriminants::Display, + 2 => UniffiTraitDiscriminants::Eq, + 3 => UniffiTraitDiscriminants::Hash, + _ => anyhow::bail!("invalid trait discriminant {v}"), + }) + } +} + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum ErrorMetadata { Enum { enum_: EnumMetadata, is_flat: bool }, @@ -397,6 +438,7 @@ pub enum Metadata { Method(MethodMetadata), TraitMethod(TraitMethodMetadata), CustomType(CustomTypeMetadata), + UniffiTrait(UniffiTraitMetadata), } impl Metadata { @@ -418,6 +460,7 @@ impl Metadata { Metadata::TraitMethod(meta) => &meta.module_path, Metadata::Error(meta) => meta.module_path(), Metadata::CustomType(meta) => &meta.module_path, + Metadata::UniffiTrait(meta) => meta.module_path(), } } } @@ -493,3 +536,9 @@ impl From for Metadata { Self::CustomType(v) } } + +impl From for Metadata { + fn from(v: UniffiTraitMetadata) -> Self { + Self::UniffiTrait(v) + } +} diff --git a/uniffi_meta/src/metadata.rs b/uniffi_meta/src/metadata.rs index 5b0bcaae13..6e490a4866 100644 --- a/uniffi_meta/src/metadata.rs +++ b/uniffi_meta/src/metadata.rs @@ -21,6 +21,7 @@ pub mod codes { pub const UDL_FILE: u8 = 8; pub const CALLBACK_INTERFACE: u8 = 9; pub const TRAIT_METHOD: u8 = 10; + pub const UNIFFI_TRAIT: u8 = 11; //pub const UNKNOWN: u8 = 255; // Type codes diff --git a/uniffi_meta/src/reader.rs b/uniffi_meta/src/reader.rs index 8cbe8d021e..bf6525f2b5 100644 --- a/uniffi_meta/src/reader.rs +++ b/uniffi_meta/src/reader.rs @@ -57,6 +57,7 @@ impl<'a> MetadataReader<'a> { codes::INTERFACE => self.read_object()?.into(), codes::CALLBACK_INTERFACE => self.read_callback_interface()?.into(), codes::TRAIT_METHOD => self.read_trait_method()?.into(), + codes::UNIFFI_TRAIT => self.read_uniffi_trait()?.into(), _ => bail!("Unexpected metadata code: {value:?}"), }) } @@ -289,7 +290,31 @@ impl<'a> MetadataReader<'a> { module_path: self.read_string()?, name: self.read_string()?, imp: ObjectImpl::from_is_trait(self.read_bool()?), - uniffi_traits: vec![], // TODO: not yet emitted + }) + } + + fn read_uniffi_trait(&mut self) -> Result { + let code = self.read_u8()?; + let mut read_metadata_method = || -> Result { + let code = self.read_u8()?; + ensure!(code == codes::METHOD, "expected METHOD but read {code}"); + self.read_method() + }; + + Ok(match UniffiTraitDiscriminants::from(code)? { + UniffiTraitDiscriminants::Debug => UniffiTraitMetadata::Debug { + fmt: read_metadata_method()?, + }, + UniffiTraitDiscriminants::Display => UniffiTraitMetadata::Display { + fmt: read_metadata_method()?, + }, + UniffiTraitDiscriminants::Eq => UniffiTraitMetadata::Eq { + eq: read_metadata_method()?, + ne: read_metadata_method()?, + }, + UniffiTraitDiscriminants::Hash => UniffiTraitMetadata::Hash { + hash: read_metadata_method()?, + }, }) } diff --git a/uniffi_udl/src/converters/interface.rs b/uniffi_udl/src/converters/interface.rs index 5c77168f87..58e6a9c8a0 100644 --- a/uniffi_udl/src/converters/interface.rs +++ b/uniffi_udl/src/converters/interface.rs @@ -123,11 +123,13 @@ impl APIConverter for weedle::InterfaceDefinition<'_> { }) }) .collect::>>()?; + for ut in uniffi_traits { + ci.items.insert(ut.into()); + } Ok(ObjectMetadata { module_path: ci.module_path(), name: object_name.to_string(), imp: object_impl, - uniffi_traits, }) } } From 14547aed4d36813d86c2219e027048ea393109fe Mon Sep 17 00:00:00 2001 From: Kristupas Antanavicius Date: Thu, 5 Oct 2023 16:53:31 +0300 Subject: [PATCH 14/24] Allow library mode to be used by external generators (#1780) --- .../src/bindings/kotlin/gen_kotlin/mod.rs | 2 - .../src/bindings/python/gen_python/mod.rs | 2 - .../src/bindings/ruby/gen_ruby/mod.rs | 2 - .../src/bindings/swift/gen_swift/mod.rs | 2 - uniffi_bindgen/src/lib.rs | 143 ++++++++---------- uniffi_bindgen/src/library_mode.rs | 51 ++++--- 6 files changed, 95 insertions(+), 107 deletions(-) diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs index 0008916b7d..2461b590d3 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs @@ -66,8 +66,6 @@ impl Config { } impl BindingsConfig for Config { - const TOML_KEY: &'static str = "kotlin"; - fn update_from_ci(&mut self, ci: &ComponentInterface) { self.package_name .get_or_insert_with(|| format!("uniffi.{}", ci.namespace())); diff --git a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs index 0fd99cab02..78a59c1b33 100644 --- a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs +++ b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs @@ -97,8 +97,6 @@ impl Config { } impl BindingsConfig for Config { - const TOML_KEY: &'static str = "python"; - fn update_from_ci(&mut self, ci: &ComponentInterface) { self.cdylib_name .get_or_insert_with(|| format!("uniffi_{}", ci.namespace())); diff --git a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs index 1f7260d00b..1f1bf8e299 100644 --- a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs +++ b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs @@ -104,8 +104,6 @@ impl Config { } impl BindingsConfig for Config { - const TOML_KEY: &'static str = "ruby"; - fn update_from_ci(&mut self, ci: &ComponentInterface) { self.cdylib_name .get_or_insert_with(|| format!("uniffi_{}", ci.namespace())); diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs index 0855bcb282..ec38ec11c8 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs @@ -204,8 +204,6 @@ impl Config { } impl BindingsConfig for Config { - const TOML_KEY: &'static str = "swift"; - fn update_from_ci(&mut self, ci: &ComponentInterface) { self.module_name .get_or_insert_with(|| ci.namespace().into()); diff --git a/uniffi_bindgen/src/lib.rs b/uniffi_bindgen/src/lib.rs index aecffc81a4..4e24f55f10 100644 --- a/uniffi_bindgen/src/lib.rs +++ b/uniffi_bindgen/src/lib.rs @@ -98,7 +98,7 @@ use fs_err::{self as fs, File}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::io::prelude::*; use std::io::ErrorKind; -use std::{collections::HashMap, process::Command, str::FromStr}; +use std::{collections::HashMap, process::Command}; pub mod backend; pub mod bindings; @@ -116,9 +116,6 @@ use scaffolding::RustScaffolding; /// BindingsConfigs are initially loaded from `uniffi.toml` file. Then the trait methods are used /// to fill in missing values. pub trait BindingsConfig: DeserializeOwned { - /// key in the `bindings` table from `uniffi.toml` for this configuration - const TOML_KEY: &'static str; - /// Update missing values using the `ComponentInterface` fn update_from_ci(&mut self, ci: &ComponentInterface); @@ -134,63 +131,16 @@ pub trait BindingsConfig: DeserializeOwned { fn update_from_dependency_configs(&mut self, config_map: HashMap<&str, &Self>); } -fn load_bindings_config( - ci: &ComponentInterface, - crate_root: &Utf8Path, - config_file_override: Option<&Utf8Path>, -) -> Result { - // Load the config from the TOML value, falling back to an empty map if it doesn't exist - let toml_config = load_bindings_config_toml(crate_root, config_file_override)? - .and_then(|mut v| v.as_table_mut().and_then(|t| t.remove(BC::TOML_KEY))) - .unwrap_or_else(|| toml::Value::from(toml::value::Table::default())); - - let mut config: BC = toml_config.try_into()?; - config.update_from_ci(ci); - Ok(config) -} - /// Binding generator config with no members #[derive(Clone, Debug, Deserialize, Hash, PartialEq, PartialOrd, Ord, Eq)] pub struct EmptyBindingsConfig; impl BindingsConfig for EmptyBindingsConfig { - const TOML_KEY: &'static str = ""; - fn update_from_ci(&mut self, _ci: &ComponentInterface) {} fn update_from_cdylib_name(&mut self, _cdylib_name: &str) {} fn update_from_dependency_configs(&mut self, _config_map: HashMap<&str, &Self>) {} } -// Load the binding-specific config -// -// This function calculates the location of the config TOML file, parses it, and returns the result -// as a toml::Value -// -// If there is an error parsing the file then Err will be returned. If the file is missing or the -// entry for the bindings is missing, then Ok(None) will be returned. -fn load_bindings_config_toml( - crate_root: &Utf8Path, - config_file_override: Option<&Utf8Path>, -) -> Result> { - let config_path = match config_file_override { - Some(cfg) => cfg.to_owned(), - None => crate_root.join("uniffi.toml"), - }; - - if !config_path.exists() { - return Ok(None); - } - - let contents = fs::read_to_string(&config_path) - .with_context(|| format!("Failed to read config file from {config_path}"))?; - let mut full_config = toml::Value::from_str(&contents) - .with_context(|| format!("Failed to parse config file {config_path}"))?; - - Ok(full_config - .as_table_mut() - .and_then(|t| t.remove("bindings"))) -} - /// A trait representing a UniFFI Binding Generator /// /// External crates that implement binding generators, should implement this type @@ -207,10 +157,49 @@ pub trait BindingGenerator: Sized { /// - `out_dir`: The path to where the binding generator should write the output bindings fn write_bindings( &self, - ci: ComponentInterface, - config: Self::Config, + ci: &ComponentInterface, + config: &Self::Config, out_dir: &Utf8Path, ) -> Result<()>; + + /// Check if `library_path` used by library mode is valid for this generator + fn check_library_path(&self, library_path: &Utf8Path, cdylib_name: Option<&str>) -> Result<()>; +} + +struct BindingGeneratorDefault { + target_languages: Vec, + try_format_code: bool, +} + +impl BindingGenerator for BindingGeneratorDefault { + type Config = Config; + + fn write_bindings( + &self, + ci: &ComponentInterface, + config: &Self::Config, + out_dir: &Utf8Path, + ) -> Result<()> { + for &language in &self.target_languages { + bindings::write_bindings( + &config.bindings, + ci, + out_dir, + language, + self.try_format_code, + )?; + } + Ok(()) + } + + fn check_library_path(&self, library_path: &Utf8Path, cdylib_name: Option<&str>) -> Result<()> { + for &language in &self.target_languages { + if cdylib_name.is_none() && language != TargetLanguage::Swift { + bail!("Generate bindings for {language} requires a cdylib, but {library_path} was given"); + } + } + Ok(()) + } } /// Generate bindings for an external binding generator @@ -243,13 +232,9 @@ pub fn generate_external_bindings( let crate_root = &guess_crate_root(udl_file.as_ref()).context("Failed to guess crate root")?; let config_file_override = config_file_override.as_ref().map(|p| p.as_ref()); - let mut config = - load_bindings_config::(&component, crate_root, config_file_override)?; - config.update_from_ci(&component); let config = { - let mut config = - load_bindings_config::(&component, crate_root, config_file_override)?; + let mut config = load_initial_config::(crate_root, config_file_override)?; config.update_from_ci(&component); if let Some(ref library_file) = library_file { if let Some(cdylib_name) = crate::library_mode::calc_cdylib_name(library_file.as_ref()) @@ -264,7 +249,7 @@ pub fn generate_external_bindings( udl_file.as_ref(), out_dir_override.as_ref().map(|p| p.as_ref()), )?; - binding_generator.write_bindings(component, config, &out_dir) + binding_generator.write_bindings(&component, &config, &out_dir) } // Generate the infrastructural Rust code for implementing the UDL interface, @@ -330,7 +315,7 @@ pub fn generate_bindings( } let crate_root = &guess_crate_root(udl_file).context("Failed to guess crate root")?; - let mut config = Config::load_initial(crate_root, config_file_override)?; + let mut config = load_initial_config::(crate_root, config_file_override)?; config.update_from_ci(&component); let out_dir = get_out_dir(udl_file, out_dir_override)?; for language in target_languages { @@ -440,31 +425,31 @@ fn format_code_with_rustfmt(path: &Utf8Path) -> Result<()> { Ok(()) } +fn load_initial_config( + crate_root: &Utf8Path, + config_file_override: Option<&Utf8Path>, +) -> Result { + let path = match config_file_override { + Some(cfg) => Some(cfg.to_owned()), + None => crate_root.join("uniffi.toml").canonicalize_utf8().ok(), + }; + let toml_config = match path { + Some(path) => { + let contents = fs::read_to_string(path).context("Failed to read config file")?; + toml::de::from_str(&contents)? + } + None => toml::Value::from(toml::value::Table::default()), + }; + Ok(toml_config.try_into()?) +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Config { #[serde(default)] bindings: bindings::Config, } -impl Config { - fn load_initial( - crate_root: &Utf8Path, - config_file_override: Option<&Utf8Path>, - ) -> Result { - let path = match config_file_override { - Some(cfg) => Some(cfg.to_owned()), - None => crate_root.join("uniffi.toml").canonicalize_utf8().ok(), - }; - let toml_config = match path { - Some(path) => { - let contents = fs::read_to_string(path).context("Failed to read config file")?; - toml::de::from_str(&contents)? - } - None => toml::Value::from(toml::value::Table::default()), - }; - Ok(toml_config.try_into()?) - } - +impl BindingsConfig for Config { fn update_from_ci(&mut self, ci: &ComponentInterface) { self.bindings.kotlin.update_from_ci(ci); self.bindings.swift.update_from_ci(ci); diff --git a/uniffi_bindgen/src/library_mode.rs b/uniffi_bindgen/src/library_mode.rs index 488d2e98fc..f170ea5e91 100644 --- a/uniffi_bindgen/src/library_mode.rs +++ b/uniffi_bindgen/src/library_mode.rs @@ -16,8 +16,8 @@ /// - UniFFI can figure out the package/module names for each crate, eliminating the external /// package maps. use crate::{ - bindings::{self, TargetLanguage}, - macro_metadata, ComponentInterface, Config, Result, + bindings::TargetLanguage, load_initial_config, macro_metadata, BindingGenerator, + BindingGeneratorDefault, BindingsConfig, ComponentInterface, Result, }; use anyhow::{bail, Context}; use camino::Utf8Path; @@ -39,11 +39,33 @@ pub fn generate_bindings( target_languages: &[TargetLanguage], out_dir: &Utf8Path, try_format_code: bool, -) -> Result> { +) -> Result>> { + generate_external_bindings( + BindingGeneratorDefault { + target_languages: target_languages.into(), + try_format_code, + }, + library_path, + crate_name, + out_dir, + ) +} + +/// Generate foreign bindings +/// +/// Returns the list of sources used to generate the bindings, in no particular order. +pub fn generate_external_bindings( + binding_generator: T, + library_path: &Utf8Path, + crate_name: Option, + out_dir: &Utf8Path, +) -> Result>> { let cargo_metadata = MetadataCommand::new() .exec() .context("error running cargo metadata")?; let cdylib_name = calc_cdylib_name(library_path); + binding_generator.check_library_path(library_path, cdylib_name)?; + let mut sources = find_sources(&cargo_metadata, library_path, cdylib_name)?; for i in 0..sources.len() { // Partition up the sources list because we're eventually going to call @@ -55,7 +77,7 @@ pub fn generate_bindings( // Calculate which configs come from dependent crates let dependencies = HashSet::<&str>::from_iter(source.package.dependencies.iter().map(|d| d.name.as_str())); - let config_map: HashMap<&str, &Config> = other_sources + let config_map: HashMap<&str, &T::Config> = other_sources .filter_map(|s| { dependencies .contains(s.package.name.as_str()) @@ -79,18 +101,7 @@ pub fn generate_bindings( } for source in sources.iter() { - for &language in target_languages { - if cdylib_name.is_none() && language != TargetLanguage::Swift { - bail!("Generate bindings for {language} requires a cdylib, but {library_path} was given"); - } - bindings::write_bindings( - &source.config.bindings, - &source.ci, - out_dir, - language, - try_format_code, - )?; - } + binding_generator.write_bindings(&source.ci, &source.config, out_dir)?; } Ok(sources) @@ -98,7 +109,7 @@ pub fn generate_bindings( // A single source that we generate bindings for #[derive(Debug)] -pub struct Source { +pub struct Source { pub package: Package, pub crate_name: String, pub ci: ComponentInterface, @@ -118,11 +129,11 @@ pub fn calc_cdylib_name(library_path: &Utf8Path) -> Option<&str> { None } -fn find_sources( +fn find_sources( cargo_metadata: &cargo_metadata::Metadata, library_path: &Utf8Path, cdylib_name: Option<&str>, -) -> Result> { +) -> Result>> { let items = macro_metadata::extract_from_library(library_path)?; let mut metadata_groups = create_metadata_groups(&items); group_metadata(&mut metadata_groups, items)?; @@ -167,7 +178,7 @@ fn find_sources( ci.add_metadata(metadata)?; }; ci.add_metadata(group)?; - let mut config = Config::load_initial(crate_root, None)?; + let mut config = load_initial_config::(crate_root, None)?; if let Some(cdylib_name) = cdylib_name { config.update_from_cdylib_name(cdylib_name); } From b2d4714b435046ecea493726a4469ac27793c4ce Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Mon, 9 Oct 2023 13:45:12 -0400 Subject: [PATCH 15/24] Update docs re uniffi.toml and tweaks to a couple of examples (#1783) --- docs/manual/src/SUMMARY.md | 13 ++++++-- docs/manual/src/bindings.md | 16 ++++++++++ docs/manual/src/kotlin/configuration.md | 36 +++++++++++++++++++++++ docs/manual/src/python/configuration.md | 22 ++++++++++++++ docs/manual/src/swift/configuration.md | 2 ++ docs/manual/src/udl/ext_types_external.md | 3 -- examples/custom-types/src/lib.rs | 10 +++++++ examples/fxa-client/README.md | 4 +++ 8 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 docs/manual/src/bindings.md create mode 100644 docs/manual/src/kotlin/configuration.md create mode 100644 docs/manual/src/python/configuration.md create mode 100644 examples/fxa-client/README.md diff --git a/docs/manual/src/SUMMARY.md b/docs/manual/src/SUMMARY.md index fb5f37b6c5..58206070f8 100644 --- a/docs/manual/src/SUMMARY.md +++ b/docs/manual/src/SUMMARY.md @@ -22,18 +22,27 @@ - [Procedural Macros: Attributes and Derives](./proc_macro/index.md) - [Futures and async support](./futures.md) -# Kotlin +# Bindings +- [Generating bindings](./bindings) +- [Customizing binding generation](./bindings#customizing-the-binding-generation) + +## Kotlin + +- [Configuration](./kotlin/configuration) - [Integrating with Gradle](./kotlin/gradle.md) - [Kotlin Lifetimes](./kotlin/lifetimes.md) -# Swift +## Swift - [Overview](./swift/overview.md) - [Configuration](./swift/configuration.md) - [Building a Swift module](./swift/module.md) - [Integrating with Xcode](./swift/xcode.md) +## Python +- [Configuration](./python/configuration) + # Internals - [Design Principles](./internals/design_principles.md) - [Navigating the Code](./internals/crates.md) diff --git a/docs/manual/src/bindings.md b/docs/manual/src/bindings.md new file mode 100644 index 0000000000..0c379dc913 --- /dev/null +++ b/docs/manual/src/bindings.md @@ -0,0 +1,16 @@ +# Generating bindings + +Bindings is the term used for the code generates for foreign languages which integrate +with Rust crates - that is, the generated Python, Swift or Kotlin code which drives the +examples. + +UniFFI comes with a `uniffi_bindgen` which generates these bindings. For introductory +information, see [Foreign Language Bindings in the tutorial](./tutorial/foreign_language_bindings.md) + +# Customizing the binding generation. + +Each of the bindings reads a file `uniffi.toml` in the root of a crate which supports +various options which influence how the bindings are generated. Default options will be used +if this file is missing. + +Each binding supports different options, so please see the documentation for each binding language. diff --git a/docs/manual/src/kotlin/configuration.md b/docs/manual/src/kotlin/configuration.md new file mode 100644 index 0000000000..74adc09f1f --- /dev/null +++ b/docs/manual/src/kotlin/configuration.md @@ -0,0 +1,36 @@ +# Configuration + +The generated Kotlin modules can be configured using a `uniffi.toml` configuration file. + +## Available options + +| Configuration name | Default | Description | +| ------------------ | ------- |------------ | +| `package_name` | `uniffi` | The Kotlin package name - ie, the value used in the `package` statement at the top of generated files. | +| `cdylib_name` | `uniffi_{namespace}`[^1] | The name of the compiled Rust library containing the FFI implementation (not needed when using `generate --library`). | +| `custom_types` | | A map which controls how custom types are exposed to Kotlin. See the [custom types section of the manual](../udl/custom_types#custom-types-in-the-bindings-code)| +| `external_packages` | | A map of packages to be used for the specified external crates. The key is the Rust crate name, the value is the Kotlin package which will be used referring to types in that crate. See the [external types section of the manual](../udl/ext_types_external#kotlin) + + +## Example + +Custom types +```toml +# Assuming a Custom Type named URL using a String as the builtin. +[bindings.kotlin.custom_types.Url] +# Name of the type in the Kotlin code +type_name = "URL" +# Classes that need to be imported +imports = [ "java.net.URI", "java.net.URL" ] +# Functions to convert between strings and URLs +into_custom = "URI({}).toURL()" +from_custom = "{}.toString()" +``` + +External types +```toml +[bindings.kotlin.external_packages] +# This specifies that external types from the crate `rust-crate-name` will be referred by by the package `"kotlin.package.name`. +rust-crate-name = "kotlin.package.name" +``` + diff --git a/docs/manual/src/python/configuration.md b/docs/manual/src/python/configuration.md new file mode 100644 index 0000000000..cd6ff9c18c --- /dev/null +++ b/docs/manual/src/python/configuration.md @@ -0,0 +1,22 @@ +# Configuration + +The generated Python modules can be configured using a `uniffi.toml` configuration file. + +## Available options + +| Configuration name | Default | Description | +| ------------------ | ------- |------------ | +| `cdylib_name` | `uniffi_{namespace}`[^1] | The name of the compiled Rust library containing the FFI implementation (not needed when using `generate --library`). | +| `custom_types` | | A map which controls how custom types are exposed to Python. See the [custom types section of the manual](../udl/custom_types#custom-types-in-the-bindings-code)| + + +## Example + +```toml +# Assuming a Custom Type named URL using a String as the builtin. +[bindings.python.custom_types.Url] +imports = ["urllib.parse"] +# Functions to convert between strings and the ParsedUrl class +into_custom = "urllib.parse.urlparse({})" +from_custom = "urllib.parse.urlunparse({})" +``` diff --git a/docs/manual/src/swift/configuration.md b/docs/manual/src/swift/configuration.md index 794fc518e3..0c41b5cc48 100644 --- a/docs/manual/src/swift/configuration.md +++ b/docs/manual/src/swift/configuration.md @@ -12,6 +12,8 @@ The generated Swift module can be configured using a `uniffi.toml` configuration | `ffi_module_filename` | `{ffi_module_name}` | The filename stem for the lower-level C module containing the FFI declarations. | | `generate_module_map` | `true` | Whether to generate a `.modulemap` file for the lower-level C module with FFI declarations. | | `omit_argument_labels` | `false` | Whether to omit argument labels in Swift function definitions. | +| `custom_types` | | A map which controls how custom types are exposed to Swift. See the [custom types section of the manual](../udl/custom_types#custom-types-in-the-bindings-code)| + [^1]: `namespace` is the top-level namespace from your UDL file. diff --git a/docs/manual/src/udl/ext_types_external.md b/docs/manual/src/udl/ext_types_external.md index ad0b4834cd..5872a14553 100644 --- a/docs/manual/src/udl/ext_types_external.md +++ b/docs/manual/src/udl/ext_types_external.md @@ -80,9 +80,6 @@ By default, UniFFI assumes that the Kotlin module name matches the Rust crate na rust-crate-name = "kotlin.package.name" ``` -See the [`ext-types` fixture](https://github.com/mozilla/uniffi-rs/blob/main/fixtures/ext-types/lib/uniffi.toml) -for an example - ### Swift For Swift, you must compile all generated `.swift` files together in a single diff --git a/examples/custom-types/src/lib.rs b/examples/custom-types/src/lib.rs index 6a709af891..55c502cc5a 100644 --- a/examples/custom-types/src/lib.rs +++ b/examples/custom-types/src/lib.rs @@ -1,5 +1,10 @@ use url::Url; +// A custom guid defined via a proc-macro (ie, not referenced in the UDL) +// By far the easiest way to define custom types. +pub struct ExampleCustomType(String); +uniffi::custom_newtype!(ExampleCustomType, String); + // Custom Handle type which trivially wraps an i64. pub struct Handle(pub i64); @@ -108,4 +113,9 @@ pub fn get_custom_types_demo(v: Option) -> CustomTypesDemo { }) } +#[uniffi::export] +pub fn get_example_custom_type() -> ExampleCustomType { + ExampleCustomType("abadidea".to_string()) +} + uniffi::include_scaffolding!("custom-types"); diff --git a/examples/fxa-client/README.md b/examples/fxa-client/README.md new file mode 100644 index 0000000000..12f792bc31 --- /dev/null +++ b/examples/fxa-client/README.md @@ -0,0 +1,4 @@ +# Firefox Account Example. + +An old version of the UDL for a Firefox Account, as the very first UniFFI consumer. +See also [the current implementation](https://github.com/mozilla/application-services/tree/main/components/fxa-client) From e9d79bc5c8b706bac852242ca3fac18bb201b6d6 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Mon, 9 Oct 2023 14:17:34 -0400 Subject: [PATCH 16/24] Fix async wake behavior If wake() is called while we are in the middle of polling a future, then we should immediately call our continuation function to schedule another poll. --- uniffi_core/src/ffi/rustfuture.rs | 91 ++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 20 deletions(-) diff --git a/uniffi_core/src/ffi/rustfuture.rs b/uniffi_core/src/ffi/rustfuture.rs index d8b791c245..a9fd64646e 100644 --- a/uniffi_core/src/ffi/rustfuture.rs +++ b/uniffi_core/src/ffi/rustfuture.rs @@ -224,9 +224,18 @@ pub unsafe fn rust_future_free(handle: RustFutureHandle) { /// foreign continuation callback. This enables us to uphold the [rust_future_poll] guarantee. /// /// [ContinuationDataCell] also tracks cancellation, which is closely tied to continuation data. +#[derive(Debug)] enum ContinuationDataCell { + /// No continuations set, neither wake() nor cancel() called. Empty, + /// `wake()` was called when there was no continuation set. The next time `store` is called, + /// the continuation should be immediately invoked with `RustFuturePoll::MaybeReady` + Waked, + /// The future has been cancelled, any future `store` calls should immediately result in the + /// continuation being called with `RustFuturePoll::Ready`. Cancelled, + /// Continuation data set, the next time `wake()` is called is called, we should invoke the + /// continuation with it. Set(*const ()), } @@ -235,33 +244,41 @@ impl ContinuationDataCell { Self::Empty } - /// Store new continuation data + /// Store new continuation data if we are in the `Empty` state. If we are in the `Waked` or + /// `Cancelled` state, call the continuation immediately with the data. fn store(&mut self, data: *const ()) { - // If we're cancelled, then call the continuation immediately rather than storing it - if matches!(self, Self::Cancelled) { - call_continuation(data, RustFuturePoll::Ready); - return; - } - - match mem::replace(self, Self::Set(data)) { - Self::Empty => (), - Self::Cancelled => unreachable!(), + match self { + Self::Empty => *self = Self::Set(data), Self::Set(old_data) => { log::error!( - "store: observed Self::Set state, is poll() being called from multiple threads at once?" + "store: observed `Self::Set` state. Is poll() being called from multiple threads at once?" ); - call_continuation(old_data, RustFuturePoll::Ready); + call_continuation(*old_data, RustFuturePoll::Ready); + *self = Self::Set(data); + } + Self::Waked => { + *self = Self::Empty; + call_continuation(data, RustFuturePoll::MaybeReady); + } + Self::Cancelled => { + call_continuation(data, RustFuturePoll::Ready); } } } - fn send(&mut self) { - if matches!(self, Self::Cancelled) { - return; - } - - if let Self::Set(old_data) = mem::replace(self, Self::Empty) { - call_continuation(old_data, RustFuturePoll::MaybeReady); + fn wake(&mut self) { + match self { + // If we had a continuation set, then call it and transition to the `Empty` state. + Self::Set(old_data) => { + let old_data = *old_data; + *self = Self::Empty; + call_continuation(old_data, RustFuturePoll::MaybeReady); + } + // If we were in the `Empty` state, then transition to `Waked`. The next time `store` + // is called, we will immediately call the continuation. + Self::Empty => *self = Self::Waked, + // This is a no-op if we were in the `Cancelled` or `Waked` state. + _ => (), } } @@ -434,7 +451,7 @@ where } fn wake(&self) { - self.continuation_data.lock().unwrap().send(); + self.continuation_data.lock().unwrap().wake(); } fn cancel(&self) { @@ -710,4 +727,38 @@ mod tests { rust_future.ffi_free(); assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready)); } + + // Test what happens if we see a `wake()` call while we're polling the future. This can + // happen, for example, with futures that are handled by a tokio thread pool. We should + // schedule another poll of the future in this case. + #[test] + fn test_wake_during_poll() { + setup_continuation_callback(); + let mut first_time = true; + let future = std::future::poll_fn(move |ctx| { + if first_time { + first_time = false; + // Wake the future while we are in the middle of polling it + ctx.waker().clone().wake(); + Poll::Pending + } else { + // The second time we're polled, we're ready + Poll::Ready("All done".to_owned()) + } + }); + let rust_future: Arc> = + RustFuture::new(future, crate::UniFfiTag); + let continuation_result = poll(&rust_future); + // The continuation function should called immediately + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::MaybeReady)); + // A second poll should finish the future + let continuation_result = poll(&rust_future); + assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready)); + let (return_buf, call_status) = complete(rust_future); + assert_eq!(call_status.code, RustCallStatusCode::Success); + assert_eq!( + >::try_lift(return_buf).unwrap(), + "All done" + ); + } } From c890e8bb3465f17798456e2bda040d7cc3ff0f3e Mon Sep 17 00:00:00 2001 From: Kristupas Antanavicius Date: Tue, 10 Oct 2023 17:32:37 +0300 Subject: [PATCH 17/24] Normalize 'generate_bindings' with 'generate_external_bindings' (#1786) --- uniffi_bindgen/src/lib.rs | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/uniffi_bindgen/src/lib.rs b/uniffi_bindgen/src/lib.rs index 4e24f55f10..019b24022f 100644 --- a/uniffi_bindgen/src/lib.rs +++ b/uniffi_bindgen/src/lib.rs @@ -217,14 +217,18 @@ impl BindingGenerator for BindingGeneratorDefault { /// - `config_file_override`: The path to the configuration toml file, most likely called `uniffi.toml`. If [`None`], the function will try to guess based on the crate's root. /// - `out_dir_override`: The path to write the bindings to. If [`None`], it will be the path to the parent directory of the `udl_file` /// - `library_file`: The path to a dynamic library to attempt to extract the definitions from and extend the component interface with. No extensions to component interface occur if it's [`None`] +/// - `crate_name`: Override the default crate name that is guessed from UDL file path. pub fn generate_external_bindings( binding_generator: T, udl_file: impl AsRef, config_file_override: Option>, out_dir_override: Option>, library_file: Option>, + crate_name: Option<&str>, ) -> Result<()> { - let crate_name = crate_name_from_cargo_toml(udl_file.as_ref())?; + let crate_name = crate_name + .map(|c| Ok(c.to_string())) + .unwrap_or_else(|| crate_name_from_cargo_toml(udl_file.as_ref()))?; let mut component = parse_udl(udl_file.as_ref(), &crate_name)?; if let Some(ref library_file) = library_file { macro_metadata::add_to_ci_from_library(&mut component, library_file.as_ref())?; @@ -306,29 +310,17 @@ pub fn generate_bindings( crate_name: Option<&str>, try_format_code: bool, ) -> Result<()> { - let crate_name = crate_name - .map(|c| Ok(c.to_string())) - .unwrap_or_else(|| crate_name_from_cargo_toml(udl_file))?; - let mut component = parse_udl(udl_file, &crate_name)?; - if let Some(library_file) = library_file { - macro_metadata::add_to_ci_from_library(&mut component, library_file)?; - } - let crate_root = &guess_crate_root(udl_file).context("Failed to guess crate root")?; - - let mut config = load_initial_config::(crate_root, config_file_override)?; - config.update_from_ci(&component); - let out_dir = get_out_dir(udl_file, out_dir_override)?; - for language in target_languages { - bindings::write_bindings( - &config.bindings, - &component, - &out_dir, - language, + generate_external_bindings( + BindingGeneratorDefault { + target_languages, try_format_code, - )?; - } - - Ok(()) + }, + udl_file, + config_file_override, + out_dir_override, + library_file, + crate_name, + ) } pub fn print_repr(library_path: &Utf8Path) -> Result<()> { From a3403ace9ca3d324e319de59a349b3edff8d1837 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Wed, 11 Oct 2023 11:36:58 -0400 Subject: [PATCH 18/24] Make From optional This is in preparation of #1578 -- allowing foreign languages to implement traits. One issue there is that if we want to allow foreign languages to implement traits, then any Result return types need to implement `LiftReturn`, and that impl currently has a bound on `E: From` impl, but use a panicking version if not. The trait bound is no longer required to implement `LiftReturn`. --- CHANGELOG.md | 6 ++ docs/manual/src/udl/callback_interfaces.md | 17 ++--- uniffi_core/src/ffi/callbackinterface.rs | 74 ++++++++++++++++++++++ uniffi_core/src/ffi_converter_impls.rs | 8 +-- uniffi_core/src/ffi_converter_traits.rs | 15 +++++ uniffi_core/src/lib.rs | 2 +- uniffi_macros/src/custom.rs | 4 +- uniffi_macros/src/enum_.rs | 4 +- uniffi_macros/src/error.rs | 6 +- uniffi_macros/src/record.rs | 6 +- uniffi_macros/src/util.rs | 21 +++++- 11 files changed, 140 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 244953b29c..a56d07276f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,12 @@ - Updated the async functionality to correctly handle cancellation (#1669) - Kotlin: Fixed low-level issue with exported async APIs +### What's changed? + +- Implementing `From`. However, this is means that any exception from the foreign bindings will lead to a panic. -### Extra requirements for errors used in callback interfaces +### Errors used in callback interfaces In order to support errors in callback interfaces, UniFFI must be able to properly [lift the error](../internals/lifting_and_lowering.md). This means that the if the error is described by an `enum` rather than an `interface` in the UDL (see [Errors](./errors.md)) then all variants of the Rust enum must be unit variants. -In addition to expected errors, a callback interface call can result in all kinds of -unexpected errors. Some examples are the foreign code throws an exception that's not part -of the exception type or there was a problem marshalling the data for the call. UniFFI -uses `uniffi::UnexpectedUniFFICallbackError` for these cases. Your code must include a -`From` impl for your error type to handle those or -the UniFFI scaffolding code will fail to compile. See `example/callbacks` for an -example of how to do this. +### Unexpected errors + +When Rust code invokes a callback interface method, that call may result in all kinds of unexpected errors. +Some examples are the foreign code throws an exception that's not part of the exception type or there was a problem marshalling the data for the call. +UniFFI creates an `uniffi::UnexpectedUniFFICallbackError` for these cases. +If your code defines a `From` impl for your error type, then those errors will be converted into your error type which will be returned to the Rust caller. +If not, then any unexpected errors will result in a panic. +See `example/callbacks` for an example of this. ## 3. Define a callback interface in the UDL diff --git a/uniffi_core/src/ffi/callbackinterface.rs b/uniffi_core/src/ffi/callbackinterface.rs index d4c8b42e3e..7be66880bb 100644 --- a/uniffi_core/src/ffi/callbackinterface.rs +++ b/uniffi_core/src/ffi/callbackinterface.rs @@ -232,3 +232,77 @@ impl fmt::Display for UnexpectedUniFFICallbackError { } impl std::error::Error for UnexpectedUniFFICallbackError {} + +// Autoref-based specialization for converting UnexpectedUniFFICallbackError into error types. +// +// For more details, see: +// https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md + +// Define two ZST types: +// - One implements `try_convert_unexpected_callback_error` by always returning an error value. +// - The specialized version implements it using `From` + +#[doc(hidden)] +#[derive(Debug)] +pub struct UnexpectedUniFFICallbackErrorConverterGeneric; + +impl UnexpectedUniFFICallbackErrorConverterGeneric { + pub fn try_convert_unexpected_callback_error( + &self, + e: UnexpectedUniFFICallbackError, + ) -> anyhow::Result { + Err(e.into()) + } +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct UnexpectedUniFFICallbackErrorConverterSpecialized; + +impl UnexpectedUniFFICallbackErrorConverterSpecialized { + pub fn try_convert_unexpected_callback_error( + &self, + e: UnexpectedUniFFICallbackError, + ) -> anyhow::Result + where + E: From, + { + Ok(E::from(e)) + } +} + +// Macro to convert an UnexpectedUniFFICallbackError value for a particular type. This is used in +// the `ConvertError` implementation. +#[doc(hidden)] +#[macro_export] +macro_rules! convert_unexpected_error { + ($error:ident, $ty:ty) => {{ + // Trait for generic conversion, implemented for all &T. + pub trait GetConverterGeneric { + fn get_converter(&self) -> $crate::UnexpectedUniFFICallbackErrorConverterGeneric; + } + + impl GetConverterGeneric for &T { + fn get_converter(&self) -> $crate::UnexpectedUniFFICallbackErrorConverterGeneric { + $crate::UnexpectedUniFFICallbackErrorConverterGeneric + } + } + // Trait for specialized conversion, implemented for all T that implements + // `Into`. I.e. it's implemented for UnexpectedUniFFICallbackError when + // ErrorType implements From. + pub trait GetConverterSpecialized { + fn get_converter(&self) -> $crate::UnexpectedUniFFICallbackErrorConverterSpecialized; + } + + impl> GetConverterSpecialized for T { + fn get_converter(&self) -> $crate::UnexpectedUniFFICallbackErrorConverterSpecialized { + $crate::UnexpectedUniFFICallbackErrorConverterSpecialized + } + } + // Here's the hack. Because of the auto-ref rules, this will use `GetConverterSpecialized` + // if it's implemented and `GetConverterGeneric` if not. + (&$error) + .get_converter() + .try_convert_unexpected_callback_error($error) + }}; +} diff --git a/uniffi_core/src/ffi_converter_impls.rs b/uniffi_core/src/ffi_converter_impls.rs index fc7824fafe..af18f3873b 100644 --- a/uniffi_core/src/ffi_converter_impls.rs +++ b/uniffi_core/src/ffi_converter_impls.rs @@ -23,8 +23,8 @@ /// "UT" means an abitrary `UniFfiTag` type. use crate::{ check_remaining, derive_ffi_traits, ffi_converter_rust_buffer_lift_and_lower, metadata, - FfiConverter, ForeignExecutor, Lift, LiftReturn, Lower, LowerReturn, MetadataBuffer, Result, - RustBuffer, UnexpectedUniFFICallbackError, + ConvertError, FfiConverter, ForeignExecutor, Lift, LiftReturn, Lower, LowerReturn, + MetadataBuffer, Result, RustBuffer, UnexpectedUniFFICallbackError, }; use anyhow::bail; use bytes::buf::{Buf, BufMut}; @@ -535,7 +535,7 @@ where unsafe impl LiftReturn for Result where R: LiftReturn, - E: Lift + From, + E: Lift + ConvertError, { fn lift_callback_return(buf: RustBuffer) -> Self { Ok(R::lift_callback_return(buf)) @@ -553,7 +553,7 @@ where } fn handle_callback_unexpected_error(e: UnexpectedUniFFICallbackError) -> Self { - Err(E::from(e)) + Err(E::try_convert_unexpected_callback_error(e).unwrap_or_else(|e| panic!("{e}"))) } const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_RESULT) diff --git a/uniffi_core/src/ffi_converter_traits.rs b/uniffi_core/src/ffi_converter_traits.rs index 7846374270..3b5914e32f 100644 --- a/uniffi_core/src/ffi_converter_traits.rs +++ b/uniffi_core/src/ffi_converter_traits.rs @@ -347,6 +347,10 @@ pub unsafe trait LiftRef { type LiftType: Lift + Borrow; } +pub trait ConvertError: Sized { + fn try_convert_unexpected_callback_error(e: UnexpectedUniFFICallbackError) -> Result; +} + /// Derive FFI traits /// /// This can be used to derive: @@ -373,6 +377,7 @@ macro_rules! derive_ffi_traits { $crate::derive_ffi_traits!(impl LowerReturn for $ty); $crate::derive_ffi_traits!(impl LiftReturn for $ty); $crate::derive_ffi_traits!(impl LiftRef for $ty); + $crate::derive_ffi_traits!(impl ConvertError for $ty); }; (local $ty:ty) => { @@ -381,6 +386,7 @@ macro_rules! derive_ffi_traits { $crate::derive_ffi_traits!(impl LowerReturn for $ty); $crate::derive_ffi_traits!(impl LiftReturn for $ty); $crate::derive_ffi_traits!(impl LiftRef for $ty); + $crate::derive_ffi_traits!(impl ConvertError for $ty); }; (impl $(<$($generic:ident),*>)? $(::uniffi::)? Lower<$ut:path> for $ty:ty $(where $($where:tt)*)?) => { @@ -448,4 +454,13 @@ macro_rules! derive_ffi_traits { type LiftType = Self; } }; + + (impl $(<$($generic:ident),*>)? $(::uniffi::)? ConvertError<$ut:path> for $ty:ty $(where $($where:tt)*)?) => { + impl $(<$($generic),*>)* $crate::ConvertError<$ut> for $ty $(where $($where)*)* + { + fn try_convert_unexpected_callback_error(e: $crate::UnexpectedUniFFICallbackError) -> $crate::deps::anyhow::Result { + $crate::convert_unexpected_error!(e, $ty) + } + } + }; } diff --git a/uniffi_core/src/lib.rs b/uniffi_core/src/lib.rs index 4bc383d1f4..9003b08f9b 100644 --- a/uniffi_core/src/lib.rs +++ b/uniffi_core/src/lib.rs @@ -45,7 +45,7 @@ pub mod metadata; pub use ffi::*; pub use ffi_converter_traits::{ - FfiConverter, FfiConverterArc, Lift, LiftRef, LiftReturn, Lower, LowerReturn, + ConvertError, FfiConverter, FfiConverterArc, Lift, LiftRef, LiftReturn, Lower, LowerReturn, }; pub use metadata::*; diff --git a/uniffi_macros/src/custom.rs b/uniffi_macros/src/custom.rs index 5fb590f3a1..9d8e5acde6 100644 --- a/uniffi_macros/src/custom.rs +++ b/uniffi_macros/src/custom.rs @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use crate::util::{derive_ffi_traits, ident_to_string, mod_path, tagged_impl_header}; +use crate::util::{derive_all_ffi_traits, ident_to_string, mod_path, tagged_impl_header}; use proc_macro2::{Ident, TokenStream}; use quote::quote; use syn::Path; @@ -15,7 +15,7 @@ pub(crate) fn expand_ffi_converter_custom_type( udl_mode: bool, ) -> syn::Result { let impl_spec = tagged_impl_header("FfiConverter", ident, udl_mode); - let derive_ffi_traits = derive_ffi_traits(ident, udl_mode); + let derive_ffi_traits = derive_all_ffi_traits(ident, udl_mode); let name = ident_to_string(ident); let mod_path = mod_path()?; diff --git a/uniffi_macros/src/enum_.rs b/uniffi_macros/src/enum_.rs index 3ddccdd7c1..32abfa08cc 100644 --- a/uniffi_macros/src/enum_.rs +++ b/uniffi_macros/src/enum_.rs @@ -3,7 +3,7 @@ use quote::quote; use syn::{Data, DataEnum, DeriveInput, Field, Index}; use crate::util::{ - create_metadata_items, derive_ffi_traits, ident_to_string, mod_path, tagged_impl_header, + create_metadata_items, derive_all_ffi_traits, ident_to_string, mod_path, tagged_impl_header, try_metadata_value_from_usize, try_read_field, }; @@ -64,7 +64,7 @@ fn enum_or_error_ffi_converter_impl( ) -> TokenStream { let name = ident_to_string(ident); let impl_spec = tagged_impl_header("FfiConverter", ident, udl_mode); - let derive_ffi_traits = derive_ffi_traits(ident, udl_mode); + let derive_ffi_traits = derive_all_ffi_traits(ident, udl_mode); let mod_path = match mod_path() { Ok(p) => p, Err(e) => return e.into_compile_error(), diff --git a/uniffi_macros/src/error.rs b/uniffi_macros/src/error.rs index 5ab6f4f424..cb25df5d68 100644 --- a/uniffi_macros/src/error.rs +++ b/uniffi_macros/src/error.rs @@ -8,8 +8,8 @@ use syn::{ use crate::{ enum_::{rich_error_ffi_converter_impl, variant_metadata}, util::{ - chain, create_metadata_items, either_attribute_arg, ident_to_string, kw, mod_path, - parse_comma_separated, tagged_impl_header, try_metadata_value_from_usize, + chain, create_metadata_items, derive_ffi_traits, either_attribute_arg, ident_to_string, kw, + mod_path, parse_comma_separated, tagged_impl_header, try_metadata_value_from_usize, AttributeSliceExt, UniffiAttributeArgs, }, }; @@ -87,6 +87,7 @@ fn flat_error_ffi_converter_impl( ) -> TokenStream { let name = ident_to_string(ident); let lower_impl_spec = tagged_impl_header("Lower", ident, udl_mode); + let derive_ffi_traits = derive_ffi_traits(ident, udl_mode, &["ConvertError"]); let mod_path = match mod_path() { Ok(p) => p, Err(e) => return e.into_compile_error(), @@ -161,6 +162,7 @@ fn flat_error_ffi_converter_impl( quote! { #lower_impl #lift_impl + #derive_ffi_traits } } diff --git a/uniffi_macros/src/record.rs b/uniffi_macros/src/record.rs index 453f67ce86..abf2743ec6 100644 --- a/uniffi_macros/src/record.rs +++ b/uniffi_macros/src/record.rs @@ -6,8 +6,8 @@ use syn::{ }; use crate::util::{ - create_metadata_items, derive_ffi_traits, either_attribute_arg, ident_to_string, kw, mod_path, - tagged_impl_header, try_metadata_value_from_usize, try_read_field, AttributeSliceExt, + create_metadata_items, derive_all_ffi_traits, either_attribute_arg, ident_to_string, kw, + mod_path, tagged_impl_header, try_metadata_value_from_usize, try_read_field, AttributeSliceExt, UniffiAttributeArgs, }; @@ -41,7 +41,7 @@ pub(crate) fn record_ffi_converter_impl( udl_mode: bool, ) -> syn::Result { let impl_spec = tagged_impl_header("FfiConverter", ident, udl_mode); - let derive_ffi_traits = derive_ffi_traits(ident, udl_mode); + let derive_ffi_traits = derive_all_ffi_traits(ident, udl_mode); let name = ident_to_string(ident); let mod_path = mod_path()?; let write_impl: TokenStream = record.fields.iter().map(write_field).collect(); diff --git a/uniffi_macros/src/util.rs b/uniffi_macros/src/util.rs index e33c4bc505..9f213ea1d7 100644 --- a/uniffi_macros/src/util.rs +++ b/uniffi_macros/src/util.rs @@ -216,7 +216,7 @@ pub(crate) fn tagged_impl_header( } } -pub(crate) fn derive_ffi_traits(ty: &Ident, udl_mode: bool) -> TokenStream { +pub(crate) fn derive_all_ffi_traits(ty: &Ident, udl_mode: bool) -> TokenStream { if udl_mode { quote! { ::uniffi::derive_ffi_traits!(local #ty); } } else { @@ -224,6 +224,25 @@ pub(crate) fn derive_ffi_traits(ty: &Ident, udl_mode: bool) -> TokenStream { } } +pub(crate) fn derive_ffi_traits(ty: &Ident, udl_mode: bool, trait_names: &[&str]) -> TokenStream { + let trait_idents = trait_names + .iter() + .map(|name| Ident::new(name, Span::call_site())); + if udl_mode { + quote! { + #( + ::uniffi::derive_ffi_traits!(impl #trait_idents for #ty); + )* + } + } else { + quote! { + #( + ::uniffi::derive_ffi_traits!(impl #trait_idents for #ty); + )* + } + } +} + /// Custom keywords pub mod kw { syn::custom_keyword!(async_runtime); From 1c4df967a0f072926c5f5bc83402fbf5a9a218e4 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Sat, 23 Sep 2023 15:37:59 -0400 Subject: [PATCH 19/24] Refactoring the trait interface code This is prep work for allowing the foreign bindings to implement traits. Moved trait interface code in `uniffi_macros` to its own module. Updated the test trait in the coverall fixture. I think this one will work better to test foreign trait impls. The `ancestor_names()` method is a good way to test trait implementations that bounce between the Rust and foreign side of the FFI. Added Getters test like we have with callback interfaces. We want to replace callback interfaces with trait interfaces, so we should have good test coverage. This actually revealed that the trait code didn't support exceptions yet. --- fixtures/coverall/src/coverall.udl | 46 +++-- fixtures/coverall/src/lib.rs | 10 +- fixtures/coverall/src/traits.rs | 184 ++++++++++++++---- .../coverall/tests/bindings/test_coverall.kts | 75 ++++++- .../coverall/tests/bindings/test_coverall.py | 57 +++++- .../tests/bindings/test_coverall.swift | 79 ++++++++ .../scaffolding/templates/ObjectTemplate.rs | 8 +- uniffi_macros/src/export.rs | 117 +---------- uniffi_macros/src/export/trait_interface.rs | 124 ++++++++++++ uniffi_macros/src/lib.rs | 8 - 10 files changed, 518 insertions(+), 190 deletions(-) create mode 100644 uniffi_macros/src/export/trait_interface.rs diff --git a/fixtures/coverall/src/coverall.udl b/fixtures/coverall/src/coverall.udl index 2c0d9550d6..6b1c2d304b 100644 --- a/fixtures/coverall/src/coverall.udl +++ b/fixtures/coverall/src/coverall.udl @@ -4,7 +4,7 @@ namespace coverall { u64 get_num_alive(); - sequence get_traits(); + sequence get_traits(); MaybeSimpleDict get_maybe_simple_dict(i8 index); @@ -17,6 +17,11 @@ namespace coverall { [Throws=CoverallRichErrorNoVariantData] void throw_rich_error_no_variant_data(); + + Getters make_rust_getters(); + void test_getters(Getters g); + + sequence ancestor_names(NodeTrait node); }; dictionary SimpleDict { @@ -41,7 +46,7 @@ dictionary SimpleDict { double float64; double? maybe_float64; Coveralls? coveralls; - TestTrait? test_trait; + NodeTrait? test_trait; }; dictionary DictWithDefaults { @@ -190,24 +195,37 @@ interface ThreadsafeCounter { i32 increment_if_busy(); }; -// This is a trait implemented on the Rust side. +// Test trait #1 +// +// The goal here is to test all possible arg, return, and error types. [Trait] -interface TestTrait { - string name(); // The name of the implementation +interface Getters { + boolean get_bool(boolean v, boolean arg2); + [Throws=CoverallError] + string get_string(string v, boolean arg2); + [Throws=ComplexError] + string? get_option(string v, boolean arg2); + sequence get_list(sequence v, boolean arg2); + void get_nothing(string v); +}; - [Self=ByArc] - u64 number(); +// Test trait #2 +// +// The goal here is test passing objects back and forth between Rust and the foreign side +[Trait] +interface NodeTrait { + string name(); // The name of the this node + + /// Takes an `Arc` and stores it our parent node, dropping any existing / reference. Note + //you can create circular references with this. + void set_parent(NodeTrait? parent); + + /// Returns what was previously set via `set_parent()`, or null. + NodeTrait? get_parent(); /// Calls `Arc::strong_count()` on the `Arc` containing `self`. [Self=ByArc] u64 strong_count(); - - /// Takes an `Arc` and stores it in `self`, dropping the existing - /// reference. Note you can create circular references by passing `self`. - void take_other(TestTrait? other); - - /// Returns what was previously set via `take_other()`, or null. - TestTrait? get_other(); }; // Forward/backward declarations are fine in UDL. diff --git a/fixtures/coverall/src/lib.rs b/fixtures/coverall/src/lib.rs index e591c0db79..82b7236cc9 100644 --- a/fixtures/coverall/src/lib.rs +++ b/fixtures/coverall/src/lib.rs @@ -10,11 +10,11 @@ use std::time::SystemTime; use once_cell::sync::Lazy; mod traits; -pub use traits::{get_traits, TestTrait}; +pub use traits::{ancestor_names, get_traits, make_rust_getters, test_getters, Getters, NodeTrait}; static NUM_ALIVE: Lazy> = Lazy::new(|| RwLock::new(0)); -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum CoverallError { #[error("The coverall has too many holes")] TooManyHoles, @@ -80,7 +80,7 @@ impl From for CoverallError { } } -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum ComplexError { #[error("OsError: {code} ({extended_code})")] OsError { code: i16, extended_code: i16 }, @@ -131,7 +131,7 @@ pub struct SimpleDict { float64: f64, maybe_float64: Option, coveralls: Option>, - test_trait: Option>, + test_trait: Option>, } #[derive(Debug, Clone)] @@ -204,7 +204,7 @@ fn create_some_dict() -> SimpleDict { float64: 0.0, maybe_float64: Some(1.0), coveralls: Some(Arc::new(Coveralls::new("some_dict".to_string()))), - test_trait: Some(Arc::new(traits::Trait2 {})), + test_trait: Some(Arc::new(traits::Trait2::default())), } } diff --git a/fixtures/coverall/src/traits.rs b/fixtures/coverall/src/traits.rs index fe9694ef0e..15785ef0c6 100644 --- a/fixtures/coverall/src/traits.rs +++ b/fixtures/coverall/src/traits.rs @@ -2,73 +2,187 @@ * 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::{ComplexError, CoverallError}; use std::sync::{Arc, Mutex}; // namespace functions. -pub fn get_traits() -> Vec> { - vec![ - Arc::new(Trait1 { - ..Default::default() - }), - Arc::new(Trait2 {}), - ] +pub fn get_traits() -> Vec> { + vec![Arc::new(Trait1::default()), Arc::new(Trait2::default())] } -pub trait TestTrait: Send + Sync + std::fmt::Debug { +pub trait NodeTrait: Send + Sync + std::fmt::Debug { fn name(&self) -> String; - fn number(self: Arc) -> u64; + fn set_parent(&self, parent: Option>); + + fn get_parent(&self) -> Option>; fn strong_count(self: Arc) -> u64 { Arc::strong_count(&self) as u64 } +} - fn take_other(&self, other: Option>); +pub fn ancestor_names(node: Arc) -> Vec { + let mut names = vec![]; + let mut parent = node.get_parent(); + while let Some(node) = parent { + names.push(node.name()); + parent = node.get_parent(); + } + names +} - fn get_other(&self) -> Option>; +/// Test trait +/// +/// The goal here is to test all possible arg, return, and error types. +pub trait Getters: Send + Sync { + fn get_bool(&self, v: bool, arg2: bool) -> bool; + fn get_string(&self, v: String, arg2: bool) -> Result; + fn get_option(&self, v: String, arg2: bool) -> Result, ComplexError>; + fn get_list(&self, v: Vec, arg2: bool) -> Vec; + fn get_nothing(&self, v: String); +} + +struct RustGetters; + +impl Getters for RustGetters { + fn get_bool(&self, v: bool, arg2: bool) -> bool { + v ^ arg2 + } + + fn get_string(&self, v: String, arg2: bool) -> Result { + if v == "too-many-holes" { + Err(CoverallError::TooManyHoles) + } else if v == "unexpected-error" { + panic!("unexpected error") + } else if arg2 { + Ok(v.to_uppercase()) + } else { + Ok(v) + } + } + + fn get_option(&self, v: String, arg2: bool) -> Result, ComplexError> { + if v == "os-error" { + Err(ComplexError::OsError { + code: 100, + extended_code: 200, + }) + } else if v == "unknown-error" { + Err(ComplexError::UnknownError) + } else if arg2 { + if !v.is_empty() { + Ok(Some(v.to_uppercase())) + } else { + Ok(None) + } + } else { + Ok(Some(v)) + } + } + + fn get_list(&self, v: Vec, arg2: bool) -> Vec { + if arg2 { + v + } else { + vec![] + } + } + + fn get_nothing(&self, _v: String) {} +} + +pub fn make_rust_getters() -> Arc { + Arc::new(RustGetters) +} + +pub fn test_getters(getters: Arc) { + assert!(!getters.get_bool(true, true)); + assert!(getters.get_bool(true, false)); + assert!(getters.get_bool(false, true)); + assert!(!getters.get_bool(false, false)); + + assert_eq!( + getters.get_string("hello".to_owned(), false).unwrap(), + "hello" + ); + assert_eq!( + getters.get_string("hello".to_owned(), true).unwrap(), + "HELLO" + ); + + assert_eq!( + getters.get_option("hello".to_owned(), true).unwrap(), + Some("HELLO".to_owned()) + ); + assert_eq!( + getters.get_option("hello".to_owned(), false).unwrap(), + Some("hello".to_owned()) + ); + assert_eq!(getters.get_option("".to_owned(), true).unwrap(), None); + + assert_eq!(getters.get_list(vec![1, 2, 3], true), vec![1, 2, 3]); + assert_eq!(getters.get_list(vec![1, 2, 3], false), Vec::::new()); + + // Call get_nothing to make sure it doesn't panic. There's no point in checking the output + // though + getters.get_nothing("hello".to_owned()); + + assert_eq!( + getters.get_string("too-many-holes".to_owned(), true), + Err(CoverallError::TooManyHoles) + ); + assert_eq!( + getters.get_option("os-error".to_owned(), true), + Err(ComplexError::OsError { + code: 100, + extended_code: 200 + }) + ); + assert_eq!( + getters.get_option("unknown-error".to_owned(), true), + Err(ComplexError::UnknownError) + ); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + getters.get_string("unexpected-error".to_owned(), true) + })); + assert!(result.is_err()); } #[derive(Debug, Default)] pub(crate) struct Trait1 { // A reference to another trait. - other: Mutex>>, + parent: Mutex>>, } -impl TestTrait for Trait1 { +impl NodeTrait for Trait1 { fn name(&self) -> String { - "trait 1".to_string() + "node-1".to_string() } - fn number(self: Arc) -> u64 { - 1_u64 + fn set_parent(&self, parent: Option>) { + *self.parent.lock().unwrap() = parent.map(|arc| Arc::clone(&arc)) } - fn take_other(&self, other: Option>) { - *self.other.lock().unwrap() = other.map(|arc| Arc::clone(&arc)) - } - - fn get_other(&self) -> Option> { - (*self.other.lock().unwrap()).as_ref().map(Arc::clone) + fn get_parent(&self) -> Option> { + (*self.parent.lock().unwrap()).as_ref().map(Arc::clone) } } -#[derive(Debug)] -pub(crate) struct Trait2 {} -impl TestTrait for Trait2 { +#[derive(Debug, Default)] +pub(crate) struct Trait2 { + parent: Mutex>>, +} +impl NodeTrait for Trait2 { fn name(&self) -> String { - "trait 2".to_string() - } - - fn number(self: Arc) -> u64 { - 2_u64 + "node-2".to_string() } - // Don't bother implementing these here - the test on the struct above is ok. - fn take_other(&self, _other: Option>) { - unimplemented!(); + fn set_parent(&self, parent: Option>) { + *self.parent.lock().unwrap() = parent.map(|arc| Arc::clone(&arc)) } - fn get_other(&self) -> Option> { - unimplemented!() + fn get_parent(&self) -> Option> { + (*self.parent.lock().unwrap()).as_ref().map(Arc::clone) } } diff --git a/fixtures/coverall/tests/bindings/test_coverall.kts b/fixtures/coverall/tests/bindings/test_coverall.kts index 62fbbb5a70..3cefa65783 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.kts +++ b/fixtures/coverall/tests/bindings/test_coverall.kts @@ -210,8 +210,79 @@ Coveralls("test_interfaces_in_dicts").use { coveralls -> assert(coveralls.getRepairs().size == 2) } -Coveralls("test_regressions").use { coveralls -> - assert(coveralls.getStatus("success") == "status: success") +// Test traits implemented in Rust +makeRustGetters().let { rustGetters -> + testGetters(rustGetters) + testGettersFromKotlin(rustGetters) +} + +fun testGettersFromKotlin(getters: Getters) { + assert(getters.getBool(true, true) == false); + assert(getters.getBool(true, false) == true); + assert(getters.getBool(false, true) == true); + assert(getters.getBool(false, false) == false); + + assert(getters.getString("hello", false) == "hello"); + assert(getters.getString("hello", true) == "HELLO"); + + assert(getters.getOption("hello", true) == "HELLO"); + assert(getters.getOption("hello", false) == "hello"); + assert(getters.getOption("", true) == null); + + assert(getters.getList(listOf(1, 2, 3), true) == listOf(1, 2, 3)) + assert(getters.getList(listOf(1, 2, 3), false) == listOf()) + + assert(getters.getNothing("hello") == Unit); + + try { + getters.getString("too-many-holes", true) + throw RuntimeException("Expected method to throw exception") + } catch(e: CoverallException.TooManyHoles) { + // Expected + } + + try { + getters.getOption("os-error", true) + throw RuntimeException("Expected method to throw exception") + } catch(e: ComplexException.OsException) { + assert(e.code.toInt() == 100) + assert(e.extendedCode.toInt() == 200) + } + + try { + getters.getOption("unknown-error", true) + throw RuntimeException("Expected method to throw exception") + } catch(e: ComplexException.UnknownException) { + // Expected + } + + try { + getters.getString("unexpected-error", true) + } catch(e: InternalException) { + // Expected + } +} + +// Test NodeTrait +getTraits().let { traits -> + assert(traits[0].name() == "node-1") + // Note: strong counts are 1 more than you might expect, because the strongCount() method + // holds a strong ref. + assert(traits[0].strongCount() == 2UL) + + assert(traits[1].name() == "node-2") + assert(traits[1].strongCount() == 2UL) + + traits[0].setParent(traits[1]) + assert(ancestorNames(traits[0]) == listOf("node-2")) + assert(ancestorNames(traits[1]).isEmpty()) + assert(traits[1].strongCount() == 3UL) + assert(traits[0].getParent()!!.name() == "node-2") + traits[0].setParent(null) + + Coveralls("test_regressions").use { coveralls -> + assert(coveralls.getStatus("success") == "status: success") + } } // This tests that the UniFFI-generated scaffolding doesn't introduce any unexpected locking. diff --git a/fixtures/coverall/tests/bindings/test_coverall.py b/fixtures/coverall/tests/bindings/test_coverall.py index a63a2e80b9..4f9d1e2df0 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.py +++ b/fixtures/coverall/tests/bindings/test_coverall.py @@ -31,7 +31,7 @@ def test_some_dict(self): self.assertEqual(d.signed64, 9223372036854775807) self.assertEqual(d.maybe_signed64, 0) self.assertEqual(d.coveralls.get_name(), "some_dict") - self.assertEqual(d.test_trait.name(), "trait 2") + self.assertEqual(d.test_trait.name(), "node-2") # floats should be "close enough" - although it's mildly surprising that # we need to specify `places=6` whereas the default is 7. @@ -279,20 +279,59 @@ def test_bytes(self): self.assertEqual(coveralls.reverse(b"123"), b"321") class TraitsTest(unittest.TestCase): - def test_simple(self): + # Test traits implemented in Rust + def test_rust_getters(self): + test_getters(make_rust_getters()) + self.check_getters_from_python(make_rust_getters()) + + def check_getters_from_python(self, getters): + self.assertEqual(getters.get_bool(True, True), False); + self.assertEqual(getters.get_bool(True, False), True); + self.assertEqual(getters.get_bool(False, True), True); + self.assertEqual(getters.get_bool(False, False), False); + + self.assertEqual(getters.get_string("hello", False), "hello"); + self.assertEqual(getters.get_string("hello", True), "HELLO"); + + self.assertEqual(getters.get_option("hello", True), "HELLO"); + self.assertEqual(getters.get_option("hello", False), "hello"); + self.assertEqual(getters.get_option("", True), None); + + self.assertEqual(getters.get_list([1, 2, 3], True), [1, 2, 3]); + self.assertEqual(getters.get_list([1, 2, 3], False), []) + + self.assertEqual(getters.get_nothing("hello"), None); + + with self.assertRaises(CoverallError.TooManyHoles): + getters.get_string("too-many-holes", True) + + with self.assertRaises(ComplexError.OsError) as cm: + getters.get_option("os-error", True) + self.assertEqual(cm.exception.code, 100) + self.assertEqual(cm.exception.extended_code, 200) + + with self.assertRaises(ComplexError.UnknownError): + getters.get_option("unknown-error", True) + + with self.assertRaises(InternalError): + getters.get_string("unexpected-error", True) + + def test_node(self): traits = get_traits() - self.assertEqual(traits[0].name(), "trait 1") - self.assertEqual(traits[0].number(), 1) + self.assertEqual(traits[0].name(), "node-1") + # Note: strong counts are 1 more than you might expect, because the strong_count() method + # holds a strong ref. self.assertEqual(traits[0].strong_count(), 2) - self.assertEqual(traits[1].name(), "trait 2") - self.assertEqual(traits[1].number(), 2) + self.assertEqual(traits[1].name(), "node-2") self.assertEqual(traits[1].strong_count(), 2) - traits[0].take_other(traits[1]) + traits[0].set_parent(traits[1]) + self.assertEqual(ancestor_names(traits[0]), ["node-2"]) + self.assertEqual(ancestor_names(traits[1]), []) self.assertEqual(traits[1].strong_count(), 3) - self.assertEqual(traits[0].get_other().name(), "trait 2") - traits[0].take_other(None) + self.assertEqual(traits[0].get_parent().name(), "node-2") + traits[0].set_parent(None) if __name__=='__main__': unittest.main() diff --git a/fixtures/coverall/tests/bindings/test_coverall.swift b/fixtures/coverall/tests/bindings/test_coverall.swift index d380881184..8db21ad78a 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.swift +++ b/fixtures/coverall/tests/bindings/test_coverall.swift @@ -247,3 +247,82 @@ do { let coveralls = Coveralls(name: "test_bytes") assert(coveralls.reverse(value: Data("123".utf8)) == Data("321".utf8)) } + +// Test traits implemented in Rust +do { + let rustGetters = makeRustGetters() + testGetters(g: rustGetters) + testGettersFromSwift(getters: rustGetters) +} + +func testGettersFromSwift(getters: Getters) { + assert(getters.getBool(v: true, arg2: true) == false); + assert(getters.getBool(v: true, arg2: false) == true); + assert(getters.getBool(v: false, arg2: true) == true); + assert(getters.getBool(v: false, arg2: false) == false); + + assert(try! getters.getString(v: "hello", arg2: false) == "hello"); + assert(try! getters.getString(v: "hello", arg2: true) == "HELLO"); + + assert(try! getters.getOption(v: "hello", arg2: true) == "HELLO"); + assert(try! getters.getOption(v: "hello", arg2: false) == "hello"); + assert(try! getters.getOption(v: "", arg2: true) == nil); + + assert(getters.getList(v: [1, 2, 3], arg2: true) == [1, 2, 3]) + assert(getters.getList(v: [1, 2, 3], arg2: false) == []) + + assert(getters.getNothing(v: "hello") == ()); + + do { + let _ = try getters.getString(v: "too-many-holes", arg2: true) + fatalError("should have thrown") + } catch CoverallError.TooManyHoles { + // Expected + } catch { + fatalError("Unexpected error: \(error)") + } + + do { + try getters.getOption(v: "os-error", arg2: true) + fatalError("should have thrown") + } catch ComplexError.OsError(let code, let extendedCode) { + assert(code == 100) + assert(extendedCode == 200) + } catch { + fatalError("Unexpected error: \(error)") + } + + do { + try getters.getOption(v: "unknown-error", arg2: true) + fatalError("should have thrown") + } catch ComplexError.UnknownError { + // Expected + } catch { + fatalError("Unexpected error: \(error)") + } + + do { + try getters.getString(v: "unexpected-error", arg2: true) + } catch { + // Expected + } +} + +// Test Node trait +do { + let traits = getTraits() + assert(traits[0].name() == "node-1") + // Note: strong counts are 1 more than you might expect, because the strongCount() method + // holds a strong ref. + assert(traits[0].strongCount() == 2) + + assert(traits[1].name() == "node-2") + assert(traits[1].strongCount() == 2) + + traits[0].setParent(parent: traits[1]) + assert(ancestorNames(node: traits[0]) == ["node-2"]) + assert(ancestorNames(node: traits[1]) == []) + assert(traits[1].strongCount() == 3) + assert(traits[0].getParent()!.name() == "node-2") + traits[0].setParent(parent: nil) +} diff --git a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs index 3346d4fbfb..e2445c670d 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs @@ -21,9 +21,11 @@ pub trait r#{{ obj.name() }} { {{ arg.name() }}: {% if arg.by_ref() %}&{% endif %}{{ arg.as_type().borrow()|type_rs }}, {%- endfor %} ) - {%- match meth.return_type() %} - {%- when Some(return_type) %} -> {{ return_type|type_rs }}; - {%- when None %}; + {%- match (meth.return_type(), meth.throws_type()) %} + {%- when (Some(return_type), None) %} -> {{ return_type|type_rs }}; + {%- when (Some(return_type), Some(error_type)) %} -> ::std::result::Result::<{{ return_type|type_rs }}, {{ error_type|type_rs }}>; + {%- when (None, Some(error_type)) %} -> ::std::result::Result::<(), {{ error_type|type_rs }}>; + {%- when (None, None) %}; {%- endmatch %} {% endfor %} } diff --git a/uniffi_macros/src/export.rs b/uniffi_macros/src/export.rs index bbb16acf90..bc7f8b40ea 100644 --- a/uniffi_macros/src/export.rs +++ b/uniffi_macros/src/export.rs @@ -10,6 +10,7 @@ mod attributes; mod callback_interface; mod item; mod scaffolding; +mod trait_interface; mod utrait; use self::{ @@ -18,13 +19,9 @@ use self::{ gen_constructor_scaffolding, gen_ffi_function, gen_fn_scaffolding, gen_method_scaffolding, }, }; -use crate::{ - object::interface_meta_static_var, - util::{ident_to_string, mod_path, tagged_impl_header}, -}; +use crate::util::{ident_to_string, mod_path}; pub use attributes::ExportAttributeArguments; pub use callback_interface::ffi_converter_callback_interface_impl; -use uniffi_meta::free_fn_symbol_name; // TODO(jplatte): Ensure no generics, … // TODO(jplatte): Aggregate errors instead of short-circuiting, wherever possible @@ -71,59 +68,7 @@ pub(crate) fn expand_export( items, self_ident, callback_interface: false, - } => { - if let Some(rt) = args.async_runtime { - return Err(syn::Error::new_spanned(rt, "not supported for traits")); - } - - let name = ident_to_string(&self_ident); - let free_fn_ident = - Ident::new(&free_fn_symbol_name(&mod_path, &name), Span::call_site()); - - let free_tokens = quote! { - #[doc(hidden)] - #[no_mangle] - pub extern "C" fn #free_fn_ident( - ptr: *const ::std::ffi::c_void, - call_status: &mut ::uniffi::RustCallStatus - ) { - uniffi::rust_call(call_status, || { - assert!(!ptr.is_null()); - drop(unsafe { ::std::boxed::Box::from_raw(ptr as *mut std::sync::Arc) }); - Ok(()) - }); - } - }; - - let impl_tokens: TokenStream = items - .into_iter() - .map(|item| match item { - ImplItem::Method(sig) => { - if sig.is_async { - return Err(syn::Error::new( - sig.span, - "async trait methods are not supported", - )); - } - gen_method_scaffolding(sig, &args, udl_mode) - } - _ => unreachable!("traits have no constructors"), - }) - .collect::>()?; - - let meta_static_var = (!udl_mode).then(|| { - interface_meta_static_var(&self_ident, true, &mod_path) - .unwrap_or_else(syn::Error::into_compile_error) - }); - let ffi_converter_tokens = ffi_converter_trait_impl(&self_ident, false); - - Ok(quote_spanned! { self_ident.span() => - #meta_static_var - #free_tokens - #ffi_converter_tokens - #impl_tokens - }) - } + } => trait_interface::gen_trait_scaffolding(&mod_path, args, self_ident, items, udl_mode), ExportItem::Trait { items, self_ident, @@ -182,62 +127,6 @@ pub(crate) fn expand_export( } } -pub(crate) fn ffi_converter_trait_impl(trait_ident: &Ident, udl_mode: bool) -> TokenStream { - let impl_spec = tagged_impl_header("FfiConverterArc", "e! { dyn #trait_ident }, udl_mode); - let lift_ref_impl_spec = tagged_impl_header("LiftRef", "e! { dyn #trait_ident }, udl_mode); - let name = ident_to_string(trait_ident); - let mod_path = match mod_path() { - Ok(p) => p, - Err(e) => return e.into_compile_error(), - }; - - quote! { - // All traits must be `Sync + Send`. The generated scaffolding will fail to compile - // if they are not, but unfortunately it fails with an unactionably obscure error message. - // By asserting the requirement explicitly, we help Rust produce a more scrutable error message - // and thus help the user debug why the requirement isn't being met. - uniffi::deps::static_assertions::assert_impl_all!(dyn #trait_ident: Sync, Send); - - unsafe #impl_spec { - type FfiType = *const ::std::os::raw::c_void; - - 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) - } - - 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); - } - - unsafe #lift_ref_impl_spec { - type LiftType = ::std::sync::Arc; - } - } -} - /// Rewrite Self type alias usage in an impl block to the type itself. /// /// For example, diff --git a/uniffi_macros/src/export/trait_interface.rs b/uniffi_macros/src/export/trait_interface.rs new file mode 100644 index 0000000000..e6cdaff5c9 --- /dev/null +++ b/uniffi_macros/src/export/trait_interface.rs @@ -0,0 +1,124 @@ +/* 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 proc_macro2::{Ident, Span, TokenStream}; +use quote::{quote, quote_spanned}; + +use crate::{ + export::{attributes::ExportAttributeArguments, gen_method_scaffolding, item::ImplItem}, + object::interface_meta_static_var, + util::{ident_to_string, tagged_impl_header}, +}; +use uniffi_meta::free_fn_symbol_name; + +pub(super) fn gen_trait_scaffolding( + mod_path: &str, + args: ExportAttributeArguments, + self_ident: Ident, + items: Vec, + udl_mode: bool, +) -> syn::Result { + if let Some(rt) = args.async_runtime { + return Err(syn::Error::new_spanned(rt, "not supported for traits")); + } + + let name = ident_to_string(&self_ident); + let free_fn_ident = Ident::new(&free_fn_symbol_name(mod_path, &name), Span::call_site()); + + let free_tokens = quote! { + #[doc(hidden)] + #[no_mangle] + pub extern "C" fn #free_fn_ident( + ptr: *const ::std::ffi::c_void, + call_status: &mut ::uniffi::RustCallStatus + ) { + uniffi::rust_call(call_status, || { + assert!(!ptr.is_null()); + drop(unsafe { ::std::boxed::Box::from_raw(ptr as *mut std::sync::Arc) }); + Ok(()) + }); + } + }; + + let impl_tokens: TokenStream = items + .into_iter() + .map(|item| match item { + ImplItem::Method(sig) => { + if sig.is_async { + return Err(syn::Error::new( + sig.span, + "async trait methods are not supported", + )); + } + gen_method_scaffolding(sig, &args, udl_mode) + } + _ => unreachable!("traits have no constructors"), + }) + .collect::>()?; + + let meta_static_var = (!udl_mode).then(|| { + interface_meta_static_var(&self_ident, true, mod_path) + .unwrap_or_else(syn::Error::into_compile_error) + }); + let ffi_converter_tokens = ffi_converter(mod_path, &self_ident, false); + + Ok(quote_spanned! { self_ident.span() => + #meta_static_var + #free_tokens + #ffi_converter_tokens + #impl_tokens + }) +} + +pub(crate) fn ffi_converter(mod_path: &str, trait_ident: &Ident, udl_mode: bool) -> TokenStream { + let impl_spec = tagged_impl_header("FfiConverterArc", "e! { dyn #trait_ident }, udl_mode); + let lift_ref_impl_spec = tagged_impl_header("LiftRef", "e! { dyn #trait_ident }, udl_mode); + let name = ident_to_string(trait_ident); + + quote! { + // All traits must be `Sync + Send`. The generated scaffolding will fail to compile + // if they are not, but unfortunately it fails with an unactionably obscure error message. + // By asserting the requirement explicitly, we help Rust produce a more scrutable error message + // and thus help the user debug why the requirement isn't being met. + uniffi::deps::static_assertions::assert_impl_all!(dyn #trait_ident: Sync, Send); + + unsafe #impl_spec { + type FfiType = *const ::std::os::raw::c_void; + + 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) + } + + 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); + } + + unsafe #lift_ref_impl_spec { + type LiftType = ::std::sync::Arc; + } + } +} diff --git a/uniffi_macros/src/lib.rs b/uniffi_macros/src/lib.rs index 4cffddfa0e..b7ba86ddc1 100644 --- a/uniffi_macros/src/lib.rs +++ b/uniffi_macros/src/lib.rs @@ -257,14 +257,6 @@ pub fn export_for_udl(attrs: TokenStream, input: TokenStream) -> TokenStream { do_export(attrs, input, true) } -/// Generate various support elements, including the FfiConverter implementation, -/// for a trait interface for the scaffolding code -#[doc(hidden)] -#[proc_macro] -pub fn expand_trait_interface_support(tokens: TokenStream) -> TokenStream { - export::ffi_converter_trait_impl(&syn::parse_macro_input!(tokens), true).into() -} - /// Generate the FfiConverter implementation for an trait interface for the scaffolding code #[doc(hidden)] #[proc_macro] From 0924af349582df05bd0271823f83227efa883e5d Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Tue, 10 Oct 2023 20:26:43 -0400 Subject: [PATCH 20/24] Recactored callback interface macro code The goal is to re-use this for trait interfaces --- uniffi_macros/src/export.rs | 42 +++----------- .../src/export/callback_interface.rs | 57 ++++++++++++++----- 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/uniffi_macros/src/export.rs b/uniffi_macros/src/export.rs index bc7f8b40ea..7d3f54da93 100644 --- a/uniffi_macros/src/export.rs +++ b/uniffi_macros/src/export.rs @@ -2,7 +2,7 @@ * 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 proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::TokenStream; use quote::{quote, quote_spanned}; use syn::{visit_mut::VisitMut, Item, Type}; @@ -75,45 +75,19 @@ pub(crate) fn expand_export( callback_interface: true, } => { let trait_name = ident_to_string(&self_ident); - let trait_impl_ident = Ident::new( - &format!("UniFFICallbackHandler{trait_name}"), - Span::call_site(), - ); - let internals_ident = Ident::new( - &format!( - "UNIFFI_FOREIGN_CALLBACK_INTERNALS_{}", - trait_name.to_ascii_uppercase() - ), - Span::call_site(), - ); - - let trait_impl = callback_interface::trait_impl( - &trait_impl_ident, - &self_ident, - &internals_ident, - &items, - ) - .unwrap_or_else(|e| e.into_compile_error()); + let trait_impl_ident = callback_interface::trait_impl_ident(&trait_name); + let trait_impl = callback_interface::trait_impl(&mod_path, &self_ident, &items) + .unwrap_or_else(|e| e.into_compile_error()); let metadata_items = callback_interface::metadata_items(&self_ident, &items, &mod_path) .unwrap_or_else(|e| vec![e.into_compile_error()]); - - let init_ident = Ident::new( - &uniffi_meta::init_callback_fn_symbol_name(&mod_path, &trait_name), - Span::call_site(), - ); + let ffi_converter_tokens = + ffi_converter_callback_interface_impl(&self_ident, &trait_impl_ident, udl_mode); Ok(quote! { - #[doc(hidden)] - static #internals_ident: ::uniffi::ForeignCallbackInternals = ::uniffi::ForeignCallbackInternals::new(); - - #[doc(hidden)] - #[no_mangle] - pub extern "C" fn #init_ident(callback: ::uniffi::ForeignCallback, _: &mut ::uniffi::RustCallStatus) { - #internals_ident.set_callback(callback); - } - #trait_impl + #ffi_converter_tokens + #(#metadata_items)* }) } diff --git a/uniffi_macros/src/export/callback_interface.rs b/uniffi_macros/src/export/callback_interface.rs index 2f2561bbc2..6297a9b289 100644 --- a/uniffi_macros/src/export/callback_interface.rs +++ b/uniffi_macros/src/export/callback_interface.rs @@ -13,34 +13,48 @@ use std::iter; use syn::Ident; pub(super) fn trait_impl( - ident: &Ident, + mod_path: &str, trait_ident: &Ident, - internals_ident: &Ident, items: &[ImplItem], ) -> syn::Result { + let trait_name = ident_to_string(trait_ident); + let trait_impl_ident = trait_impl_ident(&trait_name); + let internals_ident = internals_ident(&trait_name); + let init_ident = Ident::new( + &uniffi_meta::init_callback_fn_symbol_name(&mod_path, &trait_name), + Span::call_site(), + ); + let trait_impl_methods = items .iter() .map(|item| match item { - ImplItem::Method(sig) => gen_method_impl(sig, internals_ident), + ImplItem::Method(sig) => gen_method_impl(sig, &internals_ident), _ => unreachable!("traits have no constructors"), }) .collect::>()?; - let ffi_converter_tokens = ffi_converter_callback_interface_impl(trait_ident, ident, false); - Ok(quote! { + #[doc(hidden)] + static #internals_ident: ::uniffi::ForeignCallbackInternals = ::uniffi::ForeignCallbackInternals::new(); + + #[doc(hidden)] + #[no_mangle] + pub extern "C" fn #init_ident(callback: ::uniffi::ForeignCallback, _: &mut ::uniffi::RustCallStatus) { + #internals_ident.set_callback(callback); + } + #[doc(hidden)] #[derive(Debug)] - struct #ident { + struct #trait_impl_ident { handle: u64, } - impl #ident { + impl #trait_impl_ident { fn new(handle: u64) -> Self { Self { handle } } } - impl ::std::ops::Drop for #ident { + impl ::std::ops::Drop for #trait_impl_ident { fn drop(&mut self) { #internals_ident.invoke_callback::<(), crate::UniFfiTag>( self.handle, uniffi::IDX_CALLBACK_FREE, Default::default() @@ -48,22 +62,37 @@ pub(super) fn trait_impl( } } - ::uniffi::deps::static_assertions::assert_impl_all!(#ident: Send); + ::uniffi::deps::static_assertions::assert_impl_all!(#trait_impl_ident: Send); - impl #trait_ident for #ident { + impl #trait_ident for #trait_impl_ident { #trait_impl_methods } - - #ffi_converter_tokens }) } +pub fn trait_impl_ident(trait_name: &str) -> Ident { + Ident::new( + &format!("UniFFICallbackHandler{trait_name}"), + Span::call_site(), + ) +} + +pub fn internals_ident(trait_name: &str) -> Ident { + Ident::new( + &format!( + "UNIFFI_FOREIGN_CALLBACK_INTERNALS_{}", + trait_name.to_ascii_uppercase() + ), + Span::call_site(), + ) +} + pub fn ffi_converter_callback_interface_impl( trait_ident: &Ident, trait_impl_ident: &Ident, udl_mode: bool, ) -> TokenStream { - let name = ident_to_string(trait_ident); + let trait_name = ident_to_string(trait_ident); let dyn_trait = quote! { dyn #trait_ident }; let box_dyn_trait = quote! { ::std::boxed::Box<#dyn_trait> }; let lift_impl_spec = tagged_impl_header("Lift", &box_dyn_trait, udl_mode); @@ -93,7 +122,7 @@ pub fn ffi_converter_callback_interface_impl( ::uniffi::metadata::codes::TYPE_CALLBACK_INTERFACE, ) .concat_str(#mod_path) - .concat_str(#name); + .concat_str(#trait_name); } unsafe #lift_ref_impl_spec { From ff720413b401ce04977d941831f6c5492bb5a51b Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Wed, 11 Oct 2023 13:45:55 -0400 Subject: [PATCH 21/24] Support foreign implementations of trait interfaces (#1578) Scaffolding: * Generate a struct that implements the trait using a callback interface callback * Make `try_lift` input a callback interface handle and create one of those structs. * Don't use `try_lift` in the trait interface method scaffolding. `try_lift` expects to lift a callback handle, but scaffolding methods are called with a leaked object pointer. * Removed the unused RustCallStatus param from the callback initialization function Kotlin/Python/Swift: * Factored out the callback interface impl and interface/protocol templates so it can also be used for trait interfaces. * Changed the callback interface handle map code so that it doesn't try to re-use the handles. If an object is lowered twice, we now generate two different handles. This is required for trait interfaces, and I think it's also would be the right thing for callback interfaces if they could be passed back into the foreign language from Rust. * Make `lower()` return a callback interface handle. * Added some code to clarify how we generate the protocol and the implementation of that protocol for an object Other: * Trait interfaces are still not supported on Ruby. * Updated the coverall bindings tests to test this. * Updated the traits example, although there's definitely more room for improvement. TODO: I think a better handle solution (#1730) could help with a few things: * We're currently wrapping the object every time it's passed across the FFI. If the foreign code receives a trait object, then passes it back to Rust. Rust now has a handle to the foreign impl and that foreign impl just calls back into Rust. This can lead to some extremely inefficent FFI calls if an object is passed around enough. * The way we're coercing between pointers, usize, and uint64 is probably wrong and at the very least extremely brittle. There should be better tests for reference counts, but I'm waiting until we address the handle issue to implement them. --- CHANGELOG.md | 1 + docs/manual/src/udl/interfaces.md | 18 ++- .../traits/tests/bindings/test_traits.kts | 16 +++ examples/traits/tests/bindings/test_traits.py | 16 ++- .../traits/tests/bindings/test_traits.swift | 18 +++ .../traits/tests/test_generated_bindings.rs | 6 +- .../coverall/tests/bindings/test_coverall.kts | 97 ++++++++++++- .../coverall/tests/bindings/test_coverall.py | 90 +++++++++++- .../tests/bindings/test_coverall.swift | 100 ++++++++++++- .../kotlin/gen_kotlin/callback_interface.rs | 2 +- .../src/bindings/kotlin/gen_kotlin/mod.rs | 27 +++- .../src/bindings/kotlin/gen_kotlin/object.rs | 23 ++- .../kotlin/templates/CallbackInterfaceImpl.kt | 107 ++++++++++++++ .../templates/CallbackInterfaceRuntime.kt | 45 +++--- .../templates/CallbackInterfaceTemplate.kt | 133 ++---------------- .../bindings/kotlin/templates/Interface.kt | 12 ++ .../kotlin/templates/ObjectRuntime.kt | 2 + .../kotlin/templates/ObjectTemplate.kt | 55 ++++---- .../src/bindings/python/gen_python/mod.rs | 20 +++ .../python/templates/CallbackInterfaceImpl.py | 91 ++++++++++++ .../templates/CallbackInterfaceRuntime.py | 34 ++--- .../templates/CallbackInterfaceTemplate.py | 111 ++------------- .../python/templates/ObjectTemplate.py | 45 ++++-- .../src/bindings/python/templates/Protocol.py | 7 + .../src/bindings/python/templates/Types.py | 2 +- .../src/bindings/ruby/gen_ruby/mod.rs | 4 +- .../swift/gen_swift/callback_interface.rs | 12 +- .../src/bindings/swift/gen_swift/mod.rs | 25 +++- .../src/bindings/swift/gen_swift/object.rs | 20 ++- .../templates/CallbackInterfaceImpl.swift | 88 ++++++++++++ .../templates/CallbackInterfaceRuntime.swift | 2 +- .../templates/CallbackInterfaceTemplate.swift | 126 ++--------------- .../swift/templates/ObjectTemplate.swift | 54 ++++--- .../bindings/swift/templates/Protocol.swift | 10 ++ .../src/bindings/swift/templates/macros.swift | 4 +- uniffi_bindgen/src/interface/callbacks.rs | 10 +- uniffi_bindgen/src/interface/ffi.rs | 13 ++ uniffi_bindgen/src/interface/mod.rs | 10 +- uniffi_bindgen/src/interface/object.rs | 21 ++- .../src/export/callback_interface.rs | 4 +- uniffi_macros/src/export/scaffolding.rs | 25 +++- uniffi_macros/src/export/trait_interface.rs | 28 ++-- 42 files changed, 1001 insertions(+), 533 deletions(-) create mode 100644 examples/traits/tests/bindings/test_traits.kts create mode 100644 examples/traits/tests/bindings/test_traits.swift create mode 100644 uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt create mode 100644 uniffi_bindgen/src/bindings/kotlin/templates/Interface.kt create mode 100644 uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py create mode 100644 uniffi_bindgen/src/bindings/python/templates/Protocol.py create mode 100644 uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift create mode 100644 uniffi_bindgen/src/bindings/swift/templates/Protocol.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index a56d07276f..08a98ddd90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ - Error types must now implement `Error + Send + Sync + 'static`. - Proc-macros: The `handle_unknown_callback_error` attribute is no longer needed for callback interface errors +- Foreign types can now implement trait interfaces ### What's Fixed diff --git a/docs/manual/src/udl/interfaces.md b/docs/manual/src/udl/interfaces.md index 6041c9acfa..23db54a8d8 100644 --- a/docs/manual/src/udl/interfaces.md +++ b/docs/manual/src/udl/interfaces.md @@ -122,12 +122,28 @@ fn get_buttons() -> Vec> { ... } fn press(button: Arc) -> Arc { ... } ``` -See the ["traits" example](https://github.com/mozilla/uniffi-rs/tree/main/examples/traits) for more. +### Foreign implementations + +Traits can also be implemented on the foreign side passed into Rust, for example: + +```python +class PyButton(uniffi_module.Button): + def name(self): + return "PyButton" + +uniffi_module.press(PyButton()) +``` + +Note: This is currently supported on Python, Kotlin, and Swift. ### Traits construction Because any number of `struct`s may implement a trait, they don't have constructors. +### Traits example + +See the ["traits" example](https://github.com/mozilla/uniffi-rs/tree/main/examples/traits) for more. + ## Alternate Named Constructors In addition to the default constructor connected to the `::new()` method, you can specify diff --git a/examples/traits/tests/bindings/test_traits.kts b/examples/traits/tests/bindings/test_traits.kts new file mode 100644 index 0000000000..592073b204 --- /dev/null +++ b/examples/traits/tests/bindings/test_traits.kts @@ -0,0 +1,16 @@ +import uniffi.traits.* + +for (button in getButtons()) { + val name = button.name() + // Check that the name is one of the expected values + assert(name in listOf("go", "stop")) + // Check that we can round-trip the button through Rust + assert(press(button).name() == name) +} + +// Test a button implemented in Kotlin +class KtButton : Button { + override fun name() = "KtButton" +} + +assert(press(KtButton()).name() == "KtButton") diff --git a/examples/traits/tests/bindings/test_traits.py b/examples/traits/tests/bindings/test_traits.py index fff64de7d5..70cf2c0b81 100644 --- a/examples/traits/tests/bindings/test_traits.py +++ b/examples/traits/tests/bindings/test_traits.py @@ -1,7 +1,15 @@ from traits import * for button in get_buttons(): - if button.name() in ["go", "stop"]: - press(button) - else: - print("unknown button", button) + name = button.name() + # Check that the name is one of the expected values + assert(name in ["go", "stop"]) + # Check that we can round-trip the button through Rust + assert(press(button).name() == name) + +# Test a button implemented in Python +class PyButton(Button): + def name(self): + return "PyButton" + +assert(press(PyButton()).name() == "PyButton") diff --git a/examples/traits/tests/bindings/test_traits.swift b/examples/traits/tests/bindings/test_traits.swift new file mode 100644 index 0000000000..868bb9449c --- /dev/null +++ b/examples/traits/tests/bindings/test_traits.swift @@ -0,0 +1,18 @@ +import traits + +for button in getButtons() { + let name = button.name() + // Check that the name is one of the expected values + assert(["go", "stop"].contains(name)) + // Check that we can round-trip the button through Rust + assert(press(button: button).name() == name) +} + +// Test a Button implemented in Swift +class SwiftButton: Button { + func name() -> String { + return "SwiftButton" + } +} + +assert(press(button: SwiftButton()).name() == "SwiftButton") diff --git a/examples/traits/tests/test_generated_bindings.rs b/examples/traits/tests/test_generated_bindings.rs index 33d4998351..fc6411434b 100644 --- a/examples/traits/tests/test_generated_bindings.rs +++ b/examples/traits/tests/test_generated_bindings.rs @@ -1 +1,5 @@ -uniffi::build_foreign_language_testcases!("tests/bindings/test_traits.py",); +uniffi::build_foreign_language_testcases!( + "tests/bindings/test_traits.py", + "tests/bindings/test_traits.kts", + "tests/bindings/test_traits.swift", +); diff --git a/fixtures/coverall/tests/bindings/test_coverall.kts b/fixtures/coverall/tests/bindings/test_coverall.kts index 3cefa65783..8bf3b0077b 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.kts +++ b/fixtures/coverall/tests/bindings/test_coverall.kts @@ -210,12 +210,67 @@ Coveralls("test_interfaces_in_dicts").use { coveralls -> assert(coveralls.getRepairs().size == 2) } +Coveralls("test_regressions").use { coveralls -> + assert(coveralls.getStatus("success") == "status: success") +} + +class KotlinGetters : Getters { + override fun getBool(v: Boolean, arg2: Boolean) : Boolean { + return v != arg2 + } + + override fun getString(v: String, arg2: Boolean) : String { + if (v == "too-many-holes") { + throw CoverallException.TooManyHoles("too many holes") + } else if (v == "unexpected-error") { + throw RuntimeException("unexpected error") + } else if (arg2) { + return v.uppercase() + } else { + return v + } + } + + override fun getOption(v: String, arg2: Boolean) : String? { + if (v == "os-error") { + throw ComplexException.OsException(100, 200) + } else if (v == "unknown-error") { + throw ComplexException.UnknownException() + } else if (arg2) { + if (!v.isEmpty()) { + return v.uppercase() + } else { + return null + } + } else { + return v + } + } + + override fun getList(v: List, arg2: Boolean) : List { + if (arg2) { + return v + } else { + return listOf() + } + } + + @Suppress("UNUSED_PARAMETER") + override fun getNothing(v: String) = Unit +} + // Test traits implemented in Rust makeRustGetters().let { rustGetters -> testGetters(rustGetters) testGettersFromKotlin(rustGetters) } +// Test traits implemented in Kotlin +KotlinGetters().let { kotlinGetters -> + testGetters(kotlinGetters) + testGettersFromKotlin(kotlinGetters) +} + fun testGettersFromKotlin(getters: Getters) { assert(getters.getBool(true, true) == false); assert(getters.getBool(true, false) == true); @@ -258,11 +313,27 @@ fun testGettersFromKotlin(getters: Getters) { try { getters.getString("unexpected-error", true) - } catch(e: InternalException) { + } catch(e: Exception) { // Expected } } +class KotlinNode() : NodeTrait { + var currentParent: NodeTrait? = null + + override fun name() = "node-kt" + + override fun setParent(parent: NodeTrait?) { + currentParent = parent + } + + override fun getParent() = currentParent + + override fun strongCount() : ULong { + return 0.toULong() // TODO + } +} + // Test NodeTrait getTraits().let { traits -> assert(traits[0].name() == "node-1") @@ -273,16 +344,32 @@ getTraits().let { traits -> assert(traits[1].name() == "node-2") assert(traits[1].strongCount() == 2UL) + // Note: this doesn't increase the Rust strong count, since we wrap the Rust impl with a + // Swift impl before passing it to `setParent()` traits[0].setParent(traits[1]) assert(ancestorNames(traits[0]) == listOf("node-2")) assert(ancestorNames(traits[1]).isEmpty()) - assert(traits[1].strongCount() == 3UL) + assert(traits[1].strongCount() == 2UL) assert(traits[0].getParent()!!.name() == "node-2") + + val ktNode = KotlinNode() + traits[1].setParent(ktNode) + assert(ancestorNames(traits[0]) == listOf("node-2", "node-kt")) + assert(ancestorNames(traits[1]) == listOf("node-kt")) + assert(ancestorNames(ktNode) == listOf()) + + traits[1].setParent(null) + ktNode.setParent(traits[0]) + assert(ancestorNames(ktNode) == listOf("node-1", "node-2")) + assert(ancestorNames(traits[0]) == listOf("node-2")) + assert(ancestorNames(traits[1]) == listOf()) + + // Unset everything and check that we don't get a memory error + ktNode.setParent(null) traits[0].setParent(null) - Coveralls("test_regressions").use { coveralls -> - assert(coveralls.getStatus("success") == "status: success") - } + // FIXME: We should be calling `NodeTraitImpl.close()` to release the Rust pointer, however that's + // not possible through the `NodeTrait` interface (see #1787). } // This tests that the UniFFI-generated scaffolding doesn't introduce any unexpected locking. diff --git a/fixtures/coverall/tests/bindings/test_coverall.py b/fixtures/coverall/tests/bindings/test_coverall.py index 4f9d1e2df0..17593bc833 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.py +++ b/fixtures/coverall/tests/bindings/test_coverall.py @@ -278,11 +278,68 @@ def test_bytes(self): coveralls = Coveralls("test_bytes") self.assertEqual(coveralls.reverse(b"123"), b"321") +class PyGetters: + def get_bool(self, v, arg2): + return v ^ arg2 + + def get_string(self, v, arg2): + if v == "too-many-holes": + raise CoverallError.TooManyHoles + elif v == "unexpected-error": + raise RuntimeError("unexpected error") + elif arg2: + return v.upper() + else: + return v + + def get_option(self, v, arg2): + if v == "os-error": + raise ComplexError.OsError(100, 200) + elif v == "unknown-error": + raise ComplexError.UnknownError + elif arg2: + if v: + return v.upper() + else: + return None + else: + return v + + def get_list(self, v, arg2): + if arg2: + return v + else: + return [] + + def get_nothing(self, _v): + return None + +class PyNode: + def __init__(self): + self.parent = None + + def name(self): + return "node-py" + + def set_parent(self, parent): + self.parent = parent + + def get_parent(self): + return self.parent + + def strong_count(self): + return 0 # TODO + class TraitsTest(unittest.TestCase): # Test traits implemented in Rust - def test_rust_getters(self): - test_getters(make_rust_getters()) - self.check_getters_from_python(make_rust_getters()) + # def test_rust_getters(self): + # test_getters(None) + # self.check_getters_from_python(make_rust_getters()) + + # Test traits implemented in Rust + def test_python_getters(self): + test_getters(PyGetters()) + #self.check_getters_from_python(PyGetters()) def check_getters_from_python(self, getters): self.assertEqual(getters.get_bool(True, True), False); @@ -316,7 +373,8 @@ def check_getters_from_python(self, getters): with self.assertRaises(InternalError): getters.get_string("unexpected-error", True) - def test_node(self): + def test_path(self): + # Get traits creates 2 objects that implement the trait traits = get_traits() self.assertEqual(traits[0].name(), "node-1") # Note: strong counts are 1 more than you might expect, because the strong_count() method @@ -326,11 +384,33 @@ def test_node(self): self.assertEqual(traits[1].name(), "node-2") self.assertEqual(traits[1].strong_count(), 2) + # Let's try connecting them together traits[0].set_parent(traits[1]) + # Note: this doesn't increase the Rust strong count, since we wrap the Rust impl with a + # python impl before passing it to `set_parent()` + self.assertEqual(traits[1].strong_count(), 2) self.assertEqual(ancestor_names(traits[0]), ["node-2"]) self.assertEqual(ancestor_names(traits[1]), []) - self.assertEqual(traits[1].strong_count(), 3) self.assertEqual(traits[0].get_parent().name(), "node-2") + + # Throw in a Python implementation of the trait + # The ancestry chain now goes traits[0] -> traits[1] -> py_node + py_node = PyNode() + traits[1].set_parent(py_node) + self.assertEqual(ancestor_names(traits[0]), ["node-2", "node-py"]) + self.assertEqual(ancestor_names(traits[1]), ["node-py"]) + self.assertEqual(ancestor_names(py_node), []) + + # Rotating things. + # The ancestry chain now goes py_node -> traits[0] -> traits[1] + traits[1].set_parent(None) + py_node.set_parent(traits[0]) + self.assertEqual(ancestor_names(py_node), ["node-1", "node-2"]) + self.assertEqual(ancestor_names(traits[0]), ["node-2"]) + self.assertEqual(ancestor_names(traits[1]), []) + + # Make sure we don't crash when undoing it all + py_node.set_parent(None) traits[0].set_parent(None) if __name__=='__main__': diff --git a/fixtures/coverall/tests/bindings/test_coverall.swift b/fixtures/coverall/tests/bindings/test_coverall.swift index 8db21ad78a..c6fcba4290 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.swift +++ b/fixtures/coverall/tests/bindings/test_coverall.swift @@ -248,11 +248,56 @@ do { assert(coveralls.reverse(value: Data("123".utf8)) == Data("321".utf8)) } + +struct SomeOtherError: Error { } + + +class SwiftGetters: Getters { + func getBool(v: Bool, arg2: Bool) -> Bool { v != arg2 } + func getString(v: String, arg2: Bool) throws -> String { + if v == "too-many-holes" { + throw CoverallError.TooManyHoles(message: "Too many") + } + if v == "unexpected-error" { + throw SomeOtherError() + } + return arg2 ? "HELLO" : v + } + func getOption(v: String, arg2: Bool) throws -> String? { + if v == "os-error" { + throw ComplexError.OsError(code: 100, extendedCode: 200) + } + if v == "unknown-error" { + throw ComplexError.UnknownError + } + if arg2 { + if !v.isEmpty { + return v.uppercased() + } else { + return nil + } + } else { + return v + } + } + func getList(v: [Int32], arg2: Bool) -> [Int32] { arg2 ? v : [] } + func getNothing(v: String) -> () { + } +} + + // Test traits implemented in Rust do { - let rustGetters = makeRustGetters() - testGetters(g: rustGetters) - testGettersFromSwift(getters: rustGetters) + let getters = makeRustGetters() + testGetters(g: getters) + testGettersFromSwift(getters: getters) +} + +// Test traits implemented in Swift +do { + let getters = SwiftGetters() + testGetters(g: getters) + testGettersFromSwift(getters: getters) } func testGettersFromSwift(getters: Getters) { @@ -283,7 +328,7 @@ func testGettersFromSwift(getters: Getters) { } do { - try getters.getOption(v: "os-error", arg2: true) + let _ = try getters.getOption(v: "os-error", arg2: true) fatalError("should have thrown") } catch ComplexError.OsError(let code, let extendedCode) { assert(code == 100) @@ -293,7 +338,7 @@ func testGettersFromSwift(getters: Getters) { } do { - try getters.getOption(v: "unknown-error", arg2: true) + let _ = try getters.getOption(v: "unknown-error", arg2: true) fatalError("should have thrown") } catch ComplexError.UnknownError { // Expected @@ -302,12 +347,32 @@ func testGettersFromSwift(getters: Getters) { } do { - try getters.getString(v: "unexpected-error", arg2: true) + let _ = try getters.getString(v: "unexpected-error", arg2: true) } catch { // Expected } } +class SwiftNode: NodeTrait { + var p: NodeTrait? = nil + + func name() -> String { + return "node-swift" + } + + func setParent(parent: NodeTrait?) { + self.p = parent + } + + func getParent() -> NodeTrait? { + return self.p + } + + func strongCount() -> UInt64 { + return 0 // TODO + } +} + // Test Node trait do { let traits = getTraits() @@ -319,10 +384,31 @@ do { assert(traits[1].name() == "node-2") assert(traits[1].strongCount() == 2) + // Note: this doesn't increase the Rust strong count, since we wrap the Rust impl with a + // Swift impl before passing it to `set_parent()` traits[0].setParent(parent: traits[1]) assert(ancestorNames(node: traits[0]) == ["node-2"]) assert(ancestorNames(node: traits[1]) == []) - assert(traits[1].strongCount() == 3) + assert(traits[1].strongCount() == 2) assert(traits[0].getParent()!.name() == "node-2") + + // Throw in a Swift implementation of the trait + // The ancestry chain now goes traits[0] -> traits[1] -> swiftNode + let swiftNode = SwiftNode() + traits[1].setParent(parent: swiftNode) + assert(ancestorNames(node: traits[0]) == ["node-2", "node-swift"]) + assert(ancestorNames(node: traits[1]) == ["node-swift"]) + assert(ancestorNames(node: swiftNode) == []) + + // Rotating things. + // The ancestry chain now goes swiftNode -> traits[0] -> traits[1] + traits[1].setParent(parent: nil) + swiftNode.setParent(parent: traits[0]) + assert(ancestorNames(node: swiftNode) == ["node-1", "node-2"]) + assert(ancestorNames(node: traits[0]) == ["node-2"]) + assert(ancestorNames(node: traits[1]) == []) + + // Make sure we don't crash when undoing it all + swiftNode.setParent(parent: nil) traits[0].setParent(parent: nil) } diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/callback_interface.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/callback_interface.rs index 65afbef276..17a08745b4 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/callback_interface.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/callback_interface.rs @@ -29,6 +29,6 @@ impl CodeType for CallbackInterfaceCodeType { } fn initialization_fn(&self) -> Option { - Some(format!("{}.register", self.ffi_converter_name())) + Some(format!("uniffiCallbackInterface{}.register", self.id)) } } diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs index 2461b590d3..ef2cccfebd 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs @@ -313,6 +313,27 @@ impl KotlinCodeOracle { FfiType::RustFutureContinuationData => "USize".to_string(), } } + + /// Get the name of the interface and class name for an object. + /// + /// This depends on the `ObjectImpl`: + /// + /// For struct impls, the class name is the object name and the interface name is derived from that. + /// For trait impls, the interface name is the object name, and the class name is derived from that. + /// + /// This split is needed because of the `FfiConverter` interface. For struct impls, `lower` + /// can only lower the concrete class. For trait impls, `lower` can lower anything that + /// implement the interface. + fn object_names(&self, obj: &Object) -> (String, String) { + let class_name = self.class_name(obj.name()); + match obj.imp() { + ObjectImpl::Struct => (format!("{class_name}Interface"), class_name), + ObjectImpl::Trait => { + let interface_name = format!("{class_name}Impl"); + (class_name, interface_name) + } + } + } } pub trait AsCodeType { @@ -347,7 +368,7 @@ impl AsCodeType for T { Type::Duration => Box::new(miscellany::DurationCodeType), Type::Enum { name, .. } => Box::new(enum_::EnumCodeType::new(name)), - Type::Object { name, .. } => Box::new(object::ObjectCodeType::new(name)), + Type::Object { name, imp, .. } => Box::new(object::ObjectCodeType::new(name, imp)), Type::Record { name, .. } => Box::new(record::RecordCodeType::new(name)), Type::CallbackInterface { name, .. } => { Box::new(callback_interface::CallbackInterfaceCodeType::new(name)) @@ -520,6 +541,10 @@ pub mod filters { .ffi_converter_name()) } + pub fn object_names(obj: &Object) -> Result<(String, String), askama::Error> { + Ok(KotlinCodeOracle.object_names(obj)) + } + /// Remove the "`" chars we put around function/variable names /// /// These are used to avoid name clashes with kotlin identifiers, but sometimes you want to diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/object.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/object.rs index 16fa0f2403..c6c42194ac 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/object.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/object.rs @@ -2,29 +2,40 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use crate::backend::{CodeType, Literal}; +use crate::{ + backend::{CodeType, Literal}, + interface::ObjectImpl, +}; #[derive(Debug)] pub struct ObjectCodeType { - id: String, + name: String, + imp: ObjectImpl, } impl ObjectCodeType { - pub fn new(id: String) -> Self { - Self { id } + pub fn new(name: String, imp: ObjectImpl) -> Self { + Self { name, imp } } } impl CodeType for ObjectCodeType { fn type_label(&self) -> String { - super::KotlinCodeOracle.class_name(&self.id) + super::KotlinCodeOracle.class_name(&self.name) } fn canonical_name(&self) -> String { - format!("Type{}", self.id) + format!("Type{}", self.name) } fn literal(&self, _literal: &Literal) -> String { unreachable!(); } + + fn initialization_fn(&self) -> Option { + match &self.imp { + ObjectImpl::Struct => None, + ObjectImpl::Trait => Some(format!("uniffiCallbackInterface{}.register", self.name)), + } + } } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt new file mode 100644 index 0000000000..f1c58ee971 --- /dev/null +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt @@ -0,0 +1,107 @@ +{% if self.include_once_check("CallbackInterfaceRuntime.kt") %}{% include "CallbackInterfaceRuntime.kt" %}{% endif %} + +// Implement the foreign callback handler for {{ interface_name }} +internal class {{ callback_handler_class }} : ForeignCallback { + @Suppress("TooGenericExceptionCaught") + override fun invoke(handle: Handle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int { + val cb = {{ ffi_converter_name }}.handleMap.get(handle) + return when (method) { + IDX_CALLBACK_FREE -> { + {{ ffi_converter_name }}.handleMap.remove(handle) + + // Successful return + // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` + UNIFFI_CALLBACK_SUCCESS + } + {% for meth in methods.iter() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} + {{ loop.index }} -> { + // Call the method, write to outBuf and return a status code + // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` for info + try { + this.{{ method_name }}(cb, argsData, argsLen, outBuf) + } catch (e: Throwable) { + // Unexpected error + try { + // Try to serialize the error into a string + outBuf.setValue({{ Type::String.borrow()|ffi_converter_name }}.lower(e.toString())) + } catch (e: Throwable) { + // If that fails, then it's time to give up and just return + } + UNIFFI_CALLBACK_UNEXPECTED_ERROR + } + } + {% endfor %} + else -> { + // An unexpected error happened. + // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` + try { + // Try to serialize the error into a string + outBuf.setValue({{ Type::String.borrow()|ffi_converter_name }}.lower("Invalid Callback index")) + } catch (e: Throwable) { + // If that fails, then it's time to give up and just return + } + UNIFFI_CALLBACK_UNEXPECTED_ERROR + } + } + } + + {% for meth in methods.iter() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name %} + @Suppress("UNUSED_PARAMETER") + private fun {{ method_name }}(kotlinCallbackInterface: {{ interface_name }}, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int { + {%- if meth.arguments().len() > 0 %} + val argsBuf = argsData.getByteBuffer(0, argsLen.toLong()).also { + it.order(ByteOrder.BIG_ENDIAN) + } + {%- endif %} + + {%- match meth.return_type() %} + {%- when Some with (return_type) %} + fun makeCall() : Int { + val returnValue = kotlinCallbackInterface.{{ meth.name()|fn_name }}( + {%- for arg in meth.arguments() %} + {{ arg|read_fn }}(argsBuf) + {% if !loop.last %}, {% endif %} + {%- endfor %} + ) + outBuf.setValue({{ return_type|ffi_converter_name }}.lowerIntoRustBuffer(returnValue)) + return UNIFFI_CALLBACK_SUCCESS + } + {%- when None %} + fun makeCall() : Int { + kotlinCallbackInterface.{{ meth.name()|fn_name }}( + {%- for arg in meth.arguments() %} + {{ arg|read_fn }}(argsBuf) + {%- if !loop.last %}, {% endif %} + {%- endfor %} + ) + return UNIFFI_CALLBACK_SUCCESS + } + {%- endmatch %} + + {%- match meth.throws_type() %} + {%- when None %} + fun makeCallAndHandleError() : Int = makeCall() + {%- when Some(error_type) %} + fun makeCallAndHandleError() : Int = try { + makeCall() + } catch (e: {{ error_type|error_type_name }}) { + // Expected error, serialize it into outBuf + outBuf.setValue({{ error_type|ffi_converter_name }}.lowerIntoRustBuffer(e)) + UNIFFI_CALLBACK_ERROR + } + {%- endmatch %} + + return makeCallAndHandleError() + } + {% endfor %} + + // Registers the foreign callback with the Rust side. + // This method is generated for each callback interface. + internal fun register(lib: _UniFFILib) { + lib.{{ ffi_init_callback.name() }}(this) + } +} + +internal val {{ callback_handler_obj }} = {{ callback_handler_class }}() diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt index 62a71e02f1..d0e0686322 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt @@ -1,7 +1,10 @@ +{{- self.add_import("java.util.concurrent.atomic.AtomicLong") }} +{{- self.add_import("java.util.concurrent.locks.ReentrantLock") }} +{{- self.add_import("kotlin.concurrent.withLock") }} + internal typealias Handle = Long internal class ConcurrentHandleMap( private val leftMap: MutableMap = mutableMapOf(), - private val rightMap: MutableMap = mutableMapOf() ) { private val lock = java.util.concurrent.locks.ReentrantLock() private val currentHandle = AtomicLong(0L) @@ -9,16 +12,14 @@ internal class ConcurrentHandleMap( fun insert(obj: T): Handle = lock.withLock { - rightMap[obj] ?: - currentHandle.getAndAdd(stride) - .also { handle -> - leftMap[handle] = obj - rightMap[obj] = handle - } + currentHandle.getAndAdd(stride) + .also { handle -> + leftMap[handle] = obj + } } fun get(handle: Handle) = lock.withLock { - leftMap[handle] + leftMap[handle] ?: throw InternalException("No callback in handlemap; this is a Uniffi bug") } fun delete(handle: Handle) { @@ -27,15 +28,12 @@ internal class ConcurrentHandleMap( fun remove(handle: Handle): T? = lock.withLock { - leftMap.remove(handle)?.let { obj -> - rightMap.remove(obj) - obj - } + leftMap.remove(handle) } } interface ForeignCallback : com.sun.jna.Callback { - public fun callback(handle: Handle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int + public fun invoke(handle: Handle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int } // Magic number for the Rust proxy to call using the same mechanism as every other method, @@ -46,29 +44,20 @@ internal const val UNIFFI_CALLBACK_SUCCESS = 0 internal const val UNIFFI_CALLBACK_ERROR = 1 internal const val UNIFFI_CALLBACK_UNEXPECTED_ERROR = 2 -public abstract class FfiConverterCallbackInterface( - protected val foreignCallback: ForeignCallback -): FfiConverter { - private val handleMap = ConcurrentHandleMap() +public abstract class FfiConverterCallbackInterface: FfiConverter { + internal val handleMap = ConcurrentHandleMap() - // Registers the foreign callback with the Rust side. - // This method is generated for each callback interface. - internal abstract fun register(lib: _UniFFILib) - - fun drop(handle: Handle): RustBuffer.ByValue { - return handleMap.remove(handle).let { RustBuffer.ByValue() } + internal fun drop(handle: Handle) { + handleMap.remove(handle) } override fun lift(value: Handle): CallbackInterface { - return handleMap.get(value) ?: throw InternalException("No callback in handlemap; this is a Uniffi bug") + return handleMap.get(value) } override fun read(buf: ByteBuffer) = lift(buf.getLong()) - override fun lower(value: CallbackInterface) = - handleMap.insert(value).also { - assert(handleMap.get(it) === value) { "Handle map is not returning the object we just placed there. This is a bug in the HandleMap." } - } + override fun lower(value: CallbackInterface) = handleMap.insert(value) override fun allocationSize(value: CallbackInterface) = 8 diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt index 56ae558544..59a127b1a2 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt @@ -1,129 +1,12 @@ {%- let cbi = ci|get_callback_interface_definition(name) %} -{%- let type_name = cbi|type_name %} -{%- let foreign_callback = format!("ForeignCallback{}", canonical_type_name) %} +{%- let callback_handler_class = format!("UniffiCallbackInterface{}", name) %} +{%- let callback_handler_obj = format!("uniffiCallbackInterface{}", name) %} +{%- let ffi_init_callback = cbi.ffi_init_callback() %} +{%- let interface_name = cbi|type_name %} +{%- let methods = cbi.methods() %} -{% if self.include_once_check("CallbackInterfaceRuntime.kt") %}{% include "CallbackInterfaceRuntime.kt" %}{% endif %} -{{- self.add_import("java.util.concurrent.atomic.AtomicLong") }} -{{- self.add_import("java.util.concurrent.locks.ReentrantLock") }} -{{- self.add_import("kotlin.concurrent.withLock") }} - -// Declaration and FfiConverters for {{ type_name }} Callback Interface - -public interface {{ type_name }} { - {% for meth in cbi.methods() -%} - fun {{ meth.name()|fn_name }}({% call kt::arg_list_decl(meth) %}) - {%- match meth.return_type() -%} - {%- when Some with (return_type) %}: {{ return_type|type_name -}} - {%- else -%} - {%- endmatch %} - {% endfor %} - companion object -} - -// The ForeignCallback that is passed to Rust. -internal class {{ foreign_callback }} : ForeignCallback { - @Suppress("TooGenericExceptionCaught") - override fun callback(handle: Handle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int { - val cb = {{ ffi_converter_name }}.lift(handle) - return when (method) { - IDX_CALLBACK_FREE -> { - {{ ffi_converter_name }}.drop(handle) - // Successful return - // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - UNIFFI_CALLBACK_SUCCESS - } - {% for meth in cbi.methods() -%} - {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} - {{ loop.index }} -> { - // Call the method, write to outBuf and return a status code - // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` for info - try { - this.{{ method_name }}(cb, argsData, argsLen, outBuf) - } catch (e: Throwable) { - // Unexpected error - try { - // Try to serialize the error into a string - outBuf.setValue({{ Type::String.borrow()|ffi_converter_name }}.lower(e.toString())) - } catch (e: Throwable) { - // If that fails, then it's time to give up and just return - } - UNIFFI_CALLBACK_UNEXPECTED_ERROR - } - } - {% endfor %} - else -> { - // An unexpected error happened. - // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - try { - // Try to serialize the error into a string - outBuf.setValue({{ Type::String.borrow()|ffi_converter_name }}.lower("Invalid Callback index")) - } catch (e: Throwable) { - // If that fails, then it's time to give up and just return - } - UNIFFI_CALLBACK_UNEXPECTED_ERROR - } - } - } - - {% for meth in cbi.methods() -%} - {% let method_name = format!("invoke_{}", meth.name())|fn_name %} - @Suppress("UNUSED_PARAMETER") - private fun {{ method_name }}(kotlinCallbackInterface: {{ type_name }}, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int { - {%- if meth.arguments().len() > 0 %} - val argsBuf = argsData.getByteBuffer(0, argsLen.toLong()).also { - it.order(ByteOrder.BIG_ENDIAN) - } - {%- endif %} - - {%- match meth.return_type() %} - {%- when Some with (return_type) %} - fun makeCall() : Int { - val returnValue = kotlinCallbackInterface.{{ meth.name()|fn_name }}( - {%- for arg in meth.arguments() %} - {{ arg|read_fn }}(argsBuf) - {% if !loop.last %}, {% endif %} - {%- endfor %} - ) - outBuf.setValue({{ return_type|ffi_converter_name }}.lowerIntoRustBuffer(returnValue)) - return UNIFFI_CALLBACK_SUCCESS - } - {%- when None %} - fun makeCall() : Int { - kotlinCallbackInterface.{{ meth.name()|fn_name }}( - {%- for arg in meth.arguments() %} - {{ arg|read_fn }}(argsBuf) - {%- if !loop.last %}, {% endif %} - {%- endfor %} - ) - return UNIFFI_CALLBACK_SUCCESS - } - {%- endmatch %} - - {%- match meth.throws_type() %} - {%- when None %} - fun makeCallAndHandleError() : Int = makeCall() - {%- when Some(error_type) %} - fun makeCallAndHandleError() : Int = try { - makeCall() - } catch (e: {{ error_type|error_type_name }}) { - // Expected error, serialize it into outBuf - outBuf.setValue({{ error_type|ffi_converter_name }}.lowerIntoRustBuffer(e)) - UNIFFI_CALLBACK_ERROR - } - {%- endmatch %} - - return makeCallAndHandleError() - } - {% endfor %} -} +{% include "Interface.kt" %} +{% include "CallbackInterfaceImpl.kt" %} // The ffiConverter which transforms the Callbacks in to Handles to pass to Rust. -public object {{ ffi_converter_name }}: FfiConverterCallbackInterface<{{ type_name }}>( - foreignCallback = {{ foreign_callback }}() -) { - override fun register(lib: _UniFFILib) { - rustCall() { status -> - lib.{{ cbi.ffi_init_callback().name() }}(this.foreignCallback, status) - } - } -} +public object {{ ffi_converter_name }}: FfiConverterCallbackInterface<{{ interface_name }}>() diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Interface.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Interface.kt new file mode 100644 index 0000000000..c8610d4d65 --- /dev/null +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Interface.kt @@ -0,0 +1,12 @@ +public interface {{ interface_name }} { + {% for meth in methods.iter() -%} + {% if meth.is_async() -%}suspend {% endif -%} + fun {{ meth.name()|fn_name }}({% call kt::arg_list_decl(meth) %}) + {%- match meth.return_type() -%} + {%- when Some with (return_type) %}: {{ return_type|type_name -}} + {%- else -%} + {%- endmatch %} + {% endfor %} + companion object +} + diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt index b9352c690f..1e3ead5a7e 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt @@ -1,3 +1,5 @@ +{{- self.add_import("java.util.concurrent.atomic.AtomicLong") }} +{{- self.add_import("java.util.concurrent.atomic.AtomicBoolean") }} // Interface implemented by anything that can contain an object reference. // // Such types expose a `destroy()` method that must be called to cleanly diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt index 5cbb5d5d93..0aaf0f948c 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt @@ -1,32 +1,13 @@ {%- let obj = ci|get_object_definition(name) %} {%- if self.include_once_check("ObjectRuntime.kt") %}{% include "ObjectRuntime.kt" %}{% endif %} -{{- self.add_import("java.util.concurrent.atomic.AtomicLong") }} -{{- self.add_import("java.util.concurrent.atomic.AtomicBoolean") }} +{%- let (interface_name, impl_class_name) = obj|object_names %} +{%- let methods = obj.methods() %} -public interface {{ type_name }}Interface { - {% for meth in obj.methods() -%} - {%- match meth.throws_type() -%} - {%- when Some with (throwable) -%} - @Throws({{ throwable|error_type_name }}::class) - {%- when None -%} - {%- endmatch %} - {% if meth.is_async() -%} - suspend fun {{ meth.name()|fn_name }}({% call kt::arg_list_decl(meth) %}) - {%- else -%} - fun {{ meth.name()|fn_name }}({% call kt::arg_list_decl(meth) %}) - {%- endif %} - {%- match meth.return_type() -%} - {%- when Some with (return_type) %}: {{ return_type|type_name -}} - {%- when None -%} - {%- endmatch -%} - - {% endfor %} - companion object -} +{% include "Interface.kt" %} -class {{ type_name }}( +class {{ impl_class_name }}( pointer: Pointer -) : FFIObject(pointer), {{ type_name }}Interface { +) : FFIObject(pointer), {{ interface_name }}{ {%- match obj.primary_constructor() %} {%- when Some with (cons) %} @@ -106,8 +87,8 @@ class {{ type_name }}( {% if !obj.alternate_constructors().is_empty() -%} companion object { {% for cons in obj.alternate_constructors() -%} - fun {{ cons.name()|fn_name }}({% call kt::arg_list_decl(cons) %}): {{ type_name }} = - {{ type_name }}({% call kt::to_ffi_call(cons) %}) + fun {{ cons.name()|fn_name }}({% call kt::arg_list_decl(cons) %}): {{ impl_class_name }} = + {{ impl_class_name }}({% call kt::to_ffi_call(cons) %}) {% endfor %} } {% else %} @@ -115,11 +96,29 @@ class {{ type_name }}( {% endif %} } +{%- if obj.is_trait_interface() %} +{%- let callback_handler_class = format!("UniffiCallbackInterface{}", name) %} +{%- let callback_handler_obj = format!("uniffiCallbackInterface{}", name) %} +{%- let ffi_init_callback = obj.ffi_init_callback() %} +{% include "CallbackInterfaceImpl.kt" %} +{%- endif %} + public object {{ obj|ffi_converter_name }}: FfiConverter<{{ type_name }}, Pointer> { - override fun lower(value: {{ type_name }}): Pointer = value.callWithPointer { it } + {%- if obj.is_trait_interface() %} + internal val handleMap = ConcurrentHandleMap<{{ interface_name }}>() + {%- endif %} + + override fun lower(value: {{ type_name }}): Pointer { + {%- match obj.imp() %} + {%- when ObjectImpl::Struct %} + return value.callWithPointer { it } + {%- when ObjectImpl::Trait %} + return Pointer(handleMap.insert(value)) + {%- endmatch %} + } override fun lift(value: Pointer): {{ type_name }} { - return {{ type_name }}(value) + return {{ impl_class_name }}(value) } override fun read(buf: ByteBuffer): {{ type_name }} { diff --git a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs index 78a59c1b33..0cd858b476 100644 --- a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs +++ b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs @@ -314,6 +314,21 @@ impl PythonCodeOracle { FfiType::RustFutureContinuationData => "ctypes.c_size_t".to_string(), } } + + /// Get the name of the protocol and class name for an object. + /// + /// For struct impls, the class name is the object name and the protocol name is derived from that. + /// For trait impls, the protocol name is the object name, and the class name is derived from that. + fn object_names(&self, obj: &Object) -> (String, String) { + let class_name = self.class_name(obj.name()); + match obj.imp() { + ObjectImpl::Struct => (format!("{class_name}Protocol"), class_name), + ObjectImpl::Trait => { + let protocol_name = format!("{class_name}Impl"); + (class_name, protocol_name) + } + } + } } pub trait AsCodeType { @@ -434,4 +449,9 @@ pub mod filters { pub fn enum_variant_py(nm: &str) -> Result { Ok(PythonCodeOracle.enum_variant_name(nm)) } + + /// Get the idiomatic Python rendering of an individual enum variant. + pub fn object_names(obj: &Object) -> Result<(String, String), askama::Error> { + Ok(PythonCodeOracle.object_names(obj)) + } } diff --git a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py new file mode 100644 index 0000000000..f9a4768a5c --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py @@ -0,0 +1,91 @@ +{% if self.include_once_check("CallbackInterfaceRuntime.py") %}{% include "CallbackInterfaceRuntime.py" %}{% endif %} + +# Declaration and _UniffiConverters for {{ type_name }} Callback Interface + +def {{ callback_handler_class }}(handle, method, args_data, args_len, buf_ptr): + {% for meth in methods.iter() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name %} + def {{ method_name }}(python_callback, args_stream, buf_ptr): + {#- Unpacking args from the _UniffiRustBuffer #} + def makeCall(): + {#- Calling the concrete callback object #} + {%- if meth.arguments().len() != 0 -%} + return python_callback.{{ meth.name()|fn_name }}( + {% for arg in meth.arguments() -%} + {{ arg|read_fn }}(args_stream) + {%- if !loop.last %}, {% endif %} + {% endfor -%} + ) + {%- else %} + return python_callback.{{ meth.name()|fn_name }}() + {%- endif %} + + def makeCallAndHandleReturn(): + {%- match meth.return_type() %} + {%- when Some(return_type) %} + rval = makeCall() + with _UniffiRustBuffer.alloc_with_builder() as builder: + {{ return_type|write_fn }}(rval, builder) + buf_ptr[0] = builder.finalize() + {%- when None %} + makeCall() + {%- endmatch %} + return _UNIFFI_CALLBACK_SUCCESS + + {%- match meth.throws_type() %} + {%- when None %} + return makeCallAndHandleReturn() + {%- when Some(err) %} + try: + return makeCallAndHandleReturn() + except {{ err|type_name }} as e: + # Catch errors declared in the UDL file + with _UniffiRustBuffer.alloc_with_builder() as builder: + {{ err|write_fn }}(e, builder) + buf_ptr[0] = builder.finalize() + return _UNIFFI_CALLBACK_ERROR + {%- endmatch %} + + {% endfor %} + + cb = {{ ffi_converter_name }}._handle_map.get(handle) + + if method == IDX_CALLBACK_FREE: + {{ ffi_converter_name }}._handle_map.remove(handle) + + # Successfull return + # See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` + return _UNIFFI_CALLBACK_SUCCESS + + {% for meth in methods.iter() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} + if method == {{ loop.index }}: + # Call the method and handle any errors + # See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` for details + try: + return {{ method_name }}(cb, _UniffiRustBufferStream(args_data, args_len), buf_ptr) + except BaseException as e: + # Catch unexpected errors + try: + # Try to serialize the exception into a String + buf_ptr[0] = {{ Type::String.borrow()|lower_fn }}(repr(e)) + except: + # If that fails, just give up + pass + return _UNIFFI_CALLBACK_UNEXPECTED_ERROR + {% endfor %} + + # This should never happen, because an out of bounds method index won't + # ever be used. Once we can catch errors, we should return an InternalException. + # https://github.com/mozilla/uniffi-rs/issues/351 + + # An unexpected error happened. + # See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` + return _UNIFFI_CALLBACK_UNEXPECTED_ERROR + +# We need to keep this function reference alive: +# if they get GC'd while in use then UniFFI internals could attempt to call a function +# that is in freed memory. +# That would be...uh...bad. Yeah, that's the word. Bad. +{{ callback_handler_obj }} = _UNIFFI_FOREIGN_CALLBACK_T({{ callback_handler_class }}) +_UniffiLib.{{ ffi_init_callback.name() }}({{ callback_handler_obj }}) diff --git a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py index 0fe2ab8dc0..1b7346ba4c 100644 --- a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py +++ b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py @@ -8,33 +8,29 @@ class ConcurrentHandleMap: def __init__(self): # type Handle = int self._left_map = {} # type: Dict[Handle, Any] - self._right_map = {} # type: Dict[Any, Handle] self._lock = threading.Lock() self._current_handle = 0 self._stride = 1 - def insert(self, obj): with self._lock: - if obj in self._right_map: - return self._right_map[obj] - else: - handle = self._current_handle - self._current_handle += self._stride - self._left_map[handle] = obj - self._right_map[obj] = handle - return handle + handle = self._current_handle + self._current_handle += self._stride + self._left_map[handle] = obj + return handle def get(self, handle): with self._lock: - return self._left_map.get(handle) + obj = self._left_map.get(handle) + if not obj: + raise InternalError("No callback in handlemap; this is a uniffi bug") + return obj def remove(self, handle): with self._lock: if handle in self._left_map: obj = self._left_map.pop(handle) - del self._right_map[obj] return obj # Magic number for the Rust proxy to call using the same mechanism as every other method, @@ -45,22 +41,12 @@ def remove(self, handle): _UNIFFI_CALLBACK_ERROR = 1 _UNIFFI_CALLBACK_UNEXPECTED_ERROR = 2 -class _UniffiConverterCallbackInterface: +class UniffiCallbackInterfaceFfiConverter: _handle_map = ConcurrentHandleMap() - def __init__(self, cb): - self._foreign_callback = cb - - def drop(self, handle): - self.__class__._handle_map.remove(handle) - @classmethod def lift(cls, handle): - obj = cls._handle_map.get(handle) - if not obj: - raise InternalError("The object in the handle map has been dropped already") - - return obj + return cls._handle_map.get(handle) @classmethod def read(cls, buf): diff --git a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py index e0e926757a..dbfa094562 100644 --- a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py @@ -1,105 +1,12 @@ -{%- let cbi = ci|get_callback_interface_definition(id) %} -{%- let foreign_callback = format!("foreignCallback{}", canonical_type_name) %} +{%- let cbi = ci|get_callback_interface_definition(name) %} +{%- let callback_handler_class = format!("UniffiCallbackInterface{}", name) %} +{%- let callback_handler_obj = format!("uniffiCallbackInterface{}", name) %} +{%- let ffi_init_callback = cbi.ffi_init_callback() %} +{%- let protocol_name = type_name.clone() %} +{%- let methods = cbi.methods() %} -{% if self.include_once_check("CallbackInterfaceRuntime.py") %}{% include "CallbackInterfaceRuntime.py" %}{% endif %} - -# Declaration and _UniffiConverters for {{ type_name }} Callback Interface - -class {{ type_name }}: - {% for meth in cbi.methods() -%} - def {{ meth.name()|fn_name }}(self, {% call py::arg_list_decl(meth) %}): - raise NotImplementedError - - {% endfor %} - -def py_{{ foreign_callback }}(handle, method, args_data, args_len, buf_ptr): - {% for meth in cbi.methods() -%} - {% let method_name = format!("invoke_{}", meth.name())|fn_name %} - def {{ method_name }}(python_callback, args_stream, buf_ptr): - {#- Unpacking args from the _UniffiRustBuffer #} - def makeCall(): - {#- Calling the concrete callback object #} - {%- if meth.arguments().len() != 0 -%} - return python_callback.{{ meth.name()|fn_name }}( - {% for arg in meth.arguments() -%} - {{ arg|read_fn }}(args_stream) - {%- if !loop.last %}, {% endif %} - {% endfor -%} - ) - {%- else %} - return python_callback.{{ meth.name()|fn_name }}() - {%- endif %} - - def makeCallAndHandleReturn(): - {%- match meth.return_type() %} - {%- when Some(return_type) %} - rval = makeCall() - with _UniffiRustBuffer.alloc_with_builder() as builder: - {{ return_type|write_fn }}(rval, builder) - buf_ptr[0] = builder.finalize() - {%- when None %} - makeCall() - {%- endmatch %} - return _UNIFFI_CALLBACK_SUCCESS - - {%- match meth.throws_type() %} - {%- when None %} - return makeCallAndHandleReturn() - {%- when Some(err) %} - try: - return makeCallAndHandleReturn() - except {{ err|type_name }} as e: - # Catch errors declared in the UDL file - with _UniffiRustBuffer.alloc_with_builder() as builder: - {{ err|write_fn }}(e, builder) - buf_ptr[0] = builder.finalize() - return _UNIFFI_CALLBACK_ERROR - {%- endmatch %} - - {% endfor %} - - cb = {{ ffi_converter_name }}.lift(handle) - if not cb: - raise InternalError("No callback in handlemap; this is a uniffi bug") - - if method == IDX_CALLBACK_FREE: - {{ ffi_converter_name }}.drop(handle) - # Successfull return - # See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - return _UNIFFI_CALLBACK_SUCCESS - - {% for meth in cbi.methods() -%} - {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} - if method == {{ loop.index }}: - # Call the method and handle any errors - # See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` for details - try: - return {{ method_name }}(cb, _UniffiRustBufferStream(args_data, args_len), buf_ptr) - except BaseException as e: - # Catch unexpected errors - try: - # Try to serialize the exception into a String - buf_ptr[0] = {{ Type::String.borrow()|lower_fn }}(repr(e)) - except: - # If that fails, just give up - pass - return _UNIFFI_CALLBACK_UNEXPECTED_ERROR - {% endfor %} - - # This should never happen, because an out of bounds method index won't - # ever be used. Once we can catch errors, we should return an InternalException. - # https://github.com/mozilla/uniffi-rs/issues/351 - - # An unexpected error happened. - # See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - return _UNIFFI_CALLBACK_UNEXPECTED_ERROR - -# We need to keep this function reference alive: -# if they get GC'd while in use then UniFFI internals could attempt to call a function -# that is in freed memory. -# That would be...uh...bad. Yeah, that's the word. Bad. -{{ foreign_callback }} = _UNIFFI_FOREIGN_CALLBACK_T(py_{{ foreign_callback }}) -_rust_call(lambda err: _UniffiLib.{{ cbi.ffi_init_callback().name() }}({{ foreign_callback }}, err)) +{% include "Protocol.py" %} +{% include "CallbackInterfaceImpl.py" %} # The _UniffiConverter which transforms the Callbacks in to Handles to pass to Rust. -{{ ffi_converter_name }} = _UniffiConverterCallbackInterface({{ foreign_callback }}) +{{ ffi_converter_name }} = UniffiCallbackInterfaceFfiConverter() diff --git a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py index 7e98f7c46f..097fcd4d1d 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py @@ -1,6 +1,10 @@ {%- let obj = ci|get_object_definition(name) %} +{%- let (protocol_name, impl_name) = obj|object_names %} +{%- let methods = obj.methods() %} -class {{ type_name }}: +{% include "Protocol.py" %} + +class {{ impl_name }}: _pointer: ctypes.c_void_p {%- match obj.primary_constructor() %} @@ -63,25 +67,40 @@ def __ne__(self, other: object) -> {{ ne.return_type().unwrap()|type_name }}: {% endmatch %} {% endfor %} +{%- if obj.is_trait_interface() %} +{%- let callback_handler_class = format!("UniffiCallbackInterface{}", name) %} +{%- let callback_handler_obj = format!("uniffiCallbackInterface{}", name) %} +{%- let ffi_init_callback = obj.ffi_init_callback() %} +{% include "CallbackInterfaceImpl.py" %} +{%- endif %} class {{ ffi_converter_name }}: + {%- if obj.is_trait_interface() %} + _handle_map = ConcurrentHandleMap() + {%- endif %} + + @staticmethod + def lift(value: int): + return {{ impl_name }}._make_instance_(value) + + @staticmethod + def lower(value: {{ protocol_name }}): + {%- match obj.imp() %} + {%- when ObjectImpl::Struct %} + if not isinstance(value, {{ impl_name }}): + raise TypeError("Expected {{ impl_name }} instance, {} found".format(type(value).__name__)) + return value._pointer + {%- when ObjectImpl::Trait %} + return {{ ffi_converter_name }}._handle_map.insert(value) + {%- endmatch %} + @classmethod - def read(cls, buf): + def read(cls, buf: _UniffiRustBuffer): ptr = buf.read_u64() if ptr == 0: raise InternalError("Raw pointer value was null") return cls.lift(ptr) @classmethod - def write(cls, value, buf): - if not isinstance(value, {{ type_name }}): - raise TypeError("Expected {{ type_name }} instance, {} found".format(type(value).__name__)) + def write(cls, value: {{ protocol_name }}, buf: _UniffiRustBuffer): buf.write_u64(cls.lower(value)) - - @staticmethod - def lift(value): - return {{ type_name }}._make_instance_(value) - - @staticmethod - def lower(value): - return value._pointer diff --git a/uniffi_bindgen/src/bindings/python/templates/Protocol.py b/uniffi_bindgen/src/bindings/python/templates/Protocol.py new file mode 100644 index 0000000000..63e22769fb --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/templates/Protocol.py @@ -0,0 +1,7 @@ +class {{ protocol_name }}(typing.Protocol): + {%- for meth in methods.iter() %} + def {{ meth.name()|fn_name }}(self, {% call py::arg_list_decl(meth) %}): + raise NotImplementedError + {%- else %} + pass + {%- endfor %} diff --git a/uniffi_bindgen/src/bindings/python/templates/Types.py b/uniffi_bindgen/src/bindings/python/templates/Types.py index 5e05314c37..84afa6bbff 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Types.py +++ b/uniffi_bindgen/src/bindings/python/templates/Types.py @@ -85,7 +85,7 @@ {%- when Type::Map { key_type, value_type } %} {%- include "MapTemplate.py" %} -{%- when Type::CallbackInterface { name: id, module_path } %} +{%- when Type::CallbackInterface { name, module_path } %} {%- include "CallbackInterfaceTemplate.py" %} {%- when Type::Custom { name, module_path, builtin } %} diff --git a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs index 1f1bf8e299..e6defe65cc 100644 --- a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs +++ b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs @@ -153,7 +153,9 @@ mod filters { FfiType::RustArcPtr(_) => ":pointer".to_string(), FfiType::RustBuffer(_) => "RustBuffer.by_value".to_string(), FfiType::ForeignBytes => "ForeignBytes".to_string(), - FfiType::ForeignCallback => unimplemented!("Callback interfaces are not implemented"), + // Callback interfaces are not yet implemented, but this needs to return something in + // order for the coverall tests to pass. + FfiType::ForeignCallback => ":pointer".to_string(), FfiType::ForeignExecutorCallback => { unimplemented!("Foreign executors are not implemented") } diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/callback_interface.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/callback_interface.rs index 99d503f881..b25e3fb6dc 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/callback_interface.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/callback_interface.rs @@ -6,21 +6,25 @@ use crate::backend::CodeType; #[derive(Debug)] pub struct CallbackInterfaceCodeType { - id: String, + name: String, } impl CallbackInterfaceCodeType { - pub fn new(id: String) -> Self { - Self { id } + pub fn new(name: String) -> Self { + Self { name } } } impl CodeType for CallbackInterfaceCodeType { fn type_label(&self) -> String { - super::SwiftCodeOracle.class_name(&self.id) + super::SwiftCodeOracle.class_name(&self.name) } fn canonical_name(&self) -> String { format!("CallbackInterface{}", self.type_label()) } + + fn initialization_fn(&self) -> Option { + Some(format!("uniffiCallbackInit{}", self.name)) + } } diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs index ec38ec11c8..0bab0cf52e 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs @@ -404,7 +404,7 @@ impl SwiftCodeOracle { Type::Duration => Box::new(miscellany::DurationCodeType), Type::Enum { name, .. } => Box::new(enum_::EnumCodeType::new(name)), - Type::Object { name, .. } => Box::new(object::ObjectCodeType::new(name)), + Type::Object { name, imp, .. } => Box::new(object::ObjectCodeType::new(name, imp)), Type::Record { name, .. } => Box::new(record::RecordCodeType::new(name)), Type::CallbackInterface { name, .. } => { Box::new(callback_interface::CallbackInterfaceCodeType::new(name)) @@ -490,6 +490,25 @@ impl SwiftCodeOracle { fn ffi_canonical_name(&self, ffi_type: &FfiType) -> String { self.ffi_type_label_raw(ffi_type) } + + /// Get the name of the protocol and class name for an object. + /// + /// For struct impls, the class name is the object name and the protocol name is derived from that. + /// For trait impls, the protocol name is the object name, and the class name is derived from that. + /// + /// This split is needed because of the `FfiConverter` protocol. For struct impls, `lower` + /// can only lower the concrete class. For trait impls, `lower` can lower anything that + /// implement the protocol. + fn object_names(&self, obj: &Object) -> (String, String) { + let class_name = self.class_name(obj.name()); + match obj.imp() { + ObjectImpl::Struct => (format!("{class_name}Protocol"), class_name), + ObjectImpl::Trait => { + let protocol_name = format!("{class_name}Impl"); + (class_name, protocol_name) + } + } + } } pub mod filters { @@ -625,4 +644,8 @@ pub mod filters { } )) } + + pub fn object_names(obj: &Object) -> Result<(String, String), askama::Error> { + Ok(SwiftCodeOracle.object_names(obj)) + } } diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs index 241d694fce..02f2567d97 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs @@ -2,25 +2,33 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use crate::backend::CodeType; +use crate::{backend::CodeType, interface::ObjectImpl}; #[derive(Debug)] pub struct ObjectCodeType { - id: String, + name: String, + imp: ObjectImpl, } impl ObjectCodeType { - pub fn new(id: String) -> Self { - Self { id } + pub fn new(name: String, imp: ObjectImpl) -> Self { + Self { name, imp } } } impl CodeType for ObjectCodeType { fn type_label(&self) -> String { - super::SwiftCodeOracle.class_name(&self.id) + super::SwiftCodeOracle.class_name(&self.name) } fn canonical_name(&self) -> String { - format!("Type{}", self.id) + format!("Type{}", self.name) + } + + fn initialization_fn(&self) -> Option { + match &self.imp { + ObjectImpl::Struct => None, + ObjectImpl::Trait => Some(format!("uniffiCallbackInit{}", self.name)), + } } } diff --git a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift new file mode 100644 index 0000000000..157da46128 --- /dev/null +++ b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift @@ -0,0 +1,88 @@ +{%- if self.include_once_check("CallbackInterfaceRuntime.swift") %}{%- include "CallbackInterfaceRuntime.swift" %}{%- endif %} + +// Declaration and FfiConverters for {{ type_name }} Callback Interface + +fileprivate let {{ callback_handler }} : ForeignCallback = + { (handle: UniFFICallbackHandle, method: Int32, argsData: UnsafePointer, argsLen: Int32, out_buf: UnsafeMutablePointer) -> Int32 in + {% for meth in methods.iter() -%} + {%- let method_name = format!("invoke_{}", meth.name())|fn_name %} + + func {{ method_name }}(_ swiftCallbackInterface: {{ type_name }}, _ argsData: UnsafePointer, _ argsLen: Int32, _ out_buf: UnsafeMutablePointer) throws -> Int32 { + {%- if meth.arguments().len() > 0 %} + var reader = createReader(data: Data(bytes: argsData, count: Int(argsLen))) + {%- endif %} + + {%- match meth.return_type() %} + {%- when Some(return_type) %} + func makeCall() throws -> Int32 { + let result = {% if meth.throws() %} try{% endif %} swiftCallbackInterface.{{ meth.name()|fn_name }}( + {% for arg in meth.arguments() -%} + {% if !config.omit_argument_labels() %}{{ arg.name()|var_name }}: {% endif %} try {{ arg|read_fn }}(from: &reader) + {%- if !loop.last %}, {% endif %} + {% endfor -%} + ) + var writer = [UInt8]() + {{ return_type|write_fn }}(result, into: &writer) + out_buf.pointee = RustBuffer(bytes: writer) + return UNIFFI_CALLBACK_SUCCESS + } + {%- when None %} + func makeCall() throws -> Int32 { + {% if meth.throws() %}try {% endif %}swiftCallbackInterface.{{ meth.name()|fn_name }}( + {% for arg in meth.arguments() -%} + {% if !config.omit_argument_labels() %}{{ arg.name()|var_name }}: {% endif %} try {{ arg|read_fn }}(from: &reader) + {%- if !loop.last %}, {% endif %} + {% endfor -%} + ) + return UNIFFI_CALLBACK_SUCCESS + } + {%- endmatch %} + + {%- match meth.throws_type() %} + {%- when None %} + return try makeCall() + {%- when Some(error_type) %} + do { + return try makeCall() + } catch let error as {{ error_type|type_name }} { + out_buf.pointee = {{ error_type|lower_fn }}(error) + return UNIFFI_CALLBACK_ERROR + } + {%- endmatch %} + } + {%- endfor %} + + + switch method { + case IDX_CALLBACK_FREE: + {{ ffi_converter_name }}.handleMap.remove(handle: handle) + // Sucessful return + // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` + return UNIFFI_CALLBACK_SUCCESS + {% for meth in methods.iter() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} + case {{ loop.index }}: + guard let cb = {{ ffi_converter_name }}.handleMap.get(handle: handle) else { + out_buf.pointee = {{ Type::String.borrow()|lower_fn }}("No callback in handlemap; this is a Uniffi bug") + return UNIFFI_CALLBACK_UNEXPECTED_ERROR + } + do { + return try {{ method_name }}(cb, argsData, argsLen, out_buf) + } catch let error { + out_buf.pointee = {{ Type::String.borrow()|lower_fn }}(String(describing: error)) + return UNIFFI_CALLBACK_UNEXPECTED_ERROR + } + {% endfor %} + // This should never happen, because an out of bounds method index won't + // ever be used. Once we can catch errors, we should return an InternalError. + // https://github.com/mozilla/uniffi-rs/issues/351 + default: + // An unexpected error happened. + // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` + return UNIFFI_CALLBACK_UNEXPECTED_ERROR + } +} + +private func {{ callback_init }}() { + {{ ffi_init_callback.name() }}({{ callback_handler }}) +} diff --git a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift index 9ae62d1667..d03b7ccb3f 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift @@ -13,7 +13,7 @@ fileprivate class UniFFICallbackHandleMap { private var rightMap: [ObjectIdentifier: UniFFICallbackHandle] = [:] private let lock = NSLock() - private var currentHandle: UniFFICallbackHandle = 0 + private var currentHandle: UniFFICallbackHandle = 1 private let stride: UniFFICallbackHandle = 1 func insert(obj: T) -> UniFFICallbackHandle { diff --git a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift index aec8ded930..f9e75b2a3f 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift @@ -1,122 +1,16 @@ {%- let cbi = ci|get_callback_interface_definition(name) %} -{%- let foreign_callback = format!("foreignCallback{}", canonical_type_name) %} -{%- if self.include_once_check("CallbackInterfaceRuntime.swift") %}{%- include "CallbackInterfaceRuntime.swift" %}{%- endif %} +{%- let callback_handler = format!("uniffiCallbackHandler{}", name) %} +{%- let callback_init = format!("uniffiCallbackInit{}", name) %} +{%- let methods = cbi.methods() %} +{%- let protocol_name = type_name.clone() %} +{%- let ffi_init_callback = cbi.ffi_init_callback() %} -// Declaration and FfiConverters for {{ type_name }} Callback Interface - -public protocol {{ type_name }} : AnyObject { - {% for meth in cbi.methods() -%} - func {{ meth.name()|fn_name }}({% call swift::arg_list_protocol(meth) %}) {% call swift::throws(meth) -%} - {%- match meth.return_type() -%} - {%- when Some with (return_type) %} -> {{ return_type|type_name -}} - {%- else -%} - {%- endmatch %} - {% endfor %} -} - -// The ForeignCallback that is passed to Rust. -fileprivate let {{ foreign_callback }} : ForeignCallback = - { (handle: UniFFICallbackHandle, method: Int32, argsData: UnsafePointer, argsLen: Int32, out_buf: UnsafeMutablePointer) -> Int32 in - {% for meth in cbi.methods() -%} - {%- let method_name = format!("invoke_{}", meth.name())|fn_name %} - - func {{ method_name }}(_ swiftCallbackInterface: {{ type_name }}, _ argsData: UnsafePointer, _ argsLen: Int32, _ out_buf: UnsafeMutablePointer) throws -> Int32 { - {%- if meth.arguments().len() > 0 %} - var reader = createReader(data: Data(bytes: argsData, count: Int(argsLen))) - {%- endif %} - - {%- match meth.return_type() %} - {%- when Some(return_type) %} - func makeCall() throws -> Int32 { - let result = {% if meth.throws() %} try{% endif %} swiftCallbackInterface.{{ meth.name()|fn_name }}( - {% for arg in meth.arguments() -%} - {% if !config.omit_argument_labels() %}{{ arg.name()|var_name }}: {% endif %} try {{ arg|read_fn }}(from: &reader) - {%- if !loop.last %}, {% endif %} - {% endfor -%} - ) - var writer = [UInt8]() - {{ return_type|write_fn }}(result, into: &writer) - out_buf.pointee = RustBuffer(bytes: writer) - return UNIFFI_CALLBACK_SUCCESS - } - {%- when None %} - func makeCall() throws -> Int32 { - try swiftCallbackInterface.{{ meth.name()|fn_name }}( - {% for arg in meth.arguments() -%} - {% if !config.omit_argument_labels() %}{{ arg.name()|var_name }}: {% endif %} try {{ arg|read_fn }}(from: &reader) - {%- if !loop.last %}, {% endif %} - {% endfor -%} - ) - return UNIFFI_CALLBACK_SUCCESS - } - {%- endmatch %} - - {%- match meth.throws_type() %} - {%- when None %} - return try makeCall() - {%- when Some(error_type) %} - do { - return try makeCall() - } catch let error as {{ error_type|type_name }} { - out_buf.pointee = {{ error_type|lower_fn }}(error) - return UNIFFI_CALLBACK_ERROR - } - {%- endmatch %} - } - {%- endfor %} - - - switch method { - case IDX_CALLBACK_FREE: - {{ ffi_converter_name }}.drop(handle: handle) - // Sucessful return - // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - return UNIFFI_CALLBACK_SUCCESS - {% for meth in cbi.methods() -%} - {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} - case {{ loop.index }}: - let cb: {{ cbi|type_name }} - do { - cb = try {{ ffi_converter_name }}.lift(handle) - } catch { - out_buf.pointee = {{ Type::String.borrow()|lower_fn }}("{{ cbi.name() }}: Invalid handle") - return UNIFFI_CALLBACK_UNEXPECTED_ERROR - } - do { - return try {{ method_name }}(cb, argsData, argsLen, out_buf) - } catch let error { - out_buf.pointee = {{ Type::String.borrow()|lower_fn }}(String(describing: error)) - return UNIFFI_CALLBACK_UNEXPECTED_ERROR - } - {% endfor %} - // This should never happen, because an out of bounds method index won't - // ever be used. Once we can catch errors, we should return an InternalError. - // https://github.com/mozilla/uniffi-rs/issues/351 - default: - // An unexpected error happened. - // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - return UNIFFI_CALLBACK_UNEXPECTED_ERROR - } -} +{% include "Protocol.swift" %} +{% include "CallbackInterfaceImpl.swift" %} // FfiConverter protocol for callback interfaces fileprivate struct {{ ffi_converter_name }} { - private static let initCallbackOnce: () = { - // Swift ensures this initializer code will once run once, even when accessed by multiple threads. - try! rustCall { (err: UnsafeMutablePointer) in - {{ cbi.ffi_init_callback().name() }}({{ foreign_callback }}, err) - } - }() - - private static func ensureCallbackinitialized() { - _ = initCallbackOnce - } - - static func drop(handle: UniFFICallbackHandle) { - handleMap.remove(handle: handle) - } - - private static var handleMap = UniFFICallbackHandleMap<{{ type_name }}>() + fileprivate static var handleMap = UniFFICallbackHandleMap<{{ type_name }}>() } extension {{ ffi_converter_name }} : FfiConverter { @@ -125,7 +19,6 @@ extension {{ ffi_converter_name }} : FfiConverter { typealias FfiType = UniFFICallbackHandle public static func lift(_ handle: UniFFICallbackHandle) throws -> SwiftType { - ensureCallbackinitialized(); guard let callback = handleMap.get(handle: handle) else { throw UniffiInternalError.unexpectedStaleHandle } @@ -133,18 +26,15 @@ extension {{ ffi_converter_name }} : FfiConverter { } public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { - ensureCallbackinitialized(); let handle: UniFFICallbackHandle = try readInt(&buf) return try lift(handle) } public static func lower(_ v: SwiftType) -> UniFFICallbackHandle { - ensureCallbackinitialized(); return handleMap.insert(obj: v) } public static func write(_ v: SwiftType, into buf: inout [UInt8]) { - ensureCallbackinitialized(); writeInt(&buf, lower(v)) } } diff --git a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift index 57a77ca6df..b63631f40e 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift @@ -1,15 +1,10 @@ {%- let obj = ci|get_object_definition(name) %} -public protocol {{ obj.name() }}Protocol { - {% for meth in obj.methods() -%} - func {{ meth.name()|fn_name }}({% call swift::arg_list_protocol(meth) %}) {% call swift::async(meth) %} {% call swift::throws(meth) -%} - {%- match meth.return_type() -%} - {%- when Some with (return_type) %} -> {{ return_type|type_name -}} - {%- else -%} - {%- endmatch %} - {% endfor %} -} +{%- let (protocol_name, impl_class_name) = obj|object_names %} +{%- let methods = obj.methods() %} + +{% include "Protocol.swift" %} -public class {{ type_name }}: {{ obj.name() }}Protocol { +public class {{ impl_class_name }}: {{ protocol_name }} { fileprivate let pointer: UnsafeMutableRawPointer // TODO: We'd like this to be `private` but for Swifty reasons, @@ -33,8 +28,8 @@ public class {{ type_name }}: {{ obj.name() }}Protocol { {% for cons in obj.alternate_constructors() %} - public static func {{ cons.name()|fn_name }}({% call swift::arg_list_decl(cons) %}) {% call swift::throws(cons) %} -> {{ type_name }} { - return {{ type_name }}(unsafeFromRawPointer: {% call swift::to_ffi_call(cons) %}) + public static func {{ cons.name()|fn_name }}({% call swift::arg_list_decl(cons) %}) {% call swift::throws(cons) %} -> {{ impl_class_name }} { + return {{ impl_class_name }}(unsafeFromRawPointer: {% call swift::to_ffi_call(cons) %}) } {% endfor %} @@ -95,10 +90,37 @@ public class {{ type_name }}: {{ obj.name() }}Protocol { {% endfor %} } +{%- if obj.is_trait_interface() %} +{%- let callback_handler = format!("uniffiCallbackInterface{}", name) %} +{%- let callback_init = format!("uniffiCallbackInit{}", name) %} +{%- let ffi_init_callback = obj.ffi_init_callback() %} +{% include "CallbackInterfaceImpl.swift" %} +{%- endif %} + public struct {{ ffi_converter_name }}: FfiConverter { + {%- if obj.is_trait_interface() %} + fileprivate static var handleMap = UniFFICallbackHandleMap<{{ type_name }}>() + {%- endif %} + typealias FfiType = UnsafeMutableRawPointer typealias SwiftType = {{ type_name }} + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> {{ type_name }} { + return {{ impl_class_name }}(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: {{ type_name }}) -> UnsafeMutableRawPointer { + {%- match obj.imp() %} + {%- when ObjectImpl::Struct %} + return value.pointer + {%- when ObjectImpl::Trait %} + guard let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: handleMap.insert(obj: value))) else { + fatalError("Cast to UnsafeMutableRawPointer failed") + } + return ptr + {%- endmatch %} + } + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { let v: UInt64 = try readInt(&buf) // The Rust code won't compile if a pointer won't fit in a UInt64. @@ -115,14 +137,6 @@ public struct {{ ffi_converter_name }}: FfiConverter { // The Rust code won't compile if a pointer won't fit in a `UInt64`. writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) } - - public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> {{ type_name }} { - return {{ type_name}}(unsafeFromRawPointer: pointer) - } - - public static func lower(_ value: {{ type_name }}) -> UnsafeMutableRawPointer { - return value.pointer - } } {# diff --git a/uniffi_bindgen/src/bindings/swift/templates/Protocol.swift b/uniffi_bindgen/src/bindings/swift/templates/Protocol.swift new file mode 100644 index 0000000000..9fb2766de2 --- /dev/null +++ b/uniffi_bindgen/src/bindings/swift/templates/Protocol.swift @@ -0,0 +1,10 @@ +public protocol {{ protocol_name }} : AnyObject { + {% for meth in methods.iter() -%} + func {{ meth.name()|fn_name }}({% call swift::arg_list_protocol(meth) %}) {% call swift::async(meth) -%}{% call swift::throws(meth) -%} + {%- match meth.return_type() -%} + {%- when Some with (return_type) %} -> {{ return_type|type_name -}} + {%- else -%} + {%- endmatch %} + {% endfor %} +} + diff --git a/uniffi_bindgen/src/bindings/swift/templates/macros.swift b/uniffi_bindgen/src/bindings/swift/templates/macros.swift index 0a125e6f61..bcf6938639 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/macros.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/macros.swift @@ -77,11 +77,11 @@ {%- macro async(func) %} -{%- if func.is_async() %}async{% endif %} +{%- if func.is_async() %}async {% endif %} {%- endmacro -%} {%- macro throws(func) %} -{%- if func.throws() %}throws{% endif %} +{%- if func.throws() %}throws {% endif %} {%- endmacro -%} {%- macro try(func) %} diff --git a/uniffi_bindgen/src/interface/callbacks.rs b/uniffi_bindgen/src/interface/callbacks.rs index e3bca4f966..9bafce25de 100644 --- a/uniffi_bindgen/src/interface/callbacks.rs +++ b/uniffi_bindgen/src/interface/callbacks.rs @@ -35,7 +35,7 @@ use uniffi_meta::Checksum; -use super::ffi::{FfiArgument, FfiFunction, FfiType}; +use super::ffi::FfiFunction; use super::object::Method; use super::{AsType, Type, TypeIterator}; @@ -77,13 +77,7 @@ impl CallbackInterface { } pub(super) fn derive_ffi_funcs(&mut self) { - self.ffi_init_callback.name = - uniffi_meta::init_callback_fn_symbol_name(&self.module_path, &self.name); - self.ffi_init_callback.arguments = vec![FfiArgument { - name: "callback_stub".to_string(), - type_: FfiType::ForeignCallback, - }]; - self.ffi_init_callback.return_type = None; + self.ffi_init_callback = FfiFunction::callback_init(&self.module_path, &self.name); } pub fn iter_types(&self) -> TypeIterator<'_> { diff --git a/uniffi_bindgen/src/interface/ffi.rs b/uniffi_bindgen/src/interface/ffi.rs index d18aaf8262..8f23ccbb90 100644 --- a/uniffi_bindgen/src/interface/ffi.rs +++ b/uniffi_bindgen/src/interface/ffi.rs @@ -150,6 +150,19 @@ pub struct FfiFunction { } impl FfiFunction { + pub fn callback_init(module_path: &str, trait_name: &str) -> Self { + Self { + name: uniffi_meta::init_callback_fn_symbol_name(module_path, trait_name), + arguments: vec![FfiArgument { + name: "handle".to_string(), + type_: FfiType::ForeignCallback, + }], + return_type: None, + has_rust_call_status_arg: false, + ..Self::default() + } + } + pub fn name(&self) -> &str { &self.name } diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index 8e4df2149b..d4f5ebdfe3 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -241,13 +241,19 @@ impl ComponentInterface { let fielded = !e.is_flat(); // For flat errors, we should only generate read() methods if we need them to support // callback interface errors - let used_in_callback_interface = self + let used_in_foreign_interface = self .callback_interface_definitions() .iter() .flat_map(|cb| cb.methods()) + .chain( + self.object_definitions() + .iter() + .filter(|o| o.is_trait_interface()) + .flat_map(|o| o.methods()), + ) .any(|m| m.throws_type() == Some(&e.as_type())); - self.is_name_used_as_error(&e.name) && (fielded || used_in_callback_interface) + self.is_name_used_as_error(&e.name) && (fielded || used_in_foreign_interface) } /// Get details about all `Type::External` types. diff --git a/uniffi_bindgen/src/interface/object.rs b/uniffi_bindgen/src/interface/object.rs index 942032b3c6..d79e7fccb1 100644 --- a/uniffi_bindgen/src/interface/object.rs +++ b/uniffi_bindgen/src/interface/object.rs @@ -92,7 +92,7 @@ pub struct Object { // a regular method (albeit with a generated name) // XXX - this should really be a HashSet, but not enough transient types support hash to make it worthwhile now. pub(super) uniffi_traits: Vec, - // We don't include the FfiFunc in the hash calculation, because: + // We don't include the FfiFuncs in the hash calculation, because: // - it is entirely determined by the other fields, // so excluding it is safe. // - its `name` property includes a checksum derived from the very @@ -100,6 +100,9 @@ pub struct Object { // avoids a weird circular dependency in the calculation. #[checksum_ignore] pub(super) ffi_func_free: FfiFunction, + // Ffi function to initialize the foreign callback for trait interfaces + #[checksum_ignore] + pub(super) ffi_init_callback: Option, } impl Object { @@ -118,6 +121,10 @@ impl Object { &self.imp } + pub fn is_trait_interface(&self) -> bool { + matches!(self.imp, ObjectImpl::Trait) + } + pub fn constructors(&self) -> Vec<&Constructor> { self.constructors.iter().collect() } @@ -155,8 +162,15 @@ impl Object { &self.ffi_func_free } + pub fn ffi_init_callback(&self) -> &FfiFunction { + self.ffi_init_callback + .as_ref() + .unwrap_or_else(|| panic!("No ffi_init_callback set for {}", &self.name)) + } + pub fn iter_ffi_function_definitions(&self) -> impl Iterator { iter::once(&self.ffi_func_free) + .chain(&self.ffi_init_callback) .chain(self.constructors.iter().map(|f| &f.ffi_func)) .chain(self.methods.iter().map(|f| &f.ffi_func)) .chain( @@ -180,6 +194,10 @@ impl Object { }]; self.ffi_func_free.return_type = None; self.ffi_func_free.is_object_free_function = true; + if self.is_trait_interface() { + self.ffi_init_callback = + Some(FfiFunction::callback_init(&self.module_path, &self.name)); + } for cons in self.constructors.iter_mut() { cons.derive_ffi_func(); @@ -230,6 +248,7 @@ impl From for Object { name: ffi_free_name, ..Default::default() }, + ffi_init_callback: None, } } } diff --git a/uniffi_macros/src/export/callback_interface.rs b/uniffi_macros/src/export/callback_interface.rs index 6297a9b289..81258f0bb7 100644 --- a/uniffi_macros/src/export/callback_interface.rs +++ b/uniffi_macros/src/export/callback_interface.rs @@ -21,7 +21,7 @@ pub(super) fn trait_impl( let trait_impl_ident = trait_impl_ident(&trait_name); let internals_ident = internals_ident(&trait_name); let init_ident = Ident::new( - &uniffi_meta::init_callback_fn_symbol_name(&mod_path, &trait_name), + &uniffi_meta::init_callback_fn_symbol_name(mod_path, &trait_name), Span::call_site(), ); @@ -38,7 +38,7 @@ pub(super) fn trait_impl( #[doc(hidden)] #[no_mangle] - pub extern "C" fn #init_ident(callback: ::uniffi::ForeignCallback, _: &mut ::uniffi::RustCallStatus) { + pub extern "C" fn #init_ident(callback: ::uniffi::ForeignCallback) { #internals_ident.set_callback(callback); } diff --git a/uniffi_macros/src/export/scaffolding.rs b/uniffi_macros/src/export/scaffolding.rs index f120ccc880..d00d8403bd 100644 --- a/uniffi_macros/src/export/scaffolding.rs +++ b/uniffi_macros/src/export/scaffolding.rs @@ -125,20 +125,35 @@ impl ScaffoldingBits { udl_mode: bool, ) -> Self { let ident = &sig.ident; - let ffi_converter = if is_trait { + let lift_impl = if is_trait { quote! { - <::std::sync::Arc as ::uniffi::FfiConverter> + <::std::sync::Arc as ::uniffi::Lift> } } else { quote! { - <::std::sync::Arc<#self_ident> as ::uniffi::FfiConverter> + <::std::sync::Arc<#self_ident> as ::uniffi::Lift> } }; - let params: Vec<_> = iter::once(quote! { uniffi_self_lowered: #ffi_converter::FfiType }) + let params: Vec<_> = iter::once(quote! { uniffi_self_lowered: #lift_impl::FfiType }) .chain(sig.scaffolding_params()) .collect(); + let try_lift_self = if is_trait { + // For trait interfaces we need to special case this. Trait interfaces normally lift + // foreign trait impl pointers. However, for a method call, we want to lift a Rust + // pointer. + quote! { + { + let foreign_arc = ::std::boxed::Box::leak(unsafe { Box::from_raw(uniffi_self_lowered as *mut ::std::sync::Arc) }); + // Take a clone for our own use. + Ok(::std::sync::Arc::clone(foreign_arc)) + } + } + } else { + quote! { #lift_impl::try_lift(uniffi_self_lowered) } + }; + let lift_closure = sig.lift_closure(Some(quote! { - match #ffi_converter::try_lift(uniffi_self_lowered) { + match #try_lift_self { Ok(v) => v, Err(e) => return Err(("self", e)) } diff --git a/uniffi_macros/src/export/trait_interface.rs b/uniffi_macros/src/export/trait_interface.rs index e6cdaff5c9..c4755015dc 100644 --- a/uniffi_macros/src/export/trait_interface.rs +++ b/uniffi_macros/src/export/trait_interface.rs @@ -6,7 +6,10 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, quote_spanned}; use crate::{ - export::{attributes::ExportAttributeArguments, gen_method_scaffolding, item::ImplItem}, + export::{ + attributes::ExportAttributeArguments, callback_interface, gen_method_scaffolding, + item::ImplItem, + }, object::interface_meta_static_var, util::{ident_to_string, tagged_impl_header}, }; @@ -22,9 +25,14 @@ pub(super) fn gen_trait_scaffolding( if let Some(rt) = args.async_runtime { return Err(syn::Error::new_spanned(rt, "not supported for traits")); } + let trait_name = ident_to_string(&self_ident); + let trait_impl = callback_interface::trait_impl(mod_path, &self_ident, &items) + .unwrap_or_else(|e| e.into_compile_error()); - let name = ident_to_string(&self_ident); - let free_fn_ident = Ident::new(&free_fn_symbol_name(mod_path, &name), Span::call_site()); + let free_fn_ident = Ident::new( + &free_fn_symbol_name(mod_path, &trait_name), + Span::call_site(), + ); let free_tokens = quote! { #[doc(hidden)] @@ -66,15 +74,17 @@ pub(super) fn gen_trait_scaffolding( Ok(quote_spanned! { self_ident.span() => #meta_static_var #free_tokens - #ffi_converter_tokens + #trait_impl #impl_tokens + #ffi_converter_tokens }) } pub(crate) fn ffi_converter(mod_path: &str, trait_ident: &Ident, udl_mode: bool) -> TokenStream { let impl_spec = tagged_impl_header("FfiConverterArc", "e! { dyn #trait_ident }, udl_mode); let lift_ref_impl_spec = tagged_impl_header("LiftRef", "e! { dyn #trait_ident }, udl_mode); - let name = ident_to_string(trait_ident); + let trait_name = ident_to_string(trait_ident); + let trait_impl_ident = callback_interface::trait_impl_ident(&trait_name); quote! { // All traits must be `Sync + Send`. The generated scaffolding will fail to compile @@ -90,10 +100,8 @@ pub(crate) fn ffi_converter(mod_path: &str, trait_ident: &Ident, udl_mode: bool) ::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 try_lift(v: Self::FfiType) -> ::uniffi::deps::anyhow::Result<::std::sync::Arc> { + Ok(::std::sync::Arc::new(<#trait_impl_ident>::new(v as u64))) } fn write(obj: ::std::sync::Arc, buf: &mut Vec) { @@ -113,7 +121,7 @@ pub(crate) fn ffi_converter(mod_path: &str, trait_ident: &Ident, udl_mode: bool) const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::TYPE_INTERFACE) .concat_str(#mod_path) - .concat_str(#name) + .concat_str(#trait_name) .concat_bool(true); } From a8b5dfc49a39a0d55c2eeaf1d641543f921ffb87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 16:47:56 -0400 Subject: [PATCH 22/24] Bump rustix from 0.37.23 to 0.37.25 (#1795) Bumps [rustix](https://github.com/bytecodealliance/rustix) from 0.37.23 to 0.37.25. - [Release notes](https://github.com/bytecodealliance/rustix/releases) - [Commits](https://github.com/bytecodealliance/rustix/compare/v0.37.23...v0.37.25) --- updated-dependencies: - dependency-name: rustix dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef1d6bfe1b..a0e3fc20f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,7 +192,7 @@ dependencies = [ "log", "parking", "polling", - "rustix 0.37.23", + "rustix 0.37.25", "slab", "socket2", "waker-fn", @@ -1166,9 +1166,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.37.23" +version = "0.37.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" dependencies = [ "bitflags 1.3.2", "errno", From ca35eda00bba3ae316154dfd7d9faa3a68d20c75 Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Wed, 18 Oct 2023 16:49:16 -0400 Subject: [PATCH 23/24] Add support for `external_packages` to Python so Python packages can be used. (#1784) When using external types, the expectation is that most consumers will want to use a Python package which holds all the modules - therefore, imports for external types defaults to `from .module import name`. This behaviour can be changed to support any package name, including just doing a regular top-level module import - which we leverage for some tests. Fixes #1776 --- docs/manual/src/python/configuration.md | 40 ++++++++++++++++++- fixtures/ext-types/lib/uniffi.toml | 5 +++ fixtures/ext-types/proc-macro-lib/uniffi.toml | 5 +++ .../src/bindings/python/gen_python/mod.rs | 12 ++++++ .../python/templates/ExternalTemplate.py | 8 ++-- 5 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 fixtures/ext-types/lib/uniffi.toml create mode 100644 fixtures/ext-types/proc-macro-lib/uniffi.toml diff --git a/docs/manual/src/python/configuration.md b/docs/manual/src/python/configuration.md index cd6ff9c18c..067e830b69 100644 --- a/docs/manual/src/python/configuration.md +++ b/docs/manual/src/python/configuration.md @@ -8,10 +8,41 @@ The generated Python modules can be configured using a `uniffi.toml` configurati | ------------------ | ------- |------------ | | `cdylib_name` | `uniffi_{namespace}`[^1] | The name of the compiled Rust library containing the FFI implementation (not needed when using `generate --library`). | | `custom_types` | | A map which controls how custom types are exposed to Python. See the [custom types section of the manual](../udl/custom_types#custom-types-in-the-bindings-code)| +| `external_packages` | | A map which controls the package name used by external packages. See below for more. +## External Packages -## Example +When you reference external modules, uniffi will generate statements like `from module import Type` +in the referencing module. The `external_packages` configuration value allows you to specify how `module` +is formed in such statements. +The value is a map, keyed by the crate-name and the value is the package name which will be used by +Python for that crate. The default value is an empty map. + +When looking up crate-name, the following behavior is implemented. + +### Default value +If no value for the crate is found, it is assumed that you will be packaging up your library +as a simple Python package, so the statement will be of the form `from .module import Type`, +where `module` is the namespace specified in that crate. + +Note that this is invalid syntax unless the module lives in a package - attempting to +use the module as a stand-alone module will fail. UniFFI just generates flat .py files; the +packaging is up to you. Eg, a build process might create a directory, create an `__init__.py` +file in that directory (maybe including `from subpackage import *`) and have `uniffi-bindgen` +generate the bindings into this directory. + +### Specified value +If the crate-name is found in the map, the specified entry used as a package name, so the statement will be of the form +`from package.module import Type` (again, where `module` is the namespace specified in that crate) + +An exception is when the specified value is an empty string, in which case you will see +`from module import Type`, so each generated module functions outside a package. +This is used by some UniFFI tests to avoid the test code needing to create a Python package. + +## Examples + +Custom Types ```toml # Assuming a Custom Type named URL using a String as the builtin. [bindings.python.custom_types.Url] @@ -20,3 +51,10 @@ imports = ["urllib.parse"] into_custom = "urllib.parse.urlparse({})" from_custom = "urllib.parse.urlunparse({})" ``` + +External Packages +```toml +[bindings.python.external_packages] +# An external type `Foo` in `crate-name` (which specifies a namespace of `my_module`) will be referenced via `from MyPackageName.my_module import Foo` +crate-name = "MyPackageName" +``` diff --git a/fixtures/ext-types/lib/uniffi.toml b/fixtures/ext-types/lib/uniffi.toml new file mode 100644 index 0000000000..c090246e23 --- /dev/null +++ b/fixtures/ext-types/lib/uniffi.toml @@ -0,0 +1,5 @@ +[bindings.python.external_packages] +# This fixture does not create a Python package, so we want all modules to be top-level modules. +custom_types = "" +ext_types_guid = "" +uniffi_one_ns = "" diff --git a/fixtures/ext-types/proc-macro-lib/uniffi.toml b/fixtures/ext-types/proc-macro-lib/uniffi.toml new file mode 100644 index 0000000000..c090246e23 --- /dev/null +++ b/fixtures/ext-types/proc-macro-lib/uniffi.toml @@ -0,0 +1,5 @@ +[bindings.python.external_packages] +# This fixture does not create a Python package, so we want all modules to be top-level modules. +custom_types = "" +ext_types_guid = "" +uniffi_one_ns = "" diff --git a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs index 0cd858b476..9199bbefd0 100644 --- a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs +++ b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs @@ -75,6 +75,8 @@ pub struct Config { cdylib_name: Option, #[serde(default)] custom_types: HashMap, + #[serde(default)] + external_packages: HashMap, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -94,6 +96,16 @@ impl Config { "uniffi".into() } } + + /// Get the package name for a given external namespace. + pub fn module_for_namespace(&self, ns: &str) -> String { + let ns = ns.to_string().to_snake_case(); + match self.external_packages.get(&ns) { + None => format!(".{ns}"), + Some(value) if value.is_empty() => ns, + Some(value) => format!("{value}.{ns}"), + } + } } impl BindingsConfig for Config { diff --git a/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py index 71e05e8b06..26e4639e19 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py @@ -1,9 +1,9 @@ -{%- let ns = namespace|fn_name %} +{%- let module = python_config.module_for_namespace(namespace) -%} # External type {{name}} is in namespace "{{namespace}}", crate {{module_path}} {%- let ffi_converter_name = "_UniffiConverterType{}"|format(name) %} -{{ self.add_import_of(ns, ffi_converter_name) }} -{{ self.add_import_of(ns, name) }} {#- import the type alias itself -#} +{{ self.add_import_of(module, ffi_converter_name) }} +{{ self.add_import_of(module, name) }} {#- import the type alias itself -#} {%- let rustbuffer_local_name = "_UniffiRustBuffer{}"|format(name) %} -{{ self.add_import_of_as(ns, "_UniffiRustBuffer", rustbuffer_local_name) }} +{{ self.add_import_of_as(module, "_UniffiRustBuffer", rustbuffer_local_name) }} From 2fdaae18aba290769bd56d7c9838edc5b8e747cd Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Thu, 19 Oct 2023 15:46:50 -0400 Subject: [PATCH 24/24] Add an example of and better docs for foreign traits. (#1794) --- docs/manual/src/SUMMARY.md | 1 + docs/manual/src/foreign_traits.md | 146 ++++++++++++++ docs/manual/src/udl/callback_interfaces.md | 180 ++---------------- examples/callbacks/README.md | 5 + examples/callbacks/src/callbacks.udl | 14 +- examples/callbacks/src/lib.rs | 59 +++++- .../tests/bindings/test_callbacks.kts | 15 +- .../tests/bindings/test_callbacks.py | 31 ++- .../tests/bindings/test_callbacks.swift | 17 +- 9 files changed, 292 insertions(+), 176 deletions(-) create mode 100644 docs/manual/src/foreign_traits.md create mode 100644 examples/callbacks/README.md diff --git a/docs/manual/src/SUMMARY.md b/docs/manual/src/SUMMARY.md index 58206070f8..2b6d9b4a51 100644 --- a/docs/manual/src/SUMMARY.md +++ b/docs/manual/src/SUMMARY.md @@ -26,6 +26,7 @@ - [Generating bindings](./bindings) - [Customizing binding generation](./bindings#customizing-the-binding-generation) +- [Implementing Rust traits in foreign bindings](./foreign_traits) ## Kotlin diff --git a/docs/manual/src/foreign_traits.md b/docs/manual/src/foreign_traits.md new file mode 100644 index 0000000000..14c58915c7 --- /dev/null +++ b/docs/manual/src/foreign_traits.md @@ -0,0 +1,146 @@ +# Foreign traits + +UniFFI traits can be implemented by foreign code. +This means traits implemented in Python/Swift/Kotlin etc can provide Rust code with capabilities not easily implemented in Rust, such as: + + * device APIs not directly available to Rust. + * provide glue to clip together Rust components at runtime. + * access shared resources and assets bundled with the app. + +# Example + +To implement a Rust trait in a foreign language, you might: + +## 1. Define a Rust trait + +This toy example defines a way of Rust accessing a key-value store exposed +by the host operating system (e.g. the key chain). + +All methods of the Rust trait should return a `Result<>` with the error half being +a [compatible error type](./udl/errors.md) - see below for more on error handling. + +For example: + +```rust,no_run +pub trait Keychain: Send + Sync + Debug { + fn get(&self, key: String) -> Result, KeyChainError>; + fn put(&self, key: String, value: String) -> Result<(), KeyChainError>; +} +``` + +If you are using macros add `#[uniffi::export]` above the trait. +Otherwise define this trait in your UDL file: + +```webidl +[Trait] +interface Keychain { + [Throws=KeyChainError] + string? get(string key); + + [Throws=KeyChainError] + void put(string key, string data); +}; +``` + +## 2. Allow it to be passed into Rust + +Here, we define a new object with a constructor which takes a keychain. + +```webidl +interface Authenticator { + constructor(Keychain keychain); + void login(); +}; +``` + +In Rust we'd write: + +```rust,no_run +struct Authenticator { + keychain: Arc, +} + +impl Authenticator { + pub fn new(keychain: Arc) -> Self { + Self { keychain } + } + + pub fn login(&self) { + let username = self.keychain.get("username".into()); + let password = self.keychain.get("password".into()); + } +} +``` + +## 3. Create a foreign language implementation of the trait + +Here's a Kotlin implementation: + +```kotlin +class KotlinKeychain: Keychain { + override fun get(key: String): String? { + // … elide the implementation. + return value + } + override fun put(key: String) { + // … elide the implementation. + } +} +``` + +…and Swift: + +```swift +class SwiftKeychain: Keychain { + func get(key: String) -> String? { + // … elide the implementation. + return value + } + func put(key: String) { + // … elide the implementation. + } +} +``` + +## 4. Pass the implementation to Rust + +Again, in Kotlin + +```kt +val authenticator = Authenticator(KotlinKeychain()) +// later on: +authenticator.login() +``` + +and in Swift: + +```swift +let authenticator = Authenticator(SwiftKeychain()) +// later on: +authenticator.login() +``` + +Care is taken to ensure that things are cleaned up in the foreign language once all Rust references drop. + +## ⚠️ Avoid cycles + +Foreign trait implementations make it easy to create cycles between Rust and foreign objects causing memory leaks. +For example a foreign implementation holding a reference to a Rust object which also holds a reference to the same foreign implementation. + +UniFFI doesn't try to help here and there's no universal advice; take the usual precautions. + +# Error handling + +We must handle foreign code failing, so all methods of the Rust trait should return a `Result<>` with a [compatible error type](./udl/errors.md) otherwise these errors will panic. + +## Unexpected Error handling. + +So long as your function returns a `Result<>`, it's possible for you to define how "unexpected" errors +(ie, errors not directly covered by your `Result<>` type, panics, etc) are converted to your `Result<>`'s `Err`. + +If your code defines a `From` impl for your error type, then those errors will be converted into your error type which will be returned to the Rust caller. +If your code does not define this implementation the generated code will panic. +In other words, you really should implement this! + +See our [callbacks example](https://github.com/mozilla/uniffi-rs/tree/main/examples/callbacks) for more. + diff --git a/docs/manual/src/udl/callback_interfaces.md b/docs/manual/src/udl/callback_interfaces.md index 7ccf71cdbe..28691f85cf 100644 --- a/docs/manual/src/udl/callback_interfaces.md +++ b/docs/manual/src/udl/callback_interfaces.md @@ -1,176 +1,36 @@ # Callback interfaces -Callback interfaces are traits specified in UDL which can be implemented by foreign languages. +Callback interfaces are a special implementation of +[Rust traits implemented by foreign languages](../foreign_traits). -They can provide Rust code access available to the host language, but not easily replicated -in Rust. +These are described in both UDL and proc-macros as an explict "callback interface". +They are (soft) deprecated, remain now for backwards compatibility, but probably +should be avoided. - * accessing device APIs. - * provide glue to clip together Rust components at runtime. - * access shared resources and assets bundled with the app. - -# Using callback interfaces - -## 1. Define a Rust trait - -This toy example defines a way of Rust accessing a key-value store exposed -by the host operating system (e.g. the key chain). - -```rust,no_run -pub trait Keychain: Send + Sync + Debug { - fn get(&self, key: String) -> Result, KeyChainError>; - fn put(&self, key: String, value: String) -> Result<(), KeyChainError>; -} -``` - -### Send + Sync? - -The concrete types that UniFFI generates for callback interfaces implement `Send`, `Sync`, and `Debug`, so it's legal to -include these as supertraits of your callback interface trait. This isn't strictly necessary, but it's often useful. In -particular, `Send + Sync` is useful when: - - Storing `Box` types inside a type that needs to be `Send + Sync` (for example a UniFFI - interface type) - - Storing `Box` inside a global `Mutex` or `RwLock` - -**⚠ Warning ⚠**: this comes with a caveat: the methods of the foreign class must be safe to call -from multiple threads at once, for example because they are synchronized with a mutex, or use -thread-safe data structures. However, Rust can not enforce this in the foreign code. If you add -`Send + Sync` to your callback interface, you must make sure to inform your library consumers that -their implementations must logically be Send + Sync. - -## 2. Setup error handling - -All methods of the Rust trait should return a Result. The error half of that result must -be an [error type defined in the UDL](./errors.md). - -It's currently allowed for callback interface methods to return a regular value -rather than a `Result<>`. However, this is means that any exception from the -foreign bindings will lead to a panic. - -### Errors used in callback interfaces - -In order to support errors in callback interfaces, UniFFI must be able to -properly [lift the error](../internals/lifting_and_lowering.md). This means -that the if the error is described by an `enum` rather than an `interface` in -the UDL (see [Errors](./errors.md)) then all variants of the Rust enum must be unit variants. - -### Unexpected errors - -When Rust code invokes a callback interface method, that call may result in all kinds of unexpected errors. -Some examples are the foreign code throws an exception that's not part of the exception type or there was a problem marshalling the data for the call. -UniFFI creates an `uniffi::UnexpectedUniFFICallbackError` for these cases. -If your code defines a `From` impl for your error type, then those errors will be converted into your error type which will be returned to the Rust caller. -If not, then any unexpected errors will result in a panic. -See `example/callbacks` for an example of this. - -## 3. Define a callback interface in the UDL +This document describes the differences from regular traits. +## Defining a callback +If you must define a callback in UDL it would look like: ```webidl callback interface Keychain { - [Throws=KeyChainError] - string? get(string key); - - [Throws=KeyChainError] - void put(string key, string data); + // as described in the foreign traits docs... }; ``` -## 4. And allow it to be passed into Rust - -Here, we define a constructor to pass the keychain to rust, and then another method -which may use it. - -In UDL: +procmacros support it too, but just don't use it :) -```webidl -interface Authenticator { - constructor(Keychain keychain); - void login(); -}; -``` - -In Rust: - -```rust,no_run -struct Authenticator { - keychain: Box, -} - -impl Authenticator { - pub fn new(keychain: Box) -> Self { - Self { keychain } - } - pub fn login(&self) { - let username = self.keychain.get("username".into()); - let password = self.keychain.get("password".into()); - } -} -``` - -## 5. Create an foreign language implementation of the callback interface - -In this example, here's a Kotlin implementation. - -```kotlin -class KotlinKeychain: Keychain { - override fun get(key: String): String? { - // … elide the implementation. - return value - } - override fun put(key: String) { - // … elide the implementation. - } -} -``` - -…and Swift: - -```swift -class SwiftKeychain: Keychain { - func get(key: String) -> String? { - // … elide the implementation. - return value - } - func put(key: String) { - // … elide the implementation. - } -} -``` - -Note: in Swift, this must be a `class`. - -## 6. Pass the implementation to Rust - -Again, in Kotlin - -```kt -val authenticator = Authenticator(KotlinKeychain()) -// later on: -authenticator.login() -``` - -and in Swift: - -```swift -let authenticator = Authenticator(SwiftKeychain()) -// later on: -authenticator.login() -``` +### Box and Send + Sync? -Care is taken to ensure that once `Box` is dropped in Rust, then it is cleaned up in the foreign language. +Traits defined purely for callbacks probably don't technically need to be `Sync` in Rust, but +they conceptually are, just outside of Rust's view. -Also note, that storing the `Box` in the `Authenticator` required that all implementations -*must* implement `Send`. +That is, the methods of the foreign class must be safe to call +from multiple threads at once, but Rust can not enforce this in the foreign code. -## ⚠️ Avoid callback interfaces cycles +## Rust signature differences -Callback interfaces can create cycles between Rust and foreign objects and lead to memory leaks. For example a callback -interface object holds a reference to a Rust object which also holds a reference to the same callback interface object. -Take care to avoid this by following guidelines: +Consider the examples in [Rust traits implemented by foreign languages](../foreign_traits). -1. Avoid references to UniFFI objects in callback objects, including direct references and transitive - references through intermediate objects. -2. Avoid references to callback objects from UniFFI objects, including direct references and transitive - references through intermediate objects. -3. If you need to break the first 2 guidelines, then take steps to manually break the cycles to avoid memory leaks. - Alternatively, ensure that the number of objects that can ever be created is bounded below some acceptable level. +If the traits in question are defined as a "callback" interface, the `Arc` types +would actually be `Box` - eg, the Rust implementation of the `Authenticator` +constructor would be ```fn new(keychain: Box) -> Self``` instead of the `Arc<>`. diff --git a/examples/callbacks/README.md b/examples/callbacks/README.md new file mode 100644 index 0000000000..6c73606540 --- /dev/null +++ b/examples/callbacks/README.md @@ -0,0 +1,5 @@ +This is an example of UniFFI "callbacks". + +Callbacks are the ability to implement Rust traits in foreign languges. These are defined by +[normal UniFFI traits](../../docs/manual/src/foreign_traits) or via ["callback" definitions](../../docs/manual/src/udl/callback_interfaces). + diff --git a/examples/callbacks/src/callbacks.udl b/examples/callbacks/src/callbacks.udl index e25be0872c..00cc1e62b3 100644 --- a/examples/callbacks/src/callbacks.udl +++ b/examples/callbacks/src/callbacks.udl @@ -1,4 +1,12 @@ -namespace callbacks {}; +namespace callbacks { + // `get_sim_cards` is defined via a procmacro. +}; + +// This trait is implemented in Rust and in foreign bindings. +[Trait] +interface SimCard { + string name(); // The name of the carrier/provider. +}; [Error] enum TelephoneError { @@ -9,9 +17,11 @@ enum TelephoneError { interface Telephone { constructor(); [Throws=TelephoneError] - string call(CallAnswerer answerer); + string call(SimCard sim, CallAnswerer answerer); }; +// callback interfaces are discouraged now that foreign code can +// implement traits, but here's one! callback interface CallAnswerer { [Throws=TelephoneError] string answer(); diff --git a/examples/callbacks/src/lib.rs b/examples/callbacks/src/lib.rs index 3268b80c9c..dac5653d1b 100644 --- a/examples/callbacks/src/lib.rs +++ b/examples/callbacks/src/lib.rs @@ -2,6 +2,8 @@ * 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)] pub enum TelephoneError { #[error("Busy")] @@ -18,6 +20,25 @@ impl From for TelephoneError { } } +// SIM cards. +pub trait SimCard: Send + Sync { + fn name(&self) -> String; +} + +struct RustySim; +impl SimCard for RustySim { + fn name(&self) -> String { + "rusty!".to_string() + } +} + +// namespace functions. +#[uniffi::export] +fn get_sim_cards() -> Vec> { + vec![Arc::new(RustySim {})] +} + +// The call-answer, callback interface. pub trait CallAnswerer { fn answer(&self) -> Result; } @@ -29,8 +50,42 @@ impl Telephone { Self {} } - pub fn call(&self, answerer: Box) -> Result { - answerer.answer() + pub fn call( + &self, + // Traits are Arc<>, callbacks Box<>. + sim: Arc, + answerer: Box, + ) -> Result { + if sim.name() != "rusty!" { + Ok(format!("{} est bon marché", sim.name())) + } else { + answerer.answer() + } + } +} + +// Some proc-macro definitions of the same thing. +#[derive(uniffi::Object, Debug, Default, Clone)] +pub struct FancyTelephone; + +#[uniffi::export] +impl FancyTelephone { + #[uniffi::constructor] + pub fn new() -> Arc { + Arc::new(Self) + } + + // Exact same signature as the UDL version. + pub fn call( + &self, + sim: Arc, + answerer: Box, + ) -> Result { + if sim.name() != "rusty!" { + Ok(format!("{} est bon marché", sim.name())) + } else { + answerer.answer() + } } } diff --git a/examples/callbacks/tests/bindings/test_callbacks.kts b/examples/callbacks/tests/bindings/test_callbacks.kts index 38ddd39f09..9239bc6d9f 100644 --- a/examples/callbacks/tests/bindings/test_callbacks.kts +++ b/examples/callbacks/tests/bindings/test_callbacks.kts @@ -22,18 +22,27 @@ class CallAnswererImpl(val mode: String) : CallAnswerer { } val telephone = Telephone() +val sim = getSimCards()[0] -assert(telephone.call(CallAnswererImpl("normal")) == "Bonjour") +assert(telephone.call(sim, CallAnswererImpl("normal")) == "Bonjour") + +// Our own sim. +class Sim() : SimCard { + override fun name(): String { + return "kotlin" + } +} +assert(telephone.call(Sim(), CallAnswererImpl("normal")) == "kotlin est bon marché") try { - telephone.call(CallAnswererImpl("busy")) + telephone.call(sim, CallAnswererImpl("busy")) throw RuntimeException("Should have thrown a Busy exception!") } catch(e: TelephoneException.Busy) { // It's okay } try { - telephone.call(CallAnswererImpl("something-else")) + telephone.call(sim, CallAnswererImpl("something-else")) throw RuntimeException("Should have thrown an internal exception!") } catch(e: TelephoneException.InternalTelephoneException) { // It's okay diff --git a/examples/callbacks/tests/bindings/test_callbacks.py b/examples/callbacks/tests/bindings/test_callbacks.py index 4e20b3f8f0..ab00615749 100644 --- a/examples/callbacks/tests/bindings/test_callbacks.py +++ b/examples/callbacks/tests/bindings/test_callbacks.py @@ -1,6 +1,8 @@ import unittest from callbacks import * +# This is defined in UDL as a "callback". It's not possible to have a Rust +# implementation of a callback, they only exist on the foreign side. class CallAnswererImpl(CallAnswerer): def __init__(self, mode): self.mode = mode @@ -13,22 +15,39 @@ def answer(self): else: raise ValueError("Testing an unexpected error") +# This is a normal Rust trait - very much like a callback but can be implemented +# in Rust or in foreign code and is generally more consistent with the uniffi +# Arc<>-based object model. +class DiscountSim(SimCard): + def name(self): + return "python" + class CallbacksTest(unittest.TestCase): + TelephoneImpl = Telephone def test_answer(self): cb_object = CallAnswererImpl("ready") - telephone = Telephone() - self.assertEqual("Bonjour", telephone.call(cb_object)) + telephone = self.TelephoneImpl() + self.assertEqual("Bonjour", telephone.call(get_sim_cards()[0], cb_object)) def test_busy(self): cb_object = CallAnswererImpl("busy") - telephone = Telephone() + telephone = self.TelephoneImpl() with self.assertRaises(TelephoneError.Busy): - telephone.call(cb_object) + telephone.call(get_sim_cards()[0], cb_object) def test_unexpected_error(self): cb_object = CallAnswererImpl("something-else") - telephone = Telephone() + telephone = self.TelephoneImpl() with self.assertRaises(TelephoneError.InternalTelephoneError): - telephone.call(cb_object) + telephone.call(get_sim_cards()[0], cb_object) + + def test_sims(self): + cb_object = CallAnswererImpl("ready") + telephone = self.TelephoneImpl() + sim = DiscountSim() + self.assertEqual("python est bon marché", telephone.call(sim, cb_object)) + +class FancyCallbacksTest(CallbacksTest): + TelephoneImpl = FancyTelephone unittest.main() diff --git a/examples/callbacks/tests/bindings/test_callbacks.swift b/examples/callbacks/tests/bindings/test_callbacks.swift index b65449aaf2..42572b5c99 100644 --- a/examples/callbacks/tests/bindings/test_callbacks.swift +++ b/examples/callbacks/tests/bindings/test_callbacks.swift @@ -33,17 +33,28 @@ class OnCallAnsweredImpl : CallAnswerer { } let telephone = Telephone() +let sim = getSimCards()[0]; -assert(try! telephone.call(answerer: OnCallAnsweredImpl(withMode: "ready")) == "Bonjour") +assert(try! telephone.call(sim: sim, answerer: OnCallAnsweredImpl(withMode: "ready")) == "Bonjour") +// We can implement our own sim cards. +class Sim : SimCard { + func name() -> String { + return "swift" + } +} + +assert(try! telephone.call(sim: Sim(), answerer: OnCallAnsweredImpl(withMode: "ready")) == "swift est bon marché") + +// Error cases. do { - _ = try telephone.call(answerer: OnCallAnsweredImpl(withMode: "busy")) + _ = try telephone.call(sim: sim, answerer: OnCallAnsweredImpl(withMode: "busy")) } catch TelephoneError.Busy { // Expected error } do { - _ = try telephone.call(answerer: OnCallAnsweredImpl(withMode: "unexpected-value")) + _ = try telephone.call(sim: sim, answerer: OnCallAnsweredImpl(withMode: "unexpected-value")) } catch TelephoneError.InternalTelephoneError { // Expected error }