Skip to content

Commit d5339e6

Browse files
committed
Sketch out an actual proposed solution for non-blocking concurrency.
1 parent 8933531 commit d5339e6

File tree

10 files changed

+134
-6
lines changed

10 files changed

+134
-6
lines changed

docs/manual/book.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[book]
2-
authors = ["Edouard Oger"]
2+
authors = ["Edouard Oger", "Ryan Kelly"]
33
language = "en"
44
multilingual = false
55
src = "src"

docs/manual/src/SUMMARY.md

+6
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,9 @@ Hello everyone
2424
# Swift
2525

2626
- [Integrating with XCode](./swift/xcode.md)
27+
28+
# Internals
29+
30+
- [Lifting, Lowering, and Serialization](./internals/lifting_and_lowering.md)
31+
- [Serialization format](./internals/serialization_format.md)
32+
- [Managing object references](./internals/object_references.md)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Lifting, Lowering and Serialization
2+
3+
Here is where I will write about lifting and lowering.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Managing Object References
2+
3+
Here is where I'll talk about handlemaps, and about the upcoming "threadsafe" mode.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Serialization Format
2+
3+
Here is where I will describe our incredibly basic serialization format.

docs/manual/src/udl/interfaces.md

+109-1
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,112 @@ func display(list: TodoListProtocol) {
7171
print($0)
7272
}
7373
}
74-
```
74+
```
75+
76+
# Concurrent Access
77+
78+
Since interfaces represent mutable data, uniffi has to take extra care
79+
to uphold Rust's safety guarantees around shared and mutable references.
80+
The foreign-language code may attempt to operate on an interface instance
81+
from multiple threads, and it's important that this not violate Rust's
82+
assumption that there is at most a single mutable reference to a struct
83+
at any point in time.
84+
85+
By default, uniffi enforces this using runtime locking. Each interface instance
86+
has an associated lock which is transparently acquired at the beginning of each
87+
call to a method of that instance, and released once the method returns. This
88+
approach is simple and safe, but it means that all method calls on an instance
89+
are run in a strictly sequential fashion, limiting concurrency.
90+
91+
You can opt out of this protection by marking the interface as threadsafe:
92+
93+
```idl
94+
[Threadsafe]
95+
interface Counter {
96+
constructor();
97+
void increment();
98+
u64 get();
99+
};
100+
```
101+
102+
The uniffi-generated code will allow concurrent method calls on threadsafe interfaces
103+
without any locking.
104+
105+
For this to be safe, the underlying Rust struct must adhere to certain restrictions, and
106+
uniffi's generated Rust scaffolding will emit compile-time errors if it does not.
107+
108+
The Rust struct must not expose any methods that take `&mut self`. The following implementation
109+
of the `Counter` interface will fail to compile because it relies on mutable references:
110+
111+
```rust
112+
struct Counter {
113+
value: u64
114+
}
115+
116+
impl Counter {
117+
fn new() -> Self {
118+
Self { value: 0 }
119+
}
120+
121+
// No mutable references to self allowed in [Threadsafe] interfaces.
122+
fn increment(&mut self) {
123+
self.value = self.value + 1;
124+
}
125+
126+
fn get(&self) -> u64 {
127+
self.value
128+
}
129+
}
130+
```
131+
132+
Implementations can instead use Rust's "interior mutability" pattern. However, they
133+
must do so in a way that is both `Sync` and `Send`, since the foreign-language code
134+
may operate on the instance from multiple threads. The following implementation of the
135+
`Counter` interface will fail to compile because `RefCell` is not `Send`:
136+
137+
```rust
138+
struct Counter {
139+
value: RefCell<u64>
140+
}
141+
142+
impl Counter {
143+
fn new() -> Self {
144+
Self { value: RefCell::new(0) }
145+
}
146+
147+
fn increment(&self) {
148+
let mut value = self.value.borrow_mut();
149+
*value = *value + 1;
150+
}
151+
152+
fn get(&self) -> u64 {
153+
*self.value.borrow()
154+
}
155+
}
156+
```
157+
158+
This version uses the `AtomicU64` struct for interior mutability, which is both `Sync` and
159+
`Send` and hence will compile successfully:
160+
161+
```rust
162+
struct Counter {
163+
value: i64
164+
}
165+
166+
impl Counter {
167+
fn new() -> Self {
168+
Self { 0 }
169+
}
170+
171+
fn increment(&self) {
172+
self.value = self.value + 1;
173+
}
174+
175+
fn get(&self) -> i64 {
176+
self.value
177+
}
178+
}
179+
```
180+
181+
Uniffi aims to uphold Rust's safety guarantees at all times, without requiring the
182+
foreign-language code to know or care about them.

