From 9f3a7d5fec522175dde1ec61b245038229378214 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Fri, 27 Oct 2023 19:53:30 -0400 Subject: [PATCH] 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. Switched the keywords fixture tests to use traits rather than callback interfaces. The old tests were testing unsupported things like returning callback interfaces which just happened to pass. 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. --- fixtures/keywords/kotlin/src/keywords.udl | 3 +- fixtures/keywords/kotlin/src/lib.rs | 4 +- fixtures/keywords/rust/src/keywords.udl | 3 +- fixtures/keywords/rust/src/lib.rs | 6 +- .../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 | 28 +++- .../templates/NamespaceLibraryTemplate.py | 13 ++ .../python/templates/ObjectTemplate.py | 4 +- .../src/bindings/ruby/gen_ruby/mod.rs | 10 +- .../src/bindings/swift/gen_swift/mod.rs | 53 +++---- .../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 | 125 ++------------- 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 +- 39 files changed, 836 insertions(+), 630 deletions(-) diff --git a/fixtures/keywords/kotlin/src/keywords.udl b/fixtures/keywords/kotlin/src/keywords.udl index 6cdd7a6f07..f9a6c76597 100644 --- a/fixtures/keywords/kotlin/src/keywords.udl +++ b/fixtures/keywords/kotlin/src/keywords.udl @@ -7,7 +7,8 @@ interface break { void object(u8? class); }; -callback interface continue { +[Trait] +interface continue { return return(return v); continue? continue(); record break(break? v); diff --git a/fixtures/keywords/kotlin/src/lib.rs b/fixtures/keywords/kotlin/src/lib.rs index d5c63ab780..bfdd8bd117 100644 --- a/fixtures/keywords/kotlin/src/lib.rs +++ b/fixtures/keywords/kotlin/src/lib.rs @@ -15,9 +15,9 @@ impl r#break { } #[allow(non_camel_case_types)] -trait r#continue { +trait r#continue: Send + Sync { fn r#return(&self, v: r#return) -> r#return; - fn r#continue(&self) -> Option>; + fn r#continue(&self) -> Option>; fn r#break(&self, _v: Option>) -> HashMap>; fn r#while(&self, _v: Vec) -> r#while; fn class(&self, _v: HashMap>) -> Option>>; diff --git a/fixtures/keywords/rust/src/keywords.udl b/fixtures/keywords/rust/src/keywords.udl index 1edc1cdb6b..a66335953a 100644 --- a/fixtures/keywords/rust/src/keywords.udl +++ b/fixtures/keywords/rust/src/keywords.udl @@ -13,7 +13,8 @@ interface break { void async(u8? yield); }; -callback interface continue { +[Trait] +interface continue { return return(return v); continue? continue(); record break(break? v); diff --git a/fixtures/keywords/rust/src/lib.rs b/fixtures/keywords/rust/src/lib.rs index 0b93557307..b3a7610350 100644 --- a/fixtures/keywords/rust/src/lib.rs +++ b/fixtures/keywords/rust/src/lib.rs @@ -16,15 +16,15 @@ impl r#break { pub fn r#break(&self, v: HashMap>) -> Option>> { Some(v) } - fn r#continue(&self, _v: Vec>) {} + fn r#continue(&self, _v: Vec>) {} pub fn r#yield(&self, _async: u8) {} pub fn r#async(&self, _yield: Option) {} } #[allow(non_camel_case_types)] -pub trait r#continue { +pub trait r#continue: Send + Sync { fn r#return(&self, v: r#return) -> r#return; - fn r#continue(&self) -> Option>; + fn r#continue(&self) -> Option>; fn r#break(&self, _v: Option>) -> HashMap>; fn r#while(&self, _v: Vec) -> r#while; fn r#yield(&self, _v: HashMap>) -> Option>>; diff --git a/fixtures/proc-macro/tests/bindings/test_proc_macro.swift b/fixtures/proc-macro/tests/bindings/test_proc_macro.swift index b8029c69f0..50323d0472 100644 --- a/fixtures/proc-macro/tests/bindings/test_proc_macro.swift +++ b/fixtures/proc-macro/tests/bindings/test_proc_macro.swift @@ -80,8 +80,7 @@ class SwiftTestCallbackInterface : TestCallbackInterface { } func callbackHandler(h: Object) -> UInt32 { - var v = h.takeError(e: BasicError.InvalidInput) - return v + return h.takeError(e: BasicError.InvalidInput) } } diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs index 170961000d..9b542adf85 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs @@ -300,7 +300,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. @@ -308,11 +313,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)), @@ -320,6 +330,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 @@ -337,8 +366,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(), } } @@ -493,6 +523,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())) @@ -508,6 +543,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 d2e6048188..16bbf7c757 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: Handle, 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: RustCallStatus, + {%- 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 d0e0686322..5f4d12ddf2 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: Handle, 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 4a88d56aef..39c93a702d 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 eb878d61e3..c623c37734 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 RustCallStatus : Structure() { @JvmField var code: Byte = 0 @@ -9,15 +13,15 @@ internal open class RustCallStatus : Structure() { class ByValue: RustCallStatus(), 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: RustCallStatus, + 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: RustCallStatus, + 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 7011aab6af..7498a3b559 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: RustCallStatus, + {%- 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 797d595f11..74b37a3883 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt @@ -157,8 +157,8 @@ open class {{ impl_class_name }} : FFIObject, {{ interface_name }} { } {%- if obj.is_trait_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 dfbea24074..ca0dc7da0b 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) = rustCall() { 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 60bb0b9a80..2297627043 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(), } } @@ -510,11 +518,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 f9a4768a5c..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) - - # Successfull 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..b4dad8da12 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,21 @@ 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: + import traceback + 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 c68808d01f..c5094e1b17 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.is_trait_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 0107d5ece9..c2c55e8018 100644 --- a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs +++ b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs @@ -153,9 +153,13 @@ 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: references are not really used by the Ruby bindings yet, maybe this should + // change to a different type when they 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 4e7b3a472b..bf55c6b9b8 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. @@ -642,18 +639,11 @@ pub mod filters { FfiType::Callback(name) => { format!("{} _Nonnull", SwiftCodeOracle.ffi_callback_name(name)) } - FfiType::ForeignCallback => "ForeignCallback _Nonnull".into(), -<<<<<<< HEAD - FfiType::RustFutureContinuationCallback => { - "UniFfiRustFutureContinuation _Nonnull".into() - } -======= - FfiType::ForeignExecutorCallback => "UniFfiForeignExecutorCallback _Nonnull".into(), - FfiType::ForeignExecutorHandle => "size_t".into(), ->>>>>>> 1f20f993c (Adding `FfiType::Callback`) - 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(), }) } @@ -688,11 +678,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 157da46128..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) - // Sucessful 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 173a98b8de..553e60045f 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.is_trait_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..5ecb582567 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 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 the VTable + pub fn name(&self) -> &str { + &self.name + } + + /// Get the ( + 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 da0b091fb5..81090ec37d 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 06e32b3a37..87fc4cbc01 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}; @@ -215,8 +216,11 @@ impl Object { self.ffi_func_free.return_type = None; self.ffi_func_free.is_object_free_function = true; if self.is_trait_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() { @@ -232,6 +236,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 7be66880bb..e7a4faab64 100644 --- a/uniffi_core/src/ffi/callbackinterface.rs +++ b/uniffi_core/src/ffi/callbackinterface.rs @@ -91,121 +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, - ) - }; - 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 /// @@ -216,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 53265393c0..9f801c35a4 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 e4afe099b3..41568c7487 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 6dcf702b97..68df6c34ed 100644 --- a/uniffi_macros/src/export/callback_interface.rs +++ b/uniffi_macros/src/export/callback_interface.rs @@ -8,10 +8,16 @@ use crate::{ util::{create_metadata_items, ident_to_string, mod_path, tagged_impl_header}, }; 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, @@ -19,30 +25,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 lower_return = sig.lower_return_impl(); + quote! { + #ident: extern "C" fn(handle: u64, #(#params,)* &mut #lower_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_dec_ref: 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, @@ -54,18 +81,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_dec_ref)(self.handle); + } } }) } @@ -77,16 +103,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 98843a3cde..4da24b028f 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 610672ad83..089dac6948 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) @@ -442,13 +441,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