diff --git a/docs/manual/src/internals/lifting_and_lowering.md b/docs/manual/src/internals/lifting_and_lowering.md index 4c7a35d16a..4b3652bdb2 100644 --- a/docs/manual/src/internals/lifting_and_lowering.md +++ b/docs/manual/src/internals/lifting_and_lowering.md @@ -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 diff --git a/docs/manual/src/internals/object_references.md b/docs/manual/src/internals/object_references.md index 70e1e9590b..d2183d20d7 100644 --- a/docs/manual/src/internals/object_references.md +++ b/docs/manual/src/internals/object_references.md @@ -14,6 +14,8 @@ 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 @@ -21,7 +23,7 @@ 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: @@ -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 @@ -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. \ No newline at end of file +* 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. diff --git a/docs/manual/src/udl/interfaces.md b/docs/manual/src/udl/interfaces.md index 3679712b81..465dab869a 100644 --- a/docs/manual/src/udl/interfaces.md +++ b/docs/manual/src/udl/interfaces.md @@ -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: @@ -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 +} + +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). \ No newline at end of file