Sign in Join
Wiki copytemporary

WebAssembly/Threads: what changed

Revision 1 to 2. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11# Thread support
22
33This page contains some collected informations on the features needed for thread support in WebAssembly (in the browser).
44
55Thread support consists of 4 parts:
66
77- Atomic instructions.
88- Shared memory and passive segments.
99- Thread Local Storage (threadvars)
1010- Actually starting a thread.
1111
1212## Atomic instructions
1313
1414[The proposed specs](https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md) 
1515
1616When the Free Pascal RTL is compiled with -CTwasmthreads, the following RTL functions will use the new atomic instructions and thus should be thread safe in a multithreaded environment:
1717
18-[`InterlockedDecrement`](https://www.freepascal.org/docs-html/current/rtl/system/interlockeddecrement.html)\
19-[`InterlockedIncrement`](https://www.freepascal.org/docs-html/current/rtl/system/interlockedincrement.html)\
20-[`InterlockedExchange`](https://www.freepascal.org/docs-html/current/rtl/system/interlockedexchange.html)\
21-[`InterlockedCompareExchange`](https://www.freepascal.org/docs-html/current/rtl/system/interlockedcompareexchange.html)\
22-[`InterlockedExchangeAdd`](https://www.freepascal.org/docs-html/current/rtl/system/interlockedexchangeadd.html)
18+```text
19+InterlockedDecrement
20+InterlockedIncrement
21+InterlockedExchange
22+InterlockedCompareExchange
23+InterlockedExchangeAdd
24+```
2325
2426Note that these require proper alignment (4 bytes) of the target, otherwise they trap (i.e. terminate the program with a stack trace).
2527
2628In addition to that, there are many more atomic functions available in the WebAssembly unit:
2729
28-`const`\
29-`  { Special values for the TimeoutNanoseconds parameter of AtomicWait }`\
30-`  awtInfiniteTimeout = -1;`\
31-`  { AtomicWait result values }`\
32-`  awrOk = 0;       { woken by another agent in the cluster }`\
33-`  awrNotEqual = 1; { the loaded value did not match the expected value }`\
34-`  awrTimedOut = 2; { not woken before timeout expired }`\
30+``` pascal
31+const
32+ { Special values for the TimeoutNanoseconds parameter of AtomicWait }
33+ awtInfiniteTimeout = -1;
34+ { AtomicWait result values }
35+ awrOk = 0; { woken by another agent in the cluster }
36+ awrNotEqual = 1; { the loaded value did not match the expected value }
37+ awrTimedOut = 2; { not woken before timeout expired }
3538
36-`procedure AtomicFence; inline;`\
39+procedure AtomicFence; inline;
3740
38-`function AtomicLoad(constref Mem: Int8): Int8; inline;`\
39-`function AtomicLoad(constref Mem: UInt8): UInt8; inline;`\
40-`function AtomicLoad(constref Mem: Int16): Int16; inline;`\
41-`function AtomicLoad(constref Mem: UInt16): UInt16; inline;`\
42-`function AtomicLoad(constref Mem: Int32): Int32; inline;`\
43-`function AtomicLoad(constref Mem: UInt32): UInt32; inline;`\
44-`function AtomicLoad(constref Mem: Int64): Int64; inline;`\
45-`function AtomicLoad(constref Mem: UInt64): UInt64; inline;`\
41+function AtomicLoad(constref Mem: Int8): Int8; inline;
42+function AtomicLoad(constref Mem: UInt8): UInt8; inline;
43+function AtomicLoad(constref Mem: Int16): Int16; inline;
44+function AtomicLoad(constref Mem: UInt16): UInt16; inline;
45+function AtomicLoad(constref Mem: Int32): Int32; inline;
46+function AtomicLoad(constref Mem: UInt32): UInt32; inline;
47+function AtomicLoad(constref Mem: Int64): Int64; inline;
48+function AtomicLoad(constref Mem: UInt64): UInt64; inline;
4649
47-`procedure AtomicStore(out Mem: Int8; Data: Int8); inline;`\
48-`procedure AtomicStore(out Mem: UInt8; Data: UInt8); inline;`\
49-`procedure AtomicStore(out Mem: Int16; Data: Int16); inline;`\
50-`procedure AtomicStore(out Mem: UInt16; Data: UInt16); inline;`\
51-`procedure AtomicStore(out Mem: Int32; Data: Int32); inline;`\
52-`procedure AtomicStore(out Mem: UInt32; Data: UInt32); inline;`\
53-`procedure AtomicStore(out Mem: Int64; Data: Int64); inline;`\
54-`procedure AtomicStore(out Mem: UInt64; Data: UInt64); inline;`\
50+procedure AtomicStore(out Mem: Int8; Data: Int8); inline;
51+procedure AtomicStore(out Mem: UInt8; Data: UInt8); inline;
52+procedure AtomicStore(out Mem: Int16; Data: Int16); inline;
53+procedure AtomicStore(out Mem: UInt16; Data: UInt16); inline;
54+procedure AtomicStore(out Mem: Int32; Data: Int32); inline;
55+procedure AtomicStore(out Mem: UInt32; Data: UInt32); inline;
56+procedure AtomicStore(out Mem: Int64; Data: Int64); inline;
57+procedure AtomicStore(out Mem: UInt64; Data: UInt64); inline;
5558
56-`function AtomicAdd(var Mem: Int8; Data: Int8): Int8; inline;`\
57-`function AtomicAdd(var Mem: UInt8; Data: UInt8): UInt8; inline;`\
58-`function AtomicAdd(var Mem: Int16; Data: Int16): Int16; inline;`\
59-`function AtomicAdd(var Mem: UInt16; Data: UInt16): UInt16; inline;`\
60-`function AtomicAdd(var Mem: Int32; Data: Int32): Int32; inline;`\
61-`function AtomicAdd(var Mem: UInt32; Data: UInt32): UInt32; inline;`\
62-`function AtomicAdd(var Mem: Int64; Data: Int64): Int64; inline;`\
63-`function AtomicAdd(var Mem: UInt64; Data: UInt64): UInt64; inline;`\
59+function AtomicAdd(var Mem: Int8; Data: Int8): Int8; inline;
60+function AtomicAdd(var Mem: UInt8; Data: UInt8): UInt8; inline;
61+function AtomicAdd(var Mem: Int16; Data: Int16): Int16; inline;
62+function AtomicAdd(var Mem: UInt16; Data: UInt16): UInt16; inline;
63+function AtomicAdd(var Mem: Int32; Data: Int32): Int32; inline;
64+function AtomicAdd(var Mem: UInt32; Data: UInt32): UInt32; inline;
65+function AtomicAdd(var Mem: Int64; Data: Int64): Int64; inline;
66+function AtomicAdd(var Mem: UInt64; Data: UInt64): UInt64; inline;
6467
65-`function AtomicSub(var Mem: Int8; Data: Int8): Int8; inline;`\
66-`function AtomicSub(var Mem: UInt8; Data: UInt8): UInt8; inline;`\
67-`function AtomicSub(var Mem: Int16; Data: Int16): Int16; inline;`\
68-`function AtomicSub(var Mem: UInt16; Data: UInt16): UInt16; inline;`\
69-`function AtomicSub(var Mem: Int32; Data: Int32): Int32; inline;`\
70-`function AtomicSub(var Mem: UInt32; Data: UInt32): UInt32; inline;`\
71-`function AtomicSub(var Mem: Int64; Data: Int64): Int64; inline;`\
72-`function AtomicSub(var Mem: UInt64; Data: UInt64): UInt64; inline;`\
68+function AtomicSub(var Mem: Int8; Data: Int8): Int8; inline;
69+function AtomicSub(var Mem: UInt8; Data: UInt8): UInt8; inline;
70+function AtomicSub(var Mem: Int16; Data: Int16): Int16; inline;
71+function AtomicSub(var Mem: UInt16; Data: UInt16): UInt16; inline;
72+function AtomicSub(var Mem: Int32; Data: Int32): Int32; inline;
73+function AtomicSub(var Mem: UInt32; Data: UInt32): UInt32; inline;
74+function AtomicSub(var Mem: Int64; Data: Int64): Int64; inline;
75+function AtomicSub(var Mem: UInt64; Data: UInt64): UInt64; inline;
7376
74-`function AtomicAnd(var Mem: Int8; Data: Int8): Int8; inline;`\
75-`function AtomicAnd(var Mem: UInt8; Data: UInt8): UInt8; inline;`\
76-`function AtomicAnd(var Mem: Int16; Data: Int16): Int16; inline;`\
77-`function AtomicAnd(var Mem: UInt16; Data: UInt16): UInt16; inline;`\
78-`function AtomicAnd(var Mem: Int32; Data: Int32): Int32; inline;`\
79-`function AtomicAnd(var Mem: UInt32; Data: UInt32): UInt32; inline;`\
80-`function AtomicAnd(var Mem: Int64; Data: Int64): Int64; inline;`\
81-`function AtomicAnd(var Mem: UInt64; Data: UInt64): UInt64; inline;`\
77+function AtomicAnd(var Mem: Int8; Data: Int8): Int8; inline;
78+function AtomicAnd(var Mem: UInt8; Data: UInt8): UInt8; inline;
79+function AtomicAnd(var Mem: Int16; Data: Int16): Int16; inline;
80+function AtomicAnd(var Mem: UInt16; Data: UInt16): UInt16; inline;
81+function AtomicAnd(var Mem: Int32; Data: Int32): Int32; inline;
82+function AtomicAnd(var Mem: UInt32; Data: UInt32): UInt32; inline;
83+function AtomicAnd(var Mem: Int64; Data: Int64): Int64; inline;
84+function AtomicAnd(var Mem: UInt64; Data: UInt64): UInt64; inline;
8285
83-`function AtomicOr(var Mem: Int8; Data: Int8): Int8; inline;`\
84-`function AtomicOr(var Mem: UInt8; Data: UInt8): UInt8; inline;`\
85-`function AtomicOr(var Mem: Int16; Data: Int16): Int16; inline;`\
86-`function AtomicOr(var Mem: UInt16; Data: UInt16): UInt16; inline;`\
87-`function AtomicOr(var Mem: Int32; Data: Int32): Int32; inline;`\
88-`function AtomicOr(var Mem: UInt32; Data: UInt32): UInt32; inline;`\
89-`function AtomicOr(var Mem: Int64; Data: Int64): Int64; inline;`\
90-`function AtomicOr(var Mem: UInt64; Data: UInt64): UInt64; inline;`\
86+function AtomicOr(var Mem: Int8; Data: Int8): Int8; inline;
87+function AtomicOr(var Mem: UInt8; Data: UInt8): UInt8; inline;
88+function AtomicOr(var Mem: Int16; Data: Int16): Int16; inline;
89+function AtomicOr(var Mem: UInt16; Data: UInt16): UInt16; inline;
90+function AtomicOr(var Mem: Int32; Data: Int32): Int32; inline;
91+function AtomicOr(var Mem: UInt32; Data: UInt32): UInt32; inline;
92+function AtomicOr(var Mem: Int64; Data: Int64): Int64; inline;
93+function AtomicOr(var Mem: UInt64; Data: UInt64): UInt64; inline;
9194
92-`function AtomicXor(var Mem: Int8; Data: Int8): Int8; inline;`\
93-`function AtomicXor(var Mem: UInt8; Data: UInt8): UInt8; inline;`\
94-`function AtomicXor(var Mem: Int16; Data: Int16): Int16; inline;`\
95-`function AtomicXor(var Mem: UInt16; Data: UInt16): UInt16; inline;`\
96-`function AtomicXor(var Mem: Int32; Data: Int32): Int32; inline;`\
97-`function AtomicXor(var Mem: UInt32; Data: UInt32): UInt32; inline;`\
98-`function AtomicXor(var Mem: Int64; Data: Int64): Int64; inline;`\
99-`function AtomicXor(var Mem: UInt64; Data: UInt64): UInt64; inline;`\
95+function AtomicXor(var Mem: Int8; Data: Int8): Int8; inline;
96+function AtomicXor(var Mem: UInt8; Data: UInt8): UInt8; inline;
97+function AtomicXor(var Mem: Int16; Data: Int16): Int16; inline;
98+function AtomicXor(var Mem: UInt16; Data: UInt16): UInt16; inline;
99+function AtomicXor(var Mem: Int32; Data: Int32): Int32; inline;
100+function AtomicXor(var Mem: UInt32; Data: UInt32): UInt32; inline;
101+function AtomicXor(var Mem: Int64; Data: Int64): Int64; inline;
102+function AtomicXor(var Mem: UInt64; Data: UInt64): UInt64; inline;
100103
101-`function AtomicExchange(var Mem: Int8; Data: Int8): Int8; inline;`\
102-`function AtomicExchange(var Mem: UInt8; Data: UInt8): UInt8; inline;`\
103-`function AtomicExchange(var Mem: Int16; Data: Int16): Int16; inline;`\
104-`function AtomicExchange(var Mem: UInt16; Data: UInt16): UInt16; inline;`\
105-`function AtomicExchange(var Mem: Int32; Data: Int32): Int32; inline;`\
106-`function AtomicExchange(var Mem: UInt32; Data: UInt32): UInt32; inline;`\
107-`function AtomicExchange(var Mem: Int64; Data: Int64): Int64; inline;`\
108-`function AtomicExchange(var Mem: UInt64; Data: UInt64): UInt64; inline;`\
104+function AtomicExchange(var Mem: Int8; Data: Int8): Int8; inline;
105+function AtomicExchange(var Mem: UInt8; Data: UInt8): UInt8; inline;
106+function AtomicExchange(var Mem: Int16; Data: Int16): Int16; inline;
107+function AtomicExchange(var Mem: UInt16; Data: UInt16): UInt16; inline;
108+function AtomicExchange(var Mem: Int32; Data: Int32): Int32; inline;
109+function AtomicExchange(var Mem: UInt32; Data: UInt32): UInt32; inline;
110+function AtomicExchange(var Mem: Int64; Data: Int64): Int64; inline;
111+function AtomicExchange(var Mem: UInt64; Data: UInt64): UInt64; inline;
109112
110-`function AtomicCompareExchange(var Mem: Int8; Compare, Data: Int8): Int8; inline;`\
111-`function AtomicCompareExchange(var Mem: UInt8; Compare, Data: UInt8): UInt8; inline;`\
112-`function AtomicCompareExchange(var Mem: Int16; Compare, Data: Int16): Int16; inline;`\
113-`function AtomicCompareExchange(var Mem: UInt16; Compare, Data: UInt16): UInt16; inline;`\
114-`function AtomicCompareExchange(var Mem: Int32; Compare, Data: Int32): Int32; inline;`\
115-`function AtomicCompareExchange(var Mem: UInt32; Compare, Data: UInt32): UInt32; inline;`\
116-`function AtomicCompareExchange(var Mem: Int64; Compare, Data: Int64): Int64; inline;`\
117-`function AtomicCompareExchange(var Mem: UInt64; Compare, Data: UInt64): UInt64; inline;`\
113+function AtomicCompareExchange(var Mem: Int8; Compare, Data: Int8): Int8; inline;
114+function AtomicCompareExchange(var Mem: UInt8; Compare, Data: UInt8): UInt8; inline;
115+function AtomicCompareExchange(var Mem: Int16; Compare, Data: Int16): Int16; inline;
116+function AtomicCompareExchange(var Mem: UInt16; Compare, Data: UInt16): UInt16; inline;
117+function AtomicCompareExchange(var Mem: Int32; Compare, Data: Int32): Int32; inline;
118+function AtomicCompareExchange(var Mem: UInt32; Compare, Data: UInt32): UInt32; inline;
119+function AtomicCompareExchange(var Mem: Int64; Compare, Data: Int64): Int64; inline;
120+function AtomicCompareExchange(var Mem: UInt64; Compare, Data: UInt64): UInt64; inline;
118121
119-`function AtomicWait(constref Mem: Int32; Compare: Int32; TimeoutNanoseconds: Int64): Int32; inline;`\
120-`function AtomicWait(constref Mem: UInt32; Compare: UInt32; TimeoutNanoseconds: Int64): Int32; inline;`\
121-`function AtomicWait(constref Mem: Int64; Compare: Int64; TimeoutNanoseconds: Int64): Int32; inline;`\
122-`function AtomicWait(constref Mem: UInt64; Compare: UInt64; TimeoutNanoseconds: Int64): Int32; inline;`\
122+function AtomicWait(constref Mem: Int32; Compare: Int32; TimeoutNanoseconds: Int64): Int32; inline;
123+function AtomicWait(constref Mem: UInt32; Compare: UInt32; TimeoutNanoseconds: Int64): Int32; inline;
124+function AtomicWait(constref Mem: Int64; Compare: Int64; TimeoutNanoseconds: Int64): Int32; inline;
125+function AtomicWait(constref Mem: UInt64; Compare: UInt64; TimeoutNanoseconds: Int64): Int32; inline;
123126
124-`function AtomicNotify(constref Mem: Int32; Count: UInt32): UInt32; inline;`\
125-`function AtomicNotify(constref Mem: UInt32; Count: UInt32): UInt32; inline;`\
126-`function AtomicNotify(constref Mem: Int64; Count: UInt32): UInt32; inline;`\
127-`function AtomicNotify(constref Mem: UInt64; Count: UInt32): UInt32; inline;`
127+function AtomicNotify(constref Mem: Int32; Count: UInt32): UInt32; inline;
128+function AtomicNotify(constref Mem: UInt32; Count: UInt32): UInt32; inline;
129+function AtomicNotify(constref Mem: Int64; Count: UInt32): UInt32; inline;
130+function AtomicNotify(constref Mem: UInt64; Count: UInt32): UInt32; inline;
131+```
128132
129133## Shared memory and passive segments
130134
131135First, the memory needs to be declared shared.
132136
133137Secondly, the data segments need to be declared passive segments and extra startup code should be generated by the compiler to initialize them only once. Without this, when the module is instantiated on a new WebWorker (in order to start a new thread), this will cause memory to be initialized again to the initial state, which is not what we want when starting a thread.
134138
135139Some info: [Shared Memory and Passive Segments](https://github.com/WebAssembly/tool-conventions/blob/main/Linking.md#shared-memory-and-passive-segments)
136140
137141Turns out, this is all done by the LLVM linker (including the initialization startup code), when you pass the appropriate command line options. The compiler now passes these options to the linker, when a program is compiled with -CTbfexceptions. As a side effect, such programs no longer work with "wasmtime run --enable-features threads", but that's because wasmtime's threads support is incomplete.
138142
139143## Thread Local Storage (threadvars)
140144
141145Special consideration is needed to support threadvars.
142146
143147More info here: [Thread Local Storage](https://github.com/WebAssembly/tool-conventions/blob/main/Linking.md#thread-local-storage)
144148
145149Code generation for threadvar access is now implemented in FPC. It follows the ABI convention for TLS from Emscripten. However, it causes the LLVM 14 linker to crash. The LLVM 15 (release candidate) linker from Emscripten seems to work. The following intrinsics exist in the system unit, they should be used for things like setting up the TLS of the new thread, or for allocating memory for the TLS in the calling thread, before passing it to the JavaScript helper:
146150
147-`function fpc_wasm32_tls_size: SizeUInt; - returns the TLS size in bytes for the entire program`\
148-`function fpc_wasm32_tls_align: SizeUInt; - returns the alignment requirements for the TLS block for the program`\
149-`function fpc_wasm32_tls_base: Pointer; - the start of the TLS block for the current thread. Only becomes valid after calling __wasm_init_tls (which is a special function, generated by the linker).`\
150-`procedure fpc_wasm32_init_tls(memory: Pointer);external name '__wasm_init_tls';`
151+``` pascal
152+function fpc_wasm32_tls_size: SizeUInt; - returns the TLS size in bytes for the entire program
153+function fpc_wasm32_tls_align: SizeUInt; - returns the alignment requirements for the TLS block for the program
154+function fpc_wasm32_tls_base: Pointer; - the start of the TLS block for the current thread. Only becomes valid after calling __wasm_init_tls (which is a special function, generated by the linker).
155+procedure fpc_wasm32_init_tls(memory: Pointer);external name '__wasm_init_tls';
156+```
151157
152158TODO: initialize threadvars for the main thread, during system unit initialization, before the first ever threadvar is accessed.
153159
154160## Actually starting a thread
155161
156162Webassembly relies on the hosting environment to actually start threads.
157163
158164Some extra info:
159165
160166- [something like specs](https://web.dev/webassembly-threads/)
161167- [A demo](https://rreverser.com/wasm-bindgen-rayon-demo/)
162168- [related](https://wicg.github.io/file-system-access/)
163169- [WASI Native Threads API proposal](https://github.com/WebAssembly/wasi-native-threads)
164170
165171Unfortunately, the WASI Native Threads API proposal is very incomplete. Emscripten implements threads using a different API/ABI, but it's quite messy and poorly documented.
166172
167173Starting a thread requires the following steps:
168174
169175- **WebAssembly**: allocating a block of memory for the stack and TLS (threadvar) block for the new thread. This needs to be done in a thread safe manner (can we use the heap?). TODO: How do we determine the stack size for the new thread? Do we use the same main stack size as specified by the {\$M *stacksize*} directive?
170176- **WebAssembly**: calling an external (JavaScript) function and passing at least the following data, that needs to be passed to the new thread by JavaScript code:
171177
172-` - the start address of the new thread procedure`\
173-` - the args that need to be passed to the new thread procedure`\
174-` - the stack and TLS address`\
175-` - TODO: how/where do we determine the thread ID?`
178+``` pascal
179+ - the start address of the new thread procedure
180+ - the args that need to be passed to the new thread procedure
181+ - the stack and TLS address
182+ - TODO: how/where do we determine the thread ID?
183+```
176184
177185What does Emscripten do?
178186
179-`int __pthread_create_js(struct pthread *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);`
187+``` pascal
188+int __pthread_create_js(struct pthread *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
189+```
180190
181191Basically, it exposes the pthread structure to the JavaScript code, which expects certain things (stack start, stack limit, TLS start, thread ID, etc.) to be at certain fixed offsets.
182192
183193What does the WASI Native Threads API proposal do?
184194
185-`status thread_spawn(thread_id* thread_id, const thread_attributes* attrs, thread_start_func* function, thread_args* arg);`
195+``` pascal
196+status thread_spawn(thread_id* thread_id, const thread_attributes* attrs, thread_start_func* function, thread_args* arg);
197+```
186198
187199It doesn't (yet) define what thread_id or thread_attributes are.
188200
189201- **JavaScript**: TBD...delegate the new thread to a Web Worker, pass the arguments, etc. Decisions: Should the main thread be started in the main JavaScript GUI thread or inside a worker? Emscripten supports both (via PROXY_TO_PTHREAD). Should we support both as well? Other caveats: [1](https://emscripten.org/docs/porting/pthreads.html#blocking-on-the-main-browser-thread) and [2](https://emscripten.org/docs/porting/pthreads.html#special-considerations)
190202- **WebAssembly**: a function that sets up the new thread:
191203
192-` - setting up the stack pointer in linear memory (might need inline asm, an external wasm module or some compiler magic helper code generation, because no local variables can be used, before this is set up)`\
193-` - initialize global variables that hold the TLS (threadvar) block (calling __wasm_init_tls). Threadvars should not be used before this point.`\
194-` - maybe initialize some threadvars, that contain information about the current thread (is it the main thread, is it run in the main browser thread or in a worker, can it use atomic wait, or should it busy-wait instead?)`\
195-` - call the actual thread function and pass its parameters`
204+``` pascal
205+ - setting up the stack pointer in linear memory (might need inline asm, an external wasm module or some compiler magic helper code generation, because no local variables can be used, before this is set up)
206+ - initialize global variables that hold the TLS (threadvar) block (calling __wasm_init_tls). Threadvars should not be used before this point.
207+ - maybe initialize some threadvars, that contain information about the current thread (is it the main thread, is it run in the main browser thread or in a worker, can it use atomic wait, or should it busy-wait instead?)
208+ - call the actual thread function and pass its parameters
209+```
196210
197211What does Emscripten do?
198212
199-`void _emscripten_thread_init(pthread_t ptr, int is_main, int is_runtime, int can_block, int start_profiling);`
213+```text
214+void _emscripten_thread_init(pthread_t ptr, int is_main, int is_runtime, int can_block, int start_profiling);
215+```
200216
201217What does the WASI Native Threads API proposal do?
202218
203-`- It only mentions a function, called `*`_start_thread`*`, but doesn't specify its parameters.`
219+``` pascal
220+- It only mentions a function, called _start_thread, but doesn't specify its parameters.
221+```
204222
205223---
206224*From the Free Pascal wiki, page "WebAssembly/Threads", as it stood on 2022-07-15 after a change by Nickysn, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*
207225Category: WebAssembly

Back to history