From c714f9e4b2678656dae73fdb5dc155b8d0b9817b Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Mon, 14 Jun 2021 13:23:22 +1000 Subject: [PATCH 01/45] Publish auto-generated Rust API docs as part of the UniFFI manual. Rust has great tooling for API docs, and I think we should try to get into the habit of using it. This commit generates API docs for our main crates and includes them in the published UniFFI manual, which will hopefully help folks onboarding into the codebase. --- .circleci/config.yml | 20 ++++++++++++++++ .gitignore | 3 ++- docs/contributing.md | 2 +- docs/manual/src/SUMMARY.md | 2 +- docs/manual/src/internals/crates.md | 23 +++++++++++++++++++ uniffi/src/lib.rs | 6 ++--- uniffi_bindgen/src/interface/attributes.rs | 2 +- uniffi_bindgen/src/interface/types/mod.rs | 2 +- .../src/interface/types/resolver.rs | 2 +- uniffi_bindgen/src/lib.rs | 2 +- 10 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 docs/manual/src/internals/crates.md diff --git a/.circleci/config.yml b/.circleci/config.yml index bad93a70b3..5eb804f7f7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,6 +24,13 @@ commands: steps: - run: rustup override set 1.47.0 - run: rustup update + build-api-docs: + steps: + - run: + name: Build API Docs + command: cargo doc --no-deps --document-private-items -p uniffi_bindgen -p uniffi -p uniffi_build -p uniffi_macros + environment: + RUSTDOCFLAGS: -Dwarnings -Arustdoc::private-intra-doc-links orbs: gh-pages: sugarshin/gh-pages@0.0.6 @@ -50,6 +57,14 @@ jobs: - run: rustup component add clippy - run: cargo clippy --version - run: cargo clippy --all --all-targets -- -D warnings + Lint Rust Docs: + docker: + - image: rfkelly/uniffi-ci:latest + resource_class: small + steps: + - checkout + - prepare-rust-target-version + - build-api-docs Rust and Foreign Language tests: docker: - image: rfkelly/uniffi-ci:latest @@ -87,6 +102,8 @@ jobs: steps: - install-mdbook - checkout + - build-api-docs + - run: cp -r ./target/doc ./docs/manual/src/internals/api - run: mdbook build docs/manual - gh-pages/deploy: build-dir: docs/manual/book @@ -100,6 +117,9 @@ workflows: clippy: jobs: - Lint Rust with clippy + docs: + jobs: + - Lint Rust Docs run-tests: jobs: - Rust and Foreign Language tests diff --git a/.gitignore b/.gitignore index ccbded42fa..8e498ea51a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ target .*.swp *.jar .vscode -xcuserdata \ No newline at end of file +xcuserdata +docs/manual/src/internals/api diff --git a/docs/contributing.md b/docs/contributing.md index f67fd8ed7d..7ee8bb84c0 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -71,7 +71,7 @@ Other directories of interest include: - **[`./uniffi_bindgen`](../uniffi_bindgen):** This is the source for the `uniffi-bindgen` executable and is where most of the logic for the UniFFI tool lives. Its contents include: - **[`./uniffi_bindgen/src/interface/`](../uniffi_bindgen/src/interface):** The logic for parsing `.udl` files - into an in-memory representation called `ComponentInterface`, from which we ca generate code for different languages. + into an in-memory representation called `ComponentInterface`, from which we can generate code for different languages. - **[`./uniffi_bindgen/src/scaffolding`](../uniffi_bindgen/src/scaffolding):** This module turns a `ComponentInterface` into *Rust scaffolding*, the code that wraps the user-provided Rust code and exposes it via a C-compatible FFI layer. - **[`./uniffi_bindgen/src/bindings/`](../uniffi_bindgen/src/bindings):** This module turns a `ComponentInterface` into diff --git a/docs/manual/src/SUMMARY.md b/docs/manual/src/SUMMARY.md index efad7c1daa..14556fcea7 100644 --- a/docs/manual/src/SUMMARY.md +++ b/docs/manual/src/SUMMARY.md @@ -26,6 +26,6 @@ - [Integrating with XCode](./swift/xcode.md) # Internals - +- [Navigating the code](./internals/crates.md) - [Lifting, Lowering, and Serialization](./internals/lifting_and_lowering.md) - [Managing object references](./internals/object_references.md) diff --git a/docs/manual/src/internals/crates.md b/docs/manual/src/internals/crates.md new file mode 100644 index 0000000000..ecb4083cf8 --- /dev/null +++ b/docs/manual/src/internals/crates.md @@ -0,0 +1,23 @@ +# Navigating the code + +The code for UniFFI is organized into the following crates: + +- **[`./uniffi_bindgen`](./api/uniffi_bindgen/index.html):** This is the source for the `uniffi-bindgen` executable and is where + most of the logic for the UniFFI tool lives. Its contents include: + - **[`./uniffi_bindgen/src/interface/`](./api/uniffi_bindgen/interface/index.html):** The logic for parsing `.udl` files + into an in-memory representation called [`ComponentInterface`](./api/uniffi_bindgen/interface/struct.ComponentInterface.html), + from which we can generate code for different languages. + - **[`./uniffi_bindgen/src/scaffolding`](./api/uniffi_bindgen/scaffolding/index.html):** This module turns a + [`ComponentInterface`](./api/uniffi_bindgen/interface/struct.ComponentInterface.html) into *Rust scaffolding*, the code that + wraps the user-provided Rust code and exposes it via a C-compatible FFI layer. + - **[`./uniffi_bindgen/src/bindings/`](./api/uniffi_bindgen/bindings/index.html):** This module turns a + [`ComponentInterface`](./api/uniffi_bindgen/interface/struct.ComponentInterface.html) into *foreign-language bindings*, + the code that can load the FFI layer exposed by the scaffolding and expose it as a + higher-level API in a target language. There is a sub-module for each supported language. +- **[`./uniffi`](./api/uniffi/index.html):** This is a run-time support crate that is used by the generated Rust scaffolding. It + controls how values of various types are passed back-and-forth over the FFI layer, by means of the + [`ViaFfi`](./api/uniffi/trait.ViaFfi.html) trait. +- **[`./uniffi_build`](./api/uniffi_build/index.html):** This is a small hook to run `uniffi-bindgen` from the `build.rs` script + of a UniFFI component, in order to automatically generate the Rust scaffolding as part of its build process. +- **[`./uniffi_macros`](./api/uniffi_macros/index.html):** This contains some helper macros that UniFFI components can use to + simplify loading the generated scaffolding, and executing foreign-language tests. diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index 88bb535fcc..9992a1b813 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -11,14 +11,14 @@ //! The key concept here is the [`ViaFfi`] trait, which must be implemented for any type that can //! be passed across the FFI, and which determines: //! -//! * How to [represent](ViaFfi::Value) values of that type in the low-level C-style type +//! * How to [represent](ViaFfi::FfiType) values of that type in the low-level C-style type //! system of the FFI layer. //! * How to ["lower"](ViaFfi::lower) rust values of that type into an appropriate low-level //! FFI value. -//! * How to ["lift"](ViaFfi::lift) low-level FFI values back into rust values of that type. +//! * How to ["lift"](ViaFfi::try_lift) low-level FFI values back into rust values of that type. //! * How to [write](ViaFfi::write) rust values of that type into a buffer, for cases //! where they are part of a compound data structure that is serialized for transfer. -//! * How to [read](ViaFfi::read) rust values of that type from buffer, for cases +//! * How to [read](ViaFfi::try_read) rust values of that 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 diff --git a/uniffi_bindgen/src/interface/attributes.rs b/uniffi_bindgen/src/interface/attributes.rs index 7e599c04bd..935f21f87b 100644 --- a/uniffi_bindgen/src/interface/attributes.rs +++ b/uniffi_bindgen/src/interface/attributes.rs @@ -18,7 +18,7 @@ use std::convert::{TryFrom, TryInto}; use anyhow::{bail, Result}; -/// Represents an attribute parsed from UDL, like [ByRef] or [Throws]. +/// 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 diff --git a/uniffi_bindgen/src/interface/types/mod.rs b/uniffi_bindgen/src/interface/types/mod.rs index 80bbb3a8db..0b592dc2f5 100644 --- a/uniffi_bindgen/src/interface/types/mod.rs +++ b/uniffi_bindgen/src/interface/types/mod.rs @@ -19,7 +19,7 @@ //! //! 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`] enum, but that's a detail that is invisible to end users. +//! 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}; diff --git a/uniffi_bindgen/src/interface/types/resolver.rs b/uniffi_bindgen/src/interface/types/resolver.rs index ec0206a983..2d5f21e1ca 100644 --- a/uniffi_bindgen/src/interface/types/resolver.rs +++ b/uniffi_bindgen/src/interface/types/resolver.rs @@ -7,7 +7,7 @@ //! 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`] to populate the universe). +//! 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. diff --git a/uniffi_bindgen/src/lib.rs b/uniffi_bindgen/src/lib.rs index 5cc95b3f08..a87cd84558 100644 --- a/uniffi_bindgen/src/lib.rs +++ b/uniffi_bindgen/src/lib.rs @@ -60,7 +60,7 @@ //! //! 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](uniffi_build::generate_scaffolding) +//! 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: //! From 3beafdbd76c2ab00c02e59b8395ae2e67152517c Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Tue, 15 Jun 2021 10:24:09 +1000 Subject: [PATCH 02/45] Use rusty docker image for building API docs in CI. The docs-publishing CircleCI task is using a node-based docker image, which means it doesn't have the necessary tooling to build the API docs. Let's use the same docker image as the other tasks for simplicity. --- .circleci/config.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5eb804f7f7..23e0fdf3b4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,7 +28,9 @@ commands: steps: - run: name: Build API Docs - command: cargo doc --no-deps --document-private-items -p uniffi_bindgen -p uniffi -p uniffi_build -p uniffi_macros + # The `--lib` here is important; without it `cargo doc` will sometimes choose to document the `uniffi_bindgen` library + # and othertimes choose to document the `uniffi_bindgen` binary, which is much less useful. + command: cargo doc --no-deps --document-private-items --lib -p uniffi_bindgen -p uniffi -p uniffi_build -p uniffi_macros environment: RUSTDOCFLAGS: -Dwarnings -Arustdoc::private-intra-doc-links @@ -97,13 +99,14 @@ jobs: - run: cargo test -- --skip trybuild_ui_tests Deploy website: docker: - - image: circleci/node:latest + - image: rfkelly/uniffi-ci:latest resource_class: small steps: - - install-mdbook - checkout + - prepare-rust-target-version - build-api-docs - run: cp -r ./target/doc ./docs/manual/src/internals/api + - install-mdbook - run: mdbook build docs/manual - gh-pages/deploy: build-dir: docs/manual/book From db89e2b47bcdb77a15a1942c59f6478680335ee1 Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Mon, 24 May 2021 11:08:50 +1000 Subject: [PATCH 03/45] Implement try_read/write for concrete types, not Buf/BufMut generics. This is a followup to the upgrade of our `bytes` dependency from #454. Our use of the `bytes::Buf` trait assumes that `Buf::chunks` will return all the remaining data. This is not guaranteed by the trait, but it is guaranteed in practice by its implementation on the concrete types that we use. Since we don't really work with any generic `Buf` impl anyway, this commit removes the generics from `ViaFfi::try_read` and `ViaFii:write`, replacing them with the single concrete type that we actually ues in practice. This should reduce potential for future confusion, and also make compilation slightly easier on the Rust compiler. --- uniffi/src/lib.rs | 49 +++++++++++-------- .../templates/CallbackInterfaceTemplate.rs | 6 ++- .../src/scaffolding/templates/EnumTemplate.rs | 6 ++- .../scaffolding/templates/RecordTemplate.rs | 14 +++--- 4 files changed, 43 insertions(+), 32 deletions(-) diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index 9992a1b813..3bfa76bfc3 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -179,7 +179,7 @@ pub unsafe trait ViaFfi: Sized { /// 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. - fn write(&self, buf: &mut B); + fn write(&self, buf: &mut Vec); /// Read a rust value from a buffer, received over the FFI in serialized form. /// @@ -189,7 +189,11 @@ pub unsafe trait ViaFfi: Sized { /// /// Since we cannot statically guarantee that the foreign-language code will send valid /// serialized bytes for the target type, this method is fallible. - fn try_read(buf: &mut B) -> Result; + /// + /// 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; } /// A helper function to lower a type by serializing it into a buffer. @@ -223,7 +227,7 @@ pub fn try_lift_from_buffer(buf: RustBuffer) -> Result { /// 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: &B, num_bytes: usize) -> Result<()> { +pub fn check_remaining(buf: &[u8], num_bytes: usize) -> Result<()> { if buf.remaining() < num_bytes { bail!(format!( "not enough bytes remaining in buffer ({} < {})", @@ -254,11 +258,11 @@ macro_rules! impl_via_ffi_for_num_primitive { Ok(v) } - fn write(&self, buf: &mut B) { + fn write(&self, buf: &mut Vec) { buf.[](*self); } - fn try_read(buf: &mut B) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, std::mem::size_of::<$T>())?; Ok(buf.[]()) } @@ -295,11 +299,11 @@ unsafe impl ViaFfi for bool { }) } - fn write(&self, buf: &mut B) { + fn write(&self, buf: &mut Vec) { buf.put_i8(ViaFfi::lower(*self)); } - fn try_read(buf: &mut B) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, 1)?; ViaFfi::try_lift(buf.get_i8()) } @@ -338,7 +342,7 @@ unsafe impl ViaFfi for String { Ok(unsafe { String::from_utf8_unchecked(v) }) } - fn write(&self, buf: &mut B) { + fn write(&self, buf: &mut Vec) { // 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(self.len()).unwrap(); @@ -346,10 +350,13 @@ unsafe impl ViaFfi for String { buf.put(self.as_bytes()); } - fn try_read(buf: &mut B) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { 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); @@ -382,7 +389,7 @@ unsafe impl ViaFfi for SystemTime { try_lift_from_buffer(v) } - fn write(&self, buf: &mut B) { + fn write(&self, buf: &mut Vec) { let mut sign = 1; let epoch_offset = self .duration_since(SystemTime::UNIX_EPOCH) @@ -399,7 +406,7 @@ unsafe impl ViaFfi for SystemTime { buf.put_u32(epoch_offset.subsec_nanos()); } - fn try_read(buf: &mut B) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, 12)?; let seconds = buf.get_i64(); let nanos = buf.get_u32(); @@ -432,12 +439,12 @@ unsafe impl ViaFfi for Duration { try_lift_from_buffer(v) } - fn write(&self, buf: &mut B) { + fn write(&self, buf: &mut Vec) { buf.put_u64(self.as_secs()); buf.put_u32(self.subsec_nanos()); } - fn try_read(buf: &mut B) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, 12)?; Ok(Duration::new(buf.get_u64(), buf.get_u32())) } @@ -463,7 +470,7 @@ unsafe impl ViaFfi for Option { try_lift_from_buffer(v) } - fn write(&self, buf: &mut B) { + fn write(&self, buf: &mut Vec) { match self { None => buf.put_i8(0), Some(v) => { @@ -473,7 +480,7 @@ unsafe impl ViaFfi for Option { } } - fn try_read(buf: &mut B) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, 1)?; Ok(match buf.get_i8() { 0 => None, @@ -503,7 +510,7 @@ unsafe impl ViaFfi for Vec { try_lift_from_buffer(v) } - fn write(&self, buf: &mut B) { + fn write(&self, buf: &mut Vec) { // TODO: would be nice not to panic here :-/ let len = i32::try_from(self.len()).unwrap(); buf.put_i32(len); // We limit arrays to i32::MAX items @@ -512,7 +519,7 @@ unsafe impl ViaFfi for Vec { } } - fn try_read(buf: &mut B) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, 4)?; let len = usize::try_from(buf.get_i32())?; let mut vec = Vec::with_capacity(len); @@ -542,7 +549,7 @@ unsafe impl ViaFfi for HashMap { try_lift_from_buffer(v) } - fn write(&self, buf: &mut B) { + fn write(&self, buf: &mut Vec) { // TODO: would be nice not to panic here :-/ let len = i32::try_from(self.len()).unwrap(); buf.put_i32(len); // We limit HashMaps to i32::MAX entries @@ -552,7 +559,7 @@ unsafe impl ViaFfi for HashMap { } } - fn try_read(buf: &mut B) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, 4)?; let len = usize::try_from(buf.get_i32())?; let mut map = HashMap::with_capacity(len); @@ -608,7 +615,7 @@ unsafe impl ViaFfi for std::sync::Arc { /// 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(&self, buf: &mut B) { + fn write(&self, buf: &mut Vec) { static_assertions::const_assert!(std::mem::size_of::<*const std::ffi::c_void>() <= 8); let ptr = std::sync::Arc::clone(self).lower(); buf.put_u64(ptr as u64); @@ -619,7 +626,7 @@ unsafe impl ViaFfi for std::sync::Arc { /// /// Safety: the buffer must contain a pointer previously obtained by calling /// the `lower()` or `write()` method of this impl. - fn try_read(buf: &mut B) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { 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) diff --git a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs index f5f1d6f600..0744533d43 100644 --- a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs @@ -99,7 +99,8 @@ unsafe impl uniffi::ViaFfi for {{ trait_impl }} { self.handle } - fn write(&self, buf: &mut B) { + fn write(&self, buf: &mut Vec) { + use uniffi::deps::bytes::BufMut; buf.put_u64(self.handle); } @@ -107,7 +108,8 @@ unsafe impl uniffi::ViaFfi for {{ trait_impl }} { Ok(Self { handle: v }) } - fn try_read(buf: &mut B) -> uniffi::deps::anyhow::Result { + fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { + use uniffi::deps::bytes::Buf; uniffi::check_remaining(buf, 8)?; ::try_lift(buf.get_u64()) } diff --git a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs index ccb7761491..6485b5b226 100644 --- a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs @@ -15,7 +15,8 @@ unsafe impl uniffi::ViaFfi for {{ e.name() }} { uniffi::try_lift_from_buffer(v) } - fn write(&self, buf: &mut B) { + fn write(&self, buf: &mut Vec) { + use uniffi::deps::bytes::BufMut; match self { {%- for variant in e.variants() %} {{ e.name() }}::{{ variant.name() }} { {% for field in variant.fields() %}{{ field.name() }}, {%- endfor %} } => { @@ -28,7 +29,8 @@ unsafe impl uniffi::ViaFfi for {{ e.name() }} { }; } - fn try_read(buf: &mut B) -> uniffi::deps::anyhow::Result { + fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { + use uniffi::deps::bytes::Buf; uniffi::check_remaining(buf, 4)?; Ok(match buf.get_i32() { {%- for variant in e.variants() %} diff --git a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs index 6d13660eca..9e8fdfa5e1 100644 --- a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs @@ -16,7 +16,7 @@ unsafe impl uniffi::ViaFfi for {{ rec.name() }} { uniffi::try_lift_from_buffer(v) } - fn write(&self, buf: &mut B) { + fn write(&self, buf: &mut Vec) { // 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() %} @@ -24,11 +24,11 @@ unsafe impl uniffi::ViaFfi for {{ rec.name() }} { {%- endfor %} } - fn try_read(buf: &mut B) -> uniffi::deps::anyhow::Result { - Ok(Self { - {%- for field in rec.fields() %} - {{ field.name() }}: <{{ field.type_()|type_rs }} as uniffi::ViaFfi>::try_read(buf)?, - {%- endfor %} - }) + fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { + Ok(Self { + {%- for field in rec.fields() %} + {{ field.name() }}: <{{ field.type_()|type_rs }} as uniffi::ViaFfi>::try_read(buf)?, + {%- endfor %} + }) } } From 004b312f12394e209fe1527c6ab16b5b7dec4bb9 Mon Sep 17 00:00:00 2001 From: Aliaksandr Rahalevich Date: Mon, 14 Jun 2021 23:47:20 -0700 Subject: [PATCH 04/45] Fix U16 for python and ruby (#484) --- CHANGELOG.md | 4 ++++ fixtures/coverall/src/coverall.udl | 2 ++ fixtures/coverall/src/lib.rs | 6 ++++++ fixtures/coverall/tests/bindings/test_coverall.kts | 2 ++ fixtures/coverall/tests/bindings/test_coverall.py | 4 ++++ fixtures/coverall/tests/bindings/test_coverall.rb | 4 ++++ fixtures/coverall/tests/bindings/test_coverall.swift | 2 ++ .../src/bindings/python/templates/RustBufferBuilder.py | 2 +- .../src/bindings/python/templates/RustBufferStream.py | 2 +- .../src/bindings/ruby/templates/RustBufferBuilder.rb | 2 +- .../src/bindings/ruby/templates/RustBufferStream.rb | 2 +- 11 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea4598cbce..ec425a2b6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ [All changes in [[UnreleasedVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.12.0...HEAD). +### What's Changed + +- Both python and ruby backends now handle U16 correctly. + ## v0.12.0 (2021-06-14) [All changes in v0.12.0](https://github.com/mozilla/uniffi-rs/compare/v0.11.0...v0.12.0). diff --git a/fixtures/coverall/src/coverall.udl b/fixtures/coverall/src/coverall.udl index 4f7cef4e60..63da897b4c 100644 --- a/fixtures/coverall/src/coverall.udl +++ b/fixtures/coverall/src/coverall.udl @@ -12,6 +12,8 @@ dictionary SimpleDict { boolean? maybe_a_bool; u8 unsigned8; u8? maybe_unsigned8; + u16 unsigned16; + u16? maybe_unsigned16; u64 unsigned64; u64? maybe_unsigned64; i8 signed8; diff --git a/fixtures/coverall/src/lib.rs b/fixtures/coverall/src/lib.rs index 0bd22c55d7..4d4716cebf 100644 --- a/fixtures/coverall/src/lib.rs +++ b/fixtures/coverall/src/lib.rs @@ -25,6 +25,8 @@ pub struct SimpleDict { maybe_a_bool: Option, unsigned8: u8, maybe_unsigned8: Option, + unsigned16: u16, + maybe_unsigned16: Option, unsigned64: u64, maybe_unsigned64: Option, signed8: i8, @@ -52,6 +54,8 @@ fn create_some_dict() -> SimpleDict { maybe_a_bool: Some(false), unsigned8: 1, maybe_unsigned8: Some(2), + unsigned16: 3, + maybe_unsigned16: Some(4), unsigned64: u64::MAX, maybe_unsigned64: Some(u64::MIN), signed8: 8, @@ -74,6 +78,8 @@ fn create_none_dict() -> SimpleDict { maybe_a_bool: None, unsigned8: 1, maybe_unsigned8: None, + unsigned16: 3, + maybe_unsigned16: None, unsigned64: u64::MAX, maybe_unsigned64: None, signed8: 8, diff --git a/fixtures/coverall/tests/bindings/test_coverall.kts b/fixtures/coverall/tests/bindings/test_coverall.kts index 8f97deb9f6..3f1762fcab 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.kts +++ b/fixtures/coverall/tests/bindings/test_coverall.kts @@ -17,6 +17,8 @@ createSomeDict().use { d -> assert(d.maybeABool == false); assert(d.unsigned8 == 1.toUByte()) assert(d.maybeUnsigned8 == 2.toUByte()) + assert(d.unsigned16 == 3.toUShort()) + assert(d.maybeUnsigned16 == 4.toUShort()) assert(d.unsigned64 == 18446744073709551615UL) assert(d.maybeUnsigned64 == 0UL) assert(d.signed8 == 8.toByte()) diff --git a/fixtures/coverall/tests/bindings/test_coverall.py b/fixtures/coverall/tests/bindings/test_coverall.py index 28822ee4ee..a5238729f3 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.py +++ b/fixtures/coverall/tests/bindings/test_coverall.py @@ -19,6 +19,8 @@ def test_some_dict(self): self.assertFalse(d.maybe_a_bool) self.assertEqual(d.unsigned8, 1) self.assertEqual(d.maybe_unsigned8, 2) + self.assertEqual(d.unsigned16, 3) + self.assertEqual(d.maybe_unsigned16, 4) self.assertEqual(d.unsigned64, 18446744073709551615) self.assertEqual(d.maybe_unsigned64, 0) self.assertEqual(d.signed8, 8) @@ -42,6 +44,8 @@ def test_none_dict(self): self.assertIsNone(d.maybe_a_bool) self.assertEqual(d.unsigned8, 1) self.assertIsNone(d.maybe_unsigned8) + self.assertEqual(d.unsigned16, 3) + self.assertIsNone(d.maybe_unsigned16) self.assertEqual(d.unsigned64, 18446744073709551615) self.assertIsNone(d.maybe_unsigned64) self.assertEqual(d.signed8, 8) diff --git a/fixtures/coverall/tests/bindings/test_coverall.rb b/fixtures/coverall/tests/bindings/test_coverall.rb index 81d996b2c4..bd85f91c11 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.rb +++ b/fixtures/coverall/tests/bindings/test_coverall.rb @@ -17,6 +17,8 @@ def test_some_dict assert_false(d.maybe_a_bool) assert_equal(d.unsigned8, 1) assert_equal(d.maybe_unsigned8, 2) + assert_equal(d.unsigned16, 3) + assert_equal(d.maybe_unsigned16, 4) assert_equal(d.unsigned64, 18_446_744_073_709_551_615) assert_equal(d.maybe_unsigned64, 0) assert_equal(d.signed8, 8) @@ -41,6 +43,8 @@ def test_none_dict assert_nil(d.maybe_a_bool) assert_equal(d.unsigned8, 1) assert_nil(d.maybe_unsigned8) + assert_equal(d.unsigned16, 3) + assert_nil(d.maybe_unsigned16) assert_equal(d.unsigned64, 18_446_744_073_709_551_615) assert_nil(d.maybe_unsigned64) assert_equal(d.signed8, 8) diff --git a/fixtures/coverall/tests/bindings/test_coverall.swift b/fixtures/coverall/tests/bindings/test_coverall.swift index fd06cdc7bc..4deecfc7d8 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.swift +++ b/fixtures/coverall/tests/bindings/test_coverall.swift @@ -29,6 +29,8 @@ do { assert(d.maybeABool == false); assert(d.unsigned8 == 1) assert(d.maybeUnsigned8 == 2) + assert(d.unsigned16 == 3) + assert(d.maybeUnsigned16 == 4) assert(d.unsigned64 == 18446744073709551615) assert(d.maybeUnsigned64 == 0) assert(d.signed8 == 8) diff --git a/uniffi_bindgen/src/bindings/python/templates/RustBufferBuilder.py b/uniffi_bindgen/src/bindings/python/templates/RustBufferBuilder.py index c661d470de..8eba87fc02 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RustBufferBuilder.py +++ b/uniffi_bindgen/src/bindings/python/templates/RustBufferBuilder.py @@ -62,7 +62,7 @@ def writeI16(self, v): {% when Type::UInt16 -%} def writeU16(self, v): - self._pack_into(1, ">H", v) + self._pack_into(2, ">H", v) {% when Type::Int32 -%} diff --git a/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py b/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py index bc7e6ef551..09d3b09f1e 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py +++ b/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py @@ -51,7 +51,7 @@ def readI16(self): {% when Type::UInt16 -%} def readU16(self): - return self._unpack_from(1, ">H") + return self._unpack_from(2, ">H") {% when Type::Int32 -%} diff --git a/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb b/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb index 8f1b4ab9fa..50e6b59578 100644 --- a/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb +++ b/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb @@ -52,7 +52,7 @@ def write_I16(v) {% when Type::UInt16 -%} def write_U16(v) - pack_into(1, 'S>', v) + pack_into(2, 'S>', v) end {% when Type::Int32 -%} diff --git a/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb b/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb index 64d963009c..ab693cf0f8 100644 --- a/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb +++ b/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb @@ -46,7 +46,7 @@ def readI16 {% when Type::UInt16 -%} def readU16 - unpack_from 1, 'S>' + unpack_from 2, 'S>' end {% when Type::Int32 -%} From 98d8ef0f69228eebe2c48e59b51839960522114e Mon Sep 17 00:00:00 2001 From: Tarik Eshaq Date: Wed, 16 Jun 2021 12:12:19 -0700 Subject: [PATCH 05/45] Fixes mismatch in kotlin panic exceptions --- fixtures/coverall/src/coverall.udl | 3 +++ fixtures/coverall/src/lib.rs | 4 ++++ fixtures/coverall/tests/bindings/test_coverall.kts | 7 +++++++ .../src/bindings/kotlin/templates/ErrorTemplate.kt | 2 +- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/fixtures/coverall/src/coverall.udl b/fixtures/coverall/src/coverall.udl index 63da897b4c..c7229ddf68 100644 --- a/fixtures/coverall/src/coverall.udl +++ b/fixtures/coverall/src/coverall.udl @@ -56,6 +56,9 @@ interface Coveralls { void panic(string message); + [Throws=CoverallError] + void fallible_panic(string message); + // *** Test functions which take either `self` or other params as `Arc` *** /// Calls `Arc::strong_count()` on the `Arc` containing `self`. diff --git a/fixtures/coverall/src/lib.rs b/fixtures/coverall/src/lib.rs index 4d4716cebf..ea20281bbd 100644 --- a/fixtures/coverall/src/lib.rs +++ b/fixtures/coverall/src/lib.rs @@ -125,6 +125,10 @@ impl Coveralls { } } + fn fallible_panic(&self, message: String) -> Result<()> { + panic!("{}", message); + } + fn get_name(&self) -> String { self.name.clone() } diff --git a/fixtures/coverall/tests/bindings/test_coverall.kts b/fixtures/coverall/tests/bindings/test_coverall.kts index 3f1762fcab..04304daa16 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.kts +++ b/fixtures/coverall/tests/bindings/test_coverall.kts @@ -68,6 +68,13 @@ Coveralls("test_arcs").use { coveralls -> } catch (e: InternalException) { // No problemo! } + + try { + coveralls.falliblePanic("Expected panic in a fallible function!") + throw RuntimeException("Should have thrown an InternalException") + } catch (e: InternalException) { + // No problemo! + } coveralls.takeOther(null); assert(coveralls.strongCount() == 2UL); } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt index 1f51fc6db0..243ca63f7e 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt @@ -113,7 +113,7 @@ internal open class {{e.name()}} : RustError() { {% for value in e.values() -%} {{loop.index}} -> return {{e.name()}}Exception.{{value}}(message) as E {% endfor -%} - else -> throw RuntimeException("Invalid error received: $code, $message") + else -> return InternalException(message) as E } } } From 7802a010bb80778ce51a89636232a73ed66e3ebe Mon Sep 17 00:00:00 2001 From: Tarik Eshaq Date: Wed, 16 Jun 2021 15:34:32 -0700 Subject: [PATCH 06/45] Adds fatalError when a fallable swift function encounters a panic --- fixtures/coverall/tests/bindings/test_coverall.swift | 5 +++++ .../src/bindings/kotlin/templates/ErrorTemplate.kt | 3 ++- .../src/bindings/swift/templates/ErrorTemplate.swift | 6 +++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/fixtures/coverall/tests/bindings/test_coverall.swift b/fixtures/coverall/tests/bindings/test_coverall.swift index 4deecfc7d8..019faf322d 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.swift +++ b/fixtures/coverall/tests/bindings/test_coverall.swift @@ -66,6 +66,11 @@ do { } // TODO: kinda hard to test this, as it triggers a fatal error. // coveralls!.takeOtherPanic(message: "expected panic: with an arc!") + // do { + // try coveralls.falliblePanic(message: "Expected Panic!!") + // } catch CoverallError.TooManyHoles { + // fatalError("Should have paniced!") + // } coveralls.takeOther(other: nil); assert(coveralls.strongCount() == 2); } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt index 243ca63f7e..c72d859170 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt @@ -113,7 +113,8 @@ internal open class {{e.name()}} : RustError() { {% for value in e.values() -%} {{loop.index}} -> return {{e.name()}}Exception.{{value}}(message) as E {% endfor -%} - else -> return InternalException(message) as E + -1 -> return InternalException(message) as E + else -> throw RuntimeException("invalid error code passed across the FFI") } } } diff --git a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift index 0ad77b379f..69c3c9a81c 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift @@ -83,12 +83,16 @@ public enum {{e.name()}}: RustError { switch rustError.code { case 0: return nil + case -1: + // TODO: Return a Uniffi defined error here + // to indicate a panic + fatalError("Panic detected!") {% for value in e.values() %} case {{loop.index}}: return .{{value}}(message: String(cString: message!)) {% endfor %} default: - return nil + fatalError("Invalid error") } } } From b6472222b57a13e8fb2575ca3b06b362ce84e061 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Thu, 17 Jun 2021 15:57:24 -0400 Subject: [PATCH 07/45] Updating the examples documentiation - Updated ruby dependencies - Updated cargo commands - Link to the examples dir from the manual. These were really helpful for me and I think it would be great to link for others. Is there a better way that a direct github link? --- docs/manual/src/internals/crates.md | 3 +++ examples/README.md | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/manual/src/internals/crates.md b/docs/manual/src/internals/crates.md index ecb4083cf8..f183dda7d7 100644 --- a/docs/manual/src/internals/crates.md +++ b/docs/manual/src/internals/crates.md @@ -21,3 +21,6 @@ The code for UniFFI is organized into the following crates: of a UniFFI component, in order to automatically generate the Rust scaffolding as part of its build process. - **[`./uniffi_macros`](./api/uniffi_macros/index.html):** This contains some helper macros that UniFFI components can use to simplify loading the generated scaffolding, and executing foreign-language tests. +- **[`./examples`](https://github.com/mozilla/uniffi-rs/tree/main/examples):** + This contains code examples that you can use to explore the code generation + process. diff --git a/examples/README.md b/examples/README.md index 3697120a32..329e99e814 100644 --- a/examples/README.md +++ b/examples/README.md @@ -41,7 +41,7 @@ If you want to try them out, you will need: * The [Swift command-line tools](https://swift.org/download/), particularly `swift`, `swiftc` and the `Foundation` package. * The [Ruby FFI](https://github.com/ffi/ffi#installation) - * `gem install ffi` + * `gem install ffi test-unit` We publish a [docker image](https://hub.docker.com/r/rfkelly/uniffi-ci) that has all of this dependencies pre-installed, if you want to get up and running quickly. @@ -53,10 +53,10 @@ With that in place, try the following: * Run `cargo test`. This will run each of the foreign-language testcases against the compiled Rust code, confirming whether everything is working as intended. * Explore the build process in more detail: - * Run `cargo run -p uniffi_bindgen scaffolding ./src/.udl`. + * Run `cargo run -p uniffi_bindgen -- scaffolding ./src/.udl`. This will generate the Rust scaffolding code which exposes a C FFI for the component. You can view the generatd code in `./src/.uniffi.rs`. - * Run `cargo run -p uniffi_bindgen generate --language kotlin ./src/arithmetic.udl`. + * Run `cargo run -p uniffi_bindgen -- generate --language kotlin ./src/.udl`. This will generate the foreign-language bindings for Kotlin, which load the compiled Rust code and use the C FFI generated above to interact with it. You can view the generated code in `./src/uniffi//.kt`. From 5f6def4cbae773833849731b6ee2362ef371e469 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Thu, 17 Jun 2021 17:23:26 -0400 Subject: [PATCH 08/45] Rename kotlin errors (#442) --- CHANGELOG.md | 3 +++ .../tests/bindings/test_arithmetic.kts | 2 +- .../todolist/tests/bindings/test_todolist.kts | 8 ++++---- .../coverall/tests/bindings/test_coverall.kts | 2 +- .../tests/bindings/test_chronological.kts | 2 +- uniffi_bindgen/src/bindings/kotlin/gen_kotlin.rs | 16 ++++++++++++++++ .../bindings/kotlin/templates/ErrorTemplate.kt | 6 +++--- 7 files changed, 29 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec425a2b6b..4928d0049d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ [All changes in [[UnreleasedVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.12.0...HEAD). +- Kotlin exceptions names will now replace a trailing "Error" with "Exception" + rather than appending the string (FooException instead of FooErrorException) + ### What's Changed - Both python and ruby backends now handle U16 correctly. diff --git a/examples/arithmetic/tests/bindings/test_arithmetic.kts b/examples/arithmetic/tests/bindings/test_arithmetic.kts index b64dda6269..ef11850ae2 100644 --- a/examples/arithmetic/tests/bindings/test_arithmetic.kts +++ b/examples/arithmetic/tests/bindings/test_arithmetic.kts @@ -6,7 +6,7 @@ assert(add(4u, 8u) == 12uL) try { sub(0u, 2u) throw RuntimeException("Should have thrown a IntegerOverflow exception!") -} catch (e: ArithmeticErrorException) { +} catch (e: ArithmeticException) { // It's okay! } diff --git a/examples/todolist/tests/bindings/test_todolist.kts b/examples/todolist/tests/bindings/test_todolist.kts index 1e2984138d..bb2b292224 100644 --- a/examples/todolist/tests/bindings/test_todolist.kts +++ b/examples/todolist/tests/bindings/test_todolist.kts @@ -6,17 +6,17 @@ val todo = TodoList() try { todo.getLast() throw RuntimeException("Should have thrown a TodoError!") -} catch (e: TodoErrorException.EmptyTodoList) { +} 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: TodoErrorException) { +} catch (e: TodoException) { // It's okay, the string was empty! - assert(e is TodoErrorException.EmptyString) - assert(e !is TodoErrorException.EmptyTodoList) + assert(e is TodoException.EmptyString) + assert(e !is TodoException.EmptyTodoList) } todo.addItem("Write strings support") diff --git a/fixtures/coverall/tests/bindings/test_coverall.kts b/fixtures/coverall/tests/bindings/test_coverall.kts index 04304daa16..683e9f7e81 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.kts +++ b/fixtures/coverall/tests/bindings/test_coverall.kts @@ -59,7 +59,7 @@ Coveralls("test_arcs").use { coveralls -> try { coveralls.takeOtherFallible() throw RuntimeException("Should have thrown a IntegerOverflow exception!") - } catch (e: CoverallErrorException.TooManyHoles) { + } catch (e: CoverallException.TooManyHoles) { // It's okay! } try { diff --git a/fixtures/uniffi-fixture-time/tests/bindings/test_chronological.kts b/fixtures/uniffi-fixture-time/tests/bindings/test_chronological.kts index e61b8c1427..00f626988d 100644 --- a/fixtures/uniffi-fixture-time/tests/bindings/test_chronological.kts +++ b/fixtures/uniffi-fixture-time/tests/bindings/test_chronological.kts @@ -19,7 +19,7 @@ assert(add(Instant.parse("1955-11-05T00:06:00.283000001Z"), Duration.ofSeconds(1 try { diff(Instant.ofEpochSecond(100), Instant.ofEpochSecond(101)) throw RuntimeException("Should have thrown a TimeDiffError exception!") -} catch (e: ChronologicalErrorException) { +} catch (e: ChronologicalException) { // It's okay! } diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin.rs index 28ef1b5425..e8cef3f5ae 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin.rs @@ -280,4 +280,20 @@ mod filters { _ => format!("{}.read({})", type_kt(type_)?, nm), }) } + + /// Generate a Kotlin 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". + pub fn exception_name_kt(error: &Error) -> Result { + match error.name().strip_suffix("Error") { + None => Ok(error.name().to_owned()), + Some(stripped) => { + let mut kt_exc_name = stripped.to_owned(); + kt_exc_name.push_str("Exception"); + Ok(kt_exc_name) + } + } + } } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt index c72d859170..34f3c3dd0d 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt @@ -111,7 +111,7 @@ internal open class {{e.name()}} : RustError() { val message = this.consumeErrorMessage() when (code) { {% for value in e.values() -%} - {{loop.index}} -> return {{e.name()}}Exception.{{value}}(message) as E + {{loop.index}} -> return {{e|exception_name_kt}}.{{value}}(message) as E {% endfor -%} -1 -> return InternalException(message) as E else -> throw RuntimeException("invalid error code passed across the FFI") @@ -119,9 +119,9 @@ internal open class {{e.name()}} : RustError() { } } -open class {{e.name()}}Exception(message: String) : Exception(message) { +open class {{e|exception_name_kt}}(message: String) : Exception(message) { {% for value in e.values() -%} - class {{value}}(msg: String) : {{e.name()}}Exception(msg) + class {{value}}(msg: String) : {{e|exception_name_kt}}(msg) {% endfor %} } From 4da826e12744d508553a9ed4d2ab6a5f3b059d9f Mon Sep 17 00:00:00 2001 From: Tarik Eshaq Date: Mon, 21 Jun 2021 10:35:41 -0700 Subject: [PATCH 09/45] Adds ktlint to kotlin requirements in contributing.md --- docs/contributing.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/contributing.md b/docs/contributing.md index 7ee8bb84c0..5eae5b0eaa 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -47,7 +47,8 @@ recommend installing the language dependencies on your local machine. In order t test suite you will need: * Kotlin: - * `kotlinc`, the [Kotlin command-line compiler](https://kotlinlang.org/docs/command-line.html) + * `kotlinc`, the [Kotlin command-line compiler](https://kotlinlang.org/docs/command-line.html). + * `ktlint`, the [Kotlin linter used to format the generated bindings](https://ktlint.github.io/). * The [Java Native Access](https://github.com/java-native-access/jna#download) JAR downloaded and its path added to your `$CLASSPATH` environment variable. * Swift: From 3cd613c475006698303376c76ffaed9c83092a0f Mon Sep 17 00:00:00 2001 From: Tarik Eshaq Date: Mon, 21 Jun 2021 12:09:41 -0700 Subject: [PATCH 10/45] Generates cargo clippy safe code --- .../scaffolding/templates/ObjectTemplate.rs | 4 +--- .../templates/TopLevelFunctionTemplate.rs | 3 +-- .../src/scaffolding/templates/macros.rs | 20 +++++++++++++++---- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs index e5c8e19786..660ff1bc6e 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs @@ -40,7 +40,6 @@ pub extern "C" fn {{ ffi_free.name() }}(ptr: *const std::os::raw::c_void) { } {%- for cons in obj.constructors() %} - #[allow(clippy::all)] #[doc(hidden)] #[no_mangle] pub extern "C" fn {{ cons.ffi_func().name() }}( @@ -57,12 +56,11 @@ pub extern "C" fn {{ ffi_free.name() }}(ptr: *const std::os::raw::c_void) { {%- endfor %} {%- for meth in obj.methods() %} - #[allow(clippy::all)] #[doc(hidden)] #[no_mangle] pub extern "C" fn {{ meth.ffi_func().name() }}( {%- call rs::arg_list_ffi_decl(meth.ffi_func()) %} - ) -> {% call rs::return_type_func(meth) %} { + ) {% 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. diff --git a/uniffi_bindgen/src/scaffolding/templates/TopLevelFunctionTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/TopLevelFunctionTemplate.rs index cc6accc752..f4736bf256 100644 --- a/uniffi_bindgen/src/scaffolding/templates/TopLevelFunctionTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/TopLevelFunctionTemplate.rs @@ -4,12 +4,11 @@ // send data across the FFI, which will fail to compile if the provided function does not match what's // specified in the UDL. #} -#[allow(clippy::all)] #[doc(hidden)] #[no_mangle] pub extern "C" fn {{ func.ffi_func().name() }}( {% call rs::arg_list_ffi_decl(func.ffi_func()) %} -) -> {% call rs::return_type_func(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() }}"); diff --git a/uniffi_bindgen/src/scaffolding/templates/macros.rs b/uniffi_bindgen/src/scaffolding/templates/macros.rs index 61bc9b40ac..5c41a70d43 100644 --- a/uniffi_bindgen/src/scaffolding/templates/macros.rs +++ b/uniffi_bindgen/src/scaffolding/templates/macros.rs @@ -34,6 +34,8 @@ {%- 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) %}{{ "_retval"|lower_rs(return_type) }}{% else %}_retval{% endmatch %}{% endmacro %} @@ -68,8 +70,13 @@ uniffi::deps::ffi_support::call_with_result(err, || -> Result<{% call return_typ }) {% else %} uniffi::deps::ffi_support::call_with_output(err, || { - let _retval = {{ obj.name() }}::{% call to_rs_call(meth) %}; - {% call ret(meth) %} + {% match meth.return_type() -%} + {% when Some with (return_type) -%} + let retval = {{ obj.name() }}::{% call to_rs_call(meth) %}; + {{"retval"|lower_rs(return_type)}} + {% else -%} + {{ obj.name() }}::{% call to_rs_call(meth) %} + {% endmatch -%} }) {% endmatch -%} {% endmacro -%} @@ -83,8 +90,13 @@ uniffi::deps::ffi_support::call_with_result(err, || -> Result<{% call return_typ }) {% else %} uniffi::deps::ffi_support::call_with_output(err, || { - let _retval = {% call to_rs_call(func) %}; - {% call ret(func) %} + {% match func.return_type() -%} + {% when Some with (return_type) -%} + let retval = {% call to_rs_call(func) %}; + {{"retval"|lower_rs(return_type)}} + {% else -%} + {% call to_rs_call(func) %} + {% endmatch -%} }) {% endmatch %} {% endmacro %} From 13b35bb734e0f68eba04f159d7b3896eff951904 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Thu, 17 Jun 2021 18:45:43 -0400 Subject: [PATCH 11/45] Use UTC for Timestamps in python This fixes a bug with timestamp conversion during DST. It also gives consumers tz-aware datetimes, which are generally nicer to use. --- CHANGELOG.md | 1 + .../tests/bindings/test_chronological.py | 21 ++++++++++--------- .../python/templates/RustBufferBuilder.py | 6 +++--- .../python/templates/RustBufferStream.py | 7 +++++-- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4928d0049d..24303a7e99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ ### What's Changed - Both python and ruby backends now handle U16 correctly. +- Python timestamps will now be in UTC and timezone-aware rather than naive. ## v0.12.0 (2021-06-14) diff --git a/fixtures/uniffi-fixture-time/tests/bindings/test_chronological.py b/fixtures/uniffi-fixture-time/tests/bindings/test_chronological.py index 0b0de03cf4..728f443436 100644 --- a/fixtures/uniffi-fixture-time/tests/bindings/test_chronological.py +++ b/fixtures/uniffi-fixture-time/tests/bindings/test_chronological.py @@ -3,39 +3,40 @@ import sys # Test passing timestamp and duration while returning timestamp -assert add(datetime.fromtimestamp(100.000001), timedelta(seconds=1, microseconds=1)).timestamp() == 101.000002 +assert add(datetime.fromtimestamp(100.000001, timezone.utc), timedelta(seconds=1, microseconds=1)).timestamp() == 101.000002 # Test passing timestamp while returning duration -assert diff(datetime.fromtimestamp(101.000002), datetime.fromtimestamp(100.000001)) == timedelta(seconds=1, microseconds=1) +assert diff(datetime.fromtimestamp(101.000002, timezone.utc), datetime.fromtimestamp(100.000001, timezone.utc)) == timedelta(seconds=1, microseconds=1) # Test pre-epoch timestamps -assert add(datetime.fromisoformat('1955-11-05T00:06:00.283001'), timedelta(seconds=1, microseconds=1)) == datetime.fromisoformat('1955-11-05T00:06:01.283002') +assert add(datetime.fromisoformat('1955-11-05T00:06:00.283001+00:00'), timedelta(seconds=1, microseconds=1)) == datetime.fromisoformat('1955-11-05T00:06:01.283002+00:00') # Test exceptions are propagated try: - diff(datetime.fromtimestamp(100), datetime.fromtimestamp(101)) + diff(datetime.fromtimestamp(100, timezone.utc), datetime.fromtimestamp(101, timezone.utc)) assert(not("Should have thrown a TimeDiffError exception!")) except ChronologicalError.TimeDiffError: # It's okay! pass # Test near max timestamp bound, no microseconds due to python floating point precision issues -assert add(datetime(MAXYEAR, 12, 31, 23, 59, 59, 0), timedelta(seconds=0)) == datetime(MAXYEAR, 12, 31, 23, 59, 59, 0) +assert add(datetime(MAXYEAR, 12, 31, 23, 59, 59, 0, tzinfo=timezone.utc), timedelta(seconds=0)) == datetime(MAXYEAR, 12, 31, 23, 59, 59, 0, tzinfo=timezone.utc) # Test overflow at max timestamp bound try: - add(datetime(MAXYEAR, 12, 31, 23, 59, 59, 0), timedelta(seconds=1)) + add(datetime(MAXYEAR, 12, 31, 23, 59, 59, 0, tzinfo=timezone.utc), timedelta(seconds=1)) assert(not("Should have thrown a ValueError exception!")) except OverflowError: # It's okay! pass # Test that rust timestamps behave like kotlin timestamps -pythonBefore = datetime.now() +pythonBefore = datetime.now(timezone.utc) rustNow = now() -pythonAfter = datetime.now() +pythonAfter = datetime.now(timezone.utc) assert pythonBefore <= rustNow <= pythonAfter -# Test that uniffi returns naive datetime -assert now().tzinfo is None +# Test that uniffi returns UTC times +assert now().tzinfo is timezone.utc +assert abs(datetime.now(timezone.utc) - now()) <= timedelta(seconds=1) diff --git a/uniffi_bindgen/src/bindings/python/templates/RustBufferBuilder.py b/uniffi_bindgen/src/bindings/python/templates/RustBufferBuilder.py index 8eba87fc02..f170cfc8ea 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RustBufferBuilder.py +++ b/uniffi_bindgen/src/bindings/python/templates/RustBufferBuilder.py @@ -109,12 +109,12 @@ def writeString(self, v): {% when Type::Timestamp -%} def write{{ canonical_type_name }}(self, v): - if v >= datetime.datetime.fromtimestamp(0): + if v >= datetime.datetime.fromtimestamp(0, datetime.timezone.utc): sign = 1 - delta = v - datetime.datetime.fromtimestamp(0) + delta = v - datetime.datetime.fromtimestamp(0, datetime.timezone.utc) else: sign = -1 - delta = datetime.datetime.fromtimestamp(0) - v + delta = datetime.datetime.fromtimestamp(0, datetime.timezone.utc) - v seconds = delta.seconds + delta.days * 24 * 3600 nanoseconds = delta.microseconds * 1000 diff --git a/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py b/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py index 09d3b09f1e..82c6af894c 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py +++ b/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py @@ -113,10 +113,13 @@ def readString(self): def read{{ canonical_type_name }}(self): seconds = self._unpack_from(8, ">q") microseconds = self._unpack_from(4, ">I") / 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=None) + datetime.timedelta(seconds=seconds, microseconds=microseconds) + return datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc) + datetime.timedelta(seconds=seconds, microseconds=microseconds) else: - return datetime.datetime.fromtimestamp(0, tz=None) - datetime.timedelta(seconds=-seconds, microseconds=microseconds) + return datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc) - datetime.timedelta(seconds=-seconds, microseconds=microseconds) {% when Type::Duration -%} # The Duration type. From f80642be84a146c7ae74a4de950486db0005dfd8 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Wed, 23 Jun 2021 10:45:56 -0400 Subject: [PATCH 12/45] Replace lower_into_buffer and try_lift_from_buffer with a trait The main motivation is to eliminate the possibility of calling `try_lift_from_buffer()` on a `RustBuffer` that wasn't created with `lower_into_buffer()`. For example, this code seems reasonable, but it totally fails: ``` let buf: RustBuffer = ViaFfi::lower("Test".to_string()); assert_eq!(try_lift_from_buffer::(buf).unwrap(), "Test") ``` This is because Strings have an asymmetry between `lower()` and `write()`. `write()` needs to write out the string length, but `lower()` can optimize to avoid that and just use `RustBuffer.len`. It also reduces some boilerplate code, which is nice too. --- CHANGELOG.md | 6 +- uniffi/src/lib.rs | 124 ++++++------------ .../src/scaffolding/templates/EnumTemplate.rs | 12 +- .../scaffolding/templates/RecordTemplate.rs | 12 +- 4 files changed, 50 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24303a7e99..d9786f96e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ - + @@ -19,6 +19,10 @@ - Both python and ruby backends now handle U16 correctly. - Python timestamps will now be in UTC and timezone-aware rather than naive. +- Replaced `lower_into_buffer()` and `try_lift_from_buffer()` with the + `RustBufferViaFfi` trait. If you use those functions in your custom ViaFfi + implementation then you'll need to update the code. Check out the `Option<>` + implementation in uniffi/src/lib.rs for an example. ## v0.12.0 (2021-06-14) diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index 3bfa76bfc3..89a0e23c41 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -196,32 +196,6 @@ pub unsafe trait ViaFfi: Sized { fn try_read(buf: &mut &[u8]) -> Result; } -/// A helper function to lower a type by serializing it into a buffer. -/// -/// For complex types were it's too fiddly or too unsafe to convert them into a special-purpose -/// C-compatible value, you can use this helper function to implement `lower()` in terms of `write()` -/// and pass the value as a serialized buffer of bytes. -pub fn lower_into_buffer(value: T) -> RustBuffer { - let mut buf = Vec::new(); - ViaFfi::write(&value, &mut buf); - RustBuffer::from_vec(buf) -} - -/// A helper function to lift a type by deserializing it from a buffer. -/// -/// For complex types were it's too fiddly or too unsafe to convert them into a special-purpose -/// C-compatible value, you can use this helper function to implement `lift()` in terms of `read()` -/// and receive the value as a serialzied byte buffer. -pub fn try_lift_from_buffer(buf: RustBuffer) -> Result { - let vec = buf.destroy_into_vec(); - let mut buf = vec.as_slice(); - let value = ::try_read(&mut buf)?; - if buf.remaining() != 0 { - bail!("junk data left in buffer after lifting") - } - Ok(value) -} - /// 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 @@ -364,6 +338,44 @@ unsafe impl ViaFfi for String { } } +/// 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 RustBufferViaFfi: Sized { + fn write(&self, buf: &mut Vec); + fn try_read(buf: &mut &[u8]) -> Result; +} + +unsafe impl ViaFfi for T { + type FfiType = RustBuffer; + + fn lower(self) -> RustBuffer { + let mut buf = Vec::new(); + RustBufferViaFfi::write(&self, &mut buf); + RustBuffer::from_vec(buf) + } + + fn try_lift(v: RustBuffer) -> Result { + let vec = v.destroy_into_vec(); + let mut buf = vec.as_slice(); + let value = RustBufferViaFfi::try_read(&mut buf)?; + if buf.remaining() != 0 { + bail!("junk data left in buffer after lifting") + } + Ok(value) + } + + fn write(&self, buf: &mut Vec) { + RustBufferViaFfi::write(self, buf) + } + + fn try_read(buf: &mut &[u8]) -> Result { + RustBufferViaFfi::try_read(buf) + } +} + /// Support for passing timestamp values via the FFI. /// /// Timestamps values are currently always passed by serializing to a buffer. @@ -378,17 +390,7 @@ unsafe impl ViaFfi for String { /// 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. -unsafe impl ViaFfi for SystemTime { - type FfiType = RustBuffer; - - fn lower(self) -> Self::FfiType { - lower_into_buffer(self) - } - - fn try_lift(v: Self::FfiType) -> Result { - try_lift_from_buffer(v) - } - +impl RustBufferViaFfi for SystemTime { fn write(&self, buf: &mut Vec) { let mut sign = 1; let epoch_offset = self @@ -428,17 +430,7 @@ unsafe impl ViaFfi for SystemTime { /// 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. -unsafe impl ViaFfi for Duration { - type FfiType = RustBuffer; - - fn lower(self) -> Self::FfiType { - lower_into_buffer(self) - } - - fn try_lift(v: Self::FfiType) -> Result { - try_lift_from_buffer(v) - } - +impl RustBufferViaFfi for Duration { fn write(&self, buf: &mut Vec) { buf.put_u64(self.as_secs()); buf.put_u32(self.subsec_nanos()); @@ -459,17 +451,7 @@ unsafe impl ViaFfi for Duration { /// 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. -unsafe impl ViaFfi for Option { - type FfiType = RustBuffer; - - fn lower(self) -> Self::FfiType { - lower_into_buffer(self) - } - - fn try_lift(v: Self::FfiType) -> Result { - try_lift_from_buffer(v) - } - +impl RustBufferViaFfi for Option { fn write(&self, buf: &mut Vec) { match self { None => buf.put_i8(0), @@ -499,17 +481,7 @@ unsafe impl ViaFfi for Option { /// Ideally we would pass `Vec` directly as a `RustBuffer` rather /// than serializing, and perhaps even pass other vector types using a /// similar struct. But that's for future work. -unsafe impl ViaFfi for Vec { - type FfiType = RustBuffer; - - fn lower(self) -> Self::FfiType { - lower_into_buffer(self) - } - - fn try_lift(v: Self::FfiType) -> Result { - try_lift_from_buffer(v) - } - +impl RustBufferViaFfi for Vec { fn write(&self, buf: &mut Vec) { // TODO: would be nice not to panic here :-/ let len = i32::try_from(self.len()).unwrap(); @@ -538,17 +510,7 @@ unsafe impl ViaFfi for Vec { /// 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). -unsafe impl ViaFfi for HashMap { - type FfiType = RustBuffer; - - fn lower(self) -> Self::FfiType { - lower_into_buffer(self) - } - - fn try_lift(v: Self::FfiType) -> Result { - try_lift_from_buffer(v) - } - +impl RustBufferViaFfi for HashMap { fn write(&self, buf: &mut Vec) { // TODO: would be nice not to panic here :-/ let len = i32::try_from(self.len()).unwrap(); diff --git a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs index 6485b5b226..9d0034f704 100644 --- a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs @@ -4,17 +4,7 @@ // compile if the provided struct has a different shape to the one declared in the UDL. #} #[doc(hidden)] -unsafe impl uniffi::ViaFfi for {{ e.name() }} { - type FfiType = uniffi::RustBuffer; - - fn lower(self) -> Self::FfiType { - uniffi::lower_into_buffer(self) - } - - fn try_lift(v: Self::FfiType) -> uniffi::deps::anyhow::Result { - uniffi::try_lift_from_buffer(v) - } - +impl uniffi::RustBufferViaFfi for {{ e.name() }} { fn write(&self, buf: &mut Vec) { use uniffi::deps::bytes::BufMut; match self { diff --git a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs index 9e8fdfa5e1..d8161a84ec 100644 --- a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs @@ -5,17 +5,7 @@ // compiler will complain with a type error. #} #[doc(hidden)] -unsafe impl uniffi::ViaFfi for {{ rec.name() }} { - type FfiType = uniffi::RustBuffer; - - fn lower(self) -> Self::FfiType { - uniffi::lower_into_buffer(self) - } - - fn try_lift(v: Self::FfiType) -> uniffi::deps::anyhow::Result { - uniffi::try_lift_from_buffer(v) - } - +impl uniffi::RustBufferViaFfi for {{ rec.name() }} { fn write(&self, buf: &mut Vec) { // 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. From 4bf02fb8bf353e74125395dc2230e903b81c8d50 Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Mon, 28 Jun 2021 11:29:30 +1000 Subject: [PATCH 13/45] Apply Nightly Clippy fixes Nightly clippy seems to have some helpful new lints about needless borrows, this applies the automated fixes. --- .../src/bindings/gecko_js/gen_gecko_js.rs | 4 ++-- uniffi_bindgen/src/bindings/kotlin/mod.rs | 6 +++--- uniffi_bindgen/src/bindings/mod.rs | 14 +++++++------- uniffi_bindgen/src/bindings/python/mod.rs | 4 ++-- uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs | 4 ++-- uniffi_bindgen/src/bindings/ruby/mod.rs | 4 ++-- uniffi_bindgen/src/bindings/swift/mod.rs | 10 +++++----- uniffi_bindgen/src/interface/attributes.rs | 2 +- uniffi_bindgen/src/interface/literal.rs | 6 +++--- uniffi_bindgen/src/interface/record.rs | 8 ++++---- uniffi_bindgen/src/interface/types/finder.rs | 2 +- uniffi_bindgen/src/lib.rs | 12 ++++++------ uniffi_macros/src/lib.rs | 2 +- 13 files changed, 39 insertions(+), 39 deletions(-) diff --git a/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs b/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs index 6a3295f4c1..29ee1667ac 100644 --- a/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs +++ b/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs @@ -395,7 +395,7 @@ mod filters { _ => format!("Nullable<{}>", in_arg_type_cpp(inner, context)?), }, WebIDLType::Sequence(inner) => { - format!("Sequence<{}>", in_arg_type_cpp(&inner, context)?) + format!("Sequence<{}>", in_arg_type_cpp(inner, context)?) } _ => type_cpp(type_, context)?, }) @@ -423,7 +423,7 @@ mod filters { WebIDLType::Nullable(inner) => match inner.as_ref() { WebIDLType::Flat(Type::String) => "const nsAString&".into(), WebIDLType::Flat(Type::Object(name)) => { - format!("{}*", class_name_cpp(&name, context)?) + format!("{}*", class_name_cpp(name, context)?) } _ => format!("const {}&", in_arg_type_cpp(&arg.webidl_type(), context)?), }, diff --git a/uniffi_bindgen/src/bindings/kotlin/mod.rs b/uniffi_bindgen/src/bindings/kotlin/mod.rs index fc0fe64f3e..b1071db977 100644 --- a/uniffi_bindgen/src/bindings/kotlin/mod.rs +++ b/uniffi_bindgen/src/bindings/kotlin/mod.rs @@ -24,11 +24,11 @@ pub fn write_bindings( try_format_code: bool, _is_testing: bool, ) -> Result<()> { - let mut kt_file = full_bindings_path(&config, out_dir)?; + let mut kt_file = full_bindings_path(config, out_dir)?; std::fs::create_dir_all(&kt_file)?; kt_file.push(format!("{}.kt", ci.namespace())); let mut f = File::create(&kt_file).context("Failed to create .kt file for bindings")?; - write!(f, "{}", generate_bindings(config, &ci)?)?; + write!(f, "{}", generate_bindings(config, ci)?)?; if try_format_code { if let Err(e) = Command::new("ktlint") .arg("-F") @@ -53,7 +53,7 @@ fn full_bindings_path(config: &Config, out_dir: &Path) -> Result { // Generate kotlin bindings for the given ComponentInterface, as a string. pub fn generate_bindings(config: &Config, ci: &ComponentInterface) -> Result { use askama::Template; - KotlinWrapper::new(config.clone(), &ci) + KotlinWrapper::new(config.clone(), ci) .render() .map_err(|_| anyhow::anyhow!("failed to render kotlin bindings")) } diff --git a/uniffi_bindgen/src/bindings/mod.rs b/uniffi_bindgen/src/bindings/mod.rs index 60d024688c..3be2893542 100644 --- a/uniffi_bindgen/src/bindings/mod.rs +++ b/uniffi_bindgen/src/bindings/mod.rs @@ -120,19 +120,19 @@ where let out_dir = out_dir.as_ref(); match language { TargetLanguage::Kotlin => { - kotlin::write_bindings(&config.kotlin, &ci, out_dir, try_format_code, is_testing)? + kotlin::write_bindings(&config.kotlin, ci, out_dir, try_format_code, is_testing)? } TargetLanguage::Swift => { - swift::write_bindings(&config.swift, &ci, out_dir, try_format_code, is_testing)? + swift::write_bindings(&config.swift, ci, out_dir, try_format_code, is_testing)? } TargetLanguage::Python => { - python::write_bindings(&config.python, &ci, out_dir, try_format_code, is_testing)? + python::write_bindings(&config.python, ci, out_dir, try_format_code, is_testing)? } TargetLanguage::Ruby => { - ruby::write_bindings(&config.ruby, &ci, out_dir, try_format_code, is_testing)? + ruby::write_bindings(&config.ruby, ci, out_dir, try_format_code, is_testing)? } TargetLanguage::GeckoJs => { - gecko_js::write_bindings(&config.gecko_js, &ci, out_dir, try_format_code, is_testing)? + gecko_js::write_bindings(&config.gecko_js, ci, out_dir, try_format_code, is_testing)? } } Ok(()) @@ -150,8 +150,8 @@ where { let out_dir = out_dir.as_ref(); match language { - TargetLanguage::Kotlin => kotlin::compile_bindings(&config.kotlin, &ci, out_dir)?, - TargetLanguage::Swift => swift::compile_bindings(&config.swift, &ci, out_dir)?, + TargetLanguage::Kotlin => kotlin::compile_bindings(&config.kotlin, ci, out_dir)?, + TargetLanguage::Swift => swift::compile_bindings(&config.swift, ci, out_dir)?, TargetLanguage::Python => (), TargetLanguage::Ruby => (), TargetLanguage::GeckoJs => (), diff --git a/uniffi_bindgen/src/bindings/python/mod.rs b/uniffi_bindgen/src/bindings/python/mod.rs index cb6d9b35bb..1bf66841d9 100644 --- a/uniffi_bindgen/src/bindings/python/mod.rs +++ b/uniffi_bindgen/src/bindings/python/mod.rs @@ -30,7 +30,7 @@ pub fn write_bindings( let mut py_file = PathBuf::from(out_dir); py_file.push(format!("{}.py", ci.namespace())); let mut f = File::create(&py_file).context("Failed to create .py file for bindings")?; - write!(f, "{}", generate_python_bindings(config, &ci)?)?; + write!(f, "{}", generate_python_bindings(config, ci)?)?; if try_format_code { if let Err(e) = Command::new("yapf").arg(py_file.to_str().unwrap()).output() { @@ -49,7 +49,7 @@ pub fn write_bindings( pub fn generate_python_bindings(config: &Config, ci: &ComponentInterface) -> Result { use askama::Template; - PythonWrapper::new(config.clone(), &ci) + PythonWrapper::new(config.clone(), ci) .render() .map_err(|_| anyhow::anyhow!("failed to render python bindings")) } diff --git a/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs b/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs index ce87379851..9ae5d1816f 100644 --- a/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs +++ b/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs @@ -35,7 +35,7 @@ fn cdylib_path() { }; assert_eq!("", config.cdylib_path()); - assert_eq!(false, config.custom_cdylib_path()); + assert!(!config.custom_cdylib_path()); let config = Config { cdylib_name: None, @@ -43,5 +43,5 @@ fn cdylib_path() { }; assert_eq!("/foo/bar", config.cdylib_path()); - assert_eq!(true, config.custom_cdylib_path()); + assert!(config.custom_cdylib_path()); } diff --git a/uniffi_bindgen/src/bindings/ruby/mod.rs b/uniffi_bindgen/src/bindings/ruby/mod.rs index f127e3c591..59b323d52b 100644 --- a/uniffi_bindgen/src/bindings/ruby/mod.rs +++ b/uniffi_bindgen/src/bindings/ruby/mod.rs @@ -30,7 +30,7 @@ pub fn write_bindings( let mut rb_file = PathBuf::from(out_dir); rb_file.push(format!("{}.rb", ci.namespace())); let mut f = File::create(&rb_file).context("Failed to create .rb file for bindings")?; - write!(f, "{}", generate_ruby_bindings(config, &ci)?)?; + write!(f, "{}", generate_ruby_bindings(config, ci)?)?; if try_format_code { if let Err(e) = Command::new("rubocop") @@ -53,7 +53,7 @@ pub fn write_bindings( pub fn generate_ruby_bindings(config: &Config, ci: &ComponentInterface) -> Result { use askama::Template; - RubyWrapper::new(config.clone(), &ci) + RubyWrapper::new(config.clone(), ci) .render() .map_err(|_| anyhow::anyhow!("failed to render ruby bindings")) } diff --git a/uniffi_bindgen/src/bindings/swift/mod.rs b/uniffi_bindgen/src/bindings/swift/mod.rs index 75d9742e37..7dd8101b83 100644 --- a/uniffi_bindgen/src/bindings/swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/mod.rs @@ -38,7 +38,7 @@ pub fn write_bindings( let mut source_file = out_path.clone(); source_file.push(format!("{}.swift", ci.namespace())); - let Bindings { header, library } = generate_bindings(config, &ci, is_testing)?; + let Bindings { header, library } = generate_bindings(config, ci, is_testing)?; let header_filename = config.header_filename(); let mut header_file = out_path.clone(); @@ -59,7 +59,7 @@ pub fn write_bindings( write!( m, "{}", - generate_module_map(config, &ci, &Path::new(&header_filename))? + generate_module_map(config, ci, Path::new(&header_filename))? )?; } @@ -86,10 +86,10 @@ pub fn generate_bindings( is_testing: bool, ) -> Result { use askama::Template; - let header = BridgingHeader::new(config, &ci) + let header = BridgingHeader::new(config, ci) .render() .map_err(|_| anyhow!("failed to render Swift bridging header"))?; - let library = SwiftWrapper::new(&config, &ci, is_testing) + let library = SwiftWrapper::new(config, ci, is_testing) .render() .map_err(|_| anyhow!("failed to render Swift library"))?; Ok(Bindings { header, library }) @@ -101,7 +101,7 @@ fn generate_module_map( header_path: &Path, ) -> Result { use askama::Template; - let module_map = ModuleMap::new(&config, &ci, header_path) + let module_map = ModuleMap::new(config, ci, header_path) .render() .map_err(|_| anyhow!("failed to render Swift module map"))?; Ok(module_map) diff --git a/uniffi_bindgen/src/interface/attributes.rs b/uniffi_bindgen/src/interface/attributes.rs index 935f21f87b..725a5c7197 100644 --- a/uniffi_bindgen/src/interface/attributes.rs +++ b/uniffi_bindgen/src/interface/attributes.rs @@ -107,7 +107,7 @@ where .collect::, _>>()?; for attr in attrs.iter() { - validator(&attr)?; + validator(attr)?; } Ok(attrs) diff --git a/uniffi_bindgen/src/interface/literal.rs b/uniffi_bindgen/src/interface/literal.rs index f306970ccc..8b333c6140 100644 --- a/uniffi_bindgen/src/interface/literal.rs +++ b/uniffi_bindgen/src/interface/literal.rs @@ -116,11 +116,11 @@ pub(super) fn convert_default_value( 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)?, + (_, 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_)?, + (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), }) diff --git a/uniffi_bindgen/src/interface/record.rs b/uniffi_bindgen/src/interface/record.rs index 07b417f7e3..d6a9b7fd03 100644 --- a/uniffi_bindgen/src/interface/record.rs +++ b/uniffi_bindgen/src/interface/record.rs @@ -166,7 +166,7 @@ mod test { assert_eq!(record.fields().len(), 1); assert_eq!(record.fields()[0].name(), "field"); assert_eq!(record.fields()[0].type_().canonical_name(), "u32"); - assert_eq!(record.fields()[0].required, false); + assert!(!record.fields()[0].required); assert!(record.fields()[0].default_value().is_none()); let record = ci.get_record_definition("Complex").unwrap(); @@ -177,18 +177,18 @@ mod test { record.fields()[0].type_().canonical_name(), "Optionalstring" ); - assert_eq!(record.fields()[0].required, false); + assert!(!record.fields()[0].required); 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_eq!(record.fields()[1].required, false); + assert!(!record.fields()[1].required); 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_eq!(record.fields()[2].required, true); + assert!(record.fields()[2].required); assert!(record.fields()[2].default_value().is_none()); } diff --git a/uniffi_bindgen/src/interface/types/finder.rs b/uniffi_bindgen/src/interface/types/finder.rs index b537a74a53..c1ed8c2772 100644 --- a/uniffi_bindgen/src/interface/types/finder.rs +++ b/uniffi_bindgen/src/interface/types/finder.rs @@ -36,7 +36,7 @@ pub(in super::super) trait TypeFinder { impl TypeFinder for &[T] { fn add_type_definitions_to(&self, types: &mut TypeUniverse) -> Result<()> { for item in self.iter() { - (&item).add_type_definitions_to(types)?; + item.add_type_definitions_to(types)?; } Ok(()) } diff --git a/uniffi_bindgen/src/lib.rs b/uniffi_bindgen/src/lib.rs index a87cd84558..10200bc5f0 100644 --- a/uniffi_bindgen/src/lib.rs +++ b/uniffi_bindgen/src/lib.rs @@ -125,14 +125,14 @@ pub fn generate_component_scaffolding>( let config_file_override = config_file_override.as_ref().map(|p| p.as_ref()); let out_dir_override = out_dir_override.as_ref().map(|p| p.as_ref()); let udl_file = udl_file.as_ref(); - let component = parse_udl(&udl_file)?; + let component = parse_udl(udl_file)?; let _config = get_config(&component, udl_file, config_file_override); let mut filename = Path::new(&udl_file) .file_stem() .ok_or_else(|| anyhow!("not a file"))? .to_os_string(); filename.push(".uniffi.rs"); - let mut out_dir = get_out_dir(&udl_file, out_dir_override)?; + let mut out_dir = get_out_dir(udl_file, out_dir_override)?; out_dir.push(filename); let mut f = File::create(&out_dir).map_err(|e| anyhow!("Failed to create output file: {:?}", e))?; @@ -157,9 +157,9 @@ pub fn generate_bindings>( let config_file_override = config_file_override.as_ref().map(|p| p.as_ref()); let udl_file = udl_file.as_ref(); - let component = parse_udl(&udl_file)?; + let component = parse_udl(udl_file)?; let config = get_config(&component, udl_file, config_file_override)?; - let out_dir = get_out_dir(&udl_file, out_dir_override)?; + let out_dir = get_out_dir(udl_file, out_dir_override)?; for language in target_languages { bindings::write_bindings( &config.bindings, @@ -185,7 +185,7 @@ pub fn run_tests>( let udl_file = udl_file.as_ref(); let config_file_override = config_file_override.as_ref().map(|p| p.as_ref()); - let component = parse_udl(&udl_file)?; + let component = parse_udl(udl_file)?; let config = get_config(&component, udl_file, config_file_override)?; // Group the test scripts by language first. @@ -339,7 +339,7 @@ pub fn run_main() -> Result<()> { .short("-l") .multiple(true) .number_of_values(1) - .possible_values(&POSSIBLE_LANGUAGES) + .possible_values(POSSIBLE_LANGUAGES) .help("Foreign language(s) for which to build bindings"), ) .arg( diff --git a/uniffi_macros/src/lib.rs b/uniffi_macros/src/lib.rs index 2dfb8a1d90..eae6ecd52f 100644 --- a/uniffi_macros/src/lib.rs +++ b/uniffi_macros/src/lib.rs @@ -37,7 +37,7 @@ pub fn build_foreign_language_testcases(paths: proc_macro::TokenStream) -> proc_ let test_functions = paths.test_scripts .iter() .map(|file_path| { - let test_file_pathbuf: PathBuf = [&pkg_dir, &file_path].iter().collect(); + let test_file_pathbuf: PathBuf = [&pkg_dir, file_path].iter().collect(); let test_file_path = test_file_pathbuf.to_string_lossy(); let test_file_name = test_file_pathbuf .file_name() From 516de72c152cde98e5d6d7c71695a251dff02bbf Mon Sep 17 00:00:00 2001 From: Jan-Erik Rediger Date: Tue, 6 Jul 2021 16:25:52 +0200 Subject: [PATCH 14/45] Fix typo: languge -> language --- docs/manual/src/udl/structs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manual/src/udl/structs.md b/docs/manual/src/udl/structs.md index 2021c8ffe2..c5f9a2e914 100644 --- a/docs/manual/src/udl/structs.md +++ b/docs/manual/src/udl/structs.md @@ -57,7 +57,7 @@ struct TodoEntry { } ``` -Depending on the languge, the foreign-language bindings may also need to be aware of +Depending on the language, the foreign-language bindings may also need to be aware of these embedded references. For example in Kotlin, each Object instance must be explicitly destroyed to avoid leaking the underlying memory, and this also applies to Objects stored in record fields. From e6e0913ea6ee6cd518be4fe2fb474fe89feaf10b Mon Sep 17 00:00:00 2001 From: Jan-Erik Rediger Date: Tue, 6 Jul 2021 16:16:54 +0200 Subject: [PATCH 15/45] Document optional & default fields for dictionaries --- docs/manual/src/udl/structs.md | 69 ++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/docs/manual/src/udl/structs.md b/docs/manual/src/udl/structs.md index c5f9a2e914..ac4fff3b0f 100644 --- a/docs/manual/src/udl/structs.md +++ b/docs/manual/src/udl/structs.md @@ -63,3 +63,72 @@ destroyed to avoid leaking the underlying memory, and this also applies to Objec in record fields. You can read more about managing object references in the section on [interfaces](./interfaces.md). + +## Default values for fields + +Fields can be specified with a default value: + +```idl +dictionary TodoEntry { + boolean done = false; + string text; +}; +``` + +The corresponding generated Kotlin code would be equivalent to: + +```kotlin +data class TodoEntry ( + var done: Boolean = false, + var text: String +) { + // ... +} +``` + +This works for Swift and Python targets too. +If not set otherwise the default value for a field is passed to the Rust constructor. + +## Optional fields and default values + +Fields can be made optional using a `T?` type. + +```idl +dictionary TodoEntry { + boolean done; + string? text; +}; +``` + +The corresponding generated Kotlin code would be equivalent to: + +```kotlin +data class TodoEntry ( + var done: Boolean, + var text: String? +) { + // ... +} +``` + +Optional fields can also be set to a default `null` value: + +```idl +dictionary TodoEntry { + boolean done; + string? text = null; +}; +``` + +The corresponding generated Kotlin code would be equivalent to: + +```kotlin +data class TodoEntry ( + var done: Boolean, + var text: String? = null +) { + // ... +} +``` + +This works for Swift and Python targets too. From 44d36c1dcde5f8137d0a26454ce9311cc9ddaba7 Mon Sep 17 00:00:00 2001 From: Jan-Erik Rediger Date: Tue, 6 Jul 2021 16:25:37 +0200 Subject: [PATCH 16/45] Document optional arguments with default values for functions --- docs/manual/src/udl/functions.md | 33 +++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/manual/src/udl/functions.md b/docs/manual/src/udl/functions.md index f86273e6d0..55bf7ef97c 100644 --- a/docs/manual/src/udl/functions.md +++ b/docs/manual/src/udl/functions.md @@ -15,4 +15,35 @@ The UDL file will look like: namespace Example { string hello_world(); } -``` \ No newline at end of file +``` + +## Optional arguments & default values + +Function arguments can be marked `optional` with a default value specified. + +In the UDL file: + +```idl +namespace Example { + string hello_name(optional string name = "world"); +} +``` + +The Rust code will take a required argument: + +```rust +fn hello_name(name: String) -> String { + format!("Hello {}", name) +} +``` + +The generated foreign-language bindings will use function parameters with default values. +This works for the Kotlin, Swift and Python targets. + +For example the generated Kotlin code will be equivalent to: + +```kotlin +fun helloName(name: String = "world" ): String { + // ... +} +``` From b6e8ff2b98c9c884fb038d775925e858bff744cc Mon Sep 17 00:00:00 2001 From: Jan-Erik Rediger Date: Wed, 7 Jul 2021 09:58:18 +0200 Subject: [PATCH 17/45] Add Rust code with optional field --- docs/manual/src/udl/structs.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/manual/src/udl/structs.md b/docs/manual/src/udl/structs.md index ac4fff3b0f..7f256de137 100644 --- a/docs/manual/src/udl/structs.md +++ b/docs/manual/src/udl/structs.md @@ -6,7 +6,7 @@ Think of them like a Rust struct without any methods. A Rust struct like this: -```rust +```rust,no_run struct TodoEntry { done: bool, due_date: u64, @@ -50,7 +50,7 @@ dictionary TodoEntry { Then the corresponding Rust code would need to look like this: -```rust +```rust,no_run struct TodoEntry { owner: std::sync::Arc, text: String, @@ -87,7 +87,7 @@ data class TodoEntry ( ``` This works for Swift and Python targets too. -If not set otherwise the default value for a field is passed to the Rust constructor. +If not set otherwise the default value for a field is used when constructing the Rust struct. ## Optional fields and default values @@ -100,6 +100,15 @@ dictionary TodoEntry { }; ``` +The corresponding Rust struct would need to look like this: + +```rust,no_run +struct TodoEntry { + done: bool, + text: Option, +} +``` + The corresponding generated Kotlin code would be equivalent to: ```kotlin From 50d9feb0cd133050067d39bd61e41a6692d69620 Mon Sep 17 00:00:00 2001 From: Jan-Erik Rediger Date: Mon, 12 Jul 2021 11:06:16 +0200 Subject: [PATCH 18/45] Document how to suppress experimental unsigned type warnings for Kotlin Fixes #976 --- docs/manual/src/kotlin/gradle.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/manual/src/kotlin/gradle.md b/docs/manual/src/kotlin/gradle.md index 2de0019c8a..631161a73d 100644 --- a/docs/manual/src/kotlin/gradle.md +++ b/docs/manual/src/kotlin/gradle.md @@ -18,3 +18,27 @@ android.libraryVariants.all { variant -> ``` The generated bindings should appear in the project sources in Android Studio. + +## Using experimental unsigned types + +Unsigned integers in the defined API are translated to their equivalents in the foreign language binding, e.g. `u32` becomes Kotlin's `UInt` type. +See [Built-in types](../udl/builtin_types.md). + +However unsigned integer types are experimental in Kotlin versions prior to 1.5. +As such they require explicit annotations to suppress warnings. +Uniffi is trying to add these annotations where necessary, +but currently misses some places, see [PR #977](https://github.com/mozilla/uniffi-rs/pull/977) for details. + +To suppress all warnings for experimental unsigned types add this to your project's `build.gradle` file: + +```groovy +allprojects { + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + freeCompilerArgs += [ + "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes", + ] + } + } +} +``` From d44b96c9de105a6fafc4ff1e1f66e3248631a129 Mon Sep 17 00:00:00 2001 From: Sammy Khamis Date: Tue, 13 Jul 2021 10:09:17 -1000 Subject: [PATCH 19/45] Add @ExperimentalUnsignedTypes to unsigned types in kotlin (#977) * Add a test fixture for Kotlin "experimental unsigned types" warnings. * add annotations to kotlin when detecting unsigned types Co-authored-by: Ryan Kelly --- Cargo.toml | 1 + .../tests/bindings/test_callbacks.kts | 2 +- .../Cargo.toml | 18 +++++++ .../README.md | 18 +++++++ .../build.rs | 7 +++ .../src/lib.rs | 50 +++++++++++++++++++ .../src/test.udl | 36 +++++++++++++ .../tests/bindings/test.kts | 8 +++ .../tests/test_generated_bindings.rs | 1 + uniffi_bindgen/src/bindings/kotlin/mod.rs | 8 ++- .../bindings/kotlin/templates/EnumTemplate.kt | 1 + .../kotlin/templates/ObjectTemplate.kt | 2 + .../kotlin/templates/RecordTemplate.kt | 1 + .../kotlin/templates/RustBufferHelpers.kt | 13 ++++- .../templates/TopLevelFunctionTemplate.kt | 2 + .../src/bindings/kotlin/templates/macros.kt | 5 ++ uniffi_bindgen/src/interface/enum_.rs | 8 +++ uniffi_bindgen/src/interface/function.rs | 15 ++++++ uniffi_bindgen/src/interface/mod.rs | 23 +++++++++ uniffi_bindgen/src/interface/object.rs | 32 ++++++++++++ uniffi_bindgen/src/interface/record.rs | 6 +++ 21 files changed, 254 insertions(+), 3 deletions(-) create mode 100644 fixtures/regressions/kotlin-experimental-unsigned-types/Cargo.toml create mode 100644 fixtures/regressions/kotlin-experimental-unsigned-types/README.md create mode 100644 fixtures/regressions/kotlin-experimental-unsigned-types/build.rs create mode 100644 fixtures/regressions/kotlin-experimental-unsigned-types/src/lib.rs create mode 100644 fixtures/regressions/kotlin-experimental-unsigned-types/src/test.udl create mode 100644 fixtures/regressions/kotlin-experimental-unsigned-types/tests/bindings/test.kts create mode 100644 fixtures/regressions/kotlin-experimental-unsigned-types/tests/test_generated_bindings.rs diff --git a/Cargo.toml b/Cargo.toml index 6f0c8e35f4..0175f7fb78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "examples/todolist", "fixtures/coverall", "fixtures/regressions/enum-without-i32-helpers", + "fixtures/regressions/kotlin-experimental-unsigned-types", "fixtures/regressions/cdylib-crate-type-dependency/ffi-crate", "fixtures/regressions/cdylib-crate-type-dependency/cdylib-dependency", "fixtures/uniffi-fixture-time", diff --git a/examples/callbacks/tests/bindings/test_callbacks.kts b/examples/callbacks/tests/bindings/test_callbacks.kts index 6f0b9ef4d9..616cdb481b 100644 --- a/examples/callbacks/tests/bindings/test_callbacks.kts +++ b/examples/callbacks/tests/bindings/test_callbacks.kts @@ -57,7 +57,7 @@ val rustGetters = RustGetters() class KotlinGetters(): ForeignGetters { override fun getBool(v: Boolean, arg2: Boolean): Boolean = v xor arg2 override fun getString(v: String, arg2: Boolean): String = if (arg2) "1234567890123" else v - override fun getOption(v: String?, arg2: Boolean): String? = if (arg2) v?.toUpperCase() else v + override fun getOption(v: String?, arg2: Boolean): String? = if (arg2) v?.uppercase() else v override fun getList(v: List, arg2: Boolean): List = if (arg2) v else listOf() } diff --git a/fixtures/regressions/kotlin-experimental-unsigned-types/Cargo.toml b/fixtures/regressions/kotlin-experimental-unsigned-types/Cargo.toml new file mode 100644 index 0000000000..22c5b8c667 --- /dev/null +++ b/fixtures/regressions/kotlin-experimental-unsigned-types/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "kotlin-experimental-unsigned-types" +edition = "2018" +version = "0.8.0" +authors = ["Firefox Sync Team "] +license = "MPL-2.0" +publish = false + +[lib] +crate-type = ["cdylib"] +name = "uniffi_regression_test_kt_unsigned_types" + +[dependencies] +uniffi_macros = {path = "../../../uniffi_macros"} +uniffi = {path = "../../../uniffi", features=["builtin-bindgen"]} + +[build-dependencies] +uniffi_build = {path = "../../../uniffi_build", features=["builtin-bindgen"]} diff --git a/fixtures/regressions/kotlin-experimental-unsigned-types/README.md b/fixtures/regressions/kotlin-experimental-unsigned-types/README.md new file mode 100644 index 0000000000..54d1d43c95 --- /dev/null +++ b/fixtures/regressions/kotlin-experimental-unsigned-types/README.md @@ -0,0 +1,18 @@ +# Regression test for propagating annotations on Kotlin unsigned types. + +Kotlin's support for unsigned integers is currently experimental (kotlin stabilizes this api in [1.5](#kotlin-1.5)), +and requires explicit opt-in from the consuming application. +Since we can generate Kotlin code that uses unsigned types, we +need to ensure that such code is annotated with +[`@ExperimentalUnsignedTypes`][https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-experimental-unsigned-types/] +in order to avoid compilation warnings. + +This crate exposes an API that uses unsigned types in a variety of +ways, and tests that the resulting Kotlin bindings can be compiled +without warnings. + +--- + +## Kotlin 1.5 + +As of Kotlin 1.5, [unsigned types are stablized](https://kotlinlang.org/docs/whatsnew15.html#stable-unsigned-integer-types). This will elimate the need for this test fixture as well as any places in the code the `@ExperimentalUnsignedTypes` annotation lives. This will most likely be done when the minimum supported version of kotlin in uniffi-rs becomes 1.5. diff --git a/fixtures/regressions/kotlin-experimental-unsigned-types/build.rs b/fixtures/regressions/kotlin-experimental-unsigned-types/build.rs new file mode 100644 index 0000000000..8fb1438a05 --- /dev/null +++ b/fixtures/regressions/kotlin-experimental-unsigned-types/build.rs @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +fn main() { + uniffi_build::generate_scaffolding("./src/test.udl").unwrap(); +} diff --git a/fixtures/regressions/kotlin-experimental-unsigned-types/src/lib.rs b/fixtures/regressions/kotlin-experimental-unsigned-types/src/lib.rs new file mode 100644 index 0000000000..c1f50dec2c --- /dev/null +++ b/fixtures/regressions/kotlin-experimental-unsigned-types/src/lib.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/. */ + +pub fn returns_u16() -> u16 { + 16 +} + +pub fn accepts_and_returns_unsigned(a: u8, b: u16, c: u32, d: u64) -> u64 { + (a as u64) + (b as u64) + (c as u64) + d +} + +pub fn accepts_unsigned_returns_void(a: u8, b: u16, c: u32, d: u64) { + let _unused = accepts_and_returns_unsigned(a, b, c, d); +} + +pub struct DirectlyUsesU8 { + member: u8, + member_two: u16, + member_three: u32, + member_four: u64, + other: String, +} + +pub enum UnsignedEnum { + V1 { q1: u8 }, + V2 { addr: String }, +} + +pub struct RecursivelyUsesU8 { + member: DirectlyUsesU8, + member_two: UnsignedEnum, +} + +#[derive(Debug)] +pub struct InterfaceUsingUnsigned { + member: u64, +} + +impl InterfaceUsingUnsigned { + pub fn new(new: u64) -> Self { + Self { member: new } + } + + fn uses_unsigned_struct(&self, mut p1: RecursivelyUsesU8) { + p1.member_two = UnsignedEnum::V1 { q1: 0 } + } +} + +include!(concat!(env!("OUT_DIR"), "/test.uniffi.rs")); diff --git a/fixtures/regressions/kotlin-experimental-unsigned-types/src/test.udl b/fixtures/regressions/kotlin-experimental-unsigned-types/src/test.udl new file mode 100644 index 0000000000..776438ef96 --- /dev/null +++ b/fixtures/regressions/kotlin-experimental-unsigned-types/src/test.udl @@ -0,0 +1,36 @@ +// This API doesn't really *do* anything, it's just designed +// to include various unsigned types in various interesting +// positions. The test passes if we can compile the resulting +// Kotlin bindings without any warnings. + +namespace regression_test_kt_unsigned_types { + u16 returns_u16(); + u64 accepts_and_returns_unsigned(u8 a, u16 b, u32 c, u64 d); + void accepts_unsigned_returns_void(u8 a, u16 b, u32 c, u64 d); +}; + +[Enum] +interface UnsignedEnum { + V1(u8 q1); + V2(string addr); +}; + +dictionary DirectlyUsesU8 { + u8 member; + u16 member_two; + u32 member_three; + u64 member_four; + + string other; +}; + +dictionary RecursivelyUsesU8 { + DirectlyUsesU8 member; + UnsignedEnum member_two; +}; + +interface InterfaceUsingUnsigned { + constructor(u64 a); + + void uses_unsigned_struct(RecursivelyUsesU8 p1); +}; diff --git a/fixtures/regressions/kotlin-experimental-unsigned-types/tests/bindings/test.kts b/fixtures/regressions/kotlin-experimental-unsigned-types/tests/bindings/test.kts new file mode 100644 index 0000000000..ec497b947b --- /dev/null +++ b/fixtures/regressions/kotlin-experimental-unsigned-types/tests/bindings/test.kts @@ -0,0 +1,8 @@ +// This is just a basic "it compiled and ran" test. +// What we're really guarding against is failure to +// compile the bindings as a result of warnings about +// experimental features. + +import uniffi.regression_test_kt_unsigned_types.*; + +assert(returnsU16().toInt() == 16) diff --git a/fixtures/regressions/kotlin-experimental-unsigned-types/tests/test_generated_bindings.rs b/fixtures/regressions/kotlin-experimental-unsigned-types/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..2619519b8d --- /dev/null +++ b/fixtures/regressions/kotlin-experimental-unsigned-types/tests/test_generated_bindings.rs @@ -0,0 +1 @@ +uniffi_macros::build_foreign_language_testcases!("src/test.udl", ["tests/bindings/test.kts",]); diff --git a/uniffi_bindgen/src/bindings/kotlin/mod.rs b/uniffi_bindgen/src/bindings/kotlin/mod.rs index b1071db977..f3e9c35f6b 100644 --- a/uniffi_bindgen/src/bindings/kotlin/mod.rs +++ b/uniffi_bindgen/src/bindings/kotlin/mod.rs @@ -66,7 +66,9 @@ pub fn compile_bindings(config: &Config, ci: &ComponentInterface, out_dir: &Path let mut jar_file = PathBuf::from(out_dir); jar_file.push(format!("{}.jar", ci.namespace())); let status = Command::new("kotlinc") - .arg("-Xopt-in=kotlin.ExperimentalUnsignedTypes") + // Our generated bindings should not produce any warnings; fail tests if they do. + .arg("-Werror") + // Reflect $CLASSPATH from the environment, to help find `jna.jar`. .arg("-classpath") .arg(env::var("CLASSPATH").unwrap_or_else(|_| "".to_string())) .arg(&kt_file) @@ -105,9 +107,13 @@ pub fn run_script(out_dir: &Path, script_file: &Path) -> Result<()> { let mut cmd = Command::new("kotlinc"); // Make sure it can load the .jar and its dependencies. cmd.arg("-classpath").arg(classpath); + // Code that wants to use an API with unsigned types, must opt in to this experimental Kotlin feature. + // Specify it here in order to not have to worry about that when writing tests. cmd.arg("-Xopt-in=kotlin.ExperimentalUnsignedTypes"); // Enable runtime assertions, for easy testing etc. cmd.arg("-J-ea"); + // Our test scripts should not produce any warnings. + cmd.arg("-Werror"); cmd.arg("-script").arg(script_file); let status = cmd .spawn() diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt index 6749a1024f..e002ca7ab2 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt @@ -35,6 +35,7 @@ enum class {{ e.name()|class_name_kt }} { {% else %} +{% call kt::unsigned_types_annotation(e) %} sealed class {{ e.name()|class_name_kt }}{% if e.contains_object_references(ci) %}: Disposable {% endif %} { {% for variant in e.variants() -%} {% if !variant.has_fields() -%} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt index 6d4d5ec9f8..c8f649b522 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt @@ -1,3 +1,4 @@ +{% call kt::unsigned_types_annotation(obj) %} public interface {{ obj.name()|class_name_kt }}Interface { {% for meth in obj.methods() -%} fun {{ meth.name()|fn_name_kt }}({% call kt::arg_list_decl(meth) %}) @@ -8,6 +9,7 @@ public interface {{ obj.name()|class_name_kt }}Interface { {% endfor %} } +{% call kt::unsigned_types_annotation(obj) %} class {{ obj.name()|class_name_kt }}( pointer: Pointer ) : FFIObject(pointer), {{ obj.name()|class_name_kt }}Interface { diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt index 8afee780a3..65c1eb1be1 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt @@ -1,3 +1,4 @@ +{% call kt::unsigned_types_annotation(rec) %} data class {{ rec.name()|class_name_kt }} ( {%- for field in rec.fields() %} var {{ field.name()|var_name_kt }}: {{ field.type_()|type_kt -}} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferHelpers.kt b/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferHelpers.kt index 98c5873be4..1d3af771db 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferHelpers.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferHelpers.kt @@ -382,13 +382,14 @@ internal fun write{{ canonical_type_name }}(v: {{ type_name }}, buf: RustBufferB {% let inner_type_name = inner_type|type_kt %} // Helper functions for pasing values of type {{ typ|type_kt }} - +{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} internal fun lift{{ canonical_type_name }}(rbuf: RustBuffer.ByValue): {{ inner_type_name }}? { return liftFromRustBuffer(rbuf) { buf -> read{{ canonical_type_name }}(buf) } } +{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} internal fun read{{ canonical_type_name }}(buf: ByteBuffer): {{ inner_type_name }}? { if (buf.get().toInt() == 0) { return null @@ -396,12 +397,14 @@ internal fun read{{ canonical_type_name }}(buf: ByteBuffer): {{ inner_type_name return {{ "buf"|read_kt(inner_type) }} } +{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} internal fun lower{{ canonical_type_name }}(v: {{ inner_type_name }}?): RustBuffer.ByValue { return lowerIntoRustBuffer(v) { v, buf -> write{{ canonical_type_name }}(v, buf) } } +{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} internal fun write{{ canonical_type_name }}(v: {{ inner_type_name }}?, buf: RustBufferBuilder) { if (v == null) { buf.putByte(0) @@ -416,12 +419,14 @@ internal fun write{{ canonical_type_name }}(v: {{ inner_type_name }}?, buf: Rust // Helper functions for pasing values of type {{ typ|type_kt }} +{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} internal fun lift{{ canonical_type_name }}(rbuf: RustBuffer.ByValue): List<{{ inner_type_name }}> { return liftFromRustBuffer(rbuf) { buf -> read{{ canonical_type_name }}(buf) } } +{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} internal fun read{{ canonical_type_name }}(buf: ByteBuffer): List<{{ inner_type_name }}> { val len = buf.getInt() return List<{{ inner_type|type_kt }}>(len) { @@ -429,12 +434,14 @@ internal fun read{{ canonical_type_name }}(buf: ByteBuffer): List<{{ inner_type_ } } +{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} internal fun lower{{ canonical_type_name }}(v: List<{{ inner_type_name }}>): RustBuffer.ByValue { return lowerIntoRustBuffer(v) { v, buf -> write{{ canonical_type_name }}(v, buf) } } +{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} internal fun write{{ canonical_type_name }}(v: List<{{ inner_type_name }}>, buf: RustBufferBuilder) { buf.putInt(v.size) v.forEach { @@ -447,12 +454,14 @@ internal fun write{{ canonical_type_name }}(v: List<{{ inner_type_name }}>, buf: // Helper functions for pasing values of type {{ typ|type_kt }} +{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} internal fun lift{{ canonical_type_name }}(rbuf: RustBuffer.ByValue): Map { return liftFromRustBuffer(rbuf) { buf -> read{{ canonical_type_name }}(buf) } } +{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} internal fun read{{ canonical_type_name }}(buf: ByteBuffer): Map { // TODO: Once Kotlin's `buildMap` API is stabilized we should use it here. val items : MutableMap = mutableMapOf() @@ -465,12 +474,14 @@ internal fun read{{ canonical_type_name }}(buf: ByteBuffer): Map): RustBuffer.ByValue { return lowerIntoRustBuffer(m) { m, buf -> write{{ canonical_type_name }}(m, buf) } } +{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} internal fun write{{ canonical_type_name }}(v: Map, buf: RustBufferBuilder) { buf.putInt(v.size) // The parens on `(k, v)` here ensure we're calling the right method, diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt index 9971a5ce9f..4d0a70a152 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/TopLevelFunctionTemplate.kt @@ -1,6 +1,7 @@ {%- match func.return_type() -%} {%- when Some with (return_type) %} +{% call kt::unsigned_types_annotation(func) %} fun {{ func.name()|fn_name_kt }}({%- call kt::arg_list_decl(func) -%}): {{ return_type|type_kt }} { val _retval = {% call kt::to_ffi_call(func) %} return {{ "_retval"|lift_kt(return_type) }} @@ -8,6 +9,7 @@ fun {{ func.name()|fn_name_kt }}({%- call kt::arg_list_decl(func) -%}): {{ retur {% when None -%} +{% call kt::unsigned_types_annotation(func) %} fun {{ func.name()|fn_name_kt }}({% call kt::arg_list_decl(func) %}) = {% call kt::to_ffi_call(func) %} {% endmatch %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt b/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt index 7b0223ac29..cfa159d514 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt @@ -72,3 +72,8 @@ rustCall( {%- endfor %} {% if func.arguments().len() > 0 %},{% endif %} uniffi_out_err: Structure.ByReference {%- endmacro -%} + +// Add annotation if there are unsigned types +{%- macro unsigned_types_annotation(member) -%} +{% if member.contains_unsigned_types(ci) %}@ExperimentalUnsignedTypes{% endif %} +{%- endmacro -%} diff --git a/uniffi_bindgen/src/interface/enum_.rs b/uniffi_bindgen/src/interface/enum_.rs index 53418f13c0..4b230c91fe 100644 --- a/uniffi_bindgen/src/interface/enum_.rs +++ b/uniffi_bindgen/src/interface/enum_.rs @@ -113,6 +113,14 @@ impl Enum { // and its contained types could use a bit of a cleanup. ci.type_contains_object_references(&Type::Enum(self.name.clone())) } + + pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool { + self.variants().iter().any(|v| { + v.fields() + .iter() + .any(|f| ci.type_contains_unsigned_types(&f.type_)) + }) + } } // Note that we have two `APIConverter` impls here - one for the `enum` case diff --git a/uniffi_bindgen/src/interface/function.rs b/uniffi_bindgen/src/interface/function.rs index e843d5ac89..3da5a6d38a 100644 --- a/uniffi_bindgen/src/interface/function.rs +++ b/uniffi_bindgen/src/interface/function.rs @@ -90,6 +90,21 @@ impl Function { self.ffi_func.return_type = self.return_type.as_ref().map(|rt| rt.into()); Ok(()) } + + // Intentionally exactly the same as the Method version + pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool { + let check_return_type = { + match self.return_type() { + None => false, + Some(t) => ci.type_contains_unsigned_types(t), + } + }; + check_return_type + || self + .arguments() + .iter() + .any(|&arg| ci.type_contains_unsigned_types(&arg.type_())) + } } impl Hash for Function { diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index 9d97dd003e..b154ddcf5d 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -241,6 +241,29 @@ impl<'ci> ComponentInterface { } } + /// Check whether the given type contains any (possibly nested) unsigned types + pub fn type_contains_unsigned_types(&self, type_: &Type) -> bool { + match type_ { + Type::UInt8 | Type::UInt16 | Type::UInt32 | Type::UInt64 => true, + Type::Optional(t) | Type::Sequence(t) | Type::Map(t) => { + self.type_contains_unsigned_types(t) + } + Type::Object(t) => self + .get_object_definition(t) + .map(|obj| obj.contains_unsigned_types(&self)) + .unwrap_or(false), + Type::Record(name) => self + .get_record_definition(name) + .map(|rec| rec.contains_unsigned_types(&self)) + .unwrap_or(false), + Type::Enum(name) => self + .get_enum_definition(name) + .map(|e| e.contains_unsigned_types(&self)) + .unwrap_or(false), + _ => false, + } + } + /// Calculate a numeric checksum for this ComponentInterface. /// /// The checksum can be used to guard against accidentally using foreign-language bindings diff --git a/uniffi_bindgen/src/interface/object.rs b/uniffi_bindgen/src/interface/object.rs index 212c901fb9..35aee452c1 100644 --- a/uniffi_bindgen/src/interface/object.rs +++ b/uniffi_bindgen/src/interface/object.rs @@ -143,6 +143,17 @@ impl Object { } Ok(()) } + + // We need to check both methods and constructor + pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool { + self.methods() + .iter() + .any(|&meth| meth.contains_unsigned_types(&ci)) + || self + .constructors() + .iter() + .any(|&constructor| constructor.contains_unsigned_types(&ci)) + } } impl Hash for Object { @@ -246,6 +257,12 @@ impl Constructor { fn is_primary_constructor(&self) -> bool { self.name == "new" } + + pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool { + self.arguments() + .iter() + .any(|&arg| ci.type_contains_unsigned_types(&arg.type_())) + } } impl Hash for Constructor { @@ -354,6 +371,21 @@ impl Method { self.ffi_func.return_type = self.return_type.as_ref().map(Into::into); Ok(()) } + + // Intentionally exactly the same as the Function version + pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool { + let check_return_type = { + match self.return_type() { + None => false, + Some(t) => ci.type_contains_unsigned_types(t), + } + }; + check_return_type + || self + .arguments() + .iter() + .any(|&arg| ci.type_contains_unsigned_types(&arg.type_())) + } } impl Hash for Method { diff --git a/uniffi_bindgen/src/interface/record.rs b/uniffi_bindgen/src/interface/record.rs index d6a9b7fd03..c9652c99a1 100644 --- a/uniffi_bindgen/src/interface/record.rs +++ b/uniffi_bindgen/src/interface/record.rs @@ -75,6 +75,12 @@ impl Record { // and its contained types could use a bit of a cleanup. ci.type_contains_object_references(&Type::Record(self.name.clone())) } + + pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool { + self.fields() + .iter() + .any(|f| ci.type_contains_unsigned_types(&f.type_)) + } } impl APIConverter for weedle::DictionaryDefinition<'_> { From 1ba37734330e89a885e54780b67397027019b2cf Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Thu, 8 Jul 2021 16:12:58 -0400 Subject: [PATCH 20/45] Adding rubocop dependency to the README --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index 329e99e814..7aa76e992f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -41,7 +41,7 @@ If you want to try them out, you will need: * The [Swift command-line tools](https://swift.org/download/), particularly `swift`, `swiftc` and the `Foundation` package. * The [Ruby FFI](https://github.com/ffi/ffi#installation) - * `gem install ffi test-unit` + * `gem install ffi test-unit rubocop` We publish a [docker image](https://hub.docker.com/r/rfkelly/uniffi-ci) that has all of this dependencies pre-installed, if you want to get up and running quickly. From 7524995922a6518fd864697e8c513b03f1eada93 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Thu, 8 Jul 2021 16:10:49 -0400 Subject: [PATCH 21/45] Added uniffi module to make calls into rust This will replace the ffi-support::call_with_result() and ffi-support::call_with_output() functions. One missing piece of functionality is returning error messages for panics. I'm intentially not implementing that because I want to keep all of the code that's not wrapped by `catch_unwind()` really simple. The ffi-support code to extract the error message does things like call `to_string()`, which could panic itself if the user is OOM and that scares me. The panic hook installed by ffi-support logs information about the panic -- hopefully that's enough. Updated the CHANGELOG to indicate breaking changes. --- CHANGELOG.md | 12 +- uniffi/src/ffi/mod.rs | 2 + uniffi/src/ffi/rustcalls.rs | 251 ++++++++++++++++++++++++++++++++++++ uniffi/src/lib.rs | 2 +- 4 files changed, 261 insertions(+), 6 deletions(-) create mode 100644 uniffi/src/ffi/rustcalls.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index d9786f96e7..5e1d39f93f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,17 +12,19 @@ [All changes in [[UnreleasedVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.12.0...HEAD). -- Kotlin exceptions names will now replace a trailing "Error" with "Exception" - rather than appending the string (FooException instead of FooErrorException) - -### What's Changed -- Both python and ruby backends now handle U16 correctly. +### ⚠️ Breaking Changes ⚠️ - Python timestamps will now be in UTC and timezone-aware rather than naive. - Replaced `lower_into_buffer()` and `try_lift_from_buffer()` with the `RustBufferViaFfi` trait. If you use those functions in your custom ViaFfi implementation then you'll need to update the code. Check out the `Option<>` implementation in uniffi/src/lib.rs for an example. +- Kotlin exceptions names will now replace a trailing "Error" with "Exception" + rather than appending the string (FooException instead of FooErrorException) + +### What's Changed + +- Both python and ruby backends now handle U16 correctly. ## v0.12.0 (2021-06-14) diff --git a/uniffi/src/ffi/mod.rs b/uniffi/src/ffi/mod.rs index 03c12fabcb..adf8cb4eee 100644 --- a/uniffi/src/ffi/mod.rs +++ b/uniffi/src/ffi/mod.rs @@ -5,7 +5,9 @@ pub mod foreignbytes; pub mod foreigncallbacks; pub mod rustbuffer; +pub mod rustcalls; pub use foreignbytes::*; pub use foreigncallbacks::*; pub use rustbuffer::*; +pub use rustcalls::*; diff --git a/uniffi/src/ffi/rustcalls.rs b/uniffi/src/ffi/rustcalls.rs new file mode 100644 index 0000000000..9bd030e1de --- /dev/null +++ b/uniffi/src/ffi/rustcalls.rs @@ -0,0 +1,251 @@ +/* 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 crate::{RustBuffer, RustBufferViaFfi, ViaFfi}; +use anyhow::Result; +use ffi_support::IntoFfi; +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_SUCESS` (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, + // 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 in. In + // particular, we don't want to try to free any data the consumer has allocated. +} + +#[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: RustBufferViaFfi {} + +// Generalized rust call handling function +// callback should map errors to a RustBuffer that contains them +fn make_call(out_status: &mut RustCallStatus, callback: F) -> R::Value +where + F: panic::UnwindSafe + FnOnce() -> Result, + R: IntoFfi, +{ + let result = panic::catch_unwind(|| { + // We should call: + // + // init_panic_handling_once(); + // + // This is called in ffi-support::call_with_result_impl(). The current plan is to make it + // `pub` in ffi-support and call it from here. + callback().map(|v| v.into_ffi_value()) + }); + 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 { + 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::() { + 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 { + out_status.error_buf.as_mut_ptr().write(buf); + } + } + // Ignore the error case. We've done all that we can at this point + R::ffi_default() + } + } +} + +/// Wrap a rust function call and return the result directly +/// +/// - 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(out_status: &mut RustCallStatus, callback: F) -> R::Value +where + F: panic::UnwindSafe + FnOnce() -> R, + R: IntoFfi, +{ + make_call(out_status, || Ok(callback())) +} + +/// Wrap a rust function call that returns a `Result<>` +/// +/// - 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(out_status: &mut RustCallStatus, callback: F) -> R::Value +where + F: panic::UnwindSafe + FnOnce() -> Result, + E: FfiError, + R: IntoFfi, +{ + make_call(out_status, || callback().map_err(|e| e.lower())) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{RustBufferViaFfi, ViaFfi}; + + 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 RustBufferViaFfi to simplify lifting TestError out of RustBuffer to check it + impl RustBufferViaFfi for TestError { + fn write(&self, buf: &mut Vec) { + self.0.write(buf); + } + + fn try_read(buf: &mut &[u8]) -> Result { + String::try_read(buf).map(TestError) + } + } + + impl FfiError for TestError {} + + fn function_with_result(a: u8) -> Result { + 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)); + assert_eq!(status.code, CALL_SUCCESS); + assert_eq!(return_value, 100); + + call_with_result(&mut status, || function_with_result(1)); + 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)); + assert_eq!(status.code, CALL_PANIC); + unsafe { + assert_eq!( + String::try_lift(status.error_buf.assume_init()).unwrap(), + "Unexpected value: 2" + ); + } + } +} diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index 89a0e23c41..f1705b2724 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -128,7 +128,7 @@ macro_rules! assert_compatible_version { /// 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 `InfoFfi` trait from `ffi_support`, but local to this crate +/// (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). /// From bdc9551ceaca7ec0b556820e14edc8e36f2dd272 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Fri, 9 Jul 2021 10:39:59 -0400 Subject: [PATCH 22/45] Updating the bindings/scaffolding to use the new rust call API - Updated the coverall tests to test it - Removed the ffi_string_free function, it was only used for the old error handling - Ruby: Changed the top-level error from being a class to a module. This seems more correct to me, since you never instantiate the top-level error directly. - Kotlin: Updated exception_name_kt to input a string rather than an Error, this makes it work with the throws() method (and also matches the other name formatting methods). - Swift: Made UniffiInternalError public -- mostly to allow them to be tested. - Updated the manual doc --- CHANGELOG.md | 2 +- docs/manual/src/udl/errors.md | 9 + fixtures/coverall/src/coverall.udl | 9 + fixtures/coverall/src/lib.rs | 23 +++ .../coverall/tests/bindings/test_coverall.kts | 46 ++++- .../coverall/tests/bindings/test_coverall.py | 22 +- .../coverall/tests/bindings/test_coverall.rb | 32 ++- .../tests/bindings/test_coverall.swift | 48 ++++- .../src/bindings/gecko_js/gen_gecko_js.rs | 14 +- .../templates/FFIDeclarationsTemplate.h | 6 +- .../gecko_js/templates/InterfaceTemplate.cpp | 6 +- .../gecko_js/templates/RustBufferHelper.h | 48 ++--- .../bindings/gecko_js/templates/macros.cpp | 29 +-- .../src/bindings/kotlin/gen_kotlin.rs | 35 ++-- .../templates/CallbackInterfaceTemplate.kt | 10 +- .../kotlin/templates/ErrorTemplate.kt | 193 +++++++----------- .../kotlin/templates/ObjectTemplate.kt | 4 +- .../kotlin/templates/RustBufferTemplate.kt | 13 +- .../src/bindings/kotlin/templates/macros.kt | 25 +-- .../src/bindings/python/gen_python.rs | 2 - .../python/templates/ErrorTemplate.py | 114 +++++++---- .../python/templates/ObjectTemplate.py | 6 +- .../python/templates/RustBufferStream.py | 29 +++ .../python/templates/RustBufferTemplate.py | 6 +- .../src/bindings/python/templates/macros.py | 11 +- uniffi_bindgen/src/bindings/ruby/gen_ruby.rs | 2 - .../bindings/ruby/templates/ErrorTemplate.rb | 112 +++++++--- .../bindings/ruby/templates/ObjectTemplate.rb | 3 +- .../ruby/templates/RustBufferBuilder.rb | 2 + .../ruby/templates/RustBufferStream.rb | 36 ++++ .../ruby/templates/RustBufferTemplate.rb | 8 +- .../src/bindings/ruby/templates/macros.rb | 12 +- .../src/bindings/swift/gen_swift.rs | 2 - .../swift/templates/ErrorTemplate.swift | 176 ++++++++-------- .../swift/templates/ObjectTemplate.swift | 4 +- .../swift/templates/RustBufferHelper.swift | 8 +- .../swift/templates/RustBufferTemplate.swift | 8 +- .../templates/Template-Bridging-Header.h | 9 +- .../src/bindings/swift/templates/macros.swift | 26 +-- uniffi_bindgen/src/interface/attributes.rs | 16 +- uniffi_bindgen/src/interface/error.rs | 155 +++++++++++--- uniffi_bindgen/src/interface/ffi.rs | 8 - uniffi_bindgen/src/interface/mod.rs | 27 +-- uniffi_bindgen/src/interface/types/finder.rs | 2 + uniffi_bindgen/src/interface/types/mod.rs | 3 +- uniffi_bindgen/src/scaffolding/mod.rs | 2 - .../scaffolding/templates/ErrorTemplate.rs | 56 +++-- .../scaffolding/templates/ObjectTemplate.rs | 10 +- .../src/scaffolding/templates/RustBuffer.rs | 33 +-- .../src/scaffolding/templates/macros.rs | 16 +- .../templates/scaffolding_template.rs | 6 +- 51 files changed, 920 insertions(+), 564 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e1d39f93f..6e6251eef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,6 @@ [All changes in [[UnreleasedVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.12.0...HEAD). - ### ⚠️ Breaking Changes ⚠️ - Python timestamps will now be in UTC and timezone-aware rather than naive. - Replaced `lower_into_buffer()` and `try_lift_from_buffer()` with the @@ -25,6 +24,7 @@ ### What's Changed - Both python and ruby backends now handle U16 correctly. +- Errors with nested fields are now supported. ## v0.12.0 (2021-06-14) diff --git a/docs/manual/src/udl/errors.md b/docs/manual/src/udl/errors.md index c38dfc99f5..14c194b65f 100644 --- a/docs/manual/src/udl/errors.md +++ b/docs/manual/src/udl/errors.md @@ -33,3 +33,12 @@ namespace arithmetic { ``` On the other side (Kotlin, Swift etc.), a proper exception will be thrown if `Result::is_err()` is `true`. + +If you want to expose the assocated data as fields on the exception, use this syntax: + +``` +[Enum] +interface ArithmeticError { + IntegerOverflow(u64 a, u64 b); +}; +``` diff --git a/fixtures/coverall/src/coverall.udl b/fixtures/coverall/src/coverall.udl index c7229ddf68..b2c59d7ef4 100644 --- a/fixtures/coverall/src/coverall.udl +++ b/fixtures/coverall/src/coverall.udl @@ -38,6 +38,12 @@ enum CoverallError { "TooManyHoles" }; +[Error] +interface ComplexError { + OsError(i16 code, i16 extended_code); + PermissionDenied(string reason); +}; + interface Coveralls { constructor(string name); @@ -54,6 +60,9 @@ interface Coveralls { [Throws=CoverallError] boolean maybe_throw(boolean should_throw); + [Throws=ComplexError] + boolean maybe_throw_complex(i8 input); + void panic(string message); [Throws=CoverallError] diff --git a/fixtures/coverall/src/lib.rs b/fixtures/coverall/src/lib.rs index ea20281bbd..ba705e343f 100644 --- a/fixtures/coverall/src/lib.rs +++ b/fixtures/coverall/src/lib.rs @@ -17,6 +17,14 @@ enum CoverallError { TooManyHoles, } +#[derive(Debug, thiserror::Error)] +enum ComplexError { + #[error("OsError: {code} ({extended_code})")] + OsError { code: i16, extended_code: i16 }, + #[error("PermissionDenied: {reason}")] + PermissionDenied { reason: String }, +} + #[derive(Debug, Clone)] pub struct SimpleDict { text: String, @@ -99,6 +107,7 @@ fn get_num_alive() -> u64 { } type Result = std::result::Result; +type ComplexResult = std::result::Result; #[derive(Debug)] pub struct Coveralls { @@ -145,6 +154,20 @@ impl Coveralls { } } + fn maybe_throw_complex(&self, input: i8) -> ComplexResult { + match input { + 0 => Ok(true), + 1 => Err(ComplexError::OsError { + code: 10, + extended_code: 20, + }), + 2 => Err(ComplexError::PermissionDenied { + reason: "Forbidden".to_owned(), + }), + _ => panic!("Invalid input"), + } + } + fn panic(&self, message: String) { panic!("{}", message); } diff --git a/fixtures/coverall/tests/bindings/test_coverall.kts b/fixtures/coverall/tests/bindings/test_coverall.kts index 683e9f7e81..a149b4be44 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.kts +++ b/fixtures/coverall/tests/bindings/test_coverall.kts @@ -58,13 +58,13 @@ Coveralls("test_arcs").use { coveralls -> } try { coveralls.takeOtherFallible() - throw RuntimeException("Should have thrown a IntegerOverflow exception!") + throw RuntimeException("Should have thrown an IntegerOverflow exception!") } catch (e: CoverallException.TooManyHoles) { // It's okay! } try { coveralls.takeOtherPanic("expected panic: with an arc!") - throw RuntimeException("Should have thrown a IntegerOverflow exception!") + throw RuntimeException("Should have thrown an InternalException!") } catch (e: InternalException) { // No problemo! } @@ -103,6 +103,48 @@ Coveralls("test_return_objects").use { coveralls -> // Destroying `coveralls` will kill both. assert(getNumAlive() == 0UL); +Coveralls("test_simple_errors").use { coveralls -> + try { + coveralls.maybeThrow(true) + throw RuntimeException("Expected method to throw exception") + } catch(e: CoverallException.TooManyHoles) { + // Expected result + } + + try { + coveralls.panic("oops") + throw RuntimeException("Expected method to throw exception") + } catch(e: InternalException) { + // Expected result + assert(e.message == "oops") + } +} + +Coveralls("test_complex_errors").use { coveralls -> + assert(coveralls.maybeThrowComplex(0) == true) + + try { + coveralls.maybeThrowComplex(1) + throw RuntimeException("Expected method to throw exception") + } catch(e: ComplexException.OsException) { + assert(e.code == 10.toShort()) + assert(e.extendedCode == 20.toShort()) + } + + try { + coveralls.maybeThrowComplex(2) + throw RuntimeException("Expected method to throw exception") + } catch(e: ComplexException.PermissionDenied) { + assert(e.reason == "Forbidden") + } + + try { + coveralls.maybeThrowComplex(3) + throw RuntimeException("Expected method to throw exception") + } catch(e: InternalException) { + // Expected result + } +} // This tests that the UniFFI-generated scaffolding doesn't introduce any unexpected locking. // We have one thread busy-wait for a some period of time, while a second thread repeatedly diff --git a/fixtures/coverall/tests/bindings/test_coverall.py b/fixtures/coverall/tests/bindings/test_coverall.py index a5238729f3..50944d7e44 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.py +++ b/fixtures/coverall/tests/bindings/test_coverall.py @@ -85,7 +85,7 @@ def test_constructors(self): self.assertEqual(get_num_alive(), 0) - def test_errors(self): + def test_simple_errors(self): coveralls = Coveralls("test_errors") self.assertEqual(coveralls.get_name(), "test_errors") @@ -95,6 +95,26 @@ def test_errors(self): with self.assertRaisesRegex(InternalError, "expected panic: oh no"): coveralls.panic("expected panic: oh no") + def test_complex_errors(self): + coveralls = Coveralls("test_complex_errors") + + # Test success + self.assertEqual(True, coveralls.maybe_throw_complex(0)) + + # Test errors + with self.assertRaises(ComplexError.OsError) as cm: + coveralls.maybe_throw_complex(1) + self.assertEqual(cm.exception.code, 10) + self.assertEqual(cm.exception.extended_code, 20) + + with self.assertRaises(ComplexError.PermissionDenied) as cm: + coveralls.maybe_throw_complex(2) + self.assertEqual(cm.exception.reason, "Forbidden") + + # Test panics, which should cause InternalError to be raised + with self.assertRaises(InternalError) as cm: + coveralls.maybe_throw_complex(3) + def test_self_by_arc(self): coveralls = Coveralls("test_self_by_arc") # One reference is held by the handlemap, and one by the `Arc` method receiver. diff --git a/fixtures/coverall/tests/bindings/test_coverall.rb b/fixtures/coverall/tests/bindings/test_coverall.rb index bd85f91c11..c970f0ce3b 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.rb +++ b/fixtures/coverall/tests/bindings/test_coverall.rb @@ -94,9 +94,9 @@ def test_constructors assert_equal 2, Coverall.get_num_alive end - def test_errors - coveralls = Coverall::Coveralls.new 'test_errors' - assert_equal coveralls.get_name, 'test_errors' + def test_simple_errors + coveralls = Coverall::Coveralls.new 'test_simple_errors' + assert_equal coveralls.get_name, 'test_simple_errors' assert_raise Coverall::CoverallError::TooManyHoles do coveralls.maybe_throw true @@ -111,6 +111,32 @@ def test_errors end end + def test_complex_errors + coveralls = Coverall::Coveralls.new 'test_complex_errors' + assert_equal coveralls.maybe_throw_complex(0), true + + begin + coveralls.maybe_throw_complex(1) + rescue Coverall::ComplexError::OsError => err + assert_equal err.code, 10 + assert_equal err.extended_code, 20 + else + raise 'should have thrown' + end + + begin + coveralls.maybe_throw_complex(2) + rescue Coverall::ComplexError::PermissionDenied => err + assert_equal err.reason, "Forbidden" + else + raise 'should have thrown' + end + + assert_raise Coverall::InternalError do + coveralls.maybe_throw_complex(3) + end + end + def test_self_by_arc coveralls = Coverall::Coveralls.new 'test_self_by_arc' diff --git a/fixtures/coverall/tests/bindings/test_coverall.swift b/fixtures/coverall/tests/bindings/test_coverall.swift index 019faf322d..5ad16360bc 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.swift +++ b/fixtures/coverall/tests/bindings/test_coverall.swift @@ -74,10 +74,56 @@ do { coveralls.takeOther(other: nil); assert(coveralls.strongCount() == 2); } + +// Test simple errors +do { + let coveralls = Coveralls(name: "test_simple_errors") + + assert(try! coveralls.maybeThrow(shouldThrow: false) == true) + + do { + let _ = try coveralls.maybeThrow(shouldThrow: true) + fatalError("Should have thrown") + } catch CoverallError.TooManyHoles { + // It's okay! + } + + // Note: Can't test coveralls.panic() because rust panics trigger a fatal error in swift +} + +// Test complex errors +do { + let coveralls = Coveralls(name: "test_complex_errors") + + assert(try! coveralls.maybeThrowComplex(input: 0) == true) + + do { + let _ = try coveralls.maybeThrowComplex(input: 1) + fatalError("should have thrown") + } catch ComplexError.OsError(let code, let extendedCode) { + assert(code == 10) + assert(extendedCode == 20) + } + + do { + let _ = try coveralls.maybeThrowComplex(input: 2) + fatalError("should have thrown") + } catch ComplexError.PermissionDenied(let reason) { + assert(reason == "Forbidden") + } + + do { + let _ = try coveralls.maybeThrowComplex(input: 3) + fatalError("should have thrown") + } catch UniffiInternalError.rustPanic(let message) { + assert(message == "Invalid input") + } + +} + // Swift GC is deterministic, `coveralls` is freed when it goes out of scope. assert(getNumAlive() == 0); - // Test return objects do { let coveralls = Coveralls(name: "test_return_objects") diff --git a/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs b/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs index 29ee1667ac..5e68983d33 100644 --- a/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs +++ b/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs @@ -103,13 +103,13 @@ impl<'config, 'ci> Context<'config, 'ci> { format!("{}_ForeignBytes", self.ci.ffi_namespace()) } - /// Returns the `RustError` type name. + /// Returns the `RustCallStatus` type name. /// - /// A `RustError` is a Plain Old Data struct that holds an error code and - /// a message string. See the docs for `ffi_rustbuffer_type` about why this - /// type name must be unique for each component. - pub fn ffi_rusterror_type(&self) -> String { - format!("{}_RustError", self.ci.ffi_namespace()) + /// A `RustCallStatus` is a Plain Old Data struct that holds an call status code and + /// `RustBuffer`. See the docs for `ffi_rustbuffer_type` about why this type name must be + /// unique for each component. + pub fn ffi_rustcallstatus_type(&self) -> String { + format!("{}_RustCallStatus", self.ci.ffi_namespace()) } /// Returns the name to use for the `detail` C++ namespace, which contains @@ -331,10 +331,8 @@ mod filters { FFIType::UInt64 => "uint64_t".into(), FFIType::Float32 => "float".into(), FFIType::Float64 => "double".into(), - FFIType::RustCString => "const char*".into(), FFIType::RustArcPtr => unimplemented!("object pointers are not implemented"), FFIType::RustBuffer => context.ffi_rustbuffer_type(), - FFIType::RustError => context.ffi_rusterror_type(), FFIType::ForeignBytes => context.ffi_foreignbytes_type(), FFIType::ForeignCallback => unimplemented!("Callback interfaces are not implemented"), }) diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/FFIDeclarationsTemplate.h b/uniffi_bindgen/src/bindings/gecko_js/templates/FFIDeclarationsTemplate.h index 165e1630a6..02753d60ba 100644 --- a/uniffi_bindgen/src/bindings/gecko_js/templates/FFIDeclarationsTemplate.h +++ b/uniffi_bindgen/src/bindings/gecko_js/templates/FFIDeclarationsTemplate.h @@ -18,9 +18,9 @@ struct {{ context.ffi_foreignbytes_type() }} { int32_t mPadding2; }; -struct {{ context.ffi_rusterror_type() }} { +struct {{ context.ffi_rustcallstatus_type() }} { int32_t mCode; - char* mMessage; + {{ context.ffi_rustbuffer_type() }} mErrorBuf; }; {% for func in ci.iter_ffi_function_definitions() -%} @@ -35,7 +35,7 @@ void {{ arg.type_()|type_ffi(context) }} {{ arg.name() -}}{%- if loop.last -%}{%- else -%},{%- endif -%} {%- endfor %} {%- if func.arguments().len() > 0 %},{% endif %} - {{ context.ffi_rusterror_type() }}* uniffi_out_err + {{ context.ffi_rustcallstatus_type() }}* uniffi_out_status ); {% endfor -%} diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceTemplate.cpp b/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceTemplate.cpp index ec3aa0b76a..50c8c61fcc 100644 --- a/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceTemplate.cpp +++ b/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceTemplate.cpp @@ -27,9 +27,9 @@ NS_INTERFACE_MAP_END ) : mGlobal(aGlobal), mHandle(aHandle) {} {{ obj.name()|class_name_cpp(context) }}::~{{ obj.name()|class_name_cpp(context) }}() { - {{ context.ffi_rusterror_type() }} err = {0, nullptr}; - {{ obj.ffi_object_free().name() }}(mHandle, &err); - MOZ_ASSERT(!err.mCode); + {{ context.ffi_rustcallstatus_type() }} status = {0, { 0, 0, nullptr, 0 }}; + {{ obj.ffi_object_free().name() }}(mHandle, &status); + MOZ_ASSERT(!status.mCode); } JSObject* {{ obj.name()|class_name_cpp(context) }}::WrapObject( diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/RustBufferHelper.h b/uniffi_bindgen/src/bindings/gecko_js/templates/RustBufferHelper.h index 99c9bb4b8d..123001f76e 100644 --- a/uniffi_bindgen/src/bindings/gecko_js/templates/RustBufferHelper.h +++ b/uniffi_bindgen/src/bindings/gecko_js/templates/RustBufferHelper.h @@ -124,9 +124,9 @@ class MOZ_STACK_CLASS Reader final { class MOZ_STACK_CLASS Writer final { public: Writer() { - {{ context.ffi_rusterror_type() }} err = {0, nullptr}; - mBuffer = {{ ci.ffi_rustbuffer_alloc().name() }}(0, &err); - if (err.mCode) { + {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; + mBuffer = {{ ci.ffi_rustbuffer_alloc().name() }}(0, &status); + if (status.mCode) { MOZ_ASSERT(false, "Failed to allocate empty Rust buffer"); } } @@ -248,10 +248,10 @@ class MOZ_STACK_CLASS Writer final { if (aBytes >= static_cast(std::numeric_limits::max())) { NS_ABORT_OOM(aBytes); } - {{ context.ffi_rusterror_type() }} err = {0, nullptr}; + {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; {{ context.ffi_rustbuffer_type() }} newBuffer = {{ ci.ffi_rustbuffer_reserve().name() }}( - mBuffer, static_cast(aBytes), &err); - if (err.mCode) { + mBuffer, static_cast(aBytes), &status); + if (status.mCode) { NS_ABORT_OOM(aBytes); } mBuffer = newBuffer; @@ -391,9 +391,9 @@ struct ViaFfi { nsACString& aLifted) { if (aLowered.mData) { aLifted.Append(AsChars(Span(aLowered.mData, aLowered.mLen))); - {{ context.ffi_rusterror_type() }} err = {0, nullptr}; - {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &err); - if (err.mCode) { + {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; + {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &status); + if (status.mCode) { MOZ_ASSERT(false, "Failed to lift `nsACString` from Rust buffer"); return false; } @@ -405,12 +405,12 @@ struct ViaFfi { MOZ_RELEASE_ASSERT( aLifted.Length() <= static_cast(std::numeric_limits::max())); - {{ context.ffi_rusterror_type() }} err = {0, nullptr}; + {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; {{ context.ffi_foreignbytes_type() }} bytes = { static_cast(aLifted.Length()), reinterpret_cast(aLifted.BeginReading())}; - {{ context.ffi_rustbuffer_type() }} lowered = {{ ci.ffi_rustbuffer_from_bytes().name() }}(bytes, &err); - if (err.mCode) { + {{ context.ffi_rustbuffer_type() }} lowered = {{ ci.ffi_rustbuffer_from_bytes().name() }}(bytes, &status); + if (status.mCode) { MOZ_ASSERT(false, "Failed to lower `nsACString` into Rust string"); } return lowered; @@ -450,9 +450,9 @@ struct ViaFfi { nsAString& aLifted) { if (aLowered.mData) { CopyUTF8toUTF16(AsChars(Span(aLowered.mData, aLowered.mLen)), aLifted); - {{ context.ffi_rusterror_type() }} err = {0, nullptr}; - {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &err); - if (err.mCode) { + {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; + {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &status); + if (status.mCode) { MOZ_ASSERT(false, "Failed to lift `nsAString` from Rust buffer"); return false; } @@ -467,10 +467,10 @@ struct ViaFfi { maxSize.value() <= static_cast(std::numeric_limits::max())); - {{ context.ffi_rusterror_type() }} err = {0, nullptr}; + {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; {{ context.ffi_rustbuffer_type() }} lowered = {{ ci.ffi_rustbuffer_alloc().name() }}( - static_cast(maxSize.value()), &err); - if (err.mCode) { + static_cast(maxSize.value()), &status); + if (status.mCode) { MOZ_ASSERT(false, "Failed to lower `nsAString` into Rust string"); } @@ -659,9 +659,9 @@ struct ViaFfi { MOZ_ASSERT(false); return false; } - {{ context.ffi_rusterror_type() }} err = {0, nullptr}; - {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &err); - if (err.mCode) { + {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; + {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &status); + if (status.mCode) { MOZ_ASSERT(false, "Failed to free Rust buffer after lifting contents"); return false; } @@ -760,9 +760,9 @@ struct ViaFfi { MOZ_ASSERT(false); return false; } - {{ context.ffi_rusterror_type() }} err = {0, nullptr}; - {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &err); - if (err.mCode) { + {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; + {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &status); + if (status.mCode) { MOZ_ASSERT(false, "Failed to free Rust buffer after lifting contents"); return false; } diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/macros.cpp b/uniffi_bindgen/src/bindings/gecko_js/templates/macros.cpp index 22989fd162..b1b6a69e6d 100644 --- a/uniffi_bindgen/src/bindings/gecko_js/templates/macros.cpp +++ b/uniffi_bindgen/src/bindings/gecko_js/templates/macros.cpp @@ -1,12 +1,12 @@ {# /* Calls an FFI function. */ #} {%- macro to_ffi_call(context, func) -%} - {%- call to_ffi_call_head(context, func, "err", "loweredRetVal_") -%} - {%- call _to_ffi_call_tail(context, func, "err", "loweredRetVal_") -%} + {%- call to_ffi_call_head(context, func, "status", "loweredRetVal_") -%} + {%- call _to_ffi_call_tail(context, func, "status", "loweredRetVal_") -%} {%- endmacro -%} {# /* Calls an FFI function with an initial argument. */ #} {%- macro to_ffi_call_with_prefix(context, prefix, func) %} - {{ context.ffi_rusterror_type() }} err = {0, nullptr}; + {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0} }; {% match func.ffi_func().return_type() %}{% when Some with (type_) %}const {{ type_|type_ffi(context) }} loweredRetVal_ ={% else %}{% endmatch %}{{ func.ffi_func().name() }}( {{ prefix }} {%- let args = func.arguments() -%} @@ -14,33 +14,38 @@ {%- for arg in args %} {{ arg.webidl_type()|lower_cpp(arg.name(), context) }}{%- if !loop.last %},{% endif -%} {%- endfor %} - , &err + , &status ); - {%- call _to_ffi_call_tail(context, func, "err", "loweredRetVal_") -%} + {%- call _to_ffi_call_tail(context, func, "status", "loweredRetVal_") -%} {%- endmacro -%} {# /* Calls an FFI function without handling errors or lifting the return value. Used in the implementation of `to_ffi_call`, and for constructors. */ #} -{%- macro to_ffi_call_head(context, func, error, result) %} - {{ context.ffi_rusterror_type() }} {{ error }} = {0, nullptr}; +{%- macro to_ffi_call_head(context, func, status, result) %} + {{ context.ffi_rustcallstatus_type() }} {{ status }} = {0, {0, 0, nullptr, 0}}; {% match func.ffi_func().return_type() %}{% when Some with (type_) %}const {{ type_|type_ffi(context) }} {{ result }} ={% else %}{% endmatch %}{{ func.ffi_func().name() }}( {%- let args = func.arguments() -%} {%- for arg in args %} {{ arg.webidl_type()|lower_cpp(arg.name(), context) }}{%- if !loop.last %},{% endif -%} {%- endfor %} - {% if !args.is_empty() %}, {% endif %}&{{ error }} + {% if !args.is_empty() %}, {% endif %}&{{ status }} ); {%- endmacro -%} {# /* Handles errors and lifts the return value from an FFI function. */ #} -{%- macro _to_ffi_call_tail(context, func, err, result) %} - if ({{ err }}.mCode) { +{%- macro _to_ffi_call_tail(context, func, status, result) %} + if ({{ status }}.mCode) { {%- match func.cpp_throw_by() %} {%- when ThrowBy::ErrorResult with (rv) %} {# /* TODO: Improve error throwing. See https://github.com/mozilla/uniffi-rs/issues/295 - for details. */ -#} - {{ rv }}.ThrowOperationError(nsDependentCString({{ err }}.mMessage)); + for details. */ + // More TODOs: + // - STATUS_ERROR: Lift the error from the rustbuffer and convert it into some kind of gecko error + // - STATUS_PANIC: Try to lift the message from the rustbuffer and use it for the OperationError below + -#} + + {{ rv }}.ThrowOperationError(nsDependentCString("UniffiError")); {%- when ThrowBy::Assert %} MOZ_ASSERT(false); {%- endmatch %} diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin.rs index e8cef3f5ae..ce0ebacc1c 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin.rs @@ -113,10 +113,8 @@ mod filters { FFIType::Int64 | FFIType::UInt64 => "Long".to_string(), FFIType::Float32 => "Float".to_string(), FFIType::Float64 => "Double".to_string(), - FFIType::RustCString => "Pointer".to_string(), FFIType::RustArcPtr => "Pointer".to_string(), FFIType::RustBuffer => "RustBuffer.ByValue".to_string(), - FFIType::RustError => "RustError".to_string(), FFIType::ForeignBytes => "ForeignBytes.ByValue".to_string(), FFIType::ForeignCallback => "ForeignCallback".to_string(), }) @@ -185,6 +183,23 @@ mod filters { Ok(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". + pub fn exception_name_kt(nm: &dyn fmt::Display) -> Result { + let name = nm.to_string(); + match name.strip_suffix("Error") { + None => Ok(name), + Some(stripped) => { + let mut kt_exc_name = stripped.to_owned(); + kt_exc_name.push_str("Exception"); + Ok(kt_exc_name) + } + } + } + /// Get a Kotlin expression for lowering a value into something we can pass over the FFI. /// /// Where possible, this delegates to a `lower()` method on the type itself, but special @@ -280,20 +295,4 @@ mod filters { _ => format!("{}.read({})", type_kt(type_)?, nm), }) } - - /// Generate a Kotlin 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". - pub fn exception_name_kt(error: &Error) -> Result { - match error.name().strip_suffix("Error") { - None => Ok(error.name().to_owned()), - Some(stripped) => { - let mut kt_exc_name = stripped.to_owned(); - kt_exc_name.push_str("Exception"); - Ok(kt_exc_name) - } - } - } } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt index 203327f8c1..842d608d8b 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt @@ -24,7 +24,7 @@ internal class {{ callback_interface_impl }} : ForeignCallback { {{ loop.index }} -> this.{{ method_name }}(cb, args) {% 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. + // ever be used. Once we can catch errors, we should return an InternalException. // https://github.com/mozilla/uniffi-rs/issues/351 else -> RustBuffer.ByValue() } @@ -38,7 +38,7 @@ internal class {{ callback_interface_impl }} : ForeignCallback { {#- Unpacking args from the RustBuffer #} {%- if meth.arguments().len() != 0 -%} {#- Calling the concrete callback object #} - val buf = args.asByteBuffer() ?: throw InternalError("No ByteBuffer in RustBuffer; this is a Uniffi bug") + val buf = args.asByteBuffer() ?: throw InternalException("No ByteBuffer in RustBuffer; this is a Uniffi bug") kotlinCallbackInterface.{{ meth.name()|fn_name_kt }}( {% for arg in meth.arguments() -%} {{ "buf"|read_kt(arg.type_()) }} @@ -73,8 +73,8 @@ internal object {{ callback_internals }}: CallbackInternals<{{ type_name }}>( foreignCallback = {{ callback_interface_impl }}() ) { override fun register(lib: _UniFFILib) { - rustCall(InternalError.ByReference()) { err -> - lib.{{ cbi.ffi_init_callback().name() }}(this.foreignCallback, err) + rustCall() { status -> + lib.{{ cbi.ffi_init_callback().name() }}(this.foreignCallback, status) } } -} \ No newline at end of file +} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt index 34f3c3dd0d..dfbf1bd9f8 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt @@ -1,149 +1,102 @@ -{#- -// Here we define error conversions from native references to Kotlin exceptions. -// This is done by defining a RustErrorReference interface, where any implementers of the interface -// can be converted into a native reference for an error to flow across the FFI. - -// A generic RustError is definied as a super class for other errors. This includes some common behaviour -// across the errors, can also serve as an error in case the cause of the error is unknown --#} - -interface RustErrorReference : Structure.ByReference { - fun isFailure(): Boolean - fun intoException(): E - fun ensureConsumed() - fun getMessage(): String? - fun consumeErrorMessage(): String -} - -@Structure.FieldOrder("code", "message") -internal open class RustError : Structure() { - open class ByReference: RustError(), RustErrorReference - +@Structure.FieldOrder("code", "error_buf") +internal open class RustCallStatus : Structure() { @JvmField var code: Int = 0 - @JvmField var message: Pointer? = null + @JvmField var error_buf: RustBuffer.ByValue = RustBuffer.ByValue() - /** - * Does this represent success? - */ fun isSuccess(): Boolean { return code == 0 } - /** - * Does this represent failure? - */ - fun isFailure(): Boolean { - return code != 0 - } - - @Synchronized - fun ensureConsumed() { - if (this.message != null) { - rustCall(InternalError.ByReference()) { err -> - _UniFFILib.INSTANCE.{{ ci.ffi_string_free().name() }}(this.message!!, err) - } - this.message = null - } - } - - /** - * Get the error message or null if there is none. - */ - fun getMessage(): String? { - return this.message?.getString(0, "utf8") - } - - /** - * Get and consume the error message, or null if there is none. - */ - @Synchronized - fun consumeErrorMessage(): String { - val result = this.getMessage() - if (this.message != null) { - this.ensureConsumed() - } - if (result == null) { - throw NullPointerException("consumeErrorMessage called with null message!") - } - return result + fun isError(): Boolean { + return code == 1 } - @Suppress("ReturnCount", "TooGenericExceptionThrown") - open fun intoException(): E { - if (!isFailure()) { - // It's probably a bad idea to throw here! We're probably leaking something if this is - // ever hit! (But we shouldn't ever hit it?) - throw RuntimeException("[Bug] intoException called on non-failure!") - } - this.consumeErrorMessage() - throw RuntimeException("Generic errors are not implemented yet") + fun isPanic(): Boolean { + return code == 2 } } -internal open class InternalError : RustError() { - class ByReference: InternalError(), RustErrorReference +class InternalException(message: String) : Exception(message) - @Suppress("ReturnCount", "TooGenericExceptionThrown", "UNCHECKED_CAST") - override fun intoException(): E { - if (!isFailure()) { - // It's probably a bad idea to throw here! We're probably leaking something if this is - // ever hit! (But we shouldn't ever hit it?) - throw RuntimeException("[Bug] intoException called on non-failure!") - } - val message = this.consumeErrorMessage() - return InternalException(message) as E - } +// Each top-level error class has a companion object that can lift the error from the call status's rust buffer +interface CallStatusErrorHandler { + fun lift(error_buf: RustBuffer.ByValue): E; } -class InternalException(message: String) : Exception(message) - {%- for e in ci.iter_error_definitions() %} -internal open class {{e.name()}} : RustError() { - class ByReference: {{e.name()}}(), RustErrorReference - @Suppress("ReturnCount", "TooGenericExceptionThrown", "UNCHECKED_CAST") - override fun intoException(): E { - if (!isFailure()) { - // It's probably a bad idea to throw here! We're probably leaking something if this is - // ever hit! (But we shouldn't ever hit it?) - throw RuntimeException("[Bug] intoException called on non-failure!") +// Error {{ e.name() }} +{%- let toplevel_name=e.name()|exception_name_kt %} +open class {{ toplevel_name }}: Exception() { + // Each variant is a nested class + {% for variant in e.variants() -%} + {% if !variant.has_fields() -%} + class {{ variant.name()|exception_name_kt }} : {{ toplevel_name }}() + {% else %} + class {{ variant.name()|exception_name_kt }}( + {% for field in variant.fields() -%} + val {{ field.name()|var_name_kt }}: {{ field.type_()|type_kt}}{% if loop.last %}{% else %}, {% endif %} + {% endfor -%} + ) : {{ toplevel_name }}() + {%- endif %} + {% endfor %} + + companion object ErrorHandler : CallStatusErrorHandler<{{ toplevel_name }}> { + override fun lift(error_buf: RustBuffer.ByValue): {{ toplevel_name }} { + return liftFromRustBuffer(error_buf) { error_buf -> read(error_buf) } } - val message = this.consumeErrorMessage() - when (code) { - {% for value in e.values() -%} - {{loop.index}} -> return {{e|exception_name_kt}}.{{value}}(message) as E - {% endfor -%} - -1 -> return InternalException(message) as E - else -> throw RuntimeException("invalid error code passed across the FFI") + + fun read(error_buf: ByteBuffer): {{ toplevel_name }} { + return when(error_buf.getInt()) { + {%- for variant in e.variants() %} + {{ loop.index }} -> {{ toplevel_name }}.{{ variant.name()|exception_name_kt }}({% if variant.has_fields() %} + {% for field in variant.fields() -%} + {{ "error_buf"|read_kt(field.type_()) }}{% if loop.last %}{% else %},{% endif %} + {% endfor -%} + {%- endif -%}) + {%- endfor %} + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } } } } - -open class {{e|exception_name_kt}}(message: String) : Exception(message) { - {% for value in e.values() -%} - class {{value}}(msg: String) : {{e|exception_name_kt}}(msg) - {% endfor %} -} - {% endfor %} -// Helpers for calling Rust with errors: +// Helpers for calling Rust // In practice we usually need to be synchronized to call this safely, so it doesn't // synchronize itself -private inline fun nullableRustCall(callback: (E) -> U?, err: E): U? { - try { - val ret = callback(err) - if (err.isFailure()) { - throw err.intoException() + +// Call a rust function that returns a Result<>. Pass in the Error class companion that corresponds to the Err +private inline fun rustCallWithError(errorHandler: CallStatusErrorHandler, 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(String.lift(status.error_buf)) + } else { + throw InternalException("Rust panic") } - return ret - } finally { - // This only matters if `callback` throws (or does a non-local return, which - // we currently don't do) - err.ensureConsumed() + } else { + throw InternalException("Unknown rust call status: $status.code") + } +} + +// CallStatusErrorHandler implementation for times when we don't expect a CALL_ERROR +object NullCallStatusErrorHandler: CallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): InternalException { + RustBuffer.free(error_buf) + return InternalException("Unexpected CALL_ERROR") } } -private inline fun rustCall(err: E, callback: (E) -> U?): U { - return nullableRustCall(callback, err)!! +// Call a rust function that returns a plain value +private inline fun rustCall(callback: (RustCallStatus) -> U): U { + return rustCallWithError(NullCallStatusErrorHandler, callback); } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt index c8f649b522..658464be4d 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt @@ -30,8 +30,8 @@ class {{ obj.name()|class_name_kt }}( * Clients **must** call this method once done with the object, or cause a memory leak. */ override protected fun freeRustArcPtr() { - rustCall(InternalError.ByReference()) { err -> - _UniFFILib.INSTANCE.{{ obj.ffi_object_free().name() }}(this.pointer, err) + rustCall() { status -> + _UniFFILib.INSTANCE.{{ obj.ffi_object_free().name() }}(this.pointer, status) } } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt index a03da97439..38afb326ee 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt @@ -11,18 +11,19 @@ open class RustBuffer : Structure() { @JvmField var padding: Long = 0 class ByValue : RustBuffer(), Structure.ByValue + class ByReference : RustBuffer(), Structure.ByReference companion object { - internal fun alloc(size: Int = 0) = rustCall(InternalError.ByReference()) { err -> - _UniFFILib.INSTANCE.{{ ci.ffi_rustbuffer_alloc().name() }}(size, err) + internal fun alloc(size: Int = 0) = rustCall() { status -> + _UniFFILib.INSTANCE.{{ ci.ffi_rustbuffer_alloc().name() }}(size, status) } - internal fun free(buf: RustBuffer.ByValue) = rustCall(InternalError.ByReference()) { err -> - _UniFFILib.INSTANCE.{{ ci.ffi_rustbuffer_free().name() }}(buf, err) + internal fun free(buf: RustBuffer.ByValue) = rustCall() { status -> + _UniFFILib.INSTANCE.{{ ci.ffi_rustbuffer_free().name() }}(buf, status) } - internal fun reserve(buf: RustBuffer.ByValue, additional: Int) = rustCall(InternalError.ByReference()) { err -> - _UniFFILib.INSTANCE.{{ ci.ffi_rustbuffer_reserve().name() }}(buf, additional, err) + internal fun reserve(buf: RustBuffer.ByValue, additional: Int) = rustCall() { status -> + _UniFFILib.INSTANCE.{{ ci.ffi_rustbuffer_reserve().name() }}(buf, additional, status) } } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt b/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt index cfa159d514..cd8e0cc9a7 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt @@ -5,29 +5,25 @@ #} {%- macro to_ffi_call(func) -%} -rustCall( {%- match func.throws() %} {%- when Some with (e) %} - {{-e}}.ByReference() + rustCallWithError({{ e|exception_name_kt}}) {%- else %} - InternalError.ByReference() - {%- endmatch %} -) { err -> - _UniFFILib.INSTANCE.{{ func.ffi_func().name() }}({% call _arg_list_ffi_call(func) -%}{% if func.arguments().len() > 0 %},{% endif %}err) + 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) %} -rustCall( {%- match func.throws() %} {%- when Some with (e) %} - {{e}}.ByReference() + rustCallWithError({{ e|exception_name_kt}}) {%- else %} - InternalError.ByReference() - {%- endmatch %} -) { err -> + rustCall() + {%- endmatch %} { status -> _UniFFILib.INSTANCE.{{ func.ffi_func().name() }}( - {{- prefix }}, {% call _arg_list_ffi_call(func) %}{% if func.arguments().len() > 0 %}, {% endif %}err) + {{- prefix }}, {% call _arg_list_ffi_call(func) %}{% if func.arguments().len() > 0 %}, {% endif %}status) } {%- endmacro %} @@ -67,10 +63,9 @@ rustCall( -#} {%- macro arg_list_ffi_decl(func) %} {%- for arg in func.arguments() %} - {{- arg.name() }}: {{ arg.type_()|type_ffi -}} - {%- if loop.last %}{% else %},{% endif %} + {{- arg.name() }}: {{ arg.type_()|type_ffi -}}, {%- endfor %} - {% if func.arguments().len() > 0 %},{% endif %} uniffi_out_err: Structure.ByReference + uniffi_out_err: RustCallStatus {%- endmacro -%} // Add annotation if there are unsigned types diff --git a/uniffi_bindgen/src/bindings/python/gen_python.rs b/uniffi_bindgen/src/bindings/python/gen_python.rs index 854df819da..2c6ad4f1ca 100644 --- a/uniffi_bindgen/src/bindings/python/gen_python.rs +++ b/uniffi_bindgen/src/bindings/python/gen_python.rs @@ -72,10 +72,8 @@ mod filters { FFIType::UInt64 => "ctypes.c_uint64".to_string(), FFIType::Float32 => "ctypes.c_float".to_string(), FFIType::Float64 => "ctypes.c_double".to_string(), - FFIType::RustCString => "ctypes.c_void_p".to_string(), FFIType::RustArcPtr => "ctypes.c_void_p".to_string(), FFIType::RustBuffer => "RustBuffer".to_string(), - FFIType::RustError => "ctypes.POINTER(RustError)".to_string(), FFIType::ForeignBytes => "ForeignBytes".to_string(), FFIType::ForeignCallback => unimplemented!("Callback interfaces are not implemented"), }) diff --git a/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py index 35b812f892..68a67aaf67 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py @@ -1,49 +1,91 @@ -class RustError(ctypes.Structure): +class InternalError(Exception): + pass + +class RustCallStatus(ctypes.Structure): _fields_ = [ - ("code", ctypes.c_int32), - ("message", ctypes.c_void_p), + ("code", ctypes.c_int8), + ("error_buf", RustBuffer), ] - def free(self): - rust_call_with_error(InternalError, _UniFFILib.{{ ci.ffi_string_free().name() }}, self.message) + # These match the values from the uniffi::rustcalls module + CALL_SUCCESS = 0 + CALL_ERROR = 1 + CALL_PANIC = 2 def __str__(self): - return "RustError(code={}, message={})".format( - self.code, - str(ctypes.cast(self.message, ctypes.c_char_p).value, "utf-8"), - ) + 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_SUCCESS)" + else: + return "RustCallStatus()" +{%- for e in ci.iter_error_definitions() %} -class InternalError(Exception): - @staticmethod - def raise_err(code, message): - raise InternalError(message) - -{% for e in ci.iter_error_definitions() %} class {{ e.name()|class_name_py }}: - {%- for value in e.values() %} - class {{ value|class_name_py }}(Exception): - pass + # Each variant is a nested class of the error itself. + {%- for variant in e.variants() %} + + class {{ variant.name()|class_name_py }}(Exception): + def __init__(self{% for field in variant.fields() %}, {{ field.name()|var_name_py }}{% endfor %}): + {%- if variant.has_fields() %} + {%- for field in variant.fields() %} + self.{{ field.name()|var_name_py }} = {{ field.name()|var_name_py }} + {%- endfor %} + {% else %} + pass + {%- endif %} + + def __str__(self): + return "{{ e.name()|class_name_py }}.{{ variant.name()|class_name_py }}({% for field in variant.fields() %}{{ field.name() }}={}{% if !loop.last %},{% endif %}{% endfor %})".format({% for field in variant.fields() %}self.{{ field.name() }}{% if !loop.last %},{% endif %}{% endfor %}) {%- endfor %} +{%- endfor %} + +# Map error classes to the RustBufferStream method to read them +_error_class_to_reader_method = { +{%- for e in ci.iter_error_definitions() %} +{%- let typ=ci.get_type(e.name()).unwrap() %} +{%- let canonical_type_name = typ.canonical_name()|class_name_py %} + {{ e.name()|class_name_py }}: RustBufferStream.read{{ canonical_type_name }}, +{%- endfor %} +} - @staticmethod - def raise_err(code, message): - {%- for value in e.values() %} - if code == {{ loop.index }}: - raise {{ e.name()|class_name_py }}.{{ value|class_name_py }}(message) - {% endfor %} - raise Exception("Unknown error code") -{% endfor %} +def consume_buffer_into_error(error_class, rust_buffer): + reader_method = _error_class_to_reader_method[error_class] + with rust_buffer.consumeWithStream() as stream: + return reader_method(stream) + +def rust_call(fn, *args): + # Call a rust function + return rust_call_with_error(None, fn, *args) def rust_call_with_error(error_class, fn, *args): - error = RustError() - error.code = 0 + # 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_class must be set to the error class that corresponds to the result. + call_status = RustCallStatus(code=0, error_buf=RustBuffer(0, 0, None)) - args_with_error = args + (ctypes.byref(error),) + args_with_error = args + (ctypes.byref(call_status),) result = fn(*args_with_error) - if error.code != 0: - message = str(error) - error.free() - - error_class.raise_err(error.code, message) - - return result + if call_status.code == RustCallStatus.CALL_SUCCESS: + return result + elif call_status.code == RustCallStatus.CALL_ERROR: + if error_class is None: + call_status.err_buf.contents.free() + raise InternalError("rust_call_with_error: CALL_ERROR, but no error class set") + else: + raise consume_buffer_into_error(error_class, 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 = call_status.error_buf.consumeIntoString() + else: + msg = "Unkown rust panic" + raise InternalError(msg) + else: + raise InternalError("Invalid RustCallStatus code: {}".format( + call_status.code)) diff --git a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py index b917a338af..d7bff65051 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py @@ -11,11 +11,7 @@ def __del__(self): # In case of partial initialization of instances. pointer = getattr(self, "_pointer", None) if pointer is not None: - rust_call_with_error( - InternalError, - _UniFFILib.{{ obj.ffi_object_free().name() }}, - pointer - ) + rust_call(_UniFFILib.{{ obj.ffi_object_free().name() }}, pointer) # Used by alternative constructors or any methods which return this type. @classmethod diff --git a/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py b/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py index 82c6af894c..6335207f77 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py +++ b/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py @@ -163,6 +163,35 @@ def read{{ canonical_type_name }}(self): raise InternalError("Unexpected variant tag for {{ canonical_type_name }}") {%- endif %} + {% when Type::Error with (error_name) -%} + {%- let e = ci.get_error_definition(error_name).unwrap().wrapped_enum() %} + + # The Error type {{ error_name }} + + # Top-level read method + def read{{ canonical_type_name }}(self): + variant = self._unpack_from(4, ">i") + try: + read_variant_method = getattr(self, 'read{{canonical_type_name}}{}'.format(variant)) + except AttributeError: + raise InternalError("Unexpected variant value for error {{ canonical_type_name }} ({})".format(variant)) + return read_variant_method() + + # Read methods for each individual variants + {%- for variant in e.variants() %} + + def read{{ canonical_type_name }}{{ loop.index }}(self): + {%- if variant.has_fields() %} + return {{ error_name|class_name_py }}.{{ variant.name()|class_name_py }}( + {%- for field in variant.fields() %} + self.read{{ field.type_().canonical_name()|class_name_py }}(), + {%- endfor %} + ) + {%- else %} + return {{ error_name|class_name_py }}.{{ variant.name()|class_name_py }}() + {%- endif %} + {%- endfor %} + {% when Type::Record with (record_name) -%} {%- let rec = ci.get_record_definition(record_name).unwrap() -%} # The Record type {{ record_name }}. diff --git a/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py b/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py index 9b3b2c6265..735464e263 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py @@ -10,14 +10,14 @@ class RustBuffer(ctypes.Structure): @staticmethod def alloc(size): - return rust_call_with_error(InternalError, _UniFFILib.{{ ci.ffi_rustbuffer_alloc().name() }}, size) + return rust_call(_UniFFILib.{{ ci.ffi_rustbuffer_alloc().name() }}, size) @staticmethod def reserve(rbuf, additional): - return rust_call_with_error(InternalError, _UniFFILib.{{ ci.ffi_rustbuffer_reserve().name() }}, rbuf, additional) + return rust_call(_UniFFILib.{{ ci.ffi_rustbuffer_reserve().name() }}, rbuf, additional) def free(self): - return rust_call_with_error(InternalError, _UniFFILib.{{ ci.ffi_rustbuffer_free().name() }}, self) + return rust_call(_UniFFILib.{{ ci.ffi_rustbuffer_free().name() }}, self) def __str__(self): return "RustBuffer(capacity={}, len={}, data={})".format( diff --git a/uniffi_bindgen/src/bindings/python/templates/macros.py b/uniffi_bindgen/src/bindings/python/templates/macros.py index b6fdc1b67e..a6442a0d57 100644 --- a/uniffi_bindgen/src/bindings/python/templates/macros.py +++ b/uniffi_bindgen/src/bindings/python/templates/macros.py @@ -5,12 +5,11 @@ #} {%- macro to_ffi_call(func) -%} -rust_call_with_error( {%- match func.throws() -%} {%- when Some with (e) -%} - {{ e|class_name_py }}, +rust_call_with_error({{ e|class_name_py }}, {%- else -%} - InternalError, +rust_call( {%- endmatch -%} _UniFFILib.{{ func.ffi_func().name() }}, {%- call _arg_list_ffi_call(func) -%} @@ -18,12 +17,12 @@ {%- endmacro -%} {%- macro to_ffi_call_with_prefix(prefix, func) -%} -rust_call_with_error( {%- match func.throws() -%} {%- when Some with (e) -%} +rust_call_with_error( {{ e|class_name_py }}, {%- else -%} - InternalError, +rust_call( {%- endmatch -%} _UniFFILib.{{ func.ffi_func().name() }}, {{- prefix }}, @@ -62,7 +61,7 @@ {%- for arg in func.arguments() %} {{ arg.type_()|type_ffi }}, {%- endfor %} - ctypes.POINTER(RustError), + ctypes.POINTER(RustCallStatus), {% endmacro -%} {%- macro coerce_args(func) %} diff --git a/uniffi_bindgen/src/bindings/ruby/gen_ruby.rs b/uniffi_bindgen/src/bindings/ruby/gen_ruby.rs index dfdc4d423c..b46d5e2ade 100644 --- a/uniffi_bindgen/src/bindings/ruby/gen_ruby.rs +++ b/uniffi_bindgen/src/bindings/ruby/gen_ruby.rs @@ -92,10 +92,8 @@ mod filters { FFIType::UInt64 => ":uint64".to_string(), FFIType::Float32 => ":float".to_string(), FFIType::Float64 => ":double".to_string(), - FFIType::RustCString => ":string".to_string(), FFIType::RustArcPtr => ":pointer".to_string(), FFIType::RustBuffer => "RustBuffer.by_value".to_string(), - FFIType::RustError => "RustError.by_ref".to_string(), FFIType::ForeignBytes => "ForeignBytes".to_string(), FFIType::ForeignCallback => unimplemented!("Callback interfaces are not implemented"), }) diff --git a/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb b/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb index f45da8b156..b1489c8919 100644 --- a/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb +++ b/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb @@ -1,50 +1,106 @@ -class RustError < FFI::Struct - layout :code, :int32, - :message, :string +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 - "RustError(code=#{self[:code]}, message=#{self[:message]})" + "RustCallStatus(code=#{self[:code]})" end end -{% for e in ci.iter_error_definitions() %} -class {{ e.name()|class_name_rb }} - {%- for value in e.values() %} - {{ value|class_name_rb }} = Class.new StandardError +# These match the values from the uniffi::rustcalls module +CALL_SUCCESS = 0 +CALL_ERROR = 1 +CALL_PANIC = 2 +{%- for e in ci.iter_error_definitions() %} +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() }}{% if !loop.last %}, {% endif %}{% endfor %} + {% endif %} + end {%- endfor %} +end +{%- endfor %} - def self.raise_err(code, message) - {%- for value in e.values() %} - if code == {{ loop.index }} - raise {{ value|class_name_rb }}, message - end - {% endfor %} - raise 'Unknown error code' +# Map error modules to the RustBuffer method name that reads them +ERROR_MODULE_TO_READER_METHOD = { +{%- for e in ci.iter_error_definitions() %} +{%- let typ=ci.get_type(e.name()).unwrap() %} +{%- let canonical_type_name = typ.canonical_name()|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 -{% endfor %} class InternalError < StandardError - def self.raise_err(code, message) - raise InternalError, message - end end -def self.rust_call_with_error(error_class, fn_name, *args) - error = RustError.new - args << error +def self.rust_call(fn_name, *args) + # Call a rust function + rust_call_with_error(nil, fn_name, *args) +end - result = UniFFILib.public_send(fn_name, *args) +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. - if error.code != 0 - message = error.to_s - error_class.raise_err(error.code, message) - end + # 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) - result + 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/uniffi_bindgen/src/bindings/ruby/templates/ObjectTemplate.rb b/uniffi_bindgen/src/bindings/ruby/templates/ObjectTemplate.rb index 7acef0c7e2..6072bb309a 100644 --- a/uniffi_bindgen/src/bindings/ruby/templates/ObjectTemplate.rb +++ b/uniffi_bindgen/src/bindings/ruby/templates/ObjectTemplate.rb @@ -15,8 +15,7 @@ def self._uniffi_allocate(pointer) # 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_with_error( - InternalError, + {{ ci.namespace()|class_name_rb }}.rust_call( :{{ obj.ffi_object_free().name() }}, pointer ) diff --git a/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb b/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb index 50e6b59578..08e08d5933 100644 --- a/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb +++ b/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb @@ -204,3 +204,5 @@ def pack_into(size, format, value) end end end + +private_constant :RustBufferBuilder diff --git a/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb b/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb index ab693cf0f8..d7543927f5 100644 --- a/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb +++ b/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb @@ -146,6 +146,40 @@ def read{{ 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 }} + 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()|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 }}. @@ -227,3 +261,5 @@ def unpack_from(size, format) value[0] end end + +private_constant :RustBufferStream diff --git a/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb b/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb index cad1c8d5d3..9ddb70d79f 100644 --- a/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb +++ b/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb @@ -6,15 +6,15 @@ class RustBuffer < FFI::Struct :padding, :int64 def self.alloc(size) - return {{ ci.namespace()|class_name_rb }}.rust_call_with_error(InternalError, :{{ ci.ffi_rustbuffer_alloc().name() }}, 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_with_error(InternalError, :{{ ci.ffi_rustbuffer_reserve().name() }}, 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_with_error(InternalError, :{{ ci.ffi_rustbuffer_free().name() }}, self) + {{ ci.namespace()|class_name_rb }}.rust_call(:{{ ci.ffi_rustbuffer_free().name() }}, self) end def capacity @@ -190,3 +190,5 @@ def to_s end end end + +private_constant :UniFFILib diff --git a/uniffi_bindgen/src/bindings/ruby/templates/macros.rb b/uniffi_bindgen/src/bindings/ruby/templates/macros.rb index 9215b02316..67a5527564 100644 --- a/uniffi_bindgen/src/bindings/ruby/templates/macros.rb +++ b/uniffi_bindgen/src/bindings/ruby/templates/macros.rb @@ -5,12 +5,11 @@ #} {%- macro to_ffi_call(func) -%} -{{ ci.namespace()|class_name_rb }}.rust_call_with_error( {%- match func.throws() -%} {%- when Some with (e) -%} - {{ e|class_name_rb }}, + {{ ci.namespace()|class_name_rb }}.rust_call_with_error({{ e|class_name_rb }}, {%- else -%} - InternalError, + {{ ci.namespace()|class_name_rb }}.rust_call( {%- endmatch -%} :{{ func.ffi_func().name() }}, {%- call _arg_list_ffi_call(func) -%} @@ -18,12 +17,11 @@ {%- endmacro -%} {%- macro to_ffi_call_with_prefix(prefix, func) -%} -{{ ci.namespace()|class_name_rb }}.rust_call_with_error( {%- match func.throws() -%} {%- when Some with (e) -%} - {{ e|class_name_rb }}, + {{ ci.namespace()|class_name_rb }}.rust_call_with_error({{ e|class_name_rb }}, {%- else -%} - InternalError, + {{ ci.namespace()|class_name_rb }}.rust_call( {%- endmatch -%} :{{ func.ffi_func().name() }}, {{- prefix }}, @@ -59,7 +57,7 @@ // Note unfiltered name but type_ffi filters. -#} {%- macro arg_list_ffi_decl(func) %} - [{%- for arg in func.arguments() -%}{{ arg.type_()|type_ffi }}, {% endfor -%} RustError.by_ref] + [{%- for arg in func.arguments() -%}{{ arg.type_()|type_ffi }}, {% endfor -%} RustCallStatus.by_ref] {%- endmacro -%} {%- macro coerce_args(func) %} diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift.rs b/uniffi_bindgen/src/bindings/swift/gen_swift.rs index 8d0f264e67..2d270c5b68 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift.rs @@ -165,10 +165,8 @@ mod filters { FFIType::UInt64 => "uint64_t".into(), FFIType::Float32 => "float".into(), FFIType::Float64 => "double".into(), - FFIType::RustCString => "const char*_Nonnull".into(), FFIType::RustArcPtr => "void*_Nonnull".into(), FFIType::RustBuffer => "RustBuffer".into(), - FFIType::RustError => "NativeRustError".into(), FFIType::ForeignBytes => "ForeignBytes".into(), FFIType::ForeignCallback => unimplemented!("Callback interfaces are not implemented"), }) diff --git a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift index 69c3c9a81c..d38057a100 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift @@ -1,24 +1,14 @@ -{# -// In here we define conversions between a native reference to Swift errors -// We use the RustError protocol to define the requirements. Any implementers of the protocol -// Can be generated from a NativeRustError. - -#} - -fileprivate protocol RustError: LocalizedError { - static func fromConsuming(_ rustError: NativeRustError) throws -> Self? -} - // An error type for FFI errors. These errors occur at the UniFFI level, not // the library level. -fileprivate enum UniffiInternalError: RustError { +public enum UniffiInternalError: LocalizedError { case bufferOverflow case incompleteData case unexpectedOptionalTag case unexpectedEnumCase case unexpectedNullPointer - case emptyResult - case unknown(_ message: String) + case unexpectedRustCallStatusCode + case unexpectedRustCallError + case rustPanic(_ message: String) public var errorDescription: String? { switch self { @@ -27,103 +17,111 @@ fileprivate enum UniffiInternalError: RustError { 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 .emptyResult: return "Unexpected nil returned from FFI function" - case let .unknown(message): return "FFI function returned unknown error: \(message)" + case .unexpectedRustCallStatusCode: return "Unexpected RustCallStatus code" + case .unexpectedRustCallError: return "CALL_ERROR but no errorClass specified" + case let .rustPanic(message): return message } } +} - fileprivate static func fromConsuming(_ rustError: NativeRustError) throws -> Self? { - let message = rustError.message - defer { - if message != nil { - try! rustCall(UniffiInternalError.unknown("UniffiInternalError.fromConsuming")) { err in - {{ ci.ffi_string_free().name() }}(message!, err) - } - } - } - switch rustError.code { - case 0: return nil - default: return .unknown(String(cString: 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, + padding: 0 + ) + ) } } +{# Define enums to handle each individual error #} {% for e in ci.iter_error_definitions() %} -public enum {{e.name()}}: RustError { - case NoError - {% for value in e.values() %} - case {{value}}(message: String) +public enum {{ e.name()|class_name_swift }} { + {% for variant in e.variants() %} + case {{ variant.name()|class_name_swift }}{% if variant.fields().len() > 0 %}({% call swift::field_list_decl(variant) %}){% endif -%} {% endfor %} +} - - /// Our implementation of the localizedError protocol - public var errorDescription: String? { - switch self { - {% for value in e.values() %} - case let .{{value}}(message): - return "{{e.name()}}.{{value}}: \(message)" +extension {{ e.name()|class_name_swift }}: ViaFfiUsingByteBuffer, ViaFfi { + fileprivate static func read(from buf: Reader) throws -> {{ e.name()|class_name_swift }} { + let variant: Int32 = try buf.readInt() + switch variant { + {% for variant in e.variants() %} + case {{ loop.index }}: return .{{ variant.name()|class_name_swift }}{% if variant.has_fields() -%}( + {% for field in variant.fields() -%} + {{ field.name()|var_name_swift }}: try {{ "buf"|read_swift(field.type_()) }}{% if loop.last %}{% else %},{% endif %} + {% endfor -%} + ){% endif -%} {% endfor %} - default: - return nil + default: throw UniffiInternalError.unexpectedEnumCase } } - // The name is attempting to indicate that we free message if it - // existed, and that it's a very bad idea to touch it after you call this - // function - fileprivate static func fromConsuming(_ rustError: NativeRustError) throws -> Self? { - let message = rustError.message - defer { - if message != nil { - try! rustCall(UniffiInternalError.unknown("{{e.name()}}.fromConsuming")) { err in - {{ ci.ffi_string_free().name() }}(message!, err) - } - } - } - switch rustError.code { - case 0: - return nil - case -1: - // TODO: Return a Uniffi defined error here - // to indicate a panic - fatalError("Panic detected!") - {% for value in e.values() %} - case {{loop.index}}: - return .{{value}}(message: String(cString: message!)) - {% endfor %} - default: - fatalError("Invalid error") + fileprivate func write(into buf: Writer) { + switch self { + {% for variant in e.variants() %} + {% if variant.has_fields() %} + case let .{{ variant.name()|class_name_swift }}({% for field in variant.fields() %}{{ field.name()|var_name_swift }}{%- if loop.last -%}{%- else -%},{%- endif -%}{% endfor %}): + buf.writeInt(Int32({{ loop.index }})) + {% for field in variant.fields() -%} + {{ field.name()|var_name_swift }}.write(into: buf) + {% endfor -%} + {% else %} + case .{{ variant.name()|class_name_swift }}: + buf.writeInt(Int32({{ loop.index }})) + {% endif %} + {%- endfor %} } } } + +{% if ! e.contains_object_references(ci) %} +extension {{ e.name()|class_name_swift }}: Equatable, Hashable {} +{% endif %} +extension {{ e.name()|class_name_swift }}: Error { } {% endfor %} -private func rustCall(_ err: E, _ cb: (UnsafeMutablePointer) throws -> T?) throws -> T { - return try unwrap(err) { native_err in - return try cb(native_err) - } +private func rustCall(_ callback: (UnsafeMutablePointer) -> T) throws -> T { + try makeRustCall(callback, errorHandler: { + $0.deallocate() + return UniffiInternalError.unexpectedRustCallError + }) } -private func nullableRustCall(_ err: E, _ cb: (UnsafeMutablePointer) throws -> T?) throws -> T? { - return try tryUnwrap(err) { native_err in - return try cb(native_err) - } +private func rustCallWithError(_ errorClass: E.Type, _ callback: (UnsafeMutablePointer) -> T) throws -> T { + try makeRustCall(callback, errorHandler: { return try E.lift($0) }) } -@discardableResult -private func unwrap(_ err: E, _ callback: (UnsafeMutablePointer) throws -> T?) throws -> T { - guard let result = try tryUnwrap(err, callback) else { - throw UniffiInternalError.emptyResult - } - return result -} +private func makeRustCall(_ callback: (UnsafeMutablePointer) -> 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) -@discardableResult -private func tryUnwrap(_ err: E, _ callback: (UnsafeMutablePointer) throws -> T?) throws -> T? { - var native_err = NativeRustError(code: 0, message: nil) - let returnedVal = try callback(&native_err) - if let retErr = try E.fromConsuming(native_err) { - throw retErr + 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 String.lift(callStatus.errorBuf)) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.rustPanic("Rust panic") + } + + default: + throw UniffiInternalError.unexpectedRustCallStatusCode } - return returnedVal } diff --git a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift index 10f1233db8..7875d92683 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift @@ -28,9 +28,7 @@ public class {{ obj.name()|class_name_swift }}: {{ obj.name() }}Protocol { {%- endmatch %} deinit { - try! rustCall(UniffiInternalError.unknown("deinit")) { err in - {{ obj.ffi_object_free().name() }}(pointer, err) - } + try! rustCall { {{ obj.ffi_object_free().name() }}(pointer, $0) } } {% for cons in obj.alternate_constructors() %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/RustBufferHelper.swift b/uniffi_bindgen/src/bindings/swift/templates/RustBufferHelper.swift index 98b27a9fbb..ee7ac4df89 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/RustBufferHelper.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/RustBufferHelper.swift @@ -167,9 +167,7 @@ extension String: ViaFfi { fileprivate static func lift(_ v: FfiType) throws -> Self { defer { - try! rustCall(UniffiInternalError.unknown("String.lift")) { err in - {{ ci.ffi_rustbuffer_free().name() }}(v, err) - } + try! rustCall { {{ ci.ffi_rustbuffer_free().name() }}(v, $0) } } if v.data == nil { return String() @@ -185,9 +183,7 @@ extension String: ViaFfi { // The swift string gives us a trailing null byte, we don't want it. let buf = UnsafeBufferPointer(rebasing: ptr.prefix(upTo: ptr.count - 1)) let bytes = ForeignBytes(bufferPointer: buf) - return try! rustCall(UniffiInternalError.unknown("String.lower")) { err in - {{ ci.ffi_rustbuffer_from_bytes().name() }}(bytes, err) - } + return try! rustCall { {{ ci.ffi_rustbuffer_from_bytes().name() }}(bytes, $0) } } } } diff --git a/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift index 9d76d0d38e..d64617ac6d 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift @@ -2,9 +2,7 @@ fileprivate extension RustBuffer { // Allocate a new buffer, copying the contents of a `UInt8` array. init(bytes: [UInt8]) { let rbuf = bytes.withUnsafeBufferPointer { ptr in - try! rustCall(UniffiInternalError.unknown("RustBuffer.init")) { err in - {{ ci.ffi_rustbuffer_from_bytes().name() }}(ForeignBytes(bufferPointer: ptr), err) - } + try! rustCall { {{ ci.ffi_rustbuffer_from_bytes().name() }}(ForeignBytes(bufferPointer: ptr), $0) } } // Ref https://github.com/mozilla/uniffi-rs/issues/334 for the extra "padding" arg. self.init(capacity: rbuf.capacity, len: rbuf.len, data: rbuf.data, padding: 0) @@ -13,9 +11,7 @@ fileprivate extension RustBuffer { // Frees the buffer in place. // The buffer must not be used after this is called. func deallocate() { - try! rustCall(UniffiInternalError.unknown("RustBuffer.deallocate")) { err in - {{ ci.ffi_rustbuffer_free().name() }}(self, err) - } + try! rustCall { {{ ci.ffi_rustbuffer_free().name() }}(self, $0) } } } diff --git a/uniffi_bindgen/src/bindings/swift/templates/Template-Bridging-Header.h b/uniffi_bindgen/src/bindings/swift/templates/Template-Bridging-Header.h index 7cabcb535f..b6dc562aee 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/Template-Bridging-Header.h +++ b/uniffi_bindgen/src/bindings/swift/templates/Template-Bridging-Header.h @@ -40,11 +40,10 @@ typedef struct ForeignBytes } ForeignBytes; // Error definitions -// Each error has an error code enum, and a struct -typedef struct NativeRustError { - int32_t code; - char *_Nullable message; -} NativeRustError; +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 in all instance of UNIFFI_SHARED_HEADER_V1 in this file. ⚠️ diff --git a/uniffi_bindgen/src/bindings/swift/templates/macros.swift b/uniffi_bindgen/src/bindings/swift/templates/macros.swift index a5786240a0..dc40b5a2b2 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/macros.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/macros.swift @@ -5,29 +5,27 @@ #} {%- macro to_ffi_call(func) -%} -{% call try(func) %} rustCall( +{% call try(func) %} {% match func.throws() %} {% when Some with (e) %} - {{e}}.NoError + rustCallWithError({{ e|class_name_swift }}.self) { {% else %} - UniffiInternalError.unknown("rustCall") + rustCall() { {% endmatch %} -) { err in - {{ func.ffi_func().name() }}({% call _arg_list_ffi_call(func) -%}{% if func.arguments().len() > 0 %},{% endif %}err) + {{ 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) %} rustCall( +{% call try(func) %} {%- match func.throws() %} {%- when Some with (e) %} - {{e}}.NoError + rustCallWithError({{ e|class_name_swift }}.self) { {%- else %} - UniffiInternalError.unknown("rustCall") - {%- endmatch %} -) { err in + rustCall() { + {% endmatch %} {{ func.ffi_func().name() }}( - {{- prefix }}, {% call _arg_list_ffi_call(func) -%}{% if func.arguments().len() > 0 %},{% endif %}err + {{- prefix }}, {% call _arg_list_ffi_call(func) -%}{% if func.arguments().len() > 0 %}, {% endif %}$0 ) } {%- endmacro %} @@ -85,11 +83,9 @@ -#} {%- macro arg_list_ffi_decl(func) %} {%- for arg in func.arguments() %} - {{- arg.type_()|type_ffi }} {{ arg.name() -}} - {% if loop.last %}{% else %},{% endif %} + {{- arg.type_()|type_ffi }} {{ arg.name() -}}, {%- endfor %} - {% if func.arguments().len() > 0 %},{% endif %}NativeRustError *_Nonnull out_err - + RustCallStatus *_Nonnull out_status {%- endmacro -%} {%- macro throws(func) %} diff --git a/uniffi_bindgen/src/interface/attributes.rs b/uniffi_bindgen/src/interface/attributes.rs index 725a5c7197..a494363100 100644 --- a/uniffi_bindgen/src/interface/attributes.rs +++ b/uniffi_bindgen/src/interface/attributes.rs @@ -237,6 +237,10 @@ impl InterfaceAttributes { 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() @@ -251,6 +255,7 @@ impl TryFrom<&weedle::attribute::ExtendedAttributeList<'_>> for InterfaceAttribu ) -> Result { let attrs = parse_attributes(weedle_attributes, |attr| match attr { Attribute::Enum => Ok(()), + Attribute::Error => Ok(()), Attribute::Threadsafe => Ok(()), _ => bail!(format!("{:?} not supported for interface definition", attr)), })?; @@ -637,15 +642,4 @@ mod test { "conflicting attributes on interface definition" ); } - - #[test] - fn test_other_attributes_not_supported_for_interfaces() { - let (_, node) = - weedle::attribute::ExtendedAttributeList::parse("[Threadsafe, Error]").unwrap(); - let err = InterfaceAttributes::try_from(&node).unwrap_err(); - assert_eq!( - err.to_string(), - "Error not supported for interface definition" - ); - } } diff --git a/uniffi_bindgen/src/interface/error.rs b/uniffi_bindgen/src/interface/error.rs index e132606a7d..b3e5081e9b 100644 --- a/uniffi_bindgen/src/interface/error.rs +++ b/uniffi_bindgen/src/interface/error.rs @@ -19,12 +19,12 @@ //! # Ok::<(), anyhow::Error>(()) //! ``` //! -//! Will result in an [`Error`] member being added to the resulting [`ComponentInterface`]: +//! 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] +//! # [Error] //! # enum Example { //! # "one", //! # "two" @@ -32,49 +32,112 @@ //! # "##)?; //! let err = ci.get_error_definition("Example").unwrap(); //! assert_eq!(err.name(), "Example"); -//! assert_eq!(err.values().len(), 2); -//! assert_eq!(err.values()[0], "one"); -//! assert_eq!(err.values()[1], "two"); +//! 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 super::enum_::{Enum, Variant}; 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 represent them using the `ffi_support::ExternError` +/// 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, Default, Hash)] +#[derive(Debug, Clone, Hash)] pub struct Error { - pub(super) name: String, - pub(super) values: Vec, + pub name: String, + enum_: Enum, } impl Error { + pub fn from_enum(enum_: Enum) -> Self { + Self { + name: enum_.name.clone(), + enum_, + } + } + pub fn name(&self) -> &str { &self.name } - pub fn values(&self) -> Vec<&str> { - self.values.iter().map(|v| v.as_str()).collect() + pub fn wrapped_enum(&self) -> &Enum { + &self.enum_ + } + + pub fn variants(&self) -> Vec<&Variant> { + self.enum_.variants() + } + + pub fn is_flat(&self) -> bool { + self.enum_.is_flat() + } + + // For compatibility with the Enum interface + pub fn contains_object_references(&self, _: &ComponentInterface) -> bool { + false } } impl APIConverter for weedle::EnumDefinition<'_> { - fn convert(&self, _ci: &mut ComponentInterface) -> Result { - Ok(Error { - name: self.identifier.0.to_string(), - values: self - .values - .body - .list - .iter() - .map(|v| v.0.to_string()) - .collect(), - }) + fn convert(&self, ci: &mut ComponentInterface) -> Result { + Ok(Error::from_enum(APIConverter::::convert(self, ci)?)) + } +} + +impl APIConverter for weedle::InterfaceDefinition<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result { + Ok(Error::from_enum(APIConverter::::convert(self, ci)?)) } } @@ -82,6 +145,27 @@ impl APIConverter for weedle::EnumDefinition<'_> { 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.iter_error_definitions().len(), 1); + let error = ci.get_error_definition("Testing").unwrap(); + assert_eq!( + error + .variants() + .iter() + .map(|v| v.name()) + .collect::>(), + vec!("one", "two", "three") + ); + assert!(error.is_flat()); + } + #[test] fn test_duplicate_variants() { const UDL: &str = r#" @@ -94,8 +178,33 @@ mod test { let ci = ComponentInterface::from_webidl(UDL).unwrap(); assert_eq!(ci.iter_error_definitions().len(), 1); assert_eq!( - ci.get_error_definition("Testing").unwrap().values().len(), + 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.iter_error_definitions().len(), 1); + let error: &Error = ci.get_error_definition("Testing").unwrap(); + assert_eq!( + error + .variants() + .iter() + .map(|v| v.name()) + .collect::>(), + vec!("One", "Two") + ); + assert!(!error.is_flat()); + } } diff --git a/uniffi_bindgen/src/interface/ffi.rs b/uniffi_bindgen/src/interface/ffi.rs index b84c99b4c7..5bf398ab1a 100644 --- a/uniffi_bindgen/src/interface/ffi.rs +++ b/uniffi_bindgen/src/interface/ffi.rs @@ -31,10 +31,6 @@ pub enum FFIType { Int64, Float32, Float64, - /// A `char*` pointer belonging to a rust-owned CString. - /// If you've got one of these, you must call the appropriate rust function to free it. - /// This is currently only used for error messages, and may go away in future. - RustCString, /// A `*const c_void` pointer to a rust-owned `Arc`. /// 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. @@ -46,10 +42,6 @@ pub enum FFIType { /// 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, - /// An error struct, containing a numberic error code and char* pointer to error string. - /// The string is owned by rust and allocated on the rust heap, and must be freed by - /// passing it to the appropriate `string_free` FFI function. - RustError, /// 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, diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index b154ddcf5d..c52c75d21c 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -119,6 +119,8 @@ impl<'ci> ComponentInterface { println!("{}", remaining); bail!("parse error"); } + // Unconditionally add the String type, which is used by the error handling + let _ = 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())?; @@ -209,6 +211,11 @@ impl<'ci> ComponentInterface { self.types.iter_known_types().collect() } + /// Get a specific type + pub fn get_type(&self, name: &str) -> Option { + self.types.get_type_definition(name) + } + /// Check whether the given type contains any (possibly nested) Type::Object references. /// /// This is important to know in language bindings that cannot integrate object types @@ -376,22 +383,6 @@ impl<'ci> ComponentInterface { } } - /// Builtin FFI function for freeing a string. - /// This is needed for foreign-language code when dealing with errors, so that it can free - /// the error message string. - /// TODO: make our error class return the message in a `RustBuffer` so we can free it using - /// the exisiting bytebuffer-freeing function rather than a special one. - pub fn ffi_string_free(&self) -> FFIFunction { - FFIFunction { - name: format!("ffi_{}_string_free", self.ffi_namespace()), - arguments: vec![FFIArgument { - name: "cstr".to_string(), - type_: FFIType::RustCString, - }], - return_type: 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 @@ -418,7 +409,6 @@ impl<'ci> ComponentInterface { self.ffi_rustbuffer_from_bytes(), self.ffi_rustbuffer_free(), self.ffi_rustbuffer_reserve(), - self.ffi_string_free(), ] .iter() .cloned(), @@ -630,6 +620,9 @@ impl APIBuilder for weedle::Definition<'_> { 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); diff --git a/uniffi_bindgen/src/interface/types/finder.rs b/uniffi_bindgen/src/interface/types/finder.rs index c1ed8c2772..e3165c0949 100644 --- a/uniffi_bindgen/src/interface/types/finder.rs +++ b/uniffi_bindgen/src/interface/types/finder.rs @@ -61,6 +61,8 @@ impl TypeFinder for weedle::InterfaceDefinition<'_> { // 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)) } diff --git a/uniffi_bindgen/src/interface/types/mod.rs b/uniffi_bindgen/src/interface/types/mod.rs index 0b592dc2f5..a847fd35e2 100644 --- a/uniffi_bindgen/src/interface/types/mod.rs +++ b/uniffi_bindgen/src/interface/types/mod.rs @@ -137,10 +137,9 @@ impl From<&Type> for FFIType { Type::Object(_) => FFIType::RustArcPtr, // Callback interfaces are passed as opaque integer handles. Type::CallbackInterface(_) => FFIType::UInt64, - // Errors have their own special type. - Type::Error(_) => FFIType::RustError, // Other types are serialized into a bytebuffer and deserialized on the other side. Type::Enum(_) + | Type::Error(_) | Type::Record(_) | Type::Optional(_) | Type::Sequence(_) diff --git a/uniffi_bindgen/src/scaffolding/mod.rs b/uniffi_bindgen/src/scaffolding/mod.rs index 445f7ed3c0..077efee43f 100644 --- a/uniffi_bindgen/src/scaffolding/mod.rs +++ b/uniffi_bindgen/src/scaffolding/mod.rs @@ -63,10 +63,8 @@ mod filters { FFIType::UInt64 => "u64".into(), FFIType::Float32 => "f32".into(), FFIType::Float64 => "f64".into(), - FFIType::RustCString => "*mut std::os::raw::c_char".into(), FFIType::RustArcPtr => "*const std::os::raw::c_void".into(), FFIType::RustBuffer => "uniffi::RustBuffer".into(), - FFIType::RustError => "uniffi::deps::ffi_support::ExternError".into(), FFIType::ForeignBytes => "uniffi::ForeignBytes".into(), FFIType::ForeignCallback => "uniffi::ForeignCallback".into(), }) diff --git a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs index 407ea74fde..0085d207e2 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs @@ -1,20 +1,48 @@ {# - // For each error declared in the UDL, using the [Error] attribute, we assume the caller has provided a corresponding - // rust Error enum with the same name. We provide the traits for sending it across the FFI, which will fail to - // compile if the provided enum has a different shape to the one declared in the UDL. - // Here we define the neccessary converstion to allow the error to propegate through the FFI as an error. +// 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. #} #[doc(hidden)] -impl From<{{e.name()}}> for uniffi::deps::ffi_support::ExternError { - fn from(err: {{e.name()}}) -> uniffi::deps::ffi_support::ExternError { - // Errno just differentiate between the errors. - // They are in-order, i.e the first variant of the enum has code 1 - // As we add support for generic errors (e.g panics) - // we might find that we need to reserve some codes. - match err { - {%- for value in e.values() %} - {{ e.name()}}::{{value}}{..} => uniffi::deps::ffi_support::ExternError::new_error(uniffi::deps::ffi_support::ErrorCode::new({{ loop.index }}), err.to_string()), +impl uniffi::RustBufferViaFfi for {{ e.name() }} { + fn write(&self, buf: &mut Vec) { + use uniffi::deps::bytes::BufMut; + match self { + {%- for variant in e.variants() %} + {{ e.name() }}::{{ variant.name() }}{% if variant.has_fields() %} { {% for field in variant.fields() %}{{ field.name() }}, {%- endfor %} }{% else %}{..}{% endif %} => { + buf.put_i32({{ loop.index }}); + {% for field in variant.fields() -%} + <{{ field.type_()|type_rs }} as uniffi::ViaFfi>::write({{ field.name() }}, buf); + {%- endfor %} + }, {%- endfor %} - } + }; } + + {% if e.is_flat() %} + // 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. Let's just punt on implemented + // try_read() in that case, which is no issue since passing back Errors into the rust code + // isn't supported. + fn try_read(_buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { + panic!("try_read not supported for fieldless errors"); + } + {% else %} + fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { + use uniffi::deps::bytes::Buf; + uniffi::check_remaining(buf, 4)?; + Ok(match buf.get_i32() { + {%- for variant in e.variants() %} + {{ loop.index }} => {{ e.name() }}::{{ variant.name() }}{% if variant.has_fields() %} { + {% for field in variant.fields() %} + {{ field.name() }}: <{{ field.type_()|type_rs }} as uniffi::ViaFfi>::try_read(buf)?, + {%- endfor %} + }{% endif %}, + {%- endfor %} + v => uniffi::deps::anyhow::bail!("Invalid {{ e.name() }} enum value: {}", v), + }) + } + {% endif %} } + +impl uniffi::FfiError for {{ e.name() }} { } diff --git a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs index 660ff1bc6e..19fe9ab405 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs @@ -27,16 +27,12 @@ fn uniffi_note_threadsafe_deprecation_{{ obj.name() }}() {} {% 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) { - // We mustn't panic across the FFI, but also can't report it anywhere. - // The best we can do it catch, warn and ignore. - if let Err(e) = std::panic::catch_unwind(|| { +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 {{ obj.name() }}) }) - }) { - uniffi::deps::log::error!("{{ ffi_free.name() }} panicked: {:?}", e); - } + }) } {%- for cons in obj.constructors() %} diff --git a/uniffi_bindgen/src/scaffolding/templates/RustBuffer.rs b/uniffi_bindgen/src/scaffolding/templates/RustBuffer.rs index 2b4e9bf4ec..7411d77375 100644 --- a/uniffi_bindgen/src/scaffolding/templates/RustBuffer.rs +++ b/uniffi_bindgen/src/scaffolding/templates/RustBuffer.rs @@ -6,8 +6,8 @@ /// or by passing ownership of the buffer back into Rust code. #[doc(hidden)] #[no_mangle] -pub extern "C" fn {{ ci.ffi_rustbuffer_alloc().name() }}(size: i32, err: &mut uniffi::deps::ffi_support::ExternError) -> uniffi::RustBuffer { - uniffi::deps::ffi_support::call_with_output(err, || { +pub extern "C" fn {{ ci.ffi_rustbuffer_alloc().name() }}(size: i32, call_status: &mut uniffi::RustCallStatus) -> uniffi::RustBuffer { + uniffi::call_with_output(call_status, || { uniffi::RustBuffer::new_with_size(size.max(0) as usize) }) } @@ -22,8 +22,8 @@ pub extern "C" fn {{ ci.ffi_rustbuffer_alloc().name() }}(size: i32, err: &mut un /// make sure the `ForeignBytes` struct contains a valid pointer and length. #[doc(hidden)] #[no_mangle] -pub unsafe extern "C" fn {{ ci.ffi_rustbuffer_from_bytes().name() }}(bytes: uniffi::ForeignBytes, err: &mut uniffi::deps::ffi_support::ExternError) -> uniffi::RustBuffer { - uniffi::deps::ffi_support::call_with_output(err, || { +pub unsafe extern "C" fn {{ ci.ffi_rustbuffer_from_bytes().name() }}(bytes: uniffi::ForeignBytes, call_status: &mut uniffi::RustCallStatus) -> uniffi::RustBuffer { + uniffi::call_with_output(call_status, || { let bytes = bytes.as_slice(); uniffi::RustBuffer::from_vec(bytes.to_vec()) }) @@ -37,8 +37,8 @@ pub unsafe extern "C" fn {{ ci.ffi_rustbuffer_from_bytes().name() }}(bytes: unif /// corrupting the allocator state. #[doc(hidden)] #[no_mangle] -pub unsafe extern "C" fn {{ ci.ffi_rustbuffer_free().name() }}(buf: uniffi::RustBuffer, err: &mut uniffi::deps::ffi_support::ExternError) { - uniffi::deps::ffi_support::call_with_output(err, || { +pub unsafe extern "C" fn {{ ci.ffi_rustbuffer_free().name() }}(buf: uniffi::RustBuffer, call_status: &mut uniffi::RustCallStatus) { + uniffi::call_with_output(call_status, || { uniffi::RustBuffer::destroy(buf) }) } @@ -60,8 +60,8 @@ pub unsafe extern "C" fn {{ ci.ffi_rustbuffer_free().name() }}(buf: uniffi::Rust /// corrupting the allocator state. #[doc(hidden)] #[no_mangle] -pub unsafe extern "C" fn {{ ci.ffi_rustbuffer_reserve().name() }}(buf: uniffi::RustBuffer, additional: i32, err: &mut uniffi::deps::ffi_support::ExternError) -> uniffi::RustBuffer { - uniffi::deps::ffi_support::call_with_output(err, || { +pub unsafe extern "C" fn {{ ci.ffi_rustbuffer_reserve().name() }}(buf: uniffi::RustBuffer, additional: i32, call_status: &mut uniffi::RustCallStatus) -> uniffi::RustBuffer { + uniffi::call_with_output(call_status, || { use std::convert::TryInto; let additional: usize = additional.try_into().expect("additional buffer length negative or overflowed"); let mut v = buf.destroy_into_vec(); @@ -69,20 +69,3 @@ pub unsafe extern "C" fn {{ ci.ffi_rustbuffer_reserve().name() }}(buf: uniffi::R uniffi::RustBuffer::from_vec(v) }) } - -/// Free a String that had previously been passed to the foreign language code. -/// -/// # Safety -/// -/// In order to free the string, Rust takes ownership of a raw pointer which is an -/// unsafe operation. The argument *must* be a uniquely-owned pointer previously -/// obtained from a call into the rust code that returned a string. -/// (In practice that means you got it from the `message` field of an `ExternError`, -/// because that's currently the only place we use `char*` types in our API). -#[doc(hidden)] -#[no_mangle] -pub unsafe extern "C" fn {{ ci.ffi_string_free().name() }}(cstr: *mut std::os::raw::c_char, err: &mut uniffi::deps::ffi_support::ExternError) { - uniffi::deps::ffi_support::call_with_output(err, || { - uniffi::deps::ffi_support::destroy_c_string(cstr) - }) -} \ No newline at end of file diff --git a/uniffi_bindgen/src/scaffolding/templates/macros.rs b/uniffi_bindgen/src/scaffolding/templates/macros.rs index 5c41a70d43..390f7d480e 100644 --- a/uniffi_bindgen/src/scaffolding/templates/macros.rs +++ b/uniffi_bindgen/src/scaffolding/templates/macros.rs @@ -20,9 +20,9 @@ -#} {%- macro arg_list_ffi_decl(func) %} {%- for arg in func.arguments() %} - {{- arg.name() }}: {{ arg.type_()|type_ffi -}}{% if loop.last %}{% else %},{% endif %} + {{- arg.name() }}: {{ arg.type_()|type_ffi -}}, {%- endfor %} - {% if func.arguments().len() > 0 %},{% endif %} err: &mut uniffi::deps::ffi_support::ExternError, + call_status: &mut uniffi::RustCallStatus {%- endmacro -%} {%- macro arg_list_decl_with_prefix(prefix, meth) %} @@ -47,13 +47,13 @@ {% macro to_rs_constructor_call(obj, cons) %} {% match cons.throws() %} {% when Some with (e) %} - uniffi::deps::ffi_support::call_with_result(err, || -> Result<_, {{ e }}> { + uniffi::call_with_result(call_status, || -> Result<_, {{ e }}> { let _new = {% call construct(obj, cons) %}?; let _arc = std::sync::Arc::new(_new); Ok({{ "_arc"|lower_rs(obj.type_()) }}) }) {% else %} - uniffi::deps::ffi_support::call_with_output(err, || { + uniffi::call_with_output(call_status, || { let _new = {% call construct(obj, cons) %}; let _arc = std::sync::Arc::new(_new); {{ "_arc"|lower_rs(obj.type_()) }} @@ -64,12 +64,12 @@ {% macro to_rs_method_call(obj, meth) -%} {% match meth.throws() -%} {% when Some with (e) -%} -uniffi::deps::ffi_support::call_with_result(err, || -> Result<{% call return_type_func(meth) %}, {{e}}> { +uniffi::call_with_result(call_status, || -> Result<{% call return_type_func(meth) %}, {{e}}> { let _retval = {{ obj.name() }}::{% call to_rs_call(meth) %}?; Ok({% call ret(meth) %}) }) {% else %} -uniffi::deps::ffi_support::call_with_output(err, || { +uniffi::call_with_output(call_status, || { {% match meth.return_type() -%} {% when Some with (return_type) -%} let retval = {{ obj.name() }}::{% call to_rs_call(meth) %}; @@ -84,12 +84,12 @@ uniffi::deps::ffi_support::call_with_output(err, || { {% macro to_rs_function_call(func) %} {% match func.throws() %} {% when Some with (e) %} -uniffi::deps::ffi_support::call_with_result(err, || -> Result<{% call return_type_func(func) %}, {{e}}> { +uniffi::call_with_result(call_status, || -> Result<{% call return_type_func(func) %}, {{e}}> { let _retval = {% call to_rs_call(func) %}?; Ok({% call ret(func) %}) }) {% else %} -uniffi::deps::ffi_support::call_with_output(err, || { +uniffi::call_with_output(call_status, || { {% match func.return_type() -%} {% when Some with (return_type) -%} let retval = {% call to_rs_call(func) %}; diff --git a/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs b/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs index 8081a7d56f..bf294fa1bf 100644 --- a/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs +++ b/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs @@ -10,9 +10,9 @@ uniffi::assert_compatible_version!("{{ uniffi_version }}"); // Please check that {% include "RustBuffer.rs" %} -// We generate error mappings into ffi_support::ExternErrors -// so that the errors can propagate through the FFI -{% for e in ci.iter_error_definitions() %} +// Error definitions, corresponding to `error` in the UDL. +{% for error in ci.iter_error_definitions() %} +{% let e = error.wrapped_enum() %} {% include "ErrorTemplate.rs" %} {% endfor %} From 7153a9cfda3cc1aa1721ac71cbf7dd3860e7eb37 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Mon, 26 Jul 2021 10:52:00 -0400 Subject: [PATCH 23/45] Fixes/updates based on rfk's review - Many improvements to the comments - Addded back test_other_attributes_not_supported_for_interfaces - Improved the python __str__ method for errors and also simplified the template code that generates it. - Updated the RustBufferStream method names for reading individual variants to avoid name conflicts. - Implemented contains_object_references() and updated the code to handle errors that do --- CHANGELOG.md | 2 +- .../coverall/tests/bindings/test_coverall.py | 2 ++ uniffi/src/ffi/rustcalls.rs | 21 ++++++++++++++----- .../kotlin/templates/ErrorTemplate.kt | 19 ++++++++++++++++- .../python/templates/ErrorTemplate.py | 15 ++++++++++--- .../python/templates/RustBufferStream.py | 4 ++-- .../swift/templates/ErrorTemplate.swift | 2 +- .../templates/Template-Bridging-Header.h | 12 +++++------ uniffi_bindgen/src/interface/attributes.rs | 11 ++++++++++ uniffi_bindgen/src/interface/error.rs | 4 ++-- uniffi_bindgen/src/interface/mod.rs | 2 +- .../scaffolding/templates/ErrorTemplate.rs | 8 +++---- 12 files changed, 76 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e6251eef2..a45617576e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ ### What's Changed - Both python and ruby backends now handle U16 correctly. -- Errors with nested fields are now supported. +- Error variants can now contain named fields, similar to Enum variants ## v0.12.0 (2021-06-14) diff --git a/fixtures/coverall/tests/bindings/test_coverall.py b/fixtures/coverall/tests/bindings/test_coverall.py index 50944d7e44..37cf3f98ad 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.py +++ b/fixtures/coverall/tests/bindings/test_coverall.py @@ -106,10 +106,12 @@ def test_complex_errors(self): coveralls.maybe_throw_complex(1) self.assertEqual(cm.exception.code, 10) self.assertEqual(cm.exception.extended_code, 20) + self.assertEqual(str(cm.exception), "ComplexError.OsError(code=10, extended_code=20)") with self.assertRaises(ComplexError.PermissionDenied) as cm: coveralls.maybe_throw_complex(2) self.assertEqual(cm.exception.reason, "Forbidden") + self.assertEqual(str(cm.exception), "ComplexError.PermissionDenied(reason='Forbidden')") # Test panics, which should cause InternalError to be raised with self.assertRaises(InternalError) as cm: diff --git a/uniffi/src/ffi/rustcalls.rs b/uniffi/src/ffi/rustcalls.rs index 9bd030e1de..f6c03a8e20 100644 --- a/uniffi/src/ffi/rustcalls.rs +++ b/uniffi/src/ffi/rustcalls.rs @@ -25,7 +25,7 @@ use std::panic; /// - 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 +/// - 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 @@ -42,7 +42,7 @@ use std::panic; /// /// #### The `code` field. /// -/// - `CALL_SUCESS` (0) for successful calls +/// - `CALL_SUCCESS` (0) for successful calls /// - `CALL_ERROR` (1) for calls that returned an `Err` value /// - `CALL_PANIC` (2) for calls that panicked /// @@ -58,8 +58,12 @@ pub struct RustCallStatus { // 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 in. In + // - 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`. } #[allow(dead_code)] @@ -73,7 +77,8 @@ const CALL_PANIC: i8 = 2; pub trait FfiError: RustBufferViaFfi {} // Generalized rust call handling function -// callback should map errors to a RustBuffer that contains them +// callback is responsible for making the call to the rust function. If that function returns an +// `Err` value, callback should serialize the error into a `RustBuffer` to be returned over the FFI. fn make_call(out_status: &mut RustCallStatus, callback: F) -> R::Value where F: panic::UnwindSafe + FnOnce() -> Result, @@ -96,6 +101,8 @@ where 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() @@ -119,10 +126,14 @@ where })); 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 + // 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() } } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt index dfbf1bd9f8..f057bc7368 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt @@ -27,7 +27,7 @@ interface CallStatusErrorHandler { // Error {{ e.name() }} {%- let toplevel_name=e.name()|exception_name_kt %} -open class {{ toplevel_name }}: Exception() { +sealed class {{ toplevel_name }}: Exception(){% if e.contains_object_references(ci) %}, Disposable {% endif %} { // Each variant is a nested class {% for variant in e.variants() -%} {% if !variant.has_fields() -%} @@ -59,6 +59,23 @@ open class {{ toplevel_name }}: Exception() { } } } + + {% if e.contains_object_references(ci) %} + @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 {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }} -> { + {% for field in variant.fields() -%} + {%- if ci.type_contains_object_references(field.type_()) -%} + this.{{ field.name() }}?.destroy() + {% endif -%} + {%- endfor %} + } + {%- endfor %} + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + {% endif %} } {% endfor %} diff --git a/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py index 68a67aaf67..03d53a0dbf 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py @@ -33,12 +33,21 @@ def __init__(self{% for field in variant.fields() %}, {{ field.name()|var_name_p {%- for field in variant.fields() %} self.{{ field.name()|var_name_py }} = {{ field.name()|var_name_py }} {%- endfor %} - {% else %} + {%- else %} pass {%- endif %} def __str__(self): - return "{{ e.name()|class_name_py }}.{{ variant.name()|class_name_py }}({% for field in variant.fields() %}{{ field.name() }}={}{% if !loop.last %},{% endif %}{% endfor %})".format({% for field in variant.fields() %}self.{{ field.name() }}{% if !loop.last %},{% endif %}{% endfor %}) + {%- if variant.has_fields() %} + field_parts = [ + {%- for field in variant.fields() %} + '{{ field.name() }}={!r}'.format(self.{{ field.name() }}), + {%- endfor %} + ] + return "{{ e.name()|class_name_py }}.{{ variant.name()|class_name_py }}({})".format(', '.join(field_parts)) + {%- else %} + return "{{ e.name()|class_name_py }}.{{ variant.name()|class_name_py }}" + {%- endif %} {%- endfor %} {%- endfor %} @@ -84,7 +93,7 @@ def rust_call_with_error(error_class, fn, *args): if call_status.error_buf.len > 0: msg = call_status.error_buf.consumeIntoString() else: - msg = "Unkown rust panic" + msg = "Unknown rust panic" raise InternalError(msg) else: raise InternalError("Invalid RustCallStatus code: {}".format( diff --git a/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py b/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py index 6335207f77..31b437ea2a 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py +++ b/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py @@ -172,7 +172,7 @@ def read{{ canonical_type_name }}(self): def read{{ canonical_type_name }}(self): variant = self._unpack_from(4, ">i") try: - read_variant_method = getattr(self, 'read{{canonical_type_name}}{}'.format(variant)) + read_variant_method = getattr(self, 'readVariant{}Of{{canonical_type_name}}'.format(variant)) except AttributeError: raise InternalError("Unexpected variant value for error {{ canonical_type_name }} ({})".format(variant)) return read_variant_method() @@ -180,7 +180,7 @@ def read{{ canonical_type_name }}(self): # Read methods for each individual variants {%- for variant in e.variants() %} - def read{{ canonical_type_name }}{{ loop.index }}(self): + def readVariant{{ loop.index}}Of{{ canonical_type_name }}(self): {%- if variant.has_fields() %} return {{ error_name|class_name_py }}.{{ variant.name()|class_name_py }}( {%- for field in variant.fields() %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift index d38057a100..053a9df6c1 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift @@ -83,7 +83,7 @@ extension {{ e.name()|class_name_swift }}: ViaFfiUsingByteBuffer, ViaFfi { } } -{% if ! e.contains_object_references(ci) %} +{% if !e.contains_object_references(ci) %} extension {{ e.name()|class_name_swift }}: Equatable, Hashable {} {% endif %} extension {{ e.name()|class_name_swift }}: Error { } diff --git a/uniffi_bindgen/src/bindings/swift/templates/Template-Bridging-Header.h b/uniffi_bindgen/src/bindings/swift/templates/Template-Bridging-Header.h index b6dc562aee..c4a57ce6fe 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/Template-Bridging-Header.h +++ b/uniffi_bindgen/src/bindings/swift/templates/Template-Bridging-Header.h @@ -11,15 +11,15 @@ // 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 in UNIFFI_SHARED_HEADER_V1 - #ifndef UNIFFI_SHARED_HEADER_V1 + // If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V2 + #ifndef UNIFFI_SHARED_HEADER_V2 #error Combining helper code from multiple versions of uniffi is not supported - #endif // ndef UNIFFI_SHARED_HEADER_V1 + #endif // ndef UNIFFI_SHARED_HEADER_V2 #else #define UNIFFI_SHARED_H -#define UNIFFI_SHARED_HEADER_V1 +#define UNIFFI_SHARED_HEADER_V2 // ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ -// ⚠️ increment the version in all instance of UNIFFI_SHARED_HEADER_V1 in this file. ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V2 in this file. ⚠️ typedef struct RustBuffer { @@ -46,7 +46,7 @@ typedef struct RustCallStatus { } RustCallStatus; // ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ -// ⚠️ increment the version in all instance of UNIFFI_SHARED_HEADER_V1 in this file. ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V2 in this file. ⚠️ #endif // def UNIFFI_SHARED_H {% for func in ci.iter_ffi_function_definitions() -%} diff --git a/uniffi_bindgen/src/interface/attributes.rs b/uniffi_bindgen/src/interface/attributes.rs index a494363100..f7ce152390 100644 --- a/uniffi_bindgen/src/interface/attributes.rs +++ b/uniffi_bindgen/src/interface/attributes.rs @@ -642,4 +642,15 @@ mod test { "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" + ); + } } diff --git a/uniffi_bindgen/src/interface/error.rs b/uniffi_bindgen/src/interface/error.rs index b3e5081e9b..1275520742 100644 --- a/uniffi_bindgen/src/interface/error.rs +++ b/uniffi_bindgen/src/interface/error.rs @@ -124,8 +124,8 @@ impl Error { } // For compatibility with the Enum interface - pub fn contains_object_references(&self, _: &ComponentInterface) -> bool { - false + pub fn contains_object_references(&self, ci: &ComponentInterface) -> bool { + self.enum_.contains_object_references(ci) } } diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index c52c75d21c..8fb32ea16f 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -119,7 +119,7 @@ impl<'ci> ComponentInterface { println!("{}", remaining); bail!("parse error"); } - // Unconditionally add the String type, which is used by the error handling + // Unconditionally add the String type, which is used by the panic handling let _ = 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. diff --git a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs index 0085d207e2..d65a403f8a 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs @@ -1,5 +1,5 @@ {# -// For each enum declared in the UDL, we assume the caller has provided a corresponding +// 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. #} @@ -21,9 +21,9 @@ impl uniffi::RustBufferViaFfi for {{ e.name() }} { {% if e.is_flat() %} // 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. Let's just punt on implemented - // try_read() in that case, which is no issue since passing back Errors into the rust code - // isn't supported. + // the Rust enum has fields and they're just not listed. Let's just punt on implementing + // try_read() to avoid that case. It should be no issue since passing back Errors into the + // rust code isn't supported. fn try_read(_buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { panic!("try_read not supported for fieldless errors"); } From eae43c06fb07898e6bd414dcad1c1a590127bd2b Mon Sep 17 00:00:00 2001 From: lougeniac64 Date: Mon, 26 Jul 2021 15:16:27 -0400 Subject: [PATCH 24/45] Added logic to conditionally generate swift bindings --- .../swift/templates/RustBufferHelper.swift | 150 +++++++++++------- uniffi_bindgen/src/interface/mod.rs | 67 ++++++++ 2 files changed, 158 insertions(+), 59 deletions(-) diff --git a/uniffi_bindgen/src/bindings/swift/templates/RustBufferHelper.swift b/uniffi_bindgen/src/bindings/swift/templates/RustBufferHelper.swift index 98b27a9fbb..34f65af7d7 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/RustBufferHelper.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/RustBufferHelper.swift @@ -1,3 +1,7 @@ +// 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 libray of its own. @@ -161,7 +165,77 @@ extension ViaFfiUsingByteBuffer { } // Implement our protocols for the built-in types that we use. +{% if ci.contains_optional_types() %} +extension Optional: ViaFfiUsingByteBuffer, ViaFfi, Serializable where Wrapped: Serializable { + fileprivate static func read(from buf: Reader) throws -> Self { + switch try buf.readInt() as Int8 { + case 0: return nil + case 1: return try Wrapped.read(from: buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } + + fileprivate func write(into buf: Writer) { + guard let value = self else { + buf.writeInt(Int8(0)) + return + } + buf.writeInt(Int8(1)) + value.write(into: buf) + } +} +{% endif %} + +{% if ci.contains_sequence_types() %} +extension Array: ViaFfiUsingByteBuffer, ViaFfi, Serializable where Element: Serializable { + fileprivate static func read(from buf: Reader) throws -> Self { + let len: Int32 = try buf.readInt() + var seq = [Element]() + seq.reserveCapacity(Int(len)) + for _ in 0.. Self { + let len: Int32 = try buf.readInt() + var dict = [String: Value]() + dict.reserveCapacity(Int(len)) + for _ in 0.. Self { let seconds: Int64 = try buf.readInt() @@ -258,6 +333,7 @@ extension Date: ViaFfiUsingByteBuffer, ViaFfi { } } +{% when Type::Duration -%} extension TimeInterval { fileprivate static func liftDuration(_ buf: RustBuffer) throws -> Self { let reader = Reader(data: Data(rustBuffer: buf)) @@ -297,6 +373,7 @@ extension TimeInterval { } } +{% when Type::UInt8 -%} extension UInt8: Primitive, ViaFfi { fileprivate static func read(from buf: Reader) throws -> UInt8 { return try self.lift(buf.readInt()) @@ -307,6 +384,7 @@ extension UInt8: Primitive, ViaFfi { } } +{% when Type::Int8 -%} extension Int8: Primitive, ViaFfi { fileprivate static func read(from buf: Reader) throws -> Int8 { return try self.lift(buf.readInt()) @@ -317,6 +395,7 @@ extension Int8: Primitive, ViaFfi { } } +{% when Type::UInt16 -%} extension UInt16: Primitive, ViaFfi { fileprivate static func read(from buf: Reader) throws -> UInt16 { return try self.lift(buf.readInt()) @@ -327,6 +406,7 @@ extension UInt16: Primitive, ViaFfi { } } +{% when Type::Int16 -%} extension Int16: Primitive, ViaFfi { fileprivate static func read(from buf: Reader) throws -> Int16 { return try self.lift(buf.readInt()) @@ -337,6 +417,7 @@ extension Int16: Primitive, ViaFfi { } } +{% when Type::UInt32 -%} extension UInt32: Primitive, ViaFfi { fileprivate static func read(from buf: Reader) throws -> UInt32 { return try self.lift(buf.readInt()) @@ -347,6 +428,7 @@ extension UInt32: Primitive, ViaFfi { } } +{% when Type::Int32 -%} extension Int32: Primitive, ViaFfi { fileprivate static func read(from buf: Reader) throws -> Int32 { return try self.lift(buf.readInt()) @@ -357,6 +439,7 @@ extension Int32: Primitive, ViaFfi { } } +{% when Type::UInt64 -%} extension UInt64: Primitive, ViaFfi { fileprivate static func read(from buf: Reader) throws -> UInt64 { return try self.lift(buf.readInt()) @@ -367,6 +450,7 @@ extension UInt64: Primitive, ViaFfi { } } +{% when Type::Int64 -%} extension Int64: Primitive, ViaFfi { fileprivate static func read(from buf: Reader) throws -> Int64 { return try self.lift(buf.readInt()) @@ -377,6 +461,7 @@ extension Int64: Primitive, ViaFfi { } } +{% when Type::Float32 -%} extension Float: Primitive, ViaFfi { fileprivate static func read(from buf: Reader) throws -> Float { return try self.lift(buf.readFloat()) @@ -387,6 +472,7 @@ extension Float: Primitive, ViaFfi { } } +{% when Type::Float64 -%} extension Double: Primitive, ViaFfi { fileprivate static func read(from buf: Reader) throws -> Double { return try self.lift(buf.readDouble()) @@ -397,62 +483,8 @@ extension Double: Primitive, ViaFfi { } } -extension Optional: ViaFfiUsingByteBuffer, ViaFfi, Serializable where Wrapped: Serializable { - fileprivate static func read(from buf: Reader) throws -> Self { - switch try buf.readInt() as Int8 { - case 0: return nil - case 1: return try Wrapped.read(from: buf) - default: throw UniffiInternalError.unexpectedOptionalTag - } - } - - fileprivate func write(into buf: Writer) { - guard let value = self else { - buf.writeInt(Int8(0)) - return - } - buf.writeInt(Int8(1)) - value.write(into: buf) - } -} - -extension Array: ViaFfiUsingByteBuffer, ViaFfi, Serializable where Element: Serializable { - fileprivate static func read(from buf: Reader) throws -> Self { - let len: Int32 = try buf.readInt() - var seq = [Element]() - seq.reserveCapacity(Int(len)) - for _ in 0.. Self { - let len: Int32 = try buf.readInt() - var dict = [String: Value]() - dict.reserveCapacity(Int(len)) - for _ in 0.. ComponentInterface { } } + pub fn contains_optional_types(&self) -> bool { + self.iter_types() + .iter() + .any(|t| matches!(t, Type::Optional(_))) + } + + pub fn contains_sequence_types(&self) -> bool { + self.iter_types() + .iter() + .any(|t| matches!(t, Type::Sequence(_))) + } + + pub fn contains_map_types(&self) -> bool { + self.iter_types().iter().any(|t| matches!(t, Type::Map(_))) + } + /// Check whether the given type contains any (possibly nested) unsigned types pub fn type_contains_unsigned_types(&self, type_: &Type) -> bool { match type_ { @@ -791,4 +807,55 @@ mod test { "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_eq!(ci.contains_optional_types(), false); + + // 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_eq!(ci.contains_optional_types(), true); + } + + #[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_eq!(ci.contains_sequence_types(), false); + + // 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_eq!(ci.contains_sequence_types(), true); + } + + #[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_eq!(ci.contains_map_types(), false); + + // 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::Boolean))) + .is_ok()); + assert_eq!(ci.contains_map_types(), true); + } } From 942ce13895d40214853ecd34dc1387a37fd125b7 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Wed, 28 Jul 2021 15:01:33 -0400 Subject: [PATCH 25/45] Use ffi-support's panic handling hook --- uniffi/Cargo.toml | 2 +- uniffi/src/ffi/rustcalls.rs | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/uniffi/Cargo.toml b/uniffi/Cargo.toml index 607fae3893..233d655ba1 100644 --- a/uniffi/Cargo.toml +++ b/uniffi/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["ffi", "bindgen"] # Re-exported dependencies used in generated Rust scaffolding files. anyhow = "1" bytes = "1.0" -ffi-support = "~0.4.3" +ffi-support = "~0.4.4" lazy_static = "1.4" log = "0.4" # Regular dependencies diff --git a/uniffi/src/ffi/rustcalls.rs b/uniffi/src/ffi/rustcalls.rs index f6c03a8e20..988023e837 100644 --- a/uniffi/src/ffi/rustcalls.rs +++ b/uniffi/src/ffi/rustcalls.rs @@ -85,12 +85,8 @@ where R: IntoFfi, { let result = panic::catch_unwind(|| { - // We should call: - // - // init_panic_handling_once(); - // - // This is called in ffi-support::call_with_result_impl(). The current plan is to make it - // `pub` in ffi-support and call it from here. + // Use ffi_support's panic handling hook + ffi_support::ensure_panic_hook_is_setup(); callback().map(|v| v.into_ffi_value()) }); match result { From ca3e83833d619b0ce1ced84fd8a23214cc3ba0f6 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Wed, 28 Jul 2021 15:22:54 -0400 Subject: [PATCH 26/45] Another fix for the Kotlin chrono tests (#985) --- .../tests/bindings/test_chronological.kts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/fixtures/uniffi-fixture-time/tests/bindings/test_chronological.kts b/fixtures/uniffi-fixture-time/tests/bindings/test_chronological.kts index 00f626988d..aa8be25c2a 100644 --- a/fixtures/uniffi-fixture-time/tests/bindings/test_chronological.kts +++ b/fixtures/uniffi-fixture-time/tests/bindings/test_chronological.kts @@ -35,14 +35,13 @@ try { } // Test that rust timestamps behave like kotlin timestamps +// Unfortunately the JVM clock may be lower resolution than the Rust clock. +// Sleep for 1ms between each call, which should ensure the JVM clock ticks +// forward. val kotlinBefore = Instant.now() +Thread.sleep(10) val rustNow = now() +Thread.sleep(10) val kotlinAfter = Instant.now() -assert(kotlinBefore.isBefore(rustNow) || kotlinBefore.equals(rustNow)) -assert( - kotlinAfter.isAfter(rustNow) || - kotlinAfter.equals(rustNow) || - // Unfortunately the JVM clock may be lower resolution than the Rust clock, - // so we might observe time advance in Rust but not in Kotlin. - kotlinAfter.equals(kotlinBefore) -) +assert(kotlinBefore.isBefore(rustNow)) +assert(kotlinAfter.isAfter(rustNow)) From 0d331bca241451c2de032e16808cc8971aadb210 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Fri, 30 Jul 2021 09:57:48 -0400 Subject: [PATCH 27/45] Make UniffiInternalError fileprivate again (#987) --- fixtures/coverall/tests/bindings/test_coverall.swift | 4 ++-- .../src/bindings/swift/templates/ErrorTemplate.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fixtures/coverall/tests/bindings/test_coverall.swift b/fixtures/coverall/tests/bindings/test_coverall.swift index 5ad16360bc..5d23212168 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.swift +++ b/fixtures/coverall/tests/bindings/test_coverall.swift @@ -115,8 +115,8 @@ do { do { let _ = try coveralls.maybeThrowComplex(input: 3) fatalError("should have thrown") - } catch UniffiInternalError.rustPanic(let message) { - assert(message == "Invalid input") + } catch { + assert(String(describing: error) == "rustPanic(\"Invalid input\")") } } diff --git a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift index 053a9df6c1..c2ffe2fdc5 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift @@ -1,6 +1,6 @@ // An error type for FFI errors. These errors occur at the UniFFI level, not // the library level. -public enum UniffiInternalError: LocalizedError { +fileprivate enum UniffiInternalError: LocalizedError { case bufferOverflow case incompleteData case unexpectedOptionalTag From 0a28de66495cc3925d90e60b887fa394664e6301 Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Mon, 26 Jul 2021 17:35:13 +1000 Subject: [PATCH 28/45] Restructure Swift bindings for easier megazording. This re-arranges the way the Swift bindings are generated to make a bit more of a deliberate split between the low-level and high-level code. The intention is to make it easier for us to compile the Rust code for multiple components together into a single build artifact, while distributing the Swift code as individual Swift package artifacts. I've also taken the opportunity to add some more general docs on how the Swift backend works. Fixes https://mozilla-hub.atlassian.net/browse/SYNC-2679. --- .gitignore | 2 + CHANGELOG.md | 11 ++ docs/manual/src/SUMMARY.md | 1 + docs/manual/src/swift/module.md | 29 ++-- docs/manual/src/swift/overview.md | 44 ++++++ docs/manual/src/swift/xcode.md | 6 +- examples/app/ios/IOSApp-Bridging-Header.h | 4 +- .../app/ios/IOSApp.xcodeproj/project.pbxproj | 18 +-- uniffi_bindgen/src/bindings/gecko_js/mod.rs | 1 - uniffi_bindgen/src/bindings/kotlin/mod.rs | 1 - uniffi_bindgen/src/bindings/mod.rs | 13 +- uniffi_bindgen/src/bindings/python/mod.rs | 1 - uniffi_bindgen/src/bindings/ruby/mod.rs | 1 - .../src/bindings/swift/gen_swift.rs | 117 ++++++++++----- uniffi_bindgen/src/bindings/swift/mod.rs | 139 ++++++++++++------ ...ging-Header.h => BridgingHeaderTemplate.h} | 0 .../templates/ModuleMapTemplate.modulemap | 4 +- .../bindings/swift/templates/wrapper.swift | 10 +- uniffi_bindgen/src/lib.rs | 3 +- 19 files changed, 269 insertions(+), 136 deletions(-) create mode 100644 docs/manual/src/swift/overview.md rename uniffi_bindgen/src/bindings/swift/templates/{Template-Bridging-Header.h => BridgingHeaderTemplate.h} (100%) diff --git a/.gitignore b/.gitignore index 8e498ea51a..7f80b69346 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ target .vscode xcuserdata docs/manual/src/internals/api +examples/app/ios/Generated +examples/app/ios/IOSApp.xcodeproj/project.xcworkspace/xcshareddata/ diff --git a/CHANGELOG.md b/CHANGELOG.md index a45617576e..cd7cd6ee9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,17 @@ [All changes in [[UnreleasedVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.12.0...HEAD). ### ⚠️ Breaking Changes ⚠️ +- The Swift bindings now explicitly generate two separate Swift modules, one for + the high-level Swift code and one for the low-level C FFI. This change is intended + to simplify distribution of the bindings via Swift packages, but brings with it + some changes to the generated file layout. + - For an interface namespace "example", we now generate: + - A bridged C module named "exampleFFI" containing the low-level C FFI, + consisting of an `exampleFFI.h` file and matching `exampleFFI.modulemap` + file. The name can be customized using the `ffi_module_name` config option. + - A Swift module named "example" containing the high-level Swift bindings, + which imports and uses the low-level C FFI. The name can be customized using + the `module_name` config option. - Python timestamps will now be in UTC and timezone-aware rather than naive. - Replaced `lower_into_buffer()` and `try_lift_from_buffer()` with the `RustBufferViaFfi` trait. If you use those functions in your custom ViaFfi diff --git a/docs/manual/src/SUMMARY.md b/docs/manual/src/SUMMARY.md index 14556fcea7..f47b77da2d 100644 --- a/docs/manual/src/SUMMARY.md +++ b/docs/manual/src/SUMMARY.md @@ -22,6 +22,7 @@ # Swift +- [Overview](./swift/overview.md) - [Building a Swift module](./swift/module.md) - [Integrating with XCode](./swift/xcode.md) diff --git a/docs/manual/src/swift/module.md b/docs/manual/src/swift/module.md index c6d35fa9c6..bd433e630c 100644 --- a/docs/manual/src/swift/module.md +++ b/docs/manual/src/swift/module.md @@ -4,30 +4,21 @@ Before you can import the generated Swift bindings as a module (say, to use them from your application, or to try them out using `swift` on the command-line) you first need to compile them into a Swift module. -To do so, you'll need to crate a `.modulemap` file, which reassures Swift that it's -safe to expose the underlying C FFI layer. It should look something like this, -where `example` is the namespace as used in the `.udl` file: - -``` -module example { - header "path/to/uniffi_example-Bridging-Header.h" - export * -} -``` - -Then use `swiftc` to combine the cdylib from your Rust crate with the generated +To do so, you'll need both the generated `.swift` file and the corresponding +`.modulemap` file, which tells Swift how to expose the underlying C FFI layer. +Use `swiftc` to combine the cdylib from your Rust crate with the generated Swift bindings: ``` swiftc - -module-name example # Name for resulting Swift module - -emit-library -o libexample.dylib # File to link with if using Swift REPL - -emit-module -emit-module-path ./ # Output directory for resulting module + -module-name example # Name for resulting Swift module + -emit-library -o libexample.dylib # File to link with if using Swift REPL + -emit-module -emit-module-path ./ # Output directory for resulting module -parse-as-library - -L ./target/debug/ # Directory containing compiled Rust crate - -lexample # Name of compiled Rust crate cdylib - -Xcc -fmodule-map-file=example.modulemap # The modulemap file from above - example.swift # The generated bindings file + -L ./target/debug/ # Directory containing compiled Rust crate + -lexample # Name of compiled Rust crate cdylib + -Xcc -fmodule-map-file=exampleFFI.modulemap # The modulemap file from above + example.swift # The generated bindings file ``` This will produce an `example.swiftmodule` file that can be loaded by diff --git a/docs/manual/src/swift/overview.md b/docs/manual/src/swift/overview.md new file mode 100644 index 0000000000..8decd8c503 --- /dev/null +++ b/docs/manual/src/swift/overview.md @@ -0,0 +1,44 @@ +# Swift Bindings + +UniFFI ships with production-quality support for generating Swift bindings. +Concepts from the UDL file map into Swift as follows: + +* Primitive datatypes map to their obvious Swift counterpart, e.g. `u32` becomes `UInt32`, + `string` becomes `String`, etc. +* An object interface declared as `interface T` is represented as a Swift `protocol TProtocol` + and a concrete Swift `class T` that conforms to it. Having the protocol declared explicitly + can be useful for mocking instances of the class in unittests. +* A dictionary struct declared as `dictionary T` is represented as a Swift `struct T` + with public mutable fields. +* An enum declared `enum T` or `[Enum] interface T` is represented as a Swift + `enum T` with appropriate variants. +* Optional types are represented using Swift's builtin optional type syntax `T?`. +* Sequences are represented as Swift arrays, and Maps as Swift dictionaries. +* Errors are represented as Swift enums that conform to the `Error` protocol. +* Function calls that have an associated error type are marked with `throws`, + and hence must be called using one of Swift's `try` syntax variants. +* Failing assertions, Rust panics, and other unexpected errors in the generated code + are translated into a private enum conforming to the `Error` protocol. + * If this happens inside a throwing Swift function, it can be caught and handled + by a catch-all `catch` statement (but do so at your own risk, because it indicates + that something has gone seriously wrong). + * If this happens inside a non-throwing Swift function, it will be converted + into a fatal Swift error that cannot be caught. + +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. + +Splitting up the bindings in this way gives you flexibility over how both the Rust code +and the Swift code are distributed to consumers. For example, you may choose to compile +and distribute the Rust code for several UniFFI components as a single shared library +in order to reduce the compiled code size, while distributing their Swift wrappers as +individual modules. + +For more technical details on how the bindings work internally, please see the +[module documentation](./api/uniffi_bindgen/bindings/swift/index.html) diff --git a/docs/manual/src/swift/xcode.md b/docs/manual/src/swift/xcode.md index ad02fdcdd7..5402d1e044 100644 --- a/docs/manual/src/swift/xcode.md +++ b/docs/manual/src/swift/xcode.md @@ -36,7 +36,7 @@ how the generated files are used. * `$HOME/.cargo/bin/uniffi-bindgen generate $INPUT_FILE_PATH --language swift --out-dir $INPUT_FILE_DIR/Generated` * Add both the `.swift` file and the generated bridging header as output files: * `$(INPUT_FILE_DIR)/Generated/$(INPUT_FILE_BASE).swift` - * `$(INPUT_FILE_DIR)/Generated/uniffi_$(INPUT_FILE_BASE)-Bridging-Header.h` + * `$(INPUT_FILE_DIR)/Generated/$(INPUT_FILE_BASE)FFI.h` * Add your `.udl` file to the "Compile Sources" build phase for your framework, so that XCode will process it using the new build rule and will include the resulting outputs in the build. @@ -48,11 +48,11 @@ your .udl file. ## Including the bridging header -In the overall bridging header for your module, include the bridging header +In the overall bridging header for your module, include the header file generated by UniFFI in the previous step: ``` -#include "uniffi_example-Bridging-Header.h" +#include "exampleFFI.h" ``` For this to work without complaint from XCode, you also need to add the diff --git a/examples/app/ios/IOSApp-Bridging-Header.h b/examples/app/ios/IOSApp-Bridging-Header.h index 35087ee515..6748541ba4 100644 --- a/examples/app/ios/IOSApp-Bridging-Header.h +++ b/examples/app/ios/IOSApp-Bridging-Header.h @@ -6,7 +6,7 @@ #ifndef IOSApp_Bridging_Header_h #define IOSApp_Bridging_Header_h -#import "uniffi_arithmetic-Bridging-Header.h" -#import "uniffi_todolist-Bridging-Header.h" +#import "arithmeticFFI.h" +#import "todolistFFI.h" #endif /* IOSApp_Bridging_Header_h */ diff --git a/examples/app/ios/IOSApp.xcodeproj/project.pbxproj b/examples/app/ios/IOSApp.xcodeproj/project.pbxproj index ca27947a2c..9a138b5f16 100644 --- a/examples/app/ios/IOSApp.xcodeproj/project.pbxproj +++ b/examples/app/ios/IOSApp.xcodeproj/project.pbxproj @@ -16,8 +16,8 @@ 395A8829251A062D00562F33 /* IOSAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 395A8828251A062D00562F33 /* IOSAppTests.swift */; }; 395A8834251A062D00562F33 /* IOSAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 395A8833251A062D00562F33 /* IOSAppUITests.swift */; }; 39ADC3DF25E98792000EE485 /* libarithmetical.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 39ADC3DE25E98792000EE485 /* libarithmetical.a */; }; - 39ADD16D25E990CA000EE485 /* uniffi_arithmetic-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 39ADD16925E990CA000EE485 /* uniffi_arithmetic-Bridging-Header.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 39ADD16F25E990CA000EE485 /* uniffi_todolist-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 39ADD16B25E990CA000EE485 /* uniffi_todolist-Bridging-Header.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 39ADD16D25E990CA000EE485 /* arithmeticFFI.h in Headers */ = {isa = PBXBuildFile; fileRef = 39ADD16925E990CA000EE485 /* arithmeticFFI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 39ADD16F25E990CA000EE485 /* todolistFFI.h in Headers */ = {isa = PBXBuildFile; fileRef = 39ADD16B25E990CA000EE485 /* todolistFFI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CEEB5A0025264123003C87D1 /* libuniffi_todolist.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CEEB59FF25264123003C87D1 /* libuniffi_todolist.a */; }; /* End PBXBuildFile section */ @@ -31,7 +31,7 @@ ); isEditable = 1; outputFiles = ( - "$(SRCROOT)/Generated/uniffi_$(INPUT_FILE_BASE)-Bridging-Header.h", + "$(SRCROOT)/Generated/$(INPUT_FILE_BASE)FFI.h", "$(SRCROOT)/Generated/$(INPUT_FILE_BASE).swift", ); script = "# Generate swift bindings for the todolist rust library.\necho \"Generating files for $INPUT_FILE_PATH\"\n\"$SRCROOT/../../../target/debug/uniffi-bindgen\" generate \"$INPUT_FILE_PATH\" --language swift --out-dir \"$SRCROOT/Generated\"\necho \"Generated files for $INPUT_FILE_BASE in $SRCROOT/Generated\"\n"; @@ -71,9 +71,9 @@ 39ADC3DE25E98792000EE485 /* libarithmetical.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libarithmetical.a; path = ../../../target/universal/debug/libarithmetical.a; sourceTree = ""; }; 39ADD15E25E98F92000EE485 /* arithmetic.udl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = arithmetic.udl; path = ../../arithmetic/src/arithmetic.udl; sourceTree = ""; }; 39ADD16325E98FA5000EE485 /* todolist.udl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = todolist.udl; path = ../../todolist/src/todolist.udl; sourceTree = ""; }; - 39ADD16925E990CA000EE485 /* uniffi_arithmetic-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "uniffi_arithmetic-Bridging-Header.h"; sourceTree = ""; }; + 39ADD16925E990CA000EE485 /* arithmeticFFI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = arithmeticFFI.h; sourceTree = ""; }; 39ADD16A25E990CA000EE485 /* arithmetic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = arithmetic.swift; sourceTree = ""; }; - 39ADD16B25E990CA000EE485 /* uniffi_todolist-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "uniffi_todolist-Bridging-Header.h"; sourceTree = ""; }; + 39ADD16B25E990CA000EE485 /* todolistFFI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = todolistFFI.h; sourceTree = ""; }; 39ADD16C25E990CA000EE485 /* todolist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = todolist.swift; sourceTree = ""; }; 39C5D6D82524F95400BDBA8A /* IOSApp-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "IOSApp-Bridging-Header.h"; sourceTree = ""; }; CEEB59FF25264123003C87D1 /* libuniffi_todolist.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libuniffi_todolist.a; path = ../../../target/universal/debug/libuniffi_todolist.a; sourceTree = ""; }; @@ -172,9 +172,9 @@ 39ADD16825E990CA000EE485 /* Generated */ = { isa = PBXGroup; children = ( - 39ADD16925E990CA000EE485 /* uniffi_arithmetic-Bridging-Header.h */, + 39ADD16925E990CA000EE485 /* arithmeticFFI.h */, 39ADD16A25E990CA000EE485 /* arithmetic.swift */, - 39ADD16B25E990CA000EE485 /* uniffi_todolist-Bridging-Header.h */, + 39ADD16B25E990CA000EE485 /* todolistFFI.h */, 39ADD16C25E990CA000EE485 /* todolist.swift */, ); path = Generated; @@ -196,8 +196,8 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 39ADD16F25E990CA000EE485 /* uniffi_todolist-Bridging-Header.h in Headers */, - 39ADD16D25E990CA000EE485 /* uniffi_arithmetic-Bridging-Header.h in Headers */, + 39ADD16F25E990CA000EE485 /* todolistFFI.h in Headers */, + 39ADD16D25E990CA000EE485 /* arithmeticFFI.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/uniffi_bindgen/src/bindings/gecko_js/mod.rs b/uniffi_bindgen/src/bindings/gecko_js/mod.rs index accde618ba..3b4df5a497 100644 --- a/uniffi_bindgen/src/bindings/gecko_js/mod.rs +++ b/uniffi_bindgen/src/bindings/gecko_js/mod.rs @@ -53,7 +53,6 @@ pub fn write_bindings( ci: &ComponentInterface, out_dir: &Path, _try_format_code: bool, - _is_testing: bool, ) -> Result<()> { let out_path = PathBuf::from(out_dir); let bindings = generate_bindings(config, ci)?; diff --git a/uniffi_bindgen/src/bindings/kotlin/mod.rs b/uniffi_bindgen/src/bindings/kotlin/mod.rs index f3e9c35f6b..d4633138e2 100644 --- a/uniffi_bindgen/src/bindings/kotlin/mod.rs +++ b/uniffi_bindgen/src/bindings/kotlin/mod.rs @@ -22,7 +22,6 @@ pub fn write_bindings( ci: &ComponentInterface, out_dir: &Path, try_format_code: bool, - _is_testing: bool, ) -> Result<()> { let mut kt_file = full_bindings_path(config, out_dir)?; std::fs::create_dir_all(&kt_file)?; diff --git a/uniffi_bindgen/src/bindings/mod.rs b/uniffi_bindgen/src/bindings/mod.rs index 3be2893542..316989a784 100644 --- a/uniffi_bindgen/src/bindings/mod.rs +++ b/uniffi_bindgen/src/bindings/mod.rs @@ -112,7 +112,6 @@ pub fn write_bindings

( out_dir: P, language: TargetLanguage, try_format_code: bool, - is_testing: bool, ) -> Result<()> where P: AsRef, @@ -120,19 +119,17 @@ where let out_dir = out_dir.as_ref(); match language { TargetLanguage::Kotlin => { - kotlin::write_bindings(&config.kotlin, ci, out_dir, try_format_code, is_testing)? + kotlin::write_bindings(&config.kotlin, ci, out_dir, try_format_code)? } TargetLanguage::Swift => { - swift::write_bindings(&config.swift, ci, out_dir, try_format_code, is_testing)? + swift::write_bindings(&config.swift, ci, out_dir, try_format_code)? } TargetLanguage::Python => { - python::write_bindings(&config.python, ci, out_dir, try_format_code, is_testing)? - } - TargetLanguage::Ruby => { - ruby::write_bindings(&config.ruby, ci, out_dir, try_format_code, is_testing)? + python::write_bindings(&config.python, ci, out_dir, try_format_code)? } + TargetLanguage::Ruby => ruby::write_bindings(&config.ruby, ci, out_dir, try_format_code)?, TargetLanguage::GeckoJs => { - gecko_js::write_bindings(&config.gecko_js, ci, out_dir, try_format_code, is_testing)? + gecko_js::write_bindings(&config.gecko_js, ci, out_dir, try_format_code)? } } Ok(()) diff --git a/uniffi_bindgen/src/bindings/python/mod.rs b/uniffi_bindgen/src/bindings/python/mod.rs index 1bf66841d9..5febf8cf2b 100644 --- a/uniffi_bindgen/src/bindings/python/mod.rs +++ b/uniffi_bindgen/src/bindings/python/mod.rs @@ -25,7 +25,6 @@ pub fn write_bindings( ci: &ComponentInterface, out_dir: &Path, try_format_code: bool, - _is_testing: bool, ) -> Result<()> { let mut py_file = PathBuf::from(out_dir); py_file.push(format!("{}.py", ci.namespace())); diff --git a/uniffi_bindgen/src/bindings/ruby/mod.rs b/uniffi_bindgen/src/bindings/ruby/mod.rs index 59b323d52b..d3bc826b6a 100644 --- a/uniffi_bindgen/src/bindings/ruby/mod.rs +++ b/uniffi_bindgen/src/bindings/ruby/mod.rs @@ -25,7 +25,6 @@ pub fn write_bindings( ci: &ComponentInterface, out_dir: &Path, try_format_code: bool, - _is_testing: bool, ) -> Result<()> { let mut rb_file = PathBuf::from(out_dir); rb_file.push(format!("{}.rb", ci.namespace())); diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift.rs b/uniffi_bindgen/src/bindings/swift/gen_swift.rs index 2d270c5b68..f351364068 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift.rs @@ -2,8 +2,6 @@ * 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::path::Path; - use anyhow::Result; use askama::Template; use heck::{CamelCase, MixedCase}; @@ -12,28 +10,55 @@ use serde::{Deserialize, Serialize}; use crate::interface::*; use crate::MergeWith; -// Some 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`. - +/// 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 { - module_name: Option, cdylib_name: Option, + module_name: Option, + ffi_module_name: Option, + ffi_module_filename: Option, + generate_module_map: Option, } + 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.module_name()) + 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!("{}-Bridging-Header.h", self.module_name()) + 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() @@ -41,13 +66,19 @@ impl Config { "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) + } } impl From<&ComponentInterface> for Config { fn from(ci: &ComponentInterface) -> Self { Config { - module_name: Some(format!("uniffi_{}", ci.namespace())), + module_name: Some(ci.namespace().into()), cdylib_name: Some(format!("uniffi_{}", ci.namespace())), + ..Default::default() } } } @@ -56,13 +87,25 @@ 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), } } } +/// 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 = "Template-Bridging-Header.h")] +#[template(syntax = "c", escape = "none", path = "BridgingHeaderTemplate.h")] pub struct BridgingHeader<'config, 'ci> { _config: &'config Config, ci: &'ci ComponentInterface, @@ -77,48 +120,47 @@ impl<'config, 'ci> BridgingHeader<'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, 'header> { +pub struct ModuleMap<'config, 'ci> { config: &'config Config, _ci: &'ci ComponentInterface, - header: &'header Path, } -impl<'config, 'ci, 'header> ModuleMap<'config, 'ci, 'header> { - pub fn new( - config: &'config Config, - _ci: &'ci ComponentInterface, - header: &'header Path, - ) -> Self { - Self { - config, - _ci, - header, - } +impl<'config, 'ci> ModuleMap<'config, 'ci> { + pub fn new(config: &'config Config, _ci: &'ci ComponentInterface) -> Self { + Self { config, _ci } } } +/// Template for generating the `.modulemap` file that exposes the low-level C FFI. +/// +/// This file wraps the low-level C FFI from [`BridgingHeader`] into a more ergonomic, +/// higher-level Swift API. It's the part that knows about API-level concepts like +/// Objects and Records and so-forth. #[derive(Template)] #[template(syntax = "swift", escape = "none", path = "wrapper.swift")] pub struct SwiftWrapper<'config, 'ci> { config: &'config Config, ci: &'ci ComponentInterface, - is_testing: bool, } impl<'config, 'ci> SwiftWrapper<'config, 'ci> { - pub fn new(config: &'config Config, ci: &'ci ComponentInterface, is_testing: bool) -> Self { - Self { - config, - ci, - is_testing, - } + pub fn new(config: &'config Config, ci: &'ci ComponentInterface) -> Self { + Self { config, ci } } } -/// Filters for our Askama templates above. These output C (for the bridging -/// header) and Swift (for the actual library) declarations. +/// Filters for our Askama templates above. +/// +/// These output C (for the bridging header) and Swift (for the actual library) declarations, +/// mostly for dealing with types. mod filters { use super::*; use std::fmt; @@ -172,6 +214,7 @@ mod filters { }) } + /// Render a literal value for Swift code. pub fn literal_swift(literal: &Literal) -> Result { fn typed_number(type_: &Type, num_str: String) -> Result { Ok(match type_ { @@ -263,23 +306,23 @@ mod filters { } } + /// Render the idiomatic Swift casing for the name of an enum. pub fn enum_variant_swift(nm: &dyn fmt::Display) -> Result { Ok(nm.to_string().to_mixed_case()) } + /// Render the idiomatic Swift casing for the name of a class. pub fn class_name_swift(nm: &dyn fmt::Display) -> Result { Ok(nm.to_string().to_camel_case()) } + /// Render the idiomatic Swift casing for the name of a function. pub fn fn_name_swift(nm: &dyn fmt::Display) -> Result { Ok(nm.to_string().to_mixed_case()) } + /// Render the idiomatic Swift casing for the name of a variable. pub fn var_name_swift(nm: &dyn fmt::Display) -> Result { Ok(nm.to_string().to_mixed_case()) } - - pub fn header_path(path: &Path) -> Result { - Ok(path.to_str().expect("Invalid bridging header path").into()) - } } diff --git a/uniffi_bindgen/src/bindings/swift/mod.rs b/uniffi_bindgen/src/bindings/swift/mod.rs index 7dd8101b83..18d43ee23c 100644 --- a/uniffi_bindgen/src/bindings/swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/mod.rs @@ -2,6 +2,33 @@ * 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 anyhow::{anyhow, bail, Context, Result}; use std::{ ffi::OsString, @@ -16,51 +43,52 @@ pub use gen_swift::{BridgingHeader, Config, ModuleMap, SwiftWrapper}; use super::super::interface::ComponentInterface; +/// The Swift bindings generated from a [`ComponentInterface`]. +/// pub struct Bindings { - header: String, + /// 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, } -/// Generate uniffi component bindings for swift. +/// Write UniFFI component bindings for Swift as files on disk. /// -/// Unlike other target languages, binding to rust code from swift involves more than just +/// 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. +/// declarations, and a `.modulemap` file to tell Swift how to use it. pub fn write_bindings( config: &Config, ci: &ComponentInterface, out_dir: &Path, try_format_code: bool, - is_testing: bool, ) -> Result<()> { let out_path = PathBuf::from(out_dir); - let mut source_file = out_path.clone(); - source_file.push(format!("{}.swift", ci.namespace())); + let Bindings { + header, + library, + modulemap, + } = generate_bindings(config, ci)?; - let Bindings { header, library } = generate_bindings(config, ci, is_testing)?; + let mut source_file = out_path.clone(); + source_file.push(format!("{}.swift", config.module_name())); + let mut l = File::create(&source_file).context("Failed to create .swift file for bindings")?; + write!(l, "{}", library)?; - let header_filename = config.header_filename(); let mut header_file = out_path.clone(); - header_file.push(&header_filename); - + header_file.push(config.header_filename()); let mut h = File::create(&header_file).context("Failed to create .h file for bindings")?; write!(h, "{}", header)?; - let mut l = File::create(&source_file).context("Failed to create .swift file for bindings")?; - write!(l, "{}", library)?; - - if is_testing { - let mut module_map_file = out_path; - module_map_file.push(config.modulemap_filename()); - - let mut m = File::create(&module_map_file) + if let Some(modulemap) = modulemap { + let mut modulemap_file = out_path; + modulemap_file.push(config.modulemap_filename()); + let mut m = File::create(&modulemap_file) .context("Failed to create .modulemap file for bindings")?; - write!( - m, - "{}", - generate_module_map(config, ci, Path::new(&header_filename))? - )?; + write!(m, "{}", modulemap)?; } if try_format_code { @@ -79,38 +107,46 @@ pub fn write_bindings( Ok(()) } -/// Generate Swift bindings for the given ComponentInterface, as a string. -pub fn generate_bindings( - config: &Config, - ci: &ComponentInterface, - is_testing: bool, -) -> Result { +/// Generate UniFFI component bindings for Swift, as strings in memory. +/// +pub fn generate_bindings(config: &Config, ci: &ComponentInterface) -> Result { use askama::Template; let header = BridgingHeader::new(config, ci) .render() .map_err(|_| anyhow!("failed to render Swift bridging header"))?; - let library = SwiftWrapper::new(config, ci, is_testing) + let library = SwiftWrapper::new(config, ci) .render() .map_err(|_| anyhow!("failed to render Swift library"))?; - Ok(Bindings { header, library }) + let modulemap = if config.generate_module_map() { + Some( + ModuleMap::new(config, ci) + .render() + .map_err(|_| anyhow!("failed to render Swift modulemap"))?, + ) + } else { + None + }; + Ok(Bindings { + library, + header, + modulemap, + }) } -fn generate_module_map( - config: &Config, - ci: &ComponentInterface, - header_path: &Path, -) -> Result { - use askama::Template; - let module_map = ModuleMap::new(config, ci, header_path) - .render() - .map_err(|_| anyhow!("failed to render Swift module map"))?; - Ok(module_map) -} - -/// ... +/// Compile UniFFI component bindings for Swift for use from the `swift` command-line. +/// +/// This is a utility function to help with running Swift tests. While the `swift` command-line +/// tool is able to execute Swift code from source, that code can only load other Swift modules +/// if they have been pre-compiled into a `.dylib` and corresponding `.swiftmodule`. Since our +/// test scripts need to be able to import the generated bindings, we have to compile them +/// ahead of time before running the tests. +/// pub fn compile_bindings(config: &Config, ci: &ComponentInterface, out_dir: &Path) -> Result<()> { let out_path = PathBuf::from(out_dir); + if !config.generate_module_map() { + bail!("Cannot compile Swift bindings when `generate_module_map` is `false`") + } let mut module_map_file = out_path.clone(); module_map_file.push(config.modulemap_filename()); @@ -118,10 +154,10 @@ pub fn compile_bindings(config: &Config, ci: &ComponentInterface, out_dir: &Path module_map_file_option.push(module_map_file.as_os_str()); let mut source_file = out_path.clone(); - source_file.push(format!("{}.swift", ci.namespace())); + source_file.push(format!("{}.swift", config.module_name())); let mut dylib_file = out_path.clone(); - dylib_file.push(format!("lib{}.dylib", ci.namespace())); + dylib_file.push(format!("lib{}.dylib", config.module_name())); // `-emit-library -o ` generates a `.dylib`, so that we can use the // Swift module from the REPL. Otherwise, we'll get "Couldn't lookup @@ -154,10 +190,19 @@ pub fn compile_bindings(config: &Config, ci: &ComponentInterface, out_dir: &Path Ok(()) } +/// Run a Swift script, allowing it to load modules from the given output directory. +/// +/// This executes the given Swift script file in a way that allows it to import any other +/// Swift modules in the given output directory. The modules must have been pre-compiled +/// using the [`compile_bindings`] function. +/// pub fn run_script(out_dir: &Path, script_file: &Path) -> Result<()> { let mut cmd = Command::new("swift"); // Find any module maps and/or dylibs in the target directory, and tell swift to use them. + // Listing the directory like this is a little bit hacky - it would be nicer if we could tell + // Swift to load only the module(s) for the component under test, but the way we're calling + // this test function doesn't allow us to pass that name in to the call. cmd.arg("-I").arg(out_dir).arg("-L").arg(out_dir); for entry in PathBuf::from(out_dir) diff --git a/uniffi_bindgen/src/bindings/swift/templates/Template-Bridging-Header.h b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h similarity index 100% rename from uniffi_bindgen/src/bindings/swift/templates/Template-Bridging-Header.h rename to uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h diff --git a/uniffi_bindgen/src/bindings/swift/templates/ModuleMapTemplate.modulemap b/uniffi_bindgen/src/bindings/swift/templates/ModuleMapTemplate.modulemap index 7502cda18d..f5f73ceb1b 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ModuleMapTemplate.modulemap +++ b/uniffi_bindgen/src/bindings/swift/templates/ModuleMapTemplate.modulemap @@ -1,6 +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.module_name() }} { - header "{{ header|header_path }}" +module {{ config.ffi_module_name() }} { + header "{{ config.header_filename() }}" export * } diff --git a/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift b/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift index 2fccb9b70e..a052b7bfa5 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift @@ -2,9 +2,13 @@ // Trust me, you don't want to mess with it! import Foundation -{% if is_testing -%} -import {{ config.module_name() }} -{% endif -%} + +// 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 "RustBufferHelper.swift" %} diff --git a/uniffi_bindgen/src/lib.rs b/uniffi_bindgen/src/lib.rs index 10200bc5f0..86d3761279 100644 --- a/uniffi_bindgen/src/lib.rs +++ b/uniffi_bindgen/src/lib.rs @@ -167,7 +167,6 @@ pub fn generate_bindings>( &out_dir, language.try_into()?, try_format_code, - false, )?; } Ok(()) @@ -202,7 +201,7 @@ pub fn run_tests>( } for (lang, test_scripts) in language_tests { - bindings::write_bindings(&config.bindings, &component, &cdylib_dir, lang, true, true)?; + bindings::write_bindings(&config.bindings, &component, &cdylib_dir, lang, true)?; bindings::compile_bindings(&config.bindings, &component, &cdylib_dir, lang)?; for test_script in test_scripts { bindings::run_script(cdylib_dir, &test_script, lang)?; From e602e11d1466e2ba4f409484871848c272ac90b0 Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Mon, 2 Aug 2021 13:08:22 +1000 Subject: [PATCH 29/45] Remove the unused Gecko-JS bindings. A lot of excellent work went into making working Gecko-JS bindings, but the fact is that we're not using them and we expect to have to make non-trivial changes to the way they're architected before we can use this in Desktop Firefox for real. Given that they're becoming a maintenance burding during unrelated refactors of UniFFI, I think we're better off removing them until we take another concerted run at integrating into Desktop Firefox. We can get them out of the git history if and when we need them. --- docs/manual/src/Overview.md | 2 +- .../tests/gecko_js/test_rondpoint.js | 257 ------ .../rondpoint/tests/gecko_js/xpcshell.ini | 1 - uniffi_bindgen/askama.toml | 11 +- .../src/bindings/gecko_js/gen_gecko_js.rs | 615 -------------- uniffi_bindgen/src/bindings/gecko_js/mod.rs | 135 --- .../templates/FFIDeclarationsTemplate.h | 43 - .../templates/InterfaceHeaderTemplate.h | 61 -- .../gecko_js/templates/InterfaceTemplate.cpp | 83 -- .../templates/NamespaceHeaderTemplate.h | 33 - .../gecko_js/templates/NamespaceTemplate.cpp | 25 - .../gecko_js/templates/RustBufferHelper.h | 780 ------------------ .../gecko_js/templates/SharedHeaderTemplate.h | 91 -- .../gecko_js/templates/WebIDLTemplate.webidl | 104 --- .../bindings/gecko_js/templates/macros.cpp | 64 -- .../src/bindings/gecko_js/webidl.rs | 449 ---------- uniffi_bindgen/src/bindings/mod.rs | 12 - uniffi_bindgen/src/lib.rs | 2 +- 18 files changed, 3 insertions(+), 2765 deletions(-) delete mode 100644 examples/rondpoint/tests/gecko_js/test_rondpoint.js delete mode 100644 examples/rondpoint/tests/gecko_js/xpcshell.ini delete mode 100644 uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs delete mode 100644 uniffi_bindgen/src/bindings/gecko_js/mod.rs delete mode 100644 uniffi_bindgen/src/bindings/gecko_js/templates/FFIDeclarationsTemplate.h delete mode 100644 uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceHeaderTemplate.h delete mode 100644 uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceTemplate.cpp delete mode 100644 uniffi_bindgen/src/bindings/gecko_js/templates/NamespaceHeaderTemplate.h delete mode 100644 uniffi_bindgen/src/bindings/gecko_js/templates/NamespaceTemplate.cpp delete mode 100644 uniffi_bindgen/src/bindings/gecko_js/templates/RustBufferHelper.h delete mode 100644 uniffi_bindgen/src/bindings/gecko_js/templates/SharedHeaderTemplate.h delete mode 100644 uniffi_bindgen/src/bindings/gecko_js/templates/WebIDLTemplate.webidl delete mode 100644 uniffi_bindgen/src/bindings/gecko_js/templates/macros.cpp delete mode 100644 uniffi_bindgen/src/bindings/gecko_js/webidl.rs diff --git a/docs/manual/src/Overview.md b/docs/manual/src/Overview.md index 69cda31717..a9e13a98cd 100644 --- a/docs/manual/src/Overview.md +++ b/docs/manual/src/Overview.md @@ -16,4 +16,4 @@ This .udl (UniFFI Definition Language) file, whose definitions must match with t - Kotlin - Swift - Python -- [Gecko](https://en.wikipedia.org/wiki/Gecko_(software)) C++ (Work in Progress) +- Ruby diff --git a/examples/rondpoint/tests/gecko_js/test_rondpoint.js b/examples/rondpoint/tests/gecko_js/test_rondpoint.js deleted file mode 100644 index d87aadd703..0000000000 --- a/examples/rondpoint/tests/gecko_js/test_rondpoint.js +++ /dev/null @@ -1,257 +0,0 @@ -/* - * This file is an xpcshell test that exercises the Rondpoint binding in - * Firefox. Non-Gecko JS consumers can safely ignore it. - * - * If you're working on the Gecko JS bindings, you'll want to either copy or - * symlink this folder into m-c, and add the `xpcshell.ini` manifest in this - * folder to an `XPCSHELL_TESTS_MANIFESTS` section in the `moz.build` file - * that includes the generated bindings. - * - * Currently, this must be done manually, though we're looking at ways to - * run `uniffi-bindgen` as part of the Firefox build, and keep the UniFFI - * bindings tests in the tree. https://github.com/mozilla/uniffi-rs/issues/272 - * has more details. - */ - -add_task(async function test_rondpoint() { - deepEqual( - Rondpoint.copieDictionnaire({ - un: "deux", - deux: true, - petitNombre: 0, - grosNombre: 123456789, - }), - { - un: "deux", - deux: true, - petitNombre: 0, - grosNombre: 123456789, - } - ); - equal(Rondpoint.copieEnumeration("deux"), "deux"); - deepEqual(Rondpoint.copieEnumerations(["un", "deux"]), ["un", "deux"]); - deepEqual( - Rondpoint.copieCarte({ - 1: "un", - 2: "deux", - }), - { - 1: "un", - 2: "deux", - } - ); - ok(Rondpoint.switcheroo(false)); -}); - -add_task(async function test_retourneur() { - let rt = new Retourneur(); - - // Booleans. - [true, false].forEach((v) => strictEqual(rt.identiqueBoolean(v), v)); - - // Bytes. - [-128, 127].forEach((v) => equal(rt.identiqueI8(v), v)); - [0x00, 0xff].forEach((v) => equal(rt.identiqueU8(v), v)); - - // Shorts. - [-Math.pow(2, 15), Math.pow(2, 15) - 1].forEach((v) => - equal(rt.identiqueI16(v), v) - ); - [0, 0xffff].forEach((v) => equal(rt.identiqueU16(v), v)); - - // Ints. - [0, 1, -1, -Math.pow(2, 31), Math.pow(2, 31) - 1].forEach((v) => - equal(rt.identiqueI32(v), v) - ); - [0, Math.pow(2, 32) - 1].forEach((v) => equal(rt.identiqueU32(v), v)); - - // Longs. - [0, 1, -1, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER].forEach((v) => - equal(rt.identiqueI64(v), v) - ); - [0, 1, Number.MAX_SAFE_INTEGER].forEach((v) => equal(rt.identiqueU64(v), v)); - - // Floats. - [0, 1, 0.25].forEach((v) => equal(rt.identiqueFloat(v), v)); - - // Doubles. - [0, 1, 0.25].forEach((v) => equal(rt.identiqueDouble(v), v)); - - // Strings. - [ - "", - "abc", - "null\0byte", - "été", - "ښي لاس ته لوستلو لوستل", - "😻emoji 👨‍👧‍👦multi-emoji, 🇨🇭a flag, a canal, panama", - ].forEach((v) => equal(rt.identiqueString(v), v)); - - [-1, 0, 1].forEach((v) => { - let dict = { - petitNombre: v, - courtNombre: v, - nombreSimple: v, - grosNombre: v, - }; - deepEqual(rt.identiqueNombresSignes(dict), dict); - }); - - [0, 1].forEach((v) => { - let dict = { - petitNombre: v, - courtNombre: v, - nombreSimple: v, - grosNombre: v, - }; - deepEqual(rt.identiqueNombres(dict), dict); - }); -}); - -add_task(async function test_stringifier() { - let st = new Stringifier(); - - let wellKnown = st.wellKnownString("firefox"); - equal(wellKnown, "uniffi 💚 firefox!"); - - let table = { - toStringBoolean: [ - [true, "true"], - [false, "false"], - ], - toStringI8: [ - [-128, "-128"], - [127, "127"], - ], - toStringU8: [ - [0x00, "0"], - [0xff, "255"], - ], - toStringI16: [ - [-Math.pow(2, 15), "-32768"], - [Math.pow(2, 15) - 1, "32767"], - ], - toStringU16: [ - [0, "0"], - [0xffff, "65535"], - ], - toStringI32: [ - [0, "0"], - [1, "1"], - [-1, "-1"], - [-Math.pow(2, 31), "-2147483648"], - [Math.pow(2, 31) - 1, "2147483647"], - ], - toStringU32: [ - [0, "0"], - [Math.pow(2, 32) - 1, "4294967295"], - ], - toStringI64: [ - [0, "0"], - [1, "1"], - [-1, "-1"], - [Number.MIN_SAFE_INTEGER, "-9007199254740991"], - [Number.MAX_SAFE_INTEGER, "9007199254740991"], - ], - toStringU64: [ - [0, "0"], - [1, "1"], - [Number.MAX_SAFE_INTEGER, "9007199254740991"], - ], - toStringFloat: [ - [0, "0"], - [1, "1"], - [0.25, "0.25"], - ], - toStringDouble: [ - [0, "0"], - [1, "1"], - [0.25, "0.25"], - ], - }; - for (let method in table) { - for (let [v, expected] of table[method]) { - strictEqual(st[method](v), expected); - } - } -}); - -add_task(async function test_optionneur() { - // Step 1: call the methods without arguments, and check against the UDL. - - let op = new Optionneur(); - - equal(op.sinonString(), "default"); - strictEqual(op.sinonBoolean(), false); - deepEqual(op.sinonSequence(), []); - - // Nullables. - strictEqual(op.sinonNull(), null); - strictEqual(op.sinonZero(), 0); - - // Decimal integers. - equal(op.sinonI8Dec(), -42); - equal(op.sinonU8Dec(), 42); - equal(op.sinonI16Dec(), 42); - equal(op.sinonU16Dec(), 42); - equal(op.sinonI32Dec(), 42); - equal(op.sinonU32Dec(), 42); - equal(op.sinonI64Dec(), 42); - equal(op.sinonU64Dec(), 42); - - // Hexadecimal integers. - equal(op.sinonI8Hex(), -0x7f); - equal(op.sinonU8Hex(), 0xff); - equal(op.sinonI16Hex(), 0x7f); - equal(op.sinonU16Hex(), 0xffff); - equal(op.sinonI32Hex(), 0x7fffffff); - equal(op.sinonU32Hex(), 0xffffffff); - - // Octal integers. - equal(op.sinonU32Oct(), 493); - - // Floats. - equal(op.sinonF32(), 42); - equal(op.sinonF64(), 42.1); - - // Enums. - equal(op.sinonEnum(), "trois"); - - // Step 2. Convince ourselves that if we pass something else, then that - // changes the output. - - let table = { - sinonString: ["foo", "bar"], - sinonBoolean: [true, false], - sinonNull: ["0", "1"], - sinonZero: [0, 1], - sinonU8Dec: [0, 1], - sinonI8Dec: [0, 1], - sinonU16Dec: [0, 1], - sinonI16Dec: [0, 1], - sinonU32Dec: [0, 1], - sinonI32Dec: [0, 1], - sinonU64Dec: [0, 1], - sinonI64Dec: [0, 1], - sinonU8Hex: [0, 1], - sinonI8Hex: [0, 1], - sinonU16Hex: [0, 1], - sinonI16Hex: [0, 1], - sinonU32Hex: [0, 1], - sinonI32Hex: [0, 1], - sinonU64Hex: [0, 1], - sinonI64Hex: [0, 1], - sinonU32Oct: [0, 1], - sinonF32: [0, 1], - sinonF64: [0, 1], - sinonEnum: ["un", "deux", "trois"], - }; - for (let method in table) { - for (let v of table[method]) { - strictEqual(op[method](v), v); - } - } - [["a", "b"], []].forEach((v) => { - deepEqual(op.sinonSequence(v), v); - }); -}); diff --git a/examples/rondpoint/tests/gecko_js/xpcshell.ini b/examples/rondpoint/tests/gecko_js/xpcshell.ini deleted file mode 100644 index 6fc0739ca4..0000000000 --- a/examples/rondpoint/tests/gecko_js/xpcshell.ini +++ /dev/null @@ -1 +0,0 @@ -[test_rondpoint.js] diff --git a/uniffi_bindgen/askama.toml b/uniffi_bindgen/askama.toml index 00e7784530..a0aae5f41b 100644 --- a/uniffi_bindgen/askama.toml +++ b/uniffi_bindgen/askama.toml @@ -1,6 +1,6 @@ [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/gecko_js/templates", "src/bindings/ruby/templates" ] +dirs = [ "src/scaffolding/templates", "src/bindings/kotlin/templates", "src/bindings/python/templates", "src/bindings/swift/templates", "src/bindings/ruby/templates" ] [[syntax]] name = "kt" @@ -17,14 +17,5 @@ name = "c" [[syntax]] name = "rs" -[[syntax]] -name = "webidl" - -[[syntax]] -name = "xpidl" - -[[syntax]] -name = "cpp" - [[syntax]] name = "rb" diff --git a/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs b/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs deleted file mode 100644 index 5e68983d33..0000000000 --- a/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs +++ /dev/null @@ -1,615 +0,0 @@ -/* 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::Cow; - -use anyhow::Result; -use askama::Template; -use heck::{CamelCase, MixedCase}; -use serde::{Deserialize, Serialize}; - -use crate::interface::*; -use crate::MergeWith; - -use super::webidl::{ - ArgumentExt, CPPArgument, ConstructorExt, FieldExt, FunctionExt, MethodExt, ReturnBy, ThrowBy, - WebIDLType, -}; - -/// Config options for the generated Firefox front-end bindings. Note that this -/// can only be used to control details *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 { - /// Specifies an optional prefix to use for all definitions (interfaces, - /// dictionaries, enums, and namespaces) in the generated Firefox WebIDL - /// binding. If a prefix is not specified, the Firefox WebIDL definitions - /// will use the same names as the UDL. - /// - /// For example, if the prefix is `Hola`, and the UDL for the component - /// declares `namespace foo`, `dictionary Bar`, and `interface Baz`, the - /// definitions will be exposed in Firefox WebIDL as `HolaFoo`, `HolaBar`, - /// and `HolaBaz`. - /// - /// This option exists because definition names all share a global - /// namespace (further, all WebIDL namespaces, interfaces, and enums are - /// exposed on `window`), so they must be unique. Firefox will fail to - /// compile if two different WebIDL files declare interfaces, dictionaries, - /// enums, or namespaces with the same name. - /// - /// For this reason, web standards often prefix their definitions: for - /// example, the dictionary to create a `PushSubscription` is called - /// `PushSubscriptionOptionsInit`, not just `Init`. For UniFFI components, - /// prefixing definitions in UDL would make it awkward to consume from other - /// languages that _do_ have namespaces. - /// - /// So we expose the prefix as an option for just Gecko JS bindings. - pub definition_prefix: Option, -} - -impl From<&ComponentInterface> for Config { - fn from(_ci: &ComponentInterface) -> Self { - Config::default() - } -} - -impl MergeWith for Config { - fn merge_with(&self, other: &Self) -> Self { - Config { - definition_prefix: self.definition_prefix.merge_with(&other.definition_prefix), - } - } -} - -/// A context associates config options with a component interface, and provides -/// helper methods that are shared between all templates and filters in this -/// module. -#[derive(Clone, Copy)] -pub struct Context<'config, 'ci> { - config: &'config Config, - ci: &'ci ComponentInterface, -} - -impl<'config, 'ci> Context<'config, 'ci> { - /// Creates a new context with options for the given component interface. - pub fn new(config: &'config Config, ci: &'ci ComponentInterface) -> Self { - Context { config, ci } - } - - /// Returns the `RustBuffer` type name. - /// - /// A `RustBuffer` is a Plain Old Data struct that holds a pointer to a - /// Rust byte buffer, along with its length and capacity. Because the - /// generated binding for each component declares its own FFI symbols in an - /// `extern "C"` block, the `RustBuffer` type name must be unique for each - /// component. - /// - /// Declaring multiple types with the same name in an `extern "C"` block, - /// even if they're in different header files, will fail the build because - /// it violates the One Definition Rule. - pub fn ffi_rustbuffer_type(&self) -> String { - format!("{}_RustBuffer", self.ci.ffi_namespace()) - } - - /// Returns the `ForeignBytes` type name. - /// - /// `ForeignBytes` is a Plain Old Data struct that holds a pointer to some - /// memory allocated by C++, along with its length. See the docs for - /// `ffi_rustbuffer_type` about why this type name must be unique for each - /// component. - pub fn ffi_foreignbytes_type(&self) -> String { - format!("{}_ForeignBytes", self.ci.ffi_namespace()) - } - - /// Returns the `RustCallStatus` type name. - /// - /// A `RustCallStatus` is a Plain Old Data struct that holds an call status code and - /// `RustBuffer`. See the docs for `ffi_rustbuffer_type` about why this type name must be - /// unique for each component. - pub fn ffi_rustcallstatus_type(&self) -> String { - format!("{}_RustCallStatus", self.ci.ffi_namespace()) - } - - /// Returns the name to use for the `detail` C++ namespace, which contains - /// the serialization helpers and other internal types. This name must be - /// unique for each component. - pub fn detail_name(&self) -> String { - format!("{}_detail", self.ci.namespace()) - } - - /// Returns the unprefixed, unmodified component namespace name. This is - /// exposed for convenience, where a template has access to the context but - /// not the component interface. - pub fn namespace(&self) -> &'ci str { - self.ci.namespace() - } - - /// Returns the type name to use for an interface, dictionary, enum, or - /// namespace with the given `ident` in the generated WebIDL and C++ code. - pub fn type_name<'a>(&self, ident: &'a str) -> Cow<'a, str> { - // Prepend the definition prefix if there is one; otherwise, just pass - // the name back as-is. - match self.config.definition_prefix.as_ref() { - Some(prefix) => Cow::Owned(format!("{}{}", prefix, ident)), - None => Cow::Borrowed(ident), - } - } - - /// Returns the C++ header or source file name to use for the given - /// WebIDL interface or namespace name. - pub fn header_name(&self, ident: &str) -> String { - self.type_name(ident).to_camel_case() - } -} - -/// A template for a Firefox WebIDL file. We only generate one of these per -/// component. -#[derive(Template)] -#[template(syntax = "webidl", escape = "none", path = "WebIDLTemplate.webidl")] -pub struct WebIDL<'config, 'ci> { - context: Context<'config, 'ci>, - ci: &'ci ComponentInterface, -} - -impl<'config, 'ci> WebIDL<'config, 'ci> { - pub fn new(context: Context<'config, 'ci>, ci: &'ci ComponentInterface) -> Self { - Self { context, ci } - } -} - -/// A shared header file that's included by all our bindings. This defines -/// common serialization logic and `extern` declarations for the FFI. These -/// namespace and interface source files `#include` this file. -#[derive(Template)] -#[template(syntax = "c", escape = "none", path = "SharedHeaderTemplate.h")] -pub struct SharedHeader<'config, 'ci> { - context: Context<'config, 'ci>, - ci: &'ci ComponentInterface, -} - -impl<'config, 'ci> SharedHeader<'config, 'ci> { - pub fn new(context: Context<'config, 'ci>, ci: &'ci ComponentInterface) -> Self { - Self { context, ci } - } -} - -/// A header file generated for a namespace containing top-level functions. If -/// the namespace in the UDL file is empty, this file isn't generated. -#[derive(Template)] -#[template(syntax = "c", escape = "none", path = "NamespaceHeaderTemplate.h")] -pub struct NamespaceHeader<'config, 'ci> { - context: Context<'config, 'ci>, - functions: &'ci [Function], -} - -impl<'config, 'ci> NamespaceHeader<'config, 'ci> { - pub fn new(context: Context<'config, 'ci>, functions: &'ci [Function]) -> Self { - Self { context, functions } - } -} - -/// An implementation file for a namespace with top-level functions. If the -/// namespace in the UDL is empty, this isn't generated. -#[derive(Template)] -#[template(syntax = "cpp", escape = "none", path = "NamespaceTemplate.cpp")] -pub struct Namespace<'config, 'ci> { - context: Context<'config, 'ci>, - functions: &'ci [Function], -} - -impl<'config, 'ci> Namespace<'config, 'ci> { - pub fn new(context: Context<'config, 'ci>, functions: &'ci [Function]) -> Self { - Self { context, functions } - } -} - -/// A header file generated for each interface in the UDL. -#[derive(Template)] -#[template(syntax = "c", escape = "none", path = "InterfaceHeaderTemplate.h")] -pub struct InterfaceHeader<'config, 'ci> { - context: Context<'config, 'ci>, - obj: &'ci Object, -} - -impl<'config, 'ci> InterfaceHeader<'config, 'ci> { - pub fn new(context: Context<'config, 'ci>, obj: &'ci Object) -> Self { - Self { context, obj } - } -} - -/// An implementation file generated for each interface in the UDL. -#[derive(Template)] -#[template(syntax = "cpp", escape = "none", path = "InterfaceTemplate.cpp")] -pub struct Interface<'config, 'ci> { - context: Context<'config, 'ci>, - obj: &'ci Object, -} - -impl<'config, 'ci> Interface<'config, 'ci> { - pub fn new(context: Context<'config, 'ci>, obj: &'ci Object) -> Self { - Self { context, obj } - } -} - -/// Filters for our Askama templates above. These output C++ and WebIDL. -mod filters { - use super::*; - - /// Declares a WebIDL type. - /// - /// Terminology clarification: UDL, the `ComponentInterface`, - /// and Firefox's WebIDL use different but overlapping names for - /// the same types. - /// - /// * `Type::Record` is called a "dictionary" in Firefox WebIDL. It's - /// represented as `dictionary T` in UDL and WebIDL. - /// * `Type::Object` is called an "interface" in Firefox WebIDL. It's - /// represented as `interface T` in UDL and WebIDL. - /// * `Type::Optional` is called "nullable" in Firefox WebIDL. It's - /// represented as `T?` in UDL and WebIDL. - /// * `Type::Map` is called a "record" in Firefox WebIDL. It's represented - /// as `record` in UDL, and `record` in - /// WebIDL. - /// - /// There are also semantic differences: - /// - /// * In UDL, all `dictionary` members are required by default; in - /// WebIDL, they're all optional. The generated WebIDL file adds a - /// `required` keyword to each member. - /// * In UDL, an argument can specify a default value directly. - /// In WebIDL, arguments with default values must have the `optional` - /// keyword. - pub fn type_webidl( - type_: &WebIDLType, - context: &Context<'_, '_>, - ) -> Result { - Ok(match type_ { - WebIDLType::Flat(Type::Int8) => "byte".into(), - WebIDLType::Flat(Type::UInt8) => "octet".into(), - WebIDLType::Flat(Type::Int16) => "short".into(), - WebIDLType::Flat(Type::UInt16) => "unsigned short".into(), - WebIDLType::Flat(Type::Int32) => "long".into(), - WebIDLType::Flat(Type::UInt32) => "unsigned long".into(), - WebIDLType::Flat(Type::Int64) => "long long".into(), - WebIDLType::Flat(Type::UInt64) => "unsigned long long".into(), - WebIDLType::Flat(Type::Float32) => "float".into(), - // Note: Not `unrestricted double`; we don't want to allow NaNs - // and infinity. - WebIDLType::Flat(Type::Float64) => "double".into(), - WebIDLType::Flat(Type::Boolean) => "boolean".into(), - WebIDLType::Flat(Type::String) => "DOMString".into(), - WebIDLType::Flat(Type::Enum(name)) - | WebIDLType::Flat(Type::Record(name)) - | WebIDLType::Flat(Type::Object(name)) => class_name_webidl(name, context)?, - WebIDLType::Nullable(inner) => format!("{}?", type_webidl(inner, context)?), - WebIDLType::Optional(inner) | WebIDLType::OptionalWithDefaultValue(inner) => { - type_webidl(inner, context)? - } - WebIDLType::Sequence(inner) => format!("sequence<{}>", type_webidl(inner, context)?), - WebIDLType::Map(inner) => { - format!("record", type_webidl(inner, context)?) - } - _ => unreachable!("type_webidl({:?})", type_), - }) - } - - /// Emits a literal default value for WebIDL. - pub fn literal_webidl(literal: &Literal) -> Result { - Ok(match literal { - Literal::Boolean(v) => format!("{}", v), - Literal::String(s) => format!("\"{}\"", s), - Literal::Null => "null".into(), - Literal::EmptySequence => "[]".into(), - Literal::EmptyMap => "{}".into(), - Literal::Enum(v, _) => format!("\"{}\"", enum_variant_webidl(v)?), - Literal::Int(i, radix, _) => match radix { - Radix::Octal => format!("0{:o}", i), - Radix::Decimal => format!("{}", i), - Radix::Hexadecimal => format!("{:#x}", i), - }, - Literal::UInt(i, radix, _) => match radix { - Radix::Octal => format!("0{:o}", i), - Radix::Decimal => format!("{}", i), - Radix::Hexadecimal => format!("{:#x}", i), - }, - Literal::Float(string, _) => string.into(), - }) - } - - /// Declares a C type in the `extern` declarations. - pub fn type_ffi(type_: &FFIType, context: &Context<'_, '_>) -> Result { - Ok(match 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 => unimplemented!("object pointers are not implemented"), - FFIType::RustBuffer => context.ffi_rustbuffer_type(), - FFIType::ForeignBytes => context.ffi_foreignbytes_type(), - FFIType::ForeignCallback => unimplemented!("Callback interfaces are not implemented"), - }) - } - - /// Declares a C++ type. - pub fn type_cpp( - type_: &WebIDLType, - context: &Context<'_, '_>, - ) -> Result { - Ok(match type_ { - WebIDLType::Flat(Type::Int8) => "int8_t".into(), - WebIDLType::Flat(Type::UInt8) => "uint8_t".into(), - WebIDLType::Flat(Type::Int16) => "int16_t".into(), - WebIDLType::Flat(Type::UInt16) => "uint16_t".into(), - WebIDLType::Flat(Type::Int32) => "int32_t".into(), - WebIDLType::Flat(Type::UInt32) => "uint32_t".into(), - WebIDLType::Flat(Type::Int64) => "int64_t".into(), - WebIDLType::Flat(Type::UInt64) => "uint64_t".into(), - WebIDLType::Flat(Type::Float32) => "float".into(), - WebIDLType::Flat(Type::Float64) => "double".into(), - WebIDLType::Flat(Type::Boolean) => "bool".into(), - WebIDLType::Flat(Type::String) => "nsString".into(), - WebIDLType::Flat(Type::Enum(name)) | WebIDLType::Flat(Type::Record(name)) => { - class_name_cpp(name, context)? - } - WebIDLType::Flat(Type::Object(name)) => { - format!("OwningNonNull<{}>", class_name_cpp(name, context)?) - } - WebIDLType::Nullable(inner) => { - // Nullable objects become `RefPtr` (instead of - // `OwningNonNull`); all others become `Nullable`. - match inner.as_ref() { - WebIDLType::Flat(Type::Object(name)) => { - format!("RefPtr<{}>", class_name_cpp(name, context)?) - } - WebIDLType::Flat(Type::String) => "nsString".into(), - _ => format!("Nullable<{}>", type_cpp(inner, context)?), - } - } - WebIDLType::OptionalWithDefaultValue(inner) => type_cpp(inner, context)?, - WebIDLType::Optional(inner) => format!("Optional<{}>", type_cpp(inner, context)?), - WebIDLType::Sequence(inner) => format!("nsTArray<{}>", type_cpp(inner, context)?), - WebIDLType::Map(inner) => format!("Record", type_cpp(inner, context)?), - _ => unreachable!("type_cpp({:?})", type_), - }) - } - - fn in_arg_type_cpp( - type_: &WebIDLType, - context: &Context<'_, '_>, - ) -> Result { - Ok(match type_ { - WebIDLType::Nullable(inner) => match inner.as_ref() { - WebIDLType::Flat(Type::Object(_)) | WebIDLType::Flat(Type::String) => { - type_cpp(type_, context)? - } - _ => format!("Nullable<{}>", in_arg_type_cpp(inner, context)?), - }, - WebIDLType::Sequence(inner) => { - format!("Sequence<{}>", in_arg_type_cpp(inner, context)?) - } - _ => type_cpp(type_, context)?, - }) - } - - /// Declares a C++ in or out argument type. - pub fn arg_type_cpp( - arg: &CPPArgument<'_>, - context: &Context<'_, '_>, - ) -> Result { - Ok(match arg { - CPPArgument::GlobalObject => "GlobalObject&".into(), - CPPArgument::ErrorResult => "ErrorResult&".into(), - CPPArgument::In(arg) => { - // In arguments are usually passed by `const` reference for - // object types, and by value for primitives. As an exception, - // `nsString` becomes `nsAString` when passed as an argument, - // and nullable objects are passed as pointers. Sequences map - // to the `Sequence` type, not `nsTArray`. - match arg.webidl_type() { - WebIDLType::Flat(Type::String) => "const nsAString&".into(), - WebIDLType::Flat(Type::Object(name)) => { - format!("{}&", class_name_cpp(&name, context)?) - } - WebIDLType::Nullable(inner) => match inner.as_ref() { - WebIDLType::Flat(Type::String) => "const nsAString&".into(), - WebIDLType::Flat(Type::Object(name)) => { - format!("{}*", class_name_cpp(name, context)?) - } - _ => format!("const {}&", in_arg_type_cpp(&arg.webidl_type(), context)?), - }, - WebIDLType::Flat(Type::Record(_)) - | WebIDLType::Map(_) - | WebIDLType::Sequence(_) - | WebIDLType::Optional(_) - | WebIDLType::OptionalWithDefaultValue(_) => { - format!("const {}&", in_arg_type_cpp(&arg.webidl_type(), context)?) - } - _ => in_arg_type_cpp(&arg.webidl_type(), context)?, - } - } - CPPArgument::Out(type_) => { - // Out arguments are usually passed by reference. `nsString` - // becomes `nsAString`. - match type_ { - WebIDLType::Flat(Type::String) => "nsAString&".into(), - WebIDLType::Nullable(inner) => match inner.as_ref() { - WebIDLType::Flat(Type::String) => "nsAString&".into(), - _ => format!("{}&", type_cpp(type_, context)?), - }, - _ => format!("{}&", type_cpp(type_, context)?), - } - } - }) - } - - /// Declares a C++ return type. - pub fn ret_type_cpp( - type_: &WebIDLType, - context: &Context<'_, '_>, - ) -> Result { - Ok(match type_ { - WebIDLType::Flat(Type::Object(name)) => { - format!("already_AddRefed<{}>", class_name_cpp(name, context)?) - } - WebIDLType::Nullable(inner) => match inner.as_ref() { - WebIDLType::Flat(Type::Object(name)) => { - format!("already_AddRefed<{}>", class_name_cpp(name, context)?) - } - _ => type_cpp(type_, context)?, - }, - _ => type_cpp(type_, context)?, - }) - } - - /// Generates a dummy value for a given return type. A C++ function that - /// declares a return type must return some value of that type, even if it - /// throws a DOM exception via the `ErrorResult`. - pub fn dummy_ret_value_cpp( - return_type: &WebIDLType, - context: &Context<'_, '_>, - ) -> Result { - Ok(match return_type { - WebIDLType::Flat(Type::Int8) - | WebIDLType::Flat(Type::UInt8) - | WebIDLType::Flat(Type::Int16) - | WebIDLType::Flat(Type::UInt16) - | WebIDLType::Flat(Type::Int32) - | WebIDLType::Flat(Type::UInt32) - | WebIDLType::Flat(Type::Int64) - | WebIDLType::Flat(Type::UInt64) => "0".into(), - WebIDLType::Flat(Type::Float32) => "0.0f".into(), - WebIDLType::Flat(Type::Float64) => "0.0".into(), - WebIDLType::Flat(Type::Boolean) => "false".into(), - WebIDLType::Flat(Type::Enum(name)) => { - format!("{}::EndGuard_", class_name_cpp(name, context)?) - } - WebIDLType::Flat(Type::Object(_)) => "nullptr".into(), - WebIDLType::Flat(Type::String) => "EmptyString()".into(), - WebIDLType::OptionalWithDefaultValue(inner) => { - dummy_ret_value_cpp(inner.as_ref(), context)? - } - WebIDLType::Optional(_) - | WebIDLType::Nullable(_) - | WebIDLType::Flat(Type::Record(_)) - | WebIDLType::Map(_) - | WebIDLType::Sequence(_) => format!("{}()", type_cpp(return_type, context)?), - _ => unreachable!("dummy_ret_value_cpp({:?})", return_type), - }) - } - - /// Generates an expression for lowering a C++ type into a C type when - /// calling an FFI function. - pub fn lower_cpp( - type_: &WebIDLType, - from: &str, - context: &Context<'_, '_>, - ) -> Result { - let (lifted, nullable) = match type_ { - // Since our in argument type is `nsAString`, we need to use that - // to instantiate `ViaFfi`, not `nsString`. - WebIDLType::Flat(Type::String) => ("nsAString".into(), false), - WebIDLType::OptionalWithDefaultValue(_) => (type_cpp(type_, context)?, true), - WebIDLType::Nullable(inner) => match inner.as_ref() { - WebIDLType::Flat(Type::String) => ("nsAString".into(), true), - _ => (in_arg_type_cpp(type_, context)?, false), - }, - _ => (in_arg_type_cpp(type_, context)?, false), - }; - Ok(format!( - "{}::ViaFfi<{}, {}, {}>::Lower({})", - context.detail_name(), - lifted, - type_ffi(&FFIType::from(type_), context)?, - nullable, - from - )) - } - - /// Generates an expression for lifting a C return type from the FFI into a - /// C++ out parameter. - pub fn lift_cpp( - type_: &WebIDLType, - from: &str, - into: &str, - context: &Context<'_, '_>, - ) -> Result { - let (lifted, nullable) = match type_ { - // Out arguments are also `nsAString`, so we need to use it for the - // instantiation. - WebIDLType::Flat(Type::String) => ("nsAString".into(), false), - WebIDLType::OptionalWithDefaultValue(_) => (type_cpp(type_, context)?, true), - WebIDLType::Nullable(inner) => match inner.as_ref() { - WebIDLType::Flat(Type::String) => ("nsAString".into(), true), - _ => (type_cpp(type_, context)?, false), - }, - _ => (type_cpp(type_, context)?, false), - }; - Ok(format!( - "{}::ViaFfi<{}, {}, {}>::Lift({}, {})", - context.detail_name(), - lifted, - type_ffi(&FFIType::from(type_), context)?, - nullable, - from, - into, - )) - } - - pub fn var_name_webidl(nm: &str) -> Result { - Ok(nm.to_mixed_case()) - } - - pub fn enum_variant_webidl(nm: &str) -> Result { - Ok(nm.to_mixed_case()) - } - - pub fn header_name_cpp(nm: &str, context: &Context<'_, '_>) -> Result { - Ok(context.header_name(nm)) - } - - /// Declares an interface, dictionary, enum, or namespace name in WebIDL. - pub fn class_name_webidl(nm: &str, context: &Context<'_, '_>) -> Result { - Ok(context.type_name(nm).to_camel_case()) - } - - /// Declares a class name in C++. - pub fn class_name_cpp(nm: &str, context: &Context<'_, '_>) -> Result { - Ok(context.type_name(nm).to_camel_case()) - } - - /// Declares a method name in WebIDL. - pub fn fn_name_webidl(nm: &str) -> Result { - Ok(nm.to_string().to_mixed_case()) - } - - /// Declares a class or instance method name in C++. Function and methods - /// names are UpperCamelCase in C++, even though they're mixedCamelCase in - /// WebIDL. - pub fn fn_name_cpp(nm: &str) -> Result { - Ok(nm.to_string().to_camel_case()) - } - - /// `Codegen.py` emits field names as `mFieldName`. The `m` prefix is Gecko - /// style for struct members. - pub fn field_name_cpp(nm: &str) -> Result { - Ok(format!("m{}", nm.to_camel_case())) - } - - pub fn enum_variant_cpp(nm: &str) -> Result { - // TODO: Make sure this does the right thing for hyphenated variants - // (https://github.com/mozilla/uniffi-rs/issues/294), or the generated - // code won't compile. - // - // Example: "bookmark-added" should become `Bookmark_added`, because - // that's what Firefox's `Codegen.py` spits out. - Ok(nm.to_camel_case()) - } -} diff --git a/uniffi_bindgen/src/bindings/gecko_js/mod.rs b/uniffi_bindgen/src/bindings/gecko_js/mod.rs deleted file mode 100644 index 3b4df5a497..0000000000 --- a/uniffi_bindgen/src/bindings/gecko_js/mod.rs +++ /dev/null @@ -1,135 +0,0 @@ -/* 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::{ - fs::File, - io::Write, - path::{Path, PathBuf}, -}; - -use anyhow::{Context, Result}; - -pub mod gen_gecko_js; -mod webidl; -pub use gen_gecko_js::{ - Config, Interface, InterfaceHeader, Namespace, NamespaceHeader, SharedHeader, WebIDL, -}; - -use super::super::interface::ComponentInterface; - -pub struct Binding { - name: String, - contents: String, -} - -/// Generate uniffi component bindings for Firefox. -/// -/// Firefox's WebIDL binding declarations, generated by `Codegen.py` in m-c, -/// expect to find a `.h`/`.cpp` pair per interface, even if those interfaces -/// are declared in a single WebIDL file. Dictionaries and enums are -/// autogenerated by `Codegen.py`, so we don't need to worry about them...but -/// we do need to emit serialization code for them, plus the actual interface -/// and top-level function implementations, in the UniFFI bindings. -/// -/// So the Gecko backend generates: -/// -/// * A single WebIDL file with the component interface. This is similar to the -/// UniFFI UDL format, but the names of some types are different. -/// * A shared C++ header, with serialization helpers for all built-in and -/// interface types. -/// * A header and source file for the namespace, if the component defines any -/// top-level functions. -/// * A header and source file for each `interface` declaration in the UDL. -/// -/// These files should be checked in to the Firefox source tree. The WebIDL -/// file goes in `dom/chrome-webidl`, and the header and source files can be -/// added to any directory and referenced in `moz.build`. The Rust component -/// library must also be added as a dependency to `gkrust-shared` (in -/// `toolkit/library/rust/shared`), so that the FFI symbols are linked into -/// libxul. -pub fn write_bindings( - config: &Config, - ci: &ComponentInterface, - out_dir: &Path, - _try_format_code: bool, -) -> Result<()> { - let out_path = PathBuf::from(out_dir); - let bindings = generate_bindings(config, ci)?; - for binding in bindings { - let mut file = out_path.clone(); - file.push(&binding.name); - let mut f = File::create(&file) - .with_context(|| format!("Failed to create file `{}`", binding.name))?; - write!(f, "{}", binding.contents)?; - } - Ok(()) -} - -/// Generate Gecko bindings for the given ComponentInterface, as a string. -pub fn generate_bindings(config: &Config, ci: &ComponentInterface) -> Result> { - use askama::Template; - - let mut bindings = Vec::new(); - - let context = gen_gecko_js::Context::new(config, ci); - - let webidl = WebIDL::new(context, ci) - .render() - .context("Failed to render WebIDL bindings")?; - bindings.push(Binding { - name: format!("{}.webidl", context.header_name(context.namespace())), - contents: webidl, - }); - - let shared_header = SharedHeader::new(context, ci) - .render() - .context("Failed to render shared header")?; - bindings.push(Binding { - name: format!("{}Shared.h", context.header_name(context.namespace())), - contents: shared_header, - }); - - // Top-level functions go in one namespace, which needs its own header and - // source file. - let functions = ci.iter_function_definitions(); - if !functions.is_empty() { - let header = NamespaceHeader::new(context, functions.as_slice()) - .render() - .context("Failed to render top-level namespace header")?; - bindings.push(Binding { - name: format!("{}.h", context.header_name(context.namespace())), - contents: header, - }); - - let source = Namespace::new(context, functions.as_slice()) - .render() - .context("Failed to render top-level namespace binding")?; - bindings.push(Binding { - name: format!("{}.cpp", context.header_name(context.namespace())), - contents: source, - }); - } - - // Now generate one header/source pair for each interface. - let objects = ci.iter_object_definitions(); - for obj in objects { - let header = InterfaceHeader::new(context, &obj) - .render() - .with_context(|| format!("Failed to render {} header", obj.name()))?; - bindings.push(Binding { - name: format!("{}.h", context.header_name(obj.name())), - contents: header, - }); - - let source = Interface::new(context, &obj) - .render() - .with_context(|| format!("Failed to render {} binding", obj.name()))?; - bindings.push(Binding { - name: format!("{}.cpp", context.header_name(obj.name())), - contents: source, - }) - } - - Ok(bindings) -} diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/FFIDeclarationsTemplate.h b/uniffi_bindgen/src/bindings/gecko_js/templates/FFIDeclarationsTemplate.h deleted file mode 100644 index 02753d60ba..0000000000 --- a/uniffi_bindgen/src/bindings/gecko_js/templates/FFIDeclarationsTemplate.h +++ /dev/null @@ -1,43 +0,0 @@ -extern "C" { - -struct {{ context.ffi_rustbuffer_type() }} { - int32_t mCapacity; - int32_t mLen; - uint8_t* mData; - - // Ref https://github.com/mozilla/uniffi-rs/issues/334 re mPadding workaround - int64_t mPadding; -}; - -struct {{ context.ffi_foreignbytes_type() }} { - int32_t mLen; - const uint8_t* mData; - - // Ref https://github.com/mozilla/uniffi-rs/issues/334 re padding workarounds - int64_t mPadding; - int32_t mPadding2; -}; - -struct {{ context.ffi_rustcallstatus_type() }} { - int32_t mCode; - {{ context.ffi_rustbuffer_type() }} mErrorBuf; -}; - -{% for func in ci.iter_ffi_function_definitions() -%} -{%- match func.return_type() -%} -{%- when Some with (type_) %} -{{ type_|type_ffi(context) }} -{% when None %} -void -{%- endmatch %} -{{ func.name() }}( - {%- for arg in func.arguments() %} - {{ arg.type_()|type_ffi(context) }} {{ arg.name() -}}{%- if loop.last -%}{%- else -%},{%- endif -%} - {%- endfor %} - {%- if func.arguments().len() > 0 %},{% endif %} - {{ context.ffi_rustcallstatus_type() }}* uniffi_out_status -); - -{% endfor -%} - -} // extern "C" diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceHeaderTemplate.h b/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceHeaderTemplate.h deleted file mode 100644 index fc633bec46..0000000000 --- a/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceHeaderTemplate.h +++ /dev/null @@ -1,61 +0,0 @@ -// This file was autogenerated by some hot garbage in the `uniffi` crate. -// Trust me, you don't want to mess with it! - -{% import "macros.cpp" as cpp %} - -#ifndef mozilla_dom_{{ obj.name()|header_name_cpp(context) }} -#define mozilla_dom_{{ obj.name()|header_name_cpp(context) }} - -#include "jsapi.h" -#include "nsCOMPtr.h" -#include "nsIGlobalObject.h" -#include "nsWrapperCache.h" - -#include "mozilla/RefPtr.h" - -#include "mozilla/dom/{{ context.namespace()|header_name_cpp(context) }}Binding.h" - -namespace mozilla { -namespace dom { - -class {{ obj.name()|class_name_cpp(context) }} final : public nsISupports, public nsWrapperCache { - public: - NS_DECL_CYCLE_COLLECTING_ISUPPORTS - NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS({{ obj.name()|class_name_cpp(context) }}) - - {{ obj.name()|class_name_cpp(context) }}(nsIGlobalObject* aGlobal, uint64_t aHandle); - - JSObject* WrapObject(JSContext* aCx, - JS::Handle aGivenProto) override; - - nsIGlobalObject* GetParentObject() const { return mGlobal; } - - {%- for cons in obj.constructors() %} - - static already_AddRefed<{{ obj.name()|class_name_cpp(context) }}> Constructor( - {%- for arg in cons.cpp_arguments() %} - {{ arg|arg_type_cpp(context) }} {{ arg.name() }}{%- if !loop.last %},{% endif %} - {%- endfor %} - ); - {%- endfor %} - - {%- for meth in obj.methods() %} - - {% match meth.cpp_return_type() %}{% when Some with (type_) %}{{ type_|ret_type_cpp(context) }}{% else %}void{% endmatch %} {{ meth.name()|fn_name_cpp }}( - {%- for arg in meth.cpp_arguments() %} - {{ arg|arg_type_cpp(context) }} {{ arg.name() }}{%- if !loop.last %},{% endif %} - {%- endfor %} - ); - {%- endfor %} - - private: - ~{{ obj.name()|class_name_cpp(context) }}(); - - nsCOMPtr mGlobal; - uint64_t mHandle; -}; - -} // namespace dom -} // namespace mozilla - -#endif // mozilla_dom_{{ obj.name()|header_name_cpp(context) }} diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceTemplate.cpp b/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceTemplate.cpp deleted file mode 100644 index 50c8c61fcc..0000000000 --- a/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceTemplate.cpp +++ /dev/null @@ -1,83 +0,0 @@ -// This file was autogenerated by some hot garbage in the `uniffi` crate. -// Trust me, you don't want to mess with it! - -{% import "macros.cpp" as cpp %} - -#include "mozilla/dom/{{ obj.name()|header_name_cpp(context) }}.h" -#include "mozilla/dom/{{ context.namespace()|header_name_cpp(context) }}Shared.h" - -namespace mozilla { -namespace dom { - -// Cycle collection boilerplate for our interface implementation. `mGlobal` is -// the only member that needs to be cycle-collected; if we ever add any JS -// object members or other interfaces to the class, those should be collected, -// too. -NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE({{ obj.name()|class_name_cpp(context) }}, mGlobal) -NS_IMPL_CYCLE_COLLECTING_ADDREF({{ obj.name()|class_name_cpp(context) }}) -NS_IMPL_CYCLE_COLLECTING_RELEASE({{ obj.name()|class_name_cpp(context) }}) -NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION({{ obj.name()|class_name_cpp(context) }}) - NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY - NS_INTERFACE_MAP_ENTRY(nsISupports) -NS_INTERFACE_MAP_END - -{{ obj.name()|class_name_cpp(context) }}::{{ obj.name()|class_name_cpp(context) }}( - nsIGlobalObject* aGlobal, - uint64_t aHandle -) : mGlobal(aGlobal), mHandle(aHandle) {} - -{{ obj.name()|class_name_cpp(context) }}::~{{ obj.name()|class_name_cpp(context) }}() { - {{ context.ffi_rustcallstatus_type() }} status = {0, { 0, 0, nullptr, 0 }}; - {{ obj.ffi_object_free().name() }}(mHandle, &status); - MOZ_ASSERT(!status.mCode); -} - -JSObject* {{ obj.name()|class_name_cpp(context) }}::WrapObject( - JSContext* aCx, - JS::Handle aGivenProto -) { - return dom::{{ obj.name()|class_name_cpp(context) }}_Binding::Wrap(aCx, this, aGivenProto); -} - -{%- match obj.primary_constructor() %} -{%- when Some with (cons) %} - -/* static */ -already_AddRefed<{{ obj.name()|class_name_cpp(context) }}> {{ obj.name()|class_name_cpp(context) }}::Constructor( - {%- for arg in cons.cpp_arguments() %} - {{ arg|arg_type_cpp(context) }} {{ arg.name() }}{%- if !loop.last %},{% endif %} - {%- endfor %} -) { - {%- call cpp::to_ffi_call_head(context, cons, "err", "handle") %} - if (err.mCode) { - {%- match cons.cpp_throw_by() %} - {%- when ThrowBy::ErrorResult with (rv) %} - {{ rv }}.ThrowOperationError(nsDependentCString(err.mMessage)); - {%- when ThrowBy::Assert %} - MOZ_ASSERT(false); - {%- endmatch %} - return nullptr; - } - nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); - auto result = MakeRefPtr<{{ obj.name()|class_name_cpp(context) }}>(global, handle); - return result.forget(); -} -{%- when None %} -{%- endmatch %} - -{%- for cons in obj.alternate_constructors() %} -MOZ_STATIC_ASSERT(false, "Sorry the gecko-js backend does not yet support alternate constructors"); -{%- endfor %} - -{%- for meth in obj.methods() %} -{% match meth.cpp_return_type() %}{% when Some with (type_) %}{{ type_|ret_type_cpp(context) }}{% else %}void{% endmatch %} {{ obj.name()|class_name_cpp(context) }}::{{ meth.name()|fn_name_cpp }}( - {%- for arg in meth.cpp_arguments() %} - {{ arg|arg_type_cpp(context) }} {{ arg.name() }}{%- if !loop.last %},{% endif %} - {%- endfor %} -) { - {%- call cpp::to_ffi_call_with_prefix(context, "mHandle", meth) %} -} -{%- endfor %} - -} // namespace dom -} // namespace mozilla diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/NamespaceHeaderTemplate.h b/uniffi_bindgen/src/bindings/gecko_js/templates/NamespaceHeaderTemplate.h deleted file mode 100644 index e0cdeaa962..0000000000 --- a/uniffi_bindgen/src/bindings/gecko_js/templates/NamespaceHeaderTemplate.h +++ /dev/null @@ -1,33 +0,0 @@ -// This file was autogenerated by some hot garbage in the `uniffi` crate. -// Trust me, you don't want to mess with it! - -{% import "macros.cpp" as cpp %} - -#ifndef mozilla_dom_{{ context.namespace()|header_name_cpp(context) }} -#define mozilla_dom_{{ context.namespace()|header_name_cpp(context) }} - -#include "mozilla/dom/{{ context.namespace()|header_name_cpp(context) }}Binding.h" - -namespace mozilla { -namespace dom { - -class {{ context.namespace()|class_name_cpp(context) }} final { - public: - {{ context.namespace()|class_name_cpp(context) }}() = default; - ~{{ context.namespace()|class_name_cpp(context) }}() = default; - - {%- for func in functions %} - - static {% match func.cpp_return_type() %}{% when Some with (type_) %}{{ type_|ret_type_cpp(context) }}{% else %}void{% endmatch %} {{ func.name()|fn_name_cpp }}( - {%- for arg in func.cpp_arguments() %} - {{ arg|arg_type_cpp(context) }} {{ arg.name() }}{%- if !loop.last %},{% endif %} - {%- endfor %} - ); - - {%- endfor %} -}; - -} // namespace dom -} // namespace mozilla - -#endif // mozilla_dom_{{ context.namespace()|header_name_cpp(context) }} diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/NamespaceTemplate.cpp b/uniffi_bindgen/src/bindings/gecko_js/templates/NamespaceTemplate.cpp deleted file mode 100644 index d5837b2668..0000000000 --- a/uniffi_bindgen/src/bindings/gecko_js/templates/NamespaceTemplate.cpp +++ /dev/null @@ -1,25 +0,0 @@ -// This file was autogenerated by some hot garbage in the `uniffi` crate. -// Trust me, you don't want to mess with it! - -{% import "macros.cpp" as cpp %} - -#include "mozilla/dom/{{ context.namespace()|header_name_cpp(context) }}.h" -#include "mozilla/dom/{{ context.namespace()|header_name_cpp(context) }}Shared.h" - -namespace mozilla { -namespace dom { - -{%- for func in functions %} - -/* static */ -{% match func.cpp_return_type() %}{% when Some with (type_) %}{{ type_|ret_type_cpp(context) }}{% else %}void{% endmatch %} {{ context.namespace()|class_name_cpp(context) }}::{{ func.name()|fn_name_cpp }}( - {%- for arg in func.cpp_arguments() %} - {{ arg|arg_type_cpp(context) }} {{ arg.name() }}{%- if !loop.last %},{% endif %} - {%- endfor %} -) { - {%- call cpp::to_ffi_call(context, func) %} -} -{%- endfor %} - -} // namespace dom -} // namespace mozilla diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/RustBufferHelper.h b/uniffi_bindgen/src/bindings/gecko_js/templates/RustBufferHelper.h deleted file mode 100644 index 123001f76e..0000000000 --- a/uniffi_bindgen/src/bindings/gecko_js/templates/RustBufferHelper.h +++ /dev/null @@ -1,780 +0,0 @@ -namespace {{ context.detail_name() }} { - -/// Estimates the worst-case UTF-8 encoded length for a UTF-16 string. -CheckedInt EstimateUTF8Length(size_t aUTF16Length) { - // `ConvertUtf16toUtf8` expects the destination to have at least three times - // as much space as the source string, even if it doesn't use the excess - // capacity. - CheckedInt length(aUTF16Length); - length *= 3; - return length; -} - -/// Reads values out of a byte buffer received from Rust. -class MOZ_STACK_CLASS Reader final { - public: - explicit Reader(const {{ context.ffi_rustbuffer_type() }}& aBuffer) : mBuffer(aBuffer), mOffset(0) {} - - /// Returns `true` if there are unread bytes in the buffer, or `false` if the - /// current position has reached the end of the buffer. If `HasRemaining()` - /// returns `false`, attempting to read any more values from the buffer will - /// assert. - bool HasRemaining() { return mOffset.value() < mBuffer.mLen; } - - /// `Read{U}Int{8, 16, 32, 64}` read fixed-width integers from the buffer at - /// the current position. - - uint8_t ReadUInt8() { - return ReadAt( - [this](size_t aOffset) { return mBuffer.mData[aOffset]; }); - } - - int8_t ReadInt8() { return BitwiseCast(ReadUInt8()); } - - uint16_t ReadUInt16() { - return ReadAt([this](size_t aOffset) { - uint16_t value; - memcpy(&value, &mBuffer.mData[aOffset], sizeof(uint16_t)); - // `PR_ntohs` ("network to host, short") because the UniFFI serialization - // format encodes integers in big-endian order (also called - // "network byte order"). - return PR_ntohs(value); - }); - } - - int16_t ReadInt16() { return BitwiseCast(ReadUInt16()); } - - uint32_t ReadUInt32() { - return ReadAt([this](size_t aOffset) { - uint32_t value; - memcpy(&value, &mBuffer.mData[aOffset], sizeof(uint32_t)); - return PR_ntohl(value); - }); - } - - int32_t ReadInt32() { return BitwiseCast(ReadUInt32()); } - - uint64_t ReadUInt64() { - return ReadAt([this](size_t aOffset) { - uint64_t value; - memcpy(&value, &mBuffer.mData[aOffset], sizeof(uint64_t)); - return PR_ntohll(value); - }); - } - - int64_t ReadInt64() { return BitwiseCast(ReadUInt64()); } - - /// `Read{Float, Double}` reads a floating-point number from the buffer at - /// the current position. - - float ReadFloat() { return BitwiseCast(ReadUInt32()); } - - double ReadDouble() { return BitwiseCast(ReadUInt64()); } - - /// Reads a sequence or record length from the buffer at the current position. - size_t ReadLength() { - // The UniFFI serialization format uses signed integers for lengths. - auto length = ReadInt32(); - MOZ_RELEASE_ASSERT(length >= 0); - return static_cast(length); - } - - /// Reads a UTF-8 encoded string at the current position. - void ReadCString(nsACString& aValue) { - auto length = ReadInt32(); - CheckedInt newOffset = mOffset; - newOffset += length; - AssertInBounds(newOffset); - aValue.Append(AsChars(Span(&mBuffer.mData[mOffset.value()], length))); - mOffset = newOffset; - } - - /// Reads a UTF-16 encoded string at the current position. - void ReadString(nsAString& aValue) { - auto length = ReadInt32(); - CheckedInt newOffset = mOffset; - newOffset += length; - AssertInBounds(newOffset); - AppendUTF8toUTF16(AsChars(Span(&mBuffer.mData[mOffset.value()], length)), - aValue); - mOffset = newOffset; - } - - private: - void AssertInBounds(const CheckedInt& aNewOffset) const { - MOZ_RELEASE_ASSERT(aNewOffset.isValid() && - aNewOffset.value() <= mBuffer.mLen); - } - - template - T ReadAt(const std::function& aClosure) { - CheckedInt newOffset = mOffset; - newOffset += sizeof(T); - AssertInBounds(newOffset); - T result = aClosure(mOffset.value()); - mOffset = newOffset; - return result; - } - - const {{ context.ffi_rustbuffer_type() }}& mBuffer; - CheckedInt mOffset; -}; - -/// Writes values into a Rust buffer. -class MOZ_STACK_CLASS Writer final { - public: - Writer() { - {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; - mBuffer = {{ ci.ffi_rustbuffer_alloc().name() }}(0, &status); - if (status.mCode) { - MOZ_ASSERT(false, "Failed to allocate empty Rust buffer"); - } - } - - /// `Write{U}Int{8, 16, 32, 64}` write fixed-width integers into the buffer at - /// the current position. - - void WriteUInt8(const uint8_t& aValue) { - WriteAt(aValue, [](uint8_t* aBuffer, const uint8_t& aValue) { - *aBuffer = aValue; - }); - } - - void WriteInt8(const int8_t& aValue) { - WriteUInt8(BitwiseCast(aValue)); - } - - void WriteUInt16(const uint16_t& aValue) { - WriteAt(aValue, [](uint8_t* aBuffer, const uint16_t& aValue) { - // `PR_htons` ("host to network, short") because, as mentioned above, the - // UniFFI serialization format encodes integers in big-endian (network - // byte) order. - uint16_t value = PR_htons(aValue); - memcpy(aBuffer, &value, sizeof(uint16_t)); - }); - } - - void WriteInt16(const int16_t& aValue) { - WriteUInt16(BitwiseCast(aValue)); - } - - void WriteUInt32(const uint32_t& aValue) { - WriteAt(aValue, [](uint8_t* aBuffer, const uint32_t& aValue) { - uint32_t value = PR_htonl(aValue); - memcpy(aBuffer, &value, sizeof(uint32_t)); - }); - } - - void WriteInt32(const int32_t& aValue) { - WriteUInt32(BitwiseCast(aValue)); - } - - void WriteUInt64(const uint64_t& aValue) { - WriteAt(aValue, [](uint8_t* aBuffer, const uint64_t& aValue) { - uint64_t value = PR_htonll(aValue); - memcpy(aBuffer, &value, sizeof(uint64_t)); - }); - } - - void WriteInt64(const int64_t& aValue) { - WriteUInt64(BitwiseCast(aValue)); - } - - /// `Write{Float, Double}` writes a floating-point number into the buffer at - /// the current position. - - void WriteFloat(const float& aValue) { - WriteUInt32(BitwiseCast(aValue)); - } - - void WriteDouble(const double& aValue) { - WriteUInt64(BitwiseCast(aValue)); - } - - /// Writes a sequence or record length into the buffer at the current - /// position. - void WriteLength(size_t aValue) { - MOZ_RELEASE_ASSERT( - aValue <= static_cast(std::numeric_limits::max())); - WriteInt32(static_cast(aValue)); - } - - /// Writes a UTF-8 encoded string at the current offset. - void WriteCString(const nsACString& aValue) { - CheckedInt size(aValue.Length()); - size += sizeof(uint32_t); // For the length prefix. - MOZ_RELEASE_ASSERT(size.isValid()); - Reserve(size.value()); - - // Write the length prefix first... - uint32_t lengthPrefix = PR_htonl(aValue.Length()); - memcpy(&mBuffer.mData[mBuffer.mLen], &lengthPrefix, sizeof(uint32_t)); - - // ...Then the string. We just copy the string byte-for-byte into the - // buffer here; the Rust side of the FFI will ensure it's valid UTF-8. - memcpy(&mBuffer.mData[mBuffer.mLen + sizeof(uint32_t)], - aValue.BeginReading(), aValue.Length()); - - mBuffer.mLen += static_cast(size.value()); - } - - /// Writes a UTF-16 encoded string at the current offset. - void WriteString(const nsAString& aValue) { - auto maxSize = EstimateUTF8Length(aValue.Length()); - maxSize += sizeof(uint32_t); // For the length prefix. - MOZ_RELEASE_ASSERT(maxSize.isValid()); - Reserve(maxSize.value()); - - // Convert the string to UTF-8 first... - auto span = AsWritableChars(Span( - &mBuffer.mData[mBuffer.mLen + sizeof(uint32_t)], aValue.Length() * 3)); - size_t bytesWritten = ConvertUtf16toUtf8(aValue, span); - - // And then write the length prefix, with the actual number of bytes - // written. - uint32_t lengthPrefix = PR_htonl(bytesWritten); - memcpy(&mBuffer.mData[mBuffer.mLen], &lengthPrefix, sizeof(uint32_t)); - - mBuffer.mLen += static_cast(bytesWritten) + sizeof(uint32_t); - } - - /// Returns the buffer. - {{ context.ffi_rustbuffer_type() }} Buffer() { return mBuffer; } - - private: - /// Reserves the requested number of bytes in the Rust buffer, aborting on - /// allocation failure. - void Reserve(size_t aBytes) { - if (aBytes >= static_cast(std::numeric_limits::max())) { - NS_ABORT_OOM(aBytes); - } - {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; - {{ context.ffi_rustbuffer_type() }} newBuffer = {{ ci.ffi_rustbuffer_reserve().name() }}( - mBuffer, static_cast(aBytes), &status); - if (status.mCode) { - NS_ABORT_OOM(aBytes); - } - mBuffer = newBuffer; - } - - template - void WriteAt(const T& aValue, - const std::function& aClosure) { - Reserve(sizeof(T)); - aClosure(&mBuffer.mData[mBuffer.mLen], aValue); - mBuffer.mLen += sizeof(T); - } - - {{ context.ffi_rustbuffer_type() }} mBuffer; -}; - -/// A "trait" struct with specializations for types that can be read and -/// written into a byte buffer. This struct is specialized for all serializable -/// types. -template -struct Serializable { - /// Reads a value of type `T` from a byte buffer. - static bool ReadFrom(Reader& aReader, T& aValue) = delete; - - /// Writes a value of type `T` into a byte buffer. - static void WriteInto(Writer& aWriter, const T& aValue) = delete; -}; - -/// A "trait" with specializations for types that can be transferred back and -/// forth over the FFI. This is analogous to the Rust trait of the same name. -/// As above, this gives us compile-time type checking for type pairs. If -/// `ViaFfi::Lift(U, T)` compiles, we know that a value of type `U` from -/// the FFI can be lifted into a value of type `T`. -/// -/// The `Nullable` parameter is used to specialize nullable and non-null -/// strings, which have the same `T` and `FfiType`, but are represented -/// differently over the FFI. -template -struct ViaFfi { - /// Converts a low-level `FfiType`, which is a POD (Plain Old Data) type that - /// can be passed over the FFI, into a high-level type `T`. - /// - /// `T` is passed as an "out" parameter because some high-level types, like - /// dictionaries, can't be returned by value. - static bool Lift(const FfiType& aLowered, T& aLifted) = delete; - - /// Converts a high-level type `T` into a low-level `FfiType`. `FfiType` is - /// returned by value because it's guaranteed to be a POD, and because it - /// simplifies the `ViaFfi::Lower` calls that are generated for each argument - /// to an FFI function. - static FfiType Lower(const T& aLifted) = delete; -}; - -// This macro generates boilerplate specializations for primitive numeric types -// that are passed directly over the FFI without conversion. -#define UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(Type, readFunc, writeFunc) \ - template <> \ - struct Serializable { \ - [[nodiscard]] static bool ReadFrom(Reader& aReader, Type& aValue) { \ - aValue = aReader.readFunc(); \ - return true; \ - } \ - static void WriteInto(Writer& aWriter, const Type& aValue) { \ - aWriter.writeFunc(aValue); \ - } \ - }; \ - template <> \ - struct ViaFfi { \ - [[nodiscard]] static bool Lift(const Type& aLowered, Type& aLifted) { \ - aLifted = aLowered; \ - return true; \ - } \ - [[nodiscard]] static Type Lower(const Type& aLifted) { return aLifted; } \ - } - -UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(uint8_t, ReadUInt8, WriteUInt8); -UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(int8_t, ReadInt8, WriteInt8); -UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(uint16_t, ReadUInt16, WriteUInt16); -UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(int16_t, ReadInt16, WriteInt16); -UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(uint32_t, ReadUInt32, WriteUInt32); -UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(int32_t, ReadInt32, WriteInt32); -UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(uint64_t, ReadUInt64, WriteUInt64); -UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(int64_t, ReadInt64, WriteInt64); -UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(float, ReadFloat, WriteFloat); -UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(double, ReadDouble, WriteDouble); - -#undef UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE - -/// In the UniFFI serialization format, Booleans are passed as `int8_t`s over -/// the FFI. - -template <> -struct Serializable { - [[nodiscard]] static bool ReadFrom(Reader& aReader, bool& aValue) { - aValue = aReader.ReadInt8() != 0; - return true; - } - static void WriteInto(Writer& aWriter, const bool& aValue) { - aWriter.WriteInt8(aValue ? 1 : 0); - } -}; - -template <> -struct ViaFfi { - [[nodiscard]] static bool Lift(const int8_t& aLowered, bool& aLifted) { - aLifted = aLowered != 0; - return true; - } - [[nodiscard]] static int8_t Lower(const bool& aLifted) { - return aLifted ? 1 : 0; - } -}; - -/// Strings are length-prefixed and UTF-8 encoded when serialized -/// into Rust buffers, and are passed as UTF-8 encoded `RustBuffer`s over -/// the FFI. - -/// `ns{A}CString` is Gecko's "narrow" (8 bits per character) string type. -/// These don't have a fixed encoding: they can be ASCII, Latin-1, or UTF-8. -/// They're called `ByteString`s in WebIDL, and they're pretty uncommon compared -/// to `ns{A}String`. - -template <> -struct Serializable { - [[nodiscard]] static bool ReadFrom(Reader& aReader, nsACString& aValue) { - aReader.ReadCString(aValue); - return true; - } - static void WriteInto(Writer& aWriter, const nsACString& aValue) { - aWriter.WriteCString(aValue); - } -}; - -template <> -struct ViaFfi { - [[nodiscard]] static bool Lift(const {{ context.ffi_rustbuffer_type() }}& aLowered, - nsACString& aLifted) { - if (aLowered.mData) { - aLifted.Append(AsChars(Span(aLowered.mData, aLowered.mLen))); - {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; - {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &status); - if (status.mCode) { - MOZ_ASSERT(false, "Failed to lift `nsACString` from Rust buffer"); - return false; - } - } - return true; - } - - [[nodiscard]] static {{ context.ffi_rustbuffer_type() }} Lower(const nsACString& aLifted) { - MOZ_RELEASE_ASSERT( - aLifted.Length() <= - static_cast(std::numeric_limits::max())); - {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; - {{ context.ffi_foreignbytes_type() }} bytes = { - static_cast(aLifted.Length()), - reinterpret_cast(aLifted.BeginReading())}; - {{ context.ffi_rustbuffer_type() }} lowered = {{ ci.ffi_rustbuffer_from_bytes().name() }}(bytes, &status); - if (status.mCode) { - MOZ_ASSERT(false, "Failed to lower `nsACString` into Rust string"); - } - return lowered; - } -}; - -template <> -struct Serializable { - [[nodiscard]] static bool ReadFrom(Reader& aReader, nsCString& aValue) { - aReader.ReadCString(aValue); - return true; - } - static void WriteInto(Writer& aWriter, const nsCString& aValue) { - aWriter.WriteCString(aValue); - } -}; - -/// `ns{A}String` is Gecko's "wide" (16 bits per character) string type. -/// These are always UTF-16, so we need to convert them to UTF-8 before -/// passing them to Rust. WebIDL calls these `DOMString`s, and they're -/// ubiquitous. - -template <> -struct Serializable { - [[nodiscard]] static bool ReadFrom(Reader& aReader, nsAString& aValue) { - aReader.ReadString(aValue); - return true; - } - static void WriteInto(Writer& aWriter, const nsAString& aValue) { - aWriter.WriteString(aValue); - } -}; - -template <> -struct ViaFfi { - [[nodiscard]] static bool Lift(const {{ context.ffi_rustbuffer_type() }}& aLowered, - nsAString& aLifted) { - if (aLowered.mData) { - CopyUTF8toUTF16(AsChars(Span(aLowered.mData, aLowered.mLen)), aLifted); - {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; - {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &status); - if (status.mCode) { - MOZ_ASSERT(false, "Failed to lift `nsAString` from Rust buffer"); - return false; - } - } - return true; - } - - [[nodiscard]] static {{ context.ffi_rustbuffer_type() }} Lower(const nsAString& aLifted) { - auto maxSize = EstimateUTF8Length(aLifted.Length()); - MOZ_RELEASE_ASSERT( - maxSize.isValid() && - maxSize.value() <= - static_cast(std::numeric_limits::max())); - - {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; - {{ context.ffi_rustbuffer_type() }} lowered = {{ ci.ffi_rustbuffer_alloc().name() }}( - static_cast(maxSize.value()), &status); - if (status.mCode) { - MOZ_ASSERT(false, "Failed to lower `nsAString` into Rust string"); - } - - auto span = AsWritableChars(Span(lowered.mData, aLifted.Length() * 3)); - size_t bytesWritten = ConvertUtf16toUtf8(aLifted, span); - lowered.mLen = static_cast(bytesWritten); - - return lowered; - } -}; - -template <> -struct Serializable { - [[nodiscard]] static bool ReadFrom(Reader& aReader, nsString& aValue) { - aReader.ReadString(aValue); - return true; - } - static void WriteInto(Writer& aWriter, const nsString& aValue) { - aWriter.WriteString(aValue); - } -}; - -/// Nullable values are prefixed by a tag: 0 if none; 1 followed by the -/// serialized value if some. These are turned into Rust `Option`s. -/// -/// These are always serialized, never passed directly over the FFI. - -template -struct Serializable> { - [[nodiscard]] static bool ReadFrom(Reader& aReader, - dom::Nullable& aValue) { - auto hasValue = aReader.ReadInt8(); - if (hasValue != 0 && hasValue != 1) { - MOZ_ASSERT(false); - return false; - } - if (!hasValue) { - aValue = dom::Nullable(); - return true; - } - T value; - if (!Serializable::ReadFrom(aReader, value)) { - return false; - } - aValue = dom::Nullable(std::move(value)); - return true; - }; - - static void WriteInto(Writer& aWriter, const dom::Nullable& aValue) { - if (aValue.IsNull()) { - aWriter.WriteInt8(0); - } else { - aWriter.WriteInt8(1); - Serializable::WriteInto(aWriter, aValue.Value()); - } - } -}; - -/// Optionals are serialized the same way as nullables. The distinction -/// doesn't matter in UniFFI, because Rust doesn't have optional -/// arguments or struct fields, but does matter in WebIDL: nullable means -/// "was passed, but can be `null`," but optional means "may or may not -/// be passed." -/// -/// These are always serialized, never passed directly over the FFI. - -template -struct Serializable> { - [[nodiscard]] static bool ReadFrom(Reader& aReader, - dom::Optional& aValue) { - auto hasValue = aReader.ReadInt8(); - if (hasValue != 0 && hasValue != 1) { - MOZ_ASSERT(false); - return false; - } - if (!hasValue) { - return true; - } - T value; - if (!Serializable::ReadFrom(aReader, value)) { - return false; - } - aValue.Construct(std::move(value)); - return true; - }; - - static void WriteInto(Writer& aWriter, const dom::Optional& aValue) { - if (!aValue.WasPassed()) { - aWriter.WriteInt8(0); - } else { - aWriter.WriteInt8(1); - Serializable::WriteInto(aWriter, aValue.Value()); - } - } -}; - -/// Sequences are length-prefixed, followed by the serialization of each -/// element. They're always serialized, and never passed directly over the -/// FFI. -/// -/// WebIDL has two different representations for sequences, though they both -/// use `nsTArray` under the hood. `dom::Sequence` is for sequence -/// arguments; `nsTArray` is for sequence return values and dictionary -/// members. - -template -struct Serializable> { - // We leave `ReadFrom` unimplemented because sequences should only be - // lowered from the C++ WebIDL binding to the FFI. If the FFI function - // returns a sequence, it'll be lifted into an `nsTArray`, not a - // `dom::Sequence`. See the note about sequences above. - [[nodiscard]] static bool ReadFrom(Reader& aReader, - dom::Sequence& aValue) = delete; - - static void WriteInto(Writer& aWriter, const dom::Sequence& aValue) { - aWriter.WriteLength(aValue.Length()); - for (const T& element : aValue) { - Serializable::WriteInto(aWriter, element); - } - } -}; - -template -struct Serializable> { - [[nodiscard]] static bool ReadFrom(Reader& aReader, nsTArray& aValue) { - auto length = aReader.ReadLength(); - aValue.SetCapacity(length); - for (size_t i = 0; i < length; ++i) { - if (!Serializable::ReadFrom(aReader, *aValue.AppendElement())) { - return false; - } - } - return true; - }; - - static void WriteInto(Writer& aWriter, const nsTArray& aValue) { - aWriter.WriteLength(aValue.Length()); - for (const T& element : aValue) { - Serializable::WriteInto(aWriter, element); - } - } -}; - -/// Records are length-prefixed, followed by the serialization of each -/// key and value. They're always serialized, and never passed directly over the -/// FFI. - -template -struct Serializable> { - [[nodiscard]] static bool ReadFrom(Reader& aReader, Record& aValue) { - auto length = aReader.ReadLength(); - aValue.Entries().SetCapacity(length); - for (size_t i = 0; i < length; ++i) { - typename Record::EntryType* entry = - aValue.Entries().AppendElement(); - if (!Serializable::ReadFrom(aReader, entry->mKey)) { - return false; - } - if (!Serializable::ReadFrom(aReader, entry->mValue)) { - return false; - } - } - return true; - }; - - static void WriteInto(Writer& aWriter, const Record& aValue) { - aWriter.WriteLength(aValue.Entries().Length()); - for (const typename Record::EntryType& entry : aValue.Entries()) { - Serializable::WriteInto(aWriter, entry.mKey); - Serializable::WriteInto(aWriter, entry.mValue); - } - } -}; - -/// Partial specialization for all types that can be serialized into a byte -/// buffer. This is analogous to the `ViaFfiUsingByteBuffer` trait in Rust. - -template -struct ViaFfi { - [[nodiscard]] static bool Lift(const {{ context.ffi_rustbuffer_type() }}& aLowered, T& aLifted) { - auto reader = Reader(aLowered); - if (!Serializable::ReadFrom(reader, aLifted)) { - return false; - } - if (reader.HasRemaining()) { - MOZ_ASSERT(false); - return false; - } - {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; - {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &status); - if (status.mCode) { - MOZ_ASSERT(false, "Failed to free Rust buffer after lifting contents"); - return false; - } - return true; - } - - [[nodiscard]] static {{ context.ffi_rustbuffer_type() }} Lower(const T& aLifted) { - auto writer = Writer(); - Serializable::WriteInto(writer, aLifted); - return writer.Buffer(); - } -}; - -/// Nullable strings are a special case. In Gecko C++, there's no type-level -/// way to distinguish between nullable and non-null strings: the WebIDL -/// bindings layer passes `nsAString` for both `DOMString` and `DOMString?`. -/// But the Rust side of the FFI expects nullable strings to be serialized as -/// `Nullable`, not `nsA{C}String`. -/// -/// These specializations serialize nullable strings as if they were -/// `Nullable`. - -template <> -struct ViaFfi { - [[nodiscard]] static bool Lift(const {{ context.ffi_rustbuffer_type() }}& aLowered, - nsACString& aLifted) { - auto value = dom::Nullable(); - if (!ViaFfi, {{ context.ffi_rustbuffer_type() }}>::Lift(aLowered, value)) { - return false; - } - if (value.IsNull()) { - // `SetIsVoid` marks the string as "voided". The JS engine will reflect - // voided strings as `null`, not `""`. - aLifted.SetIsVoid(true); - } else { - aLifted = value.Value(); - } - return true; - } - - [[nodiscard]] static {{ context.ffi_rustbuffer_type() }} Lower(const nsACString& aLifted) { - auto value = dom::Nullable(); - if (!aLifted.IsVoid()) { - value.SetValue() = aLifted; - } - return ViaFfi, {{ context.ffi_rustbuffer_type() }}>::Lower(value); - } -}; - -template <> -struct ViaFfi { - [[nodiscard]] static bool Lift(const {{ context.ffi_rustbuffer_type() }}& aLowered, - nsAString& aLifted) { - auto value = dom::Nullable(); - if (!ViaFfi, {{ context.ffi_rustbuffer_type() }}>::Lift(aLowered, value)) { - return false; - } - if (value.IsNull()) { - aLifted.SetIsVoid(true); - } else { - aLifted = value.Value(); - } - return true; - } - - [[nodiscard]] static {{ context.ffi_rustbuffer_type() }} Lower(const nsAString& aLifted) { - auto value = dom::Nullable(); - if (!aLifted.IsVoid()) { - value.SetValue() = aLifted; - } - return ViaFfi, {{ context.ffi_rustbuffer_type() }}>::Lower(value); - } -}; - -/// Partial specialization for all non-null types on the C++ side that should be -/// serialized as if they were nullable. This is analogous to a blanket -/// implementation of `ViaFfiUsingByteBuffer` for `Option` in Rust. - -template -struct ViaFfi { - [[nodiscard]] static bool Lift(const {{ context.ffi_rustbuffer_type() }}& aLowered, - T& aLifted) { - auto reader = Reader(aLowered); - auto hasValue = reader.ReadInt8(); - if (hasValue != 0 && hasValue != 1) { - MOZ_ASSERT(false); - return false; - } - if (!hasValue) { - return true; - } - if (!Serializable::ReadFrom(reader, aLifted)) { - return false; - } - if (reader.HasRemaining()) { - MOZ_ASSERT(false); - return false; - } - {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0}}; - {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &status); - if (status.mCode) { - MOZ_ASSERT(false, "Failed to free Rust buffer after lifting contents"); - return false; - } - return true; - } - - [[nodiscard]] static {{ context.ffi_rustbuffer_type() }} Lower(const T& aLifted) { - auto writer = Writer(); - writer.WriteInt8(1); - Serializable::WriteInto(writer, aLifted); - return writer.Buffer(); - } -}; - -} // namespace {{ context.detail_name() }} diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/SharedHeaderTemplate.h b/uniffi_bindgen/src/bindings/gecko_js/templates/SharedHeaderTemplate.h deleted file mode 100644 index 1783f7fcd4..0000000000 --- a/uniffi_bindgen/src/bindings/gecko_js/templates/SharedHeaderTemplate.h +++ /dev/null @@ -1,91 +0,0 @@ -// This file was autogenerated by some hot garbage in the `uniffi` crate. -// Trust me, you don't want to mess with it! - -#ifndef mozilla_dom_{{ context.namespace()|header_name_cpp(context) }}_Shared -#define mozilla_dom_{{ context.namespace()|header_name_cpp(context) }}_Shared - -#include - -#include "nsDebug.h" -#include "nsTArray.h" -#include "prnetdb.h" - -#include "mozilla/Casting.h" -#include "mozilla/CheckedInt.h" -#include "mozilla/ErrorResult.h" -#include "mozilla/Utf8.h" - -#include "mozilla/dom/BindingDeclarations.h" -#include "mozilla/dom/Record.h" -#include "mozilla/dom/{{ context.namespace()|header_name_cpp(context) }}Binding.h" - -{% include "FFIDeclarationsTemplate.h" %} - -namespace mozilla { -namespace dom { - -{% include "RustBufferHelper.h" %} - -namespace {{ context.detail_name() }} { - -{% for e in ci.iter_enum_definitions() %} -{% if !e.is_flat() %} -MOZ_STATIC_ASSERT(false, "Sorry the gecko-js backend does not yet support enums with associated data: {{ e.name() }}"); -{% else %} -template <> -struct Serializable<{{ e.name()|class_name_cpp(context) }}> { - [[nodiscard]] static bool ReadFrom(Reader& aReader, {{ e.name()|class_name_cpp(context) }}& aValue) { - auto variant = aReader.ReadInt32(); - switch (variant) { - {% for variant in e.variants() -%} - case {{ loop.index }}: - aValue = {{ e.name()|class_name_cpp(context) }}::{{ variant.name()|enum_variant_cpp }}; - break; - {% endfor -%} - default: - MOZ_ASSERT(false, "Unexpected enum case"); - return false; - } - return true; - } - - static void WriteInto(Writer& aWriter, const {{ e.name()|class_name_cpp(context) }}& aValue) { - switch (aValue) { - {% for variant in e.variants() -%} - case {{ e.name()|class_name_cpp(context) }}::{{ variant.name()|enum_variant_cpp }}: - aWriter.WriteInt32({{ loop.index }}); - {% endfor -%} - default: - MOZ_ASSERT(false, "Unknown raw enum value"); - } - } -}; -{% endif %} -{% endfor %} - -{% for rec in ci.iter_record_definitions() -%} -template <> -struct Serializable<{{ rec.name()|class_name_cpp(context) }}> { - [[nodiscard]] static bool ReadFrom(Reader& aReader, {{ rec.name()|class_name_cpp(context) }}& aValue) { - {%- for field in rec.fields() %} - if (!Serializable<{{ field.webidl_type()|type_cpp(context) }}>::ReadFrom(aReader, aValue.{{ field.name()|field_name_cpp }})) { - return false; - } - {%- endfor %} - return true; - } - - static void WriteInto(Writer& aWriter, const {{ rec.name()|class_name_cpp(context) }}& aValue) { - {%- for field in rec.fields() %} - Serializable<{{ field.webidl_type()|type_cpp(context) }}>::WriteInto(aWriter, aValue.{{ field.name()|field_name_cpp }}); - {%- endfor %} - } -}; -{% endfor %} - -} // namespace {{ context.detail_name() }} - -} // namespace dom -} // namespace mozilla - -#endif // mozilla_dom_{{ context.namespace()|header_name_cpp(context) }}_Shared diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/WebIDLTemplate.webidl b/uniffi_bindgen/src/bindings/gecko_js/templates/WebIDLTemplate.webidl deleted file mode 100644 index 539820a14a..0000000000 --- a/uniffi_bindgen/src/bindings/gecko_js/templates/WebIDLTemplate.webidl +++ /dev/null @@ -1,104 +0,0 @@ -// This file was autogenerated by some hot garbage in the `uniffi` crate. -// Trust me, you don't want to mess with it! - -{%- for rec in ci.iter_record_definitions() %} -dictionary {{ rec.name()|class_name_webidl(context) }} { - {%- for field in rec.fields() %} - {% if field.required() -%}required{%- else -%}{%- endif %} {{ field.webidl_type()|type_webidl(context) }} {{ field.name()|var_name_webidl }} - {%- match field.webidl_default_value() %} - {%- when Some with (literal) %} = {{ literal|literal_webidl }} - {%- else %} - {%- endmatch %}; - {%- endfor %} -}; -{% endfor %} - -{%- for e in ci.iter_enum_definitions() %} -{% if ! e.is_flat() %} -// Sorry the gecko-js backend does not yet support enums with associated data, -// so this probably isn't going to compile just yet... -{% endif %} -enum {{ e.name()|class_name_webidl(context) }} { - {% for variant in e.variants() %} - "{{ variant.name()|enum_variant_webidl }}"{%- if !loop.last %}, {% endif %} - {% endfor %} -}; -{% endfor %} - -{%- let functions = ci.iter_function_definitions() %} -{%- if !functions.is_empty() %} -[ChromeOnly, Exposed=Window] -namespace {{ context.namespace()|class_name_webidl(context) }} { - {% for func in functions %} - {%- if func.throws().is_some() %} - [Throws] - {% endif %} - {%- match func.webidl_return_type() -%}{%- when Some with (type_) %}{{ type_|type_webidl(context) }}{% when None %}void{% endmatch %} {{ func.name()|fn_name_webidl }}( - {%- for arg in func.arguments() %} - {% if arg.optional() -%}optional{%- else -%}{%- endif %} {{ arg.webidl_type()|type_webidl(context) }} {{ arg.name() }} - {%- match arg.webidl_default_value() %} - {%- when Some with (literal) %} = {{ literal|literal_webidl }} - {%- else %} - {%- endmatch %} - {%- if !loop.last %}, {% endif %} - {%- endfor %} - ); - {% endfor %} -}; -{% endif -%} - -{%- for obj in ci.iter_object_definitions() %} -[ChromeOnly, Exposed=Window] -interface {{ obj.name()|class_name_webidl(context) }} { - - {%- match obj.primary_constructor() %} - {%- when Some with (cons) %} - {%- if cons.throws().is_some() %} - [Throws] - {% endif %} - constructor( - {%- for arg in cons.arguments() %} - {% if arg.optional() -%}optional{%- else -%}{%- endif %} {{ arg.webidl_type()|type_webidl(context) }} {{ arg.name() }} - {%- match arg.webidl_default_value() %} - {%- when Some with (literal) %} = {{ literal|literal_webidl }} - {%- else %} - {%- endmatch %} - {%- if !loop.last %}, {% endif %} - {%- endfor %} - ); - {%- when None %} - {%- endmatch %} - - {%- for cons in obj.alternate_constructors() %} - {%- if cons.throws().is_some() %} - [Throws] - {% endif %} - {{ obj.name()|class_name_webidl(context) }} {{ cons.name()|fn_name_webidl }}( - {%- for arg in cons.arguments() %} - {% if arg.optional() -%}optional{%- else -%}{%- endif %} {{ arg.webidl_type()|type_webidl(context) }} {{ arg.name() }} - {%- match arg.webidl_default_value() %} - {%- when Some with (literal) %} = {{ literal|literal_webidl }} - {%- else %} - {%- endmatch %} - {%- if !loop.last %}, {% endif %} - {%- endfor %} - ); - {%- endfor %} - - {% for meth in obj.methods() -%} - {%- if meth.throws().is_some() %} - [Throws] - {% endif %} - {%- match meth.webidl_return_type() -%}{%- when Some with (type_) %}{{ type_|type_webidl(context) }}{% when None %}void{% endmatch %} {{ meth.name()|fn_name_webidl }}( - {%- for arg in meth.arguments() %} - {% if arg.optional() -%}optional{%- else -%}{%- endif %} {{ arg.webidl_type()|type_webidl(context) }} {{ arg.name() }} - {%- match arg.webidl_default_value() %} - {%- when Some with (literal) %} = {{ literal|literal_webidl }} - {%- else %} - {%- endmatch %} - {%- if !loop.last %}, {% endif %} - {%- endfor %} - ); - {% endfor %} -}; -{% endfor %} diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/macros.cpp b/uniffi_bindgen/src/bindings/gecko_js/templates/macros.cpp deleted file mode 100644 index b1b6a69e6d..0000000000 --- a/uniffi_bindgen/src/bindings/gecko_js/templates/macros.cpp +++ /dev/null @@ -1,64 +0,0 @@ -{# /* Calls an FFI function. */ #} -{%- macro to_ffi_call(context, func) -%} - {%- call to_ffi_call_head(context, func, "status", "loweredRetVal_") -%} - {%- call _to_ffi_call_tail(context, func, "status", "loweredRetVal_") -%} -{%- endmacro -%} - -{# /* Calls an FFI function with an initial argument. */ #} -{%- macro to_ffi_call_with_prefix(context, prefix, func) %} - {{ context.ffi_rustcallstatus_type() }} status = {0, {0, 0, nullptr, 0} }; - {% match func.ffi_func().return_type() %}{% when Some with (type_) %}const {{ type_|type_ffi(context) }} loweredRetVal_ ={% else %}{% endmatch %}{{ func.ffi_func().name() }}( - {{ prefix }} - {%- let args = func.arguments() -%} - {%- if !args.is_empty() %},{% endif -%} - {%- for arg in args %} - {{ arg.webidl_type()|lower_cpp(arg.name(), context) }}{%- if !loop.last %},{% endif -%} - {%- endfor %} - , &status - ); - {%- call _to_ffi_call_tail(context, func, "status", "loweredRetVal_") -%} -{%- endmacro -%} - -{# /* Calls an FFI function without handling errors or lifting the return - value. Used in the implementation of `to_ffi_call`, and for - constructors. */ #} -{%- macro to_ffi_call_head(context, func, status, result) %} - {{ context.ffi_rustcallstatus_type() }} {{ status }} = {0, {0, 0, nullptr, 0}}; - {% match func.ffi_func().return_type() %}{% when Some with (type_) %}const {{ type_|type_ffi(context) }} {{ result }} ={% else %}{% endmatch %}{{ func.ffi_func().name() }}( - {%- let args = func.arguments() -%} - {%- for arg in args %} - {{ arg.webidl_type()|lower_cpp(arg.name(), context) }}{%- if !loop.last %},{% endif -%} - {%- endfor %} - {% if !args.is_empty() %}, {% endif %}&{{ status }} - ); -{%- endmacro -%} - -{# /* Handles errors and lifts the return value from an FFI function. */ #} -{%- macro _to_ffi_call_tail(context, func, status, result) %} - if ({{ status }}.mCode) { - {%- match func.cpp_throw_by() %} - {%- when ThrowBy::ErrorResult with (rv) %} - {# /* TODO: Improve error throwing. See https://github.com/mozilla/uniffi-rs/issues/295 - for details. */ - // More TODOs: - // - STATUS_ERROR: Lift the error from the rustbuffer and convert it into some kind of gecko error - // - STATUS_PANIC: Try to lift the message from the rustbuffer and use it for the OperationError below - -#} - - {{ rv }}.ThrowOperationError(nsDependentCString("UniffiError")); - {%- when ThrowBy::Assert %} - MOZ_ASSERT(false); - {%- endmatch %} - return {% match func.cpp_return_type() %}{% when Some with (type_) %}{{ type_|dummy_ret_value_cpp(context) }}{% else %}{% endmatch %}; - } - {%- match func.cpp_return_by() %} - {%- when ReturnBy::OutParam with (name, type_) %} - DebugOnly ok_ = {{ type_|lift_cpp(result, name, context) }}; - MOZ_ASSERT(ok_); - {%- when ReturnBy::Value with (type_) %} - {{ type_|type_cpp(context) }} retVal_; - DebugOnly ok_ = {{ type_|lift_cpp(result, "retVal_", context) }}; - MOZ_ASSERT(ok_); - return retVal_; - {%- when ReturnBy::Void %}{%- endmatch %} -{%- endmacro -%} diff --git a/uniffi_bindgen/src/bindings/gecko_js/webidl.rs b/uniffi_bindgen/src/bindings/gecko_js/webidl.rs deleted file mode 100644 index 9e13ab498d..0000000000 --- a/uniffi_bindgen/src/bindings/gecko_js/webidl.rs +++ /dev/null @@ -1,449 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -//! This file contains Gecko JS backend-specific extensions for the UniFFI -//! component interface types. These extensions are used to generate the WebIDL -//! file and C++ implementation for Firefox. -//! -//! Although UDL (the UniFFI Definition Language) and WebIDL share the same -//! syntax, they have completely different semantics. WebIDL distinguishes -//! between "nullable" (`T?`) and "optional" types (`optional T`), dictionary -//! members are optional by default, dictionaries are not nullable (but can be -//! optional, and must have a default value if they are). The UniFFI type -//! system is much simpler, but can't represent these semantics exactly. -//! -//! The Firefox C++ bindings are also peculiar. A C++ function that implements -//! a WebIDL static or namespace method takes an extra `GlobalObject` argument; -//! methods that return an `ArrayBuffer` also take a `JSContext`; some return -//! values are passed via out parameters while others are returned directly; -//! some WebIDL types map to different C++ types depending on where they're used -//! (in parameters, out parameters, or dictionary members); and throwing -//! functions take an extra `ErrorResult` argument. -//! -//! https://developer.mozilla.org/en-US/docs/Mozilla/WebIDL_bindings describes -//! how Firefox WebIDL constructs are reflected in C++. Since UniFFI is -//! generating both the WebIDL file and the C++ implementation, it must do the -//! same. -//! -//! These combinations of special cases are impossible to express in Askama, so -//! we have a "shadow type system" with extension traits implemented for the -//! UniFFI interface types. Capturing this logic here lets us keep our templates -//! and filters clean. - -use crate::interface::{Argument, Constructor, FFIType, Field, Function, Literal, Method, Type}; - -/// WebIDL types correspond to UniFFI interface types, but carry additional -/// information for compound types. -#[derive(Debug)] -pub enum WebIDLType { - /// Flat (non-recursive) types include integers, floats, Booleans, strings, - /// enums, objects (called "interfaces" in WebIDL), and records - /// ("dictionaries"). These don't have special semantics, so we just wrap - /// the underlying UniFFI type. - Flat(Type), - - /// `Nullable` and `Optional` both correspond to UniFFI optional types. - /// Semantically, "nullable" means "must be passed as an argument or a - /// dictionary member, but can be `null`". "Optional" means the argument - /// or member can be omitted entirely, or set to `undefined`. - Nullable(Box), - Optional(Box), - - /// Optionals with a default value are a grab bag of special cases in Gecko. - /// In the generated C++ bindings, the type of an optional with a default - /// value is `T`, not `Optional`. However, it must be serialized as if - /// it's an `Optional`, since that's what the Rust side of the FFI - /// expects. - OptionalWithDefaultValue(Box), - - /// Sequences are the same as their UniFFI counterparts. - Sequence(Box), - - /// Maps correspond to WebIDL records. - Map(Box), -} - -impl WebIDLType { - /// Returns `true` if the WebIDL type is returned via an out parameter in - /// the C++ implementation; `false` if by value. - pub fn needs_out_param(&self) -> bool { - match self { - WebIDLType::Flat(Type::String) | WebIDLType::Flat(Type::Record(_)) => true, - WebIDLType::Map(_) | WebIDLType::Sequence(_) => true, - WebIDLType::Optional(inner) - | WebIDLType::OptionalWithDefaultValue(inner) - | WebIDLType::Nullable(inner) => inner.needs_out_param(), - _ => false, - } - } - - pub fn is_optional_record(&self) -> bool { - match self { - WebIDLType::OptionalWithDefaultValue(inner) => { - matches!(inner.as_ref(), WebIDLType::Flat(Type::Record(_))) - } - _ => false, - } - } -} - -impl From for WebIDLType { - fn from(type_: Type) -> WebIDLType { - match type_ { - inner @ Type::UInt8 - | inner @ Type::Int8 - | inner @ Type::UInt16 - | inner @ Type::Int16 - | inner @ Type::UInt32 - | inner @ Type::Int32 - | inner @ Type::UInt64 - | inner @ Type::Int64 - | inner @ Type::Float32 - | inner @ Type::Float64 - | inner @ Type::Boolean - | inner @ Type::String - | inner @ Type::Enum(_) - | inner @ Type::Object(_) - | inner @ Type::Record(_) => WebIDLType::Flat(inner), - Type::Error(_) => { - // TODO: We don't currently throw typed errors; see - // https://github.com/mozilla/uniffi-rs/issues/295. - panic!("[TODO: From({:?})]", type_) - } - Type::Timestamp => panic!("Timestamp unimplemented"), - Type::Duration => panic!("Duration unimplemented"), - Type::CallbackInterface(_) => panic!("Callback interfaces unimplemented"), - Type::Optional(inner) => match *inner { - Type::Record(name) => { - WebIDLType::OptionalWithDefaultValue(Box::new(Type::Record(name).into())) - } - inner => WebIDLType::Nullable(Box::new(inner.into())), - }, - Type::Sequence(inner) => WebIDLType::Sequence(Box::new((*inner).into())), - Type::Map(inner) => WebIDLType::Map(Box::new((*inner).into())), - } - } -} - -impl From<&WebIDLType> for FFIType { - fn from(type_: &WebIDLType) -> FFIType { - match type_ { - WebIDLType::Flat(inner) => inner.into(), - WebIDLType::Optional(_) - | WebIDLType::OptionalWithDefaultValue(_) - | WebIDLType::Nullable(_) - | WebIDLType::Sequence(_) - | WebIDLType::Map(_) => FFIType::RustBuffer, - } - } -} - -/// Extensions to support WebIDL namespace methods. -pub trait FunctionExt { - /// Returns the WebIDL return type of this function. - fn webidl_return_type(&self) -> Option; - - /// Returns a list of arguments to declare for this function in the C++ - /// implementation, including any extras and out parameters. - fn cpp_arguments(&self) -> Vec>; - - /// Returns the C++ return type of this function, or `None` if the function - /// doesn't return a value, or returns it via an out parameter. - fn cpp_return_type(&self) -> Option; - - /// Indicates how this function returns its result, either by value or via - /// an out parameter. - fn cpp_return_by(&self) -> ReturnBy; - - /// Indicates how this function throws errors, either by an `ErrorResult` - /// parameter, or by a fatal assertion. - fn cpp_throw_by(&self) -> ThrowBy; -} - -impl FunctionExt for Function { - fn webidl_return_type(&self) -> Option { - self.return_type().cloned().map(WebIDLType::from) - } - - fn cpp_arguments(&self) -> Vec> { - let args = self.arguments(); - let mut result = Vec::with_capacity(args.len() + 3); - // All static methods take a `GlobalObject`. - result.push(CPPArgument::GlobalObject); - // ...Then the declared WebIDL arguments... - result.extend(args.into_iter().map(|arg| CPPArgument::In(arg))); - // ...Then the out param, depending on the return type. - if let Some(type_) = self - .webidl_return_type() - .filter(|type_| type_.needs_out_param()) - { - result.push(CPPArgument::Out(type_)); - } - // ...And finally, the `ErrorResult` to throw errors. - if self.throws().is_some() { - result.push(CPPArgument::ErrorResult); - } - result - } - - fn cpp_return_type(&self) -> Option { - self.webidl_return_type() - .filter(|type_| !type_.needs_out_param()) - } - - fn cpp_return_by(&self) -> ReturnBy { - self.webidl_return_type() - .map(ReturnBy::from_return_type) - .unwrap_or(ReturnBy::Void) - } - - fn cpp_throw_by(&self) -> ThrowBy { - if self.throws().is_some() { - ThrowBy::ErrorResult("aRv") - } else { - ThrowBy::Assert - } - } -} - -/// Extensions to support WebIDL interface constructors. -pub trait ConstructorExt { - /// Returns a list of arguments to declare for this constructor in the C++ - /// implementation, including any extras and out parameters. - fn cpp_arguments(&self) -> Vec>; - - /// Indicates how this constructor throws errors, either by an `ErrorResult` - /// parameter, or by a fatal assertion. - fn cpp_throw_by(&self) -> ThrowBy; -} - -impl ConstructorExt for Constructor { - fn cpp_arguments(&self) -> Vec> { - let args = self.arguments(); - let mut result = Vec::with_capacity(args.len() + 2); - // First the `GlobalObject`, just like for static methods... - result.push(CPPArgument::GlobalObject); - result.extend(args.into_iter().map(|arg| CPPArgument::In(arg))); - // Constructors never take out params, since they must return an - // instance of the object. - if self.throws().is_some() { - // ...But they can throw, so pass an `ErrorResult` if we need to - // throw errors. - result.push(CPPArgument::ErrorResult); - } - result - } - - fn cpp_throw_by(&self) -> ThrowBy { - if self.throws().is_some() { - ThrowBy::ErrorResult("aRv") - } else { - ThrowBy::Assert - } - } -} - -/// Extensions to support WebIDL interface methods. -pub trait MethodExt { - /// Returns the WebIDL return type of this method. - fn webidl_return_type(&self) -> Option; - - /// Returns a list of arguments to declare for this method in the C++ - /// implementation, including any extras and out parameters. - fn cpp_arguments(&self) -> Vec>; - - /// Returns the C++ return type of this function, or `None` if the method - /// doesn't return a value, or returns it via an out parameter. - fn cpp_return_type(&self) -> Option; - - /// Indicates how this function returns its result, either by value or via - /// an out parameter. - fn cpp_return_by(&self) -> ReturnBy; - - /// Indicates how this method throws errors, either by an `ErrorResult` - /// parameter, or by a fatal assertion. - fn cpp_throw_by(&self) -> ThrowBy; -} - -impl MethodExt for Method { - fn webidl_return_type(&self) -> Option { - self.return_type().cloned().map(WebIDLType::from) - } - - fn cpp_arguments(&self) -> Vec> { - let args = self.arguments(); - let mut result = Vec::with_capacity(args.len() + 2); - // Methods don't take a `GlobalObject` as their first argument. - result.extend(args.into_iter().map(|arg| CPPArgument::In(arg))); - if let Some(type_) = self - .webidl_return_type() - .filter(|type_| type_.needs_out_param()) - { - // ...But they can take out params, since they return values. - result.push(CPPArgument::Out(type_)); - } - if self.throws().is_some() { - // ...And they can throw. - result.push(CPPArgument::ErrorResult); - } - result - } - - fn cpp_return_type(&self) -> Option { - self.webidl_return_type() - .filter(|type_| !type_.needs_out_param()) - } - - fn cpp_return_by(&self) -> ReturnBy { - self.webidl_return_type() - .map(ReturnBy::from_return_type) - .unwrap_or(ReturnBy::Void) - } - - fn cpp_throw_by(&self) -> ThrowBy { - if self.throws().is_some() { - ThrowBy::ErrorResult("aRv") - } else { - ThrowBy::Assert - } - } -} - -/// Extensions to support WebIDL static method, constructor, and interface -/// method arguments. -pub trait ArgumentExt { - /// Returns the argument type. - fn webidl_type(&self) -> WebIDLType; - - /// Indicates if the argument should have an `optional` keyword. `true` - /// for nullable dictionaries and arguments that declare a default value - /// in UDL; `false` otherwise. - fn optional(&self) -> bool; - - /// Returns the default value for this argument, if one is specified. - fn webidl_default_value(&self) -> Option; -} - -impl ArgumentExt for Argument { - fn webidl_type(&self) -> WebIDLType { - self.type_().into() - } - - fn optional(&self) -> bool { - if self.webidl_default_value().is_some() { - return true; - } - false - } - - fn webidl_default_value(&self) -> Option { - if let Some(literal) = self.default_value() { - return Some(literal); - } - if self.webidl_type().is_optional_record() { - // Nullable UDL dictionaries must declare a default value - // in WebIDL. - return Some(Literal::EmptyMap); - } - None - } -} - -/// Extensions to support WebIDL dictionary members. -pub trait FieldExt { - /// Returns the member type. - fn webidl_type(&self) -> WebIDLType; - - /// Indicates if the member should have a `required` keyword. In UDL, all - /// dictionary members are required by default; in WebIDL, they're optional. - /// For WebIDL, all members are `required`, except for nullable - /// dictionaries, which must be optional. - fn required(&self) -> bool; - - /// Returns the default value for this member, if one is defined. - fn webidl_default_value(&self) -> Option; -} - -impl FieldExt for Field { - fn webidl_type(&self) -> WebIDLType { - match self.type_() { - Type::Optional(inner) => WebIDLType::Optional(Box::new((*inner).into())), - type_ => type_.into(), - } - } - - fn required(&self) -> bool { - !matches!(self.type_(), Type::Optional(_)) - } - - fn webidl_default_value(&self) -> Option { - if self.webidl_type().is_optional_record() { - // Nullable UDL dictionaries must declare a default value - // in WebIDL. - return Some(Literal::EmptyMap); - } - None - } -} - -/// Describes how a function returns its result. -pub enum ReturnBy { - /// The function returns its result in an out parameter with the given - /// name and type. - OutParam(&'static str, WebIDLType), - - /// The function returns its result by value. - Value(WebIDLType), - - /// The function doesn't declare a return type. - Void, -} - -impl ReturnBy { - fn from_return_type(type_: WebIDLType) -> Self { - if type_.needs_out_param() { - ReturnBy::OutParam("aRetVal", type_) - } else { - ReturnBy::Value(type_) - } - } -} - -/// Describes how a function throws errors. -pub enum ThrowBy { - /// Errors should be set on the `ErrorResult` parameter with the given - /// name. - ErrorResult(&'static str), - - /// Errors should fatally assert. - Assert, -} - -/// Describes a function argument. -pub enum CPPArgument<'a> { - /// The argument is a `GlobalObject`, passed to constructors, static, and - /// namespace methods. - GlobalObject, - - /// The argument is an `ErrorResult`, passed to throwing functions. - ErrorResult, - - /// The argument is a normal input parameter. - In(&'a Argument), - - /// The argument is an out parameter used to return results by reference. - Out(WebIDLType), -} - -impl<'a> CPPArgument<'a> { - /// Returns the argument name. - pub fn name(&self) -> &'a str { - match self { - CPPArgument::GlobalObject => "aGlobal", - CPPArgument::ErrorResult => "aRv", - CPPArgument::In(arg) => arg.name(), - CPPArgument::Out(_) => "aRetVal", - } - } -} diff --git a/uniffi_bindgen/src/bindings/mod.rs b/uniffi_bindgen/src/bindings/mod.rs index 316989a784..a65089fd27 100644 --- a/uniffi_bindgen/src/bindings/mod.rs +++ b/uniffi_bindgen/src/bindings/mod.rs @@ -15,7 +15,6 @@ use std::path::Path; use crate::interface::ComponentInterface; use crate::MergeWith; -pub mod gecko_js; pub mod kotlin; pub mod python; pub mod ruby; @@ -33,7 +32,6 @@ pub enum TargetLanguage { Swift, Python, Ruby, - GeckoJs, } impl TryFrom<&str> for TargetLanguage { @@ -44,7 +42,6 @@ impl TryFrom<&str> for TargetLanguage { "swift" => TargetLanguage::Swift, "python" | "py" => TargetLanguage::Python, "ruby" | "rb" => TargetLanguage::Ruby, - "gecko_js" => TargetLanguage::GeckoJs, _ => bail!("Unknown or unsupported target language: \"{}\"", value), }) } @@ -77,8 +74,6 @@ pub struct Config { python: python::Config, #[serde(default)] ruby: ruby::Config, - #[serde(default)] - gecko_js: gecko_js::Config, } impl From<&ComponentInterface> for Config { @@ -88,7 +83,6 @@ impl From<&ComponentInterface> for Config { swift: ci.into(), python: ci.into(), ruby: ci.into(), - gecko_js: ci.into(), } } } @@ -100,7 +94,6 @@ impl MergeWith for Config { swift: self.swift.merge_with(&other.swift), python: self.python.merge_with(&other.python), ruby: self.ruby.merge_with(&other.ruby), - gecko_js: self.gecko_js.merge_with(&other.gecko_js), } } } @@ -128,9 +121,6 @@ where python::write_bindings(&config.python, ci, out_dir, try_format_code)? } TargetLanguage::Ruby => ruby::write_bindings(&config.ruby, ci, out_dir, try_format_code)?, - TargetLanguage::GeckoJs => { - gecko_js::write_bindings(&config.gecko_js, ci, out_dir, try_format_code)? - } } Ok(()) } @@ -151,7 +141,6 @@ where TargetLanguage::Swift => swift::compile_bindings(&config.swift, ci, out_dir)?, TargetLanguage::Python => (), TargetLanguage::Ruby => (), - TargetLanguage::GeckoJs => (), } Ok(()) } @@ -169,7 +158,6 @@ where TargetLanguage::Swift => swift::run_script(out_dir, script_file)?, TargetLanguage::Python => python::run_script(out_dir, script_file)?, TargetLanguage::Ruby => ruby::run_script(out_dir, script_file)?, - TargetLanguage::GeckoJs => bail!("Can't run Gecko code standalone"), } Ok(()) } diff --git a/uniffi_bindgen/src/lib.rs b/uniffi_bindgen/src/lib.rs index 86d3761279..9499efda81 100644 --- a/uniffi_bindgen/src/lib.rs +++ b/uniffi_bindgen/src/lib.rs @@ -323,7 +323,7 @@ impl MergeWith for Option { } pub fn run_main() -> Result<()> { - const POSSIBLE_LANGUAGES: &[&str] = &["kotlin", "python", "swift", "gecko_js", "ruby"]; + const POSSIBLE_LANGUAGES: &[&str] = &["kotlin", "python", "swift", "ruby"]; let matches = clap::App::new("uniffi-bindgen") .about("Scaffolding and bindings generator for Rust") .version(clap::crate_version!()) From 75d965259952fb6d8c6052918a553daf835b8d4e Mon Sep 17 00:00:00 2001 From: Jan-Erik Rediger Date: Tue, 3 Aug 2021 18:13:02 +0200 Subject: [PATCH 30/45] Move callbacks testing code into its own fixture crate for testing --- Cargo.toml | 1 + examples/README.md | 2 +- examples/callbacks/Cargo.toml | 2 +- examples/callbacks/src/callbacks.udl | 37 ---------- examples/callbacks/src/lib.rs | 61 ---------------- .../tests/bindings/test_callbacks.kts | 66 +----------------- examples/callbacks/uniffi.toml | 3 + fixtures/callbacks/Cargo.toml | 19 +++++ fixtures/callbacks/README.md | 11 +++ fixtures/callbacks/build.rs | 7 ++ fixtures/callbacks/src/callbacks.udl | 38 ++++++++++ fixtures/callbacks/src/lib.rs | 66 ++++++++++++++++++ .../tests/bindings/test_callbacks.kts | 69 +++++++++++++++++++ .../tests/test_generated_bindings.rs | 4 ++ 14 files changed, 221 insertions(+), 165 deletions(-) create mode 100644 examples/callbacks/uniffi.toml create mode 100644 fixtures/callbacks/Cargo.toml create mode 100644 fixtures/callbacks/README.md create mode 100644 fixtures/callbacks/build.rs create mode 100644 fixtures/callbacks/src/callbacks.udl create mode 100644 fixtures/callbacks/src/lib.rs create mode 100644 fixtures/callbacks/tests/bindings/test_callbacks.kts create mode 100644 fixtures/callbacks/tests/test_generated_bindings.rs diff --git a/Cargo.toml b/Cargo.toml index 0175f7fb78..04b848f0e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "examples/sprites", "examples/todolist", "fixtures/coverall", + "fixtures/callbacks", "fixtures/regressions/enum-without-i32-helpers", "fixtures/regressions/kotlin-experimental-unsigned-types", "fixtures/regressions/cdylib-crate-type-dependency/ffi-crate", diff --git a/examples/README.md b/examples/README.md index 7aa76e992f..ab04d0df22 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,7 +14,7 @@ Newcomers are recommended to explore them in the following order: object-oriented style. * [`./todolist`](./todolist/) is a simplistic todo-list that can only add items and show the last item, meant to show how interacting with strings works. -* [`.rondpoint`](./rondpoint/) exercises complex data types by round-tripping them from the foreign-language +* [`./rondpoint`](./rondpoint/) exercises complex data types by round-tripping them from the foreign-language code, through rust and back agian. * [`./fxa-client`](./fxa-client/) doesn't work yet, but it contains aspirational example of what the UDL might look like for an actual real-world component. diff --git a/examples/callbacks/Cargo.toml b/examples/callbacks/Cargo.toml index 2ba8463cf2..29917afaaa 100644 --- a/examples/callbacks/Cargo.toml +++ b/examples/callbacks/Cargo.toml @@ -8,7 +8,7 @@ publish = false [lib] crate-type = ["cdylib", "lib"] -name = "uniffi_callbacks" +name = "callbacks" [dependencies] uniffi_macros = {path = "../../uniffi_macros"} diff --git a/examples/callbacks/src/callbacks.udl b/examples/callbacks/src/callbacks.udl index a8bca47083..0293b14cf1 100644 --- a/examples/callbacks/src/callbacks.udl +++ b/examples/callbacks/src/callbacks.udl @@ -9,40 +9,3 @@ callback interface OnCallAnswered { void busy(); void text_received(string text); }; - -/// These objects are implemented by the foreign language and passed -/// to Rust. Rust then calls methods on it when it needs to. -callback interface ForeignGetters { - boolean get_bool(boolean v, boolean arg2); - string get_string(string v, boolean arg2); - string? get_option(string? v, boolean arg2); - sequence get_list(sequence v, boolean arg2); -}; - -/// These objects are implemented in Rust, and call out to `ForeignGetters` -/// to get the value. -interface RustGetters { - boolean get_bool(ForeignGetters callback, boolean v, boolean arg2); - string get_string(ForeignGetters callback, string v, boolean arg2); - string? get_option(ForeignGetters callback, string? v, boolean arg2); - sequence get_list(ForeignGetters callback, sequence v, boolean arg2); -}; - -/// These objects are implemented by the foreign language and passed -/// to Rust. Rust then calls methods on it when it needs to. -/// Rust developers need to declare these traits extending `Send` so -/// they can be stored in Rust— i.e. not passed in as an argument to -/// be used immediately. -callback interface StoredForeignStringifier { - string from_simple_type(i32 value); - // Test if types are collected from callback interfaces. - // kotlinc compile time error if not. - string from_complex_type(sequence? values); -}; - -/// Rust object that uses the StoredForeignStringifier to produce string representations -/// of passed arguments. -interface RustStringifier { - constructor(StoredForeignStringifier callback); - string from_simple_type(i32 value); -}; diff --git a/examples/callbacks/src/lib.rs b/examples/callbacks/src/lib.rs index a21464db13..49058d6483 100644 --- a/examples/callbacks/src/lib.rs +++ b/examples/callbacks/src/lib.rs @@ -24,65 +24,4 @@ impl Telephone { } } -trait ForeignGetters { - fn get_bool(&self, v: bool, arg2: bool) -> bool; - fn get_string(&self, v: String, arg2: bool) -> String; - fn get_option(&self, v: Option, arg2: bool) -> Option; - fn get_list(&self, v: Vec, arg2: bool) -> Vec; -} - -#[derive(Debug, Clone)] -pub struct RustGetters; - -impl RustGetters { - pub fn new() -> Self { - RustGetters - } - fn get_bool(&self, callback: Box, v: bool, arg2: bool) -> bool { - callback.get_bool(v, arg2) - } - fn get_string(&self, callback: Box, v: String, arg2: bool) -> String { - callback.get_string(v, arg2) - } - fn get_option( - &self, - callback: Box, - v: Option, - arg2: bool, - ) -> Option { - callback.get_option(v, arg2) - } - fn get_list(&self, callback: Box, v: Vec, arg2: bool) -> Vec { - callback.get_list(v, arg2) - } -} - -impl Default for RustGetters { - fn default() -> Self { - Self::new() - } -} - -// Use `Send+Send` because we want to store the callback in an exposed -// `Send+Sync` object. -trait StoredForeignStringifier: Send + Sync + std::fmt::Debug { - fn from_simple_type(&self, value: i32) -> String; - fn from_complex_type(&self, values: Option>>) -> String; -} - -#[derive(Debug)] -pub struct RustStringifier { - callback: Box, -} - -impl RustStringifier { - fn new(callback: Box) -> Self { - RustStringifier { callback } - } - - fn from_simple_type(&self, value: i32) -> String { - self.callback.from_simple_type(value) - } -} - include!(concat!(env!("OUT_DIR"), "/callbacks.uniffi.rs")); diff --git a/examples/callbacks/tests/bindings/test_callbacks.kts b/examples/callbacks/tests/bindings/test_callbacks.kts index 616cdb481b..2007f0f1aa 100644 --- a/examples/callbacks/tests/bindings/test_callbacks.kts +++ b/examples/callbacks/tests/bindings/test_callbacks.kts @@ -4,7 +4,7 @@ import uniffi.callbacks.* -// 0. Simple example just to see it work. +// Simple example just to see it work. // Pass in a string, get a string back. // Pass in nothing, get unit back. class OnCallAnsweredImpl : OnCallAnswered { @@ -47,67 +47,3 @@ assert(cbObjet2.busyCount == 0) { "yesCount=${cbObjet2.busyCount} (should be 0)" assert(cbObjet2.yesCount == 1) { "yesCount=${cbObjet2.yesCount} (should be 1)" } telephone.destroy() - -// A bit more systematic in testing, but this time in English. -// -// 1. Pass in the callback as arguments. -// Make the callback methods use multiple aruments, with a variety of types, and -// with a variety of return types. -val rustGetters = RustGetters() -class KotlinGetters(): ForeignGetters { - override fun getBool(v: Boolean, arg2: Boolean): Boolean = v xor arg2 - override fun getString(v: String, arg2: Boolean): String = if (arg2) "1234567890123" else v - override fun getOption(v: String?, arg2: Boolean): String? = if (arg2) v?.uppercase() else v - override fun getList(v: List, arg2: Boolean): List = if (arg2) v else listOf() -} - -val callback = KotlinGetters() -listOf(true, false).forEach { v -> - val flag = true - val expected = callback.getBool(v, flag) - val observed = rustGetters.getBool(callback, v, flag) - assert(expected == observed) { "roundtripping through callback: $expected != $observed" } -} - -listOf(listOf(1,2), listOf(0,1)).forEach { v -> - val flag = true - val expected = callback.getList(v, flag) - val observed = rustGetters.getList(callback, v, flag) - assert(expected == observed) { "roundtripping through callback: $expected != $observed" } -} - -listOf("Hello", "world").forEach { v -> - val flag = true - val expected = callback.getString(v, flag) - val observed = rustGetters.getString(callback, v, flag) - assert(expected == observed) { "roundtripping through callback: $expected != $observed" } -} - -listOf("Some", null).forEach { v -> - val flag = false - val expected = callback.getOption(v, flag) - val observed = rustGetters.getOption(callback, v, flag) - assert(expected == observed) { "roundtripping through callback: $expected != $observed" } -} -rustGetters.destroy() - -// 2. Pass the callback in as a constructor argument, to be stored on the Object struct. -// This is crucial if we want to configure a system at startup, -// then use it without passing callbacks all the time. - -class StoredKotlinStringifier: StoredForeignStringifier { - override fun fromSimpleType(value: Int): String = "kotlin: $value" - // We don't test this, but we're checking that the arg type is included in the minimal list of types used - // in the UDL. - // If this doesn't compile, then look at TypeResolver. - override fun fromComplexType(values: List?): String = "kotlin: $values" -} - -val kotlinStringifier = StoredKotlinStringifier() -val rustStringifier = RustStringifier(kotlinStringifier) -listOf(1, 2).forEach { v -> - val expected = kotlinStringifier.fromSimpleType(v) - val observed = rustStringifier.fromSimpleType(v) - assert(expected == observed) { "callback is sent on construction: $expected != $observed" } -} -rustStringifier.destroy() \ No newline at end of file diff --git a/examples/callbacks/uniffi.toml b/examples/callbacks/uniffi.toml new file mode 100644 index 0000000000..a2bfcce7b9 --- /dev/null +++ b/examples/callbacks/uniffi.toml @@ -0,0 +1,3 @@ +[bindings.kotlin] +package_name = "uniffi.callbacks" +cdylib_name = "callbacks" diff --git a/fixtures/callbacks/Cargo.toml b/fixtures/callbacks/Cargo.toml new file mode 100644 index 0000000000..c1da936220 --- /dev/null +++ b/fixtures/callbacks/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "callbacks" +version = "0.12.0" +authors = ["Firefox Sync Team "] +edition = "2018" +publish = false + +[lib] +crate-type = ["staticlib", "cdylib"] +name = "uniffi_callbacks" + +[dependencies] +uniffi_macros = {path = "../../uniffi_macros"} +uniffi = {path = "../../uniffi", features=["builtin-bindgen"]} +thiserror = "1.0" +lazy_static = "1.4" + +[build-dependencies] +uniffi_build = {path = "../../uniffi_build", features=["builtin-bindgen"]} diff --git a/fixtures/callbacks/README.md b/fixtures/callbacks/README.md new file mode 100644 index 0000000000..bbb999994a --- /dev/null +++ b/fixtures/callbacks/README.md @@ -0,0 +1,11 @@ +# A "Callbacks" test for uniffi components + +This is similar to the `callbacks` example, but it's intended to be contrived and to +ensure we get good test coverage of all possible options. + +It's here so it doesn't even need to make a cursory effort to be a "good" +example; it intentionally panics, asserts params are certain values, has +no-op methods etc. If you're trying to get your head around uniffi then the +"examples" directory will be a much better bet. + +This is its own crate, because the callback mechanism is not implemented for all backends yet. diff --git a/fixtures/callbacks/build.rs b/fixtures/callbacks/build.rs new file mode 100644 index 0000000000..a98aacddb3 --- /dev/null +++ b/fixtures/callbacks/build.rs @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +fn main() { + uniffi_build::generate_scaffolding("./src/callbacks.udl").unwrap(); +} diff --git a/fixtures/callbacks/src/callbacks.udl b/fixtures/callbacks/src/callbacks.udl new file mode 100644 index 0000000000..db61326e66 --- /dev/null +++ b/fixtures/callbacks/src/callbacks.udl @@ -0,0 +1,38 @@ +namespace callbacks {}; + +/// These objects are implemented by the foreign language and passed +/// to Rust. Rust then calls methods on it when it needs to. +callback interface ForeignGetters { + boolean get_bool(boolean v, boolean arg2); + string get_string(string v, boolean arg2); + string? get_option(string? v, boolean arg2); + sequence get_list(sequence v, boolean arg2); +}; + +/// These objects are implemented in Rust, and call out to `ForeignGetters` +/// to get the value. +interface RustGetters { + boolean get_bool(ForeignGetters callback, boolean v, boolean arg2); + string get_string(ForeignGetters callback, string v, boolean arg2); + string? get_option(ForeignGetters callback, string? v, boolean arg2); + sequence get_list(ForeignGetters callback, sequence v, boolean arg2); +}; + +/// These objects are implemented by the foreign language and passed +/// to Rust. Rust then calls methods on it when it needs to. +/// Rust developers need to declare these traits extending `Send` so +/// they can be stored in Rust— i.e. not passed in as an argument to +/// be used immediately. +callback interface StoredForeignStringifier { + string from_simple_type(i32 value); + // Test if types are collected from callback interfaces. + // kotlinc compile time error if not. + string from_complex_type(sequence? values); +}; + +/// Rust object that uses the StoredForeignStringifier to produce string representations +/// of passed arguments. +interface RustStringifier { + constructor(StoredForeignStringifier callback); + string from_simple_type(i32 value); +}; diff --git a/fixtures/callbacks/src/lib.rs b/fixtures/callbacks/src/lib.rs new file mode 100644 index 0000000000..8f41583614 --- /dev/null +++ b/fixtures/callbacks/src/lib.rs @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +trait ForeignGetters { + fn get_bool(&self, v: bool, arg2: bool) -> bool; + fn get_string(&self, v: String, arg2: bool) -> String; + fn get_option(&self, v: Option, arg2: bool) -> Option; + fn get_list(&self, v: Vec, arg2: bool) -> Vec; +} + +#[derive(Debug, Clone)] +pub struct RustGetters; + +impl RustGetters { + pub fn new() -> Self { + RustGetters + } + fn get_bool(&self, callback: Box, v: bool, arg2: bool) -> bool { + callback.get_bool(v, arg2) + } + fn get_string(&self, callback: Box, v: String, arg2: bool) -> String { + callback.get_string(v, arg2) + } + fn get_option( + &self, + callback: Box, + v: Option, + arg2: bool, + ) -> Option { + callback.get_option(v, arg2) + } + fn get_list(&self, callback: Box, v: Vec, arg2: bool) -> Vec { + callback.get_list(v, arg2) + } +} + +impl Default for RustGetters { + fn default() -> Self { + Self::new() + } +} + +// Use `Send+Send` because we want to store the callback in an exposed +// `Send+Sync` object. +trait StoredForeignStringifier: Send + Sync + std::fmt::Debug { + fn from_simple_type(&self, value: i32) -> String; + fn from_complex_type(&self, values: Option>>) -> String; +} + +#[derive(Debug)] +pub struct RustStringifier { + callback: Box, +} + +impl RustStringifier { + fn new(callback: Box) -> Self { + RustStringifier { callback } + } + + fn from_simple_type(&self, value: i32) -> String { + self.callback.from_simple_type(value) + } +} + +include!(concat!(env!("OUT_DIR"), "/callbacks.uniffi.rs")); diff --git a/fixtures/callbacks/tests/bindings/test_callbacks.kts b/fixtures/callbacks/tests/bindings/test_callbacks.kts new file mode 100644 index 0000000000..863b531cbb --- /dev/null +++ b/fixtures/callbacks/tests/bindings/test_callbacks.kts @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import uniffi.callbacks.* + +// A bit more systematic in testing, but this time in English. +// +// 1. Pass in the callback as arguments. +// Make the callback methods use multiple aruments, with a variety of types, and +// with a variety of return types. +val rustGetters = RustGetters() +class KotlinGetters(): ForeignGetters { + override fun getBool(v: Boolean, arg2: Boolean): Boolean = v xor arg2 + override fun getString(v: String, arg2: Boolean): String = if (arg2) "1234567890123" else v + override fun getOption(v: String?, arg2: Boolean): String? = if (arg2) v?.uppercase() else v + override fun getList(v: List, arg2: Boolean): List = if (arg2) v else listOf() +} + +val callback = KotlinGetters() +listOf(true, false).forEach { v -> + val flag = true + val expected = callback.getBool(v, flag) + val observed = rustGetters.getBool(callback, v, flag) + assert(expected == observed) { "roundtripping through callback: $expected != $observed" } +} + +listOf(listOf(1,2), listOf(0,1)).forEach { v -> + val flag = true + val expected = callback.getList(v, flag) + val observed = rustGetters.getList(callback, v, flag) + assert(expected == observed) { "roundtripping through callback: $expected != $observed" } +} + +listOf("Hello", "world").forEach { v -> + val flag = true + val expected = callback.getString(v, flag) + val observed = rustGetters.getString(callback, v, flag) + assert(expected == observed) { "roundtripping through callback: $expected != $observed" } +} + +listOf("Some", null).forEach { v -> + val flag = false + val expected = callback.getOption(v, flag) + val observed = rustGetters.getOption(callback, v, flag) + assert(expected == observed) { "roundtripping through callback: $expected != $observed" } +} +rustGetters.destroy() + +// 2. Pass the callback in as a constructor argument, to be stored on the Object struct. +// This is crucial if we want to configure a system at startup, +// then use it without passing callbacks all the time. + +class StoredKotlinStringifier: StoredForeignStringifier { + override fun fromSimpleType(value: Int): String = "kotlin: $value" + // We don't test this, but we're checking that the arg type is included in the minimal list of types used + // in the UDL. + // If this doesn't compile, then look at TypeResolver. + override fun fromComplexType(values: List?): String = "kotlin: $values" +} + +val kotlinStringifier = StoredKotlinStringifier() +val rustStringifier = RustStringifier(kotlinStringifier) +listOf(1, 2).forEach { v -> + val expected = kotlinStringifier.fromSimpleType(v) + val observed = rustStringifier.fromSimpleType(v) + assert(expected == observed) { "callback is sent on construction: $expected != $observed" } +} +rustStringifier.destroy() diff --git a/fixtures/callbacks/tests/test_generated_bindings.rs b/fixtures/callbacks/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..95d37265f3 --- /dev/null +++ b/fixtures/callbacks/tests/test_generated_bindings.rs @@ -0,0 +1,4 @@ +uniffi_macros::build_foreign_language_testcases!( + "src/callbacks.udl", + ["tests/bindings/test_callbacks.kts"] +); From 61ae6951914c9619c3afe18f29196790cde27c59 Mon Sep 17 00:00:00 2001 From: Jan-Erik Rediger Date: Tue, 3 Aug 2021 12:54:15 +0200 Subject: [PATCH 31/45] Remove workaround for JNA bug when passing small structs by value. This (partially) reverts commit 937f9b76f56e52247bb9a53d281554bacba2553e The upstream JNA bug[1] has been resolved. JNA 5.7 includes the fix. We're upgrading our Docker test image to 5.8 already. Fixes #334 [1]: https://github.com/java-native-access/jna/issues/1259 --- docker/Dockerfile-build | 2 +- uniffi/src/ffi/foreignbytes.rs | 11 +---------- uniffi/src/ffi/rustbuffer.rs | 4 ---- .../bindings/kotlin/templates/RustBufferTemplate.kt | 9 ++------- .../bindings/python/templates/RustBufferTemplate.py | 5 ----- .../src/bindings/ruby/templates/RustBufferTemplate.rb | 8 ++------ .../bindings/swift/templates/BridgingHeaderTemplate.h | 7 +------ .../src/bindings/swift/templates/ErrorTemplate.swift | 3 +-- .../bindings/swift/templates/RustBufferTemplate.swift | 6 ++---- 9 files changed, 10 insertions(+), 45 deletions(-) diff --git a/docker/Dockerfile-build b/docker/Dockerfile-build index 912b2ff08c..0d564cd185 100644 --- a/docker/Dockerfile-build +++ b/docker/Dockerfile-build @@ -66,7 +66,7 @@ RUN mkdir -p /tmp/setup-kotlin \ RUN mkdir -p /tmp/setup-jna \ && cd /tmp/setup-jna \ - && curl -o jna.jar https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.6.0/jna-5.6.0.jar \ + && curl -o jna.jar https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.8.0/jna-5.8.0.jar \ # XXX TODO: should check a sha256sum or something here... && sudo mv jna.jar /opt \ && echo "export CLASSPATH=\"\$CLASSPATH:/opt/jna.jar\"" >> /home/circleci/.bashrc \ diff --git a/uniffi/src/ffi/foreignbytes.rs b/uniffi/src/ffi/foreignbytes.rs index de604dd682..561173efd6 100644 --- a/uniffi/src/ffi/foreignbytes.rs +++ b/uniffi/src/ffi/foreignbytes.rs @@ -31,10 +31,6 @@ pub struct ForeignBytes { len: i32, /// The pointer to the foreign-owned bytes. data: *const u8, - /// This forces the struct to be larger than 16 bytes, as a temporary workaround for a bug in JNA. - /// See https://github.com/mozilla/uniffi-rs/issues/334 for details. - padding: i64, - padding2: i32, } impl ForeignBytes { @@ -47,12 +43,7 @@ impl ForeignBytes { /// /// 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, - padding: 0, - padding2: 0, - } + Self { len, data } } /// View the foreign bytes as a `&[u8]`. diff --git a/uniffi/src/ffi/rustbuffer.rs b/uniffi/src/ffi/rustbuffer.rs index 079870ebc4..6b0d99fd75 100644 --- a/uniffi/src/ffi/rustbuffer.rs +++ b/uniffi/src/ffi/rustbuffer.rs @@ -58,9 +58,6 @@ pub struct RustBuffer { len: i32, /// The pointer to the allocated buffer of the `Vec`. data: *mut u8, - /// This forces the struct to be larger than 16 bytes, as a temporary workaround for a bug in JNA. - /// See https://github.com/mozilla/uniffi-rs/issues/334 for details. - padding: i64, } impl RustBuffer { @@ -85,7 +82,6 @@ impl RustBuffer { capacity, len, data, - padding: 0, } } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt index 38afb326ee..80c5e9391f 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt @@ -2,13 +2,11 @@ // A rust-owned buffer is represented by its capacity, its current length, and a // pointer to the underlying data. -@Structure.FieldOrder("capacity", "len", "data", "padding") +@Structure.FieldOrder("capacity", "len", "data") open class RustBuffer : Structure() { @JvmField var capacity: Int = 0 @JvmField var len: Int = 0 @JvmField var data: Pointer? = null - // Ref https://github.com/mozilla/uniffi-rs/issues/334 for this weird "padding" field. - @JvmField var padding: Long = 0 class ByValue : RustBuffer(), Structure.ByValue class ByReference : RustBuffer(), Structure.ByReference @@ -40,13 +38,10 @@ open class RustBuffer : Structure() { // then we might as well copy it into a `RustBuffer`. But it's here for API // completeness. -@Structure.FieldOrder("len", "data", "padding", "padding2") +@Structure.FieldOrder("len", "data") open class ForeignBytes : Structure() { @JvmField var len: Int = 0 @JvmField var data: Pointer? = null - // Ref https://github.com/mozilla/uniffi-rs/issues/334 for these weird "padding" fields. - @JvmField var padding: Long = 0 - @JvmField var padding2: Int = 0 class ByValue : ForeignBytes(), Structure.ByValue } diff --git a/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py b/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py index 735464e263..f60f9ec54f 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py @@ -4,8 +4,6 @@ class RustBuffer(ctypes.Structure): ("capacity", ctypes.c_int32), ("len", ctypes.c_int32), ("data", ctypes.POINTER(ctypes.c_char)), - # Ref https://github.com/mozilla/uniffi-rs/issues/334 for this weird "padding" field. - ("padding", ctypes.c_int64), ] @staticmethod @@ -179,9 +177,6 @@ class ForeignBytes(ctypes.Structure): _fields_ = [ ("len", ctypes.c_int32), ("data", ctypes.POINTER(ctypes.c_char)), - # Ref https://github.com/mozilla/uniffi-rs/issues/334 for these weird "padding" fields. - ("padding", ctypes.c_int64), - ("padding2", ctypes.c_int32), ] def __str__(self): diff --git a/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb b/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb index 9ddb70d79f..5fd5ad2b64 100644 --- a/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb +++ b/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb @@ -1,9 +1,7 @@ class RustBuffer < FFI::Struct - # Ref https://github.com/mozilla/uniffi-rs/issues/334 for this weird "padding" field. layout :capacity, :int32, :len, :int32, - :data, :pointer, - :padding, :int64 + :data, :pointer def self.alloc(size) return {{ ci.namespace()|class_name_rb }}.rust_call(:{{ ci.ffi_rustbuffer_alloc().name() }}, size) @@ -173,9 +171,7 @@ def consumeInto{{ canonical_type_name }} module UniFFILib class ForeignBytes < FFI::Struct layout :len, :int32, - :data, :pointer, - :padding, :int64, - :padding2, :int32 + :data, :pointer def len self[:len] diff --git a/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h index c4a57ce6fe..6cfe2683a3 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h +++ b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h @@ -26,17 +26,12 @@ typedef struct RustBuffer int32_t capacity; int32_t len; uint8_t *_Nullable data; - // Ref https://github.com/mozilla/uniffi-rs/issues/334 for this weird "padding" field. - int64_t padding; } RustBuffer; typedef struct ForeignBytes { int32_t len; const uint8_t *_Nullable data; - // Ref https://github.com/mozilla/uniffi-rs/issues/334 for these weird "padding" fields. - int64_t padding; - int32_t padding2; } ForeignBytes; // Error definitions @@ -48,7 +43,7 @@ typedef struct 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_V2 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_|type_ffi }}{% when None %}void{% endmatch %} {{ func.name() }}( {% call swift::arg_list_ffi_decl(func) %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift index c2ffe2fdc5..bcc093b8ab 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift @@ -35,8 +35,7 @@ fileprivate extension RustCallStatus { errorBuf: RustBuffer.init( capacity: 0, len: 0, - data: nil, - padding: 0 + data: nil ) ) } diff --git a/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift index d64617ac6d..b2ae9eb569 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift @@ -4,8 +4,7 @@ fileprivate extension RustBuffer { let rbuf = bytes.withUnsafeBufferPointer { ptr in try! rustCall { {{ ci.ffi_rustbuffer_from_bytes().name() }}(ForeignBytes(bufferPointer: ptr), $0) } } - // Ref https://github.com/mozilla/uniffi-rs/issues/334 for the extra "padding" arg. - self.init(capacity: rbuf.capacity, len: rbuf.len, data: rbuf.data, padding: 0) + self.init(capacity: rbuf.capacity, len: rbuf.len, data: rbuf.data) } // Frees the buffer in place. @@ -17,7 +16,6 @@ fileprivate extension RustBuffer { fileprivate extension ForeignBytes { init(bufferPointer: UnsafeBufferPointer) { - // Ref https://github.com/mozilla/uniffi-rs/issues/334 for the extra "padding" args. - self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress, padding: 0, padding2: 0) + self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress) } } From 0fbce14d790dc9f0c1361f3ec7dc910d6dcf8f97 Mon Sep 17 00:00:00 2001 From: Jan-Erik Rediger Date: Tue, 3 Aug 2021 13:04:28 +0200 Subject: [PATCH 32/45] Document JNA minimum version requirement. --- CHANGELOG.md | 1 + docs/manual/src/kotlin/gradle.md | 15 +++++++++++++++ .../swift/templates/BridgingHeaderTemplate.h | 12 ++++++------ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd7cd6ee9f..ec957e9317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ implementation in uniffi/src/lib.rs for an example. - Kotlin exceptions names will now replace a trailing "Error" with "Exception" rather than appending the string (FooException instead of FooErrorException) +- JNA 5.7 or greater is required for Kotlin consumers ### What's Changed diff --git a/docs/manual/src/kotlin/gradle.md b/docs/manual/src/kotlin/gradle.md index 631161a73d..85dcbd44d4 100644 --- a/docs/manual/src/kotlin/gradle.md +++ b/docs/manual/src/kotlin/gradle.md @@ -42,3 +42,18 @@ allprojects { } } ``` + +## JNA dependency + +UniFFI relies on [JNA] for the ability to call native methods. +JNA 5.7 or greater is required. + +Set the dependency in your `build.gradle`: + +```groovy +dependencies { + implementation "net.java.dev.jna:jna:5.7.0@aar" +} +``` + +[JNA]: https://github.com/java-native-access/jna diff --git a/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h index 6cfe2683a3..e0c9bde907 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h +++ b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h @@ -11,15 +11,15 @@ // 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_V2 - #ifndef UNIFFI_SHARED_HEADER_V2 + // If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V3 + #ifndef UNIFFI_SHARED_HEADER_V3 #error Combining helper code from multiple versions of uniffi is not supported - #endif // ndef UNIFFI_SHARED_HEADER_V2 + #endif // ndef UNIFFI_SHARED_HEADER_V3 #else #define UNIFFI_SHARED_H -#define UNIFFI_SHARED_HEADER_V2 +#define UNIFFI_SHARED_HEADER_V3 // ⚠️ 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_V2 in this file. ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V3 in this file. ⚠️ typedef struct RustBuffer { @@ -41,7 +41,7 @@ typedef struct RustCallStatus { } 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_V2 in this file. ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V3 in this file. ⚠️ #endif // def UNIFFI_SHARED_H {% for func in ci.iter_ffi_function_definitions() -%} From 62b52432ede3acc28f2946b8a6aef385b0aa3ca5 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Tue, 3 Aug 2021 19:36:03 -0400 Subject: [PATCH 33/45] Make `ViaFfi.write()` input an owned value instead of a reference (#999) This makes sense since the idea is that code in FFI-land is taking ownership of the value. --- uniffi/src/ffi/rustcalls.rs | 2 +- uniffi/src/lib.rs | 40 ++++++++++--------- uniffi_bindgen/src/scaffolding/mod.rs | 2 +- .../templates/CallbackInterfaceTemplate.rs | 4 +- .../src/scaffolding/templates/EnumTemplate.rs | 2 +- .../scaffolding/templates/ErrorTemplate.rs | 2 +- .../scaffolding/templates/RecordTemplate.rs | 4 +- 7 files changed, 29 insertions(+), 27 deletions(-) diff --git a/uniffi/src/ffi/rustcalls.rs b/uniffi/src/ffi/rustcalls.rs index 988023e837..cec8736b7c 100644 --- a/uniffi/src/ffi/rustcalls.rs +++ b/uniffi/src/ffi/rustcalls.rs @@ -210,7 +210,7 @@ mod test { // Use RustBufferViaFfi to simplify lifting TestError out of RustBuffer to check it impl RustBufferViaFfi for TestError { - fn write(&self, buf: &mut Vec) { + fn write(self, buf: &mut Vec) { self.0.write(buf); } diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index f1705b2724..a5e925dbd9 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -179,7 +179,10 @@ pub unsafe trait ViaFfi: Sized { /// 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. - fn write(&self, buf: &mut Vec); + /// + /// Note that this method takes an owned `self` because it's transfering ownership + /// to the foreign language code via the RustBuffer. + fn write(self, buf: &mut Vec); /// Read a rust value from a buffer, received over the FFI in serialized form. /// @@ -232,8 +235,8 @@ macro_rules! impl_via_ffi_for_num_primitive { Ok(v) } - fn write(&self, buf: &mut Vec) { - buf.[](*self); + fn write(self, buf: &mut Vec) { + buf.[](self); } fn try_read(buf: &mut &[u8]) -> Result { @@ -273,8 +276,8 @@ unsafe impl ViaFfi for bool { }) } - fn write(&self, buf: &mut Vec) { - buf.put_i8(ViaFfi::lower(*self)); + fn write(self, buf: &mut Vec) { + buf.put_i8(ViaFfi::lower(self)); } fn try_read(buf: &mut &[u8]) -> Result { @@ -316,7 +319,7 @@ unsafe impl ViaFfi for String { Ok(unsafe { String::from_utf8_unchecked(v) }) } - fn write(&self, buf: &mut Vec) { + fn write(self, buf: &mut Vec) { // 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(self.len()).unwrap(); @@ -344,7 +347,7 @@ unsafe impl ViaFfi for String { /// C-compatible value, you can use this trait to implement `lower()` in terms of `write()` and /// `lift` in terms of `read()`. pub trait RustBufferViaFfi: Sized { - fn write(&self, buf: &mut Vec); + fn write(self, buf: &mut Vec); fn try_read(buf: &mut &[u8]) -> Result; } @@ -353,7 +356,7 @@ unsafe impl ViaFfi for T { fn lower(self) -> RustBuffer { let mut buf = Vec::new(); - RustBufferViaFfi::write(&self, &mut buf); + RustBufferViaFfi::write(self, &mut buf); RustBuffer::from_vec(buf) } @@ -367,7 +370,7 @@ unsafe impl ViaFfi for T { Ok(value) } - fn write(&self, buf: &mut Vec) { + fn write(self, buf: &mut Vec) { RustBufferViaFfi::write(self, buf) } @@ -391,7 +394,7 @@ unsafe impl ViaFfi for T { /// 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 RustBufferViaFfi for SystemTime { - fn write(&self, buf: &mut Vec) { + fn write(self, buf: &mut Vec) { let mut sign = 1; let epoch_offset = self .duration_since(SystemTime::UNIX_EPOCH) @@ -431,7 +434,7 @@ impl RustBufferViaFfi for SystemTime { /// of the magnitude. The nanosecond portion is expected to be between 0 /// and 999,999,999. impl RustBufferViaFfi for Duration { - fn write(&self, buf: &mut Vec) { + fn write(self, buf: &mut Vec) { buf.put_u64(self.as_secs()); buf.put_u32(self.subsec_nanos()); } @@ -452,7 +455,7 @@ impl RustBufferViaFfi for Duration { /// `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 RustBufferViaFfi for Option { - fn write(&self, buf: &mut Vec) { + fn write(self, buf: &mut Vec) { match self { None => buf.put_i8(0), Some(v) => { @@ -482,11 +485,11 @@ impl RustBufferViaFfi for Option { /// than serializing, and perhaps even pass other vector types using a /// similar struct. But that's for future work. impl RustBufferViaFfi for Vec { - fn write(&self, buf: &mut Vec) { + fn write(self, buf: &mut Vec) { // TODO: would be nice not to panic here :-/ let len = i32::try_from(self.len()).unwrap(); buf.put_i32(len); // We limit arrays to i32::MAX items - for item in self.iter() { + for item in self.into_iter() { ViaFfi::write(item, buf); } } @@ -511,11 +514,11 @@ impl RustBufferViaFfi for Vec { /// key followed by the value) in turn. /// (It's a signed type due to limits of the JVM). impl RustBufferViaFfi for HashMap { - fn write(&self, buf: &mut Vec) { + fn write(self, buf: &mut Vec) { // TODO: would be nice not to panic here :-/ let len = i32::try_from(self.len()).unwrap(); buf.put_i32(len); // We limit HashMaps to i32::MAX entries - for (key, value) in self.iter() { + for (key, value) in self.into_iter() { ViaFfi::write(key, buf); ViaFfi::write(value, buf); } @@ -577,10 +580,9 @@ unsafe impl ViaFfi for std::sync::Arc { /// 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(&self, buf: &mut Vec) { + fn write(self, buf: &mut Vec) { static_assertions::const_assert!(std::mem::size_of::<*const std::ffi::c_void>() <= 8); - let ptr = std::sync::Arc::clone(self).lower(); - buf.put_u64(ptr as u64); + buf.put_u64(self.lower() as u64); } /// When reading as a field of a complex structure, we receive a "borrow" of the `Arc` diff --git a/uniffi_bindgen/src/scaffolding/mod.rs b/uniffi_bindgen/src/scaffolding/mod.rs index 077efee43f..04ce19d30d 100644 --- a/uniffi_bindgen/src/scaffolding/mod.rs +++ b/uniffi_bindgen/src/scaffolding/mod.rs @@ -111,7 +111,7 @@ mod filters { type_name ), _ => format!( - "<{} as uniffi::ViaFfi>::write(&{}, {})", + "<{} as uniffi::ViaFfi>::write({}, {})", type_rs(type_)?, nm, target diff --git a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs index 0744533d43..ea602fb398 100644 --- a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs @@ -99,7 +99,7 @@ unsafe impl uniffi::ViaFfi for {{ trait_impl }} { self.handle } - fn write(&self, buf: &mut Vec) { + fn write(self, buf: &mut Vec) { use uniffi::deps::bytes::BufMut; buf.put_u64(self.handle); } @@ -113,4 +113,4 @@ unsafe impl uniffi::ViaFfi for {{ trait_impl }} { uniffi::check_remaining(buf, 8)?; ::try_lift(buf.get_u64()) } -} \ No newline at end of file +} diff --git a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs index 9d0034f704..3542da7833 100644 --- a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs @@ -5,7 +5,7 @@ #} #[doc(hidden)] impl uniffi::RustBufferViaFfi for {{ e.name() }} { - fn write(&self, buf: &mut Vec) { + fn write(self, buf: &mut Vec) { use uniffi::deps::bytes::BufMut; match self { {%- for variant in e.variants() %} diff --git a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs index d65a403f8a..70dd2e0b21 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs @@ -5,7 +5,7 @@ #} #[doc(hidden)] impl uniffi::RustBufferViaFfi for {{ e.name() }} { - fn write(&self, buf: &mut Vec) { + fn write(self, buf: &mut Vec) { use uniffi::deps::bytes::BufMut; match self { {%- for variant in e.variants() %} diff --git a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs index d8161a84ec..80bfb25bbc 100644 --- a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs @@ -6,11 +6,11 @@ #} #[doc(hidden)] impl uniffi::RustBufferViaFfi for {{ rec.name() }} { - fn write(&self, buf: &mut Vec) { + fn write(self, buf: &mut Vec) { // 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() %} - uniffi::ViaFfi::write(&self.{{ field.name() }}, buf); + uniffi::ViaFfi::write(self.{{ field.name() }}, buf); {%- endfor %} } From a5a71efb56c83f6b15e1757f839e331f8da0c375 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Wed, 4 Aug 2021 12:10:25 -0400 Subject: [PATCH 34/45] Refactor ViaFfi to use an associated type No functional changes yet -- this change is mostly just replacing `Self` with `Self::RustType`. The only actual code change was to shift the responsibility of lowering rust errors from the `rustcalls` module to the code that calls into it. This opens the door for handling external types because we can define our own struct that implements ViaFfi for the external type. --- uniffi/src/ffi/rustcalls.rs | 37 +++-- uniffi/src/lib.rs | 152 ++++++++++-------- .../templates/CallbackInterfaceTemplate.rs | 9 +- .../src/scaffolding/templates/EnumTemplate.rs | 6 +- .../scaffolding/templates/ErrorTemplate.rs | 6 +- .../scaffolding/templates/RecordTemplate.rs | 6 +- .../src/scaffolding/templates/macros.rs | 12 +- 7 files changed, 136 insertions(+), 92 deletions(-) diff --git a/uniffi/src/ffi/rustcalls.rs b/uniffi/src/ffi/rustcalls.rs index cec8736b7c..3bee138cb3 100644 --- a/uniffi/src/ffi/rustcalls.rs +++ b/uniffi/src/ffi/rustcalls.rs @@ -77,8 +77,6 @@ const CALL_PANIC: i8 = 2; pub trait FfiError: RustBufferViaFfi {} // Generalized rust call handling function -// callback is responsible for making the call to the rust function. If that function returns an -// `Err` value, callback should serialize the error into a `RustBuffer` to be returned over the FFI. fn make_call(out_status: &mut RustCallStatus, callback: F) -> R::Value where F: panic::UnwindSafe + FnOnce() -> Result, @@ -137,6 +135,9 @@ where /// 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 ViaFfi::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` @@ -149,7 +150,12 @@ where make_call(out_status, || Ok(callback())) } -/// Wrap a rust function call that returns a `Result<>` +/// 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 ViaFfi::lower()) /// /// - If the function returns an `Ok` value it will be unwrapped and returned /// - If the function returns an `Err`: @@ -160,13 +166,12 @@ where /// - If the function panics: /// - `out_status.code` will be set to `CALL_PANIC` /// - the return value is undefined -pub fn call_with_result(out_status: &mut RustCallStatus, callback: F) -> R::Value +pub fn call_with_result(out_status: &mut RustCallStatus, callback: F) -> R::Value where - F: panic::UnwindSafe + FnOnce() -> Result, - E: FfiError, + F: panic::UnwindSafe + FnOnce() -> Result, R: IntoFfi, { - make_call(out_status, || callback().map_err(|e| e.lower())) + make_call(out_status, callback) } #[cfg(test)] @@ -210,8 +215,10 @@ mod test { // Use RustBufferViaFfi to simplify lifting TestError out of RustBuffer to check it impl RustBufferViaFfi for TestError { - fn write(self, buf: &mut Vec) { - self.0.write(buf); + type RustType = Self; + + fn write(obj: Self::RustType, buf: &mut Vec) { + ::write(obj.0, buf); } fn try_read(buf: &mut &[u8]) -> Result { @@ -232,11 +239,15 @@ mod test { #[test] fn test_call_with_result() { let mut status = create_call_status(); - let return_value = call_with_result(&mut status, || function_with_result(0)); + 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)); + call_with_result(&mut status, || { + function_with_result(1).map_err(TestError::lower) + }); assert_eq!(status.code, CALL_ERROR); unsafe { assert_eq!( @@ -246,7 +257,9 @@ mod test { } let mut status = create_call_status(); - call_with_result(&mut status, || function_with_result(2)); + call_with_result(&mut status, || { + function_with_result(2).map_err(TestError::lower) + }); assert_eq!(status.code, CALL_PANIC); unsafe { assert_eq!( diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index a5e925dbd9..24938091c7 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -8,8 +8,8 @@ //! 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 [`ViaFfi`] trait, which must be implemented for any type that can -//! be passed across the FFI, and which determines: +//! The key concept here is the [`ViaFfi`] trait, we create an implementation for each type that +//! can be passed across the FFI. It's responsible for: //! //! * How to [represent](ViaFfi::FfiType) values of that type in the low-level C-style type //! system of the FFI layer. @@ -143,6 +143,14 @@ macro_rules! assert_compatible_version { /// implementations generated from your component UDL via the `uniffi-bindgen scaffolding` command. pub unsafe trait ViaFfi: Sized { + /// The type used in rust code. + /// + /// For primitive / standard types, we implement ViaFfi on the type itself and RustType=Self. + /// For user-defined types we create a unit struct and implement it there. This sidesteps + /// Rust's orphan rules with types from external crates, since we're not implementing ViaFfi on + /// the external type directly. + 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 @@ -162,7 +170,7 @@ pub unsafe trait ViaFfi: Sized { /// /// Note that this method takes an owned `self`; 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(self) -> Self::FfiType; + fn lower(obj: Self::RustType) -> Self::FfiType; /// Lift a rust value of the target type, from an FFI value of type Self::FfiType. /// @@ -172,7 +180,7 @@ pub unsafe trait ViaFfi: Sized { /// /// 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; + fn try_lift(v: Self::FfiType) -> Result; /// Write a rust value into a buffer, to send over the FFI in serialized form. /// @@ -182,7 +190,7 @@ pub unsafe trait ViaFfi: Sized { /// /// Note that this method takes an owned `self` because it's transfering ownership /// to the foreign language code via the RustBuffer. - fn write(self, buf: &mut Vec); + fn write(obj: Self::RustType, buf: &mut Vec); /// Read a rust value from a buffer, received over the FFI in serialized form. /// @@ -196,7 +204,7 @@ pub unsafe trait ViaFfi: Sized { /// 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; + fn try_read(buf: &mut &[u8]) -> Result; } /// A helper function to ensure we don't read past the end of a buffer. @@ -225,18 +233,19 @@ macro_rules! impl_via_ffi_for_num_primitive { $( paste! { unsafe impl ViaFfi for $T { + type RustType = Self; type FfiType = Self; - fn lower(self) -> Self::FfiType { - self + fn lower(obj: Self::RustType) -> Self::FfiType { + obj } fn try_lift(v: Self::FfiType) -> Result { Ok(v) } - fn write(self, buf: &mut Vec) { - buf.[](self); + fn write(obj: Self::RustType, buf: &mut Vec) { + buf.[](obj); } fn try_read(buf: &mut &[u8]) -> Result { @@ -258,17 +267,18 @@ impl_via_ffi_for_num_primitive! { /// Booleans are passed as an `i8` in order to avoid problems with handling /// C-compatible boolean values on JVM-based languages. unsafe impl ViaFfi for bool { + type RustType = Self; type FfiType = i8; - fn lower(self) -> Self::FfiType { - if self { + fn lower(obj: Self::RustType) -> Self::FfiType { + if obj { 1 } else { 0 } } - fn try_lift(v: Self::FfiType) -> Result { + fn try_lift(v: Self::FfiType) -> Result { Ok(match v { 0 => false, 1 => true, @@ -276,13 +286,13 @@ unsafe impl ViaFfi for bool { }) } - fn write(self, buf: &mut Vec) { - buf.put_i8(ViaFfi::lower(self)); + fn write(obj: Self::RustType, buf: &mut Vec) { + buf.put_i8(::lower(obj)); } - fn try_read(buf: &mut &[u8]) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, 1)?; - ViaFfi::try_lift(buf.get_i8()) + ::try_lift(buf.get_i8()) } } @@ -298,18 +308,19 @@ unsafe impl ViaFfi for bool { /// followed by utf8-encoded bytes. (It's a signed integer because unsigned types are /// currently experimental in Kotlin). unsafe impl ViaFfi 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(self) -> Self::FfiType { - RustBuffer::from_vec(self.into_bytes()) + 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 { + fn try_lift(v: Self::FfiType) -> Result { 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` @@ -319,15 +330,15 @@ unsafe impl ViaFfi for String { Ok(unsafe { String::from_utf8_unchecked(v) }) } - fn write(self, buf: &mut Vec) { + fn write(obj: Self::RustType, buf: &mut Vec) { // 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(self.len()).unwrap(); + let len = i32::try_from(obj.len()).unwrap(); buf.put_i32(len); // We limit strings to u32::MAX bytes - buf.put(self.as_bytes()); + buf.put(obj.as_bytes()); } - fn try_read(buf: &mut &[u8]) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, 4)?; let len = usize::try_from(buf.get_i32())?; check_remaining(buf, len)?; @@ -347,35 +358,37 @@ unsafe impl ViaFfi for String { /// C-compatible value, you can use this trait to implement `lower()` in terms of `write()` and /// `lift` in terms of `read()`. pub trait RustBufferViaFfi: Sized { - fn write(self, buf: &mut Vec); - fn try_read(buf: &mut &[u8]) -> Result; + type RustType; + fn write(obj: Self::RustType, buf: &mut Vec); + fn try_read(buf: &mut &[u8]) -> Result; } unsafe impl ViaFfi for T { + type RustType = T::RustType; type FfiType = RustBuffer; - fn lower(self) -> RustBuffer { + fn lower(obj: Self::RustType) -> RustBuffer { let mut buf = Vec::new(); - RustBufferViaFfi::write(self, &mut buf); + ::write(obj, &mut buf); RustBuffer::from_vec(buf) } - fn try_lift(v: RustBuffer) -> Result { + fn try_lift(v: RustBuffer) -> Result { let vec = v.destroy_into_vec(); let mut buf = vec.as_slice(); - let value = RustBufferViaFfi::try_read(&mut buf)?; + let value = T::try_read(&mut buf)?; if buf.remaining() != 0 { bail!("junk data left in buffer after lifting") } Ok(value) } - fn write(self, buf: &mut Vec) { - RustBufferViaFfi::write(self, buf) + fn write(obj: Self::RustType, buf: &mut Vec) { + T::write(obj, buf) } - fn try_read(buf: &mut &[u8]) -> Result { - RustBufferViaFfi::try_read(buf) + fn try_read(buf: &mut &[u8]) -> Result { + T::try_read(buf) } } @@ -394,9 +407,11 @@ unsafe impl ViaFfi for T { /// 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 RustBufferViaFfi for SystemTime { - fn write(self, buf: &mut Vec) { + type RustType = Self; + + fn write(obj: Self::RustType, buf: &mut Vec) { let mut sign = 1; - let epoch_offset = self + let epoch_offset = obj .duration_since(SystemTime::UNIX_EPOCH) .unwrap_or_else(|error| { sign = -1; @@ -411,7 +426,7 @@ impl RustBufferViaFfi for SystemTime { buf.put_u32(epoch_offset.subsec_nanos()); } - fn try_read(buf: &mut &[u8]) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, 12)?; let seconds = buf.get_i64(); let nanos = buf.get_u32(); @@ -434,12 +449,14 @@ impl RustBufferViaFfi for SystemTime { /// of the magnitude. The nanosecond portion is expected to be between 0 /// and 999,999,999. impl RustBufferViaFfi for Duration { - fn write(self, buf: &mut Vec) { - buf.put_u64(self.as_secs()); - buf.put_u32(self.subsec_nanos()); + type RustType = Self; + + fn write(obj: Self::RustType, buf: &mut Vec) { + buf.put_u64(obj.as_secs()); + buf.put_u32(obj.subsec_nanos()); } - fn try_read(buf: &mut &[u8]) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, 12)?; Ok(Duration::new(buf.get_u64(), buf.get_u32())) } @@ -455,17 +472,19 @@ impl RustBufferViaFfi for Duration { /// `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 RustBufferViaFfi for Option { - fn write(self, buf: &mut Vec) { - match self { + type RustType = Option; + + fn write(obj: Self::RustType, buf: &mut Vec) { + match obj { None => buf.put_i8(0), Some(v) => { buf.put_i8(1); - ViaFfi::write(v, buf); + ::write(v, buf); } } } - fn try_read(buf: &mut &[u8]) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, 1)?; Ok(match buf.get_i8() { 0 => None, @@ -485,16 +504,18 @@ impl RustBufferViaFfi for Option { /// than serializing, and perhaps even pass other vector types using a /// similar struct. But that's for future work. impl RustBufferViaFfi for Vec { - fn write(self, buf: &mut Vec) { + type RustType = Vec; + + fn write(obj: Self::RustType, buf: &mut Vec) { // TODO: would be nice not to panic here :-/ - let len = i32::try_from(self.len()).unwrap(); + let len = i32::try_from(obj.len()).unwrap(); buf.put_i32(len); // We limit arrays to i32::MAX items - for item in self.into_iter() { - ViaFfi::write(item, buf); + for item in obj.into_iter() { + ::write(item, buf); } } - fn try_read(buf: &mut &[u8]) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, 4)?; let len = usize::try_from(buf.get_i32())?; let mut vec = Vec::with_capacity(len); @@ -514,17 +535,19 @@ impl RustBufferViaFfi for Vec { /// key followed by the value) in turn. /// (It's a signed type due to limits of the JVM). impl RustBufferViaFfi for HashMap { - fn write(self, buf: &mut Vec) { + type RustType = HashMap; + + fn write(obj: Self::RustType, buf: &mut Vec) { // TODO: would be nice not to panic here :-/ - let len = i32::try_from(self.len()).unwrap(); + let len = i32::try_from(obj.len()).unwrap(); buf.put_i32(len); // We limit HashMaps to i32::MAX entries - for (key, value) in self.into_iter() { - ViaFfi::write(key, buf); - ViaFfi::write(value, buf); + for (key, value) in obj.into_iter() { + ::write(key, buf); + ::write(value, buf); } } - fn try_read(buf: &mut &[u8]) -> Result { + fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, 4)?; let len = usize::try_from(buf.get_i32())?; let mut map = HashMap::with_capacity(len); @@ -543,6 +566,7 @@ impl RustBufferViaFfi for HashMap { /// by reference must be encapsulated in an `Arc`, and must be safe to share /// across threads. unsafe impl ViaFfi for std::sync::Arc { + type RustType = Self; // Don't use a pointer to as that requires a `pub ` type FfiType = *const std::os::raw::c_void; @@ -555,8 +579,8 @@ unsafe impl ViaFfi for std::sync::Arc { /// 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(self) -> Self::FfiType { - std::sync::Arc::into_raw(self) as Self::FfiType + 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` that is owned by @@ -564,7 +588,7 @@ unsafe impl ViaFfi for std::sync::Arc { /// /// 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 { + fn try_lift(v: Self::FfiType) -> Result { let v = v as *const T; // We musn't drop the `Arc` that is owned by the foreign-language code. let foreign_arc = std::mem::ManuallyDrop::new(unsafe { Self::from_raw(v) }); @@ -580,9 +604,9 @@ unsafe impl ViaFfi for std::sync::Arc { /// 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(self, buf: &mut Vec) { + fn write(obj: Self::RustType, buf: &mut Vec) { static_assertions::const_assert!(std::mem::size_of::<*const std::ffi::c_void>() <= 8); - buf.put_u64(self.lower() as u64); + buf.put_u64(Self::lower(obj) as u64); } /// When reading as a field of a complex structure, we receive a "borrow" of the `Arc` @@ -590,7 +614,7 @@ unsafe impl ViaFfi for std::sync::Arc { /// /// 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 { + fn try_read(buf: &mut &[u8]) -> Result { 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) @@ -610,14 +634,14 @@ mod test { #[test] fn timestamp_roundtrip_post_epoch() { let expected = SystemTime::UNIX_EPOCH + Duration::new(100, 100); - let result = SystemTime::try_lift(expected.lower()).expect("Failed to lift!"); + 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(expected.lower()).expect("Failed to lift!"); + 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/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs index ea602fb398..dee8a62ddc 100644 --- a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs @@ -82,6 +82,7 @@ impl {{ trait_name }} for {{ trait_impl }} { } unsafe impl uniffi::ViaFfi for {{ trait_impl }} { + type RustType = Self; type FfiType = u64; // Lower and write are trivially implemented, but carry lots of thread safety risks, down to @@ -95,13 +96,13 @@ unsafe impl uniffi::ViaFfi for {{ trait_impl }} { // // They are implemented here for runtime use, but at scaffolding.rs will bail instead of generating // the code to call these methods. - fn lower(self) -> Self::FfiType { - self.handle + fn lower(obj: Self::RustType) -> Self::FfiType { + obj.handle } - fn write(self, buf: &mut Vec) { + fn write(obj: Self::RustType, buf: &mut Vec) { use uniffi::deps::bytes::BufMut; - buf.put_u64(self.handle); + buf.put_u64(obj.handle); } fn try_lift(v: Self::FfiType) -> uniffi::deps::anyhow::Result { diff --git a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs index 3542da7833..8b0e6b18ad 100644 --- a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs @@ -5,9 +5,11 @@ #} #[doc(hidden)] impl uniffi::RustBufferViaFfi for {{ e.name() }} { - fn write(self, buf: &mut Vec) { + type RustType = Self; + + fn write(obj: Self::RustType, buf: &mut Vec) { use uniffi::deps::bytes::BufMut; - match self { + match obj { {%- for variant in e.variants() %} {{ e.name() }}::{{ variant.name() }} { {% for field in variant.fields() %}{{ field.name() }}, {%- endfor %} } => { buf.put_i32({{ loop.index }}); diff --git a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs index 70dd2e0b21..210b622a09 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs @@ -5,9 +5,11 @@ #} #[doc(hidden)] impl uniffi::RustBufferViaFfi for {{ e.name() }} { - fn write(self, buf: &mut Vec) { + type RustType = Self; + + fn write(obj: Self::RustType, buf: &mut Vec) { use uniffi::deps::bytes::BufMut; - match self { + match obj { {%- for variant in e.variants() %} {{ e.name() }}::{{ variant.name() }}{% if variant.has_fields() %} { {% for field in variant.fields() %}{{ field.name() }}, {%- endfor %} }{% else %}{..}{% endif %} => { buf.put_i32({{ loop.index }}); diff --git a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs index 80bfb25bbc..357da05c3a 100644 --- a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs @@ -6,11 +6,13 @@ #} #[doc(hidden)] impl uniffi::RustBufferViaFfi for {{ rec.name() }} { - fn write(self, buf: &mut Vec) { + type RustType = Self; + + fn write(obj: Self::RustType, buf: &mut Vec) { // 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() %} - uniffi::ViaFfi::write(self.{{ field.name() }}, buf); + <{{ field.type_()|type_rs }} as uniffi::ViaFfi>::write(obj.{{ field.name() }}, buf); {%- endfor %} } diff --git a/uniffi_bindgen/src/scaffolding/templates/macros.rs b/uniffi_bindgen/src/scaffolding/templates/macros.rs index 390f7d480e..a57b31ab7c 100644 --- a/uniffi_bindgen/src/scaffolding/templates/macros.rs +++ b/uniffi_bindgen/src/scaffolding/templates/macros.rs @@ -47,8 +47,8 @@ {% macro to_rs_constructor_call(obj, cons) %} {% match cons.throws() %} {% when Some with (e) %} - uniffi::call_with_result(call_status, || -> Result<_, {{ e }}> { - let _new = {% call construct(obj, cons) %}?; + uniffi::call_with_result(call_status, || { + let _new = {% call construct(obj, cons) %}.map_err(<{{ e }} as uniffi::ViaFfi>::lower)?; let _arc = std::sync::Arc::new(_new); Ok({{ "_arc"|lower_rs(obj.type_()) }}) }) @@ -64,8 +64,8 @@ {% macro to_rs_method_call(obj, meth) -%} {% match meth.throws() -%} {% when Some with (e) -%} -uniffi::call_with_result(call_status, || -> Result<{% call return_type_func(meth) %}, {{e}}> { - let _retval = {{ obj.name() }}::{% call to_rs_call(meth) %}?; +uniffi::call_with_result(call_status, || { + let _retval = {{ obj.name() }}::{% call to_rs_call(meth) %}.map_err(<{{ e }} as uniffi::ViaFfi>::lower)?; Ok({% call ret(meth) %}) }) {% else %} @@ -84,8 +84,8 @@ uniffi::call_with_output(call_status, || { {% macro to_rs_function_call(func) %} {% match func.throws() %} {% when Some with (e) %} -uniffi::call_with_result(call_status, || -> Result<{% call return_type_func(func) %}, {{e}}> { - let _retval = {% call to_rs_call(func) %}?; +uniffi::call_with_result(call_status, || { + let _retval = {% call to_rs_call(func) %}.map_err(<{{ e }} as uniffi::ViaFfi>::lower)?; Ok({% call ret(func) %}) }) {% else %} From 40031f743e7133afce0b23d9e7cfcafe43e8c9ec Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Wed, 4 Aug 2021 16:06:55 -0400 Subject: [PATCH 35/45] New pattern for mapping types to ViaFfi traits Before, the system was that if you wanted to lift/lower type T, then you would use . This change implements a more flexible mapping: - The `Type` enum defines `viaffi_impl_name()`, which determines which ViaFfi implementation to use for that type. Sometimes it's the type, sometimes it's something else. - With the help of some template code, we can map any `Type` to Rust code to do the lowering/lifting (e.g. `Type` -> `::lift(val)`) This gives us some flexibility and moves us towards composable ViaFfi types. One nice feature is all of the smarts are in the ViaFfi impl, which can use whatever Rust techniques it wants to to implement composition. It's also just nice to have almost all of the code in one place, rather than spread out and mixed up between template code, rust code, the component interface, etc. Some things are already nicer: - For Records/Enums/Errors, we define a new unit-struct to implement ViaFfi for the type. This allows us to support external types without hitting the orphan rule. - We can implement Option with `Option<{{ Type.viaffi_impl_name()}}>` and a generic implementation and this should work with any adapter-style ViaFfi implementations we write in the future. - We can move the code to wrap CallbackInteface in a `Box<>` out of the template code and into the CallbackInterface template. Before it was split up between that template code and the scaffolding filters. Also, Option was not working, this adds support for that (maybe Vec too, although I haven't tested that one). --- fixtures/callbacks/src/callbacks.udl | 1 + fixtures/callbacks/src/lib.rs | 4 + .../tests/bindings/test_callbacks.kts | 4 + uniffi_bindgen/src/interface/enum_.rs | 10 +- uniffi_bindgen/src/interface/error.rs | 7 ++ uniffi_bindgen/src/interface/function.rs | 6 + uniffi_bindgen/src/interface/object.rs | 12 ++ uniffi_bindgen/src/interface/record.rs | 10 +- uniffi_bindgen/src/scaffolding/mod.rs | 104 ++++++++---------- .../templates/CallbackInterfaceTemplate.rs | 38 +++---- .../src/scaffolding/templates/EnumTemplate.rs | 15 ++- .../scaffolding/templates/ErrorTemplate.rs | 21 ++-- .../scaffolding/templates/RecordTemplate.rs | 18 +-- .../src/scaffolding/templates/macros.rs | 25 ++--- .../templates/scaffolding_template.rs | 3 +- 15 files changed, 157 insertions(+), 121 deletions(-) diff --git a/fixtures/callbacks/src/callbacks.udl b/fixtures/callbacks/src/callbacks.udl index db61326e66..2c9fecab6b 100644 --- a/fixtures/callbacks/src/callbacks.udl +++ b/fixtures/callbacks/src/callbacks.udl @@ -16,6 +16,7 @@ interface RustGetters { string get_string(ForeignGetters callback, string v, boolean arg2); string? get_option(ForeignGetters callback, string? v, boolean arg2); sequence get_list(ForeignGetters callback, sequence v, boolean arg2); + string? get_string_optional_callback(ForeignGetters? callback, string v, boolean arg2); }; /// These objects are implemented by the foreign language and passed diff --git a/fixtures/callbacks/src/lib.rs b/fixtures/callbacks/src/lib.rs index 8f41583614..16e649f9f2 100644 --- a/fixtures/callbacks/src/lib.rs +++ b/fixtures/callbacks/src/lib.rs @@ -33,6 +33,10 @@ impl RustGetters { fn get_list(&self, callback: Box, v: Vec, arg2: bool) -> Vec { callback.get_list(v, arg2) } + + fn get_string_optional_callback<'a>(&self, callback: Option>, v: String, arg2: bool) -> Option { + callback.map(|c| c.get_string(v, arg2)) + } } impl Default for RustGetters { diff --git a/fixtures/callbacks/tests/bindings/test_callbacks.kts b/fixtures/callbacks/tests/bindings/test_callbacks.kts index 863b531cbb..e5c0591d1b 100644 --- a/fixtures/callbacks/tests/bindings/test_callbacks.kts +++ b/fixtures/callbacks/tests/bindings/test_callbacks.kts @@ -45,6 +45,10 @@ listOf("Some", null).forEach { v -> val observed = rustGetters.getOption(callback, v, flag) assert(expected == observed) { "roundtripping through callback: $expected != $observed" } } + +assert(rustGetters.getStringOptionalCallback(callback, "TestString", false) == "TestString") +assert(rustGetters.getStringOptionalCallback(null, "TestString", false) == null) + rustGetters.destroy() // 2. Pass the callback in as a constructor argument, to be stored on the Object struct. diff --git a/uniffi_bindgen/src/interface/enum_.rs b/uniffi_bindgen/src/interface/enum_.rs index 4b230c91fe..3498ccf577 100644 --- a/uniffi_bindgen/src/interface/enum_.rs +++ b/uniffi_bindgen/src/interface/enum_.rs @@ -100,6 +100,12 @@ impl Enum { &self.name } + pub fn type_(&self) -> Type { + // *sigh* at the clone here, the relationship between a ComponentInterace + // and its contained types could use a bit of a cleanup. + Type::Enum(self.name.clone()) + } + pub fn variants(&self) -> Vec<&Variant> { self.variants.iter().collect() } @@ -109,9 +115,7 @@ impl Enum { } pub fn contains_object_references(&self, ci: &ComponentInterface) -> bool { - // *sigh* at the clone here, the relationship between a ComponentInterace - // and its contained types could use a bit of a cleanup. - ci.type_contains_object_references(&Type::Enum(self.name.clone())) + ci.type_contains_object_references(&self.type_()) } pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool { diff --git a/uniffi_bindgen/src/interface/error.rs b/uniffi_bindgen/src/interface/error.rs index 1275520742..750b907de1 100644 --- a/uniffi_bindgen/src/interface/error.rs +++ b/uniffi_bindgen/src/interface/error.rs @@ -85,6 +85,7 @@ use anyhow::Result; use super::enum_::{Enum, Variant}; +use super::types::Type; use super::{APIConverter, ComponentInterface}; /// Represents an Error that might be thrown by functions/methods in the component interface. @@ -107,6 +108,12 @@ impl Error { } } + pub fn type_(&self) -> Type { + // *sigh* at the clone here, the relationship between a ComponentInterace + // and its contained types could use a bit of a cleanup. + Type::Error(self.name.clone()) + } + pub fn name(&self) -> &str { &self.name } diff --git a/uniffi_bindgen/src/interface/function.rs b/uniffi_bindgen/src/interface/function.rs index 3da5a6d38a..33e3e747a2 100644 --- a/uniffi_bindgen/src/interface/function.rs +++ b/uniffi_bindgen/src/interface/function.rs @@ -82,6 +82,12 @@ impl Function { self.attributes.get_throws_err() } + pub fn throws_type(&self) -> Option { + self.attributes + .get_throws_err() + .map(|name| Type::Error(name.to_owned())) + } + pub fn derive_ffi_func(&mut self, ci_prefix: &str) -> Result<()> { self.ffi_func.name.push_str(ci_prefix); self.ffi_func.name.push('_'); diff --git a/uniffi_bindgen/src/interface/object.rs b/uniffi_bindgen/src/interface/object.rs index 35aee452c1..07f3c5f3b6 100644 --- a/uniffi_bindgen/src/interface/object.rs +++ b/uniffi_bindgen/src/interface/object.rs @@ -244,6 +244,12 @@ impl Constructor { self.attributes.get_throws_err() } + pub fn throws_type(&self) -> Option { + self.attributes + .get_throws_err() + .map(|name| Type::Error(name.to_owned())) + } + fn derive_ffi_func(&mut self, ci_prefix: &str, obj_prefix: &str) { self.ffi_func.name.push_str(ci_prefix); self.ffi_func.name.push('_'); @@ -357,6 +363,12 @@ impl Method { self.attributes.get_throws_err() } + pub fn throws_type(&self) -> Option { + 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() } diff --git a/uniffi_bindgen/src/interface/record.rs b/uniffi_bindgen/src/interface/record.rs index c9652c99a1..e8283e5b14 100644 --- a/uniffi_bindgen/src/interface/record.rs +++ b/uniffi_bindgen/src/interface/record.rs @@ -66,14 +66,18 @@ impl Record { &self.name } + pub fn type_(&self) -> Type { + // *sigh* at the clone here, the relationship between a ComponentInterace + // and its contained types could use a bit of a cleanup. + Type::Record(self.name.clone()) + } + pub fn fields(&self) -> Vec<&Field> { self.fields.iter().collect() } pub fn contains_object_references(&self, ci: &ComponentInterface) -> bool { - // *sigh* at the clone here, the relationship between a ComponentInterace - // and its contained types could use a bit of a cleanup. - ci.type_contains_object_references(&Type::Record(self.name.clone())) + ci.type_contains_object_references(&self.type_()) } pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool { diff --git a/uniffi_bindgen/src/scaffolding/mod.rs b/uniffi_bindgen/src/scaffolding/mod.rs index 04ce19d30d..a041e54d92 100644 --- a/uniffi_bindgen/src/scaffolding/mod.rs +++ b/uniffi_bindgen/src/scaffolding/mod.rs @@ -21,10 +21,8 @@ impl<'a> RustScaffolding<'a> { } } } - mod filters { use super::*; - use std::fmt; pub fn type_rs(type_: &Type) -> Result { Ok(match type_ { @@ -70,67 +68,53 @@ mod filters { }) } - pub fn lower_rs(nm: &dyn fmt::Display, type_: &Type) -> Result { - // By explicitly naming the type here, we help the rust compiler to type-check the user-provided - // implementations of the functions that we're wrapping (and also to type-check our generated code). + /// Get the name of the ViaFfi 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 + pub fn viaffi_impl_name(type_: &Type) -> askama::Result { Ok(match type_ { - Type::CallbackInterface(type_name) => unimplemented!( - "uniffi::ViaFfi::lower is not supported for callback interfaces ({})", - type_name - ), - _ => format!("<{} as uniffi::ViaFfi>::lower({})", type_rs(type_)?, nm), - }) - } - - pub fn lift_rs(nm: &dyn fmt::Display, type_: &Type) -> Result { - // By explicitly naming the type here, we help the rust compiler to type-check the user-provided - // implementations of the functions that we're wrapping (and also to type-check our generated code). - // This will panic if the bindings provide an invalid value over the FFI. - Ok(match type_ { - Type::CallbackInterface(type_name) => format!( - "Box::new(<{}Proxy as uniffi::ViaFfi>::try_lift({}).unwrap())", - type_name, nm, - ), - _ => format!( - "<{} as uniffi::ViaFfi>::try_lift({}).unwrap()", - type_rs(type_)?, - nm - ), - }) - } - - /// Get a Rust expression for writing a value into a byte buffer. - pub fn write_rs( - nm: &dyn fmt::Display, - target: &dyn fmt::Display, - type_: &Type, - ) -> Result { - Ok(match type_ { - Type::CallbackInterface(type_name) => unimplemented!( - "uniffi::ViaFfi::write is not supported for callback interfaces ({})", - type_name - ), - _ => format!( - "<{} as uniffi::ViaFfi>::write({}, {})", - type_rs(type_)?, - nm, - target - ), + // Timestamp/Duraration are handled by standard types + Type::Timestamp => "std::time::SystemTime".into(), + Type::Duration => "std::time::Duration".into(), + // Object is handled by Arc + Type::Object(name) => format!("std::sync::Arc<{}>", name), + // Other user-defined types are handled by a unit-struct that we generate. The + // ViaFfi implementation for this can be found in one of the scaffolding template code. + // + // We generate a unit-struct to sidestep Rust's orphan rules since we can't directly + // implement ViaFfi on a type from an external crate. + // + // 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!("ViaFfi{}", type_.canonical_name()) + } + // Wrapper types are implemented by generics that wrap the viaffi implementation of the + // inner type. + Type::Optional(inner) => format!("Option<{}>", viaffi_impl_name(inner)?), + Type::Sequence(inner) => format!("Vec<{}>", viaffi_impl_name(inner)?), + Type::Map(inner) => format!("HashMap", viaffi_impl_name(inner)?), + // 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(), }) } - /// Get a Rust expression for writing a value into a byte buffer. - pub fn read_rs(target: &dyn fmt::Display, type_: &Type) -> Result { - Ok(match type_ { - Type::CallbackInterface(type_name) => unimplemented!( - "uniffi::ViaFfi::try_read is not supported for callback interfaces ({})", - type_name - ), - _ => format!( - "<{} as uniffi::ViaFfi>::try_read({}).unwrap()", - type_rs(type_)?, - target - ), - }) + // Map a type to Rust code that specifies the ViaFfi implementation. + // + // This outputs something like `` + pub fn viaffi_impl(type_: &Type) -> Result { + Ok(format!("<{} as uniffi::ViaFfi>", viaffi_impl_name(type_)?)) } } diff --git a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs index dee8a62ddc..a837abe0b7 100644 --- a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs @@ -11,7 +11,7 @@ // - a `Drop` `impl`, which tells the foreign language to forget about the real callback object. #} {% let trait_name = cbi.name() -%} -{% let trait_impl = format!("{}Proxy", trait_name) -%} +{% let trait_impl = cbi.type_()|viaffi_impl_name -%} {% let foreign_callback_internals = format!("foreign_callback_{}_internals", trait_name)|upper -%} // Register a foreign callback for getting across the FFI. @@ -59,9 +59,9 @@ impl {{ trait_name }} for {{ trait_impl }} { {% else -%} let mut args_buf = Vec::new(); {% endif -%} - {% for arg in meth.arguments() -%} - {{ arg.name()|write_rs("&mut args_buf", arg.type_()) -}}; - {% endfor -%} + {%- for arg in meth.arguments() %} + {{ arg.type_()|viaffi_impl }}::write({{ arg.name() }}, &mut args_buf); + {%- endfor -%} let args_rbuf = uniffi::RustBuffer::from_vec(args_buf); {#- Calling into foreign code. #} @@ -73,7 +73,7 @@ impl {{ trait_name }} for {{ trait_impl }} { {% when Some with (return_type) -%} let vec = ret_rbuf.destroy_into_vec(); let mut ret_buf = vec.as_slice(); - {{ "&mut ret_buf"|read_rs(return_type) }} + {{ return_type|viaffi_impl }}::try_read(&mut ret_buf).unwrap() {%- else -%} uniffi::RustBuffer::destroy(ret_rbuf); {%- endmatch %} @@ -82,34 +82,32 @@ impl {{ trait_name }} for {{ trait_impl }} { } unsafe impl uniffi::ViaFfi for {{ trait_impl }} { - type RustType = Self; + // This RustType allows for rust code that inputs this type as a Box param + type RustType = Box; type FfiType = u64; - // Lower and write are trivially implemented, but carry lots of thread safety risks, down to - // impedence mismatches between Rust and foreign languages, and our uncertainty around implementations - // of concurrent handlemaps. + // Lower and write are tricky to implement because we have a dyn trait as our type. There's + // probably a way to, but but this carries lots of thread safety risks, down to impedence + // 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. - // - // They are implemented here for runtime use, but at scaffolding.rs will bail instead of generating - // the code to call these methods. - fn lower(obj: Self::RustType) -> Self::FfiType { - obj.handle + fn lower(_obj: Self::RustType) -> Self::FfiType { + panic!("Lowering CallbackInterface not supported") } - fn write(obj: Self::RustType, buf: &mut Vec) { - use uniffi::deps::bytes::BufMut; - buf.put_u64(obj.handle); + fn write(_obj: Self::RustType, _buf: &mut Vec) { + panic!("Writing CallbackInterface not supported") } - fn try_lift(v: Self::FfiType) -> uniffi::deps::anyhow::Result { - Ok(Self { handle: v }) + fn try_lift(v: Self::FfiType) -> uniffi::deps::anyhow::Result { + Ok(Box::new(Self { handle: v })) } - fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { + fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { use uniffi::deps::bytes::Buf; uniffi::check_remaining(buf, 8)?; ::try_lift(buf.get_u64()) diff --git a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs index 8b0e6b18ad..b23f91d6b3 100644 --- a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs @@ -2,10 +2,15 @@ // 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 the orphan rule. #} + +struct {{ e.type_()|viaffi_impl_name }}; + #[doc(hidden)] -impl uniffi::RustBufferViaFfi for {{ e.name() }} { - type RustType = Self; +impl uniffi::RustBufferViaFfi for {{ e.type_()|viaffi_impl_name }} { + type RustType = {{ e.name() }}; fn write(obj: Self::RustType, buf: &mut Vec) { use uniffi::deps::bytes::BufMut; @@ -14,21 +19,21 @@ impl uniffi::RustBufferViaFfi for {{ e.name() }} { {{ e.name() }}::{{ variant.name() }} { {% for field in variant.fields() %}{{ field.name() }}, {%- endfor %} } => { buf.put_i32({{ loop.index }}); {% for field in variant.fields() -%} - <{{ field.type_()|type_rs }} as uniffi::ViaFfi>::write({{ field.name() }}, buf); + {{ field.type_()|viaffi_impl }}::write({{ field.name() }}, buf); {%- endfor %} }, {%- endfor %} }; } - fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { + fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result<{{ e.name() }}> { use uniffi::deps::bytes::Buf; uniffi::check_remaining(buf, 4)?; Ok(match buf.get_i32() { {%- for variant in e.variants() %} {{ loop.index }} => {{ e.name() }}::{{ variant.name() }}{% if variant.has_fields() %} { {% for field in variant.fields() %} - {{ field.name() }}: <{{ field.type_()|type_rs }} as uniffi::ViaFfi>::try_read(buf)?, + {{ field.name() }}: {{ field.type_()|viaffi_impl }}::try_read(buf)?, {%- endfor %} }{% endif %}, {%- endfor %} diff --git a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs index 210b622a09..23ac6c1256 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs @@ -2,19 +2,24 @@ // 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 the orphan rule. #} + +struct {{ e.type_()|viaffi_impl_name }}; + #[doc(hidden)] -impl uniffi::RustBufferViaFfi for {{ e.name() }} { - type RustType = Self; +impl uniffi::RustBufferViaFfi for {{ e.type_()|viaffi_impl_name }} { + type RustType = {{ e.name() }}; - fn write(obj: Self::RustType, buf: &mut Vec) { + fn write(obj: {{ e.name() }}, buf: &mut Vec) { use uniffi::deps::bytes::BufMut; match obj { {%- for variant in e.variants() %} {{ e.name() }}::{{ variant.name() }}{% if variant.has_fields() %} { {% for field in variant.fields() %}{{ field.name() }}, {%- endfor %} }{% else %}{..}{% endif %} => { buf.put_i32({{ loop.index }}); {% for field in variant.fields() -%} - <{{ field.type_()|type_rs }} as uniffi::ViaFfi>::write({{ field.name() }}, buf); + {{ field.type_()|viaffi_impl }}::write({{ field.name() }}, buf); {%- endfor %} }, {%- endfor %} @@ -26,18 +31,18 @@ impl uniffi::RustBufferViaFfi for {{ e.name() }} { // the Rust enum has fields and they're just not listed. Let's just punt on implementing // try_read() to avoid that case. It should be no issue since passing back Errors into the // rust code isn't supported. - fn try_read(_buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { + fn try_read(_buf: &mut &[u8]) -> uniffi::deps::anyhow::Result<{{ e.name() }}> { panic!("try_read not supported for fieldless errors"); } {% else %} - fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { + fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result<{{ e.name() }}> { use uniffi::deps::bytes::Buf; uniffi::check_remaining(buf, 4)?; Ok(match buf.get_i32() { {%- for variant in e.variants() %} {{ loop.index }} => {{ e.name() }}::{{ variant.name() }}{% if variant.has_fields() %} { {% for field in variant.fields() %} - {{ field.name() }}: <{{ field.type_()|type_rs }} as uniffi::ViaFfi>::try_read(buf)?, + {{ field.name() }}: {{ field.type_()|viaffi_impl }}::try_read(buf)?, {%- endfor %} }{% endif %}, {%- endfor %} @@ -47,4 +52,4 @@ impl uniffi::RustBufferViaFfi for {{ e.name() }} { {% endif %} } -impl uniffi::FfiError for {{ e.name() }} { } +impl uniffi::FfiError for {{ e.type_()|viaffi_impl_name }} { } diff --git a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs index 357da05c3a..8db08b4ad0 100644 --- a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs @@ -3,23 +3,27 @@ // 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 the orphan rule. #} +struct {{ rec.type_()|viaffi_impl_name }}; + #[doc(hidden)] -impl uniffi::RustBufferViaFfi for {{ rec.name() }} { - type RustType = Self; +impl uniffi::RustBufferViaFfi for {{ rec.type_()|viaffi_impl_name }} { + type RustType = {{ rec.name() }}; - fn write(obj: Self::RustType, buf: &mut Vec) { + fn write(obj: {{ rec.name() }}, buf: &mut Vec) { // 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_()|type_rs }} as uniffi::ViaFfi>::write(obj.{{ field.name() }}, buf); + {{ field.type_()|viaffi_impl }}::write(obj.{{ field.name() }}, buf); {%- endfor %} } - fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { - Ok(Self { + fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result<{{ rec.name() }}> { + Ok({{ rec.name() }} { {%- for field in rec.fields() %} - {{ field.name() }}: <{{ field.type_()|type_rs }} as uniffi::ViaFfi>::try_read(buf)?, + {{ field.name() }}: {{ field.type_()|viaffi_impl }}::try_read(buf)?, {%- endfor %} }) } diff --git a/uniffi_bindgen/src/scaffolding/templates/macros.rs b/uniffi_bindgen/src/scaffolding/templates/macros.rs index a57b31ab7c..f161f1c51b 100644 --- a/uniffi_bindgen/src/scaffolding/templates/macros.rs +++ b/uniffi_bindgen/src/scaffolding/templates/macros.rs @@ -9,7 +9,7 @@ {%- macro _arg_list_rs_call(func) %} {%- for arg in func.full_arguments() %} {%- if arg.by_ref() %}&{% endif %} - {{- arg.name()|lift_rs(arg.type_()) }} + {{- arg.type_()|viaffi_impl }}::try_lift({{ arg.name() }}).unwrap() {%- if !loop.last %}, {% endif %} {%- endfor %} {%- endmacro -%} @@ -38,34 +38,34 @@ {% 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) %}{{ "_retval"|lower_rs(return_type) }}{% else %}_retval{% endmatch %}{% endmacro %} +{% macro ret(func) %}{% match func.return_type() %}{% when Some with (return_type) %}{{ return_type|viaffi_impl }}::lower(_retval){% else %}_retval{% endmatch %}{% endmacro %} {% macro construct(obj, cons) %} {{- obj.name() }}::{% call to_rs_call(cons) -%} {% endmacro %} {% macro to_rs_constructor_call(obj, cons) %} -{% match cons.throws() %} +{% match cons.throws_type() %} {% when Some with (e) %} uniffi::call_with_result(call_status, || { - let _new = {% call construct(obj, cons) %}.map_err(<{{ e }} as uniffi::ViaFfi>::lower)?; + let _new = {% call construct(obj, cons) %}.map_err({{ e|viaffi_impl }}::lower)?; let _arc = std::sync::Arc::new(_new); - Ok({{ "_arc"|lower_rs(obj.type_()) }}) + Ok({{ obj.type_()|viaffi_impl }}::lower(_arc)) }) {% else %} uniffi::call_with_output(call_status, || { let _new = {% call construct(obj, cons) %}; let _arc = std::sync::Arc::new(_new); - {{ "_arc"|lower_rs(obj.type_()) }} + {{ obj.type_()|viaffi_impl }}::lower(_arc) }) {% endmatch %} {% endmacro %} {% macro to_rs_method_call(obj, meth) -%} -{% match meth.throws() -%} +{% match meth.throws_type() -%} {% when Some with (e) -%} uniffi::call_with_result(call_status, || { - let _retval = {{ obj.name() }}::{% call to_rs_call(meth) %}.map_err(<{{ e }} as uniffi::ViaFfi>::lower)?; + let _retval = {{ obj.name() }}::{% call to_rs_call(meth) %}.map_err({{ e|viaffi_impl }}::lower)?; Ok({% call ret(meth) %}) }) {% else %} @@ -73,7 +73,7 @@ uniffi::call_with_output(call_status, || { {% match meth.return_type() -%} {% when Some with (return_type) -%} let retval = {{ obj.name() }}::{% call to_rs_call(meth) %}; - {{"retval"|lower_rs(return_type)}} + {{ return_type|viaffi_impl }}::lower(retval) {% else -%} {{ obj.name() }}::{% call to_rs_call(meth) %} {% endmatch -%} @@ -82,18 +82,17 @@ uniffi::call_with_output(call_status, || { {% endmacro -%} {% macro to_rs_function_call(func) %} -{% match func.throws() %} +{% match func.throws_type() %} {% when Some with (e) %} uniffi::call_with_result(call_status, || { - let _retval = {% call to_rs_call(func) %}.map_err(<{{ e }} as uniffi::ViaFfi>::lower)?; + let _retval = {% call to_rs_call(func) %}.map_err({{ e|viaffi_impl }}::lower)?; Ok({% call ret(func) %}) }) {% else %} uniffi::call_with_output(call_status, || { {% match func.return_type() -%} {% when Some with (return_type) -%} - let retval = {% call to_rs_call(func) %}; - {{"retval"|lower_rs(return_type)}} + {{ return_type|viaffi_impl }}::lower({% call to_rs_call(func) %}) {% else -%} {% call to_rs_call(func) %} {% endmatch -%} diff --git a/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs b/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs index bf294fa1bf..f785e479e9 100644 --- a/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs +++ b/uniffi_bindgen/src/scaffolding/templates/scaffolding_template.rs @@ -11,8 +11,7 @@ uniffi::assert_compatible_version!("{{ uniffi_version }}"); // Please check that {% include "RustBuffer.rs" %} // Error definitions, corresponding to `error` in the UDL. -{% for error in ci.iter_error_definitions() %} -{% let e = error.wrapped_enum() %} +{% for e in ci.iter_error_definitions() %} {% include "ErrorTemplate.rs" %} {% endfor %} From 820d45664c6f4722ac8da4aea5c135942ca0634a Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Wed, 4 Aug 2021 16:45:17 -0400 Subject: [PATCH 36/45] Let's actually test that external types work (and they do!) --- Cargo.toml | 3 +++ fixtures/external-types/crate-one/Cargo.toml | 16 ++++++++++++ fixtures/external-types/crate-one/src/lib.rs | 3 +++ fixtures/external-types/crate-two/Cargo.toml | 17 +++++++++++++ fixtures/external-types/crate-two/src/lib.rs | 3 +++ fixtures/external-types/lib/Cargo.toml | 22 ++++++++++++++++ fixtures/external-types/lib/build.rs | 7 ++++++ .../lib/src/external-types-lib.udl | 16 ++++++++++++ fixtures/external-types/lib/src/lib.rs | 18 +++++++++++++ .../tests/bindings/test_external_types.kts | 20 +++++++++++++++ .../lib/tests/bindings/test_external_types.py | 23 +++++++++++++++++ .../lib/tests/bindings/test_external_types.rb | 25 +++++++++++++++++++ .../tests/bindings/test_external_types.swift | 25 +++++++++++++++++++ .../lib/tests/test_generated_bindings.rs | 9 +++++++ 14 files changed, 207 insertions(+) create mode 100644 fixtures/external-types/crate-one/Cargo.toml create mode 100644 fixtures/external-types/crate-one/src/lib.rs create mode 100644 fixtures/external-types/crate-two/Cargo.toml create mode 100644 fixtures/external-types/crate-two/src/lib.rs create mode 100644 fixtures/external-types/lib/Cargo.toml create mode 100644 fixtures/external-types/lib/build.rs create mode 100644 fixtures/external-types/lib/src/external-types-lib.udl create mode 100644 fixtures/external-types/lib/src/lib.rs create mode 100644 fixtures/external-types/lib/tests/bindings/test_external_types.kts create mode 100644 fixtures/external-types/lib/tests/bindings/test_external_types.py create mode 100644 fixtures/external-types/lib/tests/bindings/test_external_types.rb create mode 100644 fixtures/external-types/lib/tests/bindings/test_external_types.swift create mode 100644 fixtures/external-types/lib/tests/test_generated_bindings.rs diff --git a/Cargo.toml b/Cargo.toml index 04b848f0e8..1fa71f9044 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,9 @@ members = [ "examples/todolist", "fixtures/coverall", "fixtures/callbacks", + "fixtures/external-types/crate-one", + "fixtures/external-types/crate-two", + "fixtures/external-types/lib", "fixtures/regressions/enum-without-i32-helpers", "fixtures/regressions/kotlin-experimental-unsigned-types", "fixtures/regressions/cdylib-crate-type-dependency/ffi-crate", diff --git a/fixtures/external-types/crate-one/Cargo.toml b/fixtures/external-types/crate-one/Cargo.toml new file mode 100644 index 0000000000..11d4716366 --- /dev/null +++ b/fixtures/external-types/crate-one/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "crate_one" +edition = "2018" +version = "0.11.0" +authors = ["Firefox Sync Team "] +license = "MPL-2.0" +publish = false + +[dependencies] +anyhow = "1" +bytes = "1.0" +uniffi_macros = {path = "../../../uniffi_macros"} +uniffi = {path = "../../../uniffi", features=["builtin-bindgen"]} + +[build-dependencies] +uniffi_build = {path = "../../../uniffi_build", features=["builtin-bindgen"]} \ No newline at end of file diff --git a/fixtures/external-types/crate-one/src/lib.rs b/fixtures/external-types/crate-one/src/lib.rs new file mode 100644 index 0000000000..28631274a8 --- /dev/null +++ b/fixtures/external-types/crate-one/src/lib.rs @@ -0,0 +1,3 @@ +pub struct CrateOneType { + pub sval: String, +} diff --git a/fixtures/external-types/crate-two/Cargo.toml b/fixtures/external-types/crate-two/Cargo.toml new file mode 100644 index 0000000000..86b58f4a69 --- /dev/null +++ b/fixtures/external-types/crate-two/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "crate_two" +edition = "2018" +version = "0.11.0" +authors = ["Firefox Sync Team "] +license = "MPL-2.0" +publish = false + +[dependencies] +anyhow = "1" +bytes = "1.0" +uniffi_macros = {path = "../../../uniffi_macros"} +uniffi = {path = "../../../uniffi", features=["builtin-bindgen"]} +crate_one = {path = "../crate-one"} + +[build-dependencies] +uniffi_build = {path = "../../../uniffi_build", features=["builtin-bindgen"]} \ No newline at end of file diff --git a/fixtures/external-types/crate-two/src/lib.rs b/fixtures/external-types/crate-two/src/lib.rs new file mode 100644 index 0000000000..32fff6c304 --- /dev/null +++ b/fixtures/external-types/crate-two/src/lib.rs @@ -0,0 +1,3 @@ +pub struct CrateTwoType { + pub ival: i32, +} diff --git a/fixtures/external-types/lib/Cargo.toml b/fixtures/external-types/lib/Cargo.toml new file mode 100644 index 0000000000..647aab67a4 --- /dev/null +++ b/fixtures/external-types/lib/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "external_types_lib" +edition = "2018" +version = "0.11.0" +authors = ["Firefox Sync Team "] +license = "MPL-2.0" +publish = false + +[lib] +crate-type = ["staticlib", "cdylib", "lib"] +name = "uniffi_external_types_lib" + +[dependencies] +anyhow = "1" +bytes = "1.0" +uniffi_macros = {path = "../../../uniffi_macros"} +uniffi = {path = "../../../uniffi", features=["builtin-bindgen"]} +crate_one = {path = "../crate-one"} +crate_two = {path = "../crate-two"} + +[build-dependencies] +uniffi_build = {path = "../../../uniffi_build", features=["builtin-bindgen"]} diff --git a/fixtures/external-types/lib/build.rs b/fixtures/external-types/lib/build.rs new file mode 100644 index 0000000000..ec3ce613ae --- /dev/null +++ b/fixtures/external-types/lib/build.rs @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +fn main() { + uniffi_build::generate_scaffolding("./src/external-types-lib.udl").unwrap(); +} diff --git a/fixtures/external-types/lib/src/external-types-lib.udl b/fixtures/external-types/lib/src/external-types-lib.udl new file mode 100644 index 0000000000..c906d71a76 --- /dev/null +++ b/fixtures/external-types/lib/src/external-types-lib.udl @@ -0,0 +1,16 @@ +namespace external_types_lib { + CombinedType get_combined_type(optional CombinedType? cval); +}; + +dictionary CrateOneType { + string sval; +}; + +dictionary CrateTwoType { + i32 ival; +}; + +dictionary CombinedType { + CrateOneType cot; + CrateTwoType ctt; +}; diff --git a/fixtures/external-types/lib/src/lib.rs b/fixtures/external-types/lib/src/lib.rs new file mode 100644 index 0000000000..b7d99f9a61 --- /dev/null +++ b/fixtures/external-types/lib/src/lib.rs @@ -0,0 +1,18 @@ +use crate_one::CrateOneType; +use crate_two::CrateTwoType; + +pub struct CombinedType { + pub cot: CrateOneType, + pub ctt: CrateTwoType, +} + +fn get_combined_type(existing: Option) -> CombinedType { + existing.unwrap_or_else(|| CombinedType { + cot: CrateOneType { + sval: "hello".to_string(), + }, + ctt: CrateTwoType { ival: 1 }, + }) +} + +include!(concat!(env!("OUT_DIR"), "/external-types-lib.uniffi.rs")); diff --git a/fixtures/external-types/lib/tests/bindings/test_external_types.kts b/fixtures/external-types/lib/tests/bindings/test_external_types.kts new file mode 100644 index 0000000000..8c4e1226a1 --- /dev/null +++ b/fixtures/external-types/lib/tests/bindings/test_external_types.kts @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import java.util.concurrent.* + +import uniffi.external_types_lib.* + +// TODO: use an actual test runner. + +val ct = getCombinedType(CombinedType( + CrateOneType("test"), + CrateTwoType(42), +)); +assert(ct.cot.sval == "test"); +assert(ct.ctt.ival == 42); + +val ct2 = getCombinedType(null); +assert(ct2.cot.sval == "hello"); +assert(ct2.ctt.ival == 1); diff --git a/fixtures/external-types/lib/tests/bindings/test_external_types.py b/fixtures/external-types/lib/tests/bindings/test_external_types.py new file mode 100644 index 0000000000..6b28cabbd1 --- /dev/null +++ b/fixtures/external-types/lib/tests/bindings/test_external_types.py @@ -0,0 +1,23 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import unittest +from external_types_lib import * + +class TestExternalTypes(unittest.TestCase): + def test_round_trip(self): + ct = get_combined_type(CombinedType( + CrateOneType("test"), + CrateTwoType(42), + )) + self.assertEqual(ct.cot.sval, "test") + self.assertEqual(ct.ctt.ival, 42) + + def test_none_value(self): + ct = get_combined_type(None) + self.assertEqual(ct.cot.sval, "hello") + self.assertEqual(ct.ctt.ival, 1) + +if __name__=='__main__': + unittest.main() diff --git a/fixtures/external-types/lib/tests/bindings/test_external_types.rb b/fixtures/external-types/lib/tests/bindings/test_external_types.rb new file mode 100644 index 0000000000..ab33c688bf --- /dev/null +++ b/fixtures/external-types/lib/tests/bindings/test_external_types.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# 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/. */ + +require 'test/unit' +require 'external_types_lib' + +class TestExternalTypes < Test::Unit::TestCase + def test_round_trip + ct = ExternalTypesLib.get_combined_type(ExternalTypesLib::CombinedType.new( + ExternalTypesLib::CrateOneType.new("test"), + ExternalTypesLib::CrateTwoType.new(42), + )) + assert_equal(ct.cot.sval, "test") + assert_equal(ct.ctt.ival, 42) + end + + def test_none_value + ct = ExternalTypesLib.get_combined_type(nil) + assert_equal(ct.cot.sval, "hello") + assert_equal(ct.ctt.ival, 1) + end +end diff --git a/fixtures/external-types/lib/tests/bindings/test_external_types.swift b/fixtures/external-types/lib/tests/bindings/test_external_types.swift new file mode 100644 index 0000000000..210e09033c --- /dev/null +++ b/fixtures/external-types/lib/tests/bindings/test_external_types.swift @@ -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/. */ + +import external_types_lib + +// TODO: use an actual test runner. + + +// Test a round trip +do { + let ct = getCombinedType(cval: CombinedType( + cot: CrateOneType(sval: "test"), + ctt: CrateTwoType(ival: 42) + )) + assert(ct.cot.sval == "test") + assert(ct.ctt.ival == 42) +} + +// Test passing in null value +do { + let ct = getCombinedType(cval: nil) + assert(ct.cot.sval == "hello") + assert(ct.ctt.ival == 1) +} diff --git a/fixtures/external-types/lib/tests/test_generated_bindings.rs b/fixtures/external-types/lib/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..3aff369c0a --- /dev/null +++ b/fixtures/external-types/lib/tests/test_generated_bindings.rs @@ -0,0 +1,9 @@ +uniffi_macros::build_foreign_language_testcases!( + "src/external-types-lib.udl", + [ + "tests/bindings/test_external_types.py", + "tests/bindings/test_external_types.kts", + "tests/bindings/test_external_types.rb", + "tests/bindings/test_external_types.swift", + ] +); From 136db854ce0ebe2db404b955f62de87f8fd7d6e7 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Fri, 6 Aug 2021 08:57:36 -0400 Subject: [PATCH 37/45] Rename `ViaFfi` -> `FfiConverter` This was Ryan's suggestion and I think it's a good one. The two traits accomplish the same task, but they're fundamentally different so we should use a different name. - Didn't change the name on swift, since its code still follows the ViaFfi model. - Removed a line from the upcoming changelog. It's kind of weird to have it reference a name that's been change. I also realize that it's not possible for consumer code to use a custom ViaFfi/FfiConverter yet, so that wasn't really a visible change to them. --- CHANGELOG.md | 4 -- docs/contributing.md | 2 +- docs/manual/src/internals/crates.md | 2 +- .../manual/src/internals/object_references.md | 10 +-- uniffi/src/ffi/rustcalls.rs | 16 ++--- uniffi/src/lib.rs | 72 ++++++++++--------- uniffi_bindgen/src/scaffolding/mod.rs | 26 +++---- .../templates/CallbackInterfaceTemplate.rs | 10 +-- .../src/scaffolding/templates/EnumTemplate.rs | 8 +-- .../scaffolding/templates/ErrorTemplate.rs | 10 +-- .../scaffolding/templates/RecordTemplate.rs | 8 +-- .../src/scaffolding/templates/macros.rs | 18 ++--- 12 files changed, 92 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec957e9317..3f6590f523 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,10 +25,6 @@ which imports and uses the low-level C FFI. The name can be customized using the `module_name` config option. - Python timestamps will now be in UTC and timezone-aware rather than naive. -- Replaced `lower_into_buffer()` and `try_lift_from_buffer()` with the - `RustBufferViaFfi` trait. If you use those functions in your custom ViaFfi - implementation then you'll need to update the code. Check out the `Option<>` - implementation in uniffi/src/lib.rs for an example. - Kotlin exceptions names will now replace a trailing "Error" with "Exception" rather than appending the string (FooException instead of FooErrorException) - JNA 5.7 or greater is required for Kotlin consumers diff --git a/docs/contributing.md b/docs/contributing.md index 5eae5b0eaa..b89721538f 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -79,7 +79,7 @@ Other directories of interest include: *foreign-language bindings*, the code that can load the FFI layer exposed by the scaffolding and expose it as a higher-level API in a target language. There is a sub-module for each supported language. - **[`./uniffi`](../uniffi):** This is a run-time support crate that is used by the generated Rust scaffolding. It - controls how values of various types are passed back-and-forth over the FFI layer, by means of the `ViaFfi` trait. + controls how values of various types are passed back-and-forth over the FFI layer, by means of the `FfiConverter` trait. - **[`./uniffi_build`](../uniffi_build):** This is a small hook to run `uniffi-bindgen` from the `build.rs` script of a UniFFI component, in order to automatically generate the Rust scaffolding as part of its build process. - **[`./uniffi_macros`](../uniffi_macros):** This contains some helper macros that UniFFI components can use to diff --git a/docs/manual/src/internals/crates.md b/docs/manual/src/internals/crates.md index f183dda7d7..fd12c33250 100644 --- a/docs/manual/src/internals/crates.md +++ b/docs/manual/src/internals/crates.md @@ -16,7 +16,7 @@ The code for UniFFI is organized into the following crates: higher-level API in a target language. There is a sub-module for each supported language. - **[`./uniffi`](./api/uniffi/index.html):** This is a run-time support crate that is used by the generated Rust scaffolding. It controls how values of various types are passed back-and-forth over the FFI layer, by means of the - [`ViaFfi`](./api/uniffi/trait.ViaFfi.html) trait. + [`FfiConverter`](./api/uniffi/trait.FfiConverter.html) trait. - **[`./uniffi_build`](./api/uniffi_build/index.html):** This is a small hook to run `uniffi-bindgen` from the `build.rs` script of a UniFFI component, in order to automatically generate the Rust scaffolding as part of its build process. - **[`./uniffi_macros`](./api/uniffi_macros/index.html):** This contains some helper macros that UniFFI components can use to diff --git a/docs/manual/src/internals/object_references.md b/docs/manual/src/internals/object_references.md index 6ceeff4379..c4e479649e 100644 --- a/docs/manual/src/internals/object_references.md +++ b/docs/manual/src/internals/object_references.md @@ -67,7 +67,7 @@ pub extern "C" fn todolist_12ba_TodoList_new( uniffi::deps::ffi_support::call_with_output(err, || { let _new = TodoList::new(); let _arc = std::sync::Arc::new(_new); - as uniffi::ViaFfi>::lower(_arc) + as uniffi::FfiConverter>::lower(_arc) }) } ``` @@ -75,7 +75,7 @@ pub extern "C" fn todolist_12ba_TodoList_new( The UniFFI runtime implements lowering for object instances using `Arc::into_raw`: ```rust -unsafe impl ViaFfi for std::sync::Arc { +unsafe impl FfiConverter for std::sync::Arc { type FfiType = *const std::os::raw::c_void; fn lower(self) -> Self::FfiType { std::sync::Arc::into_raw(self) as Self::FfiType @@ -101,8 +101,8 @@ pub extern "C" fn todolist_12ba_TodoList_add_item( ) -> () { uniffi::deps::ffi_support::call_with_result(err, || -> Result<_, TodoError> { let _retval = TodoList::add_item( - & as uniffi::ViaFfi>::try_lift(ptr).unwrap(), - ::try_lift(todo).unwrap())?, + & as uniffi::FfiConverter>::try_lift(ptr).unwrap(), + ::try_lift(todo).unwrap())?, ) Ok(_retval) }) @@ -112,7 +112,7 @@ pub extern "C" fn todolist_12ba_TodoList_add_item( The UniFFI runtime implements lifting for object instances using `Arc::from_raw`: ```rust -unsafe impl ViaFfi for std::sync::Arc { +unsafe impl FfiConverter for std::sync::Arc { type FfiType = *const std::os::raw::c_void; fn try_lift(v: Self::FfiType) -> Result { let v = v as *const T; diff --git a/uniffi/src/ffi/rustcalls.rs b/uniffi/src/ffi/rustcalls.rs index 3bee138cb3..9d2921768f 100644 --- a/uniffi/src/ffi/rustcalls.rs +++ b/uniffi/src/ffi/rustcalls.rs @@ -10,7 +10,7 @@ //! - Catching panics //! - Adapting `Result<>` types into either a return value or an error -use crate::{RustBuffer, RustBufferViaFfi, ViaFfi}; +use crate::{RustBuffer, RustBufferFfiConverter, FfiConverter}; use anyhow::Result; use ffi_support::IntoFfi; use std::mem::MaybeUninit; @@ -74,7 +74,7 @@ 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: RustBufferViaFfi {} +pub trait FfiError: RustBufferFfiConverter {} // Generalized rust call handling function fn make_call(out_status: &mut RustCallStatus, callback: F) -> R::Value @@ -136,7 +136,7 @@ where /// 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 ViaFfi::lower()). +/// 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: @@ -155,7 +155,7 @@ where /// 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 ViaFfi::lower()) +/// - (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`: @@ -177,7 +177,7 @@ where #[cfg(test)] mod test { use super::*; - use crate::{RustBufferViaFfi, ViaFfi}; + use crate::{RustBufferFfiConverter, FfiConverter}; fn function(a: u8) -> i8 { match a { @@ -213,12 +213,12 @@ mod test { #[derive(Debug, PartialEq)] struct TestError(String); - // Use RustBufferViaFfi to simplify lifting TestError out of RustBuffer to check it - impl RustBufferViaFfi for TestError { + // 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) { - ::write(obj.0, buf); + ::write(obj.0, buf); } fn try_read(buf: &mut &[u8]) -> Result { diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index 24938091c7..1d2df37e87 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -8,23 +8,24 @@ //! 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 [`ViaFfi`] trait, we create an implementation for each type that -//! can be passed across the FFI. It's responsible for: +//! 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](ViaFfi::FfiType) values of that type in the low-level C-style type +//! * 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"](ViaFfi::lower) rust values of that type into an appropriate low-level +//! * How to ["lower"](FfiConverter::lower) values of the rust type into an appropriate low-level //! FFI value. -//! * How to ["lift"](ViaFfi::try_lift) low-level FFI values back into rust values of that type. -//! * How to [write](ViaFfi::write) rust values of that type into a buffer, for cases +//! * 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](ViaFfi::try_read) rust values of that type from buffer, for cases +//! * 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` ViaFfi` trait, we provide a handful of struct definitions useful +//! 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`]. use anyhow::{bail, Result}; @@ -124,7 +125,7 @@ macro_rules! assert_compatible_version { /// Trait defining how to transfer values via the FFI layer. /// -/// The `ViaFfi` trait defines how to pass values of a particular type back-and-forth over +/// 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. /// @@ -142,12 +143,13 @@ macro_rules! assert_compatible_version { /// 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 ViaFfi: Sized { +pub unsafe trait FfiConverter: Sized { /// The type used in rust code. /// - /// For primitive / standard types, we implement ViaFfi on the type itself and RustType=Self. + /// For primitive / standard types, we implement FfiConverter on the type itself and RustType=Self. /// For user-defined types we create a unit struct and implement it there. This sidesteps - /// Rust's orphan rules with types from external crates, since we're not implementing ViaFfi on + /// Rust's orphan rules with types from external crates, since we're not implementing + /// FfiConverter on /// the external type directly. type RustType; @@ -223,7 +225,7 @@ pub fn check_remaining(buf: &[u8], num_bytes: usize) -> Result<()> { Ok(()) } -/// Blanket implementation of ViaFfi for numeric primitives. +/// 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! @@ -232,7 +234,7 @@ macro_rules! impl_via_ffi_for_num_primitive { ($($T:ty),*) => { $( paste! { - unsafe impl ViaFfi for $T { + unsafe impl FfiConverter for $T { type RustType = Self; type FfiType = Self; @@ -266,7 +268,7 @@ impl_via_ffi_for_num_primitive! { /// /// Booleans are passed as an `i8` in order to avoid problems with handling /// C-compatible boolean values on JVM-based languages. -unsafe impl ViaFfi for bool { +unsafe impl FfiConverter for bool { type RustType = Self; type FfiType = i8; @@ -287,18 +289,18 @@ unsafe impl ViaFfi for bool { } fn write(obj: Self::RustType, buf: &mut Vec) { - buf.put_i8(::lower(obj)); + buf.put_i8(::lower(obj)); } fn try_read(buf: &mut &[u8]) -> Result { check_remaining(buf, 1)?; - ::try_lift(buf.get_i8()) + ::try_lift(buf.get_i8()) } } /// Support for passing Strings via the FFI. /// -/// Unlike many other implementations of `ViaFfi`, this passes a struct containing +/// 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 @@ -307,7 +309,7 @@ unsafe impl ViaFfi for bool { /// 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 ViaFfi for String { +unsafe impl FfiConverter for String { type RustType = Self; type FfiType = RustBuffer; @@ -357,19 +359,19 @@ unsafe impl ViaFfi for String { /// 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 RustBufferViaFfi: Sized { +pub trait RustBufferFfiConverter: Sized { type RustType; fn write(obj: Self::RustType, buf: &mut Vec); fn try_read(buf: &mut &[u8]) -> Result; } -unsafe impl ViaFfi for T { +unsafe impl FfiConverter for T { type RustType = T::RustType; type FfiType = RustBuffer; fn lower(obj: Self::RustType) -> RustBuffer { let mut buf = Vec::new(); - ::write(obj, &mut buf); + ::write(obj, &mut buf); RustBuffer::from_vec(buf) } @@ -406,7 +408,7 @@ unsafe impl ViaFfi for T { /// 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 RustBufferViaFfi for SystemTime { +impl RustBufferFfiConverter for SystemTime { type RustType = Self; fn write(obj: Self::RustType, buf: &mut Vec) { @@ -448,7 +450,7 @@ impl RustBufferViaFfi for SystemTime { /// 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 RustBufferViaFfi for Duration { +impl RustBufferFfiConverter for Duration { type RustType = Self; fn write(obj: Self::RustType, buf: &mut Vec) { @@ -471,7 +473,7 @@ impl RustBufferViaFfi for Duration { /// 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 RustBufferViaFfi for Option { +impl RustBufferFfiConverter for Option { type RustType = Option; fn write(obj: Self::RustType, buf: &mut Vec) { @@ -479,7 +481,7 @@ impl RustBufferViaFfi for Option { None => buf.put_i8(0), Some(v) => { buf.put_i8(1); - ::write(v, buf); + ::write(v, buf); } } } @@ -488,7 +490,7 @@ impl RustBufferViaFfi for Option { check_remaining(buf, 1)?; Ok(match buf.get_i8() { 0 => None, - 1 => Some(::try_read(buf)?), + 1 => Some(::try_read(buf)?), _ => bail!("unexpected tag byte for Option"), }) } @@ -503,7 +505,7 @@ impl RustBufferViaFfi for Option { /// Ideally we would pass `Vec` 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 RustBufferViaFfi for Vec { +impl RustBufferFfiConverter for Vec { type RustType = Vec; fn write(obj: Self::RustType, buf: &mut Vec) { @@ -511,7 +513,7 @@ impl RustBufferViaFfi for Vec { let len = i32::try_from(obj.len()).unwrap(); buf.put_i32(len); // We limit arrays to i32::MAX items for item in obj.into_iter() { - ::write(item, buf); + ::write(item, buf); } } @@ -520,7 +522,7 @@ impl RustBufferViaFfi for Vec { let len = usize::try_from(buf.get_i32())?; let mut vec = Vec::with_capacity(len); for _ in 0..len { - vec.push(::try_read(buf)?) + vec.push(::try_read(buf)?) } Ok(vec) } @@ -534,7 +536,7 @@ impl RustBufferViaFfi for Vec { /// 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 RustBufferViaFfi for HashMap { +impl RustBufferFfiConverter for HashMap { type RustType = HashMap; fn write(obj: Self::RustType, buf: &mut Vec) { @@ -542,8 +544,8 @@ impl RustBufferViaFfi for HashMap { let len = i32::try_from(obj.len()).unwrap(); buf.put_i32(len); // We limit HashMaps to i32::MAX entries for (key, value) in obj.into_iter() { - ::write(key, buf); - ::write(value, buf); + ::write(key, buf); + ::write(value, buf); } } @@ -553,7 +555,7 @@ impl RustBufferViaFfi for HashMap { let mut map = HashMap::with_capacity(len); for _ in 0..len { let key = String::try_read(buf)?; - let value = ::try_read(buf)?; + let value = ::try_read(buf)?; map.insert(key, value); } Ok(map) @@ -565,7 +567,7 @@ impl RustBufferViaFfi for HashMap { /// 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 ViaFfi for std::sync::Arc { +unsafe impl FfiConverter for std::sync::Arc { type RustType = Self; // Don't use a pointer to as that requires a `pub ` type FfiType = *const std::os::raw::c_void; diff --git a/uniffi_bindgen/src/scaffolding/mod.rs b/uniffi_bindgen/src/scaffolding/mod.rs index a041e54d92..8667c7c096 100644 --- a/uniffi_bindgen/src/scaffolding/mod.rs +++ b/uniffi_bindgen/src/scaffolding/mod.rs @@ -68,11 +68,11 @@ mod filters { }) } - /// Get the name of the ViaFfi implementation for this type + /// 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 - pub fn viaffi_impl_name(type_: &Type) -> askama::Result { + pub fn ffi_converter_impl_name(type_: &Type) -> askama::Result { Ok(match type_ { // Timestamp/Duraration are handled by standard types Type::Timestamp => "std::time::SystemTime".into(), @@ -80,21 +80,21 @@ mod filters { // Object is handled by Arc Type::Object(name) => format!("std::sync::Arc<{}>", name), // Other user-defined types are handled by a unit-struct that we generate. The - // ViaFfi implementation for this can be found in one of the scaffolding template code. + // 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 since we can't directly - // implement ViaFfi on a type from an external crate. + // implement FfiConverter on a type from an external crate. // // 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!("ViaFfi{}", type_.canonical_name()) + format!("FfiConverter{}", type_.canonical_name()) } - // Wrapper types are implemented by generics that wrap the viaffi implementation of the + // Wrapper types are implemented by generics that wrap the FfiConverter implementation of the // inner type. - Type::Optional(inner) => format!("Option<{}>", viaffi_impl_name(inner)?), - Type::Sequence(inner) => format!("Vec<{}>", viaffi_impl_name(inner)?), - Type::Map(inner) => format!("HashMap", viaffi_impl_name(inner)?), + Type::Optional(inner) => format!("Option<{}>", ffi_converter_impl_name(inner)?), + Type::Sequence(inner) => format!("Vec<{}>", ffi_converter_impl_name(inner)?), + Type::Map(inner) => format!("HashMap", ffi_converter_impl_name(inner)?), // Primitive types / strings are implemented by their rust type Type::Int8 => "i8".into(), Type::UInt8 => "u8".into(), @@ -111,10 +111,10 @@ mod filters { }) } - // Map a type to Rust code that specifies the ViaFfi implementation. + // Map a type to Rust code that specifies the FfiConverter implementation. // - // This outputs something like `` - pub fn viaffi_impl(type_: &Type) -> Result { - Ok(format!("<{} as uniffi::ViaFfi>", viaffi_impl_name(type_)?)) + // This outputs something like `` + pub fn ffi_converter_impl(type_: &Type) -> Result { + Ok(format!("<{} as uniffi::FfiConverter>", ffi_converter_impl_name(type_)?)) } } diff --git a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs index a837abe0b7..f07ec1667b 100644 --- a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs @@ -11,7 +11,7 @@ // - 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_()|viaffi_impl_name -%} +{% let trait_impl = cbi.type_()|ffi_converter_impl_name -%} {% let foreign_callback_internals = format!("foreign_callback_{}_internals", trait_name)|upper -%} // Register a foreign callback for getting across the FFI. @@ -60,7 +60,7 @@ impl {{ trait_name }} for {{ trait_impl }} { let mut args_buf = Vec::new(); {% endif -%} {%- for arg in meth.arguments() %} - {{ arg.type_()|viaffi_impl }}::write({{ arg.name() }}, &mut args_buf); + {{ arg.type_()|ffi_converter_impl }}::write({{ arg.name() }}, &mut args_buf); {%- endfor -%} let args_rbuf = uniffi::RustBuffer::from_vec(args_buf); @@ -73,7 +73,7 @@ impl {{ trait_name }} for {{ trait_impl }} { {% when Some with (return_type) -%} let vec = ret_rbuf.destroy_into_vec(); let mut ret_buf = vec.as_slice(); - {{ return_type|viaffi_impl }}::try_read(&mut ret_buf).unwrap() + {{ return_type|ffi_converter_impl }}::try_read(&mut ret_buf).unwrap() {%- else -%} uniffi::RustBuffer::destroy(ret_rbuf); {%- endmatch %} @@ -81,7 +81,7 @@ impl {{ trait_name }} for {{ trait_impl }} { {%- endfor %} } -unsafe impl uniffi::ViaFfi for {{ trait_impl }} { +unsafe impl uniffi::FfiConverter for {{ trait_impl }} { // This RustType allows for rust code that inputs this type as a Box param type RustType = Box; type FfiType = u64; @@ -110,6 +110,6 @@ unsafe impl uniffi::ViaFfi for {{ trait_impl }} { fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result { use uniffi::deps::bytes::Buf; uniffi::check_remaining(buf, 8)?; - ::try_lift(buf.get_u64()) + ::try_lift(buf.get_u64()) } } diff --git a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs index b23f91d6b3..15b4c1ef16 100644 --- a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs @@ -6,10 +6,10 @@ // We define a unit-struct to implement the trait to sidestep the orphan rule. #} -struct {{ e.type_()|viaffi_impl_name }}; +struct {{ e.type_()|ffi_converter_impl_name }}; #[doc(hidden)] -impl uniffi::RustBufferViaFfi for {{ e.type_()|viaffi_impl_name }} { +impl uniffi::RustBufferFfiConverter for {{ e.type_()|ffi_converter_impl_name }} { type RustType = {{ e.name() }}; fn write(obj: Self::RustType, buf: &mut Vec) { @@ -19,7 +19,7 @@ impl uniffi::RustBufferViaFfi for {{ e.type_()|viaffi_impl_name }} { {{ e.name() }}::{{ variant.name() }} { {% for field in variant.fields() %}{{ field.name() }}, {%- endfor %} } => { buf.put_i32({{ loop.index }}); {% for field in variant.fields() -%} - {{ field.type_()|viaffi_impl }}::write({{ field.name() }}, buf); + {{ field.type_()|ffi_converter_impl }}::write({{ field.name() }}, buf); {%- endfor %} }, {%- endfor %} @@ -33,7 +33,7 @@ impl uniffi::RustBufferViaFfi for {{ e.type_()|viaffi_impl_name }} { {%- for variant in e.variants() %} {{ loop.index }} => {{ e.name() }}::{{ variant.name() }}{% if variant.has_fields() %} { {% for field in variant.fields() %} - {{ field.name() }}: {{ field.type_()|viaffi_impl }}::try_read(buf)?, + {{ field.name() }}: {{ field.type_()|ffi_converter_impl }}::try_read(buf)?, {%- endfor %} }{% endif %}, {%- endfor %} diff --git a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs index 23ac6c1256..895fe4619e 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs @@ -6,10 +6,10 @@ // We define a unit-struct to implement the trait to sidestep the orphan rule. #} -struct {{ e.type_()|viaffi_impl_name }}; +struct {{ e.type_()|ffi_converter_impl_name }}; #[doc(hidden)] -impl uniffi::RustBufferViaFfi for {{ e.type_()|viaffi_impl_name }} { +impl uniffi::RustBufferFfiConverter for {{ e.type_()|ffi_converter_impl_name }} { type RustType = {{ e.name() }}; fn write(obj: {{ e.name() }}, buf: &mut Vec) { @@ -19,7 +19,7 @@ impl uniffi::RustBufferViaFfi for {{ e.type_()|viaffi_impl_name }} { {{ e.name() }}::{{ variant.name() }}{% if variant.has_fields() %} { {% for field in variant.fields() %}{{ field.name() }}, {%- endfor %} }{% else %}{..}{% endif %} => { buf.put_i32({{ loop.index }}); {% for field in variant.fields() -%} - {{ field.type_()|viaffi_impl }}::write({{ field.name() }}, buf); + {{ field.type_()|ffi_converter_impl }}::write({{ field.name() }}, buf); {%- endfor %} }, {%- endfor %} @@ -42,7 +42,7 @@ impl uniffi::RustBufferViaFfi for {{ e.type_()|viaffi_impl_name }} { {%- for variant in e.variants() %} {{ loop.index }} => {{ e.name() }}::{{ variant.name() }}{% if variant.has_fields() %} { {% for field in variant.fields() %} - {{ field.name() }}: {{ field.type_()|viaffi_impl }}::try_read(buf)?, + {{ field.name() }}: {{ field.type_()|ffi_converter_impl }}::try_read(buf)?, {%- endfor %} }{% endif %}, {%- endfor %} @@ -52,4 +52,4 @@ impl uniffi::RustBufferViaFfi for {{ e.type_()|viaffi_impl_name }} { {% endif %} } -impl uniffi::FfiError for {{ e.type_()|viaffi_impl_name }} { } +impl uniffi::FfiError for {{ e.type_()|ffi_converter_impl_name }} { } diff --git a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs index 8db08b4ad0..1de5cf136c 100644 --- a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs @@ -6,24 +6,24 @@ // // We define a unit-struct to implement the trait to sidestep the orphan rule. #} -struct {{ rec.type_()|viaffi_impl_name }}; +struct {{ rec.type_()|ffi_converter_impl_name }}; #[doc(hidden)] -impl uniffi::RustBufferViaFfi for {{ rec.type_()|viaffi_impl_name }} { +impl uniffi::RustBufferFfiConverter for {{ rec.type_()|ffi_converter_impl_name }} { type RustType = {{ rec.name() }}; fn write(obj: {{ rec.name() }}, buf: &mut Vec) { // 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_()|viaffi_impl }}::write(obj.{{ field.name() }}, buf); + {{ field.type_()|ffi_converter_impl }}::write(obj.{{ field.name() }}, buf); {%- endfor %} } fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result<{{ rec.name() }}> { Ok({{ rec.name() }} { {%- for field in rec.fields() %} - {{ field.name() }}: {{ field.type_()|viaffi_impl }}::try_read(buf)?, + {{ field.name() }}: {{ field.type_()|ffi_converter_impl }}::try_read(buf)?, {%- endfor %} }) } diff --git a/uniffi_bindgen/src/scaffolding/templates/macros.rs b/uniffi_bindgen/src/scaffolding/templates/macros.rs index f161f1c51b..a63582aeed 100644 --- a/uniffi_bindgen/src/scaffolding/templates/macros.rs +++ b/uniffi_bindgen/src/scaffolding/templates/macros.rs @@ -9,7 +9,7 @@ {%- macro _arg_list_rs_call(func) %} {%- for arg in func.full_arguments() %} {%- if arg.by_ref() %}&{% endif %} - {{- arg.type_()|viaffi_impl }}::try_lift({{ arg.name() }}).unwrap() + {{- arg.type_()|ffi_converter_impl }}::try_lift({{ arg.name() }}).unwrap() {%- if !loop.last %}, {% endif %} {%- endfor %} {%- endmacro -%} @@ -38,7 +38,7 @@ {% 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|viaffi_impl }}::lower(_retval){% else %}_retval{% endmatch %}{% endmacro %} +{% macro ret(func) %}{% match func.return_type() %}{% when Some with (return_type) %}{{ return_type|ffi_converter_impl }}::lower(_retval){% else %}_retval{% endmatch %}{% endmacro %} {% macro construct(obj, cons) %} {{- obj.name() }}::{% call to_rs_call(cons) -%} @@ -48,15 +48,15 @@ {% match cons.throws_type() %} {% when Some with (e) %} uniffi::call_with_result(call_status, || { - let _new = {% call construct(obj, cons) %}.map_err({{ e|viaffi_impl }}::lower)?; + let _new = {% call construct(obj, cons) %}.map_err({{ e|ffi_converter_impl }}::lower)?; let _arc = std::sync::Arc::new(_new); - Ok({{ obj.type_()|viaffi_impl }}::lower(_arc)) + Ok({{ obj.type_()|ffi_converter_impl }}::lower(_arc)) }) {% else %} uniffi::call_with_output(call_status, || { let _new = {% call construct(obj, cons) %}; let _arc = std::sync::Arc::new(_new); - {{ obj.type_()|viaffi_impl }}::lower(_arc) + {{ obj.type_()|ffi_converter_impl }}::lower(_arc) }) {% endmatch %} {% endmacro %} @@ -65,7 +65,7 @@ {% match meth.throws_type() -%} {% when Some with (e) -%} uniffi::call_with_result(call_status, || { - let _retval = {{ obj.name() }}::{% call to_rs_call(meth) %}.map_err({{ e|viaffi_impl }}::lower)?; + let _retval = {{ obj.name() }}::{% call to_rs_call(meth) %}.map_err({{ e|ffi_converter_impl }}::lower)?; Ok({% call ret(meth) %}) }) {% else %} @@ -73,7 +73,7 @@ uniffi::call_with_output(call_status, || { {% match meth.return_type() -%} {% when Some with (return_type) -%} let retval = {{ obj.name() }}::{% call to_rs_call(meth) %}; - {{ return_type|viaffi_impl }}::lower(retval) + {{ return_type|ffi_converter_impl }}::lower(retval) {% else -%} {{ obj.name() }}::{% call to_rs_call(meth) %} {% endmatch -%} @@ -85,14 +85,14 @@ uniffi::call_with_output(call_status, || { {% match func.throws_type() %} {% when Some with (e) %} uniffi::call_with_result(call_status, || { - let _retval = {% call to_rs_call(func) %}.map_err({{ e|viaffi_impl }}::lower)?; + let _retval = {% call to_rs_call(func) %}.map_err({{ e|ffi_converter_impl }}::lower)?; Ok({% call ret(func) %}) }) {% else %} uniffi::call_with_output(call_status, || { {% match func.return_type() -%} {% when Some with (return_type) -%} - {{ return_type|viaffi_impl }}::lower({% call to_rs_call(func) %}) + {{ return_type|ffi_converter_impl }}::lower({% call to_rs_call(func) %}) {% else -%} {% call to_rs_call(func) %} {% endmatch -%} From 1d5a63da5fc8cd68a10c1ddba8c5500c04a9ce7a Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Fri, 6 Aug 2021 09:42:48 -0400 Subject: [PATCH 38/45] Added an ADR describing what's going on in this branch. --- docs/adr/0006-external-types.md | 82 +++++++++++++++++++ uniffi/src/lib.rs | 4 +- uniffi_bindgen/src/scaffolding/mod.rs | 3 +- .../src/scaffolding/templates/EnumTemplate.rs | 2 +- .../scaffolding/templates/ErrorTemplate.rs | 2 +- .../scaffolding/templates/RecordTemplate.rs | 2 +- 6 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 docs/adr/0006-external-types.md diff --git a/docs/adr/0006-external-types.md b/docs/adr/0006-external-types.md new file mode 100644 index 0000000000..d503a48263 --- /dev/null +++ b/docs/adr/0006-external-types.md @@ -0,0 +1,82 @@ +# Replace ViaFfi with FfiConverter to handle external types + +* Status: proposed +* Deciders: bendk, rfkelly, mhammond +* Consulted: jhugman, jan-erik, travis +* Date: 2021-08-06 + +Discussion : [PR 1001](https://github.com/mozilla/uniffi-rs/pull/1001). + +## Context and Problem Statement + +UniFFI currently cannot support types from external crates because of Rust's +orphan rule prevents implementing `ViaFfi`. The orphan rules specifies that +the trait needs to be in the `uniffi` crate or the external crate, but we are +generating the scaffolding code for the crate being uniffied. + +Because of the orphan rule, any solution to this must be implemented using a +new struct that we create in the scaffolding code. The question is how to use +that struct to lift/lower the external type that we actually want to use. + +## Decision Drivers + +* We want a solution that's general and easy to extend. The solution should + enable, or at least not conflict with, other features we're planning to add: + * Importing types from another UDL + * Wrapping primitive types with a Rust type (for example String with Guid) + * Extension types that wrap a primitive on both the Rust and foreign + language side (JSON) + +## Considered Options + +* **[Option 1] Extend the template code to wrap the type** + + * In the Record/Enum/Object/Error code, we would use the newtype pattern to + wrap the external type (`struct WrapperType(ExternalType)`) + + * In the filters functions we generate code to wrap lift/lower/read/write. + For example the lower_rs filter could output `External_type(x).lower()` to + lower `WrapperType`. + +* **[Option 2] Replace the `ViaFfi` trait with something more generic** + + * We define `FfiConverter` which works almost the same as `ViaFfi` except + instead of always converting between `Self` and `FfiType`, we define a + second associated type `RustType` and convert between `RustType` and + `FfiType`. + + * For each user-defined type (Record, Error, Object, and Enum), we create a + new unit struct and set `RustType` to the type. This handles external types + without issues since we're implementing `FfiConverter` on our on struct. + The orphan rule doesn't apply to associated types. + + +## Decision Outcome + +Chosen option: + +* **[Option 2] Replace the `ViaFfi` trait with something more generic** + +This decision is taken because: + +* This solution has a simpler implementation. The template code to generate the + above example is not trivial. It would need to work with any variable name, + wrapping a return value, and also with recursive types (for example Option + would need to work with wrapped structs). + +* We believe it will make it easier to implement the other wrapping-style + features mentioned above. One sign of this was the CallbackInterface code, + which converts it's type to `Box`. The + `FfiConverter` trait was able to implement this, removing the current + lift/lower/read/write template code and fixing a bug with `Option<>`. + +### Positive Consequences + +* The wrapper/adapter pattern should be easier to implement in the future. + +### Negative Consequences + +* It requires a shift in all of our mental models. `FfiConverter` seems like + it's basically the same as `ViaFfi`, but the small code change actually + fundamentally changes how things work. This motivated us to change the name + of the trait. diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index 1d2df37e87..b9c7f185ec 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -148,9 +148,7 @@ pub unsafe trait FfiConverter: Sized { /// /// For primitive / standard types, we implement FfiConverter on the type itself and RustType=Self. /// For user-defined types we create a unit struct and implement it there. This sidesteps - /// Rust's orphan rules with types from external crates, since we're not implementing - /// FfiConverter on - /// the external type directly. + /// Rust's orphan rules (ADR-0006). type RustType; /// The low-level type used for passing values of this type over the FFI. diff --git a/uniffi_bindgen/src/scaffolding/mod.rs b/uniffi_bindgen/src/scaffolding/mod.rs index 8667c7c096..ae112f60ae 100644 --- a/uniffi_bindgen/src/scaffolding/mod.rs +++ b/uniffi_bindgen/src/scaffolding/mod.rs @@ -82,8 +82,7 @@ mod filters { // 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 since we can't directly - // implement FfiConverter on a type from an external crate. + // 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. diff --git a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs index 15b4c1ef16..d0eb3e67b6 100644 --- a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs @@ -3,7 +3,7 @@ // 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 the orphan rule. +// We define a unit-struct to implement the trait to sidestep Rust's orphan rule (ADR-0006). #} struct {{ e.type_()|ffi_converter_impl_name }}; diff --git a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs index 895fe4619e..ce2f360c04 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs @@ -3,7 +3,7 @@ // 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 the orphan rule. +// We define a unit-struct to implement the trait to sidestep Rust's orphan rule (ADR-0006). #} struct {{ e.type_()|ffi_converter_impl_name }}; diff --git a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs index 1de5cf136c..27c5f1ae4f 100644 --- a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs @@ -4,7 +4,7 @@ // 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 the orphan rule. +// We define a unit-struct to implement the trait to sidestep Rust's orphan rule (ADR-0006) #} struct {{ rec.type_()|ffi_converter_impl_name }}; From 83c62ed7850f0f1e3b40f81ea60261b0ac21a264 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Fri, 6 Aug 2021 10:01:06 -0400 Subject: [PATCH 39/45] Cargo fmt --- fixtures/callbacks/src/lib.rs | 7 ++++++- uniffi/src/ffi/rustcalls.rs | 4 ++-- uniffi_bindgen/src/scaffolding/mod.rs | 5 ++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/fixtures/callbacks/src/lib.rs b/fixtures/callbacks/src/lib.rs index 16e649f9f2..1603262988 100644 --- a/fixtures/callbacks/src/lib.rs +++ b/fixtures/callbacks/src/lib.rs @@ -34,7 +34,12 @@ impl RustGetters { callback.get_list(v, arg2) } - fn get_string_optional_callback<'a>(&self, callback: Option>, v: String, arg2: bool) -> Option { + fn get_string_optional_callback<'a>( + &self, + callback: Option>, + v: String, + arg2: bool, + ) -> Option { callback.map(|c| c.get_string(v, arg2)) } } diff --git a/uniffi/src/ffi/rustcalls.rs b/uniffi/src/ffi/rustcalls.rs index 9d2921768f..2e8b977b71 100644 --- a/uniffi/src/ffi/rustcalls.rs +++ b/uniffi/src/ffi/rustcalls.rs @@ -10,7 +10,7 @@ //! - Catching panics //! - Adapting `Result<>` types into either a return value or an error -use crate::{RustBuffer, RustBufferFfiConverter, FfiConverter}; +use crate::{FfiConverter, RustBuffer, RustBufferFfiConverter}; use anyhow::Result; use ffi_support::IntoFfi; use std::mem::MaybeUninit; @@ -177,7 +177,7 @@ where #[cfg(test)] mod test { use super::*; - use crate::{RustBufferFfiConverter, FfiConverter}; + use crate::{FfiConverter, RustBufferFfiConverter}; fn function(a: u8) -> i8 { match a { diff --git a/uniffi_bindgen/src/scaffolding/mod.rs b/uniffi_bindgen/src/scaffolding/mod.rs index ae112f60ae..fa79e1fd61 100644 --- a/uniffi_bindgen/src/scaffolding/mod.rs +++ b/uniffi_bindgen/src/scaffolding/mod.rs @@ -114,6 +114,9 @@ mod filters { // // This outputs something like `` pub fn ffi_converter_impl(type_: &Type) -> Result { - Ok(format!("<{} as uniffi::FfiConverter>", ffi_converter_impl_name(type_)?)) + Ok(format!( + "<{} as uniffi::FfiConverter>", + ffi_converter_impl_name(type_)? + )) } } From 1efdf29ba331a2c02ac346075dad3a3c5c5be80d Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Fri, 6 Aug 2021 11:05:00 -0400 Subject: [PATCH 40/45] Use "ffi_converter" instead of "ffi_converter_impl" It's easier to read and to type. The main reason that I used "viaffi_impl" was to hint that we had a new system and that it wasn't always going to be ``. Renaming the trait to `FfiConverter` accomplishes the same thing better. --- uniffi_bindgen/src/scaffolding/mod.rs | 14 +++++++------- .../templates/CallbackInterfaceTemplate.rs | 6 +++--- .../src/scaffolding/templates/EnumTemplate.rs | 8 ++++---- .../src/scaffolding/templates/ErrorTemplate.rs | 10 +++++----- .../scaffolding/templates/RecordTemplate.rs | 8 ++++---- .../src/scaffolding/templates/macros.rs | 18 +++++++++--------- 6 files changed, 32 insertions(+), 32 deletions(-) diff --git a/uniffi_bindgen/src/scaffolding/mod.rs b/uniffi_bindgen/src/scaffolding/mod.rs index fa79e1fd61..14335b98ce 100644 --- a/uniffi_bindgen/src/scaffolding/mod.rs +++ b/uniffi_bindgen/src/scaffolding/mod.rs @@ -72,7 +72,7 @@ mod filters { /// /// - 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 - pub fn ffi_converter_impl_name(type_: &Type) -> askama::Result { + pub fn ffi_converter_name(type_: &Type) -> askama::Result { Ok(match type_ { // Timestamp/Duraration are handled by standard types Type::Timestamp => "std::time::SystemTime".into(), @@ -91,9 +91,9 @@ mod filters { } // Wrapper types are implemented by generics that wrap the FfiConverter implementation of the // inner type. - Type::Optional(inner) => format!("Option<{}>", ffi_converter_impl_name(inner)?), - Type::Sequence(inner) => format!("Vec<{}>", ffi_converter_impl_name(inner)?), - Type::Map(inner) => format!("HashMap", ffi_converter_impl_name(inner)?), + Type::Optional(inner) => format!("Option<{}>", ffi_converter_name(inner)?), + Type::Sequence(inner) => format!("Vec<{}>", ffi_converter_name(inner)?), + Type::Map(inner) => format!("HashMap", ffi_converter_name(inner)?), // Primitive types / strings are implemented by their rust type Type::Int8 => "i8".into(), Type::UInt8 => "u8".into(), @@ -112,11 +112,11 @@ mod filters { // Map a type to Rust code that specifies the FfiConverter implementation. // - // This outputs something like `` - pub fn ffi_converter_impl(type_: &Type) -> Result { + // This outputs something like `` + pub fn ffi_converter(type_: &Type) -> Result { Ok(format!( "<{} as uniffi::FfiConverter>", - ffi_converter_impl_name(type_)? + ffi_converter_name(type_)? )) } } diff --git a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs index f07ec1667b..77a787a63a 100644 --- a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs @@ -11,7 +11,7 @@ // - 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_()|ffi_converter_impl_name -%} +{% let trait_impl = cbi.type_()|ffi_converter_name -%} {% let foreign_callback_internals = format!("foreign_callback_{}_internals", trait_name)|upper -%} // Register a foreign callback for getting across the FFI. @@ -60,7 +60,7 @@ impl {{ trait_name }} for {{ trait_impl }} { let mut args_buf = Vec::new(); {% endif -%} {%- for arg in meth.arguments() %} - {{ arg.type_()|ffi_converter_impl }}::write({{ arg.name() }}, &mut args_buf); + {{ arg.type_()|ffi_converter }}::write({{ arg.name() }}, &mut args_buf); {%- endfor -%} let args_rbuf = uniffi::RustBuffer::from_vec(args_buf); @@ -73,7 +73,7 @@ impl {{ trait_name }} for {{ trait_impl }} { {% when Some with (return_type) -%} let vec = ret_rbuf.destroy_into_vec(); let mut ret_buf = vec.as_slice(); - {{ return_type|ffi_converter_impl }}::try_read(&mut ret_buf).unwrap() + {{ return_type|ffi_converter }}::try_read(&mut ret_buf).unwrap() {%- else -%} uniffi::RustBuffer::destroy(ret_rbuf); {%- endmatch %} diff --git a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs index d0eb3e67b6..ffb9024e55 100644 --- a/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/EnumTemplate.rs @@ -6,10 +6,10 @@ // We define a unit-struct to implement the trait to sidestep Rust's orphan rule (ADR-0006). #} -struct {{ e.type_()|ffi_converter_impl_name }}; +struct {{ e.type_()|ffi_converter_name }}; #[doc(hidden)] -impl uniffi::RustBufferFfiConverter for {{ e.type_()|ffi_converter_impl_name }} { +impl uniffi::RustBufferFfiConverter for {{ e.type_()|ffi_converter_name }} { type RustType = {{ e.name() }}; fn write(obj: Self::RustType, buf: &mut Vec) { @@ -19,7 +19,7 @@ impl uniffi::RustBufferFfiConverter for {{ e.type_()|ffi_converter_impl_name }} {{ e.name() }}::{{ variant.name() }} { {% for field in variant.fields() %}{{ field.name() }}, {%- endfor %} } => { buf.put_i32({{ loop.index }}); {% for field in variant.fields() -%} - {{ field.type_()|ffi_converter_impl }}::write({{ field.name() }}, buf); + {{ field.type_()|ffi_converter }}::write({{ field.name() }}, buf); {%- endfor %} }, {%- endfor %} @@ -33,7 +33,7 @@ impl uniffi::RustBufferFfiConverter for {{ e.type_()|ffi_converter_impl_name }} {%- for variant in e.variants() %} {{ loop.index }} => {{ e.name() }}::{{ variant.name() }}{% if variant.has_fields() %} { {% for field in variant.fields() %} - {{ field.name() }}: {{ field.type_()|ffi_converter_impl }}::try_read(buf)?, + {{ field.name() }}: {{ field.type_()|ffi_converter }}::try_read(buf)?, {%- endfor %} }{% endif %}, {%- endfor %} diff --git a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs index ce2f360c04..ae80674348 100644 --- a/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/ErrorTemplate.rs @@ -6,10 +6,10 @@ // We define a unit-struct to implement the trait to sidestep Rust's orphan rule (ADR-0006). #} -struct {{ e.type_()|ffi_converter_impl_name }}; +struct {{ e.type_()|ffi_converter_name }}; #[doc(hidden)] -impl uniffi::RustBufferFfiConverter for {{ e.type_()|ffi_converter_impl_name }} { +impl uniffi::RustBufferFfiConverter for {{ e.type_()|ffi_converter_name }} { type RustType = {{ e.name() }}; fn write(obj: {{ e.name() }}, buf: &mut Vec) { @@ -19,7 +19,7 @@ impl uniffi::RustBufferFfiConverter for {{ e.type_()|ffi_converter_impl_name }} {{ e.name() }}::{{ variant.name() }}{% if variant.has_fields() %} { {% for field in variant.fields() %}{{ field.name() }}, {%- endfor %} }{% else %}{..}{% endif %} => { buf.put_i32({{ loop.index }}); {% for field in variant.fields() -%} - {{ field.type_()|ffi_converter_impl }}::write({{ field.name() }}, buf); + {{ field.type_()|ffi_converter }}::write({{ field.name() }}, buf); {%- endfor %} }, {%- endfor %} @@ -42,7 +42,7 @@ impl uniffi::RustBufferFfiConverter for {{ e.type_()|ffi_converter_impl_name }} {%- for variant in e.variants() %} {{ loop.index }} => {{ e.name() }}::{{ variant.name() }}{% if variant.has_fields() %} { {% for field in variant.fields() %} - {{ field.name() }}: {{ field.type_()|ffi_converter_impl }}::try_read(buf)?, + {{ field.name() }}: {{ field.type_()|ffi_converter }}::try_read(buf)?, {%- endfor %} }{% endif %}, {%- endfor %} @@ -52,4 +52,4 @@ impl uniffi::RustBufferFfiConverter for {{ e.type_()|ffi_converter_impl_name }} {% endif %} } -impl uniffi::FfiError for {{ e.type_()|ffi_converter_impl_name }} { } +impl uniffi::FfiError for {{ e.type_()|ffi_converter_name }} { } diff --git a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs index 27c5f1ae4f..1d3d16d5d9 100644 --- a/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/RecordTemplate.rs @@ -6,24 +6,24 @@ // // We define a unit-struct to implement the trait to sidestep Rust's orphan rule (ADR-0006) #} -struct {{ rec.type_()|ffi_converter_impl_name }}; +struct {{ rec.type_()|ffi_converter_name }}; #[doc(hidden)] -impl uniffi::RustBufferFfiConverter for {{ rec.type_()|ffi_converter_impl_name }} { +impl uniffi::RustBufferFfiConverter for {{ rec.type_()|ffi_converter_name }} { type RustType = {{ rec.name() }}; fn write(obj: {{ rec.name() }}, buf: &mut Vec) { // 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_()|ffi_converter_impl }}::write(obj.{{ field.name() }}, buf); + {{ field.type_()|ffi_converter }}::write(obj.{{ field.name() }}, buf); {%- endfor %} } fn try_read(buf: &mut &[u8]) -> uniffi::deps::anyhow::Result<{{ rec.name() }}> { Ok({{ rec.name() }} { {%- for field in rec.fields() %} - {{ field.name() }}: {{ field.type_()|ffi_converter_impl }}::try_read(buf)?, + {{ field.name() }}: {{ field.type_()|ffi_converter }}::try_read(buf)?, {%- endfor %} }) } diff --git a/uniffi_bindgen/src/scaffolding/templates/macros.rs b/uniffi_bindgen/src/scaffolding/templates/macros.rs index a63582aeed..16664038b6 100644 --- a/uniffi_bindgen/src/scaffolding/templates/macros.rs +++ b/uniffi_bindgen/src/scaffolding/templates/macros.rs @@ -9,7 +9,7 @@ {%- macro _arg_list_rs_call(func) %} {%- for arg in func.full_arguments() %} {%- if arg.by_ref() %}&{% endif %} - {{- arg.type_()|ffi_converter_impl }}::try_lift({{ arg.name() }}).unwrap() + {{- arg.type_()|ffi_converter }}::try_lift({{ arg.name() }}).unwrap() {%- if !loop.last %}, {% endif %} {%- endfor %} {%- endmacro -%} @@ -38,7 +38,7 @@ {% 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_impl }}::lower(_retval){% else %}_retval{% 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) %} {{- obj.name() }}::{% call to_rs_call(cons) -%} @@ -48,15 +48,15 @@ {% match cons.throws_type() %} {% when Some with (e) %} uniffi::call_with_result(call_status, || { - let _new = {% call construct(obj, cons) %}.map_err({{ e|ffi_converter_impl }}::lower)?; + let _new = {% call construct(obj, cons) %}.map_err({{ e|ffi_converter }}::lower)?; let _arc = std::sync::Arc::new(_new); - Ok({{ obj.type_()|ffi_converter_impl }}::lower(_arc)) + Ok({{ obj.type_()|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_()|ffi_converter_impl }}::lower(_arc) + {{ obj.type_()|ffi_converter }}::lower(_arc) }) {% endmatch %} {% endmacro %} @@ -65,7 +65,7 @@ {% match meth.throws_type() -%} {% when Some with (e) -%} uniffi::call_with_result(call_status, || { - let _retval = {{ obj.name() }}::{% call to_rs_call(meth) %}.map_err({{ e|ffi_converter_impl }}::lower)?; + let _retval = {{ obj.name() }}::{% call to_rs_call(meth) %}.map_err({{ e|ffi_converter }}::lower)?; Ok({% call ret(meth) %}) }) {% else %} @@ -73,7 +73,7 @@ uniffi::call_with_output(call_status, || { {% match meth.return_type() -%} {% when Some with (return_type) -%} let retval = {{ obj.name() }}::{% call to_rs_call(meth) %}; - {{ return_type|ffi_converter_impl }}::lower(retval) + {{ return_type|ffi_converter }}::lower(retval) {% else -%} {{ obj.name() }}::{% call to_rs_call(meth) %} {% endmatch -%} @@ -85,14 +85,14 @@ uniffi::call_with_output(call_status, || { {% match func.throws_type() %} {% when Some with (e) %} uniffi::call_with_result(call_status, || { - let _retval = {% call to_rs_call(func) %}.map_err({{ e|ffi_converter_impl }}::lower)?; + let _retval = {% call to_rs_call(func) %}.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_impl }}::lower({% call to_rs_call(func) %}) + {{ return_type|ffi_converter }}::lower({% call to_rs_call(func) %}) {% else -%} {% call to_rs_call(func) %} {% endmatch -%} From 1eb74bac89edaaad44355db544535c198dcfaddc Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Fri, 6 Aug 2021 12:26:20 -0400 Subject: [PATCH 41/45] Drop ffi-support dependency This seems like a nice little cleanup. The `rustcalls` mod was importing `IntoFfi`, but only using a trivial part of the interface. Now we just define our own trait to do that. I was a slightly worried about this when I initially implemented the `rustcalls` module, because I was concenced about uniffi consumers defining their own `ViaFfi` implementation with some other `FfiType`. However I now realize this is impossible, we have a `FfiType` enum in the scaffolding code that spells out the possible types. The last bit of functionality was the panic handling hook, I just copied the code for that. --- CHANGELOG.md | 3 ++ uniffi/Cargo.toml | 1 - uniffi/src/ffi/ffidefault.rs | 52 ++++++++++++++++++++++++++++++ uniffi/src/ffi/foreigncallbacks.rs | 11 ++----- uniffi/src/ffi/mod.rs | 2 ++ uniffi/src/ffi/rustbuffer.rs | 10 ------ uniffi/src/ffi/rustcalls.rs | 19 ++++++----- uniffi/src/lib.rs | 3 +- uniffi/src/panichook.rs | 34 +++++++++++++++++++ 9 files changed, 104 insertions(+), 31 deletions(-) create mode 100644 uniffi/src/ffi/ffidefault.rs create mode 100644 uniffi/src/panichook.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ec957e9317..6a848b14bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ [All changes in [[UnreleasedVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.12.0...HEAD). ### ⚠️ Breaking Changes ⚠️ +- UniFFI no longer has ffi-support as a dependency. This means it handles + panic logging on its own. If you previously enabled the `log_panics` feature + for `ffi-support`, now you should enable it for `uniffi`. - The Swift bindings now explicitly generate two separate Swift modules, one for the high-level Swift code and one for the low-level C FFI. This change is intended to simplify distribution of the bindings via Swift packages, but brings with it diff --git a/uniffi/Cargo.toml b/uniffi/Cargo.toml index 233d655ba1..ea38627194 100644 --- a/uniffi/Cargo.toml +++ b/uniffi/Cargo.toml @@ -14,7 +14,6 @@ keywords = ["ffi", "bindgen"] # Re-exported dependencies used in generated Rust scaffolding files. anyhow = "1" bytes = "1.0" -ffi-support = "~0.4.4" lazy_static = "1.4" log = "0.4" # Regular dependencies diff --git a/uniffi/src/ffi/ffidefault.rs b/uniffi/src/ffi/ffidefault.rs new file mode 100644 index 0000000000..f247312be8 --- /dev/null +++ b/uniffi/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/uniffi/src/ffi/foreigncallbacks.rs b/uniffi/src/ffi/foreigncallbacks.rs index 1f6b86af99..3727ea1232 100644 --- a/uniffi/src/ffi/foreigncallbacks.rs +++ b/uniffi/src/ffi/foreigncallbacks.rs @@ -139,15 +139,8 @@ pub const IDX_CALLBACK_FREE: u32 = 0; // 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. -ffi_support::static_assert!( - STATIC_ASSERT_USIZE_EQ_FUNC_SIZE, - std::mem::size_of::() == std::mem::size_of::() -); - -ffi_support::static_assert!( - STATIC_ASSERT_USIZE_EQ_OPT_FUNC_SIZE, - std::mem::size_of::() == std::mem::size_of::>() -); +static_assertions::assert_eq_size!(usize, ForeignCallback); +static_assertions::assert_eq_size!(usize, Option); /// Struct to hold a foreign callback. pub struct ForeignCallbackInternals { diff --git a/uniffi/src/ffi/mod.rs b/uniffi/src/ffi/mod.rs index adf8cb4eee..73ee721435 100644 --- a/uniffi/src/ffi/mod.rs +++ b/uniffi/src/ffi/mod.rs @@ -2,11 +2,13 @@ * 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::*; diff --git a/uniffi/src/ffi/rustbuffer.rs b/uniffi/src/ffi/rustbuffer.rs index 6b0d99fd75..d1a8369922 100644 --- a/uniffi/src/ffi/rustbuffer.rs +++ b/uniffi/src/ffi/rustbuffer.rs @@ -186,16 +186,6 @@ impl Default for RustBuffer { } } -unsafe impl crate::deps::ffi_support::IntoFfi for RustBuffer { - type Value = Self; - fn ffi_default() -> Self { - unsafe { Self::from_raw_parts(std::ptr::null_mut(), 0, 0) } - } - fn into_ffi_value(self) -> Self::Value { - self - } -} - #[cfg(test)] mod test { use super::*; diff --git a/uniffi/src/ffi/rustcalls.rs b/uniffi/src/ffi/rustcalls.rs index cec8736b7c..41407fea8d 100644 --- a/uniffi/src/ffi/rustcalls.rs +++ b/uniffi/src/ffi/rustcalls.rs @@ -10,9 +10,9 @@ //! - Catching panics //! - Adapting `Result<>` types into either a return value or an error +use super::FfiDefault; use crate::{RustBuffer, RustBufferViaFfi, ViaFfi}; use anyhow::Result; -use ffi_support::IntoFfi; use std::mem::MaybeUninit; use std::panic; @@ -79,15 +79,14 @@ pub trait FfiError: RustBufferViaFfi {} // Generalized rust call handling function // callback is responsible for making the call to the rust function. If that function returns an // `Err` value, callback should serialize the error into a `RustBuffer` to be returned over the FFI. -fn make_call(out_status: &mut RustCallStatus, callback: F) -> R::Value +fn make_call(out_status: &mut RustCallStatus, callback: F) -> R where F: panic::UnwindSafe + FnOnce() -> Result, - R: IntoFfi, + R: FfiDefault, { let result = panic::catch_unwind(|| { - // Use ffi_support's panic handling hook - ffi_support::ensure_panic_hook_is_setup(); - callback().map(|v| v.into_ffi_value()) + crate::panichook::ensure_setup(); + callback() }); match result { // Happy path. Note: no need to update out_status in this case because the calling code @@ -141,10 +140,10 @@ where /// - If the function panics: /// - `out_status.code` will be set to `CALL_PANIC` /// - the return value is undefined -pub fn call_with_output(out_status: &mut RustCallStatus, callback: F) -> R::Value +pub fn call_with_output(out_status: &mut RustCallStatus, callback: F) -> R where F: panic::UnwindSafe + FnOnce() -> R, - R: IntoFfi, + R: FfiDefault, { make_call(out_status, || Ok(callback())) } @@ -160,11 +159,11 @@ where /// - If the function panics: /// - `out_status.code` will be set to `CALL_PANIC` /// - the return value is undefined -pub fn call_with_result(out_status: &mut RustCallStatus, callback: F) -> R::Value +pub fn call_with_result(out_status: &mut RustCallStatus, callback: F) -> R where F: panic::UnwindSafe + FnOnce() -> Result, E: FfiError, - R: IntoFfi, + R: FfiDefault, { make_call(out_status, || callback().map_err(|e| e.lower())) } diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index a5e925dbd9..c3c84e64c0 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -48,11 +48,12 @@ pub mod testing; pub mod deps { pub use anyhow; pub use bytes; - pub use ffi_support; 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 diff --git a/uniffi/src/panichook.rs b/uniffi/src/panichook.rs new file mode 100644 index 0000000000..ce49be422b --- /dev/null +++ b/uniffi/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. + ("", 0) + }; + log::error!("### Rust `panic!` hit at file '{}', line {}", file, 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() {} From 35fe355dd47f141349415b4ad82d15fcac63641e Mon Sep 17 00:00:00 2001 From: bendk Date: Mon, 9 Aug 2021 09:31:57 -0400 Subject: [PATCH 42/45] Apply suggestions from code review Thanks badboy! I've never used the github suggestion UI before, that was great. Co-authored-by: Jan-Erik Rediger --- uniffi/src/ffi/rustcalls.rs | 12 ++++++------ uniffi/src/lib.rs | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/uniffi/src/ffi/rustcalls.rs b/uniffi/src/ffi/rustcalls.rs index 2e8b977b71..8fa6834f0f 100644 --- a/uniffi/src/ffi/rustcalls.rs +++ b/uniffi/src/ffi/rustcalls.rs @@ -135,8 +135,8 @@ where /// 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()). +/// `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: @@ -152,10 +152,10 @@ where /// 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()) +/// `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`: diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index b9c7f185ec..73400360f8 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -11,21 +11,21 @@ //! 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 +//! * 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 +//! * 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 +//! * 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 +//! * 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 +//! * 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 +//! 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 +//! 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`]. use anyhow::{bail, Result}; @@ -144,9 +144,9 @@ macro_rules! assert_compatible_version { /// implementations generated from your component UDL via the `uniffi-bindgen scaffolding` command. pub unsafe trait FfiConverter: Sized { - /// The type used in rust code. + /// The type used in Rust code. /// - /// For primitive / standard types, we implement FfiConverter on the type itself and RustType=Self. + /// 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; @@ -168,7 +168,7 @@ pub unsafe trait FfiConverter: Sized { /// by (hopefully cheaply!) converting it into someting that can be passed over the FFI /// and reconstructed on the other side. /// - /// Note that this method takes an owned `self`; this allows it to transfer ownership + /// 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; @@ -188,7 +188,7 @@ pub unsafe trait FfiConverter: Sized { /// 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` because it's transfering ownership + /// Note that this method takes an owned `Self::RustType` because it's transfering ownership /// to the foreign language code via the RustBuffer. fn write(obj: Self::RustType, buf: &mut Vec); @@ -223,7 +223,7 @@ pub fn check_remaining(buf: &[u8], num_bytes: usize) -> Result<()> { Ok(()) } -/// Blanket implementation of FfiConverter for numeric primitives. +/// 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! From ea6d91c1ac8b3830a509f3c07632748cce1d4516 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Mon, 9 Aug 2021 09:37:37 -0400 Subject: [PATCH 43/45] Changes based on rfk`s review --- CHANGELOG.md | 4 ++++ docs/adr/0006-external-types.md | 6 +++--- fixtures/callbacks/src/lib.rs | 4 ++-- uniffi_bindgen/src/scaffolding/mod.rs | 1 + .../src/scaffolding/templates/CallbackInterfaceTemplate.rs | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f6590f523..ca97c4c63c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,10 @@ - Both python and ruby backends now handle U16 correctly. - Error variants can now contain named fields, similar to Enum variants +- Replaced the `ViaFfi` trait with the `FfiConverter` trait. `FfiConverter` is + a more flexible version of `ViaFfi` because it can convert any Rust + type to/from an Ffi type, rather than only Self. This allows for using + UniFFI with a type defined in an external crate. ## v0.12.0 (2021-06-14) diff --git a/docs/adr/0006-external-types.md b/docs/adr/0006-external-types.md index d503a48263..150418ce6b 100644 --- a/docs/adr/0006-external-types.md +++ b/docs/adr/0006-external-types.md @@ -9,7 +9,7 @@ Discussion : [PR 1001](https://github.com/mozilla/uniffi-rs/pull/1001). ## Context and Problem Statement -UniFFI currently cannot support types from external crates because of Rust's +UniFFI currently cannot support types from external crates because Rust's orphan rule prevents implementing `ViaFfi`. The orphan rules specifies that the trait needs to be in the `uniffi` crate or the external crate, but we are generating the scaffolding code for the crate being uniffied. @@ -35,7 +35,7 @@ that struct to lift/lower the external type that we actually want to use. wrap the external type (`struct WrapperType(ExternalType)`) * In the filters functions we generate code to wrap lift/lower/read/write. - For example the lower_rs filter could output `External_type(x).lower()` to + For example the lower_rs filter could output `WrapperType(x).lower()` to lower `WrapperType`. * **[Option 2] Replace the `ViaFfi` trait with something more generic** @@ -66,7 +66,7 @@ This decision is taken because: * We believe it will make it easier to implement the other wrapping-style features mentioned above. One sign of this was the CallbackInterface code, - which converts it's type to `Box`. The + which converts its type to `Box`. The `FfiConverter` trait was able to implement this, removing the current lift/lower/read/write template code and fixing a bug with `Option<>`. diff --git a/fixtures/callbacks/src/lib.rs b/fixtures/callbacks/src/lib.rs index 1603262988..0272892d93 100644 --- a/fixtures/callbacks/src/lib.rs +++ b/fixtures/callbacks/src/lib.rs @@ -34,9 +34,9 @@ impl RustGetters { callback.get_list(v, arg2) } - fn get_string_optional_callback<'a>( + fn get_string_optional_callback( &self, - callback: Option>, + callback: Option>, v: String, arg2: bool, ) -> Option { diff --git a/uniffi_bindgen/src/scaffolding/mod.rs b/uniffi_bindgen/src/scaffolding/mod.rs index 14335b98ce..2f6542a864 100644 --- a/uniffi_bindgen/src/scaffolding/mod.rs +++ b/uniffi_bindgen/src/scaffolding/mod.rs @@ -72,6 +72,7 @@ mod filters { /// /// - 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 { Ok(match type_ { // Timestamp/Duraration are handled by standard types diff --git a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs index 77a787a63a..6a757e5b0d 100644 --- a/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs +++ b/uniffi_bindgen/src/scaffolding/templates/CallbackInterfaceTemplate.rs @@ -87,7 +87,7 @@ unsafe impl uniffi::FfiConverter for {{ trait_impl }} { 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 but this carries lots of thread safety risks, down to impedence + // probably a way to, but this carries lots of thread safety risks, down to impedence // mismatches between Rust and foreign languages, and our uncertainty around implementations of // concurrent handlemaps. // From bf0a5a0d52425b8f30aa061157ffb2274c6d42ec Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Fri, 6 Aug 2021 12:14:45 +1000 Subject: [PATCH 44/45] Add a re-usable abstraction for iterating over contained types. We currently have several methods that want to be able to recurse through all the types contained within a particular item, such as `type_contains_object_references` and `type_contains_unsigned_types`. These methods duplicate the logic for recursing into our various types of struct and are a bit weird to use. This commit proposes a cleanup to make these methods appear more consistent to the caller, and reduce code duplication in their implementation. The core idea is a new trait `IterTypes`. It can be implemented by anything that contains instances of the `Type` enum, and provides a single method `iter_types` to iterate over the the contained types. It's designed to encapsulate the recursion logic, e.g. the `IterTypes` impl for `Object` knows that it needs to recurse into each of the methods, the `IterTypes` impl for a `Method` knows that it needs to recurse into each argument and return type, etc. On top of this, the `CompontentInterface` can provide a generic implementation of methods like `item_contains_object_references` and `item_contains_unsigned_types` that will work for any `IterTypes`. I think this provides a nice little increase in consistency with how those methods are implemented and called. --- .../bindings/kotlin/templates/EnumTemplate.kt | 6 +- .../kotlin/templates/ErrorTemplate.kt | 6 +- .../kotlin/templates/RecordTemplate.kt | 6 +- .../kotlin/templates/RustBufferHelpers.kt | 24 +- .../src/bindings/kotlin/templates/macros.kt | 2 +- .../swift/templates/EnumTemplate.swift | 2 +- .../swift/templates/ErrorTemplate.swift | 2 +- .../swift/templates/RecordTemplate.swift | 2 +- uniffi_bindgen/src/interface/callbacks.rs | 8 +- uniffi_bindgen/src/interface/enum_.rs | 22 +- uniffi_bindgen/src/interface/error.rs | 9 +- uniffi_bindgen/src/interface/function.rs | 29 ++- uniffi_bindgen/src/interface/mod.rs | 236 +++++++++++++----- uniffi_bindgen/src/interface/object.rs | 48 ++-- uniffi_bindgen/src/interface/record.rs | 20 +- uniffi_bindgen/src/interface/types/mod.rs | 61 +++++ 16 files changed, 338 insertions(+), 145 deletions(-) diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt index e002ca7ab2..fcb1deda22 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/EnumTemplate.kt @@ -36,7 +36,7 @@ enum class {{ e.name()|class_name_kt }} { {% else %} {% call kt::unsigned_types_annotation(e) %} -sealed class {{ e.name()|class_name_kt }}{% if e.contains_object_references(ci) %}: Disposable {% endif %} { +sealed class {{ e.name()|class_name_kt }}{% if ci.item_contains_object_references(e) %}: Disposable {% endif %} { {% for variant in e.variants() -%} {% if !variant.has_fields() -%} object {{ variant.name()|class_name_kt }} : {{ e.name()|class_name_kt }}() @@ -85,14 +85,14 @@ sealed class {{ e.name()|class_name_kt }}{% if e.contains_object_references(ci) }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } - {% if e.contains_object_references(ci) %} + {% if ci.item_contains_object_references(e) %} @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 {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }} -> { {% for field in variant.fields() -%} - {%- if ci.type_contains_object_references(field.type_()) -%} + {%- if ci.item_contains_object_references(field) -%} this.{{ field.name() }}?.destroy() {% endif -%} {%- endfor %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt index f057bc7368..90657e8d5c 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt @@ -27,7 +27,7 @@ interface CallStatusErrorHandler { // Error {{ e.name() }} {%- let toplevel_name=e.name()|exception_name_kt %} -sealed class {{ toplevel_name }}: Exception(){% if e.contains_object_references(ci) %}, Disposable {% endif %} { +sealed class {{ toplevel_name }}: Exception(){% if ci.item_contains_object_references(e) %}, Disposable {% endif %} { // Each variant is a nested class {% for variant in e.variants() -%} {% if !variant.has_fields() -%} @@ -60,14 +60,14 @@ sealed class {{ toplevel_name }}: Exception(){% if e.contains_object_references( } } - {% if e.contains_object_references(ci) %} + {% if ci.item_contains_object_references(e) %} @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 {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }} -> { {% for field in variant.fields() -%} - {%- if ci.type_contains_object_references(field.type_()) -%} + {%- if ci.item_contains_object_references(field) -%} this.{{ field.name() }}?.destroy() {% endif -%} {%- endfor %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt index 65c1eb1be1..b6a9da8296 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/RecordTemplate.kt @@ -8,7 +8,7 @@ data class {{ rec.name()|class_name_kt }} ( {%- endmatch -%} {% if !loop.last %}, {% endif %} {%- endfor %} -) {% if rec.contains_object_references(ci) %}: Disposable {% endif %}{ +) {% if ci.item_contains_object_references(rec) %}: Disposable {% endif %}{ companion object { internal fun lift(rbuf: RustBuffer.ByValue): {{ rec.name()|class_name_kt }} { return liftFromRustBuffer(rbuf) { buf -> {{ rec.name()|class_name_kt }}.read(buf) } @@ -33,11 +33,11 @@ data class {{ rec.name()|class_name_kt }} ( {% endfor %} } - {% if rec.contains_object_references(ci) %} + {% if ci.item_contains_object_references(rec) %} @Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here override fun destroy() { {% for field in rec.fields() %} - {%- if ci.type_contains_object_references(field.type_()) -%} + {%- if ci.item_contains_object_references(field) -%} this.{{ field.name() }}?.destroy() {% endif -%} {%- endfor %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferHelpers.kt b/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferHelpers.kt index 1d3af771db..16b1ae235f 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferHelpers.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferHelpers.kt @@ -382,14 +382,14 @@ internal fun write{{ canonical_type_name }}(v: {{ type_name }}, buf: RustBufferB {% let inner_type_name = inner_type|type_kt %} // Helper functions for pasing values of type {{ typ|type_kt }} -{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} +{% call kt::unsigned_types_annotation(inner_type) %} internal fun lift{{ canonical_type_name }}(rbuf: RustBuffer.ByValue): {{ inner_type_name }}? { return liftFromRustBuffer(rbuf) { buf -> read{{ canonical_type_name }}(buf) } } -{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} +{% call kt::unsigned_types_annotation(inner_type) %} internal fun read{{ canonical_type_name }}(buf: ByteBuffer): {{ inner_type_name }}? { if (buf.get().toInt() == 0) { return null @@ -397,14 +397,14 @@ internal fun read{{ canonical_type_name }}(buf: ByteBuffer): {{ inner_type_name return {{ "buf"|read_kt(inner_type) }} } -{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} +{% call kt::unsigned_types_annotation(inner_type) %} internal fun lower{{ canonical_type_name }}(v: {{ inner_type_name }}?): RustBuffer.ByValue { return lowerIntoRustBuffer(v) { v, buf -> write{{ canonical_type_name }}(v, buf) } } -{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} +{% call kt::unsigned_types_annotation(inner_type) %} internal fun write{{ canonical_type_name }}(v: {{ inner_type_name }}?, buf: RustBufferBuilder) { if (v == null) { buf.putByte(0) @@ -419,14 +419,14 @@ internal fun write{{ canonical_type_name }}(v: {{ inner_type_name }}?, buf: Rust // Helper functions for pasing values of type {{ typ|type_kt }} -{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} +{% call kt::unsigned_types_annotation(inner_type) %} internal fun lift{{ canonical_type_name }}(rbuf: RustBuffer.ByValue): List<{{ inner_type_name }}> { return liftFromRustBuffer(rbuf) { buf -> read{{ canonical_type_name }}(buf) } } -{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} +{% call kt::unsigned_types_annotation(inner_type) %} internal fun read{{ canonical_type_name }}(buf: ByteBuffer): List<{{ inner_type_name }}> { val len = buf.getInt() return List<{{ inner_type|type_kt }}>(len) { @@ -434,14 +434,14 @@ internal fun read{{ canonical_type_name }}(buf: ByteBuffer): List<{{ inner_type_ } } -{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} +{% call kt::unsigned_types_annotation(inner_type) %} internal fun lower{{ canonical_type_name }}(v: List<{{ inner_type_name }}>): RustBuffer.ByValue { return lowerIntoRustBuffer(v) { v, buf -> write{{ canonical_type_name }}(v, buf) } } -{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} +{% call kt::unsigned_types_annotation(inner_type) %} internal fun write{{ canonical_type_name }}(v: List<{{ inner_type_name }}>, buf: RustBufferBuilder) { buf.putInt(v.size) v.forEach { @@ -454,14 +454,14 @@ internal fun write{{ canonical_type_name }}(v: List<{{ inner_type_name }}>, buf: // Helper functions for pasing values of type {{ typ|type_kt }} -{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} +{% call kt::unsigned_types_annotation(inner_type) %} internal fun lift{{ canonical_type_name }}(rbuf: RustBuffer.ByValue): Map { return liftFromRustBuffer(rbuf) { buf -> read{{ canonical_type_name }}(buf) } } -{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} +{% call kt::unsigned_types_annotation(inner_type) %} internal fun read{{ canonical_type_name }}(buf: ByteBuffer): Map { // TODO: Once Kotlin's `buildMap` API is stabilized we should use it here. val items : MutableMap = mutableMapOf() @@ -474,14 +474,14 @@ internal fun read{{ canonical_type_name }}(buf: ByteBuffer): Map): RustBuffer.ByValue { return lowerIntoRustBuffer(m) { m, buf -> write{{ canonical_type_name }}(m, buf) } } -{% if ci.type_contains_unsigned_types(inner_type) %}@ExperimentalUnsignedTypes{% endif %} +{% call kt::unsigned_types_annotation(inner_type) %} internal fun write{{ canonical_type_name }}(v: Map, buf: RustBufferBuilder) { buf.putInt(v.size) // The parens on `(k, v)` here ensure we're calling the right method, diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt b/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt index cd8e0cc9a7..49ce04346d 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/macros.kt @@ -70,5 +70,5 @@ // Add annotation if there are unsigned types {%- macro unsigned_types_annotation(member) -%} -{% if member.contains_unsigned_types(ci) %}@ExperimentalUnsignedTypes{% endif %} +{% if ci.item_contains_unsigned_types(member) %}@ExperimentalUnsignedTypes{% endif %} {%- endmacro -%} diff --git a/uniffi_bindgen/src/bindings/swift/templates/EnumTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/EnumTemplate.swift index ab4c09efb9..54cae3398f 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/EnumTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/EnumTemplate.swift @@ -40,6 +40,6 @@ extension {{ e.name()|class_name_swift }}: ViaFfiUsingByteBuffer, ViaFfi { } } -{% if ! e.contains_object_references(ci) %} +{% if ! ci.item_contains_object_references(e) %} extension {{ e.name()|class_name_swift }}: Equatable, Hashable {} {% endif %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift index bcc093b8ab..473f838aa6 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift @@ -82,7 +82,7 @@ extension {{ e.name()|class_name_swift }}: ViaFfiUsingByteBuffer, ViaFfi { } } -{% if !e.contains_object_references(ci) %} +{% if !ci.item_contains_object_references(e) %} extension {{ e.name()|class_name_swift }}: Equatable, Hashable {} {% endif %} extension {{ e.name()|class_name_swift }}: Error { } diff --git a/uniffi_bindgen/src/bindings/swift/templates/RecordTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/RecordTemplate.swift index 751a4c965b..770b7659a9 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/RecordTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/RecordTemplate.swift @@ -12,7 +12,7 @@ public struct {{ rec.name()|class_name_swift }} { } } -{% if ! rec.contains_object_references(ci) %} +{% if ! ci.item_contains_object_references(rec) %} extension {{ rec.name()|class_name_swift }}: Equatable, Hashable { public static func ==(lhs: {{ rec.name()|class_name_swift }}, rhs: {{ rec.name()|class_name_swift }}) -> Bool { {%- for field in rec.fields() %} diff --git a/uniffi_bindgen/src/interface/callbacks.rs b/uniffi_bindgen/src/interface/callbacks.rs index cb2c0aba8b..e770bc9b5b 100644 --- a/uniffi_bindgen/src/interface/callbacks.rs +++ b/uniffi_bindgen/src/interface/callbacks.rs @@ -39,7 +39,7 @@ use anyhow::{bail, Result}; use super::ffi::{FFIArgument, FFIFunction, FFIType}; use super::object::Method; -use super::types::Type; +use super::types::{IterTypes, Type, TypeIterator}; use super::{APIConverter, ComponentInterface}; #[derive(Debug, Clone)] @@ -84,6 +84,12 @@ impl CallbackInterface { } } +impl IterTypes for CallbackInterface { + fn iter_types(&self) -> TypeIterator<'_> { + Box::new(self.methods.iter().map(IterTypes::iter_types).flatten()) + } +} + impl Hash for CallbackInterface { fn hash(&self, state: &mut H) { // We don't include the FFIFunc in the hash calculation, because: diff --git a/uniffi_bindgen/src/interface/enum_.rs b/uniffi_bindgen/src/interface/enum_.rs index 3498ccf577..8e54333403 100644 --- a/uniffi_bindgen/src/interface/enum_.rs +++ b/uniffi_bindgen/src/interface/enum_.rs @@ -79,7 +79,7 @@ use anyhow::{bail, Result}; use super::record::Field; -use super::types::Type; +use super::types::{IterTypes, Type, TypeIterator}; use super::{APIConverter, ComponentInterface}; /// Represents an enum with named variants, each of which may have named @@ -113,17 +113,11 @@ impl Enum { pub fn is_flat(&self) -> bool { self.flat } +} - pub fn contains_object_references(&self, ci: &ComponentInterface) -> bool { - ci.type_contains_object_references(&self.type_()) - } - - pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool { - self.variants().iter().any(|v| { - v.fields() - .iter() - .any(|f| ci.type_contains_unsigned_types(&f.type_)) - }) +impl IterTypes for Enum { + fn iter_types(&self) -> TypeIterator<'_> { + Box::new(self.variants.iter().map(IterTypes::iter_types).flatten()) } } @@ -199,6 +193,12 @@ impl Variant { } } +impl IterTypes for Variant { + fn iter_types(&self) -> TypeIterator<'_> { + Box::new(self.fields.iter().map(IterTypes::iter_types).flatten()) + } +} + impl APIConverter for weedle::interface::OperationInterfaceMember<'_> { fn convert(&self, ci: &mut ComponentInterface) -> Result { if self.special.is_some() { diff --git a/uniffi_bindgen/src/interface/error.rs b/uniffi_bindgen/src/interface/error.rs index 750b907de1..f4176a5585 100644 --- a/uniffi_bindgen/src/interface/error.rs +++ b/uniffi_bindgen/src/interface/error.rs @@ -85,7 +85,7 @@ use anyhow::Result; use super::enum_::{Enum, Variant}; -use super::types::Type; +use super::types::{IterTypes, Type, TypeIterator}; use super::{APIConverter, ComponentInterface}; /// Represents an Error that might be thrown by functions/methods in the component interface. @@ -129,10 +129,11 @@ impl Error { pub fn is_flat(&self) -> bool { self.enum_.is_flat() } +} - // For compatibility with the Enum interface - pub fn contains_object_references(&self, ci: &ComponentInterface) -> bool { - self.enum_.contains_object_references(ci) +impl IterTypes for Error { + fn iter_types(&self) -> TypeIterator<'_> { + self.wrapped_enum().iter_types() } } diff --git a/uniffi_bindgen/src/interface/function.rs b/uniffi_bindgen/src/interface/function.rs index 33e3e747a2..a65969ca37 100644 --- a/uniffi_bindgen/src/interface/function.rs +++ b/uniffi_bindgen/src/interface/function.rs @@ -39,7 +39,7 @@ use anyhow::{bail, Result}; use super::attributes::{ArgumentAttributes, FunctionAttributes}; use super::ffi::{FFIArgument, FFIFunction}; use super::literal::{convert_default_value, Literal}; -use super::types::Type; +use super::types::{IterTypes, Type, TypeIterator}; use super::{APIConverter, ComponentInterface}; /// Represents a standalone function. @@ -96,20 +96,17 @@ impl Function { self.ffi_func.return_type = self.return_type.as_ref().map(|rt| rt.into()); Ok(()) } +} - // Intentionally exactly the same as the Method version - pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool { - let check_return_type = { - match self.return_type() { - None => false, - Some(t) => ci.type_contains_unsigned_types(t), - } - }; - check_return_type - || self - .arguments() +impl IterTypes for Function { + fn iter_types(&self) -> TypeIterator<'_> { + Box::new( + self.arguments .iter() - .any(|&arg| ci.type_contains_unsigned_types(&arg.type_())) + .map(IterTypes::iter_types) + .flatten() + .chain(self.return_type.iter_types()), + ) } } @@ -180,6 +177,12 @@ impl Argument { } } +impl IterTypes for Argument { + fn iter_types(&self) -> TypeIterator<'_> { + self.type_.iter_types() + } +} + impl From<&Argument> for FFIArgument { fn from(a: &Argument) -> FFIArgument { FFIArgument { diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index 2464530150..446fc3fe54 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -45,7 +45,7 @@ //! * Error messages and general developer experience leave a lot to be desired. use std::{ - collections::hash_map::DefaultHasher, + collections::{hash_map::DefaultHasher, HashSet}, convert::TryFrom, hash::{Hash, Hasher}, str::FromStr, @@ -55,7 +55,7 @@ use anyhow::{bail, Result}; pub mod types; pub use types::Type; -use types::TypeUniverse; +use types::{IterTypes, TypeIterator, TypeUniverse}; mod attributes; mod callbacks; @@ -216,75 +216,52 @@ impl<'ci> ComponentInterface { self.types.get_type_definition(name) } - /// Check whether the given type contains any (possibly nested) Type::Object references. + /// Iterate over all types contained in the given item. + /// + /// This method uses `IterTypes::iter_types` to iterate over the types contained within the + /// given item, 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, T: IterTypes>( + &'a self, + item: &'a T, + ) -> impl Iterator + '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 type_contains_object_references(&self, type_: &Type) -> bool { - match type_ { - Type::Object(_) => true, - Type::Optional(t) | Type::Sequence(t) | Type::Map(t) => { - self.type_contains_object_references(t) - } - Type::Record(name) => self - .get_record_definition(name) - .map(|rec| { - rec.fields() - .iter() - .any(|f| self.type_contains_object_references(&f.type_)) - }) - .unwrap_or(false), - Type::Enum(name) => self - .get_enum_definition(name) - .map(|e| { - e.variants().iter().any(|v| { - v.fields() - .iter() - .any(|f| self.type_contains_object_references(&f.type_)) - }) - }) - .unwrap_or(false), - _ => false, - } + pub fn item_contains_object_references(&self, item: &T) -> 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: &T) -> 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.iter_types() - .iter() + 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.iter_types() - .iter() + 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.iter_types().iter().any(|t| matches!(t, Type::Map(_))) - } - - /// Check whether the given type contains any (possibly nested) unsigned types - pub fn type_contains_unsigned_types(&self, type_: &Type) -> bool { - match type_ { - Type::UInt8 | Type::UInt16 | Type::UInt32 | Type::UInt64 => true, - Type::Optional(t) | Type::Sequence(t) | Type::Map(t) => { - self.type_contains_unsigned_types(t) - } - Type::Object(t) => self - .get_object_definition(t) - .map(|obj| obj.contains_unsigned_types(&self)) - .unwrap_or(false), - Type::Record(name) => self - .get_record_definition(name) - .map(|rec| rec.contains_unsigned_types(&self)) - .unwrap_or(false), - Type::Enum(name) => self - .get_enum_definition(name) - .map(|e| e.contains_unsigned_types(&self)) - .unwrap_or(false), - _ => false, - } + self.types + .iter_known_types() + .any(|t| matches!(t, Type::Map(_))) } /// Calculate a numeric checksum for this ComponentInterface. @@ -589,6 +566,121 @@ impl Hash for ComponentInterface { } } +impl IterTypes for ComponentInterface { + fn iter_types(&self) -> TypeIterator<'_> { + self.types.iter_types() + } +} + +/// Stateful iterator for yielding all types contained in a given item. +/// +/// 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 `IterTypes::iter_types` on a +/// given item, 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 T) -> 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`. + // 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(IterTypes::iter_types), + Type::Enum(nm) => self.ci.get_enum_definition(&nm).map(IterTypes::iter_types), + Type::Error(nm) => self.ci.get_error_definition(&nm).map(IterTypes::iter_types), + Type::Object(nm) => self + .ci + .get_object_definition(&nm) + .map(IterTypes::iter_types), + Type::CallbackInterface(nm) => self + .ci + .get_callback_interface_definition(&nm) + .map(IterTypes::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 { + 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 @@ -851,4 +943,34 @@ mod test { .is_ok()); assert_eq!(ci.contains_map_types(), true); } + + #[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/uniffi_bindgen/src/interface/object.rs b/uniffi_bindgen/src/interface/object.rs index 07f3c5f3b6..d7383d2c67 100644 --- a/uniffi_bindgen/src/interface/object.rs +++ b/uniffi_bindgen/src/interface/object.rs @@ -54,7 +54,7 @@ use anyhow::{bail, Result}; use super::attributes::{ConstructorAttributes, InterfaceAttributes, MethodAttributes}; use super::ffi::{FFIArgument, FFIFunction, FFIType}; use super::function::Argument; -use super::types::Type; +use super::types::{IterTypes, Type, TypeIterator}; use super::{APIConverter, ComponentInterface}; /// An "object" is an opaque type that can be instantiated and passed around by reference, @@ -143,16 +143,17 @@ impl Object { } Ok(()) } +} - // We need to check both methods and constructor - pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool { - self.methods() - .iter() - .any(|&meth| meth.contains_unsigned_types(&ci)) - || self - .constructors() +impl IterTypes for Object { + fn iter_types(&self) -> TypeIterator<'_> { + Box::new( + self.methods .iter() - .any(|&constructor| constructor.contains_unsigned_types(&ci)) + .map(IterTypes::iter_types) + .chain(self.constructors.iter().map(IterTypes::iter_types)) + .flatten(), + ) } } @@ -263,11 +264,11 @@ impl Constructor { fn is_primary_constructor(&self) -> bool { self.name == "new" } +} - pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool { - self.arguments() - .iter() - .any(|&arg| ci.type_contains_unsigned_types(&arg.type_())) +impl IterTypes for Constructor { + fn iter_types(&self) -> TypeIterator<'_> { + Box::new(self.arguments.iter().map(IterTypes::iter_types).flatten()) } } @@ -383,20 +384,17 @@ impl Method { self.ffi_func.return_type = self.return_type.as_ref().map(Into::into); Ok(()) } +} - // Intentionally exactly the same as the Function version - pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool { - let check_return_type = { - match self.return_type() { - None => false, - Some(t) => ci.type_contains_unsigned_types(t), - } - }; - check_return_type - || self - .arguments() +impl IterTypes for Method { + fn iter_types(&self) -> TypeIterator<'_> { + Box::new( + self.arguments .iter() - .any(|&arg| ci.type_contains_unsigned_types(&arg.type_())) + .map(IterTypes::iter_types) + .flatten() + .chain(self.return_type.iter_types()), + ) } } diff --git a/uniffi_bindgen/src/interface/record.rs b/uniffi_bindgen/src/interface/record.rs index e8283e5b14..022fa30916 100644 --- a/uniffi_bindgen/src/interface/record.rs +++ b/uniffi_bindgen/src/interface/record.rs @@ -47,7 +47,7 @@ use anyhow::{bail, Result}; use super::literal::{convert_default_value, Literal}; -use super::types::Type; +use super::types::{IterTypes, Type, TypeIterator}; use super::{APIConverter, ComponentInterface}; /// Represents a "data class" style object, for passing around complex values. @@ -75,15 +75,11 @@ impl Record { pub fn fields(&self) -> Vec<&Field> { self.fields.iter().collect() } +} - pub fn contains_object_references(&self, ci: &ComponentInterface) -> bool { - ci.type_contains_object_references(&self.type_()) - } - - pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool { - self.fields() - .iter() - .any(|f| ci.type_contains_unsigned_types(&f.type_)) +impl IterTypes for Record { + fn iter_types(&self) -> TypeIterator<'_> { + Box::new(self.fields.iter().map(IterTypes::iter_types).flatten()) } } @@ -123,6 +119,12 @@ impl Field { } } +impl IterTypes for Field { + fn iter_types(&self) -> TypeIterator<'_> { + self.type_.iter_types() + } +} + impl APIConverter for weedle::dictionary::DictionaryMember<'_> { fn convert(&self, ci: &mut ComponentInterface) -> Result { if self.attributes.is_some() { diff --git a/uniffi_bindgen/src/interface/types/mod.rs b/uniffi_bindgen/src/interface/types/mod.rs index a847fd35e2..6280d306a1 100644 --- a/uniffi_bindgen/src/interface/types/mod.rs +++ b/uniffi_bindgen/src/interface/types/mod.rs @@ -228,6 +228,67 @@ impl TypeUniverse { } } +/// 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` on any method that yields types. +/// Unfortunately existential types are not currently supported in trait method +/// signatures, so for now we hide the concrete type behind a box. +pub type TypeIterator<'a> = Box + 'a>; + +/// A trait for objects that may contain references to types. +/// +/// Various objects in our interface will contain (possibly nested) references to types - +/// for example a `Record` struct will contain one or more `Field` structs which will each +/// have an associated type. This trait provides a uniform interface for inspecting the +/// types references by an object. + +pub trait IterTypes { + /// Iterate over all types contained within on object. + /// + /// This method iterates over the types contained with in object, making + /// no particular guarantees about ordering or handling of duplicates. + /// + /// The return type is a Box in order to hide the concrete implementation + /// details of the iterator. Ideally we would return `impl Iterator` here + /// but that's not currently supported for trait methods. + fn iter_types(&self) -> TypeIterator<'_>; +} + +impl IterTypes for &T { + fn iter_types(&self) -> TypeIterator<'_> { + (*self).iter_types() + } +} + +impl IterTypes for Box { + fn iter_types(&self) -> TypeIterator<'_> { + self.as_ref().iter_types() + } +} + +impl IterTypes for Option { + fn iter_types(&self) -> TypeIterator<'_> { + Box::new(self.iter().map(IterTypes::iter_types).flatten()) + } +} + +impl IterTypes for Type { + fn iter_types(&self) -> TypeIterator<'_> { + let nested_types = match self { + Type::Optional(t) | Type::Sequence(t) | Type::Map(t) => Some(t.iter_types()), + _ => None, + }; + Box::new(std::iter::once(self).chain(nested_types.into_iter().flatten())) + } +} + +impl IterTypes for TypeUniverse { + fn iter_types(&self) -> TypeIterator<'_> { + Box::new(self.all_known_types.iter()) + } +} + #[cfg(test)] mod test_type { use super::*; From d606439014cdae4967bf032dbd5639292cc57647 Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Tue, 10 Aug 2021 08:56:33 +1000 Subject: [PATCH 45/45] (cargo-release) version v0.13.0 --- CHANGELOG.md | 10 +++++++--- examples/arithmetic/Cargo.toml | 2 +- examples/callbacks/Cargo.toml | 2 +- examples/geometry/Cargo.toml | 2 +- examples/rondpoint/Cargo.toml | 2 +- examples/sprites/Cargo.toml | 2 +- examples/todolist/Cargo.toml | 2 +- fixtures/callbacks/Cargo.toml | 2 +- fixtures/coverall/Cargo.toml | 2 +- fixtures/external-types/crate-one/Cargo.toml | 4 ++-- fixtures/external-types/crate-two/Cargo.toml | 4 ++-- fixtures/external-types/lib/Cargo.toml | 2 +- .../cdylib-dependency/Cargo.toml | 2 +- .../cdylib-crate-type-dependency/ffi-crate/Cargo.toml | 2 +- .../regressions/enum-without-i32-helpers/Cargo.toml | 2 +- .../kotlin-experimental-unsigned-types/Cargo.toml | 2 +- fixtures/uniffi-fixture-time/Cargo.toml | 2 +- uniffi/Cargo.toml | 4 ++-- uniffi_bindgen/Cargo.toml | 2 +- uniffi_build/Cargo.toml | 4 ++-- uniffi_macros/Cargo.toml | 2 +- 21 files changed, 31 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9db35fdc8b..0de1a11cd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,11 @@ ## [[UnreleasedVersion]] (_[[ReleaseDate]]_) -[All changes in [[UnreleasedVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.12.0...HEAD). +[All changes in [[UnreleasedVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.13.0...HEAD). + +## v0.13.0 (_2021-08-09_) + +[All changes in v0.13.0](https://github.com/mozilla/uniffi-rs/compare/v0.12.0...v0.13.0). ### ⚠️ Breaking Changes ⚠️ - UniFFI no longer has ffi-support as a dependency. This means it handles @@ -41,7 +45,7 @@ type to/from an Ffi type, rather than only Self. This allows for using UniFFI with a type defined in an external crate. -## v0.12.0 (2021-06-14) +## v0.12.0 (_2021-06-14_) [All changes in v0.12.0](https://github.com/mozilla/uniffi-rs/compare/v0.11.0...v0.12.0). @@ -61,7 +65,7 @@ - Kotlin objects now implement `AutoCloseable` by default; closing an object instance is equivalent to calling its `destroy()` method. -## v0.11.0 (2021-06-03) +## v0.11.0 (_2021-06-03_) [All changes in v0.11.0](https://github.com/mozilla/uniffi-rs/compare/v0.10.0...v0.11.0). diff --git a/examples/arithmetic/Cargo.toml b/examples/arithmetic/Cargo.toml index 5fa649675c..5603c21996 100644 --- a/examples/arithmetic/Cargo.toml +++ b/examples/arithmetic/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "uniffi-example-arithmetic" edition = "2018" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" publish = false diff --git a/examples/callbacks/Cargo.toml b/examples/callbacks/Cargo.toml index 29917afaaa..08043ded8b 100644 --- a/examples/callbacks/Cargo.toml +++ b/examples/callbacks/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "uniffi-example-callbacks" edition = "2018" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" publish = false diff --git a/examples/geometry/Cargo.toml b/examples/geometry/Cargo.toml index 8d34faf05e..23645b8125 100644 --- a/examples/geometry/Cargo.toml +++ b/examples/geometry/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "uniffi-example-geometry" edition = "2018" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" publish = false diff --git a/examples/rondpoint/Cargo.toml b/examples/rondpoint/Cargo.toml index 8694725520..febba3d27a 100644 --- a/examples/rondpoint/Cargo.toml +++ b/examples/rondpoint/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "uniffi-example-rondpoint" edition = "2018" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" publish = false diff --git a/examples/sprites/Cargo.toml b/examples/sprites/Cargo.toml index 8046e5c018..068b2ae6b7 100644 --- a/examples/sprites/Cargo.toml +++ b/examples/sprites/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "uniffi-example-sprites" edition = "2018" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" publish = false diff --git a/examples/todolist/Cargo.toml b/examples/todolist/Cargo.toml index b396a96ab5..341c0a7d45 100644 --- a/examples/todolist/Cargo.toml +++ b/examples/todolist/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "uniffi-example-todolist" edition = "2018" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" publish = false diff --git a/fixtures/callbacks/Cargo.toml b/fixtures/callbacks/Cargo.toml index c1da936220..6042e999ad 100644 --- a/fixtures/callbacks/Cargo.toml +++ b/fixtures/callbacks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "callbacks" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] edition = "2018" publish = false diff --git a/fixtures/coverall/Cargo.toml b/fixtures/coverall/Cargo.toml index 532b6053fc..a1fff4acf6 100644 --- a/fixtures/coverall/Cargo.toml +++ b/fixtures/coverall/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "coverall" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] edition = "2018" publish = false diff --git a/fixtures/external-types/crate-one/Cargo.toml b/fixtures/external-types/crate-one/Cargo.toml index 11d4716366..25082f4ab9 100644 --- a/fixtures/external-types/crate-one/Cargo.toml +++ b/fixtures/external-types/crate-one/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "crate_one" edition = "2018" -version = "0.11.0" +version = "0.13.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" publish = false @@ -13,4 +13,4 @@ uniffi_macros = {path = "../../../uniffi_macros"} uniffi = {path = "../../../uniffi", features=["builtin-bindgen"]} [build-dependencies] -uniffi_build = {path = "../../../uniffi_build", features=["builtin-bindgen"]} \ No newline at end of file +uniffi_build = {path = "../../../uniffi_build", features=["builtin-bindgen"]} diff --git a/fixtures/external-types/crate-two/Cargo.toml b/fixtures/external-types/crate-two/Cargo.toml index 86b58f4a69..28b389da3c 100644 --- a/fixtures/external-types/crate-two/Cargo.toml +++ b/fixtures/external-types/crate-two/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "crate_two" edition = "2018" -version = "0.11.0" +version = "0.13.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" publish = false @@ -14,4 +14,4 @@ uniffi = {path = "../../../uniffi", features=["builtin-bindgen"]} crate_one = {path = "../crate-one"} [build-dependencies] -uniffi_build = {path = "../../../uniffi_build", features=["builtin-bindgen"]} \ No newline at end of file +uniffi_build = {path = "../../../uniffi_build", features=["builtin-bindgen"]} diff --git a/fixtures/external-types/lib/Cargo.toml b/fixtures/external-types/lib/Cargo.toml index 647aab67a4..5ff6ee00af 100644 --- a/fixtures/external-types/lib/Cargo.toml +++ b/fixtures/external-types/lib/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "external_types_lib" edition = "2018" -version = "0.11.0" +version = "0.13.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" publish = false diff --git a/fixtures/regressions/cdylib-crate-type-dependency/cdylib-dependency/Cargo.toml b/fixtures/regressions/cdylib-crate-type-dependency/cdylib-dependency/Cargo.toml index 87fcdae95d..609e0ad735 100644 --- a/fixtures/regressions/cdylib-crate-type-dependency/cdylib-dependency/Cargo.toml +++ b/fixtures/regressions/cdylib-crate-type-dependency/cdylib-dependency/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cdylib-dependency" -version = "0.12.0" +version = "0.13.0" authors = ["king6cong "] edition = "2018" publish = false diff --git a/fixtures/regressions/cdylib-crate-type-dependency/ffi-crate/Cargo.toml b/fixtures/regressions/cdylib-crate-type-dependency/ffi-crate/Cargo.toml index 38fcde81d3..eba459e678 100644 --- a/fixtures/regressions/cdylib-crate-type-dependency/ffi-crate/Cargo.toml +++ b/fixtures/regressions/cdylib-crate-type-dependency/ffi-crate/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ffi-crate" edition = "2018" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" publish = false diff --git a/fixtures/regressions/enum-without-i32-helpers/Cargo.toml b/fixtures/regressions/enum-without-i32-helpers/Cargo.toml index 40ed4432a6..8d93a0945d 100644 --- a/fixtures/regressions/enum-without-i32-helpers/Cargo.toml +++ b/fixtures/regressions/enum-without-i32-helpers/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "i356-enum-without-int-helpers" edition = "2018" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" publish = false diff --git a/fixtures/regressions/kotlin-experimental-unsigned-types/Cargo.toml b/fixtures/regressions/kotlin-experimental-unsigned-types/Cargo.toml index 22c5b8c667..beec92a641 100644 --- a/fixtures/regressions/kotlin-experimental-unsigned-types/Cargo.toml +++ b/fixtures/regressions/kotlin-experimental-unsigned-types/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "kotlin-experimental-unsigned-types" edition = "2018" -version = "0.8.0" +version = "0.13.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" publish = false diff --git a/fixtures/uniffi-fixture-time/Cargo.toml b/fixtures/uniffi-fixture-time/Cargo.toml index ac39f21b05..e8c30f0ca4 100644 --- a/fixtures/uniffi-fixture-time/Cargo.toml +++ b/fixtures/uniffi-fixture-time/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "uniffi-fixture-time" edition = "2018" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" publish = false diff --git a/uniffi/Cargo.toml b/uniffi/Cargo.toml index ea38627194..22886feb56 100644 --- a/uniffi/Cargo.toml +++ b/uniffi/Cargo.toml @@ -4,7 +4,7 @@ description = "a multi-language bindings generator for rust (runtime support cod documentation = "https://mozilla.github.io/uniffi-rs" homepage = "https://mozilla.github.io/uniffi-rs" repository = "https://github.com/mozilla/uniffi-rs" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" edition = "2018" @@ -19,7 +19,7 @@ log = "0.4" # Regular dependencies cargo_metadata = "0.13" paste = "1.0" -uniffi_bindgen = { path = "../uniffi_bindgen", optional = true, version = "= 0.12.0"} +uniffi_bindgen = { path = "../uniffi_bindgen", optional = true, version = "= 0.13.0"} static_assertions = "1.1.0" [features] diff --git a/uniffi_bindgen/Cargo.toml b/uniffi_bindgen/Cargo.toml index 62c4925bf9..01955842c9 100644 --- a/uniffi_bindgen/Cargo.toml +++ b/uniffi_bindgen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uniffi_bindgen" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] description = "a multi-language bindings generator for rust (codegen and cli tooling)" documentation = "https://mozilla.github.io/uniffi-rs" diff --git a/uniffi_build/Cargo.toml b/uniffi_build/Cargo.toml index a3d6e74399..67414aadab 100644 --- a/uniffi_build/Cargo.toml +++ b/uniffi_build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uniffi_build" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] description = "a multi-language bindings generator for rust (build script helpers)" documentation = "https://mozilla.github.io/uniffi-rs" @@ -12,7 +12,7 @@ keywords = ["ffi", "bindgen"] [dependencies] anyhow = "1" -uniffi_bindgen = { path = "../uniffi_bindgen", optional = true, version = "= 0.12.0"} +uniffi_bindgen = { path = "../uniffi_bindgen", optional = true, version = "= 0.13.0"} [features] default = [] diff --git a/uniffi_macros/Cargo.toml b/uniffi_macros/Cargo.toml index a01739ca81..12c28e58ef 100644 --- a/uniffi_macros/Cargo.toml +++ b/uniffi_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uniffi_macros" -version = "0.12.0" +version = "0.13.0" authors = ["Firefox Sync Team "] description = "a multi-language bindings generator for rust (convenience macros)" documentation = "https://mozilla.github.io/uniffi-rs"