]> granicus.if.org Git - nethack/commitdiff
fix JS event loop blocking
authorAdam Powers <apowers@ato.ms>
Mon, 7 Sep 2020 00:32:08 +0000 (17:32 -0700)
committerAdam Powers <apowers@ato.ms>
Mon, 7 Sep 2020 00:32:08 +0000 (17:32 -0700)
sys/lib/README.md
sys/lib/hints/wasm
sys/lib/npm-package/README.md
sys/lib/npm-package/src/nethackShim.js
sys/lib/test/libtest.c
win/shim/winshim.c

index cbbd87b5152fd955370f679de5d3ab39e0eb4cb9..3bc7c283aef94023415f8b1d473e3ab2c24a47bb 100644 (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,
-
-    // after loading the library, set the callback function
-    onRuntimeInitialized: function (... args) {
-        setGraphicsCallback();
-    }
-};
-
-var factory = require("./src/nethack.js");
-
-// 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 */
-}
-
-// 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);
+const path = require("path");
+
+// starts nethack
+function nethackStart(cb, inputModule = {}) {
+    // set callback
+    let cbName = cb.name;
+    if (cbName === "") cbName = "__anonymousNetHackCallback";
+    let userCallback = globalThis[cbName] = cb;
+
+    // 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
+        );
+    };
+
+    // load and run the module
+    var factory = require(path.join(__dirname, "../build/nethack.js"));
+    factory(Module);
 }
 
-// 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);
 ```
\ No newline at end of file
index 711017baeaead062c82e92a548113a7a5b48b58f..7f85090aff686a6596aaf13d0b0670de53c0eefb 100644 (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
index e71f3e5a124f1b60f262283d574e3943326fe7f2..e39db8292328b4b495a3fde42964537d73d7d56d 100644 (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) {
index 63846027801fe013b6f71e5b27b1b0e5964ae46b..e7a802c304cfe6a68cf6eb3454676487520ba88e 100644 (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;
 
index 9b554b986df02dc378e7a9d9f0adf45bc7c23777..31ca10e713c8086c9f15b7b5a54f42d2e9aadf1a 100644 (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) {
index 05ed09c073dd96a5b672ec7918932485b4426974..eec616f4fa754adda788f14ede1f3d7d47456cbf 100644 (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>
 
 #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 */
\ No newline at end of file