Skip to content

Commit a0a3f24

Browse files
authored
Move emscripten_atomic_wait_async to atomic.h and add core testing (#20404)
This API was orininally added for wasm workers but is not wasm worker specific. Followup to #20381.
1 parent 6ba6be6 commit a0a3f24

22 files changed

+397
-302
lines changed

src/library_atomic.js

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* @license
3+
* Copyright 2023 The Emscripten Authors
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
7+
assert(SHARED_MEMORY);
8+
9+
addToLibrary({
10+
// Chrome 87 (and hence Edge 87) shipped Atomics.waitAsync:
11+
// https://www.chromestatus.com/feature/6243382101803008
12+
// However its implementation is faulty:
13+
// https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
14+
// Firefox Nightly 86.0a1 (2021-01-15) does not yet have it:
15+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1467846
16+
// And at the time of writing, no other browser has it either.
17+
#if MIN_EDGE_VERSION < 91 || MIN_CHROME_VERSION < 91 || MIN_SAFARI_VERSION != TARGET_NOT_SUPPORTED || MIN_FIREFOX_VERSION != TARGET_NOT_SUPPORTED || ENVIRONMENT_MAY_BE_NODE
18+
// Partially polyfill Atomics.waitAsync() if not available in the browser.
19+
// Also polyfill for old Chrome-based browsers, where Atomics.waitAsync is
20+
// broken until Chrome 91, see:
21+
// https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
22+
// https://github.com/tc39/proposal-atomics-wait-async/blob/master/PROPOSAL.md
23+
// This polyfill performs polling with setTimeout() to observe a change in the
24+
// target memory location.
25+
$polyfillWaitAsync__postset: `if (!Atomics.waitAsync || (typeof navigator !== 'undefined' && navigator.userAgent && jstoi_q((navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./)||[])[2]) < 91)) {
26+
let __Atomics_waitAsyncAddresses = [/*[i32a, index, value, maxWaitMilliseconds, promiseResolve]*/];
27+
function __Atomics_pollWaitAsyncAddresses() {
28+
let now = performance.now();
29+
let l = __Atomics_waitAsyncAddresses.length;
30+
for (let i = 0; i < l; ++i) {
31+
let a = __Atomics_waitAsyncAddresses[i];
32+
let expired = (now > a[3]);
33+
let awoken = (Atomics.load(a[0], a[1]) != a[2]);
34+
if (expired || awoken) {
35+
__Atomics_waitAsyncAddresses[i--] = __Atomics_waitAsyncAddresses[--l];
36+
__Atomics_waitAsyncAddresses.length = l;
37+
a[4](awoken ? 'ok': 'timed-out');
38+
}
39+
}
40+
if (l) {
41+
// If we still have addresses to wait, loop the timeout handler to continue polling.
42+
setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
43+
}
44+
}
45+
#if ASSERTIONS && WASM_WORKERS
46+
if (!ENVIRONMENT_IS_WASM_WORKER) err('Current environment does not support Atomics.waitAsync(): polyfilling it, but this is going to be suboptimal.');
47+
#endif
48+
/**
49+
* @param {number=} maxWaitMilliseconds
50+
*/
51+
Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => {
52+
let val = Atomics.load(i32a, index);
53+
if (val != value) return { async: false, value: 'not-equal' };
54+
if (maxWaitMilliseconds <= 0) return { async: false, value: 'timed-out' };
55+
maxWaitMilliseconds = performance.now() + (maxWaitMilliseconds || Infinity);
56+
let promiseResolve;
57+
let promise = new Promise((resolve) => { promiseResolve = resolve; });
58+
if (!__Atomics_waitAsyncAddresses[0]) setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
59+
__Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, promiseResolve]);
60+
return { async: true, value: promise };
61+
};
62+
}`,
63+
$polyfillWaitAsync__deps: ['$jstoi_q'],
64+
#endif
65+
66+
$polyfillWaitAsync__internal: true,
67+
$polyfillWaitAsync: () => {
68+
// nop, used for its postset to ensure `Atomics.waitAsync()` polyfill is
69+
// included exactly once and only included when needed.
70+
// Any function using Atomics.waitAsync should depend on this.
71+
},
72+
73+
$atomicWaitStates__internal: true,
74+
$atomicWaitStates: ['ok', 'not-equal', 'timed-out'],
75+
$liveAtomicWaitAsyncs: {},
76+
$liveAtomicWaitAsyncs__internal: true,
77+
$liveAtomicWaitAsyncCounter: 0,
78+
$liveAtomicWaitAsyncCounter__internal: true,
79+
80+
emscripten_atomic_wait_async__deps: ['$atomicWaitStates', '$liveAtomicWaitAsyncs', '$liveAtomicWaitAsyncCounter', '$polyfillWaitAsync', '$callUserCallback'],
81+
emscripten_atomic_wait_async: (addr, val, asyncWaitFinished, userData, maxWaitMilliseconds) => {
82+
let wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds);
83+
if (!wait.async) return atomicWaitStates.indexOf(wait.value);
84+
// Increment waitAsync generation counter, account for wraparound in case
85+
// application does huge amounts of waitAsyncs per second (not sure if
86+
// possible?)
87+
// Valid counterrange: 0...2^31-1
88+
let counter = liveAtomicWaitAsyncCounter;
89+
liveAtomicWaitAsyncCounter = Math.max(0, (liveAtomicWaitAsyncCounter+1)|0);
90+
liveAtomicWaitAsyncs[counter] = addr;
91+
{{{ runtimeKeepalivePush() }}}
92+
wait.value.then((value) => {
93+
if (liveAtomicWaitAsyncs[counter]) {
94+
{{{ runtimeKeepalivePop() }}}
95+
delete liveAtomicWaitAsyncs[counter];
96+
callUserCallback(() => {{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(addr, val, atomicWaitStates.indexOf(value), userData));
97+
}
98+
});
99+
return -counter;
100+
},
101+
102+
emscripten_atomic_cancel_wait_async__deps: ['$liveAtomicWaitAsyncs'],
103+
emscripten_atomic_cancel_wait_async: (waitToken) => {
104+
#if ASSERTIONS
105+
if (waitToken == {{{ cDefs.ATOMICS_WAIT_NOT_EQUAL }}}) {
106+
warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_NOT_EQUAL (1) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()');
107+
} else if (waitToken == {{{ cDefs.ATOMICS_WAIT_TIMED_OUT }}}) {
108+
warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_TIMED_OUT (2) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()');
109+
} else if (waitToken > 0) {
110+
warnOnce(`Attempted to call emscripten_atomic_cancel_wait_async() with an invalid wait token value ${waitToken}`);
111+
}
112+
#endif
113+
var address = liveAtomicWaitAsyncs[waitToken];
114+
if (address) {
115+
// Notify the waitAsync waiters on the memory location, so that JavaScript
116+
// garbage collection can occur.
117+
// See https://github.com/WebAssembly/threads/issues/176
118+
// This has the unfortunate effect of causing spurious wakeup of all other
119+
// waiters at the address (which causes a small performance loss).
120+
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
121+
delete liveAtomicWaitAsyncs[waitToken];
122+
{{{ runtimeKeepalivePop() }}}
123+
return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}};
124+
}
125+
// This waitToken does not exist.
126+
return {{{ cDefs.EMSCRIPTEN_RESULT_INVALID_PARAM }}};
127+
},
128+
129+
emscripten_atomic_cancel_all_wait_asyncs__deps: ['$liveAtomicWaitAsyncs'],
130+
emscripten_atomic_cancel_all_wait_asyncs: () => {
131+
let waitAsyncs = Object.values(liveAtomicWaitAsyncs);
132+
waitAsyncs.forEach((address) => {
133+
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
134+
});
135+
liveAtomicWaitAsyncs = {};
136+
return waitAsyncs.length;
137+
},
138+
139+
emscripten_atomic_cancel_all_wait_asyncs_at_address__deps: ['$liveAtomicWaitAsyncs'],
140+
emscripten_atomic_cancel_all_wait_asyncs_at_address: (address) => {
141+
let numCancelled = 0;
142+
Object.keys(liveAtomicWaitAsyncs).forEach((waitToken) => {
143+
if (liveAtomicWaitAsyncs[waitToken] == address) {
144+
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
145+
delete liveAtomicWaitAsyncs[waitToken];
146+
numCancelled++;
147+
}
148+
});
149+
return numCancelled;
150+
},
151+
});

