Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move emscripten_atomic_wait_async to atomic.h and add core testing #20404

Merged
merged 1 commit into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions src/library_atomic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* @license
* Copyright 2023 The Emscripten Authors
* SPDX-License-Identifier: MIT
*/

assert(SHARED_MEMORY);

addToLibrary({
// Chrome 87 (and hence Edge 87) shipped Atomics.waitAsync:
// https://www.chromestatus.com/feature/6243382101803008
// However its implementation is faulty:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
// Firefox Nightly 86.0a1 (2021-01-15) does not yet have it:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1467846
// And at the time of writing, no other browser has it either.
#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
// Partially polyfill Atomics.waitAsync() if not available in the browser.
// Also polyfill for old Chrome-based browsers, where Atomics.waitAsync is
// broken until Chrome 91, see:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
// https://github.com/tc39/proposal-atomics-wait-async/blob/master/PROPOSAL.md
// This polyfill performs polling with setTimeout() to observe a change in the
// target memory location.
$polyfillWaitAsync__postset: `if (!Atomics.waitAsync || (typeof navigator !== 'undefined' && navigator.userAgent && jstoi_q((navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./)||[])[2]) < 91)) {
let __Atomics_waitAsyncAddresses = [/*[i32a, index, value, maxWaitMilliseconds, promiseResolve]*/];
function __Atomics_pollWaitAsyncAddresses() {
let now = performance.now();
let l = __Atomics_waitAsyncAddresses.length;
for (let i = 0; i < l; ++i) {
let a = __Atomics_waitAsyncAddresses[i];
let expired = (now > a[3]);
let awoken = (Atomics.load(a[0], a[1]) != a[2]);
if (expired || awoken) {
__Atomics_waitAsyncAddresses[i--] = __Atomics_waitAsyncAddresses[--l];
__Atomics_waitAsyncAddresses.length = l;
a[4](awoken ? 'ok': 'timed-out');
}
}
if (l) {
// If we still have addresses to wait, loop the timeout handler to continue polling.
setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
}
}
#if ASSERTIONS && WASM_WORKERS
if (!ENVIRONMENT_IS_WASM_WORKER) err('Current environment does not support Atomics.waitAsync(): polyfilling it, but this is going to be suboptimal.');
#endif
/**
* @param {number=} maxWaitMilliseconds
*/
Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => {
let val = Atomics.load(i32a, index);
if (val != value) return { async: false, value: 'not-equal' };
if (maxWaitMilliseconds <= 0) return { async: false, value: 'timed-out' };
maxWaitMilliseconds = performance.now() + (maxWaitMilliseconds || Infinity);
let promiseResolve;
let promise = new Promise((resolve) => { promiseResolve = resolve; });
if (!__Atomics_waitAsyncAddresses[0]) setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
__Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, promiseResolve]);
return { async: true, value: promise };
};
}`,
$polyfillWaitAsync__deps: ['$jstoi_q'],
#endif

$polyfillWaitAsync__internal: true,
$polyfillWaitAsync: () => {
// nop, used for its postset to ensure `Atomics.waitAsync()` polyfill is
// included exactly once and only included when needed.
// Any function using Atomics.waitAsync should depend on this.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I suppose you are looking to fix a situation where only JS code would ever be doing Atomics.waitAsync()s, and Wasm code is doing all the wakes, hence the polyfill would never be emitted?

This situation looks like something that would be good to go in the Atomics related documentation?

Copy link
Collaborator

@juj juj Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing this PR for these functional changes is a bit tricky, given the code move. Are there other functional differences?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I suppose you are looking to fix a situation where only JS code would ever be doing Atomics.waitAsync()s, and Wasm code is doing all the wakes, hence the polyfill would never be emitted?

This situation looks like something that would be good to go in the Atomics related documentation?

I'm not quite sure I understand. Atomics.waitAsync can only ever be performed by JS code, since its JS only function, right? There is no wasm version of it.

This dependency just means the polyfill is only ever included when the library functions that call it are included. Isn't that desirable? Or would you rather the polyfill always be included?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing this PR for these functional changes is a bit tricky, given the code move. Are there other functional differences?

I'm pretty sure I didn't change anything functionally on the JS library side, just moved stuff around.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I suppose you are looking to fix a situation where only JS code would ever be doing Atomics.waitAsync()s, and Wasm code is doing all the wakes, hence the polyfill would never be emitted?

This situation looks like something that would be good to go in the Atomics related documentation?

I can split out the __deps fix and land that first to make it more obvious what is going on here ..

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one change to the JS code here, in order to support running emscripten_atomic_wait_async under node: I added runtimeKeepalivePush/runtimeKeepalivePop and callUserCallback to emscripten_atomic_wait_async. This means that the runtime will not exit will async waits are running, but will exit cleanly once the last one is done.

},

$atomicWaitStates__internal: true,
$atomicWaitStates: ['ok', 'not-equal', 'timed-out'],
$liveAtomicWaitAsyncs: {},
$liveAtomicWaitAsyncs__internal: true,
$liveAtomicWaitAsyncCounter: 0,
$liveAtomicWaitAsyncCounter__internal: true,

emscripten_atomic_wait_async__deps: ['$atomicWaitStates', '$liveAtomicWaitAsyncs', '$liveAtomicWaitAsyncCounter', '$polyfillWaitAsync', '$callUserCallback'],
emscripten_atomic_wait_async: (addr, val, asyncWaitFinished, userData, maxWaitMilliseconds) => {
let wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds);
if (!wait.async) return atomicWaitStates.indexOf(wait.value);
// Increment waitAsync generation counter, account for wraparound in case
// application does huge amounts of waitAsyncs per second (not sure if
// possible?)
// Valid counterrange: 0...2^31-1
let counter = liveAtomicWaitAsyncCounter;
liveAtomicWaitAsyncCounter = Math.max(0, (liveAtomicWaitAsyncCounter+1)|0);
liveAtomicWaitAsyncs[counter] = addr;
{{{ runtimeKeepalivePush() }}}
wait.value.then((value) => {
if (liveAtomicWaitAsyncs[counter]) {
{{{ runtimeKeepalivePop() }}}
delete liveAtomicWaitAsyncs[counter];
callUserCallback(() => {{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(addr, val, atomicWaitStates.indexOf(value), userData));
}
});
return -counter;
},

emscripten_atomic_cancel_wait_async__deps: ['$liveAtomicWaitAsyncs'],
emscripten_atomic_cancel_wait_async: (waitToken) => {
#if ASSERTIONS
if (waitToken == {{{ cDefs.ATOMICS_WAIT_NOT_EQUAL }}}) {
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()');
} else if (waitToken == {{{ cDefs.ATOMICS_WAIT_TIMED_OUT }}}) {
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()');
} else if (waitToken > 0) {
warnOnce(`Attempted to call emscripten_atomic_cancel_wait_async() with an invalid wait token value ${waitToken}`);
}
#endif
var address = liveAtomicWaitAsyncs[waitToken];
if (address) {
// Notify the waitAsync waiters on the memory location, so that JavaScript
// garbage collection can occur.
// See https://github.com/WebAssembly/threads/issues/176
// This has the unfortunate effect of causing spurious wakeup of all other
// waiters at the address (which causes a small performance loss).
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
delete liveAtomicWaitAsyncs[waitToken];
{{{ runtimeKeepalivePop() }}}
return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}};
}
// This waitToken does not exist.
return {{{ cDefs.EMSCRIPTEN_RESULT_INVALID_PARAM }}};
},

emscripten_atomic_cancel_all_wait_asyncs__deps: ['$liveAtomicWaitAsyncs'],
emscripten_atomic_cancel_all_wait_asyncs: () => {
let waitAsyncs = Object.values(liveAtomicWaitAsyncs);
waitAsyncs.forEach((address) => {
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
});
liveAtomicWaitAsyncs = {};
return waitAsyncs.length;
},

emscripten_atomic_cancel_all_wait_asyncs_at_address__deps: ['$liveAtomicWaitAsyncs'],
emscripten_atomic_cancel_all_wait_asyncs_at_address: (address) => {
let numCancelled = 0;
Object.keys(liveAtomicWaitAsyncs).forEach((waitToken) => {
if (liveAtomicWaitAsyncs[waitToken] == address) {
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
delete liveAtomicWaitAsyncs[waitToken];
numCancelled++;
}
});
return numCancelled;
},
});
137 changes: 6 additions & 131 deletions src/library_wasm_worker.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* @license
* Copyright 2023 The Emscripten Authors
* SPDX-License-Identifier: MIT
*/

