Skip to content

Commit

Permalink
Sketch out an actual proposed solution for non-blocking concurrency.
Browse files Browse the repository at this point in the history
  • Loading branch information
rfk committed Jan 12, 2021
1 parent 8933531 commit d5339e6
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 6 deletions.
2 changes: 1 addition & 1 deletion docs/manual/book.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[book]
authors = ["Edouard Oger"]
authors = ["Edouard Oger", "Ryan Kelly"]
language = "en"
multilingual = false
src = "src"
Expand Down
6 changes: 6 additions & 0 deletions docs/manual/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ Hello everyone
# Swift

- [Integrating with XCode](./swift/xcode.md)

# Internals

- [Lifting, Lowering, and Serialization](./internals/lifting_and_lowering.md)
- [Serialization format](./internals/serialization_format.md)
- [Managing object references](./internals/object_references.md)
3 changes: 3 additions & 0 deletions docs/manual/src/internals/lifting_and_lowering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Lifting, Lowering and Serialization

Here is where I will write about lifting and lowering.
3 changes: 3 additions & 0 deletions docs/manual/src/internals/object_references.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Managing Object References

Here is where I'll talk about handlemaps, and about the upcoming "threadsafe" mode.
3 changes: 3 additions & 0 deletions docs/manual/src/internals/serialization_format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Serialization Format

Here is where I will describe our incredibly basic serialization format.
110 changes: 109 additions & 1 deletion docs/manual/src/udl/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,112 @@ func display(list: TodoListProtocol) {
print($0)
}
}
```
```

# 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.

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 {
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 the `AtomicU64` struct for interior mutability, which is both `Sync` and
`Send` and hence will compile successfully:

```rust
struct Counter {
value: i64
}

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

fn increment(&self) {
self.value = self.value + 1;
}

fn get(&self) -> i64 {
self.value
}
}
```

Uniffi aims to uphold Rust's safety guarantees at all times, without requiring the
foreign-language code to know or care about them.
1 change: 1 addition & 0 deletions uniffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ bytes = "0.5"
ffi-support = "~0.4.2"
lazy_static = "1.4"
log = "0.4"
static_assertions = "1"
# Regular dependencies
cargo_metadata = "0.11"
paste = "1.0"
Expand Down
1 change: 1 addition & 0 deletions uniffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub mod deps {
pub use ffi_support;
pub use lazy_static;
pub use log;
pub use static_assertions;
}

/// Trait defining how to transfer values via the FFI layer.
Expand Down
3 changes: 3 additions & 0 deletions uniffi_bindgen/src/templates/ObjectTemplate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ unsafe impl uniffi::deps::ffi_support::IntoFfi for {{ obj.name() }} {
fn into_ffi_value(self) -> Self::Value { Some(Box::new(self)) }
}

// For thread-safety, we only support raw pointers on things that are Sync and Send.
uniffi::deps::static_assertions::assert_impl_all!({{ obj.name() }}: Sync, Send);

#[no_mangle]
pub extern "C" fn {{ obj.ffi_object_free().name() }}(obj : Option<Box<{{ obj.name() }}>>) {
drop(obj);
Expand Down
8 changes: 4 additions & 4 deletions uniffi_bindgen/src/templates/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,19 @@ uniffi::deps::ffi_support::call_with_output(err, || {
uniffi::deps::ffi_support::call_with_result(err, || -> Result<{% call return_type_func(meth) %}, {{e}}> {
// We're declaring the argument type as an `Option<Box<T>>` but the value is owned by the foreign code,
// we so don't want to drop the Box. Probably it would be better to encode this as a reference type.
let mut obj_box = std::mem::ManuallyDrop::new(obj.expect("Must receive a non-null object pointer"));
let obj_box = std::mem::ManuallyDrop::new(obj.expect("Must receive a non-null object pointer"));
// TODO: terrifically unsafe to assume we can take a mutable reference here! Needs locks etc.
let obj = obj_box.as_mut();
let obj = obj_box.as_ref();
let _retval = {{ obj.name() }}::{%- call to_rs_call_with_prefix("obj", meth) -%}?;
Ok({% call ret(meth) %})
})
{% else %}
uniffi::deps::ffi_support::call_with_output(err, || {
// We're declaring the argument type as an `Option<Box<T>>` but the value is owned by the foreign code,
// we so don't want to drop the Box. Probably it would be better to encode this as a reference type.
let mut obj_box = std::mem::ManuallyDrop::new(obj.expect("Must receive a non-null object pointer"));
let obj_box = std::mem::ManuallyDrop::new(obj.expect("Must receive a non-null object pointer"));
// TODO: terrifically unsafe to assume we can take a mutable reference here! Needs locks etc.
let obj = obj_box.as_mut();
let obj = obj_box.as_ref();
let _retval = {{ obj.name() }}::{%- call to_rs_call_with_prefix("obj", meth) -%};
{% call ret(meth) %}
})
Expand Down

0 comments on commit d5339e6

Please sign in to comment.