diff --git a/Cargo.lock b/Cargo.lock index 198703b950..64e41b632f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1630,6 +1630,7 @@ dependencies = [ "once_cell", "thiserror", "uniffi", + "uniffi_meta", ] [[package]] diff --git a/examples/arithmetic/Cargo.toml b/examples/arithmetic/Cargo.toml index b497b8cccc..aa58541cb8 100644 --- a/examples/arithmetic/Cargo.toml +++ b/examples/arithmetic/Cargo.toml @@ -15,7 +15,8 @@ uniffi = { workspace = true } thiserror = "1.0" [build-dependencies] -uniffi = { workspace = true, features = ["build"] } +# Add the "scaffolding-ffi-buffer-fns" feature to make sure things can build correctly +uniffi = { workspace = true, features = ["build", "scaffolding-ffi-buffer-fns"] } [dev-dependencies] uniffi = { workspace = true, features = ["bindgen-tests"] } diff --git a/fixtures/callbacks/Cargo.toml b/fixtures/callbacks/Cargo.toml index 52a93de814..82990bbe54 100644 --- a/fixtures/callbacks/Cargo.toml +++ b/fixtures/callbacks/Cargo.toml @@ -11,7 +11,8 @@ crate-type = ["lib", "cdylib"] name = "uniffi_fixture_callbacks" [dependencies] -uniffi = { workspace = true } +# Add the "scaffolding-ffi-buffer-fns" feature to make sure things can build correctly +uniffi = { workspace = true, features=["scaffolding-ffi-buffer-fns"] } thiserror = "1.0" [build-dependencies] diff --git a/fixtures/coverall/Cargo.toml b/fixtures/coverall/Cargo.toml index e0c7e61818..00f071922a 100644 --- a/fixtures/coverall/Cargo.toml +++ b/fixtures/coverall/Cargo.toml @@ -11,7 +11,8 @@ crate-type = ["lib", "cdylib"] name = "uniffi_coverall" [dependencies] -uniffi = { workspace = true } +# Add the "scaffolding-ffi-buffer-fns" feature to make sure things can build correctly +uniffi = { workspace = true, features=["scaffolding-ffi-buffer-fns"]} once_cell = "1.12" thiserror = "1.0" @@ -20,3 +21,4 @@ uniffi = { workspace = true, features = ["build"] } [dev-dependencies] uniffi = { workspace = true, features = ["bindgen-tests"] } +uniffi_meta = { path = "../../uniffi_meta/" } diff --git a/fixtures/coverall/src/coverall.udl b/fixtures/coverall/src/coverall.udl index ff38369b69..ce8d9fdb79 100644 --- a/fixtures/coverall/src/coverall.udl +++ b/fixtures/coverall/src/coverall.udl @@ -30,8 +30,12 @@ namespace coverall { void try_input_return_only_dict(ReturnOnlyDict d); + [Throws=ComplexError] + f32 divide_by_text(f32 value, string value_as_text); + Getters test_round_trip_through_rust(Getters getters); void test_round_trip_through_foreign(Getters getters); + }; dictionary SimpleDict { diff --git a/fixtures/coverall/src/ffi_buffer_scaffolding_test.rs b/fixtures/coverall/src/ffi_buffer_scaffolding_test.rs new file mode 100644 index 0000000000..5a037f480c --- /dev/null +++ b/fixtures/coverall/src/ffi_buffer_scaffolding_test.rs @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::ComplexError; +use uniffi::{ + ffi_buffer_size, FfiBufferElement, FfiSerialize, LiftReturn, Lower, RustBuffer, RustCallStatus, +}; + +/// Test the FFI-buffer version of our scaffolding functions by manually calling one. +/// +/// We use the `get_complex_error` version, since it's one of more complex cases: +/// - It inputs multiple arguments +/// - The Rust function returns a Result<> type, which means the ffi-buffer scaffolding function +/// needs to deserialize the `RustCallStatus` out pointer, pass it to the regular scaffolding +/// function, and everything needs to be put back together in the end. +#[test] +fn test_ffi_buffer_scaffolding() { + // Call the ffi-buffer version of the scaffolding function for `divide_by_text` + // + // This simulates the work that happens on the foreign side. + fn call_ffi_buffer_divide_by_text( + value: f32, + value_as_text: String, + ) -> Result { + // Get buffers ready to store the arguments/return values + let mut args_ffi_buffer = [FfiBufferElement::default(); ffi_buffer_size!(f32, RustBuffer)]; + let mut return_ffi_buffer = + [FfiBufferElement::default(); ffi_buffer_size!(f32, RustCallStatus)]; + // Lower the arguments + let value_lowered = >::lower(value); + let value_as_text_lowered = >::lower(value_as_text); + // Serialize the lowered arguments plus the RustCallStatus into the argument buffer + let args_cursor = &mut args_ffi_buffer.as_mut_slice(); + ::write(args_cursor, value_lowered); + ::write(args_cursor, value_as_text_lowered); + // Call the ffi-buffer version of the scaffolding function + unsafe { + crate::uniffi_ffibuffer_uniffi_coverall_fn_func_divide_by_text( + args_ffi_buffer.as_mut_ptr(), + return_ffi_buffer.as_mut_ptr(), + ); + } + // Deserialize the return and the RustCallStatus from the return buffer + let return_cursor = &mut return_ffi_buffer.as_slice(); + let return_value = ::read(return_cursor); + let rust_call_status = ::read(return_cursor); + // Lift the return from the deserialized value. + as LiftReturn>::lift_foreign_return( + return_value, + rust_call_status, + ) + } + + assert_eq!(call_ffi_buffer_divide_by_text(1.0, "2".into()), Ok(0.5)); + assert_eq!(call_ffi_buffer_divide_by_text(5.0, "2.5".into()), Ok(2.0)); + assert_eq!( + call_ffi_buffer_divide_by_text(1.0, "two".into()), + Err(ComplexError::UnknownError) + ); +} diff --git a/fixtures/coverall/src/lib.rs b/fixtures/coverall/src/lib.rs index 89428ab20a..76d4639fbd 100644 --- a/fixtures/coverall/src/lib.rs +++ b/fixtures/coverall/src/lib.rs @@ -9,6 +9,9 @@ use std::time::SystemTime; use once_cell::sync::Lazy; +#[cfg(test)] +mod ffi_buffer_scaffolding_test; + mod traits; pub use traits::{ ancestor_names, get_string_util_traits, get_traits, make_rust_getters, test_getters, @@ -232,6 +235,13 @@ fn try_input_return_only_dict(_d: ReturnOnlyDict) { // FIXME: should be a compile-time error rather than a runtime error (#1850) } +pub fn divide_by_text(value: f32, value_as_text: String) -> Result { + match value_as_text.parse::() { + Ok(divisor) if divisor != 0.0 => Ok(value / divisor), + _ => Err(ComplexError::UnknownError), + } +} + #[derive(Debug, Clone)] pub struct DictWithDefaults { name: String, diff --git a/fixtures/ext-types/lib/Cargo.toml b/fixtures/ext-types/lib/Cargo.toml index 812f714c59..28343804a2 100644 --- a/fixtures/ext-types/lib/Cargo.toml +++ b/fixtures/ext-types/lib/Cargo.toml @@ -22,7 +22,8 @@ name = "uniffi_ext_types_lib" [dependencies] anyhow = "1" bytes = "1.3" -uniffi = { workspace = true } +# Add the "scaffolding-ffi-buffer-fns" feature to make sure things can build correctly +uniffi = { workspace = true, features = ["scaffolding-ffi-buffer-fns"] } uniffi-fixture-ext-types-external-crate = {path = "../external-crate"} uniffi-fixture-ext-types-lib-one = {path = "../uniffi-one"} diff --git a/fixtures/futures/Cargo.toml b/fixtures/futures/Cargo.toml index be22f29041..a1fd38e430 100644 --- a/fixtures/futures/Cargo.toml +++ b/fixtures/futures/Cargo.toml @@ -15,7 +15,8 @@ name = "uniffi-fixtures-futures" path = "src/bin.rs" [dependencies] -uniffi = { workspace = true, features = ["tokio", "cli"] } +# Add the "scaffolding-ffi-buffer-fns" feature to make sure things can build correctly +uniffi = { workspace = true, features = ["tokio", "cli", "scaffolding-ffi-buffer-fns"] } async-trait = "0.1" futures = "0.3" thiserror = "1.0" @@ -23,7 +24,7 @@ tokio = { version = "1.24.1", features = ["time", "sync"] } once_cell = "1.18.0" [build-dependencies] -uniffi = { workspace = true, features = ["build"] } +uniffi = { workspace = true, features = ["build", "scaffolding-ffi-buffer-fns"] } [dev-dependencies] uniffi = { workspace = true, features = ["bindgen-tests"] } diff --git a/fixtures/proc-macro/Cargo.toml b/fixtures/proc-macro/Cargo.toml index 23704c4ae0..c09fd77701 100644 --- a/fixtures/proc-macro/Cargo.toml +++ b/fixtures/proc-macro/Cargo.toml @@ -11,12 +11,13 @@ name = "uniffi_proc_macro" crate-type = ["lib", "cdylib"] [dependencies] -uniffi = { workspace = true } +# Add the "scaffolding-ffi-buffer-fns" feature to make sure things can build correctly +uniffi = { workspace = true, features = ["scaffolding-ffi-buffer-fns"] } thiserror = "1.0" lazy_static = "1.4" [build-dependencies] -uniffi = { workspace = true, features = ["build"] } +uniffi = { workspace = true, features = ["build", "scaffolding-ffi-buffer-fns"] } [dev-dependencies] uniffi = { workspace = true, features = ["bindgen-tests"] } diff --git a/uniffi/Cargo.toml b/uniffi/Cargo.toml index 7903694c59..bca109d746 100644 --- a/uniffi/Cargo.toml +++ b/uniffi/Cargo.toml @@ -43,3 +43,6 @@ bindgen-tests = [ "dep:uniffi_bindgen" ] # Enable support for Tokio's futures. # This must still be opted into on a per-function basis using `#[uniffi::export(async_runtime = "tokio")]`. tokio = ["uniffi_core/tokio"] +# Generate extra scaffolding functions that use FfiBuffer to pass arguments and return values +# This is needed for the gecko-js bindings. +scaffolding-ffi-buffer-fns = ["uniffi_macros/scaffolding-ffi-buffer-fns"] diff --git a/uniffi_bindgen/src/interface/ffi.rs b/uniffi_bindgen/src/interface/ffi.rs index b27cb78477..a1dc29713a 100644 --- a/uniffi_bindgen/src/interface/ffi.rs +++ b/uniffi_bindgen/src/interface/ffi.rs @@ -218,6 +218,12 @@ impl FfiFunction { &self.name } + /// Name of the FFI buffer version of this function that's generated when the + /// `scaffolding-ffi-buffer-fns` feature is enabled. + pub fn ffi_buffer_fn_name(&self) -> String { + uniffi_meta::ffi_buffer_symbol_name(&self.name) + } + pub fn is_async(&self) -> bool { self.is_async } diff --git a/uniffi_core/src/ffi/ffiserialize.rs b/uniffi_core/src/ffi/ffiserialize.rs new file mode 100644 index 0000000000..5932217183 --- /dev/null +++ b/uniffi_core/src/ffi/ffiserialize.rs @@ -0,0 +1,302 @@ +/* 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::ptr::NonNull; + +/// FFIBuffer element +/// +/// This is the union of all possible primitive FFI types. +/// Composite FFI types like `RustBuffer` and `RustCallStatus` are stored using multiple elements. +#[repr(C)] +#[derive(Clone, Copy)] +pub union FfiBufferElement { + pub u8: u8, + pub i8: i8, + pub u16: u16, + pub i16: i16, + pub u32: u32, + pub i32: i32, + pub u64: u64, + pub i64: i64, + pub float: std::ffi::c_float, + pub double: std::ffi::c_double, + pub ptr: *const std::ffi::c_void, +} + +impl Default for FfiBufferElement { + fn default() -> Self { + Self { u64: 0 } + } +} + +/// Serialize a FFI value to a buffer +/// +/// This trait allows FFI types to be read from/written to FFIBufferElement slices. +/// It's similar, to the [crate::Lift::read] and [crate::Lower::write] methods, but implemented on the FFI types rather than Rust types. +/// It's useful to compare the two: +/// +/// - [crate::Lift] and [crate::Lower] are implemented on Rust types like String and user-defined records. +/// - [FfiSerialize] is implemented on the FFI types like RustBuffer, RustCallStatus, and vtable structs. +/// - All 3 traits are implemented for simple cases where the FFI type and Rust type are the same, for example numeric types. +/// - [FfiSerialize] uses FFIBuffer elements rather than u8 elements. Using a union eliminates the need to cast values and creates better alignment. +/// - [FfiSerialize] uses a constant size to store each type. +/// +/// [FfiSerialize] is used to generate alternate forms of the scaffolding functions that simplify work needed to implement the bindings on the other side. +/// This is currently only used in the gecko-js bindings for Firefox, but could maybe be useful for other external bindings or even some of the builtin bindings like Python/Kotlin. +/// +/// The FFI-buffer version of the scaffolding functions: +/// - Input two pointers to ffi buffers, one to read arguments from and one to write the return value to. +/// - Rather than inputting an out pointer for `RustCallStatus` it's written to the return buffer after the normal return value. +/// +pub trait FfiSerialize: Sized { + /// Number of elements required to store this FFI type + const SIZE: usize; + + /// Get a value from a ffi buffer + /// + /// Note: `buf` should be thought of as `&[FFIBufferElement; Self::SIZE]`, but it can't be spelled out that way + /// since Rust doesn't support that usage of const generics yet. + fn get(buf: &[FfiBufferElement]) -> Self; + + /// Put a value to a ffi buffer + /// + /// Note: `buf` should be thought of as `&[FFIBufferElement; Self::SIZE]`, but it can't be spelled out that way + /// since Rust doesn't support that usage of const generics yet. + fn put(buf: &mut [FfiBufferElement], value: Self); + + /// Read a value from a ffi buffer ref and advance it + /// + /// buf must have a length of at least `Self::Size` + fn read(buf: &mut &[FfiBufferElement]) -> Self { + let value = Self::get(buf); + *buf = &buf[Self::SIZE..]; + value + } + + /// Write a value to a ffi buffer ref and advance it + /// + /// buf must have a length of at least `Self::Size` + fn write(buf: &mut &mut [FfiBufferElement], value: Self) { + Self::put(buf, value); + // Lifetime dance taken from `bytes::BufMut` + let (_, new_buf) = core::mem::take(buf).split_at_mut(Self::SIZE); + *buf = new_buf; + } +} + +/// Get the FFI buffer size for list of types +#[macro_export] +macro_rules! ffi_buffer_size { + ($($T:ty),* $(,)?) => { + ( + 0 + $( + + <$T as $crate::FfiSerialize>::SIZE + )* + ) + } +} + +macro_rules! define_ffi_serialize_simple_cases { + ($(($name: ident, $T:ty)),* $(,)?) => { + $( + impl FfiSerialize for $T { + const SIZE: usize = 1; + + fn get(buf: &[FfiBufferElement]) -> Self { + // Safety: the foreign bindings are responsible for sending us the correct data. + unsafe { buf[0].$name } + } + + fn put(buf: &mut[FfiBufferElement], value: Self) { + buf[0].$name = value + } + } + )* + }; +} + +define_ffi_serialize_simple_cases! { + (i8, i8), + (u8, u8), + (i16, i16), + (u16, u16), + (i32, i32), + (u32, u32), + (i64, i64), + (u64, u64), + (ptr, *const std::ffi::c_void), +} + +impl FfiSerialize for f32 { + const SIZE: usize = 1; + + fn get(buf: &[FfiBufferElement]) -> Self { + // Safety: the foreign bindings are responsible for sending us the correct data. + unsafe { buf[0].float as Self } + } + + fn put(buf: &mut [FfiBufferElement], value: Self) { + // Use a cast since it's theoretically possible for float to not be f32 on some systems. + buf[0].float = value as std::ffi::c_float; + } +} + +impl FfiSerialize for f64 { + const SIZE: usize = 1; + + fn get(buf: &[FfiBufferElement]) -> Self { + // Safety: the foreign bindings are responsible for sending us the correct data. + unsafe { buf[0].double as Self } + } + + fn put(buf: &mut [FfiBufferElement], value: Self) { + // Use a cast since it's theoretically possible for double to not be f64 on some systems. + buf[0].double = value as std::ffi::c_double; + } +} + +impl FfiSerialize for bool { + const SIZE: usize = 1; + + fn get(buf: &[FfiBufferElement]) -> Self { + // Safety: the foreign bindings are responsible for sending us the correct data. + unsafe { buf[0].i8 == 1 } + } + + fn put(buf: &mut [FfiBufferElement], value: Self) { + buf[0].i8 = if value { 1 } else { 0 } + } +} + +impl FfiSerialize for () { + const SIZE: usize = 0; + + fn get(_buf: &[FfiBufferElement]) -> Self {} + + fn put(_buf: &mut [FfiBufferElement], _value: Self) {} +} + +impl FfiSerialize for NonNull { + const SIZE: usize = 1; + + fn get(buf: &[FfiBufferElement]) -> Self { + // Safety: this relies on the foreign code passing us valid pointers + unsafe { Self::new_unchecked(buf[0].ptr as *mut T) } + } + + fn put(buf: &mut [FfiBufferElement], value: Self) { + buf[0].ptr = value.as_ptr() as *const std::ffi::c_void + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{Handle, RustBuffer, RustCallStatus, RustCallStatusCode}; + + #[test] + fn test_ffi_buffer_size() { + assert_eq!(ffi_buffer_size!(u8), 1); + assert_eq!(ffi_buffer_size!(i8), 1); + assert_eq!(ffi_buffer_size!(u16), 1); + assert_eq!(ffi_buffer_size!(i16), 1); + assert_eq!(ffi_buffer_size!(u32), 1); + assert_eq!(ffi_buffer_size!(i32), 1); + assert_eq!(ffi_buffer_size!(u64), 1); + assert_eq!(ffi_buffer_size!(i64), 1); + assert_eq!(ffi_buffer_size!(f32), 1); + assert_eq!(ffi_buffer_size!(f64), 1); + assert_eq!(ffi_buffer_size!(bool), 1); + assert_eq!(ffi_buffer_size!(*const std::ffi::c_void), 1); + assert_eq!(ffi_buffer_size!(RustBuffer), 3); + assert_eq!(ffi_buffer_size!(RustCallStatus), 4); + assert_eq!(ffi_buffer_size!(Handle), 1); + assert_eq!(ffi_buffer_size!(()), 0); + + assert_eq!(ffi_buffer_size!(u8, f32, bool, Handle, (), RustBuffer), 7); + } + + #[test] + fn test_ffi_serialize() { + let mut some_data = vec![1, 2, 3]; + let void_ptr = some_data.as_mut_ptr() as *const std::ffi::c_void; + let rust_buffer = unsafe { RustBuffer::from_raw_parts(some_data.as_mut_ptr(), 2, 3) }; + let orig_rust_buffer_data = ( + rust_buffer.data_pointer(), + rust_buffer.len(), + rust_buffer.capacity(), + ); + let handle = Handle::from_raw(101).unwrap(); + let rust_call_status = RustCallStatus::new(); + let rust_call_status_error_buf = unsafe { rust_call_status.error_buf.assume_init_ref() }; + let orig_rust_call_status_buffer_data = ( + rust_call_status_error_buf.data_pointer(), + rust_call_status_error_buf.len(), + rust_call_status_error_buf.capacity(), + ); + let mut buf = [FfiBufferElement::default(); 21]; + let mut buf_writer = buf.as_mut_slice(); + ::write(&mut buf_writer, 0); + ::write(&mut buf_writer, 1); + ::write(&mut buf_writer, 2); + ::write(&mut buf_writer, 3); + ::write(&mut buf_writer, 4); + ::write(&mut buf_writer, 5); + ::write(&mut buf_writer, 6); + ::write(&mut buf_writer, 7); + ::write(&mut buf_writer, 0.1); + ::write(&mut buf_writer, 0.2); + ::write(&mut buf_writer, true); + <*const std::ffi::c_void as FfiSerialize>::write(&mut buf_writer, void_ptr); + ::write(&mut buf_writer, rust_buffer); + ::write(&mut buf_writer, rust_call_status); + ::write(&mut buf_writer, handle); + #[allow(clippy::needless_borrows_for_generic_args)] + <() as FfiSerialize>::write(&mut buf_writer, ()); + + let mut buf_reader = buf.as_slice(); + assert_eq!(::read(&mut buf_reader), 0); + assert_eq!(::read(&mut buf_reader), 1); + assert_eq!(::read(&mut buf_reader), 2); + assert_eq!(::read(&mut buf_reader), 3); + assert_eq!(::read(&mut buf_reader), 4); + assert_eq!(::read(&mut buf_reader), 5); + assert_eq!(::read(&mut buf_reader), 6); + assert_eq!(::read(&mut buf_reader), 7); + assert_eq!(::read(&mut buf_reader), 0.1); + assert_eq!(::read(&mut buf_reader), 0.2); + assert!(::read(&mut buf_reader)); + assert_eq!( + <*const std::ffi::c_void as FfiSerialize>::read(&mut buf_reader), + void_ptr + ); + let rust_buffer2 = ::read(&mut buf_reader); + assert_eq!( + ( + rust_buffer2.data_pointer(), + rust_buffer2.len(), + rust_buffer2.capacity() + ), + orig_rust_buffer_data, + ); + + let rust_call_status2 = ::read(&mut buf_reader); + assert_eq!(rust_call_status2.code, RustCallStatusCode::Success); + + let rust_call_status2_error_buf = unsafe { rust_call_status2.error_buf.assume_init() }; + assert_eq!( + ( + rust_call_status2_error_buf.data_pointer(), + rust_call_status2_error_buf.len(), + rust_call_status2_error_buf.capacity(), + ), + orig_rust_call_status_buffer_data + ); + assert_eq!(::read(&mut buf_reader), handle); + // Ensure that `read` with a unit struct doesn't panic. No need to assert anything, since + // the return type is (). + <() as FfiSerialize>::read(&mut buf_reader); + } +} diff --git a/uniffi_core/src/ffi/handle.rs b/uniffi_core/src/ffi/handle.rs index 8ee2f46c35..631e7b44b2 100644 --- a/uniffi_core/src/ffi/handle.rs +++ b/uniffi_core/src/ffi/handle.rs @@ -2,6 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use crate::{FfiBufferElement, FfiSerialize}; + /// Object handle /// /// Handles opaque `u64` values used to pass objects across the FFI, both for objects implemented in @@ -44,3 +46,16 @@ impl Handle { self.0 } } + +impl FfiSerialize for Handle { + const SIZE: usize = 1; + + fn get(buf: &[FfiBufferElement]) -> Self { + // Safety: the foreign bindings are responsible for sending us the correct data. + unsafe { Handle(buf[0].u64) } + } + + fn put(buf: &mut [FfiBufferElement], value: Self) { + buf[0].u64 = value.0 + } +} diff --git a/uniffi_core/src/ffi/mod.rs b/uniffi_core/src/ffi/mod.rs index acaf2b0d06..fd9951b6a7 100644 --- a/uniffi_core/src/ffi/mod.rs +++ b/uniffi_core/src/ffi/mod.rs @@ -6,6 +6,7 @@ pub mod callbackinterface; pub mod ffidefault; +pub mod ffiserialize; pub mod foreignbytes; pub mod foreigncallbacks; pub mod foreignfuture; @@ -16,6 +17,7 @@ pub mod rustfuture; pub use callbackinterface::*; pub use ffidefault::FfiDefault; +pub use ffiserialize::FfiSerialize; pub use foreignbytes::*; pub use foreigncallbacks::*; pub use foreignfuture::*; diff --git a/uniffi_core/src/ffi/rustbuffer.rs b/uniffi_core/src/ffi/rustbuffer.rs index 8b2972968c..1b6eb35fb0 100644 --- a/uniffi_core/src/ffi/rustbuffer.rs +++ b/uniffi_core/src/ffi/rustbuffer.rs @@ -2,7 +2,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use crate::ffi::{rust_call, ForeignBytes, RustCallStatus}; +use crate::{ + ffi::{rust_call, FfiSerialize, ForeignBytes, RustCallStatus}, + FfiBufferElement, +}; /// Support for passing an allocated-by-Rust buffer of bytes over the FFI. /// @@ -107,6 +110,12 @@ impl RustBuffer { .expect("buffer length negative or overflowed") } + pub fn capacity(&self) -> usize { + self.capacity + .try_into() + .expect("buffer length negative or overflowed") + } + /// Get a pointer to the data pub fn data_pointer(&self) -> *const u8 { self.data @@ -194,6 +203,22 @@ impl Default for RustBuffer { } } +impl FfiSerialize for RustBuffer { + const SIZE: usize = 3; + + fn get(buf: &[FfiBufferElement]) -> Self { + // Safety: the foreign bindings are responsible for sending us the correct data. + let (capacity, len, data) = unsafe { (buf[0].u64, buf[1].u64, buf[2].ptr as *mut u8) }; + unsafe { crate::RustBuffer::from_raw_parts(data, len, capacity) } + } + + fn put(buf: &mut [FfiBufferElement], value: Self) { + buf[0].u64 = value.capacity; + buf[1].u64 = value.len; + buf[2].ptr = value.data as *const std::ffi::c_void; + } +} + // Functions for the RustBuffer functionality. // // The scaffolding code re-exports these functions, prefixed with the component name and UDL hash diff --git a/uniffi_core/src/ffi/rustcalls.rs b/uniffi_core/src/ffi/rustcalls.rs index 16b0c76f2e..6e589ba51f 100644 --- a/uniffi_core/src/ffi/rustcalls.rs +++ b/uniffi_core/src/ffi/rustcalls.rs @@ -11,7 +11,7 @@ //! - Adapting the result of `Return::lower_return()` into either a return value or an //! exception -use crate::{FfiDefault, Lower, RustBuffer, UniFfiTag}; +use crate::{FfiBufferElement, FfiDefault, FfiSerialize, Lower, RustBuffer, UniFfiTag}; use std::mem::MaybeUninit; use std::panic; @@ -87,6 +87,26 @@ impl Default for RustCallStatus { } } +impl FfiSerialize for RustCallStatus { + const SIZE: usize = 4; + + fn get(buf: &[FfiBufferElement]) -> Self { + // Safety: the foreign bindings are responsible for sending us the correct data. + let code = unsafe { buf[0].i8 }; + Self { + code: RustCallStatusCode::try_from(code).unwrap_or(RustCallStatusCode::UnexpectedError), + error_buf: MaybeUninit::new(RustBuffer::get(&buf[1..])), + } + } + + fn put(buf: &mut [FfiBufferElement], value: Self) { + buf[0].i8 = value.code as i8; + // Safety: This is okay even if the error buf is not initialized. It just means we'll be + // copying the garbage data. + unsafe { RustBuffer::put(&mut buf[1..], value.error_buf.assume_init()) } + } +} + /// Result of a FFI call to a Rust function #[repr(i8)] #[derive(Debug, PartialEq, Eq)] @@ -106,6 +126,20 @@ pub enum RustCallStatusCode { Cancelled = 3, } +impl TryFrom for RustCallStatusCode { + type Error = i8; + + fn try_from(value: i8) -> Result { + match value { + 0 => Ok(Self::Success), + 1 => Ok(Self::Error), + 2 => Ok(Self::UnexpectedError), + 3 => Ok(Self::Cancelled), + n => Err(n), + } + } +} + /// Handle a scaffolding calls /// /// `callback` is responsible for making the actual Rust call and returning a special result type: diff --git a/uniffi_core/src/lib.rs b/uniffi_core/src/lib.rs index 1f3a2403f8..fe89585c43 100644 --- a/uniffi_core/src/lib.rs +++ b/uniffi_core/src/lib.rs @@ -48,6 +48,7 @@ pub use ffi_converter_traits::{ ConvertError, FfiConverter, FfiConverterArc, HandleAlloc, Lift, LiftRef, LiftReturn, Lower, LowerReturn, }; +pub use ffiserialize::FfiBufferElement; pub use metadata::*; // Re-export the libs that we use in the generated code, diff --git a/uniffi_macros/Cargo.toml b/uniffi_macros/Cargo.toml index b301e37559..dddd988748 100644 --- a/uniffi_macros/Cargo.toml +++ b/uniffi_macros/Cargo.toml @@ -31,6 +31,8 @@ uniffi_meta = { path = "../uniffi_meta", version = "=0.27.1" } default = [] # Enable the generate_and_include_scaffolding! macro trybuild = [ "dep:uniffi_build" ] +# Generate extra scaffolding functions that use FfiBuffer to pass arguments and return values +scaffolding-ffi-buffer-fns = [] # Enable extra features that require a nightly compiler: # * Add the full module path of exported items to FFI metadata instead of just the crate name. # This may be used by language backends to generate nested module structures in the future. diff --git a/uniffi_macros/src/export/scaffolding.rs b/uniffi_macros/src/export/scaffolding.rs index 58b8d83a35..e0e2d75c6d 100644 --- a/uniffi_macros/src/export/scaffolding.rs +++ b/uniffi_macros/src/export/scaffolding.rs @@ -244,6 +244,12 @@ pub(super) fn gen_ffi_function( let return_impl = &sig.lower_return_impl(); Ok(if !sig.is_async { + let scaffolding_fn_ffi_buffer_version = ffi_buffer_scaffolding_fn( + &ffi_ident, + "e! { #return_impl::ReturnType}, + ¶m_types, + true, + ); quote! { #[doc(hidden)] #[no_mangle] @@ -267,12 +273,16 @@ pub(super) fn gen_ffi_function( ) }) } + + #scaffolding_fn_ffi_buffer_version } } else { let mut future_expr = rust_fn_call; if matches!(ar, Some(AsyncRuntime::Tokio(_))) { future_expr = quote! { ::uniffi::deps::async_compat::Compat::new(#future_expr) } } + let scaffolding_fn_ffi_buffer_version = + ffi_buffer_scaffolding_fn(&ffi_ident, "e! { ::uniffi::Handle}, ¶m_types, false); quote! { #[doc(hidden)] @@ -300,6 +310,71 @@ pub(super) fn gen_ffi_function( }, } } + + #scaffolding_fn_ffi_buffer_version } }) } + +#[cfg(feature = "scaffolding-ffi-buffer-fns")] +fn ffi_buffer_scaffolding_fn( + fn_ident: &Ident, + return_type: &TokenStream, + param_types: &[TokenStream], + has_rust_call_status: bool, +) -> TokenStream { + let fn_name = fn_ident.to_string(); + let ffi_buffer_fn_name = uniffi_meta::ffi_buffer_symbol_name(&fn_name); + let ident = Ident::new(&ffi_buffer_fn_name, proc_macro2::Span::call_site()); + let type_list: Vec<_> = param_types.iter().map(|ty| quote! { #ty }).collect(); + if has_rust_call_status { + quote! { + #[doc(hidden)] + #[no_mangle] + pub unsafe extern "C" fn #ident( + arg_ptr: *mut ::uniffi::FfiBufferElement, + return_ptr: *mut ::uniffi::FfiBufferElement, + ) { + let mut arg_buf = unsafe { ::std::slice::from_raw_parts(arg_ptr, ::uniffi::ffi_buffer_size!(#(#type_list),*)) }; + let mut return_buf = unsafe { ::std::slice::from_raw_parts_mut(return_ptr, ::uniffi::ffi_buffer_size!(#return_type, ::uniffi::RustCallStatus)) }; + let mut out_status = ::uniffi::RustCallStatus::default(); + + let return_value = #fn_ident( + #( + <#type_list as ::uniffi::FfiSerialize>::read(&mut arg_buf), + )* + &mut out_status, + ); + <#return_type as ::uniffi::FfiSerialize>::write(&mut return_buf, return_value); + <::uniffi::RustCallStatus as ::uniffi::FfiSerialize>::write(&mut return_buf, out_status); + } + } + } else { + quote! { + #[doc(hidden)] + #[no_mangle] + pub unsafe extern "C" fn #ident( + arg_ptr: *mut ::uniffi::FfiBufferElement, + return_ptr: *mut ::uniffi::FfiBufferElement, + ) { + let mut arg_buf = unsafe { ::std::slice::from_raw_parts(arg_ptr, ::uniffi::ffi_buffer_size!(#(#type_list),*)) }; + let mut return_buf = unsafe { ::std::slice::from_raw_parts_mut(return_ptr, ::uniffi::ffi_buffer_size!(#return_type)) }; + + let return_value = #fn_ident(#( + <#type_list as ::uniffi::FfiSerialize>::read(&mut arg_buf), + )*); + <#return_type as ::uniffi::FfiSerialize>::put(&mut return_buf, return_value); + } + } + } +} + +#[cfg(not(feature = "scaffolding-ffi-buffer-fns"))] +fn ffi_buffer_scaffolding_fn( + _fn_ident: &Ident, + _return_type: &TokenStream, + _param_types: &[TokenStream], + _add_rust_call_status: bool, +) -> TokenStream { + quote! {} +} diff --git a/uniffi_macros/src/util.rs b/uniffi_macros/src/util.rs index 97faad9c4d..cf4727e2f1 100644 --- a/uniffi_macros/src/util.rs +++ b/uniffi_macros/src/util.rs @@ -110,7 +110,6 @@ pub fn create_metadata_items( let const_ident = format_ident!("UNIFFI_META_CONST_{crate_name_upper}_{kind_upper}_{name_upper}"); let static_ident = format_ident!("UNIFFI_META_{crate_name_upper}_{kind_upper}_{name_upper}"); - let checksum_fn = checksum_fn_name.map(|name| { let ident = Ident::new(&name, Span::call_site()); quote! { diff --git a/uniffi_meta/src/ffi_names.rs b/uniffi_meta/src/ffi_names.rs index 5c931a09e3..56bff2c231 100644 --- a/uniffi_meta/src/ffi_names.rs +++ b/uniffi_meta/src/ffi_names.rs @@ -73,3 +73,12 @@ pub fn method_checksum_symbol_name(namespace: &str, object_name: &str, name: &st let name = name.to_ascii_lowercase(); format!("uniffi_{namespace}_checksum_method_{object_name}_{name}") } + +/// Get the symbol name for a FFI-buffer version of a function +pub fn ffi_buffer_symbol_name(fn_name: &str) -> String { + match fn_name.strip_prefix("uniffi_") { + Some(rest) => format!("uniffi_ffibuffer_{rest}"), + // this should never happen, but if it does let's try our best to prefix things properl. + None => format!("uniffi_ffibuffer_{fn_name}"), + } +}