src/library_wasm_worker.js

Lines changed: 6 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright 2023 The Emscripten Authors
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
17
{{{
28
global.captureModuleArg = () => MODULARIZE ? '' : 'self.Module=d;';
39
global.instantiateModule = () => MODULARIZE ? `${EXPORT_NAME}(d);` : '';
@@ -241,137 +247,6 @@ if (ENVIRONMENT_IS_WASM_WORKER) {
241247
_wasmWorkers[id].postMessage({'_wsc': funcPtr, 'x': readEmAsmArgs(sigPtr, varargs) });
242248
},
243249

244-
$atomicWaitStates: "['ok', 'not-equal', 'timed-out']",
245-
246-
// Chrome 87 (and hence Edge 87) shipped Atomics.waitAsync (https://www.chromestatus.com/feature/6243382101803008)
247-
// However its implementation is faulty: https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
248-
// Firefox Nightly 86.0a1 (2021-01-15) does not yet have it, https://bugzilla.mozilla.org/show_bug.cgi?id=1467846
249-
// And at the time of writing, no other browser has it either.
250-
#if MIN_EDGE_VERSION < 91 || MIN_CHROME_VERSION < 91 || MIN_SAFARI_VERSION != TARGET_NOT_SUPPORTED || MIN_FIREFOX_VERSION != TARGET_NOT_SUPPORTED || ENVIRONMENT_MAY_BE_NODE
251-
// Partially polyfill Atomics.waitAsync() if not available in the browser.
252-
// Also polyfill for old Chrome-based browsers, where Atomics.waitAsync is
253-
// broken until Chrome 91, see
254-
// https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
255-
// https://github.com/tc39/proposal-atomics-wait-async/blob/master/PROPOSAL.md
256-
// This polyfill performs polling with setTimeout() to observe a change in the
257-
// target memory location.
258-
$polyfillWaitAsync__deps: ['$jstoi_q'],
259-
$polyfillWaitAsync__postset: `if (!Atomics.waitAsync || (typeof navigator !== 'undefined' && navigator.userAgent && jstoi_q((navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./)||[])[2]) < 91)) {
260-
let __Atomics_waitAsyncAddresses = [/*[i32a, index, value, maxWaitMilliseconds, promiseResolve]*/];
261-
function __Atomics_pollWaitAsyncAddresses() {
262-
let now = performance.now();
263-
let l = __Atomics_waitAsyncAddresses.length;
264-
for (let i = 0; i < l; ++i) {
265-
let a = __Atomics_waitAsyncAddresses[i];
266-
let expired = (now > a[3]);
267-
let awoken = (Atomics.load(a[0], a[1]) != a[2]);
268-
if (expired || awoken) {
269-
__Atomics_waitAsyncAddresses[i--] = __Atomics_waitAsyncAddresses[--l];
270-
__Atomics_waitAsyncAddresses.length = l;
271-
a[4](awoken ? 'ok': 'timed-out');
272-
}
273-
}
274-
if (l) {
275-
// If we still have addresses to wait, loop the timeout handler to continue polling.
276-
setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
277-
}
278-
}
279-
#if ASSERTIONS
280-
if (!ENVIRONMENT_IS_WASM_WORKER) err('Current environment does not support Atomics.waitAsync(): polyfilling it, but this is going to be suboptimal.');
281-
#endif
282-
Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => {
283-
let val = Atomics.load(i32a, index);
284-
if (val != value) return { async: false, value: 'not-equal' };
285-
if (maxWaitMilliseconds <= 0) return { async: false, value: 'timed-out' };
286-
maxWaitMilliseconds = performance.now() + (maxWaitMilliseconds || Infinity);
287-
let promiseResolve;
288-
let promise = new Promise((resolve) => { promiseResolve = resolve; });
289-
if (!__Atomics_waitAsyncAddresses[0]) setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
290-
__Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, promiseResolve]);
291-
return { async: true, value: promise };
292-
};
293-
}`,
294-
#endif
295-
296-
$polyfillWaitAsync__internal: true,
297-
$polyfillWaitAsync: () => {
298-
// nop, used for its postset to ensure `Atomics.waitAsync()` polyfill is
299-
// included exactly once and only included when needed.
300-
// Any function using Atomics.waitAsync should depend on this.
301-
},
302-
303-
$liveAtomicWaitAsyncs: {},
304-
$liveAtomicWaitAsyncCounter: 0,
305-
306-
emscripten_atomic_wait_async__deps: ['$atomicWaitStates', '$liveAtomicWaitAsyncs', '$liveAtomicWaitAsyncCounter', '$polyfillWaitAsync'],
307-
emscripten_atomic_wait_async: (addr, val, asyncWaitFinished, userData, maxWaitMilliseconds) => {
308-
let wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds);
309-
if (!wait.async) return atomicWaitStates.indexOf(wait.value);
310-
// Increment waitAsync generation counter, account for wraparound in case
311-
// application does huge amounts of waitAsyncs per second (not sure if
312-
// possible?)
313-
// Valid counterrange: 0...2^31-1
314-
let counter = liveAtomicWaitAsyncCounter;
315-
liveAtomicWaitAsyncCounter = Math.max(0, (liveAtomicWaitAsyncCounter+1)|0);
316-
liveAtomicWaitAsyncs[counter] = addr;
317-
wait.value.then((value) => {
318-
if (liveAtomicWaitAsyncs[counter]) {
319-
delete liveAtomicWaitAsyncs[counter];
320-
{{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(addr, val, atomicWaitStates.indexOf(value), userData);
321-
}
322-
});
323-
return -counter;
324-
},
325-
326-
emscripten_atomic_cancel_wait_async__deps: ['$liveAtomicWaitAsyncs'],
327-
emscripten_atomic_cancel_wait_async: (waitToken) => {
328-
#if ASSERTIONS
329-
if (waitToken == {{{ cDefs.ATOMICS_WAIT_NOT_EQUAL }}}) {
330-
warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_NOT_EQUAL (1) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()');
331-
} else if (waitToken == {{{ cDefs.ATOMICS_WAIT_TIMED_OUT }}}) {
332-
warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_TIMED_OUT (2) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()');
333-
} else if (waitToken > 0) {
334-
warnOnce(`Attempted to call emscripten_atomic_cancel_wait_async() with an invalid wait token value ${waitToken}`);
335-
}
336-
#endif
337-
var address = liveAtomicWaitAsyncs[waitToken];
338-
if (address) {
339-
// Notify the waitAsync waiters on the memory location, so that JavaScript
340-
// garbage collection can occur.
341-
// See https://github.com/WebAssembly/threads/issues/176
342-
// This has the unfortunate effect of causing spurious wakeup of all other
343-
// waiters at the address (which causes a small performance loss).
344-
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
345-
delete liveAtomicWaitAsyncs[waitToken];
346-
return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}};
347-
}
348-
// This waitToken does not exist.
349-
return {{{ cDefs.EMSCRIPTEN_RESULT_INVALID_PARAM }}};
350-
},
351-
352-
emscripten_atomic_cancel_all_wait_asyncs__deps: ['$liveAtomicWaitAsyncs'],
353-
emscripten_atomic_cancel_all_wait_asyncs: () => {
354-
let waitAsyncs = Object.values(liveAtomicWaitAsyncs);
355-
waitAsyncs.forEach((address) => {
356-
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
357-
});
358-
liveAtomicWaitAsyncs = {};
359-
return waitAsyncs.length;
360-
},
361-
362-
emscripten_atomic_cancel_all_wait_asyncs_at_address__deps: ['$liveAtomicWaitAsyncs'],
363-
emscripten_atomic_cancel_all_wait_asyncs_at_address: (address) => {
364-
let numCancelled = 0;
365-
Object.keys(liveAtomicWaitAsyncs).forEach((waitToken) => {
366-
if (liveAtomicWaitAsyncs[waitToken] == address) {
367-
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
368-
delete liveAtomicWaitAsyncs[waitToken];
369-
numCancelled++;
370-
}
371-
});
372-
return numCancelled;
373-
},
374-
375250
emscripten_navigator_hardware_concurrency: () => {
376251
#if ENVIRONMENT_MAY_BE_NODE
377252
if (ENVIRONMENT_IS_NODE) return require('os').cpus().length;

src/modules.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ global.LibraryManager = {
153153
libraries.push('library_lz4.js');
154154
}
155155

156+
if (SHARED_MEMORY) {
157+
libraries.push('library_atomic.js');
158+
}
159+
156160
if (MAX_WEBGL_VERSION >= 2) {
157161
// library_webgl2.js must be included only after library_webgl.js, so if we are
158162
// about to include library_webgl2.js, first squeeze in library_webgl.js.

system/include/emscripten/atomic.h

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
#include <inttypes.h>
1111

12+
#include <emscripten/em_types.h>
13+
1214
#ifdef __cplusplus
1315
extern "C" {
1416
#endif
@@ -223,6 +225,71 @@ _EM_INLINE int64_t emscripten_atomic_notify(void *addr __attribute__((nonnull)),
223225
return __builtin_wasm_memory_atomic_notify((int*)addr, count);
224226
}
225227

228+
#define EMSCRIPTEN_WAIT_ASYNC_INFINITY __builtin_inf()
229+
230+
// Represents a pending 'Atomics.waitAsync' wait operation.
231+
#define ATOMICS_WAIT_TOKEN_T int32_t
232+
233+
#define EMSCRIPTEN_IS_VALID_WAIT_TOKEN(token) ((token) <= 0)
234+
235+
// Issues the JavaScript 'Atomics.waitAsync' instruction:
236+
// performs an asynchronous wait operation on the main thread. If the given
237+
// 'addr' contains 'value', issues a deferred wait that will invoke the
238+
// specified callback function 'asyncWaitFinished' once that address has been
239+
// notified by another thread.
240+
// NOTE: Unlike functions emscripten_atomic_wait_u32() and
241+
// emscripten_atomic_wait_u64() which take in the wait timeout parameter as int64
242+
// nanosecond units, this function takes in the wait timeout parameter as double
243+
// millisecond units. See https://github.com/WebAssembly/threads/issues/175 for
244+
// more information.
245+
// Pass in maxWaitMilliseconds == EMSCRIPTEN_WAIT_ASYNC_INFINITY
246+
// (==__builtin_inf()) to wait infinitely long.
247+
// Returns one of:
248+
// - ATOMICS_WAIT_NOT_EQUAL if the waitAsync operation could not be registered
249+
// since the memory value did not contain the value 'value'.
250+
// - ATOMICS_WAIT_TIMED_OUT if the waitAsync operation timeout parameter was <= 0.
251+
// - Any other value: denotes a 'wait token' that can be passed to function
252+
// emscripten_atomic_cancel_wait_async() to unregister an asynchronous wait.
253+
// You can use the macro EMSCRIPTEN_IS_VALID_WAIT_TOKEN(retval) to check if
254+
// this function returned a valid wait token.
255+
ATOMICS_WAIT_TOKEN_T emscripten_atomic_wait_async(void *addr __attribute__((nonnull)),
256+
uint32_t value,
257+
void (*asyncWaitFinished)(int32_t *addr, uint32_t value, ATOMICS_WAIT_RESULT_T waitResult, void *userData) __attribute__((nonnull)),
258+
void *userData,
259+
double maxWaitMilliseconds);
260+
261+
// Unregisters a pending Atomics.waitAsync operation that was established via a
262+
// call to emscripten_atomic_wait_async() in the calling thread. Pass in the
263+
// wait token handle that was received as the return value from the wait
264+
// function. Returns EMSCRIPTEN_RESULT_SUCCESS if the cancellation was
265+
// successful, or EMSCRIPTEN_RESULT_INVALID_PARAM if the asynchronous wait has
266+
// already resolved prior and the callback has already been called.
267+
// NOTE: Because of needing to work around issue
268+
// https://github.com/WebAssembly/threads/issues/176, calling this function has
269+
// an effect of introducing spurious wakeups to any other threads waiting on the
270+
// same address that the async wait denoted by the token does. This means that
271+
// in order to safely use this function, the mechanisms used in any wait code on
272+
// that address must be written to be spurious wakeup safe. (this is the case
273+
// for all the synchronization primitives declared in this header, but if you
274+
// are rolling out your own, you need to be aware of this). If
275+
// https://github.com/tc39/proposal-cancellation/issues/29 is resolved, then the
276+
// spurious wakeups can be avoided.
277+
EMSCRIPTEN_RESULT emscripten_atomic_cancel_wait_async(ATOMICS_WAIT_TOKEN_T waitToken);
278+
279+
// Cancels all pending async waits in the calling thread. Because of
280+
// https://github.com/WebAssembly/threads/issues/176, if you are using
281+
// asynchronous waits in your application, and need to be able to let GC reclaim
282+
// Wasm heap memory when deinitializing an application, you *must* call this
283+
// function to help the GC unpin all necessary memory. Otherwise, you can wrap
284+
// the Wasm content in an iframe and unload the iframe to let GC occur.
285+
// (navigating away from the page or closing that tab will also naturally
286+
// reclaim the memory)
287+
int emscripten_atomic_cancel_all_wait_asyncs(void);
288+
289+
// Cancels all pending async waits in the calling thread to the given memory
290+
// address. Returns the number of async waits canceled.
291+
int emscripten_atomic_cancel_all_wait_asyncs_at_address(void *addr __attribute__((nonnull)));
292+
226293
#undef _EM_INLINE
227294

228295
#ifdef __cplusplus

0 commit comments

Comments
 (0)