|
1 | | -# ecmascript_atomics |
2 | | -Rust crate providing unsafe implementations of the ECMAScript atomic memory model. |
| 1 | +# ECMAScript atomics |
| 2 | + |
| 3 | +This library provides unsafe (and potentially unsound) APIs for interacting |
| 4 | +with memory that adheres to the |
| 5 | +[ECMAScript memory model](https://tc39.es/ecma262/#sec-memory-model). The |
| 6 | +important difference between the Rust/C++ and ECMAScript memory models is that |
| 7 | +ECMAScript allows data races between atomic and non-atomic reads and writes, as |
| 8 | +well as between mismatched reads and writes (ie. different sizes, atomic or |
| 9 | +not). These sorts of data races are strictly undefined behaviour in Rust. |
| 10 | + |
| 11 | +Because it is undefined behaviour to perform such actions in Rust, the |
| 12 | +ECMAScript memory model interactions must happen such that from Rust's |
| 13 | +perspective the memory participating in data races is not memory at all, and |
| 14 | +the operations performing potentially racing reads and writes are outside of |
| 15 | +the Rust compiler's view. |
| 16 | + |
| 17 | +The first step to satisfying these requirements is to perform all the |
| 18 | +operations either behind an FFI layer, or using inline assembly. This library's |
| 19 | +choice is to use the latter option. The second step needed is to ensure that |
| 20 | +from Rust's perspective, the memory participating in data races cannot be |
| 21 | +considered as memory for the duration of the racy behaviour. This means |
| 22 | +laundering the owning pointer of this memory through an inline assembly block |
| 23 | +that is intended to signal to the Rust compiler that the memory may have been |
| 24 | +deallocated, and then ensuring that the new pointer received from the |
| 25 | +laundering is itself never used to perform memory reads or writes within Rust, |
| 26 | +thus ensuring that the compiler cannot reason from example that the memory is |
| 27 | +still allocated. |
| 28 | + |
| 29 | +This leads to a concept of the three stages of ECMAScript memory, separated by |
| 30 | +explicit fences injected by the `RacyMemory::enter` and `RacyMemory::exit` |
| 31 | +method calls. |
| 32 | + |
| 33 | +## First stage – initialisation |
| 34 | + |
| 35 | +This library does not offer ways to allocate memory. All memory allocation |
| 36 | +and initialisation must happen outside of this library (this happens to also |
| 37 | +correspond with the ECMAScript memory model ordering of `INIT`). The likely |
| 38 | +scenario is that such memory is initialised in Rust's view (if using eg. `Box` |
| 39 | +or `alloc`) though it is possible to avoid that using lower level APIs (eg. |
| 40 | +mmap can be used to allocate memory zeroed which initialises the memory without |
| 41 | +Rust seeing it as memory), and that means that the memory is Rust memory and |
| 42 | +must adhere to Rust's memory model without data races. |
| 43 | + |
| 44 | +At this point the memory is free to interact with according to normal Rust |
| 45 | +rules. To move into the ECMAScript memory model, the `RacyMemory::enter` API |
| 46 | +or its siblings must be used. |
| 47 | + |
| 48 | +## Second stage – ECMAScript memory model |
| 49 | + |
| 50 | +Once the `enter` is used, the Rust memory is deallocated (in an abstract sense) |
| 51 | +and a new ECMAScript memory slab is allocated in its place, though from the |
| 52 | +code point of view it is an entirely new pointer with potentially different |
| 53 | +address and provenance. As the Rust memory is now deallocated, using pointers |
| 54 | +to read or write into the previous memory area is equivalent to use-after-free |
| 55 | +and is thus undefined behaviour. |
| 56 | + |
| 57 | +The newly gained ECMAScript memory in the form of `RacyMemory` can be accessed |
| 58 | +using a slice-like data structure called `RacySlice` and its APIs. These APIs |
| 59 | +adhere to the ECMAScript memory model, meaning that reads and writes may be |
| 60 | +either unordered (or unsynchronising by another name) or sequentially |
| 61 | +consistent, and that both atomic and unordered reads and writes may race with |
| 62 | +other reads and writes, including ones with mismatching sizes or alignments. |
| 63 | + |
| 64 | +When the ECMAScript memory is no longer needed needed, it can be deallocated |
| 65 | +using the `exit` API. This API must not race with any data reads or writes in |
| 66 | +the ECMAScript memory (ie. must be synchronised), and produces a new Rust |
| 67 | +memory allocation and returns its pointer and length (in case the ECMAScript |
| 68 | +memory was slice-like). This memory holds the data that the ECMAScript memory |
| 69 | +held at the time of its deallocation and can be used normally. |
| 70 | + |
| 71 | +## Third stage – deallocation |
| 72 | + |
| 73 | +After the `exit` method call, it is up to the caller to deallocate the Rust |
| 74 | +memory by using the pointer and length that the `exit` call returns. |
| 75 | + |
| 76 | +# Wait, so you're allocating and deallocating memory at the fences?! That's insanely slow! |
| 77 | + |
| 78 | +Well, no... The fences are inline assembly blocks that take a pointer and |
| 79 | +return a pointer, but inside the assembly block is only a comment. The |
| 80 | +deallocation and reallocation at the fence is an explanation to the abstract |
| 81 | +Rust machine. In truth, no explicit deallocation happens but due to how the |
| 82 | +assembly blocks are used (pointer in, pointer out, no |
| 83 | +[`pure`](https://doc.rust-lang.org/reference/inline-assembly.html#r-asm.options.supported-options.pure) |
| 84 | +option), the compiler has to assume that the allocation may have been |
| 85 | +deallocated. |
| 86 | + |
| 87 | +The internals of the library then take care to never perform any reads or |
| 88 | +writes through the pointers, nor create any references to non-ZST data from |
| 89 | +them. This should keep the compiler from realising that the pointed-to memory |
| 90 | +is still actually allocated and useable, as it should since the compiler must |
| 91 | +not assume it can eg. inject suprious reads or writes on these pointers. |
| 92 | + |
| 93 | +# Should you use this crate |
| 94 | + |
| 95 | +No, probably not. Unless you're writing a JavaScript, WebAssembly, or Java |
| 96 | +virtual machine and you're not willing to emulate the weaker memory model, you |
| 97 | +should run away from here and fast. If you are writing a virtual machine for |
| 98 | +one of the mentioned languages or some other language with the same memory |
| 99 | +model, then first consider the following alternatives: |
| 100 | + |
| 101 | +1. If you're reading this in the future and Rust allows mixed-size atomics, |
| 102 | + then strongly consider using normal Rust atomics with Unordered ordering |
| 103 | + replaced with Relaxed. This gives you the same memory model except for on |
| 104 | + some obscure hardware platforms (if you thought DEC, you're right), and |
| 105 | + perhaps some extra locks on ARM platforms sometimes. That would be my |
| 106 | + choice. |
| 107 | +2. Use `AtomicUsize` as your atomic storage and simulate mixed-size atomics by |
| 108 | + performing CAS loops on these usize chunks, and replace Unordered ordering |
| 109 | + with Relaxed. This may be the smartest option compared, when the other |
| 110 | + option is to put your trust in some random library filled with inline |
| 111 | + assembly copied off the Internet. |
| 112 | +3. Use `AtomicU8` as your atomic storage and hope that either the compiler |
| 113 | + takes pity on you and optimises the atomic byte operations into larger |
| 114 | + atomic chunk operations (I am not aware of such optimisations existing), or |
| 115 | + hope your users don't notice that their atomics are not actually atomic and |
| 116 | + can always tear. This is probably a bad idea. |
| 117 | + |
| 118 | +If none of those sounds appetising, then this library is what you want. I'll |
| 119 | +even give you a guarantee: ... |
| 120 | + |
| 121 | +*tail lights flicker in the night as the author speeds off* |
0 commit comments