diff --git a/docs/manual/src/tutorial/callback_interfaces.md b/docs/manual/src/tutorial/callback_interfaces.md new file mode 100644 index 0000000000..27dd488b0c --- /dev/null +++ b/docs/manual/src/tutorial/callback_interfaces.md @@ -0,0 +1,93 @@ +# Callback interfaces + +Callback interfaces are traits specified in UDL which can be implemented by foreign languages. + +They can provide Rust code access available to the host language, but not easily replicated +in Rust. + + * accessing device APIs + * provide glue to clip together Rust components at runtime. + * access shared resources and assets bundled with the app. + +# Using callback interfaces + +1. Define a Rust trait. + +This toy example defines a way of Rust accessing a key-value store exposed +by the host operating system (e.g. the key chain). + +``` +trait Keychain: Send { + pub fn get(key: String) -> Option + pub fn put(key: String, value: String) +} +``` + +2. Define a callback interface in the UDL + +``` +callback interface Keychain { + string? get(string key); + void put(string key, string data); +}; +``` + +3. And allow it to be passed into Rust. + +Here, we define a constructor to pass the keychain to rust, and then another method +which may use it. + +In UDL: +``` +object Authenticator { + constructor(Keychain keychain); + void login(); +} +``` + +In Rust: + +``` +struct Authenticator { + keychain: Box, +} + +impl Authenticator { + pub fn new(keychain: Box) -> Self { + Self { keychain } + } + pub fn login() { + let username = keychain.get("username".into()); + let password = keychain.get("password".into()); + } +} +``` +4. Create an foreign language implementation of the callback interface. + +In this example, here's a Kotlin implementation. + +``` +class AndroidKeychain: Keychain { + override fun get(key: String): String? { + // … elide the implementation. + return value + } + override fun put(key: String) { + // … elide the implementation. + } +} +``` +5. Pass the implementation to Rust. + +Again, in Kotlin + +``` +val authenticator = Authenticator(AndroidKeychain()) +// later on: +authenticator.login() +``` + +Care is taken to ensure that once `Box` is dropped in Rust, then it is cleaned up in Kotlin. + +Also note, that storing the `Box` in the `Authenticator` required that all implementations +*must* implement `Send`. \ No newline at end of file diff --git a/uniffi/Cargo.toml b/uniffi/Cargo.toml index 5d5608e842..91274d430a 100644 --- a/uniffi/Cargo.toml +++ b/uniffi/Cargo.toml @@ -21,6 +21,7 @@ log = "0.4" cargo_metadata = "0.11" paste = "1.0" uniffi_bindgen = { path = "../uniffi_bindgen", optional = true, version = "= 0.6.1"} +static_assertions = "1.1.0" [features] default = [] diff --git a/uniffi/src/ffi/foreigncallbacks.rs b/uniffi/src/ffi/foreigncallbacks.rs index 68a9ecc3ec..7c878509c4 100644 --- a/uniffi/src/ffi/foreigncallbacks.rs +++ b/uniffi/src/ffi/foreigncallbacks.rs @@ -2,15 +2,134 @@ * 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/. */ +//! Callback interfaces are traits specified in UDL which can be implemented by foreign languages. +//! +//! # Using callback interfaces +//! +//! 1. Define a Rust trait. +//! +//! This toy example defines a way of Rust accessing a key-value store exposed +//! by the host operating system (e.g. the key chain). +//! +//! ``` +//! trait Keychain: Send { +//! fn get(&self, key: String) -> Option; +//! fn put(&self, key: String, value: String); +//! } +//! ``` +//! +//! 2. Define a callback interface in the UDL +//! +//! ```idl +//! callback interface Keychain { +//! string? get(string key); +//! void put(string key, string data); +//! }; +//! ``` +//! +//! 3. And allow it to be passed into Rust. +//! +//! Here, we define a constructor to pass the keychain to rust, and then another method +//! which may use it. +//! +//! In UDL: +//! ```idl +//! object Authenticator { +//! constructor(Keychain keychain); +//! void login(); +//! } +//! ``` +//! +//! In Rust: +//! +//! ``` +//!# trait Keychain: Send { +//!# fn get(&self, key: String) -> Option; +//!# fn put(&self, key: String, value: String); +//!# } +//! struct Authenticator { +//! keychain: Box, +//! } +//! +//! impl Authenticator { +//! pub fn new(keychain: Box) -> Self { +//! Self { keychain } +//! } +//! pub fn login(&self) { +//! let username = self.keychain.get("username".into()); +//! let password = self.keychain.get("password".into()); +//! } +//! } +//! ``` +//! 4. Create an foreign language implementation of the callback interface. +//! +//! In this example, here's a Kotlin implementation. +//! +//! ```kotlin +//! class AndroidKeychain: Keychain { +//! override fun get(key: String): String? { +//! // … elide the implementation. +//! return value +//! } +//! override fun put(key: String) { +//! // … elide the implementation. +//! } +//! } +//! ``` +//! 5. Pass the implementation to Rust. +//! +//! Again, in Kotlin +//! +//! ```kotlin +//! val authenticator = Authenticator(AndroidKeychain()) +//! authenticator.login() +//! ``` +//! +//! # How it works. +//! +//! ## High level +//! +//! 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. +//! +//! 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. +//! + use super::RustBuffer; use std::sync::atomic::{AtomicUsize, Ordering}; -/// ForeignCallback is the function that will do the method dispatch on the foreign language side. +/// 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 index is 1 indexed. Note that the list of methods is generated by at 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. pub type ForeignCallback = unsafe extern "C" fn(handle: u64, method: u32, args: RustBuffer) -> RustBuffer; +/// 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; // Overly-paranoid sanity checking to ensure that these types are diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index 6b27d362bb..4c46d37e4f 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -47,6 +47,7 @@ pub mod deps { pub use ffi_support; pub use lazy_static; pub use log; + pub use static_assertions; } /// Trait defining how to transfer values via the FFI layer. diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt index a91b76f884..203327f8c1 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt @@ -23,6 +23,9 @@ internal class {{ callback_interface_impl }} : ForeignCallback { {% let method_name = format!("invoke_{}", meth.name())|fn_name_kt -%} {{ loop.index }} -> this.{{ method_name }}(cb, args) {% 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 else -> RustBuffer.ByValue() } } @@ -57,6 +60,8 @@ internal class {{ callback_interface_impl }} : ForeignCallback { {%- else -%} .let { RustBuffer.ByValue() } {% endmatch -%} + // TODO catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 } finally { RustBuffer.free(args) } diff --git a/uniffi_bindgen/src/templates/CallbackInterfaceTemplate.rs b/uniffi_bindgen/src/templates/CallbackInterfaceTemplate.rs index 7491a88343..f16b41e62a 100644 --- a/uniffi_bindgen/src/templates/CallbackInterfaceTemplate.rs +++ b/uniffi_bindgen/src/templates/CallbackInterfaceTemplate.rs @@ -35,6 +35,8 @@ impl Drop for {{ trait_impl }} { } } +uniffi::deps::static_assertions::assert_impl_all!({{ trait_impl }}: Send); + impl {{ trait_name }} for {{ trait_impl }} { {%- for meth in cbi.methods() %} @@ -68,8 +70,7 @@ impl {{ trait_name }} for {{ trait_impl }} { {% when Some with (return_type) -%} let vec = ret_rbuf.destroy_into_vec(); let mut ret_buf = vec.as_slice(); - let rval = {{ "&mut ret_buf"|read_rs(return_type) }}; - rval + {{ "&mut ret_buf"|read_rs(return_type) }} {%- else -%} uniffi::RustBuffer::destroy(ret_rbuf); {%- endmatch %}