diff options
Diffstat (limited to 'toolkit/components/uniffi-fixtures')
66 files changed, 2665 insertions, 0 deletions
diff --git a/toolkit/components/uniffi-fixtures/arithmetic/Cargo.toml b/toolkit/components/uniffi-fixtures/arithmetic/Cargo.toml new file mode 100644 index 0000000000..b497b8cccc --- /dev/null +++ b/toolkit/components/uniffi-fixtures/arithmetic/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "uniffi-example-arithmetic" +edition = "2021" +version = "0.22.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +license = "MPL-2.0" +publish = false + +[lib] +crate-type = ["lib", "cdylib"] +name = "arithmetical" + +[dependencies] +uniffi = { workspace = true } +thiserror = "1.0" + +[build-dependencies] +uniffi = { workspace = true, features = ["build"] } + +[dev-dependencies] +uniffi = { workspace = true, features = ["bindgen-tests"] } diff --git a/toolkit/components/uniffi-fixtures/arithmetic/build.rs b/toolkit/components/uniffi-fixtures/arithmetic/build.rs new file mode 100644 index 0000000000..303ac22d8d --- /dev/null +++ b/toolkit/components/uniffi-fixtures/arithmetic/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::generate_scaffolding("src/arithmetic.udl").unwrap(); +} diff --git a/toolkit/components/uniffi-fixtures/arithmetic/src/arithmetic.udl b/toolkit/components/uniffi-fixtures/arithmetic/src/arithmetic.udl new file mode 100644 index 0000000000..117df4834a --- /dev/null +++ b/toolkit/components/uniffi-fixtures/arithmetic/src/arithmetic.udl @@ -0,0 +1,16 @@ +[Error] +enum ArithmeticError { + "IntegerOverflow", +}; + +namespace arithmetic { + [Throws=ArithmeticError] + u64 add(u64 a, u64 b); + + [Throws=ArithmeticError] + u64 sub(u64 a, u64 b); + + u64 div(u64 dividend, u64 divisor); + + boolean equal(u64 a, u64 b); +}; diff --git a/toolkit/components/uniffi-fixtures/arithmetic/src/lib.rs b/toolkit/components/uniffi-fixtures/arithmetic/src/lib.rs new file mode 100644 index 0000000000..92ab8c072b --- /dev/null +++ b/toolkit/components/uniffi-fixtures/arithmetic/src/lib.rs @@ -0,0 +1,34 @@ +/* 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/. */ + +#[derive(Debug, thiserror::Error)] +pub enum ArithmeticError { + #[error("Integer overflow on an operation with {a} and {b}")] + IntegerOverflow { a: u64, b: u64 }, +} + +fn add(a: u64, b: u64) -> Result<u64> { + a.checked_add(b) + .ok_or(ArithmeticError::IntegerOverflow { a, b }) +} + +fn sub(a: u64, b: u64) -> Result<u64> { + a.checked_sub(b) + .ok_or(ArithmeticError::IntegerOverflow { a, b }) +} + +fn div(dividend: u64, divisor: u64) -> u64 { + if divisor == 0 { + panic!("Can't divide by zero"); + } + dividend / divisor +} + +fn equal(a: u64, b: u64) -> bool { + a == b +} + +type Result<T, E = ArithmeticError> = std::result::Result<T, E>; + +uniffi::include_scaffolding!("arithmetic"); diff --git a/toolkit/components/uniffi-fixtures/arithmetic/tests/bindings/test_arithmetic.kts b/toolkit/components/uniffi-fixtures/arithmetic/tests/bindings/test_arithmetic.kts new file mode 100644 index 0000000000..ef11850ae2 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/arithmetic/tests/bindings/test_arithmetic.kts @@ -0,0 +1,29 @@ +import org.mozilla.uniffi.example.arithmetic.*; + +assert(add(2u, 4u) == 6uL) +assert(add(4u, 8u) == 12uL) + +try { + sub(0u, 2u) + throw RuntimeException("Should have thrown a IntegerOverflow exception!") +} catch (e: ArithmeticException) { + // It's okay! +} + +assert(sub(4u, 2u) == 2uL) +assert(sub(8u, 4u) == 4uL) + +assert(div(8u, 4u) == 2uL) + +try { + div(8u, 0u) + throw RuntimeException("Should have panicked when dividing by zero") +} catch (e: InternalException) { + // It's okay! +} + +assert(equal(2u, 2uL)) +assert(equal(4u, 4uL)) + +assert(!equal(2u, 4uL)) +assert(!equal(4u, 8uL)) diff --git a/toolkit/components/uniffi-fixtures/arithmetic/tests/bindings/test_arithmetic.py b/toolkit/components/uniffi-fixtures/arithmetic/tests/bindings/test_arithmetic.py new file mode 100644 index 0000000000..e1b4f71277 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/arithmetic/tests/bindings/test_arithmetic.py @@ -0,0 +1,37 @@ +from arithmetic import InternalError, add, div, equal, sub + +try: + add(18446744073709551615, 1) + assert not ("Should have thrown a IntegerOverflow exception!") +except ArithmeticError.IntegerOverflow: + # It's okay! + pass + +assert add(2, 4) == 6 +assert add(4, 8) == 12 + +try: + sub(0, 1) + assert not ("Should have thrown a IntegerOverflow exception!") +except ArithmeticError.IntegerOverflow: + # It's okay! + pass + +assert sub(4, 2) == 2 +assert sub(8, 4) == 4 + +assert div(8, 4) == 2 + +try: + div(8, 0) +except InternalError: + # It's okay! + pass +else: + assert not ("Should have panicked when dividing by zero") + +assert equal(2, 2) +assert equal(4, 4) + +assert not equal(2, 4) +assert not equal(4, 8) diff --git a/toolkit/components/uniffi-fixtures/arithmetic/tests/bindings/test_arithmetic.rb b/toolkit/components/uniffi-fixtures/arithmetic/tests/bindings/test_arithmetic.rb new file mode 100644 index 0000000000..6669eb279f --- /dev/null +++ b/toolkit/components/uniffi-fixtures/arithmetic/tests/bindings/test_arithmetic.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'test/unit' +require 'arithmetic' + +include Test::Unit::Assertions + +assert_raise Arithmetic::ArithmeticError::IntegerOverflow do + Arithmetic.add 18_446_744_073_709_551_615, 1 +end + +assert_equal Arithmetic.add(2, 4), 6 +assert_equal Arithmetic.add(4, 8), 12 + +assert_raise Arithmetic::ArithmeticError::IntegerOverflow do + Arithmetic.sub 0, 1 +end + +assert_equal Arithmetic.sub(4, 2), 2 +assert_equal Arithmetic.sub(8, 4), 4 +assert_equal Arithmetic.div(8, 4), 2 + +assert_raise Arithmetic::InternalError do + Arithmetic.div 8, 0 +end + +assert Arithmetic.equal(2, 2) +assert Arithmetic.equal(4, 4) + +assert !Arithmetic.equal(2, 4) +assert !Arithmetic.equal(4, 8) diff --git a/toolkit/components/uniffi-fixtures/arithmetic/tests/bindings/test_arithmetic.swift b/toolkit/components/uniffi-fixtures/arithmetic/tests/bindings/test_arithmetic.swift new file mode 100644 index 0000000000..a8e34680e4 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/arithmetic/tests/bindings/test_arithmetic.swift @@ -0,0 +1,32 @@ +import arithmetic + +do { + let _ = try add(a: 18446744073709551615, b: 1) + fatalError("Should have thrown a IntegerOverflow exception!") +} catch ArithmeticError.IntegerOverflow { + // It's okay! +} + +assert(try! add(a: 2, b: 4) == 6, "add work") +assert(try! add(a: 4, b: 8) == 12, "add work") + +do { + let _ = try sub(a: 0, b: 1) + fatalError("Should have thrown a IntegerOverflow exception!") +} catch ArithmeticError.IntegerOverflow { + // It's okay! +} + +assert(try! sub(a: 4, b: 2) == 2, "sub work") +assert(try! sub(a: 8, b: 4) == 4, "sub work") + +assert(div(dividend: 8, divisor: 4) == 2, "div works") + +// We can't test panicking in Swift because we force unwrap the error in +// `div`, which we can't catch. + +assert(equal(a: 2, b: 2), "equal works") +assert(equal(a: 4, b: 4), "equal works") + +assert(!equal(a: 2, b: 4), "non-equal works") +assert(!equal(a: 4, b: 8), "non-equal works") diff --git a/toolkit/components/uniffi-fixtures/arithmetic/tests/test_generated_bindings.rs b/toolkit/components/uniffi-fixtures/arithmetic/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..168e6e1d4c --- /dev/null +++ b/toolkit/components/uniffi-fixtures/arithmetic/tests/test_generated_bindings.rs @@ -0,0 +1,6 @@ +uniffi::build_foreign_language_testcases!( + "tests/bindings/test_arithmetic.rb", + "tests/bindings/test_arithmetic.py", + "tests/bindings/test_arithmetic.kts", + "tests/bindings/test_arithmetic.swift", +); diff --git a/toolkit/components/uniffi-fixtures/arithmetic/uniffi.toml b/toolkit/components/uniffi-fixtures/arithmetic/uniffi.toml new file mode 100644 index 0000000000..883231dcaa --- /dev/null +++ b/toolkit/components/uniffi-fixtures/arithmetic/uniffi.toml @@ -0,0 +1,2 @@ +[bindings.kotlin] +package_name = "org.mozilla.uniffi.example.arithmetic" diff --git a/toolkit/components/uniffi-fixtures/callbacks/Cargo.toml b/toolkit/components/uniffi-fixtures/callbacks/Cargo.toml new file mode 100644 index 0000000000..19a8d61070 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/callbacks/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "uniffi-fixture-callbacks" +edition = "2021" +version = "0.21.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +license = "MPL-2.0" +publish = false + +[dependencies] +uniffi = { workspace = true } +thiserror = "1.0" + +[build-dependencies] +uniffi = { workspace = true, features = ["build"] } diff --git a/toolkit/components/uniffi-fixtures/callbacks/build.rs b/toolkit/components/uniffi-fixtures/callbacks/build.rs new file mode 100644 index 0000000000..e7152d922a --- /dev/null +++ b/toolkit/components/uniffi-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::generate_scaffolding("./src/callbacks.udl").unwrap(); +} diff --git a/toolkit/components/uniffi-fixtures/callbacks/src/callbacks.udl b/toolkit/components/uniffi-fixtures/callbacks/src/callbacks.udl new file mode 100644 index 0000000000..a6d44ef598 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/callbacks/src/callbacks.udl @@ -0,0 +1,12 @@ +callback interface Logger { + void log(string message); + void finished(); + }; + +namespace fixture_callbacks { + // Log all even numbers in a vec, then call the finished() method + void log_even_numbers(Logger logger, sequence<i32> items); + // Works exactly the same as `log_even_numbers()`, except we configure this + // to run on the main thread in `uniffi-bindgen-gecko-js/config.toml` + void log_even_numbers_main_thread(Logger logger, sequence<i32> items); +}; diff --git a/toolkit/components/uniffi-fixtures/callbacks/src/lib.rs b/toolkit/components/uniffi-fixtures/callbacks/src/lib.rs new file mode 100644 index 0000000000..9ada66bca5 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/callbacks/src/lib.rs @@ -0,0 +1,23 @@ +/* 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 Logger { + fn log(&self, message: String); + fn finished(&self); +} + +fn log_even_numbers(logger: Box<dyn Logger>, items: Vec<i32>) { + for i in items { + if i % 2 == 0 { + logger.log(format!("Saw even number: {i}")) + } + } + logger.finished(); +} + +fn log_even_numbers_main_thread(logger: Box<dyn Logger>, items: Vec<i32>) { + log_even_numbers(logger, items) +} + +include!(concat!(env!("OUT_DIR"), "/callbacks.uniffi.rs")); diff --git a/toolkit/components/uniffi-fixtures/custom-types/Cargo.toml b/toolkit/components/uniffi-fixtures/custom-types/Cargo.toml new file mode 100644 index 0000000000..a05324952e --- /dev/null +++ b/toolkit/components/uniffi-fixtures/custom-types/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "uniffi-example-custom-types" +edition = "2021" +version = "0.19.6" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +license = "MPL-2.0" +publish = false + +[lib] +crate-type = ["lib", "cdylib"] +name = "uniffi_custom_types" + +[dependencies] +anyhow = "1" +bytes = "1.0" +serde_json = "1" +uniffi = { workspace = true } +url = "2.4" + +[build-dependencies] +uniffi = { workspace = true, features = ["build"] } diff --git a/toolkit/components/uniffi-fixtures/custom-types/build.rs b/toolkit/components/uniffi-fixtures/custom-types/build.rs new file mode 100644 index 0000000000..10b6d220da --- /dev/null +++ b/toolkit/components/uniffi-fixtures/custom-types/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::generate_scaffolding("./src/custom-types.udl").unwrap(); +} diff --git a/toolkit/components/uniffi-fixtures/custom-types/src/custom-types.udl b/toolkit/components/uniffi-fixtures/custom-types/src/custom-types.udl new file mode 100644 index 0000000000..8fef8ff41a --- /dev/null +++ b/toolkit/components/uniffi-fixtures/custom-types/src/custom-types.udl @@ -0,0 +1,14 @@ +[Custom] +typedef string Url; + +[Custom] +typedef i64 Handle; + +dictionary CustomTypesDemo { + Url url; + Handle handle; +}; + +namespace custom_types { + CustomTypesDemo get_custom_types_demo(CustomTypesDemo? demo); +}; diff --git a/toolkit/components/uniffi-fixtures/custom-types/src/lib.rs b/toolkit/components/uniffi-fixtures/custom-types/src/lib.rs new file mode 100644 index 0000000000..61aea7a11a --- /dev/null +++ b/toolkit/components/uniffi-fixtures/custom-types/src/lib.rs @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use url::Url; + +// Custom Handle type which trivially wraps an i64. +pub struct Handle(pub i64); + +// We must implement the UniffiCustomTypeConverter trait for each custom type on the scaffolding side +impl UniffiCustomTypeConverter for Handle { + // The `Builtin` type will be used to marshall values across the FFI + type Builtin = i64; + + // Convert Builtin to our custom type + fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> { + Ok(Handle(val)) + } + + // Convert our custom type to Builtin + fn from_custom(obj: Self) -> Self::Builtin { + obj.0 + } +} + +// Use `url::Url` as a custom type, with `String` as the Builtin +impl UniffiCustomTypeConverter for Url { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> { + Ok(Url::parse(&val)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +// And a little struct and function that ties them together. +pub struct CustomTypesDemo { + url: Url, + handle: Handle, +} + +pub fn get_custom_types_demo(v: Option<CustomTypesDemo>) -> CustomTypesDemo { + v.unwrap_or_else(|| CustomTypesDemo { + url: Url::parse("http://example.com/").unwrap(), + handle: Handle(123), + }) +} + +include!(concat!(env!("OUT_DIR"), "/custom-types.uniffi.rs")); diff --git a/toolkit/components/uniffi-fixtures/custom-types/tests/bindings/test_custom_types.kts b/toolkit/components/uniffi-fixtures/custom-types/tests/bindings/test_custom_types.kts new file mode 100644 index 0000000000..d75bd99eee --- /dev/null +++ b/toolkit/components/uniffi-fixtures/custom-types/tests/bindings/test_custom_types.kts @@ -0,0 +1,21 @@ +/* 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 java.net.URL + +import customtypes.* + +// TODO: use an actual test runner. + +// Get the custom types and check their data +val demo = getCustomTypesDemo(null) +// URL is customized on the bindings side +assert(demo.url == URL("http://example.com/")) +// Handle isn't so it appears as a plain Long +assert(demo.handle == 123L) + +// Change some data and ensure that the round-trip works +demo.url = URL("http://new.example.com/") +demo.handle = 456; +assert(demo == getCustomTypesDemo(demo)) diff --git a/toolkit/components/uniffi-fixtures/custom-types/tests/bindings/test_custom_types.swift b/toolkit/components/uniffi-fixtures/custom-types/tests/bindings/test_custom_types.swift new file mode 100644 index 0000000000..5aaf6ff3b0 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/custom-types/tests/bindings/test_custom_types.swift @@ -0,0 +1,20 @@ +/* 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 custom_types +import Foundation + +// TODO: use an actual test runner. + +do { + // Test simple values. + var demo = getCustomTypesDemo(demo: nil) + assert(demo.url == URL(string: "http://example.com/")) + assert(demo.handle == 123) + + // Change some data and ensure that the round-trip works + demo.url = URL(string: "http://new.example.com/")! + demo.handle = 456 + assert(demo == getCustomTypesDemo(demo: demo)) +} diff --git a/toolkit/components/uniffi-fixtures/custom-types/tests/test_generated_bindings.rs b/toolkit/components/uniffi-fixtures/custom-types/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..38db89790e --- /dev/null +++ b/toolkit/components/uniffi-fixtures/custom-types/tests/test_generated_bindings.rs @@ -0,0 +1,7 @@ +uniffi::build_foreign_language_testcases!( + ["src/custom-types.udl"], + [ + "tests/bindings/test_custom_types.kts", + "tests/bindings/test_custom_types.swift", + ] +); diff --git a/toolkit/components/uniffi-fixtures/custom-types/uniffi.toml b/toolkit/components/uniffi-fixtures/custom-types/uniffi.toml new file mode 100644 index 0000000000..873fa090fb --- /dev/null +++ b/toolkit/components/uniffi-fixtures/custom-types/uniffi.toml @@ -0,0 +1,40 @@ +[bindings.swift] +cdylib_name = "custom_types" + +[bindings.swift.custom_types.Url] +# Name of the type in the Swift code +type_name = "URL" +# Modules that need to be imported +imports = ["Foundation"] +# Functions to convert between strings and URLs +into_custom = "URL(string: {})!" +from_custom = "String(describing: {})" + +[bindings.kotlin] +cdylib_name = "custom_types" +package_name = "customtypes" + +[bindings.kotlin.custom_types.Url] +# Name of the type in the Kotlin code +type_name = "URL" +# Classes that need to be imported +imports = [ "java.net.URL" ] +# Functions to convert between strings and URLs +into_custom = "URL({})" +from_custom = "{}.toString()" + +[bindings.python] +cdylib_name = "custom_types" + +[bindings.python.custom_types.Url] +# We're going to be the urllib.parse.ParseResult class, which is the closest +# thing Python has to a Url class. No need to specify `type_name` though, +# since Python is loosely typed. +# modules to import +imports = ["urllib.parse"] +# Functions to convert between strings and the ParsedUrl class +into_custom = "urllib.parse.urlparse({})" +from_custom = "urllib.parse.urlunparse({})" + +[bindings.ruby] +cdylib_name = "custom_types" diff --git a/toolkit/components/uniffi-fixtures/external-types/Cargo.toml b/toolkit/components/uniffi-fixtures/external-types/Cargo.toml new file mode 100644 index 0000000000..b70673374e --- /dev/null +++ b/toolkit/components/uniffi-fixtures/external-types/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "uniffi-fixture-external-types" +edition = "2021" +version = "0.21.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +license = "MPL-2.0" +publish = false + +[dependencies] +uniffi-example-geometry = { path = "../geometry/" } +uniffi = { workspace = true } +thiserror = "1.0" + +[build-dependencies] +uniffi = { workspace = true, features = ["build"] } diff --git a/toolkit/components/uniffi-fixtures/external-types/build.rs b/toolkit/components/uniffi-fixtures/external-types/build.rs new file mode 100644 index 0000000000..13f56ca56b --- /dev/null +++ b/toolkit/components/uniffi-fixtures/external-types/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::generate_scaffolding("./src/external-types.udl").unwrap(); +} diff --git a/toolkit/components/uniffi-fixtures/external-types/src/external-types.udl b/toolkit/components/uniffi-fixtures/external-types/src/external-types.udl new file mode 100644 index 0000000000..d50fc680f7 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/external-types/src/external-types.udl @@ -0,0 +1,11 @@ +// A type defined in the `geometry` component +[External="uniffi_geometry"] +typedef extern Point; + +[External="uniffi_geometry"] +typedef extern Line; + +namespace external_types { + double gradient(Line? value); + Point? intersection(Line ln1, Line ln2); +}; diff --git a/toolkit/components/uniffi-fixtures/external-types/src/lib.rs b/toolkit/components/uniffi-fixtures/external-types/src/lib.rs new file mode 100644 index 0000000000..2d12332312 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/external-types/src/lib.rs @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use uniffi_geometry::{Line, Point}; + +pub fn gradient(value: Option<Line>) -> f64 { + match value { + None => 0.0, + Some(value) => uniffi_geometry::gradient(value), + } +} + +pub fn intersection(ln1: Line, ln2: Line) -> Option<Point> { + uniffi_geometry::intersection(ln1, ln2) +} + +include!(concat!(env!("OUT_DIR"), "/external-types.uniffi.rs")); diff --git a/toolkit/components/uniffi-fixtures/geometry/Cargo.toml b/toolkit/components/uniffi-fixtures/geometry/Cargo.toml new file mode 100644 index 0000000000..a35ce906aa --- /dev/null +++ b/toolkit/components/uniffi-fixtures/geometry/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "uniffi-example-geometry" +edition = "2021" +version = "0.22.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +license = "MPL-2.0" +publish = false + +[lib] +crate-type = ["lib", "cdylib"] +name = "uniffi_geometry" + +[dependencies] +uniffi = { workspace = true } + +[build-dependencies] +uniffi = { workspace = true, features = ["build"] } + +[dev-dependencies] +uniffi = { workspace = true, features = ["bindgen-tests"] } diff --git a/toolkit/components/uniffi-fixtures/geometry/build.rs b/toolkit/components/uniffi-fixtures/geometry/build.rs new file mode 100644 index 0000000000..2dd1c96bc3 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/geometry/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::generate_scaffolding("src/geometry.udl").unwrap(); +} diff --git a/toolkit/components/uniffi-fixtures/geometry/src/geometry.udl b/toolkit/components/uniffi-fixtures/geometry/src/geometry.udl new file mode 100644 index 0000000000..af60d429bf --- /dev/null +++ b/toolkit/components/uniffi-fixtures/geometry/src/geometry.udl @@ -0,0 +1,15 @@ + +namespace geometry { + double gradient(Line ln); + Point? intersection(Line ln1, Line ln2); +}; + +dictionary Point { + double coord_x; + double coord_y; +}; + +dictionary Line { + Point start; + Point end; +}; diff --git a/toolkit/components/uniffi-fixtures/geometry/src/lib.rs b/toolkit/components/uniffi-fixtures/geometry/src/lib.rs new file mode 100644 index 0000000000..d710b150bb --- /dev/null +++ b/toolkit/components/uniffi-fixtures/geometry/src/lib.rs @@ -0,0 +1,47 @@ +/* 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/. */ + +// https://rust-lang.github.io/rust-clippy/master/index.html#float_cmp +// Silence, clippy! +const EPSILON: f64 = 0.0001f64; + +#[derive(Debug, Clone)] +pub struct Point { + coord_x: f64, + coord_y: f64, +} + +#[derive(Debug, Clone)] +pub struct Line { + start: Point, + end: Point, +} + +pub fn gradient(ln: Line) -> f64 { + let rise = ln.end.coord_y - ln.start.coord_y; + let run = ln.end.coord_x - ln.start.coord_x; + rise / run +} + +pub fn intersection(ln1: Line, ln2: Line) -> Option<Point> { + // TODO: yuck, should be able to take &Line as argument here + // and have rust figure it out with a bunch of annotations... + let g1 = gradient(ln1.clone()); + let z1 = ln1.start.coord_y - g1 * ln1.start.coord_x; + let g2 = gradient(ln2.clone()); + let z2 = ln2.start.coord_y - g2 * ln2.start.coord_x; + // Parallel lines do not intersect. + if (g1 - g2).abs() < EPSILON { + return None; + } + // Otherwise, they intersect at this fancy calculation that + // I found on wikipedia. + let x = (z2 - z1) / (g1 - g2); + Some(Point { + coord_x: x, + coord_y: g1 * x + z1, + }) +} + +uniffi::include_scaffolding!("geometry"); diff --git a/toolkit/components/uniffi-fixtures/geometry/tests/bindings/test_geometry.kts b/toolkit/components/uniffi-fixtures/geometry/tests/bindings/test_geometry.kts new file mode 100644 index 0000000000..77bb9932ec --- /dev/null +++ b/toolkit/components/uniffi-fixtures/geometry/tests/bindings/test_geometry.kts @@ -0,0 +1,10 @@ +import uniffi.geometry.*; + +val ln1 = Line(Point(0.0,0.0), Point(1.0,2.0)) +val ln2 = Line(Point(1.0,1.0), Point(2.0,2.0)) + +assert( gradient(ln1) == 2.0 ) +assert( gradient(ln2) == 1.0 ) + +assert( intersection(ln1, ln2) == Point(0.0, 0.0) ) +assert( intersection(ln1, ln1) == null ) diff --git a/toolkit/components/uniffi-fixtures/geometry/tests/bindings/test_geometry.py b/toolkit/components/uniffi-fixtures/geometry/tests/bindings/test_geometry.py new file mode 100644 index 0000000000..bfb6560626 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/geometry/tests/bindings/test_geometry.py @@ -0,0 +1,10 @@ +from geometry import Line, Point, gradient, intersection + +ln1 = Line(start=Point(coord_x=0, coord_y=0), end=Point(coord_x=1, coord_y=2)) +ln2 = Line(start=Point(coord_x=1, coord_y=1), end=Point(coord_x=2, coord_y=2)) + +assert gradient(ln1) == 2 +assert gradient(ln2) == 1 + +assert intersection(ln1, ln2) == Point(coord_x=0, coord_y=0) +assert intersection(ln1, ln1) is None diff --git a/toolkit/components/uniffi-fixtures/geometry/tests/bindings/test_geometry.rb b/toolkit/components/uniffi-fixtures/geometry/tests/bindings/test_geometry.rb new file mode 100644 index 0000000000..90fdff684e --- /dev/null +++ b/toolkit/components/uniffi-fixtures/geometry/tests/bindings/test_geometry.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'test/unit' +require 'geometry' + +include Test::Unit::Assertions +include Geometry + +ln1 = Line.new(start: Point.new(coord_x: 0.0, coord_y: 0.0), _end: Point.new(coord_x: 1.0, coord_y: 2.0)) +ln2 = Line.new(start: Point.new(coord_x: 1.0, coord_y: 1.0), _end: Point.new(coord_x: 2.0, coord_y: 2.0)) + +assert_equal Geometry.gradient(ln1), 2 +assert_equal Geometry.gradient(ln2), 1 + +assert_equal Geometry.intersection(ln1, ln2), Point.new(coord_x: 0, coord_y: 0) +assert Geometry.intersection(ln1, ln1).nil? diff --git a/toolkit/components/uniffi-fixtures/geometry/tests/bindings/test_geometry.swift b/toolkit/components/uniffi-fixtures/geometry/tests/bindings/test_geometry.swift new file mode 100644 index 0000000000..58bd65607f --- /dev/null +++ b/toolkit/components/uniffi-fixtures/geometry/tests/bindings/test_geometry.swift @@ -0,0 +1,10 @@ +import geometry + +let ln1 = Line(start: Point(coordX: 0, coordY: 0), end: Point(coordX: 1, coordY: 2)) +let ln2 = Line(start: Point(coordX: 1, coordY: 1), end: Point(coordX: 2, coordY: 2)) + +assert(gradient(ln: ln1) == 2.0) +assert(gradient(ln: ln2) == 1.0) + +assert(intersection(ln1: ln1, ln2: ln2) == Point(coordX: 0, coordY: 0)) +assert(intersection(ln1: ln1, ln2: ln1) == nil) diff --git a/toolkit/components/uniffi-fixtures/geometry/tests/test_generated_bindings.rs b/toolkit/components/uniffi-fixtures/geometry/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..4638d847c8 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/geometry/tests/test_generated_bindings.rs @@ -0,0 +1,6 @@ +uniffi::build_foreign_language_testcases!( + "tests/bindings/test_geometry.py", + "tests/bindings/test_geometry.rb", + "tests/bindings/test_geometry.kts", + "tests/bindings/test_geometry.swift", +); diff --git a/toolkit/components/uniffi-fixtures/refcounts/Cargo.toml b/toolkit/components/uniffi-fixtures/refcounts/Cargo.toml new file mode 100644 index 0000000000..877e502711 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/refcounts/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "uniffi-fixture-refcounts" +edition = "2021" +version = "0.21.0" +license = "MPL-2.0" +publish = false + +[dependencies] +uniffi = { workspace = true } + +[build-dependencies] +uniffi = { workspace = true, features = ["build"] } diff --git a/toolkit/components/uniffi-fixtures/refcounts/build.rs b/toolkit/components/uniffi-fixtures/refcounts/build.rs new file mode 100644 index 0000000000..9ea03e12de --- /dev/null +++ b/toolkit/components/uniffi-fixtures/refcounts/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::generate_scaffolding("./src/refcounts.udl").unwrap(); +} diff --git a/toolkit/components/uniffi-fixtures/refcounts/src/lib.rs b/toolkit/components/uniffi-fixtures/refcounts/src/lib.rs new file mode 100644 index 0000000000..6554625c62 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/refcounts/src/lib.rs @@ -0,0 +1,32 @@ +/* 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/. */ + +/// This crate exists to test managing the Rust Arc strong counts as JS objects are +/// created/destroyed. See `test_refcounts.js` for how it's used. +use std::sync::{Arc, Mutex}; + +pub struct SingletonObject; + +impl SingletonObject { + pub fn method(&self) {} +} + +static SINGLETON: Mutex<Option<Arc<SingletonObject>>> = Mutex::new(None); + +pub fn get_singleton() -> Arc<SingletonObject> { + Arc::clone( + SINGLETON + .lock() + .unwrap() + .get_or_insert_with(|| Arc::new(SingletonObject)), + ) +} + +pub fn get_js_refcount() -> i32 { + // Subtract 2: one for the reference in the Mutex and one for the temporary reference that + // we're calling Arc::strong_count on. + (Arc::strong_count(&get_singleton()) as i32) - 2 +} + +include!(concat!(env!("OUT_DIR"), "/refcounts.uniffi.rs")); diff --git a/toolkit/components/uniffi-fixtures/refcounts/src/refcounts.udl b/toolkit/components/uniffi-fixtures/refcounts/src/refcounts.udl new file mode 100644 index 0000000000..25ec83cfcc --- /dev/null +++ b/toolkit/components/uniffi-fixtures/refcounts/src/refcounts.udl @@ -0,0 +1,8 @@ +namespace refcounts { + SingletonObject get_singleton(); + i32 get_js_refcount(); +}; + +interface SingletonObject { + void method(); +}; diff --git a/toolkit/components/uniffi-fixtures/rondpoint/Cargo.toml b/toolkit/components/uniffi-fixtures/rondpoint/Cargo.toml new file mode 100644 index 0000000000..1d0be84785 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/rondpoint/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "uniffi-example-rondpoint" +edition = "2021" +version = "0.22.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +license = "MPL-2.0" +publish = false + +[lib] +crate-type = ["lib", "cdylib"] +name = "uniffi_rondpoint" + +[dependencies] +uniffi = { workspace = true } + +[build-dependencies] +uniffi = { workspace = true, features = ["build"] } + +[dev-dependencies] +uniffi = { workspace = true, features = ["bindgen-tests"] } diff --git a/toolkit/components/uniffi-fixtures/rondpoint/build.rs b/toolkit/components/uniffi-fixtures/rondpoint/build.rs new file mode 100644 index 0000000000..f830879d09 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/rondpoint/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::generate_scaffolding("src/rondpoint.udl").unwrap(); +} diff --git a/toolkit/components/uniffi-fixtures/rondpoint/src/lib.rs b/toolkit/components/uniffi-fixtures/rondpoint/src/lib.rs new file mode 100644 index 0000000000..3f2233ddaa --- /dev/null +++ b/toolkit/components/uniffi-fixtures/rondpoint/src/lib.rs @@ -0,0 +1,293 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub struct Dictionnaire { + un: Enumeration, + deux: bool, + petit_nombre: u8, + gros_nombre: u64, +} + +#[derive(Debug, Clone)] +pub struct DictionnaireNombres { + petit_nombre: u8, + court_nombre: u16, + nombre_simple: u32, + gros_nombre: u64, +} + +#[derive(Debug, Clone)] +pub struct DictionnaireNombresSignes { + petit_nombre: i8, + court_nombre: i16, + nombre_simple: i32, + gros_nombre: i64, +} + +#[derive(Debug, Clone)] +pub enum Enumeration { + Un, + Deux, + Trois, +} + +#[derive(Debug, Clone)] +pub enum EnumerationAvecDonnees { + Zero, + Un { premier: u32 }, + Deux { premier: u32, second: String }, +} + +#[allow(non_camel_case_types)] +#[allow(non_snake_case)] +pub struct minusculeMAJUSCULEDict { + minusculeMAJUSCULEField: bool, +} + +#[allow(non_camel_case_types)] +pub enum minusculeMAJUSCULEEnum { + minusculeMAJUSCULEVariant, +} + +fn copie_enumeration(e: Enumeration) -> Enumeration { + e +} + +fn copie_enumerations(e: Vec<Enumeration>) -> Vec<Enumeration> { + e +} + +fn copie_carte( + e: HashMap<String, EnumerationAvecDonnees>, +) -> HashMap<String, EnumerationAvecDonnees> { + e +} + +fn copie_dictionnaire(d: Dictionnaire) -> Dictionnaire { + d +} + +fn switcheroo(b: bool) -> bool { + !b +} + +// Test that values can traverse both ways across the FFI. +// Even if roundtripping works, it's possible we have +// symmetrical errors that cancel each other out. +#[derive(Debug, Clone)] +struct Retourneur; +impl Retourneur { + fn new() -> Self { + Retourneur + } + fn identique_i8(&self, value: i8) -> i8 { + value + } + fn identique_u8(&self, value: u8) -> u8 { + value + } + fn identique_i16(&self, value: i16) -> i16 { + value + } + fn identique_u16(&self, value: u16) -> u16 { + value + } + fn identique_i32(&self, value: i32) -> i32 { + value + } + fn identique_u32(&self, value: u32) -> u32 { + value + } + fn identique_i64(&self, value: i64) -> i64 { + value + } + fn identique_u64(&self, value: u64) -> u64 { + value + } + fn identique_float(&self, value: f32) -> f32 { + value + } + fn identique_double(&self, value: f64) -> f64 { + value + } + fn identique_boolean(&self, value: bool) -> bool { + value + } + fn identique_string(&self, value: String) -> String { + value + } + fn identique_nombres_signes( + &self, + value: DictionnaireNombresSignes, + ) -> DictionnaireNombresSignes { + value + } + fn identique_nombres(&self, value: DictionnaireNombres) -> DictionnaireNombres { + value + } + fn identique_optionneur_dictionnaire( + &self, + value: OptionneurDictionnaire, + ) -> OptionneurDictionnaire { + value + } +} + +#[derive(Debug, Clone)] +struct Stringifier; + +#[allow(dead_code)] +impl Stringifier { + fn new() -> Self { + Stringifier + } + fn to_string_i8(&self, value: i8) -> String { + value.to_string() + } + fn to_string_u8(&self, value: u8) -> String { + value.to_string() + } + fn to_string_i16(&self, value: i16) -> String { + value.to_string() + } + fn to_string_u16(&self, value: u16) -> String { + value.to_string() + } + fn to_string_i32(&self, value: i32) -> String { + value.to_string() + } + fn to_string_u32(&self, value: u32) -> String { + value.to_string() + } + fn to_string_i64(&self, value: i64) -> String { + value.to_string() + } + fn to_string_u64(&self, value: u64) -> String { + value.to_string() + } + fn to_string_float(&self, value: f32) -> String { + value.to_string() + } + fn to_string_double(&self, value: f64) -> String { + value.to_string() + } + fn to_string_boolean(&self, value: bool) -> String { + value.to_string() + } + fn well_known_string(&self, value: String) -> String { + format!("uniffi 💚 {value}!") + } +} + +#[derive(Debug, Clone)] +struct Optionneur; +impl Optionneur { + fn new() -> Self { + Optionneur + } + fn sinon_string(&self, value: String) -> String { + value + } + fn sinon_null(&self, value: Option<String>) -> Option<String> { + value + } + fn sinon_boolean(&self, value: bool) -> bool { + value + } + fn sinon_sequence(&self, value: Vec<String>) -> Vec<String> { + value + } + + fn sinon_zero(&self, value: Option<i32>) -> Option<i32> { + value + } + + fn sinon_u8_dec(&self, value: u8) -> u8 { + value + } + fn sinon_i8_dec(&self, value: i8) -> i8 { + value + } + fn sinon_u16_dec(&self, value: u16) -> u16 { + value + } + fn sinon_i16_dec(&self, value: i16) -> i16 { + value + } + fn sinon_u32_dec(&self, value: u32) -> u32 { + value + } + fn sinon_i32_dec(&self, value: i32) -> i32 { + value + } + fn sinon_u64_dec(&self, value: u64) -> u64 { + value + } + fn sinon_i64_dec(&self, value: i64) -> i64 { + value + } + + fn sinon_u8_hex(&self, value: u8) -> u8 { + value + } + fn sinon_i8_hex(&self, value: i8) -> i8 { + value + } + fn sinon_u16_hex(&self, value: u16) -> u16 { + value + } + fn sinon_i16_hex(&self, value: i16) -> i16 { + value + } + fn sinon_u32_hex(&self, value: u32) -> u32 { + value + } + fn sinon_i32_hex(&self, value: i32) -> i32 { + value + } + fn sinon_u64_hex(&self, value: u64) -> u64 { + value + } + fn sinon_i64_hex(&self, value: i64) -> i64 { + value + } + + fn sinon_u32_oct(&self, value: u32) -> u32 { + value + } + + fn sinon_f32(&self, value: f32) -> f32 { + value + } + fn sinon_f64(&self, value: f64) -> f64 { + value + } + + fn sinon_enum(&self, value: Enumeration) -> Enumeration { + value + } +} + +pub struct OptionneurDictionnaire { + i8_var: i8, + u8_var: u8, + i16_var: i16, + u16_var: u16, + i32_var: i32, + u32_var: u32, + i64_var: i64, + u64_var: u64, + float_var: f32, + double_var: f64, + boolean_var: bool, + string_var: String, + list_var: Vec<String>, + enumeration_var: Enumeration, + dictionnaire_var: Option<minusculeMAJUSCULEEnum>, +} + +uniffi::include_scaffolding!("rondpoint"); diff --git a/toolkit/components/uniffi-fixtures/rondpoint/src/rondpoint.udl b/toolkit/components/uniffi-fixtures/rondpoint/src/rondpoint.udl new file mode 100644 index 0000000000..7c8261d74e --- /dev/null +++ b/toolkit/components/uniffi-fixtures/rondpoint/src/rondpoint.udl @@ -0,0 +1,146 @@ +namespace rondpoint { + Dictionnaire copie_dictionnaire(Dictionnaire d); + Enumeration copie_enumeration(Enumeration e); + sequence<Enumeration> copie_enumerations(sequence<Enumeration> e); + record<string, EnumerationAvecDonnees> copie_carte(record<string, EnumerationAvecDonnees> c); + boolean switcheroo(boolean b); +}; + +dictionary minusculeMAJUSCULEDict { + boolean minusculeMAJUSCULEField; +}; + +enum minusculeMAJUSCULEEnum { + "minusculeMAJUSCULEVariant", +}; + +enum Enumeration { + "Un", + "Deux", + "Trois", +}; + +[Enum] +interface EnumerationAvecDonnees { + Zero(); + Un(u32 premier); + Deux(u32 premier, string second); +}; + +dictionary Dictionnaire { + Enumeration un; + boolean deux; + u8 petit_nombre; + u64 gros_nombre; +}; + +dictionary DictionnaireNombres { + u8 petit_nombre; + u16 court_nombre; + u32 nombre_simple; + u64 gros_nombre; +}; + +dictionary DictionnaireNombresSignes { + i8 petit_nombre; + i16 court_nombre; + i32 nombre_simple; + i64 gros_nombre; +}; + +interface Retourneur { + constructor(); + i8 identique_i8(i8 value); + u8 identique_u8(u8 value); + i16 identique_i16(i16 value); + u16 identique_u16(u16 value); + i32 identique_i32(i32 value); + u32 identique_u32(u32 value); + i64 identique_i64(i64 value); + u64 identique_u64(u64 value); + float identique_float(float value); + double identique_double(double value); + boolean identique_boolean(boolean value); + string identique_string(string value); + + DictionnaireNombresSignes identique_nombres_signes(DictionnaireNombresSignes value); + DictionnaireNombres identique_nombres(DictionnaireNombres value); + OptionneurDictionnaire identique_optionneur_dictionnaire(OptionneurDictionnaire value); +}; + +interface Stringifier { + constructor(); + string well_known_string(string value); + + string to_string_i8(i8 value); + string to_string_u8(u8 value); + string to_string_i16(i16 value); + string to_string_u16(u16 value); + string to_string_i32(i32 value); + string to_string_u32(u32 value); + string to_string_i64(i64 value); + string to_string_u64(u64 value); + string to_string_float(float value); + string to_string_double(double value); + string to_string_boolean(boolean value); +}; + +interface Optionneur { + constructor(); + boolean sinon_boolean(optional boolean value = false); + string sinon_string(optional string value = "default"); + + sequence<string> sinon_sequence(optional sequence<string> value = []); + + // Either sides of nullable. + string? sinon_null(optional string? value = null); + i32? sinon_zero(optional i32? value = 0); + + // Decimal integers, all 42. + u8 sinon_u8_dec(optional u8 value = 42); + i8 sinon_i8_dec(optional i8 value = -42); + u16 sinon_u16_dec(optional u16 value = 42); + i16 sinon_i16_dec(optional i16 value = 42); + u32 sinon_u32_dec(optional u32 value = 42); + i32 sinon_i32_dec(optional i32 value = 42); + u64 sinon_u64_dec(optional u64 value = 42); + i64 sinon_i64_dec(optional i64 value = 42); + + // Hexadecimal, including negatgives. + u8 sinon_u8_hex(optional u8 value = 0xff); + i8 sinon_i8_hex(optional i8 value = -0x7f); + u16 sinon_u16_hex(optional u16 value = 0xffff); + i16 sinon_i16_hex(optional i16 value = 0x7f); + u32 sinon_u32_hex(optional u32 value = 0xffffffff); + i32 sinon_i32_hex(optional i32 value = 0x7fffffff); + u64 sinon_u64_hex(optional u64 value = 0xffffffffffffffff); + i64 sinon_i64_hex(optional i64 value = 0x7fffffffffffffff); + + // Octal, FWIW. + u32 sinon_u32_oct(optional u32 value = 0755); + + // Floats + f32 sinon_f32(optional f32 value = 42.0); + f64 sinon_f64(optional f64 value = 42.1); + + // Enums, which we have to treat as strings in the UDL frontend. + Enumeration sinon_enum(optional Enumeration value = "Trois"); +}; + +dictionary OptionneurDictionnaire { + i8 i8_var = -8; + u8 u8_var = 8; + i16 i16_var = -0x10; + u16 u16_var = 0x10; + i32 i32_var = -32; + u32 u32_var = 32; + i64 i64_var = -64; + u64 u64_var = 64; + float float_var = 4.0; + double double_var = 8.0; + boolean boolean_var = true; + string string_var = "default"; + sequence<string> list_var = []; + Enumeration enumeration_var = "DEUX"; + minusculeMAJUSCULEEnum? dictionnaire_var = null; +}; diff --git a/toolkit/components/uniffi-fixtures/rondpoint/tests/bindings/test_rondpoint.kts b/toolkit/components/uniffi-fixtures/rondpoint/tests/bindings/test_rondpoint.kts new file mode 100644 index 0000000000..cc5ddf2a86 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/rondpoint/tests/bindings/test_rondpoint.kts @@ -0,0 +1,250 @@ +import uniffi.rondpoint.* + +val dico = Dictionnaire(Enumeration.DEUX, true, 0u, 123456789u) +val copyDico = copieDictionnaire(dico) +assert(dico == copyDico) + +assert(copieEnumeration(Enumeration.DEUX) == Enumeration.DEUX) +assert(copieEnumerations(listOf(Enumeration.UN, Enumeration.DEUX)) == listOf(Enumeration.UN, Enumeration.DEUX)) +assert(copieCarte(mapOf( + "0" to EnumerationAvecDonnees.Zero, + "1" to EnumerationAvecDonnees.Un(1u), + "2" to EnumerationAvecDonnees.Deux(2u, "deux") +)) == mapOf( + "0" to EnumerationAvecDonnees.Zero, + "1" to EnumerationAvecDonnees.Un(1u), + "2" to EnumerationAvecDonnees.Deux(2u, "deux") +)) + +val var1: EnumerationAvecDonnees = EnumerationAvecDonnees.Zero +val var2: EnumerationAvecDonnees = EnumerationAvecDonnees.Un(1u) +val var3: EnumerationAvecDonnees = EnumerationAvecDonnees.Un(2u) +assert(var1 != var2) +assert(var2 != var3) +assert(var1 == EnumerationAvecDonnees.Zero) +assert(var1 != EnumerationAvecDonnees.Un(1u)) +assert(var2 == EnumerationAvecDonnees.Un(1u)) + +assert(switcheroo(false)) + +// Test the roundtrip across the FFI. +// This shows that the values we send come back in exactly the same state as we sent them. +// i.e. it shows that lowering from kotlin and lifting into rust is symmetrical with +// lowering from rust and lifting into kotlin. +val rt = Retourneur() + +fun <T> List<T>.affirmAllerRetour(fn: (T) -> T) { + this.forEach { v -> + assert(fn.invoke(v) == v) { "$fn($v)" } + } +} + +// Booleans +listOf(true, false).affirmAllerRetour(rt::identiqueBoolean) + +// Bytes. +listOf(Byte.MIN_VALUE, Byte.MAX_VALUE).affirmAllerRetour(rt::identiqueI8) +listOf(0x00, 0xFF).map { it.toUByte() }.affirmAllerRetour(rt::identiqueU8) + +// Shorts +listOf(Short.MIN_VALUE, Short.MAX_VALUE).affirmAllerRetour(rt::identiqueI16) +listOf(0x0000, 0xFFFF).map { it.toUShort() }.affirmAllerRetour(rt::identiqueU16) + +// Ints +listOf(0, 1, -1, Int.MIN_VALUE, Int.MAX_VALUE).affirmAllerRetour(rt::identiqueI32) +listOf(0x00000000, 0xFFFFFFFF).map { it.toUInt() }.affirmAllerRetour(rt::identiqueU32) + +// Longs +listOf(0L, 1L, -1L, Long.MIN_VALUE, Long.MAX_VALUE).affirmAllerRetour(rt::identiqueI64) +listOf(0u, 1u, ULong.MIN_VALUE, ULong.MAX_VALUE).affirmAllerRetour(rt::identiqueU64) + +// Floats +listOf(0.0F, 0.5F, 0.25F, Float.MIN_VALUE, Float.MAX_VALUE).affirmAllerRetour(rt::identiqueFloat) + +// Doubles +listOf(0.0, 1.0, Double.MIN_VALUE, Double.MAX_VALUE).affirmAllerRetour(rt::identiqueDouble) + +// Strings +listOf("", "abc", "null\u0000byte", "été", "ښي لاس ته لوستلو لوستل", "😻emoji 👨👧👦multi-emoji, 🇨🇭a flag, a canal, panama") + .affirmAllerRetour(rt::identiqueString) + +listOf(-1, 0, 1).map { DictionnaireNombresSignes(it.toByte(), it.toShort(), it.toInt(), it.toLong()) } + .affirmAllerRetour(rt::identiqueNombresSignes) + +listOf(0, 1).map { DictionnaireNombres(it.toUByte(), it.toUShort(), it.toUInt(), it.toULong()) } + .affirmAllerRetour(rt::identiqueNombres) + + +rt.destroy() + +// Test one way across the FFI. +// +// We send one representation of a value to lib.rs, and it transforms it into another, a string. +// lib.rs sends the string back, and then we compare here in kotlin. +// +// This shows that the values are transformed into strings the same way in both kotlin and rust. +// i.e. if we assume that the string return works (we test this assumption elsewhere) +// we show that lowering from kotlin and lifting into rust has values that both kotlin and rust +// both stringify in the same way. i.e. the same values. +// +// If we roundtripping proves the symmetry of our lowering/lifting from here to rust, and lowering/lifting from rust t here, +// and this convinces us that lowering/lifting from here to rust is correct, then +// together, we've shown the correctness of the return leg. +val st = Stringifier() + +typealias StringyEquals<T> = (observed: String, expected: T) -> Boolean +fun <T> List<T>.affirmEnchaine( + fn: (T) -> String, + equals: StringyEquals<T> = { obs, exp -> obs == exp.toString() } +) { + this.forEach { exp -> + val obs = fn.invoke(exp) + assert(equals(obs, exp)) { "$fn($exp): observed=$obs, expected=$exp" } + } +} + +// Test the efficacy of the string transport from rust. If this fails, but everything else +// works, then things are very weird. +val wellKnown = st.wellKnownString("kotlin") +assert("uniffi 💚 kotlin!" == wellKnown) { "wellKnownString 'uniffi 💚 kotlin!' == '$wellKnown'" } + +// Booleans +listOf(true, false).affirmEnchaine(st::toStringBoolean) + +// Bytes. +listOf(Byte.MIN_VALUE, Byte.MAX_VALUE).affirmEnchaine(st::toStringI8) +listOf(UByte.MIN_VALUE, UByte.MAX_VALUE).affirmEnchaine(st::toStringU8) + +// Shorts +listOf(Short.MIN_VALUE, Short.MAX_VALUE).affirmEnchaine(st::toStringI16) +listOf(UShort.MIN_VALUE, UShort.MAX_VALUE).affirmEnchaine(st::toStringU16) + +// Ints +listOf(0, 1, -1, Int.MIN_VALUE, Int.MAX_VALUE).affirmEnchaine(st::toStringI32) +listOf(0u, 1u, UInt.MIN_VALUE, UInt.MAX_VALUE).affirmEnchaine(st::toStringU32) + +// Longs +listOf(0L, 1L, -1L, Long.MIN_VALUE, Long.MAX_VALUE).affirmEnchaine(st::toStringI64) +listOf(0u, 1u, ULong.MIN_VALUE, ULong.MAX_VALUE).affirmEnchaine(st::toStringU64) + +// Floats +// MIN_VALUE is 1.4E-45. Accuracy and formatting get weird at small sizes. +listOf(0.0F, 1.0F, -1.0F, Float.MIN_VALUE, Float.MAX_VALUE).affirmEnchaine(st::toStringFloat) { s, n -> s.toFloat() == n } + +// Doubles +// MIN_VALUE is 4.9E-324. Accuracy and formatting get weird at small sizes. +listOf(0.0, 1.0, -1.0, Double.MIN_VALUE, Double.MAX_VALUE).affirmEnchaine(st::toStringDouble) { s, n -> s.toDouble() == n } + +st.destroy() + +// Prove to ourselves that default arguments are being used. +// Step 1: call the methods without arguments, and check against the UDL. +val op = Optionneur() + +assert(op.sinonString() == "default") + +assert(op.sinonBoolean() == false) + +assert(op.sinonSequence() == listOf<String>()) + +// optionals +assert(op.sinonNull() == null) +assert(op.sinonZero() == 0) + +// decimal integers +assert(op.sinonI8Dec() == (-42).toByte()) +assert(op.sinonU8Dec() == 42.toUByte()) +assert(op.sinonI16Dec() == 42.toShort()) +assert(op.sinonU16Dec() == 42.toUShort()) +assert(op.sinonI32Dec() == 42) +assert(op.sinonU32Dec() == 42.toUInt()) +assert(op.sinonI64Dec() == 42L) +assert(op.sinonU64Dec() == 42uL) + +// hexadecimal integers +assert(op.sinonI8Hex() == (-0x7f).toByte()) +assert(op.sinonU8Hex() == 0xff.toUByte()) +assert(op.sinonI16Hex() == 0x7f.toShort()) +assert(op.sinonU16Hex() == 0xffff.toUShort()) +assert(op.sinonI32Hex() == 0x7fffffff) +assert(op.sinonU32Hex() == 0xffffffff.toUInt()) +assert(op.sinonI64Hex() == 0x7fffffffffffffffL) +assert(op.sinonU64Hex() == 0xffffffffffffffffuL) + +// octal integers +assert(op.sinonU32Oct() == 493u) // 0o755 + +// floats +assert(op.sinonF32() == 42.0f) +assert(op.sinonF64() == 42.1) + +// enums +assert(op.sinonEnum() == Enumeration.TROIS) + +// Step 2. Convince ourselves that if we pass something else, then that changes the output. +// We have shown something coming out of the sinon methods, but without eyeballing the Rust +// we can't be sure that the arguments will change the return value. +listOf("foo", "bar").affirmAllerRetour(op::sinonString) +listOf(true, false).affirmAllerRetour(op::sinonBoolean) +listOf(listOf("a", "b"), listOf()).affirmAllerRetour(op::sinonSequence) + +// optionals +listOf("0", "1").affirmAllerRetour(op::sinonNull) +listOf(0, 1).affirmAllerRetour(op::sinonZero) + +// integers +listOf(0, 1).map { it.toUByte() }.affirmAllerRetour(op::sinonU8Dec) +listOf(0, 1).map { it.toByte() }.affirmAllerRetour(op::sinonI8Dec) +listOf(0, 1).map { it.toUShort() }.affirmAllerRetour(op::sinonU16Dec) +listOf(0, 1).map { it.toShort() }.affirmAllerRetour(op::sinonI16Dec) +listOf(0, 1).map { it.toUInt() }.affirmAllerRetour(op::sinonU32Dec) +listOf(0, 1).map { it.toInt() }.affirmAllerRetour(op::sinonI32Dec) +listOf(0, 1).map { it.toULong() }.affirmAllerRetour(op::sinonU64Dec) +listOf(0, 1).map { it.toLong() }.affirmAllerRetour(op::sinonI64Dec) + +listOf(0, 1).map { it.toUByte() }.affirmAllerRetour(op::sinonU8Hex) +listOf(0, 1).map { it.toByte() }.affirmAllerRetour(op::sinonI8Hex) +listOf(0, 1).map { it.toUShort() }.affirmAllerRetour(op::sinonU16Hex) +listOf(0, 1).map { it.toShort() }.affirmAllerRetour(op::sinonI16Hex) +listOf(0, 1).map { it.toUInt() }.affirmAllerRetour(op::sinonU32Hex) +listOf(0, 1).map { it.toInt() }.affirmAllerRetour(op::sinonI32Hex) +listOf(0, 1).map { it.toULong() }.affirmAllerRetour(op::sinonU64Hex) +listOf(0, 1).map { it.toLong() }.affirmAllerRetour(op::sinonI64Hex) + +listOf(0, 1).map { it.toUInt() }.affirmAllerRetour(op::sinonU32Oct) + +// floats +listOf(0.0f, 1.0f).affirmAllerRetour(op::sinonF32) +listOf(0.0, 1.0).affirmAllerRetour(op::sinonF64) + +// enums +Enumeration.values().toList().affirmAllerRetour(op::sinonEnum) + +op.destroy() + +// Testing defaulting properties in record types. +val defaultes = OptionneurDictionnaire() +val explicites = OptionneurDictionnaire( + i8Var = -8, + u8Var = 8u, + i16Var = -16, + u16Var = 0x10u, + i32Var = -32, + u32Var = 32u, + i64Var = -64L, + u64Var = 64uL, + floatVar = 4.0f, + doubleVar = 8.0, + booleanVar = true, + stringVar = "default", + listVar = listOf(), + enumerationVar = Enumeration.DEUX, + dictionnaireVar = null +) +assert(defaultes == explicites) + +// …and makes sure they travel across and back the FFI. +val rt2 = Retourneur() +listOf(defaultes).affirmAllerRetour(rt2::identiqueOptionneurDictionnaire) + +rt2.destroy() diff --git a/toolkit/components/uniffi-fixtures/rondpoint/tests/bindings/test_rondpoint.py b/toolkit/components/uniffi-fixtures/rondpoint/tests/bindings/test_rondpoint.py new file mode 100644 index 0000000000..df3e3fab18 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/rondpoint/tests/bindings/test_rondpoint.py @@ -0,0 +1,183 @@ +import ctypes +import sys + +from rondpoint import ( + Dictionnaire, + Enumeration, + EnumerationAvecDonnees, + Retourneur, + Stringifier, + copie_carte, + copie_dictionnaire, + copie_enumeration, + copie_enumerations, + switcheroo, +) + +dico = Dictionnaire( + un=Enumeration.DEUX, deux=True, petit_nombre=0, gros_nombre=123456789 +) +copyDico = copie_dictionnaire(dico) +assert dico == copyDico + +assert copie_enumeration(Enumeration.DEUX) == Enumeration.DEUX +assert copie_enumerations([Enumeration.UN, Enumeration.DEUX]) == [ + Enumeration.UN, + Enumeration.DEUX, +] +assert copie_carte( + { + "0": EnumerationAvecDonnees.ZERO(), + "1": EnumerationAvecDonnees.UN(1), + "2": EnumerationAvecDonnees.DEUX(2, "deux"), + } +) == { + "0": EnumerationAvecDonnees.ZERO(), + "1": EnumerationAvecDonnees.UN(1), + "2": EnumerationAvecDonnees.DEUX(2, "deux"), +} + +assert switcheroo(False) is True + +assert EnumerationAvecDonnees.ZERO() != EnumerationAvecDonnees.UN(1) +assert EnumerationAvecDonnees.UN(1) == EnumerationAvecDonnees.UN(1) +assert EnumerationAvecDonnees.UN(1) != EnumerationAvecDonnees.UN(2) + +# Test the roundtrip across the FFI. +# This shows that the values we send come back in exactly the same state as we sent them. +# i.e. it shows that lowering from python and lifting into rust is symmetrical with +# lowering from rust and lifting into python. +rt = Retourneur() + + +def affirmAllerRetour(vals, identique): + for v in vals: + id_v = identique(v) + assert id_v == v, f"Round-trip failure: {v} => {id_v}" + + +MIN_I8 = -1 * 2**7 +MAX_I8 = 2**7 - 1 +MIN_I16 = -1 * 2**15 +MAX_I16 = 2**15 - 1 +MIN_I32 = -1 * 2**31 +MAX_I32 = 2**31 - 1 +MIN_I64 = -1 * 2**31 +MAX_I64 = 2**31 - 1 + +# Python floats are always doubles, so won't round-trip through f32 correctly. +# This truncates them appropriately. +F32_ONE_THIRD = ctypes.c_float(1.0 / 3).value + +# Booleans +affirmAllerRetour([True, False], rt.identique_boolean) + +# Bytes. +affirmAllerRetour([MIN_I8, -1, 0, 1, MAX_I8], rt.identique_i8) +affirmAllerRetour([0x00, 0x12, 0xFF], rt.identique_u8) + +# Shorts +affirmAllerRetour([MIN_I16, -1, 0, 1, MAX_I16], rt.identique_i16) +affirmAllerRetour([0x0000, 0x1234, 0xFFFF], rt.identique_u16) + +# Ints +affirmAllerRetour([MIN_I32, -1, 0, 1, MAX_I32], rt.identique_i32) +affirmAllerRetour([0x00000000, 0x12345678, 0xFFFFFFFF], rt.identique_u32) + +# Longs +affirmAllerRetour([MIN_I64, -1, 0, 1, MAX_I64], rt.identique_i64) +affirmAllerRetour( + [0x0000000000000000, 0x1234567890ABCDEF, 0xFFFFFFFFFFFFFFFF], rt.identique_u64 +) + +# Floats +affirmAllerRetour([0.0, 0.5, 0.25, 1.0, F32_ONE_THIRD], rt.identique_float) + +# Doubles +affirmAllerRetour( + [0.0, 0.5, 0.25, 1.0, 1.0 / 3, sys.float_info.max, sys.float_info.min], + rt.identique_double, +) + +# Strings +affirmAllerRetour( + [ + "", + "abc", + "été", + "ښي لاس ته لوستلو لوستل", + "😻emoji 👨👧👦multi-emoji, 🇨🇭a flag, a canal, panama", + ], + rt.identique_string, +) + +# Test one way across the FFI. +# +# We send one representation of a value to lib.rs, and it transforms it into another, a string. +# lib.rs sends the string back, and then we compare here in python. +# +# This shows that the values are transformed into strings the same way in both python and rust. +# i.e. if we assume that the string return works (we test this assumption elsewhere) +# we show that lowering from python and lifting into rust has values that both python and rust +# both stringify in the same way. i.e. the same values. +# +# If we roundtripping proves the symmetry of our lowering/lifting from here to rust, and lowering/lifting from rust to here, +# and this convinces us that lowering/lifting from here to rust is correct, then +# together, we've shown the correctness of the return leg. +st = Stringifier() + + +def affirmEnchaine(vals, toString, rustyStringify=lambda v: str(v).lower()): + for v in vals: + str_v = toString(v) + assert rustyStringify(v) == str_v, f"String compare error {v} => {str_v}" + + +# Test the efficacy of the string transport from rust. If this fails, but everything else +# works, then things are very weird. +wellKnown = st.well_known_string("python") +assert "uniffi 💚 python!" == wellKnown + +# Booleans +affirmEnchaine([True, False], st.to_string_boolean) + +# Bytes. +affirmEnchaine([MIN_I8, -1, 0, 1, MAX_I8], st.to_string_i8) +affirmEnchaine([0x00, 0x12, 0xFF], st.to_string_u8) + +# Shorts +affirmEnchaine([MIN_I16, -1, 0, 1, MAX_I16], st.to_string_i16) +affirmEnchaine([0x0000, 0x1234, 0xFFFF], st.to_string_u16) + +# Ints +affirmEnchaine([MIN_I32, -1, 0, 1, MAX_I32], st.to_string_i32) +affirmEnchaine([0x00000000, 0x12345678, 0xFFFFFFFF], st.to_string_u32) + +# Longs +affirmEnchaine([MIN_I64, -1, 0, 1, MAX_I64], st.to_string_i64) +affirmEnchaine( + [0x0000000000000000, 0x1234567890ABCDEF, 0xFFFFFFFFFFFFFFFF], st.to_string_u64 +) + + +# Floats +def rustyFloatToStr(v): + """Stringify a float in the same way that rust seems to.""" + # Rust doesn't include the decimal part of whole enumber floats when stringifying. + if int(v) == v: + return str(int(v)) + return str(v) + + +affirmEnchaine([0.0, 0.5, 0.25, 1.0], st.to_string_float, rustyFloatToStr) +assert ( + st.to_string_float(F32_ONE_THIRD) == "0.33333334" +) # annoyingly different string repr + +# Doubles +# TODO: float_info.max/float_info.min don't stringify-roundtrip properly yet, TBD. +affirmEnchaine( + [0.0, 0.5, 0.25, 1.0, 1.0 / 3], + st.to_string_double, + rustyFloatToStr, +) diff --git a/toolkit/components/uniffi-fixtures/rondpoint/tests/bindings/test_rondpoint.rb b/toolkit/components/uniffi-fixtures/rondpoint/tests/bindings/test_rondpoint.rb new file mode 100644 index 0000000000..faa4062019 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/rondpoint/tests/bindings/test_rondpoint.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +require 'test/unit' +require 'rondpoint' + +include Test::Unit::Assertions +include Rondpoint + +dico = Dictionnaire.new( + un: Enumeration::DEUX, + deux: true, + petit_nombre: 0, + gros_nombre: 123_456_789 +) + +assert_equal dico, Rondpoint.copie_dictionnaire(dico) + +assert_equal Rondpoint.copie_enumeration(Enumeration::DEUX), Enumeration::DEUX + +assert_equal Rondpoint.copie_enumerations([ + Enumeration::UN, + Enumeration::DEUX + ]), [Enumeration::UN, Enumeration::DEUX] + +assert_equal Rondpoint.copie_carte({ + '0' => EnumerationAvecDonnees::ZERO.new, + '1' => EnumerationAvecDonnees::UN.new(1), + '2' => EnumerationAvecDonnees::DEUX.new(2, 'deux') + }), { + '0' => EnumerationAvecDonnees::ZERO.new, + '1' => EnumerationAvecDonnees::UN.new(1), + '2' => EnumerationAvecDonnees::DEUX.new(2, 'deux') + } + +assert Rondpoint.switcheroo(false) + +assert_not_equal EnumerationAvecDonnees::ZERO.new, EnumerationAvecDonnees::UN.new(1) +assert_equal EnumerationAvecDonnees::UN.new(1), EnumerationAvecDonnees::UN.new(1) +assert_not_equal EnumerationAvecDonnees::UN.new(1), EnumerationAvecDonnees::UN.new(2) + +# Test the roundtrip across the FFI. +# This shows that the values we send come back in exactly the same state as we sent them. +# i.e. it shows that lowering from ruby and lifting into rust is symmetrical with +# lowering from rust and lifting into ruby. +RT = Retourneur.new + +def affirm_aller_retour(vals, fn_name) + vals.each do |v| + id_v = RT.public_send fn_name, v + + assert_equal id_v, v, "Round-trip failure: #{v} => #{id_v}" + end +end + +MIN_I8 = -1 * 2**7 +MAX_I8 = 2**7 - 1 +MIN_I16 = -1 * 2**15 +MAX_I16 = 2**15 - 1 +MIN_I32 = -1 * 2**31 +MAX_I32 = 2**31 - 1 +MIN_I64 = -1 * 2**31 +MAX_I64 = 2**31 - 1 + +# Ruby floats are always doubles, so won't round-trip through f32 correctly. +# This truncates them appropriately. +F32_ONE_THIRD = [1.0 / 3].pack('f').unpack('f')[0] + +# Booleans +affirm_aller_retour([true, false], :identique_boolean) + +# Bytes. +affirm_aller_retour([MIN_I8, -1, 0, 1, MAX_I8], :identique_i8) +affirm_aller_retour([0x00, 0x12, 0xFF], :identique_u8) + +# Shorts +affirm_aller_retour([MIN_I16, -1, 0, 1, MAX_I16], :identique_i16) +affirm_aller_retour([0x0000, 0x1234, 0xFFFF], :identique_u16) + +# Ints +affirm_aller_retour([MIN_I32, -1, 0, 1, MAX_I32], :identique_i32) +affirm_aller_retour([0x00000000, 0x12345678, 0xFFFFFFFF], :identique_u32) + +# Longs +affirm_aller_retour([MIN_I64, -1, 0, 1, MAX_I64], :identique_i64) +affirm_aller_retour([0x0000000000000000, 0x1234567890ABCDEF, 0xFFFFFFFFFFFFFFFF], :identique_u64) + +# Floats +affirm_aller_retour([0.0, 0.5, 0.25, 1.0, F32_ONE_THIRD], :identique_float) + +# Doubles +affirm_aller_retour( + [0.0, 0.5, 0.25, 1.0, 1.0 / 3, Float::MAX, Float::MIN], + :identique_double +) + +# Strings +affirm_aller_retour( + ['', 'abc', 'été', 'ښي لاس ته لوستلو لوستل', + '😻emoji 👨👧👦multi-emoji, 🇨🇭a flag, a canal, panama'], + :identique_string +) + +# Test one way across the FFI. +# +# We send one representation of a value to lib.rs, and it transforms it into another, a string. +# lib.rs sends the string back, and then we compare here in ruby. +# +# This shows that the values are transformed into strings the same way in both ruby and rust. +# i.e. if we assume that the string return works (we test this assumption elsewhere) +# we show that lowering from ruby and lifting into rust has values that both ruby and rust +# both stringify in the same way. i.e. the same values. +# +# If we roundtripping proves the symmetry of our lowering/lifting from here to rust, and lowering/lifting from rust to here, +# and this convinces us that lowering/lifting from here to rust is correct, then +# together, we've shown the correctness of the return leg. +ST = Stringifier.new + +def affirm_enchaine(vals, fn_name) + vals.each do |v| + str_v = ST.public_send fn_name, v + + assert_equal v.to_s, str_v, "String compare error #{v} => #{str_v}" + end +end + +# Test the efficacy of the string transport from rust. If this fails, but everything else +# works, then things are very weird. +assert_equal ST.well_known_string('ruby'), 'uniffi 💚 ruby!' + +# Booleans +affirm_enchaine([true, false], :to_string_boolean) + +# Bytes. +affirm_enchaine([MIN_I8, -1, 0, 1, MAX_I8], :to_string_i8) +affirm_enchaine([0x00, 0x12, 0xFF], :to_string_u8) + +# Shorts +affirm_enchaine([MIN_I16, -1, 0, 1, MAX_I16], :to_string_i16) +affirm_enchaine([0x0000, 0x1234, 0xFFFF], :to_string_u16) + +# Ints +affirm_enchaine([MIN_I32, -1, 0, 1, MAX_I32], :to_string_i32) +affirm_enchaine([0x00000000, 0x12345678, 0xFFFFFFFF], :to_string_u32) + +# Longs +affirm_enchaine([MIN_I64, -1, 0, 1, MAX_I64], :to_string_i64) +affirm_enchaine([0x0000000000000000, 0x1234567890ABCDEF, 0xFFFFFFFFFFFFFFFF], :to_string_u64) diff --git a/toolkit/components/uniffi-fixtures/rondpoint/tests/bindings/test_rondpoint.swift b/toolkit/components/uniffi-fixtures/rondpoint/tests/bindings/test_rondpoint.swift new file mode 100644 index 0000000000..d9f47058ed --- /dev/null +++ b/toolkit/components/uniffi-fixtures/rondpoint/tests/bindings/test_rondpoint.swift @@ -0,0 +1,232 @@ +import rondpoint + +let dico = Dictionnaire(un: .deux, deux: false, petitNombre: 0, grosNombre: 123456789) +let copyDico = copieDictionnaire(d: dico) +assert(dico == copyDico) + +assert(copieEnumeration(e: .deux) == .deux) +assert(copieEnumerations(e: [.un, .deux]) == [.un, .deux]) +assert(copieCarte(c: + ["0": .zero, + "1": .un(premier: 1), + "2": .deux(premier: 2, second: "deux") +]) == [ + "0": .zero, + "1": .un(premier: 1), + "2": .deux(premier: 2, second: "deux") +]) + +assert(EnumerationAvecDonnees.zero != EnumerationAvecDonnees.un(premier: 1)) +assert(EnumerationAvecDonnees.un(premier: 1) == EnumerationAvecDonnees.un(premier: 1)) +assert(EnumerationAvecDonnees.un(premier: 1) != EnumerationAvecDonnees.un(premier: 2)) + + +assert(switcheroo(b: false)) + +// Test the roundtrip across the FFI. +// This shows that the values we send come back in exactly the same state as we sent them. +// i.e. it shows that lowering from swift and lifting into rust is symmetrical with +// lowering from rust and lifting into swift. +let rt = Retourneur() + +// Booleans +[true, false].affirmAllerRetour(rt.identiqueBoolean) + +// Bytes. +[.min, .max].affirmAllerRetour(rt.identiqueI8) +[0x00, 0xFF].map { $0 as UInt8 }.affirmAllerRetour(rt.identiqueU8) + +// Shorts +[.min, .max].affirmAllerRetour(rt.identiqueI16) +[0x0000, 0xFFFF].map { $0 as UInt16 }.affirmAllerRetour(rt.identiqueU16) + +// Ints +[0, 1, -1, .min, .max].affirmAllerRetour(rt.identiqueI32) +[0x00000000, 0xFFFFFFFF].map { $0 as UInt32 }.affirmAllerRetour(rt.identiqueU32) + +// Longs +[.zero, 1, -1, .min, .max].affirmAllerRetour(rt.identiqueI64) +[.zero, 1, .min, .max].affirmAllerRetour(rt.identiqueU64) + +// Floats +[.zero, 1, 0.25, .leastNonzeroMagnitude, .greatestFiniteMagnitude].affirmAllerRetour(rt.identiqueFloat) + +// Doubles +[0.0, 1.0, .leastNonzeroMagnitude, .greatestFiniteMagnitude].affirmAllerRetour(rt.identiqueDouble) + +// Strings +["", "abc", "null\0byte", "été", "ښي لاس ته لوستلو لوستل", "😻emoji 👨👧👦multi-emoji, 🇨🇭a flag, a canal, panama"] + .affirmAllerRetour(rt.identiqueString) + +// Test one way across the FFI. +// +// We send one representation of a value to lib.rs, and it transforms it into another, a string. +// lib.rs sends the string back, and then we compare here in swift. +// +// This shows that the values are transformed into strings the same way in both swift and rust. +// i.e. if we assume that the string return works (we test this assumption elsewhere) +// we show that lowering from swift and lifting into rust has values that both swift and rust +// both stringify in the same way. i.e. the same values. +// +// If we roundtripping proves the symmetry of our lowering/lifting from here to rust, and lowering/lifting from rust t here, +// and this convinces us that lowering/lifting from here to rust is correct, then +// together, we've shown the correctness of the return leg. +let st = Stringifier() + +// Test the effigacy of the string transport from rust. If this fails, but everything else +// works, then things are very weird. +let wellKnown = st.wellKnownString(value: "swift") +assert("uniffi 💚 swift!" == wellKnown, "wellKnownString 'uniffi 💚 swift!' == '\(wellKnown)'") + +// Booleans +[true, false].affirmEnchaine(st.toStringBoolean) + +// Bytes. +[.min, .max].affirmEnchaine(st.toStringI8) +[.min, .max].affirmEnchaine(st.toStringU8) + +// Shorts +[.min, .max].affirmEnchaine(st.toStringI16) +[.min, .max].affirmEnchaine(st.toStringU16) + +// Ints +[0, 1, -1, .min, .max].affirmEnchaine(st.toStringI32) +[0, 1, .min, .max].affirmEnchaine(st.toStringU32) + +// Longs +[.zero, 1, -1, .min, .max].affirmEnchaine(st.toStringI64) +[.zero, 1, .min, .max].affirmEnchaine(st.toStringU64) + +// Floats +[.zero, 1, -1, .leastNonzeroMagnitude, .greatestFiniteMagnitude].affirmEnchaine(st.toStringFloat) { Float.init($0) == $1 } + +// Doubles +[.zero, 1, -1, .leastNonzeroMagnitude, .greatestFiniteMagnitude].affirmEnchaine(st.toStringDouble) { Double.init($0) == $1 } + +// Some extension functions for testing the results of roundtripping and stringifying +extension Array where Element: Equatable { + static func defaultEquals(_ observed: String, expected: Element) -> Bool { + let exp = "\(expected)" + return observed == exp + } + + func affirmEnchaine(_ fn: (Element) -> String, equals: (String, Element) -> Bool = defaultEquals) { + self.forEach { v in + let obs = fn(v) + assert(equals(obs, v), "toString_\(type(of:v))(\(v)): observed=\(obs), expected=\(v)") + } + } + + func affirmAllerRetour(_ fn: (Element) -> Element) { + self.forEach { v in + assert(fn(v) == v, "identique_\(type(of:v))(\(v))") + } + } +} + +// Prove to ourselves that default arguments are being used. +// Step 1: call the methods without arguments, and check against the UDL. +let op = Optionneur() + +assert(op.sinonString() == "default") + +assert(op.sinonBoolean() == false) + +assert(op.sinonSequence() == []) + +// optionals +assert(op.sinonNull() == nil) +assert(op.sinonZero() == 0) + +// decimal integers +assert(op.sinonU8Dec() == UInt8(42)) +assert(op.sinonI8Dec() == Int8(-42)) +assert(op.sinonU16Dec() == UInt16(42)) +assert(op.sinonI16Dec() == Int16(42)) +assert(op.sinonU32Dec() == UInt32(42)) +assert(op.sinonI32Dec() == Int32(42)) +assert(op.sinonU64Dec() == UInt64(42)) +assert(op.sinonI64Dec() == Int64(42)) + +// hexadecimal integers +assert(op.sinonU8Hex() == UInt8(0xff)) +assert(op.sinonI8Hex() == Int8(-0x7f)) +assert(op.sinonU16Hex() == UInt16(0xffff)) +assert(op.sinonI16Hex() == Int16(0x7f)) +assert(op.sinonU32Hex() == UInt32(0xffffffff)) +assert(op.sinonI32Hex() == Int32(0x7fffffff)) +assert(op.sinonU64Hex() == UInt64(0xffffffffffffffff)) +assert(op.sinonI64Hex() == Int64(0x7fffffffffffffff)) + +// octal integers +assert(op.sinonU32Oct() == UInt32(0o755)) + +// floats +assert(op.sinonF32() == 42.0) +assert(op.sinonF64() == Double(42.1)) + +// enums +assert(op.sinonEnum() == .trois) + +// Step 2. Convince ourselves that if we pass something else, then that changes the output. +// We have shown something coming out of the sinon methods, but without eyeballing the Rust +// we can't be sure that the arguments will change the return value. +["foo", "bar"].affirmAllerRetour(op.sinonString) +[true, false].affirmAllerRetour(op.sinonBoolean) +[["a", "b"], []].affirmAllerRetour(op.sinonSequence) + +// optionals +["0", "1"].affirmAllerRetour(op.sinonNull) +[0, 1].affirmAllerRetour(op.sinonZero) + +// integers +[0, 1].affirmAllerRetour(op.sinonU8Dec) +[0, 1].affirmAllerRetour(op.sinonI8Dec) +[0, 1].affirmAllerRetour(op.sinonU16Dec) +[0, 1].affirmAllerRetour(op.sinonI16Dec) +[0, 1].affirmAllerRetour(op.sinonU32Dec) +[0, 1].affirmAllerRetour(op.sinonI32Dec) +[0, 1].affirmAllerRetour(op.sinonU64Dec) +[0, 1].affirmAllerRetour(op.sinonI64Dec) + +[0, 1].affirmAllerRetour(op.sinonU8Hex) +[0, 1].affirmAllerRetour(op.sinonI8Hex) +[0, 1].affirmAllerRetour(op.sinonU16Hex) +[0, 1].affirmAllerRetour(op.sinonI16Hex) +[0, 1].affirmAllerRetour(op.sinonU32Hex) +[0, 1].affirmAllerRetour(op.sinonI32Hex) +[0, 1].affirmAllerRetour(op.sinonU64Hex) +[0, 1].affirmAllerRetour(op.sinonI64Hex) + +[0, 1].affirmAllerRetour(op.sinonU32Oct) + +// floats +[0.0, 1.0].affirmAllerRetour(op.sinonF32) +[0.0, 1.0].affirmAllerRetour(op.sinonF64) + +// enums +[.un, .deux, .trois].affirmAllerRetour(op.sinonEnum) + +// Testing defaulting properties in record types. +let defaultes = OptionneurDictionnaire() +let explicites = OptionneurDictionnaire( + i8Var: Int8(-8), + u8Var: UInt8(8), + i16Var: Int16(-16), + u16Var: UInt16(0x10), + i32Var: -32, + u32Var: UInt32(32), + i64Var: Int64(-64), + u64Var: UInt64(64), + floatVar: Float(4.0), + doubleVar: Double(8.0), + booleanVar: true, + stringVar: "default", + listVar: [], + enumerationVar: .deux, + dictionnaireVar: nil +) + +// …and makes sure they travel across and back the FFI. +assert(defaultes == explicites) +[defaultes].affirmAllerRetour(rt.identiqueOptionneurDictionnaire) diff --git a/toolkit/components/uniffi-fixtures/rondpoint/tests/test_generated_bindings.rs b/toolkit/components/uniffi-fixtures/rondpoint/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..d337374334 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/rondpoint/tests/test_generated_bindings.rs @@ -0,0 +1,6 @@ +uniffi::build_foreign_language_testcases!( + "tests/bindings/test_rondpoint.kts", + "tests/bindings/test_rondpoint.swift", + "tests/bindings/test_rondpoint.py", + "tests/bindings/test_rondpoint.rb", +); diff --git a/toolkit/components/uniffi-fixtures/sprites/Cargo.toml b/toolkit/components/uniffi-fixtures/sprites/Cargo.toml new file mode 100644 index 0000000000..3b4f96e143 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/sprites/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "uniffi-example-sprites" +edition = "2021" +version = "0.22.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +license = "MPL-2.0" +publish = false + +[lib] +crate-type = ["lib", "cdylib"] +name = "uniffi_sprites" + +[dependencies] +uniffi = { workspace = true } + +[build-dependencies] +uniffi = { workspace = true, features = ["build"] } + +[dev-dependencies] +uniffi = { workspace = true, features = ["bindgen-tests"] } diff --git a/toolkit/components/uniffi-fixtures/sprites/build.rs b/toolkit/components/uniffi-fixtures/sprites/build.rs new file mode 100644 index 0000000000..26ac3085b8 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/sprites/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::generate_scaffolding("src/sprites.udl").unwrap(); +} diff --git a/toolkit/components/uniffi-fixtures/sprites/src/lib.rs b/toolkit/components/uniffi-fixtures/sprites/src/lib.rs new file mode 100644 index 0000000000..d3cc11e408 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/sprites/src/lib.rs @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::sync::RwLock; + +// A point in two-dimensional space. +#[derive(Debug, Clone)] +pub struct Point { + x: f64, + y: f64, +} + +// A magnitude and direction in two-dimensional space. +// For simplicity we represent this as a point relative to the origin. +#[derive(Debug, Clone)] +pub struct Vector { + dx: f64, + dy: f64, +} + +// Move from the given Point, according to the given Vector. +pub fn translate(p: &Point, v: Vector) -> Point { + Point { + x: p.x + v.dx, + y: p.y + v.dy, + } +} + +// An entity in our imaginary world, which occupies a position in space +// and which can move about over time. +#[derive(Debug)] +pub struct Sprite { + // We must use interior mutability for managing mutable state, hence the `RwLock`. + current_position: RwLock<Point>, +} + +impl Sprite { + fn new(initial_position: Option<Point>) -> Sprite { + Sprite { + current_position: RwLock::new(initial_position.unwrap_or(Point { x: 0.0, y: 0.0 })), + } + } + + fn new_relative_to(reference: Point, direction: Vector) -> Sprite { + Sprite { + current_position: RwLock::new(translate(&reference, direction)), + } + } + + fn get_position(&self) -> Point { + self.current_position.read().unwrap().clone() + } + + fn move_to(&self, position: Point) { + *self.current_position.write().unwrap() = position; + } + + fn move_by(&self, direction: Vector) { + let mut current_position = self.current_position.write().unwrap(); + *current_position = translate(¤t_position, direction) + } +} + +uniffi::include_scaffolding!("sprites"); diff --git a/toolkit/components/uniffi-fixtures/sprites/src/sprites.udl b/toolkit/components/uniffi-fixtures/sprites/src/sprites.udl new file mode 100644 index 0000000000..6781c6cee5 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/sprites/src/sprites.udl @@ -0,0 +1,22 @@ + +namespace sprites { + Point translate([ByRef] Point position, Vector direction); +}; + +dictionary Point { + double x; + double y; +}; + +dictionary Vector { + double dx; + double dy; +}; + +interface Sprite { + constructor(Point? initial_position); + [Name=new_relative_to] constructor(Point reference, Vector direction); + Point get_position(); + void move_to(Point position); + void move_by(Vector direction); +}; diff --git a/toolkit/components/uniffi-fixtures/sprites/tests/bindings/test_sprites.kts b/toolkit/components/uniffi-fixtures/sprites/tests/bindings/test_sprites.kts new file mode 100644 index 0000000000..42451f28dd --- /dev/null +++ b/toolkit/components/uniffi-fixtures/sprites/tests/bindings/test_sprites.kts @@ -0,0 +1,25 @@ +import uniffi.sprites.*; + +val sempty = Sprite(null) +assert( sempty.getPosition() == Point(0.0, 0.0) ) + +val s = Sprite(Point(0.0, 1.0)) +assert( s.getPosition() == Point(0.0, 1.0) ) + +s.moveTo(Point(1.0, 2.0)) +assert( s.getPosition() == Point(1.0, 2.0) ) + +s.moveBy(Vector(-4.0, 2.0)) +assert( s.getPosition() == Point(-3.0, 4.0) ) + +s.destroy() +try { + s.moveBy(Vector(0.0, 0.0)) + assert(false) { "Should not be able to call anything after `destroy`" } +} catch(e: IllegalStateException) { + assert(true) +} + +val srel = Sprite.newRelativeTo(Point(0.0, 1.0), Vector(1.0, 1.5)) +assert( srel.getPosition() == Point(1.0, 2.5) ) + diff --git a/toolkit/components/uniffi-fixtures/sprites/tests/bindings/test_sprites.py b/toolkit/components/uniffi-fixtures/sprites/tests/bindings/test_sprites.py new file mode 100644 index 0000000000..1e91997001 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/sprites/tests/bindings/test_sprites.py @@ -0,0 +1,16 @@ +from sprites import Point, Sprite, Vector + +sempty = Sprite(None) +assert sempty.get_position() == Point(x=0, y=0) + +s = Sprite(Point(x=0, y=1)) +assert s.get_position() == Point(x=0, y=1) + +s.move_to(Point(x=1, y=2)) +assert s.get_position() == Point(x=1, y=2) + +s.move_by(Vector(dx=-4, dy=2)) +assert s.get_position() == Point(x=-3, y=4) + +srel = Sprite.new_relative_to(Point(x=0, y=1), Vector(dx=1, dy=1.5)) +assert srel.get_position() == Point(x=1, y=2.5) diff --git a/toolkit/components/uniffi-fixtures/sprites/tests/bindings/test_sprites.rb b/toolkit/components/uniffi-fixtures/sprites/tests/bindings/test_sprites.rb new file mode 100644 index 0000000000..fa73043979 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/sprites/tests/bindings/test_sprites.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'test/unit' +require 'sprites' + +include Test::Unit::Assertions +include Sprites + +sempty = Sprite.new(nil) +assert_equal sempty.get_position, Point.new(x: 0, y: 0) + +s = Sprite.new(Point.new(x: 0, y: 1)) +assert_equal s.get_position, Point.new(x: 0, y: 1) + +s.move_to(Point.new(x: 1, y: 2)) +assert_equal s.get_position, Point.new(x: 1, y: 2) + +s.move_by(Vector.new(dx: -4, dy: 2)) +assert_equal s.get_position, Point.new(x: -3, y: 4) + +srel = Sprite.new_relative_to(Point.new(x: 0, y: 1), Vector.new(dx: 1, dy: 1.5)) +assert_equal srel.get_position, Point.new(x: 1, y: 2.5) diff --git a/toolkit/components/uniffi-fixtures/sprites/tests/bindings/test_sprites.swift b/toolkit/components/uniffi-fixtures/sprites/tests/bindings/test_sprites.swift new file mode 100644 index 0000000000..d5428ac679 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/sprites/tests/bindings/test_sprites.swift @@ -0,0 +1,16 @@ +import sprites + +let sempty = Sprite(initialPosition: nil) +assert( sempty.getPosition() == Point(x: 0, y: 0)) + +let s = Sprite(initialPosition: Point(x: 0, y: 1)) +assert( s.getPosition() == Point(x: 0, y: 1)) + +s.moveTo(position: Point(x: 1.0, y: 2.0)) +assert( s.getPosition() == Point(x: 1, y: 2)) + +s.moveBy(direction: Vector(dx: -4, dy: 2)) +assert( s.getPosition() == Point(x: -3, y: 4)) + +let srel = Sprite.newRelativeTo(reference: Point(x: 0.0, y: 1.0), direction: Vector(dx: 1, dy: 1.5)) +assert( srel.getPosition() == Point(x: 1.0, y: 2.5) ) diff --git a/toolkit/components/uniffi-fixtures/sprites/tests/test_generated_bindings.rs b/toolkit/components/uniffi-fixtures/sprites/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..00dd779d68 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/sprites/tests/test_generated_bindings.rs @@ -0,0 +1,6 @@ +uniffi::build_foreign_language_testcases!( + "tests/bindings/test_sprites.py", + "tests/bindings/test_sprites.rb", + "tests/bindings/test_sprites.kts", + "tests/bindings/test_sprites.swift", +); diff --git a/toolkit/components/uniffi-fixtures/todolist/Cargo.toml b/toolkit/components/uniffi-fixtures/todolist/Cargo.toml new file mode 100644 index 0000000000..6673d01e07 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/todolist/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "uniffi-example-todolist" +edition = "2021" +version = "0.22.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +license = "MPL-2.0" +publish = false + +[lib] +crate-type = ["lib", "cdylib"] +name = "uniffi_todolist" + +[dependencies] +uniffi = { workspace = true } +once_cell = "1.12" +thiserror = "1.0" + +[build-dependencies] +uniffi = { workspace = true, features = ["build"] } + +[dev-dependencies] +uniffi = { workspace = true, features = ["bindgen-tests"] } diff --git a/toolkit/components/uniffi-fixtures/todolist/build.rs b/toolkit/components/uniffi-fixtures/todolist/build.rs new file mode 100644 index 0000000000..2dd2f68b75 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/todolist/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::generate_scaffolding("src/todolist.udl").unwrap(); +} diff --git a/toolkit/components/uniffi-fixtures/todolist/src/lib.rs b/toolkit/components/uniffi-fixtures/todolist/src/lib.rs new file mode 100644 index 0000000000..11cdc63aee --- /dev/null +++ b/toolkit/components/uniffi-fixtures/todolist/src/lib.rs @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::sync::{Arc, RwLock}; + +use once_cell::sync::Lazy; + +#[derive(Debug, Clone)] +pub struct TodoEntry { + text: String, +} + +// There is a single "default" TodoList that can be shared +// by all consumers of this component. Depending on requirements, +// a real app might like to use a `Weak<>` rather than an `Arc<>` +// here to reduce the risk of circular references. +static DEFAULT_LIST: Lazy<RwLock<Option<Arc<TodoList>>>> = Lazy::new(|| RwLock::new(None)); + +#[derive(Debug, thiserror::Error)] +pub enum TodoError { + #[error("The todo does not exist!")] + TodoDoesNotExist, + #[error("The todolist is empty!")] + EmptyTodoList, + #[error("That todo already exists!")] + DuplicateTodo, + #[error("Empty String error!: {0}")] + EmptyString(String), + #[error("I am a delegated Error: {0}")] + DeligatedError(#[from] std::io::Error), +} + +/// Get a reference to the global default TodoList, if set. +/// +fn get_default_list() -> Option<Arc<TodoList>> { + DEFAULT_LIST.read().unwrap().clone() +} + +/// Set the global default TodoList. +/// +/// This will silently drop any previously set value. +/// +fn set_default_list(list: Arc<TodoList>) { + *DEFAULT_LIST.write().unwrap() = Some(list); +} + +/// Create a new TodoEntry from the given string. +/// +fn create_entry_with<S: Into<String>>(item: S) -> Result<TodoEntry> { + let text = item.into(); + if text.is_empty() { + return Err(TodoError::EmptyString( + "Cannot add empty string as entry".to_string(), + )); + } + Ok(TodoEntry { text }) +} + +type Result<T, E = TodoError> = std::result::Result<T, E>; + +// A simple Todolist. +// UniFFI requires that we use interior mutability for managing mutable state, so we wrap our `Vec` in a RwLock. +// (A Mutex would also work, but a RwLock is more appropriate for this use-case, so we use it). +#[derive(Debug)] +pub struct TodoList { + items: RwLock<Vec<String>>, +} + +impl TodoList { + fn new() -> Self { + Self { + items: RwLock::new(Vec::new()), + } + } + + fn add_item<S: Into<String>>(&self, item: S) -> Result<()> { + let item = item.into(); + if item.is_empty() { + return Err(TodoError::EmptyString( + "Cannot add empty string as item".to_string(), + )); + } + let mut items = self.items.write().unwrap(); + if items.contains(&item) { + return Err(TodoError::DuplicateTodo); + } + items.push(item); + Ok(()) + } + + fn get_last(&self) -> Result<String> { + let items = self.items.read().unwrap(); + items.last().cloned().ok_or(TodoError::EmptyTodoList) + } + + fn get_first(&self) -> Result<String> { + let items = self.items.read().unwrap(); + items.first().cloned().ok_or(TodoError::EmptyTodoList) + } + + fn add_entries(&self, entries: Vec<TodoEntry>) { + let mut items = self.items.write().unwrap(); + items.extend(entries.into_iter().map(|e| e.text)) + } + + fn add_entry(&self, entry: TodoEntry) -> Result<()> { + self.add_item(entry.text) + } + + fn add_items<S: Into<String>>(&self, items: Vec<S>) { + let mut my_items = self.items.write().unwrap(); + my_items.extend(items.into_iter().map(Into::into)) + } + + fn get_items(&self) -> Vec<String> { + let items = self.items.read().unwrap(); + items.clone() + } + + fn get_entries(&self) -> Vec<TodoEntry> { + let items = self.items.read().unwrap(); + items + .iter() + .map(|text| TodoEntry { text: text.clone() }) + .collect() + } + + fn get_last_entry(&self) -> Result<TodoEntry> { + let text = self.get_last()?; + Ok(TodoEntry { text }) + } + + fn clear_item<S: Into<String>>(&self, item: S) -> Result<()> { + let item = item.into(); + let mut items = self.items.write().unwrap(); + let idx = items + .iter() + .position(|s| s == &item) + .ok_or(TodoError::TodoDoesNotExist)?; + items.remove(idx); + Ok(()) + } + + fn make_default(self: Arc<Self>) { + set_default_list(self); + } +} + +uniffi::include_scaffolding!("todolist"); diff --git a/toolkit/components/uniffi-fixtures/todolist/src/todolist.udl b/toolkit/components/uniffi-fixtures/todolist/src/todolist.udl new file mode 100644 index 0000000000..5c923314cd --- /dev/null +++ b/toolkit/components/uniffi-fixtures/todolist/src/todolist.udl @@ -0,0 +1,38 @@ +namespace todolist { + TodoList? get_default_list(); + undefined set_default_list(TodoList list); + + [Throws=TodoError] + TodoEntry create_entry_with(string todo); +}; + +dictionary TodoEntry { + string text; +}; + +[Error] +enum TodoError { + "TodoDoesNotExist", "EmptyTodoList", "DuplicateTodo", "EmptyString", "DeligatedError" +}; + +interface TodoList { + constructor(); + [Throws=TodoError] + void add_item(string todo); + [Throws=TodoError] + void add_entry(TodoEntry entry); + sequence<TodoEntry> get_entries(); + sequence<string> get_items(); + void add_entries(sequence<TodoEntry> entries); + void add_items(sequence<string> items); + [Throws=TodoError] + TodoEntry get_last_entry(); + [Throws=TodoError] + string get_last(); + [Throws=TodoError] + string get_first(); + [Throws=TodoError] + void clear_item(string todo); + [Self=ByArc] + undefined make_default(); +}; diff --git a/toolkit/components/uniffi-fixtures/todolist/tests/bindings/test_todolist.kts b/toolkit/components/uniffi-fixtures/todolist/tests/bindings/test_todolist.kts new file mode 100644 index 0000000000..bb2b292224 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/todolist/tests/bindings/test_todolist.kts @@ -0,0 +1,83 @@ +import uniffi.todolist.* + +val todo = TodoList() + +// This throws an exception: +try { + todo.getLast() + throw RuntimeException("Should have thrown a TodoError!") +} catch (e: TodoException.EmptyTodoList) { + // It's okay, we don't have any items yet! +} + +try { + createEntryWith("") + throw RuntimeException("Should have thrown a TodoError!") +} catch (e: TodoException) { + // It's okay, the string was empty! + assert(e is TodoException.EmptyString) + assert(e !is TodoException.EmptyTodoList) +} + +todo.addItem("Write strings support") + +assert(todo.getLast() == "Write strings support") + +todo.addItem("Write tests for strings support") + +assert(todo.getLast() == "Write tests for strings support") + +val entry = createEntryWith("Write bindings for strings as record members") + +todo.addEntry(entry) +assert(todo.getLast() == "Write bindings for strings as record members") +assert(todo.getLastEntry().text == "Write bindings for strings as record members") + +todo.addItem("Test Ünicode hàndling without an entry can't believe I didn't test this at first 🤣") +assert(todo.getLast() == "Test Ünicode hàndling without an entry can't believe I didn't test this at first 🤣") + +val entry2 = TodoEntry("Test Ünicode hàndling in an entry can't believe I didn't test this at first 🤣") +todo.addEntry(entry2) +assert(todo.getLastEntry().text == "Test Ünicode hàndling in an entry can't believe I didn't test this at first 🤣") + +assert(todo.getEntries().size == 5) + +todo.addEntries(listOf(TodoEntry("foo"), TodoEntry("bar"))) +assert(todo.getEntries().size == 7) +assert(todo.getLastEntry().text == "bar") + +todo.addItems(listOf("bobo", "fofo")) +assert(todo.getItems().size == 9) +assert(todo.getItems()[7] == "bobo") + +assert(getDefaultList() == null) + +// Note that each individual object instance needs to be explicitly destroyed, +// either by using the `.use` helper or explicitly calling its `.destroy` method. +// Failure to do so will leak the underlying Rust object. +TodoList().use { todo2 -> + setDefaultList(todo) + getDefaultList()!!.use { default -> + assert(todo.getEntries() == default.getEntries()) + assert(todo2.getEntries() != default.getEntries()) + } + + todo2.makeDefault() + getDefaultList()!!.use { default -> + assert(todo.getEntries() != default.getEntries()) + assert(todo2.getEntries() == default.getEntries()) + } + + todo.addItem("Test liveness after being demoted from default") + assert(todo.getLast() == "Test liveness after being demoted from default") + + todo2.addItem("Test shared state through local vs default reference") + getDefaultList()!!.use { default -> + assert(default.getLast() == "Test shared state through local vs default reference") + } +} + +// Ensure the kotlin version of deinit doesn't crash, and is idempotent. +todo.destroy() +todo.destroy() + diff --git a/toolkit/components/uniffi-fixtures/todolist/tests/bindings/test_todolist.py b/toolkit/components/uniffi-fixtures/todolist/tests/bindings/test_todolist.py new file mode 100644 index 0000000000..e4e2cda6d6 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/todolist/tests/bindings/test_todolist.py @@ -0,0 +1,56 @@ +from todolist import TodoEntry, TodoList, get_default_list, set_default_list + +todo = TodoList() + +entry = TodoEntry(text="Write bindings for strings in records") + +todo.add_item("Write python bindings") + +assert todo.get_last() == "Write python bindings" + +todo.add_item("Write tests for bindings") + +assert todo.get_last() == "Write tests for bindings" + +todo.add_entry(entry) + +assert todo.get_last() == "Write bindings for strings in records" +assert todo.get_last_entry().text == "Write bindings for strings in records" + +todo.add_item( + "Test Ünicode hàndling without an entry can't believe I didn't test this at first 🤣" +) +assert ( + todo.get_last() + == "Test Ünicode hàndling without an entry can't believe I didn't test this at first 🤣" +) + +entry2 = TodoEntry( + text="Test Ünicode hàndling in an entry can't believe I didn't test this at first 🤣" +) +todo.add_entry(entry2) +assert ( + todo.get_last_entry().text + == "Test Ünicode hàndling in an entry can't believe I didn't test this at first 🤣" +) + +todo2 = TodoList() +assert todo != todo2 +assert todo is not todo2 + +assert get_default_list() is None + +set_default_list(todo) +assert todo.get_items() == get_default_list().get_items() + +todo2.make_default() +assert todo2.get_items() == get_default_list().get_items() + +todo.add_item("Test liveness after being demoted from default") +assert todo.get_last() == "Test liveness after being demoted from default" + +todo2.add_item("Test shared state through local vs default reference") +assert ( + get_default_list().get_last() + == "Test shared state through local vs default reference" +) diff --git a/toolkit/components/uniffi-fixtures/todolist/tests/bindings/test_todolist.rb b/toolkit/components/uniffi-fixtures/todolist/tests/bindings/test_todolist.rb new file mode 100644 index 0000000000..fc1a823f52 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/todolist/tests/bindings/test_todolist.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'test/unit' +require 'todolist' + +include Test::Unit::Assertions +include Todolist + +todo = TodoList.new +entry = TodoEntry.new(text: 'Write bindings for strings in records') + +todo.add_item('Write ruby bindings') + +assert_equal todo.get_last, 'Write ruby bindings' + +todo.add_item('Write tests for bindings') + +assert_equal todo.get_last, 'Write tests for bindings' + +todo.add_entry(entry) + +assert_equal todo.get_last, 'Write bindings for strings in records' +assert_equal todo.get_last_entry.text, 'Write bindings for strings in records' + +todo.add_item("Test Ünicode hàndling without an entry can't believe I didn't test this at first 🤣") +assert_equal todo.get_last, "Test Ünicode hàndling without an entry can't believe I didn't test this at first 🤣" + +entry2 = TodoEntry.new(text: "Test Ünicode hàndling in an entry can't believe I didn't test this at first 🤣") +todo.add_entry(entry2) +assert_equal todo.get_last_entry.text, "Test Ünicode hàndling in an entry can't believe I didn't test this at first 🤣" + +todo2 = TodoList.new +assert todo2.get_items != todo.get_items + +assert Todolist.get_default_list == nil + +Todolist.set_default_list todo +assert todo.get_items == Todolist.get_default_list.get_items + +todo2.make_default +assert todo2.get_items == Todolist.get_default_list.get_items + +todo.add_item "Test liveness after being demoted from default" +assert todo.get_last == "Test liveness after being demoted from default" + +todo2.add_item "Test shared state through local vs default reference" +assert Todolist.get_default_list.get_last == "Test shared state through local vs default reference" diff --git a/toolkit/components/uniffi-fixtures/todolist/tests/bindings/test_todolist.swift b/toolkit/components/uniffi-fixtures/todolist/tests/bindings/test_todolist.swift new file mode 100644 index 0000000000..6ce72cadb2 --- /dev/null +++ b/toolkit/components/uniffi-fixtures/todolist/tests/bindings/test_todolist.swift @@ -0,0 +1,69 @@ +import todolist + + +let todo = TodoList() +do { + let _ = try todo.getLast() + fatalError("Should have thrown an EmptyTodoList error!") +} catch TodoError.EmptyTodoList{ + //It's okay! There are not todos! +} +try! todo.addItem(todo: "Write swift bindings") +assert( try! todo.getLast() == "Write swift bindings") + +try! todo.addItem(todo: "Write tests for bindings") +assert(try! todo.getLast() == "Write tests for bindings") + +let entry = TodoEntry(text: "Write bindings for strings as record members") +try! todo.addEntry(entry: entry) +assert(try! todo.getLast() == "Write bindings for strings as record members") + +try! todo.addItem(todo: "Test Ünicode hàndling without an entry can't believe I didn't test this at first 🤣") +assert(try! todo.getLast() == "Test Ünicode hàndling without an entry can't believe I didn't test this at first 🤣") + +do { + let _ = try createEntryWith(todo: "") + fatalError("Should have thrown an EmptyString error!") +} catch TodoError.EmptyString { + // It's okay! It was an empty string +} + +let entry2 = TodoEntry(text: "Test Ünicode hàndling in an entry can't believe I didn't test this at first 🤣") +try! todo.addEntry(entry: entry2) +assert(try! todo.getLastEntry() == entry2) + +assert(todo.getEntries().count == 5) + +todo.addEntries(entries: [TodoEntry(text: "foo"), TodoEntry(text: "bar")]) +assert(todo.getEntries().count == 7) +assert(todo.getItems().count == 7) +assert(try! todo.getLast() == "bar") + +todo.addItems(items: ["bobo", "fofo"]) +assert(todo.getItems().count == 9) +assert(todo.getItems()[7] == "bobo") + +// Ensure deinit doesn't crash. +for _ in 0..<10 { + let list = TodoList() + try! list.addItem(todo: "todo") +} + +let todo2 = TodoList() + +assert(getDefaultList() == nil) + +setDefaultList(list: todo) +assert(todo.getItems() == getDefaultList()!.getItems()) +assert(todo2.getItems() != getDefaultList()!.getItems()) + +todo2.makeDefault() +assert(todo.getItems() != getDefaultList()!.getItems()) +assert(todo2.getItems() == getDefaultList()!.getItems()) + +try! todo.addItem(todo: "Test liveness after being demoted from default") +assert(try! todo.getLast() == "Test liveness after being demoted from default") + +try! todo2.addItem(todo: "Test shared state through local vs default reference") +assert(try! getDefaultList()!.getLast() == "Test shared state through local vs default reference") + diff --git a/toolkit/components/uniffi-fixtures/todolist/tests/test_generated_bindings.rs b/toolkit/components/uniffi-fixtures/todolist/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..cefdbfe1dc --- /dev/null +++ b/toolkit/components/uniffi-fixtures/todolist/tests/test_generated_bindings.rs @@ -0,0 +1,6 @@ +uniffi::build_foreign_language_testcases!( + "tests/bindings/test_todolist.kts", + "tests/bindings/test_todolist.swift", + "tests/bindings/test_todolist.rb", + "tests/bindings/test_todolist.py" +); |