fix JS event loop blocking

This commit is contained in:
Adam Powers
2020-09-06 17:32:08 -07:00
parent 045a5879f1
commit 96cf11ad71
6 changed files with 229 additions and 247 deletions

View File

@@ -34,12 +34,10 @@ Where is the header file for the API you ask? There isn't one. It's three functi
## API: nethack.js
The WebAssembly API has a similar signature to `libnethack.a` with minor syntactic differences:
* `main(int argc, char argv[])` - The main function for NetHack
* `shim_graphics_set_callback(shim_callback_t cb)` - The same as above, but the signature of the callback is slightly different because WASM can't handle variadic callbacks. The callback is: `void shim_callback_t(const char *name, void *ret_ptr, const char *fmt, void *args[])`
* `name` - same as above
* `ret_ptr` - same as above
* `fmt` - same as above
* `args` - an array of pointers to the arguments, where each pointer can be de-referenced to a value as specified in the `fmt` string.
* `shim_graphics_set_callback(char *cbName)` - A `String` representing a name of a callback function. The callback function be registered as `globalThis[cbName] = function yourCallback(name, ... args) { /* your stuff */ }`. Note that [globalThis](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis) points to `window` in browsers and `global` in node.js.
* `name` is the name of the [window function](https://github.com/NetHack/NetHack/blob/NetHack-3.7/doc/window.doc) that needs to be handled
* `... args` is a variable number and type of arguments depending on the `window function` that is being called. The arguments associated with each `name` are described in the [NetHack window.doc](https://github.com/NetHack/NetHack/blob/NetHack-3.7/doc/window.doc)
* The function must return the value expected for the specified `name`
## API Stability
@@ -54,7 +52,7 @@ typedef void(*shim_callback_t)(const char *name, void *ret_ptr, const char *fmt,
void shim_graphics_set_callback(shim_callback_t cb);
void window_cb(const char *name, void *ret_ptr, const char *fmt, ...) {
/* TODO -- see windowCallback below for hints */
/* TODO */
}
int main(int argc, char *argv[]) {
@@ -65,117 +63,33 @@ int main(int argc, char *argv[]) {
## nethack.js example
``` js
// Module is defined by emscripten
// https://emscripten.org/docs/api_reference/module.html
let Module = {
// if this is true, main() won't be called automatically
// noInitialRun: true,
const path = require("path");
// after loading the library, set the callback function
onRuntimeInitialized: function (... args) {
setGraphicsCallback();
}
};
// starts nethack
function nethackStart(cb, inputModule = {}) {
// set callback
let cbName = cb.name;
if (cbName === "") cbName = "__anonymousNetHackCallback";
let userCallback = globalThis[cbName] = cb;
var factory = require("./src/nethack.js");
// Emscripten Module config
let Module = inputModule;
savedOnRuntimeInitialized = Module.onRuntimeInitialized;
Module.onRuntimeInitialized = function (... args) {
// after the WASM is loaded, add the shim graphics callback function
Module.ccall(
"shim_graphics_set_callback", // C function name
null, // return type
["string"], // arg types
[cbName], // arg values
{async: true} // options
);
};
// run NetHack!
factory(Module);
// register the callback with the WASM library
function setGraphicsCallback() {
console.log("creating WASM callback function");
let cb = Module.addFunction(windowCallback, "viiii");
console.log("setting callback function with library");
Module.ccall(
"shim_graphics_set_callback", // C function name
null, // return type
["number"], // arg types
[cb], // arg values
{async: true} // options
);
/* TODO: removeFunction */
// load and run the module
var factory = require(path.join(__dirname, "../build/nethack.js"));
factory(Module);
}
// this is the "shim graphics" callback function
// it gets called every time something needs to be displayed
// or input needs to be gathered from the user
function windowCallback(name, retPtr, fmt, args) {
name = Module.UTF8ToString(name);
fmt = Module.UTF8ToString(fmt);
let argTypes = fmt.split("");
let retType = argTypes.shift();
// convert arguments to JavaScript types
let jsArgs = [];
for (let i = 0; i < argTypes.length; i++) {
let ptr = args + (4*i);
let val = typeLookup(argTypes[i], ptr);
jsArgs.push(val);
}
console.log(`graphics callback: ${name} [${jsArgs}]`);
/**********
* YOU HAVE TO IMPLEMENT THIS FUNCTION to render things
**********/
let ret = yourFunctionToRenderGraphics(name, jsArgs);
setReturn(retPtr, retType, ret);
}
// takes a character `type` and a WASM pointer and returns a JavaScript value
function typeLookup(type, ptr) {
switch(type) {
case "s": // string
return Module.UTF8ToString(Module.getValue(ptr, "*"));
case "p": // pointer
return Module.getValue(Module.getValue(ptr, "*"), "*");
case "c": // char
return String.fromCharCode(Module.getValue(Module.getValue(ptr, "*"), "i8"));
case "0": /* 2^0 = 1 byte */
return Module.getValue(Module.getValue(ptr, "*"), "i8");
case "1": /* 2^1 = 2 bytes */
return Module.getValue(Module.getValue(ptr, "*"), "i16");
case "2": /* 2^2 = 4 bytes */
case "i": // integer
case "n": // number
return Module.getValue(Module.getValue(ptr, "*"), "i32");
case "f": // float
return Module.getValue(Module.getValue(ptr, "*"), "float");
case "d": // double
return Module.getValue(Module.getValue(ptr, "*"), "double");
default:
throw new TypeError ("unknown type:" + type);
}
}
// takes a a WASM pointer, a charater `type` and a value and sets the value at pointer
function setReturn(ptr, type, value = 0) {
switch (type) {
case "p":
throw new Error("not implemented");
case "s":
value=value?value:"(no value)";
var strPtr = Module.getValue(ptr, "i32");
Module.stringToUTF8(value, strPtr, 1024);
break;
case "i":
Module.setValue(ptr, value, "i32");
break;
case "c":
Module.setValue(ptr, value, "i8");
break;
case "f":
// XXX: I'm not sure why 'double' works and 'float' doesn't
Module.setValue(ptr, value, "double");
break;
case "d":
Module.setValue(ptr, value, "double");
break;
case "v":
break;
default:
throw new Error("unknown type");
}
}
nethackStart(yourCallbackFunction);
```

View File

@@ -15,21 +15,30 @@ EMRANLIB=emranlib
EMCC_LFLAGS=-s SINGLE_FILE=1
EMCC_LFLAGS=-s WASM=1
EMCC_LFLAGS+=-s ALLOW_TABLE_GROWTH
EMCC_LFLAGS+=-s ASYNCIFY -s ASYNCIFY_IMPORTS='["_nhmain"]' -O3
EMCC_LFLAGS+=-s ASYNCIFY -s ASYNCIFY_IMPORTS='["local_callback"]'
EMCC_LFLAGS+=-O3
EMCC_LFLAGS+=-s MODULARIZE
EMCC_LFLAGS+=-s EXPORTED_FUNCTIONS='["_main", "_shim_graphics_set_callback"]'
EMCC_LFLAGS+=-s EXPORTED_RUNTIME_METHODS='["cwrap", "ccall", "addFunction", "removeFunction", "UTF8ToString", "getValue", "setValue"]'
EMCC_LFLAGS+=-s ERROR_ON_UNDEFINED_SYMBOLS=0
EMCC_LFLAGS+=--embed-file wasm-data@/
# For a list of EMCC settings:
# https://github.com/emscripten-core/emscripten/blob/master/src/settings.js
# WASM C flags
EMCC_CFLAGS=
EMCC_CFLAGS+=-Wall
EMCC_CFLAGS+=-Werror
#EMCC_CFLAGS+=-s DISABLE_EXCEPTION_CATCHING=0
EMCC_DEBUG_CFLAGS+=-s ASSERTIONS=1
#EMCC_DEBUG_CFLAGS+=-s ASSERTIONS=2
EMCC_DEBUG_CFLAGS+=-s STACK_OVERFLOW_CHECK=2
EMCC_DEBUG_CFLAGS+=-s SAFE_HEAP=1
EMCC_DEBUG_CFLAGS+=-s LLD_REPORT_UNDEFINED
EMCC_DEBUG_CFLAGS+=-s LLD_REPORT_UNDEFINED=1
#EMCC_DEBUG_CFLAGS+=-s EXCEPTION_DEBUG=1
#EMCC_DEBUG_CFLAGS+=-fsanitize=undefined -fsanitize=address -fsanitize=leak
#EMCC_DEBUG_CFLAGS+=-s EXIT_RUNTIME
EMCC_PROD_CFLAGS+=-O3
# Nethack C flags

View File

@@ -11,7 +11,7 @@ npm install nethack
## API
The main module returns a setup function: `startNethack(uiCallback, moduleOptions)`.
* `uiCallback(name, ... args)` - Your callback function that will handle rendering NetHack on the screen of your choice. The `name` argument is one of the UI functions of the [NetHack Window Interface](https://github.com/NetHack/NetHack/blob/NetHack-3.7/doc/window.doc) and the `args` are corresponding to the window interface function that is being called. You are required to return the correct type of data for the function that is implemented.
* `uiCallback(name, ... args)` - Your callback function that will handle rendering NetHack on the screen of your choice. The `name` argument is one of the UI functions of the [NetHack Window Interface](https://github.com/NetHack/NetHack/blob/NetHack-3.7/doc/window.doc) and the `args` are corresponding to the window interface function that is being called. You are required to return the correct type of data for the function that is implemented. The `uiCallback` may be an `async` function.
* `moduleOptions` - An optional [emscripten Module object](https://emscripten.org/docs/api_reference/module.html) for configuring the WASM that will be run.
* `Module.arguments` - Of note is the [arguments property](https://emscripten.org/docs/api_reference/module.html#Module.arguments) which gets passed to NetHack as its [command line parameters](https://nethackwiki.com/wiki/Options).
@@ -22,7 +22,7 @@ let nethackStart = require("nethack");
nethackStart(doGraphics);
let winCount = 0;
function doGraphics(name, ... args) {
async function doGraphics(name, ... args) {
console.log(`shim graphics: ${name} [${args}]`);
switch(name) {

View File

@@ -3,27 +3,38 @@ const path = require("path");
let Module;
let userCallback;
let savedOnRuntimeInitialized;
// starts nethack
function nethackStart(cb, inputModule = {}) {
if(typeof cb !== "function") throw new TypeError("expected first argument to be callback function");
if(typeof cb !== "string" && typeof cb !== "function") throw new TypeError("expected first argument to be 'Function' or 'String' representing global callback function name");
if(typeof inputModule !== "object") throw new TypeError("expected second argument to be object");
let cbName;
if(typeof cb === "function") {
cbName = cb.name;
if (cbName === "") cbName = "__anonymousNetHackCallback";
if (globalThis[cbName] === undefined) globalThis[cbName] = cb;
else if (globalThis[cbName] !== cb) throw new Error (`'globalThis["${cbName}"]' is not the same as specified callback`);
}
/* global globalThis */
userCallback = globalThis[cbName];
if(typeof userCallback !== "function") throw new TypeError(`expected 'globalThis["${cbName}"]' to be a function`);
// if(userCallback.constructor.name !== "AsyncFunction") throw new TypeError(`expected 'globalThis["${cbName}"]' to be an async function`);
// Emscripten Module config
Module = inputModule;
userCallback = cb;
savedOnRuntimeInitialized = Module.onRuntimeInitialized;
Module.onRuntimeInitialized = function (... args) {
// after the WASM is loaded, add the shim graphics callback function
let cb = Module.addFunction(windowCallback, "viiii");
Module.ccall(
"shim_graphics_set_callback", // C function name
null, // return type
["number"], // arg types
[cb], // arg values
["string"], // arg types
[cbName], // arg values
{async: true} // options
);
/* TODO: Module.removeFunction() */
// if the user had their own onRuntimeInitialized(), call it now
if (savedOnRuntimeInitialized) savedOnRuntimeInitialized(... args);
};
@@ -33,94 +44,6 @@ function nethackStart(cb, inputModule = {}) {
factory(Module);
}
function windowCallback(name, retPtr, fmt, args) {
name = Module.UTF8ToString(name);
fmt = Module.UTF8ToString(fmt);
let argTypes = fmt.split("");
let retType = argTypes.shift();
// build array of JavaScript args from WASM parameters
let jsArgs = [];
for (let i = 0; i < argTypes.length; i++) {
let ptr = args + (4*i);
let val = typeLookup(argTypes[i], ptr);
jsArgs.push(val);
}
let retVal = userCallback(name, ... jsArgs);
setReturn(name, retPtr, retType, retVal);
}
function typeLookup(type, ptr) {
switch(type) {
case "s": // string
return Module.UTF8ToString(Module.getValue(ptr, "*"));
case "p": // pointer
ptr = Module.getValue(ptr, "*");
if(!ptr) return 0; // null pointer
return Module.getValue(ptr, "*");
case "c": // char
return String.fromCharCode(Module.getValue(Module.getValue(ptr, "*"), "i8"));
case "0": /* 2^0 = 1 byte */
return Module.getValue(Module.getValue(ptr, "*"), "i8");
case "1": /* 2^1 = 2 bytes */
return Module.getValue(Module.getValue(ptr, "*"), "i16");
case "2": /* 2^2 = 4 bytes */
case "i": // integer
case "n": // number
return Module.getValue(Module.getValue(ptr, "*"), "i32");
case "f": // float
return Module.getValue(Module.getValue(ptr, "*"), "float");
case "d": // double
return Module.getValue(Module.getValue(ptr, "*"), "double");
default:
throw new TypeError ("unknown type:" + type);
}
}
function setReturn(name, ptr, type, value = 0) {
switch (type) {
case "p":
throw new Error("not implemented");
case "s":
if(typeof value !== "string")
throw new TypeError(`expected ${name} return type to be string`);
value=value?value:"(no value)";
var strPtr = Module.getValue(ptr, "i32");
Module.stringToUTF8(value, strPtr, 1024);
break;
case "i":
if(typeof value !== "number" || !Number.isInteger(value))
throw new TypeError(`expected ${name} return type to be integer`);
Module.setValue(ptr, value, "i32");
break;
case "c":
if(typeof value !== "number" || value < 0 || value > 128)
throw new TypeError(`expected ${name} return type to be integer representing an ASCII character`);
Module.setValue(ptr, value, "i8");
break;
case "f":
if(typeof value !== "number" || isFloat(value))
throw new TypeError(`expected ${name} return type to be float`);
// XXX: I'm not sure why 'double' works and 'float' doesn't
Module.setValue(ptr, value, "double");
break;
case "d":
if(typeof value !== "number" || isFloat(value))
throw new TypeError(`expected ${name} return type to be float`);
Module.setValue(ptr, value, "double");
break;
case "v":
break;
default:
throw new Error("unknown type");
}
function isFloat(n){
return n === +n && n !== (n|0) && !Number.isInteger(n);
}
}
// TODO: ES6 'import' style module
module.exports = nethackStart;

View File

@@ -1,28 +1,37 @@
#include <stdio.h>
#include <stdarg.h>
/* external functions */
int nhmain(int argc, char *argv[]);
typedef void(*stub_callback_t)(const char *name, void *ret_ptr, const char *fmt, ...);
void stub_graphics_set_callback(stub_callback_t cb);
void shim_graphics_set_callback(stub_callback_t cb);
/* forward declarations */
void window_cb(const char *name, void *ret_ptr, const char *fmt, ...);
void *yourFunctionToRenderGraphics(const char *name, va_list args);
int main(int argc, char *argv[]) {
stub_graphics_set_callback(window_cb);
shim_graphics_set_callback(window_cb);
nhmain(argc, argv);
}
void window_cb(const char *name, void *ret_ptr, const char *fmt, ...) {
/* TODO -- see windowCallback below for hints */
void *yourFunctionToRenderGraphics(const char *name, va_list args) {
printf("yourFunctionToRenderGraphics name %s\n", name);
/* DO SOMETHING HERE */
*ret_ptr = yourFunctionToRenderGraphics(name, va_list args);
return NULL;
}
void window_cb(const char *name, void *ret_ptr, const char *fmt, ...) {
void *ret;
va_list args;
/* TODO -- see windowCallback below for hints */
va_start(args, fmt);
ret = yourFunctionToRenderGraphics(name, args);
// *((int *)ret_ptr = *((int *)ret); // e.g. yourFunctionToRenderGraphics returns an int
va_end(args);
}
#if 0
function variadicCallback(name, retPtr, fmt, args) {

View File

@@ -5,6 +5,7 @@
/* not an actual windowing port, but a fake win port for libnethack */
#include "hack.h"
#include <string.h>
#ifdef SHIM_GRAPHICS
#include <stdarg.h>
@@ -15,26 +16,30 @@
#undef SHIM_DEBUG
#ifndef __EMSCRIPTEN__
typedef void(*shim_callback_t)(const char *name, void *ret_ptr, const char *fmt, ...);
#else /* __EMSCRIPTEN__ */
/* WASM can't handle a variadic callback, so we pass back an array of pointers instead... */
typedef void(*shim_callback_t)(const char *name, void *ret_ptr, const char *fmt, void *args[]);
#endif /* !__EMSCRIPTEN__ */
#ifdef SHIM_DEBUG
#define debugf printf
#else /* !SHIM_DEBUG */
#define debugf(...)
#endif /* SHIM_DEBUG */
/* this is the primary interface to shim graphics,
/* shim_graphics_callback is the primary interface to shim graphics,
* call this function with your declared callback function
* and you will receive all the windowing calls
*/
static shim_callback_t shim_graphics_callback = NULL;
#ifdef __EMSCRIPTEN__
EMSCRIPTEN_KEEPALIVE
#endif
void shim_graphics_set_callback(shim_callback_t cb) {
shim_graphics_callback = cb;
/************
* WASM interface
************/
EMSCRIPTEN_KEEPALIVE
static char *shim_callback_name = NULL;
void shim_graphics_set_callback(char *cbName) {
if (shim_callback_name != NULL) free(shim_callback_name);
shim_callback_name = strdup(cbName);
/* TODO: free(shim_callback_name) during shutdown? */
}
void local_callback (const char *cb_name, const char *shim_name, void *ret_ptr, const char *fmt_str, void *args);
#ifdef __EMSCRIPTEN__
/* A2P = Argument to Pointer */
#define A2P &
/* P2V = Pointer to Void */
@@ -44,8 +49,8 @@ ret_type name fn_args { \
void *args[] = { __VA_ARGS__ }; \
ret_type ret = (ret_type) 0; \
debugf("SHIM GRAPHICS: " #name "\n"); \
if (!shim_graphics_callback) return ret; \
shim_graphics_callback(#name, (void *)&ret, fmt, args); \
if (!shim_callback_name) return ret; \
local_callback(shim_callback_name, #name, (void *)&ret, fmt, args); \
return ret; \
}
@@ -53,10 +58,21 @@ ret_type name fn_args { \
void name fn_args { \
void *args[] = { __VA_ARGS__ }; \
debugf("SHIM GRAPHICS: " #name "\n"); \
if (!shim_graphics_callback) return; \
shim_graphics_callback(#name, NULL, fmt, args); \
if (!shim_callback_name) return; \
local_callback(shim_callback_name, #name, NULL, fmt, args); \
}
#else /* !__EMSCRIPTEN__ */
/************
* libnethack.a interface
************/
typedef void(*shim_callback_t)(const char *name, void *ret_ptr, const char *fmt, ...);
static shim_callback_t shim_graphics_callback = NULL;
void shim_graphics_set_callback(shim_callback_t cb) {
shim_graphics_callback = cb;
}
#define A2P
#define P2V
#define DECLCB(ret_type, name, fn_args, fmt, ...) \
@@ -64,7 +80,7 @@ ret_type name fn_args { \
ret_type ret = (ret_type) 0; \
debugf("SHIM GRAPHICS: " #name "\n"); \
if (!shim_graphics_callback) return ret; \
shim_graphics_callback(#name, (void *)&ret, fmt, __VA_ARGS__); \
shim_graphics_callback(#name, (void *)&ret, fmt, ## __VA_ARGS__); \
return ret; \
}
@@ -72,17 +88,10 @@ ret_type name fn_args { \
void name fn_args { \
debugf("SHIM GRAPHICS: " #name "\n"); \
if (!shim_graphics_callback) return; \
shim_graphics_callback(#name, NULL, fmt, __VA_ARGS__); \
shim_graphics_callback(#name, NULL, fmt, ## __VA_ARGS__); \
}
#endif /* __EMSCRIPTEN__ */
#ifdef SHIM_DEBUG
#define debugf printf
#else /* !SHIM_DEBUG */
#define debugf(...)
#endif /* SHIM_DEBUG */
enum win_types {
WINSHIM_MESSAGE = 1,
WINSHIM_MAP,
@@ -222,4 +231,122 @@ struct window_procs shim_procs = {
genl_can_suspend_yes,
};
#ifdef __EMSCRIPTEN__
/* convert the C callback to a JavaScript callback */
EM_JS(void, local_callback, (const char *cb_name, const char *shim_name, void *ret_ptr, const char *fmt_str, void *args), {
Asyncify.handleAsync(async () => {
// convert callback arguments to proper JavaScript varaidic arguments
let name = Module.UTF8ToString(shim_name);
let fmt = Module.UTF8ToString(fmt_str);
let cbName = Module.UTF8ToString(cb_name);
// console.log("local_callback:", cbName, fmt, name);
let argTypes = fmt.split("");
let retType = argTypes.shift();
// build array of JavaScript args from WASM parameters
let jsArgs = [];
for (let i = 0; i < argTypes.length; i++) {
let ptr = args + (4*i);
let val = typeLookup(argTypes[i], ptr);
jsArgs.push(val);
}
// do the callback
let userCallback = globalThis[cbName];
let retVal = await runJsLoop(() => userCallback(name, ... jsArgs));
// save the return value
setReturn(name, ret_ptr, retType, retVal);
// convert 'ptr' to the type indicated by 'type'
function typeLookup(type, ptr) {
switch(type) {
case "s": // string
return Module.UTF8ToString(Module.getValue(ptr, "*"));
case "p": // pointer
ptr = Module.getValue(ptr, "*");
if(!ptr) return 0; // null pointer
return Module.getValue(ptr, "*");
case "c": // char
return String.fromCharCode(Module.getValue(Module.getValue(ptr, "*"), "i8"));
case "0": /* 2^0 = 1 byte */
return Module.getValue(Module.getValue(ptr, "*"), "i8");
case "1": /* 2^1 = 2 bytes */
return Module.getValue(Module.getValue(ptr, "*"), "i16");
case "2": /* 2^2 = 4 bytes */
case "i": // integer
case "n": // number
return Module.getValue(Module.getValue(ptr, "*"), "i32");
case "f": // float
return Module.getValue(Module.getValue(ptr, "*"), "float");
case "d": // double
return Module.getValue(Module.getValue(ptr, "*"), "double");
default:
throw new TypeError ("unknown type:" + type);
}
}
// setTimeout() with value of '0' is similar to setImmediate() (which isn't standard)
// this lets the JS loop run for a tick so that other events can occur
// XXX: I also tried replacing the for(;;) in allmain.c:moveloop() with emscripten_set_main_loop()
// unfortunately that won't work -- if the simulate_infinite_loop arg is false, it falls through;
// if is true, it throws an exception to break out of main(), but doesn't get caught because
// the stack isn't running under main() anymore...
// I think this is suboptimal, but we will have to live with it
async function runJsLoop(cb) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(cb());
}, 0);
});
}
// sets the return value of the function to the type expected
function setReturn(name, ptr, type, value = 0) {
switch (type) {
case "p":
throw new Error("not implemented");
case "s":
if(typeof value !== "string")
throw new TypeError(`expected ${name} return type to be string`);
value=value?value:"(no value)";
var strPtr = Module.getValue(ptr, "i32");
Module.stringToUTF8(value, strPtr, 1024);
break;
case "i":
if(typeof value !== "number" || !Number.isInteger(value))
throw new TypeError(`expected ${name} return type to be integer`);
Module.setValue(ptr, value, "i32");
break;
case "c":
if(typeof value !== "number" || value < 0 || value > 128)
throw new TypeError(`expected ${name} return type to be integer representing an ASCII character`);
Module.setValue(ptr, value, "i8");
break;
case "f":
if(typeof value !== "number" || isFloat(value))
throw new TypeError(`expected ${name} return type to be float`);
// XXX: I'm not sure why 'double' works and 'float' doesn't
Module.setValue(ptr, value, "double");
break;
case "d":
if(typeof value !== "number" || isFloat(value))
throw new TypeError(`expected ${name} return type to be float`);
Module.setValue(ptr, value, "double");
break;
case "v":
break;
default:
throw new Error("unknown type");
}
function isFloat(n){
return n === +n && n !== (n|0) && !Number.isInteger(n);
}
}
});
})
#endif /* __EMSCRIPTEN__ */
#endif /* SHIM_GRAPHICS */