From f844e0c57681b2d6c524e27509f8283d6f1eaff5 Mon Sep 17 00:00:00 2001 From: James Hugman Date: Tue, 13 Oct 2020 00:09:12 +0100 Subject: [PATCH] Add callback interfaces for Kotlin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We'd like to have a Rust trait: ```rust trait FetchClient { fn fetch(url: String) -> String } // later impl Nimbus { fn set_fetch_client(client: Box) {} } ``` Declared in the IDL: ```idl interface Nimbus { set_fetch_client(FetchClient client); update_experiments(); } callback interface FetchClient { string fetch(string url); }; ``` And used in Kotlin: ```kotlin class ConceptFetchClient: FetchClient { override fun fetch(url: String): String { ... } } nimbus.setFetchClient(ConceptFetchClient()) nimbus.updateExperiments() // uses ConceptFetchClient ``` This commit sends JNA Callbacks to send a callback interface to Rust (a.k.a. `ForeignCallback`). This implementation of the `ForeignCallback` is individual to the callback interface— on the kotlin side; in this case this is called: `CallbackInterfaceFetchClientFFI`. As the `ConceptFetchClient` is sent from kotlin, it is placed in a kotlin `ConcurrentHandleMap`. A Rust `impl` of corresponding `FetchClient` trait is generated which proxies all calls through the `ForeignCallback` to kotlin, i.e. `CallbackInterfaceFetchClientProxy` in Rust calls to `CallbackInterfaceFetchClientFFI` in kotlin. The callback object i.e. the `ConceptFetchClient` is then found, and then methods are called from `CallbackInterfaceFetchClientFFI` to the client code `ConceptFetchClient`. --- Cargo.toml | 3 +- examples/callbacks/Cargo.toml | 18 +++ examples/callbacks/build.rs | 7 ++ examples/callbacks/src/callbacks.udl | 37 ++++++ examples/callbacks/src/lib.rs | 81 ++++++++++++ .../tests/bindings/test_callbacks.kts | 113 +++++++++++++++++ .../tests/test_generated_bindings.rs | 8 ++ uniffi/src/ffi/foreigncallbacks.rs | 35 ++++++ uniffi/src/ffi/mod.rs | 2 + uniffi/src/lib.rs | 8 +- .../src/bindings/gecko_js/gen_gecko_js.rs | 1 + .../src/bindings/gecko_js/webidl.rs | 1 + .../src/bindings/kotlin/gen_kotlin.rs | 30 ++++- .../templates/CallbackInterfaceTemplate.kt | 75 +++++++++++ .../src/bindings/kotlin/templates/Helpers.kt | 70 +++++++++++ .../templates/NamespaceLibraryTemplate.kt | 12 +- .../kotlin/templates/RustBufferHelpers.kt | 3 + .../src/bindings/kotlin/templates/wrapper.kt | 8 ++ .../src/bindings/python/gen_python.rs | 4 + .../python/templates/RustBufferBuilder.py | 7 ++ .../python/templates/RustBufferStream.py | 7 ++ .../src/bindings/swift/gen_swift.rs | 9 +- uniffi_bindgen/src/interface/mod.rs | 116 ++++++++++++++++++ uniffi_bindgen/src/interface/types.rs | 18 +++ uniffi_bindgen/src/scaffolding.rs | 64 ++++++++-- .../templates/CallbackInterfaceTemplate.rs | 102 +++++++++++++++ uniffi_bindgen/src/templates/macros.rs | 9 ++ .../src/templates/scaffolding_template.rs | 12 ++ 28 files changed, 840 insertions(+), 20 deletions(-) create mode 100644 examples/callbacks/Cargo.toml create mode 100644 examples/callbacks/build.rs create mode 100644 examples/callbacks/src/callbacks.udl create mode 100644 examples/callbacks/src/lib.rs create mode 100644 examples/callbacks/tests/bindings/test_callbacks.kts create mode 100644 examples/callbacks/tests/test_generated_bindings.rs create mode 100644 uniffi/src/ffi/foreigncallbacks.rs create mode 100644 uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt create mode 100644 uniffi_bindgen/src/templates/CallbackInterfaceTemplate.rs diff --git a/Cargo.toml b/Cargo.toml index f78327fe44..90d48244ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,6 @@ members = [ "examples/geometry", "examples/rondpoint", "examples/sprites", - "examples/todolist" + "examples/todolist", + "examples/callbacks" ] diff --git a/examples/callbacks/Cargo.toml b/examples/callbacks/Cargo.toml new file mode 100644 index 0000000000..9d9574b40b --- /dev/null +++ b/examples/callbacks/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "uniffi-example-callbacks" +edition = "2018" +version = "0.1.0" +authors = ["Firefox Sync Team "] +license = "MPL-2.0" +publish = false + +[lib] +crate-type = ["cdylib", "lib"] +name = "uniffi_callbacks" + +[dependencies] +uniffi_macros = {path = "../../uniffi_macros"} +uniffi = {path = "../../uniffi", features=["builtin-bindgen"]} + +[build-dependencies] +uniffi_build = {path = "../../uniffi_build", features=["builtin-bindgen"]} diff --git a/examples/callbacks/build.rs b/examples/callbacks/build.rs new file mode 100644 index 0000000000..a98aacddb3 --- /dev/null +++ b/examples/callbacks/build.rs @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +fn main() { + uniffi_build::generate_scaffolding("./src/callbacks.udl").unwrap(); +} diff --git a/examples/callbacks/src/callbacks.udl b/examples/callbacks/src/callbacks.udl new file mode 100644 index 0000000000..71bb240a10 --- /dev/null +++ b/examples/callbacks/src/callbacks.udl @@ -0,0 +1,37 @@ +namespace callbacks {}; + +interface Telephone { + void call(boolean domestic, OnCallAnswered call_responder); +}; + +callback interface OnCallAnswered { + string hello(); + void busy(); + void text_received(string text); +}; + +callback interface RoundTripper { + boolean get_bool(boolean v, boolean arg2); + string get_string(string v, boolean arg2); + string? get_option(string? v, boolean arg2); + sequence get_list(sequence v, boolean arg2); +}; + +interface RoundTripperToRust { + boolean get_bool(RoundTripper callback, boolean v, boolean arg2); + string get_string(RoundTripper callback, string v, boolean arg2); + string? get_option(RoundTripper callback, string? v, boolean arg2); + sequence get_list(RoundTripper callback, sequence v, boolean arg2); +}; + +callback interface Stringifier { + string from_simple_type(i32 value); + // Test if types are collected from callback interfaces. + // kotlinc compile time error if not. + string from_complex_type(sequence? values); +}; + +interface StringUtil { + constructor(Stringifier callback); + string from_simple_type(i32 value); +}; \ No newline at end of file diff --git a/examples/callbacks/src/lib.rs b/examples/callbacks/src/lib.rs new file mode 100644 index 0000000000..a368ffd822 --- /dev/null +++ b/examples/callbacks/src/lib.rs @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub trait OnCallAnswered { + fn hello(&self) -> String; + fn busy(&self); + fn text_received(&self, text: String); +} + +#[derive(Debug, Clone)] +struct Telephone; +impl Telephone { + fn new() -> Self { + Telephone + } + fn call(&self, domestic: bool, call_responder: Box) { + if domestic { + let _ = call_responder.hello(); + } else { + call_responder.busy(); + call_responder.text_received("Pas maintenant!".into()); + } + } +} + +trait RoundTripper { + fn get_bool(&self, v: bool, arg2: bool) -> bool; + fn get_string(&self, v: String, arg2: bool) -> String; + fn get_option(&self, v: Option, arg2: bool) -> Option; + fn get_list(&self, v: Vec, arg2: bool) -> Vec; +} + +#[derive(Debug, Clone)] +pub struct RoundTripperToRust; + +impl RoundTripperToRust { + pub fn new() -> Self { + RoundTripperToRust + } + fn get_bool(&self, callback: Box, v: bool, arg2: bool) -> bool { + callback.get_bool(v, arg2) + } + fn get_string(&self, callback: Box, v: String, arg2: bool) -> String { + callback.get_string(v, arg2) + } + fn get_option( + &self, + callback: Box, + v: Option, + arg2: bool, + ) -> Option { + callback.get_option(v, arg2) + } + fn get_list(&self, callback: Box, v: Vec, arg2: bool) -> Vec { + callback.get_list(v, arg2) + } +} + +// Use Send if we want to store the callback in an exposed object. +trait Stringifier: Send + std::fmt::Debug { + fn from_simple_type(&self, value: i32) -> String; + fn from_complex_type(&self, values: Option>>) -> String; +} + +#[derive(Debug)] +pub struct StringUtil { + callback: Box, +} + +impl StringUtil { + fn new(callback: Box) -> Self { + StringUtil { callback } + } + + fn from_simple_type(&self, value: i32) -> String { + self.callback.from_simple_type(value) + } +} + +include!(concat!(env!("OUT_DIR"), "/callbacks.uniffi.rs")); diff --git a/examples/callbacks/tests/bindings/test_callbacks.kts b/examples/callbacks/tests/bindings/test_callbacks.kts new file mode 100644 index 0000000000..0b342cd1d2 --- /dev/null +++ b/examples/callbacks/tests/bindings/test_callbacks.kts @@ -0,0 +1,113 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import uniffi.callbacks.* + +// 0. Simple example just to see it work. +// Pass in a string, get a string back. +// Pass in nothing, get unit back. +class OnCallAnsweredImpl : OnCallAnswered { + var yesCount: Int = 0 + var busyCount: Int = 0 + var stringReceived = "" + + override fun hello(): String { + yesCount ++ + return "Hi hi $yesCount" + } + + override fun busy() { + busyCount ++ + } + + override fun textReceived(text: String) { + stringReceived = text + } +} + +val cbObject = OnCallAnsweredImpl() +val telephone = Telephone() + +telephone.call(true, cbObject) +assert(cbObject.busyCount == 0) { "yesCount=${cbObject.busyCount} (should be 0)" } +assert(cbObject.yesCount == 1) { "yesCount=${cbObject.yesCount} (should be 1)" } + +telephone.call(true, cbObject) +assert(cbObject.busyCount == 0) { "yesCount=${cbObject.busyCount} (should be 0)" } +assert(cbObject.yesCount == 2) { "yesCount=${cbObject.yesCount} (should be 2)" } + +telephone.call(false, cbObject) +assert(cbObject.busyCount == 1) { "yesCount=${cbObject.busyCount} (should be 1)" } +assert(cbObject.yesCount == 2) { "yesCount=${cbObject.yesCount} (should be 2)" } + +val cbObjet2 = OnCallAnsweredImpl() +telephone.call(true, cbObjet2) +assert(cbObjet2.busyCount == 0) { "yesCount=${cbObjet2.busyCount} (should be 0)" } +assert(cbObjet2.yesCount == 1) { "yesCount=${cbObjet2.yesCount} (should be 1)" } + +telephone.destroy() + +// A bit more systematic in testing, but this time in English. +// +// 1. Pass in the callback as arguments. +// Make the callback methods use multiple aruments, with a variety of types, and +// with a variety of return types. +val rtToRust = RoundTripperToRust() +class RoundTripperToKt(): RoundTripper { + override fun getBool(v: Boolean, arg2: Boolean): Boolean = v xor arg2 + override fun getString(v: String, arg2: Boolean): String = if (arg2) "1234567890123" else v + override fun getOption(v: String?, arg2: Boolean): String? = if (arg2) v?.toUpperCase() else v + override fun getList(v: List, arg2: Boolean): List = if (arg2) v else listOf() +} + +val callback = RoundTripperToKt() +listOf(true, false).forEach { v -> + val flag = true + val expected = callback.getBool(v, flag) + val observed = rtToRust.getBool(callback, v, flag) + assert(expected == observed) { "roundtripping through callback: $expected != $observed" } +} + +listOf(listOf(1,2), listOf(0,1)).forEach { v -> + val flag = true + val expected = callback.getList(v, flag) + val observed = rtToRust.getList(callback, v, flag) + assert(expected == observed) { "roundtripping through callback: $expected != $observed" } +} + +listOf("Hello", "world").forEach { v -> + val flag = true + val expected = callback.getString(v, flag) + val observed = rtToRust.getString(callback, v, flag) + assert(expected == observed) { "roundtripping through callback: $expected != $observed" } +} + +listOf("Some", null).forEach { v -> + val flag = false + val expected = callback.getOption(v, flag) + val observed = rtToRust.getOption(callback, v, flag) + assert(expected == observed) { "roundtripping through callback: $expected != $observed" } +} +rtToRust.destroy() + +// 2. Pass the callback in as a constructor argument, to be stored on the Object struct. +// This is crucial if we want to configure a system at startup, +// then use it without passing callbacks all the time. + +class StringifierImpl: Stringifier { + override fun fromSimpleType(value: Int): String = "kotlin: $value" + // We don't test this, but we're checking that the arg type is included in the minimal list of types used + // in the UDL. + // If this doesn't compile, then look at TypeResolver. + override fun fromComplexType(values: List?): String = "kotlin: $values" +} + +val stCallback = StringifierImpl() +val st = StringUtil(stCallback) +listOf(1, 2).forEach { v -> + val expected = stCallback.fromSimpleType(v) + val observed = st.fromSimpleType(v) + assert(expected == observed) { "callback is sent on construction: $expected != $observed" } +} +st.destroy() \ No newline at end of file diff --git a/examples/callbacks/tests/test_generated_bindings.rs b/examples/callbacks/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..46236a1a6c --- /dev/null +++ b/examples/callbacks/tests/test_generated_bindings.rs @@ -0,0 +1,8 @@ +uniffi_macros::build_foreign_language_testcases!( + "src/callbacks.udl", + [ + "tests/bindings/test_callbacks.kts", + //"tests/bindings/test_callbacks.swift", + //"tests/bindings/test_callbacks.py", + ] +); diff --git a/uniffi/src/ffi/foreigncallbacks.rs b/uniffi/src/ffi/foreigncallbacks.rs new file mode 100644 index 0000000000..6ccf6cc870 --- /dev/null +++ b/uniffi/src/ffi/foreigncallbacks.rs @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use super::RustBuffer; +use std::sync::atomic::{AtomicUsize, Ordering}; + +// Mechanics behind callback interfaces. + +/// ForeignCallback is the function that will do the method dispatch on the foreign language side. +/// It is the basis for all callbacks interfaces. It is registered exactly once per callback interface, +/// at library start up time. +pub type ForeignCallback = + unsafe extern "C" fn(handle: u64, method: u32, args: RustBuffer) -> RustBuffer; + +/// Set the function pointer to the ForeignCallback. Returns false if we did nothing because the callback had already been initialized +pub fn set_foreign_callback(callback_ptr: &AtomicUsize, h: ForeignCallback) -> bool { + let as_usize = h as usize; + let old_ptr = callback_ptr.compare_and_swap(0, as_usize, Ordering::SeqCst); + if old_ptr != 0 { + // This is an internal bug, the other side of the FFI should ensure + // it sets this only once. Note that this is actually going to be + // before logging is initialized in practice, so there's not a lot + // we can actually do here. + log::error!("Bug: Initialized CALLBACK_PTR multiple times"); + } + old_ptr == 0 +} + +/// Get the function pointer to the ForeignCallback. Panics if the callback +/// has not yet been initialized. +pub fn get_foreign_callback(callback_ptr: &AtomicUsize) -> Option { + let ptr_value = callback_ptr.load(Ordering::SeqCst); + unsafe { std::mem::transmute::>(ptr_value) } +} diff --git a/uniffi/src/ffi/mod.rs b/uniffi/src/ffi/mod.rs index e18969b085..03c12fabcb 100644 --- a/uniffi/src/ffi/mod.rs +++ b/uniffi/src/ffi/mod.rs @@ -3,7 +3,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ pub mod foreignbytes; +pub mod foreigncallbacks; pub mod rustbuffer; pub use foreignbytes::*; +pub use foreigncallbacks::*; pub use rustbuffer::*; diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index ac7ec6de54..6b27d362bb 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -17,7 +17,7 @@ //! FFI value. //! * How to ["lift"](ViaFfi::lift) low-level FFI values back into rust values of that type. //! * How to [write](ViaFfi::write) rust values of that type into a buffer, for cases -//! where they are part of a compount data structure that is serialized for transfer. +//! where they are part of a compound data structure that is serialized for transfer. //! * How to [read](ViaFfi::read) rust values of that type from buffer, for cases //! where they are received as part of a compound data structure that was serialized for transfer. //! @@ -152,7 +152,11 @@ pub fn try_lift_from_buffer(buf: RustBuffer) -> Result { /// helper function to instead return an explicit error, to help with debugging. pub fn check_remaining(buf: &B, num_bytes: usize) -> Result<()> { if buf.remaining() < num_bytes { - bail!("not enough bytes remaining in buffer"); + bail!(format!( + "not enough bytes remaining in buffer ({} < {})", + buf.remaining(), + num_bytes + )); } Ok(()) } diff --git a/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs b/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs index bed19f3126..6966c50584 100644 --- a/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs +++ b/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs @@ -335,6 +335,7 @@ mod filters { FFIType::RustBuffer => context.ffi_rustbuffer_type(), FFIType::RustError => context.ffi_rusterror_type(), FFIType::ForeignBytes => context.ffi_foreignbytes_type(), + FFIType::ForeignCallback => unimplemented!("Callback interfaces are not unimplemented"), }) } diff --git a/uniffi_bindgen/src/bindings/gecko_js/webidl.rs b/uniffi_bindgen/src/bindings/gecko_js/webidl.rs index af0e9ed809..418511b4c5 100644 --- a/uniffi_bindgen/src/bindings/gecko_js/webidl.rs +++ b/uniffi_bindgen/src/bindings/gecko_js/webidl.rs @@ -112,6 +112,7 @@ impl From for WebIDLType { // https://github.com/mozilla/uniffi-rs/issues/295. panic!("[TODO: From({:?})]", type_) } + Type::CallbackInterface(_) => panic!("Callback interfaces unimplemented"), Type::Optional(inner) => match *inner { Type::Record(name) => { WebIDLType::OptionalWithDefaultValue(Box::new(Type::Record(name).into())) diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin.rs index fc09ec9c56..03425ea6e3 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin.rs @@ -81,9 +81,11 @@ mod filters { // These types need conversion, and special handling for lifting/lowering. Type::Boolean => "Boolean".to_string(), Type::String => "String".to_string(), - Type::Enum(name) | Type::Record(name) | Type::Object(name) | Type::Error(name) => { - class_name_kt(name)? - } + Type::Enum(name) + | Type::Record(name) + | Type::Object(name) + | Type::Error(name) + | Type::CallbackInterface(name) => class_name_kt(name)?, Type::Optional(t) => format!("{}?", type_kt(t)?), Type::Sequence(t) => format!("List<{}>", type_kt(t)?), Type::Map(t) => format!("Map", type_kt(t)?), @@ -106,6 +108,7 @@ mod filters { FFIType::RustBuffer => "RustBuffer.ByValue".to_string(), FFIType::RustError => "RustError".to_string(), FFIType::ForeignBytes => "ForeignBytes.ByValue".to_string(), + FFIType::ForeignCallback => "ForeignCallback".to_string(), }) } @@ -179,6 +182,11 @@ mod filters { pub fn lower_kt(nm: &dyn fmt::Display, type_: &Type) -> Result { let nm = var_name_kt(nm)?; Ok(match type_ { + Type::CallbackInterface(_) => format!( + "{}Internals.lower({})", + class_name_kt(&type_.canonical_name())?, + nm, + ), Type::Optional(_) | Type::Sequence(_) | Type::Map(_) => { format!("lower{}({})", class_name_kt(&type_.canonical_name())?, nm,) } @@ -197,6 +205,12 @@ mod filters { ) -> Result { let nm = var_name_kt(nm)?; Ok(match type_ { + Type::CallbackInterface(_) => format!( + "{}Internals.write({}, {})", + class_name_kt(&type_.canonical_name())?, + nm, + target, + ), Type::Optional(_) | Type::Sequence(_) | Type::Map(_) => format!( "write{}({}, {})", class_name_kt(&type_.canonical_name())?, @@ -214,6 +228,11 @@ mod filters { pub fn lift_kt(nm: &dyn fmt::Display, type_: &Type) -> Result { let nm = nm.to_string(); Ok(match type_ { + Type::CallbackInterface(_) => format!( + "{}Internals.lift({})", + class_name_kt(&type_.canonical_name())?, + nm, + ), Type::Optional(_) | Type::Sequence(_) | Type::Map(_) => { format!("lift{}({})", class_name_kt(&type_.canonical_name())?, nm,) } @@ -228,6 +247,11 @@ mod filters { pub fn read_kt(nm: &dyn fmt::Display, type_: &Type) -> Result { let nm = nm.to_string(); Ok(match type_ { + Type::CallbackInterface(_) => format!( + "{}Internals.read({})", + class_name_kt(&type_.canonical_name())?, + nm, + ), Type::Optional(_) | Type::Sequence(_) | Type::Map(_) => { format!("read{}({})", class_name_kt(&type_.canonical_name())?, nm,) } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt new file mode 100644 index 0000000000..48b9893c63 --- /dev/null +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt @@ -0,0 +1,75 @@ +{% let type_name = obj.name()|class_name_kt %} +public interface {{ type_name }} { + {% for meth in obj.methods() -%} + fun {{ meth.name()|fn_name_kt }}({% call kt::arg_list_decl(meth) %}) + {%- match meth.return_type() -%} + {%- when Some with (return_type) %}: {{ return_type|type_kt -}} + {%- else -%} + {%- endmatch %} + {% endfor %} +} + +{% let canonical_type_name = format!("CallbackInterface{}", type_name) %} +{% let callback_internals = format!("{}Internals", canonical_type_name) -%} +{% let callback_interface_impl = format!("{}FFI", canonical_type_name) -%} + +internal class {{ callback_interface_impl }} : ForeignCallback { + @Suppress("TooGenericExceptionCaught") + override fun invoke(handle: Long, method: Int, args: RustBuffer.ByValue): RustBuffer.ByValue { + return {{ callback_internals }}.handleMap.callWithResult(handle) { cb -> + when (method) { + 0 -> {{ callback_internals }}.drop(handle) + {% for meth in obj.methods() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name_kt -%} + {{ loop.index }} -> this.{{ method_name }}(cb, args) + {% endfor %} + else -> RustBuffer.ByValue() + } + } + } + + {% for meth in obj.methods() -%} + {% let method_name = format!("invoke_{}", meth.name())|fn_name_kt %} + private fun {{ method_name }}(kotlinCallbackInterface: {{ type_name }}, args: RustBuffer.ByValue): RustBuffer.ByValue = + try { + {#- Unpacking args from the RustBuffer #} + {%- if meth.arguments().len() != 0 -%} + {#- Calling the concrete callback object #} + val buf = args.asByteBuffer() ?: throw InternalError("No ByteBuffer in RustBuffer; this is a Uniffi bug") + kotlinCallbackInterface.{{ meth.name()|fn_name_kt }}( + {% for arg in meth.arguments() -%} + {{ "buf"|read_kt(arg.type_()) }} + {%- if !loop.last %}, {% endif %} + {% endfor -%} + ) + {% else %} + kotlinCallbackInterface.{{ meth.name()|fn_name_kt }}() + {% endif -%} + + {#- Packing up the return value into a RustBuffer #} + {%- match meth.return_type() -%} + {%- when Some with (return_type) -%} + .let { rval -> + val rbuf = RustBufferBuilder() + {{ "rval"|write_kt("rbuf", return_type) }} + rbuf.finalize() + } + {%- else -%} + .let { RustBuffer.ByValue() } + {% endmatch -%} + } finally { + RustBuffer.free(args) + } + + {% endfor %} +} + +internal object {{ callback_internals }}: CallbackInternals<{{ type_name }}>( + {{ callback_interface_impl }}() +) { + override fun register(lib: _UniFFILib) { + rustCall(InternalError.ByReference()) { err -> + lib.{{ obj.ffi_init_callback().name() }}(foreignCallback, err) + } + } +} \ No newline at end of file diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt index c6fdc154cb..438db3ce92 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt @@ -28,3 +28,73 @@ inline fun T.use(block: (T) -> R) = // swallow } } + +typealias Handle = Long +class ConcurrentHandleMap( + private val leftMap: MutableMap = mutableMapOf(), + private val rightMap: MutableMap = mutableMapOf() +) { + private val lock = java.util.concurrent.locks.ReentrantLock() + private val currentHandle = AtomicLong(0L) + private val stride = 1L + + fun insert(obj: T): Handle = + lock.withLock { + rightMap[obj] ?: + currentHandle.getAndAccumulate(stride) { a, b -> a + b } + .also { handle -> + leftMap[handle] = obj + rightMap[obj] = handle + } + } + + fun callWithResult(handle: Handle, fn: (T) -> R): R = + lock.withLock { + val obj = leftMap[handle] ?: throw RuntimeException("Panic: handle not in handlemap") + fn.invoke(obj) + } + + fun get(handle: Handle) = lock.withLock { + leftMap[handle] + } + + fun delete(handle: Handle) { + this.remove(handle) + } + + fun remove(handle: Handle): T? = + lock.withLock { + leftMap.remove(handle)?.let { obj -> + rightMap.remove(obj) + obj + } + } +} + +interface ForeignCallback : com.sun.jna.Callback { + public fun invoke(handle: Long, method: Int, args: RustBuffer.ByValue): RustBuffer.ByValue +} + +internal abstract class CallbackInternals( + val foreignCallback: ForeignCallback +) { + val handleMap = ConcurrentHandleMap() + + abstract fun register(lib: _UniFFILib) + + fun drop(handle: Long): RustBuffer.ByValue { + return handleMap.remove(handle).let { RustBuffer.ByValue() } + } + + fun lift(n: Long) = handleMap.get(n) + + fun read(buf: ByteBuffer) = lift(buf.getLong()) + + fun lower(v: CallbackInterface) = + handleMap.insert(v).also { + assert(handleMap.get(it) === v) { "Handle map is not returning the object we just placed there. This is a bug in the HandleMap." } + } + + fun write(v: CallbackInterface, buf: RustBufferBuilder) = + buf.putLong(lower(v)) +} \ No newline at end of file diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt index f5b77aa5f5..4411b204e6 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt @@ -18,7 +18,17 @@ inline fun loadIndirect( internal interface _UniFFILib : Library { companion object { - internal var INSTANCE: _UniFFILib = loadIndirect(componentName = "{{ ci.namespace() }}") + internal val INSTANCE: _UniFFILib by lazy { + loadIndirect<_UniFFILib>(componentName = "{{ ci.namespace() }}") + {% let callback_interfaces = ci.iter_callback_interface_definitions() %} + {%- if !callback_interfaces.is_empty() -%} + .also { lib: _UniFFILib -> + {% for cb in callback_interfaces -%} + CallbackInterface{{ cb.name()|class_name_kt }}Internals.register(lib) + {% endfor -%} + } + {% endif %} + } } {% for func in ci.iter_ffi_function_definitions() -%} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferHelpers.kt b/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferHelpers.kt index ac82a65fcd..1a1b2cd730 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferHelpers.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/RustBufferHelpers.kt @@ -392,6 +392,9 @@ internal fun write{{ canonical_type_name }}(v: Map "RustBuffer".to_string(), FFIType::RustError => "ctypes.POINTER(RustError)".to_string(), FFIType::ForeignBytes => "ForeignBytes".to_string(), + FFIType::ForeignCallback => unimplemented!("Callback interfaces are not unimplemented"), }) } @@ -127,6 +128,7 @@ mod filters { Type::Float32 | Type::Float64 => format!("float({})", nm), Type::Boolean => format!("bool({})", nm), Type::String | Type::Object(_) | Type::Error(_) | Type::Record(_) => nm.to_string(), + Type::CallbackInterface(_) => panic!("No support for coercing callback interfaces yet"), Type::Enum(name) => format!("{}({})", class_name_py(name)?, nm), Type::Optional(t) => format!("(None if {} is None else {})", nm, coerce_py(nm, t)?), Type::Sequence(t) => format!("list({} for x in {})", coerce_py(&"x", t)?, nm), @@ -155,6 +157,7 @@ mod filters { Type::String => format!("RustBuffer.allocFromString({})", nm), Type::Enum(_) => format!("({}.value)", nm), Type::Object(_) => format!("({}._handle)", nm), + Type::CallbackInterface(_) => panic!("No support for lowering callback interfaces yet"), Type::Error(_) => panic!("No support for lowering errors, yet"), Type::Record(_) | Type::Optional(_) | Type::Sequence(_) | Type::Map(_) => format!( "RustBuffer.allocFrom{}({})", @@ -179,6 +182,7 @@ mod filters { Type::String => format!("{}.consumeIntoString()", nm), Type::Enum(name) => format!("{}({})", class_name_py(name)?, nm), Type::Object(_) => panic!("No support for lifting objects, yet"), + Type::CallbackInterface(_) => panic!("No support for lifting callback interfaces, yet"), Type::Error(_) => panic!("No support for lowering errors, yet"), Type::Record(_) | Type::Optional(_) | Type::Sequence(_) | Type::Map(_) => format!( "{}.consumeInto{}()", diff --git a/uniffi_bindgen/src/bindings/python/templates/RustBufferBuilder.py b/uniffi_bindgen/src/bindings/python/templates/RustBufferBuilder.py index 166b67ae03..98722dfca4 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RustBufferBuilder.py +++ b/uniffi_bindgen/src/bindings/python/templates/RustBufferBuilder.py @@ -110,6 +110,13 @@ def writeString(self, v): # The Object type {{ object_name }}. # Objects cannot currently be serialized, but we can produce a helpful error. + def write{{ canonical_type_name }}(self): + raise InternalError("RustBufferStream.write() not implemented yet for {{ canonical_type_name }}") + + {% when Type::CallbackInterface with (object_name) -%} + # The Callback Interface type {{ object_name }}. + # Objects cannot currently be serialized, but we can produce a helpful error. + def write{{ canonical_type_name }}(self): raise InternalError("RustBufferStream.write() not implemented yet for {{ canonical_type_name }}") diff --git a/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py b/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py index 428af8fa62..78bcea88f6 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py +++ b/uniffi_bindgen/src/bindings/python/templates/RustBufferStream.py @@ -107,6 +107,13 @@ def readString(self): # The Object type {{ object_name }}. # Objects cannot currently be serialized, but we can produce a helpful error. + def read{{ canonical_type_name }}(self): + raise InternalError("RustBufferStream.read not implemented yet for {{ canonical_type_name }}") + + {% when Type::CallbackInterface with (object_name) -%} + # The Callback Interface type {{ object_name }}. + # Objects cannot currently be serialized, but we can produce a helpful error. + def read{{ canonical_type_name }}(self): raise InternalError("RustBufferStream.read not implemented yet for {{ canonical_type_name }}") diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift.rs b/uniffi_bindgen/src/bindings/swift/gen_swift.rs index a322559a08..ccec25e90d 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift.rs @@ -128,9 +128,11 @@ mod filters { Type::Float64 => "Double".into(), Type::Boolean => "Bool".into(), Type::String => "String".into(), - Type::Enum(name) | Type::Record(name) | Type::Object(name) | Type::Error(name) => { - class_name_swift(name)? - } + Type::Enum(name) + | Type::Record(name) + | Type::Object(name) + | Type::Error(name) + | Type::CallbackInterface(name) => class_name_swift(name)?, Type::Optional(type_) => format!("{}?", type_swift(type_)?), Type::Sequence(type_) => format!("[{}]", type_swift(type_)?), Type::Map(type_) => format!("[String:{}]", type_swift(type_)?), @@ -155,6 +157,7 @@ mod filters { FFIType::RustBuffer => "RustBuffer".into(), FFIType::RustError => "NativeRustError".into(), FFIType::ForeignBytes => "ForeignBytes".into(), + FFIType::ForeignCallback => unimplemented!("Callback interfaces are not unimplemented"), }) } diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index 0a44938704..082882612b 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -76,6 +76,7 @@ pub struct ComponentInterface { records: Vec, functions: Vec, objects: Vec, + callback_interfaces: Vec, errors: Vec, } @@ -152,6 +153,20 @@ impl<'ci> ComponentInterface { self.objects.iter().find(|o| o.name == name) } + /// List the definitions for every Callback Interface type in the interface. + pub fn iter_callback_interface_definitions(&self) -> Vec { + self.callback_interfaces.to_vec() + } + + /// Get an Callback interface definition by name, or None if no such interface is defined. + pub fn get_foreign_callback_interface_definition( + &self, + name: &str, + ) -> Option<&CallbackInterface> { + // TODO: probably we could store these internally in a HashMap to make this easier? + self.callback_interfaces.iter().find(|o| o.name == name) + } + /// List the definitions for every Error type in the interface. pub fn iter_error_definitions(&self) -> Vec { self.errors.to_vec() @@ -308,6 +323,11 @@ impl<'ci> ComponentInterface { .chain(obj.methods.iter().map(|f| f.ffi_func.clone())) }) .flatten() + .chain( + self.callback_interfaces + .iter() + .map(|cb| cb.ffi_init_callback.clone()), + ) .chain(self.functions.iter().map(|f| f.ffi_func.clone())) .chain( vec![ @@ -365,6 +385,12 @@ impl<'ci> ComponentInterface { Ok(()) } + fn add_callback_interface_definition(&mut self, defn: CallbackInterface) -> Result<()> { + // Note that there will be no duplicates thanks to the previous type-finding pass. + self.callback_interfaces.push(defn); + Ok(()) + } + fn add_error_definition(&mut self, defn: Error) -> Result<()> { // Note that there will be no duplicates thanks to the previous type-finding pass. self.errors.push(defn); @@ -383,6 +409,9 @@ impl<'ci> ComponentInterface { for obj in self.objects.iter_mut() { obj.derive_ffi_funcs(&ci_prefix)?; } + for callback in self.callback_interfaces.iter_mut() { + callback.derive_ffi_funcs(&ci_prefix)?; + } Ok(()) } } @@ -405,6 +434,7 @@ impl Hash for ComponentInterface { self.records.hash(state); self.functions.hash(state); self.objects.hash(state); + self.callback_interfaces.hash(state); self.errors.hash(state); } } @@ -455,6 +485,10 @@ impl APIBuilder for weedle::Definition<'_> { let obj = d.convert(ci)?; ci.add_object_definition(obj) } + weedle::Definition::CallbackInterface(d) => { + let obj = d.convert(ci)?; + ci.add_callback_interface_definition(obj) + } _ => bail!("don't know how to deal with {:?}", self), } } @@ -1039,6 +1073,88 @@ impl APIConverter for weedle::EnumDefinition<'_> { } } +#[derive(Debug, Clone)] +pub struct CallbackInterface { + name: String, + methods: Vec, + ffi_init_callback: FFIFunction, +} + +impl CallbackInterface { + fn new(name: String) -> CallbackInterface { + CallbackInterface { + name, + methods: Default::default(), + ffi_init_callback: Default::default(), + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn methods(&self) -> Vec<&Method> { + self.methods.iter().collect() + } + + pub fn ffi_init_callback(&self) -> &FFIFunction { + &self.ffi_init_callback + } + + fn derive_ffi_funcs(&mut self, ci_prefix: &str) -> Result<()> { + self.ffi_init_callback.name = format!("ffi_{}_{}_init_callback", ci_prefix, self.name); + self.ffi_init_callback.arguments = vec![FFIArgument { + name: "callback_stub".to_string(), + type_: FFIType::ForeignCallback, + }]; + self.ffi_init_callback.return_type = None; + + // for meth in self.methods.iter_mut() { + // meth.derive_ffi_func(ci_prefix, &self.name)? + // } + Ok(()) + } +} + +impl Hash for CallbackInterface { + fn hash(&self, state: &mut H) { + // We don't include the FFIFunc in the hash calculation, because: + // - it is entirely determined by the other fields, + // so excluding it is safe. + // - its `name` property includes a checksum derived from the very + // hash value we're trying to calculate here, so excluding it + // avoids a weird circular depenendency in the calculation. + self.name.hash(state); + self.methods.hash(state); + } +} + +impl APIConverter for weedle::CallbackInterfaceDefinition<'_> { + fn convert(&self, ci: &mut ComponentInterface) -> Result { + if self.attributes.is_some() { + bail!("interface attributes are not supported yet"); + } + if self.inheritance.is_some() { + bail!("interface inheritence is not supported"); + } + let mut object = CallbackInterface::new(self.identifier.0.to_string()); + for member in &self.members.body { + match member { + weedle::interface::InterfaceMember::Operation(t) => { + let mut method = t.convert(ci)?; + method.object_name.push_str(object.name.as_str()); + object.methods.push(method); + } + _ => bail!( + "no support for callback interface member type {:?} yet", + member + ), + } + } + Ok(object) + } +} + /// Represents a "data class" style object, for passing around complex values. /// /// In the FFI these are represented as a byte buffer, which one side explicitly diff --git a/uniffi_bindgen/src/interface/types.rs b/uniffi_bindgen/src/interface/types.rs index 6448d58c5f..6bf07d1319 100644 --- a/uniffi_bindgen/src/interface/types.rs +++ b/uniffi_bindgen/src/interface/types.rs @@ -64,6 +64,9 @@ pub enum FFIType { /// The string is owned by rust and allocated on the rust heap, and must be freed by /// passing it to the appropriate `string_free` FFI function. RustError, + /// A pointer to a single function in to the foreign language. + /// Typically, this isn't the callback that's exposed to foreign languages + ForeignCallback, // 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. } @@ -91,6 +94,7 @@ pub enum Type { Record(String), Enum(String), Error(String), + CallbackInterface(String), // Structurally recursive types. Optional(Box), Sequence(Box), @@ -126,6 +130,8 @@ impl From<&Type> for FFIType { Type::String => FFIType::RustBuffer, // Objects are passed as opaque integer handles. Type::Object(_) => FFIType::UInt64, + // Callback interfaces are passed as opaque integer handles. + Type::CallbackInterface(_) => FFIType::UInt64, // Enums are passed as integers. Type::Enum(_) => FFIType::UInt32, // Errors have their own special type. @@ -168,6 +174,7 @@ impl Type { Type::Error(nm) => format!("Error{}", nm), Type::Enum(nm) => format!("Enum{}", nm), Type::Record(nm) => format!("Record{}", nm), + Type::CallbackInterface(nm) => format!("CallbackInterface{}", nm), // Recursive types. // These add a prefix to the name of the underlying type. // The component API definition cannot give names to recursive types, so as long as the @@ -285,6 +292,7 @@ impl TypeFinder for weedle::Definition<'_> { weedle::Definition::Dictionary(d) => d.add_type_definitions_to(types), weedle::Definition::Enum(d) => d.add_type_definitions_to(types), weedle::Definition::Typedef(d) => d.add_type_definitions_to(types), + weedle::Definition::CallbackInterface(d) => d.add_type_definitions_to(types), _ => Ok(()), } } @@ -331,6 +339,16 @@ impl TypeFinder for weedle::TypedefDefinition<'_> { } } +impl TypeFinder for weedle::CallbackInterfaceDefinition<'_> { + fn add_type_definitions_to(&self, types: &mut TypeUniverse) -> Result<()> { + if self.attributes.is_some() { + bail!("no typedef attributes are currently supported"); + } + let name = self.identifier.0.to_string(); + types.add_type_definition(self.identifier.0, Type::CallbackInterface(name)) + } +} + /// Trait to help resolving an UDL type node to a [Type]. /// /// Ths trait does structural matching against type-related weedle AST nodes from diff --git a/uniffi_bindgen/src/scaffolding.rs b/uniffi_bindgen/src/scaffolding.rs index fd19e619c1..ef53df55d0 100644 --- a/uniffi_bindgen/src/scaffolding.rs +++ b/uniffi_bindgen/src/scaffolding.rs @@ -39,6 +39,7 @@ mod filters { Type::Enum(name) | Type::Record(name) | Type::Object(name) | Type::Error(name) => { name.clone() } + Type::CallbackInterface(name) => format!("Box", name), Type::Optional(t) => format!("Option<{}>", type_rs(t)?), Type::Sequence(t) => format!("Vec<{}>", type_rs(t)?), Type::Map(t) => format!("std::collections::HashMap", type_rs(t)?), @@ -61,27 +62,70 @@ mod filters { FFIType::RustBuffer => "uniffi::RustBuffer".into(), FFIType::RustError => "uniffi::deps::ffi_support::ExternError".into(), FFIType::ForeignBytes => "uniffi::ForeignBytes".into(), + FFIType::ForeignCallback => "uniffi::ForeignCallback".into(), }) } pub fn lower_rs(nm: &dyn fmt::Display, type_: &Type) -> Result { // By explicitly naming the type here, we help the rust compiler to type-check the user-provided // implementations of the functions that we're wrapping (and also to type-check our generated code). - Ok(format!( - "<{} as uniffi::ViaFfi>::lower({})", - type_rs(type_)?, - nm - )) + Ok(match type_ { + Type::CallbackInterface(type_name) => { + format!("<{}Proxy as uniffi::ViaFfi>::lower({})", type_name, nm,) + } + _ => format!("<{} as uniffi::ViaFfi>::lower({})", type_rs(type_)?, nm), + }) } pub fn lift_rs(nm: &dyn fmt::Display, type_: &Type) -> Result { // By explicitly naming the type here, we help the rust compiler to type-check the user-provided // implementations of the functions that we're wrapping (and also to type-check our generated code). // This will panic if the bindings provide an invalid value over the FFI. - Ok(format!( - "<{} as uniffi::ViaFfi>::try_lift({}).unwrap()", - type_rs(type_)?, - nm - )) + Ok(match type_ { + Type::CallbackInterface(type_name) => format!( + "Box::new(<{}Proxy as uniffi::ViaFfi>::try_lift({}).unwrap())", + type_name, nm, + ), + _ => format!( + "<{} as uniffi::ViaFfi>::try_lift({}).unwrap()", + type_rs(type_)?, + nm + ), + }) + } + + /// Get a Rust expression for writing a value into a byte buffer. + pub fn write_rs( + nm: &dyn fmt::Display, + target: &dyn fmt::Display, + type_: &Type, + ) -> Result { + Ok(match type_ { + Type::CallbackInterface(type_name) => format!( + "Box::new(<{}Proxy as uniffi::ViaFfi>::write(&{}, {})", + type_name, nm, target, + ), + _ => format!( + "<{} as uniffi::ViaFfi>::write(&{}, {})", + type_rs(type_)?, + nm, + target + ), + }) + } + + /// Get a Rust expression for writing a value into a byte buffer. + pub fn read_rs(target: &dyn fmt::Display, type_: &Type) -> Result { + Ok(match type_ { + Type::CallbackInterface(type_name) => format!( + "Box::new(<{}Proxy as uniffi::ViaFfi>::try_read({}).unwrap()", + type_name, target, + ), + _ => format!( + "<{} as uniffi::ViaFfi>::try_read({}).unwrap()", + type_rs(type_)?, + target + ), + }) } } diff --git a/uniffi_bindgen/src/templates/CallbackInterfaceTemplate.rs b/uniffi_bindgen/src/templates/CallbackInterfaceTemplate.rs new file mode 100644 index 0000000000..86df3bdc25 --- /dev/null +++ b/uniffi_bindgen/src/templates/CallbackInterfaceTemplate.rs @@ -0,0 +1,102 @@ +{# +// For each callback interface in the UDL, we assume the caller has provided a corresponding +// Rust `trait` with the declared methods. We provide the traits for sending it across the FFI. +// If the caller's trait does not match the shape and types declared in the UDL then the rust +// compiler will complain with a type error. +// +// The generated proxy will implement `Drop`, `Send` and `Debug`. +#} +{% let trait_name = obj.name() -%} +{% let trait_impl = format!("{}Proxy", trait_name) -%} +{% let foreign_callback_holder = format!("foreign_callback_{}_holder", trait_name)|upper -%} + +// Register a foreign callback for getting across the FFI. +static {{ foreign_callback_holder }}: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + +#[no_mangle] +pub extern "C" fn {{ obj.ffi_init_callback().name() }}(callback: uniffi::ForeignCallback) { + uniffi::set_foreign_callback(&{{ foreign_callback_holder }}, callback); +} + +// Make an implementation which will shell out to the foreign language. +#[derive(Debug)] +struct {{ trait_impl }} { + handle: u64 +} + +// We need Send so we can stash the callbacks in objects we're sharing with foreign languages +// i.e. in the handle map. +// Prepared for our target trait to declare: +// `trait {{ trait_name }}: Send + std::fmt::Debug` +unsafe impl Send for {{ trait_impl }} {} + +impl {{ trait_name }} for {{ trait_impl }} { + {%- for meth in obj.methods() %} + + {#- Method declaration #} + fn {{ meth.name() -}} + ({% call rs::arg_list_decl_with_prefix("&self", meth) %}) + {%- match meth.return_type() %} + {%- when Some with (return_type) %} -> {{ return_type|type_rs }} + {% else -%} + {%- endmatch -%} { + + {#- Method body #} + uniffi::deps::log::debug!("{{ obj.name() }}.{{ meth.name() }}"); + let callback = uniffi::get_foreign_callback(&{{ foreign_callback_holder }}).unwrap(); + + {#- Packing args into a RustBuffer #} + {% if meth.arguments().len() == 0 -%} + let in_rbuf = uniffi::RustBuffer::new(); + {% else -%} + let mut in_buf = Vec::new(); + {% for arg in meth.arguments() -%} + {{ arg.name()|write_rs("&mut in_buf", arg.type_()) -}}; + {% endfor -%} + let in_rbuf = uniffi::RustBuffer::from_vec(in_buf); + {% endif -%} + + {#- Calling into foreign code. #} + let _out_rbuf = unsafe { callback(self.handle, {{ loop.index }}, in_rbuf) }; + + {#- Unpacking the RustBuffer to return to Rust #} + {% match meth.return_type() -%} + {% when Some with (return_type) -%} + let vec = _out_rbuf.destroy_into_vec(); + let mut out_buf = vec.as_slice(); + let rval = {{ "&mut out_buf"|read_rs(return_type) }}; + rval + {%- else -%} + uniffi::RustBuffer::destroy(_out_rbuf); + {%- endmatch %} + } + {%- endfor %} +} + +unsafe impl uniffi::ViaFfi for {{ trait_impl }} { + type FfiType = u64; + + fn lower(self) -> Self::FfiType { + self.handle + } + + fn try_lift(v: Self::FfiType) -> uniffi::deps::anyhow::Result { + Ok(Self { handle: v }) + } + + fn write(&self, buf: &mut B) { + buf.put_u64(self.handle); + } + + fn try_read(buf: &mut B) -> uniffi::deps::anyhow::Result { + uniffi::check_remaining(buf, 8)?; + ::try_lift(buf.get_u64()) + } +} + +impl Drop for {{ trait_impl }} { + fn drop(&mut self) { + let callback = uniffi::get_foreign_callback(&{{ foreign_callback_holder }}).unwrap(); + unsafe { callback(self.handle, 0, Default::default()) }; + } +} \ No newline at end of file diff --git a/uniffi_bindgen/src/templates/macros.rs b/uniffi_bindgen/src/templates/macros.rs index cc570029aa..2822f2919b 100644 --- a/uniffi_bindgen/src/templates/macros.rs +++ b/uniffi_bindgen/src/templates/macros.rs @@ -31,6 +31,15 @@ {% if func.arguments().len() > 0 %},{% endif %} err: &mut uniffi::deps::ffi_support::ExternError, {%- endmacro -%} +{%- macro arg_list_decl_with_prefix(prefix, meth) %} + {{- prefix -}} + {%- if meth.arguments().len() > 0 %}, {# whitespace #} + {%- for arg in meth.arguments() %} + {{- arg.name() }}: {{ arg.type_()|type_rs -}}{% if loop.last %}{% else %},{% endif %} + {%- endfor %} + {%- endif %} +{%- endmacro -%} + {% macro return_type_func(func) %}{% match func.ffi_func().return_type() %}{% when Some with (return_type) %}{{ return_type|type_ffi }}{%- else -%}(){%- endmatch -%}{%- endmacro -%} {% macro ret(func) %}{% match func.return_type() %}{% when Some with (return_type) %}{{ "_retval"|lower_rs(return_type) }}{% else %}_retval{% endmatch %}{% endmacro %} diff --git a/uniffi_bindgen/src/templates/scaffolding_template.rs b/uniffi_bindgen/src/templates/scaffolding_template.rs index 634486122e..37f7d320fe 100644 --- a/uniffi_bindgen/src/templates/scaffolding_template.rs +++ b/uniffi_bindgen/src/templates/scaffolding_template.rs @@ -49,4 +49,16 @@ {% include "ObjectTemplate.rs" %} {% endfor %} +// For each Callback Interface definition we generate: +// * a holder for a `ForeignCallback`, which will be responsible for all method calls for all callbacks of that type. +// * an init function to accept that `ForeignCallback` from the foreign language, and put it in the holder. +// * a proxy `struct` which implements the trait that the Callback Interface corresponds to. +// - for each method, arguments will be packed into a `RustBuffer` and sent over the `ForeignCallback` to be +// unpacked and called. The return value is packed into another `RustBuffer` and sent back to Rust. +// - a `Drop` `impl`, which tells the foreign language to forget about the real callback object. +// - a `Send` `impl` so `Object`s can store callbacks. +{% for obj in ci.iter_callback_interface_definitions() %} +{% include "CallbackInterfaceTemplate.rs" %} +{% endfor %} + {%- import "macros.rs" as rs -%}