From a2d5febde005624f8e629d93f7ddeacaa58193f1 Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Sun, 11 Feb 2024 21:30:32 -0500 Subject: [PATCH] Add async constructor support. All constructors can be async. Alternate constructors work in Python, Kotlin and Swift, although only Swift supports primary constructors. Cleans up and removes duplication in the Kotlin and Swift bindings too. Fixes #1919 --- CHANGELOG.md | 3 + docs/manual/src/udl/interfaces.md | 5 + .../coverall/tests/bindings/test_coverall.kts | 7 +- fixtures/futures/src/futures.udl | 11 ++ fixtures/futures/src/lib.rs | 30 +++++ .../futures/tests/bindings/test_futures.kts | 6 + .../futures/tests/bindings/test_futures.py | 17 +++ .../futures/tests/bindings/test_futures.swift | 22 ++++ fixtures/trait-methods/tests/bindings/test.py | 2 +- .../trait-methods/tests/bindings/test.swift | 1 + .../bindings/kotlin/templates/Interface.kt | 2 +- .../kotlin/templates/ObjectTemplate.kt | 99 +++------------- .../templates/TopLevelFunctionTemplate.kt | 53 +-------- .../src/bindings/kotlin/templates/macros.kt | 110 +++++++++++++----- .../python/templates/ObjectTemplate.py | 20 ++++ .../swift/templates/ObjectTemplate.swift | 75 ++---------- .../templates/TopLevelFunctionTemplate.swift | 52 +-------- .../src/bindings/swift/templates/macros.swift | 92 +++++++++++++-- uniffi_bindgen/src/interface/function.rs | 7 ++ uniffi_bindgen/src/interface/object.rs | 15 ++- .../scaffolding/templates/ObjectTemplate.rs | 2 +- uniffi_macros/src/export/scaffolding.rs | 3 - uniffi_macros/src/fnsig.rs | 8 +- uniffi_meta/src/lib.rs | 1 + uniffi_meta/src/reader.rs | 2 + uniffi_udl/src/attributes.rs | 10 ++ uniffi_udl/src/converters/callables.rs | 1 + 27 files changed, 347 insertions(+), 309 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5623e2f5c8..b9aa599d54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ ### What's new? +- Constructors can be async. Alternate constructors work in Python, Kotlin and Swift; + only Swift supports primary constructors. + - Enums created with proc macros can now produce literals for variants in Kotlin and Swift. See [the section on enum proc-macros](https://mozilla.github.io/uniffi-rs/proc_macro/index.html#the-uniffienum-derive) for more information. diff --git a/docs/manual/src/udl/interfaces.md b/docs/manual/src/udl/interfaces.md index dbdad7d13b..fdaa52a035 100644 --- a/docs/manual/src/udl/interfaces.md +++ b/docs/manual/src/udl/interfaces.md @@ -164,6 +164,9 @@ interface TodoList { // This alternate constructor makes a new TodoList from a list of string items. [Name=new_from_items] constructor(sequence items); + // This alternate constructor is async. + [Async, Name=new_async] + constructor(sequence items); ... ``` @@ -171,6 +174,8 @@ For each alternate constructor, UniFFI will expose an appropriate static-method, in the foreign language binding, and will connect it to the Rust method of the same name on the underlying Rust struct. +Constructors can be async, although support for async primary constructors in bindings is minimal. + ## Exposing methods from standard Rust traits Rust has a number of general purpose traits which add functionality to objects, such diff --git a/fixtures/coverall/tests/bindings/test_coverall.kts b/fixtures/coverall/tests/bindings/test_coverall.kts index 176894915c..afabf695e7 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.kts +++ b/fixtures/coverall/tests/bindings/test_coverall.kts @@ -528,10 +528,11 @@ Coveralls("using_fakes_with_real_objects_crashes").use { coveralls -> // * We need to System.gc() and/or sleep. // * There's one stray thing alive, not sure what that is, but it's unrelated. for (i in 1..100) { - if (getNumAlive() > 1UL) { - System.gc() - Thread.sleep(100) + if (getNumAlive() <= 1UL) { + break } + System.gc() + Thread.sleep(100) } assert(getNumAlive() <= 1UL) { "Num alive is ${getNumAlive()}. GC/Cleaner thread has starved" }; diff --git a/fixtures/futures/src/futures.udl b/fixtures/futures/src/futures.udl index e4fd710a72..11e734e4a3 100644 --- a/fixtures/futures/src/futures.udl +++ b/fixtures/futures/src/futures.udl @@ -8,3 +8,14 @@ interface SayAfterUdlTrait { [Async] string say_after(u16 ms, string who); }; + +interface UdlMegaphone { + [Async] + constructor(); + + [Async, Name="secondary"] + constructor(); + + [Async] + string say_after(u16 ms, string who); +}; diff --git a/fixtures/futures/src/lib.rs b/fixtures/futures/src/lib.rs index 15bc32b9cf..2a18a65555 100644 --- a/fixtures/futures/src/lib.rs +++ b/fixtures/futures/src/lib.rs @@ -172,6 +172,20 @@ pub struct Megaphone; #[uniffi::export] impl Megaphone { + // the default constructor - many bindings will not support this. + #[uniffi::constructor] + pub async fn new() -> Arc { + TimerFuture::new(Duration::from_millis(0)).await; + Arc::new(Self {}) + } + + // most should support this. + #[uniffi::constructor] + pub async fn secondary() -> Arc { + TimerFuture::new(Duration::from_millis(0)).await; + Arc::new(Self {}) + } + /// An async method that yells something after a certain time. pub async fn say_after(self: Arc, ms: u16, who: String) -> String { say_after(ms, who).await.to_uppercase() @@ -218,6 +232,22 @@ pub async fn say_after_with_tokio(ms: u16, who: String) -> String { format!("Hello, {who} (with Tokio)!") } +pub struct UdlMegaphone; + +impl UdlMegaphone { + pub async fn new() -> Self { + Self {} + } + + pub async fn secondary() -> Self { + Self {} + } + + pub async fn say_after(self: Arc, ms: u16, who: String) -> String { + say_after(ms, who).await.to_uppercase() + } +} + #[derive(uniffi::Record)] pub struct MyRecord { pub a: String, diff --git a/fixtures/futures/tests/bindings/test_futures.kts b/fixtures/futures/tests/bindings/test_futures.kts index f853ddb4ea..d1d3c59012 100644 --- a/fixtures/futures/tests/bindings/test_futures.kts +++ b/fixtures/futures/tests/bindings/test_futures.kts @@ -103,6 +103,12 @@ runBlocking { assertApproximateTime(time, 200, "async methods") } +// Test async constructors +runBlocking { + val megaphone = Megaphone.secondary() + assert(megaphone.sayAfter(1U, "hi") == "HELLO, HI!") +} + // Test async method returning optional object runBlocking { val megaphone = asyncMaybeNewMegaphone(true) diff --git a/fixtures/futures/tests/bindings/test_futures.py b/fixtures/futures/tests/bindings/test_futures.py index 1b84451b5d..d91407391b 100644 --- a/fixtures/futures/tests/bindings/test_futures.py +++ b/fixtures/futures/tests/bindings/test_futures.py @@ -76,6 +76,23 @@ async def test(): asyncio.run(test()) + def test_async_constructors(self): + # Check the default constructor has been disabled. + with self.assertRaises(ValueError) as e: + Megaphone() + self.assertTrue(str(e.exception).startswith("async constructors not supported")) + + async def test(): + megaphone = await Megaphone.secondary() + result_alice = await megaphone.say_after(0, 'Alice') + self.assertEqual(result_alice, 'HELLO, ALICE!') + + udl_megaphone = await UdlMegaphone.secondary() + result_udl = await udl_megaphone.say_after(0, 'udl') + self.assertEqual(result_udl, 'HELLO, UDL!') + + asyncio.run(test()) + def test_async_trait_interface_methods(self): async def test(): traits = get_say_after_traits() diff --git a/fixtures/futures/tests/bindings/test_futures.swift b/fixtures/futures/tests/bindings/test_futures.swift index 11dacd870e..80fe7e469d 100644 --- a/fixtures/futures/tests/bindings/test_futures.swift +++ b/fixtures/futures/tests/bindings/test_futures.swift @@ -253,6 +253,28 @@ Task { counter.leave() } +counter.enter() + +Task { + let megaphone = await Megaphone() + + let result = try await megaphone.fallibleMe(doFail: false) + assert(result == 42) + + counter.leave() +} + +counter.enter() + +Task { + let megaphone = await Megaphone.secondary() + + let result = try await megaphone.fallibleMe(doFail: false) + assert(result == 42) + + counter.leave() +} + // Test with the Tokio runtime. counter.enter() diff --git a/fixtures/trait-methods/tests/bindings/test.py b/fixtures/trait-methods/tests/bindings/test.py index 9ec51a4cb0..b8555c3bdb 100644 --- a/fixtures/trait-methods/tests/bindings/test.py +++ b/fixtures/trait-methods/tests/bindings/test.py @@ -19,7 +19,7 @@ def test_eq(self): self.assertEqual(m, TraitMethods("yo")) self.assertNotEqual(m, TraitMethods("yoyo")) - def test_eq(self): + def test_eq_wrong_type(self): m = TraitMethods("yo") self.assertNotEqual(m, 17) diff --git a/fixtures/trait-methods/tests/bindings/test.swift b/fixtures/trait-methods/tests/bindings/test.swift index c7a7adbc03..a2c44b21cd 100644 --- a/fixtures/trait-methods/tests/bindings/test.swift +++ b/fixtures/trait-methods/tests/bindings/test.swift @@ -6,6 +6,7 @@ assert(String(reflecting: m) == "TraitMethods { val: \"yo\" }") // eq assert(m == TraitMethods(name: "yo")) +assert(m != TraitMethods(name: "foo")) // hash var set: Set = [TraitMethods(name: "yo")] diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Interface.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Interface.kt index c68e972c95..0b4249fb11 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/Interface.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Interface.kt @@ -3,7 +3,7 @@ public interface {{ interface_name }} { {% for meth in methods.iter() -%} {%- call kt::docstring(meth, 4) %} {% if meth.is_async() -%}suspend {% endif -%} - fun {{ meth.name()|fn_name }}({% call kt::arg_list_decl(meth) %}) + fun {{ meth.name()|fn_name }}({% call kt::arg_list(meth, true) %}) {%- match meth.return_type() -%} {%- when Some with (return_type) %}: {{ return_type|type_name(ci) -}} {%- else -%} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt index e3000a5573..62cac7a4d0 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt @@ -134,9 +134,13 @@ open class {{ impl_class_name }}: Disposable, AutoCloseable, {{ interface_name } {%- match obj.primary_constructor() %} {%- when Some(cons) %} + {%- if cons.is_async() %} + // Note no constructor generated for this object as it is async. + {%- else %} {%- call kt::docstring(cons, 4) %} - constructor({% call kt::arg_list_decl(cons) -%}) : + constructor({% call kt::arg_list(cons, true) -%}) : this({% call kt::to_ffi_call(cons) %}) + {%- endif %} {%- when None %} {%- endmatch %} @@ -204,93 +208,26 @@ open class {{ impl_class_name }}: Disposable, AutoCloseable, {{ interface_name } } {% for meth in obj.methods() -%} - {%- call kt::docstring(meth, 4) %} - {%- match meth.throws_type() -%} - {%- when Some(throwable) %} - @Throws({{ throwable|type_name(ci) }}::class) - {%- else -%} - {%- endmatch -%} - {%- 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(ci) }}{% when None %}{%- endmatch %} { - return uniffiRustCallAsync( - callWithPointer { thisPtr -> - UniffiLib.INSTANCE.{{ meth.ffi_func().name() }}( - thisPtr, - {% call kt::arg_list_lowered(meth) %} - ) - }, - {{ meth|async_poll(ci) }}, - {{ meth|async_complete(ci) }}, - {{ meth|async_free(ci) }}, - // 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|type_name(ci) }}.ErrorHandler, - {%- when None %} - UniffiNullRustCallStatusErrorHandler, - {%- endmatch %} - ) - } - {%- else -%} - {%- match meth.return_type() -%} - {%- when Some with (return_type) -%} - override fun {{ meth.name()|fn_name }}( - {%- call kt::arg_list_protocol(meth) -%} - ): {{ return_type|type_name(ci) }} = - callWithPointer { - {%- call kt::to_ffi_call_with_prefix("it", meth) %} - }.let { - {{ return_type|lift_fn }}(it) - } - - {%- when None -%} - override fun {{ meth.name()|fn_name }}( - {%- call kt::arg_list_protocol(meth) -%} - ) = - callWithPointer { - {%- call kt::to_ffi_call_with_prefix("it", meth) %} - } - {% endmatch %} - {% endif %} + {%- call kt::func_decl("override", meth, 4) %} {% endfor %} {%- for tm in obj.uniffi_traits() %} {%- match tm %} - {%- when UniffiTrait::Display { fmt } %} - override fun toString(): String = - callWithPointer { - {%- call kt::to_ffi_call_with_prefix("it", fmt) %} - }.let { - {{ fmt.return_type().unwrap()|lift_fn }}(it) - } - {%- when UniffiTrait::Eq { eq, ne } %} + {% when UniffiTrait::Display { fmt } %} + override fun toString(): String { + return {{ fmt.return_type().unwrap()|lift_fn }}({% call kt::to_ffi_call(fmt) %}) + } + {% when UniffiTrait::Eq { eq, ne } %} {# only equals used #} override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is {{ impl_class_name}}) return false - return callWithPointer { - {%- call kt::to_ffi_call_with_prefix("it", eq) %} - }.let { - {{ eq.return_type().unwrap()|lift_fn }}(it) - } + return {{ eq.return_type().unwrap()|lift_fn }}({% call kt::to_ffi_call(eq) %}) + } + {% when UniffiTrait::Hash { hash } %} + override fun hashCode(): Int { + return {{ hash.return_type().unwrap()|lift_fn }}({%- call kt::to_ffi_call(hash) %}).toInt() } - {%- when UniffiTrait::Hash { hash } %} - override fun hashCode(): Int = - callWithPointer { - {%- call kt::to_ffi_call_with_prefix("it", hash) %} - }.let { - {{ hash.return_type().unwrap()|lift_fn }}(it).toInt() - } {%- else %} {%- endmatch %} {%- endfor %} @@ -299,9 +236,7 @@ open class {{ impl_class_name }}: Disposable, AutoCloseable, {{ interface_name } {% if !obj.alternate_constructors().is_empty() -%} companion object { {% for cons in obj.alternate_constructors() -%} - {%- call kt::docstring(cons, 4) %} - fun {{ cons.name()|fn_name }}({% call kt::arg_list_decl(cons) %}): {{ impl_class_name }} = - {{ impl_class_name }}({% call kt::to_ffi_call(cons) %}) + {% call kt::func_decl("", cons, 4) %} {% endfor %} } {% else if is_error %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt index fc6f730ef0..681c48093a 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt @@ -1,52 +1 @@ -{%- call kt::docstring(func, 8) %} -{%- if func.is_async() %} -{%- match func.throws_type() -%} -{%- when Some with (throwable) %} -@Throws({{ throwable|type_name(ci) }}::class) -{%- else -%} -{%- endmatch %} - -@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(ci) }}{% when None %}{%- endmatch %} { - return uniffiRustCallAsync( - UniffiLib.INSTANCE.{{ func.ffi_func().name() }}({% call kt::arg_list_lowered(func) %}), - {{ func|async_poll(ci) }}, - {{ func|async_complete(ci) }}, - {{ func|async_free(ci) }}, - // 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|type_name(ci) }}.ErrorHandler, - {%- when None %} - UniffiNullRustCallStatusErrorHandler, - {%- endmatch %} - ) -} - -{%- else %} -{%- match func.throws_type() -%} -{%- when Some with (throwable) %} -@Throws({{ throwable|type_name(ci) }}::class) -{%- else -%} -{%- endmatch -%} - -{%- match func.return_type() -%} -{%- when Some with (return_type) %} - -fun {{ func.name()|fn_name }}({%- call kt::arg_list_decl(func) -%}): {{ return_type|type_name(ci) }} { - return {{ return_type|lift_fn }}({% call kt::to_ffi_call(func) %}) -} -{% when None %} - -fun {{ func.name()|fn_name }}({% call kt::arg_list_decl(func) %}) = - {% call kt::to_ffi_call(func) %} - -{% endmatch %} -{%- endif %} +{%- call kt::func_decl("", func, 8) %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt b/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt index 335534f439..7acfdc8861 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt @@ -1,21 +1,20 @@ {# // Template to call into rust. Used in several places. -// Variable names in `arg_list_decl` should match up with arg lists +// Variable names in `arg_list` should match up with arg lists // passed to rust via `arg_list_lowered` #} {%- macro to_ffi_call(func) -%} - {%- match func.throws_type() %} - {%- when Some with (e) %} - uniffiRustCallWithError({{ e|type_name(ci) }}) - {%- else %} - uniffiRustCall() - {%- endmatch %} { _status -> - UniffiLib.INSTANCE.{{ func.ffi_func().name() }}({% call arg_list_lowered(func) -%} _status) -} -{%- endmacro -%} + {%- if func.takes_self() %} + callWithPointer { + {%- call to_raw_ffi_call(func) %} + } + {% else %} + {%- call to_raw_ffi_call(func) %} + {% endif %} +{%- endmacro %} -{%- macro to_ffi_call_with_prefix(prefix, func) %} +{%- macro to_raw_ffi_call(func) -%} {%- match func.throws_type() %} {%- when Some with (e) %} uniffiRustCallWithError({{ e|type_name(ci) }}) @@ -23,10 +22,70 @@ uniffiRustCall() {%- endmatch %} { _status -> UniffiLib.INSTANCE.{{ func.ffi_func().name() }}( - {{- prefix }}, - {% call arg_list_lowered(func) %} + {% if func.takes_self() %}it, {% endif -%} + {% call arg_list_lowered(func) -%} _status) } +{%- endmacro -%} + +{%- macro func_decl(func_decl, callable, indent) %} + {%- call docstring(callable, indent) %} + {%- match callable.throws_type() -%} + {%- when Some(throwable) %} + @Throws({{ throwable|type_name(ci) }}::class) + {%- else -%} + {%- endmatch -%} + {%- if callable.is_async() %} + @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + {{ func_decl }} suspend fun {{ callable.name()|fn_name }}( + {%- call arg_list(callable, !callable.takes_self()) -%} + ){% match callable.return_type() %}{% when Some with (return_type) %} : {{ return_type|type_name(ci) }}{% when None %}{%- endmatch %} { + return {% call call_async(callable) %} + } + {%- else -%} + {{ func_decl }} fun {{ callable.name()|fn_name }}( + {%- call arg_list(callable, !callable.takes_self()) -%} + ){%- match callable.return_type() -%} + {%- when Some with (return_type) -%} + : {{ return_type|type_name(ci) }} { + return {{ return_type|lift_fn }}({% call to_ffi_call(callable) %}) + } + {%- when None %} + = {% call to_ffi_call(callable) %} + {%- endmatch %} + {% endif %} +{% endmacro %} + +{%- macro call_async(callable) -%} + uniffiRustCallAsync( +{%- if callable.takes_self() %} + callWithPointer { thisPtr -> + UniffiLib.INSTANCE.{{ callable.ffi_func().name() }}( + thisPtr, + {% call arg_list_lowered(callable) %} + ) + }, +{%- else %} + UniffiLib.INSTANCE.{{ callable.ffi_func().name() }}({% call arg_list_lowered(callable) %}), +{%- endif %} + {{ callable|async_poll(ci) }}, + {{ callable|async_complete(ci) }}, + {{ callable|async_free(ci) }}, + // lift function + {%- match callable.return_type() %} + {%- when Some(return_type) %} + { {{ return_type|lift_fn }}(it) }, + {%- when None %} + { Unit }, + {% endmatch %} + // Error FFI converter + {%- match callable.throws_type() %} + {%- when Some(e) %} + {{ e|type_name(ci) }}.ErrorHandler, + {%- when None %} + UniffiNullRustCallStatusErrorHandler, + {%- endmatch %} + ) {%- endmacro %} {%- macro arg_list_lowered(func) %} @@ -37,26 +96,23 @@ {#- // Arglist as used in kotlin declarations of methods, functions and constructors. +// If is_decl, then default values be specified. // Note the var_name and type_name filters. -#} -{% macro arg_list_decl(func) %} - {%- for arg in func.arguments() -%} +{% macro arg_list(func, is_decl) %} +{%- for arg in func.arguments() -%} {{ arg.name()|var_name }}: {{ arg|type_name(ci) }} - {%- match arg.default_value() %} - {%- when Some with(literal) %} = {{ literal|render_literal(arg, ci) }} - {%- else %} - {%- endmatch %} - {%- if !loop.last %}, {% endif -%} - {%- endfor %} +{%- if is_decl %} +{%- match arg.default_value() %} +{%- when Some with(literal) %} = {{ literal|render_literal(arg, ci) }} +{%- else %} +{%- endmatch %} +{%- endif %} +{%- if !loop.last %}, {% endif -%} +{%- endfor %} {%- endmacro %} -{% macro arg_list_protocol(func) %} - {%- for arg in func.arguments() -%} - {{ arg.name()|var_name }}: {{ arg|type_name(ci) }} - {%- if !loop.last %}, {% endif -%} - {%- endfor %} -{%- endmacro %} {#- // Arglist as used in the UniffiLib function declarations. // Note unfiltered name but ffi_type_name filters. diff --git a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py index d9c0f0d868..18dca4743a 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py @@ -15,10 +15,15 @@ class {{ impl_name }}: {%- match obj.primary_constructor() %} {%- when Some with (cons) %} +{%- if cons.is_async() %} + def __init__(self, *args, **kw): + raise ValueError("async constructors not supported.") +{%- else %} def __init__(self, {% call py::arg_list_decl(cons) -%}): {%- call py::docstring(cons, 8) %} {%- call py::setup_args_extra_indent(cons) %} self._pointer = {% call py::to_ffi_call(cons) %} +{%- endif %} {%- when None %} {# no __init__ means simple construction without a pointer works, which can confuse #} def __init__(self, *args, **kwargs): @@ -46,12 +51,27 @@ def _make_instance_(cls, pointer): {%- for cons in obj.alternate_constructors() %} @classmethod +{%- if cons.is_async() %} + async def {{ cons.name()|fn_name }}(cls, {% call py::arg_list_decl(cons) %}): + {%- call py::docstring(cons, 8) %} + {%- call py::setup_args_extra_indent(cons) %} + + return await _uniffi_rust_call_async( + _UniffiLib.{{ cons.ffi_func().name() }}({% call py::arg_list_lowered(cons) %}), + _UniffiLib.{{ cons.ffi_rust_future_poll(ci) }}, + _UniffiLib.{{ cons.ffi_rust_future_complete(ci) }}, + _UniffiLib.{{ cons.ffi_rust_future_free(ci) }}, + {{ ffi_converter_name }}.lift, + {% call py::error_ffi_converter(cons) %} + ) +{%- else %} def {{ cons.name()|fn_name }}(cls, {% call py::arg_list_decl(cons) %}): {%- call py::docstring(cons, 8) %} {%- call py::setup_args_extra_indent(cons) %} # Call the (fallible) function before creating any half-baked object instances. pointer = {% call py::to_ffi_call(cons) %} return cls._make_instance_(pointer) +{%- endif %} {% endfor %} {%- for meth in obj.methods() -%} diff --git a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift index 8cd072e825..0c28bc4c09 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift @@ -55,11 +55,9 @@ open class {{ impl_class_name }}: {%- match obj.primary_constructor() %} {%- when Some with (cons) %} - {%- call swift::docstring(cons, 4) %} - public convenience init({% call swift::arg_list_decl(cons) -%}) {% call swift::throws(cons) %} { - self.init(unsafeFromRawPointer: {% call swift::to_ffi_call(cons) %}) - } + {%- call swift::ctor_decl(cons, 4) %} {%- when None %} + // No primary constructor declared for this class. {%- endmatch %} deinit { @@ -71,66 +69,11 @@ open class {{ impl_class_name }}: } {% for cons in obj.alternate_constructors() %} - {%- call swift::docstring(cons, 4) %} - 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) %}) - } - + {%- call swift::func_decl("public static func", cons, 4) %} {% endfor %} - {# // TODO: Maybe merge the two templates (i.e the one with a return type and the one without) #} {% for meth in obj.methods() -%} - {%- if meth.is_async() %} - {%- call swift::docstring(meth, 4) %} - open 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 %} { - return {% call swift::try(meth) %} await uniffiRustCallAsync( - rustFutureFunc: { - {{ meth.ffi_func().name() }}( - self.uniffiClonePointer() - {%- for arg in meth.arguments() -%} - , - {{ arg|lower_fn }}({{ arg.name()|var_name }}) - {%- 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_error_converter_name }}.lift - {%- else %} - errorHandler: nil - {% endmatch %} - ) - } - - {% else -%} - - {%- match meth.return_type() -%} - - {%- when Some with (return_type) %} - {%- call swift::docstring(meth, 4) %} - open func {{ meth.name()|fn_name }}({% call swift::arg_list_decl(meth) %}) {% call swift::throws(meth) %} -> {{ return_type|type_name }} { - return {% call swift::try(meth) %} {{ return_type|lift_fn }}( - {% call swift::to_ffi_call_with_prefix("self.uniffiClonePointer()", meth) %} - ) - } - - {%- when None %} - {%- call swift::docstring(meth, 4) %} - open func {{ meth.name()|fn_name }}({% call swift::arg_list_decl(meth) %}) {% call swift::throws(meth) %} { - {% call swift::to_ffi_call_with_prefix("self.uniffiClonePointer()", meth) %} - } - - {%- endmatch -%} - {%- endif -%} + {%- call swift::func_decl("open func", meth, 4) %} {% endfor %} {%- for tm in obj.uniffi_traits() %} @@ -138,25 +81,25 @@ open class {{ impl_class_name }}: {%- when UniffiTrait::Display { fmt } %} open var description: String { return {% call swift::try(fmt) %} {{ fmt.return_type().unwrap()|lift_fn }}( - {% call swift::to_ffi_call_with_prefix("self.uniffiClonePointer()", fmt) %} + {% call swift::to_ffi_call(fmt) %} ) } {%- when UniffiTrait::Debug { fmt } %} open var debugDescription: String { return {% call swift::try(fmt) %} {{ fmt.return_type().unwrap()|lift_fn }}( - {% call swift::to_ffi_call_with_prefix("self.uniffiClonePointer()", fmt) %} + {% call swift::to_ffi_call(fmt) %} ) } {%- when UniffiTrait::Eq { eq, ne } %} - public static func == (lhs: {{ impl_class_name }}, other: {{ impl_class_name }}) -> Bool { + public static func == (self: {{ impl_class_name }}, other: {{ impl_class_name }}) -> Bool { return {% call swift::try(eq) %} {{ eq.return_type().unwrap()|lift_fn }}( - {% call swift::to_ffi_call_with_prefix("lhs.uniffiClonePointer()", eq) %} + {% call swift::to_ffi_call(eq) %} ) } {%- when UniffiTrait::Hash { hash } %} open func hash(into hasher: inout Hasher) { let val = {% call swift::try(hash) %} {{ hash.return_type().unwrap()|lift_fn }}( - {% call swift::to_ffi_call_with_prefix("self.uniffiClonePointer()", hash) %} + {% call swift::to_ffi_call(hash) %} ) hasher.combine(val) } diff --git a/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift index 2e6870cacb..ce946076f7 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift @@ -1,51 +1 @@ -{%- if func.is_async() %} - -{%- call swift::docstring(func, 0) %} -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 %} { - return {% call swift::try(func) %} await uniffiRustCallAsync( - rustFutureFunc: { - {{ func.ffi_func().name() }}( - {%- 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_error_converter_name }}.lift - {%- else %} - errorHandler: nil - {% endmatch %} - ) -} - -{% else %} - -{%- match func.return_type() -%} -{%- when Some with (return_type) %} - -{%- call swift::docstring(func, 0) %} -public func {{ func.name()|fn_name }}({%- call swift::arg_list_decl(func) -%}) {% call swift::throws(func) %} -> {{ return_type|type_name }} { - return {% call swift::try(func) %} {{ return_type|lift_fn }}( - {% call swift::to_ffi_call(func) %} - ) -} - -{%- when None %} - -{%- call swift::docstring(func, 0) %} -public func {{ func.name()|fn_name }}({% call swift::arg_list_decl(func) %}) {% call swift::throws(func) %} { - {% call swift::to_ffi_call(func) %} -} - -{% endmatch %} -{%- endif %} +{%- call swift::func_decl("public func", func, 0) %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/macros.swift b/uniffi_bindgen/src/bindings/swift/templates/macros.swift index bdc883ec65..be6794e59b 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/macros.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/macros.swift @@ -12,24 +12,93 @@ {%- else -%} rustCall() { {%- endmatch %} - {{ func.ffi_func().name() }}({% call arg_list_lowered(func) -%} $0) + {{ func.ffi_func().name() }}( + {%- if func.takes_self() %}self.uniffiClonePointer(),{% endif %} + {%- call arg_list_lowered(func) -%} $0 + ) } {%- endmacro -%} -{%- macro to_ffi_call_with_prefix(prefix, func) -%} -{% call try(func) %} - {%- match func.throws_type() %} - {%- when Some with (e) %} - rustCallWithError({{ e|ffi_error_converter_name }}.lift) { +// eg, `public func foo_bar() { body }` +{%- macro func_decl(func_decl, callable, indent) %} +{%- call docstring(callable, indent) %} +{{ func_decl }} {{ callable.name()|fn_name }}( + {%- call arg_list_decl(callable) -%}) + {%- call async(callable) %} + {%- call throws(callable) %} + {%- match callable.return_type() %} + {%- when Some with (return_type) %} -> {{ return_type|type_name }} + {%- when None %} + {%- endmatch %} { + {%- call call_body(callable) %} +} +{%- endmacro %} + +// primary ctor - no name, no return-type. +{%- macro ctor_decl(callable, indent) %} +{%- call docstring(callable, indent) %} +public convenience init( + {%- call arg_list_decl(callable) -%}) {%- call async(callable) %} { + {%- if callable.is_async() %} + let pointer = {% call throws(callable) %} + {%- call call_async(callable) %} + {# The async mechanism returns an already constructed self. + We work around that by cloning the pointer from that object, then + assune the old object dies as there are no other references possible. + #} + .uniffiClonePointer() {%- else %} - rustCall() { - {% endmatch %} - {{ func.ffi_func().name() }}( - {{- prefix }}, {% call arg_list_lowered(func) -%} $0 - ) + let pointer = {% call throws(callable) %} + {%- call to_ffi_call(callable) %} + {%- endif %} + self.init(unsafeFromRawPointer: pointer) } {%- endmacro %} +{%- macro call_body(callable) %} +{%- if callable.is_async() %} + return {%- call call_async(callable) %} +{%- else %} +{%- match callable.return_type() -%} +{%- when Some with (return_type) %} + return {% call try(callable) %} {{ return_type|lift_fn }}({% call to_ffi_call(callable) %}) +{%- when None %} +{%- call to_ffi_call(callable) %} +{%- endmatch %} +{%- endif %} + +{%- endmacro %} + +{%- macro call_async(callable) %} + {% call try(callable) %} await uniffiRustCallAsync( + rustFutureFunc: { + {{ callable.ffi_func().name() }}( + {%- if callable.takes_self() %} + self.uniffiClonePointer(){% if !callable.arguments().is_empty() %},{% endif %} + {% endif %} + {%- for arg in callable.arguments() -%} + {{ arg|lower_fn }}({{ arg.name()|var_name }}){% if !loop.last %},{% endif %} + {%- endfor %} + ) + }, + pollFunc: {{ callable.ffi_rust_future_poll(ci) }}, + completeFunc: {{ callable.ffi_rust_future_complete(ci) }}, + freeFunc: {{ callable.ffi_rust_future_free(ci) }}, + {%- match callable.return_type() %} + {%- when Some(return_type) %} + liftFunc: {{ return_type|lift_fn }}, + {%- when None %} + liftFunc: { $0 }, + {%- endmatch %} + {%- match callable.throws_type() %} + {%- when Some with (e) %} + errorHandler: {{ e|ffi_error_converter_name }}.lift + {%- else %} + errorHandler: nil + {% endmatch %} + ) +{%- endmacro %} + {%- macro arg_list_lowered(func) %} {%- for arg in func.arguments() %} {{ arg|lower_fn }}({{ arg.name()|var_name }}), @@ -88,7 +157,6 @@ v{{- field_num -}} {%- endfor %} {%- endmacro %} - {%- macro async(func) %} {%- if func.is_async() %}async {% endif %} {%- endmacro -%} diff --git a/uniffi_bindgen/src/interface/function.rs b/uniffi_bindgen/src/interface/function.rs index 2692b4bcc9..8effc4c876 100644 --- a/uniffi_bindgen/src/interface/function.rs +++ b/uniffi_bindgen/src/interface/function.rs @@ -249,6 +249,9 @@ pub trait Callable { fn return_type(&self) -> Option; fn throws_type(&self) -> Option; fn is_async(&self) -> bool; + fn takes_self(&self) -> bool { + false + } fn result_type(&self) -> ResultType { ResultType { return_type: self.return_type(), @@ -318,6 +321,10 @@ impl Callable for &T { fn is_async(&self) -> bool { (*self).is_async() } + + fn takes_self(&self) -> bool { + (*self).takes_self() + } } #[cfg(test)] diff --git a/uniffi_bindgen/src/interface/object.rs b/uniffi_bindgen/src/interface/object.rs index 5ef8332dfd..2b86e54a45 100644 --- a/uniffi_bindgen/src/interface/object.rs +++ b/uniffi_bindgen/src/interface/object.rs @@ -355,6 +355,7 @@ pub struct Constructor { pub(super) name: String, pub(super) object_name: String, pub(super) object_module_path: String, + pub(super) is_async: bool, pub(super) arguments: Vec, // We don't include the FFIFunc in the hash calculation, because: // - it is entirely determined by the other fields, @@ -420,8 +421,10 @@ impl Constructor { fn derive_ffi_func(&mut self) { assert!(!self.ffi_func.name().is_empty()); - self.ffi_func.arguments = self.arguments.iter().map(Into::into).collect(); - self.ffi_func.return_type = Some(FfiType::RustArcPtr(self.object_name.clone())); + self.ffi_func.init( + Some(FfiType::RustArcPtr(self.object_name.clone())), + self.arguments.iter().map(Into::into), + ); } pub fn iter_types(&self) -> TypeIterator<'_> { @@ -437,11 +440,13 @@ impl From for Constructor { let ffi_func = FfiFunction { name: ffi_name, + is_async: meta.is_async, ..FfiFunction::default() }; Self { name: meta.name, object_name: meta.self_name, + is_async: meta.is_async, object_module_path: meta.module_path, arguments, ffi_func, @@ -697,7 +702,7 @@ impl Callable for Constructor { } fn is_async(&self) -> bool { - false + self.is_async } } @@ -717,6 +722,10 @@ impl Callable for Method { fn is_async(&self) -> bool { self.is_async } + + fn takes_self(&self) -> bool { + true + } } #[cfg(test)] diff --git a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs index c63a20c8c1..e752878af5 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs @@ -40,7 +40,7 @@ struct {{ obj.rust_name() }} { } #[::uniffi::export_for_udl] impl {{ obj.rust_name() }} { #[uniffi::constructor] - pub fn r#{{ cons.name() }}( + pub {% if cons.is_async() %}async {% endif %}fn r#{{ cons.name() }}( {%- for arg in cons.arguments() %} r#{{ arg.name() }}: {% if arg.by_ref() %}&{% endif %}{{ arg.as_type().borrow()|type_rs }}, {%- endfor %} diff --git a/uniffi_macros/src/export/scaffolding.rs b/uniffi_macros/src/export/scaffolding.rs index b461e8d552..fa7b61deca 100644 --- a/uniffi_macros/src/export/scaffolding.rs +++ b/uniffi_macros/src/export/scaffolding.rs @@ -50,9 +50,6 @@ pub(super) fn gen_constructor_scaffolding( "constructors must not have a self parameter", )); } - if sig.is_async { - return Err(syn::Error::new(sig.span, "constructors can't be async")); - } let metadata_items = (!udl_mode).then(|| { sig.metadata_items() .unwrap_or_else(syn::Error::into_compile_error) diff --git a/uniffi_macros/src/fnsig.rs b/uniffi_macros/src/fnsig.rs index fa5460cfc3..9c59125207 100644 --- a/uniffi_macros/src/fnsig.rs +++ b/uniffi_macros/src/fnsig.rs @@ -101,13 +101,6 @@ impl FnSignature { }; let is_async = sig.asyncness.is_some(); - if is_async && matches!(kind, FnKind::Constructor { .. }) { - return Err(syn::Error::new( - span, - "Async constructors are not supported", - )); - } - let mut input_iter = sig .inputs .into_iter() @@ -323,6 +316,7 @@ impl FnSignature { .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) diff --git a/uniffi_meta/src/lib.rs b/uniffi_meta/src/lib.rs index faed6fc460..26cf18a3e6 100644 --- a/uniffi_meta/src/lib.rs +++ b/uniffi_meta/src/lib.rs @@ -161,6 +161,7 @@ pub struct ConstructorMetadata { pub module_path: String, pub self_name: String, pub name: String, + pub is_async: bool, pub inputs: Vec, pub throws: Option, pub checksum: Option, diff --git a/uniffi_meta/src/reader.rs b/uniffi_meta/src/reader.rs index f51bb786f8..2135533ab4 100644 --- a/uniffi_meta/src/reader.rs +++ b/uniffi_meta/src/reader.rs @@ -247,6 +247,7 @@ impl<'a> MetadataReader<'a> { let module_path = self.read_string()?; let self_name = self.read_string()?; let name = self.read_string()?; + let is_async = self.read_bool()?; let inputs = self.read_inputs()?; let (return_type, throws) = self.read_return_type()?; let docstring = self.read_optional_long_string()?; @@ -263,6 +264,7 @@ impl<'a> MetadataReader<'a> { Ok(ConstructorMetadata { module_path, self_name, + is_async, name, inputs, throws, diff --git a/uniffi_udl/src/attributes.rs b/uniffi_udl/src/attributes.rs index 89491fa3a5..7bb05808c4 100644 --- a/uniffi_udl/src/attributes.rs +++ b/uniffi_udl/src/attributes.rs @@ -456,6 +456,10 @@ impl ConstructorAttributes { _ => None, }) } + + pub(super) fn is_async(&self) -> bool { + self.0.iter().any(|attr| matches!(attr, Attribute::Async)) + } } impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for ConstructorAttributes { @@ -466,6 +470,7 @@ impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for ConstructorAttri let attrs = parse_attributes(weedle_attributes, |attr| match attr { Attribute::Throws(_) => Ok(()), Attribute::Name(_) => Ok(()), + Attribute::Async => Ok(()), _ => bail!(format!("{attr:?} not supported for constructors")), })?; Ok(Self(attrs)) @@ -824,6 +829,11 @@ mod test { let attrs = ConstructorAttributes::try_from(&node).unwrap(); assert!(matches!(attrs.get_throws_err(), Some("Error"))); assert!(matches!(attrs.get_name(), Some("MyFactory"))); + assert!(!attrs.is_async()); + + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Async]").unwrap(); + let attrs = ConstructorAttributes::try_from(&node).unwrap(); + assert!(attrs.is_async()); } #[test] diff --git a/uniffi_udl/src/converters/callables.rs b/uniffi_udl/src/converters/callables.rs index 27959901eb..dda3c3a3ce 100644 --- a/uniffi_udl/src/converters/callables.rs +++ b/uniffi_udl/src/converters/callables.rs @@ -126,6 +126,7 @@ impl APIConverter for weedle::interface::ConstructorInterfa name: String::from(attributes.get_name().unwrap_or("new")), // We don't know the name of the containing `Object` at this point, fill it in later. self_name: Default::default(), + is_async: attributes.is_async(), // Also fill in checksum_fn_name later, since it depends on object_name inputs: self.args.body.list.convert(ci)?, throws,