Skip to content

Commit

Permalink
Final review comments
Browse files Browse the repository at this point in the history
  • Loading branch information
jhugman committed Jan 21, 2021
1 parent 3f965b8 commit 12539d0
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 3 deletions.
93 changes: 93 additions & 0 deletions docs/manual/src/tutorial/callback_interfaces.md
Original file line number Diff line number Diff line change
@@ -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<String>
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<dyn Keychain>,
}
impl Authenticator {
pub fn new(keychain: Box<dyn Keychain>) -> 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<dyn Keychain>` is dropped in Rust, then it is cleaned up in Kotlin.

Also note, that storing the `Box<dyn Keychain>` in the `Authenticator` required that all implementations
*must* implement `Send`.
1 change: 1 addition & 0 deletions uniffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
121 changes: 120 additions & 1 deletion uniffi/src/ffi/foreigncallbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>;
//! 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<String>;
//!# fn put(&self, key: String, value: String);
//!# }
//! struct Authenticator {
//! keychain: Box<dyn Keychain>,
//! }
//!
//! impl Authenticator {
//! pub fn new(keychain: Box<dyn Keychain>) -> 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<dyn Keychain>`.
//!
//! 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
Expand Down
1 change: 1 addition & 0 deletions uniffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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)
}
Expand Down
5 changes: 3 additions & 2 deletions uniffi_bindgen/src/templates/CallbackInterfaceTemplate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() %}

Expand Down Expand Up @@ -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 %}
Expand Down

0 comments on commit 12539d0

Please sign in to comment.