From dd4eb6528d3b9d9a1bcaae8c43a3f9ca50bd8ac7 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Fri, 27 Oct 2023 16:58:22 -0400 Subject: [PATCH 1/2] Adding `FfiType::Callback` This type can be used whenever we need to define a callback functions. Rather than adding the type by hand, which was quite annoying when I was doing the async work, we now can just add an item in `ffi_callback_definitions()`. This also can help avoid FFI type mismatches, like how the Kotlin signature used to an i16 instead of an i8 by accident. Use the new type for the future continuation callback. I have another plan for callback interface callbacks. I didn't touch the foreign executor callback, I hope to work on that very soon. --- .../src/bindings/kotlin/gen_kotlin/mod.rs | 24 +++++++---- .../src/bindings/kotlin/templates/Async.kt | 8 ++-- .../src/bindings/kotlin/templates/Helpers.kt | 5 --- .../templates/NamespaceLibraryTemplate.kt | 16 +++++++ .../src/bindings/python/gen_python/mod.rs | 16 +++++-- .../src/bindings/python/templates/Async.py | 2 +- .../src/bindings/python/templates/Helpers.py | 3 -- .../templates/NamespaceLibraryTemplate.py | 14 ++++++ .../src/bindings/ruby/gen_ruby/mod.rs | 5 +-- .../src/bindings/swift/gen_swift/mod.rs | 19 +++++--- .../src/bindings/swift/templates/Async.swift | 2 +- .../swift/templates/BridgingHeaderTemplate.h | 13 +++++- uniffi_bindgen/src/interface/ffi.rs | 43 ++++++++++++++++++- uniffi_bindgen/src/interface/mod.rs | 19 +++++++- 14 files changed, 150 insertions(+), 39 deletions(-) diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs index a5791d98e6..74e0b2eef4 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs @@ -318,14 +318,19 @@ impl KotlinCodeOracle { nm.to_string().to_shouty_snake_case() } - fn ffi_type_label_by_value(ffi_type: &FfiType) -> String { + /// Get the idiomatic Python rendering of an FFI callback function + fn ffi_callback_name(&self, nm: &str) -> String { + format!("Uniffi{}", nm.to_upper_camel_case()) + } + + fn ffi_type_label_by_value(&self, ffi_type: &FfiType) -> String { match ffi_type { - FfiType::RustBuffer(_) => format!("{}.ByValue", Self::ffi_type_label(ffi_type)), - _ => Self::ffi_type_label(ffi_type), + FfiType::RustBuffer(_) => format!("{}.ByValue", self.ffi_type_label(ffi_type)), + _ => self.ffi_type_label(ffi_type), } } - fn ffi_type_label(ffi_type: &FfiType) -> String { + fn ffi_type_label(&self, ffi_type: &FfiType) -> String { match ffi_type { // Note that unsigned integers in Kotlin are currently experimental, but java.nio.ByteBuffer does not // support them yet. Thus, we use the signed variants to represent both signed and unsigned @@ -341,11 +346,9 @@ impl KotlinCodeOracle { format!("RustBuffer{}", maybe_suffix.as_deref().unwrap_or_default()) } FfiType::ForeignBytes => "ForeignBytes.ByValue".to_string(), + FfiType::Callback(name) => self.ffi_callback_name(name), FfiType::ForeignCallback => "ForeignCallback".to_string(), FfiType::RustFutureHandle => "Pointer".to_string(), - FfiType::RustFutureContinuationCallback => { - "UniFffiRustFutureContinuationCallbackType".to_string() - } FfiType::RustFutureContinuationData => "USize".to_string(), } } @@ -479,7 +482,7 @@ mod filters { } pub fn ffi_type_name_by_value(type_: &FfiType) -> Result { - Ok(KotlinCodeOracle::ffi_type_label_by_value(type_)) + Ok(KotlinCodeOracle.ffi_type_label_by_value(type_)) } /// Get the idiomatic Kotlin rendering of a function name. @@ -507,6 +510,11 @@ mod filters { Ok(KotlinCodeOracle.convert_error_suffix(&name)) } + /// Get the idiomatic Kotlin rendering of an FFI callback function name + pub fn ffi_callback_name(nm: &str) -> Result { + Ok(KotlinCodeOracle.ffi_callback_name(nm)) + } + pub fn object_names( obj: &Object, ci: &ComponentInterface, diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Async.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Async.kt index 341e2a1781..141a9681de 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/Async.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Async.kt @@ -6,15 +6,15 @@ internal const val UNIFFI_RUST_FUTURE_POLL_MAYBE_READY = 1.toByte() internal val uniffiContinuationHandleMap = UniFfiHandleMap>() // FFI type for Rust future continuations -internal object uniffiRustFutureContinuationCallback: UniFffiRustFutureContinuationCallbackType { - override fun callback(continuationHandle: USize, pollResult: Byte) { - uniffiContinuationHandleMap.remove(continuationHandle)?.resume(pollResult) +internal object uniffiRustFutureContinuationCallback: UniffiRustFutureContinuationCallback { + override fun callback(data: USize, pollResult: Byte) { + uniffiContinuationHandleMap.remove(data)?.resume(pollResult) } } internal suspend fun uniffiRustCallAsync( rustFuture: Pointer, - pollFunc: (Pointer, UniFffiRustFutureContinuationCallbackType, USize) -> Unit, + pollFunc: (Pointer, UniffiRustFutureContinuationCallback, USize) -> Unit, completeFunc: (Pointer, UniffiRustCallStatus) -> F, freeFunc: (Pointer) -> Unit, liftFunc: (F) -> T, diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt index a2dcf0a146..a3a992571c 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt @@ -154,8 +154,3 @@ internal class UniFfiHandleMap { return map.remove(handle) } } - -// FFI type for Rust future continuations -internal interface UniFffiRustFutureContinuationCallbackType : com.sun.jna.Callback { - fun callback(continuationHandle: USize, pollResult: Byte); -} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt index 160aa81814..6348c3068f 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt @@ -13,6 +13,22 @@ private inline fun loadIndirect( return Native.load(findLibraryName(componentName), Lib::class.java) } +// Define FFI callback types +{%- for callback in ci.ffi_callback_definitions() %} +internal interface {{ callback.name()|ffi_callback_name }} : com.sun.jna.Callback { + fun callback( + {%- for arg in callback.arguments() -%} + {{ arg.name().borrow()|var_name }}: {{ arg.type_().borrow()|ffi_type_name_by_value }}, + {%- endfor -%} + ) + {%- match callback.return_type() %} + {%- when Some(return_type) %}: {{ return_type|ffi_type_name_by_value }} + {%- when None %} + {%- endmatch %} +} + +{%- endfor %} + // A JNA Library to expose the extern-C FFI definitions. // This is an implementation detail which will be called internally by the public API. diff --git a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs index 7438416d02..8ea4e658d7 100644 --- a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs +++ b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs @@ -337,7 +337,12 @@ impl PythonCodeOracle { fixup_keyword(nm.to_string().to_shouty_snake_case()) } - fn ffi_type_label(ffi_type: &FfiType) -> String { + /// Get the idiomatic Python rendering of an FFI callback function + fn ffi_callback_name(&self, nm: &str) -> String { + format!("UNIFFI_{}", nm.to_shouty_snake_case()) + } + + fn ffi_type_label(&self, ffi_type: &FfiType) -> String { match ffi_type { FfiType::Int8 => "ctypes.c_int8".to_string(), FfiType::UInt8 => "ctypes.c_uint8".to_string(), @@ -355,10 +360,10 @@ impl PythonCodeOracle { None => "_UniffiRustBuffer".to_string(), }, FfiType::ForeignBytes => "_UniffiForeignBytes".to_string(), + FfiType::Callback(name) => self.ffi_callback_name(name), FfiType::ForeignCallback => "_UNIFFI_FOREIGN_CALLBACK_T".to_string(), // Pointer to an `asyncio.EventLoop` instance FfiType::RustFutureHandle => "ctypes.c_void_p".to_string(), - FfiType::RustFutureContinuationCallback => "_UNIFFI_FUTURE_CONTINUATION_T".to_string(), FfiType::RustFutureContinuationData => "ctypes.c_size_t".to_string(), } } @@ -485,7 +490,7 @@ pub mod filters { } pub fn ffi_type_name(type_: &FfiType) -> Result { - Ok(PythonCodeOracle::ffi_type_label(type_)) + Ok(PythonCodeOracle.ffi_type_label(type_)) } /// Get the idiomatic Python rendering of a class name (for enums, records, errors, etc). @@ -508,6 +513,11 @@ pub mod filters { Ok(PythonCodeOracle.enum_variant_name(nm)) } + /// Get the idiomatic Python rendering of a FFI callback function name + pub fn ffi_callback_name(nm: &str) -> Result { + Ok(PythonCodeOracle.ffi_callback_name(nm)) + } + /// Get the idiomatic Python rendering of an individual enum variant. pub fn object_names(obj: &Object) -> Result<(String, String), askama::Error> { Ok(PythonCodeOracle.object_names(obj)) diff --git a/uniffi_bindgen/src/bindings/python/templates/Async.py b/uniffi_bindgen/src/bindings/python/templates/Async.py index 51bc31b595..aae87ab98b 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Async.py +++ b/uniffi_bindgen/src/bindings/python/templates/Async.py @@ -7,7 +7,7 @@ # Continuation callback for async functions # lift the return value or error and resolve the future, causing the async function to resume. -@_UNIFFI_FUTURE_CONTINUATION_T +@UNIFFI_RUST_FUTURE_CONTINUATION_CALLBACK def _uniffi_continuation_callback(future_ptr, poll_code): (eventloop, future) = _UniffiContinuationPointerManager.release_pointer(future_ptr) eventloop.call_soon_threadsafe(_uniffi_set_future_result, future, poll_code) diff --git a/uniffi_bindgen/src/bindings/python/templates/Helpers.py b/uniffi_bindgen/src/bindings/python/templates/Helpers.py index dca962f176..e8eeff7b8a 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Helpers.py +++ b/uniffi_bindgen/src/bindings/python/templates/Helpers.py @@ -70,6 +70,3 @@ def _uniffi_check_call_status(error_ffi_converter, call_status): # Rust definition `fn(handle: u64, method: u32, args: _UniffiRustBuffer, buf_ptr: *mut _UniffiRustBuffer) -> int` _UNIFFI_FOREIGN_CALLBACK_T = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_ulonglong, ctypes.c_ulong, ctypes.POINTER(ctypes.c_char), ctypes.c_int, ctypes.POINTER(_UniffiRustBuffer)) -# UniFFI future continuation -_UNIFFI_FUTURE_CONTINUATION_T = ctypes.CFUNCTYPE(None, ctypes.c_size_t, ctypes.c_int8) - diff --git a/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py b/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py index 00822252e6..3e5450ad12 100644 --- a/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py @@ -52,6 +52,20 @@ def _uniffi_check_api_checksums(lib): pass {%- endfor %} + +# Define FFI callback types +{%- for callback in ci.ffi_callback_definitions() %} +{{ callback.name()|ffi_callback_name }} = ctypes.CFUNCTYPE( + {%- match callback.return_type() %} + {%- when Some(return_type) %}{{ return_type|ffi_type_name }}, + {%- when None %}None, + {%- endmatch %} + {%- for arg in callback.arguments() -%} + {{ arg.type_().borrow()|ffi_type_name }}, + {%- endfor -%} +) +{%- endfor %} + # A ctypes library to expose the extern-C FFI definitions. # This is an implementation detail which will be called internally by the public API. diff --git a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs index 7c5ff89d69..de2deb4410 100644 --- a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs +++ b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs @@ -152,12 +152,11 @@ mod filters { FfiType::RustArcPtr(_) => ":pointer".to_string(), FfiType::RustBuffer(_) => "RustBuffer.by_value".to_string(), FfiType::ForeignBytes => "ForeignBytes".to_string(), + FfiType::Callback(_) => unimplemented!("FFI Callbacks not implemented"), // Callback interfaces are not yet implemented, but this needs to return something in // order for the coverall tests to pass. FfiType::ForeignCallback => ":pointer".to_string(), - FfiType::RustFutureHandle - | FfiType::RustFutureContinuationCallback - | FfiType::RustFutureContinuationData => { + FfiType::RustFutureHandle | FfiType::RustFutureContinuationData => { unimplemented!("Async functions are not implemented") } }) diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs index 85bac015d1..9ba0e67820 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs @@ -507,6 +507,11 @@ impl SwiftCodeOracle { nm.to_string().to_lower_camel_case() } + /// Get the idiomatic Python rendering of an FFI callback function + fn ffi_callback_name(&self, nm: &str) -> String { + format!("Uniffi{}", nm.to_upper_camel_case()) + } + fn ffi_type_label_raw(&self, ffi_type: &FfiType) -> String { match ffi_type { FfiType::Int8 => "Int8".into(), @@ -522,8 +527,8 @@ impl SwiftCodeOracle { FfiType::RustArcPtr(_) => "UnsafeMutableRawPointer".into(), FfiType::RustBuffer(_) => "RustBuffer".into(), FfiType::ForeignBytes => "ForeignBytes".into(), + FfiType::Callback(name) => self.ffi_callback_name(name), FfiType::ForeignCallback => "ForeignCallback".into(), - FfiType::RustFutureContinuationCallback => "UniFfiRustFutureContinuation".into(), FfiType::RustFutureHandle | FfiType::RustFutureContinuationData => { "UnsafeMutableRawPointer".into() } @@ -534,7 +539,6 @@ impl SwiftCodeOracle { match ffi_type { FfiType::ForeignCallback | FfiType::RustFutureHandle - | FfiType::RustFutureContinuationCallback | FfiType::RustFutureContinuationData => { format!("{} _Nonnull", self.ffi_type_label_raw(ffi_type)) } @@ -634,10 +638,10 @@ pub mod filters { FfiType::RustArcPtr(_) => "void*_Nonnull".into(), FfiType::RustBuffer(_) => "RustBuffer".into(), FfiType::ForeignBytes => "ForeignBytes".into(), - FfiType::ForeignCallback => "ForeignCallback _Nonnull".into(), - FfiType::RustFutureContinuationCallback => { - "UniFfiRustFutureContinuation _Nonnull".into() + FfiType::Callback(name) => { + format!("{} _Nonnull", SwiftCodeOracle.ffi_callback_name(name)) } + FfiType::ForeignCallback => "ForeignCallback _Nonnull".into(), FfiType::RustFutureHandle | FfiType::RustFutureContinuationData => { "void* _Nonnull".into() } @@ -675,6 +679,11 @@ pub mod filters { Ok(oracle().enum_variant_name(nm)) } + /// Get the idiomatic Swift rendering of an ffi callback function name + pub fn ffi_callback_name(nm: &str) -> Result { + Ok(oracle().ffi_callback_name(nm)) + } + /// Get the idiomatic Swift rendering of docstring pub fn docstring(docstring: &str, spaces: &i32) -> Result { let middle = textwrap::indent(&textwrap::dedent(docstring), " * "); diff --git a/uniffi_bindgen/src/bindings/swift/templates/Async.swift b/uniffi_bindgen/src/bindings/swift/templates/Async.swift index f947408182..578ff4ddf4 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/Async.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/Async.swift @@ -3,7 +3,7 @@ private let UNIFFI_RUST_FUTURE_POLL_MAYBE_READY: Int8 = 1 fileprivate func uniffiRustCallAsync( rustFutureFunc: () -> UnsafeMutableRawPointer, - pollFunc: (UnsafeMutableRawPointer, @escaping UniFfiRustFutureContinuation, UnsafeMutableRawPointer) -> (), + pollFunc: (UnsafeMutableRawPointer, @escaping UniffiRustFutureContinuationCallback, UnsafeMutableRawPointer) -> (), completeFunc: (UnsafeMutableRawPointer, UnsafeMutablePointer) -> F, freeFunc: (UnsafeMutableRawPointer) -> (), liftFunc: (F) throws -> T, diff --git a/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h index a7d2580e9f..352b804993 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h +++ b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h @@ -47,8 +47,17 @@ typedef struct RustCallStatus { // ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ #endif // def UNIFFI_SHARED_H -// Continuation callback for UniFFI Futures -typedef void (*UniFfiRustFutureContinuation)(void * _Nonnull, int8_t); +// Define FFI callback types +{%- for callback in ci.ffi_callback_definitions() %} +typedef + {%- match callback.return_type() %}{% when Some(return_type) %} {{ return_type|ffi_type_name }} {% when None %} void {% endmatch -%} + (*{{ callback.name()|ffi_callback_name }})( + {%- for arg in callback.arguments() -%} + {{ arg.type_().borrow()|header_ffi_type_name }} + {%- if !loop.last %}, {% endif %} + {%- endfor -%} + ); +{%- endfor %} // Scaffolding functions {%- for func in ci.iter_ffi_function_definitions() %} diff --git a/uniffi_bindgen/src/interface/ffi.rs b/uniffi_bindgen/src/interface/ffi.rs index 1bce40d924..2eeaffe9b7 100644 --- a/uniffi_bindgen/src/interface/ffi.rs +++ b/uniffi_bindgen/src/interface/ffi.rs @@ -47,12 +47,13 @@ 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, + /// Pointer to a callback function. The inner type is the name of the callback, which matches + /// one of the items in [crate::ComponentInterface::ffi_callback_definitions]. + Callback(String), /// Pointer to a callback function that handles all callbacks on the foreign language side. ForeignCallback, /// Pointer to a Rust future RustFutureHandle, - /// Continuation function for a Rust future - RustFutureContinuationCallback, RustFutureContinuationData, // TODO: you can imagine a richer structural typesystem here, e.g. `Ref` or something. // We don't need that yet and it's possible we never will, so it isn't here for now. @@ -224,14 +225,52 @@ pub struct FfiArgument { } impl FfiArgument { + pub fn new(name: impl Into, type_: FfiType) -> Self { + Self { + name: name.into(), + type_, + } + } + pub fn name(&self) -> &str { &self.name } + pub fn type_(&self) -> FfiType { self.type_.clone() } } +/// Represents an "extern C"-style callback function +/// +/// These are defined in the foreign code and passed to Rust as a function pointer. +#[derive(Debug, Default, Clone)] +pub struct FfiCallbackFunction { + // Name for this function type. This matches the value inside `FfiType::Callback` + pub(super) name: String, + pub(super) arguments: Vec, + pub(super) return_type: Option, + pub(super) has_rust_call_status_arg: bool, +} + +impl FfiCallbackFunction { + pub fn name(&self) -> &str { + &self.name + } + + pub fn arguments(&self) -> Vec<&FfiArgument> { + self.arguments.iter().collect() + } + + pub fn return_type(&self) -> Option<&FfiType> { + self.return_type.as_ref() + } + + pub fn has_rust_call_status_arg(&self) -> bool { + self.has_rust_call_status_arg + } +} + #[cfg(test)] mod test { // There's not really much to test here to be honest, diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index 1e1bac27ac..e6c646becd 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -67,7 +67,7 @@ mod record; pub use record::{Field, Record}; pub mod ffi; -pub use ffi::{FfiArgument, FfiFunction, FfiType}; +pub use ffi::{FfiArgument, FfiCallbackFunction, FfiFunction, FfiType}; pub use uniffi_meta::Radix; use uniffi_meta::{ ConstructorMetadata, LiteralMetadata, NamespaceMetadata, ObjectMetadata, TraitMethodMetadata, @@ -213,6 +213,21 @@ impl ComponentInterface { self.objects.iter().find(|o| o.name == name) } + /// Get the definitions for callback FFI functions + /// + /// These are defined by the foreign code and invoked by Rust. + pub fn ffi_callback_definitions(&self) -> impl IntoIterator { + [FfiCallbackFunction { + name: "RustFutureContinuationCallback".to_owned(), + arguments: vec![ + FfiArgument::new("data", FfiType::RustFutureContinuationData), + FfiArgument::new("poll_result", FfiType::Int8), + ], + return_type: None, + has_rust_call_status_arg: false, + }] + } + /// Get the definitions for every Callback Interface type in the interface. pub fn callback_interface_definitions(&self) -> &[CallbackInterface] { &self.callback_interfaces @@ -472,7 +487,7 @@ impl ComponentInterface { }, FfiArgument { name: "callback".to_owned(), - type_: FfiType::RustFutureContinuationCallback, + type_: FfiType::Callback("RustFutureContinuationCallback".to_owned()), }, FfiArgument { name: "callback_data".to_owned(), From af04dd07f5f09c36a7dbec81a95af23dedcfd3e3 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Fri, 27 Oct 2023 19:53:30 -0400 Subject: [PATCH 2/2] Use VTables for callback interfaces Instead of registering a single method to implement a callback interface, foreign code now registers a VTable. VTable sounds fancy, but it's just a repr(C) struct where each field is a callback function. This gives us some more flexibility with the method signatures. Before, all arguments were passed using a RustBuffer, but not all FFI types can be written to a RustBuffer. In particular, I want to be able to pass callback function pointers. This also makes the callback interface FFI closer to the Rust one. I wanted to make it match exactly, but it didn't work out. Unfortunately, we can't directly return the return value on Python because of an old ctypes bug (https://bugs.python.org/issue5710). Instead, input an out param for the return type. The other main possibility would be to change `RustBuffer` to be a simple `*mut u8` (#1779), which would then be returnable by Python. However, it seems bad to restrict ourselves from ever returning a struct in the future. Eventually, we want to stop using `RustBuffer` for all complex data types and that probably means using a struct instead in some cases. Renamed `CALL_PANIC` to `CALL_UNEXPECTED_ERROR` in the foreign bindings templates. This matches the name in the Rust code and makes more sense for foreign trait implementations. Removed the reexport-scaffolding-macro fixture. I don't think this one is giving us a lot of value anymore and I don't want to keep updating it when the FFI changes. --- CHANGELOG.md | 5 + .../tests/bindings/test_proc_macro.swift | 3 +- .../src/bindings/kotlin/gen_kotlin/mod.rs | 48 +++++- .../kotlin/templates/CallbackInterfaceImpl.kt | 143 +++++++----------- .../templates/CallbackInterfaceRuntime.kt | 4 - .../templates/CallbackInterfaceTemplate.kt | 6 +- .../src/bindings/kotlin/templates/Helpers.kt | 43 +++++- .../templates/NamespaceLibraryTemplate.kt | 12 ++ .../kotlin/templates/ObjectTemplate.kt | 4 +- .../kotlin/templates/RustBufferTemplate.kt | 6 + .../src/bindings/python/gen_python/mod.rs | 21 ++- .../python/templates/CallbackInterfaceImpl.py | 141 +++++++---------- .../templates/CallbackInterfaceTemplate.py | 6 +- .../src/bindings/python/templates/Helpers.py | 27 +++- .../templates/NamespaceLibraryTemplate.py | 13 ++ .../python/templates/ObjectTemplate.py | 4 +- .../src/bindings/ruby/gen_ruby/mod.rs | 12 +- .../src/bindings/swift/gen_swift/mod.rs | 45 +++--- .../swift/templates/BridgingHeaderTemplate.h | 18 ++- .../templates/CallbackInterfaceImpl.swift | 137 +++++++---------- .../templates/CallbackInterfaceTemplate.swift | 2 + .../bindings/swift/templates/Helpers.swift | 34 ++++- .../swift/templates/ObjectTemplate.swift | 2 + uniffi_bindgen/src/interface/callbacks.rs | 87 ++++++++++- uniffi_bindgen/src/interface/ffi.rs | 72 +++++++-- uniffi_bindgen/src/interface/mod.rs | 53 +++++-- uniffi_bindgen/src/interface/object.rs | 45 +++++- uniffi_core/src/ffi/callbackinterface.rs | 126 ++------------- uniffi_core/src/ffi/foreigncallbacks.rs | 93 ++++-------- uniffi_core/src/ffi/rustcalls.rs | 7 + uniffi_core/src/ffi_converter_impls.rs | 16 +- uniffi_core/src/ffi_converter_traits.rs | 45 +++++- .../src/export/callback_interface.rs | 136 ++++++++++------- uniffi_macros/src/export/scaffolding.rs | 2 +- uniffi_macros/src/fnsig.rs | 24 +-- uniffi_meta/src/ffi_names.rs | 7 +- 36 files changed, 833 insertions(+), 616 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97cf24e9a1..67d06a4b85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ - Fixed a memory leak in callback interface handling. +### ⚠️ Breaking Changes for external bindings authors ⚠️ + +- The callback interface code was reworked to use vtables rather than a single callback method. + See https://github.com/mozilla/uniffi-rs/pull/1818 for details and how the other bindings were updated. + ## v0.26.0 (backend crates: v0.26.0) - (_2024-01-23_) ### What's changed? diff --git a/fixtures/proc-macro/tests/bindings/test_proc_macro.swift b/fixtures/proc-macro/tests/bindings/test_proc_macro.swift index f8a1618f15..bd9fef7f3f 100644 --- a/fixtures/proc-macro/tests/bindings/test_proc_macro.swift +++ b/fixtures/proc-macro/tests/bindings/test_proc_macro.swift @@ -84,8 +84,7 @@ class SwiftTestCallbackInterface : TestCallbackInterface { } func callbackHandler(h: Object) -> UInt32 { - var v = h.takeError(e: BasicError.InvalidInput) - return v + return h.takeError(e: BasicError.InvalidInput) } func getOtherCallbackInterface() -> OtherCallbackInterface { diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs index 74e0b2eef4..86c08aae6c 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs @@ -310,7 +310,12 @@ impl KotlinCodeOracle { /// Get the idiomatic Kotlin rendering of a variable name. fn var_name(&self, nm: &str) -> String { - format!("`{}`", nm.to_string().to_lower_camel_case()) + format!("`{}`", self.var_name_raw(nm)) + } + + /// `var_name` without the backticks. Useful for using in `@Structure.FieldOrder`. + pub fn var_name_raw(&self, nm: &str) -> String { + nm.to_string().to_lower_camel_case() } /// Get the idiomatic Kotlin rendering of an individual enum variant. @@ -318,11 +323,16 @@ impl KotlinCodeOracle { nm.to_string().to_shouty_snake_case() } - /// Get the idiomatic Python rendering of an FFI callback function + /// Get the idiomatic Kotlin rendering of an FFI callback function name fn ffi_callback_name(&self, nm: &str) -> String { format!("Uniffi{}", nm.to_upper_camel_case()) } + /// Get the idiomatic Kotlin rendering of an FFI struct name + fn ffi_struct_name(&self, nm: &str) -> String { + format!("Uniffi{}", nm.to_upper_camel_case()) + } + fn ffi_type_label_by_value(&self, ffi_type: &FfiType) -> String { match ffi_type { FfiType::RustBuffer(_) => format!("{}.ByValue", self.ffi_type_label(ffi_type)), @@ -330,6 +340,25 @@ impl KotlinCodeOracle { } } + fn ffi_type_label_by_reference(&self, ffi_type: &FfiType) -> String { + match ffi_type { + FfiType::Int8 + | FfiType::UInt8 + | FfiType::Int16 + | FfiType::UInt16 + | FfiType::Int32 + | FfiType::UInt32 + | FfiType::Int64 + | FfiType::UInt64 + | FfiType::Float32 + | FfiType::Float64 => format!("{}ByReference", self.ffi_type_label(ffi_type)), + FfiType::RustArcPtr(_) => "PointerByReference".to_owned(), + // JNA structs default to ByReference + FfiType::RustBuffer(_) | FfiType::Struct(_) => self.ffi_type_label(ffi_type), + _ => panic!("{ffi_type:?} by reference is not implemented"), + } + } + fn ffi_type_label(&self, ffi_type: &FfiType) -> String { match ffi_type { // Note that unsigned integers in Kotlin are currently experimental, but java.nio.ByteBuffer does not @@ -347,8 +376,9 @@ impl KotlinCodeOracle { } FfiType::ForeignBytes => "ForeignBytes.ByValue".to_string(), FfiType::Callback(name) => self.ffi_callback_name(name), - FfiType::ForeignCallback => "ForeignCallback".to_string(), - FfiType::RustFutureHandle => "Pointer".to_string(), + FfiType::Struct(name) => self.ffi_struct_name(name), + FfiType::Reference(inner) => self.ffi_type_label_by_reference(inner), + FfiType::VoidPointer | FfiType::RustFutureHandle => "Pointer".to_string(), FfiType::RustFutureContinuationData => "USize".to_string(), } } @@ -500,6 +530,11 @@ mod filters { Ok(KotlinCodeOracle.var_name(nm)) } + /// Get the idiomatic Kotlin rendering of a variable name. + pub fn var_name_raw(nm: &str) -> Result { + Ok(KotlinCodeOracle.var_name_raw(nm)) + } + /// Get a String representing the name used for an individual enum variant. pub fn variant_name(v: &Variant) -> Result { Ok(KotlinCodeOracle.enum_variant_name(v.name())) @@ -515,6 +550,11 @@ mod filters { Ok(KotlinCodeOracle.ffi_callback_name(nm)) } + /// Get the idiomatic Kotlin rendering of an FFI struct name + pub fn ffi_struct_name(nm: &str) -> Result { + Ok(KotlinCodeOracle.ffi_struct_name(nm)) + } + pub fn object_names( obj: &Object, ci: &ComponentInterface, diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt index 77794cb045..80bc4d3399 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt @@ -1,107 +1,70 @@ {% if self.include_once_check("CallbackInterfaceRuntime.kt") %}{% include "CallbackInterfaceRuntime.kt" %}{% endif %} -// Implement the foreign callback handler for {{ interface_name }} -internal class {{ callback_handler_class }} : ForeignCallback { - @Suppress("TooGenericExceptionCaught") - override fun invoke(handle: UniffiHandle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int { - val cb = {{ ffi_converter_name }}.handleMap.get(handle) - return when (method) { - IDX_CALLBACK_FREE -> { - {{ ffi_converter_name }}.handleMap.remove(handle) +{%- let trait_impl=format!("uniffiCallbackInterface{}", name) %} - // Successful return - // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - UNIFFI_CALLBACK_SUCCESS - } - {% for meth in methods.iter() -%} - {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} - {{ loop.index }} -> { - // Call the method, write to outBuf and return a status code - // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` for info - try { - this.{{ method_name }}(cb, argsData, argsLen, outBuf) - } catch (e: Throwable) { - // Unexpected error - try { - // Try to serialize the error into a string - outBuf.setValue({{ Type::String.borrow()|ffi_converter_name }}.lower(e.toString())) - } catch (e: Throwable) { - // If that fails, then it's time to give up and just return - } - UNIFFI_CALLBACK_UNEXPECTED_ERROR - } - } - {% endfor %} - else -> { - // An unexpected error happened. - // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - try { - // Try to serialize the error into a string - outBuf.setValue({{ Type::String.borrow()|ffi_converter_name }}.lower("Invalid Callback index")) - } catch (e: Throwable) { - // If that fails, then it's time to give up and just return - } - UNIFFI_CALLBACK_UNEXPECTED_ERROR +// Put the implementation in an object so we don't pollute the top-level namespace +internal object {{ trait_impl }} { + {%- for (ffi_callback, meth) in vtable_methods.iter() %} + internal object {{ meth.name()|var_name }}: {{ ffi_callback.name()|ffi_callback_name }} { + override fun callback( + {%- for arg in ffi_callback.arguments() -%} + {{ arg.name().borrow()|var_name }}: {{ arg.type_().borrow()|ffi_type_name_by_value }}, + {%- endfor -%} + {%- if ffi_callback.has_rust_call_status_arg() -%} + uniffiCallStatus: UniffiRustCallStatus, + {%- endif -%} + ) + {%- match ffi_callback.return_type() %} + {%- when Some(return_type) %}: {{ return_type|ffi_type_name_by_value }}, + {%- when None %} + {%- endmatch %} { + val uniffiObj = {{ ffi_converter_name }}.handleMap.get(uniffiHandle) + val makeCall = { -> + uniffiObj.{{ meth.name()|fn_name() }}( + {%- for arg in meth.arguments() %} + {{ arg|lift_fn }}({{ arg.name()|var_name }}), + {%- endfor %} + ) } - } - } - {% for meth in methods.iter() -%} - {% let method_name = format!("invoke_{}", meth.name())|fn_name %} - @Suppress("UNUSED_PARAMETER") - private fun {{ method_name }}(kotlinCallbackInterface: {{ interface_name }}, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int { - {%- if meth.arguments().len() > 0 %} - val argsBuf = argsData.getByteBuffer(0, argsLen.toLong()).also { - it.order(ByteOrder.BIG_ENDIAN) - } - {%- endif %} + {%- match meth.return_type() %} + {%- when Some(return_type) %} + val writeReturn = { value: {{ return_type|type_name(ci) }} -> uniffiOutReturn.setValue({{ return_type|lower_fn }}(value)) } + {%- when None %} + val writeReturn = { _: Unit -> Unit } + {%- endmatch %} - {%- match meth.return_type() %} - {%- when Some with (return_type) %} - fun makeCall() : Int { - val returnValue = kotlinCallbackInterface.{{ meth.name()|fn_name }}( - {%- for arg in meth.arguments() %} - {{ arg|read_fn }}(argsBuf) - {% if !loop.last %}, {% endif %} - {%- endfor %} - ) - outBuf.setValue({{ return_type|ffi_converter_name }}.lowerIntoRustBuffer(returnValue)) - return UNIFFI_CALLBACK_SUCCESS - } - {%- when None %} - fun makeCall() : Int { - kotlinCallbackInterface.{{ meth.name()|fn_name }}( - {%- for arg in meth.arguments() %} - {{ arg|read_fn }}(argsBuf) - {%- if !loop.last %}, {% endif %} - {%- endfor %} + {%- match meth.throws_type() %} + {%- when None %} + uniffiTraitInterfaceCall(uniffiCallStatus, makeCall, writeReturn) + {%- when Some(error_type) %} + uniffiTraitInterfaceCallWithError( + uniffiCallStatus, + makeCall, + writeReturn, + { e: {{error_type|type_name(ci) }} -> {{ error_type|lower_fn }}(e) } ) - return UNIFFI_CALLBACK_SUCCESS + {%- endmatch %} } - {%- endmatch %} + } + {%- endfor %} - {%- match meth.throws_type() %} - {%- when None %} - fun makeCallAndHandleError() : Int = makeCall() - {%- when Some(error_type) %} - fun makeCallAndHandleError() : Int = try { - makeCall() - } catch (e: {{ error_type|type_name(ci) }}) { - // Expected error, serialize it into outBuf - outBuf.setValue({{ error_type|ffi_converter_name }}.lowerIntoRustBuffer(e)) - UNIFFI_CALLBACK_ERROR + internal object uniffiFree: {{ "CallbackInterfaceFree"|ffi_callback_name }} { + override fun callback(handle: Long) { + {{ ffi_converter_name }}.handleMap.remove(handle) } - {%- endmatch %} - - return makeCallAndHandleError() } - {% endfor %} + + internal var vtable = {{ vtable|ffi_type_name_by_value }}( + {%- for (ffi_callback, meth) in vtable_methods.iter() %} + {{ meth.name()|var_name() }}, + {%- endfor %} + uniffiFree + ) // Registers the foreign callback with the Rust side. // This method is generated for each callback interface. internal fun register(lib: UniffiLib) { - lib.{{ ffi_init_callback.name() }}(this) + lib.{{ ffi_init_callback.name() }}(vtable) } } - -internal val {{ callback_handler_obj }} = {{ callback_handler_class }}() diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt index e1b25f0e26..e7b7dc4725 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt @@ -32,10 +32,6 @@ internal class ConcurrentHandleMap( } } -interface ForeignCallback : com.sun.jna.Callback { - public fun invoke(handle: UniffiHandle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int -} - // Magic number for the Rust proxy to call using the same mechanism as every other method, // to free the callback once it's dropped by Rust. internal const val IDX_CALLBACK_FREE = 0 diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt index 9f104118c7..5285cbc4ab 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt @@ -1,10 +1,10 @@ {%- let cbi = ci|get_callback_interface_definition(name) %} -{%- let callback_handler_class = format!("UniffiCallbackInterface{}", name) %} -{%- let callback_handler_obj = format!("uniffiCallbackInterface{}", name) %} {%- let ffi_init_callback = cbi.ffi_init_callback() %} {%- let interface_name = cbi|type_name(ci) %} -{%- let methods = cbi.methods() %} {%- let interface_docstring = cbi.docstring() %} +{%- let methods = cbi.methods() %} +{%- let vtable = cbi.vtable() %} +{%- let vtable_methods = cbi.vtable_methods() %} {% include "Interface.kt" %} {% include "CallbackInterfaceImpl.kt" %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt index a3a992571c..d82a6a7407 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt @@ -1,6 +1,10 @@ // A handful of classes and functions to support the generated data structures. // This would be a good candidate for isolating in its own ffi-support lib. -// Error runtime. + +internal const val UNIFFI_CALL_SUCCESS = 0.toByte() +internal const val UNIFFI_CALL_ERROR = 1.toByte() +internal const val UNIFFI_CALL_UNEXPECTED_ERROR = 2.toByte() + @Structure.FieldOrder("code", "error_buf") internal open class UniffiRustCallStatus : Structure() { @JvmField var code: Byte = 0 @@ -9,15 +13,15 @@ internal open class UniffiRustCallStatus : Structure() { class ByValue: UniffiRustCallStatus(), Structure.ByValue fun isSuccess(): Boolean { - return code == 0.toByte() + return code == UNIFFI_CALL_SUCCESS } fun isError(): Boolean { - return code == 1.toByte() + return code == UNIFFI_CALL_ERROR } fun isPanic(): Boolean { - return code == 2.toByte() + return code == UNIFFI_CALL_UNEXPECTED_ERROR } } @@ -118,6 +122,37 @@ public class USize(value: Long = 0) : IntegerType(Native.SIZE_T_SIZE, value, tru } } +internal inline fun uniffiTraitInterfaceCall( + callStatus: UniffiRustCallStatus, + makeCall: () -> T, + writeReturn: (T) -> Unit, +) { + try { + writeReturn(makeCall()) + } catch(e: Exception) { + callStatus.code = UNIFFI_CALL_UNEXPECTED_ERROR + callStatus.error_buf = {{ Type::String.borrow()|lower_fn }}(e.toString()) + } +} + +internal inline fun uniffiTraitInterfaceCallWithError( + callStatus: UniffiRustCallStatus, + makeCall: () -> T, + writeReturn: (T) -> Unit, + lowerError: (E) -> RustBuffer.ByValue +) { + try { + writeReturn(makeCall()) + } catch(e: Exception) { + if (e is E) { + callStatus.code = UNIFFI_CALL_ERROR + callStatus.error_buf = lowerError(e) + } else { + callStatus.code = UNIFFI_CALL_UNEXPECTED_ERROR + callStatus.error_buf = {{ Type::String.borrow()|lower_fn }}(e.toString()) + } + } +} // Map handles to objects // diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt index 6348c3068f..36ffbfcf88 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt @@ -20,13 +20,25 @@ internal interface {{ callback.name()|ffi_callback_name }} : com.sun.jna.Callbac {%- for arg in callback.arguments() -%} {{ arg.name().borrow()|var_name }}: {{ arg.type_().borrow()|ffi_type_name_by_value }}, {%- endfor -%} + {%- if callback.has_rust_call_status_arg() -%} + uniffiCallStatus: UniffiRustCallStatus, + {%- endif -%} ) {%- match callback.return_type() %} {%- when Some(return_type) %}: {{ return_type|ffi_type_name_by_value }} {%- when None %} {%- endmatch %} } +{%- endfor %} +// Define FFI structs +{%- for ffi_struct in ci.ffi_struct_definitions() %} +@Structure.FieldOrder({% for field in ffi_struct.fields() %}"{{ field.name()|var_name_raw }}"{% if !loop.last %}, {% endif %}{% endfor %}) +internal class {{ ffi_struct.name()|ffi_struct_name }}( + {%- for field in ffi_struct.fields() %} + @JvmField internal var {{ field.name()|var_name }}: {{ field.type_().borrow()|ffi_type_name_by_value }}, + {%- endfor %} +) : Structure() { } {%- endfor %} // A JNA Library to expose the extern-C FFI definitions. diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt index e73ff3a959..8492b832c7 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt @@ -304,8 +304,8 @@ open class {{ impl_class_name }}: Disposable, AutoCloseable, {{ interface_name } } {%- if obj.has_callback_interface() %} -{%- let callback_handler_class = format!("UniffiCallbackInterface{}", name) %} -{%- let callback_handler_obj = format!("uniffiCallbackInterface{}", name) %} +{%- let vtable = obj.vtable().expect("trait interface should have a vtable") %} +{%- let vtable_methods = obj.vtable_methods() %} {%- let ffi_init_callback = obj.ffi_init_callback() %} {% include "CallbackInterfaceImpl.kt" %} {%- endif %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt index e914d481a4..9e75870175 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferTemplate.kt @@ -11,6 +11,12 @@ open class RustBuffer : Structure() { class ByValue: RustBuffer(), Structure.ByValue class ByReference: RustBuffer(), Structure.ByReference + internal fun setValue(other: RustBuffer) { + capacity = other.capacity + len = other.len + data = other.data + } + companion object { internal fun alloc(size: Int = 0) = uniffiRustCall() { status -> UniffiLib.INSTANCE.{{ ci.ffi_rustbuffer_alloc().name() }}(size, status) diff --git a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs index 8ea4e658d7..699cf22b2f 100644 --- a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs +++ b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs @@ -337,11 +337,18 @@ impl PythonCodeOracle { fixup_keyword(nm.to_string().to_shouty_snake_case()) } - /// Get the idiomatic Python rendering of an FFI callback function + /// Get the idiomatic Python rendering of an FFI callback function name fn ffi_callback_name(&self, nm: &str) -> String { format!("UNIFFI_{}", nm.to_shouty_snake_case()) } + /// Get the idiomatic Python rendering of an FFI struct name + fn ffi_struct_name(&self, nm: &str) -> String { + // The ctypes docs use both SHOUTY_SNAKE_CASE AND UpperCamelCase for structs. Let's use + // UpperCamelCase and reserve shouting for global variables + format!("Uniffi{}", nm.to_upper_camel_case()) + } + fn ffi_type_label(&self, ffi_type: &FfiType) -> String { match ffi_type { FfiType::Int8 => "ctypes.c_int8".to_string(), @@ -361,9 +368,10 @@ impl PythonCodeOracle { }, FfiType::ForeignBytes => "_UniffiForeignBytes".to_string(), FfiType::Callback(name) => self.ffi_callback_name(name), - FfiType::ForeignCallback => "_UNIFFI_FOREIGN_CALLBACK_T".to_string(), + FfiType::Struct(name) => self.ffi_struct_name(name), // Pointer to an `asyncio.EventLoop` instance - FfiType::RustFutureHandle => "ctypes.c_void_p".to_string(), + FfiType::Reference(inner) => format!("ctypes.POINTER({})", self.ffi_type_label(inner)), + FfiType::VoidPointer | FfiType::RustFutureHandle => "ctypes.c_void_p".to_string(), FfiType::RustFutureContinuationData => "ctypes.c_size_t".to_string(), } } @@ -513,11 +521,16 @@ pub mod filters { Ok(PythonCodeOracle.enum_variant_name(nm)) } - /// Get the idiomatic Python rendering of a FFI callback function name + /// Get the idiomatic Python rendering of an FFI callback function name pub fn ffi_callback_name(nm: &str) -> Result { Ok(PythonCodeOracle.ffi_callback_name(nm)) } + /// Get the idiomatic Python rendering of an FFI struct name + pub fn ffi_struct_name(nm: &str) -> Result { + Ok(PythonCodeOracle.ffi_struct_name(nm)) + } + /// Get the idiomatic Python rendering of an individual enum variant. pub fn object_names(obj: &Object) -> Result<(String, String), askama::Error> { Ok(PythonCodeOracle.object_names(obj)) diff --git a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py index 23866a1994..82907e6cd3 100644 --- a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py +++ b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py @@ -1,91 +1,62 @@ {% if self.include_once_check("CallbackInterfaceRuntime.py") %}{% include "CallbackInterfaceRuntime.py" %}{% endif %} - -# Declaration and _UniffiConverters for {{ type_name }} Callback Interface - -def {{ callback_handler_class }}(handle, method, args_data, args_len, buf_ptr): - {% for meth in methods.iter() -%} - {% let method_name = format!("invoke_{}", meth.name())|fn_name %} - def {{ method_name }}(python_callback, args_stream, buf_ptr): - {#- Unpacking args from the _UniffiRustBuffer #} - def makeCall(): - {#- Calling the concrete callback object #} - {%- if meth.arguments().len() != 0 -%} - return python_callback.{{ meth.name()|fn_name }}( - {% for arg in meth.arguments() -%} - {{ arg|read_fn }}(args_stream) - {%- if !loop.last %}, {% endif %} - {% endfor -%} - ) - {%- else %} - return python_callback.{{ meth.name()|fn_name }}() +{%- let trait_impl=format!("UniffiTraitImpl{}", name) %} + +# Put all the bits inside a class to keep the top-level namespace clean +class {{ trait_impl }}: + # For each method, generate a callback function to pass to Rust + {%- for (ffi_callback, meth) in vtable_methods.iter() %} + + @{{ ffi_callback.name()|ffi_callback_name }} + def {{ meth.name()|fn_name }}( + {%- for arg in ffi_callback.arguments() %} + {{ arg.name()|var_name }}, + {%- endfor -%} + {%- if ffi_callback.has_rust_call_status_arg() %} + uniffi_call_status_ptr, {%- endif %} - - def makeCallAndHandleReturn(): - {%- match meth.return_type() %} - {%- when Some(return_type) %} - rval = makeCall() - with _UniffiRustBuffer.alloc_with_builder() as builder: - {{ return_type|write_fn }}(rval, builder) - buf_ptr[0] = builder.finalize() - {%- when None %} - makeCall() - {%- endmatch %} - return _UNIFFI_CALLBACK_SUCCESS + ): + uniffi_obj = {{ ffi_converter_name }}._handle_map.get(uniffi_handle) + def make_call(): + args = ({% for arg in meth.arguments() %}{{ arg|lift_fn }}({{ arg.name()|var_name }}), {% endfor %}) + method = uniffi_obj.{{ meth.name()|fn_name }} + return method(*args) + {%- match meth.return_type() %} + {%- when Some(return_type) %} + def write_return_value(v): + uniffi_out_return[0] = {{ return_type|lower_fn }}(v) + {%- when None %} + write_return_value = lambda v: None + {%- endmatch %} {%- match meth.throws_type() %} {%- when None %} - return makeCallAndHandleReturn() - {%- when Some(err) %} - try: - return makeCallAndHandleReturn() - except {{ err|type_name }} as e: - # Catch errors declared in the UDL file - with _UniffiRustBuffer.alloc_with_builder() as builder: - {{ err|write_fn }}(e, builder) - buf_ptr[0] = builder.finalize() - return _UNIFFI_CALLBACK_ERROR + _uniffi_trait_interface_call( + uniffi_call_status_ptr.contents, + make_call, + write_return_value, + ) + {%- when Some(error) %} + _uniffi_trait_interface_call_with_error( + uniffi_call_status_ptr.contents, + make_call, + write_return_value, + {{ error|type_name }}, + {{ error|lower_fn }}, + ) {%- endmatch %} - - {% endfor %} - - cb = {{ ffi_converter_name }}._handle_map.get(handle) - - if method == IDX_CALLBACK_FREE: - {{ ffi_converter_name }}._handle_map.remove(handle) - - # Successful return - # See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - return _UNIFFI_CALLBACK_SUCCESS - - {% for meth in methods.iter() -%} - {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} - if method == {{ loop.index }}: - # Call the method and handle any errors - # See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` for details - try: - return {{ method_name }}(cb, _UniffiRustBufferStream(args_data, args_len), buf_ptr) - except BaseException as e: - # Catch unexpected errors - try: - # Try to serialize the exception into a String - buf_ptr[0] = {{ Type::String.borrow()|lower_fn }}(repr(e)) - except: - # If that fails, just give up - pass - return _UNIFFI_CALLBACK_UNEXPECTED_ERROR - {% endfor %} - - # This should never happen, because an out of bounds method index won't - # ever be used. Once we can catch errors, we should return an InternalException. - # https://github.com/mozilla/uniffi-rs/issues/351 - - # An unexpected error happened. - # See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - return _UNIFFI_CALLBACK_UNEXPECTED_ERROR - -# We need to keep this function reference alive: -# if they get GC'd while in use then UniFFI internals could attempt to call a function -# that is in freed memory. -# That would be...uh...bad. Yeah, that's the word. Bad. -{{ callback_handler_obj }} = _UNIFFI_FOREIGN_CALLBACK_T({{ callback_handler_class }}) -_UniffiLib.{{ ffi_init_callback.name() }}({{ callback_handler_obj }}) + {%- endfor %} + + @{{ "CallbackInterfaceFree"|ffi_callback_name }} + def uniffi_free(uniffi_handle): + {{ ffi_converter_name }}._handle_map.remove(uniffi_handle) + + # Generate the FFI VTable. This has a field for each callback interface method. + uniffi_vtable = {{ vtable|ffi_type_name }}( + {%- for (_, meth) in vtable_methods.iter() %} + {{ meth.name()|fn_name }}, + {%- endfor %} + uniffi_free + ) + # Send Rust a pointer to the VTable. Note: this means we need to keep the struct alive forever, + # or else bad things will happen when Rust tries to access it. + _UniffiLib.{{ ffi_init_callback.name() }}(ctypes.byref(uniffi_vtable)) diff --git a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py index ab9e725a05..a41e58e635 100644 --- a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py @@ -1,10 +1,10 @@ {%- let cbi = ci|get_callback_interface_definition(name) %} -{%- let callback_handler_class = format!("UniffiCallbackInterface{}", name) %} -{%- let callback_handler_obj = format!("uniffiCallbackInterface{}", name) %} {%- let ffi_init_callback = cbi.ffi_init_callback() %} {%- let protocol_name = type_name.clone() %} -{%- let methods = cbi.methods() %} {%- let protocol_docstring = cbi.docstring() %} +{%- let vtable = cbi.vtable() %} +{%- let methods = cbi.methods() %} +{%- let vtable_methods = cbi.vtable_methods() %} {% include "Protocol.py" %} {% include "CallbackInterfaceImpl.py" %} diff --git a/uniffi_bindgen/src/bindings/python/templates/Helpers.py b/uniffi_bindgen/src/bindings/python/templates/Helpers.py index e8eeff7b8a..b349e3ac29 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Helpers.py +++ b/uniffi_bindgen/src/bindings/python/templates/Helpers.py @@ -16,15 +16,15 @@ class _UniffiRustCallStatus(ctypes.Structure): # These match the values from the uniffi::rustcalls module CALL_SUCCESS = 0 CALL_ERROR = 1 - CALL_PANIC = 2 + CALL_UNEXPECTED_ERROR = 2 def __str__(self): if self.code == _UniffiRustCallStatus.CALL_SUCCESS: return "_UniffiRustCallStatus(CALL_SUCCESS)" elif self.code == _UniffiRustCallStatus.CALL_ERROR: return "_UniffiRustCallStatus(CALL_ERROR)" - elif self.code == _UniffiRustCallStatus.CALL_PANIC: - return "_UniffiRustCallStatus(CALL_PANIC)" + elif self.code == _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR: + return "_UniffiRustCallStatus(CALL_UNEXPECTED_ERROR)" else: return "_UniffiRustCallStatus()" @@ -53,7 +53,7 @@ def _uniffi_check_call_status(error_ffi_converter, call_status): raise InternalError("_rust_call_with_error: CALL_ERROR, but error_ffi_converter is None") else: raise error_ffi_converter.lift(call_status.error_buf) - elif call_status.code == _UniffiRustCallStatus.CALL_PANIC: + elif call_status.code == _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR: # When the rust code sees a panic, it tries to construct a _UniffiRustBuffer # with the message. But if that code panics, then it just sends back # an empty buffer. @@ -66,7 +66,20 @@ def _uniffi_check_call_status(error_ffi_converter, call_status): raise InternalError("Invalid _UniffiRustCallStatus code: {}".format( call_status.code)) -# A function pointer for a callback as defined by UniFFI. -# Rust definition `fn(handle: u64, method: u32, args: _UniffiRustBuffer, buf_ptr: *mut _UniffiRustBuffer) -> int` -_UNIFFI_FOREIGN_CALLBACK_T = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_ulonglong, ctypes.c_ulong, ctypes.POINTER(ctypes.c_char), ctypes.c_int, ctypes.POINTER(_UniffiRustBuffer)) +def _uniffi_trait_interface_call(call_status, make_call, write_return_value): + try: + return write_return_value(make_call()) + except Exception as e: + call_status.code = _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR + call_status.error_buf = {{ Type::String.borrow()|lower_fn }}(repr(e)) +def _uniffi_trait_interface_call_with_error(call_status, make_call, write_return_value, error_type, lower_error): + try: + try: + return write_return_value(make_call()) + except error_type as e: + call_status.code = _UniffiRustCallStatus.CALL_ERROR + call_status.error_buf = lower_error(e) + except Exception as e: + call_status.code = _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR + call_status.error_buf = {{ Type::String.borrow()|lower_fn }}(repr(e)) diff --git a/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py b/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py index 3e5450ad12..3845d8a8b9 100644 --- a/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py @@ -63,9 +63,22 @@ def _uniffi_check_api_checksums(lib): {%- for arg in callback.arguments() -%} {{ arg.type_().borrow()|ffi_type_name }}, {%- endfor -%} + {%- if callback.has_rust_call_status_arg() %} + ctypes.POINTER(_UniffiRustCallStatus), + {%- endif %} ) {%- endfor %} +# Define FFI structs +{%- for ffi_struct in ci.ffi_struct_definitions() %} +class {{ ffi_struct.name()|ffi_struct_name }}(ctypes.Structure): + _fields_ = [ + {%- for field in ffi_struct.fields() %} + ("{{ field.name()|var_name }}", {{ field.type_().borrow()|ffi_type_name }}), + {%- endfor %} + ] +{%- endfor %} + # A ctypes library to expose the extern-C FFI definitions. # This is an implementation detail which will be called internally by the public API. diff --git a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py index 6efb739156..ddcb9246bf 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py @@ -76,9 +76,9 @@ def __ne__(self, other: object) -> {{ ne.return_type().unwrap()|type_name }}: {% endfor %} {%- if obj.has_callback_interface() %} -{%- let callback_handler_class = format!("UniffiCallbackInterface{}", name) %} -{%- let callback_handler_obj = format!("uniffiCallbackInterface{}", name) %} {%- let ffi_init_callback = obj.ffi_init_callback() %} +{%- let vtable = obj.vtable().expect("trait interface should have a vtable") %} +{%- let vtable_methods = obj.vtable_methods() %} {% include "CallbackInterfaceImpl.py" %} {%- endif %} diff --git a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs index de2deb4410..8e30ee0980 100644 --- a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs +++ b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs @@ -153,9 +153,15 @@ mod filters { FfiType::RustBuffer(_) => "RustBuffer.by_value".to_string(), FfiType::ForeignBytes => "ForeignBytes".to_string(), FfiType::Callback(_) => unimplemented!("FFI Callbacks not implemented"), - // Callback interfaces are not yet implemented, but this needs to return something in - // order for the coverall tests to pass. - FfiType::ForeignCallback => ":pointer".to_string(), + // Note: this can't just be `unimplemented!()` because some of the FFI function + // definitions use references. Those FFI functions aren't actually used, so we just + // pick something that runs and makes some sense. Revisit this once the references + // are actually implemented. + FfiType::Reference(_) => ":pointer".to_string(), + FfiType::VoidPointer => ":pointer".to_string(), + FfiType::Struct(_) => { + unimplemented!("Structs are not implemented") + } FfiType::RustFutureHandle | FfiType::RustFutureContinuationData => { unimplemented!("Async functions are not implemented") } diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs index 9ba0e67820..14d1c01a0d 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs @@ -507,12 +507,17 @@ impl SwiftCodeOracle { nm.to_string().to_lower_camel_case() } - /// Get the idiomatic Python rendering of an FFI callback function + /// Get the idiomatic Swift rendering of an FFI callback function name fn ffi_callback_name(&self, nm: &str) -> String { format!("Uniffi{}", nm.to_upper_camel_case()) } - fn ffi_type_label_raw(&self, ffi_type: &FfiType) -> String { + /// Get the idiomatic Swift rendering of an FFI struct name + fn ffi_struct_name(&self, nm: &str) -> String { + format!("Uniffi{}", nm.to_upper_camel_case()) + } + + fn ffi_type_label(&self, ffi_type: &FfiType) -> String { match ffi_type { FfiType::Int8 => "Int8".into(), FfiType::UInt8 => "UInt8".into(), @@ -528,26 +533,18 @@ impl SwiftCodeOracle { FfiType::RustBuffer(_) => "RustBuffer".into(), FfiType::ForeignBytes => "ForeignBytes".into(), FfiType::Callback(name) => self.ffi_callback_name(name), - FfiType::ForeignCallback => "ForeignCallback".into(), - FfiType::RustFutureHandle | FfiType::RustFutureContinuationData => { - "UnsafeMutableRawPointer".into() + FfiType::Struct(name) => self.ffi_struct_name(name), + FfiType::Reference(inner) => { + format!("UnsafeMutablePointer<{}>", self.ffi_type_label(inner)) } - } - } - - fn ffi_type_label(&self, ffi_type: &FfiType) -> String { - match ffi_type { - FfiType::ForeignCallback + FfiType::VoidPointer | FfiType::RustFutureHandle - | FfiType::RustFutureContinuationData => { - format!("{} _Nonnull", self.ffi_type_label_raw(ffi_type)) - } - _ => self.ffi_type_label_raw(ffi_type), + | FfiType::RustFutureContinuationData => "UnsafeMutableRawPointer".into(), } } fn ffi_canonical_name(&self, ffi_type: &FfiType) -> String { - self.ffi_type_label_raw(ffi_type) + self.ffi_type_label(ffi_type) } /// Get the name of the protocol and class name for an object. @@ -641,10 +638,11 @@ pub mod filters { FfiType::Callback(name) => { format!("{} _Nonnull", SwiftCodeOracle.ffi_callback_name(name)) } - FfiType::ForeignCallback => "ForeignCallback _Nonnull".into(), - FfiType::RustFutureHandle | FfiType::RustFutureContinuationData => { - "void* _Nonnull".into() - } + FfiType::Struct(name) => SwiftCodeOracle.ffi_struct_name(name), + FfiType::Reference(inner) => format!("{}* _Nonnull", header_ffi_type_name(inner)?), + FfiType::VoidPointer + | FfiType::RustFutureHandle + | FfiType::RustFutureContinuationData => "void* _Nonnull".into(), }) } @@ -679,11 +677,16 @@ pub mod filters { Ok(oracle().enum_variant_name(nm)) } - /// Get the idiomatic Swift rendering of an ffi callback function name + /// Get the idiomatic Swift rendering of an FFI callback function name pub fn ffi_callback_name(nm: &str) -> Result { Ok(oracle().ffi_callback_name(nm)) } + /// Get the idiomatic Swift rendering of an FFI struct name + pub fn ffi_struct_name(nm: &str) -> Result { + Ok(oracle().ffi_struct_name(nm)) + } + /// Get the idiomatic Swift rendering of docstring pub fn docstring(docstring: &str, spaces: &i32) -> Result { let middle = textwrap::indent(&textwrap::dedent(docstring), " * "); diff --git a/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h index 352b804993..0c0a81652a 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h +++ b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h @@ -29,8 +29,6 @@ typedef struct RustBuffer uint8_t *_Nullable data; } RustBuffer; -typedef int32_t (*ForeignCallback)(uint64_t, int32_t, const uint8_t *_Nonnull, int32_t, RustBuffer *_Nonnull); - typedef struct ForeignBytes { int32_t len; @@ -50,15 +48,27 @@ typedef struct RustCallStatus { // Define FFI callback types {%- for callback in ci.ffi_callback_definitions() %} typedef - {%- match callback.return_type() %}{% when Some(return_type) %} {{ return_type|ffi_type_name }} {% when None %} void {% endmatch -%} + {%- match callback.return_type() %}{% when Some(return_type) %} {{ return_type|header_ffi_type_name }} {% when None %} void {% endmatch -%} (*{{ callback.name()|ffi_callback_name }})( {%- for arg in callback.arguments() -%} {{ arg.type_().borrow()|header_ffi_type_name }} - {%- if !loop.last %}, {% endif %} + {%- if !loop.last || callback.has_rust_call_status_arg() %}, {% endif %} {%- endfor -%} + {%- if callback.has_rust_call_status_arg() %} + RustCallStatus *_Nonnull uniffiCallStatus + {%- endif %} ); {%- endfor %} +// Define FFI structs +{%- for struct in ci.ffi_struct_definitions() %} +typedef struct {{ struct.name()|ffi_struct_name }} { + {%- for field in struct.fields() %} + {{ field.type_().borrow()|header_ffi_type_name }} {{ field.name()|var_name }}; + {%- endfor %} +} {{ struct.name()|ffi_struct_name }}; +{%- endfor %} + // Scaffolding functions {%- for func in ci.iter_ffi_function_definitions() %} {% match func.return_type() -%}{%- when Some with (type_) %}{{ type_|header_ffi_type_name }}{% when None %}void{% endmatch %} {{ func.name() }}( diff --git a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift index 53760a90a9..91d1d854e2 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift @@ -1,88 +1,61 @@ {%- if self.include_once_check("CallbackInterfaceRuntime.swift") %}{%- include "CallbackInterfaceRuntime.swift" %}{%- endif %} - -// Declaration and FfiConverters for {{ type_name }} Callback Interface - -fileprivate let {{ callback_handler }} : ForeignCallback = - { (handle: UniFFICallbackHandle, method: Int32, argsData: UnsafePointer, argsLen: Int32, out_buf: UnsafeMutablePointer) -> Int32 in - {% for meth in methods.iter() -%} - {%- let method_name = format!("invoke_{}", meth.name())|fn_name %} - - func {{ method_name }}(_ swiftCallbackInterface: {{ type_name }}, _ argsData: UnsafePointer, _ argsLen: Int32, _ out_buf: UnsafeMutablePointer) throws -> Int32 { - {%- if meth.arguments().len() > 0 %} - var reader = createReader(data: Data(bytes: argsData, count: Int(argsLen))) - {%- endif %} - - {%- match meth.return_type() %} - {%- when Some(return_type) %} - func makeCall() throws -> Int32 { - let result = {% if meth.throws() %} try{% endif %} swiftCallbackInterface.{{ meth.name()|fn_name }}( - {% for arg in meth.arguments() -%} - {% if !config.omit_argument_labels() %}{{ arg.name()|var_name }}: {% endif %} try {{ arg|read_fn }}(from: &reader) - {%- if !loop.last %}, {% endif %} - {% endfor -%} - ) - var writer = [UInt8]() - {{ return_type|write_fn }}(result, into: &writer) - out_buf.pointee = RustBuffer(bytes: writer) - return UNIFFI_CALLBACK_SUCCESS - } - {%- when None %} - func makeCall() throws -> Int32 { - {% if meth.throws() %}try {% endif %}swiftCallbackInterface.{{ meth.name()|fn_name }}( - {% for arg in meth.arguments() -%} - {% if !config.omit_argument_labels() %}{{ arg.name()|var_name }}: {% endif %} try {{ arg|read_fn }}(from: &reader) - {%- if !loop.last %}, {% endif %} - {% endfor -%} - ) - return UNIFFI_CALLBACK_SUCCESS - } - {%- endmatch %} - - {%- match meth.throws_type() %} - {%- when None %} - return try makeCall() - {%- when Some(error_type) %} - do { - return try makeCall() - } catch let error as {{ error_type|type_name }} { - out_buf.pointee = {{ error_type|lower_fn }}(error) - return UNIFFI_CALLBACK_ERROR - } - {%- endmatch %} - } - {%- endfor %} - - - switch method { - case IDX_CALLBACK_FREE: - {{ ffi_converter_name }}.handleMap.remove(handle: handle) - // Successful return - // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - return UNIFFI_CALLBACK_SUCCESS - {% for meth in methods.iter() -%} - {% let method_name = format!("invoke_{}", meth.name())|fn_name -%} - case {{ loop.index }}: - guard let cb = {{ ffi_converter_name }}.handleMap.get(handle: handle) else { - out_buf.pointee = {{ Type::String.borrow()|lower_fn }}("No callback in handlemap; this is a Uniffi bug") - return UNIFFI_CALLBACK_UNEXPECTED_ERROR - } - do { - return try {{ method_name }}(cb, argsData, argsLen, out_buf) - } catch let error { - out_buf.pointee = {{ Type::String.borrow()|lower_fn }}(String(describing: error)) - return UNIFFI_CALLBACK_UNEXPECTED_ERROR +{%- let trait_impl=format!("UniffiCallbackInterface{}", name) %} + +// Put the implementation in a struct so we don't pollute the top-level namespace +fileprivate struct {{ trait_impl }} { + + // Create the VTable using a series of closures. + // Swift automatically converts these into C callback functions. + static var vtable: {{ vtable|ffi_type_name }} = {{ vtable|ffi_type_name }}( + {%- for (ffi_callback, meth) in vtable_methods %} + {{ meth.name()|fn_name }}: { ( + {%- for arg in ffi_callback.arguments() %} + {{ arg.name()|var_name }}: {{ arg.type_().borrow()|ffi_type_name }}, + {%- endfor -%} + {%- if ffi_callback.has_rust_call_status_arg() %} + uniffiCallStatus: UnsafeMutablePointer + {%- endif %} + ) in + guard let uniffiObj = {{ ffi_converter_name }}.handleMap.get(handle: uniffiHandle) else { + uniffiCallStatus.pointee.code = CALL_UNEXPECTED_ERROR + uniffiCallStatus.pointee.errorBuf = {{ Type::String.borrow()|lower_fn }}("No callback in handlemap; this is a Uniffi bug") + return } - {% endfor %} - // This should never happen, because an out of bounds method index won't - // ever be used. Once we can catch errors, we should return an InternalError. - // https://github.com/mozilla/uniffi-rs/issues/351 - default: - // An unexpected error happened. - // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` - return UNIFFI_CALLBACK_UNEXPECTED_ERROR - } + let makeCall = { {% if meth.throws() %}try {% endif %}uniffiObj.{{ meth.name()|fn_name }}( + {%- for arg in meth.arguments() %} + {% if !config.omit_argument_labels() %} {{ arg.name()|arg_name }}: {% endif %}try {{ arg|lift_fn }}({{ arg.name()|var_name }}){% if !loop.last %},{% endif %} + {%- endfor %} + ) } + {% match meth.return_type() %} + {%- when Some(t) %} + let writeReturn = { uniffiOutReturn.pointee = {{ t|lower_fn }}($0) } + {%- when None %} + let writeReturn = { () } + {%- endmatch %} + + {%- match meth.throws_type() %} + {%- when None %} + uniffiTraitInterfaceCall( + callStatus: uniffiCallStatus, + makeCall: makeCall, + writeReturn: writeReturn + ) + {%- when Some(error_type) %} + uniffiTraitInterfaceCallWithError( + callStatus: uniffiCallStatus, + makeCall: makeCall, + writeReturn: writeReturn, + lowerError: {{ error_type|lower_fn }} + ) + {%- endmatch %} + }, + {%- endfor %} + uniffiFree: { (uniffiHandle: UInt64) -> () in + {{ ffi_converter_name }}.handleMap.delete(handle: uniffiHandle) + } + ) } private func {{ callback_init }}() { - {{ ffi_init_callback.name() }}({{ callback_handler }}) + {{ ffi_init_callback.name() }}(&{{ trait_impl }}.vtable) } diff --git a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift index f985ae3cd3..8cdd735b9a 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceTemplate.swift @@ -4,6 +4,8 @@ {%- let methods = cbi.methods() %} {%- let protocol_name = type_name.clone() %} {%- let protocol_docstring = cbi.docstring() %} +{%- let vtable = cbi.vtable() %} +{%- let vtable_methods = cbi.vtable_methods() %} {%- let ffi_init_callback = cbi.ffi_init_callback() %} {% include "Protocol.swift" %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift b/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift index 31b2dadfc2..d233d4b762 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift @@ -28,7 +28,7 @@ fileprivate enum UniffiInternalError: LocalizedError { fileprivate let CALL_SUCCESS: Int8 = 0 fileprivate let CALL_ERROR: Int8 = 1 -fileprivate let CALL_PANIC: Int8 = 2 +fileprivate let CALL_UNEXPECTED_ERROR: Int8 = 2 fileprivate let CALL_CANCELLED: Int8 = 3 fileprivate extension RustCallStatus { @@ -81,7 +81,7 @@ private func uniffiCheckCallStatus( throw UniffiInternalError.unexpectedRustCallError } - case CALL_PANIC: + case CALL_UNEXPECTED_ERROR: // 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. @@ -99,3 +99,33 @@ private func uniffiCheckCallStatus( throw UniffiInternalError.unexpectedRustCallStatusCode } } + +private func uniffiTraitInterfaceCall( + callStatus: UnsafeMutablePointer, + makeCall: () throws -> T, + writeReturn: (T) -> () +) { + do { + try writeReturn(makeCall()) + } catch let error { + callStatus.pointee.code = CALL_UNEXPECTED_ERROR + callStatus.pointee.errorBuf = {{ Type::String.borrow()|lower_fn }}(String(describing: error)) + } +} + +private func uniffiTraitInterfaceCallWithError( + callStatus: UnsafeMutablePointer, + makeCall: () throws -> T, + writeReturn: (T) -> (), + lowerError: (E) -> RustBuffer +) { + do { + try writeReturn(makeCall()) + } catch let error as E { + callStatus.pointee.code = CALL_ERROR + callStatus.pointee.errorBuf = lowerError(error) + } catch { + callStatus.pointee.code = CALL_UNEXPECTED_ERROR + callStatus.pointee.errorBuf = {{ Type::String.borrow()|lower_fn }}(String(describing: error)) + } +} diff --git a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift index 518beb5aa0..fc5d47516e 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift @@ -146,6 +146,8 @@ public class {{ impl_class_name }}: {%- if obj.has_callback_interface() %} {%- let callback_handler = format!("uniffiCallbackInterface{}", name) %} {%- let callback_init = format!("uniffiCallbackInit{}", name) %} +{%- let vtable = obj.vtable().expect("trait interface should have a vtable") %} +{%- let vtable_methods = obj.vtable_methods() %} {%- let ffi_init_callback = obj.ffi_init_callback() %} {% include "CallbackInterfaceImpl.swift" %} {%- endif %} diff --git a/uniffi_bindgen/src/interface/callbacks.rs b/uniffi_bindgen/src/interface/callbacks.rs index 9573e07932..90c34a9c17 100644 --- a/uniffi_bindgen/src/interface/callbacks.rs +++ b/uniffi_bindgen/src/interface/callbacks.rs @@ -33,9 +33,11 @@ //! # Ok::<(), anyhow::Error>(()) //! ``` +use std::iter; + use uniffi_meta::Checksum; -use super::ffi::FfiFunction; +use super::ffi::{FfiArgument, FfiCallbackFunction, FfiField, FfiFunction, FfiStruct, FfiType}; use super::object::Method; use super::{AsType, Type, TypeIterator}; @@ -70,7 +72,32 @@ impl CallbackInterface { } pub(super) fn derive_ffi_funcs(&mut self) { - self.ffi_init_callback = FfiFunction::callback_init(&self.module_path, &self.name); + self.ffi_init_callback = + FfiFunction::callback_init(&self.module_path, &self.name, vtable_name(&self.name)); + } + + /// FfiCallbacks to define for our methods. + pub fn ffi_callbacks(&self) -> Vec { + ffi_callbacks(&self.name, &self.methods) + } + + /// The VTable FFI type + pub fn vtable(&self) -> FfiType { + FfiType::Struct(vtable_name(&self.name)) + } + + /// the VTable struct to define. + pub fn vtable_definition(&self) -> FfiStruct { + vtable_struct(&self.name, &self.methods) + } + + /// Vec of (ffi_callback, method) pairs + pub fn vtable_methods(&self) -> Vec<(FfiCallbackFunction, &Method)> { + self.methods + .iter() + .enumerate() + .map(|(i, method)| (method_ffi_callback(&self.name, method, i), method)) + .collect() } pub fn iter_types(&self) -> TypeIterator<'_> { @@ -105,6 +132,62 @@ impl TryFrom for CallbackInterface { } } +/// [FfiCallbackFunction] functions for the methods of a callback/trait interface +pub fn ffi_callbacks(trait_name: &str, methods: &[Method]) -> Vec { + methods + .iter() + .enumerate() + .map(|(i, method)| method_ffi_callback(trait_name, method, i)) + .collect() +} + +pub fn method_ffi_callback(trait_name: &str, method: &Method, index: usize) -> FfiCallbackFunction { + FfiCallbackFunction { + name: method_ffi_callback_name(trait_name, index), + arguments: iter::once(FfiArgument::new("uniffi_handle", FfiType::UInt64)) + .chain(method.arguments().into_iter().map(Into::into)) + .chain(iter::once(match method.return_type() { + Some(t) => FfiArgument::new("uniffi_out_return", FfiType::from(t).reference()), + None => FfiArgument::new("uniffi_out_return", FfiType::VoidPointer), + })) + .collect(), + has_rust_call_status_arg: true, + return_type: None, + } +} + +/// [FfiStruct] for a callback/trait interface VTable +/// +/// This struct has a FfiCallbackFunction field for each method, plus extra fields for special +/// methods +pub fn vtable_struct(trait_name: &str, methods: &[Method]) -> FfiStruct { + FfiStruct { + name: vtable_name(trait_name), + fields: methods + .iter() + .enumerate() + .map(|(i, method)| { + FfiField::new( + method.name(), + FfiType::Callback(format!("CallbackInterface{trait_name}Method{i}")), + ) + }) + .chain([FfiField::new( + "uniffi_free", + FfiType::Callback("CallbackInterfaceFree".to_owned()), + )]) + .collect(), + } +} + +pub fn method_ffi_callback_name(trait_name: &str, index: usize) -> String { + format!("CallbackInterface{trait_name}Method{index}") +} + +pub fn vtable_name(trait_name: &str) -> String { + format!("VTableCallbackInterface{trait_name}") +} + #[cfg(test)] mod test { use super::super::ComponentInterface; diff --git a/uniffi_bindgen/src/interface/ffi.rs b/uniffi_bindgen/src/interface/ffi.rs index 2eeaffe9b7..7259b5bf32 100644 --- a/uniffi_bindgen/src/interface/ffi.rs +++ b/uniffi_bindgen/src/interface/ffi.rs @@ -47,16 +47,25 @@ 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, - /// Pointer to a callback function. The inner type is the name of the callback, which matches - /// one of the items in [crate::ComponentInterface::ffi_callback_definitions]. + /// Pointer to a callback function. The inner value which matches one of the items in + /// [crate::ComponentInterface::ffi_callback_definitions]. Callback(String), - /// Pointer to a callback function that handles all callbacks on the foreign language side. - ForeignCallback, + /// Pointer to a FFI struct (e.g. a VTable). The inner value matches one of the items in + /// [crate::ComponentInterface::ffi_struct_definitions]. + Struct(String), /// Pointer to a Rust future RustFutureHandle, RustFutureContinuationData, - // TODO: you can imagine a richer structural typesystem here, e.g. `Ref` or something. - // We don't need that yet and it's possible we never will, so it isn't here for now. + /// Pointer to an FfiType. + Reference(Box), + /// Opaque pointer + VoidPointer, +} + +impl FfiType { + pub fn reference(self) -> FfiType { + FfiType::Reference(Box::new(self)) + } } /// When passing data across the FFI, each `Type` value will be lowered into a corresponding @@ -150,12 +159,12 @@ pub struct FfiFunction { } impl FfiFunction { - pub fn callback_init(module_path: &str, trait_name: &str) -> Self { + pub fn callback_init(module_path: &str, trait_name: &str, vtable_name: String) -> Self { Self { - name: uniffi_meta::init_callback_fn_symbol_name(module_path, trait_name), + name: uniffi_meta::init_callback_vtable_fn_symbol_name(module_path, trait_name), arguments: vec![FfiArgument { - name: "handle".to_string(), - type_: FfiType::ForeignCallback, + name: "vtable".to_string(), + type_: FfiType::Struct(vtable_name).reference(), }], return_type: None, has_rust_call_status_arg: false, @@ -271,6 +280,49 @@ impl FfiCallbackFunction { } } +/// Represents a repr(C) struct used in the FFI +#[derive(Debug, Default, Clone)] +pub struct FfiStruct { + pub(super) name: String, + pub(super) fields: Vec, +} + +impl FfiStruct { + /// Get the name of this struct + pub fn name(&self) -> &str { + &self.name + } + + /// Get the fields for this struct + pub fn fields(&self) -> &[FfiField] { + &self.fields + } +} + +/// Represents a field of an [FfiStruct] +#[derive(Debug, Clone)] +pub struct FfiField { + pub(super) name: String, + pub(super) type_: FfiType, +} + +impl FfiField { + pub fn new(name: impl Into, type_: FfiType) -> Self { + Self { + name: name.into(), + type_, + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn type_(&self) -> FfiType { + self.type_.clone() + } +} + #[cfg(test)] mod test { // There's not really much to test here to be honest, diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index e6c646becd..823d3672cc 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -67,7 +67,7 @@ mod record; pub use record::{Field, Record}; pub mod ffi; -pub use ffi::{FfiArgument, FfiCallbackFunction, FfiFunction, FfiType}; +pub use ffi::{FfiArgument, FfiCallbackFunction, FfiFunction, FfiStruct, FfiType}; pub use uniffi_meta::Radix; use uniffi_meta::{ ConstructorMetadata, LiteralMetadata, NamespaceMetadata, ObjectMetadata, TraitMethodMetadata, @@ -216,16 +216,47 @@ impl ComponentInterface { /// Get the definitions for callback FFI functions /// /// These are defined by the foreign code and invoked by Rust. - pub fn ffi_callback_definitions(&self) -> impl IntoIterator { - [FfiCallbackFunction { - name: "RustFutureContinuationCallback".to_owned(), - arguments: vec![ - FfiArgument::new("data", FfiType::RustFutureContinuationData), - FfiArgument::new("poll_result", FfiType::Int8), - ], - return_type: None, - has_rust_call_status_arg: false, - }] + pub fn ffi_callback_definitions(&self) -> impl IntoIterator + '_ { + self.builtin_ffi_callback_definitions().into_iter().chain( + self.callback_interfaces + .iter() + .flat_map(|cbi| cbi.ffi_callbacks()) + .chain(self.objects.iter().flat_map(|o| o.ffi_callbacks())), + ) + } + + fn builtin_ffi_callback_definitions(&self) -> impl IntoIterator { + [ + FfiCallbackFunction { + name: "RustFutureContinuationCallback".to_owned(), + arguments: vec![ + FfiArgument::new("data", FfiType::RustFutureContinuationData), + FfiArgument::new("poll_result", FfiType::Int8), + ], + return_type: None, + has_rust_call_status_arg: false, + }, + FfiCallbackFunction { + name: "CallbackInterfaceFree".to_owned(), + arguments: vec![FfiArgument::new("handle", FfiType::UInt64)], + return_type: None, + has_rust_call_status_arg: false, + }, + ] + } + + /// Get the definitions for callback FFI functions + /// + /// These are defined by the foreign code and invoked by Rust. + pub fn ffi_struct_definitions(&self) -> impl IntoIterator + '_ { + self.callback_interface_definitions() + .iter() + .map(|cbi| cbi.vtable_definition()) + .chain( + self.object_definitions() + .iter() + .flat_map(|o| o.vtable_definition()), + ) } /// Get the definitions for every Callback Interface type in the interface. diff --git a/uniffi_bindgen/src/interface/object.rs b/uniffi_bindgen/src/interface/object.rs index 84778dd53b..48b4c34e1d 100644 --- a/uniffi_bindgen/src/interface/object.rs +++ b/uniffi_bindgen/src/interface/object.rs @@ -60,7 +60,8 @@ use anyhow::Result; use uniffi_meta::Checksum; -use super::ffi::{FfiArgument, FfiFunction, FfiType}; +use super::callbacks; +use super::ffi::{FfiArgument, FfiCallbackFunction, FfiFunction, FfiStruct, FfiType}; use super::function::{Argument, Callable}; use super::{AsType, ObjectImpl, Type, TypeIterator}; @@ -219,8 +220,11 @@ impl Object { self.ffi_func_free.return_type = None; self.ffi_func_free.is_object_free_function = true; if self.has_callback_interface() { - self.ffi_init_callback = - Some(FfiFunction::callback_init(&self.module_path, &self.name)); + self.ffi_init_callback = Some(FfiFunction::callback_init( + &self.module_path, + &self.name, + callbacks::vtable_name(&self.name), + )); } for cons in self.constructors.iter_mut() { @@ -236,6 +240,41 @@ impl Object { Ok(()) } + /// For trait interfaces, FfiCallbacks to define for our methods, otherwise an empty vec. + pub fn ffi_callbacks(&self) -> Vec { + if self.is_trait_interface() { + callbacks::ffi_callbacks(&self.name, &self.methods) + } else { + vec![] + } + } + + /// For trait interfaces, the VTable FFI type + pub fn vtable(&self) -> Option { + self.is_trait_interface() + .then(|| FfiType::Struct(callbacks::vtable_name(&self.name))) + } + + /// For trait interfaces, the VTable struct to define. Otherwise None. + pub fn vtable_definition(&self) -> Option { + self.is_trait_interface() + .then(|| callbacks::vtable_struct(&self.name, &self.methods)) + } + + /// Vec of (ffi_callback_name, method) pairs + pub fn vtable_methods(&self) -> Vec<(FfiCallbackFunction, &Method)> { + self.methods + .iter() + .enumerate() + .map(|(i, method)| { + ( + callbacks::method_ffi_callback(&self.name, method, i), + method, + ) + }) + .collect() + } + pub fn iter_types(&self) -> TypeIterator<'_> { Box::new( self.methods diff --git a/uniffi_core/src/ffi/callbackinterface.rs b/uniffi_core/src/ffi/callbackinterface.rs index 41c85dcac8..e7a4faab64 100644 --- a/uniffi_core/src/ffi/callbackinterface.rs +++ b/uniffi_core/src/ffi/callbackinterface.rs @@ -91,122 +91,20 @@ //! //! Uniffi generates a protocol or interface in client code in the foreign language must implement. //! -//! For each callback interface, a `CallbackInternals` (on the Foreign Language side) and `ForeignCallbackInternals` -//! (on Rust side) manages the process through a `ForeignCallback`. There is one `ForeignCallback` per callback interface. +//! For each callback interface, UniFFI defines a VTable. +//! This is a `repr(C)` struct where each field is a `repr(C)` callback function pointer. +//! There is one field for each method, plus an extra field for the `uniffi_free` method. +//! The foreign code registers one VTable per callback interface with Rust. //! -//! Passing a callback interface implementation from foreign language (e.g. `AndroidKeychain`) into Rust causes the -//! `KeychainCallbackInternals` to store the instance in a handlemap. -//! -//! The object handle is passed over to Rust, and used to instantiate a struct `KeychainProxy` which implements -//! the trait. This proxy implementation is generate by Uniffi. The `KeychainProxy` object is then passed to -//! client code as `Box`. -//! -//! Methods on `KeychainProxy` objects (e.g. `self.keychain.get("username".into())`) encode the arguments into a `RustBuffer`. -//! Using the `ForeignCallback`, it calls the `CallbackInternals` object on the foreign language side using the -//! object handle, and the method selector. -//! -//! The `CallbackInternals` object unpacks the arguments from the passed buffer, gets the object out from the handlemap, -//! and calls the actual implementation of the method. -//! -//! If there's a return value, it is packed up in to another `RustBuffer` and used as the return value for -//! `ForeignCallback`. The caller of `ForeignCallback`, the `KeychainProxy` unpacks the returned buffer into the correct -//! type and then returns to client code. +//! VTable methods have a similar signature to Rust scaffolding functions. +//! The one difference is that values are returned via an out pointer to work around a Python bug (https://bugs.python.org/issue5710). //! +//! The foreign object that implements the interface is represented by an opaque handle. +//! UniFFI generates a struct that implements the trait by calling VTable methods, passing the handle as the first parameter. +//! When the struct is dropped, the `uniffi_free` method is called. -use crate::{ForeignCallback, ForeignCallbackCell, Lift, LiftReturn, RustBuffer}; use std::fmt; -/// The method index used by the Drop trait to communicate to the foreign language side that Rust has finished with it, -/// and it can be deleted from the handle map. -pub const IDX_CALLBACK_FREE: u32 = 0; - -/// Result of a foreign callback invocation -#[repr(i32)] -#[derive(Debug, PartialEq, Eq)] -pub enum CallbackResult { - /// Successful call. - /// The return value is serialized to `buf_ptr`. - Success = 0, - /// Expected error. - /// This is returned when a foreign method throws an exception that corresponds to the Rust Err half of a Result. - /// The error value is serialized to `buf_ptr`. - Error = 1, - /// Unexpected error. - /// An error message string is serialized to `buf_ptr`. - UnexpectedError = 2, -} - -impl TryFrom for CallbackResult { - // On errors we return the unconverted value - type Error = i32; - - fn try_from(value: i32) -> Result { - match value { - 0 => Ok(Self::Success), - 1 => Ok(Self::Error), - 2 => Ok(Self::UnexpectedError), - n => Err(n), - } - } -} - -/// Struct to hold a foreign callback. -pub struct ForeignCallbackInternals { - callback_cell: ForeignCallbackCell, -} - -impl ForeignCallbackInternals { - pub const fn new() -> Self { - ForeignCallbackInternals { - callback_cell: ForeignCallbackCell::new(), - } - } - - pub fn set_callback(&self, callback: ForeignCallback) { - self.callback_cell.set(callback); - } - - /// Invoke a callback interface method on the foreign side and return the result - pub fn invoke_callback(&self, handle: u64, method: u32, args: RustBuffer) -> R - where - R: LiftReturn, - { - let mut ret_rbuf = RustBuffer::new(); - let callback = self.callback_cell.get(); - let raw_result = unsafe { - callback( - handle, - method, - args.data_pointer(), - args.len() as i32, - &mut ret_rbuf, - ) - }; - RustBuffer::destroy(args); - let result = CallbackResult::try_from(raw_result) - .unwrap_or_else(|code| panic!("Callback failed with unexpected return code: {code}")); - match result { - CallbackResult::Success => R::lift_callback_return(ret_rbuf), - CallbackResult::Error => R::lift_callback_error(ret_rbuf), - CallbackResult::UnexpectedError => { - let reason = if !ret_rbuf.is_empty() { - match >::try_lift(ret_rbuf) { - Ok(s) => s, - Err(e) => { - log::error!("{{ trait_name }} Error reading ret_buf: {e}"); - String::from("[Error reading reason]") - } - } - } else { - RustBuffer::destroy(ret_rbuf); - String::from("[Unknown Reason]") - }; - R::handle_callback_unexpected_error(UnexpectedUniFFICallbackError { reason }) - } - } - } -} - /// Used when internal/unexpected error happened when calling a foreign callback, for example when /// a unknown exception is raised /// @@ -217,8 +115,10 @@ pub struct UnexpectedUniFFICallbackError { } impl UnexpectedUniFFICallbackError { - pub fn from_reason(reason: String) -> Self { - Self { reason } + pub fn new(reason: impl fmt::Display) -> Self { + Self { + reason: reason.to_string(), + } } } diff --git a/uniffi_core/src/ffi/foreigncallbacks.rs b/uniffi_core/src/ffi/foreigncallbacks.rs index 68d9a0d9b1..326ff12747 100644 --- a/uniffi_core/src/ffi/foreigncallbacks.rs +++ b/uniffi_core/src/ffi/foreigncallbacks.rs @@ -8,73 +8,32 @@ //! code loads the exported library. For each callback type, we also define a "cell" type for //! storing the callback. -use std::sync::atomic::{AtomicUsize, Ordering}; - -use crate::RustBuffer; - -/// ForeignCallback is the Rust representation of a foreign language function. -/// It is the basis for all callbacks interfaces. It is registered exactly once per callback interface, -/// at library start up time. -/// Calling this method is only done by generated objects which mirror callback interfaces objects in the foreign language. -/// -/// * The `handle` is the key into a handle map on the other side of the FFI used to look up the foreign language object -/// that implements the callback interface/trait. -/// * The `method` selector specifies the method that will be called on the object, by looking it up in a list of methods from -/// the IDL. The list is 1 indexed. Note that the list of methods is generated by UniFFI from the IDL and used in all -/// bindings, so we can rely on the method list being stable within the same run of UniFFI. -/// * `args_data` and `args_len` represents a serialized buffer of arguments to the function. The scaffolding code -/// writes the callback arguments to this buffer, in order, using `FfiConverter.write()`. The bindings code reads the -/// arguments from the buffer and passes them to the user's callback. -/// * `buf_ptr` is a pointer to where the resulting buffer will be written. UniFFI will allocate a -/// buffer to write the result into. -/// * Callbacks return one of the `CallbackResult` values -/// Note: The output buffer might still contain 0 bytes of data. -pub type ForeignCallback = unsafe extern "C" fn( - handle: u64, - method: u32, - args_data: *const u8, - args_len: i32, - buf_ptr: *mut RustBuffer, -) -> i32; - -/// Store a [ForeignCallback] pointer -pub(crate) struct ForeignCallbackCell(AtomicUsize); - -/// Macro to define foreign callback types as well as the callback cell. -macro_rules! impl_foreign_callback_cell { - ($callback_type:ident, $cell_type:ident) => { - // Overly-paranoid sanity checking to ensure that these types are - // convertible between each-other. `transmute` actually should check this for - // us too, but this helps document the invariants we rely on in this code. - // - // Note that these are guaranteed by - // https://rust-lang.github.io/unsafe-code-guidelines/layout/function-pointers.html - // and thus this is a little paranoid. - static_assertions::assert_eq_size!(usize, $callback_type); - static_assertions::assert_eq_size!(usize, Option<$callback_type>); - - impl $cell_type { - pub const fn new() -> Self { - Self(AtomicUsize::new(0)) - } - - pub fn set(&self, callback: $callback_type) { - // Store the pointer using Ordering::Relaxed. This is sufficient since callback - // should be set at startup, before there's any chance of using them. - self.0.store(callback as usize, Ordering::Relaxed); - } - - pub fn get(&self) -> $callback_type { - let ptr_value = self.0.load(Ordering::Relaxed); - unsafe { - // SAFETY: self.0 was set in `set` from our function pointer type, so - // it's safe to transmute it back here. - ::std::mem::transmute::>(ptr_value) - .expect("Bug: callback not set. This is likely a uniffi bug.") - } - } +use std::{ + ptr::{null_mut, NonNull}, + sync::atomic::{AtomicPtr, Ordering}, +}; + +// Cell type that stores any NonNull +#[doc(hidden)] +pub struct UniffiForeignPointerCell(AtomicPtr); + +impl UniffiForeignPointerCell { + pub const fn new() -> Self { + Self(AtomicPtr::new(null_mut())) + } + + pub fn set(&self, callback: NonNull) { + self.0.store(callback.as_ptr(), Ordering::Relaxed); + } + + pub fn get(&self) -> &T { + unsafe { + NonNull::new(self.0.load(Ordering::Relaxed)) + .expect("Foreign pointer not set. This is likely a uniffi bug.") + .as_mut() } - }; + } } -impl_foreign_callback_cell!(ForeignCallback, ForeignCallbackCell); +unsafe impl Send for UniffiForeignPointerCell {} +unsafe impl Sync for UniffiForeignPointerCell {} diff --git a/uniffi_core/src/ffi/rustcalls.rs b/uniffi_core/src/ffi/rustcalls.rs index 51204ef261..16b0c76f2e 100644 --- a/uniffi_core/src/ffi/rustcalls.rs +++ b/uniffi_core/src/ffi/rustcalls.rs @@ -56,6 +56,13 @@ pub struct RustCallStatus { } impl RustCallStatus { + pub fn new() -> Self { + Self { + code: RustCallStatusCode::Success, + error_buf: MaybeUninit::new(RustBuffer::new()), + } + } + pub fn cancelled() -> Self { Self { code: RustCallStatusCode::Cancelled, diff --git a/uniffi_core/src/ffi_converter_impls.rs b/uniffi_core/src/ffi_converter_impls.rs index 5be5f04da5..aec093154a 100644 --- a/uniffi_core/src/ffi_converter_impls.rs +++ b/uniffi_core/src/ffi_converter_impls.rs @@ -456,7 +456,11 @@ unsafe impl LowerReturn for () { } unsafe impl LiftReturn for () { - fn lift_callback_return(_buf: RustBuffer) -> Self {} + type ReturnType = (); + + fn try_lift_successful_return(_: ()) -> Result { + Ok(()) + } const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_UNIT); } @@ -493,13 +497,15 @@ where unsafe impl LiftReturn for Result where R: LiftReturn, - E: Lift + ConvertError, + E: Lift + ConvertError, { - fn lift_callback_return(buf: RustBuffer) -> Self { - Ok(R::lift_callback_return(buf)) + type ReturnType = R::ReturnType; + + fn try_lift_successful_return(v: R::ReturnType) -> Result { + R::try_lift_successful_return(v).map(Ok) } - fn lift_callback_error(buf: RustBuffer) -> Self { + fn lift_error(buf: RustBuffer) -> Self { match E::try_lift_from_rust_buffer(buf) { Ok(lifted_error) => Err(lifted_error), Err(anyhow_error) => { diff --git a/uniffi_core/src/ffi_converter_traits.rs b/uniffi_core/src/ffi_converter_traits.rs index 3b5914e32f..987a47c414 100644 --- a/uniffi_core/src/ffi_converter_traits.rs +++ b/uniffi_core/src/ffi_converter_traits.rs @@ -51,7 +51,10 @@ use std::{borrow::Borrow, sync::Arc}; use anyhow::bail; use bytes::Buf; -use crate::{FfiDefault, MetadataBuffer, Result, RustBuffer, UnexpectedUniFFICallbackError}; +use crate::{ + FfiDefault, MetadataBuffer, Result, RustBuffer, RustCallStatus, RustCallStatusCode, + UnexpectedUniFFICallbackError, +}; /// Generalized FFI conversions /// @@ -302,14 +305,41 @@ pub unsafe trait LowerReturn: Sized { /// These traits should not be used directly, only in generated code, and the generated code should /// have fixture tests to test that everything works correctly together. pub unsafe trait LiftReturn: Sized { - /// Lift a Rust value for a callback interface method result - fn lift_callback_return(buf: RustBuffer) -> Self; + /// FFI return type for trait interfaces + type ReturnType; + + /// Lift a successfully returned value from a trait interface + fn try_lift_successful_return(v: Self::ReturnType) -> Result; + + /// Lift a foreign returned value from a trait interface + /// + /// When we call a foreign-implemented trait interface method, we pass a &mut RustCallStatus + /// and get [Self::ReturnType] returned. This method takes both of those and lifts `Self` from + /// it. + fn lift_foreign_return(ffi_return: Self::ReturnType, call_status: RustCallStatus) -> Self { + match call_status.code { + RustCallStatusCode::Success => Self::try_lift_successful_return(ffi_return) + .unwrap_or_else(|e| { + Self::handle_callback_unexpected_error(UnexpectedUniFFICallbackError::new(e)) + }), + RustCallStatusCode::Error => { + Self::lift_error(unsafe { call_status.error_buf.assume_init() }) + } + _ => { + let e = >::try_lift(unsafe { + call_status.error_buf.assume_init() + }) + .unwrap_or_else(|e| format!("(Error lifting message: {e}")); + Self::handle_callback_unexpected_error(UnexpectedUniFFICallbackError::new(e)) + } + } + } /// Lift a Rust value for a callback interface method error result /// /// This is called for "expected errors" -- the callback method returns a Result<> type and the /// foreign code throws an exception that corresponds to the error type. - fn lift_callback_error(_buf: RustBuffer) -> Self { + fn lift_error(_buf: RustBuffer) -> Self { panic!("Callback interface method returned unexpected error") } @@ -439,9 +469,10 @@ macro_rules! derive_ffi_traits { (impl $(<$($generic:ident),*>)? $(::uniffi::)? LiftReturn<$ut:path> for $ty:ty $(where $($where:tt)*)?) => { unsafe impl $(<$($generic),*>)* $crate::LiftReturn<$ut> for $ty $(where $($where)*)* { - fn lift_callback_return(buf: $crate::RustBuffer) -> Self { - >::try_lift_from_rust_buffer(buf) - .expect("Error reading callback interface result") + type ReturnType = >::FfiType; + + fn try_lift_successful_return(v: Self::ReturnType) -> $crate::Result { + >::try_lift(v) } const TYPE_ID_META: $crate::MetadataBuffer = >::TYPE_ID_META; diff --git a/uniffi_macros/src/export/callback_interface.rs b/uniffi_macros/src/export/callback_interface.rs index 576df52c98..01879af173 100644 --- a/uniffi_macros/src/export/callback_interface.rs +++ b/uniffi_macros/src/export/callback_interface.rs @@ -10,10 +10,16 @@ use crate::{ }, }; use proc_macro2::{Span, TokenStream}; -use quote::quote; +use quote::{format_ident, quote}; use std::iter; use syn::Ident; +/// Generate a trait impl that calls foreign callbacks +/// +/// This generates: +/// * A `repr(C)` VTable struct where each field is the FFI function for the trait method. +/// * A FFI function for foreign code to set their VTable for the interface +/// * An implementation of the trait using that VTable pub(super) fn trait_impl( mod_path: &str, trait_ident: &Ident, @@ -21,30 +27,51 @@ pub(super) fn trait_impl( ) -> syn::Result { let trait_name = ident_to_string(trait_ident); let trait_impl_ident = trait_impl_ident(&trait_name); - let internals_ident = internals_ident(&trait_name); + let vtable_type = format_ident!("UniFfiTraitVtable{trait_name}"); + let vtable_cell = format_ident!("UNIFFI_TRAIT_CELL_{}", trait_name.to_uppercase()); let init_ident = Ident::new( - &uniffi_meta::init_callback_fn_symbol_name(mod_path, &trait_name), + &uniffi_meta::init_callback_vtable_fn_symbol_name(mod_path, &trait_name), Span::call_site(), ); - - let trait_impl_methods = items + let methods = items .iter() .map(|item| match item { - ImplItem::Method(sig) => gen_method_impl(sig, &internals_ident), - _ => unreachable!("traits have no constructors"), + ImplItem::Constructor(sig) => Err(syn::Error::new( + sig.span, + "Constructors not allowed in trait interfaces", + )), + ImplItem::Method(sig) => Ok(sig), }) - .collect::>()?; + .collect::>>()?; + + let vtable_fields = methods.iter() + .map(|sig| { + let ident = &sig.ident; + let params = sig.scaffolding_params(); + let lift_return = sig.lift_return_impl(); + quote! { + #ident: extern "C" fn(handle: u64, #(#params,)* &mut #lift_return::ReturnType, &mut ::uniffi::RustCallStatus), + } + }); + + let trait_impl_methods = methods + .iter() + .map(|sig| gen_method_impl(sig, &vtable_cell)) + .collect::>>()?; + Ok(quote! { - #[doc(hidden)] - static #internals_ident: ::uniffi::ForeignCallbackInternals = ::uniffi::ForeignCallbackInternals::new(); + struct #vtable_type { + #(#vtable_fields)* + uniffi_free: extern "C" fn(handle: u64), + } + + static #vtable_cell: ::uniffi::UniffiForeignPointerCell::<#vtable_type> = ::uniffi::UniffiForeignPointerCell::<#vtable_type>::new(); - #[doc(hidden)] #[no_mangle] - pub extern "C" fn #init_ident(callback: ::uniffi::ForeignCallback) { - #internals_ident.set_callback(callback); + extern "C" fn #init_ident(vtable: ::std::ptr::NonNull<#vtable_type>) { + #vtable_cell.set(vtable); } - #[doc(hidden)] #[derive(Debug)] struct #trait_impl_ident { handle: u64, @@ -56,18 +83,17 @@ pub(super) fn trait_impl( } } - impl ::std::ops::Drop for #trait_impl_ident { - fn drop(&mut self) { - #internals_ident.invoke_callback::<(), crate::UniFfiTag>( - self.handle, uniffi::IDX_CALLBACK_FREE, Default::default() - ) - } - } - ::uniffi::deps::static_assertions::assert_impl_all!(#trait_impl_ident: ::core::marker::Send); impl #trait_ident for #trait_impl_ident { - #trait_impl_methods + #(#trait_impl_methods)* + } + + impl ::std::ops::Drop for #trait_impl_ident { + fn drop(&mut self) { + let vtable = #vtable_cell.get(); + (vtable.uniffi_free)(self.handle); + } } }) } @@ -79,16 +105,6 @@ pub fn trait_impl_ident(trait_name: &str) -> Ident { ) } -pub fn internals_ident(trait_name: &str) -> Ident { - Ident::new( - &format!( - "UNIFFI_FOREIGN_CALLBACK_INTERNALS_{}", - trait_name.to_ascii_uppercase() - ), - Span::call_site(), - ) -} - pub fn ffi_converter_callback_interface_impl( trait_ident: &Ident, trait_impl_ident: &Ident, @@ -131,49 +147,55 @@ pub fn ffi_converter_callback_interface_impl( } } -fn gen_method_impl(sig: &FnSignature, internals_ident: &Ident) -> syn::Result { +/// Generate a single method for [trait_impl]. This implements a trait method by invoking a +/// foreign-supplied callback. +fn gen_method_impl(sig: &FnSignature, vtable_cell: &Ident) -> syn::Result { let FnSignature { ident, return_ty, kind, receiver, + name, + span, .. } = sig; - let index = match kind { - // Note: the callback index is 1-based, since 0 is reserved for the free function - FnKind::TraitMethod { index, .. } => index + 1, - k => { - return Err(syn::Error::new( - sig.span, - format!( - "Internal UniFFI error: Unexpected function kind for callback interface {k:?}" - ), - )); - } - }; + + if !matches!(kind, FnKind::TraitMethod { .. }) { + return Err(syn::Error::new( + *span, + format!( + "Internal UniFFI error: Unexpected function kind for callback interface {name}: {kind:?}", + ), + )); + } let self_param = match receiver { + Some(ReceiverArg::Ref) => quote! { &self }, + Some(ReceiverArg::Arc) => quote! { self: Arc }, None => { return Err(syn::Error::new( - sig.span, + *span, "callback interface methods must take &self as their first argument", )); } - Some(ReceiverArg::Ref) => quote! { &self }, - Some(ReceiverArg::Arc) => quote! { self: Arc }, }; + let params = sig.params(); - let buf_ident = Ident::new("uniffi_args_buf", Span::call_site()); - let write_exprs = sig.write_exprs(&buf_ident); + let lower_exprs = sig.args.iter().map(|a| { + let lower_impl = a.lower_impl(); + let ident = &a.ident; + quote! { #lower_impl::lower(#ident) } + }); + + let lift_return = sig.lift_return_impl(); Ok(quote! { fn #ident(#self_param, #(#params),*) -> #return_ty { - #[allow(unused_mut)] - let mut #buf_ident = ::std::vec::Vec::new(); - #(#write_exprs;)* - let uniffi_args_rbuf = uniffi::RustBuffer::from_vec(#buf_ident); - - #internals_ident.invoke_callback::<#return_ty, crate::UniFfiTag>(self.handle, #index, uniffi_args_rbuf) + let vtable = #vtable_cell.get(); + let mut uniffi_call_status = ::uniffi::RustCallStatus::new(); + let mut return_value: #lift_return::ReturnType = ::uniffi::FfiDefault::ffi_default(); + (vtable.#ident)(self.handle, #(#lower_exprs,)* &mut return_value, &mut uniffi_call_status); + #lift_return::lift_foreign_return(return_value, uniffi_call_status) } }) } diff --git a/uniffi_macros/src/export/scaffolding.rs b/uniffi_macros/src/export/scaffolding.rs index aec4916de8..0e0da87a2e 100644 --- a/uniffi_macros/src/export/scaffolding.rs +++ b/uniffi_macros/src/export/scaffolding.rs @@ -231,7 +231,7 @@ pub(super) fn gen_ffi_function( let ffi_ident = sig.scaffolding_fn_ident()?; let name = &sig.name; - let return_impl = &sig.return_impl(); + let return_impl = &sig.lower_return_impl(); Ok(if !sig.is_async { quote! { diff --git a/uniffi_macros/src/fnsig.rs b/uniffi_macros/src/fnsig.rs index bcf1e21876..cc554da856 100644 --- a/uniffi_macros/src/fnsig.rs +++ b/uniffi_macros/src/fnsig.rs @@ -111,13 +111,20 @@ impl FnSignature { }) } - pub fn return_impl(&self) -> TokenStream { + pub fn lower_return_impl(&self) -> TokenStream { let return_ty = &self.return_ty; quote! { <#return_ty as ::uniffi::LowerReturn> } } + pub fn lift_return_impl(&self) -> TokenStream { + let return_ty = &self.return_ty; + quote! { + <#return_ty as ::uniffi::LiftReturn> + } + } + /// Generate a closure that tries to lift all arguments into a tuple. /// /// The closure moves all scaffolding arguments into itself and returns: @@ -162,14 +169,6 @@ impl FnSignature { quote! { #(#args),* } } - /// Write expressions for each of our arguments - pub fn write_exprs<'a>( - &'a self, - buf_ident: &'a Ident, - ) -> impl Iterator + 'a { - self.args.iter().map(|a| a.write_expr(buf_ident)) - } - /// Parameters expressions for each of our arguments pub fn params(&self) -> impl Iterator + '_ { self.args.iter().map(NamedArg::param) @@ -501,13 +500,6 @@ impl NamedArg { quote! { #ident: #lift_impl::FfiType } } - /// Generate the expression to write the scaffolding parameter for this arg - pub(crate) fn write_expr(&self, buf_ident: &Ident) -> TokenStream { - let ident = &self.ident; - let lower_impl = self.lower_impl(); - quote! { #lower_impl::write(#ident, &mut #buf_ident) } - } - pub(crate) fn arg_metadata(&self) -> TokenStream { let name = &self.name; let lift_impl = self.lift_impl(); diff --git a/uniffi_meta/src/ffi_names.rs b/uniffi_meta/src/ffi_names.rs index a58977b790..5c931a09e3 100644 --- a/uniffi_meta/src/ffi_names.rs +++ b/uniffi_meta/src/ffi_names.rs @@ -46,9 +46,12 @@ pub fn free_fn_symbol_name(namespace: &str, object_name: &str) -> String { } /// FFI symbol name for the `init_callback` function for a callback interface -pub fn init_callback_fn_symbol_name(namespace: &str, callback_interface_name: &str) -> String { +pub fn init_callback_vtable_fn_symbol_name( + namespace: &str, + callback_interface_name: &str, +) -> String { let callback_interface_name = callback_interface_name.to_ascii_lowercase(); - format!("uniffi_{namespace}_fn_init_callback_{callback_interface_name}") + format!("uniffi_{namespace}_fn_init_callback_vtable_{callback_interface_name}") } /// FFI checksum symbol name for a top-level function