uniffi/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ bytes = "0.5"
1717
ffi-support = "~0.4.2"
1818
lazy_static = "1.4"
1919
log = "0.4"
20+
static_assertions = "1"
2021
# Regular dependencies
2122
cargo_metadata = "0.11"
2223
paste = "1.0"

uniffi/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub mod deps {
4747
pub use ffi_support;
4848
pub use lazy_static;
4949
pub use log;
50+
pub use static_assertions;
5051
}
5152

5253
/// Trait defining how to transfer values via the FFI layer.

uniffi_bindgen/src/templates/ObjectTemplate.rs

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ unsafe impl uniffi::deps::ffi_support::IntoFfi for {{ obj.name() }} {
2222
fn into_ffi_value(self) -> Self::Value { Some(Box::new(self)) }
2323
}
2424

25+
// For thread-safety, we only support raw pointers on things that are Sync and Send.
26+
uniffi::deps::static_assertions::assert_impl_all!({{ obj.name() }}: Sync, Send);
27+
2528
#[no_mangle]
2629
pub extern "C" fn {{ obj.ffi_object_free().name() }}(obj : Option<Box<{{ obj.name() }}>>) {
2730
drop(obj);

uniffi_bindgen/src/templates/macros.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -55,19 +55,19 @@ uniffi::deps::ffi_support::call_with_output(err, || {
5555
uniffi::deps::ffi_support::call_with_result(err, || -> Result<{% call return_type_func(meth) %}, {{e}}> {
5656
// We're declaring the argument type as an `Option<Box<T>>` but the value is owned by the foreign code,
5757
// we so don't want to drop the Box. Probably it would be better to encode this as a reference type.
58-
let mut obj_box = std::mem::ManuallyDrop::new(obj.expect("Must receive a non-null object pointer"));
58+
let obj_box = std::mem::ManuallyDrop::new(obj.expect("Must receive a non-null object pointer"));
5959
// TODO: terrifically unsafe to assume we can take a mutable reference here! Needs locks etc.
60-
let obj = obj_box.as_mut();
60+
let obj = obj_box.as_ref();
6161
let _retval = {{ obj.name() }}::{%- call to_rs_call_with_prefix("obj", meth) -%}?;
6262
Ok({% call ret(meth) %})
6363
})
6464
{% else %}
6565
uniffi::deps::ffi_support::call_with_output(err, || {
6666
// We're declaring the argument type as an `Option<Box<T>>` but the value is owned by the foreign code,
6767
// we so don't want to drop the Box. Probably it would be better to encode this as a reference type.
68-
let mut obj_box = std::mem::ManuallyDrop::new(obj.expect("Must receive a non-null object pointer"));
68+
let obj_box = std::mem::ManuallyDrop::new(obj.expect("Must receive a non-null object pointer"));
6969
// TODO: terrifically unsafe to assume we can take a mutable reference here! Needs locks etc.
70-
let obj = obj_box.as_mut();
70+
let obj = obj_box.as_ref();
7171
let _retval = {{ obj.name() }}::{%- call to_rs_call_with_prefix("obj", meth) -%};
7272
{% call ret(meth) %}
7373
})

0 commit comments

Comments
 (0)