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..074d2bbcaf 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" diff --git a/fixtures/coverall/src/coverall.udl b/fixtures/coverall/src/coverall.udl index a51f8f36aa..3e97972c3d 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..fd82cab26e --- /dev/null +++ b/fixtures/coverall/src/ffi_buffer_scaffolding_test.rs @@ -0,0 +1,55 @@ +/* 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::{uniffi_ffibuffer_uniffi_coverall_fn_func_divide_by_text, ComplexError}; +use uniffi::{ffi_buffer_size, 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() { + 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) + ); +} + +// 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 = [0_u64; ffi_buffer_size!(RustBuffer, RustCallStatus)]; + let mut return_ffi_buffer = [0_u64; ffi_buffer_size!(RustBuffer)]; + // Lower the arguments + let value_lowered = >::lower(value); + let value_as_text_lowered = >::lower(value_as_text); + // Create a RustCallStatus that the scaffolding function will write to + let mut rust_call_status = RustCallStatus::default(); + // 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); + <&mut RustCallStatus as FfiSerialize>::write(args_cursor, &mut rust_call_status); + // Call the ffi-buffer version of the scaffolding function + unsafe { + uniffi_ffibuffer_uniffi_coverall_fn_func_divide_by_text( + args_ffi_buffer.as_mut_ptr(), + return_ffi_buffer.as_mut_ptr(), + ); + } + // Deserialize the return from the return buffer + let return_value = ::get(&return_ffi_buffer); + // Lift the return from the deserialized value. + as LiftReturn>::lift_foreign_return( + return_value, + rust_call_status, + ) +} diff --git a/fixtures/coverall/src/lib.rs b/fixtures/coverall/src/lib.rs index d7ba238568..0270852df1 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 8155bc62fa..213c0411a5 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_core/src/ffi/ffiserialize.rs b/uniffi_core/src/ffi/ffiserialize.rs new file mode 100644 index 0000000000..f03f4ee87d --- /dev/null +++ b/uniffi_core/src/ffi/ffiserialize.rs @@ -0,0 +1,293 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::ptr::NonNull; + +/// Serialize a FFI value to a buffer +/// +/// This trait allows FFI types to be read/written from buffers. +/// 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 u64 elements rather than u8 elements. This creates better alignment of the arguments at the cost of extra size. +/// - [FfiSerialize] uses a constant size to store each type. +/// +/// [FfiSerialize] is used to generate alternate forms of the scaffolding functions that input two pointers to `u64` buffers. +/// One is used to read the arguments from, one to write the return value to. +/// +/// This is currently only used in the gecko-js bindings for Firefox, but could maybe be useful for other external bindings. +pub trait FfiSerialize: Sized { + /// Number of u64 items required to store this FFI type + const SIZE: usize; + + /// Get a value from a u64 buffer + /// + /// Note: buf should be thought of as &[u64: 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: &[u64]) -> Self; + + /// Put a value to a u64 buffer + /// + /// Note: buf should be thought of as &[u64: 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 [u64], value: Self); + + /// Read a value from a u64 buffer ref and advance it + fn read(buf: &mut &[u64]) -> Self { + let value = Self::get(buf); + *buf = &buf[Self::SIZE..]; + value + } + + /// Write a value to a u64 buffer ref and advance it + fn write(buf: &mut &mut [u64], 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 { + ($($T:ty),* $(,)?) => { + $( + impl FfiSerialize for $T { + const SIZE: usize = 1; + + fn get(buf: &[u64]) -> Self { + buf[0] as Self + } + + fn put(buf: &mut[u64], value: Self) { + buf[0] = value as u64 + } + } + )* + }; +} + +define_ffi_serialize_simple_cases! { + i8, + u8, + i16, + u16, + i32, + u32, + i64, + u64, + *const std::ffi::c_void, +} + +impl FfiSerialize for f64 { + const SIZE: usize = 1; + + fn get(buf: &[u64]) -> Self { + f64::from_bits(buf[0]) + } + + fn put(buf: &mut [u64], value: Self) { + buf[0] = f64::to_bits(value) + } +} + +impl FfiSerialize for f32 { + const SIZE: usize = 1; + + fn get(buf: &[u64]) -> Self { + f32::from_bits(buf[0] as u32) + } + + fn put(buf: &mut [u64], value: Self) { + buf[0] = f32::to_bits(value) as u64 + } +} + +impl FfiSerialize for bool { + const SIZE: usize = 1; + + fn get(buf: &[u64]) -> Self { + buf[0] == 1 + } + + fn put(buf: &mut [u64], value: Self) { + buf[0] = value as u64 + } +} + +impl FfiSerialize for () { + const SIZE: usize = 0; + + fn get(_buf: &[u64]) -> Self {} + + fn put(_buf: &mut [u64], _value: Self) {} +} + +impl FfiSerialize for &T { + const SIZE: usize = 1; + + fn get(buf: &[u64]) -> Self { + // Safety: this relies on the foreign code passing us valid pointers + unsafe { &*(buf[0] as *const T) } + } + + fn put(buf: &mut [u64], value: Self) { + buf[0] = value as *const T as u64 + } +} + +impl FfiSerialize for &mut T { + const SIZE: usize = 1; + + fn get(buf: &[u64]) -> Self { + // Safety: this relies on the foreign code passing us valid pointers + unsafe { &mut *(buf[0] as *mut T) } + } + + fn put(buf: &mut [u64], value: Self) { + buf[0] = value as *mut T as u64 + } +} + +impl FfiSerialize for NonNull { + const SIZE: usize = 1; + + fn get(buf: &[u64]) -> Self { + // Safety: this relies on the foreign code passing us valid pointers + unsafe { &mut *(buf[0] as *mut T) }.into() + } + + fn put(buf: &mut [u64], value: Self) { + buf[0] = value.as_ptr() as u64 + } +} + +#[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!(&u8), 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 f64_value = 2.5; + let mut buf = [0_u64; 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_borrow)] + <&f64 as FfiSerialize>::write(&mut buf_writer, &f64_value); + <() 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); + assert_eq!(<&f64 as FfiSerialize>::read(&mut buf_reader), &f64_value); + // 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..3aa85baf08 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::FfiSerialize; + /// Object handle /// /// Handles opaque `u64` values used to pass objects across the FFI, both for objects implemented in @@ -44,3 +46,15 @@ impl Handle { self.0 } } + +impl FfiSerialize for Handle { + const SIZE: usize = 1; + + fn get(buf: &[u64]) -> Self { + Handle(buf[0]) + } + + fn put(buf: &mut [u64], value: Self) { + buf[0] = 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..5fcca98cee 100644 --- a/uniffi_core/src/ffi/rustbuffer.rs +++ b/uniffi_core/src/ffi/rustbuffer.rs @@ -2,7 +2,7 @@ * 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}; /// Support for passing an allocated-by-Rust buffer of bytes over the FFI. /// @@ -107,6 +107,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 +200,20 @@ impl Default for RustBuffer { } } +impl FfiSerialize for RustBuffer { + const SIZE: usize = 3; + + fn get(buf: &[u64]) -> Self { + unsafe { crate::RustBuffer::from_raw_parts(buf[0] as *mut u8, buf[1], buf[2]) } + } + + fn put(buf: &mut [u64], value: Self) { + buf[0] = value.data as u64; + buf[1] = value.len; + buf[2] = value.capacity; + } +} + // 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..4135b70c48 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::{FfiDefault, FfiSerialize, Lower, RustBuffer, UniFfiTag}; use std::mem::MaybeUninit; use std::panic; @@ -87,6 +87,25 @@ impl Default for RustCallStatus { } } +impl FfiSerialize for RustCallStatus { + const SIZE: usize = 4; + + fn get(buf: &[u64]) -> Self { + Self { + code: RustCallStatusCode::try_from(buf[0] as i8) + .unwrap_or(RustCallStatusCode::UnexpectedError), + error_buf: MaybeUninit::new(RustBuffer::get(&buf[1..])), + } + } + + fn put(buf: &mut [u64], value: Self) { + buf[0] = value.code as u64; + // 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 +125,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_macros/Cargo.toml b/uniffi_macros/Cargo.toml index 20ed498aa1..b94199584b 100644 --- a/uniffi_macros/Cargo.toml +++ b/uniffi_macros/Cargo.toml @@ -31,6 +31,8 @@ uniffi_meta = { path = "../uniffi_meta", version = "=0.27.0" } 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.rs b/uniffi_macros/src/export.rs index 3843544b21..7c4bbbe90d 100644 --- a/uniffi_macros/src/export.rs +++ b/uniffi_macros/src/export.rs @@ -22,6 +22,7 @@ use self::{ use crate::util::{ident_to_string, mod_path}; pub use attributes::{AsyncRuntime, DefaultMap, ExportFnArgs}; pub use callback_interface::ffi_converter_callback_interface_impl; +pub use scaffolding::ffi_buffer_scaffolding_fn; // TODO(jplatte): Ensure no generics, … // TODO(jplatte): Aggregate errors instead of short-circuiting, wherever possible diff --git a/uniffi_macros/src/export/callback_interface.rs b/uniffi_macros/src/export/callback_interface.rs index fe145384ec..f3e9c1ecc6 100644 --- a/uniffi_macros/src/export/callback_interface.rs +++ b/uniffi_macros/src/export/callback_interface.rs @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use crate::{ - export::ImplItem, + export::{ffi_buffer_scaffolding_fn, ImplItem}, fnsig::{FnKind, FnSignature, ReceiverArg}, util::{ create_metadata_items, derive_ffi_traits, ident_to_string, mod_path, tagged_impl_header, @@ -79,6 +79,13 @@ pub(super) fn trait_impl( let has_async_method = methods.iter().any(|m| m.is_async); let impl_attributes = has_async_method.then(|| quote! { #[::async_trait::async_trait] }); + let init_fn_ffi_buffer_version = ffi_buffer_scaffolding_fn( + &init_ident, + "e! { () }, + &[quote! { ::std::ptr::NonNull<#vtable_type> }], + false, + ); + Ok(quote! { struct #vtable_type { #(#vtable_fields)* @@ -92,6 +99,8 @@ pub(super) fn trait_impl( #vtable_cell.set(vtable); } + #init_fn_ffi_buffer_version + #[derive(Debug)] struct #trait_impl_ident { handle: u64, diff --git a/uniffi_macros/src/export/scaffolding.rs b/uniffi_macros/src/export/scaffolding.rs index 58b8d83a35..b57c605c8c 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,55 @@ pub(super) fn gen_ffi_function( }, } } + + #scaffolding_fn_ffi_buffer_version } }) } + +#[cfg(feature = "scaffolding-ffi-buffer-fns")] +pub fn ffi_buffer_scaffolding_fn( + fn_ident: &Ident, + return_type: &TokenStream, + param_types: &[TokenStream], + add_rust_call_status: bool, +) -> TokenStream { + let fn_name = fn_ident.to_string(); + let ffi_buffer_fn_name = 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}"), + }; + let ident = Ident::new(&ffi_buffer_fn_name, proc_macro2::Span::call_site()); + let mut type_list: Vec<_> = param_types.iter().map(|ty| quote! { #ty }).collect(); + if add_rust_call_status { + type_list.push(quote! { &mut ::uniffi::RustCallStatus }) + } + + quote! { + #[doc(hidden)] + #[no_mangle] + pub unsafe extern "C" fn #ident( + arg_ptr: *mut u64, + return_ptr: *mut u64, + ) { + 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"))] +pub 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/export/trait_interface.rs b/uniffi_macros/src/export/trait_interface.rs index 841d50786b..c8d3bd7652 100644 --- a/uniffi_macros/src/export/trait_interface.rs +++ b/uniffi_macros/src/export/trait_interface.rs @@ -9,7 +9,8 @@ use uniffi_meta::ObjectImpl; use crate::{ export::{ - attributes::ExportTraitArgs, callback_interface, gen_method_scaffolding, item::ImplItem, + attributes::ExportTraitArgs, callback_interface, ffi_buffer_scaffolding_fn, + gen_method_scaffolding, item::ImplItem, }, object::interface_meta_static_var, util::{ident_to_string, tagged_impl_header}, @@ -42,6 +43,19 @@ pub(super) fn gen_trait_scaffolding( Span::call_site(), ); + let clone_fn_ffi_buffer_version = ffi_buffer_scaffolding_fn( + &clone_fn_ident, + "e! { *const ::std::ffi::c_void }, + &[quote! { *const ::std::ffi::c_void }], + true, + ); + let free_fn_ffi_buffer_version = ffi_buffer_scaffolding_fn( + &free_fn_ident, + "e! { () }, + &[quote! { *const ::std::ffi::c_void }], + true, + ); + let helper_fn_tokens = quote! { #[doc(hidden)] #[no_mangle] @@ -79,6 +93,9 @@ pub(super) fn gen_trait_scaffolding( Ok(()) }); } + + #clone_fn_ffi_buffer_version + #free_fn_ffi_buffer_version }; let impl_tokens: TokenStream = items diff --git a/uniffi_macros/src/object.rs b/uniffi_macros/src/object.rs index 6bcc07a14e..f378b541ef 100644 --- a/uniffi_macros/src/object.rs +++ b/uniffi_macros/src/object.rs @@ -2,8 +2,11 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use syn::DeriveInput; -use crate::util::{ - create_metadata_items, extract_docstring, ident_to_string, mod_path, tagged_impl_header, +use crate::{ + export::ffi_buffer_scaffolding_fn, + util::{ + create_metadata_items, extract_docstring, ident_to_string, mod_path, tagged_impl_header, + }, }; use uniffi_meta::ObjectImpl; @@ -26,6 +29,19 @@ pub fn expand_object(input: DeriveInput, udl_mode: bool) -> syn::Result syn::Result u16 { #const_ident.checksum() } + + #checksum_fn_ffi_buffer_version } });