-
Notifications
You must be signed in to change notification settings - Fork 234
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
28 changed files
with
840 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"]} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
] | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.