Skip to content

Commit

Permalink
Merge pull request #2048 from mhammond/async-ctors
Browse files Browse the repository at this point in the history
Add async constructor support.
  • Loading branch information
bendk authored Mar 26, 2024
2 parents 013f475 + a2d5feb commit 2a81b2e
Show file tree
Hide file tree
Showing 27 changed files with 347 additions and 309 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 5 additions & 0 deletions docs/manual/src/udl/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,18 @@ interface TodoList {
// This alternate constructor makes a new TodoList from a list of string items.
[Name=new_from_items]
constructor(sequence<string> items);
// This alternate constructor is async.
[Async, Name=new_async]
constructor(sequence<string> items);
...
```

For each alternate constructor, UniFFI will expose an appropriate static-method, class-method or similar
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
Expand Down
7 changes: 4 additions & 3 deletions fixtures/coverall/tests/bindings/test_coverall.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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" };
11 changes: 11 additions & 0 deletions fixtures/futures/src/futures.udl
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
30 changes: 30 additions & 0 deletions fixtures/futures/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
TimerFuture::new(Duration::from_millis(0)).await;
Arc::new(Self {})
}

// most should support this.
#[uniffi::constructor]
pub async fn secondary() -> Arc<Self> {
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<Self>, ms: u16, who: String) -> String {
say_after(ms, who).await.to_uppercase()
Expand Down Expand Up @@ -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<Self>, ms: u16, who: String) -> String {
say_after(ms, who).await.to_uppercase()
}
}

#[derive(uniffi::Record)]
pub struct MyRecord {
pub a: String,
Expand Down
6 changes: 6 additions & 0 deletions fixtures/futures/tests/bindings/test_futures.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions fixtures/futures/tests/bindings/test_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
22 changes: 22 additions & 0 deletions fixtures/futures/tests/bindings/test_futures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion fixtures/trait-methods/tests/bindings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions fixtures/trait-methods/tests/bindings/test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
2 changes: 1 addition & 1 deletion uniffi_bindgen/src/bindings/kotlin/templates/Interface.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 -%}
Expand Down
99 changes: 17 additions & 82 deletions uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}

Expand Down Expand Up @@ -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 %}
Expand All @@ -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 %}
Expand Down
Original file line number Diff line number Diff line change
@@ -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) %}
Loading

0 comments on commit 2a81b2e

Please sign in to comment.