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