diff --git a/CHANGELOG.md b/CHANGELOG.md index 079dd86860..101ed1c712 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 b6d265b53b..90f7b2d3cd 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 88a6e1906c..6fec6cb3a3 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,