|
| 1 | +# How do UniFFI handles and handle maps work? |
| 2 | + |
| 3 | +UniFFI uses handles to pass Rust objects across the FFI to the foreign code. |
| 4 | +The handles point to an entry inside a `HandleMap` |
| 5 | + |
| 6 | +## HandleMap |
| 7 | + |
| 8 | +A `HandleMap` is a `Vec` where each item is either: |
| 9 | + |
| 10 | +- **Occupied** |
| 11 | + - The foreign side holds a handle that's associated with the entry. |
| 12 | + - Stores a `T` value (typically an `Arc<>`). |
| 13 | + - Stores a `generation` counter used to detect use-after-free bugs |
| 14 | + - Stores a `map-id` value used to detect handles used with the wrong `HandleMap` |
| 15 | +- **Vacant** |
| 16 | + - Each vacant entry stores the index of the next vacant entry. |
| 17 | + These form a linked-list of available entries and allow us to quickly allocate a new entry in the `HandleMap` |
| 18 | + - Stores the `generation` counter value from the last time it was occupied, or 0 if it was never occupied. |
| 19 | + |
| 20 | +Furthermore, each `HandleMap` stores its own `next` value, which points to the first vacant entry on the free list. |
| 21 | +The value u32::MAX indicates indicates there is no next item and is represented by the `EOL` const. |
| 22 | + |
| 23 | +Here's an example `HandleMap`: |
| 24 | + |
| 25 | +``` |
| 26 | +---------------------------------------------------------------- |
| 27 | +| OCCUPIED | VACANT | OCCUPIED | |
| 28 | +| item: Foo | next: EOL | item: Bar | |
| 29 | +| generation: 100 | generation: 40 | generation: 30 | |
| 30 | +---------------------------------------------------------------- |
| 31 | +
|
| 32 | +HandleMap.next: 1 |
| 33 | +``` |
| 34 | + |
| 35 | +### Inserting entries |
| 36 | + |
| 37 | +To insert a new entry: |
| 38 | + - Remove the first entry in the free list, |
| 39 | + - Convert it to an `OCCUPIED` |
| 40 | + - Increment the generation counter |
| 41 | + |
| 42 | +For example inserting a `Baz` entry in the above `HandleMap` would result in: |
| 43 | + |
| 44 | +``` |
| 45 | +---------------------------------------------------------------- |
| 46 | +| OCCUPIED | OCCUPIED | OCCUPIED | |
| 47 | +| item: Foo | item: Baz | item: Bar | |
| 48 | +| generation: 100 | generation: 41 | generation: 30 | |
| 49 | +---------------------------------------------------------------- |
| 50 | +
|
| 51 | +HandleMap.next: EOL |
| 52 | +``` |
| 53 | + |
| 54 | +If there are no vacant entries, then we append an entry to the end of the list. |
| 55 | +For example, inserting a `Qux` entry in the above `HandleMap` would result in: |
| 56 | + |
| 57 | +``` |
| 58 | +------------------------------------------------------------------------------------- |
| 59 | +| OCCUPIED | OCCUPIED | OCCUPIED | OCCUPIED | |
| 60 | +| item: Foo | item: Baz | item: Bar | item: Qux | |
| 61 | +| generation: 100 | generation: 41 | generation: 30 | generation: 0 | |
| 62 | +------------------------------------------------------------------------------------- |
| 63 | +
|
| 64 | +HandleMap.next: EOL |
| 65 | +``` |
| 66 | + |
| 67 | +### Removing entries |
| 68 | + |
| 69 | +To remove an entry: |
| 70 | + - Convert it to `VACANT` |
| 71 | + - Add it to the head of the free list. |
| 72 | + |
| 73 | +For example, removing the `Foo` entry from the above handle map would result in: |
| 74 | + |
| 75 | +``` |
| 76 | +------------------------------------------------------------------------------------- |
| 77 | +| VACANT | OCCUPIED | OCCUPIED | OCCUPIED | |
| 78 | +| next: EOL | item: Baz | item: Bar | item: Qux | |
| 79 | +| generation: 100 | generation: 41 | generation: 30 | generation: 0 | |
| 80 | +------------------------------------------------------------------------------------- |
| 81 | +
|
| 82 | +HandleMap.next: 0 |
| 83 | +``` |
| 84 | + |
| 85 | +Removing the `Bar` entry after that would result in: |
| 86 | + |
| 87 | +``` |
| 88 | +------------------------------------------------------------------------------------- |
| 89 | +| VACANT | OCCUPIED | OCCUPIED | OCCUPIED | |
| 90 | +| next: EOL | item: Baz | next: 0 | item: Qux | |
| 91 | +| generation: 100 | generation: 41 | generation: 30 | generation: 0 | |
| 92 | +------------------------------------------------------------------------------------- |
| 93 | +
|
| 94 | +HandleMap.next: 2 |
| 95 | +``` |
| 96 | + |
| 97 | +### Getting entries |
| 98 | + |
| 99 | +When an entry is inserted, we return a `Handle`. |
| 100 | +This is a 64-bit integer, segmented as follows: |
| 101 | +- Bits 0-32: `Vec` index |
| 102 | +- Bit 32: foreign bit that's set for handles for foreign objects, but not Rust objects. |
| 103 | + This allows us to differentiate trait interface implementations. |
| 104 | +- Bits 33-40: map id -- a unique value that corresponds to the map that generated the handle |
| 105 | +- Bits 40-48: generation counter |
| 106 | +- Bits 48-64: unused |
| 107 | + |
| 108 | +When the foreign code passes the Rust code a handle, we use it to get the entry as follows: |
| 109 | + |
| 110 | +- Use the index to get the entry in the raw `Vec` |
| 111 | +- Check that the entry is `OCCUPIED`, the generation counter value matches, and the map_id matches. |
| 112 | +- Get the stored item and do something with it. Usually this means cloning the `Arc<>`. |
| 113 | + |
| 114 | +These checks can usually ensure that handles are only used with the `HandleMap` that generated them and that they aren't used after the entry is removed. |
| 115 | +However, this is limited by the bit-width of the handle segments: |
| 116 | + |
| 117 | +- Because the generation counter is 8-bits, we will fail to detect use-after-free bugs if an entry has been reused exactly 256 items or some multiple of 256. |
| 118 | +- Because the map id is only 7-bits, we may fail to detect handles being used with the wrong map if we generate over 128 `HandleMap` tables. |
| 119 | + This can only happen if there more than 100 user-defined types and less than 1% of the time in that case. |
| 120 | + |
| 121 | +### Handle map creation / management |
| 122 | + |
| 123 | +The Rust codegen creates a static `HandleMap` for each object type that needs to be sent across the FFI, for example: |
| 124 | + - `HandleMap<Arc<T>>` for each object type exposed by UniFFI. |
| 125 | + - `HandleMap<Arc<dyn Trait>>` for each trait interface exposed by UniFFI. |
| 126 | + - `HandleMap<Arc<dyn RustFutureFfi<FfiType>>>` for each FFI type. This is used to implement async Rust functions. |
| 127 | + - `HandleMap<oneshot::Sender<FfiType>>` for each FFI type. This will be used to implement async callback methods. |
| 128 | + |
| 129 | +The `HandleAlloc` trait manages access to the static `HandleMap` instances and provides the following methods: |
| 130 | + - `insert(value: Self) -> Handle` insert a new entry into the handle map |
| 131 | + - `remove(handle: Handle) -> Self` remove an entry from the handle map |
| 132 | + - `get(handle: Handle) -> Self` get a cloned object from the handle map without removing the entry. |
| 133 | + - `clone_handle(handle: Handle) -> Handle` get a cloned handle that refers to the same object. |
| 134 | + |
| 135 | +If the user defines a type `Foo` in their interface then: |
| 136 | + - `<Arc<Foo> as HandleAlloc>::insert` is called when lowering `Foo` to pass it across the FFI to the foreign side. |
| 137 | + - `<Arc<Foo> as HandleAlloc>::get` is called when lifting `Foo` after it's passed back across the FFI to Rust. |
| 138 | + - `<Arc<Foo> as HandleAlloc>::clone_handle` is called when the foreign side needs to clone the handle. |
| 139 | + See https://github.com/mozilla/uniffi-rs/issues/1797 for why this is needed. |
| 140 | + - `<Arc<Foo> as HandleAlloc>::remove` is called when the foreign side calls the `free` function for the object. |
| 141 | + |
| 142 | +Extra details: |
| 143 | + - The trait is actually `HandleAlloc<UT>`, where `UT` is the "UniFFI Tag" type. See `uniffi_core/src/ffi_converter_traits.rs` for details. |
| 144 | + - The last two `HandleAlloc` methods are only implemented for `T: Clone`, which is true for the `Arc<>` cases, but not `oneshot::Sender`. |
| 145 | + This is fine because we only use `insert`/`remove` for the `oneshot::Sender` case. |
| 146 | + |
| 147 | +### Concurrency |
| 148 | + |
| 149 | +`insert` and `remove` require serialization since there's no way to atomically update the free list. |
| 150 | +In general, `get` does not require any serialization since it will only read occupied entries, while `insert` and `remove` only access vacant entries. |
| 151 | +However, there are 2 edge cases where `get` does access the same entries as `insert`/`remove`: |
| 152 | + |
| 153 | + - If `insert` causes the Vec to grow this may cause the entire array to be moved, which will affect `get` |
| 154 | + - If the foreign code has a use-after-free bug, then `get` may access the same entry as an `insert`/`remove` operation. |
| 155 | + |
| 156 | +UniFFI uses the following system to handle this: |
| 157 | + |
| 158 | +- A standard `Mutex` is used to serialize `insert` and `remove`. |
| 159 | +- We use the `append_only_vec` crate, which avoids moving the array when the `Vec` grows. |
| 160 | +- Each entry has a 8-bit read-write spin-lock to avoid issues in the face of use-after-free bugs. |
| 161 | + This lock will only be contested if there's a use-after-free bug. |
| 162 | + |
| 163 | +### Concurrency: alternative option if we choose 2 from the ADR. This one is simpler, but slower. |
| 164 | + |
| 165 | +To allow concurrent access, a `RwLock` is used to protect the entire `HandleMap`. |
| 166 | +`insert` and `remove` acquire the write lock while accessing entries acquires the read lock. |
| 167 | + |
| 168 | +### Space usage |
| 169 | + |
| 170 | +The `HandleMap` adds an extra 64-bits of memory to each occupied item, which is the lower-limit on a 64-bit machine. |
| 171 | +This means that `HandleMap` tables that store normal `Arc` pointers add ~100% extra space overhead and ones that store wide-pointers add ~50% overhead. |
| 172 | +`HandleMap` tables don't have any way of reclaiming unused space after items are removed. |
| 173 | + |
| 174 | +This is can be a very large amount of memory, but in practice libraries only generate a relatively small amount of handles. |
0 commit comments