Skip to content

Commit d7c4c62

Browse files
committed
remove unsound API, add README
1 parent b347ccb commit d7c4c62

File tree

4 files changed

+621
-1126
lines changed

4 files changed

+621
-1126
lines changed

README.md

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,121 @@
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

Comments
 (0)