Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add callback interfaces for Kotlin #344

Merged
merged 5 commits into from
Jan 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"uniffi_macros",
"uniffi",
"examples/arithmetic",
"examples/callbacks",
"examples/geometry",
"examples/rondpoint",
"examples/sprites",
Expand Down
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`.
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();
}
48 changes: 48 additions & 0 deletions examples/callbacks/src/callbacks.udl
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace callbacks {};

interface Telephone {
void call(boolean domestic, OnCallAnswered call_responder);
};

callback interface OnCallAnswered {
string hello();
void busy();
void text_received(string text);
};

/// These objects are implemented by the foreign language and passed
/// to Rust. Rust then calls methods on it when it needs to.
callback interface ForeignGetters {
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);
};

/// These objects are implemented in Rust, and call out to `ForeignGetters`
/// to get the value.
interface RustGetters {
boolean get_bool(ForeignGetters callback, boolean v, boolean arg2);
string get_string(ForeignGetters callback, string v, boolean arg2);
string? get_option(ForeignGetters callback, string? v, boolean arg2);
sequence<i32> get_list(ForeignGetters callback, sequence<i32> v, boolean arg2);
};
jhugman marked this conversation as resolved.
Show resolved Hide resolved

/// These objects are implemented by the foreign language and passed
/// to Rust. Rust then calls methods on it when it needs to.
/// Rust developers need to declare these traits extending `Send` so
/// they can be stored in Rust— i.e. not passed in as an argument to
/// be used immediately.
callback interface StoredForeignStringifier {
string from_simple_type(i32 value);
// Test if types are collected from callback interfaces.
// kotlinc compile time error if not.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to make sure I understand: we're testing whether the f64 here correctly ends up in the TypeUniverse of the resulting ComponentInterface?

string from_complex_type(sequence<f64?>? values);
};

/// Rust object that uses the StoredForeignStringifier to produce string representations
/// of passed arguments.
interface RustStringifier {
constructor(StoredForeignStringifier 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("Not now, I'm on another call!".into());
}
}
}

trait ForeignGetters {
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 RustGetters;

impl RustGetters {
pub fn new() -> Self {
RustGetters
}
fn get_bool(&self, callback: Box<dyn ForeignGetters>, v: bool, arg2: bool) -> bool {
callback.get_bool(v, arg2)
}
fn get_string(&self, callback: Box<dyn ForeignGetters>, v: String, arg2: bool) -> String {
callback.get_string(v, arg2)
}
fn get_option(
&self,
callback: Box<dyn ForeignGetters>,
v: Option<String>,
arg2: bool,
) -> Option<String> {
callback.get_option(v, arg2)
}
fn get_list(&self, callback: Box<dyn ForeignGetters>, 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 StoredForeignStringifier: 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 RustStringifier {
callback: Box<dyn StoredForeignStringifier>,
}

impl RustStringifier {
fn new(callback: Box<dyn StoredForeignStringifier>) -> Self {
RustStringifier { 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 rustGetters = RustGetters()
class KotlinGetters(): ForeignGetters {
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 = KotlinGetters()
listOf(true, false).forEach { v ->
val flag = true
val expected = callback.getBool(v, flag)
val observed = rustGetters.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 = rustGetters.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 = rustGetters.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 = rustGetters.getOption(callback, v, flag)
assert(expected == observed) { "roundtripping through callback: $expected != $observed" }
}
rustGetters.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 StoredKotlinStringifier: StoredForeignStringifier {
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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but the comments here are a good reminder that we should have more thorough tests for this at the Rust level. There's nothing Kotlin-specific about the behaviour of TypeResolver in this regard, we should be able to write a pure Rust test that parses some UDL and then asserts things about the state of the resulting ComponentInterface and its TypeUniverse.

override fun fromComplexType(values: List<Double?>?): String = "kotlin: $values"
}

val kotlinStringifier = StoredKotlinStringifier()
val rustStringifier = RustStringifier(kotlinStringifier)
listOf(1, 2).forEach { v ->
val expected = kotlinStringifier.fromSimpleType(v)
val observed = rustStringifier.fromSimpleType(v)
assert(expected == observed) { "callback is sent on construction: $expected != $observed" }
}
rustStringifier.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",
]
);
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
Loading