diff --git a/Cargo.toml b/Cargo.toml index 0175f7fb78..04b848f0e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "examples/sprites", "examples/todolist", "fixtures/coverall", + "fixtures/callbacks", "fixtures/regressions/enum-without-i32-helpers", "fixtures/regressions/kotlin-experimental-unsigned-types", "fixtures/regressions/cdylib-crate-type-dependency/ffi-crate", diff --git a/examples/README.md b/examples/README.md index 7aa76e992f..ab04d0df22 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,7 +14,7 @@ Newcomers are recommended to explore them in the following order: object-oriented style. * [`./todolist`](./todolist/) is a simplistic todo-list that can only add items and show the last item, meant to show how interacting with strings works. -* [`.rondpoint`](./rondpoint/) exercises complex data types by round-tripping them from the foreign-language +* [`./rondpoint`](./rondpoint/) exercises complex data types by round-tripping them from the foreign-language code, through rust and back agian. * [`./fxa-client`](./fxa-client/) doesn't work yet, but it contains aspirational example of what the UDL might look like for an actual real-world component. diff --git a/examples/callbacks/Cargo.toml b/examples/callbacks/Cargo.toml index 2ba8463cf2..29917afaaa 100644 --- a/examples/callbacks/Cargo.toml +++ b/examples/callbacks/Cargo.toml @@ -8,7 +8,7 @@ publish = false [lib] crate-type = ["cdylib", "lib"] -name = "uniffi_callbacks" +name = "callbacks" [dependencies] uniffi_macros = {path = "../../uniffi_macros"} diff --git a/examples/callbacks/src/callbacks.udl b/examples/callbacks/src/callbacks.udl index a8bca47083..0293b14cf1 100644 --- a/examples/callbacks/src/callbacks.udl +++ b/examples/callbacks/src/callbacks.udl @@ -9,40 +9,3 @@ callback interface OnCallAnswered { void busy(); void text_received(string text); }; - -/// These objects are implemented by the foreign language and passed -/// to Rust. Rust then calls methods on it when it needs to. -callback interface ForeignGetters { - boolean get_bool(boolean v, boolean arg2); - string get_string(string v, boolean arg2); - string? get_option(string? v, boolean arg2); - sequence get_list(sequence v, boolean arg2); -}; - -/// These objects are implemented in Rust, and call out to `ForeignGetters` -/// to get the value. -interface RustGetters { - boolean get_bool(ForeignGetters callback, boolean v, boolean arg2); - string get_string(ForeignGetters callback, string v, boolean arg2); - string? get_option(ForeignGetters callback, string? v, boolean arg2); - sequence get_list(ForeignGetters callback, sequence v, boolean arg2); -}; - -/// These objects are implemented by the foreign language and passed -/// to Rust. Rust then calls methods on it when it needs to. -/// Rust developers need to declare these traits extending `Send` so -/// they can be stored in Rust— i.e. not passed in as an argument to -/// be used immediately. -callback interface StoredForeignStringifier { - string from_simple_type(i32 value); - // Test if types are collected from callback interfaces. - // kotlinc compile time error if not. - string from_complex_type(sequence? values); -}; - -/// Rust object that uses the StoredForeignStringifier to produce string representations -/// of passed arguments. -interface RustStringifier { - constructor(StoredForeignStringifier callback); - string from_simple_type(i32 value); -}; diff --git a/examples/callbacks/src/lib.rs b/examples/callbacks/src/lib.rs index a21464db13..49058d6483 100644 --- a/examples/callbacks/src/lib.rs +++ b/examples/callbacks/src/lib.rs @@ -24,65 +24,4 @@ impl Telephone { } } -trait ForeignGetters { - fn get_bool(&self, v: bool, arg2: bool) -> bool; - fn get_string(&self, v: String, arg2: bool) -> String; - fn get_option(&self, v: Option, arg2: bool) -> Option; - fn get_list(&self, v: Vec, arg2: bool) -> Vec; -} - -#[derive(Debug, Clone)] -pub struct RustGetters; - -impl RustGetters { - pub fn new() -> Self { - RustGetters - } - fn get_bool(&self, callback: Box, v: bool, arg2: bool) -> bool { - callback.get_bool(v, arg2) - } - fn get_string(&self, callback: Box, v: String, arg2: bool) -> String { - callback.get_string(v, arg2) - } - fn get_option( - &self, - callback: Box, - v: Option, - arg2: bool, - ) -> Option { - callback.get_option(v, arg2) - } - fn get_list(&self, callback: Box, v: Vec, arg2: bool) -> Vec { - callback.get_list(v, arg2) - } -} - -impl Default for RustGetters { - fn default() -> Self { - Self::new() - } -} - -// Use `Send+Send` because we want to store the callback in an exposed -// `Send+Sync` object. -trait StoredForeignStringifier: Send + Sync + std::fmt::Debug { - fn from_simple_type(&self, value: i32) -> String; - fn from_complex_type(&self, values: Option>>) -> String; -} - -#[derive(Debug)] -pub struct RustStringifier { - callback: Box, -} - -impl RustStringifier { - fn new(callback: Box) -> Self { - RustStringifier { callback } - } - - fn from_simple_type(&self, value: i32) -> String { - self.callback.from_simple_type(value) - } -} - include!(concat!(env!("OUT_DIR"), "/callbacks.uniffi.rs")); diff --git a/examples/callbacks/tests/bindings/test_callbacks.kts b/examples/callbacks/tests/bindings/test_callbacks.kts index 616cdb481b..2007f0f1aa 100644 --- a/examples/callbacks/tests/bindings/test_callbacks.kts +++ b/examples/callbacks/tests/bindings/test_callbacks.kts @@ -4,7 +4,7 @@ import uniffi.callbacks.* -// 0. Simple example just to see it work. +// Simple example just to see it work. // Pass in a string, get a string back. // Pass in nothing, get unit back. class OnCallAnsweredImpl : OnCallAnswered { @@ -47,67 +47,3 @@ assert(cbObjet2.busyCount == 0) { "yesCount=${cbObjet2.busyCount} (should be 0)" assert(cbObjet2.yesCount == 1) { "yesCount=${cbObjet2.yesCount} (should be 1)" } telephone.destroy() - -// A bit more systematic in testing, but this time in English. -// -// 1. Pass in the callback as arguments. -// Make the callback methods use multiple aruments, with a variety of types, and -// with a variety of return types. -val rustGetters = RustGetters() -class KotlinGetters(): ForeignGetters { - override fun getBool(v: Boolean, arg2: Boolean): Boolean = v xor arg2 - override fun getString(v: String, arg2: Boolean): String = if (arg2) "1234567890123" else v - override fun getOption(v: String?, arg2: Boolean): String? = if (arg2) v?.uppercase() else v - override fun getList(v: List, arg2: Boolean): List = if (arg2) v else listOf() -} - -val callback = KotlinGetters() -listOf(true, false).forEach { v -> - val flag = true - val expected = callback.getBool(v, flag) - val observed = rustGetters.getBool(callback, v, flag) - assert(expected == observed) { "roundtripping through callback: $expected != $observed" } -} - -listOf(listOf(1,2), listOf(0,1)).forEach { v -> - val flag = true - val expected = callback.getList(v, flag) - val observed = rustGetters.getList(callback, v, flag) - assert(expected == observed) { "roundtripping through callback: $expected != $observed" } -} - -listOf("Hello", "world").forEach { v -> - val flag = true - val expected = callback.getString(v, flag) - val observed = rustGetters.getString(callback, v, flag) - assert(expected == observed) { "roundtripping through callback: $expected != $observed" } -} - -listOf("Some", null).forEach { v -> - val flag = false - val expected = callback.getOption(v, flag) - val observed = rustGetters.getOption(callback, v, flag) - assert(expected == observed) { "roundtripping through callback: $expected != $observed" } -} -rustGetters.destroy() - -// 2. Pass the callback in as a constructor argument, to be stored on the Object struct. -// This is crucial if we want to configure a system at startup, -// then use it without passing callbacks all the time. - -class StoredKotlinStringifier: StoredForeignStringifier { - override fun fromSimpleType(value: Int): String = "kotlin: $value" - // We don't test this, but we're checking that the arg type is included in the minimal list of types used - // in the UDL. - // If this doesn't compile, then look at TypeResolver. - override fun fromComplexType(values: List?): String = "kotlin: $values" -} - -val kotlinStringifier = StoredKotlinStringifier() -val rustStringifier = RustStringifier(kotlinStringifier) -listOf(1, 2).forEach { v -> - val expected = kotlinStringifier.fromSimpleType(v) - val observed = rustStringifier.fromSimpleType(v) - assert(expected == observed) { "callback is sent on construction: $expected != $observed" } -} -rustStringifier.destroy() \ No newline at end of file diff --git a/examples/callbacks/uniffi.toml b/examples/callbacks/uniffi.toml new file mode 100644 index 0000000000..a2bfcce7b9 --- /dev/null +++ b/examples/callbacks/uniffi.toml @@ -0,0 +1,3 @@ +[bindings.kotlin] +package_name = "uniffi.callbacks" +cdylib_name = "callbacks" diff --git a/fixtures/callbacks/Cargo.toml b/fixtures/callbacks/Cargo.toml new file mode 100644 index 0000000000..c1da936220 --- /dev/null +++ b/fixtures/callbacks/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "callbacks" +version = "0.12.0" +authors = ["Firefox Sync Team "] +edition = "2018" +publish = false + +[lib] +crate-type = ["staticlib", "cdylib"] +name = "uniffi_callbacks" + +[dependencies] +uniffi_macros = {path = "../../uniffi_macros"} +uniffi = {path = "../../uniffi", features=["builtin-bindgen"]} +thiserror = "1.0" +lazy_static = "1.4" + +[build-dependencies] +uniffi_build = {path = "../../uniffi_build", features=["builtin-bindgen"]} diff --git a/fixtures/callbacks/README.md b/fixtures/callbacks/README.md new file mode 100644 index 0000000000..bbb999994a --- /dev/null +++ b/fixtures/callbacks/README.md @@ -0,0 +1,11 @@ +# A "Callbacks" test for uniffi components + +This is similar to the `callbacks` example, but it's intended to be contrived and to +ensure we get good test coverage of all possible options. + +It's here so it doesn't even need to make a cursory effort to be a "good" +example; it intentionally panics, asserts params are certain values, has +no-op methods etc. If you're trying to get your head around uniffi then the +"examples" directory will be a much better bet. + +This is its own crate, because the callback mechanism is not implemented for all backends yet. diff --git a/fixtures/callbacks/build.rs b/fixtures/callbacks/build.rs new file mode 100644 index 0000000000..a98aacddb3 --- /dev/null +++ b/fixtures/callbacks/build.rs @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +fn main() { + uniffi_build::generate_scaffolding("./src/callbacks.udl").unwrap(); +} diff --git a/fixtures/callbacks/src/callbacks.udl b/fixtures/callbacks/src/callbacks.udl new file mode 100644 index 0000000000..db61326e66 --- /dev/null +++ b/fixtures/callbacks/src/callbacks.udl @@ -0,0 +1,38 @@ +namespace callbacks {}; + +/// These objects are implemented by the foreign language and passed +/// to Rust. Rust then calls methods on it when it needs to. +callback interface ForeignGetters { + boolean get_bool(boolean v, boolean arg2); + string get_string(string v, boolean arg2); + string? get_option(string? v, boolean arg2); + sequence get_list(sequence v, boolean arg2); +}; + +/// These objects are implemented in Rust, and call out to `ForeignGetters` +/// to get the value. +interface RustGetters { + boolean get_bool(ForeignGetters callback, boolean v, boolean arg2); + string get_string(ForeignGetters callback, string v, boolean arg2); + string? get_option(ForeignGetters callback, string? v, boolean arg2); + sequence get_list(ForeignGetters callback, sequence v, boolean arg2); +}; + +/// These objects are implemented by the foreign language and passed +/// to Rust. Rust then calls methods on it when it needs to. +/// Rust developers need to declare these traits extending `Send` so +/// they can be stored in Rust— i.e. not passed in as an argument to +/// be used immediately. +callback interface StoredForeignStringifier { + string from_simple_type(i32 value); + // Test if types are collected from callback interfaces. + // kotlinc compile time error if not. + string from_complex_type(sequence? values); +}; + +/// Rust object that uses the StoredForeignStringifier to produce string representations +/// of passed arguments. +interface RustStringifier { + constructor(StoredForeignStringifier callback); + string from_simple_type(i32 value); +}; diff --git a/fixtures/callbacks/src/lib.rs b/fixtures/callbacks/src/lib.rs new file mode 100644 index 0000000000..8f41583614 --- /dev/null +++ b/fixtures/callbacks/src/lib.rs @@ -0,0 +1,66 @@ +/* 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/. */ + +trait ForeignGetters { + fn get_bool(&self, v: bool, arg2: bool) -> bool; + fn get_string(&self, v: String, arg2: bool) -> String; + fn get_option(&self, v: Option, arg2: bool) -> Option; + fn get_list(&self, v: Vec, arg2: bool) -> Vec; +} + +#[derive(Debug, Clone)] +pub struct RustGetters; + +impl RustGetters { + pub fn new() -> Self { + RustGetters + } + fn get_bool(&self, callback: Box, v: bool, arg2: bool) -> bool { + callback.get_bool(v, arg2) + } + fn get_string(&self, callback: Box, v: String, arg2: bool) -> String { + callback.get_string(v, arg2) + } + fn get_option( + &self, + callback: Box, + v: Option, + arg2: bool, + ) -> Option { + callback.get_option(v, arg2) + } + fn get_list(&self, callback: Box, v: Vec, arg2: bool) -> Vec { + callback.get_list(v, arg2) + } +} + +impl Default for RustGetters { + fn default() -> Self { + Self::new() + } +} + +// Use `Send+Send` because we want to store the callback in an exposed +// `Send+Sync` object. +trait StoredForeignStringifier: Send + Sync + std::fmt::Debug { + fn from_simple_type(&self, value: i32) -> String; + fn from_complex_type(&self, values: Option>>) -> String; +} + +#[derive(Debug)] +pub struct RustStringifier { + callback: Box, +} + +impl RustStringifier { + fn new(callback: Box) -> Self { + RustStringifier { callback } + } + + fn from_simple_type(&self, value: i32) -> String { + self.callback.from_simple_type(value) + } +} + +include!(concat!(env!("OUT_DIR"), "/callbacks.uniffi.rs")); diff --git a/fixtures/callbacks/tests/bindings/test_callbacks.kts b/fixtures/callbacks/tests/bindings/test_callbacks.kts new file mode 100644 index 0000000000..863b531cbb --- /dev/null +++ b/fixtures/callbacks/tests/bindings/test_callbacks.kts @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import uniffi.callbacks.* + +// A bit more systematic in testing, but this time in English. +// +// 1. Pass in the callback as arguments. +// Make the callback methods use multiple aruments, with a variety of types, and +// with a variety of return types. +val rustGetters = RustGetters() +class KotlinGetters(): ForeignGetters { + override fun getBool(v: Boolean, arg2: Boolean): Boolean = v xor arg2 + override fun getString(v: String, arg2: Boolean): String = if (arg2) "1234567890123" else v + override fun getOption(v: String?, arg2: Boolean): String? = if (arg2) v?.uppercase() else v + override fun getList(v: List, arg2: Boolean): List = if (arg2) v else listOf() +} + +val callback = KotlinGetters() +listOf(true, false).forEach { v -> + val flag = true + val expected = callback.getBool(v, flag) + val observed = rustGetters.getBool(callback, v, flag) + assert(expected == observed) { "roundtripping through callback: $expected != $observed" } +} + +listOf(listOf(1,2), listOf(0,1)).forEach { v -> + val flag = true + val expected = callback.getList(v, flag) + val observed = rustGetters.getList(callback, v, flag) + assert(expected == observed) { "roundtripping through callback: $expected != $observed" } +} + +listOf("Hello", "world").forEach { v -> + val flag = true + val expected = callback.getString(v, flag) + val observed = rustGetters.getString(callback, v, flag) + assert(expected == observed) { "roundtripping through callback: $expected != $observed" } +} + +listOf("Some", null).forEach { v -> + val flag = false + val expected = callback.getOption(v, flag) + val observed = rustGetters.getOption(callback, v, flag) + assert(expected == observed) { "roundtripping through callback: $expected != $observed" } +} +rustGetters.destroy() + +// 2. Pass the callback in as a constructor argument, to be stored on the Object struct. +// This is crucial if we want to configure a system at startup, +// then use it without passing callbacks all the time. + +class StoredKotlinStringifier: StoredForeignStringifier { + override fun fromSimpleType(value: Int): String = "kotlin: $value" + // We don't test this, but we're checking that the arg type is included in the minimal list of types used + // in the UDL. + // If this doesn't compile, then look at TypeResolver. + override fun fromComplexType(values: List?): String = "kotlin: $values" +} + +val kotlinStringifier = StoredKotlinStringifier() +val rustStringifier = RustStringifier(kotlinStringifier) +listOf(1, 2).forEach { v -> + val expected = kotlinStringifier.fromSimpleType(v) + val observed = rustStringifier.fromSimpleType(v) + assert(expected == observed) { "callback is sent on construction: $expected != $observed" } +} +rustStringifier.destroy() diff --git a/fixtures/callbacks/tests/test_generated_bindings.rs b/fixtures/callbacks/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..95d37265f3 --- /dev/null +++ b/fixtures/callbacks/tests/test_generated_bindings.rs @@ -0,0 +1,4 @@ +uniffi_macros::build_foreign_language_testcases!( + "src/callbacks.udl", + ["tests/bindings/test_callbacks.kts"] +);