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 narrative docs for threadsafe instances. #371

Merged
merged 1 commit into from
Jan 25, 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
2 changes: 1 addition & 1 deletion docs/manual/src/internals/lifting_and_lowering.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ namespace example {

Calling this function from foreign language code involves the following steps:

1. The user-provided calling code invokes the `add_to_list` function that is expoed by the
1. The user-provided calling code invokes the `add_to_list` function that is exposed by the
uniffi-generated foreign language bindings, passing `item` as an appropriate language-native
integer.
2. The foreign language bindings ***lower*** each argument to a function call into
Expand Down
27 changes: 23 additions & 4 deletions docs/manual/src/internals/object_references.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ Uniffi aims to maintain these guarantees even when the Rust code is being invoke
from a foreign language, at the cost of turning them into run-time checks rather
than compile-time guarantees.

## Handle Maps

We achieve this by indirecting all object access through a
[handle map](https://docs.rs/ffi-support/0.4.0/ffi_support/handle_map/index.html),
a mapping from opaque integer handles to object instances. This indirection
imposes a small runtime cost but helps us guard against errors or oversights
in the generated bindings.

For each interface declared in the UDL, the uniffi-generated Rust scaffolding
will create a global [ffi_support::ConcurrentHandleMap](https://docs.rs/ffi-support/0.4.0/ffi_support/handle_map/struct.ConcurrentHandleMap.html) that is responsible for owning all instances
will create a global handlemap that is responsible for owning all instances
of that interface, and handing out references to them when methods are called.

For example, given a interface definition like this:
Expand Down Expand Up @@ -55,7 +57,7 @@ pub extern "C" fn todolist_TodoList_new(err: &mut ExternError) -> u64 {
```

When invoking a method on the instance, the foreign-language code passes the integer handle back
to the Rust code, which borrows a reference to the instance from the handlemap for the duration
to the Rust code, which borrows a mutable reference to the instance from the handlemap for the duration
of the method call:

```rust
Expand All @@ -82,5 +84,22 @@ This indirection gives us some important safety properties:
* If the generated bindings incorrectly pass an invalid handle, or a handle for a different type of object,
then the handlemap will throw an error with high probability, providing some amount of run-time typechecking
for correctness of the generated bindings.
* The `ConcurrentHandleMap` class wraps each instance with a `Mutex`, which serializes access to the instance
and upholds Rust's guarantees against shared mutable access.
* The handlemap can ensure we uphold Rust's requirements around unique mutable references and threadsafey,
using a combination of compile-time checks and runtime locking depending on the details of the underlying
Rust struct that implements the interface.

## Managing Concurrency

By default, uniffi uses the [ffi_support::ConcurrentHandleMap](https://docs.rs/ffi-support/0.4.0/ffi_support/handle_map/struct.ConcurrentHandleMap.html) struct as the handlemap for each declared instance. This class
wraps each instance with a `Mutex`, which serializes access to the instance and upholds Rust's guarantees
against shared mutable access. This approach is simple and safe, but it means that all method calls
on an instance are run in a strictly sequential fashion, limiting concurrency.

For instances that are explicited tagged with the `[Threadsafe]` attribute, uniffi instead uses
a custom `ArcHandleMap` struct. This replaces the run-time `Mutex` with compile-time assertions
about the safety of the underlying Rust struct. Specifically:

* The `ArcHandleMap` will never give out a mutable reference to an instance, forcing the
underlying struct to use interior mutability and manage its own locking.
* The `ArcHandleMap` can only contain structs that are `Sync` and `Send`, ensuring that
shared references can safely be accessed from multiple threads.
96 changes: 94 additions & 2 deletions docs/manual/src/udl/interfaces.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 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.
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](./structs.md).

The following Rust code:
Expand Down Expand Up @@ -82,11 +83,102 @@ 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 using runtime locking. Each interface instance
By default, uniffi enforces this using runtime locking. Each interface instance
has an associated lock which is transparently acquired at the beginning of each
call to a method of that instance, and released once the method returns. This
approach is simple and safe, but it means that all method calls on an instance
are run in a strictly sequential fashion, limiting concurrency.

You can opt out of this protection by marking the interface as threadsafe:

```idl
[Threadsafe]
interface Counter {
constructor();
void increment();
u64 get();
};
```

The uniffi-generated code will allow concurrent method calls on threadsafe interfaces
without any locking.

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:

```rust
struct Counter {
value: u64
}

impl Counter {
fn new() -> Self {
Self { value: 0 }
}

// No mutable references to self allowed in [Threadsafe] 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 `Send`:

```rust
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:

```rust
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](../internals/object_references.md).