Skip to content

Commit 6bb74fe

Browse files
committed
Adding ADR-0009
This revisits the decision in `ADR-0005` and explores using going back to handle maps to pass objects across the FFI.
1 parent fbc6631 commit 6bb74fe

File tree

2 files changed

+136
-1
lines changed

2 files changed

+136
-1
lines changed

docs/adr/0005-arc-pointers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Use raw `Arc<T>` pointers to pass objects across the FFI
22

3-
* Status: proposed
3+
* Status: accepted
44
* Deciders: mhammond, rfkelly
55
* Consulted: travis, jhugman, dmose
66
* Date: 2021-04-19

docs/adr/0009-handles.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Use handle map handles to pass objects across the FFI
2+
3+
* Status: proposed
4+
* Deciders:
5+
* Consulted:
6+
7+
Discussion and approval:
8+
9+
ADR-0005 discussion: [PR 430](https://github.com/mozilla/uniffi-rs/pull/430).
10+
11+
## Context and Problem Statement
12+
13+
UniFFI currently passes objects from Rust to the foreign side by leaking an Arc reference into a word-sized opaque pointer and passing it across the FFI.
14+
The basic approach uses `Arc::into_raw` / `Arc::from_raw` and was chosen in [ADR-0005](./0005-arc-pointers.md) for several reasons:
15+
16+
1. Clearer generated code.
17+
2. Ability to pass objects as arguments (https://github.com/mozilla/uniffi-rs/issues/40).
18+
This was deemed difficult to do with the existing codegen + HandleMap.
19+
3. Ability to track object identity (https://github.com/mozilla/uniffi-rs/issues/197). If two function calls return the same object, then this should result in an identical object on the foreign side.
20+
4. Increased performance.
21+
22+
Recently, this approach was extended to work with unsized types (`Arc<dyn Trait>`), which are normally wide pointers (i.e double-word sized).
23+
For these types, we box the Arc to create `Box<Arc<dyn Trait>>`, then leak the box pointer.
24+
This results in a regular, word-sized, pointer since `Arc<dyn Trait>` is sized (2 words) even when `dyn Trait` is not.
25+
26+
Now that we have several years of experience, it's a good time to revisit some of the reasoning in ADR-0005 because it seems like we're not getting the benefits we wanted:
27+
28+
* The code that deals with these isn't so clear, especially when we have to deal with unsized types (for example
29+
the RustFuture
30+
[allocation](https://github.com/mozilla/uniffi-rs/blob/fbc6631953a889c7af6e5f1af94de9242589b75b/uniffi_core/src/ffi/rustfuture/mod.rs#L56-L63) / [dellocation](https://github.com/mozilla/uniffi-rs/blob/fbc6631953a889c7af6e5f1af94de9242589b75b/uniffi_core/src/ffi/rustfuture/mod.rs#L124-L125) or the similar code for trait interfaces).
31+
* The codegen has progressed and it would be easy to support `[2]`.
32+
We could simply `clone` the handle as part of the `lower()` call.
33+
* We've never implemented the reverse identity map needed for `[3]`.
34+
The `NimbusClient` example given in https://github.com/mozilla/uniffi-rs/issues/419 would still fail today.
35+
Given that there has been little to no demand for this feature, this should be changed to a non-goal.
36+
* The performance benefit decreases when discussing unsized types which require an additional layer of boxing.
37+
In that case, instead of a strict decrease in work, we are trading a `HandleMap` insertion for a Box allocation.
38+
This is a complex tradeoff, with the box allocation likely being faster, but not by much.
39+
40+
Furthermore, practice has shown that dealing with raw pointers makes debugging difficult, with errors often resulting in segfaults or UB.
41+
Dealing with any sort of FFI handle is going to be error prone, but at least with a handle map we can generate better error messages and correct stack traces.
42+
There are also more error modes with this code.
43+
44+
### Foreign handles
45+
46+
A related question is how to handle handles to foreign objects that are passed into Rust.
47+
However, that question is orthogonal to this one and is out-of-scope for this ADR.
48+
49+
## Considered Options
50+
51+
### [Option 1] Continue using raw Arc pointers to pass Rust objects across the FFI
52+
53+
Stay with the current status quo.
54+
55+
### [Option 2] Use the old `HandleMap` to pass Rust objects across the FFI
56+
57+
We could switch back to the old handle map code, which is still around in the [ffi-support crate](https://github.com/mozilla/ffi-support/blob/main/src/handle_map.rs).
58+
This implements a relatively simple handle-map that uses a `RWLock` to manage concurrency.
59+
60+
This code has 3 basic operations:
61+
- `insert`: insert a new entry in the `HandleMap` and get a `Handle` (takes the write lock).
62+
- `remove`: remove an existing entry via a `Handle` (takes the write lock).
63+
- `get`: get an existing entry via a `Handle` (takes the read lock).
64+
65+
These handles have some extra safety features over raw pointers:
66+
* A map ID to identify handles used with the wrong handle map.
67+
* A generation counter to identify use-after-free bugs.
68+
69+
Handles are passed as a `u64` values, but they only actually use 48 bits.
70+
This works better with JS, where the `Value` type only supports integers up to 53-bits wide.
71+
72+
### [Option 3] Use a `HandleMap` with more performant/complex concurrency strategy
73+
74+
We could switch to something like the [handle map implementation from #1808](https://github.com/bendk/uniffi-rs/blob/d305f7e47203b260e2e44009e37e7435fd554eaa/uniffi_core/src/ffi/slab.rs).
75+
The struct in that code was named `Slab` because it was inspired by the `tokio` `slab` crate.
76+
However, it's very similar to the original UniFFI `HandleMap` and this PR will call it a `HandleMap` to follow in that tradition.
77+
78+
Handles are almost identical to the original handle map implementation, including the 48-bit size.
79+
The difference is mostly the number of bits are allocated for the map id, generation counter, and index.
80+
81+
Instead of an `RwLock` this implementation uses a hand-written locking system.
82+
`insert` and `remove` still require a lock, but `get` doesn't.
83+
This is possible because:
84+
85+
* In general, `get` will access different indexes of the underlying `Vec` than `insert`/`remove`.
86+
* One exception is an `insert` that grows the `Vec`, which would normally affect all elements.
87+
The code leverages the [append_only_vec](https://docs.rs/append-only-vec/latest/append_only_vec/) crate to avoid this issue.
88+
* The other exception is a use-after-free bug.
89+
The code avoids this by storing an 8-bit read-write spin-lock alongside each element.
90+
Spinning will only happen if there's a use-after-free bug.
91+
92+
### [Option 4] Use a 3rd-party crate to pass Rust objects across the FFI
93+
94+
We could also use a 3rd-party crate to handle this.
95+
The `sharded-slab` crate promises lock-free concurrency and supports generation counters.
96+
97+
## Decision Drivers
98+
99+
## Decision Outcome
100+
101+
???
102+
103+
## Pros and Cons of the Options
104+
105+
### [Option 1] Continue using raw Arc pointers to pass Rust objects across the FFI
106+
107+
* Good, because it has the fastest performance, especially for sized types.
108+
* Good, because it doesn't require code changes.
109+
* Bad, because it's hard to debug errors.
110+
111+
### [Option 2] Use the original handle map to pass Rust objects across the FFI
112+
113+
* Good, because it's easier to debug errors.
114+
* Bad, because it requires a read-write lock.
115+
In particular, it seems bad that `insert`/`remove` can block `get`.
116+
* Good, because it works better with Javascript
117+
* Good, because it works with any type, not just `Arc<T>`.
118+
For example, we might want to pass a handle to a [oneshot::Sender](https://docs.rs/oneshot/latest/oneshot/) across the FFI to implement async callback interface methods.
119+
120+
### [Option 3] Use a handle map with a simpler concurrency strategy
121+
122+
* Good, because it's easier to debug errors.
123+
* Good because `get` doesn't require a lock.
124+
* Bad because `insert` and `remove` requires a lock.
125+
* Bad, because it requires consumers to depend on `append-only-vec`.
126+
However, this is a quite small crate.
127+
* Good, because it works better with Javascript
128+
* Good, because it works with any type, not just `Arc<T>`.
129+
130+
### [Option 4] Use a 3rd-party crate to pass Rust objects across the FFI
131+
132+
* Good, because it's easier to debug errors.
133+
* Bad, because it requires consumers to take this dependency.
134+
* Bad, because it makes it harder to implement custom functionality.
135+
For example, supporting clone to fix https://github.com/mozilla/uniffi-rs/issues/1797 or adding a foreign bit to improve trait interface handling.

0 commit comments

Comments
 (0)