-
Notifications
You must be signed in to change notification settings - Fork 232
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
Experiment with using raw pointers for objects. #366
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. |
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. |
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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,4 +71,113 @@ func display(list: TodoListProtocol) { | |
print($0) | ||
} | ||
} | ||
``` | ||
``` | ||
|
||
# Concurrent Access | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Note that the PR doesn't actually implement proposal yet, although I'm confident that it can be implemented without much fuss) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems awesome. While it doesn't handle the "multiple bindings" use-case, I wonder if we can avoid uniffi knowing about that kind of setup? Eg, I wonder if we can arrange so that instead of lib.rs having, say:
instead it does something like:
(ie, that it's OK if there are multiple instances of the bindings, because they all just delegate). I'd need to play a little to see if that might work in practice though. Did you have anything in mind for that use-case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (We discussed this a little in meetings and slack, but commenting here for posterity) I'd be interested in exposing the singleton concept more directly through the IDL. For example, imagine a Nimbus API definition that looks like this:
Uniffi doesn't support this today. Specifically, we don't let you pass object references as arguments (#40) or return them from functions (#197), so you wouldn't be able to declare the getter/setter for the singleton. However, I think it's clear that we want to support such functionality at some point, and I don't think there's any deep technical reason why we couldn't do so, there's just a bunch of details to carefully work through (as described in the linked issues). |
||
|
||
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] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bikeshedding also welcome on how to expose this in the UDL... |
||
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 the `AtomicU64` struct 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) | ||
} | ||
} | ||
``` | ||
|
||
Uniffi aims to uphold Rust's safety guarantees at all times, without requiring the | ||
foreign-language code to know or care about them. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much like our lifting/lowering/serialization story, the way we currently use handlemaps is under-documented. Let's add a new section to the manual that we can use for narrative docs about such things.