Interfaces/Objects
Interfaces are represented in the Rust world as a struct with an impl
block containing methods. In the Kotlin or Swift world, it's a class.
Because Objects are passed by reference and Dictionaries by value, in the UniFFI world it is impossible to be both an Object and a Dictionary.
The following Rust code:
struct TodoList {
items: RwLock<Vec<String>>
}
impl TodoList {
fn new() -> Self {
TodoList {
items: RwLock::new(Vec::new())
}
}
fn add_item(&self, todo: String) {
self.items.write().unwrap().push(todo);
}
fn get_items(&self) -> Vec<String> {
self.items.read().unwrap().clone()
}
}
would be exposed using:
interface TodoList {
constructor();
void add_item(string todo);
sequence<string> get_items();
};
By convention, the constructor()
calls the Rust's new()
method.
Conceptually, these interface
objects are live Rust structs that have a proxy object on the foreign language side; calling any methods on them, including a constructor or destructor results in the corresponding methods being called in Rust. If you do not specify a constructor the bindings will be unable to create the interface directly.
UniFFI will generate these proxies with an interface or protocol to help with testing in the foreign-language code. For example in Kotlin, the TodoList
would generate:
interface TodoListInterface {
fun addItem(todo: String)
fun getItems(): List<String>
}
class TodoList : TodoListInterface {
// implementations to call the Rust code.
}
When working with these objects, it may be helpful to always pass the interface or protocol, but construct the concrete implementation. For example in Swift:
let todoList = TodoList()
todoList.addItem(todo: "Write documentation")
display(list: todoList)
func display(list: TodoListProtocol) {
let items = list.getItems()
items.forEach {
print($0)
}
}
Following this pattern will make it easier for you to provide mock implementation of the Rust-based objects for testing.
Exposing Traits as interfaces
It's possible to have UniFFI expose a Rust trait as an interface by specifying a Trait
attribute.
For example, in the UDL file you might specify:
[Trait]
interface Button {
string name();
};
With the following Rust implementation:
pub trait Button: Send + Sync {
fn name(&self) -> String;
}
struct StopButton {}
impl Button for StopButton {
fn name(&self) -> String {
"stop".to_string()
}
}
Uniffi explicitly checks all interfaces are Send + Sync
- there's a ui-test which demonstrates obscure rust compiler errors when it's not true. Traits however need to explicitly add those bindings.
References to traits are passed around like normal interface objects - in an Arc<>
.
For example, this UDL:
namespace traits {
sequence<Button> get_buttons();
Button press(Button button);
};
would have these signatures in Rust:
fn get_buttons() -> Vec<Arc<dyn Button>> { ... }
fn press(button: Arc<dyn Button>) -> Arc<dyn Button> { ... }
Foreign implementations
Use the WithForeign
attribute to allow traits to also be implemented on the foreign side passed into Rust, for example:
[Trait, WithForeign]
interface Button {
string name();
};
class PyButton(uniffi_module.Button):
def name(self):
return "PyButton"
uniffi_module.press(PyButton())
Note: This is currently only supported on Python, Kotlin, and Swift.
Traits construction
Because any number of struct
s may implement a trait, they don't have constructors.
Traits example
See the "traits" example for more.
Alternate Named Constructors
In addition to the default constructor connected to the ::new()
method, you can specify
alternate named constructors to create object instances in different ways. Each such constructor
must be given an explicit name, provided in the UDL with the [Name]
attribute like so:
interface TodoList {
// The default constructor makes an empty list.
constructor();
// This alternate constructor makes a new TodoList from a list of string items.
[Name=new_from_items]
constructor(sequence<string> items);
// This alternate constructor is async.
[Async, Name=new_async]
constructor(sequence<string> items);
...
For each alternate constructor, UniFFI will expose an appropriate static-method, class-method or similar in the foreign language binding, and will connect it to the Rust method of the same name on the underlying Rust struct.
Constructors can be async, although support for async primary constructors in bindings is minimal.
Exposing methods from standard Rust traits
Rust has a number of general purpose traits which add functionality to objects, such
as Debug
, Display
, etc. It's possible to tell UniFFI that your object implements these
traits and to generate FFI functions to expose them to consumers. Bindings may then optionally
generate special methods on the object.
For example, consider the following example:
[Traits=(Debug)]
interface TodoList {
...
}
#[derive(Debug)]
struct TodoList {
...
}
#[derive(Debug, uniffi::Object)]
#[uniffi::export(Debug)]
struct TodoList {
...
}
This will cause the Python bindings to generate a __repr__
method that returns the value implemented by the Debug
trait.
Not all bindings support generating special methods, so they may be ignored.
It is your responsibility to implement the trait on your objects; UniFFI will attempt to generate a meaningful error if you do not.
The list of supported traits is hard-coded in UniFFI's internals, and at time of writing
is Debug
, Display
, Eq
and Hash
.
Managing Shared References
To the foreign-language consumer, UniFFI object instances are designed to behave as much like regular language objects as possible. They can be freely passed as arguments or returned as values, like this:
interface TodoList {
...
// Copy the items from another TodoList into this one.
void import_items(TodoList other);
// Make a copy of this TodoList as a new instance.
TodoList duplicate();
// Create a list of lists, one for each item this one
sequence<TodoList> split();
};
To ensure that this is safe, UniFFI allocates every object instance on the heap using
Arc
, Rust's built-in smart pointer
type for managing shared references at runtime.
The use of Arc
is transparent to the foreign-language code, but sometimes shows up
in the function signatures of the underlying Rust code.
When returning interface objects, UniFFI supports both Rust functions that wrap the value in an
Arc<>
and ones that don't. This only applies if the interface type is returned directly:
impl TodoList {
// When the foreign function/method returns `TodoList`, the Rust code can return either `TodoList` or `Arc<TodoList>`.
fn duplicate(&self) -> TodoList {
TodoList {
items: RwLock::new(self.items.read().unwrap().clone())
}
}
// However, if TodoList is nested inside another type then `Arc<TodoList>` is required
fn split(&self) -> Vec<Arc<TodoList>> {
self.items.read()
.iter()
.map(|i| Arc::new(TodoList::from_item(i.clone()))
.collect()
}
}
By default, object instances passed as function arguments will also be passed as an Arc<T>
, so the
Rust implementation of TodoList::import_items
would also need to accept an Arc<TodoList>
:
impl TodoList {
fn import_items(&self, other: Arc<TodoList>) {
self.items.write().unwrap().append(other.get_items());
}
}
If the Rust code does not need an owned reference to the Arc
, you can use the [ByRef]
UDL attribute
to signal that a function accepts a borrowed reference:
interface TodoList {
...
// +-- indicate that we only need to borrow the other list
// V
void import_items([ByRef] TodoList other);
...
};
impl TodoList {
// +-- don't need to care about the `Arc` here
// V
fn import_items(&self, other: &TodoList) {
self.items.write().unwrap().append(other.get_items());
}
}
Conversely, if the Rust code explicitly wants to deal with an Arc<T>
in the special case of
the self
parameter, it can signal this using the [Self=ByArc]
UDL attribute on the method:
interface TodoList {
...
// +-- indicate that we want the `Arc` containing `self`
// V
[Self=ByArc]
void import_items(TodoList other);
...
};
impl TodoList {
// `Arc`s everywhere! --+-----------------+
// V V
fn import_items(self: Arc<Self>, other: Arc<TodoList>) {
self.items.write().unwrap().append(other.get_items());
}
}
You can read more about the technical details in the docs on the internal details of managing object references.
Concurrent Access
Since interfaces represent mutable data, UniFFI has to take extra care to uphold Rust's safety guarantees around shared and mutable references. The foreign-language code may attempt to operate on an interface instance from multiple threads, and it's important that this not violate Rust's assumption that there is at most a single mutable reference to a struct at any point in time.
UniFFI enforces this by requiring that the Rust implementation of an interface
be Sync+Send
, and you will get a compile-time error if your implementation
does not satisfy this requirement. For example, consider a small "counter"
object declared like so:
interface Counter {
constructor();
void increment();
u64 get();
};
For this to be safe, the underlying Rust struct must adhere to certain restrictions, and UniFFI's generated Rust scaffolding will emit compile-time errors if it does not.
The Rust struct must not expose any methods that take &mut self
. The following implementation
of the Counter
interface will fail to compile because it relies on mutable references:
struct Counter {
value: u64
}
impl Counter {
fn new() -> Self {
Self { value: 0 }
}
// No mutable references to self allowed in UniFFI interfaces.
fn increment(&mut self) {
self.value = self.value + 1;
}
fn get(&self) -> u64 {
self.value
}
}
Implementations can instead use Rust's "interior mutability" pattern. However, they
must do so in a way that is both Sync
and Send
, since the foreign-language code
may operate on the instance from multiple threads. The following implementation of the
Counter
interface will fail to compile because RefCell
is not Sync
:
struct Counter {
value: RefCell<u64>
}
impl Counter {
fn new() -> Self {
// `RefCell` is not `Sync`, so neither is `Counter`.
Self { value: RefCell::new(0) }
}
fn increment(&self) {
let mut value = self.value.borrow_mut();
*value = *value + 1;
}
fn get(&self) -> u64 {
*self.value.borrow()
}
}
This version uses an AtomicU64
for interior mutability, which is both Sync
and
Send
and hence will compile successfully:
struct Counter {
value: AtomicU64
}
impl Counter {
fn new() -> Self {
Self { value: AtomicU64::new(0) }
}
fn increment(&self) {
self.value.fetch_add(1, Ordering::SeqCst);
}
fn get(&self) -> u64 {
self.value.load(Ordering::SeqCst)
}
}
You can read more about the technical details in the docs on the internal details of managing object references.