{{{
global.captureModuleArg = () => MODULARIZE ? '' : 'self.Module=d;';
global.instantiateModule = () => MODULARIZE ? `${EXPORT_NAME}(d);` : '';
Expand Down Expand Up @@ -241,137 +247,6 @@ if (ENVIRONMENT_IS_WASM_WORKER) {
_wasmWorkers[id].postMessage({'_wsc': funcPtr, 'x': readEmAsmArgs(sigPtr, varargs) });
},

$atomicWaitStates: "['ok', 'not-equal', 'timed-out']",

// Chrome 87 (and hence Edge 87) shipped Atomics.waitAsync (https://www.chromestatus.com/feature/6243382101803008)
// However its implementation is faulty: https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
// Firefox Nightly 86.0a1 (2021-01-15) does not yet have it, https://bugzilla.mozilla.org/show_bug.cgi?id=1467846
// And at the time of writing, no other browser has it either.
#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
// Partially polyfill Atomics.waitAsync() if not available in the browser.
// Also polyfill for old Chrome-based browsers, where Atomics.waitAsync is
// broken until Chrome 91, see
// https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
// https://github.com/tc39/proposal-atomics-wait-async/blob/master/PROPOSAL.md
// This polyfill performs polling with setTimeout() to observe a change in the
// target memory location.
$polyfillWaitAsync__deps: ['$jstoi_q'],
$polyfillWaitAsync__postset: `if (!Atomics.waitAsync || (typeof navigator !== 'undefined' && navigator.userAgent && jstoi_q((navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./)||[])[2]) < 91)) {
let __Atomics_waitAsyncAddresses = [/*[i32a, index, value, maxWaitMilliseconds, promiseResolve]*/];
function __Atomics_pollWaitAsyncAddresses() {
let now = performance.now();
let l = __Atomics_waitAsyncAddresses.length;
for (let i = 0; i < l; ++i) {
let a = __Atomics_waitAsyncAddresses[i];
let expired = (now > a[3]);
let awoken = (Atomics.load(a[0], a[1]) != a[2]);
if (expired || awoken) {
__Atomics_waitAsyncAddresses[i--] = __Atomics_waitAsyncAddresses[--l];
__Atomics_waitAsyncAddresses.length = l;
a[4](awoken ? 'ok': 'timed-out');
}
}
if (l) {
// If we still have addresses to wait, loop the timeout handler to continue polling.
setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
}
}
#if ASSERTIONS
if (!ENVIRONMENT_IS_WASM_WORKER) err('Current environment does not support Atomics.waitAsync(): polyfilling it, but this is going to be suboptimal.');
#endif
Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => {
let val = Atomics.load(i32a, index);
if (val != value) return { async: false, value: 'not-equal' };
if (maxWaitMilliseconds <= 0) return { async: false, value: 'timed-out' };
maxWaitMilliseconds = performance.now() + (maxWaitMilliseconds || Infinity);
let promiseResolve;
let promise = new Promise((resolve) => { promiseResolve = resolve; });
if (!__Atomics_waitAsyncAddresses[0]) setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
__Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, promiseResolve]);
return { async: true, value: promise };
};
}`,
#endif

$polyfillWaitAsync__internal: true,
$polyfillWaitAsync: () => {
// nop, used for its postset to ensure `Atomics.waitAsync()` polyfill is
// included exactly once and only included when needed.
// Any function using Atomics.waitAsync should depend on this.
},

$liveAtomicWaitAsyncs: {},
$liveAtomicWaitAsyncCounter: 0,

emscripten_atomic_wait_async__deps: ['$atomicWaitStates', '$liveAtomicWaitAsyncs', '$liveAtomicWaitAsyncCounter', '$polyfillWaitAsync'],
emscripten_atomic_wait_async: (addr, val, asyncWaitFinished, userData, maxWaitMilliseconds) => {
let wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds);
if (!wait.async) return atomicWaitStates.indexOf(wait.value);
// Increment waitAsync generation counter, account for wraparound in case
// application does huge amounts of waitAsyncs per second (not sure if
// possible?)
// Valid counterrange: 0...2^31-1
let counter = liveAtomicWaitAsyncCounter;
liveAtomicWaitAsyncCounter = Math.max(0, (liveAtomicWaitAsyncCounter+1)|0);
liveAtomicWaitAsyncs[counter] = addr;
wait.value.then((value) => {
if (liveAtomicWaitAsyncs[counter]) {
delete liveAtomicWaitAsyncs[counter];
{{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(addr, val, atomicWaitStates.indexOf(value), userData);
}
});
return -counter;
},

emscripten_atomic_cancel_wait_async__deps: ['$liveAtomicWaitAsyncs'],
emscripten_atomic_cancel_wait_async: (waitToken) => {
#if ASSERTIONS
if (waitToken == {{{ cDefs.ATOMICS_WAIT_NOT_EQUAL }}}) {
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()');
} else if (waitToken == {{{ cDefs.ATOMICS_WAIT_TIMED_OUT }}}) {
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()');
} else if (waitToken > 0) {
warnOnce(`Attempted to call emscripten_atomic_cancel_wait_async() with an invalid wait token value ${waitToken}`);
}
#endif
var address = liveAtomicWaitAsyncs[waitToken];
if (address) {
// Notify the waitAsync waiters on the memory location, so that JavaScript
// garbage collection can occur.
// See https://github.com/WebAssembly/threads/issues/176
// This has the unfortunate effect of causing spurious wakeup of all other
// waiters at the address (which causes a small performance loss).
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
delete liveAtomicWaitAsyncs[waitToken];
return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}};
}
// This waitToken does not exist.
return {{{ cDefs.EMSCRIPTEN_RESULT_INVALID_PARAM }}};
},

emscripten_atomic_cancel_all_wait_asyncs__deps: ['$liveAtomicWaitAsyncs'],
emscripten_atomic_cancel_all_wait_asyncs: () => {
let waitAsyncs = Object.values(liveAtomicWaitAsyncs);
waitAsyncs.forEach((address) => {
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
});
liveAtomicWaitAsyncs = {};
return waitAsyncs.length;
},

emscripten_atomic_cancel_all_wait_asyncs_at_address__deps: ['$liveAtomicWaitAsyncs'],
emscripten_atomic_cancel_all_wait_asyncs_at_address: (address) => {
let numCancelled = 0;
Object.keys(liveAtomicWaitAsyncs).forEach((waitToken) => {
if (liveAtomicWaitAsyncs[waitToken] == address) {
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
delete liveAtomicWaitAsyncs[waitToken];
numCancelled++;
}
});
return numCancelled;
},

emscripten_navigator_hardware_concurrency: () => {
#if ENVIRONMENT_MAY_BE_NODE
if (ENVIRONMENT_IS_NODE) return require('os').cpus().length;
Expand Down
4 changes: 4 additions & 0 deletions src/modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ global.LibraryManager = {
libraries.push('library_lz4.js');
}

if (SHARED_MEMORY) {
libraries.push('library_atomic.js');
}

if (MAX_WEBGL_VERSION >= 2) {
// library_webgl2.js must be included only after library_webgl.js, so if we are
// about to include library_webgl2.js, first squeeze in library_webgl.js.
Expand Down
67 changes: 67 additions & 0 deletions system/include/emscripten/atomic.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

#include <inttypes.h>

#include <emscripten/em_types.h>

#ifdef __cplusplus
extern "C" {
#endif
Expand Down Expand Up @@ -223,6 +225,71 @@ _EM_INLINE int64_t emscripten_atomic_notify(void *addr __attribute__((nonnull)),
return __builtin_wasm_memory_atomic_notify((int*)addr, count);
}

#define EMSCRIPTEN_WAIT_ASYNC_INFINITY __builtin_inf()

// Represents a pending 'Atomics.waitAsync' wait operation.
#define ATOMICS_WAIT_TOKEN_T int32_t

#define EMSCRIPTEN_IS_VALID_WAIT_TOKEN(token) ((token) <= 0)

// Issues the JavaScript 'Atomics.waitAsync' instruction:
// performs an asynchronous wait operation on the main thread. If the given
// 'addr' contains 'value', issues a deferred wait that will invoke the
// specified callback function 'asyncWaitFinished' once that address has been
// notified by another thread.
// NOTE: Unlike functions emscripten_atomic_wait_u32() and
// emscripten_atomic_wait_u64() which take in the wait timeout parameter as int64
// nanosecond units, this function takes in the wait timeout parameter as double
// millisecond units. See https://github.com/WebAssembly/threads/issues/175 for
// more information.
// Pass in maxWaitMilliseconds == EMSCRIPTEN_WAIT_ASYNC_INFINITY
// (==__builtin_inf()) to wait infinitely long.
// Returns one of:
// - ATOMICS_WAIT_NOT_EQUAL if the waitAsync operation could not be registered
// since the memory value did not contain the value 'value'.
// - ATOMICS_WAIT_TIMED_OUT if the waitAsync operation timeout parameter was <= 0.
// - Any other value: denotes a 'wait token' that can be passed to function
// emscripten_atomic_cancel_wait_async() to unregister an asynchronous wait.
// You can use the macro EMSCRIPTEN_IS_VALID_WAIT_TOKEN(retval) to check if
// this function returned a valid wait token.
ATOMICS_WAIT_TOKEN_T emscripten_atomic_wait_async(void *addr __attribute__((nonnull)),
uint32_t value,
void (*asyncWaitFinished)(int32_t *addr, uint32_t value, ATOMICS_WAIT_RESULT_T waitResult, void *userData) __attribute__((nonnull)),
void *userData,
double maxWaitMilliseconds);

// Unregisters a pending Atomics.waitAsync operation that was established via a
// call to emscripten_atomic_wait_async() in the calling thread. Pass in the
// wait token handle that was received as the return value from the wait
// function. Returns EMSCRIPTEN_RESULT_SUCCESS if the cancellation was
// successful, or EMSCRIPTEN_RESULT_INVALID_PARAM if the asynchronous wait has
// already resolved prior and the callback has already been called.
// NOTE: Because of needing to work around issue
// https://github.com/WebAssembly/threads/issues/176, calling this function has
// an effect of introducing spurious wakeups to any other threads waiting on the
// same address that the async wait denoted by the token does. This means that
// in order to safely use this function, the mechanisms used in any wait code on
// that address must be written to be spurious wakeup safe. (this is the case
// for all the synchronization primitives declared in this header, but if you
// are rolling out your own, you need to be aware of this). If
// https://github.com/tc39/proposal-cancellation/issues/29 is resolved, then the
// spurious wakeups can be avoided.
EMSCRIPTEN_RESULT emscripten_atomic_cancel_wait_async(ATOMICS_WAIT_TOKEN_T waitToken);

// Cancels all pending async waits in the calling thread. Because of
// https://github.com/WebAssembly/threads/issues/176, if you are using
// asynchronous waits in your application, and need to be able to let GC reclaim
// Wasm heap memory when deinitializing an application, you *must* call this
// function to help the GC unpin all necessary memory. Otherwise, you can wrap
// the Wasm content in an iframe and unload the iframe to let GC occur.
// (navigating away from the page or closing that tab will also naturally
// reclaim the memory)
int emscripten_atomic_cancel_all_wait_asyncs(void);

// Cancels all pending async waits in the calling thread to the given memory
// address. Returns the number of async waits canceled.
int emscripten_atomic_cancel_all_wait_asyncs_at_address(void *addr __attribute__((nonnull)));

#undef _EM_INLINE

#ifdef __cplusplus
Expand Down
Loading