Skip to content

Commit

Permalink
Add callback interfaces for Kotlin
Browse files Browse the repository at this point in the history
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<dyn FetchClient>) {}
}
```

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`.
  • Loading branch information
jhugman committed Nov 6, 2020
1 parent aea2e73 commit f844e0c
Show file tree
Hide file tree
Showing 28 changed files with 840 additions and 20 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ members = [
"examples/geometry",
"examples/rondpoint",
"examples/sprites",
"examples/todolist"
"examples/todolist",
"examples/callbacks"
]
18 changes: 18 additions & 0 deletions examples/callbacks/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "uniffi-example-callbacks"
edition = "2018"
version = "0.1.0"
authors = ["Firefox Sync Team <[email protected]>"]
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"]}
7 changes: 7 additions & 0 deletions examples/callbacks/build.rs
Original file line number Diff line number Diff line change
@@ -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();
}
37 changes: 37 additions & 0 deletions examples/callbacks/src/callbacks.udl
Original file line number Diff line number Diff line change
@@ -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<i32> get_list(sequence<i32> 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<i32> get_list(RoundTripper callback, sequence<i32> 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<f64?>? values);
};

interface StringUtil {
constructor(Stringifier callback);
string from_simple_type(i32 value);
};
81 changes: 81 additions & 0 deletions examples/callbacks/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<dyn OnCallAnswered>) {
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<String>, arg2: bool) -> Option<String>;
fn get_list(&self, v: Vec<i32>, arg2: bool) -> Vec<i32>;
}

#[derive(Debug, Clone)]
pub struct RoundTripperToRust;

impl RoundTripperToRust {
pub fn new() -> Self {
RoundTripperToRust
}
fn get_bool(&self, callback: Box<dyn RoundTripper>, v: bool, arg2: bool) -> bool {
callback.get_bool(v, arg2)
}
fn get_string(&self, callback: Box<dyn RoundTripper>, v: String, arg2: bool) -> String {
callback.get_string(v, arg2)
}
fn get_option(
&self,
callback: Box<dyn RoundTripper>,
v: Option<String>,
arg2: bool,
) -> Option<String> {
callback.get_option(v, arg2)
}
fn get_list(&self, callback: Box<dyn RoundTripper>, v: Vec<i32>, arg2: bool) -> Vec<i32> {
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<Vec<Option<f64>>>) -> String;
}

#[derive(Debug)]
pub struct StringUtil {
callback: Box<dyn Stringifier>,
}

impl StringUtil {
fn new(callback: Box<dyn Stringifier>) -> 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"));
113 changes: 113 additions & 0 deletions examples/callbacks/tests/bindings/test_callbacks.kts
Original file line number Diff line number Diff line change
@@ -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<Int>, arg2: Boolean): List<Int> = 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<Double?>?): 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()
8 changes: 8 additions & 0 deletions examples/callbacks/tests/test_generated_bindings.rs
Original file line number Diff line number Diff line change
@@ -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",
]
);
35 changes: 35 additions & 0 deletions uniffi/src/ffi/foreigncallbacks.rs
Original file line number Diff line number Diff line change
@@ -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<ForeignCallback> {
let ptr_value = callback_ptr.load(Ordering::SeqCst);
unsafe { std::mem::transmute::<usize, Option<ForeignCallback>>(ptr_value) }
}
2 changes: 2 additions & 0 deletions uniffi/src/ffi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
8 changes: 6 additions & 2 deletions uniffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//!
Expand Down Expand Up @@ -152,7 +152,11 @@ pub fn try_lift_from_buffer<T: ViaFfi>(buf: RustBuffer) -> Result<T> {
/// helper function to instead return an explicit error, to help with debugging.
pub fn check_remaining<B: Buf>(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(())
}
Expand Down
1 change: 1 addition & 0 deletions uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
})
}

Expand Down
1 change: 1 addition & 0 deletions uniffi_bindgen/src/bindings/gecko_js/webidl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ impl From<Type> for WebIDLType {
// https://github.com/mozilla/uniffi-rs/issues/295.
panic!("[TODO: From<Type>({:?})]", type_)
}
Type::CallbackInterface(_) => panic!("Callback interfaces unimplemented"),
Type::Optional(inner) => match *inner {
Type::Record(name) => {
WebIDLType::OptionalWithDefaultValue(Box::new(Type::Record(name).into()))
Expand Down
Loading

0 comments on commit f844e0c

Please sign in to comment.