diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/uniffi | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
292 files changed, 24075 insertions, 0 deletions
diff --git a/third_party/rust/uniffi-example-arithmetic/.cargo-checksum.json b/third_party/rust/uniffi-example-arithmetic/.cargo-checksum.json new file mode 100644 index 0000000000..65c9cc95fa --- /dev/null +++ b/third_party/rust/uniffi-example-arithmetic/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"33bc6eceebec9d3aa6e97e1481ba74f2e1949d0b9a5edd8b978be548713ee338","build.rs":"937e2a5fdb7c0e850af07a4d7676e57f62f06b842cbc0aa7291ffe58b5725caa","src/arithmetic.udl":"8554c6907ece627645f6b896f71430e5412bf19b0ac6becf63eb9a69868d0f7a","src/lib.rs":"c454193443e92d49f997c760f4131192fb66bf213bbac1710c1ebde19e765e5d","tests/bindings/test_arithmetic.kts":"e0e9347755db4e18f70b1b74c2d5a4aa328373015090ed959b46d65c2a205d92","tests/bindings/test_arithmetic.py":"3e41d69e21e96a6830197c760f3b7bddd754edc0c5515b7bd33b79cccb10f941","tests/bindings/test_arithmetic.rb":"ea0fdce0a4c7b557b427db77521da05240cd6e87d60a128ac2307fab3bbbc76d","tests/bindings/test_arithmetic.swift":"455b87d95fc690af9c35f9e43676e9c855dedddd2fc1c9e1cbaa6a02835c2d4c","tests/test_generated_bindings.rs":"26b92d6b3e648f6fadd4182cbdba4f412b73da48a789785fd98cd486b29abf05","uniffi.toml":"ad149df611a6e3a853a029d90a88a694660f6a4ebe18dcb5f9f503819761dacd"},"package":null}
\ No newline at end of file diff --git a/third_party/rust/uniffi-example-arithmetic/Cargo.toml b/third_party/rust/uniffi-example-arithmetic/Cargo.toml new file mode 100644 index 0000000000..bac35f5d80 --- /dev/null +++ b/third_party/rust/uniffi-example-arithmetic/Cargo.toml @@ -0,0 +1,39 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi-example-arithmetic" +version = "0.22.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +publish = false +license = "MPL-2.0" + +[lib] +name = "arithmetical" +crate-type = [ + "lib", + "cdylib", +] + +[dependencies] +thiserror = "1.0" + +[dependencies.uniffi] +path = "../../uniffi" + +[dev-dependencies.uniffi] +path = "../../uniffi" +features = ["bindgen-tests"] + +[build-dependencies.uniffi] +path = "../../uniffi" +features = ["build"] diff --git a/third_party/rust/uniffi-example-arithmetic/build.rs b/third_party/rust/uniffi-example-arithmetic/build.rs new file mode 100644 index 0000000000..fcf5fe5b5c --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-arithmetic/src/arithmetic.udl b/third_party/rust/uniffi-example-arithmetic/src/arithmetic.udl new file mode 100644 index 0000000000..117df4834a --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-arithmetic/src/lib.rs b/third_party/rust/uniffi-example-arithmetic/src/lib.rs new file mode 100644 index 0000000000..92ab8c072b --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-arithmetic/tests/bindings/test_arithmetic.kts b/third_party/rust/uniffi-example-arithmetic/tests/bindings/test_arithmetic.kts new file mode 100644 index 0000000000..ef11850ae2 --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-arithmetic/tests/bindings/test_arithmetic.py b/third_party/rust/uniffi-example-arithmetic/tests/bindings/test_arithmetic.py new file mode 100644 index 0000000000..0d4e666fbf --- /dev/null +++ b/third_party/rust/uniffi-example-arithmetic/tests/bindings/test_arithmetic.py @@ -0,0 +1,37 @@ +from arithmetic import * + +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/third_party/rust/uniffi-example-arithmetic/tests/bindings/test_arithmetic.rb b/third_party/rust/uniffi-example-arithmetic/tests/bindings/test_arithmetic.rb new file mode 100644 index 0000000000..6669eb279f --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-arithmetic/tests/bindings/test_arithmetic.swift b/third_party/rust/uniffi-example-arithmetic/tests/bindings/test_arithmetic.swift new file mode 100644 index 0000000000..a8e34680e4 --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-arithmetic/tests/test_generated_bindings.rs b/third_party/rust/uniffi-example-arithmetic/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..168e6e1d4c --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-arithmetic/uniffi.toml b/third_party/rust/uniffi-example-arithmetic/uniffi.toml new file mode 100644 index 0000000000..a9fab4c0b4 --- /dev/null +++ b/third_party/rust/uniffi-example-arithmetic/uniffi.toml @@ -0,0 +1,12 @@ +[bindings.kotlin] +package_name = "org.mozilla.uniffi.example.arithmetic" +cdylib_name = "arithmetical" + +[bindings.python] +cdylib_name = "arithmetical" + +[bindings.ruby] +cdylib_name = "arithmetical" + +[bindings.swift] +cdylib_name = "arithmetical" diff --git a/third_party/rust/uniffi-example-geometry/.cargo-checksum.json b/third_party/rust/uniffi-example-geometry/.cargo-checksum.json new file mode 100644 index 0000000000..0354019788 --- /dev/null +++ b/third_party/rust/uniffi-example-geometry/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"ead86fd17caf11fb99bfac6eb5fb897d976226d018e32f33a2f55894815e6b34","build.rs":"8b13adbcc027872d444923e5b5d5b941d085a5de0f4a32ffa4f77f9a86ad78a0","src/geometry.udl":"7da7a6ec080c7117ec3c25206e23f9ed436e60b1a26fba34f991547680443550","src/lib.rs":"be9c624f691a8d1ebe3be6dbbcde44033759b8321a3a298453018dd69b9c14c7","tests/bindings/test_geometry.kts":"e537185e3c699df1c0468525700e8a38f9a504b2a663c38442146b951e38e9a7","tests/bindings/test_geometry.py":"3ea483b8a4fbe13aefa6641177ae149f75f734bc32bf0da533b97c1abf3dc317","tests/bindings/test_geometry.rb":"17c2fe8a7b477419a6646983dd88f1b07a0304b58a568c03e9bfa640d5b2df5c","tests/bindings/test_geometry.swift":"a61fec6bfe16020809e20e4da372748c24366767138c5672a0bfff85c4b62d78","tests/test_generated_bindings.rs":"ff8fc093ccb6ee3ee2235c09276c7bb87234ad143667429cb721e46379577f3d","uniffi.toml":"5b28f45d3c2581a52cf886a502f034778a002815b66994e5da2081a5c9f5284b"},"package":null}
\ No newline at end of file diff --git a/third_party/rust/uniffi-example-geometry/Cargo.toml b/third_party/rust/uniffi-example-geometry/Cargo.toml new file mode 100644 index 0000000000..5314bbcda3 --- /dev/null +++ b/third_party/rust/uniffi-example-geometry/Cargo.toml @@ -0,0 +1,36 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi-example-geometry" +version = "0.22.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +publish = false +license = "MPL-2.0" + +[lib] +name = "uniffi_geometry" +crate-type = [ + "lib", + "cdylib", +] + +[dependencies.uniffi] +path = "../../uniffi" + +[dev-dependencies.uniffi] +path = "../../uniffi" +features = ["bindgen-tests"] + +[build-dependencies.uniffi] +path = "../../uniffi" +features = ["build"] diff --git a/third_party/rust/uniffi-example-geometry/build.rs b/third_party/rust/uniffi-example-geometry/build.rs new file mode 100644 index 0000000000..86c91badd9 --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-geometry/src/geometry.udl b/third_party/rust/uniffi-example-geometry/src/geometry.udl new file mode 100644 index 0000000000..af60d429bf --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-geometry/src/lib.rs b/third_party/rust/uniffi-example-geometry/src/lib.rs new file mode 100644 index 0000000000..0e6add54f2 --- /dev/null +++ b/third_party/rust/uniffi-example-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, + }) +} + +include!(concat!(env!("OUT_DIR"), "/geometry.uniffi.rs")); diff --git a/third_party/rust/uniffi-example-geometry/tests/bindings/test_geometry.kts b/third_party/rust/uniffi-example-geometry/tests/bindings/test_geometry.kts new file mode 100644 index 0000000000..77bb9932ec --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-geometry/tests/bindings/test_geometry.py b/third_party/rust/uniffi-example-geometry/tests/bindings/test_geometry.py new file mode 100644 index 0000000000..a40e2af6ec --- /dev/null +++ b/third_party/rust/uniffi-example-geometry/tests/bindings/test_geometry.py @@ -0,0 +1,10 @@ +from geometry import * + +ln1 = Line(Point(0,0), Point(1,2)) +ln2 = Line(Point(1,1), Point(2,2)) + +assert gradient(ln1) == 2 +assert gradient(ln2) == 1 + +assert intersection(ln1, ln2) == Point(0, 0) +assert intersection(ln1, ln1) is None diff --git a/third_party/rust/uniffi-example-geometry/tests/bindings/test_geometry.rb b/third_party/rust/uniffi-example-geometry/tests/bindings/test_geometry.rb new file mode 100644 index 0000000000..8b1280d823 --- /dev/null +++ b/third_party/rust/uniffi-example-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(Point.new(0.0, 0.0), Point.new(1.0, 2.0)) +ln2 = Line.new(Point.new(1.0, 1.0), Point.new(2.0, 2.0)) + +assert_equal Geometry.gradient(ln1), 2 +assert_equal Geometry.gradient(ln2), 1 + +assert_equal Geometry.intersection(ln1, ln2), Point.new(0, 0) +assert Geometry.intersection(ln1, ln1).nil? diff --git a/third_party/rust/uniffi-example-geometry/tests/bindings/test_geometry.swift b/third_party/rust/uniffi-example-geometry/tests/bindings/test_geometry.swift new file mode 100644 index 0000000000..58bd65607f --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-geometry/tests/test_generated_bindings.rs b/third_party/rust/uniffi-example-geometry/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..4638d847c8 --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-geometry/uniffi.toml b/third_party/rust/uniffi-example-geometry/uniffi.toml new file mode 100644 index 0000000000..969379365a --- /dev/null +++ b/third_party/rust/uniffi-example-geometry/uniffi.toml @@ -0,0 +1 @@ +[bindings.swift] diff --git a/third_party/rust/uniffi-example-rondpoint/.cargo-checksum.json b/third_party/rust/uniffi-example-rondpoint/.cargo-checksum.json new file mode 100644 index 0000000000..89fd7e5d7b --- /dev/null +++ b/third_party/rust/uniffi-example-rondpoint/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"61b717bb1057108afdc32cb57de6efbd44fe839a8b9137ef1eefca2c3ac31ea8","build.rs":"8fcc699e02a7a7a163c03784a0d78f527ca34ab79a1fe583a01c1b320d8669ce","src/lib.rs":"b5eb9861fc436ab9fe9b0c00f95e374d812bbc0ea10b98cf746d38c96e44343a","src/rondpoint.udl":"ca4d8720758608b06ffd5f81bfc73881fbd0693a7a5b21bfe61a4557ea052048","tests/bindings/test_rondpoint.kts":"4aac8353278807f4add95c81f4c6c61187204b9767f882fd64872ed8ac1f6451","tests/bindings/test_rondpoint.py":"d618274170af767f8a5614a2565ea698b26ea3e1a222d5c110e7b2d00763e73b","tests/bindings/test_rondpoint.rb":"9cc49df311823d6caedbe7b05ff8c4da6329063c2ce16810192aaaa7edcdf5f5","tests/bindings/test_rondpoint.swift":"fa806e7e09c22ed44496658f6e0781765447bbdd250d7adf4b1152248ed70e69","tests/test_generated_bindings.rs":"5464f89e91c458f164b83a454c6df67a2953873e8a785a4720a2253d843f88e5"},"package":null}
\ No newline at end of file diff --git a/third_party/rust/uniffi-example-rondpoint/Cargo.toml b/third_party/rust/uniffi-example-rondpoint/Cargo.toml new file mode 100644 index 0000000000..c4efbe5b8f --- /dev/null +++ b/third_party/rust/uniffi-example-rondpoint/Cargo.toml @@ -0,0 +1,36 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi-example-rondpoint" +version = "0.22.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +publish = false +license = "MPL-2.0" + +[lib] +name = "uniffi_rondpoint" +crate-type = [ + "lib", + "cdylib", +] + +[dependencies.uniffi] +path = "../../uniffi" + +[dev-dependencies.uniffi] +path = "../../uniffi" +features = ["bindgen-tests"] + +[build-dependencies.uniffi] +path = "../../uniffi" +features = ["build"] diff --git a/third_party/rust/uniffi-example-rondpoint/build.rs b/third_party/rust/uniffi-example-rondpoint/build.rs new file mode 100644 index 0000000000..529881fefc --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-rondpoint/src/lib.rs b/third_party/rust/uniffi-example-rondpoint/src/lib.rs new file mode 100644 index 0000000000..b5b78fb767 --- /dev/null +++ b/third_party/rust/uniffi-example-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>, +} + +include!(concat!(env!("OUT_DIR"), "/rondpoint.uniffi.rs")); diff --git a/third_party/rust/uniffi-example-rondpoint/src/rondpoint.udl b/third_party/rust/uniffi-example-rondpoint/src/rondpoint.udl new file mode 100644 index 0000000000..05b1bfe825 --- /dev/null +++ b/third_party/rust/uniffi-example-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<DOMString, EnumerationAvecDonnees> copie_carte(record<DOMString, 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/third_party/rust/uniffi-example-rondpoint/tests/bindings/test_rondpoint.kts b/third_party/rust/uniffi-example-rondpoint/tests/bindings/test_rondpoint.kts new file mode 100644 index 0000000000..cc5ddf2a86 --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-rondpoint/tests/bindings/test_rondpoint.py b/third_party/rust/uniffi-example-rondpoint/tests/bindings/test_rondpoint.py new file mode 100644 index 0000000000..ecfcc1e527 --- /dev/null +++ b/third_party/rust/uniffi-example-rondpoint/tests/bindings/test_rondpoint.py @@ -0,0 +1,146 @@ +import sys +import ctypes +from rondpoint import * + +dico = Dictionnaire(Enumeration.DEUX, True, 0, 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/third_party/rust/uniffi-example-rondpoint/tests/bindings/test_rondpoint.rb b/third_party/rust/uniffi-example-rondpoint/tests/bindings/test_rondpoint.rb new file mode 100644 index 0000000000..0121f6e0f9 --- /dev/null +++ b/third_party/rust/uniffi-example-rondpoint/tests/bindings/test_rondpoint.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +require 'test/unit' +require 'rondpoint' + +include Test::Unit::Assertions +include Rondpoint + +dico = Dictionnaire.new Enumeration::DEUX, true, 0, 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/third_party/rust/uniffi-example-rondpoint/tests/bindings/test_rondpoint.swift b/third_party/rust/uniffi-example-rondpoint/tests/bindings/test_rondpoint.swift new file mode 100644 index 0000000000..d9f47058ed --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-rondpoint/tests/test_generated_bindings.rs b/third_party/rust/uniffi-example-rondpoint/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..d337374334 --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-sprites/.cargo-checksum.json b/third_party/rust/uniffi-example-sprites/.cargo-checksum.json new file mode 100644 index 0000000000..fac6b3ddfc --- /dev/null +++ b/third_party/rust/uniffi-example-sprites/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"78f11927c8b2562a9975726e0ac20b51d124b6a21fdd633a9c0b1f0b69c5c788","build.rs":"84ba28d15fc0987d8efb5d523a643717c6b4b9b29570c0cc9cff81126ecfce7f","src/lib.rs":"857ae59f3194cb7fe117a308854bc793b2b56441e297bd0e327795fd435494da","src/sprites.udl":"bfd35f04ba0549301189dfb8fc45b0f39bad00956c324f33be0e845fb7ff78aa","tests/bindings/test_sprites.kts":"06ed115325f37ce59ed6f33e2d651cd2aa352fddcc644580f62a6da6ca075844","tests/bindings/test_sprites.py":"2e6ce838cfb387586257703c3500062438e840dd7ae57d185cdc244dc0745b8f","tests/bindings/test_sprites.rb":"6289a1833c7c8f4583ee4f0488d680de2ee46cfb203095a9b66d7234e2f07d53","tests/bindings/test_sprites.swift":"b2c0a6f4d5edfd7de7c2ba77b838865ffda153a6f364f273456175192d3e6e00","tests/test_generated_bindings.rs":"9a22d693c97fc6d90031cc60f61ece1d9279165ad6a92c9fe937448e126e8de6"},"package":null}
\ No newline at end of file diff --git a/third_party/rust/uniffi-example-sprites/Cargo.toml b/third_party/rust/uniffi-example-sprites/Cargo.toml new file mode 100644 index 0000000000..6c55e9d88c --- /dev/null +++ b/third_party/rust/uniffi-example-sprites/Cargo.toml @@ -0,0 +1,36 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi-example-sprites" +version = "0.22.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +publish = false +license = "MPL-2.0" + +[lib] +name = "uniffi_sprites" +crate-type = [ + "lib", + "cdylib", +] + +[dependencies.uniffi] +path = "../../uniffi" + +[dev-dependencies.uniffi] +path = "../../uniffi" +features = ["bindgen-tests"] + +[build-dependencies.uniffi] +path = "../../uniffi" +features = ["build"] diff --git a/third_party/rust/uniffi-example-sprites/build.rs b/third_party/rust/uniffi-example-sprites/build.rs new file mode 100644 index 0000000000..858b8d1615 --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-sprites/src/lib.rs b/third_party/rust/uniffi-example-sprites/src/lib.rs new file mode 100644 index 0000000000..7aa3741d19 --- /dev/null +++ b/third_party/rust/uniffi-example-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) + } +} + +include!(concat!(env!("OUT_DIR"), "/sprites.uniffi.rs")); diff --git a/third_party/rust/uniffi-example-sprites/src/sprites.udl b/third_party/rust/uniffi-example-sprites/src/sprites.udl new file mode 100644 index 0000000000..6781c6cee5 --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-sprites/tests/bindings/test_sprites.kts b/third_party/rust/uniffi-example-sprites/tests/bindings/test_sprites.kts new file mode 100644 index 0000000000..42451f28dd --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-sprites/tests/bindings/test_sprites.py b/third_party/rust/uniffi-example-sprites/tests/bindings/test_sprites.py new file mode 100644 index 0000000000..5142c2fc42 --- /dev/null +++ b/third_party/rust/uniffi-example-sprites/tests/bindings/test_sprites.py @@ -0,0 +1,17 @@ +from sprites import * + +sempty = Sprite(None) +assert sempty.get_position() == Point(0, 0) + +s = Sprite(Point(0, 1)) +assert s.get_position() == Point(0, 1) + +s.move_to(Point(1, 2)) +assert s.get_position() == Point(1, 2) + +s.move_by(Vector(-4, 2)) +assert s.get_position() == Point(-3, 4) + +srel = Sprite.new_relative_to(Point(0, 1), Vector(1, 1.5)) +assert srel.get_position() == Point(1, 2.5) + diff --git a/third_party/rust/uniffi-example-sprites/tests/bindings/test_sprites.rb b/third_party/rust/uniffi-example-sprites/tests/bindings/test_sprites.rb new file mode 100644 index 0000000000..9d79b57026 --- /dev/null +++ b/third_party/rust/uniffi-example-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(0, 0) + +s = Sprite.new(Point.new(0, 1)) +assert_equal s.get_position, Point.new(0, 1) + +s.move_to(Point.new(1, 2)) +assert_equal s.get_position, Point.new(1, 2) + +s.move_by(Vector.new(-4, 2)) +assert_equal s.get_position, Point.new(-3, 4) + +srel = Sprite.new_relative_to(Point.new(0, 1), Vector.new(1, 1.5)) +assert_equal srel.get_position, Point.new(1, 2.5) diff --git a/third_party/rust/uniffi-example-sprites/tests/bindings/test_sprites.swift b/third_party/rust/uniffi-example-sprites/tests/bindings/test_sprites.swift new file mode 100644 index 0000000000..d5428ac679 --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-sprites/tests/test_generated_bindings.rs b/third_party/rust/uniffi-example-sprites/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..00dd779d68 --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-todolist/.cargo-checksum.json b/third_party/rust/uniffi-example-todolist/.cargo-checksum.json new file mode 100644 index 0000000000..0f7516a258 --- /dev/null +++ b/third_party/rust/uniffi-example-todolist/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"2cadeccd37f54f3fe48ee85eeed4acdf6e7299acb6b43ed026d16695150cbab7","build.rs":"696168e6dd8a1f181748cb36161bee14a7523b9e5d9b2e3938f02cf24d374e45","src/lib.rs":"338494783dbbcd8abbc7a1b5206f52296064091faa0a04e2d622a0d370ad12fe","src/todolist.udl":"1f8a24049c2340b9184e95facfc191ecdcb91541729ae7f20b4625d67685f13c","tests/bindings/test_todolist.kts":"f3d29b48e0193563fc4f131d91ea697f758174dcdb80ea554f233949e575bf55","tests/bindings/test_todolist.py":"f7430af9347df0daa954d38bc2203ce400affbb9a53fced4bb67a6796afa0664","tests/bindings/test_todolist.rb":"6524b5271a9cc0e2d78ca9f86ccb6973889926688a0843b4505a4f62d48f6dcb","tests/bindings/test_todolist.swift":"d1911b85fe0c8c0b42e5421b5af5d7359c9a65bba477d23560eb4b0f52e80662","tests/test_generated_bindings.rs":"46ef1fbedaac0c4867812ef2632a641eab36ab0ee12f5757567dd037aeddcbd3"},"package":null}
\ No newline at end of file diff --git a/third_party/rust/uniffi-example-todolist/Cargo.toml b/third_party/rust/uniffi-example-todolist/Cargo.toml new file mode 100644 index 0000000000..a6f4895972 --- /dev/null +++ b/third_party/rust/uniffi-example-todolist/Cargo.toml @@ -0,0 +1,40 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi-example-todolist" +version = "0.22.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +publish = false +license = "MPL-2.0" + +[lib] +name = "uniffi_todolist" +crate-type = [ + "lib", + "cdylib", +] + +[dependencies] +once_cell = "1.12" +thiserror = "1.0" + +[dependencies.uniffi] +path = "../../uniffi" + +[dev-dependencies.uniffi] +path = "../../uniffi" +features = ["bindgen-tests"] + +[build-dependencies.uniffi] +path = "../../uniffi" +features = ["build"] diff --git a/third_party/rust/uniffi-example-todolist/build.rs b/third_party/rust/uniffi-example-todolist/build.rs new file mode 100644 index 0000000000..7376ae1a20 --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-todolist/src/lib.rs b/third_party/rust/uniffi-example-todolist/src/lib.rs new file mode 100644 index 0000000000..fb2900e615 --- /dev/null +++ b/third_party/rust/uniffi-example-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); + } +} + +include!(concat!(env!("OUT_DIR"), "/todolist.uniffi.rs")); diff --git a/third_party/rust/uniffi-example-todolist/src/todolist.udl b/third_party/rust/uniffi-example-todolist/src/todolist.udl new file mode 100644 index 0000000000..5c923314cd --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-todolist/tests/bindings/test_todolist.kts b/third_party/rust/uniffi-example-todolist/tests/bindings/test_todolist.kts new file mode 100644 index 0000000000..bb2b292224 --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-todolist/tests/bindings/test_todolist.py b/third_party/rust/uniffi-example-todolist/tests/bindings/test_todolist.py new file mode 100644 index 0000000000..017e999fb2 --- /dev/null +++ b/third_party/rust/uniffi-example-todolist/tests/bindings/test_todolist.py @@ -0,0 +1,44 @@ +from todolist import * + +todo = TodoList() + +entry = TodoEntry("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("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/third_party/rust/uniffi-example-todolist/tests/bindings/test_todolist.rb b/third_party/rust/uniffi-example-todolist/tests/bindings/test_todolist.rb new file mode 100644 index 0000000000..d9e04f92e7 --- /dev/null +++ b/third_party/rust/uniffi-example-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 '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("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"
\ No newline at end of file diff --git a/third_party/rust/uniffi-example-todolist/tests/bindings/test_todolist.swift b/third_party/rust/uniffi-example-todolist/tests/bindings/test_todolist.swift new file mode 100644 index 0000000000..6ce72cadb2 --- /dev/null +++ b/third_party/rust/uniffi-example-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/third_party/rust/uniffi-example-todolist/tests/test_generated_bindings.rs b/third_party/rust/uniffi-example-todolist/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..cefdbfe1dc --- /dev/null +++ b/third_party/rust/uniffi-example-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" +); diff --git a/third_party/rust/uniffi/.cargo-checksum.json b/third_party/rust/uniffi/.cargo-checksum.json new file mode 100644 index 0000000000..5b1e3bd47e --- /dev/null +++ b/third_party/rust/uniffi/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"aa6288a6de59c4f25216578c7b27a51fbe98fb41c22c88f561dd171dfd905bd6","release.toml":"183ddd766a32826c448f33db7420abf2332b327f563e5de771c9e1733ef93e7a","src/cli.rs":"d8c8ebf37db05b31c33190de80923fde91c96c520a09c62ff6754b68a26dcf08","src/lib.rs":"08c447841ed496727112ccc4f6469bf00c50109575281c3b64f5dced65bbc119","tests/ui/proc_macro_arc.rs":"d766dffee3fe6a93522d40f44a7f15592db141fd674034fa5f016e06f510e87b","tests/ui/proc_macro_arc.stderr":"32096490256b658d42b8187547d07473439329e62676aa9b1ec6dbeef401ad7a","tests/ui/version_mismatch.rs":"16ea359e5853517ee0d0704c015ae8c825533109fbefd715130d0f4a51f15898","tests/ui/version_mismatch.stderr":"21dcb836253312ba8e3a0502cce6ff279818aaaadcea9628a41b196e0c8c94b6"},"package":"f71cc01459bc34cfe43fabf32b39f1228709bc6db1b3a664a92940af3d062376"}
\ No newline at end of file diff --git a/third_party/rust/uniffi/Cargo.toml b/third_party/rust/uniffi/Cargo.toml new file mode 100644 index 0000000000..ddaa6c536c --- /dev/null +++ b/third_party/rust/uniffi/Cargo.toml @@ -0,0 +1,68 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi" +version = "0.23.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +description = "a multi-language bindings generator for rust" +homepage = "https://mozilla.github.io/uniffi-rs" +documentation = "https://mozilla.github.io/uniffi-rs" +keywords = [ + "ffi", + "bindgen", +] +license = "MPL-2.0" +repository = "https://github.com/mozilla/uniffi-rs" + +[dependencies.anyhow] +version = "1" + +[dependencies.camino] +version = "1.0.8" +optional = true + +[dependencies.clap] +version = "3.1" +features = [ + "cargo", + "std", + "derive", +] +optional = true + +[dependencies.uniffi_bindgen] +version = "=0.23.0" +optional = true + +[dependencies.uniffi_build] +version = "=0.23.0" +optional = true + +[dependencies.uniffi_core] +version = "=0.23.0" + +[dependencies.uniffi_macros] +version = "=0.23.0" + +[dev-dependencies.trybuild] +version = "1" + +[features] +bindgen-tests = ["dep:uniffi_bindgen"] +build = ["dep:uniffi_build"] +cli = [ + "dep:uniffi_bindgen", + "dep:clap", + "dep:camino", +] +default = [] diff --git a/third_party/rust/uniffi/release.toml b/third_party/rust/uniffi/release.toml new file mode 100644 index 0000000000..3decae3abf --- /dev/null +++ b/third_party/rust/uniffi/release.toml @@ -0,0 +1,14 @@ +# Note that this `release.toml` exists to capture things that must only be +# done once for `cargo release-uniffi`. +# +# [../uniffi_core/release.toml](../uniffi_core/release.toml) captures things that must only be done for `cargo release-backend-crates` +# +# All other config exists in [../release.toml](../release.toml). + +tag = true + +# This is how we manage the sections in CHANGELOG.md +pre-release-replacements = [ + {file="../CHANGELOG.md", search="\\[\\[UnreleasedUniFFIVersion\\]\\]", replace="v{{version}}", exactly=2}, + {file="../CHANGELOG.md", search="\\[\\[NextUnreleasedUniFFIVersion\\]\\]", replace="[[UnreleasedUniFFIVersion]]", exactly=1}, +] diff --git a/third_party/rust/uniffi/src/cli.rs b/third_party/rust/uniffi/src/cli.rs new file mode 100644 index 0000000000..705ea9f453 --- /dev/null +++ b/third_party/rust/uniffi/src/cli.rs @@ -0,0 +1,106 @@ +/* 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 camino::Utf8PathBuf; +use clap::{Parser, Subcommand}; + +// Structs to help our cmdline parsing. Note that docstrings below form part +// of the "help" output. + +/// Scaffolding and bindings generator for Rust +#[derive(Parser)] +#[clap(name = "uniffi-bindgen")] +#[clap(version = clap::crate_version!())] +#[clap(propagate_version = true)] +struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Generate foreign language bindings + Generate { + /// Foreign language(s) for which to build bindings. + #[clap(long, short, possible_values = &["kotlin", "python", "swift", "ruby"])] + language: Vec<String>, + + /// Directory in which to write generated files. Default is same folder as .udl file. + #[clap(long, short)] + out_dir: Option<Utf8PathBuf>, + + /// Do not try to format the generated bindings. + #[clap(long, short)] + no_format: bool, + + /// Path to the optional uniffi config file. If not provided, uniffi-bindgen will try to guess it from the UDL's file location. + #[clap(long, short)] + config: Option<Utf8PathBuf>, + + /// Extract proc-macro metadata from a native lib (cdylib or staticlib) for this crate. + #[clap(long)] + lib_file: Option<Utf8PathBuf>, + + /// Path to the UDL file. + udl_file: Utf8PathBuf, + }, + + /// Generate Rust scaffolding code + Scaffolding { + /// Directory in which to write generated files. Default is same folder as .udl file. + #[clap(long, short)] + out_dir: Option<Utf8PathBuf>, + + /// Path to the optional uniffi config file. If not provided, uniffi-bindgen will try to guess it from the UDL's file location. + #[clap(long, short)] + config: Option<Utf8PathBuf>, + + /// Do not try to format the generated bindings. + #[clap(long, short)] + no_format: bool, + + /// Path to the UDL file. + udl_file: Utf8PathBuf, + }, + + /// Print the JSON representation of the interface from a dynamic library + PrintJson { + /// Path to the library file (.so, .dll, .dylib, or .a) + path: Utf8PathBuf, + }, +} + +pub fn run_main() -> anyhow::Result<()> { + let cli = Cli::parse(); + match &cli.command { + Commands::Generate { + language, + out_dir, + no_format, + config, + lib_file, + udl_file, + } => uniffi_bindgen::generate_bindings( + udl_file, + config.as_deref(), + language.iter().map(String::as_str).collect(), + out_dir.as_deref(), + lib_file.as_deref(), + !no_format, + ), + Commands::Scaffolding { + out_dir, + config, + no_format, + udl_file, + } => uniffi_bindgen::generate_component_scaffolding( + udl_file, + config.as_deref(), + out_dir.as_deref(), + !no_format, + ), + Commands::PrintJson { path } => uniffi_bindgen::print_json(path), + }?; + Ok(()) +} diff --git a/third_party/rust/uniffi/src/lib.rs b/third_party/rust/uniffi/src/lib.rs new file mode 100644 index 0000000000..bb308c55f2 --- /dev/null +++ b/third_party/rust/uniffi/src/lib.rs @@ -0,0 +1,37 @@ +/* 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/. */ + +/// Reexport items from other uniffi creates +pub use uniffi_core::*; +pub use uniffi_macros::{export, include_scaffolding, Enum, Error, Object, Record}; +#[cfg(feature = "cli")] +mod cli; +#[cfg(feature = "bindgen-tests")] +pub use uniffi_bindgen::bindings::kotlin::run_test as kotlin_run_test; +#[cfg(feature = "bindgen-tests")] +pub use uniffi_bindgen::bindings::python::run_test as python_run_test; +#[cfg(feature = "bindgen-tests")] +pub use uniffi_bindgen::bindings::ruby::run_test as ruby_run_test; +#[cfg(feature = "bindgen-tests")] +pub use uniffi_bindgen::bindings::swift::run_test as swift_run_test; +#[cfg(feature = "cli")] +pub use uniffi_bindgen::{generate_bindings, generate_component_scaffolding, print_json}; +#[cfg(feature = "build")] +pub use uniffi_build::generate_scaffolding; +#[cfg(feature = "bindgen-tests")] +pub use uniffi_macros::build_foreign_language_testcases; + +#[cfg(test)] +mod test { + #[test] + fn trybuild_ui_tests() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/*.rs"); + } +} + +#[cfg(feature = "cli")] +pub fn uniffi_bindgen_main() { + cli::run_main().unwrap(); +} diff --git a/third_party/rust/uniffi/tests/ui/proc_macro_arc.rs b/third_party/rust/uniffi/tests/ui/proc_macro_arc.rs new file mode 100644 index 0000000000..bdffc020e0 --- /dev/null +++ b/third_party/rust/uniffi/tests/ui/proc_macro_arc.rs @@ -0,0 +1,25 @@ +use std::sync::Arc; + +fn main() {} + +pub struct Foo; + +#[uniffi::export] +fn make_foo() -> Arc<Foo> { + Arc::new(Foo) +} + +mod child { + use std::sync::Arc; + + enum Foo {} + + #[uniffi::export] + fn take_foo(foo: Arc<Foo>) { + match &*foo {} + } +} + +mod uniffi_types { + pub use super::Foo; +} diff --git a/third_party/rust/uniffi/tests/ui/proc_macro_arc.stderr b/third_party/rust/uniffi/tests/ui/proc_macro_arc.stderr new file mode 100644 index 0000000000..9e89f7bccf --- /dev/null +++ b/third_party/rust/uniffi/tests/ui/proc_macro_arc.stderr @@ -0,0 +1,31 @@ +error[E0271]: type mismatch resolving `<Arc<child::Foo> as child::_::_::{closure#0}::TypeEq>::This == Arc<Foo>` + --> tests/ui/proc_macro_arc.rs:18:22 + | +18 | fn take_foo(foo: Arc<Foo>) { + | ^^^^^^^^ type mismatch resolving `<Arc<child::Foo> as child::_::_::{closure#0}::TypeEq>::This == Arc<Foo>` + | +note: expected this to be `Arc<Foo>` + --> tests/ui/proc_macro_arc.rs:18:22 + | +18 | fn take_foo(foo: Arc<Foo>) { + | ^^^ + = note: enum `child::Foo` and struct `Foo` have similar names, but are actually distinct types +note: enum `child::Foo` is defined in module `crate::child` of the current crate + --> tests/ui/proc_macro_arc.rs:15:5 + | +15 | enum Foo {} + | ^^^^^^^^ +note: struct `Foo` is defined in module `crate` of the current crate + --> tests/ui/proc_macro_arc.rs:5:1 + | +5 | pub struct Foo; + | ^^^^^^^^^^^^^^ +note: required by a bound in `child::_::_::{closure#0}::assert_type_eq_all` + --> tests/ui/proc_macro_arc.rs:18:22 + | +18 | fn take_foo(foo: Arc<Foo>) { + | ^^^ + | | + | required by a bound in this + | required by this bound in `child::_::_::{closure#0}::assert_type_eq_all` + = note: this error originates in the macro `::uniffi::deps::static_assertions::assert_type_eq_all` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/third_party/rust/uniffi/tests/ui/version_mismatch.rs b/third_party/rust/uniffi/tests/ui/version_mismatch.rs new file mode 100644 index 0000000000..6a7edb891d --- /dev/null +++ b/third_party/rust/uniffi/tests/ui/version_mismatch.rs @@ -0,0 +1,4 @@ +// This should fail with a version mismatch. +uniffi::assert_compatible_version!("0.0.1"); // An error message would go here. + +fn main() {} diff --git a/third_party/rust/uniffi/tests/ui/version_mismatch.stderr b/third_party/rust/uniffi/tests/ui/version_mismatch.stderr new file mode 100644 index 0000000000..bc30714099 --- /dev/null +++ b/third_party/rust/uniffi/tests/ui/version_mismatch.stderr @@ -0,0 +1,7 @@ +error[E0080]: evaluation of constant value failed + --> tests/ui/version_mismatch.rs:2:1 + | +2 | uniffi::assert_compatible_version!("0.0.1"); // An error message would go here. + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ attempt to compute `0_usize - 1_usize`, which would overflow + | + = note: this error originates in the macro `uniffi::deps::static_assertions::const_assert` which comes from the expansion of the macro `uniffi::assert_compatible_version` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/third_party/rust/uniffi_bindgen/.cargo-checksum.json b/third_party/rust/uniffi_bindgen/.cargo-checksum.json new file mode 100644 index 0000000000..2032b8dcaa --- /dev/null +++ b/third_party/rust/uniffi_bindgen/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"5bba6c7346e8263e0dac4e8a346cc96c89ed14d0e329ebb993e555ad3008e11c","askama.toml":"1a245b7803adca782837e125c49100147d2de0d5a1c949ff95e91af1701f6058","src/backend/config.rs":"4861dbf251dbb10beb1ed7e3eea7d79499a0de1cd9ce9ee8381a0e729c097dea","src/backend/declarations.rs":"12b8d6e651f84634de5cd02a47601965df7438f64f1a73f136bd89b6b5d515cf","src/backend/mod.rs":"899cd3b816d0467e35789b92ac3b8d5910f6dab98156d405db4803da8721fd36","src/backend/oracle.rs":"fefe7c77fe807cd2b6cc7e27e9d33c02a6f055a0168487fe98a9e53c979fab7c","src/backend/types.rs":"16a99765b562becb47a8057897b5de810d432690c8db7f26ba646aff1f07e532","src/bindings/kotlin/gen_kotlin/callback_interface.rs":"b7fe795670830f3aa8a955c787b1127fe68313ee751013948527948fe5526b01","src/bindings/kotlin/gen_kotlin/compounds.rs":"d1e9a4237ff2ff711a3eae7a564c39e26f598c156ebfd34c0f04879e3533dd4f","src/bindings/kotlin/gen_kotlin/custom.rs":"4176f6ed5f66504f8fd981198bbfbae795dab5ef0d0281881d19b697f5560c44","src/bindings/kotlin/gen_kotlin/enum_.rs":"f85ae8dcb55c8f274139bf321af0ba237ae69894165ad6bd693d793f58af8e5e","src/bindings/kotlin/gen_kotlin/error.rs":"867f583aea5da7aabeb9b6d2544d7e52984cdea4aa008ce5f2ec941074735e1a","src/bindings/kotlin/gen_kotlin/external.rs":"1f7e91d7439891fe3c403274e35880ee4fc3a0da555510bdfa23c1ed2bbd8020","src/bindings/kotlin/gen_kotlin/miscellany.rs":"644ee5bb1f3619be5a36b2b3900af554ea38073cd054004f421e69c3cb8d50bc","src/bindings/kotlin/gen_kotlin/mod.rs":"01b50b6c6282023b3160be2475700b0e8a4065ed3a93e01159eda5846fd2cd1d","src/bindings/kotlin/gen_kotlin/object.rs":"6478a3e9d5e66186521730d0d481abd1ee4f123050ea050ac5a483842f08b003","src/bindings/kotlin/gen_kotlin/primitives.rs":"9b5114201f97b1e400825aef47b33ea9718ee89b161702093142f73ddf42b801","src/bindings/kotlin/gen_kotlin/record.rs":"7961fcfbec5ebf8fc010b564ea4bd59402c919f6922898d48226d8c995569dd7","src/bindings/kotlin/mod.rs":"12e18af4dad5ecc2b5a9fbe6b185452ad874c707f8efcc989afad1df4e4e0502","src/bindings/kotlin/templates/BooleanHelper.kt":"28e8a5088c8d58c9bfdbc575af8d8725060521fdd7d092684a8044b24ae567c7","src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt":"6ede374b0fcbb3bcc939894e6f4729b3bec7ec7356831a60fba96ca38dc91aa8","src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt":"f2f767a256dcb2cb62b5809792f7335e014c10b6a25351028a2845dac2387bb5","src/bindings/kotlin/templates/CustomTypeTemplate.kt":"5d4dacf29e89bcdcc46d155d993e6059d2df704e775dc853469310198253b231","src/bindings/kotlin/templates/DurationHelper.kt":"414a98161538a26f3a9b357353270c1f245ad6ceed99496aca7162cf473a92fd","src/bindings/kotlin/templates/EnumTemplate.kt":"4cacb93506f46f09e80e695f16da3d706d52a6e3c0b282158e2c466cc7b10962","src/bindings/kotlin/templates/ErrorTemplate.kt":"a5ec2bdfc026838e1096dbf3301f21aa4ea22e8c93458d45bb1c8c7b9ee1fc5b","src/bindings/kotlin/templates/ExternalTypeTemplate.kt":"155df3cc8fce46ef43948da9a9062eb6516f4966d4969e278df8ed497cc6d657","src/bindings/kotlin/templates/FfiConverterTemplate.kt":"aa22962aaa9f641d48ccf44cb56d9f8a7736cbfaa01e1a1656662cfe5dd5c1d7","src/bindings/kotlin/templates/Float32Helper.kt":"662d95af3b629d143fb4d47cb7e9aa26ed28a5f3846de0341e28b0f9fb08bc25","src/bindings/kotlin/templates/Float64Helper.kt":"a77d099fa7d91e8702c1700e7949ffb6aaba9c6e8041ff48bab34b8e1fc9a0aa","src/bindings/kotlin/templates/Helpers.kt":"46c07798a26b53b06405c8bbbf86e3fcf38fadc1484ea04ce9d482defea89288","src/bindings/kotlin/templates/Int16Helper.kt":"7f83c4a48e1f3b2a59a3ca6a2662be8bc9baf3a5a748b31223cb3f51721ef249","src/bindings/kotlin/templates/Int32Helper.kt":"e02e4702175554b09fd2dd6ac3089dcd2c395f08ec60e762159566a9c9889450","src/bindings/kotlin/templates/Int64Helper.kt":"7a6fd6ca486852c89399c699935a9dfa1c32b9356d9a965cfde532581f05d9fa","src/bindings/kotlin/templates/Int8Helper.kt":"0554545494b6b9a76ce94f9c1723f8cf4230a13076feb75d620b1c9ca1ac4668","src/bindings/kotlin/templates/MapTemplate.kt":"399569d6443e8ad01e2deb95d78d8d2d15bf8eccee8be4fbe9ce4b8ebc0a6101","src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt":"1eba4e77381155c2c96a6af2ef30fca881ade4957852f730fd10aa4f3d9cd3c4","src/bindings/kotlin/templates/ObjectRuntime.kt":"7f38f54a0c889d7534d23afdace6b87b6ced5c024a36b5450078a06e071caad8","src/bindings/kotlin/templates/ObjectTemplate.kt":"7e32d92ec6116da9b7d7f278b15333a2fd2a82dde8c21534e3f6fe371d19f333","src/bindings/kotlin/templates/OptionalTemplate.kt":"5f9f2c1baa829ed3c9b61c3edb0f1fccf5ea3cccc052a69cf8966715d8fcf149","src/bindings/kotlin/templates/RecordTemplate.kt":"193ecdad9322fb5483b95bf2a259270a9b22ba054790794e9abb3fd219196bc5","src/bindings/kotlin/templates/RustBufferTemplate.kt":"415637f80a78c12b3d00db063c14a7ab5c61b098bdb1fc81a0be8bae9511776b","src/bindings/kotlin/templates/SequenceTemplate.kt":"786693b20c608a4f059b91df115278f5f12122b4c14a2e7ce18b6fc9b22b1296","src/bindings/kotlin/templates/StringHelper.kt":"060839663580d8199671b7c3bb3dce5e9106aa766ce2c6e0afc2d2bd788a1d83","src/bindings/kotlin/templates/TimestampHelper.kt":"353c2890f06ad6dda238e9aebb4bdff7bb838e17e46abf351ed3ff1fbc4e6580","src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt":"09b8bb5ffea7075518ee657f83017d44ff0cbf584fd85676b082199139c9be09","src/bindings/kotlin/templates/Types.kt":"1041a3ecfeb3be10423fe01f351fc8e1e5cf6e860ee5610596537905e5130f5a","src/bindings/kotlin/templates/UInt16Helper.kt":"e84a1f30a5a899ba2c5db614d3f3c74f25bccf6dd99bf68b8830829332d051e9","src/bindings/kotlin/templates/UInt32Helper.kt":"7cdf08cc580046935f27ba07b53685968608a102e0a6be305111037c63d7ddf8","src/bindings/kotlin/templates/UInt64Helper.kt":"fd7baacbf3ab6202ff83edcc66e5f7beb11a10053ba66d0b49547616cc7cbe1f","src/bindings/kotlin/templates/UInt8Helper.kt":"bbf5a6d66c995aea9fe2fa9840c6bfa78b03520a09469b984f0e1d43191e453a","src/bindings/kotlin/templates/macros.kt":"53c6a41afc03d1501bf5301e317994c314ee492827e93f3d39c7a1e49acc9660","src/bindings/kotlin/templates/wrapper.kt":"03e0e56b49036b144acc7044e9ab94679a6a76c3590f64df47f6eb9b589692c3","src/bindings/kotlin/test.rs":"2676303bc721561b5efb02966184905a4d75105ea6abf6825fd1297e66190687","src/bindings/mod.rs":"b156e444c4a55cdd17f9e700e393ef2464f59219121821163a4f9bba02afe474","src/bindings/python/gen_python/callback_interface.rs":"e3ffb8ba1aa8ac8ddcfff6554e7ec0d2d695d12955a98a04d114d2d6ca5dacc8","src/bindings/python/gen_python/compounds.rs":"e406c773c3b66368ea74973df742cdcba3014521812048f15de7c0e783eedfaf","src/bindings/python/gen_python/custom.rs":"33756f6bdafbd9b1a3a439c1cced3c83dc1fdb82b5ec235c064e69670ea6086c","src/bindings/python/gen_python/enum_.rs":"634c8406b07cbd24ea4f71cae4c971169e4989ce8019188f4bcd515bc3c77c5d","src/bindings/python/gen_python/error.rs":"d1a3b3edb91d9502064463cc3324770a1a6e0f234d6b49b5d7c43636bfe81d59","src/bindings/python/gen_python/external.rs":"99c1822466045f721fb1def8228b1dcefaabdc3615d005b3360e4a578ec45bfb","src/bindings/python/gen_python/miscellany.rs":"f3898b75cf494b39662d886eb78d9cc06685650265fc8a1e87e5de67baa342ae","src/bindings/python/gen_python/mod.rs":"2e6ce9aa3fc83e909f54cf3e33999d72c128abe06554b5973569d77fa76cbd3f","src/bindings/python/gen_python/object.rs":"b4d3d8a935d4acd689cf1f3857d461dbab3a51c7c8bd821890198fb58ef3a63f","src/bindings/python/gen_python/primitives.rs":"dff24f2c224723258fcad1a867a5cf984eea01dd2f32b8c1067ad562e1270826","src/bindings/python/gen_python/record.rs":"f961dfe8dd1e5caa633754de7ecc684c2211c9b6633a5e6beb6053e60500b9f2","src/bindings/python/mod.rs":"9e6b347e8d5ab165083a6e241d28763cffb99d34d76b6b245268f265b8585355","src/bindings/python/templates/BooleanHelper.py":"d384ffeefcb5982c4875e819d06e919a295eaa9ff57735e6fef0801bb810d5c9","src/bindings/python/templates/CallbackInterfaceRuntime.py":"7ffef485fc008e2d9efcd07326102f300bd4673b4351353e9e2908355936c3d7","src/bindings/python/templates/CallbackInterfaceTemplate.py":"6443f9494dd84752bd8f64e607e1fa68fd53ce57c3078563ce1d05e5e912b89e","src/bindings/python/templates/CustomType.py":"db3309b3f944fb813d4cef47a04d67a86f824183f7e31289184683155fd985d9","src/bindings/python/templates/DurationHelper.py":"179c14dccd8cc7dc9791f896414f0b5d124ec116eb78173371bd8a0743208da1","src/bindings/python/templates/EnumTemplate.py":"825ca373286f1b3b252b8a1263e29c9e2f0d3e170ceb364bd6c28d6c4595597b","src/bindings/python/templates/ErrorTemplate.py":"b6b1b0d3fc074a7c3ba7db394c5d6cb3279b0fc0e6a331154303c2c6c907458b","src/bindings/python/templates/ExternalTemplate.py":"3b8c035dca67f1e2699481498ec02a0c92fe41d666409140e282bfeaf14889a1","src/bindings/python/templates/Float32Helper.py":"7dbc51889cff47ebf1600fad490849e4a87cac4fc0d1756eebd21609eb80b4a9","src/bindings/python/templates/Float64Helper.py":"ba9f334d1339b6eaedcacc1e35068939727170a684f41dec9ee04762ed98cec1","src/bindings/python/templates/Helpers.py":"3a4999b02cbd2439814b28318e9a1ae07dbd607931d907f6c17b81e92f4354a1","src/bindings/python/templates/Int16Helper.py":"60c22fb8b445841ebb3c68be11b81c9eba84a680eaa0e30770953361231da9b5","src/bindings/python/templates/Int32Helper.py":"aff0a017cf767394174e46d8c4fe5a5a704a8e6384fcc38d227634dfe7826916","src/bindings/python/templates/Int64Helper.py":"6c314b91699a6c6ad7db3ef636713bc2a0af9609c82acfd6062b0588177c0026","src/bindings/python/templates/Int8Helper.py":"a6e2d121b1a6d59886fceab3949e571aba1abc06dfede52666954bf15366fb6f","src/bindings/python/templates/MapTemplate.py":"bbe609b865010b98b38c58f2d4fcc97f2adec3e931903bf67263a5e440a84400","src/bindings/python/templates/NamespaceLibraryTemplate.py":"4726dbeb61508a71153436126bc04d03af966dca4f5b37511beb8bcfb6f1f307","src/bindings/python/templates/ObjectTemplate.py":"68302f6da6e4e80fc0cc8eb4ef2d4353d19ad3e9955112a2fabec8bf1b1293c7","src/bindings/python/templates/OptionalTemplate.py":"ab6da433370ba7c1316e266247ee1b7165bc02f6f288e40a7c68806c018e3282","src/bindings/python/templates/RecordTemplate.py":"98cef2adbc2b890e5c67257fb32a57380070988ef4112408eeca85e0b87b566b","src/bindings/python/templates/RustBufferHelper.py":"11f733051e63733c637fb19c4758cb58a40d045792028465f36891f89c7c5f36","src/bindings/python/templates/RustBufferTemplate.py":"90950cfeeb7a028aac9b65aeca897b217eddcfa165a0d59e8af037e834f34146","src/bindings/python/templates/SequenceTemplate.py":"faf2b1d5272a66258972d88c29d5b527cf9e589c8399e30f7ad5a0503133ce9b","src/bindings/python/templates/StringHelper.py":"941a7ad71d9598701efa15323df93766934583a55f4266d26db31e6b744603fc","src/bindings/python/templates/TimestampHelper.py":"b412cea69117858c05bae3210d378c6296658ed02a50e87c52c392dcb62c7892","src/bindings/python/templates/TopLevelFunctionTemplate.py":"2b2883b14f324e543cfeea5293d2a4907fa9ebe93b779da7174a58929d8b0442","src/bindings/python/templates/Types.py":"65012f62eb048124efa2a200ee6d52dd9e1c4a0c1cb0d7370d1750a77fe8a958","src/bindings/python/templates/UInt16Helper.py":"06be5c9dacdf20e586f8236ed75cf2ca2470078fd8570843ea97c581b25bf860","src/bindings/python/templates/UInt32Helper.py":"41bb9bbf9b7be1060945e1267b1cc052585ec43696b1591f0ee779a0be0feaff","src/bindings/python/templates/UInt64Helper.py":"ba2825fc295a07292d9fb4aeebe74dabb9e6dbe505643e2347875ab12e511f31","src/bindings/python/templates/UInt8Helper.py":"dca5b3fc4a429fb233326224f85c4eccd3a7802ca9958ec309c7f197d59b4e3d","src/bindings/python/templates/macros.py":"0f953985263bbd7c5c0353cfc7ffb34582152e34520836a4756c5de652427fc9","src/bindings/python/templates/wrapper.py":"95570ea057b9fdd4fbc8a2fea68a24e6f06b0daf2660d21d06869b858b5a0542","src/bindings/python/test.rs":"8d3a5389b9764f7dbeb4dd9d025607868e1bcb5a2999f9abe44b144d3f55a980","src/bindings/ruby/gen_ruby/mod.rs":"c8e258096bb869abe92dc42645960aebafecac738306708eeda45bed4fb64fa7","src/bindings/ruby/gen_ruby/tests.rs":"7dcb86b08e643c43503f4cac6396833497f6988b004321c0067700ee29ffbf32","src/bindings/ruby/mod.rs":"5b7aad30caaf36502bc5965863e7e54d64253308457bd77b4142caa9423229ae","src/bindings/ruby/templates/EnumTemplate.rb":"5480edb347f5829e478d19474691babd72f37616ed846d519b5a61cb1d6cf047","src/bindings/ruby/templates/ErrorTemplate.rb":"147b2c3ff44c19624e7bf7b3a2a04b7badbba5743eaefa0d5e6c05943119c87e","src/bindings/ruby/templates/NamespaceLibraryTemplate.rb":"9b1454208bc83ef8f26aef33713d681e2284dbfea986ec0dd6c9b9c8b7d65e4a","src/bindings/ruby/templates/ObjectTemplate.rb":"0cfd9438e4821cf2164b23d748b3227a8cffbe2fab5b7eb70832228ccb628ee0","src/bindings/ruby/templates/RecordTemplate.rb":"4aeff886928ca972e5dc9b799581b30c66a6f6dce446af3285dd3ed6b422dea9","src/bindings/ruby/templates/RustBufferBuilder.rb":"42ae86a51b931f410e957232dbbc30a2bf49f6795b2c34dd618d1ea78e98f261","src/bindings/ruby/templates/RustBufferStream.rb":"147fa14a8bcb23f22df1bea6c2dabe42f5109187a117db8e982d79f83f731fcd","src/bindings/ruby/templates/RustBufferTemplate.rb":"8f37664f5436ba74ccdd801e16220f1b879d2fb56f51f5845b6047c92dc079f8","src/bindings/ruby/templates/TopLevelFunctionTemplate.rb":"88213e7e25bef664da939c04dd5621f438af735ffcb4d2d0c24a529538630069","src/bindings/ruby/templates/macros.rb":"a4c0cdd8b063ae08fd5b971fd747c6483edff92751f45a6a7458bcd85829cc00","src/bindings/ruby/templates/wrapper.rb":"542cdf46fb871e66089c9f008cf472cca411fe217d8c66a0a66776c35469aab5","src/bindings/ruby/test.rs":"efae020dd2cb26127da8a084f974e7720724d161a2c014bd1b7794a158768b33","src/bindings/swift/gen_swift/callback_interface.rs":"e331871ac6c9ac9b9533848fb6ddfcabc1e605970343cad307b6d86b72ebe96a","src/bindings/swift/gen_swift/compounds.rs":"f9e87b342f1f9a14295d87bad59d786d4c253a24d22900c2aba44816713718ae","src/bindings/swift/gen_swift/custom.rs":"45cdfa35ef7345dc353d0d2f3cebb17d726e90abdf5ef49422d6b2db65f8fd25","src/bindings/swift/gen_swift/enum_.rs":"018eea78ef85c9f8d715a5bc15c8273030d4f6ba297019949eb578c5cc6276fd","src/bindings/swift/gen_swift/error.rs":"bd95c3303e40f03a321f2cdc8e15a0251f4c7ddbc3c32c4c57eb9569db218488","src/bindings/swift/gen_swift/external.rs":"ef70de30186e07e16888b17876fcbcf440a488ac599bbe7d23606f09845d4ca1","src/bindings/swift/gen_swift/miscellany.rs":"66f16968f6cccc0b61c544e336a49b96218551731dcce566a176903c9afb3b57","src/bindings/swift/gen_swift/mod.rs":"ee7b97574a99fa712cbcbcbc077104c92cbc3b6342df0724e43311587320d880","src/bindings/swift/gen_swift/object.rs":"072a44f484cc66694c57a9fa41ba50a531c9ce19738e11ce9df17cdfc007648f","src/bindings/swift/gen_swift/primitives.rs":"9d086634994e5993c1a1ff327f637a826174b169d949cd390c71af77ceac5d3b","src/bindings/swift/gen_swift/record.rs":"48296332960be3731b9139dc664d4b8a5d56d04cffa34dc995b62cf202b4dbfd","src/bindings/swift/mod.rs":"155a5ffe5eb924471924864c83446130e6af67d51fddddda7fdfc12e369c103a","src/bindings/swift/templates/BooleanHelper.swift":"f02e391bed44ca0e03c66c4e6a1545caaae490fc72c4cf5935e66220082a8c30","src/bindings/swift/templates/BridgingHeaderTemplate.h":"93a289e393ecdbe1bf986215c3b19d2aed7677d47f79b1833ce73cf6f8762e80","src/bindings/swift/templates/CallbackInterfaceRuntime.swift":"c67193f2fc11dbf26f286439dce8f5733b6a7b8733704ac4f4a6c5605b9d5e7a","src/bindings/swift/templates/CallbackInterfaceTemplate.swift":"e1197096a99e9cfe477ba586a88c558561d94474106e011ad30671b8af78c874","src/bindings/swift/templates/CustomType.swift":"45dba980e4f1f1359f4609cb6493c613185e4fe12231baf324f34810e9500b60","src/bindings/swift/templates/DurationHelper.swift":"cbc41aaa58bda6c2313ede36a9f656a01a28f9c72aa1624e0e1c1da7b841ffb6","src/bindings/swift/templates/EnumTemplate.swift":"f1c03a389474cc3f2aa8d379f6800ff0d9df49f899f5a7e68c21009c8c3bd76f","src/bindings/swift/templates/ErrorTemplate.swift":"5014392d7a29ac271e140f27dd231b5a031abe9b6cf909d2b80200443ee03c18","src/bindings/swift/templates/Float32Helper.swift":"ea32538058c4b3c72b1cd2530ac00d0349fadab5e1bc617d33aae4c87692fc98","src/bindings/swift/templates/Float64Helper.swift":"e27e9424dc6e97b8cacc6ca4c796dd2d16dcfcb877e2f19c45eca03381a41e78","src/bindings/swift/templates/Helpers.swift":"3cf3a5342a1ef0c7e078b0e58c32ae4437c4fb5dc41acbbd56b1128393c76602","src/bindings/swift/templates/Int16Helper.swift":"204906911813a3931436057c23032f4c4e39e023df90d641d6c6086aefe2f820","src/bindings/swift/templates/Int32Helper.swift":"0997f059c9c4edd3c41aee0bbad4aa2bda6d791a0d623ad8014d5aa6bdae718d","src/bindings/swift/templates/Int64Helper.swift":"bcf8c2deebb3ee9bce87735adc4bd100981989943b69f6a7fb499a9aec4c25d9","src/bindings/swift/templates/Int8Helper.swift":"ad1ec0fa213724933fa4dc4e2e304e13ac4722b774bfffac44793986b997dd33","src/bindings/swift/templates/MapTemplate.swift":"53971ec388417b02519f8deb8d66361ab4693eae77d116f6051cbea4738054ec","src/bindings/swift/templates/ModuleMapTemplate.modulemap":"99ad1e9bf550a21497296f9248ecd4385dd6d0b5892951d24cf990cdbf3eec2c","src/bindings/swift/templates/ObjectTemplate.swift":"9e77b66477b50b77159e6fe885313833976219202bbb0c03613c8c79c5b31a36","src/bindings/swift/templates/OptionalTemplate.swift":"2376487ceadca3975f0e82ddf6ce61af8bbbf5b0592fa9cd977460f148d8c99d","src/bindings/swift/templates/RecordTemplate.swift":"5209d2438686a19bbf3903091796d3840a75de2df1beea2f99c5fbce6a83cd44","src/bindings/swift/templates/RustBufferTemplate.swift":"f4422fdf0cb5b4db267d461f063dedc319ea1a5a13bae1b82c3f108ba8c658bb","src/bindings/swift/templates/SequenceTemplate.swift":"8425b279197582a94b4cf363ab0463907a68a624e24045720ef7df2bcacf3383","src/bindings/swift/templates/StringHelper.swift":"968b9b9b7fbe06a2ac2143016edaff3552e201652accb8d613b03645f0d24a90","src/bindings/swift/templates/TimestampHelper.swift":"82eece13aa186c8e3745c8ad2f1290639ca9689573018a2bdc5c75afbae58c26","src/bindings/swift/templates/TopLevelFunctionTemplate.swift":"ca7fcbaa06ee31c7433fd6aa238b6c1135a843f469d9b4f37d4874d514fd80d0","src/bindings/swift/templates/Types.swift":"8faebfad82be0788acb4b61716761366cd85e280e1c89ca741d423eebf5d220a","src/bindings/swift/templates/UInt16Helper.swift":"d6fba577324fc0e9e50329c968df99341de418011be126bd29702f8a94d87c02","src/bindings/swift/templates/UInt32Helper.swift":"5e0cf151a0c40098b3d96815ba3de23e15fe52f3c517577e1b0c7e7a4c12428f","src/bindings/swift/templates/UInt64Helper.swift":"17237b38d09ced8d2a8ff1ad9ca86873a19e417280e0e60f33d7063397ea4b7b","src/bindings/swift/templates/UInt8Helper.swift":"c4cb2ee4a78b54ca8d0013383c6b43e9ecd42776e3dc7d6e40086325d41714e5","src/bindings/swift/templates/macros.swift":"17819060db72ff2d67d0fee8b4932e0fe5cfe1cb25b70bcfd83bcfbf0cbbd9a7","src/bindings/swift/templates/wrapper.swift":"ca1af5fe47bca972c76fb8c9f52b18f727d4343204408de3dd0aae384f0a4171","src/bindings/swift/test.rs":"72ba6a6fbc65ffd035d4a5694e462d5fa0734cb5b9b585246a5508dba9ce200b","src/interface/attributes.rs":"31bb5b30f6be41871bade5dfc3958156079d91c48110b279efdb2ae96102bf4c","src/interface/callbacks.rs":"e14abd2a9575ae55891ddc7f20d6643211b6eaad575ac3143f0591f185ad498d","src/interface/enum_.rs":"e93ce9d66fa77d565bb02256a1e356cead49af31774999f6b481d5ec81582fe9","src/interface/error.rs":"e51b64505b38a17807ce82070b253c61b5cb7ca71e42dae3913fa462f3a84356","src/interface/ffi.rs":"f056724ad06074e7ebb4adc32372d8d7b5f8a29d0c817b709e4d4cfc7f5d83b0","src/interface/function.rs":"0576142a6dbcb3f9f435f0d9ac78ef588b152462969123c1ed38fd01bd42078c","src/interface/literal.rs":"771956995994e2a215c84c4ba8d91dfe52e5b36b5137e49902af90273a1b663c","src/interface/mod.rs":"d956a482926a521e641409734eb311be97dfb29af3281634a988e5b29e34dd55","src/interface/namespace.rs":"ab0f63241bb6a0a32f9fd2a1544468cb6daa1f91f6116b7246b52a4a94e4f461","src/interface/object.rs":"7043293251d8c67c5fa37c42026c65a56defe68688a98a4d05ddb94545024f08","src/interface/record.rs":"5274385d79198cf100e9aa307a8021ba6edbd75e24ae5db2f8244986c951f55a","src/interface/types/finder.rs":"e1a0697a51c847d58f5adc529abb006e7fc55954ec92a9c822926a661acc0386","src/interface/types/mod.rs":"dc23b5626031df99ca414430d339deaa6966fe5c9969cebf46be057582e9faf8","src/interface/types/resolver.rs":"83f80d582279e16d6c6bfb2b5dd7950639d99e1d5c6627f6b3d284a7abf255ef","src/lib.rs":"af929600e6cf23801d494ce2fd44913a4f5c1a770f903aef982f9bf33f9a6fbc","src/macro_metadata/ci.rs":"c3768df2bf1c12089077ef04b542ddcc1bf2d6c1539aa0ee7afa6f3bfb026dc6","src/macro_metadata/extract.rs":"d07beb4e1e03befaad6ce57c8103a03cb7ce6092871ae4bff9eb1282293653e6","src/macro_metadata/mod.rs":"1668e6d947fd4a957f557410a8095649de6f8d7fd3eb6a403b8587280dca2d54","src/scaffolding/mod.rs":"d2b1e373627cbc5e54b1bb82ffca8555982bfa3e71af33fb2ab7e20979bb946a","src/scaffolding/templates/CallbackInterfaceTemplate.rs":"9cc291b565a07a48418bd219e375a174e484562599fa543188e6e88409b528ad","src/scaffolding/templates/EnumTemplate.rs":"eb8802f04f1fd1c9451ad72c1195c136288fdb4e488922b24db75b2fdae65cd7","src/scaffolding/templates/ErrorTemplate.rs":"e7ca4538908a273dd7ba3834219b30fa0e537b2171144f06b6a96fbefb90cd1d","src/scaffolding/templates/ExternalTypesTemplate.rs":"3805714b9c31b83650326b783150e281162ff03d204044bdba7912de2730c7d9","src/scaffolding/templates/ObjectTemplate.rs":"34de640428486e17a61ded4f42ce13d8eac3e66cc9f60196ce2575cdc0bd8df6","src/scaffolding/templates/RecordTemplate.rs":"e8d5af954f46f023a243721d6fc70aa176c3a4c0a9dd340048bfe46f3eeed783","src/scaffolding/templates/ReexportUniFFIScaffolding.rs":"559a17c8e39c473ff1effe9651f05b83d443ecd8abed13e03f2b63872d7e1593","src/scaffolding/templates/RustBuffer.rs":"ccf7521012d93c41265375c6d2e80ce861ec93b41383da83c37718386dd726f6","src/scaffolding/templates/TopLevelFunctionTemplate.rs":"35eaefb0862f25ff683d58fb0be6ad5f74bbe2fc85047273200c3a5f4c728434","src/scaffolding/templates/macros.rs":"d42ed1801927b38cd6a526b2b00cfc2b9bfdf65b5501216c4a3d903e0765af7c","src/scaffolding/templates/scaffolding_template.rs":"51a34e249f788c02f117f3bd7c3de1633fcc765c6625c762c5bd870541cae6e3"},"package":"dbbba5103051c18f10b22f80a74439ddf7100273f217a547005d2735b2498994"}
\ No newline at end of file diff --git a/third_party/rust/uniffi_bindgen/Cargo.toml b/third_party/rust/uniffi_bindgen/Cargo.toml new file mode 100644 index 0000000000..2ef7afb6c4 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/Cargo.toml @@ -0,0 +1,75 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi_bindgen" +version = "0.23.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +description = "a multi-language bindings generator for rust (codegen and cli tooling)" +homepage = "https://mozilla.github.io/uniffi-rs" +documentation = "https://mozilla.github.io/uniffi-rs" +keywords = [ + "ffi", + "bindgen", +] +license = "MPL-2.0" +repository = "https://github.com/mozilla/uniffi-rs" + +[dependencies.anyhow] +version = "1" + +[dependencies.askama] +version = "0.11" +features = ["config"] +default-features = false + +[dependencies.bincode] +version = "1.3" + +[dependencies.camino] +version = "1.0.8" + +[dependencies.fs-err] +version = "2.7.0" + +[dependencies.glob] +version = "0.3" + +[dependencies.goblin] +version = "0.6" + +[dependencies.heck] +version = "0.4" + +[dependencies.once_cell] +version = "1.12" + +[dependencies.paste] +version = "1.0" + +[dependencies.serde] +version = "1" + +[dependencies.serde_json] +version = "1.0.80" + +[dependencies.toml] +version = "0.5" + +[dependencies.uniffi_meta] +version = "=0.23.0" + +[dependencies.uniffi_testing] +version = "=0.23.0" + +[dependencies.weedle2] +version = "4.0.0" diff --git a/third_party/rust/uniffi_bindgen/askama.toml b/third_party/rust/uniffi_bindgen/askama.toml new file mode 100644 index 0000000000..a0aae5f41b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/askama.toml @@ -0,0 +1,21 @@ +[general] +# Directories to search for templates, relative to the crate root. +dirs = [ "src/scaffolding/templates", "src/bindings/kotlin/templates", "src/bindings/python/templates", "src/bindings/swift/templates", "src/bindings/ruby/templates" ] + +[[syntax]] +name = "kt" + +[[syntax]] +name = "py" + +[[syntax]] +name = "swift" + +[[syntax]] +name = "c" + +[[syntax]] +name = "rs" + +[[syntax]] +name = "rb" diff --git a/third_party/rust/uniffi_bindgen/src/backend/config.rs b/third_party/rust/uniffi_bindgen/src/backend/config.rs new file mode 100644 index 0000000000..616cde0656 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/backend/config.rs @@ -0,0 +1,17 @@ +/* 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 serde::{Deserialize, Serialize}; + +/// Config value for template expressions +/// +/// These are strings that support simple template substitution. `{}` gets replaced by a value. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct TemplateExpression(String); + +impl TemplateExpression { + pub fn render(&self, var: &str) -> String { + self.0.replace("{}", var) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/backend/declarations.rs b/third_party/rust/uniffi_bindgen/src/backend/declarations.rs new file mode 100644 index 0000000000..320cfe995c --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/backend/declarations.rs @@ -0,0 +1,31 @@ +/* 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 super::CodeOracle; + +/// A trait that is able to render a declaration about a particular member declared in +/// the `ComponentInterface`. +/// Like `CodeType`, it can render declaration code and imports. It also is able to render +/// code at start-up of the FFI. +/// All methods are optional, and there is no requirement that the trait be used for a particular +/// `interface::` member. Thus, it can also be useful for conditionally rendering code. +pub trait CodeDeclaration { + /// A list of imports that are needed if this type is in use. + /// Classes are imported exactly once. + fn imports(&self, _oracle: &dyn CodeOracle) -> Option<Vec<String>> { + None + } + + /// Code (one or more statements) that is run on start-up of the library, + /// but before the client code has access to it. + fn initialization_code(&self, _oracle: &dyn CodeOracle) -> Option<String> { + None + } + + /// Code which represents this member. e.g. the foreign language class definition for + /// a given Object type. + fn definition_code(&self, _oracle: &dyn CodeOracle) -> Option<String> { + None + } +} diff --git a/third_party/rust/uniffi_bindgen/src/backend/mod.rs b/third_party/rust/uniffi_bindgen/src/backend/mod.rs new file mode 100644 index 0000000000..ed4c032508 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/backend/mod.rs @@ -0,0 +1,16 @@ +/* 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/. */ + +mod config; +mod declarations; +mod oracle; +mod types; + +pub use config::TemplateExpression; +pub use declarations::CodeDeclaration; +pub use oracle::CodeOracle; +pub use types::CodeType; + +pub type TypeIdentifier = crate::interface::Type; +pub type Literal = crate::interface::Literal; diff --git a/third_party/rust/uniffi_bindgen/src/backend/oracle.rs b/third_party/rust/uniffi_bindgen/src/backend/oracle.rs new file mode 100644 index 0000000000..c911dc7df7 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/backend/oracle.rs @@ -0,0 +1,35 @@ +/* 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 super::{CodeType, TypeIdentifier}; +use crate::interface::FfiType; + +/// An object to look up a foreign language code specific renderer for a given type used. +/// Every `Type` referred to in the `ComponentInterface` should map to a corresponding +/// `CodeType`. +/// +/// The mapping may be opaque, but the oracle always knows the answer. +/// +/// In adddition, the oracle knows how to render identifiers (function names, +/// class names, variable names etc). +pub trait CodeOracle { + fn find(&self, type_: &TypeIdentifier) -> Box<dyn CodeType>; + + /// Get the idiomatic rendering of a class name (for enums, records, errors, etc). + fn class_name(&self, nm: &str) -> String; + + /// Get the idiomatic rendering of a function name. + fn fn_name(&self, nm: &str) -> String; + + /// Get the idiomatic rendering of a variable name. + fn var_name(&self, nm: &str) -> String; + + /// Get the idiomatic rendering of an individual enum variant. + fn enum_variant_name(&self, nm: &str) -> String; + + /// Get the idiomatic rendering of an error name. + fn error_name(&self, nm: &str) -> String; + + fn ffi_type_label(&self, ffi_type: &FfiType) -> String; +} diff --git a/third_party/rust/uniffi_bindgen/src/backend/types.rs b/third_party/rust/uniffi_bindgen/src/backend/types.rs new file mode 100644 index 0000000000..217523c290 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/backend/types.rs @@ -0,0 +1,218 @@ +/* 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/. */ + +//! # Backend traits +//! +//! This module provides a number of traits useful for implementing a backend for Uniffi. +//! +//! A `CodeType` is needed for each type that will cross the FFI. It should provide helper machinery +//! in the target language to lift from and lower into a value of that type into a primitive type +//! (the FfiType), and foreign language expressions that call into the machinery. This helper code +//! can be provided by a template file. +//! +//! A `CodeDeclaration` is needed for each type that is declared in the UDL file. This has access to +//! the [crate::interface::ComponentInterface], which is the closest thing to an Intermediate Representation. +//! +//! `CodeDeclaration`s provide the target language's version of the UDL type, including forwarding method calls +//! into Rust. It is likely if you're implementing a `CodeDeclaration` for this purpose, it will also need to cross +//! the FFI, and you'll also need a `CodeType`. +//! +//! `CodeDeclaration`s can also be used to conditionally include code: e.g. only include the CallbackInterfaceRuntime +//! if the user has used at least one callback interface. +//! +//! Each backend has a wrapper template for each file it needs to generate. This should collect the `CodeDeclaration`s that +//! the backend and `ComponentInterface` between them specify and use them to stitch together a file in the target language. +//! +//! The `CodeOracle` provides methods to map the `Type` values found in the `ComponentInterface` to the `CodeType`s specified +//! by the backend. It also provides methods for transforming identifiers into the coding standard for the target language. +//! +//! Each backend will have its own `filter` module, which is used by the askama templates used in all `CodeType`s and `CodeDeclaration`s. +//! This filter provides methods to generate expressions and identifiers in the target language. These are all forwarded to the oracle. + +use super::{CodeOracle, Literal}; +use crate::interface::*; + +/// A Trait to emit foreign language code to handle referenced types. +/// A type which is specified in the UDL (i.e. a member of the component interface) +/// will have a `CodeDeclaration` as well, but for types used e.g. primitive types, Strings, etc +/// only a `CodeType` is needed. +pub trait CodeType { + /// The language specific label used to reference this type. This will be used in + /// method signatures and property declarations. + fn type_label(&self, oracle: &dyn CodeOracle) -> String; + + /// A representation of this type label that can be used as part of another + /// identifier. e.g. `read_foo()`, or `FooInternals`. + /// + /// This is especially useful when creating specialized objects or methods to deal + /// with this type only. + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + self.type_label(oracle) + } + + /// A representation of the given literal for this type. + /// N.B. `Literal` is aliased from `interface::Literal`, so may not be whole suited to this task. + fn literal(&self, oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unimplemented!("Unimplemented for {}", self.type_label(oracle)) + } + + /// Name of the FfiConverter + /// + /// This is the object that contains the lower, write, lift, and read methods for this type. + /// Depending on the binding this will either be a singleton or a class with static methods. + /// + /// This is the newer way of handling these methods and replaces the lower, write, lift, and + /// read CodeType methods. Currently only used by Kotlin, but the plan is to move other + /// backends to using this. + fn ffi_converter_name(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&format!("FfiConverter{}", self.canonical_name(oracle))) + } + + /// An expression for lowering a value into something we can pass over the FFI. + fn lower(&self, oracle: &dyn CodeOracle) -> String { + format!("{}.lower", self.ffi_converter_name(oracle)) + } + + /// An expression for writing a value into a byte buffer. + fn write(&self, oracle: &dyn CodeOracle) -> String { + format!("{}.write", self.ffi_converter_name(oracle)) + } + + /// An expression for lifting a value from something we received over the FFI. + fn lift(&self, oracle: &dyn CodeOracle) -> String { + format!("{}.lift", self.ffi_converter_name(oracle)) + } + + /// An expression for reading a value from a byte buffer. + fn read(&self, oracle: &dyn CodeOracle) -> String { + format!("{}.read", self.ffi_converter_name(oracle)) + } + + /// A list of imports that are needed if this type is in use. + /// Classes are imported exactly once. + fn imports(&self, _oracle: &dyn CodeOracle) -> Option<Vec<String>> { + None + } + + /// Function to run at startup + fn initialization_fn(&self, _oracle: &dyn CodeOracle) -> Option<String> { + None + } + + /// An expression to coerce the given variable to the expected type. + fn coerce(&self, oracle: &dyn CodeOracle, _nm: &str) -> String { + panic!("Unimplemented for {}", self.type_label(oracle)); + } +} + +/// This trait is used to implement `CodeType` for `Type` and type-like structs (`Record`, `Enum`, `Field`, +/// etc). We forward all method calls to a `Box<dyn CodeType>`, which we get by calling +/// `CodeOracle.find()`. +pub trait CodeTypeDispatch { + fn code_type_impl(&self, oracle: &dyn CodeOracle) -> Box<dyn CodeType>; +} + +impl CodeTypeDispatch for Type { + fn code_type_impl(&self, oracle: &dyn CodeOracle) -> Box<dyn CodeType> { + oracle.find(self) + } +} + +impl CodeTypeDispatch for Record { + fn code_type_impl(&self, oracle: &dyn CodeOracle) -> Box<dyn CodeType> { + oracle.find(&self.type_()) + } +} + +impl CodeTypeDispatch for Enum { + fn code_type_impl(&self, oracle: &dyn CodeOracle) -> Box<dyn CodeType> { + oracle.find(&self.type_()) + } +} + +impl CodeTypeDispatch for Error { + fn code_type_impl(&self, oracle: &dyn CodeOracle) -> Box<dyn CodeType> { + oracle.find(&self.type_()) + } +} + +impl CodeTypeDispatch for Object { + fn code_type_impl(&self, oracle: &dyn CodeOracle) -> Box<dyn CodeType> { + oracle.find(&self.type_()) + } +} + +impl CodeTypeDispatch for CallbackInterface { + fn code_type_impl(&self, oracle: &dyn CodeOracle) -> Box<dyn CodeType> { + oracle.find(&self.type_()) + } +} + +impl CodeTypeDispatch for Field { + fn code_type_impl(&self, oracle: &dyn CodeOracle) -> Box<dyn CodeType> { + oracle.find(self.type_()) + } +} + +impl CodeTypeDispatch for Argument { + fn code_type_impl(&self, oracle: &dyn CodeOracle) -> Box<dyn CodeType> { + oracle.find(self.type_()) + } +} + +// Needed to handle &&Type and &&&Type values, which we sometimes end up with in the template code +impl<T, C> CodeTypeDispatch for T +where + T: std::ops::Deref<Target = C>, + C: CodeTypeDispatch, +{ + fn code_type_impl(&self, oracle: &dyn CodeOracle) -> Box<dyn CodeType> { + self.deref().code_type_impl(oracle) + } +} + +impl<T: CodeTypeDispatch> CodeType for T { + // The above code implements `CodeTypeDispatch` for `Type` and type-like structs (`Record`, + // `Enum`, `Field`, etc). Now we can leverage that to implement `CodeType` for all of them. + // This allows for simpler template code (`field|lower` instead of `field.type_()|lower`) + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + self.code_type_impl(oracle).type_label(oracle) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + self.code_type_impl(oracle).canonical_name(oracle) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + self.code_type_impl(oracle).literal(oracle, literal) + } + + fn lower(&self, oracle: &dyn CodeOracle) -> String { + self.code_type_impl(oracle).lower(oracle) + } + + fn write(&self, oracle: &dyn CodeOracle) -> String { + self.code_type_impl(oracle).write(oracle) + } + + fn lift(&self, oracle: &dyn CodeOracle) -> String { + self.code_type_impl(oracle).lift(oracle) + } + + fn read(&self, oracle: &dyn CodeOracle) -> String { + self.code_type_impl(oracle).read(oracle) + } + + fn imports(&self, oracle: &dyn CodeOracle) -> Option<Vec<String>> { + self.code_type_impl(oracle).imports(oracle) + } + + fn initialization_fn(&self, oracle: &dyn CodeOracle) -> Option<String> { + self.code_type_impl(oracle).initialization_fn(oracle) + } + + fn coerce(&self, oracle: &dyn CodeOracle, nm: &str) -> String { + self.code_type_impl(oracle).coerce(oracle, nm) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/callback_interface.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/callback_interface.rs new file mode 100644 index 0000000000..c0c7ab29b9 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/callback_interface.rs @@ -0,0 +1,33 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct CallbackInterfaceCodeType { + id: String, +} + +impl CallbackInterfaceCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for CallbackInterfaceCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } + + fn initialization_fn(&self, oracle: &dyn CodeOracle) -> Option<String> { + Some(format!("{}.register", self.ffi_converter_name(oracle))) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/compounds.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/compounds.rs new file mode 100644 index 0000000000..7d5f77141f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/compounds.rs @@ -0,0 +1,94 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal, TypeIdentifier}; +use paste::paste; + +fn render_literal(oracle: &dyn CodeOracle, literal: &Literal, inner: &TypeIdentifier) -> String { + match literal { + Literal::Null => "null".into(), + Literal::EmptySequence => "listOf()".into(), + Literal::EmptyMap => "mapOf()".into(), + + // For optionals + _ => oracle.find(inner).literal(oracle, literal), + } +} + +macro_rules! impl_code_type_for_compound { + ($T:ty, $type_label_pattern:literal, $canonical_name_pattern: literal) => { + paste! { + pub struct $T { + inner: TypeIdentifier, + } + + impl $T { + pub fn new(inner: TypeIdentifier) -> Self { + Self { inner } + } + fn inner(&self) -> &TypeIdentifier { + &self.inner + } + } + + impl CodeType for $T { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + format!($type_label_pattern, oracle.find(self.inner()).type_label(oracle)) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!($canonical_name_pattern, oracle.find(self.inner()).canonical_name(oracle)) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + render_literal(oracle, literal, self.inner()) + } + } + } + } + } + +impl_code_type_for_compound!(OptionalCodeType, "{}?", "Optional{}"); +impl_code_type_for_compound!(SequenceCodeType, "List<{}>", "Sequence{}"); + +pub struct MapCodeType { + key: TypeIdentifier, + value: TypeIdentifier, +} + +impl MapCodeType { + pub fn new(key: TypeIdentifier, value: TypeIdentifier) -> Self { + Self { key, value } + } + + fn key(&self) -> &TypeIdentifier { + &self.key + } + + fn value(&self) -> &TypeIdentifier { + &self.value + } +} + +impl CodeType for MapCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Map<{}, {}>", + self.key().type_label(oracle), + self.value().type_label(oracle), + ) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Map{}{}", + self.key().type_label(oracle), + self.value().type_label(oracle), + ) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + render_literal(oracle, literal, &self.value) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/custom.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/custom.rs new file mode 100644 index 0000000000..4a5636a228 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/custom.rs @@ -0,0 +1,29 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct CustomCodeType { + name: String, +} + +impl CustomCodeType { + pub fn new(name: String) -> Self { + CustomCodeType { name } + } +} + +impl CodeType for CustomCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + self.name.clone() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.name) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!("Can't have a literal of a custom type"); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/enum_.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/enum_.rs new file mode 100644 index 0000000000..941008e370 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/enum_.rs @@ -0,0 +1,37 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct EnumCodeType { + id: String, +} + +impl EnumCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for EnumCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + if let Literal::Enum(v, _) = literal { + format!( + "{}.{}", + self.type_label(oracle), + oracle.enum_variant_name(v) + ) + } else { + unreachable!(); + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/error.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/error.rs new file mode 100644 index 0000000000..3ab5f0ac9f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/error.rs @@ -0,0 +1,29 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct ErrorCodeType { + id: String, +} + +impl ErrorCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for ErrorCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.error_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/external.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/external.rs new file mode 100644 index 0000000000..32d47cb3f1 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/external.rs @@ -0,0 +1,29 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct ExternalCodeType { + name: String, +} + +impl ExternalCodeType { + pub fn new(name: String) -> Self { + Self { name } + } +} + +impl CodeType for ExternalCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + self.name.clone() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.name) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!("Can't have a literal of an external type"); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/miscellany.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/miscellany.rs new file mode 100644 index 0000000000..57b9a145e7 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/miscellany.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/. */ + +use crate::backend::{CodeOracle, CodeType, Literal}; +use paste::paste; + +macro_rules! impl_code_type_for_miscellany { + ($T:ty, $class_name:literal, $canonical_name:literal) => { + paste! { + pub struct $T; + + impl CodeType for $T { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + $class_name.into() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + $canonical_name.into() + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!() + } + } + } + }; +} + +impl_code_type_for_miscellany!(TimestampCodeType, "java.time.Instant", "Timestamp"); + +impl_code_type_for_miscellany!(DurationCodeType, "java.time.Duration", "Duration"); diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs new file mode 100644 index 0000000000..8fe12f30c7 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs @@ -0,0 +1,417 @@ +/* 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::borrow::Borrow; +use std::cell::RefCell; +use std::collections::{BTreeSet, HashMap, HashSet}; + +use anyhow::{Context, Result}; +use askama::Template; +use heck::{ToLowerCamelCase, ToShoutySnakeCase, ToUpperCamelCase}; +use serde::{Deserialize, Serialize}; + +use crate::backend::{CodeOracle, CodeType, TemplateExpression, TypeIdentifier}; +use crate::interface::*; +use crate::MergeWith; + +mod callback_interface; +mod compounds; +mod custom; +mod enum_; +mod error; +mod external; +mod miscellany; +mod object; +mod primitives; +mod record; + +// config options to customize the generated Kotlin. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Config { + package_name: Option<String>, + cdylib_name: Option<String>, + #[serde(default)] + custom_types: HashMap<String, CustomTypeConfig>, + #[serde(default)] + external_packages: HashMap<String, String>, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct CustomTypeConfig { + imports: Option<Vec<String>>, + type_name: Option<String>, + into_custom: TemplateExpression, + from_custom: TemplateExpression, +} + +impl Config { + pub fn package_name(&self) -> String { + if let Some(package_name) = &self.package_name { + package_name.clone() + } else { + "uniffi".into() + } + } + + pub fn cdylib_name(&self) -> String { + if let Some(cdylib_name) = &self.cdylib_name { + cdylib_name.clone() + } else { + "uniffi".into() + } + } +} + +impl From<&ComponentInterface> for Config { + fn from(ci: &ComponentInterface) -> Self { + Config { + package_name: Some(format!("uniffi.{}", ci.namespace())), + cdylib_name: Some(format!("uniffi_{}", ci.namespace())), + custom_types: HashMap::new(), + external_packages: HashMap::new(), + } + } +} + +impl MergeWith for Config { + fn merge_with(&self, other: &Self) -> Self { + Config { + package_name: self.package_name.merge_with(&other.package_name), + cdylib_name: self.cdylib_name.merge_with(&other.cdylib_name), + custom_types: self.custom_types.merge_with(&other.custom_types), + external_packages: self.external_packages.merge_with(&other.external_packages), + } + } +} + +// Generate kotlin bindings for the given ComponentInterface, as a string. +pub fn generate_bindings(config: &Config, ci: &ComponentInterface) -> Result<String> { + KotlinWrapper::new(config.clone(), ci) + .render() + .context("failed to render kotlin bindings") +} + +/// A struct to record a Kotlin import statement. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum ImportRequirement { + /// The name we are importing. + Import { name: String }, + /// Import the name with the specified local name. + ImportAs { name: String, as_name: String }, +} + +impl ImportRequirement { + /// Render the Kotlin import statement. + fn render(&self) -> String { + match &self { + ImportRequirement::Import { name } => format!("import {name}"), + ImportRequirement::ImportAs { name, as_name } => { + format!("import {name} as {as_name}") + } + } + } +} + +/// Renders Kotlin helper code for all types +/// +/// This template is a bit different than others in that it stores internal state from the render +/// process. Make sure to only call `render()` once. +#[derive(Template)] +#[template(syntax = "kt", escape = "none", path = "Types.kt")] +pub struct TypeRenderer<'a> { + kotlin_config: &'a Config, + ci: &'a ComponentInterface, + // Track included modules for the `include_once()` macro + include_once_names: RefCell<HashSet<String>>, + // Track imports added with the `add_import()` macro + imports: RefCell<BTreeSet<ImportRequirement>>, +} + +impl<'a> TypeRenderer<'a> { + fn new(kotlin_config: &'a Config, ci: &'a ComponentInterface) -> Self { + Self { + kotlin_config, + ci, + include_once_names: RefCell::new(HashSet::new()), + imports: RefCell::new(BTreeSet::new()), + } + } + + // Get the package name for an external type + fn external_type_package_name(&self, crate_name: &str) -> String { + match self.kotlin_config.external_packages.get(crate_name) { + Some(name) => name.clone(), + None => crate_name.to_string(), + } + } + + // The following methods are used by the `Types.kt` macros. + + // Helper for the including a template, but only once. + // + // The first time this is called with a name it will return true, indicating that we should + // include the template. Subsequent calls will return false. + fn include_once_check(&self, name: &str) -> bool { + self.include_once_names + .borrow_mut() + .insert(name.to_string()) + } + + // Helper to add an import statement + // + // Call this inside your template to cause an import statement to be added at the top of the + // file. Imports will be sorted and de-deuped. + // + // Returns an empty string so that it can be used inside an askama `{{ }}` block. + fn add_import(&self, name: &str) -> &str { + self.imports.borrow_mut().insert(ImportRequirement::Import { + name: name.to_owned(), + }); + "" + } + + // Like add_import, but arranges for `import name as as_name` + fn add_import_as(&self, name: &str, as_name: &str) -> &str { + self.imports + .borrow_mut() + .insert(ImportRequirement::ImportAs { + name: name.to_owned(), + as_name: as_name.to_owned(), + }); + "" + } +} + +#[derive(Template)] +#[template(syntax = "kt", escape = "none", path = "wrapper.kt")] +pub struct KotlinWrapper<'a> { + config: Config, + ci: &'a ComponentInterface, + type_helper_code: String, + type_imports: BTreeSet<ImportRequirement>, +} + +impl<'a> KotlinWrapper<'a> { + pub fn new(config: Config, ci: &'a ComponentInterface) -> Self { + let type_renderer = TypeRenderer::new(&config, ci); + let type_helper_code = type_renderer.render().unwrap(); + let type_imports = type_renderer.imports.into_inner(); + Self { + config, + ci, + type_helper_code, + type_imports, + } + } + + pub fn initialization_fns(&self) -> Vec<String> { + self.ci + .iter_types() + .filter_map(|t| t.initialization_fn(&KotlinCodeOracle)) + .collect() + } + + pub fn imports(&self) -> Vec<ImportRequirement> { + self.type_imports.iter().cloned().collect() + } +} + +#[derive(Clone)] +pub struct KotlinCodeOracle; + +impl KotlinCodeOracle { + // Map `Type` instances to a `Box<dyn CodeType>` for that type. + // + // There is a companion match in `templates/Types.kt` which performs a similar function for the + // template code. + // + // - When adding additional types here, make sure to also add a match arm to the `Types.kt` template. + // - To keep things manageable, let's try to limit ourselves to these 2 mega-matches + fn create_code_type(&self, type_: TypeIdentifier) -> Box<dyn CodeType> { + match type_ { + Type::UInt8 => Box::new(primitives::UInt8CodeType), + Type::Int8 => Box::new(primitives::Int8CodeType), + Type::UInt16 => Box::new(primitives::UInt16CodeType), + Type::Int16 => Box::new(primitives::Int16CodeType), + Type::UInt32 => Box::new(primitives::UInt32CodeType), + Type::Int32 => Box::new(primitives::Int32CodeType), + Type::UInt64 => Box::new(primitives::UInt64CodeType), + Type::Int64 => Box::new(primitives::Int64CodeType), + Type::Float32 => Box::new(primitives::Float32CodeType), + Type::Float64 => Box::new(primitives::Float64CodeType), + Type::Boolean => Box::new(primitives::BooleanCodeType), + Type::String => Box::new(primitives::StringCodeType), + + Type::Timestamp => Box::new(miscellany::TimestampCodeType), + Type::Duration => Box::new(miscellany::DurationCodeType), + + Type::Enum(id) => Box::new(enum_::EnumCodeType::new(id)), + Type::Object(id) => Box::new(object::ObjectCodeType::new(id)), + Type::Record(id) => Box::new(record::RecordCodeType::new(id)), + Type::Error(id) => Box::new(error::ErrorCodeType::new(id)), + Type::CallbackInterface(id) => { + Box::new(callback_interface::CallbackInterfaceCodeType::new(id)) + } + Type::Optional(inner) => Box::new(compounds::OptionalCodeType::new(*inner)), + Type::Sequence(inner) => Box::new(compounds::SequenceCodeType::new(*inner)), + Type::Map(key, value) => Box::new(compounds::MapCodeType::new(*key, *value)), + Type::External { name, .. } => Box::new(external::ExternalCodeType::new(name)), + Type::Custom { name, .. } => Box::new(custom::CustomCodeType::new(name)), + Type::Unresolved { name } => { + unreachable!("Type `{name}` must be resolved before calling create_code_type") + } + } + } +} + +impl CodeOracle for KotlinCodeOracle { + fn find(&self, type_: &TypeIdentifier) -> Box<dyn CodeType> { + self.create_code_type(type_.clone()) + } + + /// Get the idiomatic Kotlin rendering of a class name (for enums, records, errors, etc). + fn class_name(&self, nm: &str) -> String { + nm.to_string().to_upper_camel_case() + } + + /// Get the idiomatic Kotlin rendering of a function name. + fn fn_name(&self, nm: &str) -> String { + format!("`{}`", nm.to_string().to_lower_camel_case()) + } + + /// Get the idiomatic Kotlin rendering of a variable name. + fn var_name(&self, nm: &str) -> String { + format!("`{}`", nm.to_string().to_lower_camel_case()) + } + + /// Get the idiomatic Kotlin rendering of an individual enum variant. + fn enum_variant_name(&self, nm: &str) -> String { + nm.to_string().to_shouty_snake_case() + } + + /// Get the idiomatic Kotlin rendering of an exception name + /// + /// This replaces "Error" at the end of the name with "Exception". Rust code typically uses + /// "Error" for any type of error but in the Java world, "Error" means a non-recoverable error + /// and is distinguished from an "Exception". + fn error_name(&self, nm: &str) -> String { + // errors are a class in kotlin. + let name = self.class_name(nm); + match name.strip_suffix("Error") { + None => name, + Some(stripped) => format!("{stripped}Exception"), + } + } + + fn ffi_type_label(&self, ffi_type: &FfiType) -> String { + match ffi_type { + // Note that unsigned integers in Kotlin are currently experimental, but java.nio.ByteBuffer does not + // support them yet. Thus, we use the signed variants to represent both signed and unsigned + // types from the component API. + FfiType::Int8 | FfiType::UInt8 => "Byte".to_string(), + FfiType::Int16 | FfiType::UInt16 => "Short".to_string(), + FfiType::Int32 | FfiType::UInt32 => "Int".to_string(), + FfiType::Int64 | FfiType::UInt64 => "Long".to_string(), + FfiType::Float32 => "Float".to_string(), + FfiType::Float64 => "Double".to_string(), + FfiType::RustArcPtr(_) => "Pointer".to_string(), + FfiType::RustBuffer(maybe_suffix) => match maybe_suffix { + Some(suffix) => format!("RustBuffer{}.ByValue", suffix), + None => "RustBuffer.ByValue".to_string(), + }, + FfiType::ForeignBytes => "ForeignBytes.ByValue".to_string(), + FfiType::ForeignCallback => "ForeignCallback".to_string(), + } + } +} + +pub mod filters { + use super::*; + + fn oracle() -> &'static KotlinCodeOracle { + &KotlinCodeOracle + } + + pub fn type_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.type_label(oracle())) + } + + pub fn canonical_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.canonical_name(oracle())) + } + + pub fn ffi_converter_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.ffi_converter_name(oracle())) + } + + pub fn lower_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.lower", codetype.ffi_converter_name(oracle()))) + } + + pub fn allocation_size_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!( + "{}.allocationSize", + codetype.ffi_converter_name(oracle()) + )) + } + + pub fn write_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.write", codetype.ffi_converter_name(oracle()))) + } + + pub fn lift_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.lift", codetype.ffi_converter_name(oracle()))) + } + + pub fn read_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.read", codetype.ffi_converter_name(oracle()))) + } + + pub fn render_literal( + literal: &Literal, + codetype: &impl CodeType, + ) -> Result<String, askama::Error> { + Ok(codetype.literal(oracle(), literal)) + } + + /// Get the Kotlin syntax for representing a given low-level `FfiType`. + pub fn ffi_type_name(type_: &FfiType) -> Result<String, askama::Error> { + Ok(oracle().ffi_type_label(type_)) + } + + /// Get the idiomatic Kotlin rendering of a class name (for enums, records, errors, etc). + pub fn class_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().class_name(nm)) + } + + /// Get the idiomatic Kotlin rendering of a function name. + pub fn fn_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().fn_name(nm)) + } + + /// Get the idiomatic Kotlin rendering of a variable name. + pub fn var_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().var_name(nm)) + } + + /// Get the idiomatic Kotlin rendering of an individual enum variant. + pub fn enum_variant(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().enum_variant_name(nm)) + } + + /// Get the idiomatic Kotlin rendering of an exception name, replacing + /// `Error` with `Exception`. + pub fn exception_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().error_name(nm)) + } + + /// Remove the "`" chars we put around function/variable names + /// + /// These are used to avoid name clashes with kotlin identifiers, but sometimes you want to + /// render the name unquoted. One example is the message property for errors where we want to + /// display the name for the user. + pub fn unquote(nm: &str) -> Result<String, askama::Error> { + Ok(nm.trim_matches('`').to_string()) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/object.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/object.rs new file mode 100644 index 0000000000..7a1cddbafd --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/object.rs @@ -0,0 +1,29 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct ObjectCodeType { + id: String, +} + +impl ObjectCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for ObjectCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/primitives.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/primitives.rs new file mode 100644 index 0000000000..afc8fc556d --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/primitives.rs @@ -0,0 +1,79 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; +use crate::interface::{types::Type, Radix}; +use paste::paste; + +fn render_literal(_oracle: &dyn CodeOracle, literal: &Literal) -> String { + fn typed_number(type_: &Type, num_str: String) -> String { + match type_ { + // Bytes, Shorts and Ints can all be inferred from the type. + Type::Int8 | Type::Int16 | Type::Int32 => num_str, + Type::Int64 => format!("{num_str}L"), + + Type::UInt8 | Type::UInt16 | Type::UInt32 => format!("{num_str}u"), + Type::UInt64 => format!("{num_str}uL"), + + Type::Float32 => format!("{num_str}f"), + Type::Float64 => num_str, + _ => panic!("Unexpected literal: {num_str} is not a number"), + } + } + + match literal { + Literal::Boolean(v) => format!("{v}"), + Literal::String(s) => format!("\"{s}\""), + Literal::Int(i, radix, type_) => typed_number( + type_, + match radix { + Radix::Octal => format!("{i:#x}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + ), + Literal::UInt(i, radix, type_) => typed_number( + type_, + match radix { + Radix::Octal => format!("{i:#x}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + ), + Literal::Float(string, type_) => typed_number(type_, string.clone()), + + _ => unreachable!("Literal"), + } +} + +macro_rules! impl_code_type_for_primitive { + ($T:ty, $class_name:literal) => { + paste! { + pub struct $T; + + impl CodeType for $T { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + $class_name.into() + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + render_literal(oracle, &literal) + } + } + } + }; +} + +impl_code_type_for_primitive!(BooleanCodeType, "Boolean"); +impl_code_type_for_primitive!(StringCodeType, "String"); +impl_code_type_for_primitive!(Int8CodeType, "Byte"); +impl_code_type_for_primitive!(Int16CodeType, "Short"); +impl_code_type_for_primitive!(Int32CodeType, "Int"); +impl_code_type_for_primitive!(Int64CodeType, "Long"); +impl_code_type_for_primitive!(UInt8CodeType, "UByte"); +impl_code_type_for_primitive!(UInt16CodeType, "UShort"); +impl_code_type_for_primitive!(UInt32CodeType, "UInt"); +impl_code_type_for_primitive!(UInt64CodeType, "ULong"); +impl_code_type_for_primitive!(Float32CodeType, "Float"); +impl_code_type_for_primitive!(Float64CodeType, "Double"); diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/record.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/record.rs new file mode 100644 index 0000000000..f923f102c4 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/record.rs @@ -0,0 +1,29 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct RecordCodeType { + id: String, +} + +impl RecordCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for RecordCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/mod.rs new file mode 100644 index 0000000000..c372eab83b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/mod.rs @@ -0,0 +1,43 @@ +/* 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 anyhow::Result; +use camino::{Utf8Path, Utf8PathBuf}; +use fs_err::{self as fs, File}; +use std::{io::Write, process::Command}; + +pub mod gen_kotlin; +pub use gen_kotlin::{generate_bindings, Config}; +mod test; + +use super::super::interface::ComponentInterface; +pub use test::run_test; + +pub fn write_bindings( + config: &Config, + ci: &ComponentInterface, + out_dir: &Utf8Path, + try_format_code: bool, +) -> Result<()> { + let mut kt_file = full_bindings_path(config, out_dir); + fs::create_dir_all(&kt_file)?; + kt_file.push(format!("{}.kt", ci.namespace())); + let mut f = File::create(&kt_file)?; + write!(f, "{}", generate_bindings(config, ci)?)?; + if try_format_code { + if let Err(e) = Command::new("ktlint").arg("-F").arg(&kt_file).output() { + println!( + "Warning: Unable to auto-format {} using ktlint: {:?}", + kt_file.file_name().unwrap(), + e + ) + } + } + Ok(()) +} + +fn full_bindings_path(config: &Config, out_dir: &Utf8Path) -> Utf8PathBuf { + let package_path: Utf8PathBuf = config.package_name().split('.').collect(); + Utf8PathBuf::from(out_dir).join(package_path) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/BooleanHelper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/BooleanHelper.kt new file mode 100644 index 0000000000..8cfa2ce000 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/BooleanHelper.kt @@ -0,0 +1,19 @@ +public object FfiConverterBoolean: FfiConverter<Boolean, Byte> { + override fun lift(value: Byte): Boolean { + return value.toInt() != 0 + } + + override fun read(buf: ByteBuffer): Boolean { + return lift(buf.get()) + } + + override fun lower(value: Boolean): Byte { + return if (value) 1.toByte() else 0.toByte() + } + + override fun allocationSize(value: Boolean) = 1 + + override fun write(value: Boolean, buf: ByteBuffer) { + buf.put(lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt new file mode 100644 index 0000000000..3f5d72a608 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt @@ -0,0 +1,74 @@ +internal typealias Handle = Long +internal class ConcurrentHandleMap<T>( + private val leftMap: MutableMap<Handle, T> = mutableMapOf(), + private val rightMap: MutableMap<T, Handle> = mutableMapOf() +) { + private val lock = java.util.concurrent.locks.ReentrantLock() + private val currentHandle = AtomicLong(0L) + private val stride = 1L + + fun insert(obj: T): Handle = + lock.withLock { + rightMap[obj] ?: + currentHandle.getAndAdd(stride) + .also { handle -> + leftMap[handle] = obj + rightMap[obj] = handle + } + } + + fun get(handle: Handle) = lock.withLock { + leftMap[handle] + } + + fun delete(handle: Handle) { + this.remove(handle) + } + + fun remove(handle: Handle): T? = + lock.withLock { + leftMap.remove(handle)?.let { obj -> + rightMap.remove(obj) + obj + } + } +} + +interface ForeignCallback : com.sun.jna.Callback { + public fun invoke(handle: Handle, method: Int, args: RustBuffer.ByValue, outBuf: RustBufferByReference): Int +} + +// Magic number for the Rust proxy to call using the same mechanism as every other method, +// to free the callback once it's dropped by Rust. +internal const val IDX_CALLBACK_FREE = 0 + +public abstract class FfiConverterCallbackInterface<CallbackInterface>( + protected val foreignCallback: ForeignCallback +): FfiConverter<CallbackInterface, Handle> { + private val handleMap = ConcurrentHandleMap<CallbackInterface>() + + // Registers the foreign callback with the Rust side. + // This method is generated for each callback interface. + internal abstract fun register(lib: _UniFFILib) + + fun drop(handle: Handle): RustBuffer.ByValue { + return handleMap.remove(handle).let { RustBuffer.ByValue() } + } + + override fun lift(value: Handle): CallbackInterface { + return handleMap.get(value) ?: throw InternalException("No callback in handlemap; this is a Uniffi bug") + } + + override fun read(buf: ByteBuffer) = lift(buf.getLong()) + + override fun lower(value: CallbackInterface) = + handleMap.insert(value).also { + assert(handleMap.get(it) === value) { "Handle map is not returning the object we just placed there. This is a bug in the HandleMap." } + } + + override fun allocationSize(value: CallbackInterface) = 8 + + override fun write(value: CallbackInterface, buf: ByteBuffer) { + buf.putLong(lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt new file mode 100644 index 0000000000..c7c4d2b973 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt @@ -0,0 +1,130 @@ +{%- let cbi = ci.get_callback_interface_definition(name).unwrap() %} +{%- let type_name = cbi|type_name %} +{%- let foreign_callback = format!("ForeignCallback{}", canonical_type_name) %} + +{% if self.include_once_check("CallbackInterfaceRuntime.kt") %}{% include "CallbackInterfaceRuntime.kt" %}{% endif %} +{{- self.add_import("java.util.concurrent.atomic.AtomicLong") }} +{{- self.add_import("java.util.concurrent.locks.ReentrantLock") }} +{{- self.add_import("kotlin.concurrent.withLock") }} + +// Declaration and FfiConverters for {{ type_name }} Callback Interface + +public interface {{ type_name }} { + {% for meth in cbi.methods() -%} + fun {{ meth.name()|fn_name }}({% call kt::arg_list_decl(meth) %}) + {%- match meth.return_type() -%} + {%- when Some with (return_type) %}: {{ return_type|type_name -}} + {%- else -%} + {%- endmatch %} + {% endfor %} +} + +// The ForeignCallback that is passed to Rust. +internal class {{ foreign_callback }} : ForeignCallback { + @Suppress("TooGenericExceptionCaught") + override fun invoke(handle: Handle, method: Int, args: RustBuffer.ByValue, outBuf: RustBufferByReference): Int { + val cb = {{ ffi_converter_name }}.lift(handle) + return when (method) { + IDX_CALLBACK_FREE -> { + {{ ffi_converter_name }}.drop(handle) + // No return value. + // See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + 0 + } + {% for meth in cbi.methods() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} + {{ loop.index }} -> { + // Call the method, write to outBuf and return a status code + // See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` for info + try { + {%- match meth.throws_type() %} + {%- when Some(error_type) %} + try { + val buffer = this.{{ method_name }}(cb, args) + // Success + outBuf.setValue(buffer) + 1 + } catch (e: {{ error_type|type_name }}) { + // Expected error + val buffer = {{ error_type|ffi_converter_name }}.lowerIntoRustBuffer(e) + outBuf.setValue(buffer) + -2 + } + {%- else %} + val buffer = this.{{ method_name }}(cb, args) + // Success + outBuf.setValue(buffer) + 1 + {%- endmatch %} + } catch (e: Throwable) { + // Unexpected error + try { + // Try to serialize the error into a string + outBuf.setValue({{ Type::String.borrow()|ffi_converter_name }}.lower(e.toString())) + } catch (e: Throwable) { + // If that fails, then it's time to give up and just return + } + -1 + } + } + {% endfor %} + else -> { + // An unexpected error happened. + // See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + try { + // Try to serialize the error into a string + outBuf.setValue({{ Type::String.borrow()|ffi_converter_name }}.lower("Invalid Callback index")) + } catch (e: Throwable) { + // If that fails, then it's time to give up and just return + } + -1 + } + } + } + + {% for meth in cbi.methods() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name %} + private fun {{ method_name }}(kotlinCallbackInterface: {{ type_name }}, args: RustBuffer.ByValue): RustBuffer.ByValue = + try { + {#- Unpacking args from the RustBuffer #} + {%- if meth.arguments().len() != 0 -%} + {#- Calling the concrete callback object #} + val buf = args.asByteBuffer() ?: throw InternalException("No ByteBuffer in RustBuffer; this is a Uniffi bug") + kotlinCallbackInterface.{{ meth.name()|fn_name }}( + {% for arg in meth.arguments() -%} + {{ arg|read_fn }}(buf) + {%- if !loop.last %}, {% endif %} + {% endfor -%} + ) + {% else %} + kotlinCallbackInterface.{{ meth.name()|fn_name }}() + {% endif -%} + + {#- Packing up the return value into a RustBuffer #} + {%- match meth.return_type() -%} + {%- when Some with (return_type) -%} + .let { + {{ return_type|ffi_converter_name }}.lowerIntoRustBuffer(it) + } + {%- else -%} + .let { RustBuffer.ByValue() } + {% endmatch -%} + // TODO catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } finally { + RustBuffer.free(args) + } + + {% endfor %} +} + +// The ffiConverter which transforms the Callbacks in to Handles to pass to Rust. +public object {{ ffi_converter_name }}: FfiConverterCallbackInterface<{{ type_name }}>( + foreignCallback = {{ foreign_callback }}() +) { + override fun register(lib: _UniFFILib) { + rustCall() { status -> + lib.{{ cbi.ffi_init_callback().name() }}(this.foreignCallback, status) + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CustomTypeTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CustomTypeTemplate.kt new file mode 100644 index 0000000000..c7807f2f33 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/CustomTypeTemplate.kt @@ -0,0 +1,62 @@ +{%- match kotlin_config.custom_types.get(name.as_str()) %} +{%- when None %} +{#- Define the type using typealiases to the builtin #} +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias {{ name }} = {{ builtin|type_name }} +public typealias {{ ffi_converter_name }} = {{ builtin|ffi_converter_name }} + +{%- when Some with (config) %} + +{%- let ffi_type_name=builtin.ffi_type().borrow()|ffi_type_name %} + +{# When the config specifies a different type name, create a typealias for it #} +{%- match config.type_name %} +{%- when Some(concrete_type_name) %} +/** + * Typealias from the type name used in the UDL file to the custom type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias {{ name }} = {{ concrete_type_name }} +{%- else %} +{%- endmatch %} + +{%- match config.imports %} +{%- when Some(imports) %} +{%- for import_name in imports %} +{{ self.add_import(import_name) }} +{%- endfor %} +{%- else %} +{%- endmatch %} + +public object {{ ffi_converter_name }}: FfiConverter<{{ name }}, {{ ffi_type_name }}> { + override fun lift(value: {{ ffi_type_name }}): {{ name }} { + val builtinValue = {{ builtin|lift_fn }}(value) + return {{ config.into_custom.render("builtinValue") }} + } + + override fun lower(value: {{ name }}): {{ ffi_type_name }} { + val builtinValue = {{ config.from_custom.render("value") }} + return {{ builtin|lower_fn }}(builtinValue) + } + + override fun read(buf: ByteBuffer): {{ name }} { + val builtinValue = {{ builtin|read_fn }}(buf) + return {{ config.into_custom.render("builtinValue") }} + } + + override fun allocationSize(value: {{ name }}): Int { + val builtinValue = {{ config.from_custom.render("value") }} + return {{ builtin|allocation_size_fn }}(builtinValue) + } + + override fun write(value: {{ name }}, buf: ByteBuffer) { + val builtinValue = {{ config.from_custom.render("value") }} + {{ builtin|write_fn }}(builtinValue, buf) + } +} +{%- endmatch %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/DurationHelper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/DurationHelper.kt new file mode 100644 index 0000000000..4237c6f9a8 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/DurationHelper.kt @@ -0,0 +1,36 @@ +public object FfiConverterDuration: FfiConverterRustBuffer<java.time.Duration> { + override fun read(buf: ByteBuffer): java.time.Duration { + // Type mismatch (should be u64) but we check for overflow/underflow below + val seconds = buf.getLong() + // Type mismatch (should be u32) but we check for overflow/underflow below + val nanoseconds = buf.getInt().toLong() + if (seconds < 0) { + throw java.time.DateTimeException("Duration exceeds minimum or maximum value supported by uniffi") + } + if (nanoseconds < 0) { + throw java.time.DateTimeException("Duration nanoseconds exceed minimum or maximum supported by uniffi") + } + return java.time.Duration.ofSeconds(seconds, nanoseconds) + } + + // 8 bytes for seconds, 4 bytes for nanoseconds + override fun allocationSize(value: java.time.Duration) = 12 + + override fun write(value: java.time.Duration, buf: ByteBuffer) { + if (value.seconds < 0) { + // Rust does not support negative Durations + throw IllegalArgumentException("Invalid duration, must be non-negative") + } + + if (value.nano < 0) { + // Java docs provide guarantee that nano will always be positive, so this should be impossible + // See: https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html + throw IllegalArgumentException("Invalid duration, nano value must be non-negative") + } + + // Type mismatch (should be u64) but since Rust doesn't support negative durations we should be OK + buf.putLong(value.seconds) + // Type mismatch (should be u32) but since values will always be between 0 and 999,999,999 it should be OK + buf.putInt(value.nano) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt new file mode 100644 index 0000000000..be198ac7b9 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt @@ -0,0 +1,107 @@ +{# +// Kotlin's `enum class` construct doesn't support variants with associated data, +// but is a little nicer for consumers than its `sealed class` enum pattern. +// So, we switch here, using `enum class` for enums with no associated data +// and `sealed class` for the general case. +#} +{%- let e = ci.get_enum_definition(name).unwrap() %} + +{%- if e.is_flat() %} + +enum class {{ type_name }} { + {% for variant in e.variants() -%} + {{ variant.name()|enum_variant }}{% if loop.last %};{% else %},{% endif %} + {%- endfor %} +} + +public object {{ e|ffi_converter_name }}: FfiConverterRustBuffer<{{ type_name }}> { + override fun read(buf: ByteBuffer) = try { + {{ type_name }}.values()[buf.getInt() - 1] + } catch (e: IndexOutOfBoundsException) { + throw RuntimeException("invalid enum value, something is very wrong!!", e) + } + + override fun allocationSize(value: {{ type_name }}) = 4 + + override fun write(value: {{ type_name }}, buf: ByteBuffer) { + buf.putInt(value.ordinal + 1) + } +} + +{% else %} + +sealed class {{ type_name }}{% if contains_object_references %}: Disposable {% endif %} { + {% for variant in e.variants() -%} + {% if !variant.has_fields() -%} + object {{ variant.name()|class_name }} : {{ type_name }}() + {% else -%} + data class {{ variant.name()|class_name }}( + {% for field in variant.fields() -%} + val {{ field.name()|var_name }}: {{ field|type_name}}{% if loop.last %}{% else %}, {% endif %} + {% endfor -%} + ) : {{ type_name }}() + {%- endif %} + {% endfor %} + + {% if contains_object_references %} + @Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here + override fun destroy() { + when(this) { + {%- for variant in e.variants() %} + is {{ type_name }}.{{ variant.name()|class_name }} -> { + {%- if variant.has_fields() %} + {% call kt::destroy_fields(variant) %} + {% else -%} + // Nothing to destroy + {%- endif %} + } + {%- endfor %} + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + {% endif %} +} + +public object {{ e|ffi_converter_name }} : FfiConverterRustBuffer<{{ type_name }}>{ + override fun read(buf: ByteBuffer): {{ type_name }} { + return when(buf.getInt()) { + {%- for variant in e.variants() %} + {{ loop.index }} -> {{ type_name }}.{{ variant.name()|class_name }}{% if variant.has_fields() %}( + {% for field in variant.fields() -%} + {{ field|read_fn }}(buf), + {% endfor -%} + ){%- endif -%} + {%- endfor %} + else -> throw RuntimeException("invalid enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: {{ type_name }}) = when(value) { + {%- for variant in e.variants() %} + is {{ type_name }}.{{ variant.name()|class_name }} -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4 + {%- for field in variant.fields() %} + + {{ field|allocation_size_fn }}(value.{{ field.name()|var_name }}) + {%- endfor %} + ) + } + {%- endfor %} + } + + override fun write(value: {{ type_name }}, buf: ByteBuffer) { + when(value) { + {%- for variant in e.variants() %} + is {{ type_name }}.{{ variant.name()|class_name }} -> { + buf.putInt({{ loop.index }}) + {%- for field in variant.fields() %} + {{ field|write_fn }}(value.{{ field.name()|var_name }}, buf) + {%- endfor %} + Unit + } + {%- endfor %} + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } +} + +{% endif %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt new file mode 100644 index 0000000000..9ddef9bb73 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt @@ -0,0 +1,109 @@ +{%- let e = ci.get_error_definition(name).unwrap() %} + +{% if e.is_flat() %} +sealed class {{ type_name }}(message: String): Exception(message){% if contains_object_references %}, Disposable {% endif %} { + // Each variant is a nested class + // Flat enums carries a string error message, so no special implementation is necessary. + {% for variant in e.variants() -%} + class {{ variant.name()|exception_name }}(message: String) : {{ type_name }}(message) + {% endfor %} + + companion object ErrorHandler : CallStatusErrorHandler<{{ type_name }}> { + override fun lift(error_buf: RustBuffer.ByValue): {{ type_name }} = {{ e|lift_fn }}(error_buf) + } +} +{%- else %} +sealed class {{ type_name }}: Exception(){% if contains_object_references %}, Disposable {% endif %} { + // Each variant is a nested class + {% for variant in e.variants() -%} + {%- let variant_name = variant.name()|exception_name %} + class {{ variant_name }}( + {% for field in variant.fields() -%} + val {{ field.name()|var_name }}: {{ field|type_name}}{% if loop.last %}{% else %}, {% endif %} + {% endfor -%} + ) : {{ type_name }}() { + override val message + get() = "{%- for field in variant.fields() %}{{ field.name()|var_name|unquote }}=${ {{field.name()|var_name }} }{% if !loop.last %}, {% endif %}{% endfor %}" + } + {% endfor %} + + companion object ErrorHandler : CallStatusErrorHandler<{{ type_name }}> { + override fun lift(error_buf: RustBuffer.ByValue): {{ type_name }} = {{ e|lift_fn }}(error_buf) + } + + {% if contains_object_references %} + @Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here + override fun destroy() { + when(this) { + {%- for variant in e.variants() %} + is {{ type_name }}.{{ variant.name()|exception_name }} -> { + {%- if variant.has_fields() %} + {% call kt::destroy_fields(variant) %} + {% else -%} + // Nothing to destroy + {%- endif %} + } + {%- endfor %} + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + {% endif %} +} +{%- endif %} + +public object {{ e|ffi_converter_name }} : FfiConverterRustBuffer<{{ type_name }}> { + override fun read(buf: ByteBuffer): {{ type_name }} { + {% if e.is_flat() %} + return when(buf.getInt()) { + {%- for variant in e.variants() %} + {{ loop.index }} -> {{ type_name }}.{{ variant.name()|exception_name }}({{ TypeIdentifier::String.borrow()|read_fn }}(buf)) + {%- endfor %} + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + {% else %} + + return when(buf.getInt()) { + {%- for variant in e.variants() %} + {{ loop.index }} -> {{ type_name }}.{{ variant.name()|exception_name }}({% if variant.has_fields() %} + {% for field in variant.fields() -%} + {{ field|read_fn }}(buf), + {% endfor -%} + {%- endif -%}) + {%- endfor %} + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + {%- endif %} + } + + override fun allocationSize(value: {{ type_name }}): Int { + {%- if e.is_flat() %} + return 4 + {%- else %} + return when(value) { + {%- for variant in e.variants() %} + is {{ type_name }}.{{ variant.name()|exception_name }} -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + {%- for field in variant.fields() %} + + {{ field|allocation_size_fn }}(value.{{ field.name()|var_name }}) + {%- endfor %} + ) + {%- endfor %} + } + {%- endif %} + } + + override fun write(value: {{ type_name }}, buf: ByteBuffer) { + when(value) { + {%- for variant in e.variants() %} + is {{ type_name }}.{{ variant.name()|exception_name }} -> { + buf.putInt({{ loop.index }}) + {%- for field in variant.fields() %} + {{ field|write_fn }}(value.{{ field.name()|var_name }}, buf) + {%- endfor %} + Unit + } + {%- endfor %} + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ExternalTypeTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ExternalTypeTemplate.kt new file mode 100644 index 0000000000..21d96dcdf1 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ExternalTypeTemplate.kt @@ -0,0 +1,9 @@ +{%- let package_name=self.external_type_package_name(crate_name) %} +{%- let fully_qualified_type_name = "{}.{}"|format(package_name, name) %} +{%- let fully_qualified_ffi_converter_name = "{}.FfiConverterType{}"|format(package_name, name) %} +{%- let fully_qualified_rustbuffer_name = "{}.RustBuffer"|format(package_name) %} +{%- let local_rustbuffer_name = "RustBuffer{}"|format(name) %} + +{{- self.add_import(fully_qualified_type_name) }} +{{- self.add_import(fully_qualified_ffi_converter_name) }} +{{ self.add_import_as(fully_qualified_rustbuffer_name, local_rustbuffer_name) }} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/FfiConverterTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/FfiConverterTemplate.kt new file mode 100644 index 0000000000..3b2c9d225a --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/FfiConverterTemplate.kt @@ -0,0 +1,71 @@ +// The FfiConverter interface handles converter types to and from the FFI +// +// All implementing objects should be public to support external types. When a +// type is external we need to import it's FfiConverter. +public interface FfiConverter<KotlinType, FfiType> { + // Convert an FFI type to a Kotlin type + fun lift(value: FfiType): KotlinType + + // Convert an Kotlin type to an FFI type + fun lower(value: KotlinType): FfiType + + // Read a Kotlin type from a `ByteBuffer` + fun read(buf: ByteBuffer): KotlinType + + // Calculate bytes to allocate when creating a `RustBuffer` + // + // This must return at least as many bytes as the write() function will + // write. It can return more bytes than needed, for example when writing + // Strings we can't know the exact bytes needed until we the UTF-8 + // encoding, so we pessimistically allocate the largest size possible (3 + // bytes per codepoint). Allocating extra bytes is not really a big deal + // because the `RustBuffer` is short-lived. + fun allocationSize(value: KotlinType): Int + + // Write a Kotlin type to a `ByteBuffer` + fun write(value: KotlinType, buf: ByteBuffer) + + // Lower a value into a `RustBuffer` + // + // This method lowers a value into a `RustBuffer` rather than the normal + // FfiType. It's used by the callback interface code. Callback interface + // returns are always serialized into a `RustBuffer` regardless of their + // normal FFI type. + fun lowerIntoRustBuffer(value: KotlinType): RustBuffer.ByValue { + val rbuf = RustBuffer.alloc(allocationSize(value)) + try { + val bbuf = rbuf.data!!.getByteBuffer(0, rbuf.capacity.toLong()).also { + it.order(ByteOrder.BIG_ENDIAN) + } + write(value, bbuf) + rbuf.writeField("len", bbuf.position()) + return rbuf + } catch (e: Throwable) { + RustBuffer.free(rbuf) + throw e + } + } + + // Lift a value from a `RustBuffer`. + // + // This here mostly because of the symmetry with `lowerIntoRustBuffer()`. + // It's currently only used by the `FfiConverterRustBuffer` class below. + fun liftFromRustBuffer(rbuf: RustBuffer.ByValue): KotlinType { + val byteBuf = rbuf.asByteBuffer()!! + try { + val item = read(byteBuf) + if (byteBuf.hasRemaining()) { + throw RuntimeException("junk remaining in buffer after lifting, something is very wrong!!") + } + return item + } finally { + RustBuffer.free(rbuf) + } + } +} + +// FfiConverter that uses `RustBuffer` as the FfiType +public interface FfiConverterRustBuffer<KotlinType>: FfiConverter<KotlinType, RustBuffer.ByValue> { + override fun lift(value: RustBuffer.ByValue) = liftFromRustBuffer(value) + override fun lower(value: KotlinType) = lowerIntoRustBuffer(value) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Float32Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Float32Helper.kt new file mode 100644 index 0000000000..eafec5d122 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Float32Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterFloat: FfiConverter<Float, Float> { + override fun lift(value: Float): Float { + return value + } + + override fun read(buf: ByteBuffer): Float { + return buf.getFloat() + } + + override fun lower(value: Float): Float { + return value + } + + override fun allocationSize(value: Float) = 4 + + override fun write(value: Float, buf: ByteBuffer) { + buf.putFloat(value) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Float64Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Float64Helper.kt new file mode 100644 index 0000000000..9fc2892c95 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Float64Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterDouble: FfiConverter<Double, Double> { + override fun lift(value: Double): Double { + return value + } + + override fun read(buf: ByteBuffer): Double { + return buf.getDouble() + } + + override fun lower(value: Double): Double { + return value + } + + override fun allocationSize(value: Double) = 8 + + override fun write(value: Double, buf: ByteBuffer) { + buf.putDouble(value) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt new file mode 100644 index 0000000000..d7e61ef4c7 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt @@ -0,0 +1,66 @@ +// A handful of classes and functions to support the generated data structures. +// This would be a good candidate for isolating in its own ffi-support lib. +// Error runtime. +@Structure.FieldOrder("code", "error_buf") +internal open class RustCallStatus : Structure() { + @JvmField var code: Int = 0 + @JvmField var error_buf: RustBuffer.ByValue = RustBuffer.ByValue() + + fun isSuccess(): Boolean { + return code == 0 + } + + fun isError(): Boolean { + return code == 1 + } + + fun isPanic(): Boolean { + return code == 2 + } +} + +class InternalException(message: String) : Exception(message) + +// Each top-level error class has a companion object that can lift the error from the call status's rust buffer +interface CallStatusErrorHandler<E> { + fun lift(error_buf: RustBuffer.ByValue): E; +} + +// Helpers for calling Rust +// In practice we usually need to be synchronized to call this safely, so it doesn't +// synchronize itself + +// Call a rust function that returns a Result<>. Pass in the Error class companion that corresponds to the Err +private inline fun <U, E: Exception> rustCallWithError(errorHandler: CallStatusErrorHandler<E>, callback: (RustCallStatus) -> U): U { + var status = RustCallStatus(); + val return_value = callback(status) + if (status.isSuccess()) { + return return_value + } else if (status.isError()) { + throw errorHandler.lift(status.error_buf) + } else if (status.isPanic()) { + // when the rust code sees a panic, it tries to construct a rustbuffer + // with the message. but if that code panics, then it just sends back + // an empty buffer. + if (status.error_buf.len > 0) { + throw InternalException({{ TypeIdentifier::String.borrow()|lift_fn }}(status.error_buf)) + } else { + throw InternalException("Rust panic") + } + } else { + throw InternalException("Unknown rust call status: $status.code") + } +} + +// CallStatusErrorHandler implementation for times when we don't expect a CALL_ERROR +object NullCallStatusErrorHandler: CallStatusErrorHandler<InternalException> { + override fun lift(error_buf: RustBuffer.ByValue): InternalException { + RustBuffer.free(error_buf) + return InternalException("Unexpected CALL_ERROR") + } +} + +// Call a rust function that returns a plain value +private inline fun <U> rustCall(callback: (RustCallStatus) -> U): U { + return rustCallWithError(NullCallStatusErrorHandler, callback); +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int16Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int16Helper.kt new file mode 100644 index 0000000000..75564276be --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int16Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterShort: FfiConverter<Short, Short> { + override fun lift(value: Short): Short { + return value + } + + override fun read(buf: ByteBuffer): Short { + return buf.getShort() + } + + override fun lower(value: Short): Short { + return value + } + + override fun allocationSize(value: Short) = 2 + + override fun write(value: Short, buf: ByteBuffer) { + buf.putShort(value) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int32Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int32Helper.kt new file mode 100644 index 0000000000..b7a8131c8b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int32Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterInt: FfiConverter<Int, Int> { + override fun lift(value: Int): Int { + return value + } + + override fun read(buf: ByteBuffer): Int { + return buf.getInt() + } + + override fun lower(value: Int): Int { + return value + } + + override fun allocationSize(value: Int) = 4 + + override fun write(value: Int, buf: ByteBuffer) { + buf.putInt(value) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int64Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int64Helper.kt new file mode 100644 index 0000000000..601cfc7c2c --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int64Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterLong: FfiConverter<Long, Long> { + override fun lift(value: Long): Long { + return value + } + + override fun read(buf: ByteBuffer): Long { + return buf.getLong() + } + + override fun lower(value: Long): Long { + return value + } + + override fun allocationSize(value: Long) = 8 + + override fun write(value: Long, buf: ByteBuffer) { + buf.putLong(value) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int8Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int8Helper.kt new file mode 100644 index 0000000000..9237768dbf --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Int8Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterByte: FfiConverter<Byte, Byte> { + override fun lift(value: Byte): Byte { + return value + } + + override fun read(buf: ByteBuffer): Byte { + return buf.get() + } + + override fun lower(value: Byte): Byte { + return value + } + + override fun allocationSize(value: Byte) = 1 + + override fun write(value: Byte, buf: ByteBuffer) { + buf.put(value) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt new file mode 100644 index 0000000000..c8cbecb68b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/MapTemplate.kt @@ -0,0 +1,35 @@ +{%- let key_type_name = key_type|type_name %} +{%- let value_type_name = value_type|type_name %} +public object {{ ffi_converter_name }}: FfiConverterRustBuffer<Map<{{ key_type_name }}, {{ value_type_name }}>> { + override fun read(buf: ByteBuffer): Map<{{ key_type_name }}, {{ value_type_name }}> { + // TODO: Once Kotlin's `buildMap` API is stabilized we should use it here. + val items : MutableMap<{{ key_type_name }}, {{ value_type_name }}> = mutableMapOf() + val len = buf.getInt() + repeat(len) { + val k = {{ key_type|read_fn }}(buf) + val v = {{ value_type|read_fn }}(buf) + items[k] = v + } + return items + } + + override fun allocationSize(value: Map<{{ key_type_name }}, {{ value_type_name }}>): Int { + val spaceForMapSize = 4 + val spaceForChildren = value.map { (k, v) -> + {{ key_type|allocation_size_fn }}(k) + + {{ value_type|allocation_size_fn }}(v) + }.sum() + return spaceForMapSize + spaceForChildren + } + + override fun write(value: Map<{{ key_type_name }}, {{ value_type_name }}>, buf: ByteBuffer) { + buf.putInt(value.size) + // The parens on `(k, v)` here ensure we're calling the right method, + // which is important for compatibility with older android devices. + // Ref https://blog.danlew.net/2017/03/16/kotlin-puzzler-whose-line-is-it-anyways/ + value.forEach { (k, v) -> + {{ key_type|write_fn }}(k, buf) + {{ value_type|write_fn }}(v, buf) + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt new file mode 100644 index 0000000000..df96ee5cbb --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt @@ -0,0 +1,40 @@ +@Synchronized +private fun findLibraryName(componentName: String): String { + val libOverride = System.getProperty("uniffi.component.$componentName.libraryOverride") + if (libOverride != null) { + return libOverride + } + return "{{ config.cdylib_name() }}" +} + +private inline fun <reified Lib : Library> loadIndirect( + componentName: String +): Lib { + return Native.load<Lib>(findLibraryName(componentName), Lib::class.java) +} + +// A JNA Library to expose the extern-C FFI definitions. +// This is an implementation detail which will be called internally by the public API. + +internal interface _UniFFILib : Library { + companion object { + internal val INSTANCE: _UniFFILib by lazy { + loadIndirect<_UniFFILib>(componentName = "{{ ci.namespace() }}") + {% let initialization_fns = self.initialization_fns() %} + {%- if !initialization_fns.is_empty() -%} + .also { lib: _UniFFILib -> + {% for fn in initialization_fns -%} + {{ fn }}(lib) + {% endfor -%} + } + {% endif %} + } + } + + {% for func in ci.iter_ffi_function_definitions() -%} + fun {{ func.name() }}( + {%- call kt::arg_list_ffi_decl(func) %} + ){%- match func.return_type() -%}{%- when Some with (type_) %}: {{ type_.borrow()|ffi_type_name }}{% when None %}: Unit{% endmatch %} + + {% endfor %} +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt new file mode 100644 index 0000000000..b9352c690f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt @@ -0,0 +1,161 @@ +// Interface implemented by anything that can contain an object reference. +// +// Such types expose a `destroy()` method that must be called to cleanly +// dispose of the contained objects. Failure to call this method may result +// in memory leaks. +// +// The easiest way to ensure this method is called is to use the `.use` +// helper method to execute a block and destroy the object at the end. +interface Disposable { + fun destroy() + companion object { + fun destroy(vararg args: Any?) { + args.filterIsInstance<Disposable>() + .forEach(Disposable::destroy) + } + } +} + +inline fun <T : Disposable?, R> T.use(block: (T) -> R) = + try { + block(this) + } finally { + try { + // N.B. our implementation is on the nullable type `Disposable?`. + this?.destroy() + } catch (e: Throwable) { + // swallow + } + } + +// The base class for all UniFFI Object types. +// +// This class provides core operations for working with the Rust `Arc<T>` pointer to +// the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// the Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each `FFIObject` instance holds an opaque pointer to the underlying Rust struct. +// Method calls need to read this pointer from the object's state and pass it in to +// the Rust FFI. +// +// * When an `FFIObject` is no longer needed, its pointer should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an `FFIObject` instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so will +// leak the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// If we try to implement this with mutual exclusion on access to the pointer, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the pointer, but is interrupted +// before it can pass the pointer over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read pointer value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each `FFIObject` an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc<T>` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// In the future we may be able to replace some of this with automatic finalization logic, such as using +// the new "Cleaner" functionaility in Java 9. The above scheme has been designed to work even if `destroy` is +// invoked by garbage-collection machinery rather than by calling code (which by the way, it's apparently also +// possible for the JVM to finalize an object while there is an in-flight call to one of its methods [1], +// so there would still be some complexity here). +// +// Sigh...all of this for want of a robust finalization mechanism. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// +abstract class FFIObject( + protected val pointer: Pointer +): Disposable, AutoCloseable { + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + open protected fun freeRustArcPtr() { + // To be overridden in subclasses. + } + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + this.freeRustArcPtr() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun <R> callWithPointer(block: (ptr: Pointer) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (! this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the pointer being freed concurrently. + try { + return block(this.pointer) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + this.freeRustArcPtr() + } + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt new file mode 100644 index 0000000000..5d654d9fae --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt @@ -0,0 +1,100 @@ +{%- let obj = ci.get_object_definition(name).unwrap() %} +{%- if self.include_once_check("ObjectRuntime.kt") %}{% include "ObjectRuntime.kt" %}{% endif %} +{{- self.add_import("java.util.concurrent.atomic.AtomicLong") }} +{{- self.add_import("java.util.concurrent.atomic.AtomicBoolean") }} + +public interface {{ type_name }}Interface { + {% for meth in obj.methods() -%} + {%- match meth.throws_type() -%} + {%- when Some with (throwable) %} + @Throws({{ throwable|type_name }}::class) + {%- else -%} + {%- endmatch %} + fun {{ meth.name()|fn_name }}({% call kt::arg_list_decl(meth) %}) + {%- match meth.return_type() -%} + {%- when Some with (return_type) %}: {{ return_type|type_name -}} + {%- else -%} + {%- endmatch %} + {% endfor %} +} + +class {{ type_name }}( + pointer: Pointer +) : FFIObject(pointer), {{ type_name }}Interface { + + {%- match obj.primary_constructor() %} + {%- when Some with (cons) %} + constructor({% call kt::arg_list_decl(cons) -%}) : + this({% call kt::to_ffi_call(cons) %}) + {%- when None %} + {%- endmatch %} + + /** + * Disconnect the object from the underlying Rust object. + * + * It can be called more than once, but once called, interacting with the object + * causes an `IllegalStateException`. + * + * Clients **must** call this method once done with the object, or cause a memory leak. + */ + override protected fun freeRustArcPtr() { + rustCall() { status -> + _UniFFILib.INSTANCE.{{ obj.ffi_object_free().name() }}(this.pointer, status) + } + } + + {% for meth in obj.methods() -%} + {%- match meth.throws_type() -%} + {%- when Some with (throwable) %} + @Throws({{ throwable|type_name }}::class) + {%- else -%} + {%- endmatch %} + {%- match meth.return_type() -%} + + {%- when Some with (return_type) -%} + override fun {{ meth.name()|fn_name }}({% call kt::arg_list_protocol(meth) %}): {{ return_type|type_name }} = + callWithPointer { + {%- call kt::to_ffi_call_with_prefix("it", meth) %} + }.let { + {{ return_type|lift_fn }}(it) + } + + {%- when None -%} + override fun {{ meth.name()|fn_name }}({% call kt::arg_list_protocol(meth) %}) = + callWithPointer { + {%- call kt::to_ffi_call_with_prefix("it", meth) %} + } + {% endmatch %} + {% endfor %} + + {% if !obj.alternate_constructors().is_empty() -%} + companion object { + {% for cons in obj.alternate_constructors() -%} + fun {{ cons.name()|fn_name }}({% call kt::arg_list_decl(cons) %}): {{ type_name }} = + {{ type_name }}({% call kt::to_ffi_call(cons) %}) + {% endfor %} + } + {% endif %} +} + +public object {{ obj|ffi_converter_name }}: FfiConverter<{{ type_name }}, Pointer> { + override fun lower(value: {{ type_name }}): Pointer = value.callWithPointer { it } + + override fun lift(value: Pointer): {{ type_name }} { + return {{ type_name }}(value) + } + + override fun read(buf: ByteBuffer): {{ type_name }} { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: {{ type_name }}) = 8 + + override fun write(value: {{ type_name }}, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/OptionalTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/OptionalTemplate.kt new file mode 100644 index 0000000000..6830b51863 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/OptionalTemplate.kt @@ -0,0 +1,27 @@ +{%- let inner_type_name = inner_type|type_name %} + +public object {{ ffi_converter_name }}: FfiConverterRustBuffer<{{ inner_type_name }}?> { + override fun read(buf: ByteBuffer): {{ inner_type_name }}? { + if (buf.get().toInt() == 0) { + return null + } + return {{ inner_type|read_fn }}(buf) + } + + override fun allocationSize(value: {{ inner_type_name }}?): Int { + if (value == null) { + return 1 + } else { + return 1 + {{ inner_type|allocation_size_fn }}(value) + } + } + + override fun write(value: {{ inner_type_name }}?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + {{ inner_type|write_fn }}(value, buf) + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt new file mode 100644 index 0000000000..a17f4d42eb --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt @@ -0,0 +1,41 @@ +{%- let rec = ci.get_record_definition(name).unwrap() %} + +data class {{ type_name }} ( + {%- for field in rec.fields() %} + var {{ field.name()|var_name }}: {{ field|type_name -}} + {%- match field.default_value() %} + {%- when Some with(literal) %} = {{ literal|render_literal(field) }} + {%- else %} + {%- endmatch -%} + {% if !loop.last %}, {% endif %} + {%- endfor %} +) {% if contains_object_references %}: Disposable {% endif %}{ + {% if contains_object_references %} + @Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here + override fun destroy() { + {% call kt::destroy_fields(rec) %} + } + {% endif %} +} + +public object {{ rec|ffi_converter_name }}: FfiConverterRustBuffer<{{ type_name }}> { + override fun read(buf: ByteBuffer): {{ type_name }} { + return {{ type_name }}( + {%- for field in rec.fields() %} + {{ field|read_fn }}(buf), + {%- endfor %} + ) + } + + override fun allocationSize(value: {{ type_name }}) = ( + {%- for field in rec.fields() %} + {{ field|allocation_size_fn }}(value.{{ field.name()|var_name }}){% if !loop.last %} +{% endif%} + {%- endfor %} + ) + + override fun write(value: {{ type_name }}, buf: ByteBuffer) { + {%- for field in rec.fields() %} + {{ field|write_fn }}(value.{{ field.name()|var_name }}, buf) + {%- endfor %} + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt new file mode 100644 index 0000000000..6cc218e940 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt @@ -0,0 +1,66 @@ +// This is a helper for safely working with byte buffers returned from the Rust code. +// A rust-owned buffer is represented by its capacity, its current length, and a +// pointer to the underlying data. + +@Structure.FieldOrder("capacity", "len", "data") +open class RustBuffer : Structure() { + @JvmField var capacity: Int = 0 + @JvmField var len: Int = 0 + @JvmField var data: Pointer? = null + + class ByValue : RustBuffer(), Structure.ByValue + class ByReference : RustBuffer(), Structure.ByReference + + companion object { + internal fun alloc(size: Int = 0) = rustCall() { status -> + _UniFFILib.INSTANCE.{{ ci.ffi_rustbuffer_alloc().name() }}(size, status).also { + if(it.data == null) { + throw RuntimeException("RustBuffer.alloc() returned null data pointer (size=${size})") + } + } + } + + internal fun free(buf: RustBuffer.ByValue) = rustCall() { status -> + _UniFFILib.INSTANCE.{{ ci.ffi_rustbuffer_free().name() }}(buf, status) + } + } + + @Suppress("TooGenericExceptionThrown") + fun asByteBuffer() = + this.data?.getByteBuffer(0, this.len.toLong())?.also { + it.order(ByteOrder.BIG_ENDIAN) + } +} + +/** + * The equivalent of the `*mut RustBuffer` type. + * Required for callbacks taking in an out pointer. + * + * Size is the sum of all values in the struct. + */ +class RustBufferByReference : ByReference(16) { + /** + * Set the pointed-to `RustBuffer` to the given value. + */ + fun setValue(value: RustBuffer.ByValue) { + // NOTE: The offsets are as they are in the C-like struct. + val pointer = getPointer() + pointer.setInt(0, value.capacity) + pointer.setInt(4, value.len) + pointer.setPointer(8, value.data) + } +} + +// This is a helper for safely passing byte references into the rust code. +// It's not actually used at the moment, because there aren't many things that you +// can take a direct pointer to in the JVM, and if we're going to copy something +// then we might as well copy it into a `RustBuffer`. But it's here for API +// completeness. + +@Structure.FieldOrder("len", "data") +open class ForeignBytes : Structure() { + @JvmField var len: Int = 0 + @JvmField var data: Pointer? = null + + class ByValue : ForeignBytes(), Structure.ByValue +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/SequenceTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/SequenceTemplate.kt new file mode 100644 index 0000000000..b5c699ba3b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/SequenceTemplate.kt @@ -0,0 +1,23 @@ +{%- let inner_type_name = inner_type|type_name %} + +public object {{ ffi_converter_name }}: FfiConverterRustBuffer<List<{{ inner_type_name }}>> { + override fun read(buf: ByteBuffer): List<{{ inner_type_name }}> { + val len = buf.getInt() + return List<{{ inner_type_name }}>(len) { + {{ inner_type|read_fn }}(buf) + } + } + + override fun allocationSize(value: List<{{ inner_type_name }}>): Int { + val sizeForLength = 4 + val sizeForItems = value.map { {{ inner_type|allocation_size_fn }}(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List<{{ inner_type_name }}>, buf: ByteBuffer) { + buf.putInt(value.size) + value.forEach { + {{ inner_type|write_fn }}(it, buf) + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/StringHelper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/StringHelper.kt new file mode 100644 index 0000000000..d3443215c1 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/StringHelper.kt @@ -0,0 +1,45 @@ +public object FfiConverterString: FfiConverter<String, RustBuffer.ByValue> { + // Note: we don't inherit from FfiConverterRustBuffer, because we use a + // special encoding when lowering/lifting. We can use `RustBuffer.len` to + // store our length and avoid writing it out to the buffer. + override fun lift(value: RustBuffer.ByValue): String { + try { + val byteArr = ByteArray(value.len) + value.asByteBuffer()!!.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } finally { + RustBuffer.free(value) + } + } + + override fun read(buf: ByteBuffer): String { + val len = buf.getInt() + val byteArr = ByteArray(len) + buf.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } + + override fun lower(value: String): RustBuffer.ByValue { + val byteArr = value.toByteArray(Charsets.UTF_8) + // Ideally we'd pass these bytes to `ffi_bytebuffer_from_bytes`, but doing so would require us + // to copy them into a JNA `Memory`. So we might as well directly copy them into a `RustBuffer`. + val rbuf = RustBuffer.alloc(byteArr.size) + rbuf.asByteBuffer()!!.put(byteArr) + return rbuf + } + + // We aren't sure exactly how many bytes our string will be once it's UTF-8 + // encoded. Allocate 3 bytes per unicode codepoint which will always be + // enough. + override fun allocationSize(value: String): Int { + val sizeForLength = 4 + val sizeForString = value.length * 3 + return sizeForLength + sizeForString + } + + override fun write(value: String, buf: ByteBuffer) { + val byteArr = value.toByteArray(Charsets.UTF_8) + buf.putInt(byteArr.size) + buf.put(byteArr) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/TimestampHelper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/TimestampHelper.kt new file mode 100644 index 0000000000..21069d7ce8 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/TimestampHelper.kt @@ -0,0 +1,38 @@ +public object FfiConverterTimestamp: FfiConverterRustBuffer<java.time.Instant> { + override fun read(buf: ByteBuffer): java.time.Instant { + val seconds = buf.getLong() + // Type mismatch (should be u32) but we check for overflow/underflow below + val nanoseconds = buf.getInt().toLong() + if (nanoseconds < 0) { + throw java.time.DateTimeException("Instant nanoseconds exceed minimum or maximum supported by uniffi") + } + if (seconds >= 0) { + return java.time.Instant.EPOCH.plus(java.time.Duration.ofSeconds(seconds, nanoseconds)) + } else { + return java.time.Instant.EPOCH.minus(java.time.Duration.ofSeconds(-seconds, nanoseconds)) + } + } + + // 8 bytes for seconds, 4 bytes for nanoseconds + override fun allocationSize(value: java.time.Instant) = 12 + + override fun write(value: java.time.Instant, buf: ByteBuffer) { + var epochOffset = java.time.Duration.between(java.time.Instant.EPOCH, value) + + var sign = 1 + if (epochOffset.isNegative()) { + sign = -1 + epochOffset = epochOffset.negated() + } + + if (epochOffset.nano < 0) { + // Java docs provide guarantee that nano will always be positive, so this should be impossible + // See: https://docs.oracle.com/javase/8/docs/api/java/time/Instant.html + throw IllegalArgumentException("Invalid timestamp, nano value must be non-negative") + } + + buf.putLong(sign * epochOffset.seconds) + // Type mismatch (should be u32) but since values will always be between 0 and 999,999,999 it should be OK + buf.putInt(epochOffset.nano) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt new file mode 100644 index 0000000000..180a64066a --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt @@ -0,0 +1,17 @@ +{%- match func.throws_type() -%} +{%- when Some with (throwable) %} +@Throws({{ throwable|type_name }}::class) +{%- else -%} +{%- endmatch %} +{%- match func.return_type() -%} +{%- when Some with (return_type) %} + +fun {{ func.name()|fn_name }}({%- call kt::arg_list_decl(func) -%}): {{ return_type|type_name }} { + return {{ return_type|lift_fn }}({% call kt::to_ffi_call(func) %}) +} + +{% when None %} + +fun {{ func.name()|fn_name }}({% call kt::arg_list_decl(func) %}) = + {% call kt::to_ffi_call(func) %} +{% endmatch %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt new file mode 100644 index 0000000000..068bc05de5 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt @@ -0,0 +1,94 @@ +{%- import "macros.kt" as kt %} + +{%- for type_ in ci.iter_types() %} +{%- let type_name = type_|type_name %} +{%- let ffi_converter_name = type_|ffi_converter_name %} +{%- let canonical_type_name = type_|canonical_name %} +{%- let contains_object_references = ci.item_contains_object_references(type_) %} + +{# + # Map `Type` instances to an include statement for that type. + # + # There is a companion match in `KotlinCodeOracle::create_code_type()` which performs a similar function for the + # Rust code. + # + # - When adding additional types here, make sure to also add a match arm to that function. + # - To keep things manageable, let's try to limit ourselves to these 2 mega-matches + #} +{%- match type_ %} + +{%- when Type::Boolean %} +{%- include "BooleanHelper.kt" %} + +{%- when Type::Int8 %} +{%- include "Int8Helper.kt" %} + +{%- when Type::Int16 %} +{%- include "Int16Helper.kt" %} + +{%- when Type::Int32 %} +{%- include "Int32Helper.kt" %} + +{%- when Type::Int64 %} +{%- include "Int64Helper.kt" %} + +{%- when Type::UInt8 %} +{%- include "UInt8Helper.kt" %} + +{%- when Type::UInt16 %} +{%- include "UInt16Helper.kt" %} + +{%- when Type::UInt32 %} +{%- include "UInt32Helper.kt" %} + +{%- when Type::UInt64 %} +{%- include "UInt64Helper.kt" %} + +{%- when Type::Float32 %} +{%- include "Float32Helper.kt" %} + +{%- when Type::Float64 %} +{%- include "Float64Helper.kt" %} + +{%- when Type::String %} +{%- include "StringHelper.kt" %} + +{%- when Type::Enum(name) %} +{% include "EnumTemplate.kt" %} + +{%- when Type::Error(name) %} +{% include "ErrorTemplate.kt" %} + +{%- when Type::Object(name) %} +{% include "ObjectTemplate.kt" %} + +{%- when Type::Record(name) %} +{% include "RecordTemplate.kt" %} + +{%- when Type::Optional(inner_type) %} +{% include "OptionalTemplate.kt" %} + +{%- when Type::Sequence(inner_type) %} +{% include "SequenceTemplate.kt" %} + +{%- when Type::Map(key_type, value_type) %} +{% include "MapTemplate.kt" %} + +{%- when Type::CallbackInterface(name) %} +{% include "CallbackInterfaceTemplate.kt" %} + +{%- when Type::Timestamp %} +{% include "TimestampHelper.kt" %} + +{%- when Type::Duration %} +{% include "DurationHelper.kt" %} + +{%- when Type::Custom { name, builtin } %} +{% include "CustomTypeTemplate.kt" %} + +{%- when Type::External { crate_name, name } %} +{% include "ExternalTypeTemplate.kt" %} + +{%- else %} +{%- endmatch %} +{%- endfor %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt16Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt16Helper.kt new file mode 100644 index 0000000000..279a8fa91b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt16Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterUShort: FfiConverter<UShort, Short> { + override fun lift(value: Short): UShort { + return value.toUShort() + } + + override fun read(buf: ByteBuffer): UShort { + return lift(buf.getShort()) + } + + override fun lower(value: UShort): Short { + return value.toShort() + } + + override fun allocationSize(value: UShort) = 2 + + override fun write(value: UShort, buf: ByteBuffer) { + buf.putShort(value.toShort()) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt32Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt32Helper.kt new file mode 100644 index 0000000000..da7b5b28d6 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt32Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterUInt: FfiConverter<UInt, Int> { + override fun lift(value: Int): UInt { + return value.toUInt() + } + + override fun read(buf: ByteBuffer): UInt { + return lift(buf.getInt()) + } + + override fun lower(value: UInt): Int { + return value.toInt() + } + + override fun allocationSize(value: UInt) = 4 + + override fun write(value: UInt, buf: ByteBuffer) { + buf.putInt(value.toInt()) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt64Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt64Helper.kt new file mode 100644 index 0000000000..44d27ad36b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt64Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterULong: FfiConverter<ULong, Long> { + override fun lift(value: Long): ULong { + return value.toULong() + } + + override fun read(buf: ByteBuffer): ULong { + return lift(buf.getLong()) + } + + override fun lower(value: ULong): Long { + return value.toLong() + } + + override fun allocationSize(value: ULong) = 8 + + override fun write(value: ULong, buf: ByteBuffer) { + buf.putLong(value.toLong()) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt8Helper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt8Helper.kt new file mode 100644 index 0000000000..b6d176603e --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/UInt8Helper.kt @@ -0,0 +1,19 @@ +public object FfiConverterUByte: FfiConverter<UByte, Byte> { + override fun lift(value: Byte): UByte { + return value.toUByte() + } + + override fun read(buf: ByteBuffer): UByte { + return lift(buf.get()) + } + + override fun lower(value: UByte): Byte { + return value.toByte() + } + + override fun allocationSize(value: UByte) = 1 + + override fun write(value: UByte, buf: ByteBuffer) { + buf.put(value.toByte()) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt new file mode 100644 index 0000000000..32182d5e45 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt @@ -0,0 +1,82 @@ +{# +// Template to call into rust. Used in several places. +// Variable names in `arg_list_decl` should match up with arg lists +// passed to rust via `_arg_list_ffi_call` +#} + +{%- macro to_ffi_call(func) -%} + {%- match func.throws_type() %} + {%- when Some with (e) %} + rustCallWithError({{ e|type_name}}) + {%- else %} + rustCall() + {%- endmatch %} { _status -> + _UniFFILib.INSTANCE.{{ func.ffi_func().name() }}({% call _arg_list_ffi_call(func) -%}{% if func.arguments().len() > 0 %},{% endif %} _status) +} +{%- endmacro -%} + +{%- macro to_ffi_call_with_prefix(prefix, func) %} + {%- match func.throws_type() %} + {%- when Some with (e) %} + rustCallWithError({{ e|type_name}}) + {%- else %} + rustCall() + {%- endmatch %} { _status -> + _UniFFILib.INSTANCE.{{ func.ffi_func().name() }}( + {{- prefix }}, {% call _arg_list_ffi_call(func) %}{% if func.arguments().len() > 0 %}, {% endif %} _status) +} +{%- endmacro %} + +{%- macro _arg_list_ffi_call(func) %} + {%- for arg in func.arguments() %} + {{- arg|lower_fn }}({{ arg.name()|var_name }}) + {%- if !loop.last %}, {% endif %} + {%- endfor %} +{%- endmacro -%} + +{#- +// Arglist as used in kotlin declarations of methods, functions and constructors. +// Note the var_name and type_name filters. +-#} + +{% macro arg_list_decl(func) %} + {%- for arg in func.arguments() -%} + {{ arg.name()|var_name }}: {{ arg|type_name -}} + {%- match arg.default_value() %} + {%- when Some with(literal) %} = {{ literal|render_literal(arg) }} + {%- else %} + {%- endmatch %} + {%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro %} + +{% macro arg_list_protocol(func) %} + {%- for arg in func.arguments() -%} + {{ arg.name()|var_name }}: {{ arg|type_name -}} + {%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro %} +{#- +// Arglist as used in the _UniFFILib function declarations. +// Note unfiltered name but ffi_type_name filters. +-#} +{%- macro arg_list_ffi_decl(func) %} + {%- for arg in func.arguments() %} + {{- arg.name()|var_name }}: {{ arg.type_().borrow()|ffi_type_name -}}, + {%- endfor %} + _uniffi_out_err: RustCallStatus +{%- endmacro -%} + +// Macro for destroying fields +{%- macro destroy_fields(member) %} + Disposable.destroy( + {%- for field in member.fields() %} + this.{{ field.name()|var_name }}{%- if !loop.last %}, {% endif -%} + {% endfor -%}) +{%- endmacro -%} + +{%- macro ffi_function_definition(func) %} +fun {{ func.name()|fn_name }}( + {%- call arg_list_ffi_decl(func) %} +){%- match func.return_type() -%}{%- when Some with (type_) %}: {{ type_|ffi_type_name }}{% when None %}: Unit{% endmatch %} +{% endmacro %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt new file mode 100644 index 0000000000..ef42d8e616 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt @@ -0,0 +1,48 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +@file:Suppress("NAME_SHADOWING") + +package {{ config.package_name() }}; + +// Common helper code. +// +// Ideally this would live in a separate .kt file where it can be unittested etc +// in isolation, and perhaps even published as a re-useable package. +// +// However, it's important that the detils of how this helper code works (e.g. the +// way that different builtin types are passed across the FFI) exactly match what's +// expected by the Rust code on the other side of the interface. In practice right +// now that means coming from the exact some version of `uniffi` that was used to +// compile the Rust component. The easiest way to ensure this is to bundle the Kotlin +// helpers directly inline like we're doing here. + +import com.sun.jna.Library +import com.sun.jna.Native +import com.sun.jna.Pointer +import com.sun.jna.Structure +import com.sun.jna.ptr.ByReference +import java.nio.ByteBuffer +import java.nio.ByteOrder + +{%- for req in self.imports() %} +{{ req.render() }} +{%- endfor %} + +{% include "RustBufferTemplate.kt" %} +{% include "FfiConverterTemplate.kt" %} +{% include "Helpers.kt" %} + +// Contains loading, initialization code, +// and the FFI Function declarations in a com.sun.jna.Library. +{% include "NamespaceLibraryTemplate.kt" %} + +// Public interface members begin here. +{{ type_helper_code }} + +{%- for func in ci.function_definitions() %} +{%- include "TopLevelFunctionTemplate.kt" %} +{%- endfor %} + + +{% import "macros.kt" as kt %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/test.rs b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/test.rs new file mode 100644 index 0000000000..e8172ed15b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/test.rs @@ -0,0 +1,107 @@ +/* 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 anyhow::{bail, Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use std::env; +use std::process::Command; +use uniffi_testing::UniFFITestHelper; + +/// Run Kotlin tests for a UniFFI test fixture +pub fn run_test(tmp_dir: &str, fixture_name: &str, script_file: &str) -> Result<()> { + let script_path = Utf8Path::new(".").join(script_file); + let test_helper = UniFFITestHelper::new(fixture_name).context("UniFFITestHelper::new")?; + let out_dir = test_helper + .create_out_dir(tmp_dir, &script_path) + .context("create_out_dir")?; + test_helper + .copy_cdylibs_to_out_dir(&out_dir) + .context("copy_fixture_library_to_out_dir")?; + generate_sources(&test_helper.cdylib_path()?, &out_dir, &test_helper) + .context("generate_sources")?; + let jar_file = build_jar(fixture_name, &out_dir).context("build_jar")?; + + let status = Command::new("kotlinc") + .arg("-classpath") + .arg(calc_classpath(vec![&out_dir, &jar_file])) + // Enable runtime assertions, for easy testing etc. + .arg("-J-ea") + // Our test scripts should not produce any warnings. + .arg("-Werror") + .arg("-script") + .arg(script_path) + .spawn() + .context("Failed to spawn `kotlinc` to run Kotlin script")? + .wait() + .context("Failed to wait for `kotlinc` when running Kotlin script")?; + if !status.success() { + anyhow::bail!("running `kotlinc` failed") + } + Ok(()) +} + +fn generate_sources( + library_path: &Utf8Path, + out_dir: &Utf8Path, + test_helper: &UniFFITestHelper, +) -> Result<()> { + for source in test_helper.get_compile_sources()? { + println!( + "Generating bindings: {:?} {:?} {:?}", + source.udl_path, source.config_path, out_dir + ); + crate::generate_bindings( + &source.udl_path, + source.config_path.as_deref(), + vec!["kotlin"], + Some(out_dir), + Some(library_path), + false, + )?; + } + Ok(()) +} + +/// Generate kotlin bindings for the given namespace, then use the kotlin +/// command-line tools to compile them into a .jar file. +fn build_jar(fixture_name: &str, out_dir: &Utf8Path) -> Result<Utf8PathBuf> { + let mut jar_file = Utf8PathBuf::from(out_dir); + jar_file.push(format!("{}.jar", fixture_name)); + let sources = glob::glob(out_dir.join("**/*.kt").as_str())? + .flatten() + .map(|p| String::from(p.to_string_lossy())) + .collect::<Vec<String>>(); + if sources.is_empty() { + bail!("No kotlin sources found in {out_dir}") + } + println!("building jar from: {:?}", sources); + + let status = Command::new("kotlinc") + // Our generated bindings should not produce any warnings; fail tests if they do. + .arg("-Werror") + .arg("-d") + .arg(&jar_file) + .arg("-classpath") + .arg(calc_classpath(vec![])) + .args(sources) + .spawn() + .context("Failed to spawn `kotlinc` to compile the bindings")? + .wait() + .context("Failed to wait for `kotlinc` when compiling the bindings")?; + if !status.success() { + bail!("running `kotlinc` failed") + } + Ok(jar_file) +} + +fn calc_classpath(extra_paths: Vec<&Utf8Path>) -> String { + extra_paths + .into_iter() + .map(|p| p.to_string()) + // Add the system classpath as a component, using the fact that env::var returns an Option, + // which implement Iterator + .chain(env::var("CLASSPATH")) + .collect::<Vec<String>>() + .join(":") +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/mod.rs new file mode 100644 index 0000000000..10f6e5fe2c --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/mod.rs @@ -0,0 +1,121 @@ +/* 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/. */ + +//! Generate foreign language bindings for a uniffi component. +//! +//! This module contains all the code for generating foreign language bindings, +//! along with some helpers for executing foreign language scripts or tests. + +use anyhow::{bail, Result}; +use camino::Utf8Path; +use serde::{Deserialize, Serialize}; + +use crate::interface::ComponentInterface; +use crate::MergeWith; + +pub mod kotlin; +pub mod python; +pub mod ruby; +pub mod swift; + +/// Enumeration of all foreign language targets currently supported by this crate. +/// +/// The functions in this module will delegate to a language-specific backend based +/// on the provided `TargetLanguage`. For convenience of calling code we also provide +/// a few `TryFrom` implementations to help guess the correct target language from +/// e.g. a file extension of command-line argument. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub enum TargetLanguage { + Kotlin, + Swift, + Python, + Ruby, +} + +impl TryFrom<&str> for TargetLanguage { + type Error = anyhow::Error; + fn try_from(value: &str) -> Result<Self> { + Ok(match value.to_ascii_lowercase().as_str() { + "kotlin" | "kt" | "kts" => TargetLanguage::Kotlin, + "swift" => TargetLanguage::Swift, + "python" | "py" => TargetLanguage::Python, + "ruby" | "rb" => TargetLanguage::Ruby, + _ => bail!("Unknown or unsupported target language: \"{}\"", value), + }) + } +} + +impl TryFrom<&std::ffi::OsStr> for TargetLanguage { + type Error = anyhow::Error; + fn try_from(value: &std::ffi::OsStr) -> Result<Self> { + match value.to_str() { + None => bail!("Unreadable target language"), + Some(s) => s.try_into(), + } + } +} + +impl TryFrom<String> for TargetLanguage { + type Error = anyhow::Error; + fn try_from(value: String) -> Result<Self> { + TryFrom::try_from(value.as_str()) + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Config { + #[serde(default)] + kotlin: kotlin::Config, + #[serde(default)] + swift: swift::Config, + #[serde(default)] + python: python::Config, + #[serde(default)] + ruby: ruby::Config, +} + +impl From<&ComponentInterface> for Config { + fn from(ci: &ComponentInterface) -> Self { + Config { + kotlin: ci.into(), + swift: ci.into(), + python: ci.into(), + ruby: ci.into(), + } + } +} + +impl MergeWith for Config { + fn merge_with(&self, other: &Self) -> Self { + Config { + kotlin: self.kotlin.merge_with(&other.kotlin), + swift: self.swift.merge_with(&other.swift), + python: self.python.merge_with(&other.python), + ruby: self.ruby.merge_with(&other.ruby), + } + } +} + +/// Generate foreign language bindings from a compiled `uniffi` library. +pub fn write_bindings( + config: &Config, + ci: &ComponentInterface, + out_dir: &Utf8Path, + language: TargetLanguage, + try_format_code: bool, +) -> Result<()> { + match language { + TargetLanguage::Kotlin => { + kotlin::write_bindings(&config.kotlin, ci, out_dir, try_format_code)? + } + TargetLanguage::Swift => { + swift::write_bindings(&config.swift, ci, out_dir, try_format_code)? + } + TargetLanguage::Python => { + python::write_bindings(&config.python, ci, out_dir, try_format_code)? + } + TargetLanguage::Ruby => ruby::write_bindings(&config.ruby, ci, out_dir, try_format_code)?, + } + Ok(()) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/callback_interface.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/callback_interface.rs new file mode 100644 index 0000000000..9359fbaeae --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/callback_interface.rs @@ -0,0 +1,33 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct CallbackInterfaceCodeType { + id: String, +} + +impl CallbackInterfaceCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for CallbackInterfaceCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("CallbackInterface{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/compounds.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/compounds.rs new file mode 100644 index 0000000000..80b1da040d --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/compounds.rs @@ -0,0 +1,122 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal, TypeIdentifier}; + +pub struct OptionalCodeType { + inner: TypeIdentifier, +} + +impl OptionalCodeType { + pub fn new(inner: TypeIdentifier) -> Self { + Self { inner } + } +} + +impl CodeType for OptionalCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.find(&self.inner).type_label(oracle) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Optional{}", + oracle.find(&self.inner).canonical_name(oracle), + ) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + match literal { + Literal::Null => "None".into(), + _ => oracle.find(&self.inner).literal(oracle, literal), + } + } + + fn coerce(&self, oracle: &dyn CodeOracle, nm: &str) -> String { + format!( + "(None if {} is None else {})", + nm, + oracle.find(&self.inner).coerce(oracle, nm) + ) + } +} + +pub struct SequenceCodeType { + inner: TypeIdentifier, +} + +impl SequenceCodeType { + pub fn new(inner: TypeIdentifier) -> Self { + Self { inner } + } +} + +impl CodeType for SequenceCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + "list".to_string() + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Sequence{}", + oracle.find(&self.inner).canonical_name(oracle), + ) + } + + fn literal(&self, _oracle: &dyn CodeOracle, literal: &Literal) -> String { + match literal { + Literal::EmptySequence => "[]".into(), + _ => unimplemented!(), + } + } + + fn coerce(&self, oracle: &dyn CodeOracle, nm: &str) -> String { + format!( + "list({} for x in {})", + oracle.find(&self.inner).coerce(oracle, "x"), + nm + ) + } +} + +pub struct MapCodeType { + key: TypeIdentifier, + value: TypeIdentifier, +} + +impl MapCodeType { + pub fn new(key: TypeIdentifier, value: TypeIdentifier) -> Self { + Self { key, value } + } +} + +impl CodeType for MapCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + "dict".to_string() + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Map{}{}", + oracle.find(&self.key).canonical_name(oracle), + oracle.find(&self.value).canonical_name(oracle), + ) + } + + fn literal(&self, _oracle: &dyn CodeOracle, literal: &Literal) -> String { + match literal { + Literal::EmptyMap => "{}".into(), + _ => unimplemented!(), + } + } + + fn coerce(&self, oracle: &dyn CodeOracle, nm: &str) -> String { + format!( + "dict(({}, {}) for (k, v) in {}.items())", + self.key.coerce(oracle, "k"), + self.value.coerce(oracle, "v"), + nm + ) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/custom.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/custom.rs new file mode 100644 index 0000000000..d53a946947 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/custom.rs @@ -0,0 +1,29 @@ +/* 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 crate::backend::{CodeOracle, CodeType}; + +pub struct CustomCodeType { + name: String, +} + +impl CustomCodeType { + pub fn new(name: String) -> Self { + Self { name } + } +} + +impl CodeType for CustomCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + self.name.clone() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.name) + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/enum_.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/enum_.rs new file mode 100644 index 0000000000..9a86076770 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/enum_.rs @@ -0,0 +1,41 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct EnumCodeType { + id: String, +} + +impl EnumCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for EnumCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + if let Literal::Enum(v, _) = literal { + format!( + "{}.{}", + self.type_label(oracle), + oracle.enum_variant_name(v) + ) + } else { + unreachable!(); + } + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/error.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/error.rs new file mode 100644 index 0000000000..f1567281f2 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/error.rs @@ -0,0 +1,33 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct ErrorCodeType { + id: String, +} + +impl ErrorCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for ErrorCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/external.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/external.rs new file mode 100644 index 0000000000..1739d84cec --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/external.rs @@ -0,0 +1,29 @@ +/* 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 crate::backend::{CodeOracle, CodeType}; + +pub struct ExternalCodeType { + name: String, +} + +impl ExternalCodeType { + pub fn new(name: String) -> Self { + Self { name } + } +} + +impl CodeType for ExternalCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + self.name.clone() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.name) + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.into() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/miscellany.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/miscellany.rs new file mode 100644 index 0000000000..c885428bc3 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/miscellany.rs @@ -0,0 +1,36 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; +use paste::paste; + +macro_rules! impl_code_type_for_miscellany { + ($T:ty, $canonical_name:literal) => { + paste! { + pub struct $T; + + impl CodeType for $T { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + format!("{}", $canonical_name) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("{}", $canonical_name) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!() + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } + } + } + }; +} + +impl_code_type_for_miscellany!(TimestampCodeType, "Timestamp"); + +impl_code_type_for_miscellany!(DurationCodeType, "Duration"); diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/mod.rs new file mode 100644 index 0000000000..0b2218402a --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/mod.rs @@ -0,0 +1,448 @@ +/* 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 anyhow::{Context, Result}; +use askama::Template; +use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; +use std::cell::RefCell; +use std::collections::{BTreeSet, HashMap, HashSet}; + +use crate::backend::{CodeOracle, CodeType, TemplateExpression, TypeIdentifier}; +use crate::interface::*; +use crate::MergeWith; + +mod callback_interface; +mod compounds; +mod custom; +mod enum_; +mod error; +mod external; +mod miscellany; +mod object; +mod primitives; +mod record; + +// Taken from Python's `keyword.py` module. +static KEYWORDS: Lazy<HashSet<String>> = Lazy::new(|| { + let kwlist = vec![ + "False", + "None", + "True", + "__peg_parser__", + "and", + "as", + "assert", + "async", + "await", + "break", + "class", + "continue", + "def", + "del", + "elif", + "else", + "except", + "finally", + "for", + "from", + "global", + "if", + "import", + "in", + "is", + "lambda", + "nonlocal", + "not", + "or", + "pass", + "raise", + "return", + "try", + "while", + "with", + "yield", + ]; + HashSet::from_iter(kwlist.into_iter().map(|s| s.to_string())) +}); + +// Config options to customize the generated python. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Config { + cdylib_name: Option<String>, + #[serde(default)] + custom_types: HashMap<String, CustomTypeConfig>, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CustomTypeConfig { + // This `CustomTypeConfig` doesn't have a `type_name` like the others -- which is why we have + // separate structs rather than a shared one. + imports: Option<Vec<String>>, + into_custom: TemplateExpression, + from_custom: TemplateExpression, +} + +impl Config { + pub fn cdylib_name(&self) -> String { + if let Some(cdylib_name) = &self.cdylib_name { + cdylib_name.clone() + } else { + "uniffi".into() + } + } +} + +impl From<&ComponentInterface> for Config { + fn from(ci: &ComponentInterface) -> Self { + Config { + cdylib_name: Some(format!("uniffi_{}", ci.namespace())), + custom_types: HashMap::new(), + } + } +} + +impl MergeWith for Config { + fn merge_with(&self, other: &Self) -> Self { + Config { + cdylib_name: self.cdylib_name.merge_with(&other.cdylib_name), + custom_types: self.custom_types.merge_with(&other.custom_types), + } + } +} + +// Generate python bindings for the given ComponentInterface, as a string. +pub fn generate_python_bindings(config: &Config, ci: &ComponentInterface) -> Result<String> { + PythonWrapper::new(config.clone(), ci) + .render() + .context("failed to render python bindings") +} + +/// A struct to record a Python import statement. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum ImportRequirement { + /// A simple module import. + Module { mod_name: String }, + /// A single symbol from a module. + Symbol { + mod_name: String, + symbol_name: String, + }, + /// A single symbol from a module with the specified local name. + SymbolAs { + mod_name: String, + symbol_name: String, + as_name: String, + }, +} + +impl ImportRequirement { + /// Render the Python import statement. + fn render(&self) -> String { + match &self { + ImportRequirement::Module { mod_name } => format!("import {mod_name}"), + ImportRequirement::Symbol { + mod_name, + symbol_name, + } => format!("from {mod_name} import {symbol_name}"), + ImportRequirement::SymbolAs { + mod_name, + symbol_name, + as_name, + } => format!("from {mod_name} import {symbol_name} as {as_name}"), + } + } +} + +/// Renders Python helper code for all types +/// +/// This template is a bit different than others in that it stores internal state from the render +/// process. Make sure to only call `render()` once. +#[derive(Template)] +#[template(syntax = "py", escape = "none", path = "Types.py")] +pub struct TypeRenderer<'a> { + python_config: &'a Config, + ci: &'a ComponentInterface, + // Track included modules for the `include_once()` macro + include_once_names: RefCell<HashSet<String>>, + // Track imports added with the `add_import()` macro + imports: RefCell<BTreeSet<ImportRequirement>>, +} + +impl<'a> TypeRenderer<'a> { + fn new(python_config: &'a Config, ci: &'a ComponentInterface) -> Self { + Self { + python_config, + ci, + include_once_names: RefCell::new(HashSet::new()), + imports: RefCell::new(BTreeSet::new()), + } + } + + // The following methods are used by the `Types.py` macros. + + // Helper for the including a template, but only once. + // + // The first time this is called with a name it will return true, indicating that we should + // include the template. Subsequent calls will return false. + fn include_once_check(&self, name: &str) -> bool { + self.include_once_names + .borrow_mut() + .insert(name.to_string()) + } + + // Helper to add an import statement + // + // Call this inside your template to cause an import statement to be added at the top of the + // file. Imports will be sorted and de-deuped. + // + // Returns an empty string so that it can be used inside an askama `{{ }}` block. + fn add_import(&self, name: &str) -> &str { + self.imports.borrow_mut().insert(ImportRequirement::Module { + mod_name: name.to_owned(), + }); + "" + } + + // Like add_import, but arranges for `from module import name`. + fn add_import_of(&self, mod_name: &str, name: &str) -> &str { + self.imports.borrow_mut().insert(ImportRequirement::Symbol { + mod_name: mod_name.to_owned(), + symbol_name: name.to_owned(), + }); + "" + } + + // Like add_import, but arranges for `from module import name as other`. + fn add_import_of_as(&self, mod_name: &str, symbol_name: &str, as_name: &str) -> &str { + self.imports + .borrow_mut() + .insert(ImportRequirement::SymbolAs { + mod_name: mod_name.to_owned(), + symbol_name: symbol_name.to_owned(), + as_name: as_name.to_owned(), + }); + "" + } +} + +#[derive(Template)] +#[template(syntax = "py", escape = "none", path = "wrapper.py")] +pub struct PythonWrapper<'a> { + ci: &'a ComponentInterface, + config: Config, + type_helper_code: String, + type_imports: BTreeSet<ImportRequirement>, +} +impl<'a> PythonWrapper<'a> { + pub fn new(config: Config, ci: &'a ComponentInterface) -> Self { + let type_renderer = TypeRenderer::new(&config, ci); + let type_helper_code = type_renderer.render().unwrap(); + let type_imports = type_renderer.imports.into_inner(); + Self { + config, + ci, + type_helper_code, + type_imports, + } + } + + pub fn imports(&self) -> Vec<ImportRequirement> { + self.type_imports.iter().cloned().collect() + } +} + +fn fixup_keyword(name: String) -> String { + if KEYWORDS.contains(&name) { + format!("_{name}") + } else { + name + } +} + +#[derive(Clone, Default)] +pub struct PythonCodeOracle; + +impl PythonCodeOracle { + // Map `Type` instances to a `Box<dyn CodeType>` for that type. + // + // There is a companion match in `templates/Types.py` which performs a similar function for the + // template code. + // + // - When adding additional types here, make sure to also add a match arm to the `Types.py` template. + // - To keep things manageable, let's try to limit ourselves to these 2 mega-matches + fn create_code_type(&self, type_: TypeIdentifier) -> Box<dyn CodeType> { + match type_ { + Type::UInt8 => Box::new(primitives::UInt8CodeType), + Type::Int8 => Box::new(primitives::Int8CodeType), + Type::UInt16 => Box::new(primitives::UInt16CodeType), + Type::Int16 => Box::new(primitives::Int16CodeType), + Type::UInt32 => Box::new(primitives::UInt32CodeType), + Type::Int32 => Box::new(primitives::Int32CodeType), + Type::UInt64 => Box::new(primitives::UInt64CodeType), + Type::Int64 => Box::new(primitives::Int64CodeType), + Type::Float32 => Box::new(primitives::Float32CodeType), + Type::Float64 => Box::new(primitives::Float64CodeType), + Type::Boolean => Box::new(primitives::BooleanCodeType), + Type::String => Box::new(primitives::StringCodeType), + + Type::Timestamp => Box::new(miscellany::TimestampCodeType), + Type::Duration => Box::new(miscellany::DurationCodeType), + + Type::Enum(id) => Box::new(enum_::EnumCodeType::new(id)), + Type::Object(id) => Box::new(object::ObjectCodeType::new(id)), + Type::Record(id) => Box::new(record::RecordCodeType::new(id)), + Type::Error(id) => Box::new(error::ErrorCodeType::new(id)), + Type::CallbackInterface(id) => { + Box::new(callback_interface::CallbackInterfaceCodeType::new(id)) + } + + Type::Optional(inner) => Box::new(compounds::OptionalCodeType::new(*inner)), + Type::Sequence(inner) => Box::new(compounds::SequenceCodeType::new(*inner)), + Type::Map(key, value) => Box::new(compounds::MapCodeType::new(*key, *value)), + Type::External { name, .. } => Box::new(external::ExternalCodeType::new(name)), + Type::Custom { name, .. } => Box::new(custom::CustomCodeType::new(name)), + Type::Unresolved { name } => { + unreachable!("Type `{name}` must be resolved before calling create_code_type") + } + } + } +} + +impl CodeOracle for PythonCodeOracle { + fn find(&self, type_: &TypeIdentifier) -> Box<dyn CodeType> { + self.create_code_type(type_.clone()) + } + + /// Get the idiomatic Python rendering of a class name (for enums, records, errors, etc). + fn class_name(&self, nm: &str) -> String { + fixup_keyword(nm.to_string().to_upper_camel_case()) + } + + /// Get the idiomatic Python rendering of a function name. + fn fn_name(&self, nm: &str) -> String { + fixup_keyword(nm.to_string().to_snake_case()) + } + + /// Get the idiomatic Python rendering of a variable name. + fn var_name(&self, nm: &str) -> String { + fixup_keyword(nm.to_string().to_snake_case()) + } + + /// Get the idiomatic Python rendering of an individual enum variant. + fn enum_variant_name(&self, nm: &str) -> String { + fixup_keyword(nm.to_string().to_shouty_snake_case()) + } + + /// Get the idiomatic Python rendering of an exception name + /// This replaces "Error" at the end of the name with "Exception". + fn error_name(&self, nm: &str) -> String { + let name = fixup_keyword(self.class_name(nm)); + match name.strip_suffix("Error") { + None => name, + Some(stripped) => format!("{stripped}Exception"), + } + } + + fn ffi_type_label(&self, ffi_type: &FfiType) -> String { + match ffi_type { + FfiType::Int8 => "ctypes.c_int8".to_string(), + FfiType::UInt8 => "ctypes.c_uint8".to_string(), + FfiType::Int16 => "ctypes.c_int16".to_string(), + FfiType::UInt16 => "ctypes.c_uint16".to_string(), + FfiType::Int32 => "ctypes.c_int32".to_string(), + FfiType::UInt32 => "ctypes.c_uint32".to_string(), + FfiType::Int64 => "ctypes.c_int64".to_string(), + FfiType::UInt64 => "ctypes.c_uint64".to_string(), + FfiType::Float32 => "ctypes.c_float".to_string(), + FfiType::Float64 => "ctypes.c_double".to_string(), + FfiType::RustArcPtr(_) => "ctypes.c_void_p".to_string(), + FfiType::RustBuffer(maybe_suffix) => match maybe_suffix { + Some(suffix) => format!("RustBuffer{}", suffix), + None => "RustBuffer".to_string(), + }, + FfiType::ForeignBytes => "ForeignBytes".to_string(), + FfiType::ForeignCallback => "FOREIGN_CALLBACK_T".to_string(), + } + } +} + +pub mod filters { + use super::*; + + fn oracle() -> &'static PythonCodeOracle { + &PythonCodeOracle + } + + pub fn type_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(codetype.type_label(oracle)) + } + + pub fn ffi_converter_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(codetype.ffi_converter_name(oracle)) + } + + pub fn canonical_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(codetype.canonical_name(oracle)) + } + + pub fn lift_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.lift", ffi_converter_name(codetype)?)) + } + + pub fn lower_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.lower", ffi_converter_name(codetype)?)) + } + + pub fn read_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.read", ffi_converter_name(codetype)?)) + } + + pub fn write_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(format!("{}.write", ffi_converter_name(codetype)?)) + } + + pub fn literal_py( + literal: &Literal, + codetype: &impl CodeType, + ) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(codetype.literal(oracle, literal)) + } + + /// Get the Python syntax for representing a given low-level `FfiType`. + pub fn ffi_type_name(type_: &FfiType) -> Result<String, askama::Error> { + Ok(oracle().ffi_type_label(type_)) + } + + /// Get the idiomatic Python rendering of a class name (for enums, records, errors, etc). + pub fn class_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().class_name(nm)) + } + + /// Get the idiomatic Python rendering of a function name. + pub fn fn_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().fn_name(nm)) + } + + /// Get the idiomatic Python rendering of a variable name. + pub fn var_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().var_name(nm)) + } + + /// Get the idiomatic Python rendering of an individual enum variant. + pub fn enum_variant_py(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().enum_variant_name(nm)) + } + + pub fn coerce_py(nm: &str, type_: &Type) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(oracle.find(type_).coerce(oracle, nm)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/object.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/object.rs new file mode 100644 index 0000000000..7744865ec0 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/object.rs @@ -0,0 +1,33 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct ObjectCodeType { + id: String, +} + +impl ObjectCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for ObjectCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/primitives.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/primitives.rs new file mode 100644 index 0000000000..de7918fb16 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/primitives.rs @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::backend::{CodeOracle, CodeType, Literal}; +use crate::interface::Radix; +use paste::paste; + +fn render_literal(_oracle: &dyn CodeOracle, literal: &Literal) -> String { + match literal { + Literal::Boolean(v) => { + if *v { + "True".into() + } else { + "False".into() + } + } + Literal::String(s) => format!("\"{s}\""), + // https://docs.python.org/3/reference/lexical_analysis.html#integer-literals + Literal::Int(i, radix, _) => match radix { + Radix::Octal => format!("int(0o{i:o})"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + Literal::UInt(i, radix, _) => match radix { + Radix::Octal => format!("0o{i:o}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + Literal::Float(string, _type_) => string.clone(), + + _ => unreachable!("Literal"), + } +} + +macro_rules! impl_code_type_for_primitive { + ($T:ty, $class_name:literal, $coerce_code:expr) => { + paste! { + pub struct $T; + + impl CodeType for $T { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + $class_name.into() + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + render_literal(oracle, &literal) + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + format!($coerce_code, nm) + } + } + } + }; +} + +impl_code_type_for_primitive!(BooleanCodeType, "Bool", "bool({})"); +impl_code_type_for_primitive!(StringCodeType, "String", "{}"); +impl_code_type_for_primitive!(Int8CodeType, "Int8", "int({})"); +impl_code_type_for_primitive!(Int16CodeType, "Int16", "int({})"); +impl_code_type_for_primitive!(Int32CodeType, "Int32", "int({})"); +impl_code_type_for_primitive!(Int64CodeType, "Int64", "int({})"); +impl_code_type_for_primitive!(UInt8CodeType, "UInt8", "int({})"); +impl_code_type_for_primitive!(UInt16CodeType, "UInt16", "int({})"); +impl_code_type_for_primitive!(UInt32CodeType, "UInt32", "int({})"); +impl_code_type_for_primitive!(UInt64CodeType, "UInt64", "int({})"); +impl_code_type_for_primitive!(Float32CodeType, "Float", "float({})"); +impl_code_type_for_primitive!(Float64CodeType, "Double", "float({})"); diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/record.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/record.rs new file mode 100644 index 0000000000..1dba8e49fe --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/gen_python/record.rs @@ -0,0 +1,33 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct RecordCodeType { + id: String, +} + +impl RecordCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for RecordCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/mod.rs new file mode 100644 index 0000000000..3a1cfaa78d --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/mod.rs @@ -0,0 +1,40 @@ +/* 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::{io::Write, process::Command}; + +use anyhow::Result; +use camino::Utf8Path; +use fs_err::File; + +pub mod gen_python; +mod test; + +use super::super::interface::ComponentInterface; +pub use gen_python::{generate_python_bindings, Config}; +pub use test::run_test; + +// Generate python bindings for the given ComponentInterface, in the given output directory. +pub fn write_bindings( + config: &Config, + ci: &ComponentInterface, + out_dir: &Utf8Path, + try_format_code: bool, +) -> Result<()> { + let py_file = out_dir.join(format!("{}.py", ci.namespace())); + let mut f = File::create(&py_file)?; + write!(f, "{}", generate_python_bindings(config, ci)?)?; + + if try_format_code { + if let Err(e) = Command::new("yapf").arg(&py_file).output() { + println!( + "Warning: Unable to auto-format {} using yapf: {:?}", + py_file.file_name().unwrap(), + e + ) + } + } + + Ok(()) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/BooleanHelper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/BooleanHelper.py new file mode 100644 index 0000000000..d7b64b91f7 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/BooleanHelper.py @@ -0,0 +1,16 @@ +class FfiConverterBool: + @classmethod + def read(cls, buf): + return cls.lift(buf.readU8()) + + @classmethod + def write(cls, value, buf): + buf.writeU8(cls.lower(value)) + + @staticmethod + def lift(value): + return int(value) != 0 + + @staticmethod + def lower(value): + return 1 if value else 0 diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py new file mode 100644 index 0000000000..e8a0ec56d9 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py @@ -0,0 +1,73 @@ +import threading + +class ConcurrentHandleMap: + """ + A map where inserting, getting and removing data is synchronized with a lock. + """ + + def __init__(self): + # type Handle = int + self._left_map = {} # type: Dict[Handle, Any] + self._right_map = {} # type: Dict[Any, Handle] + + self._lock = threading.Lock() + self._current_handle = 0 + self._stride = 1 + + + def insert(self, obj): + with self._lock: + if obj in self._right_map: + return self._right_map[obj] + else: + handle = self._current_handle + self._current_handle += self._stride + self._left_map[handle] = obj + self._right_map[obj] = handle + return handle + + def get(self, handle): + with self._lock: + return self._left_map.get(handle) + + def remove(self, handle): + with self._lock: + if handle in self._left_map: + obj = self._left_map.pop(handle) + del self._right_map[obj] + return obj + +# Magic number for the Rust proxy to call using the same mechanism as every other method, +# to free the callback once it's dropped by Rust. +IDX_CALLBACK_FREE = 0 + +class FfiConverterCallbackInterface: + _handle_map = ConcurrentHandleMap() + + def __init__(self, cb): + self._foreign_callback = cb + + def drop(self, handle): + self.__class__._handle_map.remove(handle) + + @classmethod + def lift(cls, handle): + obj = cls._handle_map.get(handle) + if not obj: + raise InternalError("The object in the handle map has been dropped already") + + return obj + + @classmethod + def read(cls, buf): + handle = buf.readU64() + cls.lift(handle) + + @classmethod + def lower(cls, cb): + handle = cls._handle_map.insert(cb) + return handle + + @classmethod + def write(cls, cb, buf): + buf.writeU64(cls.lower(cb)) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py new file mode 100644 index 0000000000..cf4411b8a0 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py @@ -0,0 +1,104 @@ +{%- let cbi = ci.get_callback_interface_definition(id).unwrap() %} +{%- let foreign_callback = format!("foreignCallback{}", canonical_type_name) %} + +{% if self.include_once_check("CallbackInterfaceRuntime.py") %}{% include "CallbackInterfaceRuntime.py" %}{% endif %} + +# Declaration and FfiConverters for {{ type_name }} Callback Interface + +class {{ type_name }}: + {% for meth in cbi.methods() -%} + def {{ meth.name()|fn_name }}({% call py::arg_list_decl(meth) %}): + raise NotImplementedError + + {% endfor %} + +def py_{{ foreign_callback }}(handle, method, args, buf_ptr): + {% for meth in cbi.methods() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name %} + def {{ method_name }}(python_callback, args): + {#- Unpacking args from the RustBuffer #} + {%- if meth.arguments().len() != 0 -%} + {#- Calling the concrete callback object #} + with args.consumeWithStream() as buf: + rval = python_callback.{{ meth.name()|fn_name }}( + {% for arg in meth.arguments() -%} + {{ arg|read_fn }}(buf) + {%- if !loop.last %}, {% endif %} + {% endfor -%} + ) + {% else %} + rval = python_callback.{{ meth.name()|fn_name }}() + {% endif -%} + + {#- Packing up the return value into a RustBuffer #} + {%- match meth.return_type() -%} + {%- when Some with (return_type) -%} + with RustBuffer.allocWithBuilder() as builder: + {{ return_type|write_fn }}(rval, builder) + return builder.finalize() + {%- else -%} + return RustBuffer.alloc(0) + {% endmatch -%} + {% endfor %} + + cb = {{ ffi_converter_name }}.lift(handle) + if not cb: + raise InternalError("No callback in handlemap; this is a Uniffi bug") + + if method == IDX_CALLBACK_FREE: + {{ ffi_converter_name }}.drop(handle) + # No return value. + # See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + return 0 + + {% for meth in cbi.methods() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} + if method == {{ loop.index }}: + # Call the method and handle any errors + # See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` for details + try: + {%- match meth.throws_type() %} + {%- when Some(err) %} + try: + # Successful return + buf_ptr[0] = {{ method_name }}(cb, args) + return 1 + except {{ err|type_name }} as e: + # Catch errors declared in the UDL file + with RustBuffer.allocWithBuilder() as builder: + {{ err|write_fn }}(e, builder) + buf_ptr[0] = builder.finalize() + return -2 + {%- else %} + # Successful return + buf_ptr[0] = {{ method_name }}(cb, args) + return 1 + {%- endmatch %} + except BaseException as e: + # Catch unexpected errors + try: + # Try to serialize the exception into a String + buf_ptr[0] = {{ Type::String.borrow()|lower_fn }}(repr(e)) + except: + # If that fails, just give up + pass + return -1 + {% endfor %} + + # This should never happen, because an out of bounds method index won't + # ever be used. Once we can catch errors, we should return an InternalException. + # https://github.com/mozilla/uniffi-rs/issues/351 + + # An unexpected error happened. + # See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + return -1 + +# We need to keep this function reference alive: +# if they get GC'd while in use then UniFFI internals could attempt to call a function +# that is in freed memory. +# That would be...uh...bad. Yeah, that's the word. Bad. +{{ foreign_callback }} = FOREIGN_CALLBACK_T(py_{{ foreign_callback }}) + +# The FfiConverter which transforms the Callbacks in to Handles to pass to Rust. +rust_call(lambda err: _UniFFILib.{{ cbi.ffi_init_callback().name() }}({{ foreign_callback }}, err)) +{{ ffi_converter_name }} = FfiConverterCallbackInterface({{ foreign_callback }}) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CustomType.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CustomType.py new file mode 100644 index 0000000000..9ce6a21ced --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/CustomType.py @@ -0,0 +1,52 @@ +{%- match python_config.custom_types.get(name.as_str()) %} +{% when None %} +{#- No custom type config, just forward all methods to our builtin type #} +class FfiConverterType{{ name }}: + @staticmethod + def write(value, buf): + {{ builtin|ffi_converter_name }}.write(value, buf) + + @staticmethod + def read(buf): + return {{ builtin|ffi_converter_name }}.read(buf) + + @staticmethod + def lift(value): + return {{ builtin|ffi_converter_name }}.lift(value) + + @staticmethod + def lower(value): + return {{ builtin|ffi_converter_name }}.lower(value) + +{%- when Some(config) %} + +{%- match config.imports %} +{%- when Some(imports) %} +{%- for import_name in imports %} +{{ self.add_import(import_name) }} +{%- endfor %} +{%- else %} +{%- endmatch %} + +{#- Custom type config supplied, use it to convert the builtin type #} +class FfiConverterType{{ name }}: + @staticmethod + def write(value, buf): + builtin_value = {{ config.from_custom.render("value") }} + {{ builtin|write_fn }}(builtin_value, buf) + + @staticmethod + def read(buf): + builtin_value = {{ builtin|read_fn }}(buf) + return {{ config.into_custom.render("builtin_value") }} + + @staticmethod + def lift(value): + builtin_value = {{ builtin|lift_fn }}(value) + return {{ config.into_custom.render("builtin_value") }} + + @staticmethod + def lower(value): + builtin_value = {{ config.from_custom.render("value") }} + return {{ builtin|lower_fn }}(builtin_value) +{%- endmatch %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/DurationHelper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/DurationHelper.py new file mode 100644 index 0000000000..16fdd986f3 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/DurationHelper.py @@ -0,0 +1,19 @@ +# The Duration type. +# There is a loss of precision when converting from Rust durations, +# which are accurate to the nanosecond, +# to Python durations, which are only accurate to the microsecond. +class FfiConverterDuration(FfiConverterRustBuffer): + @staticmethod + def read(buf): + seconds = buf.readU64() + microseconds = buf.readU32() / 1.0e3 + return datetime.timedelta(seconds=seconds, microseconds=microseconds) + + @staticmethod + def write(value, buf): + seconds = value.seconds + value.days * 24 * 3600 + nanoseconds = value.microseconds * 1000 + if seconds < 0: + raise ValueError("Invalid duration, must be non-negative") + buf.writeI64(seconds) + buf.writeU32(nanoseconds) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py new file mode 100644 index 0000000000..5c366d8855 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py @@ -0,0 +1,93 @@ +{# +# Python has a built-in `enum` module which is nice to use, but doesn't support +# variants with associated data. So, we switch here, and generate a stdlib `enum` +# when none of the variants have associated data, or a generic nested-class +# construct when they do. +#} +{%- let e = ci.get_enum_definition(name).unwrap() %} +{% if e.is_flat() %} + +class {{ type_name }}(enum.Enum): + {% for variant in e.variants() -%} + {{ variant.name()|enum_variant_py }} = {{ loop.index }} + {% endfor %} +{% else %} + +class {{ type_name }}: + def __init__(self): + raise RuntimeError("{{ type_name }} cannot be instantiated directly") + + # Each enum variant is a nested class of the enum itself. + {% for variant in e.variants() -%} + class {{ variant.name()|enum_variant_py }}(object): + def __init__(self,{% for field in variant.fields() %}{{ field.name()|var_name }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}): + {% if variant.has_fields() %} + {%- for field in variant.fields() %} + self.{{ field.name()|var_name }} = {{ field.name()|var_name }} + {%- endfor %} + {% else %} + pass + {% endif %} + + def __str__(self): + return "{{ type_name }}.{{ variant.name()|enum_variant_py }}({% for field in variant.fields() %}{{ field.name()|var_name }}={}{% if loop.last %}{% else %}, {% endif %}{% endfor %})".format({% for field in variant.fields() %}self.{{ field.name()|var_name }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) + + def __eq__(self, other): + if not other.is_{{ variant.name()|var_name }}(): + return False + {%- for field in variant.fields() %} + if self.{{ field.name()|var_name }} != other.{{ field.name()|var_name }}: + return False + {%- endfor %} + return True + {% endfor %} + + # For each variant, we have an `is_NAME` method for easily checking + # whether an instance is that variant. + {% for variant in e.variants() -%} + def is_{{ variant.name()|var_name }}(self): + return isinstance(self, {{ type_name }}.{{ variant.name()|enum_variant_py }}) + {% endfor %} + +# Now, a little trick - we make each nested variant class be a subclass of the main +# enum class, so that method calls and instance checks etc will work intuitively. +# We might be able to do this a little more neatly with a metaclass, but this'll do. +{% for variant in e.variants() -%} +{{ type_name }}.{{ variant.name()|enum_variant_py }} = type("{{ type_name }}.{{ variant.name()|enum_variant_py }}", ({{ type_name }}.{{variant.name()|enum_variant_py}}, {{ type_name }},), {}) +{% endfor %} + +{% endif %} + +class {{ ffi_converter_name }}(FfiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.readI32() + + {%- for variant in e.variants() %} + if variant == {{ loop.index }}: + {%- if e.is_flat() %} + return {{ type_name }}.{{variant.name()|enum_variant_py}} + {%- else %} + return {{ type_name }}.{{variant.name()|enum_variant_py}}( + {%- for field in variant.fields() %} + {{ field|read_fn }}(buf), + {%- endfor %} + ) + {%- endif %} + {%- endfor %} + raise InternalError("Raw enum value doesn't match any cases") + + def write(value, buf): + {%- for variant in e.variants() %} + {%- if e.is_flat() %} + if value == {{ type_name }}.{{ variant.name()|enum_variant_py }}: + buf.writeI32({{ loop.index }}) + {%- else %} + if value.is_{{ variant.name()|var_name }}(): + buf.writeI32({{ loop.index }}) + {%- for field in variant.fields() %} + {{ field|write_fn }}(value.{{ field.name()|var_name }}, buf) + {%- endfor %} + {%- endif %} + {%- endfor %} + diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py new file mode 100644 index 0000000000..305ae88618 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py @@ -0,0 +1,75 @@ +{%- let e = ci.get_error_definition(name).unwrap() %} + +# {{ type_name }} +# We want to define each variant as a nested class that's also a subclass, +# which is tricky in Python. To accomplish this we're going to create each +# class separated, then manually add the child classes to the base class's +# __dict__. All of this happens in dummy class to avoid polluting the module +# namespace. +class UniFFIExceptionTmpNamespace: + class {{ type_name }}(Exception): + pass + {% for variant in e.variants() %} + {%- let variant_type_name = variant.name()|class_name %} + + {%- if e.is_flat() %} + class {{ variant_type_name }}({{ type_name }}): + def __str__(self): + return "{{ type_name }}.{{ variant_type_name }}({})".format(repr(super().__str__())) + {%- else %} + class {{ variant_type_name }}({{ type_name }}): + def __init__(self{% for field in variant.fields() %}, {{ field.name()|var_name }}{% endfor %}): + {%- if variant.has_fields() %} + {%- for field in variant.fields() %} + self.{{ field.name()|var_name }} = {{ field.name()|var_name }} + {%- endfor %} + {%- else %} + pass + {%- endif %} + + def __str__(self): + {%- if variant.has_fields() %} + field_parts = [ + {%- for field in variant.fields() %} + '{{ field.name()|var_name }}={!r}'.format(self.{{ field.name()|var_name }}), + {%- endfor %} + ] + return "{{ type_name }}.{{ variant_type_name }}({})".format(', '.join(field_parts)) + {%- else %} + return "{{ type_name }}.{{ variant_type_name }}()" + {%- endif %} + {%- endif %} + + {{ type_name }}.{{ variant_type_name }} = {{ variant_type_name }} + {%- endfor %} +{{ type_name }} = UniFFIExceptionTmpNamespace.{{ type_name }} +del UniFFIExceptionTmpNamespace + + +class {{ ffi_converter_name }}(FfiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.readI32() + {%- for variant in e.variants() %} + if variant == {{ loop.index }}: + return {{ type_name }}.{{ variant.name()|class_name }}( + {%- if e.is_flat() %} + {{ Type::String.borrow()|read_fn }}(buf), + {%- else %} + {%- for field in variant.fields() %} + {{ field.name()|var_name }}={{ field|read_fn }}(buf), + {%- endfor %} + {%- endif %} + ) + {%- endfor %} + raise InternalError("Raw enum value doesn't match any cases") + + @staticmethod + def write(value, buf): + {%- for variant in e.variants() %} + if isinstance(value, {{ type_name }}.{{ variant.name()|class_name }}): + buf.writeI32({{ loop.index }}) + {%- for field in variant.fields() %} + {{ field|write_fn }}(value.{{ field.name()|var_name }}, buf) + {%- endfor %} + {%- endfor %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py new file mode 100644 index 0000000000..a5f2584c62 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py @@ -0,0 +1,7 @@ +{%- let mod_name = crate_name|fn_name %} + +{%- let ffi_converter_name = "FfiConverterType{}"|format(name) %} +{{ self.add_import_of(mod_name, ffi_converter_name) }} + +{%- let rustbuffer_local_name = "RustBuffer{}"|format(name) %} +{{ self.add_import_of_as(mod_name, "RustBuffer", rustbuffer_local_name) }} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Float32Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Float32Helper.py new file mode 100644 index 0000000000..e5783dd158 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Float32Helper.py @@ -0,0 +1,8 @@ +class FfiConverterFloat(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readFloat() + + @staticmethod + def write(value, buf): + buf.writeFloat(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Float64Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Float64Helper.py new file mode 100644 index 0000000000..57c2131eb8 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Float64Helper.py @@ -0,0 +1,8 @@ +class FfiConverterDouble(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readDouble() + + @staticmethod + def write(value, buf): + buf.writeDouble(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Helpers.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Helpers.py new file mode 100644 index 0000000000..fbce501981 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Helpers.py @@ -0,0 +1,67 @@ +# A handful of classes and functions to support the generated data structures. +# This would be a good candidate for isolating in its own ffi-support lib. + +class InternalError(Exception): + pass + +class RustCallStatus(ctypes.Structure): + """ + Error runtime. + """ + _fields_ = [ + ("code", ctypes.c_int8), + ("error_buf", RustBuffer), + ] + + # These match the values from the uniffi::rustcalls module + CALL_SUCCESS = 0 + CALL_ERROR = 1 + CALL_PANIC = 2 + + def __str__(self): + if self.code == RustCallStatus.CALL_SUCCESS: + return "RustCallStatus(CALL_SUCCESS)" + elif self.code == RustCallStatus.CALL_ERROR: + return "RustCallStatus(CALL_ERROR)" + elif self.code == RustCallStatus.CALL_PANIC: + return "RustCallStatus(CALL_PANIC)" + else: + return "RustCallStatus(<invalid code>)" + +def rust_call(fn, *args): + # Call a rust function + return rust_call_with_error(None, fn, *args) + +def rust_call_with_error(error_ffi_converter, fn, *args): + # Call a rust function and handle any errors + # + # This function is used for rust calls that return Result<> and therefore can set the CALL_ERROR status code. + # error_ffi_converter must be set to the FfiConverter for the error class that corresponds to the result. + call_status = RustCallStatus(code=RustCallStatus.CALL_SUCCESS, error_buf=RustBuffer(0, 0, None)) + + args_with_error = args + (ctypes.byref(call_status),) + result = fn(*args_with_error) + if call_status.code == RustCallStatus.CALL_SUCCESS: + return result + elif call_status.code == RustCallStatus.CALL_ERROR: + if error_ffi_converter is None: + call_status.error_buf.free() + raise InternalError("rust_call_with_error: CALL_ERROR, but error_ffi_converter is None") + else: + raise error_ffi_converter.lift(call_status.error_buf) + elif call_status.code == RustCallStatus.CALL_PANIC: + # When the rust code sees a panic, it tries to construct a RustBuffer + # with the message. But if that code panics, then it just sends back + # an empty buffer. + if call_status.error_buf.len > 0: + msg = FfiConverterString.lift(call_status.error_buf) + else: + msg = "Unknown rust panic" + raise InternalError(msg) + else: + raise InternalError("Invalid RustCallStatus code: {}".format( + call_status.code)) + +# A function pointer for a callback as defined by UniFFI. +# Rust definition `fn(handle: u64, method: u32, args: RustBuffer, buf_ptr: *mut RustBuffer) -> int` +FOREIGN_CALLBACK_T = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_ulonglong, ctypes.c_ulong, RustBuffer, ctypes.POINTER(RustBuffer)) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int16Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int16Helper.py new file mode 100644 index 0000000000..b3d9602746 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int16Helper.py @@ -0,0 +1,8 @@ +class FfiConverterInt16(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readI16() + + @staticmethod + def write(value, buf): + buf.writeI16(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int32Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int32Helper.py new file mode 100644 index 0000000000..47a54d8c4a --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int32Helper.py @@ -0,0 +1,8 @@ +class FfiConverterInt32(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readI32() + + @staticmethod + def write(value, buf): + buf.writeI32(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int64Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int64Helper.py new file mode 100644 index 0000000000..471b9967ce --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int64Helper.py @@ -0,0 +1,8 @@ +class FfiConverterInt64(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readI64() + + @staticmethod + def write(value, buf): + buf.writeI64(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int8Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int8Helper.py new file mode 100644 index 0000000000..524fea7ea9 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Int8Helper.py @@ -0,0 +1,8 @@ +class FfiConverterInt8(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readI8() + + @staticmethod + def write(value, buf): + buf.writeI8(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/MapTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/MapTemplate.py new file mode 100644 index 0000000000..7f42daa7a9 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/MapTemplate.py @@ -0,0 +1,27 @@ +{%- let key_ffi_converter = key_type|ffi_converter_name %} +{%- let value_ffi_converter = value_type|ffi_converter_name %} + +class {{ ffi_converter_name }}(FfiConverterRustBuffer): + @classmethod + def write(cls, items, buf): + buf.writeI32(len(items)) + for (key, value) in items.items(): + {{ key_ffi_converter }}.write(key, buf) + {{ value_ffi_converter }}.write(value, buf) + + @classmethod + def read(cls, buf): + count = buf.readI32() + if count < 0: + raise InternalError("Unexpected negative map size") + + # It would be nice to use a dict comprehension, + # but in Python 3.7 and before the evaluation order is not according to spec, + # so we we're reading the value before the key. + # This loop makes the order explicit: first reading the key, then the value. + d = {} + for i in range(count): + key = {{ key_ffi_converter }}.read(buf) + val = {{ value_ffi_converter }}.read(buf) + d[key] = val + return d diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py new file mode 100644 index 0000000000..4867ecb9b4 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py @@ -0,0 +1,39 @@ +# This is how we find and load the dynamic library provided by the component. +# For now we just look it up by name. +# +# XXX TODO: This will probably grow some magic for resolving megazording in future. +# E.g. we might start by looking for the named component in `libuniffi.so` and if +# that fails, fall back to loading it separately from `lib${componentName}.so`. + +from pathlib import Path + +def loadIndirect(): + if sys.platform == "darwin": + libname = "lib{}.dylib" + elif sys.platform.startswith("win"): + # As of python3.8, ctypes does not seem to search $PATH when loading DLLs. + # We could use `os.add_dll_directory` to configure the search path, but + # it doesn't feel right to mess with application-wide settings. Let's + # assume that the `.dll` is next to the `.py` file and load by full path. + libname = os.path.join( + os.path.dirname(__file__), + "{}.dll", + ) + else: + # Anything else must be an ELF platform - Linux, *BSD, Solaris/illumos + libname = "lib{}.so" + + lib = libname.format("{{ config.cdylib_name() }}") + path = str(Path(__file__).parent / lib) + return ctypes.cdll.LoadLibrary(path) + +# A ctypes library to expose the extern-C FFI definitions. +# This is an implementation detail which will be called internally by the public API. + +_UniFFILib = loadIndirect() +{%- for func in ci.iter_ffi_function_definitions() %} +_UniFFILib.{{ func.name() }}.argtypes = ( + {%- call py::arg_list_ffi_decl(func) -%} +) +_UniFFILib.{{ func.name() }}.restype = {% match func.return_type() %}{% when Some with (type_) %}{{ type_|ffi_type_name }}{% when None %}None{% endmatch %} +{%- endfor %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py new file mode 100644 index 0000000000..cc4cc35b4e --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py @@ -0,0 +1,74 @@ +{%- let obj = ci.get_object_definition(name).unwrap() %} + +class {{ type_name }}(object): + {%- match obj.primary_constructor() %} + {%- when Some with (cons) %} + def __init__(self, {% call py::arg_list_decl(cons) -%}): + {%- call py::setup_args_extra_indent(cons) %} + self._pointer = {% call py::to_ffi_call(cons) %} + {%- when None %} + {%- endmatch %} + + def __del__(self): + # In case of partial initialization of instances. + pointer = getattr(self, "_pointer", None) + if pointer is not None: + rust_call(_UniFFILib.{{ obj.ffi_object_free().name() }}, pointer) + + # Used by alternative constructors or any methods which return this type. + @classmethod + def _make_instance_(cls, pointer): + # Lightly yucky way to bypass the usual __init__ logic + # and just create a new instance with the required pointer. + inst = cls.__new__(cls) + inst._pointer = pointer + return inst + + {% for cons in obj.alternate_constructors() -%} + @classmethod + def {{ cons.name()|fn_name }}(cls, {% call py::arg_list_decl(cons) %}): + {%- call py::setup_args_extra_indent(cons) %} + # Call the (fallible) function before creating any half-baked object instances. + pointer = {% call py::to_ffi_call(cons) %} + return cls._make_instance_(pointer) + {% endfor %} + + {% for meth in obj.methods() -%} + {%- match meth.return_type() -%} + + {%- when Some with (return_type) -%} + def {{ meth.name()|fn_name }}(self, {% call py::arg_list_decl(meth) %}): + {%- call py::setup_args_extra_indent(meth) %} + return {{ return_type|lift_fn }}( + {% call py::to_ffi_call_with_prefix("self._pointer", meth) %} + ) + + {%- when None -%} + def {{ meth.name()|fn_name }}(self, {% call py::arg_list_decl(meth) %}): + {%- call py::setup_args_extra_indent(meth) %} + {% call py::to_ffi_call_with_prefix("self._pointer", meth) %} + {% endmatch %} + {% endfor %} + + +class {{ ffi_converter_name }}: + @classmethod + def read(cls, buf): + ptr = buf.readU64() + if ptr == 0: + raise InternalError("Raw pointer value was null") + return cls.lift(ptr) + + @classmethod + def write(cls, value, buf): + if not isinstance(value, {{ type_name }}): + raise TypeError("Expected {{ type_name }} instance, {} found".format(value.__class__.__name__)) + buf.writeU64(cls.lower(value)) + + @staticmethod + def lift(value): + return {{ type_name }}._make_instance_(value) + + @staticmethod + def lower(value): + return value._pointer diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/OptionalTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/OptionalTemplate.py new file mode 100644 index 0000000000..70f705362f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/OptionalTemplate.py @@ -0,0 +1,21 @@ +{%- let inner_ffi_converter = inner_type|ffi_converter_name %} + +class {{ ffi_converter_name }}(FfiConverterRustBuffer): + @classmethod + def write(cls, value, buf): + if value is None: + buf.writeU8(0) + return + + buf.writeU8(1) + {{ inner_ffi_converter }}.write(value, buf) + + @classmethod + def read(cls, buf): + flag = buf.readU8() + if flag == 0: + return None + elif flag == 1: + return {{ inner_ffi_converter }}.read(buf) + else: + raise InternalError("Unexpected flag byte for optional type") diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py new file mode 100644 index 0000000000..2009d59e8a --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py @@ -0,0 +1,45 @@ +{%- let rec = ci.get_record_definition(name).unwrap() %} +class {{ type_name }}: + + def __init__(self, {% for field in rec.fields() %} + {{- field.name()|var_name }} + {%- if field.default_value().is_some() %} = DEFAULT{% endif %} + {%- if !loop.last %}, {% endif %} + {%- endfor %}): + {%- for field in rec.fields() %} + {%- let field_name = field.name()|var_name %} + {%- match field.default_value() %} + {%- when None %} + self.{{ field_name }} = {{ field_name }} + {%- when Some with(literal) %} + if {{ field_name }} is DEFAULT: + self.{{ field_name }} = {{ literal|literal_py(field) }} + else: + self.{{ field_name }} = {{ field_name }} + {%- endmatch %} + {%- endfor %} + + def __str__(self): + return "{{ type_name }}({% for field in rec.fields() %}{{ field.name()|var_name }}={}{% if loop.last %}{% else %}, {% endif %}{% endfor %})".format({% for field in rec.fields() %}self.{{ field.name()|var_name }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) + + def __eq__(self, other): + {%- for field in rec.fields() %} + if self.{{ field.name()|var_name }} != other.{{ field.name()|var_name }}: + return False + {%- endfor %} + return True + +class {{ ffi_converter_name }}(FfiConverterRustBuffer): + @staticmethod + def read(buf): + return {{ type_name }}( + {%- for field in rec.fields() %} + {{ field.name()|var_name }}={{ field|read_fn }}(buf), + {%- endfor %} + ) + + @staticmethod + def write(value, buf): + {%- for field in rec.fields() %} + {{ field|write_fn }}(value.{{ field.name()|var_name }}, buf) + {%- endfor %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RustBufferHelper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RustBufferHelper.py new file mode 100644 index 0000000000..eafd2346bb --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RustBufferHelper.py @@ -0,0 +1,23 @@ +# Types conforming to `FfiConverterPrimitive` pass themselves directly over the FFI. +class FfiConverterPrimitive: + @classmethod + def lift(cls, value): + return value + + @classmethod + def lower(cls, value): + return value + +# Helper class for wrapper types that will always go through a RustBuffer. +# Classes should inherit from this and implement the `read` and `write` static methods. +class FfiConverterRustBuffer: + @classmethod + def lift(cls, rbuf): + with rbuf.consumeWithStream() as stream: + return cls.read(stream) + + @classmethod + def lower(cls, value): + with RustBuffer.allocWithBuilder() as builder: + cls.write(value, builder) + return builder.finalize() diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py new file mode 100644 index 0000000000..95a3b1f706 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py @@ -0,0 +1,190 @@ + +class RustBuffer(ctypes.Structure): + _fields_ = [ + ("capacity", ctypes.c_int32), + ("len", ctypes.c_int32), + ("data", ctypes.POINTER(ctypes.c_char)), + ] + + @staticmethod + def alloc(size): + return rust_call(_UniFFILib.{{ ci.ffi_rustbuffer_alloc().name() }}, size) + + @staticmethod + def reserve(rbuf, additional): + return rust_call(_UniFFILib.{{ ci.ffi_rustbuffer_reserve().name() }}, rbuf, additional) + + def free(self): + return rust_call(_UniFFILib.{{ ci.ffi_rustbuffer_free().name() }}, self) + + def __str__(self): + return "RustBuffer(capacity={}, len={}, data={})".format( + self.capacity, + self.len, + self.data[0:self.len] + ) + + @contextlib.contextmanager + def allocWithBuilder(): + """Context-manger to allocate a buffer using a RustBufferBuilder. + + The allocated buffer will be automatically freed if an error occurs, ensuring that + we don't accidentally leak it. + """ + builder = RustBufferBuilder() + try: + yield builder + except: + builder.discard() + raise + + @contextlib.contextmanager + def consumeWithStream(self): + """Context-manager to consume a buffer using a RustBufferStream. + + The RustBuffer will be freed once the context-manager exits, ensuring that we don't + leak it even if an error occurs. + """ + try: + s = RustBufferStream(self) + yield s + if s.remaining() != 0: + raise RuntimeError("junk data left in buffer after consuming") + finally: + self.free() + + +class ForeignBytes(ctypes.Structure): + _fields_ = [ + ("len", ctypes.c_int32), + ("data", ctypes.POINTER(ctypes.c_char)), + ] + + def __str__(self): + return "ForeignBytes(len={}, data={})".format(self.len, self.data[0:self.len]) + + +class RustBufferStream(object): + """ + Helper for structured reading of bytes from a RustBuffer + """ + + def __init__(self, rbuf): + self.rbuf = rbuf + self.offset = 0 + + def remaining(self): + return self.rbuf.len - self.offset + + def _unpack_from(self, size, format): + if self.offset + size > self.rbuf.len: + raise InternalError("read past end of rust buffer") + value = struct.unpack(format, self.rbuf.data[self.offset:self.offset+size])[0] + self.offset += size + return value + + def read(self, size): + if self.offset + size > self.rbuf.len: + raise InternalError("read past end of rust buffer") + data = self.rbuf.data[self.offset:self.offset+size] + self.offset += size + return data + + def readI8(self): + return self._unpack_from(1, ">b") + + def readU8(self): + return self._unpack_from(1, ">B") + + def readI16(self): + return self._unpack_from(2, ">h") + + def readU16(self): + return self._unpack_from(2, ">H") + + def readI32(self): + return self._unpack_from(4, ">i") + + def readU32(self): + return self._unpack_from(4, ">I") + + def readI64(self): + return self._unpack_from(8, ">q") + + def readU64(self): + return self._unpack_from(8, ">Q") + + def readFloat(self): + v = self._unpack_from(4, ">f") + return v + + def readDouble(self): + return self._unpack_from(8, ">d") + + +class RustBufferBuilder(object): + """ + Helper for structured writing of bytes into a RustBuffer. + """ + + def __init__(self): + self.rbuf = RustBuffer.alloc(16) + self.rbuf.len = 0 + + def finalize(self): + rbuf = self.rbuf + self.rbuf = None + return rbuf + + def discard(self): + if self.rbuf is not None: + rbuf = self.finalize() + rbuf.free() + + @contextlib.contextmanager + def _reserve(self, numBytes): + if self.rbuf.len + numBytes > self.rbuf.capacity: + self.rbuf = RustBuffer.reserve(self.rbuf, numBytes) + yield None + self.rbuf.len += numBytes + + def _pack_into(self, size, format, value): + with self._reserve(size): + # XXX TODO: I feel like I should be able to use `struct.pack_into` here but can't figure it out. + for i, byte in enumerate(struct.pack(format, value)): + self.rbuf.data[self.rbuf.len + i] = byte + + def write(self, value): + with self._reserve(len(value)): + for i, byte in enumerate(value): + self.rbuf.data[self.rbuf.len + i] = byte + + def writeI8(self, v): + self._pack_into(1, ">b", v) + + def writeU8(self, v): + self._pack_into(1, ">B", v) + + def writeI16(self, v): + self._pack_into(2, ">h", v) + + def writeU16(self, v): + self._pack_into(2, ">H", v) + + def writeI32(self, v): + self._pack_into(4, ">i", v) + + def writeU32(self, v): + self._pack_into(4, ">I", v) + + def writeI64(self, v): + self._pack_into(8, ">q", v) + + def writeU64(self, v): + self._pack_into(8, ">Q", v) + + def writeFloat(self, v): + self._pack_into(4, ">f", v) + + def writeDouble(self, v): + self._pack_into(8, ">d", v) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/SequenceTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/SequenceTemplate.py new file mode 100644 index 0000000000..6de7bf2d4d --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/SequenceTemplate.py @@ -0,0 +1,19 @@ +{%- let inner_ffi_converter = inner_type|ffi_converter_name %} + +class {{ ffi_converter_name}}(FfiConverterRustBuffer): + @classmethod + def write(cls, value, buf): + items = len(value) + buf.writeI32(items) + for item in value: + {{ inner_ffi_converter }}.write(item, buf) + + @classmethod + def read(cls, buf): + count = buf.readI32() + if count < 0: + raise InternalError("Unexpected negative sequence length") + + return [ + {{ inner_ffi_converter }}.read(buf) for i in range(count) + ] diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/StringHelper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/StringHelper.py new file mode 100644 index 0000000000..f205f7c746 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/StringHelper.py @@ -0,0 +1,25 @@ +class FfiConverterString: + @staticmethod + def read(buf): + size = buf.readI32() + if size < 0: + raise InternalError("Unexpected negative string length") + utf8Bytes = buf.read(size) + return utf8Bytes.decode("utf-8") + + @staticmethod + def write(value, buf): + utf8Bytes = value.encode("utf-8") + buf.writeI32(len(utf8Bytes)) + buf.write(utf8Bytes) + + @staticmethod + def lift(buf): + with buf.consumeWithStream() as stream: + return stream.read(stream.remaining()).decode("utf-8") + + @staticmethod + def lower(value): + with RustBuffer.allocWithBuilder() as builder: + builder.write(value.encode("utf-8")) + return builder.finalize() diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/TimestampHelper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/TimestampHelper.py new file mode 100644 index 0000000000..ac87697551 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/TimestampHelper.py @@ -0,0 +1,30 @@ +# The Timestamp type. +# There is a loss of precision when converting from Rust timestamps, +# which are accurate to the nanosecond, +# to Python datetimes, which have a variable precision due to the use of float as representation. +class FfiConverterTimestamp(FfiConverterRustBuffer): + @staticmethod + def read(buf): + seconds = buf.readI64() + microseconds = buf.readU32() / 1000 + # Use fromtimestamp(0) then add the seconds using a timedelta. This + # ensures that we get OverflowError rather than ValueError when + # seconds is too large. + if seconds >= 0: + return datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc) + datetime.timedelta(seconds=seconds, microseconds=microseconds) + else: + return datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc) - datetime.timedelta(seconds=-seconds, microseconds=microseconds) + + @staticmethod + def write(value, buf): + if value >= datetime.datetime.fromtimestamp(0, datetime.timezone.utc): + sign = 1 + delta = value - datetime.datetime.fromtimestamp(0, datetime.timezone.utc) + else: + sign = -1 + delta = datetime.datetime.fromtimestamp(0, datetime.timezone.utc) - value + + seconds = delta.seconds + delta.days * 24 * 3600 + nanoseconds = delta.microseconds * 1000 + buf.writeI64(sign * seconds) + buf.writeU32(nanoseconds) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py new file mode 100644 index 0000000000..2d0e09f22d --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py @@ -0,0 +1,13 @@ +{%- match func.return_type() -%} +{%- when Some with (return_type) %} + +def {{ func.name()|fn_name }}({%- call py::arg_list_decl(func) -%}): + {%- call py::setup_args(func) %} + return {{ return_type|lift_fn }}({% call py::to_ffi_call(func) %}) + +{% when None %} + +def {{ func.name()|fn_name }}({%- call py::arg_list_decl(func) -%}): + {%- call py::setup_args(func) %} + {% call py::to_ffi_call(func) %} +{% endmatch %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Types.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Types.py new file mode 100644 index 0000000000..ce65472a64 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/Types.py @@ -0,0 +1,93 @@ +{%- import "macros.py" as py %} + +{%- for type_ in ci.iter_types() %} +{%- let type_name = type_|type_name %} +{%- let ffi_converter_name = type_|ffi_converter_name %} +{%- let canonical_type_name = type_|canonical_name %} + +{# + # Map `Type` instances to an include statement for that type. + # + # There is a companion match in `PythonCodeOracle::create_code_type()` which performs a similar function for the + # Rust code. + # + # - When adding additional types here, make sure to also add a match arm to that function. + # - To keep things manageable, let's try to limit ourselves to these 2 mega-matches + #} +{%- match type_ %} + +{%- when Type::Boolean %} +{%- include "BooleanHelper.py" %} + +{%- when Type::Int8 %} +{%- include "Int8Helper.py" %} + +{%- when Type::Int16 %} +{%- include "Int16Helper.py" %} + +{%- when Type::Int32 %} +{%- include "Int32Helper.py" %} + +{%- when Type::Int64 %} +{%- include "Int64Helper.py" %} + +{%- when Type::UInt8 %} +{%- include "UInt8Helper.py" %} + +{%- when Type::UInt16 %} +{%- include "UInt16Helper.py" %} + +{%- when Type::UInt32 %} +{%- include "UInt32Helper.py" %} + +{%- when Type::UInt64 %} +{%- include "UInt64Helper.py" %} + +{%- when Type::Float32 %} +{%- include "Float32Helper.py" %} + +{%- when Type::Float64 %} +{%- include "Float64Helper.py" %} + +{%- when Type::String %} +{%- include "StringHelper.py" %} + +{%- when Type::Enum(name) %} +{%- include "EnumTemplate.py" %} + +{%- when Type::Error(name) %} +{%- include "ErrorTemplate.py" %} + +{%- when Type::Record(name) %} +{%- include "RecordTemplate.py" %} + +{%- when Type::Object(name) %} +{%- include "ObjectTemplate.py" %} + +{%- when Type::Timestamp %} +{%- include "TimestampHelper.py" %} + +{%- when Type::Duration %} +{%- include "DurationHelper.py" %} + +{%- when Type::Optional(inner_type) %} +{%- include "OptionalTemplate.py" %} + +{%- when Type::Sequence(inner_type) %} +{%- include "SequenceTemplate.py" %} + +{%- when Type::Map(key_type, value_type) %} +{%- include "MapTemplate.py" %} + +{%- when Type::CallbackInterface(id) %} +{%- include "CallbackInterfaceTemplate.py" %} + +{%- when Type::Custom { name, builtin } %} +{%- include "CustomType.py" %} + +{%- when Type::External { name, crate_name } %} +{%- include "ExternalTemplate.py" %} + +{%- else %} +{%- endmatch %} +{%- endfor %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt16Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt16Helper.py new file mode 100644 index 0000000000..f1cd114c0c --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt16Helper.py @@ -0,0 +1,8 @@ +class FfiConverterUInt16(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readU16() + + @staticmethod + def write(value, buf): + buf.writeU16(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt32Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt32Helper.py new file mode 100644 index 0000000000..290cd5fe82 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt32Helper.py @@ -0,0 +1,8 @@ +class FfiConverterUInt32(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readU32() + + @staticmethod + def write(value, buf): + buf.writeU32(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt64Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt64Helper.py new file mode 100644 index 0000000000..f57dec1d92 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt64Helper.py @@ -0,0 +1,8 @@ +class FfiConverterUInt64(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readU64() + + @staticmethod + def write(value, buf): + buf.writeU64(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt8Helper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt8Helper.py new file mode 100644 index 0000000000..3a1582351b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/UInt8Helper.py @@ -0,0 +1,8 @@ +class FfiConverterUInt8(FfiConverterPrimitive): + @staticmethod + def read(buf): + return buf.readU8() + + @staticmethod + def write(value, buf): + buf.writeU8(value) diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/macros.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/macros.py new file mode 100644 index 0000000000..d11fcae921 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/macros.py @@ -0,0 +1,101 @@ +{# +// Template to call into rust. Used in several places. +// Variable names in `arg_list_decl` should match up with arg lists +// passed to rust via `_arg_list_ffi_call` +#} + +{%- macro to_ffi_call(func) -%} + {%- match func.throws_type() -%} + {%- when Some with (e) -%} +rust_call_with_error({{ e|ffi_converter_name }}, + {%- else -%} +rust_call( + {%- endmatch -%} + _UniFFILib.{{ func.ffi_func().name() }}, + {%- call _arg_list_ffi_call(func) -%} +) +{%- endmacro -%} + +{%- macro to_ffi_call_with_prefix(prefix, func) -%} + {%- match func.throws_type() -%} + {%- when Some with (e) -%} +rust_call_with_error( + {{ e|ffi_converter_name }}, + {%- else -%} +rust_call( + {%- endmatch -%} + _UniFFILib.{{ func.ffi_func().name() }}, + {{- prefix }}, + {%- call _arg_list_ffi_call(func) -%} +) +{%- endmacro -%} + +{%- macro _arg_list_ffi_call(func) %} + {%- for arg in func.arguments() %} + {{ arg|lower_fn }}({{ arg.name()|var_name }}) + {%- if !loop.last %},{% endif %} + {%- endfor %} +{%- endmacro -%} + +{#- +// Arglist as used in Python declarations of methods, functions and constructors. +// Note the var_name and type_name filters. +-#} + +{% macro arg_list_decl(func) %} + {%- for arg in func.arguments() -%} + {{ arg.name()|var_name }} + {%- match arg.default_value() %} + {%- when Some with(literal) %} = DEFAULT + {%- else %} + {%- endmatch %} + {%- if !loop.last %},{% endif -%} + {%- endfor %} +{%- endmacro %} + +{#- +// Arglist as used in the _UniFFILib function declarations. +// Note unfiltered name but ffi_type_name filters. +-#} +{%- macro arg_list_ffi_decl(func) %} + {%- for arg in func.arguments() %} + {{ arg.type_().borrow()|ffi_type_name }}, + {%- endfor %} + ctypes.POINTER(RustCallStatus), +{% endmacro -%} + +{# + # Setup function arguments by initializing default values and passing other + # values through coerce. + #} +{%- macro setup_args(func) %} + {%- for arg in func.arguments() %} + {%- match arg.default_value() %} + {%- when None %} + {{ arg.name()|var_name }} = {{ arg.name()|var_name|coerce_py(arg.type_().borrow()) -}} + {%- when Some with(literal) %} + if {{ arg.name()|var_name }} is DEFAULT: + {{ arg.name()|var_name }} = {{ literal|literal_py(arg.type_().borrow()) }} + else: + {{ arg.name()|var_name }} = {{ arg.name()|var_name|coerce_py(arg.type_().borrow()) -}} + {%- endmatch %} + {% endfor -%} +{%- endmacro -%} + +{# + # Exactly the same thing as `setup_args()` but with an extra 4 spaces of + # indent so that it works with object methods. + #} +{%- macro setup_args_extra_indent(func) %} + {%- for arg in func.arguments() %} + {%- match arg.default_value() %} + {%- when None %} + {{ arg.name()|var_name }} = {{ arg.name()|var_name|coerce_py(arg.type_().borrow()) -}} + {%- when Some with(literal) %} + if {{ arg.name()|var_name }} is DEFAULT: + {{ arg.name()|var_name }} = {{ literal|literal_py(arg.type_().borrow()) }} + else: + {{ arg.name()|var_name }} = {{ arg.name()|var_name|coerce_py(arg.type_().borrow()) -}} + {%- endmatch %} + {% endfor -%} +{%- endmacro -%} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/templates/wrapper.py b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/wrapper.py new file mode 100644 index 0000000000..d30d3c9d12 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/templates/wrapper.py @@ -0,0 +1,71 @@ +# This file was autogenerated by some hot garbage in the `uniffi` crate. +# Trust me, you don't want to mess with it! + +# Tell mypy (a type checker) to ignore all errors from this file. +# See https://mypy.readthedocs.io/en/stable/config_file.html?highlight=ignore-errors#confval-ignore_errors +# mypy: ignore-errors + +# Common helper code. +# +# Ideally this would live in a separate .py file where it can be unittested etc +# in isolation, and perhaps even published as a re-useable package. +# +# However, it's important that the details of how this helper code works (e.g. the +# way that different builtin types are passed across the FFI) exactly match what's +# expected by the rust code on the other side of the interface. In practice right +# now that means coming from the exact some version of `uniffi` that was used to +# compile the rust component. The easiest way to ensure this is to bundle the Python +# helpers directly inline like we're doing here. + +import os +import sys +import ctypes +import enum +import struct +import contextlib +import datetime +{%- for req in self.imports() %} +{{ req.render() }} +{%- endfor %} + +# Used for default argument values +DEFAULT = object() + +{% include "RustBufferTemplate.py" %} +{% include "Helpers.py" %} +{% include "RustBufferHelper.py" %} + +# Contains loading, initialization code, +# and the FFI Function declarations in a com.sun.jna.Library. +{% include "NamespaceLibraryTemplate.py" %} + +# Public interface members begin here. +{{ type_helper_code }} + +{%- for func in ci.function_definitions() %} +{%- include "TopLevelFunctionTemplate.py" %} +{%- endfor %} + +__all__ = [ + "InternalError", + {%- for e in ci.enum_definitions() %} + "{{ e|type_name }}", + {%- endfor %} + {%- for record in ci.record_definitions() %} + "{{ record|type_name }}", + {%- endfor %} + {%- for func in ci.function_definitions() %} + "{{ func.name()|fn_name }}", + {%- endfor %} + {%- for obj in ci.object_definitions() %} + "{{ obj|type_name }}", + {%- endfor %} + {%- for e in ci.error_definitions() %} + "{{ e|type_name }}", + {%- endfor %} + {%- for c in ci.callback_interface_definitions() %} + "{{ c.name()|class_name }}", + {%- endfor %} +] + +{% import "macros.py" as py %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/python/test.rs b/third_party/rust/uniffi_bindgen/src/bindings/python/test.rs new file mode 100644 index 0000000000..d050fc0389 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/python/test.rs @@ -0,0 +1,62 @@ +/* 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 anyhow::{Context, Result}; +use camino::Utf8Path; +use std::env; +use std::ffi::OsString; +use std::process::{Command, Stdio}; +use uniffi_testing::UniFFITestHelper; + +/// Run Python tests for a UniFFI test fixture +pub fn run_test(tmp_dir: &str, fixture_name: &str, script_file: &str) -> Result<()> { + let script_path = Utf8Path::new(".").join(script_file).canonicalize_utf8()?; + let test_helper = UniFFITestHelper::new(fixture_name).context("UniFFITestHelper::new")?; + let out_dir = test_helper + .create_out_dir(tmp_dir, &script_path) + .context("create_out_dir")?; + test_helper + .copy_cdylibs_to_out_dir(&out_dir) + .context("copy_cdylibs_to_out_dir")?; + generate_sources(&test_helper.cdylib_path()?, &out_dir, &test_helper) + .context("generate_sources")?; + + let pythonpath = env::var_os("PYTHONPATH").unwrap_or_else(|| OsString::from("")); + let pythonpath = env::join_paths( + env::split_paths(&pythonpath).chain(vec![out_dir.to_path_buf().into_std_path_buf()]), + )?; + + let status = Command::new("python3") + .current_dir(out_dir) + .env("PYTHONPATH", pythonpath) + .arg(script_path) + .stderr(Stdio::inherit()) + .stdout(Stdio::inherit()) + .spawn() + .context("Failed to spawn `python3` when running script")? + .wait() + .context("Failed to wait for `python3` when running script")?; + if !status.success() { + anyhow::bail!("running `python3` failed"); + } + Ok(()) +} + +fn generate_sources( + library_path: &Utf8Path, + out_dir: &Utf8Path, + test_helper: &UniFFITestHelper, +) -> Result<()> { + for source in test_helper.get_compile_sources()? { + crate::generate_bindings( + &source.udl_path, + source.config_path.as_deref(), + vec!["python"], + Some(out_dir), + Some(library_path), + false, + )?; + } + Ok(()) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs new file mode 100644 index 0000000000..9d0fc55bdc --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs @@ -0,0 +1,273 @@ +/* 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 anyhow::Result; +use askama::Template; +use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; + +use crate::interface::*; +use crate::MergeWith; + +const RESERVED_WORDS: &[&str] = &[ + "alias", "and", "BEGIN", "begin", "break", "case", "class", "def", "defined?", "do", "else", + "elsif", "END", "end", "ensure", "false", "for", "if", "module", "next", "nil", "not", "or", + "redo", "rescue", "retry", "return", "self", "super", "then", "true", "undef", "unless", + "until", "when", "while", "yield", "__FILE__", "__LINE__", +]; + +fn is_reserved_word(word: &str) -> bool { + RESERVED_WORDS.contains(&word) +} + +// Some config options for it the caller wants to customize the generated ruby. +// Note that this can only be used to control details of the ruby *that do not affect the underlying component*, +// since the details of the underlying component are entirely determined by the `ComponentInterface`. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Config { + cdylib_name: Option<String>, + cdylib_path: Option<String>, +} + +impl Config { + pub fn cdylib_name(&self) -> String { + self.cdylib_name + .clone() + .unwrap_or_else(|| "uniffi".to_string()) + } + + pub fn custom_cdylib_path(&self) -> bool { + self.cdylib_path.is_some() + } + + pub fn cdylib_path(&self) -> String { + self.cdylib_path.clone().unwrap_or_default() + } +} + +impl From<&ComponentInterface> for Config { + fn from(ci: &ComponentInterface) -> Self { + Config { + cdylib_name: Some(format!("uniffi_{}", ci.namespace())), + cdylib_path: None, + } + } +} + +impl MergeWith for Config { + fn merge_with(&self, other: &Self) -> Self { + Config { + cdylib_name: self.cdylib_name.merge_with(&other.cdylib_name), + cdylib_path: self.cdylib_path.merge_with(&other.cdylib_path), + } + } +} + +#[derive(Template)] +#[template(syntax = "rb", escape = "none", path = "wrapper.rb")] +pub struct RubyWrapper<'a> { + config: Config, + ci: &'a ComponentInterface, +} +impl<'a> RubyWrapper<'a> { + pub fn new(config: Config, ci: &'a ComponentInterface) -> Self { + Self { config, ci } + } +} + +mod filters { + use super::*; + + pub fn type_ffi(type_: &FfiType) -> Result<String, askama::Error> { + Ok(match type_ { + FfiType::Int8 => ":int8".to_string(), + FfiType::UInt8 => ":uint8".to_string(), + FfiType::Int16 => ":int16".to_string(), + FfiType::UInt16 => ":uint16".to_string(), + FfiType::Int32 => ":int32".to_string(), + FfiType::UInt32 => ":uint32".to_string(), + FfiType::Int64 => ":int64".to_string(), + FfiType::UInt64 => ":uint64".to_string(), + FfiType::Float32 => ":float".to_string(), + FfiType::Float64 => ":double".to_string(), + FfiType::RustArcPtr(_) => ":pointer".to_string(), + FfiType::RustBuffer(_) => "RustBuffer.by_value".to_string(), + FfiType::ForeignBytes => "ForeignBytes".to_string(), + FfiType::ForeignCallback => unimplemented!("Callback interfaces are not implemented"), + }) + } + + pub fn literal_rb(literal: &Literal) -> Result<String, askama::Error> { + Ok(match literal { + Literal::Boolean(v) => { + if *v { + "true".into() + } else { + "false".into() + } + } + // use the double-quote form to match with the other languages, and quote escapes. + Literal::String(s) => format!("\"{s}\""), + Literal::Null => "nil".into(), + Literal::EmptySequence => "[]".into(), + Literal::EmptyMap => "{}".into(), + Literal::Enum(v, type_) => match type_ { + Type::Enum(name) => format!("{}::{}", class_name_rb(name)?, enum_name_rb(v)?), + _ => panic!("Unexpected type in enum literal: {type_:?}"), + }, + // https://docs.ruby-lang.org/en/2.0.0/syntax/literals_rdoc.html + Literal::Int(i, radix, _) => match radix { + Radix::Octal => format!("0o{i:o}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + Literal::UInt(i, radix, _) => match radix { + Radix::Octal => format!("0o{i:o}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + Literal::Float(string, _type_) => string.clone(), + }) + } + + pub fn class_name_rb(nm: &str) -> Result<String, askama::Error> { + Ok(nm.to_string().to_upper_camel_case()) + } + + pub fn fn_name_rb(nm: &str) -> Result<String, askama::Error> { + Ok(nm.to_string().to_snake_case()) + } + + pub fn var_name_rb(nm: &str) -> Result<String, askama::Error> { + let nm = nm.to_string(); + let prefix = if is_reserved_word(&nm) { "_" } else { "" }; + + Ok(format!("{prefix}{}", nm.to_snake_case())) + } + + pub fn enum_name_rb(nm: &str) -> Result<String, askama::Error> { + Ok(nm.to_string().to_shouty_snake_case()) + } + + pub fn coerce_rb(nm: &str, type_: &Type) -> Result<String, askama::Error> { + Ok(match type_ { + Type::Int8 + | Type::UInt8 + | Type::Int16 + | Type::UInt16 + | Type::Int32 + | Type::UInt32 + | Type::Int64 + | Type::UInt64 => format!("{nm}.to_i"), // TODO: check max/min value + Type::Float32 | Type::Float64 => format!("{nm}.to_f"), + Type::Boolean => format!("{nm} ? true : false"), + Type::Object(_) | Type::Enum(_) | Type::Error(_) | Type::Record(_) => nm.to_string(), + Type::String => format!("{nm}.to_s"), + Type::Timestamp | Type::Duration => nm.to_string(), + Type::CallbackInterface(_) => panic!("No support for coercing callback interfaces yet"), + Type::Optional(t) => format!("({nm} ? {} : nil)", coerce_rb(nm, t)?), + Type::Sequence(t) => { + let coerce_code = coerce_rb("v", t)?; + if coerce_code == "v" { + nm.to_string() + } else { + format!("{nm}.map {{ |v| {coerce_code} }}") + } + } + Type::Map(_k, t) => { + let k_coerce_code = coerce_rb("k", &Type::String)?; + let v_coerce_code = coerce_rb("v", t)?; + + if k_coerce_code == "k" && v_coerce_code == "v" { + nm.to_string() + } else { + format!( + "{}.each.with_object({{}}) {{ |(k, v), res| res[{}] = {} }}", + nm, k_coerce_code, v_coerce_code, + ) + } + } + Type::External { .. } => panic!("No support for external types, yet"), + Type::Custom { .. } => panic!("No support for custom types, yet"), + Type::Unresolved { name } => { + unreachable!("Type `{name}` must be resolved before calling coerce_rb") + } + }) + } + + pub fn lower_rb(nm: &str, type_: &Type) -> Result<String, askama::Error> { + Ok(match type_ { + Type::Int8 + | Type::UInt8 + | Type::Int16 + | Type::UInt16 + | Type::Int32 + | Type::UInt32 + | Type::Int64 + | Type::UInt64 + | Type::Float32 + | Type::Float64 => nm.to_string(), + Type::Boolean => format!("({nm} ? 1 : 0)"), + Type::String => format!("RustBuffer.allocFromString({nm})"), + Type::Object(name) => format!("({}._uniffi_lower {nm})", class_name_rb(name)?), + Type::CallbackInterface(_) => panic!("No support for lowering callback interfaces yet"), + Type::Error(_) => panic!("No support for lowering errors, yet"), + Type::Enum(_) + | Type::Record(_) + | Type::Optional(_) + | Type::Sequence(_) + | Type::Timestamp + | Type::Duration + | Type::Map(_, _) => format!( + "RustBuffer.alloc_from_{}({})", + class_name_rb(&type_.canonical_name())?, + nm + ), + Type::External { .. } => panic!("No support for lowering external types, yet"), + Type::Custom { .. } => panic!("No support for lowering custom types, yet"), + Type::Unresolved { name } => { + unreachable!("Type `{name}` must be resolved before calling lower_rb") + } + }) + } + + pub fn lift_rb(nm: &str, type_: &Type) -> Result<String, askama::Error> { + Ok(match type_ { + Type::Int8 + | Type::UInt8 + | Type::Int16 + | Type::UInt16 + | Type::Int32 + | Type::UInt32 + | Type::Int64 + | Type::UInt64 => format!("{nm}.to_i"), + Type::Float32 | Type::Float64 => format!("{nm}.to_f"), + Type::Boolean => format!("1 == {nm}"), + Type::String => format!("{nm}.consumeIntoString"), + Type::Object(name) => format!("{}._uniffi_allocate({nm})", class_name_rb(name)?), + Type::CallbackInterface(_) => panic!("No support for lifting callback interfaces, yet"), + Type::Error(_) => panic!("No support for lowering errors, yet"), + Type::Enum(_) + | Type::Record(_) + | Type::Optional(_) + | Type::Sequence(_) + | Type::Timestamp + | Type::Duration + | Type::Map(_, _) => format!( + "{}.consumeInto{}", + nm, + class_name_rb(&type_.canonical_name())? + ), + Type::External { .. } => panic!("No support for lifting external types, yet"), + Type::Custom { .. } => panic!("No support for lifting custom types, yet"), + Type::Unresolved { name } => { + unreachable!("Type `{name}` must be resolved before calling lift_rb") + } + }) + } +} + +#[cfg(test)] +mod tests; diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs new file mode 100644 index 0000000000..9ae5d1816f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs @@ -0,0 +1,47 @@ +use super::{is_reserved_word, Config}; + +#[test] +fn when_reserved_word() { + assert!(is_reserved_word("end")); +} + +#[test] +fn when_not_reserved_word() { + assert!(!is_reserved_word("ruby")); +} + +#[test] +fn cdylib_name() { + let config = Config { + cdylib_name: None, + cdylib_path: None, + }; + + assert_eq!("uniffi", config.cdylib_name()); + + let config = Config { + cdylib_name: Some("todolist".to_string()), + cdylib_path: None, + }; + + assert_eq!("todolist", config.cdylib_name()); +} + +#[test] +fn cdylib_path() { + let config = Config { + cdylib_name: None, + cdylib_path: None, + }; + + assert_eq!("", config.cdylib_path()); + assert!(!config.custom_cdylib_path()); + + let config = Config { + cdylib_name: None, + cdylib_path: Some("/foo/bar".to_string()), + }; + + assert_eq!("/foo/bar", config.cdylib_path()); + assert!(config.custom_cdylib_path()); +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/ruby/mod.rs new file mode 100644 index 0000000000..e0d789f42f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/mod.rs @@ -0,0 +1,50 @@ +/* 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::{io::Write, process::Command}; + +use anyhow::{Context, Result}; +use camino::Utf8Path; +use fs_err::File; + +pub mod gen_ruby; +mod test; +pub use gen_ruby::{Config, RubyWrapper}; +pub use test::run_test; + +use super::super::interface::ComponentInterface; + +// Generate ruby bindings for the given ComponentInterface, in the given output directory. + +pub fn write_bindings( + config: &Config, + ci: &ComponentInterface, + out_dir: &Utf8Path, + try_format_code: bool, +) -> Result<()> { + let rb_file = out_dir.join(format!("{}.rb", ci.namespace())); + let mut f = File::create(&rb_file)?; + write!(f, "{}", generate_ruby_bindings(config, ci)?)?; + + if try_format_code { + if let Err(e) = Command::new("rubocop").arg("-A").arg(&rb_file).output() { + println!( + "Warning: Unable to auto-format {} using rubocop: {:?}", + rb_file.file_name().unwrap(), + e + ) + } + } + + Ok(()) +} + +// Generate ruby bindings for the given ComponentInterface, as a string. + +pub fn generate_ruby_bindings(config: &Config, ci: &ComponentInterface) -> Result<String> { + use askama::Template; + RubyWrapper::new(config.clone(), ci) + .render() + .context("failed to render ruby bindings") +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/EnumTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/EnumTemplate.rb new file mode 100644 index 0000000000..23b701f6a7 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/EnumTemplate.rb @@ -0,0 +1,59 @@ +{% if e.is_flat() %} + +class {{ e.name()|class_name_rb }} + {% for variant in e.variants() -%} + {{ variant.name()|enum_name_rb }} = {{ loop.index }} + {% endfor %} +end + +{% else %} + +class {{ e.name()|class_name_rb }} + def initialize + raise RuntimeError, '{{ e.name()|class_name_rb }} cannot be instantiated directly' + end + + # Each enum variant is a nested class of the enum itself. + {% for variant in e.variants() -%} + class {{ variant.name()|enum_name_rb }} + {% if variant.has_fields() %} + attr_reader {% for field in variant.fields() %}:{{ field.name()|var_name_rb }}{% if loop.last %}{% else %}, {% endif %}{%- endfor %} + {% endif %} + def initialize({% for field in variant.fields() %}{{ field.name()|var_name_rb }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) + {% if variant.has_fields() %} + {%- for field in variant.fields() %} + @{{ field.name()|var_name_rb }} = {{ field.name()|var_name_rb }} + {%- endfor %} + {% else %} + {% endif %} + end + + def to_s + "{{ e.name()|class_name_rb }}::{{ variant.name()|enum_name_rb }}({% for field in variant.fields() %}{{ field.name() }}=#{@{{ field.name() }}}{% if loop.last %}{% else %}, {% endif %}{% endfor %})" + end + + def ==(other) + if !other.{{ variant.name()|var_name_rb }}? + return false + end + {%- for field in variant.fields() %} + if @{{ field.name()|var_name_rb }} != other.{{ field.name()|var_name_rb }} + return false + end + {%- endfor %} + + true + end + + # For each variant, we have an `NAME?` method for easily checking + # whether an instance is that variant. + {% for variant in e.variants() %} + def {{ variant.name()|var_name_rb }}? + instance_of? {{ e.name()|class_name_rb }}::{{ variant.name()|enum_name_rb }} + end + {% endfor %} + end + {% endfor %} +end + +{% endif %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb new file mode 100644 index 0000000000..3a64e9ffeb --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb @@ -0,0 +1,117 @@ +class RustCallStatus < FFI::Struct + layout :code, :int8, + :error_buf, RustBuffer + + def code + self[:code] + end + + def error_buf + self[:error_buf] + end + + def to_s + "RustCallStatus(code=#{self[:code]})" + end +end + +# These match the values from the uniffi::rustcalls module +CALL_SUCCESS = 0 +CALL_ERROR = 1 +CALL_PANIC = 2 +{%- for e in ci.error_definitions() %} +{% if e.is_flat() %} +class {{ e.name()|class_name_rb }} + {%- for variant in e.variants() %} + {{ variant.name()|class_name_rb }} = Class.new StandardError + {%- endfor %} +{% else %} +module {{ e.name()|class_name_rb }} + {%- for variant in e.variants() %} + class {{ variant.name()|class_name_rb }} < StandardError + def initialize({% for field in variant.fields() %}{{ field.name()|var_name_rb }}{% if !loop.last %}, {% endif %}{% endfor %}) + {%- for field in variant.fields() %} + @{{ field.name()|var_name_rb }} = {{ field.name()|var_name_rb }} + {%- endfor %} + super() + end + {%- if variant.has_fields() %} + + attr_reader {% for field in variant.fields() %}:{{ field.name()|var_name_rb }}{% if !loop.last %}, {% endif %}{% endfor %} + {% endif %} + + def to_s + "#{self.class.name}({% for field in variant.fields() %}{{ field.name()|var_name_rb }}=#{@{{ field.name()|var_name_rb }}.inspect}{% if !loop.last %}, {% endif %}{% endfor %})" + end + end + {%- endfor %} +{% endif %} +end +{%- endfor %} + +# Map error modules to the RustBuffer method name that reads them +ERROR_MODULE_TO_READER_METHOD = { +{%- for e in ci.error_definitions() %} +{%- let typ=ci.get_type(e.name()).unwrap() %} +{%- let canonical_type_name = typ.canonical_name().borrow()|class_name_rb %} + {{ e.name()|class_name_rb }} => :read{{ canonical_type_name }}, +{%- endfor %} +} + +private_constant :ERROR_MODULE_TO_READER_METHOD, :CALL_SUCCESS, :CALL_ERROR, :CALL_PANIC, + :RustCallStatus + +def self.consume_buffer_into_error(error_module, rust_buffer) + rust_buffer.consumeWithStream do |stream| + reader_method = ERROR_MODULE_TO_READER_METHOD[error_module] + return stream.send(reader_method) + end +end + +class InternalError < StandardError +end + +def self.rust_call(fn_name, *args) + # Call a rust function + rust_call_with_error(nil, fn_name, *args) +end + +def self.rust_call_with_error(error_module, fn_name, *args) + # Call a rust function and handle errors + # + # Use this when the rust function returns a Result<>. error_module must be the error_module that corresponds to that Result. + + + # Note: RustCallStatus.new zeroes out the struct, which is exactly what we + # want to pass to Rust (code=0, error_buf=RustBuffer(len=0, capacity=0, + # data=NULL)) + status = RustCallStatus.new + args << status + + result = UniFFILib.public_send(fn_name, *args) + + case status.code + when CALL_SUCCESS + result + when CALL_ERROR + if error_module.nil? + status.error_buf.free + raise InternalError, "CALL_ERROR with no error_module set" + else + raise consume_buffer_into_error(error_module, status.error_buf) + end + when CALL_PANIC + # When the rust code sees a panic, it tries to construct a RustBuffer + # with the message. But if that code panics, then it just sends back + # an empty buffer. + if status.error_buf.len > 0 + raise InternalError, status.error_buf.consumeIntoString() + else + raise InternalError, "Rust panic" + end + else + raise InternalError, "Unknown call status: #{status.code}" + end +end + +private_class_method :consume_buffer_into_error diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb new file mode 100644 index 0000000000..858b42bf91 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb @@ -0,0 +1,17 @@ +# This is how we find and load the dynamic library provided by the component. +# For now we just look it up by name. +module UniFFILib + extend FFI::Library + + {% if config.custom_cdylib_path() %} + ffi_lib {{ config.cdylib_path() }} + {% else %} + ffi_lib '{{ config.cdylib_name() }}' + {% endif %} + + {% for func in ci.iter_ffi_function_definitions() -%} + attach_function :{{ func.name() }}, + {%- call rb::arg_list_ffi_decl(func) %}, + {% match func.return_type() %}{% when Some with (type_) %}{{ type_|type_ffi }}{% when None %}:void{% endmatch %} + {% endfor %} +end diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ObjectTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ObjectTemplate.rb new file mode 100644 index 0000000000..677c5c729b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ObjectTemplate.rb @@ -0,0 +1,73 @@ +class {{ obj.name()|class_name_rb }} + + # A private helper for initializing instances of the class from a raw pointer, + # bypassing any initialization logic and ensuring they are GC'd properly. + def self._uniffi_allocate(pointer) + pointer.autorelease = false + inst = allocate + inst.instance_variable_set :@pointer, pointer + ObjectSpace.define_finalizer(inst, _uniffi_define_finalizer_by_pointer(pointer, inst.object_id)) + return inst + end + + # A private helper for registering an object finalizer. + # N.B. it's important that this does not capture a reference + # to the actual instance, only its underlying pointer. + def self._uniffi_define_finalizer_by_pointer(pointer, object_id) + Proc.new do |_id| + {{ ci.namespace()|class_name_rb }}.rust_call( + :{{ obj.ffi_object_free().name() }}, + pointer + ) + end + end + + # A private helper for lowering instances into a raw pointer. + # This does an explicit typecheck, because accidentally lowering a different type of + # object in a place where this type is expected, could lead to memory unsafety. + def self._uniffi_lower(inst) + if not inst.is_a? self + raise TypeError.new "Expected a {{ obj.name()|class_name_rb }} instance, got #{inst}" + end + return inst.instance_variable_get :@pointer + end + + {%- match obj.primary_constructor() %} + {%- when Some with (cons) %} + def initialize({% call rb::arg_list_decl(cons) -%}) + {%- call rb::coerce_args_extra_indent(cons) %} + pointer = {% call rb::to_ffi_call(cons) %} + @pointer = pointer + ObjectSpace.define_finalizer(self, self.class._uniffi_define_finalizer_by_pointer(pointer, self.object_id)) + end + {%- when None %} + {%- endmatch %} + + {% for cons in obj.alternate_constructors() -%} + def self.{{ cons.name()|fn_name_rb }}({% call rb::arg_list_decl(cons) %}) + {%- call rb::coerce_args_extra_indent(cons) %} + # Call the (fallible) function before creating any half-baked object instances. + # Lightly yucky way to bypass the usual "initialize" logic + # and just create a new instance with the required pointer. + return _uniffi_allocate({% call rb::to_ffi_call(cons) %}) + end + {% endfor %} + + {% for meth in obj.methods() -%} + {%- match meth.return_type() -%} + + {%- when Some with (return_type) -%} + def {{ meth.name()|fn_name_rb }}({% call rb::arg_list_decl(meth) %}) + {%- call rb::coerce_args_extra_indent(meth) %} + result = {% call rb::to_ffi_call_with_prefix("@pointer", meth) %} + return {{ "result"|lift_rb(return_type) }} + end + + {%- when None -%} + def {{ meth.name()|fn_name_rb }}({% call rb::arg_list_decl(meth) %}) + {%- call rb::coerce_args_extra_indent(meth) %} + {% call rb::to_ffi_call_with_prefix("@pointer", meth) %} + end + {% endmatch %} + {% endfor %} +end diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RecordTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RecordTemplate.rb new file mode 100644 index 0000000000..c940b31060 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RecordTemplate.rb @@ -0,0 +1,20 @@ +# Record type {{ rec.name() }} +class {{ rec.name()|class_name_rb }} + attr_reader {% for field in rec.fields() %}:{{ field.name()|var_name_rb }}{% if loop.last %}{% else %}, {% endif %}{%- endfor %} + + def initialize({% for field in rec.fields() %}{{ field.name()|var_name_rb }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) + {%- for field in rec.fields() %} + @{{ field.name()|var_name_rb }} = {{ field.name()|var_name_rb }} + {%- endfor %} + end + + def ==(other) + {%- for field in rec.fields() %} + if @{{ field.name()|var_name_rb }} != other.{{ field.name()|var_name_rb }} + return false + end + {%- endfor %} + + true + end +end diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb new file mode 100644 index 0000000000..e4b3910b6c --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb @@ -0,0 +1,246 @@ + +# Helper for structured writing of values into a RustBuffer. +class RustBufferBuilder + def initialize + @rust_buf = RustBuffer.alloc 16 + @rust_buf.len = 0 + end + + def finalize + rbuf = @rust_buf + + @rust_buf = nil + + rbuf + end + + def discard + return if @rust_buf.nil? + + rbuf = finalize + rbuf.free + end + + def write(value) + reserve(value.bytes.size) do + @rust_buf.data.put_array_of_char @rust_buf.len, value.bytes + end + end + + {% for typ in ci.iter_types() -%} + {%- let canonical_type_name = typ.canonical_name().borrow()|class_name_rb -%} + {%- match typ -%} + + {% when Type::Int8 -%} + + def write_I8(v) + pack_into(1, 'c', v) + end + + {% when Type::UInt8 -%} + + def write_U8(v) + pack_into(1, 'c', v) + end + + {% when Type::Int16 -%} + + def write_I16(v) + pack_into(2, 's>', v) + end + + {% when Type::UInt16 -%} + + def write_U16(v) + pack_into(2, 'S>', v) + end + + {% when Type::Int32 -%} + + def write_I32(v) + pack_into(4, 'l>', v) + end + + {% when Type::UInt32 -%} + + def write_U32(v) + pack_into(4, 'L>', v) + end + + {% when Type::Int64 -%} + + def write_I64(v) + pack_into(8, 'q>', v) + end + + {% when Type::UInt64 -%} + + def write_U64(v) + pack_into(8, 'Q>', v) + end + + {% when Type::Float32 -%} + + def write_F32(v) + pack_into(4, 'g', v) + end + + {% when Type::Float64 -%} + + def write_F64(v) + pack_into(8, 'G', v) + end + + {% when Type::Boolean -%} + + def write_Bool(v) + pack_into(1, 'c', v ? 1 : 0) + end + + {% when Type::String -%} + + def write_String(v) + v = v.to_s + pack_into 4, 'l>', v.bytes.size + write v + end + + {% when Type::Timestamp -%} + # The Timestamp type. + ONE_SECOND_IN_NANOSECONDS = 10**9 + + def write_{{ canonical_type_name }}(v) + seconds = v.tv_sec + nanoseconds = v.tv_nsec + + # UniFFi conventions assume that nanoseconds part has to represent nanoseconds portion of + # duration between epoch and the timestamp moment. Ruby `Time#tv_nsec` returns the number of + # nanoseconds for the subsecond part, which is sort of opposite to "duration" meaning. + # Hence we need to convert value returned by `Time#tv_nsec` back and forth with the following + # logic: + if seconds < 0 && nanoseconds != 0 + # In order to get duration nsec we shift by 1 second: + nanoseconds = ONE_SECOND_IN_NANOSECONDS - nanoseconds + + # Then we compensate 1 second shift: + seconds += 1 + end + + pack_into 8, 'q>', seconds + pack_into 4, 'L>', nanoseconds + end + + {% when Type::Duration -%} + # The Duration type. + + def write_{{ canonical_type_name }}(v) + seconds = v.tv_sec + nanoseconds = v.tv_nsec + + raise ArgumentError, 'Invalid duration, must be non-negative' if seconds < 0 + + pack_into 8, 'Q>', seconds + pack_into 4, 'L>', nanoseconds + end + + {% when Type::Object with (object_name) -%} + # The Object type {{ object_name }}. + + def write_{{ canonical_type_name }}(obj) + pointer = {{ object_name|class_name_rb}}._uniffi_lower obj + pack_into(8, 'Q>', pointer.address) + end + + {% when Type::Enum with (enum_name) -%} + {%- let e = ci.get_enum_definition(enum_name).unwrap() -%} + # The Enum type {{ enum_name }}. + + def write_{{ canonical_type_name }}(v) + {%- if e.is_flat() %} + pack_into(4, 'l>', v) + {%- else -%} + {%- for variant in e.variants() %} + if v.{{ variant.name()|var_name_rb }}? + pack_into(4, 'l>', {{ loop.index }}) + {%- for field in variant.fields() %} + self.write_{{ field.type_().canonical_name().borrow()|class_name_rb }}(v.{{ field.name() }}) + {%- endfor %} + end + {%- endfor %} + {%- endif %} + end + + {% when Type::Record with (record_name) -%} + {%- let rec = ci.get_record_definition(record_name).unwrap() -%} + # The Record type {{ record_name }}. + + def write_{{ canonical_type_name }}(v) + {%- for field in rec.fields() %} + self.write_{{ field.type_().canonical_name().borrow()|class_name_rb }}(v.{{ field.name()|var_name_rb }}) + {%- endfor %} + end + + {% when Type::Optional with (inner_type) -%} + # The Optional<T> type for {{ inner_type.canonical_name() }}. + + def write_{{ canonical_type_name }}(v) + if v.nil? + pack_into(1, 'c', 0) + else + pack_into(1, 'c', 1) + self.write_{{ inner_type.canonical_name().borrow()|class_name_rb }}(v) + end + end + + {% when Type::Sequence with (inner_type) -%} + # The Sequence<T> type for {{ inner_type.canonical_name() }}. + + def write_{{ canonical_type_name }}(items) + pack_into(4, 'l>', items.size) + + items.each do |item| + self.write_{{ inner_type.canonical_name().borrow()|class_name_rb }}(item) + end + end + + {% when Type::Map with (k, inner_type) -%} + # The Map<T> type for {{ inner_type.canonical_name() }}. + + def write_{{ canonical_type_name }}(items) + pack_into(4, 'l>', items.size) + + items.each do |k, v| + write_String(k) + self.write_{{ inner_type.canonical_name().borrow()|class_name_rb }}(v) + end + end + + {%- else -%} + # This type is not yet supported in the Ruby backend. + def write_{{ canonical_type_name }}(v) + raise InternalError('RustBufferStream.write() not implemented yet for {{ canonical_type_name }}') + end + + {%- endmatch -%} + {%- endfor %} + + private + + def reserve(num_bytes) + if @rust_buf.len + num_bytes > @rust_buf.capacity + @rust_buf = RustBuffer.reserve(@rust_buf, num_bytes) + end + + yield + + @rust_buf.len += num_bytes + end + + def pack_into(size, format, value) + reserve(size) do + @rust_buf.data.put_array_of_char @rust_buf.len, [value].pack(format).bytes + end + end +end + +private_constant :RustBufferBuilder diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb new file mode 100644 index 0000000000..f48fbffad4 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb @@ -0,0 +1,301 @@ + +# Helper for structured reading of values from a RustBuffer. +class RustBufferStream + + def initialize(rbuf) + @rbuf = rbuf + @offset = 0 + end + + def remaining + @rbuf.len - @offset + end + + def read(size) + raise InternalError, 'read past end of rust buffer' if @offset + size > @rbuf.len + + data = @rbuf.data.get_bytes @offset, size + + @offset += size + + data + end + + {% for typ in ci.iter_types() -%} + {%- let canonical_type_name = typ.canonical_name().borrow()|class_name_rb -%} + {%- match typ -%} + + {% when Type::Int8 -%} + + def readI8 + unpack_from 1, 'c' + end + + {% when Type::UInt8 -%} + + def readU8 + unpack_from 1, 'c' + end + + {% when Type::Int16 -%} + + def readI16 + unpack_from 2, 's>' + end + + {% when Type::UInt16 -%} + + def readU16 + unpack_from 2, 'S>' + end + + {% when Type::Int32 -%} + + def readI32 + unpack_from 4, 'l>' + end + + {% when Type::UInt32 -%} + + def readU32 + unpack_from 4, 'L>' + end + + {% when Type::Int64 -%} + + def readI64 + unpack_from 8, 'q>' + end + + {% when Type::UInt64 -%} + + def readU64 + unpack_from 8, 'Q>' + end + + {% when Type::Float32 -%} + + def readF32 + unpack_from 4, 'g' + end + + {% when Type::Float64 -%} + + def readF64 + unpack_from 8, 'G' + end + + {% when Type::Boolean -%} + + def readBool + v = unpack_from 1, 'c' + + return false if v == 0 + return true if v == 1 + + raise InternalError, 'Unexpected byte for Boolean type' + end + + {% when Type::String -%} + + def readString + size = unpack_from 4, 'l>' + + raise InternalError, 'Unexpected negative string length' if size.negative? + + read(size).force_encoding(Encoding::UTF_8) + end + + {% when Type::Timestamp -%} + # The Timestamp type. + ONE_SECOND_IN_NANOSECONDS = 10**9 + + def read{{ canonical_type_name }} + seconds = unpack_from 8, 'q>' + nanoseconds = unpack_from 4, 'L>' + + # UniFFi conventions assume that nanoseconds part has to represent nanoseconds portion of + # duration between epoch and the timestamp moment. Ruby `Time#tv_nsec` returns the number of + # nanoseconds for the subsecond part, which is sort of opposite to "duration" meaning. + # Hence we need to convert value returned by `Time#tv_nsec` back and forth with the following + # logic: + if seconds < 0 && nanoseconds != 0 + # In order to get duration nsec we shift by 1 second: + nanoseconds = ONE_SECOND_IN_NANOSECONDS - nanoseconds + + # Then we compensate 1 second shift: + seconds -= 1 + end + + Time.at(seconds, nanoseconds, :nanosecond, in: '+00:00').utc + end + + {% when Type::Duration -%} + # The Duration type. + + def read{{ canonical_type_name }} + seconds = unpack_from 8, 'q>' + nanoseconds = unpack_from 4, 'L>' + + Time.at(seconds, nanoseconds, :nanosecond, in: '+00:00').utc + end + + {% when Type::Object with (object_name) -%} + # The Object type {{ object_name }}. + + def read{{ canonical_type_name }} + pointer = FFI::Pointer.new unpack_from 8, 'Q>' + return {{ object_name|class_name_rb }}._uniffi_allocate(pointer) + end + + {% when Type::Enum with (enum_name) -%} + {%- let e = ci.get_enum_definition(enum_name).unwrap() -%} + # The Enum type {{ enum_name }}. + + def read{{ canonical_type_name }} + variant = unpack_from 4, 'l>' + {% if e.is_flat() -%} + {%- for variant in e.variants() %} + if variant == {{ loop.index }} + return {{ enum_name|class_name_rb }}::{{ variant.name()|enum_name_rb }} + end + {%- endfor %} + + raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}' + {%- else -%} + {%- for variant in e.variants() %} + if variant == {{ loop.index }} + {%- if variant.has_fields() %} + return {{ enum_name|class_name_rb }}::{{ variant.name()|enum_name_rb }}.new( + {%- for field in variant.fields() %} + self.read{{ field.type_().canonical_name().borrow()|class_name_rb }}(){% if loop.last %}{% else %},{% endif %} + {%- endfor %} + ) + {%- else %} + return {{ enum_name|class_name_rb }}::{{ variant.name()|enum_name_rb }}.new + {% endif %} + end + {%- endfor %} + raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}' + {%- endif %} + end + + {% when Type::Error with (error_name) -%} + {%- let e = ci.get_error_definition(error_name).unwrap().wrapped_enum() %} + + # The Error type {{ error_name }} + + def read{{ canonical_type_name }} + variant = unpack_from 4, 'l>' + {% if e.is_flat() -%} + {%- for variant in e.variants() %} + if variant == {{ loop.index }} + return {{ error_name|class_name_rb }}::{{ variant.name()|class_name_rb }}.new( + readString() + ) + end + {%- endfor %} + + raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}' + {%- else -%} + {%- for variant in e.variants() %} + if variant == {{ loop.index }} + {%- if variant.has_fields() %} + return {{ error_name|class_name_rb }}::{{ variant.name()|class_name_rb }}.new( + {%- for field in variant.fields() %} + read{{ field.type_().canonical_name().borrow()|class_name_rb }}(){% if loop.last %}{% else %},{% endif %} + {%- endfor %} + ) + {%- else %} + return {{ error_name|class_name_rb }}::{{ variant.name()|class_name_rb }}.new + {%- endif %} + end + {%- endfor %} + + raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}' + {%- endif %} + end + + {% when Type::Record with (record_name) -%} + {%- let rec = ci.get_record_definition(record_name).unwrap() -%} + # The Record type {{ record_name }}. + + def read{{ canonical_type_name }} + {{ rec.name()|class_name_rb }}.new( + {%- for field in rec.fields() %} + read{{ field.type_().canonical_name().borrow()|class_name_rb }}{% if loop.last %}{% else %},{% endif %} + {%- endfor %} + ) + end + + {% when Type::Optional with (inner_type) -%} + # The Optional<T> type for {{ inner_type.canonical_name() }}. + + def read{{ canonical_type_name }} + flag = unpack_from 1, 'c' + + if flag == 0 + return nil + elsif flag == 1 + return read{{ inner_type.canonical_name().borrow()|class_name_rb }} + else + raise InternalError, 'Unexpected flag byte for {{ canonical_type_name }}' + end + end + + {% when Type::Sequence with (inner_type) -%} + # The Sequence<T> type for {{ inner_type.canonical_name() }}. + + def read{{ canonical_type_name }} + count = unpack_from 4, 'l>' + + raise InternalError, 'Unexpected negative sequence length' if count.negative? + + items = [] + + count.times do + items.append read{{ inner_type.canonical_name().borrow()|class_name_rb }} + end + + items + end + + {% when Type::Map with (k, inner_type) -%} + # The Map<T> type for {{ inner_type.canonical_name() }}. + + def read{{ canonical_type_name }} + count = unpack_from 4, 'l>' + raise InternalError, 'Unexpected negative map size' if count.negative? + + items = {} + count.times do + key = readString + items[key] = read{{ inner_type.canonical_name().borrow()|class_name_rb }} + end + + items + end + {%- else -%} + # This type is not yet supported in the Ruby backend. + def read{{ canonical_type_name }} + raise InternalError, 'RustBufferStream.read not implemented yet for {{ canonical_type_name }}' + end + + {%- endmatch -%} + {%- endfor %} + + def unpack_from(size, format) + raise InternalError, 'read past end of rust buffer' if @offset + size > @rbuf.len + + value = @rbuf.data.get_bytes(@offset, size).unpack format + + @offset += size + + # TODO: verify this + raise 'more than one element!!!' if value.size > 1 + + value[0] + end +end + +private_constant :RustBufferStream diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb new file mode 100644 index 0000000000..deed864572 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb @@ -0,0 +1,218 @@ +class RustBuffer < FFI::Struct + layout :capacity, :int32, + :len, :int32, + :data, :pointer + + def self.alloc(size) + return {{ ci.namespace()|class_name_rb }}.rust_call(:{{ ci.ffi_rustbuffer_alloc().name() }}, size) + end + + def self.reserve(rbuf, additional) + return {{ ci.namespace()|class_name_rb }}.rust_call(:{{ ci.ffi_rustbuffer_reserve().name() }}, rbuf, additional) + end + + def free + {{ ci.namespace()|class_name_rb }}.rust_call(:{{ ci.ffi_rustbuffer_free().name() }}, self) + end + + def capacity + self[:capacity] + end + + def len + self[:len] + end + + def len=(value) + self[:len] = value + end + + def data + self[:data] + end + + def to_s + "RustBuffer(capacity=#{capacity}, len=#{len}, data=#{data.read_bytes len})" + end + + # The allocated buffer will be automatically freed if an error occurs, ensuring that + # we don't accidentally leak it. + def self.allocWithBuilder + builder = RustBufferBuilder.new + + begin + yield builder + rescue => e + builder.discard + raise e + end + end + + # The RustBuffer will be freed once the context-manager exits, ensuring that we don't + # leak it even if an error occurs. + def consumeWithStream + stream = RustBufferStream.new self + + yield stream + + raise RuntimeError, 'junk data left in buffer after consuming' if stream.remaining != 0 + ensure + free + end + + {%- for typ in ci.iter_types() -%} + {%- let canonical_type_name = typ.canonical_name() -%} + {%- match typ -%} + + {% when Type::String -%} + # The primitive String type. + + def self.allocFromString(value) + RustBuffer.allocWithBuilder do |builder| + builder.write value.encode('utf-8') + return builder.finalize + end + end + + def consumeIntoString + consumeWithStream do |stream| + return stream.read(stream.remaining).force_encoding(Encoding::UTF_8) + end + end + + {% when Type::Timestamp -%} + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Duration -%} + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Record with (record_name) -%} + {%- let rec = ci.get_record_definition(record_name).unwrap() -%} + # The Record type {{ record_name }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Enum with (enum_name) -%} + {%- let e = ci.get_enum_definition(enum_name).unwrap() -%} + # The Enum type {{ enum_name }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Optional with (inner_type) -%} + # The Optional<T> type for {{ inner_type.canonical_name() }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize() + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Sequence with (inner_type) -%} + # The Sequence<T> type for {{ inner_type.canonical_name() }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize() + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Map with (k, inner_type) -%} + # The Map<T> type for {{ inner_type.canonical_name() }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {%- else -%} + {#- No code emitted for types that don't lower into a RustBuffer -#} + {%- endmatch -%} + {%- endfor %} +end + +module UniFFILib + class ForeignBytes < FFI::Struct + layout :len, :int32, + :data, :pointer + + def len + self[:len] + end + + def data + self[:data] + end + + def to_s + "ForeignBytes(len=#{len}, data=#{data.read_bytes(len)})" + end + end +end + +private_constant :UniFFILib diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/TopLevelFunctionTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/TopLevelFunctionTemplate.rb new file mode 100644 index 0000000000..13214cf31b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/TopLevelFunctionTemplate.rb @@ -0,0 +1,16 @@ +{%- match func.return_type() -%} +{%- when Some with (return_type) %} + +def self.{{ func.name()|fn_name_rb }}({%- call rb::arg_list_decl(func) -%}) + {%- call rb::coerce_args(func) %} + result = {% call rb::to_ffi_call(func) %} + return {{ "result"|lift_rb(return_type) }} +end + +{% when None %} + +def self.{{ func.name()|fn_name_rb }}({%- call rb::arg_list_decl(func) -%}) + {%- call rb::coerce_args(func) %} + {% call rb::to_ffi_call(func) %} +end +{% endmatch %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/macros.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/macros.rb new file mode 100644 index 0000000000..c2faf63104 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/macros.rb @@ -0,0 +1,73 @@ +{# +// Template to call into rust. Used in several places. +// Variable names in `arg_list_decl` should match up with arg lists +// passed to rust via `_arg_list_ffi_call` (we use `var_name_rb` in `lower_rb`) +#} + +{%- macro to_ffi_call(func) -%} + {%- match func.throws_name() -%} + {%- when Some with (e) -%} + {{ ci.namespace()|class_name_rb }}.rust_call_with_error({{ e|class_name_rb }}, + {%- else -%} + {{ ci.namespace()|class_name_rb }}.rust_call( + {%- endmatch -%} + :{{ func.ffi_func().name() }}, + {%- call _arg_list_ffi_call(func) -%} +) +{%- endmacro -%} + +{%- macro to_ffi_call_with_prefix(prefix, func) -%} + {%- match func.throws_name() -%} + {%- when Some with (e) -%} + {{ ci.namespace()|class_name_rb }}.rust_call_with_error({{ e|class_name_rb }}, + {%- else -%} + {{ ci.namespace()|class_name_rb }}.rust_call( + {%- endmatch -%} + :{{ func.ffi_func().name() }}, + {{- prefix }}, + {%- call _arg_list_ffi_call(func) -%} +) +{%- endmacro -%} + +{%- macro _arg_list_ffi_call(func) %} + {%- for arg in func.arguments() %} + {{- arg.name()|lower_rb(arg.type_().borrow()) }} + {%- if !loop.last %},{% endif %} + {%- endfor %} +{%- endmacro -%} + +{#- +// Arglist as used in Ruby declarations of methods, functions and constructors. +// Note the var_name_rb and type_rb filters. +-#} + +{% macro arg_list_decl(func) %} + {%- for arg in func.arguments() -%} + {{ arg.name()|var_name_rb }} + {%- match arg.default_value() %} + {%- when Some with(literal) %} = {{ literal|literal_rb }} + {%- else %} + {%- endmatch %} + {%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro %} + +{#- +// Arglist as used in the UniFFILib function declarations. +// Note unfiltered name but type_ffi filters. +-#} +{%- macro arg_list_ffi_decl(func) %} + [{%- for arg in func.arguments() -%}{{ arg.type_().borrow()|type_ffi }}, {% endfor -%} RustCallStatus.by_ref] +{%- endmacro -%} + +{%- macro coerce_args(func) %} + {%- for arg in func.arguments() %} + {{ arg.name() }} = {{ arg.name()|coerce_rb(arg.type_().borrow()) -}} + {% endfor -%} +{%- endmacro -%} + +{%- macro coerce_args_extra_indent(func) %} + {%- for arg in func.arguments() %} + {{ arg.name() }} = {{ arg.name()|coerce_rb(arg.type_().borrow()) }} + {%- endfor %} +{%- endmacro -%} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/wrapper.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/wrapper.rb new file mode 100644 index 0000000000..72cb60f73f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/wrapper.rb @@ -0,0 +1,47 @@ +# This file was autogenerated by some hot garbage in the `uniffi` crate. +# Trust me, you don't want to mess with it! + +# Common helper code. +# +# Ideally this would live in a separate .rb file where it can be unittested etc +# in isolation, and perhaps even published as a re-useable package. +# +# However, it's important that the detils of how this helper code works (e.g. the +# way that different builtin types are passed across the FFI) exactly match what's +# expected by the rust code on the other side of the interface. In practice right +# now that means coming from the exact some version of `uniffi` that was used to +# compile the rust component. The easiest way to ensure this is to bundle the Ruby +# helpers directly inline like we're doing here. + +require 'ffi' + +module {{ ci.namespace()|class_name_rb }} + {% include "RustBufferTemplate.rb" %} + {% include "RustBufferStream.rb" %} + {% include "RustBufferBuilder.rb" %} + + # Error definitions + {% include "ErrorTemplate.rb" %} + + {% include "NamespaceLibraryTemplate.rb" %} + + # Public interface members begin here. + + {% for e in ci.enum_definitions() %} + {% include "EnumTemplate.rb" %} + {%- endfor -%} + + {%- for rec in ci.record_definitions() %} + {% include "RecordTemplate.rb" %} + {% endfor %} + + {% for func in ci.function_definitions() %} + {% include "TopLevelFunctionTemplate.rb" %} + {% endfor %} + + {% for obj in ci.object_definitions() %} + {% include "ObjectTemplate.rb" %} + {% endfor %} +end + +{% import "macros.rb" as rb %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/test.rs b/third_party/rust/uniffi_bindgen/src/bindings/ruby/test.rs new file mode 100644 index 0000000000..5c445bfae5 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/test.rs @@ -0,0 +1,62 @@ +/* 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 anyhow::{bail, Context, Result}; +use camino::Utf8Path; +use std::env; +use std::ffi::OsString; +use std::process::{Command, Stdio}; +use uniffi_testing::UniFFITestHelper; + +/// Run Ruby tests for a UniFFI test fixture +pub fn run_test(tmp_dir: &str, fixture_name: &str, script_file: &str) -> Result<()> { + let script_path = Utf8Path::new(".").join(script_file).canonicalize_utf8()?; + let test_helper = UniFFITestHelper::new(fixture_name).context("UniFFITestHelper::new")?; + let out_dir = test_helper + .create_out_dir(tmp_dir, &script_path) + .context("create_out_dir")?; + test_helper + .copy_cdylibs_to_out_dir(&out_dir) + .context("copy_cdylibs_to_out_dir")?; + generate_sources(&test_helper.cdylib_path()?, &out_dir, &test_helper) + .context("generate_sources")?; + + let rubypath = env::var_os("RUBYLIB").unwrap_or_else(|| OsString::from("")); + let rubypath = env::join_paths( + env::split_paths(&rubypath).chain(vec![out_dir.to_path_buf().into_std_path_buf()]), + )?; + + let status = Command::new("ruby") + .current_dir(out_dir) + .env("RUBYLIB", rubypath) + .arg(script_path) + .stderr(Stdio::inherit()) + .stdout(Stdio::inherit()) + .spawn() + .context("Failed to spawn `ruby` when running script")? + .wait() + .context("Failed to wait for `ruby` when running script")?; + if !status.success() { + bail!("running `ruby` failed"); + } + Ok(()) +} + +fn generate_sources( + library_path: &Utf8Path, + out_dir: &Utf8Path, + test_helper: &UniFFITestHelper, +) -> Result<()> { + for source in test_helper.get_compile_sources()? { + crate::generate_bindings( + &source.udl_path, + source.config_path.as_deref(), + vec!["ruby"], + Some(out_dir), + Some(library_path), + false, + )?; + } + Ok(()) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/callback_interface.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/callback_interface.rs new file mode 100644 index 0000000000..828a8823ee --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/callback_interface.rs @@ -0,0 +1,25 @@ +/* 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 crate::backend::{CodeOracle, CodeType}; + +pub struct CallbackInterfaceCodeType { + id: String, +} + +impl CallbackInterfaceCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for CallbackInterfaceCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!("CallbackInterface{}", self.type_label(oracle)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/compounds.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/compounds.rs new file mode 100644 index 0000000000..f91be9ce54 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/compounds.rs @@ -0,0 +1,98 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal, TypeIdentifier}; + +pub struct OptionalCodeType { + inner: TypeIdentifier, +} + +impl OptionalCodeType { + pub fn new(inner: TypeIdentifier) -> Self { + Self { inner } + } +} + +impl CodeType for OptionalCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + format!("{}?", oracle.find(&self.inner).type_label(oracle)) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!("Option{}", oracle.find(&self.inner).canonical_name(oracle)) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + match literal { + Literal::Null => "nil".into(), + _ => oracle.find(&self.inner).literal(oracle, literal), + } + } +} + +pub struct SequenceCodeType { + inner: TypeIdentifier, +} + +impl SequenceCodeType { + pub fn new(inner: TypeIdentifier) -> Self { + Self { inner } + } +} + +impl CodeType for SequenceCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + format!("[{}]", oracle.find(&self.inner).type_label(oracle)) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Sequence{}", + oracle.find(&self.inner).canonical_name(oracle) + ) + } + + fn literal(&self, _oracle: &dyn CodeOracle, literal: &Literal) -> String { + match literal { + Literal::EmptySequence => "[]".into(), + _ => unreachable!(), + } + } +} + +pub struct MapCodeType { + key: TypeIdentifier, + value: TypeIdentifier, +} + +impl MapCodeType { + pub fn new(key: TypeIdentifier, value: TypeIdentifier) -> Self { + Self { key, value } + } +} + +impl CodeType for MapCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + format!( + "[{}: {}]", + oracle.find(&self.key).type_label(oracle), + oracle.find(&self.value).type_label(oracle) + ) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Dictionary{}{}", + oracle.find(&self.key).canonical_name(oracle), + oracle.find(&self.value).canonical_name(oracle) + ) + } + + fn literal(&self, _oracle: &dyn CodeOracle, literal: &Literal) -> String { + match literal { + Literal::EmptyMap => "[:]".into(), + _ => unreachable!(), + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/custom.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/custom.rs new file mode 100644 index 0000000000..bf1806e5e2 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/custom.rs @@ -0,0 +1,25 @@ +/* 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 crate::backend::{CodeOracle, CodeType}; + +pub struct CustomCodeType { + name: String, +} + +impl CustomCodeType { + pub fn new(name: String) -> Self { + CustomCodeType { name } + } +} + +impl CodeType for CustomCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + self.name.clone() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.name) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/enum_.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/enum_.rs new file mode 100644 index 0000000000..04d8422f6a --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/enum_.rs @@ -0,0 +1,33 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; + +pub struct EnumCodeType { + id: String, +} + +impl EnumCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for EnumCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + if let Literal::Enum(v, _) = literal { + format!(".{}", oracle.enum_variant_name(v)) + } else { + unreachable!(); + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/error.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/error.rs new file mode 100644 index 0000000000..4219f24c39 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/error.rs @@ -0,0 +1,25 @@ +/* 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 crate::backend::{CodeOracle, CodeType}; + +pub struct ErrorCodeType { + id: String, +} + +impl ErrorCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for ErrorCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/external.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/external.rs new file mode 100644 index 0000000000..c9a925cdaa --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/external.rs @@ -0,0 +1,35 @@ +/* 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 crate::backend::{CodeOracle, CodeType}; + +pub struct ExternalCodeType { + name: String, +} + +impl ExternalCodeType { + pub fn new(name: String) -> Self { + ExternalCodeType { name } + } +} + +impl CodeType for ExternalCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + self.name.clone() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.name) + } + + // lower and lift need to call public function which were generated for + // the original types. + fn lower(&self, oracle: &dyn CodeOracle) -> String { + format!("{}_lower", self.ffi_converter_name(oracle)) + } + + fn lift(&self, oracle: &dyn CodeOracle) -> String { + format!("{}_lift", self.ffi_converter_name(oracle)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/miscellany.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/miscellany.rs new file mode 100644 index 0000000000..4f57734934 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/miscellany.rs @@ -0,0 +1,29 @@ +/* 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 crate::backend::{CodeOracle, CodeType}; + +pub struct TimestampCodeType; + +impl CodeType for TimestampCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + "Date".into() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + "Timestamp".into() + } +} + +pub struct DurationCodeType; + +impl CodeType for DurationCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + "TimeInterval".into() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + "Duration".into() + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs new file mode 100644 index 0000000000..abbe0b3539 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs @@ -0,0 +1,476 @@ +/* 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::borrow::Borrow; +use std::cell::RefCell; +use std::collections::{BTreeSet, HashMap, HashSet}; + +use anyhow::{Context, Result}; +use askama::Template; +use heck::{ToLowerCamelCase, ToUpperCamelCase}; +use serde::{Deserialize, Serialize}; + +use super::Bindings; +use crate::backend::{CodeOracle, CodeType, TemplateExpression, TypeIdentifier}; +use crate::interface::*; +use crate::MergeWith; + +mod callback_interface; +mod compounds; +mod custom; +mod enum_; +mod error; +mod external; +mod miscellany; +mod object; +mod primitives; +mod record; + +/// Config options for the caller to customize the generated Swift. +/// +/// Note that this can only be used to control details of the Swift *that do not affect the underlying component*, +/// since the details of the underlying component are entirely determined by the `ComponentInterface`. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Config { + cdylib_name: Option<String>, + module_name: Option<String>, + ffi_module_name: Option<String>, + ffi_module_filename: Option<String>, + generate_module_map: Option<bool>, + omit_argument_labels: Option<bool>, + #[serde(default)] + custom_types: HashMap<String, CustomTypeConfig>, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct CustomTypeConfig { + imports: Option<Vec<String>>, + type_name: Option<String>, + into_custom: TemplateExpression, + from_custom: TemplateExpression, +} + +impl Config { + /// The name of the Swift module containing the high-level foreign-language bindings. + pub fn module_name(&self) -> String { + match self.module_name.as_ref() { + Some(name) => name.clone(), + None => "uniffi".into(), + } + } + + /// The name of the lower-level C module containing the FFI declarations. + pub fn ffi_module_name(&self) -> String { + match self.ffi_module_name.as_ref() { + Some(name) => name.clone(), + None => format!("{}FFI", self.module_name()), + } + } + + /// The filename stem for the lower-level C module containing the FFI declarations. + pub fn ffi_module_filename(&self) -> String { + match self.ffi_module_filename.as_ref() { + Some(name) => name.clone(), + None => self.ffi_module_name(), + } + } + + /// The name of the `.modulemap` file for the lower-level C module with FFI declarations. + pub fn modulemap_filename(&self) -> String { + format!("{}.modulemap", self.ffi_module_filename()) + } + + /// The name of the `.h` file for the lower-level C module with FFI declarations. + pub fn header_filename(&self) -> String { + format!("{}.h", self.ffi_module_filename()) + } + + /// The name of the compiled Rust library containing the FFI implementation. + pub fn cdylib_name(&self) -> String { + if let Some(cdylib_name) = &self.cdylib_name { + cdylib_name.clone() + } else { + "uniffi".into() + } + } + + /// Whether to generate a `.modulemap` file for the lower-level C module with FFI declarations. + pub fn generate_module_map(&self) -> bool { + self.generate_module_map.unwrap_or(true) + } + + /// Whether to omit argument labels in Swift function definitions. + pub fn omit_argument_labels(&self) -> bool { + self.omit_argument_labels.unwrap_or(false) + } +} + +impl From<&ComponentInterface> for Config { + fn from(ci: &ComponentInterface) -> Self { + Config { + module_name: Some(ci.namespace().into()), + cdylib_name: Some(format!("uniffi_{}", ci.namespace())), + ..Default::default() + } + } +} + +impl MergeWith for Config { + fn merge_with(&self, other: &Self) -> Self { + Config { + module_name: self.module_name.merge_with(&other.module_name), + ffi_module_name: self.ffi_module_name.merge_with(&other.ffi_module_name), + cdylib_name: self.cdylib_name.merge_with(&other.cdylib_name), + ffi_module_filename: self + .ffi_module_filename + .merge_with(&other.ffi_module_filename), + generate_module_map: self + .generate_module_map + .merge_with(&other.generate_module_map), + omit_argument_labels: self + .omit_argument_labels + .merge_with(&other.omit_argument_labels), + custom_types: self.custom_types.merge_with(&other.custom_types), + } + } +} + +/// Generate UniFFI component bindings for Swift, as strings in memory. +/// +pub fn generate_bindings(config: &Config, ci: &ComponentInterface) -> Result<Bindings> { + let header = BridgingHeader::new(config, ci) + .render() + .context("failed to render Swift bridging header")?; + let library = SwiftWrapper::new(config.clone(), ci) + .render() + .context("failed to render Swift library")?; + let modulemap = if config.generate_module_map() { + Some( + ModuleMap::new(config, ci) + .render() + .context("failed to render Swift modulemap")?, + ) + } else { + None + }; + Ok(Bindings { + library, + header, + modulemap, + }) +} + +/// Renders Swift helper code for all types +/// +/// This template is a bit different than others in that it stores internal state from the render +/// process. Make sure to only call `render()` once. +#[derive(Template)] +#[template(syntax = "kt", escape = "none", path = "Types.swift")] +pub struct TypeRenderer<'a> { + config: &'a Config, + ci: &'a ComponentInterface, + // Track included modules for the `include_once()` macro + include_once_names: RefCell<HashSet<String>>, + // Track imports added with the `add_import()` macro + imports: RefCell<BTreeSet<String>>, +} + +impl<'a> TypeRenderer<'a> { + fn new(config: &'a Config, ci: &'a ComponentInterface) -> Self { + Self { + config, + ci, + include_once_names: RefCell::new(HashSet::new()), + imports: RefCell::new(BTreeSet::new()), + } + } + + // The following methods are used by the `Types.kt` macros. + + // Helper for the including a template, but only once. + // + // The first time this is called with a name it will return true, indicating that we should + // include the template. Subsequent calls will return false. + fn include_once_check(&self, name: &str) -> bool { + self.include_once_names + .borrow_mut() + .insert(name.to_string()) + } + + // Helper to add an import statement + // + // Call this inside your template to cause an import statement to be added at the top of the + // file. Imports will be sorted and de-deuped. + // + // Returns an empty string so that it can be used inside an askama `{{ }}` block. + fn add_import(&self, name: &str) -> &str { + self.imports.borrow_mut().insert(name.to_owned()); + "" + } +} + +/// Template for generating the `.h` file that defines the low-level C FFI. +/// +/// This file defines only the low-level structs and functions that are exposed +/// by the compiled Rust code. It gets wrapped into a higher-level API by the +/// code from [`SwiftWrapper`]. +#[derive(Template)] +#[template(syntax = "c", escape = "none", path = "BridgingHeaderTemplate.h")] +pub struct BridgingHeader<'config, 'ci> { + _config: &'config Config, + ci: &'ci ComponentInterface, +} + +impl<'config, 'ci> BridgingHeader<'config, 'ci> { + pub fn new(config: &'config Config, ci: &'ci ComponentInterface) -> Self { + Self { + _config: config, + ci, + } + } +} + +/// Template for generating the `.modulemap` file that exposes the low-level C FFI. +/// +/// This file defines how the low-level C FFI from [`BridgingHeader`] gets exposed +/// as a Swift module that can be called by other Swift code. In our case, its only +/// job is to define the *name* of the Swift module that will contain the FFI functions +/// so that it can be imported by the higher-level code in from [`SwiftWrapper`]. +#[derive(Template)] +#[template(syntax = "c", escape = "none", path = "ModuleMapTemplate.modulemap")] +pub struct ModuleMap<'config, 'ci> { + config: &'config Config, + _ci: &'ci ComponentInterface, +} + +impl<'config, 'ci> ModuleMap<'config, 'ci> { + pub fn new(config: &'config Config, _ci: &'ci ComponentInterface) -> Self { + Self { config, _ci } + } +} + +#[derive(Template)] +#[template(syntax = "swift", escape = "none", path = "wrapper.swift")] +pub struct SwiftWrapper<'a> { + config: Config, + ci: &'a ComponentInterface, + type_helper_code: String, + type_imports: BTreeSet<String>, +} +impl<'a> SwiftWrapper<'a> { + pub fn new(config: Config, ci: &'a ComponentInterface) -> Self { + let type_renderer = TypeRenderer::new(&config, ci); + let type_helper_code = type_renderer.render().unwrap(); + let type_imports = type_renderer.imports.into_inner(); + Self { + config, + ci, + type_helper_code, + type_imports, + } + } + + pub fn imports(&self) -> Vec<String> { + self.type_imports.iter().cloned().collect() + } + + pub fn initialization_fns(&self) -> Vec<String> { + self.ci + .iter_types() + .into_iter() + .filter_map(|t| t.initialization_fn(&SwiftCodeOracle)) + .collect() + } +} + +#[derive(Clone)] +pub struct SwiftCodeOracle; + +impl SwiftCodeOracle { + // Map `Type` instances to a `Box<dyn CodeType>` for that type. + // + // There is a companion match in `templates/Types.swift` which performs a similar function for the + // template code. + // + // - When adding additional types here, make sure to also add a match arm to the `Types.swift` template. + // - To keep things manageable, let's try to limit ourselves to these 2 mega-matches + fn create_code_type(&self, type_: TypeIdentifier) -> Box<dyn CodeType> { + match type_ { + Type::UInt8 => Box::new(primitives::UInt8CodeType), + Type::Int8 => Box::new(primitives::Int8CodeType), + Type::UInt16 => Box::new(primitives::UInt16CodeType), + Type::Int16 => Box::new(primitives::Int16CodeType), + Type::UInt32 => Box::new(primitives::UInt32CodeType), + Type::Int32 => Box::new(primitives::Int32CodeType), + Type::UInt64 => Box::new(primitives::UInt64CodeType), + Type::Int64 => Box::new(primitives::Int64CodeType), + Type::Float32 => Box::new(primitives::Float32CodeType), + Type::Float64 => Box::new(primitives::Float64CodeType), + Type::Boolean => Box::new(primitives::BooleanCodeType), + Type::String => Box::new(primitives::StringCodeType), + + Type::Timestamp => Box::new(miscellany::TimestampCodeType), + Type::Duration => Box::new(miscellany::DurationCodeType), + + Type::Enum(id) => Box::new(enum_::EnumCodeType::new(id)), + Type::Object(id) => Box::new(object::ObjectCodeType::new(id)), + Type::Record(id) => Box::new(record::RecordCodeType::new(id)), + Type::Error(id) => Box::new(error::ErrorCodeType::new(id)), + Type::CallbackInterface(id) => { + Box::new(callback_interface::CallbackInterfaceCodeType::new(id)) + } + + Type::Optional(inner) => Box::new(compounds::OptionalCodeType::new(*inner)), + Type::Sequence(inner) => Box::new(compounds::SequenceCodeType::new(*inner)), + Type::Map(key, value) => Box::new(compounds::MapCodeType::new(*key, *value)), + Type::External { name, .. } => Box::new(external::ExternalCodeType::new(name)), + Type::Custom { name, .. } => Box::new(custom::CustomCodeType::new(name)), + Type::Unresolved { name } => { + unreachable!("Type `{name}` must be resolved before calling create_code_type") + } + } + } +} + +impl CodeOracle for SwiftCodeOracle { + fn find(&self, type_: &TypeIdentifier) -> Box<dyn CodeType> { + self.create_code_type(type_.clone()) + } + + /// Get the idiomatic Swift rendering of a class name (for enums, records, errors, etc). + fn class_name(&self, nm: &str) -> String { + nm.to_string().to_upper_camel_case() + } + + /// Get the idiomatic Swift rendering of a function name. + fn fn_name(&self, nm: &str) -> String { + format!("`{}`", nm.to_string().to_lower_camel_case()) + } + + /// Get the idiomatic Swift rendering of a variable name. + fn var_name(&self, nm: &str) -> String { + format!("`{}`", nm.to_string().to_lower_camel_case()) + } + + /// Get the idiomatic Swift rendering of an individual enum variant. + fn enum_variant_name(&self, nm: &str) -> String { + format!("`{}`", nm.to_string().to_lower_camel_case()) + } + + /// Get the idiomatic Swift rendering of an exception name. + fn error_name(&self, nm: &str) -> String { + format!("`{}`", self.class_name(nm)) + } + + fn ffi_type_label(&self, ffi_type: &FfiType) -> String { + match ffi_type { + FfiType::Int8 => "int8_t".into(), + FfiType::UInt8 => "uint8_t".into(), + FfiType::Int16 => "int16_t".into(), + FfiType::UInt16 => "uint16_t".into(), + FfiType::Int32 => "int32_t".into(), + FfiType::UInt32 => "uint32_t".into(), + FfiType::Int64 => "int64_t".into(), + FfiType::UInt64 => "uint64_t".into(), + FfiType::Float32 => "float".into(), + FfiType::Float64 => "double".into(), + FfiType::RustArcPtr(_) => "void*_Nonnull".into(), + FfiType::RustBuffer(_) => "RustBuffer".into(), + FfiType::ForeignBytes => "ForeignBytes".into(), + FfiType::ForeignCallback => "ForeignCallback _Nonnull".to_string(), + } + } +} + +pub mod filters { + use super::*; + + fn oracle() -> &'static SwiftCodeOracle { + &SwiftCodeOracle + } + + pub fn type_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(codetype.type_label(oracle)) + } + + pub fn canonical_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(codetype.canonical_name(oracle)) + } + + pub fn ffi_converter_name(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.ffi_converter_name(oracle())) + } + + pub fn lower_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.lower(oracle())) + } + + pub fn write_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.write(oracle())) + } + + pub fn lift_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.lift(oracle())) + } + + pub fn read_fn(codetype: &impl CodeType) -> Result<String, askama::Error> { + Ok(codetype.read(oracle())) + } + + pub fn literal_swift( + literal: &Literal, + codetype: &impl CodeType, + ) -> Result<String, askama::Error> { + let oracle = oracle(); + Ok(codetype.literal(oracle, literal)) + } + + /// Get the Swift syntax for representing a given low-level `FfiType`. + pub fn ffi_type_name(type_: &FfiType) -> Result<String, askama::Error> { + Ok(oracle().ffi_type_label(type_)) + } + + /// Get the type that a type is lowered into. This is subtly different than `type_ffi`, see + /// #1106 for details + pub fn type_ffi_lowered(ffi_type: &FfiType) -> Result<String, askama::Error> { + Ok(match ffi_type { + FfiType::Int8 => "Int8".into(), + FfiType::UInt8 => "UInt8".into(), + FfiType::Int16 => "Int16".into(), + FfiType::UInt16 => "UInt16".into(), + FfiType::Int32 => "Int32".into(), + FfiType::UInt32 => "UInt32".into(), + FfiType::Int64 => "Int64".into(), + FfiType::UInt64 => "UInt64".into(), + FfiType::Float32 => "float".into(), + FfiType::Float64 => "double".into(), + FfiType::RustArcPtr(_) => "void*_Nonnull".into(), + FfiType::RustBuffer(_) => "RustBuffer".into(), + FfiType::ForeignBytes => "ForeignBytes".into(), + FfiType::ForeignCallback => "ForeignCallback _Nonnull".to_string(), + }) + } + + /// Get the idiomatic Swift rendering of a class name (for enums, records, errors, etc). + pub fn class_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().class_name(nm)) + } + + /// Get the idiomatic Swift rendering of a function name. + pub fn fn_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().fn_name(nm)) + } + + /// Get the idiomatic Swift rendering of a variable name. + pub fn var_name(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().var_name(nm)) + } + + /// Get the idiomatic Swift rendering of an individual enum variant. + pub fn enum_variant_swift(nm: &str) -> Result<String, askama::Error> { + Ok(oracle().enum_variant_name(nm)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs new file mode 100644 index 0000000000..d227f8e7cd --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs @@ -0,0 +1,25 @@ +/* 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 crate::backend::{CodeOracle, CodeType}; + +pub struct ObjectCodeType { + id: String, +} + +impl ObjectCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for ObjectCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/primitives.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/primitives.rs new file mode 100644 index 0000000000..9989e0a421 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/primitives.rs @@ -0,0 +1,87 @@ +/* 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 crate::backend::{CodeOracle, CodeType, Literal}; +use crate::interface::{types::Type, Radix}; +use paste::paste; + +fn render_literal(oracle: &dyn CodeOracle, literal: &Literal) -> String { + fn typed_number(oracle: &dyn CodeOracle, type_: &Type, num_str: String) -> String { + match type_ { + // special case Int32. + Type::Int32 => num_str, + // otherwise use constructor e.g. UInt8(x) + Type::Int8 + | Type::UInt8 + | Type::Int16 + | Type::UInt16 + | Type::UInt32 + | Type::Int64 + | Type::UInt64 + | Type::Float32 + | Type::Float64 => + // XXX we should pass in the codetype itself. + { + format!("{}({num_str})", oracle.find(type_).type_label(oracle)) + } + _ => panic!("Unexpected literal: {num_str} is not a number"), + } + } + + match literal { + Literal::Boolean(v) => format!("{v}"), + Literal::String(s) => format!("\"{s}\""), + Literal::Int(i, radix, type_) => typed_number( + oracle, + type_, + match radix { + Radix::Octal => format!("0o{i:o}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + ), + Literal::UInt(i, radix, type_) => typed_number( + oracle, + type_, + match radix { + Radix::Octal => format!("0o{i:o}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + ), + Literal::Float(string, type_) => typed_number(oracle, type_, string.clone()), + _ => unreachable!("Literal"), + } +} + +macro_rules! impl_code_type_for_primitive { + ($T:ty, $class_name:literal) => { + paste! { + pub struct $T; + + impl CodeType for $T { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + $class_name.into() + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + render_literal(oracle, &literal) + } + } + } + }; +} + +impl_code_type_for_primitive!(BooleanCodeType, "Bool"); +impl_code_type_for_primitive!(StringCodeType, "String"); +impl_code_type_for_primitive!(Int8CodeType, "Int8"); +impl_code_type_for_primitive!(Int16CodeType, "Int16"); +impl_code_type_for_primitive!(Int32CodeType, "Int32"); +impl_code_type_for_primitive!(Int64CodeType, "Int64"); +impl_code_type_for_primitive!(UInt8CodeType, "UInt8"); +impl_code_type_for_primitive!(UInt16CodeType, "UInt16"); +impl_code_type_for_primitive!(UInt32CodeType, "UInt32"); +impl_code_type_for_primitive!(UInt64CodeType, "UInt64"); +impl_code_type_for_primitive!(Float32CodeType, "Float"); +impl_code_type_for_primitive!(Float64CodeType, "Double"); diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/record.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/record.rs new file mode 100644 index 0000000000..0943bcaada --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/gen_swift/record.rs @@ -0,0 +1,25 @@ +/* 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 crate::backend::{CodeOracle, CodeType}; + +pub struct RecordCodeType { + id: String, +} + +impl RecordCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for RecordCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/mod.rs new file mode 100644 index 0000000000..5fd1c404cb --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/mod.rs @@ -0,0 +1,99 @@ +/* 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/. */ + +//! # Swift bindings backend for UniFFI +//! +//! This module generates Swift bindings from a [`ComponentInterface`] definition, +//! using Swift's builtin support for loading C header files. +//! +//! Conceptually, the generated bindings are split into two Swift modules, one for the low-level +//! C FFI layer and one for the higher-level Swift bindings. For a UniFFI component named "example" +//! we generate: +//! +//! * A C header file `exampleFFI.h` declaring the low-level structs and functions for calling +//! into Rust, along with a corresponding `exampleFFI.modulemap` to expose them to Swift. +//! +//! * A Swift source file `example.swift` that imports the `exampleFFI` module and wraps it +//! to provide the higher-level Swift API. +//! +//! Most of the concepts in a [`ComponentInterface`] have an obvious counterpart in Swift, +//! with the details documented in inline comments where appropriate. +//! +//! To handle lifting/lowering/serializing types across the FFI boundary, the Swift code +//! defines a `protocol ViaFfi` that is analogous to the `uniffi::ViaFfi` Rust trait. +//! Each type that can traverse the FFI conforms to the `ViaFfi` protocol, which specifies: +//! +//! * The corresponding low-level type. +//! * How to lift from and lower into into that type. +//! * How to read from and write into a byte buffer. +//! + +use std::{io::Write, process::Command}; + +use anyhow::Result; +use camino::Utf8Path; +use fs_err::File; + +pub mod gen_swift; +pub use gen_swift::{generate_bindings, Config}; +mod test; + +use super::super::interface::ComponentInterface; +pub use test::run_test; + +/// The Swift bindings generated from a [`ComponentInterface`]. +/// +pub struct Bindings { + /// The contents of the generated `.swift` file, as a string. + library: String, + /// The contents of the generated `.h` file, as a string. + header: String, + /// The contents of the generated `.modulemap` file, as a string. + modulemap: Option<String>, +} + +/// Write UniFFI component bindings for Swift as files on disk. +/// +/// Unlike other target languages, binding to Rust code from Swift involves more than just +/// generating a `.swift` file. We also need to produce a `.h` file with the C-level API +/// declarations, and a `.modulemap` file to tell Swift how to use it. +pub fn write_bindings( + config: &Config, + ci: &ComponentInterface, + out_dir: &Utf8Path, + try_format_code: bool, +) -> Result<()> { + let Bindings { + header, + library, + modulemap, + } = generate_bindings(config, ci)?; + + let source_file = out_dir.join(format!("{}.swift", config.module_name())); + let mut l = File::create(&source_file)?; + write!(l, "{library}")?; + + let mut h = File::create(out_dir.join(config.header_filename()))?; + write!(h, "{header}")?; + + if let Some(modulemap) = modulemap { + let mut m = File::create(out_dir.join(config.modulemap_filename()))?; + write!(m, "{modulemap}")?; + } + + if try_format_code { + if let Err(e) = Command::new("swiftformat") + .arg(source_file.as_str()) + .output() + { + println!( + "Warning: Unable to auto-format {} using swiftformat: {:?}", + source_file.file_name().unwrap(), + e + ) + } + } + + Ok(()) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/BooleanHelper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/BooleanHelper.swift new file mode 100644 index 0000000000..465e519628 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/BooleanHelper.swift @@ -0,0 +1,20 @@ +fileprivate struct FfiConverterBool : FfiConverter { + typealias FfiType = Int8 + typealias SwiftType = Bool + + public static func lift(_ value: Int8) throws -> Bool { + return value != 0 + } + + public static func lower(_ value: Bool) -> Int8 { + return value ? 1 : 0 + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bool { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Bool, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h new file mode 100644 index 0000000000..50b95d96e8 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h @@ -0,0 +1,55 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +#pragma once + +#include <stdbool.h> +#include <stdint.h> + +// The following structs are used to implement the lowest level +// of the FFI, and thus useful to multiple uniffied crates. +// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H. +#ifdef UNIFFI_SHARED_H + // We also try to prevent mixing versions of shared uniffi header structs. + // If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V4 + #ifndef UNIFFI_SHARED_HEADER_V4 + #error Combining helper code from multiple versions of uniffi is not supported + #endif // ndef UNIFFI_SHARED_HEADER_V4 +#else +#define UNIFFI_SHARED_H +#define UNIFFI_SHARED_HEADER_V4 +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ + +typedef struct RustBuffer +{ + int32_t capacity; + int32_t len; + uint8_t *_Nullable data; +} RustBuffer; + +typedef int32_t (*ForeignCallback)(uint64_t, int32_t, RustBuffer, RustBuffer *_Nonnull); + +typedef struct ForeignBytes +{ + int32_t len; + const uint8_t *_Nullable data; +} ForeignBytes; + +// Error definitions +typedef struct RustCallStatus { + int8_t code; + RustBuffer errorBuf; +} RustCallStatus; + +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ +#endif // def UNIFFI_SHARED_H + +{% for func in ci.iter_ffi_function_definitions() -%} + {%- match func.return_type() -%}{%- when Some with (type_) %}{{ type_|ffi_type_name }}{% when None %}void{% endmatch %} {{ func.name() }}( + {% call swift::arg_list_ffi_decl(func) %} + ); +{% endfor -%} + +{% import "macros.swift" as swift %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift new file mode 100644 index 0000000000..9ab9bdbe48 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift @@ -0,0 +1,60 @@ +fileprivate extension NSLock { + func withLock<T>(f: () throws -> T) rethrows -> T { + self.lock() + defer { self.unlock() } + return try f() + } +} + +fileprivate typealias UniFFICallbackHandle = UInt64 +fileprivate class UniFFICallbackHandleMap<T> { + private var leftMap: [UniFFICallbackHandle: T] = [:] + private var counter: [UniFFICallbackHandle: UInt64] = [:] + private var rightMap: [ObjectIdentifier: UniFFICallbackHandle] = [:] + + private let lock = NSLock() + private var currentHandle: UniFFICallbackHandle = 0 + private let stride: UniFFICallbackHandle = 1 + + func insert(obj: T) -> UniFFICallbackHandle { + lock.withLock { + let id = ObjectIdentifier(obj as AnyObject) + let handle = rightMap[id] ?? { + currentHandle += stride + let handle = currentHandle + leftMap[handle] = obj + rightMap[id] = handle + return handle + }() + counter[handle] = (counter[handle] ?? 0) + 1 + return handle + } + } + + func get(handle: UniFFICallbackHandle) -> T? { + lock.withLock { + leftMap[handle] + } + } + + func delete(handle: UniFFICallbackHandle) { + remove(handle: handle) + } + + @discardableResult + func remove(handle: UniFFICallbackHandle) -> T? { + lock.withLock { + defer { counter[handle] = (counter[handle] ?? 1) - 1 } + guard counter[handle] == 1 else { return leftMap[handle] } + let obj = leftMap.removeValue(forKey: handle) + if let obj = obj { + rightMap.removeValue(forKey: ObjectIdentifier(obj as AnyObject)) + } + return obj + } + } +} + +// Magic number for the Rust proxy to call using the same mechanism as every other method, +// to free the callback once it's dropped by Rust. +private let IDX_CALLBACK_FREE: Int32 = 0 diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift new file mode 100644 index 0000000000..7842511a83 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift @@ -0,0 +1,154 @@ +{%- let cbi = ci.get_callback_interface_definition(name).unwrap() %} +{%- let foreign_callback = format!("foreignCallback{}", canonical_type_name) %} +{%- if self.include_once_check("CallbackInterfaceRuntime.swift") %}{%- include "CallbackInterfaceRuntime.swift" %}{%- endif %} + +// Declaration and FfiConverters for {{ type_name }} Callback Interface + +public protocol {{ type_name }} : AnyObject { + {% for meth in cbi.methods() -%} + func {{ meth.name()|fn_name }}({% call swift::arg_list_protocol(meth) %}) {% call swift::throws(meth) -%} + {%- match meth.return_type() -%} + {%- when Some with (return_type) %} -> {{ return_type|type_name -}} + {%- else -%} + {%- endmatch %} + {% endfor %} +} + +// The ForeignCallback that is passed to Rust. +fileprivate let {{ foreign_callback }} : ForeignCallback = + { (handle: UniFFICallbackHandle, method: Int32, args: RustBuffer, out_buf: UnsafeMutablePointer<RustBuffer>) -> Int32 in + {% for meth in cbi.methods() -%} + {%- let method_name = format!("invoke_{}", meth.name())|fn_name -%} + + func {{ method_name }}(_ swiftCallbackInterface: {{ type_name }}, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + {#- Unpacking args from the RustBuffer #} + {%- if meth.arguments().len() != 0 -%} + {#- Calling the concrete callback object #} + + var reader = createReader(data: Data(rustBuffer: args)) + {% if meth.return_type().is_some() %}let result = {% endif -%} + {% if meth.throws() %}try {% endif -%} + swiftCallbackInterface.{{ meth.name()|fn_name }}( + {% for arg in meth.arguments() -%} + {% if !config.omit_argument_labels() %}{{ arg.name()|var_name }}: {% endif %} try {{ arg|read_fn }}(from: &reader) + {%- if !loop.last %}, {% endif %} + {% endfor -%} + ) + {% else %} + {% if meth.return_type().is_some() %}let result = {% endif -%} + {% if meth.throws() %}try {% endif -%} + swiftCallbackInterface.{{ meth.name()|fn_name }}() + {% endif -%} + + {#- Packing up the return value into a RustBuffer #} + {%- match meth.return_type() -%} + {%- when Some with (return_type) -%} + var writer = [UInt8]() + {{ return_type|write_fn }}(result, into: &writer) + return RustBuffer(bytes: writer) + {%- else -%} + return RustBuffer() + {% endmatch -%} + // TODO catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + + } + {% endfor %} + + let cb: {{ cbi|type_name }} + do { + cb = try {{ ffi_converter_name }}.lift(handle) + } catch { + out_buf.pointee = {{ Type::String.borrow()|lower_fn }}("{{ cbi.name() }}: Invalid handle") + return -1 + } + + switch method { + case IDX_CALLBACK_FREE: + {{ ffi_converter_name }}.drop(handle: handle) + // No return value. + // See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + return 0 + {% for meth in cbi.methods() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} + case {{ loop.index }}: + do { + out_buf.pointee = try {{ method_name }}(cb, args) + // Value written to out buffer. + // See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + return 1 + {%- match meth.throws_type() %} + {%- when Some(error_type) %} + } catch let error as {{ error_type|type_name }} { + out_buf.pointee = {{ error_type|lower_fn }}(error) + return -2 + {%- else %} + {%- endmatch %} + } catch let error { + out_buf.pointee = {{ Type::String.borrow()|lower_fn }}(String(describing: error)) + return -1 + } + {% endfor %} + // This should never happen, because an out of bounds method index won't + // ever be used. Once we can catch errors, we should return an InternalError. + // https://github.com/mozilla/uniffi-rs/issues/351 + default: + // An unexpected error happened. + // See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + return -1 + } + } + +// FfiConverter protocol for callback interfaces +fileprivate struct {{ ffi_converter_name }} { + // Initialize our callback method with the scaffolding code + private static var callbackInitialized = false + private static func initCallback() { + try! rustCall { (err: UnsafeMutablePointer<RustCallStatus>) in + {{ cbi.ffi_init_callback().name() }}({{ foreign_callback }}, err) + } + } + private static func ensureCallbackinitialized() { + if !callbackInitialized { + initCallback() + callbackInitialized = true + } + } + + static func drop(handle: UniFFICallbackHandle) { + handleMap.remove(handle: handle) + } + + private static var handleMap = UniFFICallbackHandleMap<{{ type_name }}>() +} + +extension {{ ffi_converter_name }} : FfiConverter { + typealias SwiftType = {{ type_name }} + // We can use Handle as the FfiType because it's a typealias to UInt64 + typealias FfiType = UniFFICallbackHandle + + public static func lift(_ handle: UniFFICallbackHandle) throws -> SwiftType { + ensureCallbackinitialized(); + guard let callback = handleMap.get(handle: handle) else { + throw UniffiInternalError.unexpectedStaleHandle + } + return callback + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + ensureCallbackinitialized(); + let handle: UniFFICallbackHandle = try readInt(&buf) + return try lift(handle) + } + + public static func lower(_ v: SwiftType) -> UniFFICallbackHandle { + ensureCallbackinitialized(); + return handleMap.insert(obj: v) + } + + public static func write(_ v: SwiftType, into buf: inout [UInt8]) { + ensureCallbackinitialized(); + writeInt(&buf, lower(v)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CustomType.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CustomType.swift new file mode 100644 index 0000000000..cb63f0b97b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/CustomType.swift @@ -0,0 +1,86 @@ +{%- let ffi_type_name=builtin.ffi_type().borrow()|type_ffi_lowered %} +{%- match config.custom_types.get(name.as_str()) %} +{%- when None %} +{#- No config, just forward all methods to our builtin type #} +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + */ +public typealias {{ name }} = {{ builtin|type_name }} +public struct FfiConverterType{{ name }}: FfiConverter { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ name }} { + return try {{ builtin|read_fn }}(from: &buf) + } + + public static func write(_ value: {{ name }}, into buf: inout [UInt8]) { + return {{ builtin|write_fn }}(value, into: &buf) + } + + public static func lift(_ value: {{ ffi_type_name }}) throws -> {{ name }} { + return try {{ builtin|lift_fn }}(value) + } + + public static func lower(_ value: {{ name }}) -> {{ ffi_type_name }} { + return {{ builtin|lower_fn }}(value) + } +} + +{%- when Some with (config) %} + +{# When the config specifies a different type name, create a typealias for it #} +{%- match config.type_name %} +{%- when Some with (concrete_type_name) %} +/** + * Typealias from the type name used in the UDL file to the custom type. This + * is needed because the UDL type name is used in function/method signatures. + */ +public typealias {{ name }} = {{ concrete_type_name }} +{%- else %} +{%- endmatch %} + +{%- match config.imports %} +{%- when Some(imports) %} +{%- for import_name in imports %} +{{ self.add_import(import_name) }} +{%- endfor %} +{%- else %} +{%- endmatch %} + +public struct FfiConverterType{{ name }}: FfiConverter { + {#- Custom type config supplied, use it to convert the builtin type #} + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ name }} { + let builtinValue = try {{ builtin|read_fn }}(from: &buf) + return {{ config.into_custom.render("builtinValue") }} + } + + public static func write(_ value: {{ name }}, into buf: inout [UInt8]) { + let builtinValue = {{ config.from_custom.render("value") }} + return {{ builtin|write_fn }}(builtinValue, into: &buf) + } + + public static func lift(_ value: {{ ffi_type_name }}) throws -> {{ name }} { + let builtinValue = try {{ builtin|lift_fn }}(value) + return {{ config.into_custom.render("builtinValue") }} + } + + public static func lower(_ value: {{ name }}) -> {{ ffi_type_name }} { + let builtinValue = {{ config.from_custom.render("value") }} + return {{ builtin|lower_fn }}(builtinValue) + } +} + +{# +We always write these public functions just incase the type is used as +an external type by another crate. +#} +public func FfiConverterType{{ name }}_lift(_ buf: RustBuffer) throws -> {{ name }} { + return try FfiConverterType{{ name }}.lift(buf) +} + +public func FfiConverterType{{ name }}_lower(_ value: {{ name }}) -> RustBuffer { + return FfiConverterType{{ name }}.lower(value) +} + +{%- endmatch %} + diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/DurationHelper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/DurationHelper.swift new file mode 100644 index 0000000000..c2aa49e9d1 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/DurationHelper.swift @@ -0,0 +1,24 @@ +fileprivate struct FfiConverterDuration: FfiConverterRustBuffer { + typealias SwiftType = TimeInterval + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TimeInterval { + let seconds: UInt64 = try readInt(&buf) + let nanoseconds: UInt32 = try readInt(&buf) + return Double(seconds) + (Double(nanoseconds) / 1.0e9) + } + + public static func write(_ value: TimeInterval, into buf: inout [UInt8]) { + if value.rounded(.down) > Double(Int64.max) { + fatalError("Duration overflow, exceeds max bounds supported by Uniffi") + } + + if value < 0 { + fatalError("Invalid duration, must be non-negative") + } + + let seconds = UInt64(value) + let nanoseconds = UInt32((value - Double(seconds)) * 1.0e9) + writeInt(&buf, seconds) + writeInt(&buf, nanoseconds) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/EnumTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/EnumTemplate.swift new file mode 100644 index 0000000000..74f0a3effe --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/EnumTemplate.swift @@ -0,0 +1,60 @@ +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. +{%- let e = ci.get_enum_definition(name).unwrap() %} +public enum {{ type_name }} { + {% for variant in e.variants() %} + case {{ variant.name()|enum_variant_swift }}{% if variant.fields().len() > 0 %}({% call swift::field_list_decl(variant) %}){% endif -%} + {% endfor %} +} + +public struct {{ ffi_converter_name }}: FfiConverterRustBuffer { + typealias SwiftType = {{ type_name }} + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { + let variant: Int32 = try readInt(&buf) + switch variant { + {% for variant in e.variants() %} + case {{ loop.index }}: return .{{ variant.name()|enum_variant_swift }}{% if variant.has_fields() %}( + {%- for field in variant.fields() %} + {{ field.name()|var_name }}: try {{ field|read_fn }}(from: &buf) + {%- if !loop.last %}, {% endif %} + {%- endfor %} + ){%- endif %} + {% endfor %} + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: {{ type_name }}, into buf: inout [UInt8]) { + switch value { + {% for variant in e.variants() %} + {% if variant.has_fields() %} + case let .{{ variant.name()|enum_variant_swift }}({% for field in variant.fields() %}{{ field.name()|var_name }}{%- if loop.last -%}{%- else -%},{%- endif -%}{% endfor %}): + writeInt(&buf, Int32({{ loop.index }})) + {% for field in variant.fields() -%} + {{ field|write_fn }}({{ field.name()|var_name }}, into: &buf) + {% endfor -%} + {% else %} + case .{{ variant.name()|enum_variant_swift }}: + writeInt(&buf, Int32({{ loop.index }})) + {% endif %} + {%- endfor %} + } + } +} + +{# +We always write these public functions just in case the enum is used as +an external type by another crate. +#} +public func {{ ffi_converter_name }}_lift(_ buf: RustBuffer) throws -> {{ type_name }} { + return try {{ ffi_converter_name }}.lift(buf) +} + +public func {{ ffi_converter_name }}_lower(_ value: {{ type_name }}) -> RustBuffer { + return {{ ffi_converter_name }}.lower(value) +} + +{% if !contains_object_references %} +extension {{ type_name }}: Equatable, Hashable {} +{% endif %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift new file mode 100644 index 0000000000..b2c4bfe492 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift @@ -0,0 +1,83 @@ +{%- let e = ci.get_error_definition(name).unwrap() %} +public enum {{ type_name }} { + + {% if e.is_flat() %} + {% for variant in e.variants() %} + // Simple error enums only carry a message + case {{ variant.name()|class_name }}(message: String) + {% endfor %} + + {%- else %} + {% for variant in e.variants() %} + case {{ variant.name()|class_name }}{% if variant.fields().len() > 0 %}({% call swift::field_list_decl(variant) %}){% endif -%} + {% endfor %} + + {%- endif %} +} + +public struct {{ ffi_converter_name }}: FfiConverterRustBuffer { + typealias SwiftType = {{ type_name }} + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { + let variant: Int32 = try readInt(&buf) + switch variant { + + {% if e.is_flat() %} + + {% for variant in e.variants() %} + case {{ loop.index }}: return .{{ variant.name()|class_name }}( + message: try {{ Type::String.borrow()|read_fn }}(from: &buf) + ) + {% endfor %} + + {% else %} + + {% for variant in e.variants() %} + case {{ loop.index }}: return .{{ variant.name()|class_name }}{% if variant.has_fields() -%}( + {% for field in variant.fields() -%} + {{ field.name()|var_name }}: try {{ field|read_fn }}(from: &buf) + {%- if !loop.last %}, {% endif %} + {% endfor -%} + ){% endif -%} + {% endfor %} + + {% endif -%} + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: {{ type_name }}, into buf: inout [UInt8]) { + switch value { + + {% if e.is_flat() %} + + {% for variant in e.variants() %} + case let .{{ variant.name()|class_name }}(message): + writeInt(&buf, Int32({{ loop.index }})) + {{ Type::String.borrow()|write_fn }}(message, into: &buf) + {%- endfor %} + + {% else %} + + {% for variant in e.variants() %} + {% if variant.has_fields() %} + case let .{{ variant.name()|class_name }}({% for field in variant.fields() %}{{ field.name()|var_name }}{%- if loop.last -%}{%- else -%},{%- endif -%}{% endfor %}): + writeInt(&buf, Int32({{ loop.index }})) + {% for field in variant.fields() -%} + {{ field|write_fn }}({{ field.name()|var_name }}, into: &buf) + {% endfor -%} + {% else %} + case .{{ variant.name()|class_name }}: + writeInt(&buf, Int32({{ loop.index }})) + {% endif %} + {%- endfor %} + + {%- endif %} + } + } +} + +{% if !contains_object_references %} +extension {{ type_name }}: Equatable, Hashable {} +{% endif %} +extension {{ type_name }}: Error { } diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Float32Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Float32Helper.swift new file mode 100644 index 0000000000..fb986beab6 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Float32Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterFloat: FfiConverterPrimitive { + typealias FfiType = Float + typealias SwiftType = Float + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Float { + return try lift(readFloat(&buf)) + } + + public static func write(_ value: Float, into buf: inout [UInt8]) { + writeFloat(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Float64Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Float64Helper.swift new file mode 100644 index 0000000000..74421c045c --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Float64Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterDouble: FfiConverterPrimitive { + typealias FfiType = Double + typealias SwiftType = Double + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Double { + return try lift(readDouble(&buf)) + } + + public static func write(_ value: Double, into buf: inout [UInt8]) { + writeDouble(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift new file mode 100644 index 0000000000..18e2397733 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift @@ -0,0 +1,84 @@ +// An error type for FFI errors. These errors occur at the UniFFI level, not +// the library level. +fileprivate enum UniffiInternalError: LocalizedError { + case bufferOverflow + case incompleteData + case unexpectedOptionalTag + case unexpectedEnumCase + case unexpectedNullPointer + case unexpectedRustCallStatusCode + case unexpectedRustCallError + case unexpectedStaleHandle + case rustPanic(_ message: String) + + public var errorDescription: String? { + switch self { + case .bufferOverflow: return "Reading the requested value would read past the end of the buffer" + case .incompleteData: return "The buffer still has data after lifting its containing value" + case .unexpectedOptionalTag: return "Unexpected optional tag; should be 0 or 1" + case .unexpectedEnumCase: return "Raw enum value doesn't match any cases" + case .unexpectedNullPointer: return "Raw pointer value was null" + case .unexpectedRustCallStatusCode: return "Unexpected RustCallStatus code" + case .unexpectedRustCallError: return "CALL_ERROR but no errorClass specified" + case .unexpectedStaleHandle: return "The object in the handle map has been dropped already" + case let .rustPanic(message): return message + } + } +} + +fileprivate let CALL_SUCCESS: Int8 = 0 +fileprivate let CALL_ERROR: Int8 = 1 +fileprivate let CALL_PANIC: Int8 = 2 + +fileprivate extension RustCallStatus { + init() { + self.init( + code: CALL_SUCCESS, + errorBuf: RustBuffer.init( + capacity: 0, + len: 0, + data: nil + ) + ) + } +} + +private func rustCall<T>(_ callback: (UnsafeMutablePointer<RustCallStatus>) -> T) throws -> T { + try makeRustCall(callback, errorHandler: { + $0.deallocate() + return UniffiInternalError.unexpectedRustCallError + }) +} + +private func rustCallWithError<T, F: FfiConverter> + (_ errorFfiConverter: F.Type, _ callback: (UnsafeMutablePointer<RustCallStatus>) -> T) throws -> T + where F.SwiftType: Error, F.FfiType == RustBuffer + { + try makeRustCall(callback, errorHandler: { return try errorFfiConverter.lift($0) }) +} + +private func makeRustCall<T>(_ callback: (UnsafeMutablePointer<RustCallStatus>) -> T, errorHandler: (RustBuffer) throws -> Error) throws -> T { + var callStatus = RustCallStatus.init() + let returnedVal = callback(&callStatus) + switch callStatus.code { + case CALL_SUCCESS: + return returnedVal + + case CALL_ERROR: + throw try errorHandler(callStatus.errorBuf) + + case CALL_PANIC: + // When the rust code sees a panic, it tries to construct a RustBuffer + // with the message. But if that code panics, then it just sends back + // an empty buffer. + if callStatus.errorBuf.len > 0 { + throw UniffiInternalError.rustPanic(try {{ Type::String.borrow()|lift_fn }}(callStatus.errorBuf)) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.rustPanic("Rust panic") + } + + default: + throw UniffiInternalError.unexpectedRustCallStatusCode + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int16Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int16Helper.swift new file mode 100644 index 0000000000..ac57fc5e58 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int16Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterInt16: FfiConverterPrimitive { + typealias FfiType = Int16 + typealias SwiftType = Int16 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Int16 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Int16, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int32Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int32Helper.swift new file mode 100644 index 0000000000..0ccfc13e4e --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int32Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterInt32: FfiConverterPrimitive { + typealias FfiType = Int32 + typealias SwiftType = Int32 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Int32 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Int32, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int64Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int64Helper.swift new file mode 100644 index 0000000000..d7d4082933 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int64Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterInt64: FfiConverterPrimitive { + typealias FfiType = Int64 + typealias SwiftType = Int64 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Int64 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Int64, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int8Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int8Helper.swift new file mode 100644 index 0000000000..f2387e4340 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Int8Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterInt8: FfiConverterPrimitive { + typealias FfiType = Int8 + typealias SwiftType = Int8 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Int8 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Int8, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/MapTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/MapTemplate.swift new file mode 100644 index 0000000000..05713aca26 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/MapTemplate.swift @@ -0,0 +1,22 @@ +fileprivate struct {{ ffi_converter_name }}: FfiConverterRustBuffer { + public static func write(_ value: {{ type_name }}, into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for (key, value) in value { + {{ key_type|write_fn }}(key, into: &buf) + {{ value_type|write_fn }}(value, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { + let len: Int32 = try readInt(&buf) + var dict = {{ type_name }}() + dict.reserveCapacity(Int(len)) + for _ in 0..<len { + let key = try {{ key_type|read_fn }}(from: &buf) + let value = try {{ value_type|read_fn }}(from: &buf) + dict[key] = value + } + return dict + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ModuleMapTemplate.modulemap b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ModuleMapTemplate.modulemap new file mode 100644 index 0000000000..f5f73ceb1b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ModuleMapTemplate.modulemap @@ -0,0 +1,6 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! +module {{ config.ffi_module_name() }} { + header "{{ config.header_filename() }}" + export * +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift new file mode 100644 index 0000000000..ab0b22b1d8 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift @@ -0,0 +1,88 @@ +{%- let obj = ci.get_object_definition(name).unwrap() %} +public protocol {{ obj.name() }}Protocol { + {% for meth in obj.methods() -%} + func {{ meth.name()|fn_name }}({% call swift::arg_list_protocol(meth) %}) {% call swift::throws(meth) -%} + {%- match meth.return_type() -%} + {%- when Some with (return_type) %} -> {{ return_type|type_name -}} + {%- else -%} + {%- endmatch %} + {% endfor %} +} + +public class {{ type_name }}: {{ obj.name() }}Protocol { + fileprivate let pointer: UnsafeMutableRawPointer + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + {%- match obj.primary_constructor() %} + {%- when Some with (cons) %} + public convenience init({% call swift::arg_list_decl(cons) -%}) {% call swift::throws(cons) %} { + self.init(unsafeFromRawPointer: {% call swift::to_ffi_call(cons) %}) + } + {%- when None %} + {%- endmatch %} + + deinit { + try! rustCall { {{ obj.ffi_object_free().name() }}(pointer, $0) } + } + + {% for cons in obj.alternate_constructors() %} + public static func {{ cons.name()|fn_name }}({% call swift::arg_list_decl(cons) %}) {% call swift::throws(cons) %} -> {{ type_name }} { + return {{ type_name }}(unsafeFromRawPointer: {% call swift::to_ffi_call(cons) %}) + } + {% endfor %} + + {# // TODO: Maybe merge the two templates (i.e the one with a return type and the one without) #} + {% for meth in obj.methods() -%} + {%- match meth.return_type() -%} + + {%- when Some with (return_type) -%} + public func {{ meth.name()|fn_name }}({% call swift::arg_list_decl(meth) %}) {% call swift::throws(meth) %} -> {{ return_type|type_name }} { + return {% call swift::try(meth) %} {{ return_type|lift_fn }}( + {% call swift::to_ffi_call_with_prefix("self.pointer", meth) %} + ) + } + + {%- when None -%} + public func {{ meth.name()|fn_name }}({% call swift::arg_list_decl(meth) %}) {% call swift::throws(meth) %} { + {% call swift::to_ffi_call_with_prefix("self.pointer", meth) %} + } + {%- endmatch %} + {% endfor %} +} + + +public struct {{ ffi_converter_name }}: FfiConverter { + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = {{ type_name }} + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: {{ type_name }}, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> {{ type_name }} { + return {{ type_name}}(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: {{ type_name }}) -> UnsafeMutableRawPointer { + return value.pointer + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/OptionalTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/OptionalTemplate.swift new file mode 100644 index 0000000000..1dac65be63 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/OptionalTemplate.swift @@ -0,0 +1,20 @@ +fileprivate struct {{ ffi_converter_name }}: FfiConverterRustBuffer { + typealias SwiftType = {{ type_name }} + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + {{ inner_type|write_fn }}(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try {{ inner_type|read_fn }}(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/RecordTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/RecordTemplate.swift new file mode 100644 index 0000000000..7f2de44052 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/RecordTemplate.swift @@ -0,0 +1,62 @@ +{%- let rec = ci.get_record_definition(name).unwrap() %} +public struct {{ type_name }} { + {%- for field in rec.fields() %} + public var {{ field.name()|var_name }}: {{ field|type_name }} + {%- endfor %} + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init({% call swift::field_list_decl(rec) %}) { + {%- for field in rec.fields() %} + self.{{ field.name()|var_name }} = {{ field.name()|var_name }} + {%- endfor %} + } +} + +{% if !contains_object_references %} +extension {{ type_name }}: Equatable, Hashable { + public static func ==(lhs: {{ type_name }}, rhs: {{ type_name }}) -> Bool { + {%- for field in rec.fields() %} + if lhs.{{ field.name()|var_name }} != rhs.{{ field.name()|var_name }} { + return false + } + {%- endfor %} + return true + } + + public func hash(into hasher: inout Hasher) { + {%- for field in rec.fields() %} + hasher.combine({{ field.name()|var_name }}) + {%- endfor %} + } +} +{% endif %} + +public struct {{ ffi_converter_name }}: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { + return try {{ type_name }}( + {%- for field in rec.fields() %} + {{ field.name()|var_name }}: {{ field|read_fn }}(from: &buf) + {%- if !loop.last %}, {% endif %} + {%- endfor %} + ) + } + + public static func write(_ value: {{ type_name }}, into buf: inout [UInt8]) { + {%- for field in rec.fields() %} + {{ field|write_fn }}(value.{{ field.name()|var_name }}, into: &buf) + {%- endfor %} + } +} + +{# +We always write these public functions just in case the struct is used as +an external type by another crate. +#} +public func {{ ffi_converter_name }}_lift(_ buf: RustBuffer) throws -> {{ type_name }} { + return try {{ ffi_converter_name }}.lift(buf) +} + +public func {{ ffi_converter_name }}_lower(_ value: {{ type_name }}) -> RustBuffer { + return {{ ffi_converter_name }}.lower(value) +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift new file mode 100644 index 0000000000..2f737b6635 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift @@ -0,0 +1,183 @@ +fileprivate extension RustBuffer { + // Allocate a new buffer, copying the contents of a `UInt8` array. + init(bytes: [UInt8]) { + let rbuf = bytes.withUnsafeBufferPointer { ptr in + RustBuffer.from(ptr) + } + self.init(capacity: rbuf.capacity, len: rbuf.len, data: rbuf.data) + } + + static func from(_ ptr: UnsafeBufferPointer<UInt8>) -> RustBuffer { + try! rustCall { {{ ci.ffi_rustbuffer_from_bytes().name() }}(ForeignBytes(bufferPointer: ptr), $0) } + } + + // Frees the buffer in place. + // The buffer must not be used after this is called. + func deallocate() { + try! rustCall { {{ ci.ffi_rustbuffer_free().name() }}(self, $0) } + } +} + +fileprivate extension ForeignBytes { + init(bufferPointer: UnsafeBufferPointer<UInt8>) { + self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress) + } +} + +// For every type used in the interface, we provide helper methods for conveniently +// lifting and lowering that type from C-compatible data, and for reading and writing +// values of that type in a buffer. + +// Helper classes/extensions that don't change. +// Someday, this will be in a library of its own. + +fileprivate extension Data { + init(rustBuffer: RustBuffer) { + // TODO: This copies the buffer. Can we read directly from a + // Rust buffer? + self.init(bytes: rustBuffer.data!, count: Int(rustBuffer.len)) + } +} + +// Define reader functionality. Normally this would be defined in a class or +// struct, but we use standalone functions instead in order to make external +// types work. +// +// With external types, one swift source file needs to be able to call the read +// method on another source file's FfiConverter, but then what visibility +// should Reader have? +// - If Reader is fileprivate, then this means the read() must also +// be fileprivate, which doesn't work with external types. +// - If Reader is internal/public, we'll get compile errors since both source +// files will try define the same type. +// +// Instead, the read() method and these helper functions input a tuple of data + +fileprivate func createReader(data: Data) -> (data: Data, offset: Data.Index) { + (data: data, offset: 0) +} + +// Reads an integer at the current offset, in big-endian order, and advances +// the offset on success. Throws if reading the integer would move the +// offset past the end of the buffer. +fileprivate func readInt<T: FixedWidthInteger>(_ reader: inout (data: Data, offset: Data.Index)) throws -> T { + let range = reader.offset..<reader.offset + MemoryLayout<T>.size + guard reader.data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + if T.self == UInt8.self { + let value = reader.data[reader.offset] + reader.offset += 1 + return value as! T + } + var value: T = 0 + let _ = withUnsafeMutableBytes(of: &value, { reader.data.copyBytes(to: $0, from: range)}) + reader.offset = range.upperBound + return value.bigEndian +} + +// Reads an arbitrary number of bytes, to be used to read +// raw bytes, this is useful when lifting strings +fileprivate func readBytes(_ reader: inout (data: Data, offset: Data.Index), count: Int) throws -> Array<UInt8> { + let range = reader.offset..<(reader.offset+count) + guard reader.data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + var value = [UInt8](repeating: 0, count: count) + value.withUnsafeMutableBufferPointer({ buffer in + reader.data.copyBytes(to: buffer, from: range) + }) + reader.offset = range.upperBound + return value +} + +// Reads a float at the current offset. +fileprivate func readFloat(_ reader: inout (data: Data, offset: Data.Index)) throws -> Float { + return Float(bitPattern: try readInt(&reader)) +} + +// Reads a float at the current offset. +fileprivate func readDouble(_ reader: inout (data: Data, offset: Data.Index)) throws -> Double { + return Double(bitPattern: try readInt(&reader)) +} + +// Indicates if the offset has reached the end of the buffer. +fileprivate func hasRemaining(_ reader: (data: Data, offset: Data.Index)) -> Bool { + return reader.offset < reader.data.count +} + +// Define writer functionality. Normally this would be defined in a class or +// struct, but we use standalone functions instead in order to make external +// types work. See the above discussion on Readers for details. + +fileprivate func createWriter() -> [UInt8] { + return [] +} + +fileprivate func writeBytes<S>(_ writer: inout [UInt8], _ byteArr: S) where S: Sequence, S.Element == UInt8 { + writer.append(contentsOf: byteArr) +} + +// Writes an integer in big-endian order. +// +// Warning: make sure what you are trying to write +// is in the correct type! +fileprivate func writeInt<T: FixedWidthInteger>(_ writer: inout [UInt8], _ value: T) { + var value = value.bigEndian + withUnsafeBytes(of: &value) { writer.append(contentsOf: $0) } +} + +fileprivate func writeFloat(_ writer: inout [UInt8], _ value: Float) { + writeInt(&writer, value.bitPattern) +} + +fileprivate func writeDouble(_ writer: inout [UInt8], _ value: Double) { + writeInt(&writer, value.bitPattern) +} + +// Protocol for types that transfer other types across the FFI. This is +// analogous go the Rust trait of the same name. +fileprivate protocol FfiConverter { + associatedtype FfiType + associatedtype SwiftType + + static func lift(_ value: FfiType) throws -> SwiftType + static func lower(_ value: SwiftType) -> FfiType + static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType + static func write(_ value: SwiftType, into buf: inout [UInt8]) +} + +// Types conforming to `Primitive` pass themselves directly over the FFI. +fileprivate protocol FfiConverterPrimitive: FfiConverter where FfiType == SwiftType { } + +extension FfiConverterPrimitive { + public static func lift(_ value: FfiType) throws -> SwiftType { + return value + } + + public static func lower(_ value: SwiftType) -> FfiType { + return value + } +} + +// Types conforming to `FfiConverterRustBuffer` lift and lower into a `RustBuffer`. +// Used for complex types where it's hard to write a custom lift/lower. +fileprivate protocol FfiConverterRustBuffer: FfiConverter where FfiType == RustBuffer {} + +extension FfiConverterRustBuffer { + public static func lift(_ buf: RustBuffer) throws -> SwiftType { + var reader = createReader(data: Data(rustBuffer: buf)) + let value = try read(from: &reader) + if hasRemaining(reader) { + throw UniffiInternalError.incompleteData + } + buf.deallocate() + return value + } + + public static func lower(_ value: SwiftType) -> RustBuffer { + var writer = createWriter() + write(value, into: &writer) + return RustBuffer(bytes: writer) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/SequenceTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/SequenceTemplate.swift new file mode 100644 index 0000000000..bf664f6411 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/SequenceTemplate.swift @@ -0,0 +1,21 @@ +fileprivate struct {{ ffi_converter_name }}: FfiConverterRustBuffer { + typealias SwiftType = {{ type_name }} + + public static func write(_ value: {{ type_name }}, into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + {{ inner_type|write_fn }}(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { + let len: Int32 = try readInt(&buf) + var seq = {{ type_name }}() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try {{ inner_type|read_fn }}(from: &buf)) + } + return seq + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/StringHelper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/StringHelper.swift new file mode 100644 index 0000000000..b7d3466bdd --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/StringHelper.swift @@ -0,0 +1,37 @@ +fileprivate struct FfiConverterString: FfiConverter { + typealias SwiftType = String + typealias FfiType = RustBuffer + + public static func lift(_ value: RustBuffer) throws -> String { + defer { + value.deallocate() + } + if value.data == nil { + return String() + } + let bytes = UnsafeBufferPointer<UInt8>(start: value.data!, count: Int(value.len)) + return String(bytes: bytes, encoding: String.Encoding.utf8)! + } + + public static func lower(_ value: String) -> RustBuffer { + return value.utf8CString.withUnsafeBufferPointer { ptr in + // The swift string gives us int8_t, we want uint8_t. + ptr.withMemoryRebound(to: UInt8.self) { ptr in + // The swift string gives us a trailing null byte, we don't want it. + let buf = UnsafeBufferPointer(rebasing: ptr.prefix(upTo: ptr.count - 1)) + return RustBuffer.from(buf) + } + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> String { + let len: Int32 = try readInt(&buf) + return String(bytes: try readBytes(&buf, count: Int(len)), encoding: String.Encoding.utf8)! + } + + public static func write(_ value: String, into buf: inout [UInt8]) { + let len = Int32(value.utf8.count) + writeInt(&buf, len) + writeBytes(&buf, value.utf8) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/TimestampHelper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/TimestampHelper.swift new file mode 100644 index 0000000000..3cd472fa0e --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/TimestampHelper.swift @@ -0,0 +1,34 @@ +fileprivate struct FfiConverterTimestamp: FfiConverterRustBuffer { + typealias SwiftType = Date + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Date { + let seconds: Int64 = try readInt(&buf) + let nanoseconds: UInt32 = try readInt(&buf) + if seconds >= 0 { + let delta = Double(seconds) + (Double(nanoseconds) / 1.0e9) + return Date.init(timeIntervalSince1970: delta) + } else { + let delta = Double(seconds) - (Double(nanoseconds) / 1.0e9) + return Date.init(timeIntervalSince1970: delta) + } + } + + public static func write(_ value: Date, into buf: inout [UInt8]) { + var delta = value.timeIntervalSince1970 + var sign: Int64 = 1 + if delta < 0 { + // The nanoseconds portion of the epoch offset must always be + // positive, to simplify the calculation we will use the absolute + // value of the offset. + sign = -1 + delta = -delta + } + if delta.rounded(.down) > Double(Int64.max) { + fatalError("Timestamp overflow, exceeds max bounds supported by Uniffi") + } + let seconds = Int64(delta) + let nanoseconds = UInt32((delta - Double(seconds)) * 1.0e9) + writeInt(&buf, sign * seconds) + writeInt(&buf, nanoseconds) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift new file mode 100644 index 0000000000..5ce762d2a0 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/TopLevelFunctionTemplate.swift @@ -0,0 +1,15 @@ +{%- match func.return_type() -%} +{%- when Some with (return_type) %} + +public func {{ func.name()|fn_name }}({%- call swift::arg_list_decl(func) -%}) {% call swift::throws(func) %} -> {{ return_type|type_name }} { + return {% call swift::try(func) %} {{ return_type|lift_fn }}( + {% call swift::to_ffi_call(func) %} + ) +} + +{% when None %} + +public func {{ func.name()|fn_name }}({% call swift::arg_list_decl(func) %}) {% call swift::throws(func) %} { + {% call swift::to_ffi_call(func) %} +} +{% endmatch %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Types.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Types.swift new file mode 100644 index 0000000000..6e37e1b7f3 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/Types.swift @@ -0,0 +1,90 @@ +{%- import "macros.swift" as swift %} +{%- for type_ in ci.iter_types() %} +{%- let type_name = type_|type_name %} +{%- let ffi_converter_name = type_|ffi_converter_name %} +{%- let canonical_type_name = type_|canonical_name %} +{%- let contains_object_references = ci.item_contains_object_references(type_) %} + +{# + # Map `Type` instances to an include statement for that type. + # + # There is a companion match in `KotlinCodeOracle::create_code_type()` which performs a similar function for the + # Rust code. + # + # - When adding additional types here, make sure to also add a match arm to that function. + # - To keep things manageable, let's try to limit ourselves to these 2 mega-matches + #} +{%- match type_ %} + +{%- when Type::Boolean %} +{%- include "BooleanHelper.swift" %} + +{%- when Type::String %} +{%- include "StringHelper.swift" %} + +{%- when Type::Int8 %} +{%- include "Int8Helper.swift" %} + +{%- when Type::Int16 %} +{%- include "Int16Helper.swift" %} + +{%- when Type::Int32 %} +{%- include "Int32Helper.swift" %} + +{%- when Type::Int64 %} +{%- include "Int64Helper.swift" %} + +{%- when Type::UInt8 %} +{%- include "UInt8Helper.swift" %} + +{%- when Type::UInt16 %} +{%- include "UInt16Helper.swift" %} + +{%- when Type::UInt32 %} +{%- include "UInt32Helper.swift" %} + +{%- when Type::UInt64 %} +{%- include "UInt64Helper.swift" %} + +{%- when Type::Float32 %} +{%- include "Float32Helper.swift" %} + +{%- when Type::Float64 %} +{%- include "Float64Helper.swift" %} + +{%- when Type::Timestamp %} +{%- include "TimestampHelper.swift" %} + +{%- when Type::Duration %} +{%- include "DurationHelper.swift" %} + +{%- when Type::CallbackInterface(name) %} +{%- include "CallbackInterfaceTemplate.swift" %} + +{%- when Type::Custom { name, builtin } %} +{%- include "CustomType.swift" %} + +{%- when Type::Enum(name) %} +{%- include "EnumTemplate.swift" %} + +{%- when Type::Error(name) %} +{%- include "ErrorTemplate.swift" %} + +{%- when Type::Object(name) %} +{%- include "ObjectTemplate.swift" %} + +{%- when Type::Record(name) %} +{%- include "RecordTemplate.swift" %} + +{%- when Type::Optional(inner_type) %} +{%- include "OptionalTemplate.swift" %} + +{%- when Type::Sequence(inner_type) %} +{%- include "SequenceTemplate.swift" %} + +{%- when Type::Map(key_type, value_type) %} +{%- include "MapTemplate.swift" %} + +{%- else %} +{%- endmatch %} +{%- endfor %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt16Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt16Helper.swift new file mode 100644 index 0000000000..b7fc0942a5 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt16Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterUInt16: FfiConverterPrimitive { + typealias FfiType = UInt16 + typealias SwiftType = UInt16 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt16 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt32Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt32Helper.swift new file mode 100644 index 0000000000..e7a64aab93 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt32Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterUInt32: FfiConverterPrimitive { + typealias FfiType = UInt32 + typealias SwiftType = UInt32 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt32 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt64Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt64Helper.swift new file mode 100644 index 0000000000..eb674a2c53 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt64Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterUInt64: FfiConverterPrimitive { + typealias FfiType = UInt64 + typealias SwiftType = UInt64 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt64 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt8Helper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt8Helper.swift new file mode 100644 index 0000000000..4baf613494 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/UInt8Helper.swift @@ -0,0 +1,12 @@ +fileprivate struct FfiConverterUInt8: FfiConverterPrimitive { + typealias FfiType = UInt8 + typealias SwiftType = UInt8 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt8 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: UInt8, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/macros.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/macros.swift new file mode 100644 index 0000000000..dcc0a2df74 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/macros.swift @@ -0,0 +1,97 @@ +{# +// Template to call into rust. Used in several places. +// Variable names in `arg_list_decl` should match up with arg lists +// passed to rust via `_arg_list_ffi_call` +#} + +{%- macro to_ffi_call(func) -%} +{% call try(func) %} + {% match func.throws_type() %} + {% when Some with (e) %} + rustCallWithError({{ e|ffi_converter_name }}.self) { + {% else %} + rustCall() { + {% endmatch %} + {{ func.ffi_func().name() }}({% call _arg_list_ffi_call(func) -%}{% if func.arguments().len() > 0 %}, {% endif %}$0) +} +{%- endmacro -%} + +{%- macro to_ffi_call_with_prefix(prefix, func) -%} +{% call try(func) %} + {%- match func.throws_type() %} + {%- when Some with (e) %} + rustCallWithError({{ e|ffi_converter_name }}.self) { + {%- else %} + rustCall() { + {% endmatch %} + {{ func.ffi_func().name() }}( + {{- prefix }}, {% call _arg_list_ffi_call(func) -%}{% if func.arguments().len() > 0 %}, {% endif %}$0 + ) +} +{%- endmacro %} + +{%- macro _arg_list_ffi_call(func) %} + {%- for arg in func.arguments() %} + {{ arg|lower_fn }}({{ arg.name()|var_name }}) + {%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro -%} + +{#- +// Arglist as used in Swift declarations of methods, functions and constructors. +// Note the var_name and type_name filters. +-#} + +{% macro arg_list_decl(func) %} + {%- for arg in func.arguments() -%} + {% if config.omit_argument_labels() %}_ {% endif %}{{ arg.name()|var_name }}: {{ arg|type_name -}} + {%- match arg.default_value() %} + {%- when Some with(literal) %} = {{ literal|literal_swift(arg) }} + {%- else %} + {%- endmatch %} + {%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro %} + +{#- +// Field lists as used in Swift declarations of Records and Enums. +// Note the var_name and type_name filters. +-#} +{% macro field_list_decl(item) %} + {%- for field in item.fields() -%} + {{ field.name()|var_name }}: {{ field|type_name -}} + {%- match field.default_value() %} + {%- when Some with(literal) %} = {{ literal|literal_swift(field) }} + {%- else %} + {%- endmatch -%} + {% if !loop.last %}, {% endif %} + {%- endfor %} +{%- endmacro %} + + +{% macro arg_list_protocol(func) %} + {%- for arg in func.arguments() -%} + {% if config.omit_argument_labels() %}_ {% endif %}{{ arg.name()|var_name }}: {{ arg|type_name -}} + {%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro %} + + +{#- +// Arglist as used in the _UniFFILib function declarations. +// Note unfiltered name but ffi_type_name filters. +-#} +{%- macro arg_list_ffi_decl(func) %} + {%- for arg in func.arguments() %} + {{- arg.type_().borrow()|ffi_type_name }} {{ arg.name() -}}, + {%- endfor %} + RustCallStatus *_Nonnull out_status +{%- endmacro -%} + +{%- macro throws(func) %} +{%- if func.throws() %}throws{% endif %} +{%- endmacro -%} + +{%- macro try(func) %} +{%- if func.throws() %}try{% else %}try!{% endif %} +{%- endmacro -%} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift new file mode 100644 index 0000000000..ac0557912e --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift @@ -0,0 +1,40 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! +{%- import "macros.swift" as swift %} +import Foundation +{%- for imported_class in self.imports() %} +import {{ imported_class }} +{%- endfor %} + +// Depending on the consumer's build setup, the low-level FFI code +// might be in a separate module, or it might be compiled inline into +// this module. This is a bit of light hackery to work with both. +#if canImport({{ config.ffi_module_name() }}) +import {{ config.ffi_module_name() }} +#endif + +{% include "RustBufferTemplate.swift" %} +{% include "Helpers.swift" %} + +// Public interface members begin here. +{{ type_helper_code }} + +{%- for func in ci.function_definitions() %} +{%- include "TopLevelFunctionTemplate.swift" %} +{%- endfor %} + +/** + * Top level initializers and tear down methods. + * + * This is generated by uniffi. + */ +public enum {{ config.module_name().borrow()|class_name }}Lifecycle { + /** + * Initialize the FFI and Rust library. This should be only called once per application. + */ + func initialize() { + {%- for initialization_fn in self.initialization_fns() %} + {{ initialization_fn }}() + {%- endfor %} + } +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/swift/test.rs b/third_party/rust/uniffi_bindgen/src/bindings/swift/test.rs new file mode 100644 index 0000000000..e51c4a9c05 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/swift/test.rs @@ -0,0 +1,225 @@ +/* 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 anyhow::{bail, Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use heck::ToSnakeCase; +use std::env::consts::{DLL_PREFIX, DLL_SUFFIX}; +use std::ffi::OsStr; +use std::fs::{read_to_string, File}; +use std::io::Write; +use std::process::Command; +use uniffi_testing::{CompileSource, UniFFITestHelper}; + +/// Run Swift tests for a UniFFI test fixture +pub fn run_test(tmp_dir: &str, fixture_name: &str, script_file: &str) -> Result<()> { + let script_path = Utf8Path::new(".").join(script_file).canonicalize_utf8()?; + let test_helper = UniFFITestHelper::new(fixture_name).context("UniFFITestHelper::new")?; + let out_dir = test_helper + .create_out_dir(tmp_dir, &script_path) + .context("create_out_dir()")?; + test_helper + .copy_cdylibs_to_out_dir(&out_dir) + .context("copy_fixture_library_to_out_dir()")?; + let generated_sources = + GeneratedSources::new(&test_helper.cdylib_path()?, &out_dir, &test_helper) + .context("generate_sources()")?; + + // Compile the generated sources together to create a single swift module + compile_swift_module( + &out_dir, + &calc_module_name(&generated_sources.main_source_filename), + &generated_sources.generated_swift_files, + &generated_sources.module_map, + )?; + + // Run the test script against compiled bindings + + let mut command = Command::new("swift"); + command + .current_dir(&out_dir) + .arg("-I") + .arg(&out_dir) + .arg("-L") + .arg(&out_dir) + .args(calc_library_args(&out_dir)?) + .arg("-Xcc") + .arg(format!( + "-fmodule-map-file={}", + generated_sources.module_map + )) + .arg(&script_path); + let status = command + .spawn() + .context("Failed to spawn `swiftc` when running test script")? + .wait() + .context("Failed to wait for `swiftc` when running test script")?; + if !status.success() { + bail!("running `swift` to run test script failed ({:?})", command) + } + Ok(()) +} + +fn calc_module_name(filename: &str) -> String { + filename.strip_suffix(".swift").unwrap().to_snake_case() +} + +fn compile_swift_module<T: AsRef<OsStr>>( + out_dir: &Utf8Path, + module_name: &str, + sources: impl IntoIterator<Item = T>, + module_map: &Utf8Path, +) -> Result<()> { + let output_filename = format!("{DLL_PREFIX}testmod_{module_name}{DLL_SUFFIX}"); + let mut command = Command::new("swiftc"); + command + .current_dir(out_dir) + .arg("-emit-module") + .arg("-module-name") + .arg(module_name) + .arg("-o") + .arg(output_filename) + .arg("-emit-library") + .arg("-Xcc") + .arg(format!("-fmodule-map-file={}", module_map)) + .arg("-I") + .arg(out_dir) + .arg("-L") + .arg(out_dir) + .args(calc_library_args(out_dir)?) + .args(sources); + let status = command + .spawn() + .context("Failed to spawn `swiftc` when compiling bindings")? + .wait() + .context("Failed to wait for `swiftc` when compiling bindings")?; + if !status.success() { + bail!( + "running `swiftc` to compile bindings failed ({:?})", + command + ) + }; + Ok(()) +} + +// Stores sources generated by `uniffi-bindgen-swift` +struct GeneratedSources { + generated_swift_files: Vec<Utf8PathBuf>, + module_map: Utf8PathBuf, + main_source_filename: String, +} + +impl GeneratedSources { + fn new( + library_path: &Utf8Path, + out_dir: &Utf8Path, + test_helper: &UniFFITestHelper, + ) -> Result<Self> { + // Generate the bindings for the main compile source, and use that for the swift module name + let main_compile_source = test_helper.get_main_compile_source()?; + Self::run_generate_bindings(&main_compile_source, library_path, out_dir)?; + let generated_files = glob(&out_dir.join("*.swift"))?; + let main_source_filename = match generated_files.len() { + 0 => bail!( + "No .swift file generated for {}", + main_compile_source.udl_path + ), + 1 => generated_files + .into_iter() + .next() + .unwrap() + .file_name() + .unwrap() + .to_string(), + n => bail!( + "{n} .swift files generated for {}", + main_compile_source.udl_path + ), + }; + + // Generate the bindings for other compile sources (crates used by external types) + for source in test_helper.get_external_compile_sources()? { + crate::generate_bindings( + &source.udl_path, + source.config_path.as_deref(), + vec!["swift"], + Some(out_dir), + Some(library_path), + false, + )?; + } + + let generated_module_maps = glob(&out_dir.join("*.modulemap"))?; + + Ok(GeneratedSources { + main_source_filename, + generated_swift_files: glob(&out_dir.join("*.swift"))?, + module_map: match generated_module_maps.len() { + 0 => bail!("No modulemap files found in {out_dir}"), + // Normally we only generate 1 module map and can return it directly + 1 => generated_module_maps.into_iter().next().unwrap(), + // When we use multiple UDL files in a test, for example the ext-types fixture, + // then we get multiple module maps and need to combine them + _ => { + let path = out_dir.join("combined.modulemap"); + let mut f = File::create(&path)?; + write!( + f, + "{}", + generated_module_maps + .into_iter() + .map(|path| Ok(read_to_string(path)?)) + .collect::<Result<Vec<String>>>()? + .join("\n") + )?; + path + } + }, + }) + } + + fn run_generate_bindings( + source: &CompileSource, + library_path: &Utf8Path, + out_dir: &Utf8Path, + ) -> Result<()> { + crate::generate_bindings( + &source.udl_path, + source.config_path.as_deref(), + vec!["swift"], + Some(out_dir), + Some(library_path), + false, + ) + } +} + +// Wraps glob to use Utf8Paths and flattens errors +fn glob(globspec: &Utf8Path) -> Result<Vec<Utf8PathBuf>> { + glob::glob(globspec.as_str())? + .map(|globresult| Ok(Utf8PathBuf::try_from(globresult?)?)) + .collect() +} + +fn calc_library_args(out_dir: &Utf8Path) -> Result<Vec<String>> { + let results = glob::glob( + out_dir + .join(format!("{}*{}", DLL_PREFIX, DLL_SUFFIX)) + .as_str(), + )?; + results + .map(|globresult| { + let path = Utf8PathBuf::try_from(globresult.unwrap())?; + Ok(format!( + "-l{}", + path.file_name() + .unwrap() + .strip_prefix(DLL_PREFIX) + .unwrap() + .strip_suffix(DLL_SUFFIX) + .unwrap() + )) + }) + .collect() +} diff --git a/third_party/rust/uniffi_bindgen/src/interface/attributes.rs b/third_party/rust/uniffi_bindgen/src/interface/attributes.rs new file mode 100644 index 0000000000..4df6042564 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/interface/attributes.rs @@ -0,0 +1,759 @@ +/* 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/. */ + +//! # Attribute definitions for a `ComponentInterface`. +//! +//! This module provides some conveniences for working with attribute definitions +//! from WebIDL. When encountering a weedle `ExtendedAttribute` node, use `TryFrom` +//! to convert it into an [`Attribute`] representing one of the attributes that we +//! support. You can also use the [`parse_attributes`] function to parse an +//! `ExtendedAttributeList` into a vec of same. +//! +//! We only support a small number of attributes, so it's manageable to have them +//! all handled by a single abstraction. This might need to be refactored in future +//! if we grow significantly more complicated attribute handling. + +use anyhow::{bail, Result}; +use uniffi_meta::Checksum; + +/// Represents an attribute parsed from UDL, like `[ByRef]` or `[Throws]`. +/// +/// This is a convenience enum for parsing UDL attributes and erroring out if we encounter +/// any unsupported ones. These don't convert directly into parts of a `ComponentInterface`, but +/// may influence the properties of things like functions and arguments. +#[derive(Debug, Clone, Checksum)] +pub(super) enum Attribute { + ByRef, + Enum, + Error, + Name(String), + SelfType(SelfType), + Threadsafe, // N.B. the `[Threadsafe]` attribute is deprecated and will be removed + Throws(String), + // `[External="crate_name"]` - We can `use crate_name::...` for the type. + External(String), + // Custom type on the scaffolding side + Custom, +} + +impl Attribute { + pub fn is_error(&self) -> bool { + matches!(self, Attribute::Error) + } + pub fn is_enum(&self) -> bool { + matches!(self, Attribute::Enum) + } +} + +/// Convert a weedle `ExtendedAttribute` into an `Attribute` for a `ComponentInterface` member, +/// or error out if the attribute is not supported. +impl TryFrom<&weedle::attribute::ExtendedAttribute<'_>> for Attribute { + type Error = anyhow::Error; + fn try_from( + weedle_attribute: &weedle::attribute::ExtendedAttribute<'_>, + ) -> Result<Self, anyhow::Error> { + match weedle_attribute { + // Matches plain named attributes like "[ByRef"]. + weedle::attribute::ExtendedAttribute::NoArgs(attr) => match (attr.0).0 { + "ByRef" => Ok(Attribute::ByRef), + "Enum" => Ok(Attribute::Enum), + "Error" => Ok(Attribute::Error), + "Threadsafe" => Ok(Attribute::Threadsafe), + "Custom" => Ok(Attribute::Custom), + _ => anyhow::bail!("ExtendedAttributeNoArgs not supported: {:?}", (attr.0).0), + }, + // Matches assignment-style attributes like ["Throws=Error"] + weedle::attribute::ExtendedAttribute::Ident(identity) => { + match identity.lhs_identifier.0 { + "Name" => Ok(Attribute::Name(name_from_id_or_string(&identity.rhs))), + "Throws" => Ok(Attribute::Throws(name_from_id_or_string(&identity.rhs))), + "Self" => Ok(Attribute::SelfType(SelfType::try_from(&identity.rhs)?)), + "External" => Ok(Attribute::External(name_from_id_or_string(&identity.rhs))), + _ => anyhow::bail!( + "Attribute identity Identifier not supported: {:?}", + identity.lhs_identifier.0 + ), + } + } + _ => anyhow::bail!("Attribute not supported: {:?}", weedle_attribute), + } + } +} + +fn name_from_id_or_string(nm: &weedle::attribute::IdentifierOrString<'_>) -> String { + match nm { + weedle::attribute::IdentifierOrString::Identifier(identifier) => identifier.0.to_string(), + weedle::attribute::IdentifierOrString::String(str_lit) => str_lit.0.to_string(), + } +} + +/// Parse a weedle `ExtendedAttributeList` into a list of `Attribute`s, +/// erroring out on duplicates. +fn parse_attributes<F>( + weedle_attributes: &weedle::attribute::ExtendedAttributeList<'_>, + validator: F, +) -> Result<Vec<Attribute>> +where + F: Fn(&Attribute) -> Result<()>, +{ + let attrs = &weedle_attributes.body.list; + + let mut hash_set = std::collections::HashSet::new(); + for attr in attrs { + if !hash_set.insert(attr) { + anyhow::bail!("Duplicated ExtendedAttribute: {:?}", attr); + } + } + + let attrs = attrs + .iter() + .map(Attribute::try_from) + .collect::<Result<Vec<_>, _>>()?; + + for attr in &attrs { + validator(attr)?; + } + + Ok(attrs) +} + +/// Attributes that can be attached to an `enum` definition in the UDL. +/// There's only one case here: using `[Error]` to mark an enum as an error class. +#[derive(Debug, Clone, Checksum, Default)] +pub(super) struct EnumAttributes(Vec<Attribute>); + +impl EnumAttributes { + pub fn contains_error_attr(&self) -> bool { + self.0.iter().any(|attr| attr.is_error()) + } +} + +impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for EnumAttributes { + type Error = anyhow::Error; + fn try_from( + weedle_attributes: &weedle::attribute::ExtendedAttributeList<'_>, + ) -> Result<Self, Self::Error> { + let attrs = parse_attributes(weedle_attributes, |attr| match attr { + Attribute::Error => Ok(()), + _ => bail!(format!("{attr:?} not supported for enums")), + })?; + Ok(Self(attrs)) + } +} + +impl<T: TryInto<EnumAttributes, Error = anyhow::Error>> TryFrom<Option<T>> for EnumAttributes { + type Error = anyhow::Error; + fn try_from(value: Option<T>) -> Result<Self, Self::Error> { + match value { + None => Ok(Default::default()), + Some(v) => v.try_into(), + } + } +} + +/// Represents UDL attributes that might appear on a function. +/// +/// This supports the `[Throws=ErrorName]` attribute for functions that +/// can produce an error. +#[derive(Debug, Clone, Checksum, Default)] +pub(super) struct FunctionAttributes(Vec<Attribute>); + +impl FunctionAttributes { + pub(super) fn get_throws_err(&self) -> Option<&str> { + self.0.iter().find_map(|attr| match attr { + // This will hopefully return a helpful compilation error + // if the error is not defined. + Attribute::Throws(inner) => Some(inner.as_ref()), + _ => None, + }) + } +} + +impl FromIterator<Attribute> for FunctionAttributes { + fn from_iter<T: IntoIterator<Item = Attribute>>(iter: T) -> Self { + Self(Vec::from_iter(iter)) + } +} + +impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for FunctionAttributes { + type Error = anyhow::Error; + fn try_from( + weedle_attributes: &weedle::attribute::ExtendedAttributeList<'_>, + ) -> Result<Self, Self::Error> { + let attrs = parse_attributes(weedle_attributes, |attr| match attr { + Attribute::Throws(_) => Ok(()), + _ => bail!(format!("{attr:?} not supported for functions")), + })?; + Ok(Self(attrs)) + } +} + +impl<T: TryInto<FunctionAttributes, Error = anyhow::Error>> TryFrom<Option<T>> + for FunctionAttributes +{ + type Error = anyhow::Error; + fn try_from(value: Option<T>) -> Result<Self, Self::Error> { + match value { + None => Ok(Default::default()), + Some(v) => v.try_into(), + } + } +} + +/// Represents UDL attributes that might appear on a function argument. +/// +/// This supports the `[ByRef]` attribute for arguments that should be passed +/// by reference in the generated Rust scaffolding. +#[derive(Debug, Clone, Checksum, Default)] +pub(super) struct ArgumentAttributes(Vec<Attribute>); + +impl ArgumentAttributes { + pub fn by_ref(&self) -> bool { + self.0.iter().any(|attr| matches!(attr, Attribute::ByRef)) + } +} + +impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for ArgumentAttributes { + type Error = anyhow::Error; + fn try_from( + weedle_attributes: &weedle::attribute::ExtendedAttributeList<'_>, + ) -> Result<Self, Self::Error> { + let attrs = parse_attributes(weedle_attributes, |attr| match attr { + Attribute::ByRef => Ok(()), + _ => bail!(format!("{attr:?} not supported for arguments")), + })?; + Ok(Self(attrs)) + } +} + +impl<T: TryInto<ArgumentAttributes, Error = anyhow::Error>> TryFrom<Option<T>> + for ArgumentAttributes +{ + type Error = anyhow::Error; + fn try_from(value: Option<T>) -> Result<Self, Self::Error> { + match value { + None => Ok(Default::default()), + Some(v) => v.try_into(), + } + } +} + +/// Represents UDL attributes that might appear on an `interface` definition. +#[derive(Debug, Clone, Checksum, Default)] +pub(super) struct InterfaceAttributes(Vec<Attribute>); + +impl InterfaceAttributes { + pub fn contains_enum_attr(&self) -> bool { + self.0.iter().any(|attr| attr.is_enum()) + } + + pub fn contains_error_attr(&self) -> bool { + self.0.iter().any(|attr| attr.is_error()) + } + + pub fn threadsafe(&self) -> bool { + self.0 + .iter() + .any(|attr| matches!(attr, Attribute::Threadsafe)) + } +} + +impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for InterfaceAttributes { + type Error = anyhow::Error; + fn try_from( + weedle_attributes: &weedle::attribute::ExtendedAttributeList<'_>, + ) -> Result<Self, Self::Error> { + let attrs = parse_attributes(weedle_attributes, |attr| match attr { + Attribute::Enum => Ok(()), + Attribute::Error => Ok(()), + Attribute::Threadsafe => Ok(()), + _ => bail!(format!("{attr:?} not supported for interface definition")), + })?; + // Can't be both `[Threadsafe]` and an `[Enum]`. + if attrs.len() > 1 { + bail!("conflicting attributes on interface definition"); + } + Ok(Self(attrs)) + } +} + +impl<T: TryInto<InterfaceAttributes, Error = anyhow::Error>> TryFrom<Option<T>> + for InterfaceAttributes +{ + type Error = anyhow::Error; + fn try_from(value: Option<T>) -> Result<Self, Self::Error> { + match value { + None => Ok(Default::default()), + Some(v) => v.try_into(), + } + } +} + +/// Represents UDL attributes that might appear on a constructor. +/// +/// This supports the `[Throws=ErrorName]` attribute for constructors that can produce +/// an error, and the `[Name=MethodName]` for non-default constructors. +#[derive(Debug, Clone, Checksum, Default)] +pub(super) struct ConstructorAttributes(Vec<Attribute>); + +impl ConstructorAttributes { + pub(super) fn get_throws_err(&self) -> Option<&str> { + self.0.iter().find_map(|attr| match attr { + // This will hopefully return a helpful compilation error + // if the error is not defined. + Attribute::Throws(inner) => Some(inner.as_ref()), + _ => None, + }) + } + + pub(super) fn get_name(&self) -> Option<&str> { + self.0.iter().find_map(|attr| match attr { + Attribute::Name(inner) => Some(inner.as_ref()), + _ => None, + }) + } +} + +impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for ConstructorAttributes { + type Error = anyhow::Error; + fn try_from( + weedle_attributes: &weedle::attribute::ExtendedAttributeList<'_>, + ) -> Result<Self, Self::Error> { + let attrs = parse_attributes(weedle_attributes, |attr| match attr { + Attribute::Throws(_) => Ok(()), + Attribute::Name(_) => Ok(()), + _ => bail!(format!("{attr:?} not supported for constructors")), + })?; + Ok(Self(attrs)) + } +} + +/// Represents UDL attributes that might appear on a method. +/// +/// This supports the `[Throws=ErrorName]` attribute for methods that can produce +/// an error, and the `[Self=ByArc]` attribute for methods that take `Arc<Self>` as receiver. +#[derive(Debug, Clone, Checksum, Default)] +pub(super) struct MethodAttributes(Vec<Attribute>); + +impl MethodAttributes { + pub(super) fn get_throws_err(&self) -> Option<&str> { + self.0.iter().find_map(|attr| match attr { + // This will hopefully return a helpful compilation error + // if the error is not defined. + Attribute::Throws(inner) => Some(inner.as_ref()), + _ => None, + }) + } + + pub(super) fn get_self_by_arc(&self) -> bool { + self.0 + .iter() + .any(|attr| matches!(attr, Attribute::SelfType(SelfType::ByArc))) + } +} + +impl FromIterator<Attribute> for MethodAttributes { + fn from_iter<T: IntoIterator<Item = Attribute>>(iter: T) -> Self { + Self(Vec::from_iter(iter)) + } +} + +impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for MethodAttributes { + type Error = anyhow::Error; + fn try_from( + weedle_attributes: &weedle::attribute::ExtendedAttributeList<'_>, + ) -> Result<Self, Self::Error> { + let attrs = parse_attributes(weedle_attributes, |attr| match attr { + Attribute::SelfType(_) => Ok(()), + Attribute::Throws(_) => Ok(()), + _ => bail!(format!("{attr:?} not supported for methods")), + })?; + Ok(Self(attrs)) + } +} + +impl<T: TryInto<MethodAttributes, Error = anyhow::Error>> TryFrom<Option<T>> for MethodAttributes { + type Error = anyhow::Error; + fn try_from(value: Option<T>) -> Result<Self, Self::Error> { + match value { + None => Ok(Default::default()), + Some(v) => v.try_into(), + } + } +} + +/// Represents the different possible types of method call receiver. +/// +/// Actually we only support one of these right now, `[Self=ByArc]`. +/// We might add more in future, e.g. a `[Self=ByRef]` if there are cases +/// where we need to force the receiver to be taken by reference. +#[derive(Debug, Clone, Checksum)] +pub(super) enum SelfType { + ByArc, // Method receiver is `Arc<Self>`. +} + +impl TryFrom<&weedle::attribute::IdentifierOrString<'_>> for SelfType { + type Error = anyhow::Error; + fn try_from(nm: &weedle::attribute::IdentifierOrString<'_>) -> Result<Self, Self::Error> { + Ok(match nm { + weedle::attribute::IdentifierOrString::Identifier(identifier) => match identifier.0 { + "ByArc" => SelfType::ByArc, + _ => bail!("Unsupported Self Type: {:?}", identifier.0), + }, + weedle::attribute::IdentifierOrString::String(_) => { + bail!("Unsupported Self Type: {:?}", nm) + } + }) + } +} + +/// Represents UDL attributes that might appear on a typedef +/// +/// This supports the `[External="crate_name"]` and `[Custom]` attributes for types. +#[derive(Debug, Clone, Checksum, Default)] +pub(super) struct TypedefAttributes(Vec<Attribute>); + +impl TypedefAttributes { + pub(super) fn get_crate_name(&self) -> String { + self.0 + .iter() + .find_map(|attr| match attr { + Attribute::External(crate_name) => Some(crate_name.clone()), + _ => None, + }) + .expect("must have a crate name") + } + + pub(super) fn is_custom(&self) -> bool { + self.0 + .iter() + .any(|attr| matches!(attr, Attribute::Custom { .. })) + } +} + +impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for TypedefAttributes { + type Error = anyhow::Error; + fn try_from( + weedle_attributes: &weedle::attribute::ExtendedAttributeList<'_>, + ) -> Result<Self, Self::Error> { + let attrs = parse_attributes(weedle_attributes, |attr| match attr { + Attribute::External { .. } | Attribute::Custom => Ok(()), + _ => bail!(format!("{attr:?} not supported for typedefs")), + })?; + Ok(Self(attrs)) + } +} + +impl<T: TryInto<TypedefAttributes, Error = anyhow::Error>> TryFrom<Option<T>> + for TypedefAttributes +{ + type Error = anyhow::Error; + fn try_from(value: Option<T>) -> Result<Self, Self::Error> { + match value { + None => Ok(Default::default()), + Some(v) => v.try_into(), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use weedle::Parse; + + #[test] + fn test_byref() -> Result<()> { + let (_, node) = weedle::attribute::ExtendedAttribute::parse("ByRef").unwrap(); + let attr = Attribute::try_from(&node)?; + assert!(matches!(attr, Attribute::ByRef)); + Ok(()) + } + + #[test] + fn test_enum() -> Result<()> { + let (_, node) = weedle::attribute::ExtendedAttribute::parse("Enum").unwrap(); + let attr = Attribute::try_from(&node)?; + assert!(matches!(attr, Attribute::Enum)); + assert!(attr.is_enum()); + Ok(()) + } + + #[test] + fn test_error() -> Result<()> { + let (_, node) = weedle::attribute::ExtendedAttribute::parse("Error").unwrap(); + let attr = Attribute::try_from(&node)?; + assert!(matches!(attr, Attribute::Error)); + assert!(attr.is_error()); + Ok(()) + } + + #[test] + fn test_name() -> Result<()> { + let (_, node) = weedle::attribute::ExtendedAttribute::parse("Name=Value").unwrap(); + let attr = Attribute::try_from(&node)?; + assert!(matches!(attr, Attribute::Name(nm) if nm == "Value")); + + let (_, node) = weedle::attribute::ExtendedAttribute::parse("Name").unwrap(); + let err = Attribute::try_from(&node).unwrap_err(); + assert_eq!( + err.to_string(), + "ExtendedAttributeNoArgs not supported: \"Name\"" + ); + + Ok(()) + } + + #[test] + fn test_selftype() -> Result<()> { + let (_, node) = weedle::attribute::ExtendedAttribute::parse("Self=ByArc").unwrap(); + let attr = Attribute::try_from(&node)?; + assert!(matches!(attr, Attribute::SelfType(SelfType::ByArc))); + let (_, node) = weedle::attribute::ExtendedAttribute::parse("Self=ByMistake").unwrap(); + let err = Attribute::try_from(&node).unwrap_err(); + assert_eq!(err.to_string(), "Unsupported Self Type: \"ByMistake\""); + Ok(()) + } + + #[test] + fn test_threadsafe() -> Result<()> { + let (_, node) = weedle::attribute::ExtendedAttribute::parse("Threadsafe").unwrap(); + let attr = Attribute::try_from(&node)?; + assert!(matches!(attr, Attribute::Threadsafe)); + Ok(()) + } + + #[test] + fn test_throws() -> Result<()> { + let (_, node) = weedle::attribute::ExtendedAttribute::parse("Throws=Name").unwrap(); + let attr = Attribute::try_from(&node)?; + assert!(matches!(attr, Attribute::Throws(nm) if nm == "Name")); + + let (_, node) = weedle::attribute::ExtendedAttribute::parse("Throws").unwrap(); + let err = Attribute::try_from(&node).unwrap_err(); + assert_eq!( + err.to_string(), + "ExtendedAttributeNoArgs not supported: \"Throws\"" + ); + + Ok(()) + } + + #[test] + fn test_unsupported() { + let (_, node) = + weedle::attribute::ExtendedAttribute::parse("UnsupportedAttribute").unwrap(); + let err = Attribute::try_from(&node).unwrap_err(); + assert_eq!( + err.to_string(), + "ExtendedAttributeNoArgs not supported: \"UnsupportedAttribute\"" + ); + + let (_, node) = + weedle::attribute::ExtendedAttribute::parse("Unsupported=Attribute").unwrap(); + let err = Attribute::try_from(&node).unwrap_err(); + assert_eq!( + err.to_string(), + "Attribute identity Identifier not supported: \"Unsupported\"" + ); + } + + #[test] + fn test_other_attributes_not_supported_for_enums() { + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Error, ByRef]").unwrap(); + let err = EnumAttributes::try_from(&node).unwrap_err(); + assert_eq!(err.to_string(), "ByRef not supported for enums"); + } + + #[test] + fn test_throws_attribute() { + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Throws=Error]").unwrap(); + let attrs = FunctionAttributes::try_from(&node).unwrap(); + assert!(matches!(attrs.get_throws_err(), Some("Error"))); + + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[]").unwrap(); + let attrs = FunctionAttributes::try_from(&node).unwrap(); + assert!(matches!(attrs.get_throws_err(), None)); + } + + #[test] + fn test_other_attributes_not_supported_for_functions() { + let (_, node) = + weedle::attribute::ExtendedAttributeList::parse("[Throws=Error, ByRef]").unwrap(); + let err = FunctionAttributes::try_from(&node).unwrap_err(); + assert_eq!(err.to_string(), "ByRef not supported for functions"); + + let (_, node) = + weedle::attribute::ExtendedAttributeList::parse("[Throws=Error, Self=ByArc]").unwrap(); + let err = FunctionAttributes::try_from(&node).unwrap_err(); + assert_eq!( + err.to_string(), + "SelfType(ByArc) not supported for functions" + ); + } + + #[test] + fn test_method_attributes() { + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Throws=Error]").unwrap(); + let attrs = MethodAttributes::try_from(&node).unwrap(); + assert!(!attrs.get_self_by_arc()); + assert!(matches!(attrs.get_throws_err(), Some("Error"))); + + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[]").unwrap(); + let attrs = MethodAttributes::try_from(&node).unwrap(); + assert!(!attrs.get_self_by_arc()); + assert!(attrs.get_throws_err().is_none()); + + let (_, node) = + weedle::attribute::ExtendedAttributeList::parse("[Self=ByArc, Throws=Error]").unwrap(); + let attrs = MethodAttributes::try_from(&node).unwrap(); + assert!(attrs.get_self_by_arc()); + assert!(attrs.get_throws_err().is_some()); + + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Self=ByArc]").unwrap(); + let attrs = MethodAttributes::try_from(&node).unwrap(); + assert!(attrs.get_self_by_arc()); + assert!(attrs.get_throws_err().is_none()); + } + + #[test] + fn test_constructor_attributes() { + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Throws=Error]").unwrap(); + let attrs = ConstructorAttributes::try_from(&node).unwrap(); + assert!(matches!(attrs.get_throws_err(), Some("Error"))); + assert!(matches!(attrs.get_name(), None)); + + let (_, node) = + weedle::attribute::ExtendedAttributeList::parse("[Name=MyFactory]").unwrap(); + let attrs = ConstructorAttributes::try_from(&node).unwrap(); + assert!(matches!(attrs.get_throws_err(), None)); + assert!(matches!(attrs.get_name(), Some("MyFactory"))); + + let (_, node) = + weedle::attribute::ExtendedAttributeList::parse("[Throws=Error, Name=MyFactory]") + .unwrap(); + let attrs = ConstructorAttributes::try_from(&node).unwrap(); + assert!(matches!(attrs.get_throws_err(), Some("Error"))); + assert!(matches!(attrs.get_name(), Some("MyFactory"))); + } + + #[test] + fn test_other_attributes_not_supported_for_constructors() { + let (_, node) = + weedle::attribute::ExtendedAttributeList::parse("[Throws=Error, ByRef]").unwrap(); + let err = ConstructorAttributes::try_from(&node).unwrap_err(); + assert_eq!(err.to_string(), "ByRef not supported for constructors"); + + let (_, node) = + weedle::attribute::ExtendedAttributeList::parse("[Throws=Error, Self=ByArc]").unwrap(); + let err = ConstructorAttributes::try_from(&node).unwrap_err(); + assert_eq!( + err.to_string(), + "SelfType(ByArc) not supported for constructors" + ); + } + + #[test] + fn test_byref_attribute() { + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[ByRef]").unwrap(); + let attrs = ArgumentAttributes::try_from(&node).unwrap(); + assert!(matches!(attrs.by_ref(), true)); + + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[]").unwrap(); + let attrs = ArgumentAttributes::try_from(&node).unwrap(); + assert!(matches!(attrs.by_ref(), false)); + } + + #[test] + fn test_other_attributes_not_supported_for_arguments() { + let (_, node) = + weedle::attribute::ExtendedAttributeList::parse("[Throws=Error, ByRef]").unwrap(); + let err = ArgumentAttributes::try_from(&node).unwrap_err(); + assert_eq!( + err.to_string(), + "Throws(\"Error\") not supported for arguments" + ); + } + + #[test] + fn test_threadsafe_attribute() { + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Threadsafe]").unwrap(); + let attrs = InterfaceAttributes::try_from(&node).unwrap(); + assert!(matches!(attrs.threadsafe(), true)); + + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[]").unwrap(); + let attrs = InterfaceAttributes::try_from(&node).unwrap(); + assert!(matches!(attrs.threadsafe(), false)); + } + + #[test] + fn test_enum_attribute() { + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Enum]").unwrap(); + let attrs = InterfaceAttributes::try_from(&node).unwrap(); + assert!(matches!(attrs.contains_enum_attr(), true)); + + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[]").unwrap(); + let attrs = InterfaceAttributes::try_from(&node).unwrap(); + assert!(matches!(attrs.contains_enum_attr(), false)); + + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Threadsafe]").unwrap(); + let attrs = InterfaceAttributes::try_from(&node).unwrap(); + assert!(matches!(attrs.contains_enum_attr(), false)); + + let (_, node) = + weedle::attribute::ExtendedAttributeList::parse("[Threadsafe, Enum]").unwrap(); + let err = InterfaceAttributes::try_from(&node).unwrap_err(); + assert_eq!( + err.to_string(), + "conflicting attributes on interface definition" + ); + } + + #[test] + fn test_other_attributes_not_supported_for_interfaces() { + let (_, node) = + weedle::attribute::ExtendedAttributeList::parse("[Threadsafe, ByRef]").unwrap(); + let err = InterfaceAttributes::try_from(&node).unwrap_err(); + assert_eq!( + err.to_string(), + "ByRef not supported for interface definition" + ); + } + + #[test] + fn test_typedef_attribute() { + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Custom]").unwrap(); + let attrs = TypedefAttributes::try_from(&node).unwrap(); + assert!(attrs.is_custom()); + + let (_, node) = + weedle::attribute::ExtendedAttributeList::parse("[External=crate_name]").unwrap(); + let attrs = TypedefAttributes::try_from(&node).unwrap(); + assert!(!attrs.is_custom()); + assert_eq!(attrs.get_crate_name(), "crate_name"); + } + + #[test] + fn test_typedef_attributes_malformed() { + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[Custom=foo]").unwrap(); + let err = TypedefAttributes::try_from(&node).unwrap_err(); + assert_eq!( + err.to_string(), + "Attribute identity Identifier not supported: \"Custom\"" + ); + + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[External]").unwrap(); + let err = TypedefAttributes::try_from(&node).unwrap_err(); + assert_eq!( + err.to_string(), + "ExtendedAttributeNoArgs not supported: \"External\"" + ); + } + + #[test] + fn test_other_attributes_not_supported_for_typedef() { + let (_, node) = weedle::attribute::ExtendedAttributeList::parse("[ByRef]").unwrap(); + let err = TypedefAttributes::try_from(&node).unwrap_err(); + assert_eq!(err.to_string(), "ByRef not supported for typedefs"); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/interface/callbacks.rs b/third_party/rust/uniffi_bindgen/src/interface/callbacks.rs new file mode 100644 index 0000000000..a7164cec5b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/interface/callbacks.rs @@ -0,0 +1,169 @@ +/* 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/. */ + +//! # Callback Interface definitions for a `ComponentInterface`. +//! +//! This module converts callback interface definitions from UDL into structures that +//! can be added to a `ComponentInterface`. A declaration in the UDL like this: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! callback interface Example { +//! string hello(); +//! }; +//! # "##)?; +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! Will result in a [`CallbackInterface`] member being added to the resulting +//! [`ComponentInterface`]: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! # callback interface Example { +//! # string hello(); +//! # }; +//! # "##)?; +//! let callback = ci.get_callback_interface_definition("Example").unwrap(); +//! assert_eq!(callback.name(), "Example"); +//! assert_eq!(callback.methods()[0].name(), "hello"); +//! # Ok::<(), anyhow::Error>(()) +//! ``` + +use anyhow::{bail, Result}; +use uniffi_meta::Checksum; + +use super::ffi::{FfiArgument, FfiFunction, FfiType}; +use super::object::Method; +use super::types::{Type, TypeIterator}; +use super::{APIConverter, ComponentInterface}; + +#[derive(Debug, Clone, Checksum)] +pub struct CallbackInterface { + pub(super) name: String, + pub(super) methods: Vec<Method>, + // We don't include the FFIFunc in the hash calculation, because: + // - it is entirely determined by the other fields, + // so excluding it is safe. + // - its `name` property includes a checksum derived from the very + // hash value we're trying to calculate here, so excluding it + // avoids a weird circular dependency in the calculation. + #[checksum_ignore] + pub(super) ffi_init_callback: FfiFunction, +} + +impl CallbackInterface { + fn new(name: String) -> CallbackInterface { + CallbackInterface { + name, + methods: Default::default(), + ffi_init_callback: Default::default(), + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn type_(&self) -> Type { + Type::CallbackInterface(self.name.clone()) + } + + pub fn methods(&self) -> Vec<&Method> { + self.methods.iter().collect() + } + + pub fn ffi_init_callback(&self) -> &FfiFunction { + &self.ffi_init_callback + } + + pub(super) fn derive_ffi_funcs(&mut self, ci_prefix: &str) { + self.ffi_init_callback.name = format!("ffi_{ci_prefix}_{}_init_callback", self.name); + self.ffi_init_callback.arguments = vec![FfiArgument { + name: "callback_stub".to_string(), + type_: FfiType::ForeignCallback, + }]; + self.ffi_init_callback.return_type = None; + } + + pub fn iter_types(&self) -> TypeIterator<'_> { + Box::new(self.methods.iter().flat_map(Method::iter_types)) + } +} + +impl APIConverter<CallbackInterface> for weedle::CallbackInterfaceDefinition<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<CallbackInterface> { + if self.attributes.is_some() { + bail!("callback interface attributes are not supported yet"); + } + if self.inheritance.is_some() { + bail!("callback interface inheritance is not supported"); + } + let mut object = CallbackInterface::new(self.identifier.0.to_string()); + for member in &self.members.body { + match member { + weedle::interface::InterfaceMember::Operation(t) => { + let mut method: Method = t.convert(ci)?; + method.object_name = object.name.clone(); + object.methods.push(method); + } + _ => bail!( + "no support for callback interface member type {:?} yet", + member + ), + } + } + Ok(object) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_empty_interface() { + const UDL: &str = r#" + namespace test{}; + // Weird, but allowed. + callback interface Testing {}; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert_eq!(ci.callback_interface_definitions().len(), 1); + assert_eq!( + ci.get_callback_interface_definition("Testing") + .unwrap() + .methods() + .len(), + 0 + ); + } + + #[test] + fn test_multiple_interfaces() { + const UDL: &str = r#" + namespace test{}; + callback interface One { + void one(); + }; + callback interface Two { + u32 two(); + u64 too(); + }; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert_eq!(ci.callback_interface_definitions().len(), 2); + + let callbacks_one = ci.get_callback_interface_definition("One").unwrap(); + assert_eq!(callbacks_one.methods().len(), 1); + assert_eq!(callbacks_one.methods()[0].name(), "one"); + + let callbacks_two = ci.get_callback_interface_definition("Two").unwrap(); + assert_eq!(callbacks_two.methods().len(), 2); + assert_eq!(callbacks_two.methods()[0].name(), "two"); + assert_eq!(callbacks_two.methods()[1].name(), "too"); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/interface/enum_.rs b/third_party/rust/uniffi_bindgen/src/interface/enum_.rs new file mode 100644 index 0000000000..c85e6ecc46 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/interface/enum_.rs @@ -0,0 +1,441 @@ +/* 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/. */ + +//! # Enum definitions for a `ComponentInterface`. +//! +//! This module converts enum definition from UDL into structures that can be +//! added to a `ComponentInterface`. A declaration in the UDL like this: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! enum Example { +//! "one", +//! "two" +//! }; +//! # "##)?; +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! Will result in a [`Enum`] member being added to the resulting [`ComponentInterface`]: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! # enum Example { +//! # "one", +//! # "two" +//! # }; +//! # "##)?; +//! let e = ci.get_enum_definition("Example").unwrap(); +//! assert_eq!(e.name(), "Example"); +//! assert_eq!(e.variants().len(), 2); +//! assert_eq!(e.variants()[0].name(), "one"); +//! assert_eq!(e.variants()[1].name(), "two"); +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! Like in Rust, UniFFI enums can contain associated data, but this needs to be +//! declared with a different syntax in order to work within the restrictions of +//! WebIDL. A declaration like this: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! [Enum] +//! interface Example { +//! Zero(); +//! One(u32 first); +//! Two(u32 first, string second); +//! }; +//! # "##)?; +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! Will result in an [`Enum`] member whose variants have associated fields: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! # [Enum] +//! # interface ExampleWithData { +//! # Zero(); +//! # One(u32 first); +//! # Two(u32 first, string second); +//! # }; +//! # "##)?; +//! let e = ci.get_enum_definition("ExampleWithData").unwrap(); +//! assert_eq!(e.name(), "ExampleWithData"); +//! assert_eq!(e.variants().len(), 3); +//! assert_eq!(e.variants()[0].name(), "Zero"); +//! assert_eq!(e.variants()[0].fields().len(), 0); +//! assert_eq!(e.variants()[1].name(), "One"); +//! assert_eq!(e.variants()[1].fields().len(), 1); +//! assert_eq!(e.variants()[1].fields()[0].name(), "first"); +//! # Ok::<(), anyhow::Error>(()) +//! ``` + +use anyhow::{bail, Result}; +use uniffi_meta::Checksum; + +use super::record::Field; +use super::types::{Type, TypeIterator}; +use super::{APIConverter, ComponentInterface}; + +/// Represents an enum with named variants, each of which may have named +/// and typed fields. +/// +/// Enums are passed across the FFI by serializing to a bytebuffer, with a +/// i32 indicating the variant followed by the serialization of each field. +#[derive(Debug, Clone, PartialEq, Eq, Checksum)] +pub struct Enum { + pub(super) name: String, + pub(super) variants: Vec<Variant>, + // "Flat" enums do not have variants with associated data. + pub(super) flat: bool, +} + +impl Enum { + pub fn name(&self) -> &str { + &self.name + } + + pub fn type_(&self) -> Type { + // *sigh* at the clone here, the relationship between a ComponentInterface + // and its contained types could use a bit of a cleanup. + Type::Enum(self.name.clone()) + } + + pub fn variants(&self) -> &[Variant] { + &self.variants + } + + pub fn is_flat(&self) -> bool { + self.flat + } + + pub fn iter_types(&self) -> TypeIterator<'_> { + Box::new(self.variants.iter().flat_map(Variant::iter_types)) + } +} + +impl From<uniffi_meta::EnumMetadata> for Enum { + fn from(meta: uniffi_meta::EnumMetadata) -> Self { + let flat = meta.variants.iter().all(|v| v.fields.is_empty()); + Self { + name: meta.name, + variants: meta.variants.into_iter().map(Into::into).collect(), + flat, + } + } +} + +// Note that we have two `APIConverter` impls here - one for the `enum` case +// and one for the `[Enum] interface` case. + +impl APIConverter<Enum> for weedle::EnumDefinition<'_> { + fn convert(&self, _ci: &mut ComponentInterface) -> Result<Enum> { + Ok(Enum { + name: self.identifier.0.to_string(), + variants: self + .values + .body + .list + .iter() + .map::<Result<_>, _>(|v| { + Ok(Variant { + name: v.0.to_string(), + ..Default::default() + }) + }) + .collect::<Result<Vec<_>>>()?, + // Enums declared using the `enum` syntax can never have variants with fields. + flat: true, + }) + } +} + +impl APIConverter<Enum> for weedle::InterfaceDefinition<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Enum> { + if self.inheritance.is_some() { + bail!("interface inheritance is not supported for enum interfaces"); + } + // We don't need to check `self.attributes` here; if calling code has dispatched + // to this impl then we already know there was an `[Enum]` attribute. + Ok(Enum { + name: self.identifier.0.to_string(), + variants: self + .members + .body + .iter() + .map::<Result<Variant>, _>(|member| match member { + weedle::interface::InterfaceMember::Operation(t) => Ok(t.convert(ci)?), + _ => bail!( + "interface member type {:?} not supported in enum interface", + member + ), + }) + .collect::<Result<Vec<_>>>()?, + // Enums declared using the `[Enum] interface` syntax might have variants with fields. + flat: false, + }) + } +} + +/// Represents an individual variant in an Enum. +/// +/// Each variant has a name and zero or more fields. +#[derive(Debug, Clone, Default, PartialEq, Eq, Checksum)] +pub struct Variant { + pub(super) name: String, + pub(super) fields: Vec<Field>, +} + +impl Variant { + pub fn name(&self) -> &str { + &self.name + } + + pub fn fields(&self) -> &[Field] { + &self.fields + } + + pub fn has_fields(&self) -> bool { + !self.fields.is_empty() + } + + pub fn iter_types(&self) -> TypeIterator<'_> { + Box::new(self.fields.iter().flat_map(Field::iter_types)) + } +} + +impl From<uniffi_meta::VariantMetadata> for Variant { + fn from(meta: uniffi_meta::VariantMetadata) -> Self { + Self { + name: meta.name, + fields: meta.fields.into_iter().map(Into::into).collect(), + } + } +} + +impl APIConverter<Variant> for weedle::interface::OperationInterfaceMember<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Variant> { + if self.special.is_some() { + bail!("special operations not supported"); + } + if let Some(weedle::interface::StringifierOrStatic::Stringifier(_)) = self.modifier { + bail!("stringifiers are not supported"); + } + // OK, so this is a little weird. + // The syntax we use for enum interface members is `Name(type arg, ...);`, which parses + // as an anonymous operation where `Name` is the return type. We re-interpret it to + // use `Name` as the name of the variant. + if self.identifier.is_some() { + bail!("enum interface members must not have a method name"); + } + let name: String = { + use weedle::types::{ + NonAnyType::Identifier, ReturnType, SingleType::NonAny, Type::Single, + }; + match &self.return_type { + ReturnType::Type(Single(NonAny(Identifier(id)))) => id.type_.0.to_owned(), + _ => bail!("enum interface members must have plain identifiers as names"), + } + }; + Ok(Variant { + name, + fields: self + .args + .body + .list + .iter() + .map(|arg| arg.convert(ci)) + .collect::<Result<Vec<_>>>()?, + }) + } +} + +impl APIConverter<Field> for weedle::argument::Argument<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Field> { + match self { + weedle::argument::Argument::Single(t) => t.convert(ci), + weedle::argument::Argument::Variadic(_) => bail!("variadic arguments not supported"), + } + } +} + +impl APIConverter<Field> for weedle::argument::SingleArgument<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Field> { + let type_ = ci.resolve_type_expression(&self.type_)?; + if let Type::Object(_) = type_ { + bail!("Objects cannot currently be used in enum variant data"); + } + if self.default.is_some() { + bail!("enum interface variant fields must not have default values"); + } + if self.attributes.is_some() { + bail!("enum interface variant fields must not have attributes"); + } + // TODO: maybe we should use our own `Field` type here with just name and type, + // rather than appropriating record::Field..? + Ok(Field { + name: self.identifier.0.to_string(), + type_, + default: None, + }) + } +} + +#[cfg(test)] +mod test { + use super::super::ffi::FfiType; + use super::*; + + #[test] + fn test_duplicate_variants() { + const UDL: &str = r#" + namespace test{}; + // Weird, but currently allowed! + // We should probably disallow this... + enum Testing { "one", "two", "one" }; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert_eq!(ci.enum_definitions().count(), 1); + assert_eq!( + ci.get_enum_definition("Testing").unwrap().variants().len(), + 3 + ); + } + + #[test] + fn test_associated_data() { + const UDL: &str = r##" + namespace test { + void takes_an_enum(TestEnum e); + void takes_an_enum_with_data(TestEnumWithData ed); + TestEnum returns_an_enum(); + TestEnumWithData returns_an_enum_with_data(); + }; + + enum TestEnum { "one", "two" }; + + [Enum] + interface TestEnumWithData { + Zero(); + One(u32 first); + Two(u32 first, string second); + }; + + [Enum] + interface TestEnumWithoutData { + One(); + Two(); + }; + "##; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert_eq!(ci.enum_definitions().count(), 3); + assert_eq!(ci.function_definitions().len(), 4); + + // The "flat" enum with no associated data. + let e = ci.get_enum_definition("TestEnum").unwrap(); + assert!(e.is_flat()); + assert_eq!(e.variants().len(), 2); + assert_eq!( + e.variants().iter().map(|v| v.name()).collect::<Vec<_>>(), + vec!["one", "two"] + ); + assert_eq!(e.variants()[0].fields().len(), 0); + assert_eq!(e.variants()[1].fields().len(), 0); + + // The enum with associated data. + let ed = ci.get_enum_definition("TestEnumWithData").unwrap(); + assert!(!ed.is_flat()); + assert_eq!(ed.variants().len(), 3); + assert_eq!( + ed.variants().iter().map(|v| v.name()).collect::<Vec<_>>(), + vec!["Zero", "One", "Two"] + ); + assert_eq!(ed.variants()[0].fields().len(), 0); + assert_eq!( + ed.variants()[1] + .fields() + .iter() + .map(|f| f.name()) + .collect::<Vec<_>>(), + vec!["first"] + ); + assert_eq!( + ed.variants()[1] + .fields() + .iter() + .map(|f| f.type_()) + .collect::<Vec<_>>(), + vec![&Type::UInt32] + ); + assert_eq!( + ed.variants()[2] + .fields() + .iter() + .map(|f| f.name()) + .collect::<Vec<_>>(), + vec!["first", "second"] + ); + assert_eq!( + ed.variants()[2] + .fields() + .iter() + .map(|f| f.type_()) + .collect::<Vec<_>>(), + vec![&Type::UInt32, &Type::String] + ); + + // The enum declared via interface, but with no associated data. + let ewd = ci.get_enum_definition("TestEnumWithoutData").unwrap(); + assert!(!ewd.is_flat()); + assert_eq!(ewd.variants().len(), 2); + assert_eq!( + ewd.variants().iter().map(|v| v.name()).collect::<Vec<_>>(), + vec!["One", "Two"] + ); + assert_eq!(ewd.variants()[0].fields().len(), 0); + assert_eq!(ewd.variants()[1].fields().len(), 0); + + // Flat enums pass over the FFI as bytebuffers. + // (It might be nice to optimize these to pass as plain integers, but that's + // difficult atop the current factoring of `ComponentInterface` and friends). + let farg = ci.get_function_definition("takes_an_enum").unwrap(); + assert_eq!(*farg.arguments()[0].type_(), Type::Enum("TestEnum".into())); + assert_eq!( + farg.ffi_func().arguments()[0].type_(), + FfiType::RustBuffer(None) + ); + let fret = ci.get_function_definition("returns_an_enum").unwrap(); + assert!(matches!(fret.return_type(), Some(Type::Enum(nm)) if nm == "TestEnum")); + assert!(matches!( + fret.ffi_func().return_type(), + Some(FfiType::RustBuffer(None)) + )); + + // Enums with associated data pass over the FFI as bytebuffers. + let farg = ci + .get_function_definition("takes_an_enum_with_data") + .unwrap(); + assert_eq!( + *farg.arguments()[0].type_(), + Type::Enum("TestEnumWithData".into()) + ); + assert_eq!( + farg.ffi_func().arguments()[0].type_(), + FfiType::RustBuffer(None) + ); + let fret = ci + .get_function_definition("returns_an_enum_with_data") + .unwrap(); + assert!(matches!(fret.return_type(), Some(Type::Enum(nm)) if nm == "TestEnumWithData")); + assert!(matches!( + fret.ffi_func().return_type(), + Some(FfiType::RustBuffer(None)) + )); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/interface/error.rs b/third_party/rust/uniffi_bindgen/src/interface/error.rs new file mode 100644 index 0000000000..9aa57255e8 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/interface/error.rs @@ -0,0 +1,230 @@ +/* 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/. */ + +//! # Error definitions for a `ComponentInterface`. +//! +//! This module converts error definition from UDL into structures that can be +//! added to a `ComponentInterface`. A declaration in the UDL like this: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! [Error] +//! enum Example { +//! "one", +//! "two" +//! }; +//! # "##)?; +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! Will result in an [`Error`] member with fieldless variants being added to the resulting [`ComponentInterface`]: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! # [Error] +//! # enum Example { +//! # "one", +//! # "two" +//! # }; +//! # "##)?; +//! let err = ci.get_error_definition("Example").unwrap(); +//! assert_eq!(err.name(), "Example"); +//! assert_eq!(err.variants().len(), 2); +//! assert_eq!(err.variants()[0].name(), "one"); +//! assert_eq!(err.variants()[1].name(), "two"); +//! assert_eq!(err.is_flat(), true); +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! A declaration in the UDL like this: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! [Error] +//! interface Example { +//! one(i16 code); +//! two(string reason); +//! three(i32 x, i32 y); +//! }; +//! # "##)?; +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! Will result in an [`Error`] member with variants that have fields being added to the resulting [`ComponentInterface`]: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! # [Error] +//! # interface Example { +//! # one(); +//! # two(string reason); +//! # three(i32 x, i32 y); +//! # }; +//! # "##)?; +//! let err = ci.get_error_definition("Example").unwrap(); +//! assert_eq!(err.name(), "Example"); +//! assert_eq!(err.variants().len(), 3); +//! assert_eq!(err.variants()[0].name(), "one"); +//! assert_eq!(err.variants()[1].name(), "two"); +//! assert_eq!(err.variants()[2].name(), "three"); +//! assert_eq!(err.variants()[0].fields().len(), 0); +//! assert_eq!(err.variants()[1].fields().len(), 1); +//! assert_eq!(err.variants()[1].fields()[0].name(), "reason"); +//! assert_eq!(err.variants()[2].fields().len(), 2); +//! assert_eq!(err.variants()[2].fields()[0].name(), "x"); +//! assert_eq!(err.variants()[2].fields()[1].name(), "y"); +//! assert_eq!(err.is_flat(), false); +//! # Ok::<(), anyhow::Error>(()) +//! ``` + +use anyhow::Result; +use uniffi_meta::Checksum; + +use super::enum_::{Enum, Variant}; +use super::types::{Type, TypeIterator}; +use super::{APIConverter, ComponentInterface}; + +/// Represents an Error that might be thrown by functions/methods in the component interface. +/// +/// Errors are represented in the UDL as enums with the special `[Error]` attribute, but +/// they're handled in the FFI very differently. We create them in `uniffi::call_with_result()` if +/// the wrapped function returns an `Err` value +/// struct and assign an integer error code to each variant. +#[derive(Debug, Clone, PartialEq, Eq, Checksum)] +pub struct Error { + pub name: String, + enum_: Enum, +} + +impl Error { + pub fn from_enum(enum_: Enum) -> Self { + Self { + name: enum_.name.clone(), + enum_, + } + } + + pub fn type_(&self) -> Type { + // *sigh* at the clone here, the relationship between a ComponentInterface + // and its contained types could use a bit of a cleanup. + Type::Error(self.name.clone()) + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn wrapped_enum(&self) -> &Enum { + &self.enum_ + } + + pub fn variants(&self) -> &[Variant] { + self.enum_.variants() + } + + pub fn is_flat(&self) -> bool { + self.enum_.is_flat() + } + + pub fn iter_types(&self) -> TypeIterator<'_> { + self.wrapped_enum().iter_types() + } +} + +impl From<uniffi_meta::ErrorMetadata> for Error { + fn from(meta: uniffi_meta::ErrorMetadata) -> Self { + Self { + name: meta.name.clone(), + enum_: Enum { + name: meta.name, + variants: meta.variants.into_iter().map(Into::into).collect(), + flat: meta.flat, + }, + } + } +} + +impl APIConverter<Error> for weedle::EnumDefinition<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Error> { + Ok(Error::from_enum(APIConverter::<Enum>::convert(self, ci)?)) + } +} + +impl APIConverter<Error> for weedle::InterfaceDefinition<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Error> { + Ok(Error::from_enum(APIConverter::<Enum>::convert(self, ci)?)) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_variants() { + const UDL: &str = r#" + namespace test{}; + [Error] + enum Testing { "one", "two", "three" }; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert_eq!(ci.error_definitions().count(), 1); + let error = ci.get_error_definition("Testing").unwrap(); + assert_eq!( + error + .variants() + .iter() + .map(|v| v.name()) + .collect::<Vec<&str>>(), + vec!("one", "two", "three") + ); + assert!(error.is_flat()); + } + + #[test] + fn test_duplicate_variants() { + const UDL: &str = r#" + namespace test{}; + // Weird, but currently allowed! + // We should probably disallow this... + [Error] + enum Testing { "one", "two", "one" }; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert_eq!(ci.error_definitions().count(), 1); + assert_eq!( + ci.get_error_definition("Testing").unwrap().variants().len(), + 3 + ); + } + + #[test] + fn test_variant_data() { + const UDL: &str = r#" + namespace test{}; + + [Error] + interface Testing { + One(string reason); + Two(u8 code); + }; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert_eq!(ci.error_definitions().count(), 1); + let error: &Error = ci.get_error_definition("Testing").unwrap(); + assert_eq!( + error + .variants() + .iter() + .map(|v| v.name()) + .collect::<Vec<&str>>(), + vec!("One", "Two") + ); + assert!(!error.is_flat()); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/interface/ffi.rs b/third_party/rust/uniffi_bindgen/src/interface/ffi.rs new file mode 100644 index 0000000000..e1f9fe9737 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/interface/ffi.rs @@ -0,0 +1,102 @@ +/* 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/. */ + +//! # Low-level typesystem for the FFI layer of a component interface. +//! +//! This module provides the "FFI-level" typesystem of a UniFFI Rust Component, that is, +//! the C-style functions and structs and primitive datatypes that are used to interface +//! between the Rust component code and the foreign-language bindings. +//! +//! These types are purely an implementation detail of UniFFI, so consumers shouldn't +//! need to know about them. But as a developer working on UniFFI itself, you're likely +//! to spend a lot of time thinking about how these low-level types are used to represent +//! the higher-level "interface types" from the [`super::types::Type`] enum. +/// Represents the restricted set of low-level types that can be used to construct +/// the C-style FFI layer between a rust component and its foreign language bindings. +/// +/// For the types that involve memory allocation, we make a distinction between +/// "owned" types (the recipient must free it, or pass it to someone else) and +/// "borrowed" types (the sender must keep it alive for the duration of the call). +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum FfiType { + // N.B. there are no booleans at this layer, since they cause problems for JNA. + UInt8, + Int8, + UInt16, + Int16, + UInt32, + Int32, + UInt64, + Int64, + Float32, + Float64, + /// A `*const c_void` pointer to a rust-owned `Arc<T>`. + /// If you've got one of these, you must call the appropriate rust function to free it. + /// The templates will generate a unique `free` function for each T. + /// The inner string references the name of the `T` type. + RustArcPtr(String), + /// A byte buffer allocated by rust, and owned by whoever currently holds it. + /// If you've got one of these, you must either call the appropriate rust function to free it + /// or pass it to someone that will. + /// If the inner option is Some, it is the name of the external type. The bindings may need + /// to use this name to import the correct RustBuffer for that type. + RustBuffer(Option<String>), + /// A borrowed reference to some raw bytes owned by foreign language code. + /// The provider of this reference must keep it alive for the duration of the receiving call. + ForeignBytes, + /// A pointer to a single function in to the foreign language. + /// This function contains all the machinery to make callbacks work on the foreign language side. + ForeignCallback, + // TODO: you can imagine a richer structural typesystem here, e.g. `Ref<String>` or something. + // We don't need that yet and it's possible we never will, so it isn't here for now. +} + +/// Represents an "extern C"-style function that will be part of the FFI. +/// +/// These can't be declared explicitly in the UDL, but rather, are derived automatically +/// from the high-level interface. Each callable thing in the component API will have a +/// corresponding `FfiFunction` through which it can be invoked, and UniFFI also provides +/// some built-in `FfiFunction` helpers for use in the foreign language bindings. +#[derive(Debug, Default, Clone)] +pub struct FfiFunction { + pub(super) name: String, + pub(super) arguments: Vec<FfiArgument>, + pub(super) return_type: Option<FfiType>, +} + +impl FfiFunction { + pub fn name(&self) -> &str { + &self.name + } + pub fn arguments(&self) -> Vec<&FfiArgument> { + self.arguments.iter().collect() + } + pub fn return_type(&self) -> Option<&FfiType> { + self.return_type.as_ref() + } +} + +/// Represents an argument to an FFI function. +/// +/// Each argument has a name and a type. +#[derive(Debug, Clone)] +pub struct FfiArgument { + pub(super) name: String, + pub(super) type_: FfiType, +} + +impl FfiArgument { + pub fn name(&self) -> &str { + &self.name + } + pub fn type_(&self) -> FfiType { + self.type_.clone() + } +} + +#[cfg(test)] +mod test { + // There's not really much to test here to be honest, + // it's mostly type declarations. +} diff --git a/third_party/rust/uniffi_bindgen/src/interface/function.rs b/third_party/rust/uniffi_bindgen/src/interface/function.rs new file mode 100644 index 0000000000..61724816cb --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/interface/function.rs @@ -0,0 +1,288 @@ +/* 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/. */ + +//! # Function definitions for a `ComponentInterface`. +//! +//! This module converts function definitions from UDL into structures that +//! can be added to a `ComponentInterface`. A declaration in the UDL like this: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! namespace example { +//! string hello(); +//! }; +//! # "##)?; +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! Will result in a [`Function`] member being added to the resulting [`ComponentInterface`]: +//! +//! ``` +//! # use uniffi_bindgen::interface::Type; +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example { +//! # string hello(); +//! # }; +//! # "##)?; +//! let func = ci.get_function_definition("hello").unwrap(); +//! assert_eq!(func.name(), "hello"); +//! assert!(matches!(func.return_type(), Some(Type::String))); +//! assert_eq!(func.arguments().len(), 0); +//! # Ok::<(), anyhow::Error>(()) +//! ``` +use std::convert::TryFrom; + +use anyhow::{bail, Result}; +use uniffi_meta::Checksum; + +use super::attributes::{ArgumentAttributes, Attribute, FunctionAttributes}; +use super::ffi::{FfiArgument, FfiFunction}; +use super::literal::{convert_default_value, Literal}; +use super::types::{Type, TypeIterator}; +use super::{convert_type, APIConverter, ComponentInterface}; + +/// Represents a standalone function. +/// +/// Each `Function` corresponds to a standalone function in the rust module, +/// and has a corresponding standalone function in the foreign language bindings. +/// +/// In the FFI, this will be a standalone function with appropriately lowered types. +#[derive(Debug, Clone, Checksum)] +pub struct Function { + pub(super) name: String, + pub(super) arguments: Vec<Argument>, + pub(super) return_type: Option<Type>, + // We don't include the FFIFunc in the hash calculation, because: + // - it is entirely determined by the other fields, + // so excluding it is safe. + // - its `name` property includes a checksum derived from the very + // hash value we're trying to calculate here, so excluding it + // avoids a weird circular dependency in the calculation. + #[checksum_ignore] + pub(super) ffi_func: FfiFunction, + pub(super) attributes: FunctionAttributes, +} + +impl Function { + pub fn name(&self) -> &str { + &self.name + } + + pub fn arguments(&self) -> Vec<&Argument> { + self.arguments.iter().collect() + } + + pub fn full_arguments(&self) -> Vec<Argument> { + self.arguments.to_vec() + } + + pub fn return_type(&self) -> Option<&Type> { + self.return_type.as_ref() + } + + pub fn ffi_func(&self) -> &FfiFunction { + &self.ffi_func + } + + pub fn throws(&self) -> bool { + self.attributes.get_throws_err().is_some() + } + + pub fn throws_name(&self) -> Option<&str> { + self.attributes.get_throws_err() + } + + pub fn throws_type(&self) -> Option<Type> { + self.attributes + .get_throws_err() + .map(|name| Type::Error(name.to_owned())) + } + + pub fn derive_ffi_func(&mut self, ci_prefix: &str) -> Result<()> { + // The name is already set if the function is defined through a proc-macro invocation + // rather than in UDL. Don't overwrite it in that case. + if self.ffi_func.name.is_empty() { + self.ffi_func.name = format!("{ci_prefix}_{}", self.name); + } + + self.ffi_func.arguments = self.arguments.iter().map(|arg| arg.into()).collect(); + self.ffi_func.return_type = self.return_type.as_ref().map(|rt| rt.into()); + Ok(()) + } +} + +impl From<uniffi_meta::FnParamMetadata> for Argument { + fn from(meta: uniffi_meta::FnParamMetadata) -> Self { + Argument { + name: meta.name, + type_: convert_type(&meta.ty), + by_ref: false, + optional: false, + default: None, + } + } +} + +impl From<uniffi_meta::FnMetadata> for Function { + fn from(meta: uniffi_meta::FnMetadata) -> Self { + let ffi_name = meta.ffi_symbol_name(); + + let return_type = meta.return_type.map(|out| convert_type(&out)); + let arguments = meta.inputs.into_iter().map(Into::into).collect(); + + let ffi_func = FfiFunction { + name: ffi_name, + ..FfiFunction::default() + }; + + Self { + name: meta.name, + arguments, + return_type, + ffi_func, + attributes: meta.throws.map(Attribute::Throws).into_iter().collect(), + } + } +} + +impl APIConverter<Function> for weedle::namespace::NamespaceMember<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Function> { + match self { + weedle::namespace::NamespaceMember::Operation(f) => f.convert(ci), + _ => bail!("no support for namespace member type {:?} yet", self), + } + } +} + +impl APIConverter<Function> for weedle::namespace::OperationNamespaceMember<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Function> { + let return_type = ci.resolve_return_type_expression(&self.return_type)?; + Ok(Function { + name: match self.identifier { + None => bail!("anonymous functions are not supported {:?}", self), + Some(id) => id.0.to_string(), + }, + return_type, + arguments: self.args.body.list.convert(ci)?, + ffi_func: Default::default(), + attributes: FunctionAttributes::try_from(self.attributes.as_ref())?, + }) + } +} + +/// Represents an argument to a function/constructor/method call. +/// +/// Each argument has a name and a type, along with some optional metadata. +#[derive(Debug, Clone, Checksum)] +pub struct Argument { + pub(super) name: String, + pub(super) type_: Type, + pub(super) by_ref: bool, + pub(super) optional: bool, + pub(super) default: Option<Literal>, +} + +impl Argument { + pub fn name(&self) -> &str { + &self.name + } + + pub fn type_(&self) -> &Type { + &self.type_ + } + + pub fn by_ref(&self) -> bool { + self.by_ref + } + + pub fn default_value(&self) -> Option<&Literal> { + self.default.as_ref() + } + + pub fn iter_types(&self) -> TypeIterator<'_> { + self.type_.iter_types() + } +} + +impl From<&Argument> for FfiArgument { + fn from(a: &Argument) -> FfiArgument { + FfiArgument { + name: a.name.clone(), + type_: (&a.type_).into(), + } + } +} + +impl APIConverter<Argument> for weedle::argument::Argument<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Argument> { + match self { + weedle::argument::Argument::Single(t) => t.convert(ci), + weedle::argument::Argument::Variadic(_) => bail!("variadic arguments not supported"), + } + } +} + +impl APIConverter<Argument> for weedle::argument::SingleArgument<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Argument> { + let type_ = ci.resolve_type_expression(&self.type_)?; + let default = match self.default { + None => None, + Some(v) => Some(convert_default_value(&v.value, &type_)?), + }; + let by_ref = ArgumentAttributes::try_from(self.attributes.as_ref())?.by_ref(); + Ok(Argument { + name: self.identifier.0.to_string(), + type_, + by_ref, + optional: self.optional.is_some(), + default, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_minimal_and_rich_function() -> Result<()> { + let ci = ComponentInterface::from_webidl( + r##" + namespace test { + void minimal(); + [Throws=TestError] + sequence<string?> rich(u32 arg1, TestDict arg2); + }; + [Error] + enum TestError { "err" }; + dictionary TestDict { + u32 field; + }; + "##, + )?; + + let func1 = ci.get_function_definition("minimal").unwrap(); + assert_eq!(func1.name(), "minimal"); + assert!(func1.return_type().is_none()); + assert!(func1.throws_type().is_none()); + assert_eq!(func1.arguments().len(), 0); + + let func2 = ci.get_function_definition("rich").unwrap(); + assert_eq!(func2.name(), "rich"); + assert_eq!( + func2.return_type().unwrap().canonical_name(), + "SequenceOptionalstring" + ); + assert!(matches!(func2.throws_type(), Some(Type::Error(s)) if s == "TestError")); + assert_eq!(func2.arguments().len(), 2); + assert_eq!(func2.arguments()[0].name(), "arg1"); + assert_eq!(func2.arguments()[0].type_().canonical_name(), "u32"); + assert_eq!(func2.arguments()[1].name(), "arg2"); + assert_eq!( + func2.arguments()[1].type_().canonical_name(), + "TypeTestDict" + ); + Ok(()) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/interface/literal.rs b/third_party/rust/uniffi_bindgen/src/interface/literal.rs new file mode 100644 index 0000000000..da02613684 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/interface/literal.rs @@ -0,0 +1,186 @@ +/* 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/. */ + +//! # Support for literal values. +//! +//! This module provides support for interpreting literal values from the UDL, +//! which appear in places such as default arguments. + +use anyhow::{bail, Result}; +use uniffi_meta::Checksum; + +use super::types::Type; + +// Represents a literal value. +// Used for e.g. default argument values. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Checksum)] +pub enum Literal { + Boolean(bool), + String(String), + // Integers are represented as the widest representation we can. + // Number formatting vary with language and radix, so we avoid a lot of parsing and + // formatting duplication by using only signed and unsigned variants. + UInt(u64, Radix, Type), + Int(i64, Radix, Type), + // Pass the string representation through as typed in the UDL. + // This avoids a lot of uncertainty around precision and accuracy, + // though bindings for languages less sophisticated number parsing than WebIDL + // will have to do extra work. + Float(String, Type), + Enum(String, Type), + EmptySequence, + EmptyMap, + Null, +} + +// Represent the radix of integer literal values. +// We preserve the radix into the generated bindings for readability reasons. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Checksum)] +pub enum Radix { + Decimal = 10, + Octal = 8, + Hexadecimal = 16, +} + +pub(super) fn convert_default_value( + default_value: &weedle::literal::DefaultValue<'_>, + type_: &Type, +) -> Result<Literal> { + fn convert_integer(literal: &weedle::literal::IntegerLit<'_>, type_: &Type) -> Result<Literal> { + let (string, radix) = match literal { + weedle::literal::IntegerLit::Dec(v) => (v.0, Radix::Decimal), + weedle::literal::IntegerLit::Hex(v) => (v.0, Radix::Hexadecimal), + weedle::literal::IntegerLit::Oct(v) => (v.0, Radix::Octal), + }; + // This is the radix of the parsed number, passed to `from_str_radix`. + let src_radix = radix as u32; + // This radix tells the backends how to represent the number in the output languages. + let dest_radix = if string == "0" || string.starts_with('-') { + // 1. weedle parses "0" as an octal literal, but we most likely want to treat this as a decimal. + // 2. Explicitly negatively signed hex numbers won't convert via i64 very well if they're not 64 bit. + // For ease of implementation, output will use decimal. + Radix::Decimal + } else { + radix + }; + + // Clippy seems to think we should be using `strip_prefix` here, but + // it seems confused as to what this is actually doing. + #[allow(clippy::manual_strip)] + let string = if string.starts_with('-') { + ("-".to_string() + string[1..].trim_start_matches("0x")).to_lowercase() + } else { + string.trim_start_matches("0x").to_lowercase() + }; + + Ok(match type_ { + Type::Int8 | Type::Int16 | Type::Int32 | Type::Int64 => Literal::Int( + i64::from_str_radix(&string, src_radix)?, + dest_radix, + type_.clone(), + ), + Type::UInt8 | Type::UInt16 | Type::UInt32 | Type::UInt64 => Literal::UInt( + u64::from_str_radix(&string, src_radix)?, + dest_radix, + type_.clone(), + ), + + _ => bail!("Cannot coerce literal {} into a non-integer type", string), + }) + } + + fn convert_float(literal: &weedle::literal::FloatLit<'_>, type_: &Type) -> Result<Literal> { + let string = match literal { + weedle::literal::FloatLit::Value(v) => v.0, + + _ => bail!("Infinity and NaN is not currently supported"), + }; + + Ok(match type_ { + Type::Float32 | Type::Float64 => Literal::Float(string.to_string(), type_.clone()), + _ => bail!("Cannot coerce literal {} into a non-float type", string), + }) + } + + Ok(match (default_value, type_) { + (weedle::literal::DefaultValue::Boolean(b), Type::Boolean) => Literal::Boolean(b.0), + (weedle::literal::DefaultValue::String(s), Type::String) => { + // Note that weedle doesn't parse escaped double quotes. + // Keeping backends using double quotes (possible for all to date) + // means we don't need to escape single quotes. But we haven't spent a lot of time + // trying to break default values with weird escapes and quotes. + Literal::String(s.0.to_string()) + } + (weedle::literal::DefaultValue::EmptyArray(_), Type::Sequence(_)) => Literal::EmptySequence, + (weedle::literal::DefaultValue::String(s), Type::Enum(_)) => { + Literal::Enum(s.0.to_string(), type_.clone()) + } + (weedle::literal::DefaultValue::Null(_), Type::Optional(_)) => Literal::Null, + (_, Type::Optional(inner_type)) => convert_default_value(default_value, inner_type)?, + + // We'll ensure the type safety in the convert_* number methods. + (weedle::literal::DefaultValue::Integer(i), _) => convert_integer(i, type_)?, + (weedle::literal::DefaultValue::Float(i), _) => convert_float(i, type_)?, + + _ => bail!("No support for {:?} literal yet", default_value), + }) +} + +#[cfg(test)] +mod test { + use super::*; + use weedle::Parse; + + fn parse_and_convert(expr: &str, t: Type) -> Result<Literal> { + let (_, node) = weedle::literal::DefaultValue::parse(expr).unwrap(); + convert_default_value(&node, &t) + } + + #[test] + fn test_default_value_conversion() -> Result<()> { + assert!(matches!( + parse_and_convert("0", Type::UInt8)?, + Literal::UInt(0, Radix::Decimal, Type::UInt8) + )); + assert!(matches!( + parse_and_convert("-12", Type::Int32)?, + Literal::Int(-12, Radix::Decimal, Type::Int32) + )); + assert!( + matches!(parse_and_convert("3.14", Type::Float32)?, Literal::Float(v, Type::Float32) if v == "3.14") + ); + assert!(matches!( + parse_and_convert("false", Type::Boolean)?, + Literal::Boolean(false) + )); + assert!( + matches!(parse_and_convert("\"TEST\"", Type::String)?, Literal::String(v) if v == "TEST") + ); + assert!( + matches!(parse_and_convert("\"one\"", Type::Enum("E".into()))?, Literal::Enum(v, Type::Enum(e)) if v == "one" && e == "E") + ); + assert!(matches!( + parse_and_convert("[]", Type::Sequence(Box::new(Type::String)))?, + Literal::EmptySequence + )); + assert!(matches!( + parse_and_convert("null", Type::Optional(Box::new(Type::String)))?, + Literal::Null + )); + Ok(()) + } + #[test] + fn test_error_on_type_mismatch() { + assert_eq!( + parse_and_convert("0", Type::Boolean) + .unwrap_err() + .to_string(), + "Cannot coerce literal 0 into a non-integer type" + ); + assert!(parse_and_convert("{}", Type::Boolean) + .unwrap_err() + .to_string() + .starts_with("No support for")); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/interface/mod.rs b/third_party/rust/uniffi_bindgen/src/interface/mod.rs new file mode 100644 index 0000000000..3daf50ef4a --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/interface/mod.rs @@ -0,0 +1,1218 @@ +/* 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/. */ + +//! # Component Interface Definition. +//! +//! This module provides an abstract representation of the interface provided by a UniFFI Rust Component, +//! in high-level terms suitable for translation into target consumer languages such as Kotlin +//! and Swift. It also provides facilities for parsing a WebIDL interface definition file into such a +//! representation. +//! +//! The entrypoint to this crate is the `ComponentInterface` struct, which holds a complete definition +//! of the interface provided by a component, in two parts: +//! +//! * The high-level consumer API, in terms of objects and records and methods and so-on +//! * The low-level FFI contract through which the foreign language code can call into Rust. +//! +//! That's really the key concept of this crate so it's worth repeating: a `ComponentInterface` completely +//! defines the shape and semantics of an interface between the Rust-based implementation of a component +//! and its foreign language consumers, including details like: +//! +//! * The names of all symbols in the compiled object file +//! * The type and arity of all exported functions +//! * The layout and conventions used for all arguments and return types +//! +//! If you have a dynamic library compiled from a Rust Component using this crate, and a foreign +//! language binding generated from the same `ComponentInterface` using the same version of this +//! module, then there should be no opportunities for them to disagree on how the two sides should +//! interact. +//! +//! General and incomplete TODO list for this thing: +//! +//! * It should prevent user error and the possibility of generating bad code by doing (at least) +//! the following checks: +//! * No duplicate names (types, methods, args, etc) +//! * No shadowing of builtin names, or names we use in code generation +//! We expect that if the user actually does one of these things, then they *should* get a compile +//! error when trying to build the component, because the codegen will be invalid. But we can't +//! guarantee that there's not some edge-case where it produces valid-but-incorrect code. +//! +//! * There is a *lot* of cloning going on, in the spirit of "first make it work". There's probably +//! a good opportunity here for e.g. interned strings, but we're nowhere near the point were we need +//! that kind of optimization just yet. +//! +//! * Error messages and general developer experience leave a lot to be desired. + +use std::{ + collections::{btree_map::Entry, BTreeMap, HashSet}, + convert::TryFrom, + iter, +}; + +use anyhow::{bail, ensure, Result}; + +pub mod types; +pub use types::Type; +use types::{TypeIterator, TypeUniverse}; + +mod attributes; +mod callbacks; +pub use callbacks::CallbackInterface; +mod enum_; +pub use enum_::Enum; +mod error; +pub use error::Error; +mod function; +pub use function::{Argument, Function}; +mod literal; +pub use literal::{Literal, Radix}; +mod namespace; +pub use namespace::Namespace; +mod object; +pub use object::{Constructor, Method, Object}; +mod record; +pub use record::{Field, Record}; + +pub mod ffi; +pub use ffi::{FfiArgument, FfiFunction, FfiType}; +use uniffi_meta::{Checksum, FnMetadata, MethodMetadata, ObjectMetadata}; + +/// This needs to match the major/minor version of the `uniffi` crate. See +/// `docs/uniffi-versioning.md` for details. +/// +/// Once we get to 1.0, then we should reformat this to only include the major version number. +const UNIFFI_CONTRACT_VERSION: &str = "0.22"; + +/// The main public interface for this module, representing the complete details of an interface exposed +/// by a rust component and the details of consuming it via an extern-C FFI layer. +/// +#[derive(Debug, Default, Checksum)] +pub struct ComponentInterface { + /// This always points to `UNIFFI_CONTRACT_VERSION`. By including it in the checksum, we + /// prevent consumers from combining scaffolding and bindings that were created with different + /// `uniffi` versions. + uniffi_version: &'static str, + /// All of the types used in the interface. + // We can't checksum `self.types`, but its contents are implied by the other fields + // anyway, so it's safe to ignore it. + #[checksum_ignore] + pub(super) types: TypeUniverse, + /// The unique prefix that we'll use for namespacing when exposing this component's API. + namespace: String, + /// The internal unique prefix used to namespace FFI symbols + #[checksum_ignore] + ffi_namespace: String, + /// The high-level API provided by the component. + enums: BTreeMap<String, Enum>, + records: BTreeMap<String, Record>, + functions: Vec<Function>, + objects: Vec<Object>, + callback_interfaces: Vec<CallbackInterface>, + errors: BTreeMap<String, Error>, +} + +impl ComponentInterface { + /// Parse a `ComponentInterface` from a string containing a WebIDL definition. + pub fn from_webidl(idl: &str) -> Result<Self> { + let mut ci = Self { + uniffi_version: UNIFFI_CONTRACT_VERSION, + ..Default::default() + }; + // There's some lifetime thing with the errors returned from weedle::Definitions::parse + // that my own lifetime is too short to worry about figuring out; unwrap and move on. + + // Note we use `weedle::Definitions::parse` instead of `weedle::parse` so + // on parse errors we can see how far weedle got, which helps locate the problem. + use weedle::Parse; // this trait must be in scope for parse to work. + let (remaining, defns) = weedle::Definitions::parse(idl.trim()).unwrap(); + if !remaining.is_empty() { + println!("Error parsing the IDL. Text remaining to be parsed is:"); + println!("{remaining}"); + bail!("parse error"); + } + // Unconditionally add the String type, which is used by the panic handling + ci.types.add_known_type(&Type::String)?; + // We process the WebIDL definitions in two passes. + // First, go through and look for all the named types. + ci.types.add_type_definitions_from(defns.as_slice())?; + // With those names resolved, we can build a complete representation of the API. + APIBuilder::process(&defns, &mut ci)?; + + // The FFI namespace must not be computed on the fly because it could otherwise be + // influenced by things added later from proc-macro metadata. Those have their own + // namespacing mechanism. + assert!(!ci.namespace.is_empty()); + ci.ffi_namespace = format!("{}_{:x}", ci.namespace, ci.checksum()); + + // The following two methods will be called later anyways, but we call them here because + // it's convenient for UDL-only tests. + ci.check_consistency()?; + // Now that the high-level API is settled, we can derive the low-level FFI. + ci.derive_ffi_funcs()?; + + Ok(ci) + } + + /// The string namespace within which this API should be presented to the caller. + /// + /// This string would typically be used to prefix function names in the FFI, to build + /// a package or module name for the foreign language, etc. + pub fn namespace(&self) -> &str { + self.namespace.as_str() + } + + /// Get the definitions for every Enum type in the interface. + pub fn enum_definitions(&self) -> impl Iterator<Item = &Enum> { + self.enums.values() + } + + /// Get an Enum definition by name, or None if no such Enum is defined. + pub fn get_enum_definition(&self, name: &str) -> Option<&Enum> { + self.enums.get(name) + } + + /// Get the definitions for every Record type in the interface. + pub fn record_definitions(&self) -> impl Iterator<Item = &Record> { + self.records.values() + } + + /// Get a Record definition by name, or None if no such Record is defined. + pub fn get_record_definition(&self, name: &str) -> Option<&Record> { + self.records.get(name) + } + + /// Get the definitions for every Function in the interface. + pub fn function_definitions(&self) -> &[Function] { + &self.functions + } + + /// Get a Function definition by name, or None if no such Function is defined. + pub fn get_function_definition(&self, name: &str) -> Option<&Function> { + // TODO: probably we could store these internally in a HashMap to make this easier? + self.functions.iter().find(|f| f.name == name) + } + + /// Get the definitions for every Object type in the interface. + pub fn object_definitions(&self) -> &[Object] { + &self.objects + } + + /// Get an Object definition by name, or None if no such Object is defined. + pub fn get_object_definition(&self, name: &str) -> Option<&Object> { + // TODO: probably we could store these internally in a HashMap to make this easier? + self.objects.iter().find(|o| o.name == name) + } + + /// Get the definitions for every Callback Interface type in the interface. + pub fn callback_interface_definitions(&self) -> &[CallbackInterface] { + &self.callback_interfaces + } + + /// Get a Callback interface definition by name, or None if no such interface is defined. + pub fn get_callback_interface_definition(&self, name: &str) -> Option<&CallbackInterface> { + // TODO: probably we could store these internally in a HashMap to make this easier? + self.callback_interfaces.iter().find(|o| o.name == name) + } + + /// Get the definitions for every Error type in the interface. + pub fn error_definitions(&self) -> impl Iterator<Item = &Error> { + self.errors.values() + } + + /// Get an Error definition by name, or None if no such Error is defined. + pub fn get_error_definition(&self, name: &str) -> Option<&Error> { + self.errors.get(name) + } + + /// Should we generate read (and lift) functions for errors? + /// + /// This is a workaround for the fact that lower/write can't be generated for some errors, + /// specifically errors that are defined as flat in the UDL, but actually have fields in the + /// Rust source. + pub fn should_generate_error_read(&self, error: &Error) -> bool { + // We can and should always generate read() methods for fielded errors + let fielded = !error.is_flat(); + // For flat errors, we should only generate read() methods if we need them to support + // callback interface errors + let used_in_callback_interface = self + .callback_interface_definitions() + .iter() + .flat_map(|cb| cb.methods()) + .any(|m| m.throws_type() == Some(error.type_())); + + fielded || used_in_callback_interface + } + + /// Get details about all `Type::External` types + pub fn iter_external_types(&self) -> impl Iterator<Item = (&String, &String)> { + self.types.iter_known_types().filter_map(|t| match t { + Type::External { name, crate_name } => Some((name, crate_name)), + _ => None, + }) + } + + /// Get details about all `Type::Custom` types + pub fn iter_custom_types(&self) -> impl Iterator<Item = (&String, &Type)> { + self.types.iter_known_types().filter_map(|t| match t { + Type::Custom { name, builtin } => Some((name, &**builtin)), + _ => None, + }) + } + + /// Iterate over all known types in the interface. + pub fn iter_types(&self) -> impl Iterator<Item = &Type> { + self.types.iter_known_types() + } + + /// Get a specific type + pub fn get_type(&self, name: &str) -> Option<Type> { + self.types.get_type_definition(name) + } + + /// Iterate over all types contained in the given item. + /// + /// This method uses `iter_types` to iterate over the types contained within the given type, + /// but additionally recurses into the definition of user-defined types like records and enums + /// to yield the types that *they* contain. + fn iter_types_in_item<'a>(&'a self, item: &'a Type) -> impl Iterator<Item = &'a Type> + 'a { + RecursiveTypeIterator::new(self, item) + } + + /// Check whether the given item contains any (possibly nested) Type::Object references. + /// + /// This is important to know in language bindings that cannot integrate object types + /// tightly with the host GC, and hence need to perform manual destruction of objects. + pub fn item_contains_object_references(&self, item: &Type) -> bool { + self.iter_types_in_item(item) + .any(|t| matches!(t, Type::Object(_))) + } + + /// Check whether the given item contains any (possibly nested) unsigned types + pub fn item_contains_unsigned_types(&self, item: &Type) -> bool { + self.iter_types_in_item(item) + .any(|t| matches!(t, Type::UInt8 | Type::UInt16 | Type::UInt32 | Type::UInt64)) + } + + /// Check whether the interface contains any optional types + pub fn contains_optional_types(&self) -> bool { + self.types + .iter_known_types() + .any(|t| matches!(t, Type::Optional(_))) + } + + /// Check whether the interface contains any sequence types + pub fn contains_sequence_types(&self) -> bool { + self.types + .iter_known_types() + .any(|t| matches!(t, Type::Sequence(_))) + } + + /// Check whether the interface contains any map types + pub fn contains_map_types(&self) -> bool { + self.types + .iter_known_types() + .any(|t| matches!(t, Type::Map(_, _))) + } + + /// Calculate a numeric checksum for this ComponentInterface. + /// + /// The checksum can be used to guard against accidentally using foreign-language bindings + /// generated from one version of an interface with the compiled Rust code from a different + /// version of that interface. It offers the following properties: + /// + /// - Two ComponentIntefaces generated from the same WebIDL file, using the same version of uniffi + /// and the same version of Rust, will always have the same checksum value. + /// - Two ComponentInterfaces will, with high probability, have different checksum values if: + /// - They were generated from two different WebIDL files. + /// - They were generated by two different versions of uniffi + /// + /// Note that this is designed to prevent accidents, not attacks, so there is no need for the + /// checksum to be cryptographically secure. + pub fn checksum(&self) -> u16 { + uniffi_meta::checksum(self) + } + + /// The namespace to use in FFI-level function definitions. + /// + /// The value returned by this method is used as a prefix to namespace all UDL-defined FFI + /// functions used in this ComponentInterface. + /// + /// Since these names are an internal implementation detail that is not typically visible to + /// consumers, we take the opportunity to add an additional safety guard by including a 4-hex-char + /// checksum in each name. If foreign-language bindings attempt to load and use a version of the + /// Rust code compiled from a different UDL definition than the one used for the bindings themselves, + /// then there is a high probability of checksum mismatch and they will fail to link against the + /// compiled Rust code. The result will be an ugly inscrutable link-time error, but that is a lot + /// better than triggering potentially arbitrary memory unsafety! + pub fn ffi_namespace(&self) -> &str { + assert!(!self.ffi_namespace.is_empty()); + &self.ffi_namespace + } + + /// Builtin FFI function for allocating a new `RustBuffer`. + /// This is needed so that the foreign language bindings can create buffers in which to pass + /// complex data types across the FFI. + pub fn ffi_rustbuffer_alloc(&self) -> FfiFunction { + FfiFunction { + name: format!("ffi_{}_rustbuffer_alloc", self.ffi_namespace()), + arguments: vec![FfiArgument { + name: "size".to_string(), + type_: FfiType::Int32, + }], + return_type: Some(FfiType::RustBuffer(None)), + } + } + + /// Builtin FFI function for copying foreign-owned bytes + /// This is needed so that the foreign language bindings can create buffers in which to pass + /// complex data types across the FFI. + pub fn ffi_rustbuffer_from_bytes(&self) -> FfiFunction { + FfiFunction { + name: format!("ffi_{}_rustbuffer_from_bytes", self.ffi_namespace()), + arguments: vec![FfiArgument { + name: "bytes".to_string(), + type_: FfiType::ForeignBytes, + }], + return_type: Some(FfiType::RustBuffer(None)), + } + } + + /// Builtin FFI function for freeing a `RustBuffer`. + /// This is needed so that the foreign language bindings can free buffers in which they received + /// complex data types returned across the FFI. + pub fn ffi_rustbuffer_free(&self) -> FfiFunction { + FfiFunction { + name: format!("ffi_{}_rustbuffer_free", self.ffi_namespace()), + arguments: vec![FfiArgument { + name: "buf".to_string(), + type_: FfiType::RustBuffer(None), + }], + return_type: None, + } + } + + /// Builtin FFI function for reserving extra space in a `RustBuffer`. + /// This is needed so that the foreign language bindings can grow buffers used for passing + /// complex data types across the FFI. + pub fn ffi_rustbuffer_reserve(&self) -> FfiFunction { + FfiFunction { + name: format!("ffi_{}_rustbuffer_reserve", self.ffi_namespace()), + arguments: vec![ + FfiArgument { + name: "buf".to_string(), + type_: FfiType::RustBuffer(None), + }, + FfiArgument { + name: "additional".to_string(), + type_: FfiType::Int32, + }, + ], + return_type: Some(FfiType::RustBuffer(None)), + } + } + + /// List the definitions of all FFI functions in the interface. + /// + /// The set of FFI functions is derived automatically from the set of higher-level types + /// along with the builtin FFI helper functions. + pub fn iter_ffi_function_definitions(&self) -> impl Iterator<Item = FfiFunction> + '_ { + self.iter_user_ffi_function_definitions() + .cloned() + .chain(self.iter_rust_buffer_ffi_function_definitions()) + } + + /// List all FFI functions definitions for user-defined interfaces + /// + /// This includes FFI functions for: + /// - Top-level functions + /// - Object methods + /// - Callback interfaces + pub fn iter_user_ffi_function_definitions(&self) -> impl Iterator<Item = &FfiFunction> + '_ { + iter::empty() + .chain( + self.objects + .iter() + .flat_map(|obj| obj.iter_ffi_function_definitions()), + ) + .chain( + self.callback_interfaces + .iter() + .map(|cb| cb.ffi_init_callback()), + ) + .chain(self.functions.iter().map(|f| &f.ffi_func)) + } + + /// List all FFI functions definitions for RustBuffer functionality + pub fn iter_rust_buffer_ffi_function_definitions(&self) -> impl Iterator<Item = FfiFunction> { + [ + self.ffi_rustbuffer_alloc(), + self.ffi_rustbuffer_from_bytes(), + self.ffi_rustbuffer_free(), + self.ffi_rustbuffer_reserve(), + ] + .into_iter() + } + + // + // Private methods for building a ComponentInterface. + // + + /// Resolve a weedle type expression into a `Type`. + /// + /// This method uses the current state of our `TypeUniverse` to turn a weedle type expression + /// into a concrete `Type` (or error if the type expression is not well defined). It abstracts + /// away the complexity of walking weedle's type struct hierarchy by dispatching to the `TypeResolver` + /// trait. + fn resolve_type_expression<T: types::TypeResolver>(&mut self, expr: T) -> Result<Type> { + self.types.resolve_type_expression(expr) + } + + /// Resolve a weedle `ReturnType` expression into an optional `Type`. + /// + /// This method is similar to `resolve_type_expression`, but tailored specifically for return types. + /// It can return `None` to represent a non-existent return value. + fn resolve_return_type_expression( + &mut self, + expr: &weedle::types::ReturnType<'_>, + ) -> Result<Option<Type>> { + Ok(match expr { + weedle::types::ReturnType::Undefined(_) => None, + weedle::types::ReturnType::Type(t) => { + // Older versions of WebIDL used `void` for functions that don't return a value, + // while newer versions have replaced it with `undefined`. Special-case this for + // backwards compatibility for our consumers. + use weedle::types::{NonAnyType::Identifier, SingleType::NonAny, Type::Single}; + match t { + Single(NonAny(Identifier(id))) if id.type_.0 == "void" => None, + _ => Some(self.resolve_type_expression(t)?), + } + } + }) + } + + /// Called by `APIBuilder` impls to add a newly-parsed namespace definition to the `ComponentInterface`. + fn add_namespace_definition(&mut self, defn: Namespace) -> Result<()> { + if !self.namespace.is_empty() { + bail!("duplicate namespace definition"); + } + self.namespace = defn.name; + Ok(()) + } + + /// Called by `APIBuilder` impls to add a newly-parsed enum definition to the `ComponentInterface`. + pub(super) fn add_enum_definition(&mut self, defn: Enum) -> Result<()> { + match self.enums.entry(defn.name().to_owned()) { + Entry::Vacant(v) => { + v.insert(defn); + } + Entry::Occupied(o) => { + let existing_def = o.get(); + if defn != *existing_def { + bail!( + "Mismatching definition for enum `{}`!\n\ + existing definition: {existing_def:#?},\n\ + new definition: {defn:#?}", + defn.name(), + ); + } + } + } + + Ok(()) + } + + /// Called by `APIBuilder` impls to add a newly-parsed record definition to the `ComponentInterface`. + pub(super) fn add_record_definition(&mut self, defn: Record) -> Result<()> { + match self.records.entry(defn.name().to_owned()) { + Entry::Vacant(v) => { + v.insert(defn); + } + Entry::Occupied(o) => { + let existing_def = o.get(); + if defn != *existing_def { + bail!( + "Mismatching definition for record `{}`!\n\ + existing definition: {existing_def:#?},\n\ + new definition: {defn:#?}", + defn.name(), + ); + } + } + } + + Ok(()) + } + + fn add_function_impl(&mut self, defn: Function) -> Result<()> { + // Since functions are not a first-class type, we have to check for duplicates here + // rather than relying on the type-finding pass to catch them. + if self.functions.iter().any(|f| f.name == defn.name) { + bail!("duplicate function definition: \"{}\"", defn.name); + } + if !matches!(self.types.get_type_definition(defn.name()), None) { + bail!("Conflicting type definition for \"{}\"", defn.name()); + } + self.functions.push(defn); + + Ok(()) + } + + /// Called by `APIBuilder` impls to add a newly-parsed function definition to the `ComponentInterface`. + fn add_function_definition(&mut self, defn: Function) -> Result<()> { + for arg in &defn.arguments { + self.types.add_known_type(&arg.type_)?; + } + if let Some(ty) = &defn.return_type { + self.types.add_known_type(ty)?; + } + + self.add_function_impl(defn) + } + + pub(super) fn add_fn_meta(&mut self, meta: FnMetadata) -> Result<()> { + self.add_function_impl(meta.into()) + } + + pub(super) fn add_method_meta(&mut self, meta: MethodMetadata) { + let object = get_or_insert_object(&mut self.objects, &meta.self_name); + let defn: Method = meta.into(); + object.methods.push(defn); + } + + pub(super) fn add_object_free_fn(&mut self, meta: ObjectMetadata) { + let object = get_or_insert_object(&mut self.objects, &meta.name); + object.ffi_func_free.name = meta.free_ffi_symbol_name(); + } + + /// Called by `APIBuilder` impls to add a newly-parsed object definition to the `ComponentInterface`. + fn add_object_definition(&mut self, defn: Object) { + // Note that there will be no duplicates thanks to the previous type-finding pass. + self.objects.push(defn); + } + + /// Called by `APIBuilder` impls to add a newly-parsed callback interface definition to the `ComponentInterface`. + fn add_callback_interface_definition(&mut self, defn: CallbackInterface) { + // Note that there will be no duplicates thanks to the previous type-finding pass. + self.callback_interfaces.push(defn); + } + + /// Called by `APIBuilder` impls to add a newly-parsed error definition to the `ComponentInterface`. + pub(super) fn add_error_definition(&mut self, defn: Error) -> Result<()> { + match self.errors.entry(defn.name().to_owned()) { + Entry::Vacant(v) => { + v.insert(defn); + } + Entry::Occupied(o) => { + let existing_def = o.get(); + if defn != *existing_def { + bail!( + "Mismatching definition for error `{}`!\n\ + existing definition: {existing_def:#?},\n\ + new definition: {defn:#?}", + defn.name(), + ); + } + } + } + + Ok(()) + } + + /// Resolve unresolved types within proc-macro function / method signatures. + pub fn resolve_types(&mut self) -> Result<()> { + fn handle_unresolved_in( + ty: &mut Type, + f: impl Fn(&str) -> Result<Type> + Clone, + ) -> Result<()> { + match ty { + Type::Unresolved { name } => { + *ty = f(name)?; + } + Type::Optional(inner) => { + handle_unresolved_in(inner, f)?; + } + Type::Sequence(inner) => { + handle_unresolved_in(inner, f)?; + } + Type::Map(k, v) => { + handle_unresolved_in(k, f.clone())?; + handle_unresolved_in(v, f)?; + } + _ => {} + } + + Ok(()) + } + + let fn_sig_types = self.functions.iter_mut().flat_map(|fun| { + fun.arguments + .iter_mut() + .map(|arg| &mut arg.type_) + .chain(&mut fun.return_type) + }); + let method_sig_types = self.objects.iter_mut().flat_map(|obj| { + obj.methods.iter_mut().flat_map(|m| { + m.arguments + .iter_mut() + .map(|arg| &mut arg.type_) + .chain(&mut m.return_type) + }) + }); + + let record_fields_types = self + .records + .values_mut() + .flat_map(|r| r.fields.iter_mut().map(|f| &mut f.type_)); + let enum_fields_types = self.enums.values_mut().flat_map(|e| { + e.variants + .iter_mut() + .flat_map(|r| r.fields.iter_mut().map(|f| &mut f.type_)) + }); + + let possibly_unresolved_types = fn_sig_types + .chain(method_sig_types) + .chain(record_fields_types) + .chain(enum_fields_types); + + for ty in possibly_unresolved_types { + handle_unresolved_in(ty, |unresolved_ty_name| { + match self.types.get_type_definition(unresolved_ty_name) { + Some(def) => { + assert!( + !matches!(&def, Type::Unresolved { .. }), + "unresolved types must not be part of TypeUniverse" + ); + Ok(def) + } + None => bail!("Failed to resolve type `{unresolved_ty_name}`"), + } + })?; + + // The proc-macro scanning metadata code doesn't add known types + // when they could contain unresolved types, so we have to do it + // here after replacing unresolveds. + self.types.add_known_type(ty)?; + } + + Ok(()) + } + + /// Perform global consistency checks on the declared interface. + /// + /// This method checks for consistency problems in the declared interface + /// as a whole, and which can only be detected after we've finished defining + /// the entire interface. + pub fn check_consistency(&self) -> Result<()> { + if self.namespace.is_empty() { + bail!("missing namespace definition"); + } + + // To keep codegen tractable, enum variant names must not shadow type names. + for e in self.enums.values() { + for variant in &e.variants { + if self.types.get_type_definition(variant.name()).is_some() { + bail!( + "Enum variant names must not shadow type names: \"{}\"", + variant.name() + ) + } + } + } + + for ty in self.iter_types() { + match ty { + Type::Object(name) => { + ensure!( + self.objects.iter().any(|o| o.name == *name), + "Object `{name}` has no definition", + ); + } + Type::Record(name) => { + ensure!( + self.records.contains_key(name), + "Record `{name}` has no definition", + ); + } + Type::Enum(name) => { + ensure!( + self.enums.contains_key(name), + "Enum `{name}` has no definition", + ); + } + Type::Unresolved { name } => { + bail!("Type `{name}` should be resolved at this point"); + } + _ => {} + } + } + + Ok(()) + } + + /// Automatically derive the low-level FFI functions from the high-level types in the interface. + /// + /// This should only be called after the high-level types have been completed defined, otherwise + /// the resulting set will be missing some entries. + pub fn derive_ffi_funcs(&mut self) -> Result<()> { + let ci_prefix = self.ffi_namespace().to_owned(); + for func in self.functions.iter_mut() { + func.derive_ffi_func(&ci_prefix)?; + } + for obj in self.objects.iter_mut() { + obj.derive_ffi_funcs(&ci_prefix)?; + } + for callback in self.callback_interfaces.iter_mut() { + callback.derive_ffi_funcs(&ci_prefix); + } + Ok(()) + } +} + +fn get_or_insert_object<'a>(objects: &'a mut Vec<Object>, name: &str) -> &'a mut Object { + // The find-based way of writing this currently runs into a borrow checker + // error, so we use position + match objects.iter_mut().position(|o| o.name == name) { + Some(idx) => &mut objects[idx], + None => { + objects.push(Object::new(name.to_owned())); + objects.last_mut().unwrap() + } + } +} + +/// Stateful iterator for yielding all types contained in a given type. +/// +/// This struct is the implementation of [`ComponentInterface::iter_types_in_item`] and should be +/// considered an opaque implementation detail. It's a separate struct because I couldn't +/// figure out a way to implement it using iterators and closures that would make the lifetimes +/// work out correctly. +/// +/// The idea here is that we want to yield all the types from `iter_types` on a given type, and +/// additionally we want to recurse into the definition of any user-provided types like records, +/// enums, etc so we can also yield the types contained therein. +/// +/// To guard against infinite recursion, we maintain a list of previously-seen user-defined +/// types, ensuring that we recurse into the definition of those types only once. To simplify +/// the implementation, we maintain a queue of pending user-defined types that we have seen +/// but not yet recursed into. (Ironically, the use of an explicit queue means our implementation +/// is not actually recursive...) +struct RecursiveTypeIterator<'a> { + /// The [`ComponentInterface`] from which this iterator was created. + ci: &'a ComponentInterface, + /// The currently-active iterator from which we're yielding. + current: TypeIterator<'a>, + /// A set of names of user-defined types that we have already seen. + seen: HashSet<&'a str>, + /// A queue of user-defined types that we need to recurse into. + pending: Vec<&'a Type>, +} + +impl<'a> RecursiveTypeIterator<'a> { + /// Allocate a new `RecursiveTypeIterator` over the given item. + fn new(ci: &'a ComponentInterface, item: &'a Type) -> RecursiveTypeIterator<'a> { + RecursiveTypeIterator { + ci, + // We begin by iterating over the types from the item itself. + current: item.iter_types(), + seen: Default::default(), + pending: Default::default(), + } + } + + /// Add a new type to the queue of pending types, if not previously seen. + fn add_pending_type(&mut self, type_: &'a Type) { + match type_ { + Type::Record(nm) + | Type::Enum(nm) + | Type::Error(nm) + | Type::Object(nm) + | Type::CallbackInterface(nm) => { + if !self.seen.contains(nm.as_str()) { + self.pending.push(type_); + self.seen.insert(nm.as_str()); + } + } + _ => (), + } + } + + /// Advance the iterator to recurse into the next pending type, if any. + /// + /// This method is called when the current iterator is empty, and it will select + /// the next pending type from the queue and start iterating over its contained types. + /// The return value will be the first item from the new iterator. + fn advance_to_next_type(&mut self) -> Option<&'a Type> { + if let Some(next_type) = self.pending.pop() { + // This is a little awkward because the various definition lookup methods return an `Option<T>`. + // In the unlikely event that one of them returns `None` then, rather than trying to advance + // to a non-existent type, we just leave the existing iterator in place and allow the recursive + // call to `next()` to try again with the next pending type. + let next_iter = match next_type { + Type::Record(nm) => self.ci.get_record_definition(nm).map(Record::iter_types), + Type::Enum(nm) => self.ci.get_enum_definition(nm).map(Enum::iter_types), + Type::Error(nm) => self.ci.get_error_definition(nm).map(Error::iter_types), + Type::Object(nm) => self.ci.get_object_definition(nm).map(Object::iter_types), + Type::CallbackInterface(nm) => self + .ci + .get_callback_interface_definition(nm) + .map(CallbackInterface::iter_types), + _ => None, + }; + if let Some(next_iter) = next_iter { + self.current = next_iter; + } + // Advance the new iterator to its first item. If the new iterator happens to be empty, + // this will recurse back in to `advance_to_next_type` until we find one that isn't. + self.next() + } else { + // We've completely finished the iteration over all pending types. + None + } + } +} + +impl<'a> Iterator for RecursiveTypeIterator<'a> { + type Item = &'a Type; + fn next(&mut self) -> Option<Self::Item> { + if let Some(type_) = self.current.next() { + self.add_pending_type(type_); + Some(type_) + } else { + self.advance_to_next_type() + } + } +} + +/// Trait to help build a `ComponentInterface` from WedIDL syntax nodes. +/// +/// This trait does structural matching on the various weedle AST nodes and +/// uses them to build up the records, enums, objects etc in the provided +/// `ComponentInterface`. +trait APIBuilder { + fn process(&self, ci: &mut ComponentInterface) -> Result<()>; +} + +/// Add to a `ComponentInterface` from a list of weedle definitions, +/// by processing each in turn. +impl<T: APIBuilder> APIBuilder for Vec<T> { + fn process(&self, ci: &mut ComponentInterface) -> Result<()> { + for item in self { + item.process(ci)?; + } + Ok(()) + } +} + +/// Add to a `ComponentInterface` from a weedle definition. +/// This is conceptually the root of the parser, and dispatches to implementations +/// for the various specific WebIDL types that we support. +impl APIBuilder for weedle::Definition<'_> { + fn process(&self, ci: &mut ComponentInterface) -> Result<()> { + match self { + weedle::Definition::Namespace(d) => d.process(ci)?, + weedle::Definition::Enum(d) => { + // We check if the enum represents an error... + let attrs = attributes::EnumAttributes::try_from(d.attributes.as_ref())?; + if attrs.contains_error_attr() { + let err = d.convert(ci)?; + ci.add_error_definition(err)?; + } else { + let e = d.convert(ci)?; + ci.add_enum_definition(e)?; + } + } + weedle::Definition::Dictionary(d) => { + let rec = d.convert(ci)?; + ci.add_record_definition(rec)?; + } + weedle::Definition::Interface(d) => { + let attrs = attributes::InterfaceAttributes::try_from(d.attributes.as_ref())?; + if attrs.contains_enum_attr() { + let e = d.convert(ci)?; + ci.add_enum_definition(e)?; + } else if attrs.contains_error_attr() { + let e = d.convert(ci)?; + ci.add_error_definition(e)?; + } else { + let obj = d.convert(ci)?; + ci.add_object_definition(obj); + } + } + weedle::Definition::CallbackInterface(d) => { + let obj = d.convert(ci)?; + ci.add_callback_interface_definition(obj); + } + // everything needed for typedefs is done in finder.rs. + weedle::Definition::Typedef(_) => {} + _ => bail!("don't know how to deal with {:?}", self), + } + Ok(()) + } +} + +/// Trait to help convert WedIDL syntax nodes into `ComponentInterface` objects. +/// +/// This trait does structural matching on the various weedle AST nodes and converts +/// them into appropriate structs that we can use to build up the contents of a +/// `ComponentInterface`. It is basically the `TryFrom` trait except that the conversion +/// always happens in the context of a given `ComponentInterface`, which is used for +/// resolving e.g. type definitions. +/// +/// The difference between this trait and `APIBuilder` is that `APIConverter` treats the +/// `ComponentInterface` as a read-only data source for resolving types, while `APIBuilder` +/// actually mutates the `ComponentInterface` to add new definitions. +trait APIConverter<T> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<T>; +} + +/// Convert a list of weedle items into a list of `ComponentInterface` items, +/// by doing a direct item-by-item mapping. +impl<U, T: APIConverter<U>> APIConverter<Vec<U>> for Vec<T> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Vec<U>> { + self.iter().map(|v| v.convert(ci)).collect::<Result<_>>() + } +} + +fn convert_type(s: &uniffi_meta::Type) -> Type { + use uniffi_meta::Type as Ty; + + match s { + Ty::U8 => Type::UInt8, + Ty::U16 => Type::UInt16, + Ty::U32 => Type::UInt32, + Ty::U64 => Type::UInt64, + Ty::I8 => Type::Int8, + Ty::I16 => Type::Int16, + Ty::I32 => Type::Int32, + Ty::I64 => Type::Int64, + Ty::F32 => Type::Float32, + Ty::F64 => Type::Float64, + Ty::Bool => Type::Boolean, + Ty::String => Type::String, + Ty::Option { inner_type } => Type::Optional(convert_type(inner_type).into()), + Ty::Vec { inner_type } => Type::Sequence(convert_type(inner_type).into()), + Ty::HashMap { + key_type, + value_type, + } => Type::Map( + convert_type(key_type).into(), + convert_type(value_type).into(), + ), + Ty::ArcObject { object_name } => Type::Object(object_name.clone()), + Ty::Unresolved { name } => Type::Unresolved { name: name.clone() }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + // Note that much of the functionality of `ComponentInterface` is tested via its interactions + // with specific member types, in the sub-modules defining those member types. + + const UDL1: &str = r#" + namespace foobar{}; + enum Test { + "test_me", + }; + "#; + + const UDL2: &str = r#" + namespace hello { + u64 world(); + }; + dictionary Test { + boolean me; + }; + "#; + + #[test] + fn test_checksum_always_matches_for_same_webidl() { + for udl in &[UDL1, UDL2] { + let ci1 = ComponentInterface::from_webidl(udl).unwrap(); + let ci2 = ComponentInterface::from_webidl(udl).unwrap(); + assert_eq!(ci1.checksum(), ci2.checksum()); + } + } + + #[test] + fn test_checksum_differs_for_different_webidl() { + // There is a small probability of this test spuriously failing due to hash collision. + // If it happens often enough to be a problem, probably this whole "checksum" thing + // is not working out as intended. + let ci1 = ComponentInterface::from_webidl(UDL1).unwrap(); + let ci2 = ComponentInterface::from_webidl(UDL2).unwrap(); + assert_ne!(ci1.checksum(), ci2.checksum()); + } + + #[test] + fn test_checksum_differs_for_different_uniffi_version() { + // There is a small probability of this test spuriously failing due to hash collision. + // If it happens often enough to be a problem, probably this whole "checksum" thing + // is not working out as intended. + for udl in &[UDL1, UDL2] { + let ci1 = ComponentInterface::from_webidl(udl).unwrap(); + let mut ci2 = ComponentInterface::from_webidl(udl).unwrap(); + ci2.uniffi_version = "99.99"; + assert_ne!(ci1.checksum(), ci2.checksum()); + } + } + + #[test] + fn test_duplicate_type_names_are_an_error() { + const UDL: &str = r#" + namespace test{}; + interface Testing { + constructor(); + }; + dictionary Testing { + u32 field; + }; + "#; + let err = ComponentInterface::from_webidl(UDL).unwrap_err(); + assert_eq!( + err.to_string(), + "Conflicting type definition for `Testing`! \ + existing definition: Object(\"Testing\"), \ + new definition: Record(\"Testing\")" + ); + + const UDL2: &str = r#" + namespace test{}; + enum Testing { + "one", "two" + }; + [Error] + enum Testing { "three", "four" }; + "#; + let err = ComponentInterface::from_webidl(UDL2).unwrap_err(); + assert_eq!( + err.to_string(), + "Conflicting type definition for `Testing`! \ + existing definition: Enum(\"Testing\"), \ + new definition: Error(\"Testing\")" + ); + + const UDL3: &str = r#" + namespace test{ + u32 Testing(); + }; + enum Testing { + "one", "two" + }; + "#; + let err = ComponentInterface::from_webidl(UDL3).unwrap_err(); + assert_eq!( + err.to_string(), + "Conflicting type definition for \"Testing\"" + ); + } + + #[test] + fn test_enum_variant_names_dont_shadow_types() { + // There are some edge-cases during codegen where we don't know how to disambiguate + // between an enum variant reference and a top-level type reference, so we + // disallow it in order to give a more scrutable error to the consumer. + const UDL: &str = r#" + namespace test{}; + interface Testing { + constructor(); + }; + [Enum] + interface HardToCodegenFor { + Testing(); + OtherVariant(u32 field); + }; + "#; + let err = ComponentInterface::from_webidl(UDL).unwrap_err(); + assert_eq!( + err.to_string(), + "Enum variant names must not shadow type names: \"Testing\"" + ); + } + + #[test] + fn test_contains_optional_types() { + let mut ci = ComponentInterface { + ..Default::default() + }; + + // check that `contains_optional_types` returns false when there is no Optional type in the interface + assert!(!ci.contains_optional_types()); + + // check that `contains_optional_types` returns true when there is an Optional type in the interface + assert!(ci + .types + .add_type_definition("TestOptional{}", Type::Optional(Box::new(Type::String))) + .is_ok()); + assert!(ci.contains_optional_types()); + } + + #[test] + fn test_contains_sequence_types() { + let mut ci = ComponentInterface { + ..Default::default() + }; + + // check that `contains_sequence_types` returns false when there is no Sequence type in the interface + assert!(!ci.contains_sequence_types()); + + // check that `contains_sequence_types` returns true when there is a Sequence type in the interface + assert!(ci + .types + .add_type_definition("TestSequence{}", Type::Sequence(Box::new(Type::UInt64))) + .is_ok()); + assert!(ci.contains_sequence_types()); + } + + #[test] + fn test_contains_map_types() { + let mut ci = ComponentInterface { + ..Default::default() + }; + + // check that `contains_map_types` returns false when there is no Map type in the interface + assert!(!ci.contains_map_types()); + + // check that `contains_map_types` returns true when there is a Map type in the interface + assert!(ci + .types + .add_type_definition( + "Map{}", + Type::Map(Box::new(Type::String), Box::new(Type::Boolean)) + ) + .is_ok()); + assert!(ci.contains_map_types()); + } + + #[test] + fn test_no_infinite_recursion_when_walking_types() { + const UDL: &str = r#" + namespace test{}; + interface Testing { + void tester(Testing foo); + }; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert!(!ci.item_contains_unsigned_types(&Type::Object("Testing".into()))); + } + + #[test] + fn test_correct_recursion_when_walking_types() { + const UDL: &str = r#" + namespace test{}; + interface TestObj { + void tester(TestRecord foo); + }; + dictionary TestRecord { + NestedRecord bar; + }; + dictionary NestedRecord { + u64 baz; + }; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert!(ci.item_contains_unsigned_types(&Type::Object("TestObj".into()))); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/interface/namespace.rs b/third_party/rust/uniffi_bindgen/src/interface/namespace.rs new file mode 100644 index 0000000000..4a57d0ff41 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/interface/namespace.rs @@ -0,0 +1,132 @@ +/* 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/. */ + +//! # Namespace definition for a `ComponentInterface`. +//! +//! This module converts a namespace definition from UDL into structures that +//! can be added to a `ComponentInterface`. +//! +//! In WebIDL proper, each `namespace` declares a set of functions and attributes that +//! are exposed as a global object of that name, and there can be any number of such +//! namespace definitions. +//! +//! For our purposes with UDL, we expect just a single `namespace` declaration, which +//! defines properties of the component as a whole (currently just the name). It also +//! contains the functions that will be exposed as individual plain functions exported by +//! the component, if any. So something like this: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! namespace example { +//! string hello(); +//! }; +//! # "##)?; +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! Declares a component named "example" with a single exported function named "hello": +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example { +//! # string hello(); +//! # }; +//! # "##)?; +//! assert_eq!(ci.namespace(), "example"); +//! assert_eq!(ci.get_function_definition("hello").unwrap().name(), "hello"); +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! While this awkward-looking syntax: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! namespace example {}; +//! # "##)?; +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! Declares a component named "example" with no exported functions: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! # "##)?; +//! assert_eq!(ci.namespace(), "example"); +//! assert_eq!(ci.function_definitions().len(), 0); +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! Yeah, it's a bit of an awkward fit syntactically, but it's enough +//! to get us up and running for a first version of this tool. + +use anyhow::{bail, Result}; + +use super::{APIBuilder, APIConverter, ComponentInterface}; + +/// A namespace is currently just a name, but might hold more metadata about +/// the component in future. +/// +#[derive(Debug, Clone, Hash)] +pub struct Namespace { + pub(super) name: String, +} + +impl APIBuilder for weedle::NamespaceDefinition<'_> { + fn process(&self, ci: &mut ComponentInterface) -> Result<()> { + if self.attributes.is_some() { + bail!("namespace attributes are not supported yet"); + } + ci.add_namespace_definition(Namespace { + name: self.identifier.0.to_string(), + })?; + for func in self.members.body.convert(ci)? { + ci.add_function_definition(func)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_empty_namespace() { + const UDL: &str = r#" + namespace foobar{}; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert_eq!(ci.namespace(), "foobar"); + } + + #[test] + fn test_namespace_with_functions() { + const UDL: &str = r#" + namespace foobar{ + boolean hello(); + void world(); + }; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert_eq!(ci.namespace(), "foobar"); + assert_eq!(ci.function_definitions().len(), 2); + assert!(ci.get_function_definition("hello").is_some()); + assert!(ci.get_function_definition("world").is_some()); + assert!(ci.get_function_definition("potato").is_none()); + } + + #[test] + fn test_rejects_duplicate_namespaces() { + const UDL: &str = r#" + namespace foobar{ + boolean hello(); + void world(); + }; + namespace something_else{}; + "#; + let err = ComponentInterface::from_webidl(UDL).unwrap_err(); + assert_eq!(err.to_string(), "duplicate namespace definition"); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/interface/object.rs b/third_party/rust/uniffi_bindgen/src/interface/object.rs new file mode 100644 index 0000000000..e8ba089261 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/interface/object.rs @@ -0,0 +1,599 @@ +/* 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/. */ + +//! # Object definitions for a `ComponentInterface`. +//! +//! This module converts "interface" definitions from UDL into [`Object`] structures +//! that can be added to a `ComponentInterface`, which are the main way we define stateful +//! objects with behaviour for a UniFFI Rust Component. An [`Object`] is an opaque handle +//! to some state on which methods can be invoked. +//! +//! (The terminology mismatch between "interface" and "object" is a historical artifact of +//! this tool prior to committing to WebIDL syntax). +//! +//! A declaration in the UDL like this: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! interface Example { +//! constructor(string? name); +//! string my_name(); +//! }; +//! # "##)?; +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! Will result in an [`Object`] member with one [`Constructor`] and one [`Method`] being added +//! to the resulting [`ComponentInterface`]: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! # interface Example { +//! # constructor(string? name); +//! # string my_name(); +//! # }; +//! # "##)?; +//! let obj = ci.get_object_definition("Example").unwrap(); +//! assert_eq!(obj.name(), "Example"); +//! assert_eq!(obj.constructors().len(), 1); +//! assert_eq!(obj.constructors()[0].arguments()[0].name(), "name"); +//! assert_eq!(obj.methods().len(),1 ); +//! assert_eq!(obj.methods()[0].name(), "my_name"); +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! It's not necessary for all interfaces to have constructors. +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! # interface Example {}; +//! # "##)?; +//! let obj = ci.get_object_definition("Example").unwrap(); +//! assert_eq!(obj.name(), "Example"); +//! assert_eq!(obj.constructors().len(), 0); +//! # Ok::<(), anyhow::Error>(()) +//! ``` + +use std::convert::TryFrom; +use std::{collections::HashSet, iter}; + +use anyhow::{bail, Result}; +use uniffi_meta::Checksum; + +use super::attributes::{Attribute, ConstructorAttributes, InterfaceAttributes, MethodAttributes}; +use super::ffi::{FfiArgument, FfiFunction, FfiType}; +use super::function::Argument; +use super::types::{Type, TypeIterator}; +use super::{convert_type, APIConverter, ComponentInterface}; + +/// An "object" is an opaque type that can be instantiated and passed around by reference, +/// have methods called on it, and so on - basically your classic Object Oriented Programming +/// type of deal, except without elaborate inheritance hierarchies. +/// +/// In UDL these correspond to the `interface` keyword. +/// +/// At the FFI layer, objects are represented by an opaque integer handle and a set of functions +/// a common prefix. The object's constructors are functions that return new objects by handle, +/// and its methods are functions that take a handle as first argument. The foreign language +/// binding code is expected to stitch these functions back together into an appropriate class +/// definition (or that language's equivalent thereof). +/// +/// TODO: +/// - maybe "Class" would be a better name than "Object" here? +#[derive(Debug, Clone, Checksum)] +pub struct Object { + pub(super) name: String, + pub(super) constructors: Vec<Constructor>, + pub(super) methods: Vec<Method>, + // We don't include the FFIFunc in the hash calculation, because: + // - it is entirely determined by the other fields, + // so excluding it is safe. + // - its `name` property includes a checksum derived from the very + // hash value we're trying to calculate here, so excluding it + // avoids a weird circular dependency in the calculation. + #[checksum_ignore] + pub(super) ffi_func_free: FfiFunction, + #[checksum_ignore] + pub(super) uses_deprecated_threadsafe_attribute: bool, +} + +impl Object { + pub(super) fn new(name: String) -> Object { + Object { + name, + constructors: Default::default(), + methods: Default::default(), + ffi_func_free: Default::default(), + uses_deprecated_threadsafe_attribute: false, + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn type_(&self) -> Type { + Type::Object(self.name.clone()) + } + + pub fn constructors(&self) -> Vec<&Constructor> { + self.constructors.iter().collect() + } + + pub fn primary_constructor(&self) -> Option<&Constructor> { + self.constructors + .iter() + .find(|cons| cons.is_primary_constructor()) + } + + pub fn alternate_constructors(&self) -> Vec<&Constructor> { + self.constructors + .iter() + .filter(|cons| !cons.is_primary_constructor()) + .collect() + } + + pub fn methods(&self) -> Vec<&Method> { + self.methods.iter().collect() + } + + pub fn get_method(&self, name: &str) -> Method { + let matches: Vec<_> = self.methods.iter().filter(|m| m.name() == name).collect(); + match matches.len() { + 1 => matches[0].clone(), + n => panic!("{n} methods named {name}"), + } + } + + pub fn ffi_object_free(&self) -> &FfiFunction { + &self.ffi_func_free + } + + pub fn uses_deprecated_threadsafe_attribute(&self) -> bool { + self.uses_deprecated_threadsafe_attribute + } + + pub fn iter_ffi_function_definitions(&self) -> impl Iterator<Item = &FfiFunction> { + iter::once(&self.ffi_func_free) + .chain(self.constructors.iter().map(|f| &f.ffi_func)) + .chain(self.methods.iter().map(|f| &f.ffi_func)) + } + + pub fn derive_ffi_funcs(&mut self, ci_prefix: &str) -> Result<()> { + // The name is already set if the function is defined through a proc-macro invocation + // rather than in UDL. Don't overwrite it in that case. + if self.ffi_func_free.name().is_empty() { + self.ffi_func_free.name = format!("ffi_{ci_prefix}_{}_object_free", self.name); + } + self.ffi_func_free.arguments = vec![FfiArgument { + name: "ptr".to_string(), + type_: FfiType::RustArcPtr(self.name().to_string()), + }]; + self.ffi_func_free.return_type = None; + + for cons in self.constructors.iter_mut() { + cons.derive_ffi_func(ci_prefix, &self.name); + } + for meth in self.methods.iter_mut() { + meth.derive_ffi_func(ci_prefix, &self.name)?; + } + + Ok(()) + } + + pub fn iter_types(&self) -> TypeIterator<'_> { + Box::new( + self.methods + .iter() + .map(Method::iter_types) + .chain(self.constructors.iter().map(Constructor::iter_types)) + .flatten(), + ) + } +} + +impl APIConverter<Object> for weedle::InterfaceDefinition<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Object> { + if self.inheritance.is_some() { + bail!("interface inheritance is not supported"); + } + let mut object = Object::new(self.identifier.0.to_string()); + let attributes = match &self.attributes { + Some(attrs) => InterfaceAttributes::try_from(attrs)?, + None => Default::default(), + }; + object.uses_deprecated_threadsafe_attribute = attributes.threadsafe(); + // Convert each member into a constructor or method, guarding against duplicate names. + let mut member_names = HashSet::new(); + for member in &self.members.body { + match member { + weedle::interface::InterfaceMember::Constructor(t) => { + let cons: Constructor = t.convert(ci)?; + if !member_names.insert(cons.name.clone()) { + bail!("Duplicate interface member name: \"{}\"", cons.name()) + } + object.constructors.push(cons); + } + weedle::interface::InterfaceMember::Operation(t) => { + let mut method: Method = t.convert(ci)?; + if !member_names.insert(method.name.clone()) { + bail!("Duplicate interface member name: \"{}\"", method.name()) + } + method.object_name = object.name.clone(); + object.methods.push(method); + } + _ => bail!("no support for interface member type {:?} yet", member), + } + } + Ok(object) + } +} + +// Represents a constructor for an object type. +// +// In the FFI, this will be a function that returns a pointer to an instance +// of the corresponding object type. +#[derive(Debug, Clone, Checksum)] +pub struct Constructor { + pub(super) name: String, + pub(super) arguments: Vec<Argument>, + // We don't include the FFIFunc in the hash calculation, because: + // - it is entirely determined by the other fields, + // so excluding it is safe. + // - its `name` property includes a checksum derived from the very + // hash value we're trying to calculate here, so excluding it + // avoids a weird circular dependency in the calculation. + #[checksum_ignore] + pub(super) ffi_func: FfiFunction, + pub(super) attributes: ConstructorAttributes, +} + +impl Constructor { + pub fn name(&self) -> &str { + &self.name + } + + pub fn arguments(&self) -> Vec<&Argument> { + self.arguments.iter().collect() + } + + pub fn full_arguments(&self) -> Vec<Argument> { + self.arguments.to_vec() + } + + pub fn ffi_func(&self) -> &FfiFunction { + &self.ffi_func + } + + pub fn throws(&self) -> bool { + self.attributes.get_throws_err().is_some() + } + + pub fn throws_name(&self) -> Option<&str> { + self.attributes.get_throws_err() + } + + pub fn throws_type(&self) -> Option<Type> { + self.attributes + .get_throws_err() + .map(|name| Type::Error(name.to_owned())) + } + + pub fn is_primary_constructor(&self) -> bool { + self.name == "new" + } + + fn derive_ffi_func(&mut self, ci_prefix: &str, obj_name: &str) { + self.ffi_func.name = format!("{ci_prefix}_{obj_name}_{}", self.name); + self.ffi_func.arguments = self.arguments.iter().map(Into::into).collect(); + self.ffi_func.return_type = Some(FfiType::RustArcPtr(obj_name.to_string())); + } + + pub fn iter_types(&self) -> TypeIterator<'_> { + Box::new(self.arguments.iter().flat_map(Argument::iter_types)) + } +} + +impl Default for Constructor { + fn default() -> Self { + Constructor { + name: String::from("new"), + arguments: Vec::new(), + ffi_func: Default::default(), + attributes: Default::default(), + } + } +} + +impl APIConverter<Constructor> for weedle::interface::ConstructorInterfaceMember<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Constructor> { + let attributes = match &self.attributes { + Some(attr) => ConstructorAttributes::try_from(attr)?, + None => Default::default(), + }; + Ok(Constructor { + name: String::from(attributes.get_name().unwrap_or("new")), + arguments: self.args.body.list.convert(ci)?, + ffi_func: Default::default(), + attributes, + }) + } +} + +// Represents an instance method for an object type. +// +// The FFI will represent this as a function whose first/self argument is a +// `FfiType::RustArcPtr` to the instance. +#[derive(Debug, Clone, Checksum)] +pub struct Method { + pub(super) name: String, + pub(super) object_name: String, + pub(super) arguments: Vec<Argument>, + pub(super) return_type: Option<Type>, + // We don't include the FFIFunc in the hash calculation, because: + // - it is entirely determined by the other fields, + // so excluding it is safe. + // - its `name` property includes a checksum derived from the very + // hash value we're trying to calculate here, so excluding it + // avoids a weird circular dependency in the calculation. + #[checksum_ignore] + pub(super) ffi_func: FfiFunction, + pub(super) attributes: MethodAttributes, +} + +impl Method { + pub fn name(&self) -> &str { + &self.name + } + + pub fn arguments(&self) -> Vec<&Argument> { + self.arguments.iter().collect() + } + + // Methods have a special implicit first argument for the object instance, + // hence `arguments` and `full_arguments` are different. + pub fn full_arguments(&self) -> Vec<Argument> { + vec![Argument { + name: "ptr".to_string(), + // TODO: ideally we'd get this via `ci.resolve_type_expression` so that it + // is contained in the proper `TypeUniverse`, but this works for now. + type_: Type::Object(self.object_name.clone()), + by_ref: !self.attributes.get_self_by_arc(), + optional: false, + default: None, + }] + .into_iter() + .chain(self.arguments.iter().cloned()) + .collect() + } + + pub fn return_type(&self) -> Option<&Type> { + self.return_type.as_ref() + } + + pub fn ffi_func(&self) -> &FfiFunction { + &self.ffi_func + } + + pub fn throws(&self) -> bool { + self.attributes.get_throws_err().is_some() + } + + pub fn throws_name(&self) -> Option<&str> { + self.attributes.get_throws_err() + } + + pub fn throws_type(&self) -> Option<Type> { + self.attributes + .get_throws_err() + .map(|name| Type::Error(name.to_owned())) + } + + pub fn takes_self_by_arc(&self) -> bool { + self.attributes.get_self_by_arc() + } + + pub fn derive_ffi_func(&mut self, ci_prefix: &str, obj_prefix: &str) -> Result<()> { + // The name is already set if the function is defined through a proc-macro invocation + // rather than in UDL. Don't overwrite it in that case. + if self.ffi_func.name.is_empty() { + self.ffi_func.name = format!("{ci_prefix}_{obj_prefix}_{}", self.name); + } + + self.ffi_func.arguments = self.full_arguments().iter().map(Into::into).collect(); + self.ffi_func.return_type = self.return_type.as_ref().map(Into::into); + Ok(()) + } + + pub fn iter_types(&self) -> TypeIterator<'_> { + Box::new( + self.arguments + .iter() + .flat_map(Argument::iter_types) + .chain(self.return_type.iter().flat_map(Type::iter_types)), + ) + } +} + +impl From<uniffi_meta::MethodMetadata> for Method { + fn from(meta: uniffi_meta::MethodMetadata) -> Self { + let ffi_name = meta.ffi_symbol_name(); + + let return_type = meta.return_type.map(|out| convert_type(&out)); + let arguments = meta.inputs.into_iter().map(Into::into).collect(); + + let ffi_func = FfiFunction { + name: ffi_name, + ..FfiFunction::default() + }; + + Self { + name: meta.name, + object_name: meta.self_name, + arguments, + return_type, + ffi_func, + attributes: meta.throws.map(Attribute::Throws).into_iter().collect(), + } + } +} + +impl APIConverter<Method> for weedle::interface::OperationInterfaceMember<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Method> { + if self.special.is_some() { + bail!("special operations not supported"); + } + if self.modifier.is_some() { + bail!("method modifiers are not supported") + } + let return_type = ci.resolve_return_type_expression(&self.return_type)?; + Ok(Method { + name: match self.identifier { + None => bail!("anonymous methods are not supported {:?}", self), + Some(id) => { + let name = id.0.to_string(); + if name == "new" { + bail!("the method name \"new\" is reserved for the default constructor"); + } + name + } + }, + // We don't know the name of the containing `Object` at this point, fill it in later. + object_name: Default::default(), + arguments: self.args.body.list.convert(ci)?, + return_type, + ffi_func: Default::default(), + attributes: MethodAttributes::try_from(self.attributes.as_ref())?, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_that_all_argument_and_return_types_become_known() { + const UDL: &str = r#" + namespace test{}; + interface Testing { + constructor(string? name, u16 age); + sequence<u32> code_points_of_name(); + }; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert_eq!(ci.object_definitions().len(), 1); + ci.get_object_definition("Testing").unwrap(); + + assert_eq!(ci.iter_types().count(), 6); + assert!(ci.iter_types().any(|t| t.canonical_name() == "u16")); + assert!(ci.iter_types().any(|t| t.canonical_name() == "u32")); + assert!(ci.iter_types().any(|t| t.canonical_name() == "Sequenceu32")); + assert!(ci.iter_types().any(|t| t.canonical_name() == "string")); + assert!(ci + .iter_types() + .any(|t| t.canonical_name() == "Optionalstring")); + assert!(ci.iter_types().any(|t| t.canonical_name() == "TypeTesting")); + } + + #[test] + fn test_alternate_constructors() { + const UDL: &str = r#" + namespace test{}; + interface Testing { + constructor(); + [Name=new_with_u32] + constructor(u32 v); + }; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert_eq!(ci.object_definitions().len(), 1); + + let obj = ci.get_object_definition("Testing").unwrap(); + assert!(obj.primary_constructor().is_some()); + assert_eq!(obj.alternate_constructors().len(), 1); + assert_eq!(obj.methods().len(), 0); + + let cons = obj.primary_constructor().unwrap(); + assert_eq!(cons.name(), "new"); + assert_eq!(cons.arguments.len(), 0); + assert_eq!(cons.ffi_func.arguments.len(), 0); + + let cons = obj.alternate_constructors()[0]; + assert_eq!(cons.name(), "new_with_u32"); + assert_eq!(cons.arguments.len(), 1); + assert_eq!(cons.ffi_func.arguments.len(), 1); + } + + #[test] + fn test_the_name_new_identifies_the_primary_constructor() { + const UDL: &str = r#" + namespace test{}; + interface Testing { + [Name=newish] + constructor(); + [Name=new] + constructor(u32 v); + }; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert_eq!(ci.object_definitions().len(), 1); + + let obj = ci.get_object_definition("Testing").unwrap(); + assert!(obj.primary_constructor().is_some()); + assert_eq!(obj.alternate_constructors().len(), 1); + assert_eq!(obj.methods().len(), 0); + + let cons = obj.primary_constructor().unwrap(); + assert_eq!(cons.name(), "new"); + assert_eq!(cons.arguments.len(), 1); + + let cons = obj.alternate_constructors()[0]; + assert_eq!(cons.name(), "newish"); + assert_eq!(cons.arguments.len(), 0); + assert_eq!(cons.ffi_func.arguments.len(), 0); + } + + #[test] + fn test_the_name_new_is_reserved_for_constructors() { + const UDL: &str = r#" + namespace test{}; + interface Testing { + constructor(); + void new(u32 v); + }; + "#; + let err = ComponentInterface::from_webidl(UDL).unwrap_err(); + assert_eq!( + err.to_string(), + "the method name \"new\" is reserved for the default constructor" + ); + } + + #[test] + fn test_duplicate_primary_constructors_not_allowed() { + const UDL: &str = r#" + namespace test{}; + interface Testing { + constructor(); + constructor(u32 v); + }; + "#; + let err = ComponentInterface::from_webidl(UDL).unwrap_err(); + assert_eq!(err.to_string(), "Duplicate interface member name: \"new\""); + + const UDL2: &str = r#" + namespace test{}; + interface Testing { + constructor(); + [Name=new] + constructor(u32 v); + }; + "#; + let err = ComponentInterface::from_webidl(UDL2).unwrap_err(); + assert_eq!(err.to_string(), "Duplicate interface member name: \"new\""); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/interface/record.rs b/third_party/rust/uniffi_bindgen/src/interface/record.rs new file mode 100644 index 0000000000..b000510884 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/interface/record.rs @@ -0,0 +1,243 @@ +/* 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/. */ + +//! # Record definitions for a `ComponentInterface`. +//! +//! This module converts "dictionary" definitions from UDL into [`Record`] structures +//! that can be added to a `ComponentInterface`, which are the main way we define structured +//! data types for a UniFFI Rust Component. A [`Record`] has a fixed set of named fields, +//! each of a specific type. +//! +//! (The terminology mismatch between "dictionary" and "record" is a historical artifact +//! due to this tool being loosely inspired by WebAssembly Interface Types, which used +//! the term "record" for this sort of data). +//! +//! A declaration in the UDL like this: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! dictionary Example { +//! string name; +//! u32 value; +//! }; +//! # "##)?; +//! # Ok::<(), anyhow::Error>(()) +//! ``` +//! +//! Will result in a [`Record`] member with two [`Field`]s being added to the resulting +//! [`ComponentInterface`]: +//! +//! ``` +//! # let ci = uniffi_bindgen::interface::ComponentInterface::from_webidl(r##" +//! # namespace example {}; +//! # dictionary Example { +//! # string name; +//! # u32 value; +//! # }; +//! # "##)?; +//! let record = ci.get_record_definition("Example").unwrap(); +//! assert_eq!(record.name(), "Example"); +//! assert_eq!(record.fields()[0].name(), "name"); +//! assert_eq!(record.fields()[1].name(), "value"); +//! # Ok::<(), anyhow::Error>(()) +//! ``` + +use anyhow::{bail, Result}; +use uniffi_meta::Checksum; + +use super::types::{Type, TypeIterator}; +use super::{ + convert_type, + literal::{convert_default_value, Literal}, +}; +use super::{APIConverter, ComponentInterface}; + +/// Represents a "data class" style object, for passing around complex values. +/// +/// In the FFI these are represented as a byte buffer, which one side explicitly +/// serializes the data into and the other serializes it out of. So I guess they're +/// kind of like "pass by clone" values. +#[derive(Debug, Clone, PartialEq, Eq, Checksum)] +pub struct Record { + pub(super) name: String, + pub(super) fields: Vec<Field>, +} + +impl Record { + pub fn name(&self) -> &str { + &self.name + } + + pub fn type_(&self) -> Type { + // *sigh* at the clone here, the relationship between a ComponentInterface + // and its contained types could use a bit of a cleanup. + Type::Record(self.name.clone()) + } + + pub fn fields(&self) -> &[Field] { + &self.fields + } + + pub fn iter_types(&self) -> TypeIterator<'_> { + Box::new(self.fields.iter().flat_map(Field::iter_types)) + } +} + +impl From<uniffi_meta::RecordMetadata> for Record { + fn from(meta: uniffi_meta::RecordMetadata) -> Self { + Self { + name: meta.name, + fields: meta.fields.into_iter().map(Into::into).collect(), + } + } +} + +impl APIConverter<Record> for weedle::DictionaryDefinition<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Record> { + if self.attributes.is_some() { + bail!("dictionary attributes are not supported yet"); + } + if self.inheritance.is_some() { + bail!("dictionary inheritance is not supported"); + } + Ok(Record { + name: self.identifier.0.to_string(), + fields: self.members.body.convert(ci)?, + }) + } +} + +// Represents an individual field on a Record. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Checksum)] +pub struct Field { + pub(super) name: String, + pub(super) type_: Type, + pub(super) default: Option<Literal>, +} + +impl Field { + pub fn name(&self) -> &str { + &self.name + } + + pub fn type_(&self) -> &Type { + &self.type_ + } + + pub fn default_value(&self) -> Option<&Literal> { + self.default.as_ref() + } + + pub fn iter_types(&self) -> TypeIterator<'_> { + self.type_.iter_types() + } +} + +impl From<uniffi_meta::FieldMetadata> for Field { + fn from(meta: uniffi_meta::FieldMetadata) -> Self { + Self { + name: meta.name, + type_: convert_type(&meta.ty), + default: None, + } + } +} + +impl APIConverter<Field> for weedle::dictionary::DictionaryMember<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result<Field> { + if self.attributes.is_some() { + bail!("dictionary member attributes are not supported yet"); + } + let type_ = ci.resolve_type_expression(&self.type_)?; + let default = match self.default { + None => None, + Some(v) => Some(convert_default_value(&v.value, &type_)?), + }; + Ok(Field { + name: self.identifier.0.to_string(), + type_, + default, + }) + } +} + +#[cfg(test)] +mod test { + use super::super::literal::Radix; + use super::*; + + #[test] + fn test_multiple_record_types() { + const UDL: &str = r#" + namespace test{}; + dictionary Empty {}; + dictionary Simple { + u32 field; + }; + dictionary Complex { + string? key; + u32 value = 0; + required boolean spin; + }; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert_eq!(ci.record_definitions().count(), 3); + + let record = ci.get_record_definition("Empty").unwrap(); + assert_eq!(record.name(), "Empty"); + assert_eq!(record.fields().len(), 0); + + let record = ci.get_record_definition("Simple").unwrap(); + assert_eq!(record.name(), "Simple"); + assert_eq!(record.fields().len(), 1); + assert_eq!(record.fields()[0].name(), "field"); + assert_eq!(record.fields()[0].type_().canonical_name(), "u32"); + assert!(record.fields()[0].default_value().is_none()); + + let record = ci.get_record_definition("Complex").unwrap(); + assert_eq!(record.name(), "Complex"); + assert_eq!(record.fields().len(), 3); + assert_eq!(record.fields()[0].name(), "key"); + assert_eq!( + record.fields()[0].type_().canonical_name(), + "Optionalstring" + ); + assert!(record.fields()[0].default_value().is_none()); + assert_eq!(record.fields()[1].name(), "value"); + assert_eq!(record.fields()[1].type_().canonical_name(), "u32"); + assert!(matches!( + record.fields()[1].default_value(), + Some(Literal::UInt(0, Radix::Decimal, Type::UInt32)) + )); + assert_eq!(record.fields()[2].name(), "spin"); + assert_eq!(record.fields()[2].type_().canonical_name(), "bool"); + assert!(record.fields()[2].default_value().is_none()); + } + + #[test] + fn test_that_all_field_types_become_known() { + const UDL: &str = r#" + namespace test{}; + dictionary Testing { + string? maybe_name; + u32 value; + }; + "#; + let ci = ComponentInterface::from_webidl(UDL).unwrap(); + assert_eq!(ci.record_definitions().count(), 1); + let record = ci.get_record_definition("Testing").unwrap(); + assert_eq!(record.fields().len(), 2); + assert_eq!(record.fields()[0].name(), "maybe_name"); + assert_eq!(record.fields()[1].name(), "value"); + + assert_eq!(ci.iter_types().count(), 4); + assert!(ci.iter_types().any(|t| t.canonical_name() == "u32")); + assert!(ci.iter_types().any(|t| t.canonical_name() == "string")); + assert!(ci + .iter_types() + .any(|t| t.canonical_name() == "Optionalstring")); + assert!(ci.iter_types().any(|t| t.canonical_name() == "TypeTesting")); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/interface/types/finder.rs b/third_party/rust/uniffi_bindgen/src/interface/types/finder.rs new file mode 100644 index 0000000000..991f910a4d --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/interface/types/finder.rs @@ -0,0 +1,244 @@ +/* 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/. */ + +//! # Helpers for finding the named types defined in a UDL interface. +//! +//! This module provides the [`TypeFinder`] trait, an abstraction for walking +//! the weedle parse tree, looking for type definitions, and accumulating them +//! in a [`TypeUniverse`]. +//! +//! The type-finding process only discovers very basic information about names +//! and their corresponding types. For example, it can discover that "Foobar" +//! names a Record, but it won't discover anything about the fields of that +//! record. +//! +//! Factoring this functionality out into a separate phase makes the subsequent +//! work of more *detailed* parsing of the UDL a lot simpler, we know how to resolve +//! names to types when building up the full interface definition. + +use std::convert::TryFrom; + +use anyhow::{bail, Result}; + +use super::super::attributes::{EnumAttributes, InterfaceAttributes, TypedefAttributes}; +use super::{Type, TypeUniverse}; + +/// Trait to help with an early "type discovery" phase when processing the UDL. +/// +/// This trait does structural matching against weedle AST nodes from a parsed +/// UDL file, looking for all the newly-defined types in the file and accumulating +/// them in the given `TypeUniverse`. +pub(in super::super) trait TypeFinder { + fn add_type_definitions_to(&self, types: &mut TypeUniverse) -> Result<()>; +} + +impl<T: TypeFinder> TypeFinder for &[T] { + fn add_type_definitions_to(&self, types: &mut TypeUniverse) -> Result<()> { + for item in *self { + item.add_type_definitions_to(types)?; + } + Ok(()) + } +} + +impl TypeFinder for weedle::Definition<'_> { + fn add_type_definitions_to(&self, types: &mut TypeUniverse) -> Result<()> { + match self { + weedle::Definition::Interface(d) => d.add_type_definitions_to(types), + weedle::Definition::Dictionary(d) => d.add_type_definitions_to(types), + weedle::Definition::Enum(d) => d.add_type_definitions_to(types), + weedle::Definition::Typedef(d) => d.add_type_definitions_to(types), + weedle::Definition::CallbackInterface(d) => d.add_type_definitions_to(types), + _ => Ok(()), + } + } +} + +impl TypeFinder for weedle::InterfaceDefinition<'_> { + fn add_type_definitions_to(&self, types: &mut TypeUniverse) -> Result<()> { + let name = self.identifier.0.to_string(); + // Some enum types are defined using an `interface` with a special attribute. + if InterfaceAttributes::try_from(self.attributes.as_ref())?.contains_enum_attr() { + types.add_type_definition(self.identifier.0, Type::Enum(name)) + } else if InterfaceAttributes::try_from(self.attributes.as_ref())?.contains_error_attr() { + types.add_type_definition(self.identifier.0, Type::Error(name)) + } else { + types.add_type_definition(self.identifier.0, Type::Object(name)) + } + } +} + +impl TypeFinder for weedle::DictionaryDefinition<'_> { + fn add_type_definitions_to(&self, types: &mut TypeUniverse) -> Result<()> { + let name = self.identifier.0.to_string(); + types.add_type_definition(self.identifier.0, Type::Record(name)) + } +} + +impl TypeFinder for weedle::EnumDefinition<'_> { + fn add_type_definitions_to(&self, types: &mut TypeUniverse) -> Result<()> { + let name = self.identifier.0.to_string(); + // Our error types are defined using an `enum` with a special attribute. + if EnumAttributes::try_from(self.attributes.as_ref())?.contains_error_attr() { + types.add_type_definition(self.identifier.0, Type::Error(name)) + } else { + types.add_type_definition(self.identifier.0, Type::Enum(name)) + } + } +} + +impl TypeFinder for weedle::TypedefDefinition<'_> { + fn add_type_definitions_to(&self, types: &mut TypeUniverse) -> Result<()> { + let name = self.identifier.0; + let attrs = TypedefAttributes::try_from(self.attributes.as_ref())?; + // If we wanted simple `typedef`s, it would be as easy as: + // > let t = types.resolve_type_expression(&self.type_)?; + // > types.add_type_definition(name, t) + // But we don't - `typedef`s are reserved for external types. + if attrs.is_custom() { + // A local type which wraps a builtin and for which we will generate an + // `FfiConverter` implementation. + let builtin = types.resolve_type_expression(&self.type_)?; + types.add_type_definition( + name, + Type::Custom { + name: name.to_string(), + builtin: builtin.into(), + }, + ) + } else { + // A crate which can supply an `FfiConverter`. + // We don't reference `self._type`, so ideally we could insist on it being + // the literal 'extern' but that's tricky + types.add_type_definition( + name, + Type::External { + name: name.to_string(), + crate_name: attrs.get_crate_name(), + }, + ) + } + } +} + +impl TypeFinder for weedle::CallbackInterfaceDefinition<'_> { + fn add_type_definitions_to(&self, types: &mut TypeUniverse) -> Result<()> { + if self.attributes.is_some() { + bail!("no typedef attributes are currently supported"); + } + let name = self.identifier.0.to_string(); + types.add_type_definition(self.identifier.0, Type::CallbackInterface(name)) + } +} + +#[cfg(test)] +mod test { + use super::*; + + // A helper to take valid UDL and a closure to check what's in it. + fn test_a_finding<F>(udl: &str, tester: F) + where + F: FnOnce(TypeUniverse), + { + let idl = weedle::parse(udl).unwrap(); + let mut types = TypeUniverse::default(); + types.add_type_definitions_from(idl.as_ref()).unwrap(); + tester(types); + } + + #[test] + fn test_type_finding() { + test_a_finding( + r#" + callback interface TestCallbacks { + string hello(u32 count); + }; + "#, + |types| { + assert!( + matches!(types.get_type_definition("TestCallbacks").unwrap(), Type::CallbackInterface(nm) if nm == "TestCallbacks") + ); + }, + ); + + test_a_finding( + r#" + dictionary TestRecord { + u32 field; + }; + "#, + |types| { + assert!( + matches!(types.get_type_definition("TestRecord").unwrap(), Type::Record(nm) if nm == "TestRecord") + ); + }, + ); + + test_a_finding( + r#" + enum TestItems { "one", "two" }; + + [Error] + enum TestError { "ErrorOne", "ErrorTwo" }; + "#, + |types| { + assert!( + matches!(types.get_type_definition("TestItems").unwrap(), Type::Enum(nm) if nm == "TestItems") + ); + assert!( + matches!(types.get_type_definition("TestError").unwrap(), Type::Error(nm) if nm == "TestError") + ); + }, + ); + + test_a_finding( + r#" + interface TestObject { + constructor(); + }; + "#, + |types| { + assert!( + matches!(types.get_type_definition("TestObject").unwrap(), Type::Object(nm) if nm == "TestObject") + ); + }, + ); + + test_a_finding( + r#" + [External="crate-name"] + typedef extern ExternalType; + + [Custom] + typedef string CustomType; + "#, + |types| { + assert!( + matches!(types.get_type_definition("ExternalType").unwrap(), Type::External { name, crate_name } + if name == "ExternalType" && crate_name == "crate-name") + ); + assert!( + matches!(types.get_type_definition("CustomType").unwrap(), Type::Custom { name, builtin } + if name == "CustomType" && builtin == Box::new(Type::String)) + ); + }, + ); + } + + fn get_err(udl: &str) -> String { + let parsed = weedle::parse(udl).unwrap(); + let mut types = TypeUniverse::default(); + let err = types + .add_type_definitions_from(parsed.as_ref()) + .unwrap_err(); + err.to_string() + } + + #[test] + #[should_panic] + fn test_typedef_error_on_no_attr() { + // Sorry, still working out what we want for non-imported typedefs.. + get_err("typedef string Custom;"); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/interface/types/mod.rs b/third_party/rust/uniffi_bindgen/src/interface/types/mod.rs new file mode 100644 index 0000000000..c31c9b3ebd --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/interface/types/mod.rs @@ -0,0 +1,342 @@ +/* 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/. */ + +//! # Basic typesystem for defining a component interface. +//! +//! This module provides the "API-level" typesystem of a UniFFI Rust Component, that is, +//! the types provided by the Rust implementation and consumed callers of the foreign language +//! bindings. Think "objects" and "enums" and "records". +//! +//! The [`Type`] enum represents high-level types that would appear in the public API of +//! a component, such as enums and records as well as primitives like ints and strings. +//! The Rust code that implements a component, and the foreign language bindings that consume it, +//! will both typically deal with such types as their core concern. +//! +//! The set of all [`Type`]s used in a component interface is represented by a `TypeUniverse`, +//! which can be used by the bindings generator code to determine what type-related helper +//! functions to emit for a given component. +//! +//! As a developer working on UniFFI itself, you're likely to spend a fair bit of time thinking +//! about how these API-level types map into the lower-level types of the FFI layer as represented +//! by the [`ffi::FfiType`](super::ffi::FfiType) enum, but that's a detail that is invisible to end users. + +use std::{collections::hash_map::Entry, collections::BTreeSet, collections::HashMap, iter}; + +use anyhow::{bail, Result}; +use heck::ToUpperCamelCase; +use uniffi_meta::Checksum; + +use super::ffi::FfiType; + +mod finder; +pub(super) use finder::TypeFinder; +mod resolver; +pub(super) use resolver::{resolve_builtin_type, TypeResolver}; + +/// Represents all the different high-level types that can be used in a component interface. +/// At this level we identify user-defined types by name, without knowing any details +/// of their internal structure apart from what type of thing they are (record, enum, etc). +#[derive(Debug, Clone, Eq, PartialEq, Checksum, Ord, PartialOrd)] +pub enum Type { + // Primitive types. + UInt8, + Int8, + UInt16, + Int16, + UInt32, + Int32, + UInt64, + Int64, + Float32, + Float64, + Boolean, + String, + Timestamp, + Duration, + // Types defined in the component API, each of which has a string name. + Object(String), + Record(String), + Enum(String), + Error(String), + CallbackInterface(String), + // Structurally recursive types. + Optional(Box<Type>), + Sequence(Box<Type>), + Map(Box<Type>, Box<Type>), + // An FfiConverter we `use` from an external crate + External { name: String, crate_name: String }, + // Custom type on the scaffolding side + Custom { name: String, builtin: Box<Type> }, + // An unresolved user-defined type inside a proc-macro exported function + // signature. Must be replaced by another type before bindings generation. + Unresolved { name: String }, +} + +impl Type { + /// Get the canonical, unique-within-this-component name for a type. + /// + /// When generating helper code for foreign language bindings, it's sometimes useful to be + /// able to name a particular type in order to e.g. call a helper function that is specific + /// to that type. We support this by defining a naming convention where each type gets a + /// unique canonical name, constructed recursively from the names of its component types (if any). + pub fn canonical_name(&self) -> String { + match self { + // Builtin primitive types, with plain old names. + Type::Int8 => "i8".into(), + Type::UInt8 => "u8".into(), + Type::Int16 => "i16".into(), + Type::UInt16 => "u16".into(), + Type::Int32 => "i32".into(), + Type::UInt32 => "u32".into(), + Type::Int64 => "i64".into(), + Type::UInt64 => "u64".into(), + Type::Float32 => "f32".into(), + Type::Float64 => "f64".into(), + Type::String => "string".into(), + Type::Boolean => "bool".into(), + // API defined types. + // Note that these all get unique names, and the parser ensures that the names do not + // conflict with a builtin type. We add a prefix to the name to guard against pathological + // cases like a record named `SequenceRecord` interfering with `sequence<Record>`. + // However, types that support importing all end up with the same prefix of "Type", so + // that the import handling code knows how to find the remote reference. + Type::Object(nm) => format!("Type{nm}"), + Type::Error(nm) => format!("Type{nm}"), + Type::Enum(nm) => format!("Type{nm}"), + Type::Record(nm) => format!("Type{nm}"), + Type::CallbackInterface(nm) => format!("CallbackInterface{nm}"), + Type::Timestamp => "Timestamp".into(), + Type::Duration => "Duration".into(), + // Recursive types. + // These add a prefix to the name of the underlying type. + // The component API definition cannot give names to recursive types, so as long as the + // prefixes we add here are all unique amongst themselves, then we have no chance of + // acccidentally generating name collisions. + Type::Optional(t) => format!("Optional{}", t.canonical_name()), + Type::Sequence(t) => format!("Sequence{}", t.canonical_name()), + Type::Map(k, v) => format!( + "Map{}{}", + k.canonical_name().to_upper_camel_case(), + v.canonical_name().to_upper_camel_case() + ), + // A type that exists externally. + Type::External { name, .. } | Type::Custom { name, .. } => format!("Type{name}"), + Type::Unresolved { name } => { + unreachable!("Type `{name}` must be resolved before calling canonical_name") + } + } + } + + pub fn ffi_type(&self) -> FfiType { + self.into() + } + + pub fn iter_types(&self) -> TypeIterator<'_> { + let nested_types = match self { + Type::Optional(t) | Type::Sequence(t) => t.iter_types(), + Type::Map(k, v) => Box::new(k.iter_types().chain(v.iter_types())), + _ => Box::new(iter::empty()), + }; + Box::new(std::iter::once(self).chain(nested_types)) + } +} + +/// When passing data across the FFI, each `Type` value will be lowered into a corresponding +/// `FfiType` value. This conversion tells you which one. +/// +/// Note that the conversion is one-way - given an FfiType, it is not in general possible to +/// tell what the corresponding Type is that it's being used to represent. +impl From<&Type> for FfiType { + fn from(t: &Type) -> FfiType { + match t { + // Types that are the same map to themselves, naturally. + Type::UInt8 => FfiType::UInt8, + Type::Int8 => FfiType::Int8, + Type::UInt16 => FfiType::UInt16, + Type::Int16 => FfiType::Int16, + Type::UInt32 => FfiType::UInt32, + Type::Int32 => FfiType::Int32, + Type::UInt64 => FfiType::UInt64, + Type::Int64 => FfiType::Int64, + Type::Float32 => FfiType::Float32, + Type::Float64 => FfiType::Float64, + // Booleans lower into an Int8, to work around a bug in JNA. + Type::Boolean => FfiType::Int8, + // Strings are always owned rust values. + // We might add a separate type for borrowed strings in future. + Type::String => FfiType::RustBuffer(None), + // Objects are pointers to an Arc<> + Type::Object(name) => FfiType::RustArcPtr(name.to_owned()), + // Callback interfaces are passed as opaque integer handles. + Type::CallbackInterface(_) => FfiType::UInt64, + // Other types are serialized into a bytebuffer and deserialized on the other side. + Type::Enum(_) + | Type::Error(_) + | Type::Record(_) + | Type::Optional(_) + | Type::Sequence(_) + | Type::Map(_, _) + | Type::Timestamp + | Type::Duration => FfiType::RustBuffer(None), + Type::External { name, .. } => FfiType::RustBuffer(Some(name.clone())), + Type::Custom { builtin, .. } => FfiType::from(builtin.as_ref()), + Type::Unresolved { name } => { + unreachable!("Type `{name}` must be resolved before lowering to FfiType") + } + } + } +} + +// Needed for rust scaffolding askama template +impl From<&&Type> for FfiType { + fn from(ty: &&Type) -> Self { + (*ty).into() + } +} + +/// The set of all possible types used in a particular component interface. +/// +/// Every component API uses a finite number of types, including primitive types, API-defined +/// types like records and enums, and recursive types such as sequences of the above. Our +/// component API doesn't support fancy generics so this is a finitely-enumerable set, which +/// is useful to be able to operate on explicitly. +/// +/// You could imagine this struct doing some clever interning of names and so-on in future, +/// to reduce the overhead of passing around [Type] instances. For now we just do a whole +/// lot of cloning. +#[derive(Debug, Default)] +pub(crate) struct TypeUniverse { + // Named type definitions (including aliases). + type_definitions: HashMap<String, Type>, + // All the types in the universe, by canonical type name, in a well-defined order. + all_known_types: BTreeSet<Type>, +} + +impl TypeUniverse { + /// Add the definitions of all named [Type]s from a given WebIDL definition. + /// + /// This will fail if you try to add a name for which an existing type definition exists. + pub(super) fn add_type_definitions_from<T: TypeFinder>(&mut self, defn: T) -> Result<()> { + defn.add_type_definitions_to(self) + } + + /// Add the definition of a named [Type]. + /// + /// This will fail if you try to add a name for which an existing type definition exists. + pub fn add_type_definition(&mut self, name: &str, type_: Type) -> Result<()> { + if let Type::Unresolved { name: name_ } = &type_ { + assert_eq!(name, name_); + bail!("attempted to add type definition of Unresolved for `{name}`"); + } + + if resolve_builtin_type(name).is_some() { + bail!( + "please don't shadow builtin types ({name}, {})", + type_.canonical_name(), + ); + } + self.add_known_type(&type_)?; + match self.type_definitions.entry(name.to_string()) { + Entry::Occupied(o) => { + let existing_def = o.get(); + if type_ == *existing_def + && matches!(type_, Type::Record(_) | Type::Enum(_) | Type::Error(_)) + { + // UDL and proc-macro metadata are allowed to define the same record, enum and + // error types, if the definitions match (fields and variants are checked in + // add_{record,enum,error}_definition) + Ok(()) + } else { + bail!( + "Conflicting type definition for `{name}`! \ + existing definition: {existing_def:?}, \ + new definition: {type_:?}" + ); + } + } + Entry::Vacant(e) => { + e.insert(type_); + Ok(()) + } + } + } + + /// Get the [Type] corresponding to a given name, if any. + pub(super) fn get_type_definition(&self, name: &str) -> Option<Type> { + self.type_definitions.get(name).cloned() + } + + /// Get the [Type] corresponding to a given WebIDL type node. + /// + /// If the node is a structural type (e.g. a sequence) then this will also add + /// it to the set of all types seen in the component interface. + pub(crate) fn resolve_type_expression<T: TypeResolver>(&mut self, expr: T) -> Result<Type> { + expr.resolve_type_expression(self) + } + + /// Add a [Type] to the set of all types seen in the component interface. + pub fn add_known_type(&mut self, type_: &Type) -> Result<()> { + // Adding potentially-unresolved types is a footgun, make sure we don't do that. + if matches!(type_, Type::Unresolved { .. }) { + bail!("Unresolved types must be resolved before being added to known types"); + } + + // Types are more likely to already be known than not, so avoid unnecessary cloning. + if !self.all_known_types.contains(type_) { + self.all_known_types.insert(type_.to_owned()); + + // Add inner types. For UDL, this is actually pointless extra work (as is calling + // add_known_type from add_function_definition), but for the proc-macro frontend + // this is important if the inner type isn't ever mentioned outside one of these + // generic builtin types. + match type_ { + Type::Optional(t) => self.add_known_type(t)?, + Type::Sequence(t) => self.add_known_type(t)?, + Type::Map(k, v) => { + self.add_known_type(k)?; + self.add_known_type(v)?; + } + _ => {} + } + } + + Ok(()) + } + + /// Iterator over all the known types in this universe. + pub fn iter_known_types(&self) -> impl Iterator<Item = &Type> { + self.all_known_types.iter() + } +} + +/// An abstract type for an iterator over &Type references. +/// +/// Ideally we would not need to name this type explicitly, and could just +/// use an `impl Iterator<Item = &Type>` on any method that yields types. +pub type TypeIterator<'a> = Box<dyn Iterator<Item = &'a Type> + 'a>; + +#[cfg(test)] +mod test_type { + use super::*; + + #[test] + fn test_canonical_names() { + // Non-exhaustive, but gives a bit of a flavour of what we want. + assert_eq!(Type::UInt8.canonical_name(), "u8"); + assert_eq!(Type::String.canonical_name(), "string"); + assert_eq!( + Type::Optional(Box::new(Type::Sequence(Box::new(Type::Object( + "Example".into() + ))))) + .canonical_name(), + "OptionalSequenceTypeExample" + ); + } +} + +#[cfg(test)] +mod test_type_universe { + // All the useful functionality of the `TypeUniverse` struct + // is tested as part of the `TypeFinder` and `TypeResolver` test suites. +} diff --git a/third_party/rust/uniffi_bindgen/src/interface/types/resolver.rs b/third_party/rust/uniffi_bindgen/src/interface/types/resolver.rs new file mode 100644 index 0000000000..35ae640254 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/interface/types/resolver.rs @@ -0,0 +1,367 @@ +/* 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/. */ + +//! # Helpers for resolving UDL type expressions into concrete types. +//! +//! This module provides the [`TypeResolver`] trait, an abstraction for walking +//! the parse tree of a weedle type expression and using a [`TypeUniverse`] to +//! convert it into a concrete type definition (so it assumes that you're already +//! used a [`TypeFinder`](super::TypeFinder) to populate the universe). +//! +//! Perhaps most importantly, it knows how to error out if the UDL tries to reference +//! an undefined or invalid type. + +use anyhow::{bail, Result}; + +use super::{Type, TypeUniverse}; + +/// Trait to help resolving an UDL type node to a [`Type`]. +/// +/// This trait does structural matching against type-related weedle AST nodes from +/// a parsed UDL file, turning them into a corresponding [`Type`] struct. It uses the +/// known type definitions in a [`TypeUniverse`] to resolve names to types. +/// +/// As a side-effect, resolving a type expression will grow the type universe with +/// references to the types seen during traversal. For example resolving the type +/// expression `sequence<<TestRecord>?` will: +/// +/// * add `Optional<Sequence<TestRecord>` and `Sequence<TestRecord>` to the +/// known types in the universe. +/// * error out if the type name `TestRecord` is not already known. +/// +pub(crate) trait TypeResolver { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type>; +} + +impl TypeResolver for &weedle::types::Type<'_> { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type> { + (*self).resolve_type_expression(types) + } +} + +impl TypeResolver for weedle::types::Type<'_> { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type> { + match self { + weedle::types::Type::Single(t) => match t { + weedle::types::SingleType::Any(_) => bail!("no support for `any` types"), + weedle::types::SingleType::NonAny(t) => t.resolve_type_expression(types), + }, + weedle::types::Type::Union(_) => bail!("no support for union types yet"), + } + } +} + +impl TypeResolver for weedle::types::NonAnyType<'_> { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type> { + match self { + weedle::types::NonAnyType::Boolean(t) => t.resolve_type_expression(types), + weedle::types::NonAnyType::Identifier(t) => t.resolve_type_expression(types), + weedle::types::NonAnyType::Integer(t) => t.resolve_type_expression(types), + weedle::types::NonAnyType::FloatingPoint(t) => t.resolve_type_expression(types), + weedle::types::NonAnyType::Sequence(t) => t.resolve_type_expression(types), + weedle::types::NonAnyType::RecordType(t) => t.resolve_type_expression(types), + _ => bail!("no support for type {:?}", self), + } + } +} + +impl TypeResolver for &weedle::types::AttributedNonAnyType<'_> { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type> { + if self.attributes.is_some() { + bail!("type attributes are not supported yet"); + } + self.type_.resolve_type_expression(types) + } +} + +impl TypeResolver for &weedle::types::AttributedType<'_> { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type> { + if self.attributes.is_some() { + bail!("type attributes are not supported yet"); + } + self.type_.resolve_type_expression(types) + } +} + +impl<T: TypeResolver> TypeResolver for weedle::types::MayBeNull<T> { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type> { + let type_ = self.type_.resolve_type_expression(types)?; + match self.q_mark { + None => Ok(type_), + Some(_) => { + let ty = Type::Optional(Box::new(type_)); + types.add_known_type(&ty)?; + Ok(ty) + } + } + } +} + +impl TypeResolver for weedle::types::IntegerType { + fn resolve_type_expression(&self, _types: &mut TypeUniverse) -> Result<Type> { + bail!( + "WebIDL integer types not implemented ({:?}); consider using u8, u16, u32 or u64", + self + ) + } +} + +impl TypeResolver for weedle::types::FloatingPointType { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type> { + match self { + weedle::types::FloatingPointType::Float(t) => t.resolve_type_expression(types), + weedle::types::FloatingPointType::Double(t) => t.resolve_type_expression(types), + } + } +} + +impl TypeResolver for weedle::types::SequenceType<'_> { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type> { + let t = self.generics.body.as_ref().resolve_type_expression(types)?; + let ty = Type::Sequence(Box::new(t)); + types.add_known_type(&ty)?; + Ok(ty) + } +} + +impl TypeResolver for weedle::types::RecordKeyType<'_> { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type> { + use weedle::types::RecordKeyType::*; + match self { + Byte(_) | USV(_) => bail!( + "WebIDL Byte or USV string type not implemented ({self:?}); \ + consider using DOMString or string", + ), + DOM(_) => { + types.add_known_type(&Type::String)?; + Ok(Type::String) + } + NonAny(t) => t.resolve_type_expression(types), + } + } +} + +impl TypeResolver for weedle::types::RecordType<'_> { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type> { + let key_type = self.generics.body.0.resolve_type_expression(types)?; + let value_type = self.generics.body.2.resolve_type_expression(types)?; + let map = Type::Map(Box::new(key_type), Box::new(value_type)); + types.add_known_type(&map)?; + Ok(map) + } +} + +impl TypeResolver for weedle::common::Identifier<'_> { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type> { + match resolve_builtin_type(self.0) { + Some(type_) => { + types.add_known_type(&type_)?; + Ok(type_) + } + None => match types.get_type_definition(self.0) { + Some(type_) => { + types.add_known_type(&type_)?; + Ok(type_) + } + None => bail!("unknown type reference: {}", self.0), + }, + } + } +} + +impl TypeResolver for weedle::term::Boolean { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type> { + types.add_known_type(&Type::Boolean)?; + Ok(Type::Boolean) + } +} + +impl TypeResolver for weedle::types::FloatType { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type> { + if self.unrestricted.is_some() { + bail!("we don't support `unrestricted float`"); + } + types.add_known_type(&Type::Float32)?; + Ok(Type::Float32) + } +} + +impl TypeResolver for weedle::types::DoubleType { + fn resolve_type_expression(&self, types: &mut TypeUniverse) -> Result<Type> { + if self.unrestricted.is_some() { + bail!("we don't support `unrestricted double`"); + } + types.add_known_type(&Type::Float64)?; + Ok(Type::Float64) + } +} + +/// Resolve built-in API types by name. +/// +/// Given an identifier from the UDL, this will return `Some(Type)` if it names one of the +/// built-in primitive types or `None` if it names something else. +pub(in super::super) fn resolve_builtin_type(name: &str) -> Option<Type> { + match name { + "string" => Some(Type::String), + "u8" => Some(Type::UInt8), + "i8" => Some(Type::Int8), + "u16" => Some(Type::UInt16), + "i16" => Some(Type::Int16), + "u32" => Some(Type::UInt32), + "i32" => Some(Type::Int32), + "u64" => Some(Type::UInt64), + "i64" => Some(Type::Int64), + "f32" => Some(Type::Float32), + "f64" => Some(Type::Float64), + "timestamp" => Some(Type::Timestamp), + "duration" => Some(Type::Duration), + _ => None, + } +} + +#[cfg(test)] +mod test { + use super::*; + use weedle::Parse; + + #[test] + fn test_named_type_resolution() -> Result<()> { + let mut types = TypeUniverse::default(); + types.add_type_definition("TestRecord", Type::Record("TestRecord".into()))?; + assert_eq!(types.iter_known_types().count(), 1); + + let (_, expr) = weedle::types::Type::parse("TestRecord").unwrap(); + let t = types.resolve_type_expression(expr).unwrap(); + assert!(matches!(t, Type::Record(nm) if nm == "TestRecord")); + assert_eq!(types.iter_known_types().count(), 1); + + let (_, expr) = weedle::types::Type::parse("TestRecord?").unwrap(); + let t = types.resolve_type_expression(expr).unwrap(); + assert!(matches!(t, Type::Optional(_))); + // Matching the Box<T> is hard, use names as a convenient workaround. + assert_eq!(t.canonical_name(), "OptionalTypeTestRecord"); + assert_eq!(types.iter_known_types().count(), 2); + + Ok(()) + } + + #[test] + fn test_resolving_optional_type_adds_inner_type() { + let mut types = TypeUniverse::default(); + assert_eq!(types.iter_known_types().count(), 0); + let (_, expr) = weedle::types::Type::parse("u32?").unwrap(); + let t = types.resolve_type_expression(expr).unwrap(); + assert_eq!(t.canonical_name(), "Optionalu32"); + assert_eq!(types.iter_known_types().count(), 2); + assert!(types + .iter_known_types() + .any(|t| t.canonical_name() == "u32")); + assert!(types + .iter_known_types() + .any(|t| t.canonical_name() == "Optionalu32")); + } + + #[test] + fn test_resolving_sequence_type_adds_inner_type() { + let mut types = TypeUniverse::default(); + assert_eq!(types.iter_known_types().count(), 0); + let (_, expr) = weedle::types::Type::parse("sequence<string>").unwrap(); + let t = types.resolve_type_expression(expr).unwrap(); + assert_eq!(t.canonical_name(), "Sequencestring"); + assert_eq!(types.iter_known_types().count(), 2); + assert!(types + .iter_known_types() + .any(|t| t.canonical_name() == "Sequencestring")); + assert!(types + .iter_known_types() + .any(|t| t.canonical_name() == "string")); + } + + #[test] + fn test_resolving_map_type_adds_string_and_inner_type() { + let mut types = TypeUniverse::default(); + assert_eq!(types.iter_known_types().count(), 0); + let (_, expr) = weedle::types::Type::parse("record<DOMString, float>").unwrap(); + let t = types.resolve_type_expression(expr).unwrap(); + assert_eq!(t.canonical_name(), "MapStringF32"); + assert_eq!(types.iter_known_types().count(), 3); + assert!(types + .iter_known_types() + .any(|t| t.canonical_name() == "MapStringF32")); + assert!(types + .iter_known_types() + .any(|t| t.canonical_name() == "string")); + assert!(types + .iter_known_types() + .any(|t| t.canonical_name() == "f32")); + } + + #[test] + fn test_resolving_map_type_adds_key_type_and_inner_type() { + let mut types = TypeUniverse::default(); + assert_eq!(types.iter_known_types().count(), 0); + let (_, expr) = weedle::types::Type::parse("record<u64, float>").unwrap(); + let t = types.resolve_type_expression(expr).unwrap(); + assert_eq!(t.canonical_name(), "MapU64F32"); + assert_eq!(types.iter_known_types().count(), 3); + assert!(types + .iter_known_types() + .any(|t| t.canonical_name() == "MapU64F32")); + assert!(types + .iter_known_types() + .any(|t| t.canonical_name() == "u64")); + assert!(types + .iter_known_types() + .any(|t| t.canonical_name() == "f32")); + } + + #[test] + fn test_error_on_unknown_type() -> Result<()> { + let mut types = TypeUniverse::default(); + types.add_type_definition("TestRecord", Type::Record("TestRecord".into()))?; + // Oh no, someone made a typo in the type-o... + let (_, expr) = weedle::types::Type::parse("TestRecrd").unwrap(); + let err = types.resolve_type_expression(expr).unwrap_err(); + assert_eq!(err.to_string(), "unknown type reference: TestRecrd"); + Ok(()) + } + + #[test] + fn test_error_on_union_type() -> Result<()> { + let mut types = TypeUniverse::default(); + types.add_type_definition("TestRecord", Type::Record("TestRecord".into()))?; + let (_, expr) = weedle::types::Type::parse("(TestRecord or u32)").unwrap(); + let err = types.resolve_type_expression(expr).unwrap_err(); + assert_eq!(err.to_string(), "no support for union types yet"); + Ok(()) + } + + #[test] + fn test_type_set_is_well_ordered() -> Result<()> { + // The set (universe) of types should have a well-defined order. When + // the data structure does not guarantee the order of its elements, such as + // HashSet, then the resulting generated source code is likely not + // deterministic, and the compiled binary file may not be reproducible. We + // avoid this issue by using an implementation that defines the order of its + // elements. This test verifies that the elements are sorted as expected. + let mut types = TypeUniverse::default(); + types.add_type_definition("TestRecord", Type::Record("TestRecord".into()))?; + assert_eq!(types.iter_known_types().count(), 1); + types.add_type_definition("TestRecord2", Type::Record("TestRecord2".into()))?; + assert_eq!(types.iter_known_types().count(), 2); + types.add_type_definition("TestInt64", Type::Int64)?; + types.add_type_definition("TestInt8", Type::Int8)?; + types.add_type_definition("TestUInt8", Type::UInt8)?; + types.add_type_definition("TestBoolean", Type::Boolean)?; + assert_eq!(types.iter_known_types().count(), 6); + let mut iter = types.iter_known_types(); + assert_eq!(Some(&Type::UInt8), iter.next()); + assert_eq!(Some(&Type::Int8), iter.next()); + assert_eq!(Some(&Type::Int64), iter.next()); + assert_eq!(Some(&Type::Boolean), iter.next()); + assert_eq!(Some(&Type::Record("TestRecord".into())), iter.next()); + assert_eq!(Some(&Type::Record("TestRecord2".into())), iter.next()); + Ok(()) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/lib.rs b/third_party/rust/uniffi_bindgen/src/lib.rs new file mode 100644 index 0000000000..4959f0324a --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/lib.rs @@ -0,0 +1,483 @@ +/* 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/. */ + +//! # Uniffi: easily build cross-platform software components in Rust +//! +//! This is a highly-experimental crate for building cross-language software components +//! in Rust, based on things we've learned and patterns we've developed in the +//! [mozilla/application-services](https://github.com/mozilla/application-services) project. +//! +//! The idea is to let you write your code once, in Rust, and then re-use it from many +//! other programming languages via Rust's C-compatible FFI layer and some automagically +//! generated binding code. If you think of it as a kind of [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) +//! wannabe, with a clunkier developer experience but support for more target languages, +//! you'll be pretty close to the mark. +//! +//! Currently supported target languages include Kotlin, Swift and Python. +//! +//! ## Usage +// +//! To build a cross-language component using `uniffi`, follow these steps. +//! +//! ### 1) Specify your Component Interface +//! +//! Start by thinking about the interface you want to expose for use +//! from other languages. Use the Interface Definition Language to specify your interface +//! in a `.udl` file, where it can be processed by the tools from this crate. +//! For example you might define an interface like this: +//! +//! ```text +//! namespace example { +//! u32 foo(u32 bar); +//! } +//! +//! dictionary MyData { +//! u32 num_foos; +//! bool has_a_bar; +//! } +//! ``` +//! +//! ### 2) Implement the Component Interface as a Rust crate +//! +//! With the interface, defined, provide a corresponding implementation of that interface +//! as a standard-looking Rust crate, using functions and structs and so-on. For example +//! an implementation of the above Component Interface might look like this: +//! +//! ```text +//! fn foo(bar: u32) -> u32 { +//! // TODO: a better example! +//! bar + 42 +//! } +//! +//! struct MyData { +//! num_foos: u32, +//! has_a_bar: bool +//! } +//! ``` +//! +//! ### 3) Generate and include component scaffolding from the UDL file +//! +//! First you will need to install `uniffi-bindgen` on your system using `cargo install uniffi_bindgen`. +//! Then add to your crate `uniffi_build` under `[build-dependencies]`. +//! Finally, add a `build.rs` script to your crate and have it call `uniffi_build::generate_scaffolding` +//! to process your `.udl` file. This will generate some Rust code to be included in the top-level source +//! code of your crate. If your UDL file is named `example.udl`, then your build script would call: +//! +//! ```text +//! uniffi_build::generate_scaffolding("./src/example.udl") +//! ``` +//! +//! This would output a rust file named `example.uniffi.rs`, ready to be +//! included into the code of your rust crate like this: +//! +//! ```text +//! include!(concat!(env!("OUT_DIR"), "/example.uniffi.rs")); +//! ``` +//! +//! ### 4) Generate foreign language bindings for the library +//! +//! The `uniffi-bindgen` utility provides a command-line tool that can produce code to +//! consume the Rust library in any of several supported languages. +//! It is done by calling (in kotlin for example): +//! +//! ```text +//! uniffi-bindgen --language kotlin ./src/example.udl +//! ``` +//! +//! This will produce a file `example.kt` in the same directory as the .udl file, containing kotlin bindings +//! to load and use the compiled rust code via its C-compatible FFI. +//! + +#![warn(rust_2018_idioms, unused_qualifications)] +#![allow(unknown_lints)] + +const BINDGEN_VERSION: &str = env!("CARGO_PKG_VERSION"); + +use anyhow::{anyhow, bail, Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use fs_err::{self as fs, File}; +use serde::{Deserialize, Serialize}; +use std::io::prelude::*; +use std::io::ErrorKind; +use std::{collections::HashMap, env, process::Command, str::FromStr}; + +pub mod backend; +pub mod bindings; +pub mod interface; +pub mod macro_metadata; +pub mod scaffolding; + +pub use interface::ComponentInterface; +use scaffolding::RustScaffolding; + +/// A trait representing a Binding Generator Configuration +/// +/// External crates that implement binding generators need to implement this trait and set it as +/// the `BindingGenerator.config` associated type. `generate_external_bindings()` then uses it to +/// generate the config that's passed to `BindingGenerator.write_bindings()` +pub trait BindingGeneratorConfig: for<'de> Deserialize<'de> { + /// Get the entry for this config from the `bindings` table. + fn get_entry_from_bindings_table(bindings: &toml::Value) -> Option<toml::Value>; + + /// Get default config values from the `ComponentInterface` + /// + /// These will replace missing entries in the bindings-specific config + fn get_config_defaults(ci: &ComponentInterface) -> Vec<(String, toml::Value)>; +} + +fn load_bindings_config<BC: BindingGeneratorConfig>( + ci: &ComponentInterface, + crate_root: &Utf8Path, + config_file_override: Option<&Utf8Path>, +) -> Result<BC> { + // Load the config from the TOML value, falling back to an empty map if it doesn't exist + let mut config_map: toml::value::Table = + match load_bindings_config_toml::<BC>(crate_root, config_file_override)? { + Some(value) => value + .try_into() + .context("Bindings config must be a TOML table")?, + None => toml::map::Map::new(), + }; + + // Update it with the defaults from the component interface + for (key, value) in BC::get_config_defaults(ci) { + config_map.entry(key).or_insert(value); + } + + // Leverage serde to convert toml::Value into the config type + toml::Value::from(config_map) + .try_into() + .context("Generating bindings config from toml::Value") +} + +/// Binding generator config with no members +#[derive(Clone, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)] +pub struct EmptyBindingGeneratorConfig; + +impl BindingGeneratorConfig for EmptyBindingGeneratorConfig { + fn get_entry_from_bindings_table(_bindings: &toml::Value) -> Option<toml::Value> { + None + } + + fn get_config_defaults(_ci: &ComponentInterface) -> Vec<(String, toml::Value)> { + Vec::new() + } +} + +// EmptyBindingGeneratorConfig is a unit struct, so the `derive(Deserialize)` implementation +// expects a null value rather than the empty map that we pass it. So we need to implement +// `Deserialize` ourselves. +impl<'de> Deserialize<'de> for EmptyBindingGeneratorConfig { + fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + Ok(EmptyBindingGeneratorConfig) + } +} + +// Load the binding-specific config +// +// This function calculates the location of the config TOML file, parses it, and returns the result +// as a toml::Value +// +// If there is an error parsing the file then Err will be returned. If the file is missing or the +// entry for the bindings is missing, then Ok(None) will be returned. +fn load_bindings_config_toml<BC: BindingGeneratorConfig>( + crate_root: &Utf8Path, + config_file_override: Option<&Utf8Path>, +) -> Result<Option<toml::Value>> { + let config_path = match config_file_override { + Some(cfg) => cfg.to_owned(), + None => crate_root.join("uniffi.toml"), + }; + + if !config_path.exists() { + return Ok(None); + } + + let contents = fs::read_to_string(&config_path) + .with_context(|| format!("Failed to read config file from {config_path}"))?; + let full_config = toml::Value::from_str(&contents) + .with_context(|| format!("Failed to parse config file {config_path}"))?; + + Ok(full_config + .get("bindings") + .and_then(BC::get_entry_from_bindings_table)) +} + +/// A trait representing a UniFFI Binding Generator +/// +/// External crates that implement binding generators, should implement this type +/// and call the [`generate_external_bindings`] using a type that implements this trait. +pub trait BindingGenerator: Sized { + /// Associated type representing a the bindings-specifig configuration parsed from the + /// uniffi.toml + type Config: BindingGeneratorConfig; + + /// Writes the bindings to the output directory + /// + /// # Arguments + /// - `ci`: A [`ComponentInterface`] representing the interface + /// - `config`: A instance of the BindingGeneratorConfig associated with this type + /// - `out_dir`: The path to where the binding generator should write the output bindings + fn write_bindings( + &self, + ci: ComponentInterface, + config: Self::Config, + out_dir: &Utf8Path, + ) -> Result<()>; +} + +/// Generate bindings for an external binding generator +/// Ideally, this should replace the [`generate_bindings`] function below +/// +/// Implements an entry point for external binding generators. +/// The function does the following: +/// - It parses the `udl` in a [`ComponentInterface`] +/// - Parses the `uniffi.toml` and loads it into the type that implements [`BindingGeneratorConfig`] +/// - Creates an instance of [`BindingGenerator`], based on type argument `B`, and run [`BindingGenerator::write_bindings`] on it +/// +/// # Arguments +/// - `binding_generator`: Type that implements BindingGenerator +/// - `udl_file`: The path to the UDL file +/// - `config_file_override`: The path to the configuration toml file, most likely called `uniffi.toml`. If [`None`], the function will try to guess based on the crate's root. +/// - `out_dir_override`: The path to write the bindings to. If [`None`], it will be the path to the parent directory of the `udl_file` +pub fn generate_external_bindings( + binding_generator: impl BindingGenerator, + udl_file: impl AsRef<Utf8Path>, + config_file_override: Option<impl AsRef<Utf8Path>>, + out_dir_override: Option<impl AsRef<Utf8Path>>, +) -> Result<()> { + let out_dir_override = out_dir_override.as_ref().map(|p| p.as_ref()); + let config_file_override = config_file_override.as_ref().map(|p| p.as_ref()); + + let crate_root = guess_crate_root(udl_file.as_ref())?; + let out_dir = get_out_dir(udl_file.as_ref(), out_dir_override)?; + let component = parse_udl(udl_file.as_ref()).context("Error parsing UDL")?; + let bindings_config = load_bindings_config(&component, crate_root, config_file_override)?; + binding_generator.write_bindings(component, bindings_config, &out_dir) +} + +// Generate the infrastructural Rust code for implementing the UDL interface, +// such as the `extern "C"` function definitions and record data types. +pub fn generate_component_scaffolding( + udl_file: &Utf8Path, + config_file_override: Option<&Utf8Path>, + out_dir_override: Option<&Utf8Path>, + format_code: bool, +) -> Result<()> { + let component = parse_udl(udl_file)?; + let _config = get_config( + &component, + guess_crate_root(udl_file)?, + config_file_override, + ); + let file_stem = udl_file.file_stem().context("not a file")?; + let filename = format!("{file_stem}.uniffi.rs"); + let out_path = get_out_dir(udl_file, out_dir_override)?.join(filename); + let mut f = File::create(&out_path)?; + write!(f, "{}", RustScaffolding::new(&component)).context("Failed to write output file")?; + if format_code { + format_code_with_rustfmt(&out_path)?; + } + Ok(()) +} + +// Generate the bindings in the target languages that call the scaffolding +// Rust code. +pub fn generate_bindings( + udl_file: &Utf8Path, + config_file_override: Option<&Utf8Path>, + target_languages: Vec<&str>, + out_dir_override: Option<&Utf8Path>, + library_file: Option<&Utf8Path>, + try_format_code: bool, +) -> Result<()> { + let mut component = parse_udl(udl_file)?; + if let Some(library_file) = library_file { + macro_metadata::add_to_ci_from_library(&mut component, library_file)?; + } + let crate_root = &guess_crate_root(udl_file)?; + + let config = get_config(&component, crate_root, config_file_override)?; + let out_dir = get_out_dir(udl_file, out_dir_override)?; + for language in target_languages { + bindings::write_bindings( + &config.bindings, + &component, + &out_dir, + language.try_into()?, + try_format_code, + )?; + } + + Ok(()) +} + +pub fn dump_json(library_path: &Utf8Path) -> Result<String> { + let metadata = macro_metadata::extract_from_library(library_path)?; + Ok(serde_json::to_string_pretty(&metadata)?) +} + +pub fn print_json(library_path: &Utf8Path) -> Result<()> { + println!("{}", dump_json(library_path)?); + Ok(()) +} + +/// Guess the root directory of the crate from the path of its UDL file. +/// +/// For now, we assume that the UDL file is in `./src/something.udl` relative +/// to the crate root. We might consider something more sophisticated in +/// future. +pub fn guess_crate_root(udl_file: &Utf8Path) -> Result<&Utf8Path> { + let path_guess = udl_file + .parent() + .context("UDL file has no parent folder!")? + .parent() + .context("UDL file has no grand-parent folder!")?; + if !path_guess.join("Cargo.toml").is_file() { + bail!("UDL file does not appear to be inside a crate") + } + Ok(path_guess) +} + +fn get_config( + component: &ComponentInterface, + crate_root: &Utf8Path, + config_file_override: Option<&Utf8Path>, +) -> Result<Config> { + let default_config: Config = component.into(); + + let config_file = match config_file_override { + Some(cfg) => Some(cfg.to_owned()), + None => crate_root.join("uniffi.toml").canonicalize_utf8().ok(), + }; + + match config_file { + Some(path) => { + let contents = fs::read_to_string(&path) + .with_context(|| format!("Failed to read config file from {path}"))?; + let loaded_config: Config = toml::de::from_str(&contents) + .with_context(|| format!("Failed to generate config from file {path}"))?; + Ok(loaded_config.merge_with(&default_config)) + } + None => Ok(default_config), + } +} + +fn get_out_dir(udl_file: &Utf8Path, out_dir_override: Option<&Utf8Path>) -> Result<Utf8PathBuf> { + Ok(match out_dir_override { + Some(s) => { + // Create the directory if it doesn't exist yet. + fs::create_dir_all(s)?; + s.canonicalize_utf8().context("Unable to find out-dir")? + } + None => udl_file + .parent() + .context("File has no parent directory")? + .to_owned(), + }) +} + +fn parse_udl(udl_file: &Utf8Path) -> Result<ComponentInterface> { + let udl = fs::read_to_string(udl_file) + .with_context(|| format!("Failed to read UDL from {udl_file}"))?; + ComponentInterface::from_webidl(&udl).context("Failed to parse UDL") +} + +fn format_code_with_rustfmt(path: &Utf8Path) -> Result<()> { + let status = Command::new("rustfmt").arg(path).status().map_err(|e| { + let ctx = match e.kind() { + ErrorKind::NotFound => "formatting was requested, but rustfmt was not found", + _ => "unknown error when calling rustfmt", + }; + anyhow!(e).context(ctx) + })?; + if !status.success() { + bail!("rustmt failed when formatting scaffolding. Note: --no-format can be used to skip formatting"); + } + Ok(()) +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +struct Config { + #[serde(default)] + bindings: bindings::Config, +} + +impl From<&ComponentInterface> for Config { + fn from(ci: &ComponentInterface) -> Self { + Config { + bindings: ci.into(), + } + } +} + +pub trait MergeWith { + fn merge_with(&self, other: &Self) -> Self; +} + +impl MergeWith for Config { + fn merge_with(&self, other: &Self) -> Self { + Config { + bindings: self.bindings.merge_with(&other.bindings), + } + } +} + +impl<T: Clone> MergeWith for Option<T> { + fn merge_with(&self, other: &Self) -> Self { + match (self, other) { + (Some(_), _) => self.clone(), + (None, Some(_)) => other.clone(), + (None, None) => None, + } + } +} + +impl<V: Clone> MergeWith for HashMap<String, V> { + fn merge_with(&self, other: &Self) -> Self { + let mut merged = HashMap::new(); + // Iterate through other first so our keys override theirs + for (key, value) in other.iter().chain(self) { + merged.insert(key.clone(), value.clone()); + } + merged + } +} + +// FIXME(HACK): +// Include the askama config file into the build. +// That way cargo tracks the file and other tools relying on file tracking see it as well. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1774585 +// In the future askama should handle that itself by using the `track_path::path` API, +// see https://github.com/rust-lang/rust/pull/84029 +#[allow(dead_code)] +mod __unused { + const _: &[u8] = include_bytes!("../askama.toml"); +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_guessing_of_crate_root_directory_from_udl_file() { + // When running this test, this will be the ./uniffi_bindgen directory. + let this_crate_root = Utf8PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + + let example_crate_root = this_crate_root + .parent() + .expect("should have a parent directory") + .join("./examples/arithmetic"); + assert_eq!( + guess_crate_root(&example_crate_root.join("./src/arthmetic.udl")).unwrap(), + example_crate_root + ); + + let not_a_crate_root = &this_crate_root.join("./src/templates"); + assert!(guess_crate_root(¬_a_crate_root.join("./src/example.udl")).is_err()); + } +} diff --git a/third_party/rust/uniffi_bindgen/src/macro_metadata/ci.rs b/third_party/rust/uniffi_bindgen/src/macro_metadata/ci.rs new file mode 100644 index 0000000000..b6fe94dd20 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/macro_metadata/ci.rs @@ -0,0 +1,100 @@ +/* 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 crate::interface::{ComponentInterface, Enum, Error, Record, Type}; +use anyhow::anyhow; +use uniffi_meta::Metadata; + +/// Add Metadata items to the ComponentInterface +/// +/// This function exists to support the transition period where the `uniffi::export` macro can only +/// handle some components. This means that crates need to continue using UDL files to define the +/// parts of the components that aren't supported yet. +/// +/// To make things work, we generate a `ComponentInterface` from the UDL file, then combine it with +/// the `Metadata` items that the macro creates. +pub fn add_to_ci( + iface: &mut ComponentInterface, + metadata_items: Vec<Metadata>, +) -> anyhow::Result<()> { + for item in metadata_items { + let (item_desc, crate_name) = match &item { + Metadata::Func(meta) => ( + format!("function `{}`", meta.name), + meta.module_path.first().unwrap(), + ), + Metadata::Method(meta) => ( + format!("method `{}.{}`", meta.self_name, meta.name), + meta.module_path.first().unwrap(), + ), + Metadata::Record(meta) => ( + format!("record `{}`", meta.name), + meta.module_path.first().unwrap(), + ), + Metadata::Enum(meta) => ( + format!("enum `{}`", meta.name), + meta.module_path.first().unwrap(), + ), + Metadata::Object(meta) => ( + format!("object `{}`", meta.name), + meta.module_path.first().unwrap(), + ), + Metadata::Error(meta) => ( + format!("error `{}`", meta.name), + meta.module_path.first().unwrap(), + ), + }; + + let ns = iface.namespace(); + if crate_name != ns { + return Err(anyhow!("Found {item_desc} from crate `{crate_name}`.") + .context(format!( + "Main crate is expected to be named `{ns}` based on the UDL namespace." + )) + .context("Mixing symbols from multiple crates is not supported yet.")); + } + + match item { + Metadata::Func(meta) => { + iface.add_fn_meta(meta)?; + } + Metadata::Method(meta) => { + iface.add_method_meta(meta); + } + Metadata::Record(meta) => { + let ty = Type::Record(meta.name.clone()); + iface.types.add_known_type(&ty)?; + iface.types.add_type_definition(&meta.name, ty)?; + + let record: Record = meta.into(); + iface.add_record_definition(record)?; + } + Metadata::Enum(meta) => { + let ty = Type::Enum(meta.name.clone()); + iface.types.add_known_type(&ty)?; + iface.types.add_type_definition(&meta.name, ty)?; + + let enum_: Enum = meta.into(); + iface.add_enum_definition(enum_)?; + } + Metadata::Object(meta) => { + iface.add_object_free_fn(meta); + } + Metadata::Error(meta) => { + let ty = Type::Error(meta.name.clone()); + iface.types.add_known_type(&ty)?; + iface.types.add_type_definition(&meta.name, ty)?; + + let error: Error = meta.into(); + iface.add_error_definition(error)?; + } + } + } + + iface.resolve_types()?; + iface.derive_ffi_funcs()?; + iface.check_consistency()?; + + Ok(()) +} diff --git a/third_party/rust/uniffi_bindgen/src/macro_metadata/extract.rs b/third_party/rust/uniffi_bindgen/src/macro_metadata/extract.rs new file mode 100644 index 0000000000..3de10bd087 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/macro_metadata/extract.rs @@ -0,0 +1,185 @@ +/* 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 anyhow::{bail, Context}; +use camino::Utf8Path; +use fs_err as fs; +use goblin::{ + archive::Archive, + elf::Elf, + mach::{segment::Section, symbols, Mach, MachO, SingleArch}, + pe::PE, + Object, +}; +use std::collections::HashSet; +use uniffi_meta::Metadata; + +/// Extract metadata written by the `uniffi::export` macro from a library file +/// +/// In addition to generating the scaffolding, that macro and also encodes the +/// `uniffi_meta::Metadata` for the components which can be used to generate the bindings side of +/// the interface. +pub fn extract_from_library(path: &Utf8Path) -> anyhow::Result<Vec<Metadata>> { + extract_from_bytes(&fs::read(path)?) +} + +fn extract_from_bytes(file_data: &[u8]) -> anyhow::Result<Vec<Metadata>> { + match Object::parse(file_data)? { + Object::Elf(elf) => extract_from_elf(elf, file_data), + Object::PE(pe) => extract_from_pe(pe, file_data), + Object::Mach(mach) => extract_from_mach(mach, file_data), + Object::Archive(archive) => extract_from_archive(archive, file_data), + Object::Unknown(_) => bail!("Unknown library format"), + } +} + +pub fn extract_from_elf(elf: Elf<'_>, file_data: &[u8]) -> anyhow::Result<Vec<Metadata>> { + let mut extracted = ExtractedItems::new(); + let iter = elf + .syms + .iter() + .filter_map(|sym| elf.section_headers.get(sym.st_shndx).map(|sh| (sym, sh))); + + for (sym, sh) in iter { + let name = elf + .strtab + .get_at(sym.st_name) + .context("Error getting symbol name")?; + if is_metadata_symbol(name) { + // Offset relative to the start of the section. + let section_offset = sym.st_value - sh.sh_addr; + // Offset relative to the start of the file contents + extracted.extract_item(name, file_data, (sh.sh_offset + section_offset) as usize)?; + } + } + Ok(extracted.into_metadata()) +} + +pub fn extract_from_pe(pe: PE<'_>, file_data: &[u8]) -> anyhow::Result<Vec<Metadata>> { + let mut extracted = ExtractedItems::new(); + for export in pe.exports { + if let Some(name) = export.name { + if is_metadata_symbol(name) { + extracted.extract_item( + name, + file_data, + export.offset.context("Error getting symbol offset")?, + )?; + } + } + } + Ok(extracted.into_metadata()) +} + +pub fn extract_from_mach(mach: Mach<'_>, file_data: &[u8]) -> anyhow::Result<Vec<Metadata>> { + match mach { + Mach::Binary(macho) => extract_from_macho(macho, file_data), + // Multi-binary library, just extract the first one + Mach::Fat(multi_arch) => match multi_arch.get(0)? { + SingleArch::MachO(macho) => extract_from_macho(macho, file_data), + SingleArch::Archive(archive) => extract_from_archive(archive, file_data), + }, + } +} + +pub fn extract_from_macho(macho: MachO<'_>, file_data: &[u8]) -> anyhow::Result<Vec<Metadata>> { + let mut sections: Vec<Section> = Vec::new(); + for sects in macho.segments.sections() { + sections.extend(sects.map(|r| r.expect("section").0)); + } + let mut extracted = ExtractedItems::new(); + sections.sort_by_key(|s| s.addr); + + // Iterate through the symbols. This picks up symbols from the .o files embedded in a Darwin + // archive. + for (name, nlist) in macho.symbols().flatten() { + // Check that the symbol: + // - Is global (exported) + // - Has type=N_SECT (it's regular data as opposed to something like + // "undefined" or "indirect") + // - Has a metadata symbol name + if nlist.is_global() && nlist.get_type() == symbols::N_SECT && is_metadata_symbol(name) { + let section = §ions[nlist.n_sect]; + // `nlist.n_value` is an address, so we can calculating the offset inside the section + // using the difference between that and `section.addr` + let offset = section.offset as usize + nlist.n_value as usize - section.addr as usize; + extracted.extract_item(name, file_data, offset)?; + } + } + + // Iterate through the exports. This picks up symbols from .dylib files. + for export in macho.exports()? { + let name = &export.name; + if is_metadata_symbol(name) { + extracted.extract_item(name, file_data, export.offset as usize)?; + } + } + Ok(extracted.into_metadata()) +} + +pub fn extract_from_archive( + archive: Archive<'_>, + file_data: &[u8], +) -> anyhow::Result<Vec<Metadata>> { + // Store the names of archive members that have metadata symbols in them + let mut members_to_check: HashSet<&str> = HashSet::new(); + for (member_name, _, symbols) in archive.summarize() { + for name in symbols { + if is_metadata_symbol(name) { + members_to_check.insert(member_name); + } + } + } + + let mut items = vec![]; + for member_name in members_to_check { + items.append(&mut extract_from_bytes( + archive.extract(member_name, file_data)?, + )?); + } + Ok(items) +} + +/// Container for extracted metadata items +#[derive(Default)] +struct ExtractedItems { + items: Vec<Metadata>, + /// symbol names for the extracted items, we use this to ensure that we don't extract the same + /// symbol twice + names: HashSet<String>, +} + +impl ExtractedItems { + fn new() -> Self { + Self::default() + } + + fn extract_item(&mut self, name: &str, file_data: &[u8], offset: usize) -> anyhow::Result<()> { + if self.names.contains(name) { + // Already extracted this item + return Ok(()); + } + + // Use the file data starting from offset, without specifying the end position. We don't + // always know the end position, because goblin reports the symbol size as 0 for PE and + // MachO files. + // + // This works fine, because bincode knows when the serialized data is terminated and will + // just ignore the trailing data. + let data = &file_data[offset..]; + self.items.push(bincode::deserialize::<Metadata>(data)?); + self.names.insert(name.to_string()); + Ok(()) + } + + fn into_metadata(self) -> Vec<Metadata> { + self.items + } +} + +fn is_metadata_symbol(name: &str) -> bool { + // Skip the "_" char that Darwin prepends, if present + let name = name.strip_prefix('_').unwrap_or(name); + name.starts_with("UNIFFI_META") +} diff --git a/third_party/rust/uniffi_bindgen/src/macro_metadata/mod.rs b/third_party/rust/uniffi_bindgen/src/macro_metadata/mod.rs new file mode 100644 index 0000000000..d303418ece --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/macro_metadata/mod.rs @@ -0,0 +1,19 @@ +/* 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 super::ComponentInterface; +use camino::Utf8Path; + +mod ci; +mod extract; + +pub use ci::add_to_ci; +pub use extract::extract_from_library; + +pub fn add_to_ci_from_library( + iface: &mut ComponentInterface, + library_path: &Utf8Path, +) -> anyhow::Result<()> { + add_to_ci(iface, extract_from_library(library_path)?) +} diff --git a/third_party/rust/uniffi_bindgen/src/scaffolding/mod.rs b/third_party/rust/uniffi_bindgen/src/scaffolding/mod.rs new file mode 100644 index 0000000000..7fc60dede4 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/scaffolding/mod.rs @@ -0,0 +1,152 @@ +/* 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 anyhow::Result; +use askama::Template; +use std::borrow::Borrow; + +use super::interface::*; +use heck::ToSnakeCase; + +#[derive(Template)] +#[template(syntax = "rs", escape = "none", path = "scaffolding_template.rs")] +pub struct RustScaffolding<'a> { + ci: &'a ComponentInterface, + uniffi_version: &'static str, +} +impl<'a> RustScaffolding<'a> { + pub fn new(ci: &'a ComponentInterface) -> Self { + Self { + ci, + uniffi_version: crate::BINDGEN_VERSION, + } + } +} +mod filters { + use super::*; + + pub fn type_rs(type_: &Type) -> Result<String, askama::Error> { + Ok(match type_ { + Type::Int8 => "i8".into(), + Type::UInt8 => "u8".into(), + Type::Int16 => "i16".into(), + Type::UInt16 => "u16".into(), + Type::Int32 => "i32".into(), + Type::UInt32 => "u32".into(), + Type::Int64 => "i64".into(), + Type::UInt64 => "u64".into(), + Type::Float32 => "f32".into(), + Type::Float64 => "f64".into(), + Type::Boolean => "bool".into(), + Type::String => "String".into(), + Type::Timestamp => "std::time::SystemTime".into(), + Type::Duration => "std::time::Duration".into(), + Type::Enum(name) | Type::Record(name) | Type::Error(name) => format!("r#{name}"), + Type::Object(name) => format!("std::sync::Arc<r#{name}>"), + Type::CallbackInterface(name) => format!("Box<dyn r#{name}>"), + Type::Optional(t) => format!("std::option::Option<{}>", type_rs(t)?), + Type::Sequence(t) => format!("std::vec::Vec<{}>", type_rs(t)?), + Type::Map(k, v) => format!( + "std::collections::HashMap<{}, {}>", + type_rs(k)?, + type_rs(v)? + ), + Type::Custom { name, .. } => format!("r#{name}"), + Type::External { .. } => panic!("External types coming to a uniffi near you soon!"), + Type::Unresolved { .. } => { + unreachable!("UDL scaffolding code never contains unresolved types") + } + }) + } + + pub fn type_ffi(type_: &FfiType) -> Result<String, askama::Error> { + Ok(match type_ { + FfiType::Int8 => "i8".into(), + FfiType::UInt8 => "u8".into(), + FfiType::Int16 => "i16".into(), + FfiType::UInt16 => "u16".into(), + FfiType::Int32 => "i32".into(), + FfiType::UInt32 => "u32".into(), + FfiType::Int64 => "i64".into(), + FfiType::UInt64 => "u64".into(), + FfiType::Float32 => "f32".into(), + FfiType::Float64 => "f64".into(), + FfiType::RustArcPtr(_) => "*const std::os::raw::c_void".into(), + FfiType::RustBuffer(_) => "uniffi::RustBuffer".into(), + FfiType::ForeignBytes => "uniffi::ForeignBytes".into(), + FfiType::ForeignCallback => "uniffi::ForeignCallback".into(), + }) + } + + /// Get the name of the FfiConverter implementation for this type + /// + /// - For primitives / standard types this is the type itself. + /// - For user-defined types, this is a unique generated name. We then generate a unit-struct + /// in the scaffolding code that implements FfiConverter. + pub fn ffi_converter_name(type_: &Type) -> askama::Result<String> { + Ok(match type_ { + // Timestamp/Duraration are handled by standard types + Type::Timestamp => "std::time::SystemTime".into(), + Type::Duration => "std::time::Duration".into(), + // Object is handled by Arc<T> + Type::Object(name) => format!("std::sync::Arc<r#{name}>"), + // Other user-defined types are handled by a unit-struct that we generate. The + // FfiConverter implementation for this can be found in one of the scaffolding template code. + // + // We generate a unit-struct to sidestep Rust's orphan rules (ADR-0006). + // + // CallbackInterface is handled by special case code on both the scaffolding and + // bindings side. It's not a unit-struct, but the same name generation code works. + Type::Enum(_) | Type::Record(_) | Type::Error(_) | Type::CallbackInterface(_) => { + format!("FfiConverter{}", type_.canonical_name()) + } + // Wrapper types are implemented by generics that wrap the FfiConverter implementation of the + // inner type. + Type::Optional(inner) => { + format!("std::option::Option<{}>", ffi_converter_name(inner)?) + } + Type::Sequence(inner) => format!("std::vec::Vec<{}>", ffi_converter_name(inner)?), + Type::Map(k, v) => format!( + "std::collections::HashMap<{}, {}>", + ffi_converter_name(k)?, + ffi_converter_name(v)? + ), + // External and Wrapped bytes have FfiConverters with a predictable name based on the type name. + Type::Custom { name, .. } | Type::External { name, .. } => { + format!("FfiConverterType{name}") + } + // Primitive types / strings are implemented by their rust type + Type::Int8 => "i8".into(), + Type::UInt8 => "u8".into(), + Type::Int16 => "i16".into(), + Type::UInt16 => "u16".into(), + Type::Int32 => "i32".into(), + Type::UInt32 => "u32".into(), + Type::Int64 => "i64".into(), + Type::UInt64 => "u64".into(), + Type::Float32 => "f32".into(), + Type::Float64 => "f64".into(), + Type::String => "String".into(), + Type::Boolean => "bool".into(), + Type::Unresolved { .. } => { + unreachable!("UDL scaffolding code never contains unresolved types") + } + }) + } + + // Map a type to Rust code that specifies the FfiConverter implementation. + // + // This outputs something like `<TheFfiConverterStruct as FfiConverter>` + pub fn ffi_converter(type_: &Type) -> Result<String, askama::Error> { + Ok(format!( + "<{} as uniffi::FfiConverter>", + ffi_converter_name(type_)? + )) + } + + // Turns a `crate-name` into the `crate_name` the .rs code needs to specify. + pub fn crate_name_rs(nm: &str) -> Result<String, askama::Error> { + Ok(nm.to_string().to_snake_case()) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs new file mode 100644 index 0000000000..58489fd6f6 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs @@ -0,0 +1,197 @@ +{# +// For each Callback Interface definition, we assume that there is a corresponding trait defined in Rust client code. +// If the UDL callback interface and Rust trait's methods don't match, the Rust compiler will complain. +// We generate: +// * an init function to accept that `ForeignCallback` from the foreign language, and stores it. +// * a holder for a `ForeignCallback`, of type `uniffi::ForeignCallbackInternals`. +// * a proxy `struct` which implements the `trait` that the Callback Interface corresponds to. This +// is the object that client code interacts with. +// - for each method, arguments will be packed into a `RustBuffer` and sent over the `ForeignCallback` to be +// unpacked and called. The return value is packed into another `RustBuffer` and sent back to Rust. +// - a `Drop` `impl`, which tells the foreign language to forget about the real callback object. +#} +{% let trait_name = cbi.name() -%} +{% let trait_impl = cbi.type_().borrow()|ffi_converter_name -%} +{% let foreign_callback_internals = format!("foreign_callback_{}_internals", trait_name)|upper -%} + +// Register a foreign callback for getting across the FFI. +#[doc(hidden)] +static {{ foreign_callback_internals }}: uniffi::ForeignCallbackInternals = uniffi::ForeignCallbackInternals::new(); + +#[doc(hidden)] +#[no_mangle] +pub extern "C" fn {{ cbi.ffi_init_callback().name() }}(callback: uniffi::ForeignCallback, _: &mut uniffi::RustCallStatus) { + {{ foreign_callback_internals }}.set_callback(callback); + // The call status should be initialized to CALL_SUCCESS, so no need to modify it. +} + +// Make an implementation which will shell out to the foreign language. +#[doc(hidden)] +#[derive(Debug)] +struct {{ trait_impl }} { + handle: u64 +} + +impl Drop for {{ trait_impl }} { + fn drop(&mut self) { + let callback = {{ foreign_callback_internals }}.get_callback().unwrap(); + let mut rbuf = uniffi::RustBuffer::new(); + unsafe { callback(self.handle, uniffi::IDX_CALLBACK_FREE, Default::default(), &mut rbuf) }; + } +} + +uniffi::deps::static_assertions::assert_impl_all!({{ trait_impl }}: Send); + +impl r#{{ trait_name }} for {{ trait_impl }} { + {%- for meth in cbi.methods() %} + + {#- Method declaration #} + fn r#{{ meth.name() -}} + ({% call rs::arg_list_decl_with_prefix("&self", meth) %}) + {%- match (meth.return_type(), meth.throws_type()) %} + {%- when (Some(return_type), None) %} -> {{ return_type.borrow()|type_rs }} + {%- when (Some(return_type), Some(err)) %} -> ::std::result::Result<{{ return_type.borrow()|type_rs }}, {{ err|type_rs }}> + {%- when (None, Some(err)) %} -> ::std::result::Result<(), {{ err|type_rs }}> + {% else -%} + {%- endmatch -%} { + {#- Method body #} + + {#- Packing args into a RustBuffer #} + {% if meth.arguments().len() == 0 -%} + let args_buf = Vec::new(); + {% else -%} + let mut args_buf = Vec::new(); + {% endif -%} + {%- for arg in meth.arguments() %} + {{ arg.type_().borrow()|ffi_converter }}::write(r#{{ arg.name() }}, &mut args_buf); + {%- endfor -%} + let args_rbuf = uniffi::RustBuffer::from_vec(args_buf); + + {#- Calling into foreign code. #} + let callback = {{ foreign_callback_internals }}.get_callback().unwrap(); + + unsafe { + // SAFETY: + // * We're passing in a pointer to an empty buffer. + // * Nothing allocated, so nothing to drop. + // * We expect the callback to write into that a valid allocated instance of a + // RustBuffer. + let mut ret_rbuf = uniffi::RustBuffer::new(); + let ret = callback(self.handle, {{ loop.index }}, args_rbuf, &mut ret_rbuf); + #[allow(clippy::let_and_return, clippy::let_unit_value)] + match ret { + 1 => { + // 1 indicates success with the return value written to the RustBuffer for + // non-void calls. + let result = { + {% match meth.return_type() -%} + {%- when Some(return_type) -%} + let vec = ret_rbuf.destroy_into_vec(); + let mut ret_buf = vec.as_slice(); + {{ return_type|ffi_converter }}::try_read(&mut ret_buf).unwrap() + {%- else %} + uniffi::RustBuffer::destroy(ret_rbuf); + {%- endmatch %} + }; + {%- if meth.throws() %} + Ok(result) + {%- else %} + result + {%- endif %} + } + -2 => { + // -2 indicates an error written to the RustBuffer + {% match meth.throws_type() -%} + {% when Some(error_type) -%} + let vec = ret_rbuf.destroy_into_vec(); + let mut ret_buf = vec.as_slice(); + Err({{ error_type|ffi_converter }}::try_read(&mut ret_buf).unwrap()) + {%- else -%} + panic!("Callback return -2, but throws_type() is None"); + {%- endmatch %} + } + // 0 is a deprecated method to indicates success for void returns + 0 => { + uniffi::deps::log::error!("UniFFI: Callback interface returned 0. Please update the bindings code to return 1 for all successful calls"); + {% match (meth.return_type(), meth.throws()) %} + {% when (Some(_), _) %} + panic!("Callback returned 0 when we were expecting a return value"); + {% when (None, false) %} + {% when (None, true) %} + Ok(()) + {%- endmatch %} + } + // -1 indicates an unexpected error + {% match meth.throws_type() %} + {%- when Some(error_type) -%} + -1 => { + let reason = if !ret_rbuf.is_empty() { + match {{ Type::String.borrow()|ffi_converter }}::try_lift(ret_rbuf) { + Ok(s) => s, + Err(e) => { + uniffi::deps::log::error!("{{ trait_name }} Error reading ret_buf: {e}"); + String::from("[Error reading reason]") + } + } + } else { + uniffi::RustBuffer::destroy(ret_rbuf); + String::from("[Unknown Reason]") + }; + let e: {{ error_type|type_rs }} = uniffi::UnexpectedUniFFICallbackError::from_reason(reason).into(); + Err(e) + } + {%- else %} + -1 => { + if !ret_rbuf.is_empty() { + let reason = match {{ Type::String.borrow()|ffi_converter }}::try_lift(ret_rbuf) { + Ok(s) => s, + Err(_) => { + String::from("[Error reading reason]") + } + }; + panic!("callback failed. Reason: {}", reason); + } else { + panic!("Callback failed") + } + }, + {%- endmatch %} + // Other values should never be returned + _ => panic!("Callback failed with unexpected return code"), + } + } + } + {%- endfor %} +} + +unsafe impl uniffi::FfiConverter for {{ trait_impl }} { + // This RustType allows for rust code that inputs this type as a Box<dyn CallbackInterfaceTrait> param + type RustType = Box<dyn r#{{ trait_name }}>; + type FfiType = u64; + + // Lower and write are tricky to implement because we have a dyn trait as our type. There's + // probably a way to, but this carries lots of thread safety risks, down to impedance + // mismatches between Rust and foreign languages, and our uncertainty around implementations of + // concurrent handlemaps. + // + // The use case for them is also quite exotic: it's passing a foreign callback back to the foreign + // language. + // + // Until we have some certainty, and use cases, we shouldn't use them. + fn lower(_obj: Self::RustType) -> Self::FfiType { + panic!("Lowering CallbackInterface not supported") + } + + fn write(_obj: Self::RustType, _buf: &mut std::vec::Vec<u8>) { + panic!("Writing CallbackInterface not supported") + } + + fn try_lift(v: Self::FfiType) -> uniffi::deps::anyhow::Result<Self::RustType> { + Ok(Box::new(Self { handle: v })) + } + + fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result<Self::RustType> { + use uniffi::deps::bytes::Buf; + uniffi::check_remaining(buf, 8)?; + <Self as uniffi::FfiConverter>::try_lift(buf.get_u64()) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs new file mode 100644 index 0000000000..0d83a99b90 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs @@ -0,0 +1,45 @@ +{# +// For each enum declared in the UDL, we assume the caller has provided a corresponding +// rust `enum`. We provide the traits for sending it across the FFI, which will fail to +// compile if the provided struct has a different shape to the one declared in the UDL. +// +// We define a unit-struct to implement the trait to sidestep Rust's orphan rule (ADR-0006). It's +// public so other crates can refer to it via an `[External='crate'] typedef` +#} + +#[doc(hidden)] +pub struct {{ e.type_().borrow()|ffi_converter_name }}; + +#[doc(hidden)] +impl uniffi::RustBufferFfiConverter for {{ e.type_().borrow()|ffi_converter_name }} { + type RustType = r#{{ e.name() }}; + + fn write(obj: Self::RustType, buf: &mut std::vec::Vec<u8>) { + use uniffi::deps::bytes::BufMut; + match obj { + {%- for variant in e.variants() %} + r#{{ e.name() }}::r#{{ variant.name() }} { {% for field in variant.fields() %}r#{{ field.name() }}, {%- endfor %} } => { + buf.put_i32({{ loop.index }}); + {% for field in variant.fields() -%} + {{ field.type_().borrow()|ffi_converter }}::write(r#{{ field.name() }}, buf); + {%- endfor %} + }, + {%- endfor %} + }; + } + + fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result<r#{{ e.name() }}> { + use uniffi::deps::bytes::Buf; + uniffi::check_remaining(buf, 4)?; + Ok(match buf.get_i32() { + {%- for variant in e.variants() %} + {{ loop.index }} => r#{{ e.name() }}::r#{{ variant.name() }}{% if variant.has_fields() %} { + {% for field in variant.fields() %} + r#{{ field.name() }}: {{ field.type_().borrow()|ffi_converter }}::try_read(buf)?, + {%- endfor %} + }{% endif %}, + {%- endfor %} + v => uniffi::deps::anyhow::bail!("Invalid {{ e.name() }} enum value: {}", v), + }) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs new file mode 100644 index 0000000000..7ca1d104fe --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs @@ -0,0 +1,95 @@ +{# +// For each error declared in the UDL, we assume the caller has provided a corresponding +// rust `enum`. We provide the traits for sending it across the FFI, which will fail to +// compile if the provided struct has a different shape to the one declared in the UDL. +// +// We define a unit-struct to implement the trait to sidestep Rust's orphan rule (ADR-0006). It's +// public so other crates can refer to it via an `[External='crate'] typedef` +#} + +#[doc(hidden)] +pub struct {{ e.type_().borrow()|ffi_converter_name }}; + +#[doc(hidden)] +impl uniffi::RustBufferFfiConverter for {{ e.type_().borrow()|ffi_converter_name }} { + type RustType = r#{{ e.name() }}; + + {% if e.is_flat() %} + + // For "flat" error enums, we stringify the error on the Rust side and surface that + // as the error message in the foreign language. + + + fn write(obj: r#{{ e.name() }}, buf: &mut std::vec::Vec<u8>) { + use uniffi::deps::bytes::BufMut; + let msg = obj.to_string(); + match obj { + {%- for variant in e.variants() %} + r#{{ e.name() }}::r#{{ variant.name() }}{..} => { + buf.put_i32({{ loop.index }}); + <String as uniffi::FfiConverter>::write(msg, buf); + }, + {%- endfor %} + }; + } + + {%- if ci.should_generate_error_read(e) %} + fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result<r#{{ e.name() }}> { + use uniffi::deps::bytes::Buf; + uniffi::check_remaining(buf, 4)?; + Ok(match buf.get_i32() { + {%- for variant in e.variants() %} + {{ loop.index }} => r#{{ e.name() }}::r#{{ variant.name() }}{ }, + {%- endfor %} + v => uniffi::deps::anyhow::bail!("Invalid {{ e.name() }} enum value: {}", v), + }) + } + {%- else %} + fn try_read(_buf: &mut &[u8]) -> uniffi::deps::anyhow::Result<r#{{ e.name() }}> { + panic!("try_read not supported for flat errors"); + } + {%- endif %} + + {% else %} + + // For rich structured enums, we map individual fields on the Rust side over to + // corresponding fields on the foreign-language side. + // + // If a variant doesn't have fields defined in the UDL, it's currently still possible that + // the Rust enum has fields and they're just not listed. In that case we use the `Variant{..}` + // syntax to match the variant while ignoring its fields. + + fn write(obj: r#{{ e.name() }}, buf: &mut std::vec::Vec<u8>) { + use uniffi::deps::bytes::BufMut; + match obj { + {%- for variant in e.variants() %} + r#{{ e.name() }}::r#{{ variant.name() }}{% if variant.has_fields() %} { {% for field in variant.fields() %}r#{{ field.name() }}, {%- endfor %} }{% else %}{..}{% endif %} => { + buf.put_i32({{ loop.index }}); + {% for field in variant.fields() -%} + {{ field.type_().borrow()|ffi_converter }}::write(r#{{ field.name() }}, buf); + {%- endfor %} + }, + {%- endfor %} + }; + } + + fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result<r#{{ e.name() }}> { + // Note: no need to call should_generate_error_read here, since it is always true for + // non-flat errors + use uniffi::deps::bytes::Buf; + uniffi::check_remaining(buf, 4)?; + Ok(match buf.get_i32() { + {%- for variant in e.variants() %} + {{ loop.index }} => r#{{ e.name() }}::r#{{ variant.name() }}{% if variant.has_fields() %} { + {% for field in variant.fields() %} + r#{{ field.name() }}: {{ field.type_().borrow()|ffi_converter }}::try_read(buf)?, + {%- endfor %} + }{% endif %}, + {%- endfor %} + v => uniffi::deps::anyhow::bail!("Invalid {{ e.name() }} enum value: {}", v), + }) + } + {% endif %} +} + +impl uniffi::FfiError for {{ e.type_().borrow()|ffi_converter_name }} { } diff --git a/third_party/rust/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs new file mode 100644 index 0000000000..cfbe080190 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/ExternalTypesTemplate.rs @@ -0,0 +1,48 @@ +// Support for external types. + +// Types with an external `FfiConverter`... +{% for (name, crate_name) in ci.iter_external_types() %} +// `{{ name }}` is defined in `{{ crate_name }}` +use {{ crate_name|crate_name_rs }}::FfiConverterType{{ name }}; +{% endfor %} + +// For custom scaffolding types we need to generate an FfiConverterType based on the +// UniffiCustomTypeConverter implementation that the library supplies +{% for (name, builtin) in ci.iter_custom_types() %} +{% if loop.first %} + +// A trait that's in our crate for our external wrapped types to implement. +trait UniffiCustomTypeConverter { + type Builtin; + + fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> where Self: Sized; + fn from_custom(obj: Self) -> Self::Builtin; +} + +{%- endif -%} + +// Type `{{ name }}` wraps a `{{ builtin.canonical_name() }}` +#[doc(hidden)] +pub struct FfiConverterType{{ name }}; + +unsafe impl uniffi::FfiConverter for FfiConverterType{{ name }} { + type RustType = r#{{ name }}; + type FfiType = {{ FfiType::from(builtin).borrow()|type_ffi }}; + + fn lower(obj: {{ name }} ) -> Self::FfiType { + <{{ builtin|type_rs }} as uniffi::FfiConverter>::lower(<{{ name }} as UniffiCustomTypeConverter>::from_custom(obj)) + } + + fn try_lift(v: Self::FfiType) -> uniffi::Result<{{ name }}> { + <r#{{ name }} as UniffiCustomTypeConverter>::into_custom(<{{ builtin|type_rs }} as uniffi::FfiConverter>::try_lift(v)?) + } + + fn write(obj: {{ name }}, buf: &mut Vec<u8>) { + <{{ builtin|type_rs }} as uniffi::FfiConverter>::write(<{{ name }} as UniffiCustomTypeConverter>::from_custom(obj), buf); + } + + fn try_read(buf: &mut &[u8]) -> uniffi::Result<r#{{ name }}> { + <{{ name }} as UniffiCustomTypeConverter>::into_custom(<{{ builtin|type_rs }} as uniffi::FfiConverter>::try_read(buf)?) + } +} +{%- endfor -%} diff --git a/third_party/rust/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs new file mode 100644 index 0000000000..3834ad994e --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs @@ -0,0 +1,73 @@ +// For each Object definition, we assume the caller has provided an appropriately-shaped `struct T` +// with an `impl` for each method on the object. We create an `Arc<T>` for "safely" handing out +// references to these structs to foreign language code, and we provide a `pub extern "C"` function +// corresponding to each method. +// +// (Note that "safely" is in "scare quotes" - that's because we use functions on an `Arc` that +// that are inherently unsafe, but the code we generate is safe in practice.) +// +// If the caller's implementation of the struct does not match with the methods or types specified +// in the UDL, then the rust compiler will complain with a (hopefully at least somewhat helpful!) +// error message when processing this generated code. + +{% if obj.uses_deprecated_threadsafe_attribute() %} +// We want to mark this as `deprecated` - long story short, the only way to +// sanely do this using `#[deprecated(..)]` is to generate a function with that +// attribute, then generate call to that function in the object constructors. +#[deprecated( + since = "0.11.0", + note = "The `[Threadsafe]` attribute on interfaces is now the default and its use is deprecated - you should upgrade \ + `{{ obj.name() }}` to remove the `[ThreadSafe]` attribute. \ + See https://github.com/mozilla/uniffi-rs/#thread-safety for more details" +)] +#[allow(non_snake_case)] +fn uniffi_note_threadsafe_deprecation_{{ obj.name() }}() {} +{% endif %} + + +// All Object structs must be `Sync + Send`. The generated scaffolding will fail to compile +// if they are not, but unfortunately it fails with an unactionably obscure error message. +// By asserting the requirement explicitly, we help Rust produce a more scrutable error message +// and thus help the user debug why the requirement isn't being met. +uniffi::deps::static_assertions::assert_impl_all!(r#{{ obj.name() }}: Sync, Send); + +{% let ffi_free = obj.ffi_object_free() -%} +#[doc(hidden)] +#[no_mangle] +pub extern "C" fn {{ ffi_free.name() }}(ptr: *const std::os::raw::c_void, call_status: &mut uniffi::RustCallStatus) { + uniffi::call_with_output(call_status, || { + assert!(!ptr.is_null()); + {#- turn it into an Arc and explicitly drop it. #} + drop(unsafe { std::sync::Arc::from_raw(ptr as *const r#{{ obj.name() }}) }) + }) +} + +{%- for cons in obj.constructors() %} + #[doc(hidden)] + #[no_mangle] + pub extern "C" fn r#{{ cons.ffi_func().name() }}( + {%- call rs::arg_list_ffi_decl(cons.ffi_func()) %}) -> *const std::os::raw::c_void /* *const {{ obj.name() }} */ { + uniffi::deps::log::debug!("{{ cons.ffi_func().name() }}"); + {% if obj.uses_deprecated_threadsafe_attribute() %} + uniffi_note_threadsafe_deprecation_{{ obj.name() }}(); + {% endif %} + + // If the constructor does not have the same signature as declared in the UDL, then + // this attempt to call it will fail with a (somewhat) helpful compiler error. + {% call rs::to_rs_constructor_call(obj, cons) %} + } +{%- endfor %} + +{%- for meth in obj.methods() %} + #[doc(hidden)] + #[no_mangle] + #[allow(clippy::let_unit_value)] // Sometimes we generate code that binds `_retval` to `()`. + pub extern "C" fn r#{{ meth.ffi_func().name() }}( + {%- call rs::arg_list_ffi_decl(meth.ffi_func()) %} + ) {% call rs::return_signature(meth) %} { + uniffi::deps::log::debug!("{{ meth.ffi_func().name() }}"); + // If the method does not have the same signature as declared in the UDL, then + // this attempt to call it will fail with a (somewhat) helpful compiler error. + {% call rs::to_rs_method_call(obj, meth) %} + } +{% endfor %} diff --git a/third_party/rust/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs new file mode 100644 index 0000000000..6ec85ed036 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs @@ -0,0 +1,33 @@ +{# +// For each record declared in the UDL, we assume the caller has provided a corresponding +// rust `struct` with the declared fields. We provide the traits for sending it across the FFI. +// If the caller's struct does not match the shape and types declared in the UDL then the rust +// compiler will complain with a type error. +// +// We define a unit-struct to implement the trait to sidestep Rust's orphan rule (ADR-0006). It's +// public so other crates can refer to it via an `[External='crate'] typedef` +#} + +#[doc(hidden)] +pub struct {{ rec.type_().borrow()|ffi_converter_name }}; + +#[doc(hidden)] +impl uniffi::RustBufferFfiConverter for {{ rec.type_().borrow()|ffi_converter_name }} { + type RustType = r#{{ rec.name() }}; + + fn write(obj: r#{{ rec.name() }}, buf: &mut std::vec::Vec<u8>) { + // If the provided struct doesn't match the fields declared in the UDL, then + // the generated code here will fail to compile with somewhat helpful error. + {%- for field in rec.fields() %} + {{ field.type_().borrow()|ffi_converter }}::write(obj.r#{{ field.name() }}, buf); + {%- endfor %} + } + + fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result<r#{{ rec.name() }}> { + Ok(r#{{ rec.name() }} { + {%- for field in rec.fields() %} + r#{{ field.name() }}: {{ field.type_().borrow()|ffi_converter }}::try_read(buf)?, + {%- endfor %} + }) + } +} diff --git a/third_party/rust/uniffi_bindgen/src/scaffolding/templates/ReexportUniFFIScaffolding.rs b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/ReexportUniFFIScaffolding.rs new file mode 100644 index 0000000000..668f1a2e63 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/ReexportUniFFIScaffolding.rs @@ -0,0 +1,27 @@ +// Code to re-export the UniFFI scaffolding functions. +// +// Rust won't always re-export the functions from dependencies +// ([rust-lang#50007](https://github.com/rust-lang/rust/issues/50007)) +// +// A workaround for this is to have the dependent crate reference a function from its dependency in +// an extern "C" function. This is clearly hacky and brittle, but at least we have some unittests +// that check if this works (fixtures/reexport-scaffolding-macro). +// +// The main way we use this macro is for that contain multiple UniFFI components (libxul, +// megazord). The combined library has a cargo dependency for each component and calls +// uniffi_reexport_scaffolding!() for each one. + +#[doc(hidden)] +pub fn uniffi_reexport_hack() { +} + +#[macro_export] +macro_rules! uniffi_reexport_scaffolding { + () => { + #[doc(hidden)] + #[no_mangle] + pub extern "C" fn {{ ci.namespace() }}_uniffi_reexport_hack() { + $crate::uniffi_reexport_hack() + } + }; +} diff --git a/third_party/rust/uniffi_bindgen/src/scaffolding/templates/RustBuffer.rs b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/RustBuffer.rs new file mode 100644 index 0000000000..39d3f32dcc --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/RustBuffer.rs @@ -0,0 +1,27 @@ +// Everybody gets basic buffer support, since it's needed for passing complex types over the FFI. +// +// See `uniffi/src/ffi/rustbuffer.rs` for documentation on these functions + +#[allow(clippy::missing_safety_doc)] +#[no_mangle] +pub extern "C" fn {{ ci.ffi_rustbuffer_alloc().name() }}(size: i32, call_status: &mut uniffi::RustCallStatus) -> uniffi::RustBuffer { + uniffi::ffi::uniffi_rustbuffer_alloc(size, call_status) +} + +#[allow(clippy::missing_safety_doc)] +#[no_mangle] +pub unsafe extern "C" fn {{ ci.ffi_rustbuffer_from_bytes().name() }}(bytes: uniffi::ForeignBytes, call_status: &mut uniffi::RustCallStatus) -> uniffi::RustBuffer { + uniffi::ffi::uniffi_rustbuffer_from_bytes(bytes, call_status) +} + +#[allow(clippy::missing_safety_doc)] +#[no_mangle] +pub unsafe extern "C" fn {{ ci.ffi_rustbuffer_free().name() }}(buf: uniffi::RustBuffer, call_status: &mut uniffi::RustCallStatus) { + uniffi::ffi::uniffi_rustbuffer_free(buf, call_status) +} + +#[allow(clippy::missing_safety_doc)] +#[no_mangle] +pub unsafe extern "C" fn {{ ci.ffi_rustbuffer_reserve().name() }}(buf: uniffi::RustBuffer, additional: i32, call_status: &mut uniffi::RustCallStatus) -> uniffi::RustBuffer { + uniffi::ffi::uniffi_rustbuffer_reserve(buf, additional, call_status) +} diff --git a/third_party/rust/uniffi_bindgen/src/scaffolding/templates/TopLevelFunctionTemplate.rs b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/TopLevelFunctionTemplate.rs new file mode 100644 index 0000000000..0c6d5a47de --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/TopLevelFunctionTemplate.rs @@ -0,0 +1,17 @@ +{# +// For each top-level function declared in the UDL, we assume the caller has provided a corresponding +// rust function of the same name. We provide a `pub extern "C"` wrapper that does type conversions to +// send data across the FFI, which will fail to compile if the provided function does not match what's +// specified in the UDL. +#} +#[doc(hidden)] +#[no_mangle] +#[allow(clippy::let_unit_value)] // Sometimes we generate code that binds `_retval` to `()`. +pub extern "C" fn r#{{ func.ffi_func().name() }}( + {% call rs::arg_list_ffi_decl(func.ffi_func()) %} +) {% call rs::return_signature(func) %} { + // If the provided function does not match the signature specified in the UDL + // then this attempt to call it will not compile, and will give guidance as to why. + uniffi::deps::log::debug!("{{ func.ffi_func().name() }}"); + {% call rs::to_rs_function_call(func) %} +} diff --git a/third_party/rust/uniffi_bindgen/src/scaffolding/templates/macros.rs b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/macros.rs new file mode 100644 index 0000000000..7ee37c234e --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/macros.rs @@ -0,0 +1,118 @@ +{# +// Template to receive calls into rust. +#} + +{%- macro to_rs_call(func) -%} +r#{{ func.name() }}({% call _arg_list_rs_call(func) -%}) +{%- endmacro -%} + +{%- macro _arg_list_rs_call(func) %} + {%- for arg in func.full_arguments() %} + match {{- arg.type_().borrow()|ffi_converter }}::try_lift(r#{{ arg.name() }}) { + {%- if arg.by_ref() %} + Ok(ref val) => val, + {%- else %} + Ok(val) => val, + {%- endif %} + + {#- If this function returns an error, we attempt to downcast errors doing arg + conversions to this error. If the downcast fails or the function doesn't + return an error, we just panic. + -#} + {%- match func.throws_type() -%} + {%- when Some with (e) %} + Err(err) => return Err(uniffi::lower_anyhow_error_or_panic::<{{ e|ffi_converter_name }}>(err, "{{ arg.name() }}")), + {%- else %} + Err(err) => panic!("Failed to convert arg '{}': {}", "{{ arg.name() }}", err), + {%- endmatch %} + } + {%- if !loop.last %},{% endif %} + {%- endfor %} +{%- endmacro -%} + +{#- +// Arglist as used in the _UniFFILib function declarations. +// Note unfiltered name but type_ffi filters. +-#} +{%- macro arg_list_ffi_decl(func) %} + {%- for arg in func.arguments() %} + r#{{- arg.name() }}: {{ arg.type_().borrow()|type_ffi -}}, + {%- endfor %} + call_status: &mut uniffi::RustCallStatus +{%- endmacro -%} + +{%- macro arg_list_decl_with_prefix(prefix, meth) %} + {{- prefix -}} + {%- if meth.arguments().len() > 0 %}, {# whitespace #} + {%- for arg in meth.arguments() %} + r#{{- arg.name() }}: {{ arg.type_().borrow()|type_rs -}}{% if loop.last %}{% else %},{% endif %} + {%- endfor %} + {%- endif %} +{%- endmacro -%} + +{% macro return_signature(func) %}{% match func.ffi_func().return_type() %}{% when Some with (return_type) %} -> {% call return_type_func(func) %}{%- else -%}{%- endmatch -%}{%- endmacro -%} + +{% macro return_type_func(func) %}{% match func.ffi_func().return_type() %}{% when Some with (return_type) %}{{ return_type|type_ffi }}{%- else -%}(){%- endmatch -%}{%- endmacro -%} + +{% macro ret(func) %}{% match func.return_type() %}{% when Some with (return_type) %}{{ return_type|ffi_converter }}::lower(_retval){% else %}_retval{% endmatch %}{% endmacro %} + +{% macro construct(obj, cons) %} + r#{{- obj.name() }}::{% call to_rs_call(cons) -%} +{% endmacro %} + +{% macro to_rs_constructor_call(obj, cons) %} +{% match cons.throws_type() %} +{% when Some with (e) %} + uniffi::call_with_result(call_status, || { + let _new = {% call construct(obj, cons) %}.map_err(Into::into).map_err({{ e|ffi_converter }}::lower)?; + let _arc = std::sync::Arc::new(_new); + Ok({{ obj.type_().borrow()|ffi_converter }}::lower(_arc)) + }) +{% else %} + uniffi::call_with_output(call_status, || { + let _new = {% call construct(obj, cons) %}; + let _arc = std::sync::Arc::new(_new); + {{ obj.type_().borrow()|ffi_converter }}::lower(_arc) + }) +{% endmatch %} +{% endmacro %} + +{% macro to_rs_method_call(obj, meth) -%} +{% match meth.throws_type() -%} +{% when Some with (e) -%} +uniffi::call_with_result(call_status, || { + let _retval = r#{{ obj.name() }}::{% call to_rs_call(meth) %}.map_err(Into::into).map_err({{ e|ffi_converter }}::lower)?; + Ok({% call ret(meth) %}) +}) +{% else %} +uniffi::call_with_output(call_status, || { + {% match meth.return_type() -%} + {% when Some with (return_type) -%} + let retval = r#{{ obj.name() }}::{% call to_rs_call(meth) %}; + {{ return_type|ffi_converter }}::lower(retval) + {% else -%} + r#{{ obj.name() }}::{% call to_rs_call(meth) %} + {% endmatch -%} +}) +{% endmatch -%} +{% endmacro -%} + +{% macro to_rs_function_call(func) %} +{% match func.throws_type() %} +{% when Some with (e) %} +uniffi::call_with_result(call_status, || { + let _retval = {% call to_rs_call(func) %}.map_err(Into::into).map_err({{ e|ffi_converter }}::lower)?; + Ok({% call ret(func) %}) +}) +{% else %} +uniffi::call_with_output(call_status, || { + {% match func.return_type() -%} + {% when Some with (return_type) -%} + {{ return_type|ffi_converter }}::lower({% call to_rs_call(func) %}) + {% else -%} + {% if func.full_arguments().is_empty() %}#[allow(clippy::redundant_closure)]{% endif %} + {% call to_rs_call(func) %} + {% endmatch -%} +}) +{% endmatch %} +{% endmacro %} diff --git a/third_party/rust/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs new file mode 100644 index 0000000000..87414659ae --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs @@ -0,0 +1,58 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! +{% import "macros.rs" as rs %} + +// Check for compatibility between `uniffi` and `uniffi_bindgen` versions. +// Note that we have an error message on the same line as the assertion. +// This is important, because if the assertion fails, the compiler only +// seems to show that single line as context for the user. +uniffi::assert_compatible_version!("{{ uniffi_version }}"); // Please check that you depend on version {{ uniffi_version }} of the `uniffi` crate. + +{% for ty in ci.iter_types() %} +{%- match ty %} +{%- when Type::Map with (k, v) -%} +{# Next comment MUST be after the line to be in the compiler output #} +uniffi::deps::static_assertions::assert_impl_all!({{ k|type_rs }}: ::std::cmp::Eq, ::std::hash::Hash); // record<{{ k|type_rs }}, {{ v|type_rs }}> +{%- else %} +{%- endmatch %} +{% endfor %} + +{% include "RustBuffer.rs" %} + +// Error definitions, corresponding to `error` in the UDL. +{% for e in ci.error_definitions() %} +{% include "ErrorTemplate.rs" %} +{% endfor %} + +// Enum definitions, corresponding to `enum` in UDL. +{% for e in ci.enum_definitions() %} +{% include "EnumTemplate.rs" %} +{% endfor %} + +// Record definitions, implemented as method-less structs, corresponding to `dictionary` objects. +{% for rec in ci.record_definitions() %} +{% include "RecordTemplate.rs" %} +{% endfor %} + +// Top level functions, corresponding to UDL `namespace` functions. +{%- for func in ci.function_definitions() %} +{% include "TopLevelFunctionTemplate.rs" %} +{% endfor -%} + +// Object definitions, corresponding to UDL `interface` definitions. +{% for obj in ci.object_definitions() %} +{% include "ObjectTemplate.rs" %} +{% endfor %} + +// Callback Interface definitions, corresponding to UDL `callback interface` definitions. +{% for cbi in ci.callback_interface_definitions() %} +{% include "CallbackInterfaceTemplate.rs" %} +{% endfor %} + +// External and Wrapped types +{% include "ExternalTypesTemplate.rs" %} + +// The `reexport_uniffi_scaffolding` macro +{% include "ReexportUniFFIScaffolding.rs" %} + +{%- import "macros.rs" as rs -%} diff --git a/third_party/rust/uniffi_build/.cargo-checksum.json b/third_party/rust/uniffi_build/.cargo-checksum.json new file mode 100644 index 0000000000..c5bdbe8be2 --- /dev/null +++ b/third_party/rust/uniffi_build/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"b0825cba8bdc4e5592b196c95bc55ef417324b440270642b8d6ed7c87f6c853b","src/lib.rs":"e344fb4e5390a17f151456d8d902b554399ef8934744ea4c7aa27f285ab59dfa"},"package":"0ee1a28368ff3d83717e3d3e2e15a66269c43488c3f036914131bb68892f29fb"}
\ No newline at end of file diff --git a/third_party/rust/uniffi_build/Cargo.toml b/third_party/rust/uniffi_build/Cargo.toml new file mode 100644 index 0000000000..c7a0cced61 --- /dev/null +++ b/third_party/rust/uniffi_build/Cargo.toml @@ -0,0 +1,39 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi_build" +version = "0.23.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +description = "a multi-language bindings generator for rust (build script helpers)" +homepage = "https://mozilla.github.io/uniffi-rs" +documentation = "https://mozilla.github.io/uniffi-rs" +keywords = [ + "ffi", + "bindgen", +] +license = "MPL-2.0" +repository = "https://github.com/mozilla/uniffi-rs" + +[dependencies.anyhow] +version = "1" + +[dependencies.camino] +version = "1.0.8" + +[dependencies.uniffi_bindgen] +version = "=0.23.0" +default-features = false + +[features] +builtin-bindgen = [] +default = [] diff --git a/third_party/rust/uniffi_build/src/lib.rs b/third_party/rust/uniffi_build/src/lib.rs new file mode 100644 index 0000000000..6a5bb9bad8 --- /dev/null +++ b/third_party/rust/uniffi_build/src/lib.rs @@ -0,0 +1,31 @@ +/* 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 anyhow::{Context, Result}; +use camino::Utf8Path; +use std::env; + +/// Generate the rust "scaffolding" required to build a uniffi component. +/// +/// Given the path to an UDL file, this function will call the `uniffi-bindgen` +/// command-line tool to generate the `pub extern "C"` functions and other supporting +/// code required to expose the defined interface from Rust. The expectation is that +/// this will be called from a crate's build script, and the resulting file will +/// be `include!()`ed into the build. +/// +/// Given an UDL file named `example.udl`, the generated scaffolding will be written +/// into a file named `example.uniffi.rs` in the `$OUT_DIR` directory. +pub fn generate_scaffolding(udl_file: impl AsRef<Utf8Path>) -> Result<()> { + let udl_file = udl_file.as_ref(); + + println!("cargo:rerun-if-changed={udl_file}"); + // The UNIFFI_TESTS_DISABLE_EXTENSIONS variable disables some bindings, but it is evaluated + // at *build* time, so we need to rebuild when it changes. + println!("cargo:rerun-if-env-changed=UNIFFI_TESTS_DISABLE_EXTENSIONS"); + // Why don't we just depend on uniffi-bindgen and call the public functions? + // Calling the command line helps making sure that the generated swift/Kotlin/whatever + // bindings were generated with the same version of uniffi as the Rust scaffolding code. + let out_dir = env::var("OUT_DIR").context("$OUT_DIR missing?!")?; + uniffi_bindgen::generate_component_scaffolding(udl_file, None, Some(out_dir.as_ref()), false) +} diff --git a/third_party/rust/uniffi_checksum_derive/.cargo-checksum.json b/third_party/rust/uniffi_checksum_derive/.cargo-checksum.json new file mode 100644 index 0000000000..659eac132f --- /dev/null +++ b/third_party/rust/uniffi_checksum_derive/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"6754bdd7074b1fda85a100034806a9fe0e66f3a04606a5e514fd3a80736259a6","src/lib.rs":"c68c69a1cf6a69e5fe78f7b069364a265c5bb6ce8c0abf0b5745eca01c79604a"},"package":"03de61393a42b4ad4984a3763c0600594ac3e57e5aaa1d05cede933958987c03"}
\ No newline at end of file diff --git a/third_party/rust/uniffi_checksum_derive/Cargo.toml b/third_party/rust/uniffi_checksum_derive/Cargo.toml new file mode 100644 index 0000000000..236e6f56ab --- /dev/null +++ b/third_party/rust/uniffi_checksum_derive/Cargo.toml @@ -0,0 +1,42 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi_checksum_derive" +version = "0.23.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +description = "a multi-language bindings generator for rust (checksum custom derive)" +homepage = "https://mozilla.github.io/uniffi-rs" +documentation = "https://mozilla.github.io/uniffi-rs" +keywords = [ + "ffi", + "bindgen", +] +license = "MPL-2.0" +repository = "https://github.com/mozilla/uniffi-rs" + +[lib] +proc-macro = true + +[dependencies.quote] +version = "1.0" + +[dependencies.syn] +version = "1.0" +features = [ + "derive", + "parsing", +] + +[features] +default = [] +nightly = [] diff --git a/third_party/rust/uniffi_checksum_derive/src/lib.rs b/third_party/rust/uniffi_checksum_derive/src/lib.rs new file mode 100644 index 0000000000..2fefdb2574 --- /dev/null +++ b/third_party/rust/uniffi_checksum_derive/src/lib.rs @@ -0,0 +1,134 @@ +/* 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/. */ +#![cfg_attr(feature = "nightly", feature(proc_macro_expand))] + +//! Custom derive for uniffi_meta::Checksum + +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, Attribute, Data, DeriveInput, Expr, ExprLit, Fields, Index, Lit}; + +fn has_ignore_attribute(attrs: &[Attribute]) -> bool { + attrs.iter().any(|attr| { + if attr.path.is_ident("checksum_ignore") { + if !attr.tokens.is_empty() { + panic!("#[checksum_ignore] doesn't accept extra information"); + } + true + } else { + false + } + }) +} + +#[proc_macro_derive(Checksum, attributes(checksum_ignore))] +pub fn checksum_derive(input: TokenStream) -> TokenStream { + let input: DeriveInput = parse_macro_input!(input); + + let name = input.ident; + + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let code = match input.data { + Data::Enum(enum_) + if enum_.variants.len() == 1 + && enum_ + .variants + .iter() + .all(|variant| matches!(variant.fields, Fields::Unit)) => + { + quote!() + } + Data::Enum(enum_) => { + let mut next_discriminant = 0u64; + let match_inner = enum_.variants.iter().map(|variant| { + let ident = &variant.ident; + if has_ignore_attribute(&variant.attrs) { + panic!("#[checksum_ignore] is not supported in enums"); + } + match &variant.discriminant { + Some((_, Expr::Lit(ExprLit { lit: Lit::Int(value), .. }))) => { + next_discriminant = value.base10_parse::<u64>().unwrap(); + } + Some(_) => { + panic!("#[derive(Checksum)] doesn't support non-numeric explicit discriminants in enums"); + } + None => {} + } + let discriminant = quote! { state.write(&#next_discriminant.to_le_bytes()) }; + next_discriminant += 1; + match &variant.fields { + Fields::Unnamed(fields) => { + let field_idents = fields + .unnamed + .iter() + .enumerate() + .map(|(num, _)| format_ident!("__self_{}", num)); + let field_stmts = field_idents + .clone() + .map(|ident| quote! { Checksum::checksum(#ident, state); }); + quote! { + Self::#ident(#(#field_idents,)*) => { + #discriminant; + #(#field_stmts)* + } + } + } + Fields::Named(fields) => { + let field_idents = fields + .named + .iter() + .map(|field| field.ident.as_ref().unwrap()); + let field_stmts = field_idents + .clone() + .map(|ident| quote! { Checksum::checksum(#ident, state); }); + quote! { + Self::#ident { #(#field_idents,)* } => { + #discriminant; + #(#field_stmts)* + } + } + } + Fields::Unit => quote! { Self::#ident => #discriminant, }, + } + }); + quote! { + match self { + #(#match_inner)* + } + } + } + Data::Struct(struct_) => { + let stmts = struct_ + .fields + .iter() + .enumerate() + .filter_map(|(num, field)| { + (!has_ignore_attribute(&field.attrs)).then(|| match field.ident.as_ref() { + Some(ident) => quote! { Checksum::checksum(&self.#ident, state); }, + None => { + let i = Index::from(num); + quote! { Checksum::checksum(&self.#i, state); } + } + }) + }); + quote! { + #(#stmts)* + } + } + Data::Union(_) => { + panic!("#[derive(Checksum)] is not supported for unions"); + } + }; + + quote! { + #[automatically_derived] + impl #impl_generics Checksum for #name #ty_generics #where_clause { + fn checksum<__H: ::core::hash::Hasher>(&self, state: &mut __H) { + #code + } + } + } + .into() +} diff --git a/third_party/rust/uniffi_core/.cargo-checksum.json b/third_party/rust/uniffi_core/.cargo-checksum.json new file mode 100644 index 0000000000..eac1ca0334 --- /dev/null +++ b/third_party/rust/uniffi_core/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"60d3960c22668433dd40198e39b2e6fbe584a041e05929dd243a1836979aa685","release.toml":"661f6aa0b76c438ea2ca1138594286482444d345c69414854acfcb3812fa42b9","src/ffi/ffidefault.rs":"c7ab752fffed17c3fabb60e818ad1d093482f95dd0bdeae6871287695c583e48","src/ffi/foreignbytes.rs":"37061e2da7135576abccb86fe27b4fefc054586a040f2ca81fe9858d5649e887","src/ffi/foreigncallbacks.rs":"d28fec5caf3fce7c52e230df061551c51064105a8e2b8ca3fc1b6b02073f6704","src/ffi/mod.rs":"3fb3b74607066e0052fc91168e9473dbf82dbae562f85c33774a7f5f6b350616","src/ffi/rustbuffer.rs":"b773637d9e4651b80cd16f7a02ed75846d02ce0a9e32b718ce644cdba3a83cdd","src/ffi/rustcalls.rs":"4a85a90b0d46974ab9e80e403a1cddaa252337b227e1a672bb6fdbbca5a66259","src/lib.rs":"6cb9e1d4f0a9e0acb1f65044f7af26ce1320a3ce19af363feb90b1c9789846d7","src/panichook.rs":"9f49c7994a8e5489c1105c488bb3f8c5571bc5f813e7be90441eca15da5c9851"},"package":"7a2b4852d638d74ca2d70e450475efb6d91fe6d54a7cd8d6bd80ad2ee6cd7daa"}
\ No newline at end of file diff --git a/third_party/rust/uniffi_core/Cargo.toml b/third_party/rust/uniffi_core/Cargo.toml new file mode 100644 index 0000000000..6dcfd8fdcd --- /dev/null +++ b/third_party/rust/uniffi_core/Cargo.toml @@ -0,0 +1,52 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi_core" +version = "0.23.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +description = "a multi-language bindings generator for rust (runtime support code)" +homepage = "https://mozilla.github.io/uniffi-rs" +documentation = "https://mozilla.github.io/uniffi-rs" +keywords = [ + "ffi", + "bindgen", +] +license = "MPL-2.0" +repository = "https://github.com/mozilla/uniffi-rs" + +[dependencies.anyhow] +version = "1" + +[dependencies.bytes] +version = "1.0" + +[dependencies.camino] +version = "1.0.8" + +[dependencies.cargo_metadata] +version = "0.15" + +[dependencies.log] +version = "0.4" + +[dependencies.once_cell] +version = "1.12" + +[dependencies.paste] +version = "1.0" + +[dependencies.static_assertions] +version = "1.1.0" + +[features] +default = [] diff --git a/third_party/rust/uniffi_core/release.toml b/third_party/rust/uniffi_core/release.toml new file mode 100644 index 0000000000..a06da056ec --- /dev/null +++ b/third_party/rust/uniffi_core/release.toml @@ -0,0 +1,16 @@ +# Note that this `release.toml` exists to capture things that must only be +# done once for `cargo release-backend-crates`. +# +# [../uniffi/release.toml](../uniffi/release.toml) captures things that must only be done for `cargo release-uniffi` +# +# All other config exists in [../release.toml](../release.toml). + +tag = false + +# This is how we manage the sections in CHANGELOG.md +pre-release-replacements = [ + {file="../CHANGELOG.md", search="\\[\\[UnreleasedBackendVersion\\]\\]", replace="v{{version}}", exactly=1}, + {file="../CHANGELOG.md", search="\\[\\[ReleaseDate\\]\\]", replace="{{date}}", exactly=1}, + {file="../CHANGELOG.md", search="\\.\\.\\.HEAD\\)", replace="...{{tag_name}})", exactly=1}, + {file="../CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n\n## [[UnreleasedUniFFIVersion]] (backend crates: [[UnreleasedBackendVersion]]) - (_[[ReleaseDate]]_)\n\n[All changes in [[UnreleasedVersion]]](https://github.com/mozilla/uniffi-rs/compare/v{{version}}...HEAD).", exactly=1}, +] diff --git a/third_party/rust/uniffi_core/src/ffi/ffidefault.rs b/third_party/rust/uniffi_core/src/ffi/ffidefault.rs new file mode 100644 index 0000000000..f247312be8 --- /dev/null +++ b/third_party/rust/uniffi_core/src/ffi/ffidefault.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/. */ + +//! FfiDefault trait +//! +//! When we make a FFI call into Rust we always need to return a value, even if that value will be +//! ignored because we're flagging an exception. This trait defines what that value is for our +//! supported FFI types. + +use paste::paste; + +pub trait FfiDefault { + fn ffi_default() -> Self; +} + +// Most types can be handled by delegating to Default +macro_rules! impl_ffi_default_with_default { + ($($T:ty,)+) => { impl_ffi_default_with_default!($($T),+); }; + ($($T:ty),*) => { + $( + paste! { + impl FfiDefault for $T { + fn ffi_default() -> Self { + $T::default() + } + } + } + )* + }; +} + +impl_ffi_default_with_default! { + i8, u8, i16, u16, i32, u32, i64, u64, f32, f64 +} + +// Implement FfiDefault for the remaining types +impl FfiDefault for () { + fn ffi_default() {} +} + +impl FfiDefault for *const std::ffi::c_void { + fn ffi_default() -> Self { + std::ptr::null() + } +} + +impl FfiDefault for crate::RustBuffer { + fn ffi_default() -> Self { + unsafe { Self::from_raw_parts(std::ptr::null_mut(), 0, 0) } + } +} diff --git a/third_party/rust/uniffi_core/src/ffi/foreignbytes.rs b/third_party/rust/uniffi_core/src/ffi/foreignbytes.rs new file mode 100644 index 0000000000..5ec93118ad --- /dev/null +++ b/third_party/rust/uniffi_core/src/ffi/foreignbytes.rs @@ -0,0 +1,118 @@ +/* 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/. */ + +/// Support for reading a slice of foreign-language-allocated bytes over the FFI. +/// +/// Foreign language code can pass a slice of bytes by providing a data pointer +/// and length, and this struct provides a convenient wrapper for working with +/// that pair. Naturally, this can be tremendously unsafe! So here are the details: +/// +/// * The foreign language code must ensure the provided buffer stays alive +/// and unchanged for the duration of the call to which the `ForeignBytes` +/// struct was provided. +/// +/// To work with the bytes in Rust code, use `as_slice()` to view the data +/// as a `&[u8]`. +/// +/// Implementation note: all the fields of this struct are private and it has no +/// constructors, so consuming crates cant create instances of it. If you've +/// got a `ForeignBytes`, then you received it over the FFI and are assuming that +/// the foreign language code is upholding the above invariants. +/// +/// This struct is based on `ByteBuffer` from the `ffi-support` crate, but modified +/// to give a read-only view of externally-provided bytes. +#[repr(C)] +pub struct ForeignBytes { + /// The length of the pointed-to data. + /// We use an `i32` for compatibility with JNA. + len: i32, + /// The pointer to the foreign-owned bytes. + data: *const u8, +} + +impl ForeignBytes { + /// Creates a `ForeignBytes` from its constituent fields. + /// + /// This is intended mainly as an internal convenience function and should not + /// be used outside of this module. + /// + /// # Safety + /// + /// You must ensure that the raw parts uphold the documented invariants of this class. + pub unsafe fn from_raw_parts(data: *const u8, len: i32) -> Self { + Self { len, data } + } + + /// View the foreign bytes as a `&[u8]`. + /// + /// # Panics + /// + /// Panics if the provided struct has a null pointer but non-zero length. + /// Panics if the provided length is negative. + pub fn as_slice(&self) -> &[u8] { + if self.data.is_null() { + assert!(self.len == 0, "null ForeignBytes had non-zero length"); + &[] + } else { + unsafe { std::slice::from_raw_parts(self.data, self.len()) } + } + } + + /// Get the length of this slice of bytes. + /// + /// # Panics + /// + /// Panics if the provided length is negative. + pub fn len(&self) -> usize { + self.len + .try_into() + .expect("bytes length negative or overflowed") + } + + /// Returns true if the length of this slice of bytes is 0. + pub fn is_empty(&self) -> bool { + self.len == 0 + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_foreignbytes_access() { + let v = vec![1u8, 2, 3]; + let fbuf = unsafe { ForeignBytes::from_raw_parts(v.as_ptr(), 3) }; + assert_eq!(fbuf.len(), 3); + assert_eq!(fbuf.as_slice(), &[1u8, 2, 3]); + } + + #[test] + fn test_foreignbytes_empty() { + let v = Vec::<u8>::new(); + let fbuf = unsafe { ForeignBytes::from_raw_parts(v.as_ptr(), 0) }; + assert_eq!(fbuf.len(), 0); + assert_eq!(fbuf.as_slice(), &[0u8; 0]); + } + + #[test] + fn test_foreignbytes_null_means_empty() { + let fbuf = unsafe { ForeignBytes::from_raw_parts(std::ptr::null_mut(), 0) }; + assert_eq!(fbuf.as_slice(), &[0u8; 0]); + } + + #[test] + #[should_panic] + fn test_foreignbytes_null_must_have_zero_length() { + let fbuf = unsafe { ForeignBytes::from_raw_parts(std::ptr::null_mut(), 12) }; + fbuf.as_slice(); + } + + #[test] + #[should_panic] + fn test_foreignbytes_provided_len_must_be_non_negative() { + let v = vec![0u8, 1, 2]; + let fbuf = unsafe { ForeignBytes::from_raw_parts(v.as_ptr(), -1) }; + fbuf.as_slice(); + } +} diff --git a/third_party/rust/uniffi_core/src/ffi/foreigncallbacks.rs b/third_party/rust/uniffi_core/src/ffi/foreigncallbacks.rs new file mode 100644 index 0000000000..9d513353a6 --- /dev/null +++ b/third_party/rust/uniffi_core/src/ffi/foreigncallbacks.rs @@ -0,0 +1,229 @@ +/* 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/. */ + +//! Callback interfaces are traits specified in UDL which can be implemented by foreign languages. +//! +//! # Using callback interfaces +//! +//! 1. Define a Rust trait. +//! +//! This toy example defines a way of Rust accessing a key-value store exposed +//! by the host operating system (e.g. the key chain). +//! +//! ``` +//! trait Keychain: Send { +//! fn get(&self, key: String) -> Option<String>; +//! fn put(&self, key: String, value: String); +//! } +//! ``` +//! +//! 2. Define a callback interface in the UDL +//! +//! ```idl +//! callback interface Keychain { +//! string? get(string key); +//! void put(string key, string data); +//! }; +//! ``` +//! +//! 3. And allow it to be passed into Rust. +//! +//! Here, we define a constructor to pass the keychain to rust, and then another method +//! which may use it. +//! +//! In UDL: +//! ```idl +//! object Authenticator { +//! constructor(Keychain keychain); +//! void login(); +//! } +//! ``` +//! +//! In Rust: +//! +//! ``` +//!# trait Keychain: Send { +//!# fn get(&self, key: String) -> Option<String>; +//!# fn put(&self, key: String, value: String); +//!# } +//! struct Authenticator { +//! keychain: Box<dyn Keychain>, +//! } +//! +//! impl Authenticator { +//! pub fn new(keychain: Box<dyn Keychain>) -> Self { +//! Self { keychain } +//! } +//! pub fn login(&self) { +//! let username = self.keychain.get("username".into()); +//! let password = self.keychain.get("password".into()); +//! } +//! } +//! ``` +//! 4. Create an foreign language implementation of the callback interface. +//! +//! In this example, here's a Kotlin implementation. +//! +//! ```kotlin +//! class AndroidKeychain: Keychain { +//! override fun get(key: String): String? { +//! // … elide the implementation. +//! return value +//! } +//! override fun put(key: String) { +//! // … elide the implementation. +//! } +//! } +//! ``` +//! 5. Pass the implementation to Rust. +//! +//! Again, in Kotlin +//! +//! ```kotlin +//! val authenticator = Authenticator(AndroidKeychain()) +//! authenticator.login() +//! ``` +//! +//! # How it works. +//! +//! ## High level +//! +//! Uniffi generates a protocol or interface in client code in the foreign language must implement. +//! +//! For each callback interface, a `CallbackInternals` (on the Foreign Language side) and `ForeignCallbackInternals` +//! (on Rust side) manages the process through a `ForeignCallback`. There is one `ForeignCallback` per callback interface. +//! +//! Passing a callback interface implementation from foreign language (e.g. `AndroidKeychain`) into Rust causes the +//! `KeychainCallbackInternals` to store the instance in a handlemap. +//! +//! The object handle is passed over to Rust, and used to instantiate a struct `KeychainProxy` which implements +//! the trait. This proxy implementation is generate by Uniffi. The `KeychainProxy` object is then passed to +//! client code as `Box<dyn Keychain>`. +//! +//! Methods on `KeychainProxy` objects (e.g. `self.keychain.get("username".into())`) encode the arguments into a `RustBuffer`. +//! Using the `ForeignCallback`, it calls the `CallbackInternals` object on the foreign language side using the +//! object handle, and the method selector. +//! +//! The `CallbackInternals` object unpacks the arguments from the passed buffer, gets the object out from the handlemap, +//! and calls the actual implementation of the method. +//! +//! If there's a return value, it is packed up in to another `RustBuffer` and used as the return value for +//! `ForeignCallback`. The caller of `ForeignCallback`, the `KeychainProxy` unpacks the returned buffer into the correct +//! type and then returns to client code. +//! + +use super::RustBuffer; +use std::fmt; +use std::os::raw::c_int; +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// ForeignCallback is the Rust representation of a foreign language function. +/// It is the basis for all callbacks interfaces. It is registered exactly once per callback interface, +/// at library start up time. +/// Calling this method is only done by generated objects which mirror callback interfaces objects in the foreign language. +/// +/// * The `handle` is the key into a handle map on the other side of the FFI used to look up the foreign language object +/// that implements the callback interface/trait. +/// * The `method` selector specifies the method that will be called on the object, by looking it up in a list of methods from +/// the IDL. The index is 1 indexed. Note that the list of methods is generated by at uniffi from the IDL and used in all +/// bindings: so we can rely on the method list being stable within the same run of uniffi. +/// * `args` is a serialized buffer of arguments to the function. UniFFI will deserialize it before +/// passing individual arguments to the user's callback. +/// * `buf_ptr` is a pointer to where the resulting buffer will be written. UniFFI will allocate a +/// buffer to write the result into. +/// * A callback returns: +/// - `-2` An error occurred that was serialized to buf_ptr +/// - `-1` An unexpected error occurred +/// - `0` is a deprecated way to signal that if the call succeeded, but did not modify buf_ptr +/// - `1` If the call succeeded. For non-void functions the return value should be serialized +/// to buf_ptr. +/// Note: The output buffer might still contain 0 bytes of data. +pub type ForeignCallback = unsafe extern "C" fn( + handle: u64, + method: u32, + args: RustBuffer, + buf_ptr: *mut RustBuffer, +) -> c_int; + +/// The method index used by the Drop trait to communicate to the foreign language side that Rust has finished with it, +/// and it can be deleted from the handle map. +pub const IDX_CALLBACK_FREE: u32 = 0; + +// Overly-paranoid sanity checking to ensure that these types are +// convertible between each-other. `transmute` actually should check this for +// us too, but this helps document the invariants we rely on in this code. +// +// Note that these are guaranteed by +// https://rust-lang.github.io/unsafe-code-guidelines/layout/function-pointers.html +// and thus this is a little paranoid. +static_assertions::assert_eq_size!(usize, ForeignCallback); +static_assertions::assert_eq_size!(usize, Option<ForeignCallback>); + +/// Struct to hold a foreign callback. +pub struct ForeignCallbackInternals { + callback_ptr: AtomicUsize, +} + +const EMPTY_PTR: usize = 0; + +impl ForeignCallbackInternals { + pub const fn new() -> Self { + ForeignCallbackInternals { + callback_ptr: AtomicUsize::new(EMPTY_PTR), + } + } + + pub fn set_callback(&self, callback: ForeignCallback) { + let as_usize = callback as usize; + let old_ptr = self.callback_ptr.compare_exchange( + EMPTY_PTR, + as_usize, + Ordering::SeqCst, + Ordering::SeqCst, + ); + match old_ptr { + // We get the previous value back. If this is anything except EMPTY_PTR, + // then this has been set before we get here. + Ok(EMPTY_PTR) => (), + _ => + // This is an internal bug, the other side of the FFI should ensure + // it sets this only once. + { + panic!("Bug: call set_callback multiple times. This is likely a uniffi bug") + } + }; + } + + pub fn get_callback(&self) -> Option<ForeignCallback> { + let ptr_value = self.callback_ptr.load(Ordering::SeqCst); + unsafe { std::mem::transmute::<usize, Option<ForeignCallback>>(ptr_value) } + } +} + +/// Used when internal/unexpected error happened when calling a foreign callback, for example when +/// a unknown exception is raised +/// +/// User callback error types must implement a From impl from this type to their own error type. +#[derive(Debug)] +pub struct UnexpectedUniFFICallbackError { + pub reason: String, +} + +impl UnexpectedUniFFICallbackError { + pub fn from_reason(reason: String) -> Self { + Self { reason } + } +} + +impl fmt::Display for UnexpectedUniFFICallbackError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "UnexpectedUniFFICallbackError(reason: {:?})", + self.reason + ) + } +} + +impl std::error::Error for UnexpectedUniFFICallbackError {} diff --git a/third_party/rust/uniffi_core/src/ffi/mod.rs b/third_party/rust/uniffi_core/src/ffi/mod.rs new file mode 100644 index 0000000000..73ee721435 --- /dev/null +++ b/third_party/rust/uniffi_core/src/ffi/mod.rs @@ -0,0 +1,15 @@ +/* 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/. */ + +pub mod ffidefault; +pub mod foreignbytes; +pub mod foreigncallbacks; +pub mod rustbuffer; +pub mod rustcalls; + +use ffidefault::FfiDefault; +pub use foreignbytes::*; +pub use foreigncallbacks::*; +pub use rustbuffer::*; +pub use rustcalls::*; diff --git a/third_party/rust/uniffi_core/src/ffi/rustbuffer.rs b/third_party/rust/uniffi_core/src/ffi/rustbuffer.rs new file mode 100644 index 0000000000..63af586fb6 --- /dev/null +++ b/third_party/rust/uniffi_core/src/ffi/rustbuffer.rs @@ -0,0 +1,353 @@ +/* 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 crate::ffi::{call_with_output, ForeignBytes, RustCallStatus}; + +/// Support for passing an allocated-by-Rust buffer of bytes over the FFI. +/// +/// We can pass a `Vec<u8>` to foreign language code by decomposing it into +/// its raw parts (buffer pointer, length, and capacity) and passing those +/// around as a struct. Naturally, this can be tremendously unsafe! So here +/// are the details: +/// +/// * `RustBuffer` structs must only ever be constructed from a `Vec<u8>`, +/// either explicitly via `RustBuffer::from_vec` or indirectly by calling +/// one of the `RustBuffer::new*` constructors. +/// +/// * `RustBuffer` structs do not implement `Drop`, since they are intended +/// to be passed to foreign-language code outside of the control of Rust's +/// ownership system. To avoid memory leaks they *must* passed back into +/// Rust and either explicitly destroyed using `RustBuffer::destroy`, or +/// converted back to a `Vec<u8>` using `RustBuffer::destroy_into_vec` +/// (which will then be dropped via Rust's usual ownership-tracking system). +/// +/// Foreign-language code should not construct `RustBuffer` structs other than +/// by receiving them from a call into the Rust code, and should not modify them +/// apart from the following safe operations: +/// +/// * Writing bytes into the buffer pointed to by `data`, without writing +/// beyond the indicated `capacity`. +/// +/// * Adjusting the `len` property to indicate the amount of data written, +/// while ensuring that 0 <= `len` <= `capacity`. +/// +/// * As a special case, constructing a `RustBuffer` with zero capacity, zero +/// length, and a null `data` pointer to indicate an empty buffer. +/// +/// In particular, it is not safe for foreign-language code to construct a `RustBuffer` +/// that points to its own allocated memory; use the `ForeignBytes` struct to +/// pass a view of foreign-owned memory in to Rust code. +/// +/// Implementation note: all the fields of this struct are private, so you can't +/// manually construct instances that don't come from a `Vec<u8>`. If you've got +/// a `RustBuffer` then it either came from a public constructor (all of which +/// are safe) or it came from foreign-language code (which should have in turn +/// received it by calling some Rust function, and should be respecting the +/// invariants listed above). +/// +/// This struct is based on `ByteBuffer` from the `ffi-support` crate, but modified +/// to retain unallocated capacity rather than truncating to the occupied length. +#[repr(C)] +pub struct RustBuffer { + /// The allocated capacity of the underlying `Vec<u8>`. + /// In Rust this is a `usize`, but we use an `i32` for compatibility with JNA. + capacity: i32, + /// The occupied length of the underlying `Vec<u8>`. + /// In Rust this is a `usize`, but we use an `i32` for compatibility with JNA. + len: i32, + /// The pointer to the allocated buffer of the `Vec<u8>`. + data: *mut u8, +} + +impl RustBuffer { + /// Creates an empty `RustBuffer`. + /// + /// The buffer will not allocate. + /// The resulting vector will not be automatically dropped; you must + /// arrange to call `destroy` or `destroy_into_vec` when finished with it. + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a `RustBuffer` from its constituent fields. + /// + /// This is intended mainly as an internal convenience function and should not + /// be used outside of this module. + /// + /// # Safety + /// + /// You must ensure that the raw parts uphold the documented invariants of this class. + pub unsafe fn from_raw_parts(data: *mut u8, len: i32, capacity: i32) -> Self { + Self { + capacity, + len, + data, + } + } + + /// Get the current length of the buffer, as a `usize`. + /// + /// This is mostly a helper function to convert the `i32` length field + /// into a `usize`, which is what Rust code usually expects. + /// + /// # Panics + /// + /// Panics if called on an invalid struct obtained from foreign-language code, + /// in which the `len` field is negative. + pub fn len(&self) -> usize { + self.len + .try_into() + .expect("buffer length negative or overflowed") + } + + /// Returns true if the length of the buffer is 0. + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Creates a `RustBuffer` zero-filed to the requested size. + /// + /// The resulting vector will not be automatically dropped; you must + /// arrange to call `destroy` or `destroy_into_vec` when finished with it. + /// + /// # Panics + /// + /// Panics if the requested size is too large to fit in an `i32`, and + /// hence would risk incompatibility with some foreign-language code. + pub fn new_with_size(size: usize) -> Self { + assert!( + size < i32::MAX as usize, + "RustBuffer requested size too large" + ); + Self::from_vec(vec![0u8; size]) + } + + /// Consumes a `Vec<u8>` and returns its raw parts as a `RustBuffer`. + /// + /// The resulting vector will not be automatically dropped; you must + /// arrange to call `destroy` or `destroy_into_vec` when finished with it. + /// + /// # Panics + /// + /// Panics if the vector's length or capacity are too large to fit in an `i32`, + /// and hence would risk incompatibility with some foreign-language code. + pub fn from_vec(v: Vec<u8>) -> Self { + let capacity = i32::try_from(v.capacity()).expect("buffer capacity cannot fit into a i32."); + let len = i32::try_from(v.len()).expect("buffer length cannot fit into a i32."); + let mut v = std::mem::ManuallyDrop::new(v); + unsafe { Self::from_raw_parts(v.as_mut_ptr(), len, capacity) } + } + + /// Converts this `RustBuffer` back into an owned `Vec<u8>`. + /// + /// This restores ownership of the underlying buffer to Rust, meaning it will + /// be dropped when the `Vec<u8>` is dropped. The `RustBuffer` *must* have been + /// previously obtained from a valid `Vec<u8>` owned by this Rust code. + /// + /// # Panics + /// + /// Panics if called on an invalid struct obtained from foreign-language code, + /// which does not respect the invairiants on `len` and `capacity`. + pub fn destroy_into_vec(self) -> Vec<u8> { + // Rust will never give us a null `data` pointer for a `Vec`, but + // foreign-language code can use it to cheaply pass an empty buffer. + if self.data.is_null() { + assert!(self.capacity == 0, "null RustBuffer had non-zero capacity"); + assert!(self.len == 0, "null RustBuffer had non-zero length"); + vec![] + } else { + let capacity: usize = self + .capacity + .try_into() + .expect("buffer capacity negative or overflowed"); + let len: usize = self + .len + .try_into() + .expect("buffer length negative or overflowed"); + assert!(len <= capacity, "RustBuffer length exceeds capacity"); + unsafe { Vec::from_raw_parts(self.data, len, capacity) } + } + } + + /// Reclaim memory stored in this `RustBuffer`. + /// + /// # Panics + /// + /// Panics if called on an invalid struct obtained from foreign-language code, + /// which does not respect the invairiants on `len` and `capacity`. + pub fn destroy(self) { + drop(self.destroy_into_vec()); + } +} + +impl Default for RustBuffer { + fn default() -> Self { + Self::new() + } +} + +// extern "C" functions for the RustBuffer functionality. +// +// These are used in two ways: +// 1. Code that statically links to UniFFI can use these directly to handle RustBuffer +// allocation/destruction. The plan is to use this for the Firefox desktop JS bindings. +// +// 2. The scaffolding code re-exports these functions, prefixed with the component name and UDL +// hash This creates a separate set of functions for each UniFFIed component, which is needed +// in the case where we create multiple dylib artifacts since each dylib will have its own +// allocator. + +/// This helper allocates a new byte buffer owned by the Rust code, and returns it +/// to the foreign-language code as a `RustBuffer` struct. Callers must eventually +/// free the resulting buffer, either by explicitly calling [`uniffi_rustbuffer_free`] defined +/// below, or by passing ownership of the buffer back into Rust code. +#[no_mangle] +pub extern "C" fn uniffi_rustbuffer_alloc( + size: i32, + call_status: &mut RustCallStatus, +) -> RustBuffer { + call_with_output(call_status, || { + RustBuffer::new_with_size(size.max(0) as usize) + }) +} + +/// This helper copies bytes owned by the foreign-language code into a new byte buffer owned +/// by the Rust code, and returns it as a `RustBuffer` struct. Callers must eventually +/// free the resulting buffer, either by explicitly calling the destructor defined below, +/// or by passing ownership of the buffer back into Rust code. +/// +/// # Safety +/// This function will dereference a provided pointer in order to copy bytes from it, so +/// make sure the `ForeignBytes` struct contains a valid pointer and length. +#[no_mangle] +pub unsafe extern "C" fn uniffi_rustbuffer_from_bytes( + bytes: ForeignBytes, + call_status: &mut RustCallStatus, +) -> RustBuffer { + call_with_output(call_status, || { + let bytes = bytes.as_slice(); + RustBuffer::from_vec(bytes.to_vec()) + }) +} + +/// Free a byte buffer that had previously been passed to the foreign language code. +/// +/// # Safety +/// The argument *must* be a uniquely-owned `RustBuffer` previously obtained from a call +/// into the Rust code that returned a buffer, or you'll risk freeing unowned memory or +/// corrupting the allocator state. +#[no_mangle] +pub unsafe extern "C" fn uniffi_rustbuffer_free(buf: RustBuffer, call_status: &mut RustCallStatus) { + call_with_output(call_status, || RustBuffer::destroy(buf)) +} + +/// Reserve additional capacity in a byte buffer that had previously been passed to the +/// foreign language code. +/// +/// The first argument *must* be a uniquely-owned `RustBuffer` previously +/// obtained from a call into the Rust code that returned a buffer. Its underlying data pointer +/// will be reallocated if necessary and returned in a new `RustBuffer` struct. +/// +/// The second argument must be the minimum number of *additional* bytes to reserve +/// capacity for in the buffer; it is likely to reserve additional capacity in practice +/// due to amortized growth strategy of Rust vectors. +/// +/// # Safety +/// The first argument *must* be a uniquely-owned `RustBuffer` previously obtained from a call +/// into the Rust code that returned a buffer, or you'll risk freeing unowned memory or +/// corrupting the allocator state. +#[no_mangle] +pub unsafe extern "C" fn uniffi_rustbuffer_reserve( + buf: RustBuffer, + additional: i32, + call_status: &mut RustCallStatus, +) -> RustBuffer { + call_with_output(call_status, || { + let additional: usize = additional + .try_into() + .expect("additional buffer length negative or overflowed"); + let mut v = buf.destroy_into_vec(); + v.reserve(additional); + RustBuffer::from_vec(v) + }) +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_rustbuffer_from_vec() { + let rbuf = RustBuffer::from_vec(vec![1u8, 2, 3]); + assert_eq!(rbuf.len(), 3); + assert_eq!(rbuf.destroy_into_vec(), vec![1u8, 2, 3]); + } + + #[test] + fn test_rustbuffer_empty() { + let rbuf = RustBuffer::new(); + assert_eq!(rbuf.len(), 0); + // Rust will never give us a null pointer, even for an empty buffer. + assert!(!rbuf.data.is_null()); + assert_eq!(rbuf.destroy_into_vec(), Vec::<u8>::new()); + } + + #[test] + fn test_rustbuffer_new_with_size() { + let rbuf = RustBuffer::new_with_size(5); + assert_eq!(rbuf.destroy_into_vec().as_slice(), &[0u8, 0, 0, 0, 0]); + + let rbuf = RustBuffer::new_with_size(0); + assert!(!rbuf.data.is_null()); + assert_eq!(rbuf.destroy_into_vec().as_slice(), &[0u8; 0]); + } + + #[test] + fn test_rustbuffer_null_means_empty() { + // This is how foreign-language code might cheaply indicate an empty buffer. + let rbuf = unsafe { RustBuffer::from_raw_parts(std::ptr::null_mut(), 0, 0) }; + assert_eq!(rbuf.destroy_into_vec().as_slice(), &[0u8; 0]); + } + + #[test] + #[should_panic] + fn test_rustbuffer_null_must_have_no_capacity() { + // We guard against foreign-language code providing this kind of invalid struct. + let rbuf = unsafe { RustBuffer::from_raw_parts(std::ptr::null_mut(), 0, 1) }; + rbuf.destroy_into_vec(); + } + #[test] + #[should_panic] + fn test_rustbuffer_null_must_have_zero_length() { + // We guard against foreign-language code providing this kind of invalid struct. + let rbuf = unsafe { RustBuffer::from_raw_parts(std::ptr::null_mut(), 12, 0) }; + rbuf.destroy_into_vec(); + } + + #[test] + #[should_panic] + fn test_rustbuffer_provided_capacity_must_be_non_negative() { + // We guard against foreign-language code providing this kind of invalid struct. + let mut v = vec![0u8, 1, 2]; + let rbuf = unsafe { RustBuffer::from_raw_parts(v.as_mut_ptr(), 3, -7) }; + rbuf.destroy_into_vec(); + } + + #[test] + #[should_panic] + fn test_rustbuffer_provided_len_must_be_non_negative() { + // We guard against foreign-language code providing this kind of invalid struct. + let mut v = vec![0u8, 1, 2]; + let rbuf = unsafe { RustBuffer::from_raw_parts(v.as_mut_ptr(), -1, 3) }; + rbuf.destroy_into_vec(); + } + + #[test] + #[should_panic] + fn test_rustbuffer_provided_len_must_not_exceed_capacity() { + // We guard against foreign-language code providing this kind of invalid struct. + let mut v = vec![0u8, 1, 2]; + let rbuf = unsafe { RustBuffer::from_raw_parts(v.as_mut_ptr(), 3, 2) }; + rbuf.destroy_into_vec(); + } +} diff --git a/third_party/rust/uniffi_core/src/ffi/rustcalls.rs b/third_party/rust/uniffi_core/src/ffi/rustcalls.rs new file mode 100644 index 0000000000..a22f776d74 --- /dev/null +++ b/third_party/rust/uniffi_core/src/ffi/rustcalls.rs @@ -0,0 +1,279 @@ +/* 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/. */ + +//! # Low-level support for calling rust functions +//! +//! This module helps the scaffolding code make calls to rust functions and pass back the result to the FFI bindings code. +//! +//! It handles: +//! - Catching panics +//! - Adapting `Result<>` types into either a return value or an error + +use super::FfiDefault; +use crate::{FfiConverter, RustBuffer, RustBufferFfiConverter}; +use anyhow::Result; +use std::mem::MaybeUninit; +use std::panic; + +/// Represents the success/error of a rust call +/// +/// ## Usage +/// +/// - The consumer code creates a `RustCallStatus` with an empty `RustBuffer` and `CALL_SUCCESS` +/// (0) as the status code +/// - A pointer to this object is passed to the rust FFI function. This is an +/// "out parameter" which will be updated with any error that occurred during the function's +/// execution. +/// - After the call, if `code` is `CALL_ERROR` then `error_buf` will be updated to contain +/// the serialized error object. The consumer is responsible for freeing `error_buf`. +/// +/// ## Layout/fields +/// +/// The layout of this struct is important since consumers on the other side of the FFI need to +/// construct it. If this were a C struct, it would look like: +/// +/// ```c,no_run +/// struct RustCallStatus { +/// int8_t code; +/// RustBuffer error_buf; +/// }; +/// ``` +/// +/// #### The `code` field. +/// +/// - `CALL_SUCCESS` (0) for successful calls +/// - `CALL_ERROR` (1) for calls that returned an `Err` value +/// - `CALL_PANIC` (2) for calls that panicked +/// +/// #### The `error_buf` field. +/// +/// - For `CALL_ERROR` this is a `RustBuffer` with the serialized error. The consumer code is +/// responsible for freeing this `RustBuffer`. +#[repr(C)] +pub struct RustCallStatus { + pub code: i8, + // code is signed because unsigned types are experimental in Kotlin + pub error_buf: MaybeUninit<RustBuffer>, + // error_buf is MaybeUninit to avoid dropping the value that the consumer code sends in: + // - Consumers should send in a zeroed out RustBuffer. In this case dropping is a no-op and + // avoiding the drop is a small optimization. + // - If consumers pass in invalid data, then we should avoid trying to drop it. In + // particular, we don't want to try to free any data the consumer has allocated. + // + // `MaybeUninit` requires unsafe code, since we are preventing rust from dropping the value. + // To use this safely we need to make sure that no code paths set this twice, since that will + // leak the first `RustBuffer`. +} + +impl Default for RustCallStatus { + fn default() -> Self { + Self { + code: 0, + error_buf: MaybeUninit::uninit(), + } + } +} + +#[allow(dead_code)] +const CALL_SUCCESS: i8 = 0; // CALL_SUCCESS is set by the calling code +const CALL_ERROR: i8 = 1; +const CALL_PANIC: i8 = 2; + +// A trait for errors that can be thrown to the FFI code +// +// This gets implemented in uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs +pub trait FfiError: RustBufferFfiConverter {} + +// Generalized rust call handling function +fn make_call<F, R>(out_status: &mut RustCallStatus, callback: F) -> R +where + F: panic::UnwindSafe + FnOnce() -> Result<R, RustBuffer>, + R: FfiDefault, +{ + let result = panic::catch_unwind(|| { + crate::panichook::ensure_setup(); + callback() + }); + match result { + // Happy path. Note: no need to update out_status in this case because the calling code + // initializes it to CALL_SUCCESS + Ok(Ok(v)) => v, + // Callback returned an Err. + Ok(Err(buf)) => { + out_status.code = CALL_ERROR; + unsafe { + // Unsafe because we're setting the `MaybeUninit` value, see above for safety + // invariants. + out_status.error_buf.as_mut_ptr().write(buf); + } + R::ffi_default() + } + // Callback panicked + Err(cause) => { + out_status.code = CALL_PANIC; + // Try to coerce the cause into a RustBuffer containing a String. Since this code can + // panic, we need to use a second catch_unwind(). + let message_result = panic::catch_unwind(panic::AssertUnwindSafe(move || { + // The documentation suggests that it will *usually* be a str or String. + let message = if let Some(s) = cause.downcast_ref::<&'static str>() { + (*s).to_string() + } else if let Some(s) = cause.downcast_ref::<String>() { + s.clone() + } else { + "Unknown panic!".to_string() + }; + log::error!("Caught a panic calling rust code: {:?}", message); + String::lower(message) + })); + if let Ok(buf) = message_result { + unsafe { + // Unsafe because we're setting the `MaybeUninit` value, see above for safety + // invariants. + out_status.error_buf.as_mut_ptr().write(buf); + } + } + // Ignore the error case. We've done all that we can at this point. In the bindings + // code, we handle this by checking if `error_buf` still has an empty `RustBuffer` and + // using a generic message. + R::ffi_default() + } + } +} + +/// Wrap a rust function call and return the result directly +/// +/// `callback` is responsible for making the call to the Rust function. It must convert any return +/// value into a type that implements `IntoFfi` (typically handled with `FfiConverter::lower()`). +/// +/// - If the function succeeds then the function's return value will be returned to the outer code +/// - If the function panics: +/// - `out_status.code` will be set to `CALL_PANIC` +/// - the return value is undefined +pub fn call_with_output<F, R>(out_status: &mut RustCallStatus, callback: F) -> R +where + F: panic::UnwindSafe + FnOnce() -> R, + R: FfiDefault, +{ + make_call(out_status, || Ok(callback())) +} + +/// Wrap a rust function call that returns a `Result<_, RustBuffer>` +/// +/// `callback` is responsible for making the call to the Rust function. +/// - `callback` must convert any return value into a type that implements `IntoFfi` +/// - `callback` must convert any `Error` the into a `RustBuffer` to be returned over the FFI +/// - (Both of these are typically handled with `FfiConverter::lower()`) +/// +/// - If the function returns an `Ok` value it will be unwrapped and returned +/// - If the function returns an `Err`: +/// - `out_status.code` will be set to `CALL_ERROR` +/// - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing the error. The calling +/// code is responsible for freeing the `RustBuffer` +/// - the return value is undefined +/// - If the function panics: +/// - `out_status.code` will be set to `CALL_PANIC` +/// - the return value is undefined +pub fn call_with_result<F, R>(out_status: &mut RustCallStatus, callback: F) -> R +where + F: panic::UnwindSafe + FnOnce() -> Result<R, RustBuffer>, + R: FfiDefault, +{ + make_call(out_status, callback) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{FfiConverter, RustBufferFfiConverter}; + + fn function(a: u8) -> i8 { + match a { + 0 => 100, + x => panic!("Unexpected value: {x}"), + } + } + + fn create_call_status() -> RustCallStatus { + RustCallStatus { + code: 0, + error_buf: MaybeUninit::new(RustBuffer::new()), + } + } + + #[test] + fn test_call_with_output() { + let mut status = create_call_status(); + let return_value = call_with_output(&mut status, || function(0)); + assert_eq!(status.code, CALL_SUCCESS); + assert_eq!(return_value, 100); + + call_with_output(&mut status, || function(1)); + assert_eq!(status.code, CALL_PANIC); + unsafe { + assert_eq!( + String::try_lift(status.error_buf.assume_init()).unwrap(), + "Unexpected value: 1" + ); + } + } + + #[derive(Debug, PartialEq)] + struct TestError(String); + + // Use RustBufferFfiConverter to simplify lifting TestError out of RustBuffer to check it + impl RustBufferFfiConverter for TestError { + type RustType = Self; + + fn write(obj: Self::RustType, buf: &mut Vec<u8>) { + <String as FfiConverter>::write(obj.0, buf); + } + + fn try_read(buf: &mut &[u8]) -> Result<Self> { + String::try_read(buf).map(TestError) + } + } + + impl FfiError for TestError {} + + fn function_with_result(a: u8) -> Result<i8, TestError> { + match a { + 0 => Ok(100), + 1 => Err(TestError("Error".to_owned())), + x => panic!("Unexpected value: {x}"), + } + } + + #[test] + fn test_call_with_result() { + let mut status = create_call_status(); + let return_value = call_with_result(&mut status, || { + function_with_result(0).map_err(TestError::lower) + }); + assert_eq!(status.code, CALL_SUCCESS); + assert_eq!(return_value, 100); + + call_with_result(&mut status, || { + function_with_result(1).map_err(TestError::lower) + }); + assert_eq!(status.code, CALL_ERROR); + unsafe { + assert_eq!( + TestError::try_lift(status.error_buf.assume_init()).unwrap(), + TestError("Error".to_owned()) + ); + } + + let mut status = create_call_status(); + call_with_result(&mut status, || { + function_with_result(2).map_err(TestError::lower) + }); + assert_eq!(status.code, CALL_PANIC); + unsafe { + assert_eq!( + String::try_lift(status.error_buf.assume_init()).unwrap(), + "Unexpected value: 2" + ); + } + } +} diff --git a/third_party/rust/uniffi_core/src/lib.rs b/third_party/rust/uniffi_core/src/lib.rs new file mode 100644 index 0000000000..44271a7a43 --- /dev/null +++ b/third_party/rust/uniffi_core/src/lib.rs @@ -0,0 +1,692 @@ +/* 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/. */ + +//! # Runtime support code for uniffi +//! +//! This crate provides the small amount of runtime code that is required by the generated uniffi +//! component scaffolding in order to transfer data back and forth across the C-style FFI layer, +//! as well as some utilities for testing the generated bindings. +//! +//! The key concept here is the [`FfiConverter`] trait, which is responsible for converting between +//! a Rust type and a low-level C-style type that can be passed across the FFI: +//! +//! * How to [represent](FfiConverter::FfiType) values of the Rust type in the low-level C-style type +//! system of the FFI layer. +//! * How to ["lower"](FfiConverter::lower) values of the Rust type into an appropriate low-level +//! FFI value. +//! * How to ["lift"](FfiConverter::try_lift) low-level FFI values back into values of the Rust +//! type. +//! * How to [write](FfiConverter::write) values of the Rust type into a buffer, for cases +//! where they are part of a compound data structure that is serialized for transfer. +//! * How to [read](FfiConverter::try_read) values of the Rust type from buffer, for cases +//! where they are received as part of a compound data structure that was serialized for transfer. +//! +//! This logic encapsulates the Rust-side handling of data transfer. Each foreign-language binding +//! must also implement a matching set of data-handling rules for each data type. +//! +//! In addition to the core `FfiConverter` trait, we provide a handful of struct definitions useful +//! for passing core rust types over the FFI, such as [`RustBuffer`]. + +#![warn(rust_2018_idioms, unused_qualifications)] + +use anyhow::bail; +use bytes::buf::{Buf, BufMut}; +use paste::paste; +use std::{ + collections::HashMap, + convert::TryFrom, + time::{Duration, SystemTime}, +}; + +// Make Result<> public to support external impls of FfiConverter +pub use anyhow::Result; + +pub mod ffi; +pub use ffi::*; + +// Re-export the libs that we use in the generated code, +// so the consumer doesn't have to depend on them directly. +pub mod deps { + pub use anyhow; + pub use bytes; + pub use log; + pub use static_assertions; +} + +mod panichook; + +const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// For the significance of this magic number 10 here, and the reason that +// it can't be a named constant, see the `check_compatible_version` function. +static_assertions::const_assert!(PACKAGE_VERSION.as_bytes().len() < 10); + +/// Check whether the uniffi runtime version is compatible a given uniffi_bindgen version. +/// +/// The result of this check may be used to ensure that generated Rust scaffolding is +/// using a compatible version of the uniffi runtime crate. It's a `const fn` so that it +/// can be used to perform such a check at compile time. +#[allow(clippy::len_zero)] +pub const fn check_compatible_version(bindgen_version: &'static str) -> bool { + // While UniFFI is still under heavy development, we require that + // the runtime support crate be precisely the same version as the + // build-time bindgen crate. + // + // What we want to achieve here is checking two strings for equality. + // Unfortunately Rust doesn't yet support calling the `&str` equals method + // in a const context. We can hack around that by doing a byte-by-byte + // comparison of the underlying bytes. + let package_version = PACKAGE_VERSION.as_bytes(); + let bindgen_version = bindgen_version.as_bytes(); + // What we want to achieve here is a loop over the underlying bytes, + // something like: + // ``` + // if package_version.len() != bindgen_version.len() { + // return false + // } + // for i in 0..package_version.len() { + // if package_version[i] != bindgen_version[i] { + // return false + // } + // } + // return true + // ``` + // Unfortunately stable Rust doesn't allow `if` or `for` in const contexts, + // so code like the above would only work in nightly. We can hack around it by + // statically asserting that the string is shorter than a certain length + // (currently 10 bytes) and then manually unrolling that many iterations of the loop. + // + // Yes, I am aware that this is horrific, but the externally-visible + // behaviour is quite nice for consumers! + package_version.len() == bindgen_version.len() + && (package_version.len() == 0 || package_version[0] == bindgen_version[0]) + && (package_version.len() <= 1 || package_version[1] == bindgen_version[1]) + && (package_version.len() <= 2 || package_version[2] == bindgen_version[2]) + && (package_version.len() <= 3 || package_version[3] == bindgen_version[3]) + && (package_version.len() <= 4 || package_version[4] == bindgen_version[4]) + && (package_version.len() <= 5 || package_version[5] == bindgen_version[5]) + && (package_version.len() <= 6 || package_version[6] == bindgen_version[6]) + && (package_version.len() <= 7 || package_version[7] == bindgen_version[7]) + && (package_version.len() <= 8 || package_version[8] == bindgen_version[8]) + && (package_version.len() <= 9 || package_version[9] == bindgen_version[9]) + && package_version.len() < 10 +} + +/// Assert that the uniffi runtime version matches an expected value. +/// +/// This is a helper hook for the generated Rust scaffolding, to produce a compile-time +/// error if the version of `uniffi_bindgen` used to generate the scaffolding was +/// incompatible with the version of `uniffi` being used at runtime. +#[macro_export] +macro_rules! assert_compatible_version { + ($v:expr $(,)?) => { + uniffi::deps::static_assertions::const_assert!(uniffi::check_compatible_version($v)); + }; +} + +/// Trait defining how to transfer values via the FFI layer. +/// +/// The `FfiConverter` trait defines how to pass values of a particular type back-and-forth over +/// the uniffi generated FFI layer, both as standalone argument or return values, and as +/// part of serialized compound data structures. +/// +/// (This trait is like the `IntoFfi` trait from `ffi_support`, but local to this crate +/// so that we can add some alternative implementations for different builtin types, +/// and so that we can add support for receiving as well as returning). +/// +/// ## Safety +/// +/// This is an unsafe trait (implementing it requires `unsafe impl`) because we can't guarantee +/// that it's safe to pass your type out to foreign-language code and back again. Buggy +/// implementations of this trait might violate some assumptions made by the generated code, +/// or might not match with the corresponding code in the generated foreign-language bindings. +/// +/// In general, you should not need to implement this trait by hand, and should instead rely on +/// implementations generated from your component UDL via the `uniffi-bindgen scaffolding` command. + +pub unsafe trait FfiConverter: Sized { + /// The type used in Rust code. + /// + /// For primitive / standard types, we implement `FfiConverter` on the type itself with `RustType=Self`. + /// For user-defined types we create a unit struct and implement it there. This sidesteps + /// Rust's orphan rules (ADR-0006). + type RustType; + + /// The low-level type used for passing values of this type over the FFI. + /// + /// This must be a C-compatible type (e.g. a numeric primitive, a `#[repr(C)]` struct) into + /// which values of the target rust type can be converted. + /// + /// For complex data types, we currently recommend using `RustBuffer` and serializing + /// the data for transfer. In theory it could be possible to build a matching + /// `#[repr(C)]` struct for a complex data type and pass that instead, but explicit + /// serialization is simpler and safer as a starting point. + type FfiType; + + /// Lower a rust value of the target type, into an FFI value of type Self::FfiType. + /// + /// This trait method is used for sending data from rust to the foreign language code, + /// by (hopefully cheaply!) converting it into something that can be passed over the FFI + /// and reconstructed on the other side. + /// + /// Note that this method takes an owned `Self::RustType`; this allows it to transfer ownership + /// in turn to the foreign language code, e.g. by boxing the value and passing a pointer. + fn lower(obj: Self::RustType) -> Self::FfiType; + + /// Lift a rust value of the target type, from an FFI value of type Self::FfiType. + /// + /// This trait method is used for receiving data from the foreign language code in rust, + /// by (hopefully cheaply!) converting it from a low-level FFI value of type Self::FfiType + /// into a high-level rust value of the target type. + /// + /// Since we cannot statically guarantee that the foreign-language code will send valid + /// values of type Self::FfiType, this method is fallible. + fn try_lift(v: Self::FfiType) -> Result<Self::RustType>; + + /// Write a rust value into a buffer, to send over the FFI in serialized form. + /// + /// This trait method can be used for sending data from rust to the foreign language code, + /// in cases where we're not able to use a special-purpose FFI type and must fall back to + /// sending serialized bytes. + /// + /// Note that this method takes an owned `Self::RustType` because it's transferring ownership + /// to the foreign language code via the RustBuffer. + fn write(obj: Self::RustType, buf: &mut Vec<u8>); + + /// Read a rust value from a buffer, received over the FFI in serialized form. + /// + /// This trait method can be used for receiving data from the foreign language code in rust, + /// in cases where we're not able to use a special-purpose FFI type and must fall back to + /// receiving serialized bytes. + /// + /// Since we cannot statically guarantee that the foreign-language code will send valid + /// serialized bytes for the target type, this method is fallible. + /// + /// Note the slightly unusual type here - we want a mutable reference to a slice of bytes, + /// because we want to be able to advance the start of the slice after reading an item + /// from it (but will not mutate the actual contents of the slice). + fn try_read(buf: &mut &[u8]) -> Result<Self::RustType>; +} + +/// Types that can be returned from exported functions. +/// +/// Blanket-implemented by any FfiConverter with RustType = Self, but additionally implemented by +/// the unit type. +/// +/// This helper trait is currently only used to simplify the code generated by the export macro and +/// is not part of the public interface of the library, hence its documentation is hidden. +#[doc(hidden)] +pub unsafe trait FfiReturn: Sized { + type FfiType; + fn lower(self) -> Self::FfiType; +} + +unsafe impl<T> FfiReturn for T +where + T: FfiConverter<RustType = T>, +{ + type FfiType = <Self as FfiConverter>::FfiType; + + fn lower(self) -> Self::FfiType { + <Self as FfiConverter>::lower(self) + } +} + +unsafe impl FfiReturn for () { + type FfiType = (); + + fn lower(self) -> Self::FfiType { + self + } +} + +/// A helper function to ensure we don't read past the end of a buffer. +/// +/// Rust won't actually let us read past the end of a buffer, but the `Buf` trait does not support +/// returning an explicit error in this case, and will instead panic. This is a look-before-you-leap +/// helper function to instead return an explicit error, to help with debugging. +pub fn check_remaining(buf: &[u8], num_bytes: usize) -> Result<()> { + if buf.remaining() < num_bytes { + bail!( + "not enough bytes remaining in buffer ({} < {num_bytes})", + buf.remaining(), + ); + } + Ok(()) +} + +/// Blanket implementation of `FfiConverter` for numeric primitives. +/// +/// Numeric primitives have a straightforward mapping into C-compatible numeric types, +/// sice they are themselves a C-compatible numeric type! +macro_rules! impl_via_ffi_for_num_primitive { + ($($T:ty,)+) => { impl_via_ffi_for_num_primitive!($($T),+); }; + ($($T:ty),*) => { + $( + paste! { + unsafe impl FfiConverter for $T { + type RustType = Self; + type FfiType = Self; + + fn lower(obj: Self::RustType) -> Self::FfiType { + obj + } + + fn try_lift(v: Self::FfiType) -> Result<Self> { + Ok(v) + } + + fn write(obj: Self::RustType, buf: &mut Vec<u8>) { + buf.[<put_ $T>](obj); + } + + fn try_read(buf: &mut &[u8]) -> Result<Self> { + check_remaining(buf, std::mem::size_of::<$T>())?; + Ok(buf.[<get_ $T>]()) + } + } + } + )* + }; +} + +impl_via_ffi_for_num_primitive! { + i8, u8, i16, u16, i32, u32, i64, u64, f32, f64 +} + +/// Support for passing boolean values via the FFI. +/// +/// Booleans are passed as an `i8` in order to avoid problems with handling +/// C-compatible boolean values on JVM-based languages. +unsafe impl FfiConverter for bool { + type RustType = Self; + type FfiType = i8; + + fn lower(obj: Self::RustType) -> Self::FfiType { + i8::from(obj) + } + + fn try_lift(v: Self::FfiType) -> Result<Self::RustType> { + Ok(match v { + 0 => false, + 1 => true, + _ => bail!("unexpected byte for Boolean"), + }) + } + + fn write(obj: Self::RustType, buf: &mut Vec<u8>) { + buf.put_i8(<bool as FfiConverter>::lower(obj)); + } + + fn try_read(buf: &mut &[u8]) -> Result<Self::RustType> { + check_remaining(buf, 1)?; + <bool as FfiConverter>::try_lift(buf.get_i8()) + } +} + +/// Support for passing Strings via the FFI. +/// +/// Unlike many other implementations of `FfiConverter`, this passes a struct containing +/// a raw pointer rather than copying the data from one side to the other. This is a +/// safety hazard, but turns out to be pretty nice for useability. This struct +/// *must* be a valid `RustBuffer` and it *must* contain valid utf-8 data (in other +/// words, it *must* be a `Vec<u8>` suitable for use as an actual rust `String`). +/// +/// When serialized in a buffer, strings are represented as a i32 byte length +/// followed by utf8-encoded bytes. (It's a signed integer because unsigned types are +/// currently experimental in Kotlin). +unsafe impl FfiConverter for String { + type RustType = Self; + type FfiType = RustBuffer; + + // This returns a struct with a raw pointer to the underlying bytes, so it's very + // important that it consume ownership of the String, which is relinquished to the + // foreign language code (and can be restored by it passing the pointer back). + fn lower(obj: Self::RustType) -> Self::FfiType { + RustBuffer::from_vec(obj.into_bytes()) + } + + // The argument here *must* be a uniquely-owned `RustBuffer` previously obtained + // from `lower` above, and hence must be the bytes of a valid rust string. + fn try_lift(v: Self::FfiType) -> Result<Self::RustType> { + let v = v.destroy_into_vec(); + // This turns the buffer back into a `String` without copying the data + // and without re-checking it for validity of the utf8. If the `RustBuffer` + // came from a valid String then there's no point in re-checking the utf8, + // and if it didn't then bad things are probably going to happen regardless + // of whether we check for valid utf8 data or not. + Ok(unsafe { String::from_utf8_unchecked(v) }) + } + + fn write(obj: Self::RustType, buf: &mut Vec<u8>) { + // N.B. `len()` gives us the length in bytes, not in chars or graphemes. + // TODO: it would be nice not to panic here. + let len = i32::try_from(obj.len()).unwrap(); + buf.put_i32(len); // We limit strings to u32::MAX bytes + buf.put(obj.as_bytes()); + } + + fn try_read(buf: &mut &[u8]) -> Result<Self::RustType> { + check_remaining(buf, 4)?; + let len = usize::try_from(buf.get_i32())?; + check_remaining(buf, len)?; + // N.B: In the general case `Buf::chunk()` may return partial data. + // But in the specific case of `<&[u8] as Buf>` it returns the full slice, + // so there is no risk of having less than `len` bytes available here. + let bytes = &buf.chunk()[..len]; + let res = String::from_utf8(bytes.to_vec())?; + buf.advance(len); + Ok(res) + } +} + +/// A helper trait to implement lowering/lifting using a `RustBuffer` +/// +/// For complex types where it's too fiddly or too unsafe to convert them into a special-purpose +/// C-compatible value, you can use this trait to implement `lower()` in terms of `write()` and +/// `lift` in terms of `read()`. +pub trait RustBufferFfiConverter: Sized { + type RustType; + fn write(obj: Self::RustType, buf: &mut Vec<u8>); + fn try_read(buf: &mut &[u8]) -> Result<Self::RustType>; +} + +unsafe impl<T: RustBufferFfiConverter> FfiConverter for T { + type RustType = T::RustType; + type FfiType = RustBuffer; + + fn lower(obj: Self::RustType) -> RustBuffer { + let mut buf = Vec::new(); + <T as RustBufferFfiConverter>::write(obj, &mut buf); + RustBuffer::from_vec(buf) + } + + fn try_lift(v: RustBuffer) -> Result<Self::RustType> { + let vec = v.destroy_into_vec(); + let mut buf = vec.as_slice(); + let value = T::try_read(&mut buf)?; + if buf.remaining() != 0 { + bail!("junk data left in buffer after lifting") + } + Ok(value) + } + + fn write(obj: Self::RustType, buf: &mut Vec<u8>) { + T::write(obj, buf) + } + + fn try_read(buf: &mut &[u8]) -> Result<Self::RustType> { + T::try_read(buf) + } +} + +/// Support for passing timestamp values via the FFI. +/// +/// Timestamps values are currently always passed by serializing to a buffer. +/// +/// Timestamps are represented on the buffer by an i64 that indicates the +/// direction and the magnitude in seconds of the offset from epoch, and a +/// u32 that indicates the nanosecond portion of the offset magnitude. The +/// nanosecond portion is expected to be between 0 and 999,999,999. +/// +/// To build an epoch offset the absolute value of the seconds portion of the +/// offset should be combined with the nanosecond portion. This is because +/// the sign of the seconds portion represents the direction of the offset +/// overall. The sign of the seconds portion can then be used to determine +/// if the total offset should be added to or subtracted from the unix epoch. +impl RustBufferFfiConverter for SystemTime { + type RustType = Self; + + fn write(obj: Self::RustType, buf: &mut Vec<u8>) { + let mut sign = 1; + let epoch_offset = obj + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_else(|error| { + sign = -1; + error.duration() + }); + // This panic should never happen as SystemTime typically stores seconds as i64 + let seconds = sign + * i64::try_from(epoch_offset.as_secs()) + .expect("SystemTime overflow, seconds greater than i64::MAX"); + + buf.put_i64(seconds); + buf.put_u32(epoch_offset.subsec_nanos()); + } + + fn try_read(buf: &mut &[u8]) -> Result<Self::RustType> { + check_remaining(buf, 12)?; + let seconds = buf.get_i64(); + let nanos = buf.get_u32(); + let epoch_offset = Duration::new(seconds.wrapping_abs() as u64, nanos); + + if seconds >= 0 { + Ok(SystemTime::UNIX_EPOCH + epoch_offset) + } else { + Ok(SystemTime::UNIX_EPOCH - epoch_offset) + } + } +} + +/// Support for passing duration values via the FFI. +/// +/// Duration values are currently always passed by serializing to a buffer. +/// +/// Durations are represented on the buffer by a u64 that indicates the +/// magnitude in seconds, and a u32 that indicates the nanosecond portion +/// of the magnitude. The nanosecond portion is expected to be between 0 +/// and 999,999,999. +impl RustBufferFfiConverter for Duration { + type RustType = Self; + + fn write(obj: Self::RustType, buf: &mut Vec<u8>) { + buf.put_u64(obj.as_secs()); + buf.put_u32(obj.subsec_nanos()); + } + + fn try_read(buf: &mut &[u8]) -> Result<Self::RustType> { + check_remaining(buf, 12)?; + Ok(Duration::new(buf.get_u64(), buf.get_u32())) + } +} + +/// Support for passing optional values via the FFI. +/// +/// Optional values are currently always passed by serializing to a buffer. +/// We write either a zero byte for `None`, or a one byte followed by the containing +/// item for `Some`. +/// +/// In future we could do the same optimization as rust uses internally, where the +/// `None` option is represented as a null pointer and the `Some` as a valid pointer, +/// but that seems more fiddly and less safe in the short term, so it can wait. +impl<T: FfiConverter> RustBufferFfiConverter for Option<T> { + type RustType = Option<T::RustType>; + + fn write(obj: Self::RustType, buf: &mut Vec<u8>) { + match obj { + None => buf.put_i8(0), + Some(v) => { + buf.put_i8(1); + <T as FfiConverter>::write(v, buf); + } + } + } + + fn try_read(buf: &mut &[u8]) -> Result<Self::RustType> { + check_remaining(buf, 1)?; + Ok(match buf.get_i8() { + 0 => None, + 1 => Some(<T as FfiConverter>::try_read(buf)?), + _ => bail!("unexpected tag byte for Option"), + }) + } +} + +/// Support for passing vectors of values via the FFI. +/// +/// Vectors are currently always passed by serializing to a buffer. +/// We write a `i32` item count followed by each item in turn. +/// (It's a signed type due to limits of the JVM). +/// +/// Ideally we would pass `Vec<u8>` directly as a `RustBuffer` rather +/// than serializing, and perhaps even pass other vector types using a +/// similar struct. But that's for future work. +impl<T: FfiConverter> RustBufferFfiConverter for Vec<T> { + type RustType = Vec<T::RustType>; + + fn write(obj: Self::RustType, buf: &mut Vec<u8>) { + // TODO: would be nice not to panic here :-/ + let len = i32::try_from(obj.len()).unwrap(); + buf.put_i32(len); // We limit arrays to i32::MAX items + for item in obj { + <T as FfiConverter>::write(item, buf); + } + } + + fn try_read(buf: &mut &[u8]) -> Result<Self::RustType> { + check_remaining(buf, 4)?; + let len = usize::try_from(buf.get_i32())?; + let mut vec = Vec::with_capacity(len); + for _ in 0..len { + vec.push(<T as FfiConverter>::try_read(buf)?) + } + Ok(vec) + } +} + +/// Support for associative arrays via the FFI. +/// Note that because of webidl limitations, +/// the key must always be of the String type. +/// +/// HashMaps are currently always passed by serializing to a buffer. +/// We write a `i32` entries count followed by each entry (string +/// key followed by the value) in turn. +/// (It's a signed type due to limits of the JVM). +impl<K: FfiConverter, V: FfiConverter> RustBufferFfiConverter for HashMap<K, V> +where + K::RustType: std::hash::Hash + Eq, +{ + type RustType = HashMap<K::RustType, V::RustType>; + + fn write(obj: Self::RustType, buf: &mut Vec<u8>) { + // TODO: would be nice not to panic here :-/ + let len = i32::try_from(obj.len()).unwrap(); + buf.put_i32(len); // We limit HashMaps to i32::MAX entries + for (key, value) in obj { + <K as FfiConverter>::write(key, buf); + <V as FfiConverter>::write(value, buf); + } + } + + fn try_read(buf: &mut &[u8]) -> Result<Self::RustType> { + check_remaining(buf, 4)?; + let len = usize::try_from(buf.get_i32())?; + let mut map = HashMap::with_capacity(len); + for _ in 0..len { + let key = <K as FfiConverter>::try_read(buf)?; + let value = <V as FfiConverter>::try_read(buf)?; + map.insert(key, value); + } + Ok(map) + } +} + +/// Support for passing reference-counted shared objects via the FFI. +/// +/// To avoid dealing with complex lifetime semantics over the FFI, any data passed +/// by reference must be encapsulated in an `Arc`, and must be safe to share +/// across threads. +unsafe impl<T: Sync + Send> FfiConverter for std::sync::Arc<T> { + type RustType = Self; + // Don't use a pointer to <T> as that requires a `pub <T>` + type FfiType = *const std::os::raw::c_void; + + /// When lowering, we have an owned `Arc<T>` and we transfer that ownership + /// to the foreign-language code, "leaking" it out of Rust's ownership system + /// as a raw pointer. This works safely because we have unique ownership of `self`. + /// The foreign-language code is responsible for freeing this by calling the + /// `ffi_object_free` FFI function provided by the corresponding UniFFI type. + /// + /// Safety: when freeing the resulting pointer, the foreign-language code must + /// call the destructor function specific to the type `T`. Calling the destructor + /// function for other types may lead to undefined behaviour. + fn lower(obj: Self::RustType) -> Self::FfiType { + std::sync::Arc::into_raw(obj) as Self::FfiType + } + + /// When lifting, we receive a "borrow" of the `Arc<T>` that is owned by + /// the foreign-language code, and make a clone of it for our own use. + /// + /// Safety: the provided value must be a pointer previously obtained by calling + /// the `lower()` or `write()` method of this impl. + fn try_lift(v: Self::FfiType) -> Result<Self::RustType> { + let v = v as *const T; + // We musn't drop the `Arc<T>` that is owned by the foreign-language code. + let foreign_arc = std::mem::ManuallyDrop::new(unsafe { Self::from_raw(v) }); + // Take a clone for our own use. + Ok(std::sync::Arc::clone(&*foreign_arc)) + } + + /// When writing as a field of a complex structure, make a clone and transfer ownership + /// of it to the foreign-language code by writing its pointer into the buffer. + /// The foreign-language code is responsible for freeing this by calling the + /// `ffi_object_free` FFI function provided by the corresponding UniFFI type. + /// + /// Safety: when freeing the resulting pointer, the foreign-language code must + /// call the destructor function specific to the type `T`. Calling the destructor + /// function for other types may lead to undefined behaviour. + fn write(obj: Self::RustType, buf: &mut Vec<u8>) { + static_assertions::const_assert!(std::mem::size_of::<*const std::ffi::c_void>() <= 8); + buf.put_u64(<Self as FfiConverter>::lower(obj) as u64); + } + + /// When reading as a field of a complex structure, we receive a "borrow" of the `Arc<T>` + /// that is owned by the foreign-language code, and make a clone for our own use. + /// + /// Safety: the buffer must contain a pointer previously obtained by calling + /// the `lower()` or `write()` method of this impl. + fn try_read(buf: &mut &[u8]) -> Result<Self::RustType> { + static_assertions::const_assert!(std::mem::size_of::<*const std::ffi::c_void>() <= 8); + check_remaining(buf, 8)?; + Self::try_lift(buf.get_u64() as Self::FfiType) + } +} + +pub fn lower_anyhow_error_or_panic<ErrConverter>( + err: anyhow::Error, + arg_name: &str, +) -> ErrConverter::FfiType +where + ErrConverter: FfiConverter, + ErrConverter::RustType: 'static + Sync + Send + std::fmt::Debug + std::fmt::Display, +{ + match err.downcast::<ErrConverter::RustType>() { + Ok(actual_error) => ErrConverter::lower(actual_error), + Err(ohno) => panic!("Failed to convert arg '{arg_name}': {ohno}"), + } +} + +#[cfg(test)] +mod test { + use std::time::{Duration, SystemTime}; + + use super::FfiConverter as _; + + #[test] + fn timestamp_roundtrip_post_epoch() { + let expected = SystemTime::UNIX_EPOCH + Duration::new(100, 100); + let result = SystemTime::try_lift(SystemTime::lower(expected)).expect("Failed to lift!"); + assert_eq!(expected, result) + } + + #[test] + fn timestamp_roundtrip_pre_epoch() { + let expected = SystemTime::UNIX_EPOCH - Duration::new(100, 100); + let result = SystemTime::try_lift(SystemTime::lower(expected)).expect("Failed to lift!"); + assert_eq!( + expected, result, + "Expected results after lowering and lifting to be equal" + ) + } +} diff --git a/third_party/rust/uniffi_core/src/panichook.rs b/third_party/rust/uniffi_core/src/panichook.rs new file mode 100644 index 0000000000..ef0ab86f1f --- /dev/null +++ b/third_party/rust/uniffi_core/src/panichook.rs @@ -0,0 +1,34 @@ +/// Initialize our panic handling hook to optionally log panics +#[cfg(feature = "log_panics")] +pub fn ensure_setup() { + use std::sync::Once; + static INIT_BACKTRACES: Once = Once::new(); + INIT_BACKTRACES.call_once(move || { + #[cfg(all(feature = "log_backtraces", not(target_os = "android")))] + { + std::env::set_var("RUST_BACKTRACE", "1"); + } + // Turn on a panic hook which logs both backtraces and the panic + // "Location" (file/line). We do both in case we've been stripped, + // ). + std::panic::set_hook(Box::new(move |panic_info| { + let (file, line) = if let Some(loc) = panic_info.location() { + (loc.file(), loc.line()) + } else { + // Apparently this won't happen but rust has reserved the + // ability to start returning None from location in some cases + // in the future. + ("<unknown>", 0) + }; + log::error!("### Rust `panic!` hit at file '{file}', line {line}"); + #[cfg(all(feature = "log_backtraces", not(target_os = "android")))] + { + log::error!(" Complete stack trace:\n{:?}", backtrace::Backtrace::new()); + } + })); + }); +} + +/// Initialize our panic handling hook to optionally log panics +#[cfg(not(feature = "log_panics"))] +pub fn ensure_setup() {} diff --git a/third_party/rust/uniffi_macros/.cargo-checksum.json b/third_party/rust/uniffi_macros/.cargo-checksum.json new file mode 100644 index 0000000000..8a84c40226 --- /dev/null +++ b/third_party/rust/uniffi_macros/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"495a97879e9a66a87a410c742fa2ac36e2b85e5f16bee1cdde878db02090d7ee","src/enum_.rs":"0b2aa1bce2b8f1c476f2874e6bb9ae49b9610abd6963f3336db63794296494a8","src/error.rs":"b5fccfa3d4aa2d7454c5d27504a2d8d2e02e761843ea2da94b76ef8ffb0d0bf5","src/export.rs":"f95d31c9445142efe59617392ce9135acbefc4ac852654a482a2360c5c78ee75","src/export/metadata.rs":"af89a9942c7c0c4043a3cd57d1e6bd71cde19005e1f9f246efac761f47eff6be","src/export/metadata/convert.rs":"82c9602ddee0af2c93733bbe6cb77c50dccb0134c927b582f04f5bb9f8b5203c","src/export/metadata/function.rs":"e775f838025bee429b294f786579ac6598966b1b4f1f415dfb9012c70a0fea6c","src/export/metadata/impl_.rs":"1f55990f15293f2a3e30ebb0c132f68caa25a56113e0451c5307c9ed32cb8fb8","src/export/scaffolding.rs":"1cd5b8cf62195e35cd851ee9dd5aa787eaa8a1c293c1b3e899a1de8feb183b2d","src/lib.rs":"4357a13d0e9deac75239007431211188d7ef6270256f002d17d9e937ee3aba5a","src/object.rs":"955b596f344304013692042bdc1760bbb1192ec33950b0dd2932cb8de94ec297","src/record.rs":"f0c2adb794d5996ff8545c10469e63f24164509f0566e6c401a26c363c278003","src/test.rs":"069d91947685de083c9faa9042c328c235445b84bcf69f03117016d9c8464736","src/util.rs":"4834ece6808595ef5fbf735f9610dd1ef1631ea2e5919e6a1da9f290beb7091a"},"package":"fa03394de21e759e0022f1ea8d992d2e39290d735b9ed52b1f74b20a684f794e"}
\ No newline at end of file diff --git a/third_party/rust/uniffi_macros/Cargo.toml b/third_party/rust/uniffi_macros/Cargo.toml new file mode 100644 index 0000000000..dc5b9a9184 --- /dev/null +++ b/third_party/rust/uniffi_macros/Cargo.toml @@ -0,0 +1,69 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi_macros" +version = "0.23.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +description = "a multi-language bindings generator for rust (convenience macros)" +homepage = "https://mozilla.github.io/uniffi-rs" +documentation = "https://mozilla.github.io/uniffi-rs" +keywords = [ + "ffi", + "bindgen", +] +license = "MPL-2.0" +repository = "https://github.com/mozilla/uniffi-rs" + +[lib] +proc-macro = true + +[dependencies.bincode] +version = "1.3" + +[dependencies.camino] +version = "1.0.8" + +[dependencies.fs-err] +version = "2.7.0" + +[dependencies.once_cell] +version = "1.10.0" + +[dependencies.proc-macro2] +version = "1.0" + +[dependencies.quote] +version = "1.0" + +[dependencies.serde] +version = "1.0.136" + +[dependencies.syn] +version = "1.0" +features = [ + "full", + "visit-mut", +] + +[dependencies.toml] +version = "0.5.9" + +[dependencies.uniffi_build] +version = "=0.23.0" + +[dependencies.uniffi_meta] +version = "=0.23.0" + +[features] +default = [] +nightly = [] diff --git a/third_party/rust/uniffi_macros/src/enum_.rs b/third_party/rust/uniffi_macros/src/enum_.rs new file mode 100644 index 0000000000..c4e49beb8b --- /dev/null +++ b/third_party/rust/uniffi_macros/src/enum_.rs @@ -0,0 +1,160 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::{punctuated::Punctuated, Data, DeriveInput, Field, Index, Token, Variant}; +use uniffi_meta::{EnumMetadata, FieldMetadata, VariantMetadata}; + +use crate::{ + export::metadata::convert::convert_type, + util::{assert_type_eq, create_metadata_static_var, try_read_field}, +}; + +pub fn expand_enum(input: DeriveInput, module_path: Vec<String>) -> TokenStream { + let variants = match input.data { + Data::Enum(e) => Some(e.variants), + _ => None, + }; + + let ident = &input.ident; + + let ffi_converter_impl = enum_ffi_converter_impl(variants.as_ref(), ident); + + let meta_static_var = if let Some(variants) = variants { + match enum_metadata(ident, variants, module_path) { + Ok(metadata) => create_metadata_static_var(ident, metadata.into()), + Err(e) => e.into_compile_error(), + } + } else { + syn::Error::new(Span::call_site(), "This derive must only be used on enums") + .into_compile_error() + }; + + let type_assertion = assert_type_eq(ident, quote! { crate::uniffi_types::#ident }); + + quote! { + #ffi_converter_impl + #meta_static_var + #type_assertion + } +} + +pub(crate) fn enum_ffi_converter_impl( + variants: Option<&Punctuated<Variant, Token![,]>>, + ident: &Ident, +) -> TokenStream { + let (write_impl, try_read_impl) = match variants { + Some(variants) => { + let write_match_arms = variants.iter().enumerate().map(|(i, v)| { + let v_ident = &v.ident; + let fields = v.fields.iter().map(|f| &f.ident); + let idx = Index::from(i + 1); + let write_fields = v.fields.iter().map(write_field); + + quote! { + Self::#v_ident { #(#fields),* } => { + ::uniffi::deps::bytes::BufMut::put_i32(buf, #idx); + #(#write_fields)* + } + } + }); + let write_impl = quote! { + match obj { #(#write_match_arms)* } + }; + + let try_read_match_arms = variants.iter().enumerate().map(|(i, v)| { + let idx = Index::from(i + 1); + let v_ident = &v.ident; + let try_read_fields = v.fields.iter().map(try_read_field); + + quote! { + #idx => Self::#v_ident { #(#try_read_fields)* }, + } + }); + let error_format_string = format!("Invalid {ident} enum value: {{}}"); + let try_read_impl = quote! { + ::uniffi::check_remaining(buf, 4)?; + + Ok(match ::uniffi::deps::bytes::Buf::get_i32(buf) { + #(#try_read_match_arms)* + v => ::uniffi::deps::anyhow::bail!(#error_format_string, v), + }) + }; + + (write_impl, try_read_impl) + } + None => { + let unimplemented = quote! { ::std::unimplemented!() }; + (unimplemented.clone(), unimplemented) + } + }; + + quote! { + #[automatically_derived] + impl ::uniffi::RustBufferFfiConverter for #ident { + type RustType = Self; + + fn write(obj: Self, buf: &mut ::std::vec::Vec<u8>) { + #write_impl + } + + fn try_read(buf: &mut &[::std::primitive::u8]) -> ::uniffi::deps::anyhow::Result<Self> { + #try_read_impl + } + } + } +} + +fn enum_metadata( + ident: &Ident, + variants: Punctuated<Variant, Token![,]>, + module_path: Vec<String>, +) -> syn::Result<EnumMetadata> { + let name = ident.to_string(); + let variants = variants + .iter() + .map(variant_metadata) + .collect::<syn::Result<_>>()?; + + Ok(EnumMetadata { + module_path, + name, + variants, + }) +} + +pub(crate) fn variant_metadata(v: &Variant) -> syn::Result<VariantMetadata> { + let name = v.ident.to_string(); + let fields = v + .fields + .iter() + .map(|f| field_metadata(f, v)) + .collect::<syn::Result<_>>()?; + + Ok(VariantMetadata { name, fields }) +} + +fn field_metadata(f: &Field, v: &Variant) -> syn::Result<FieldMetadata> { + let name = f + .ident + .as_ref() + .ok_or_else(|| { + syn::Error::new_spanned( + v, + "UniFFI only supports enum variants with named fields (or no fields at all)", + ) + })? + .to_string(); + + Ok(FieldMetadata { + name, + ty: convert_type(&f.ty)?, + }) +} + +fn write_field(f: &Field) -> TokenStream { + let ident = &f.ident; + let ty = &f.ty; + + quote! { + <#ty as ::uniffi::FfiConverter>::write(#ident, buf); + } +} diff --git a/third_party/rust/uniffi_macros/src/error.rs b/third_party/rust/uniffi_macros/src/error.rs new file mode 100644 index 0000000000..76c34cf78d --- /dev/null +++ b/third_party/rust/uniffi_macros/src/error.rs @@ -0,0 +1,170 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Data, DeriveInput, Index, Token, Variant, +}; +use uniffi_meta::{ErrorMetadata, VariantMetadata}; + +use crate::{ + enum_::{enum_ffi_converter_impl, variant_metadata}, + util::{ + assert_type_eq, chain, create_metadata_static_var, either_attribute_arg, AttributeSliceExt, + UniffiAttribute, + }, +}; + +pub fn expand_error(input: DeriveInput, module_path: Vec<String>) -> TokenStream { + let variants = match input.data { + Data::Enum(e) => Ok(e.variants), + _ => Err(syn::Error::new( + Span::call_site(), + "This derive currently only supports enums", + )), + }; + + let ident = &input.ident; + let attr = input.attrs.parse_uniffi_attributes::<ErrorAttr>(); + let ffi_converter_impl = match &attr { + Ok(a) if a.flat.is_some() => flat_error_ffi_converter_impl(variants.as_ref().ok(), ident), + _ => enum_ffi_converter_impl(variants.as_ref().ok(), ident), + }; + + let meta_static_var = match (&variants, &attr) { + (Ok(vs), Ok(a)) => Some(match error_metadata(ident, vs, module_path, a) { + Ok(metadata) => create_metadata_static_var(ident, metadata.into()), + Err(e) => e.into_compile_error(), + }), + _ => None, + }; + + let type_assertion = assert_type_eq(ident, quote! { crate::uniffi_types::#ident }); + let variant_errors: TokenStream = match variants { + Ok(vs) => vs + .iter() + .flat_map(|variant| { + chain( + variant.attrs.attributes_not_allowed_here(), + variant + .fields + .iter() + .flat_map(|field| field.attrs.attributes_not_allowed_here()), + ) + }) + .map(syn::Error::into_compile_error) + .collect(), + Err(e) => e.into_compile_error(), + }; + let attr_error = attr.err().map(syn::Error::into_compile_error); + + quote! { + #ffi_converter_impl + + #[automatically_derived] + impl ::uniffi::FfiError for #ident {} + + #meta_static_var + #type_assertion + #variant_errors + #attr_error + } +} + +pub(crate) fn flat_error_ffi_converter_impl( + variants: Option<&Punctuated<Variant, Token![,]>>, + ident: &Ident, +) -> TokenStream { + let write_impl = match variants { + Some(variants) => { + let write_match_arms = variants.iter().enumerate().map(|(i, v)| { + let v_ident = &v.ident; + let idx = Index::from(i + 1); + + quote! { + Self::#v_ident { .. } => { + ::uniffi::deps::bytes::BufMut::put_i32(buf, #idx); + <::std::string::String as ::uniffi::FfiConverter>::write(error_msg, buf); + } + } + }); + let write_impl = quote! { + let error_msg = ::std::string::ToString::to_string(&obj); + match obj { #(#write_match_arms)* } + }; + + write_impl + } + None => quote! { ::std::unimplemented!() }, + }; + + quote! { + #[automatically_derived] + impl ::uniffi::RustBufferFfiConverter for #ident { + type RustType = Self; + + fn write(obj: Self, buf: &mut ::std::vec::Vec<u8>) { + #write_impl + } + + fn try_read(buf: &mut &[::std::primitive::u8]) -> ::uniffi::deps::anyhow::Result<Self> { + ::std::panic!("try_read not supported for flat errors"); + } + } + } +} + +fn error_metadata( + ident: &Ident, + variants: &Punctuated<Variant, Token![,]>, + module_path: Vec<String>, + attr: &ErrorAttr, +) -> syn::Result<ErrorMetadata> { + let name = ident.to_string(); + let flat = attr.flat.is_some(); + let variants = if flat { + variants + .iter() + .map(|v| VariantMetadata { + name: v.ident.to_string(), + fields: vec![], + }) + .collect() + } else { + variants + .iter() + .map(variant_metadata) + .collect::<syn::Result<_>>()? + }; + + Ok(ErrorMetadata { + module_path, + name, + variants, + flat, + }) +} + +mod kw { + syn::custom_keyword!(flat_error); +} + +#[derive(Default)] +struct ErrorAttr { + flat: Option<kw::flat_error>, +} + +impl Parse for ErrorAttr { + fn parse(input: ParseStream<'_>) -> syn::Result<Self> { + let flat = input.parse()?; + Ok(ErrorAttr { flat }) + } +} + +impl UniffiAttribute for ErrorAttr { + fn merge(self, other: Self) -> syn::Result<Self> { + Ok(Self { + flat: either_attribute_arg(self.flat, other.flat)?, + }) + } +} diff --git a/third_party/rust/uniffi_macros/src/export.rs b/third_party/rust/uniffi_macros/src/export.rs new file mode 100644 index 0000000000..22d469eec6 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export.rs @@ -0,0 +1,210 @@ +/* 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::BTreeMap; + +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote, quote_spanned}; +use uniffi_meta::{checksum, FnMetadata, MethodMetadata, Type}; + +pub(crate) mod metadata; +mod scaffolding; + +pub use self::metadata::gen_metadata; +use self::{ + metadata::convert::{convert_type, try_split_result}, + scaffolding::{gen_fn_scaffolding, gen_method_scaffolding}, +}; +use crate::util::{assert_type_eq, create_metadata_static_var}; + +// TODO(jplatte): Ensure no generics, no async, … +// TODO(jplatte): Aggregate errors instead of short-circuiting, wherever possible + +pub enum ExportItem { + Function { + sig: Signature, + metadata: FnMetadata, + }, + Impl { + self_ident: Ident, + methods: Vec<syn::Result<Method>>, + }, +} + +pub struct Method { + sig: Signature, + metadata: MethodMetadata, +} + +pub struct Signature { + ident: Ident, + inputs: Vec<syn::FnArg>, + output: Option<FunctionReturn>, +} + +impl Signature { + fn new(item: syn::Signature) -> syn::Result<Self> { + let output = match item.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, ty) => Some(FunctionReturn::new(ty)?), + }; + + Ok(Self { + ident: item.ident, + inputs: item.inputs.into_iter().collect(), + output, + }) + } +} + +pub struct FunctionReturn { + ty: Box<syn::Type>, + throws: Option<Ident>, +} + +impl FunctionReturn { + fn new(ty: Box<syn::Type>) -> syn::Result<Self> { + Ok(match try_split_result(&ty)? { + Some((ok_type, throws)) => FunctionReturn { + ty: Box::new(ok_type.to_owned()), + throws: Some(throws), + }, + None => FunctionReturn { ty, throws: None }, + }) + } +} + +pub fn expand_export(metadata: ExportItem, mod_path: &[String]) -> TokenStream { + match metadata { + ExportItem::Function { sig, metadata } => { + let checksum = checksum(&metadata); + let scaffolding = gen_fn_scaffolding(&sig, mod_path, checksum); + let type_assertions = fn_type_assertions(&sig); + let meta_static_var = create_metadata_static_var(&sig.ident, metadata.into()); + + quote! { + #scaffolding + #type_assertions + #meta_static_var + } + } + ExportItem::Impl { + methods, + self_ident, + } => { + let method_tokens: TokenStream = methods + .into_iter() + .map(|res| { + res.map_or_else( + syn::Error::into_compile_error, + |Method { sig, metadata }| { + let checksum = checksum(&metadata); + let scaffolding = + gen_method_scaffolding(&sig, mod_path, checksum, &self_ident); + let type_assertions = fn_type_assertions(&sig); + let meta_static_var = create_metadata_static_var( + &format_ident!("{}_{}", metadata.self_name, sig.ident), + metadata.into(), + ); + + quote! { + #scaffolding + #type_assertions + #meta_static_var + } + }, + ) + }) + .collect(); + + quote_spanned! {self_ident.span()=> + ::uniffi::deps::static_assertions::assert_type_eq_all!( + #self_ident, + crate::uniffi_types::#self_ident + ); + + #method_tokens + } + } + } +} + +fn fn_type_assertions(sig: &Signature) -> TokenStream { + // Convert uniffi_meta::Type back to a Rust type + fn convert_type_back(ty: &Type) -> TokenStream { + match &ty { + Type::U8 => quote! { ::std::primitive::u8 }, + Type::U16 => quote! { ::std::primitive::u16 }, + Type::U32 => quote! { ::std::primitive::u32 }, + Type::U64 => quote! { ::std::primitive::u64 }, + Type::I8 => quote! { ::std::primitive::i8 }, + Type::I16 => quote! { ::std::primitive::i16 }, + Type::I32 => quote! { ::std::primitive::i32 }, + Type::I64 => quote! { ::std::primitive::i64 }, + Type::F32 => quote! { ::std::primitive::f32 }, + Type::F64 => quote! { ::std::primitive::f64 }, + Type::Bool => quote! { ::std::primitive::bool }, + Type::String => quote! { ::std::string::String }, + Type::Option { inner_type } => { + let inner = convert_type_back(inner_type); + quote! { ::std::option::Option<#inner> } + } + Type::Vec { inner_type } => { + let inner = convert_type_back(inner_type); + quote! { ::std::vec::Vec<#inner> } + } + Type::HashMap { + key_type, + value_type, + } => { + let key = convert_type_back(key_type); + let value = convert_type_back(value_type); + quote! { ::std::collections::HashMap<#key, #value> } + } + Type::ArcObject { object_name } => { + let object_ident = format_ident!("{object_name}"); + quote! { ::std::sync::Arc<crate::uniffi_types::#object_ident> } + } + Type::Unresolved { name } => { + let ident = format_ident!("{name}"); + quote! { crate::uniffi_types::#ident } + } + } + } + + let input_types = sig.inputs.iter().filter_map(|input| match input { + syn::FnArg::Receiver(_) => None, + syn::FnArg::Typed(pat_ty) => match &*pat_ty.pat { + // Self type is asserted separately for impl blocks + syn::Pat::Ident(i) if i.ident == "self" => None, + _ => Some(&pat_ty.ty), + }, + }); + let output_type = sig.output.as_ref().map(|s| &s.ty); + + let type_assertions: BTreeMap<_, _> = input_types + .chain(output_type) + .filter_map(|ty| { + convert_type(ty).ok().map(|meta_ty| { + let expected_ty = convert_type_back(&meta_ty); + let assert = assert_type_eq(ty, expected_ty); + (meta_ty, assert) + }) + }) + .collect(); + let input_output_type_assertions: TokenStream = type_assertions.into_values().collect(); + + let throws_type_assertion = sig.output.as_ref().and_then(|s| { + let ident = s.throws.as_ref()?; + Some(assert_type_eq( + ident, + quote! { crate::uniffi_types::#ident }, + )) + }); + + quote! { + #input_output_type_assertions + #throws_type_assertion + } +} diff --git a/third_party/rust/uniffi_macros/src/export/metadata.rs b/third_party/rust/uniffi_macros/src/export/metadata.rs new file mode 100644 index 0000000000..2d0b284333 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export/metadata.rs @@ -0,0 +1,26 @@ +/* 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 proc_macro2::Span; + +use super::ExportItem; + +pub(crate) mod convert; +mod function; +mod impl_; + +use self::{function::gen_fn_metadata, impl_::gen_impl_metadata}; + +pub fn gen_metadata(item: syn::Item, mod_path: &[String]) -> syn::Result<ExportItem> { + match item { + syn::Item::Fn(item) => gen_fn_metadata(item.sig, mod_path), + syn::Item::Impl(item) => gen_impl_metadata(item, mod_path), + // FIXME: Support const / static? + _ => Err(syn::Error::new( + Span::call_site(), + "unsupported item: only functions and impl \ + blocks may be annotated with this attribute", + )), + } +} diff --git a/third_party/rust/uniffi_macros/src/export/metadata/convert.rs b/third_party/rust/uniffi_macros/src/export/metadata/convert.rs new file mode 100644 index 0000000000..c19ae579c2 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export/metadata/convert.rs @@ -0,0 +1,222 @@ +/* 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 proc_macro2::Ident; +use quote::ToTokens; +use uniffi_meta::{FnParamMetadata, Type}; + +pub(super) fn fn_param_metadata(params: &[syn::FnArg]) -> syn::Result<Vec<FnParamMetadata>> { + params + .iter() + .filter_map(|a| { + let _is_method = false; + let (name, ty) = match a { + // methods currently have an implicit self parameter in uniffi_meta + syn::FnArg::Receiver(_) => return None, + syn::FnArg::Typed(pat_ty) => { + let name = match &*pat_ty.pat { + syn::Pat::Ident(pat_id) => pat_id.ident.to_string(), + _ => unimplemented!(), + }; + + // methods currently have an implicit self parameter in uniffi_meta + if name == "self" { + return None; + } + + (name, &pat_ty.ty) + } + }; + + Some(convert_type(ty).map(|ty| FnParamMetadata { name, ty })) + }) + .collect() +} + +pub(crate) fn convert_return_type(ty: &syn::Type) -> syn::Result<Option<Type>> { + match ty { + syn::Type::Tuple(tup) if tup.elems.is_empty() => Ok(None), + _ => convert_type(ty).map(Some), + } +} + +pub(crate) fn convert_type(ty: &syn::Type) -> syn::Result<Type> { + let type_path = type_as_type_path(ty)?; + + if type_path.qself.is_some() { + return Err(syn::Error::new_spanned( + type_path, + "qualified self types are not currently supported by uniffi::export", + )); + } + + if type_path.path.segments.len() > 1 { + return Err(syn::Error::new_spanned( + type_path, + "qualified paths in types are not currently supported by uniffi::export", + )); + } + + match &type_path.path.segments.first() { + Some(seg) => match &seg.arguments { + syn::PathArguments::None => Ok(convert_bare_type_name(&seg.ident)), + syn::PathArguments::AngleBracketed(a) => convert_generic_type(&seg.ident, a), + syn::PathArguments::Parenthesized(_) => Err(type_not_supported(type_path)), + }, + None => Err(syn::Error::new_spanned( + type_path, + "unreachable: TypePath must have non-empty segments", + )), + } +} + +fn convert_generic_type( + ident: &Ident, + a: &syn::AngleBracketedGenericArguments, +) -> syn::Result<Type> { + let mut it = a.args.iter(); + match it.next() { + // `u8<>` is a valid way to write `u8` in the type namespace, so why not? + None => Ok(convert_bare_type_name(ident)), + Some(arg1) => match it.next() { + None => convert_generic_type1(ident, arg1), + Some(arg2) => match it.next() { + None => convert_generic_type2(ident, arg1, arg2), + Some(_) => Err(syn::Error::new_spanned( + ident, + "types with more than two generics are not currently + supported by uniffi::export", + )), + }, + }, + } +} + +fn convert_bare_type_name(ident: &Ident) -> Type { + let name = ident.to_string(); + match name.as_str() { + "u8" => Type::U8, + "u16" => Type::U16, + "u32" => Type::U32, + "u64" => Type::U64, + "i8" => Type::I8, + "i16" => Type::I16, + "i32" => Type::I32, + "i64" => Type::I64, + "f32" => Type::F32, + "f64" => Type::F64, + "bool" => Type::Bool, + "String" => Type::String, + _ => Type::Unresolved { name }, + } +} + +fn convert_generic_type1(ident: &Ident, arg: &syn::GenericArgument) -> syn::Result<Type> { + let arg = arg_as_type(arg)?; + match ident.to_string().as_str() { + "Arc" => Ok(Type::ArcObject { + object_name: type_as_type_name(arg)?.to_string(), + }), + "Option" => Ok(Type::Option { + inner_type: convert_type(arg)?.into(), + }), + "Vec" => Ok(Type::Vec { + inner_type: convert_type(arg)?.into(), + }), + _ => Err(type_not_supported(ident)), + } +} + +fn convert_generic_type2( + ident: &Ident, + arg1: &syn::GenericArgument, + arg2: &syn::GenericArgument, +) -> syn::Result<Type> { + let arg1 = arg_as_type(arg1)?; + let arg2 = arg_as_type(arg2)?; + + match ident.to_string().as_str() { + "HashMap" => Ok(Type::HashMap { + key_type: convert_type(arg1)?.into(), + value_type: convert_type(arg2)?.into(), + }), + _ => Err(type_not_supported(ident)), + } +} + +fn type_as_type_name(arg: &syn::Type) -> syn::Result<&Ident> { + type_as_type_path(arg)? + .path + .get_ident() + .ok_or_else(|| type_not_supported(arg)) +} + +pub(super) fn type_as_type_path(ty: &syn::Type) -> syn::Result<&syn::TypePath> { + match ty { + syn::Type::Group(g) => type_as_type_path(&g.elem), + syn::Type::Paren(p) => type_as_type_path(&p.elem), + syn::Type::Path(p) => Ok(p), + _ => Err(type_not_supported(ty)), + } +} + +fn arg_as_type(arg: &syn::GenericArgument) -> syn::Result<&syn::Type> { + match arg { + syn::GenericArgument::Type(t) => Ok(t), + _ => Err(syn::Error::new_spanned( + arg, + "non-type generic parameters are not currently supported by uniffi::export", + )), + } +} + +fn type_not_supported(ty: &impl ToTokens) -> syn::Error { + syn::Error::new_spanned( + ty, + "this type is not currently supported by uniffi::export in this position", + ) +} + +pub(crate) fn try_split_result(ty: &syn::Type) -> syn::Result<Option<(&syn::Type, Ident)>> { + let type_path = type_as_type_path(ty)?; + + if type_path.qself.is_some() { + return Err(syn::Error::new_spanned( + type_path, + "qualified self types are not currently supported by uniffi::export", + )); + } + + if type_path.path.segments.len() > 1 { + return Err(syn::Error::new_spanned( + type_path, + "qualified paths in types are not currently supported by uniffi::export", + )); + } + + let (ident, a) = match &type_path.path.segments.first() { + Some(seg) => match &seg.arguments { + syn::PathArguments::AngleBracketed(a) => (&seg.ident, a), + syn::PathArguments::None | syn::PathArguments::Parenthesized(_) => return Ok(None), + }, + None => return Ok(None), + }; + + let mut it = a.args.iter(); + if let Some(arg1) = it.next() { + if let Some(arg2) = it.next() { + if it.next().is_none() { + let arg1 = arg_as_type(arg1)?; + let arg2 = arg_as_type(arg2)?; + + if let "Result" = ident.to_string().as_str() { + let throws = type_as_type_name(arg2)?.to_owned(); + return Ok(Some((arg1, throws))); + } + } + } + } + + Ok(None) +} diff --git a/third_party/rust/uniffi_macros/src/export/metadata/function.rs b/third_party/rust/uniffi_macros/src/export/metadata/function.rs new file mode 100644 index 0000000000..a2cf6b7aa1 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export/metadata/function.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/. */ + +use uniffi_meta::FnMetadata; + +use super::convert::{convert_return_type, fn_param_metadata}; +use crate::export::{ExportItem, Signature}; + +pub(super) fn gen_fn_metadata(sig: syn::Signature, mod_path: &[String]) -> syn::Result<ExportItem> { + let sig = Signature::new(sig)?; + let metadata = fn_metadata(&sig, mod_path)?; + Ok(ExportItem::Function { sig, metadata }) +} + +fn fn_metadata(sig: &Signature, mod_path: &[String]) -> syn::Result<FnMetadata> { + let (return_type, throws) = match &sig.output { + Some(ret) => ( + convert_return_type(&ret.ty)?, + ret.throws.as_ref().map(ToString::to_string), + ), + None => (None, None), + }; + + Ok(FnMetadata { + module_path: mod_path.to_owned(), + name: sig.ident.to_string(), + inputs: fn_param_metadata(&sig.inputs)?, + return_type, + throws, + }) +} diff --git a/third_party/rust/uniffi_macros/src/export/metadata/impl_.rs b/third_party/rust/uniffi_macros/src/export/metadata/impl_.rs new file mode 100644 index 0000000000..302f0bfa13 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export/metadata/impl_.rs @@ -0,0 +1,93 @@ +/* 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_meta::MethodMetadata; + +use super::convert::{convert_return_type, fn_param_metadata, type_as_type_path}; +use crate::export::{ExportItem, Method, Signature}; + +pub(super) fn gen_impl_metadata( + item: syn::ItemImpl, + mod_path: &[String], +) -> syn::Result<ExportItem> { + if !item.generics.params.is_empty() || item.generics.where_clause.is_some() { + return Err(syn::Error::new_spanned( + &item.generics, + "generic impls are not currently supported by uniffi::export", + )); + } + + let type_path = type_as_type_path(&item.self_ty)?; + + if type_path.qself.is_some() { + return Err(syn::Error::new_spanned( + type_path, + "qualified self types are not currently supported by uniffi::export", + )); + } + + let self_ident = match type_path.path.get_ident() { + Some(id) => id, + None => { + return Err(syn::Error::new_spanned( + type_path, + "qualified paths in self-types are not currently supported by uniffi::export", + )); + } + }; + + let methods = item + .items + .into_iter() + .map(|it| gen_method_metadata(it, &self_ident.to_string(), mod_path)) + .collect(); + + Ok(ExportItem::Impl { + methods, + self_ident: self_ident.to_owned(), + }) +} + +fn gen_method_metadata( + it: syn::ImplItem, + self_name: &str, + mod_path: &[String], +) -> syn::Result<Method> { + let sig = match it { + syn::ImplItem::Method(m) => Signature::new(m.sig)?, + _ => { + return Err(syn::Error::new_spanned( + it, + "only methods are supported in impl blocks annotated with uniffi::export", + )); + } + }; + + let metadata = method_metadata(self_name, &sig, mod_path)?; + + Ok(Method { sig, metadata }) +} + +fn method_metadata( + self_name: &str, + sig: &Signature, + mod_path: &[String], +) -> syn::Result<MethodMetadata> { + let (return_type, throws) = match &sig.output { + Some(ret) => ( + convert_return_type(&ret.ty)?, + ret.throws.as_ref().map(ToString::to_string), + ), + None => (None, None), + }; + + Ok(MethodMetadata { + module_path: mod_path.to_owned(), + self_name: self_name.to_owned(), + name: sig.ident.to_string(), + inputs: fn_param_metadata(&sig.inputs)?, + return_type, + throws, + }) +} diff --git a/third_party/rust/uniffi_macros/src/export/scaffolding.rs b/third_party/rust/uniffi_macros/src/export/scaffolding.rs new file mode 100644 index 0000000000..f66e8bf844 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export/scaffolding.rs @@ -0,0 +1,199 @@ +/* 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 proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote, ToTokens}; +use syn::{parse_quote, FnArg, Pat}; + +use super::{FunctionReturn, Signature}; + +pub(super) fn gen_fn_scaffolding( + sig: &Signature, + mod_path: &[String], + checksum: u16, +) -> TokenStream { + let name = &sig.ident; + let name_s = name.to_string(); + + let ffi_ident = Ident::new( + &uniffi_meta::fn_ffi_symbol_name(mod_path, &name_s, checksum), + Span::call_site(), + ); + + const ERROR_MSG: &str = + "uniffi::export must be used on the impl block, not its containing fn's"; + let (params, args): (Vec<_>, Vec<_>) = collect_params(&sig.inputs, ERROR_MSG).unzip(); + + let fn_call = quote! { + #name(#(#args),*) + }; + + gen_ffi_function(sig, ffi_ident, ¶ms, fn_call) +} + +pub(super) fn gen_method_scaffolding( + sig: &Signature, + mod_path: &[String], + checksum: u16, + self_ident: &Ident, +) -> TokenStream { + let name = &sig.ident; + let name_s = name.to_string(); + + let ffi_name = format!("impl_{self_ident}_{name_s}"); + let ffi_ident = Ident::new( + &uniffi_meta::fn_ffi_symbol_name(mod_path, &ffi_name, checksum), + Span::call_site(), + ); + + let mut params_args = (Vec::new(), Vec::new()); + + const RECEIVER_ERROR: &str = "unreachable: only first parameter can be method receiver"; + let mut assoc_fn_error = None; + let fn_call_prefix = match sig.inputs.first() { + Some(arg) if is_receiver(arg) => { + let ffi_converter = quote! { + <::std::sync::Arc<#self_ident> as ::uniffi::FfiConverter> + }; + + params_args.0.push(quote! { this: #ffi_converter::FfiType }); + + let remaining_args = sig.inputs.iter().skip(1); + params_args.extend(collect_params(remaining_args, RECEIVER_ERROR)); + + quote! { + #ffi_converter::try_lift(this).unwrap_or_else(|err| { + ::std::panic!("Failed to convert arg 'self': {}", err) + }). + } + } + _ => { + assoc_fn_error = Some( + syn::Error::new_spanned( + &sig.ident, + "associated functions are not currently supported", + ) + .into_compile_error(), + ); + params_args.extend(collect_params(&sig.inputs, RECEIVER_ERROR)); + quote! { #self_ident:: } + } + }; + + let (params, args) = params_args; + + let fn_call = quote! { + #assoc_fn_error + #fn_call_prefix #name(#(#args),*) + }; + + gen_ffi_function(sig, ffi_ident, ¶ms, fn_call) +} + +fn is_receiver(fn_arg: &FnArg) -> bool { + match fn_arg { + FnArg::Receiver(_) => true, + FnArg::Typed(pat_ty) => matches!(&*pat_ty.pat, Pat::Ident(i) if i.ident == "self"), + } +} + +fn collect_params<'a>( + inputs: impl IntoIterator<Item = &'a FnArg> + 'a, + receiver_error_msg: &'static str, +) -> impl Iterator<Item = (TokenStream, TokenStream)> + 'a { + fn receiver_error( + receiver: impl ToTokens, + receiver_error_msg: &str, + ) -> (TokenStream, TokenStream) { + let param = quote! { &self }; + let arg = syn::Error::new_spanned(receiver, receiver_error_msg).into_compile_error(); + (param, arg) + } + + inputs.into_iter().enumerate().map(|(i, arg)| { + let (ty, name) = match arg { + FnArg::Receiver(r) => { + return receiver_error(r, receiver_error_msg); + } + FnArg::Typed(pat_ty) => { + let name = match &*pat_ty.pat { + Pat::Ident(i) if i.ident == "self" => { + return receiver_error(i, receiver_error_msg); + } + Pat::Ident(i) => Some(i.ident.to_string()), + _ => None, + }; + + (&pat_ty.ty, name) + } + }; + + let arg_n = format_ident!("arg{i}"); + let param = quote! { #arg_n: <#ty as ::uniffi::FfiConverter>::FfiType }; + + // FIXME: With UDL, fallible functions use uniffi::lower_anyhow_error_or_panic instead of + // panicking unconditionally. This seems cleaner though. + let panic_fmt = match name { + Some(name) => format!("Failed to convert arg '{name}': {{}}"), + None => format!("Failed to convert arg #{i}: {{}}"), + }; + let arg = quote! { + <#ty as ::uniffi::FfiConverter>::try_lift(#arg_n).unwrap_or_else(|err| { + ::std::panic!(#panic_fmt, err) + }) + }; + + (param, arg) + }) +} + +fn gen_ffi_function( + sig: &Signature, + ffi_ident: Ident, + params: &[TokenStream], + rust_fn_call: TokenStream, +) -> TokenStream { + let name = &sig.ident; + let name_s = name.to_string(); + + let unit_slot; + let (ty, throws) = match &sig.output { + Some(FunctionReturn { ty, throws }) => (ty, throws), + None => { + unit_slot = parse_quote! { () }; + (&unit_slot, &None) + } + }; + + let return_expr = if let Some(error_ident) = throws { + quote! { + ::uniffi::call_with_result(call_status, || { + let val = #rust_fn_call.map_err(|e| { + <#error_ident as ::uniffi::FfiConverter>::lower( + ::std::convert::Into::into(e), + ) + })?; + Ok(<#ty as ::uniffi::FfiReturn>::lower(val)) + }) + } + } else { + quote! { + ::uniffi::call_with_output(call_status, || { + <#ty as ::uniffi::FfiReturn>::lower(#rust_fn_call) + }) + } + }; + + quote! { + #[doc(hidden)] + #[no_mangle] + pub extern "C" fn #ffi_ident( + #(#params,)* + call_status: &mut ::uniffi::RustCallStatus, + ) -> <#ty as ::uniffi::FfiReturn>::FfiType { + ::uniffi::deps::log::debug!(#name_s); + #return_expr + } + } +} diff --git a/third_party/rust/uniffi_macros/src/lib.rs b/third_party/rust/uniffi_macros/src/lib.rs new file mode 100644 index 0000000000..a38705e57d --- /dev/null +++ b/third_party/rust/uniffi_macros/src/lib.rs @@ -0,0 +1,179 @@ +/* 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/. */ +#![cfg_attr(feature = "nightly", feature(proc_macro_expand))] + +//! Macros for `uniffi`. +//! +//! Currently this is just for easily generating integration tests, but maybe +//! we'll put some other code-annotation helper macros in here at some point. + +use camino::Utf8Path; +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, LitStr}; +use util::rewrite_self_type; + +mod enum_; +mod error; +mod export; +mod object; +mod record; +mod test; +mod util; + +use self::{ + enum_::expand_enum, error::expand_error, export::expand_export, object::expand_object, + record::expand_record, +}; + +/// A macro to build testcases for a component's generated bindings. +/// +/// This macro provides some plumbing to write automated tests for the generated +/// foreign language bindings of a component. As a component author, you can write +/// script files in the target foreign language(s) that exercise you component API, +/// and then call this macro to produce a `cargo test` testcase from each one. +/// The generated code will execute your script file with appropriate configuration and +/// environment to let it load the component bindings, and will pass iff the script +/// exits successfully. +/// +/// To use it, invoke the macro with the name of a fixture/example crate as the first argument, +/// then one or more file paths relative to the crate root directory. It will produce one `#[test]` +/// function per file, in a manner designed to play nicely with `cargo test` and its test filtering +/// options. +#[proc_macro] +pub fn build_foreign_language_testcases(tokens: TokenStream) -> TokenStream { + test::build_foreign_language_testcases(tokens) +} + +#[proc_macro_attribute] +pub fn export(_attr: TokenStream, input: TokenStream) -> TokenStream { + let input2 = proc_macro2::TokenStream::from(input.clone()); + + let gen_output = || { + let mod_path = util::mod_path()?; + let mut item = syn::parse(input)?; + + // If the input is an `impl` block, rewrite any uses of the `Self` type + // alias to the actual type, so we don't have to special-case it in the + // metadata collection or scaffolding code generation (which generates + // new functions outside of the `impl`). + rewrite_self_type(&mut item); + + let metadata = export::gen_metadata(item, &mod_path)?; + Ok(expand_export(metadata, &mod_path)) + }; + let output = gen_output().unwrap_or_else(syn::Error::into_compile_error); + + quote! { + #input2 + #output + } + .into() +} + +#[proc_macro_derive(Record)] +pub fn derive_record(input: TokenStream) -> TokenStream { + let mod_path = match util::mod_path() { + Ok(p) => p, + Err(e) => return e.into_compile_error().into(), + }; + let input = parse_macro_input!(input); + + expand_record(input, mod_path).into() +} + +#[proc_macro_derive(Enum)] +pub fn derive_enum(input: TokenStream) -> TokenStream { + let mod_path = match util::mod_path() { + Ok(p) => p, + Err(e) => return e.into_compile_error().into(), + }; + let input = parse_macro_input!(input); + + expand_enum(input, mod_path).into() +} + +#[proc_macro_derive(Object)] +pub fn derive_object(input: TokenStream) -> TokenStream { + let mod_path = match util::mod_path() { + Ok(p) => p, + Err(e) => return e.into_compile_error().into(), + }; + let input = parse_macro_input!(input); + + expand_object(input, mod_path).into() +} + +#[proc_macro_derive(Error, attributes(uniffi))] +pub fn derive_error(input: TokenStream) -> TokenStream { + let mod_path = match util::mod_path() { + Ok(p) => p, + Err(e) => return e.into_compile_error().into(), + }; + let input = parse_macro_input!(input); + + expand_error(input, mod_path).into() +} + +/// A helper macro to include generated component scaffolding. +/// +/// This is a simple convenience macro to include the UniFFI component +/// scaffolding as built by `uniffi_build::generate_scaffolding`. +/// Use it like so: +/// +/// ```rs +/// uniffi_macros::include_scaffolding!("my_component_name"); +/// ``` +/// +/// This will expand to the appropriate `include!` invocation to include +/// the generated `my_component_name.uniffi.rs` (which it assumes has +/// been successfully built by your crate's `build.rs` script). +/// +#[proc_macro] +pub fn include_scaffolding(component_name: TokenStream) -> TokenStream { + let name = syn::parse_macro_input!(component_name as syn::LitStr); + if std::env::var("OUT_DIR").is_err() { + quote! { + compile_error!("This macro assumes the crate has a build.rs script, but $OUT_DIR is not present"); + } + } else { + quote! { + include!(concat!(env!("OUT_DIR"), "/", #name, ".uniffi.rs")); + } + }.into() +} + +/// A helper macro to generate and include component scaffolding. +/// +/// This is a convenience macro designed for writing `trybuild`-style tests and +/// probably shouldn't be used for production code. Given the path to a `.udl` file, +/// if will run `uniffi-bindgen` to produce the corresponding Rust scaffolding and then +/// include it directly into the calling file. Like so: +/// +/// ```rs +/// uniffi_macros::generate_and_include_scaffolding!("path/to/my/interface.udl"); +/// ``` +/// +#[proc_macro] +pub fn generate_and_include_scaffolding(udl_file: TokenStream) -> TokenStream { + let udl_file = syn::parse_macro_input!(udl_file as syn::LitStr); + let udl_file_string = udl_file.value(); + let udl_file_path = Utf8Path::new(&udl_file_string); + if std::env::var("OUT_DIR").is_err() { + quote! { + compile_error!("This macro assumes the crate has a build.rs script, but $OUT_DIR is not present"); + } + } else if uniffi_build::generate_scaffolding(udl_file_path).is_err() { + quote! { + compile_error!(concat!("Failed to generate scaffolding from UDL file at ", #udl_file)); + } + } else { + // We know the filename is good because `generate_scaffolding` succeeded, + // so this `unwrap` will never fail. + let name = LitStr::new(udl_file_path.file_stem().unwrap(), udl_file.span()); + quote! { + uniffi_macros::include_scaffolding!(#name); + } + }.into() +} diff --git a/third_party/rust/uniffi_macros/src/object.rs b/third_party/rust/uniffi_macros/src/object.rs new file mode 100644 index 0000000000..2f4988530c --- /dev/null +++ b/third_party/rust/uniffi_macros/src/object.rs @@ -0,0 +1,35 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::DeriveInput; +use uniffi_meta::ObjectMetadata; + +use crate::util::{assert_type_eq, create_metadata_static_var}; + +pub fn expand_object(input: DeriveInput, module_path: Vec<String>) -> TokenStream { + let ident = &input.ident; + let name = ident.to_string(); + let metadata = ObjectMetadata { module_path, name }; + let free_fn_ident = Ident::new(&metadata.free_ffi_symbol_name(), Span::call_site()); + let meta_static_var = create_metadata_static_var(ident, metadata.into()); + let type_assertion = assert_type_eq(ident, quote! { crate::uniffi_types::#ident }); + + quote! { + #[doc(hidden)] + #[no_mangle] + pub extern "C" fn #free_fn_ident( + ptr: *const ::std::ffi::c_void, + call_status: &mut ::uniffi::RustCallStatus + ) { + uniffi::call_with_output(call_status, || { + assert!(!ptr.is_null()); + let ptr = ptr.cast::<#ident>(); + unsafe { + ::std::sync::Arc::decrement_strong_count(ptr); + } + }); + } + + #meta_static_var + #type_assertion + } +} diff --git a/third_party/rust/uniffi_macros/src/record.rs b/third_party/rust/uniffi_macros/src/record.rs new file mode 100644 index 0000000000..60d0962503 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/record.rs @@ -0,0 +1,108 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::{Data, DeriveInput, Field, Fields}; +use uniffi_meta::{FieldMetadata, RecordMetadata}; + +use crate::{ + export::metadata::convert::convert_type, + util::{assert_type_eq, create_metadata_static_var, try_read_field}, +}; + +pub fn expand_record(input: DeriveInput, module_path: Vec<String>) -> TokenStream { + let fields = match input.data { + Data::Struct(s) => Some(s.fields), + _ => None, + }; + + let ident = &input.ident; + + let (write_impl, try_read_fields) = match &fields { + Some(fields) => ( + fields.iter().map(write_field).collect(), + fields.iter().map(try_read_field).collect(), + ), + None => { + let unimplemented = quote! { ::std::unimplemented!() }; + (unimplemented.clone(), unimplemented) + } + }; + + let meta_static_var = if let Some(fields) = fields { + match record_metadata(ident, fields, module_path) { + Ok(metadata) => create_metadata_static_var(ident, metadata.into()), + Err(e) => e.into_compile_error(), + } + } else { + syn::Error::new( + Span::call_site(), + "This derive must only be used on structs", + ) + .into_compile_error() + }; + + let type_assertion = assert_type_eq(ident, quote! { crate::uniffi_types::#ident }); + + quote! { + #[automatically_derived] + impl ::uniffi::RustBufferFfiConverter for #ident { + type RustType = Self; + + fn write(obj: Self, buf: &mut ::std::vec::Vec<u8>) { + #write_impl + } + + fn try_read(buf: &mut &[::std::primitive::u8]) -> ::uniffi::deps::anyhow::Result<Self> { + Ok(Self { #try_read_fields }) + } + } + + #meta_static_var + #type_assertion + } +} + +fn record_metadata( + ident: &Ident, + fields: Fields, + module_path: Vec<String>, +) -> syn::Result<RecordMetadata> { + let name = ident.to_string(); + let fields = match fields { + Fields::Named(fields) => fields.named, + _ => { + return Err(syn::Error::new( + Span::call_site(), + "UniFFI only supports structs with named fields", + )); + } + }; + + let fields = fields + .iter() + .map(field_metadata) + .collect::<syn::Result<_>>()?; + + Ok(RecordMetadata { + module_path, + name, + fields, + }) +} + +fn field_metadata(f: &Field) -> syn::Result<FieldMetadata> { + let name = f.ident.as_ref().unwrap().to_string(); + + Ok(FieldMetadata { + name, + ty: convert_type(&f.ty)?, + }) +} + +fn write_field(f: &Field) -> TokenStream { + let ident = &f.ident; + let ty = &f.ty; + + quote! { + <#ty as ::uniffi::FfiConverter>::write(obj.#ident, buf); + } +} diff --git a/third_party/rust/uniffi_macros/src/test.rs b/third_party/rust/uniffi_macros/src/test.rs new file mode 100644 index 0000000000..ddcbf46000 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/test.rs @@ -0,0 +1,90 @@ +/* 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 camino::{Utf8Path, Utf8PathBuf}; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use std::env; +use syn::{parse_macro_input, punctuated::Punctuated, LitStr, Token}; + +pub(crate) fn build_foreign_language_testcases(tokens: TokenStream) -> TokenStream { + let input = parse_macro_input!(tokens as BuildForeignLanguageTestCaseInput); + // we resolve each path relative to the crate root directory. + let pkg_dir = env::var("CARGO_MANIFEST_DIR") + .expect("Missing $CARGO_MANIFEST_DIR, cannot build tests for generated bindings"); + + // For each test file found, generate a matching testcase. + let test_functions = input + .test_scripts + .iter() + .map(|file_path| { + let test_file_pathbuf: Utf8PathBuf = [&pkg_dir, file_path].iter().collect(); + let test_file_path = test_file_pathbuf.to_string(); + let test_file_name = test_file_pathbuf + .file_name() + .expect("Test file has no name, cannot build tests for generated bindings"); + let test_name = format_ident!( + "uniffi_foreign_language_testcase_{}", + test_file_name.replace(|c: char| !c.is_alphanumeric(), "_") + ); + let run_test = match test_file_pathbuf.extension() { + Some("kts") => quote! { + uniffi::kotlin_run_test + }, + Some("swift") => quote! { + uniffi::swift_run_test + }, + Some("py") => quote! { + uniffi::python_run_test + }, + Some("rb") => quote! { + uniffi::ruby_run_test + }, + _ => panic!("Unexpected extension for test script: {test_file_name}"), + }; + let maybe_ignore = if should_skip_path(&test_file_pathbuf) { + quote! { #[ignore] } + } else { + quote! {} + }; + quote! { + #maybe_ignore + #[test] + fn #test_name () -> uniffi::deps::anyhow::Result<()> { + #run_test( + std::env!("CARGO_TARGET_TMPDIR"), + std::env!("CARGO_PKG_NAME"), + #test_file_path) + } + } + }) + .collect::<Vec<proc_macro2::TokenStream>>(); + let test_module = quote! { + #(#test_functions)* + }; + TokenStream::from(test_module) +} + +// UNIFFI_TESTS_DISABLE_EXTENSIONS contains a comma-sep'd list of extensions (without leading `.`) +fn should_skip_path(path: &Utf8Path) -> bool { + let ext = path.extension().expect("File has no extension!"); + env::var("UNIFFI_TESTS_DISABLE_EXTENSIONS") + .map(|v| v.split(',').any(|look| look == ext)) + .unwrap_or(false) +} + +struct BuildForeignLanguageTestCaseInput { + test_scripts: Vec<String>, +} + +impl syn::parse::Parse for BuildForeignLanguageTestCaseInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + let test_scripts = Punctuated::<LitStr, Token![,]>::parse_terminated(input)? + .iter() + .map(|s| s.value()) + .collect(); + + Ok(Self { test_scripts }) + } +} diff --git a/third_party/rust/uniffi_macros/src/util.rs b/third_party/rust/uniffi_macros/src/util.rs new file mode 100644 index 0000000000..e8e03b3b5d --- /dev/null +++ b/third_party/rust/uniffi_macros/src/util.rs @@ -0,0 +1,223 @@ +/* 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 proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + spanned::Spanned, + visit_mut::VisitMut, + Attribute, Item, Token, Type, +}; +use uniffi_meta::Metadata; + +#[cfg(not(feature = "nightly"))] +pub fn mod_path() -> syn::Result<Vec<String>> { + // Without the nightly feature and TokenStream::expand_expr, just return the crate name + + use std::path::Path; + + use fs_err as fs; + use once_cell::sync::Lazy; + use serde::Deserialize; + + #[derive(Deserialize)] + struct CargoToml { + package: Package, + #[serde(default)] + lib: Lib, + } + + #[derive(Deserialize)] + struct Package { + name: String, + } + + #[derive(Default, Deserialize)] + struct Lib { + name: Option<String>, + } + + static LIB_CRATE_MOD_PATH: Lazy<Result<Vec<String>, String>> = Lazy::new(|| { + let manifest_dir = + std::env::var_os("CARGO_MANIFEST_DIR").ok_or("`CARGO_MANIFEST_DIR` is not set")?; + + let cargo_toml_bytes = + fs::read(Path::new(&manifest_dir).join("Cargo.toml")).map_err(|e| e.to_string())?; + let cargo_toml = toml::from_slice::<CargoToml>(&cargo_toml_bytes) + .map_err(|e| format!("Failed to parse `Cargo.toml`: {e}"))?; + + let lib_crate_name = cargo_toml + .lib + .name + .unwrap_or_else(|| cargo_toml.package.name.replace('-', "_")); + + Ok(vec![lib_crate_name]) + }); + + LIB_CRATE_MOD_PATH + .clone() + .map_err(|e| syn::Error::new(Span::call_site(), e)) +} + +#[cfg(feature = "nightly")] +pub fn mod_path() -> syn::Result<Vec<String>> { + use proc_macro::TokenStream; + + let module_path_invoc = TokenStream::from(quote! { ::core::module_path!() }); + // We ask the compiler what `module_path!()` expands to here. + // This is a nightly feature, tracked at https://github.com/rust-lang/rust/issues/90765 + let expanded_module_path = TokenStream::expand_expr(&module_path_invoc) + .map_err(|e| syn::Error::new(Span::call_site(), e))?; + Ok(syn::parse::<syn::LitStr>(expanded_module_path)? + .value() + .split("::") + .collect()) +} + +/// Rewrite Self type alias usage in an impl block to the type itself. +/// +/// For example, +/// +/// ```ignore +/// impl some::module::Foo { +/// fn method( +/// self: Arc<Self>, +/// arg: Option<Bar<(), Self>>, +/// ) -> Result<Self, Error> { +/// todo!() +/// } +/// } +/// ``` +/// +/// will be rewritten to +/// +/// ```ignore +/// impl some::module::Foo { +/// fn method( +/// self: Arc<some::module::Foo>, +/// arg: Option<Bar<(), some::module::Foo>>, +/// ) -> Result<some::module::Foo, Error> { +/// todo!() +/// } +/// } +/// ``` +pub fn rewrite_self_type(item: &mut Item) { + let item = match item { + Item::Impl(i) => i, + _ => return, + }; + + struct RewriteSelfVisitor<'a>(&'a Type); + + impl<'a> VisitMut for RewriteSelfVisitor<'a> { + fn visit_type_mut(&mut self, i: &mut Type) { + match i { + Type::Path(p) if p.qself.is_none() && p.path.is_ident("Self") => { + *i = self.0.clone(); + } + _ => syn::visit_mut::visit_type_mut(self, i), + } + } + } + + let mut visitor = RewriteSelfVisitor(&item.self_ty); + for item in &mut item.items { + visitor.visit_impl_item_mut(item); + } +} + +pub fn try_read_field(f: &syn::Field) -> TokenStream { + let ident = &f.ident; + let ty = &f.ty; + + quote! { + #ident: <#ty as ::uniffi::FfiConverter>::try_read(buf)?, + } +} + +pub fn create_metadata_static_var(name: &Ident, val: Metadata) -> TokenStream { + let data: Vec<u8> = bincode::serialize(&val).expect("Error serializing metadata item"); + let count = data.len(); + let var_name = format_ident!("UNIFFI_META_{}", name); + + quote! { + #[no_mangle] + #[doc(hidden)] + pub static #var_name: [u8; #count] = [#(#data),*]; + } +} + +pub fn assert_type_eq(a: impl ToTokens + Spanned, b: impl ToTokens) -> TokenStream { + quote_spanned! {a.span()=> + #[allow(unused_qualifications)] + const _: () = { + ::uniffi::deps::static_assertions::assert_type_eq_all!(#a, #b); + }; + } +} + +pub fn chain<T>( + a: impl IntoIterator<Item = T>, + b: impl IntoIterator<Item = T>, +) -> impl Iterator<Item = T> { + a.into_iter().chain(b) +} + +pub trait UniffiAttribute: Default + Parse { + fn merge(self, other: Self) -> syn::Result<Self>; +} + +#[derive(Default)] +struct AttributeNotAllowedHere; + +impl Parse for AttributeNotAllowedHere { + fn parse(input: ParseStream<'_>) -> syn::Result<Self> { + Err(syn::Error::new( + input.span(), + "UniFFI attributes are not currently recognized in this position", + )) + } +} + +impl UniffiAttribute for AttributeNotAllowedHere { + fn merge(self, _other: Self) -> syn::Result<Self> { + Ok(Self) + } +} + +pub trait AttributeSliceExt { + fn parse_uniffi_attributes<T: UniffiAttribute>(&self) -> syn::Result<T>; + fn attributes_not_allowed_here(&self) -> Option<syn::Error>; +} + +impl AttributeSliceExt for [Attribute] { + fn parse_uniffi_attributes<T: UniffiAttribute>(&self) -> syn::Result<T> { + self.iter() + .filter(|attr| attr.path.is_ident("uniffi")) + .try_fold(T::default(), |res, attr| { + let list: Punctuated<T, Token![,]> = + attr.parse_args_with(Punctuated::parse_terminated)?; + list.into_iter().try_fold(res, T::merge) + }) + } + + fn attributes_not_allowed_here(&self) -> Option<syn::Error> { + self.parse_uniffi_attributes::<AttributeNotAllowedHere>() + .err() + } +} + +pub fn either_attribute_arg<T: ToTokens>(a: Option<T>, b: Option<T>) -> syn::Result<Option<T>> { + match (a, b) { + (None, None) => Ok(None), + (Some(val), None) | (None, Some(val)) => Ok(Some(val)), + (Some(a), Some(b)) => { + let mut error = syn::Error::new_spanned(a, "redundant attribute argument"); + error.combine(syn::Error::new_spanned(b, "note: first one here")); + Err(error) + } + } +} diff --git a/third_party/rust/uniffi_meta/.cargo-checksum.json b/third_party/rust/uniffi_meta/.cargo-checksum.json new file mode 100644 index 0000000000..c7b53da275 --- /dev/null +++ b/third_party/rust/uniffi_meta/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"034db422dda8022d11beeac51c91bb472c9fc8d523e95a01a348ed6c90383bc1","src/lib.rs":"fde4a760b126cacf7e8008a8a8ce4e97b3c91d1ec3db08df4945bee549dee16a"},"package":"66fdab2c436aed7a6391bec64204ec33948bfed9b11b303235740771f85c4ea6"}
\ No newline at end of file diff --git a/third_party/rust/uniffi_meta/Cargo.toml b/third_party/rust/uniffi_meta/Cargo.toml new file mode 100644 index 0000000000..ee71f4c121 --- /dev/null +++ b/third_party/rust/uniffi_meta/Cargo.toml @@ -0,0 +1,33 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi_meta" +version = "0.23.0" +description = "uniffi_meta" +homepage = "https://mozilla.github.io/uniffi-rs" +keywords = [ + "ffi", + "bindgen", +] +license = "MPL-2.0" +repository = "https://github.com/mozilla/uniffi-rs" + +[dependencies.serde] +version = "1.0.136" +features = ["derive"] + +[dependencies.siphasher] +version = "0.3" + +[dependencies.uniffi_checksum_derive] +version = "0.23.0" diff --git a/third_party/rust/uniffi_meta/src/lib.rs b/third_party/rust/uniffi_meta/src/lib.rs new file mode 100644 index 0000000000..13af3b112d --- /dev/null +++ b/third_party/rust/uniffi_meta/src/lib.rs @@ -0,0 +1,280 @@ +/* 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::BTreeMap, hash::Hasher}; +pub use uniffi_checksum_derive::Checksum; + +use serde::{Deserialize, Serialize}; + +/// Similar to std::hash::Hash. +/// +/// Implementations of this trait are expected to update the hasher state in +/// the same way across platforms. #[derive(Checksum)] will do the right thing. +pub trait Checksum { + fn checksum<H: Hasher>(&self, state: &mut H); +} + +impl Checksum for bool { + fn checksum<H: Hasher>(&self, state: &mut H) { + state.write_u8(*self as u8); + } +} + +impl Checksum for u64 { + fn checksum<H: Hasher>(&self, state: &mut H) { + state.write(&self.to_le_bytes()); + } +} + +impl Checksum for i64 { + fn checksum<H: Hasher>(&self, state: &mut H) { + state.write(&self.to_le_bytes()); + } +} + +impl<T: Checksum> Checksum for Box<T> { + fn checksum<H: Hasher>(&self, state: &mut H) { + (**self).checksum(state) + } +} + +impl<T: Checksum> Checksum for [T] { + fn checksum<H: Hasher>(&self, state: &mut H) { + state.write(&(self.len() as u64).to_le_bytes()); + for item in self { + Checksum::checksum(item, state); + } + } +} + +impl<T: Checksum> Checksum for Vec<T> { + fn checksum<H: Hasher>(&self, state: &mut H) { + Checksum::checksum(&**self, state); + } +} + +impl<K: Checksum, V: Checksum> Checksum for BTreeMap<K, V> { + fn checksum<H: Hasher>(&self, state: &mut H) { + state.write(&(self.len() as u64).to_le_bytes()); + for (key, value) in self { + Checksum::checksum(key, state); + Checksum::checksum(value, state); + } + } +} + +impl<T: Checksum> Checksum for Option<T> { + fn checksum<H: Hasher>(&self, state: &mut H) { + match self { + None => state.write(&0u64.to_le_bytes()), + Some(value) => { + state.write(&1u64.to_le_bytes()); + Checksum::checksum(value, state) + } + } + } +} + +impl Checksum for str { + fn checksum<H: Hasher>(&self, state: &mut H) { + state.write(self.as_bytes()); + state.write_u8(0xff); + } +} + +impl Checksum for String { + fn checksum<H: Hasher>(&self, state: &mut H) { + (**self).checksum(state) + } +} + +impl Checksum for &str { + fn checksum<H: Hasher>(&self, state: &mut H) { + (**self).checksum(state) + } +} + +#[derive(Clone, Debug, Checksum, Deserialize, Serialize)] +pub struct FnMetadata { + pub module_path: Vec<String>, + pub name: String, + pub inputs: Vec<FnParamMetadata>, + pub return_type: Option<Type>, + pub throws: Option<String>, +} + +impl FnMetadata { + pub fn ffi_symbol_name(&self) -> String { + fn_ffi_symbol_name(&self.module_path, &self.name, checksum(self)) + } +} + +#[derive(Clone, Debug, Checksum, Deserialize, Serialize)] +pub struct MethodMetadata { + pub module_path: Vec<String>, + pub self_name: String, + pub name: String, + pub inputs: Vec<FnParamMetadata>, + pub return_type: Option<Type>, + pub throws: Option<String>, +} + +impl MethodMetadata { + pub fn ffi_symbol_name(&self) -> String { + let full_name = format!("impl_{}_{}", self.self_name, self.name); + fn_ffi_symbol_name(&self.module_path, &full_name, checksum(self)) + } +} + +#[derive(Clone, Debug, Checksum, Deserialize, Serialize)] +pub struct FnParamMetadata { + pub name: String, + #[serde(rename = "type")] + pub ty: Type, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Checksum, Deserialize, Serialize)] +pub enum Type { + U8, + U16, + U32, + U64, + I8, + I16, + I32, + I64, + F32, + F64, + Bool, + String, + Option { + inner_type: Box<Type>, + }, + Vec { + inner_type: Box<Type>, + }, + HashMap { + key_type: Box<Type>, + value_type: Box<Type>, + }, + ArcObject { + object_name: String, + }, + Unresolved { + name: String, + }, +} + +#[derive(Clone, Debug, Checksum, Deserialize, Serialize)] +pub struct RecordMetadata { + pub module_path: Vec<String>, + pub name: String, + pub fields: Vec<FieldMetadata>, +} + +#[derive(Clone, Debug, Checksum, Deserialize, Serialize)] +pub struct FieldMetadata { + pub name: String, + #[serde(rename = "type")] + pub ty: Type, +} + +#[derive(Clone, Debug, Checksum, Deserialize, Serialize)] +pub struct EnumMetadata { + pub module_path: Vec<String>, + pub name: String, + pub variants: Vec<VariantMetadata>, +} + +#[derive(Clone, Debug, Checksum, Deserialize, Serialize)] +pub struct VariantMetadata { + pub name: String, + pub fields: Vec<FieldMetadata>, +} + +#[derive(Clone, Debug, Checksum, Deserialize, Serialize)] +pub struct ObjectMetadata { + pub module_path: Vec<String>, + pub name: String, +} + +impl ObjectMetadata { + /// FFI symbol name for the `free` function for this object. + /// + /// This function is used to free the memory used by this object. + pub fn free_ffi_symbol_name(&self) -> String { + let free_name = format!("object_free_{}", self.name); + fn_ffi_symbol_name(&self.module_path, &free_name, checksum(self)) + } +} + +#[derive(Clone, Debug, Checksum, Deserialize, Serialize)] +pub struct ErrorMetadata { + pub module_path: Vec<String>, + pub name: String, + pub variants: Vec<VariantMetadata>, + pub flat: bool, +} + +/// Returns the last 16 bits of the value's hash as computed with [`SipHasher13`]. +/// +/// To be used as a checksum of FFI symbols, as a safeguard against different UniFFI versions being +/// used for scaffolding and bindings generation. +pub fn checksum<T: Checksum>(val: &T) -> u16 { + let mut hasher = siphasher::sip::SipHasher13::new(); + val.checksum(&mut hasher); + (hasher.finish() & 0x000000000000FFFF) as u16 +} + +pub fn fn_ffi_symbol_name(mod_path: &[String], name: &str, checksum: u16) -> String { + let mod_path = mod_path.join("__"); + format!("_uniffi_{mod_path}_{name}_{checksum:x}") +} + +/// Enum covering all the possible metadata types +#[derive(Clone, Debug, Checksum, Deserialize, Serialize)] +pub enum Metadata { + Func(FnMetadata), + Method(MethodMetadata), + Record(RecordMetadata), + Enum(EnumMetadata), + Object(ObjectMetadata), + Error(ErrorMetadata), +} + +impl From<FnMetadata> for Metadata { + fn from(value: FnMetadata) -> Metadata { + Self::Func(value) + } +} + +impl From<MethodMetadata> for Metadata { + fn from(m: MethodMetadata) -> Self { + Self::Method(m) + } +} + +impl From<RecordMetadata> for Metadata { + fn from(r: RecordMetadata) -> Self { + Self::Record(r) + } +} + +impl From<EnumMetadata> for Metadata { + fn from(e: EnumMetadata) -> Self { + Self::Enum(e) + } +} + +impl From<ObjectMetadata> for Metadata { + fn from(v: ObjectMetadata) -> Self { + Self::Object(v) + } +} + +impl From<ErrorMetadata> for Metadata { + fn from(v: ErrorMetadata) -> Self { + Self::Error(v) + } +} diff --git a/third_party/rust/uniffi_testing/.cargo-checksum.json b/third_party/rust/uniffi_testing/.cargo-checksum.json new file mode 100644 index 0000000000..45c384e054 --- /dev/null +++ b/third_party/rust/uniffi_testing/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"6077dda68909ce797d4b68f6a617bafb83a7a140ea43a90633b2bb78f37fab0a","README.md":"ec6aba24af9a011ef6647422aa22efabdee519cdee3da1a9f9033b07b7cbdb0d","src/lib.rs":"925b93068b9d3a9d890f3ee3e6d760fd032359ac083350fd919cbb71a2a6580c"},"package":"92b0570953ec41d97ce23e3b92161ac18231670a1f97523258a6d2ab76d7f76c"}
\ No newline at end of file diff --git a/third_party/rust/uniffi_testing/Cargo.toml b/third_party/rust/uniffi_testing/Cargo.toml new file mode 100644 index 0000000000..9869e861c3 --- /dev/null +++ b/third_party/rust/uniffi_testing/Cargo.toml @@ -0,0 +1,43 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi_testing" +version = "0.23.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +description = "a multi-language bindings generator for rust (testing helpers)" +homepage = "https://mozilla.github.io/uniffi-rs" +documentation = "https://mozilla.github.io/uniffi-rs" +readme = "README.md" +license = "MPL-2.0" +repository = "https://github.com/mozilla/uniffi-rs" + +[dependencies.anyhow] +version = "1" + +[dependencies.camino] +version = "1.0.8" + +[dependencies.cargo_metadata] +version = "0.15" + +[dependencies.fs-err] +version = "2.7.0" + +[dependencies.once_cell] +version = "1.12" + +[dependencies.serde] +version = "1" + +[dependencies.serde_json] +version = "1" diff --git a/third_party/rust/uniffi_testing/README.md b/third_party/rust/uniffi_testing/README.md new file mode 100644 index 0000000000..c171b26373 --- /dev/null +++ b/third_party/rust/uniffi_testing/README.md @@ -0,0 +1,18 @@ +This crate contains helper code for testing bindings. Our general system is to +generate bindings for the libraries from the examples and fixtures +directories, then execute a script that tests the bindings. + +Each bindings crate can do this in a different way, but the typical system is: + + - Construct a `UniFFITestHelper` struct to assist the process + - Call `UniFFITestHelper.create_out_dir()` to create a temp directory to + store testing files + - Call `UniFFITestHelper.copy_cdylibs_to_out_dir()` to copy the dylib + artifacts for the example/fixture library to the `out_dir`. This is needed + because the bindings code dynamically links to or loads from this library. + - Call `UniFFITestHelper.get_compile_sources()` to iterate over (`udl_path`, + `uniffi_config_path`) pairs and generate the bindings from them. This step + is specific to the bindings language, it may mean creating a .jar file, + compiling a binary, or just copying script files over. + - Execute the test script and check if it succeeds. This step is also + specific to the bindings language. diff --git a/third_party/rust/uniffi_testing/src/lib.rs b/third_party/rust/uniffi_testing/src/lib.rs new file mode 100644 index 0000000000..cc08895214 --- /dev/null +++ b/third_party/rust/uniffi_testing/src/lib.rs @@ -0,0 +1,275 @@ +/* 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 anyhow::{bail, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use cargo_metadata::{Message, Metadata, MetadataCommand, Package, Target}; +use fs_err as fs; +use once_cell::sync::Lazy; +use serde::Deserialize; +use std::{ + collections::hash_map::DefaultHasher, + env, + env::consts::DLL_EXTENSION, + hash::{Hash, Hasher}, + process::{Command, Stdio}, +}; + +#[derive(Debug, Deserialize)] +struct UniFFITestingMetadata { + /// Crates that hold external types used by this crate. When running the tests, we will build + /// the libraries and generate the source files for those crates and put them in the test + /// directory + #[serde(rename = "external-crates")] + external_crates: Option<Vec<String>>, +} + +// A source to compile for a test +#[derive(Debug)] +pub struct CompileSource { + pub udl_path: Utf8PathBuf, + pub config_path: Option<Utf8PathBuf>, +} + +// Store Cargo output to in a static to avoid calling it more than once. + +static CARGO_METADATA: Lazy<Metadata> = Lazy::new(get_cargo_metadata); +static CARGO_BUILD_MESSAGES: Lazy<Vec<Message>> = Lazy::new(get_cargo_build_messages); + +/// Struct for running fixture and example tests for bindings generators +/// +/// Expectations: +/// - Used from a integration test (a `.rs` file in the tests/ directory) +/// - The working directory is the project root for the bindings crate. This is the normal case +/// for test code, just make sure you don't cd somewhere else. +/// - The bindings crate has a dev-dependency on the fixture crate +/// - The fixture crate produces a cdylib library +/// - The fixture crate, and any external-crates, has 1 UDL file in it's src/ directory +pub struct UniFFITestHelper { + name: String, + package: Package, + metadata: Option<UniFFITestingMetadata>, +} + +impl UniFFITestHelper { + pub fn new(name: &str) -> Result<Self> { + let package = Self::find_package(name)?; + let metadata: Option<UniFFITestingMetadata> = package + .metadata + .pointer("/uniffi/testing") + .cloned() + .map(serde_json::from_value) + .transpose()?; + Ok(Self { + name: name.to_string(), + package, + metadata, + }) + } + + fn find_package(name: &str) -> Result<Package> { + let matching: Vec<&Package> = CARGO_METADATA + .packages + .iter() + .filter(|p| p.name == name) + .collect(); + match matching.len() { + 1 => Ok(matching[0].clone()), + n => bail!("cargo metadata return {n} packages named {name}"), + } + } + + fn find_packages_for_external_crates(&self) -> Result<Vec<Package>> { + // Add any external crates listed in `Cargo.toml` + match &self.metadata { + None => Ok(vec![]), + Some(metadata) => metadata + .external_crates + .iter() + .flatten() + .map(|name| Self::find_package(name)) + .collect(), + } + } + + fn find_cdylib_path(package: &Package) -> Result<Utf8PathBuf> { + let cdylib_targets: Vec<&Target> = package + .targets + .iter() + .filter(|t| t.crate_types.iter().any(|t| t == "cdylib")) + .collect(); + let target = match cdylib_targets.len() { + 1 => cdylib_targets[0], + n => bail!("Found {n} cdylib targets for {}", package.name), + }; + + let artifacts = CARGO_BUILD_MESSAGES + .iter() + .filter_map(|message| match message { + Message::CompilerArtifact(artifact) => { + if artifact.target == *target { + Some(artifact.clone()) + } else { + None + } + } + _ => None, + }); + let cdylib_files: Vec<Utf8PathBuf> = artifacts + .into_iter() + .flat_map(|artifact| { + artifact + .filenames + .into_iter() + .filter(|nm| matches!(nm.extension(), Some(DLL_EXTENSION))) + .collect::<Vec<Utf8PathBuf>>() + }) + .collect(); + + match cdylib_files.len() { + 1 => Ok(cdylib_files[0].to_owned()), + n => bail!("Found {n} cdylib files for {}", package.name), + } + } + + /// Create at `out_dir` for testing + /// + /// This directory can be used for: + /// - Generated bindings files (usually via the `--out-dir` param) + /// - cdylib libraries that the bindings depend on + /// - Anything else that's useful for testing + /// + /// This directory typically created as a subdirectory of `CARGO_TARGET_TMPDIR` when running an + /// integration test. + /// + /// We use the script path to create a hash included in the outpuit directory. This avoids + /// path collutions when 2 scripts run against the same fixture. + pub fn create_out_dir( + &self, + temp_dir: impl AsRef<Utf8Path>, + script_path: impl AsRef<Utf8Path>, + ) -> Result<Utf8PathBuf> { + let dirname = format!("{}-{}", self.name, hash_path(script_path.as_ref())); + let out_dir = temp_dir.as_ref().join(dirname); + if out_dir.exists() { + // Clean out any files from previous runs + fs::remove_dir_all(&out_dir)?; + } + fs::create_dir(&out_dir)?; + Ok(out_dir) + } + + /// Copy the `cdylib` for a fixture into the out_dir + /// + /// This is typically needed for the bindings to open it when running the tests + /// + /// Returns the path to the copied library + pub fn copy_cdylibs_to_out_dir(&self, out_dir: impl AsRef<Utf8Path>) -> Result<()> { + let cdylib_paths = + std::iter::once(self.cdylib_path()?).chain(self.external_cdylib_paths()?); + + for path in cdylib_paths { + let dest = out_dir.as_ref().join(path.file_name().unwrap()); + fs::copy(&path, dest)?; + } + Ok(()) + } + + /// Get the path to the cdylib file for this package + pub fn cdylib_path(&self) -> Result<Utf8PathBuf> { + Self::find_cdylib_path(&self.package) + } + + /// Get the path to the cdylib file for external crates listed in `Cargo.toml` + pub fn external_cdylib_paths(&self) -> Result<Vec<Utf8PathBuf>> { + self.find_packages_for_external_crates()? + .into_iter() + .map(|p| Self::find_cdylib_path(&p)) + .collect() + } + + /// Get paths to the UDL and config files for a fixture + pub fn get_compile_sources(&self) -> Result<Vec<CompileSource>> { + Ok(std::iter::once(self.get_main_compile_source()?) + .chain(self.get_external_compile_sources()?) + .collect()) + } + + pub fn get_main_compile_source(&self) -> Result<CompileSource> { + self.find_compile_source(&self.package.clone()) + } + + pub fn get_external_compile_sources(&self) -> Result<Vec<CompileSource>> { + self.find_packages_for_external_crates()? + .into_iter() + .map(|p| self.find_compile_source(&p)) + .collect() + } + + fn find_compile_source(&self, package: &Package) -> Result<CompileSource> { + let crate_root = package.manifest_path.parent().unwrap(); + let src_dir = crate_root.join("src"); + let mut udl_paths = find_files( + &src_dir, + |path| matches!(path.extension(), Some(ext) if ext.to_ascii_lowercase() == "udl"), + )?; + let udl_path = match udl_paths.len() { + 1 => udl_paths.remove(0), + n => bail!("Found {n} UDL files in {src_dir}"), + }; + let mut config_paths = find_files( + crate_root, + |path| matches!(path.file_name(), Some(name) if name == "uniffi.toml"), + )?; + let config_path = match config_paths.len() { + 0 => None, + 1 => Some(config_paths.remove(0)), + n => bail!("Found {n} UDL files in {crate_root}"), + }; + + Ok(CompileSource { + udl_path, + config_path, + }) + } +} + +fn find_files<F: Fn(&Utf8Path) -> bool>(dir: &Utf8Path, predicate: F) -> Result<Vec<Utf8PathBuf>> { + fs::read_dir(dir)? + .flatten() + .map(|entry| entry.path().try_into()) + .try_fold(Vec::new(), |mut vec, path| { + let path: Utf8PathBuf = path?; + if predicate(&path) { + vec.push(path); + } + Ok(vec) + }) +} + +fn get_cargo_metadata() -> Metadata { + MetadataCommand::new() + .exec() + .expect("error running cargo metadata") +} + +fn get_cargo_build_messages() -> Vec<Message> { + let mut child = Command::new(env!("CARGO")) + .arg("build") + .arg("--message-format=json") + .arg("--tests") + .stdout(Stdio::piped()) + .spawn() + .expect("Error running cargo build"); + let output = std::io::BufReader::new(child.stdout.take().unwrap()); + Message::parse_stream(output) + .map(|m| m.expect("Error parsing cargo build messages")) + .collect() +} + +fn hash_path(path: &Utf8Path) -> String { + let mut hasher = DefaultHasher::new(); + path.hash(&mut hasher); + format!("{:x}", hasher.finish()) +} |