Mercurial > molko
changeset 198:d6f217a5e4b1
molko-js: add support for modules, closes #2514 @1h
author | David Demelier <markand@malikania.fr> |
---|---|
date | Mon, 09 Nov 2020 13:24:49 +0100 |
parents | 852d0b7817ce |
children | ebbcfb31482d |
files | doc/api-js.rst extern/libduktape/CMakeLists.txt extern/libduktape/duk_module.c extern/libduktape/duk_module.h libcore/core/util.c libcore/core/util.h molko-js/src/js.c |
diffstat | 7 files changed, 681 insertions(+), 62 deletions(-) [+] |
line wrap: on
line diff
--- a/doc/api-js.rst Mon Nov 09 10:37:36 2020 +0100 +++ b/doc/api-js.rst Mon Nov 09 13:24:49 2020 +0100 @@ -1,3 +1,10 @@ +.. toctree:: + :hidden: + + api-js-starter + +.. highlight:: js + ============== Javascript API ============== @@ -6,12 +13,62 @@ convenient, quicker and more flexible way. By using the Javascript API you may deploy your game without having to compile the project for every platform. -See the following topics to learn how to write your very first game. + +Conformance +----------- + +The Javascript engine is powered by Duktape_, it supports ECMAScript 5.1 with +some extensions of newer specifications. See the official `features tables`_ for +more information. + +Modules support +--------------- + +Support of CommonJS modules is available with some limitations. It only loads +Javascript files and the root path is taken from the initial script startup +file. + +In other terms, given the following hierarchy:: + + -- mygame/ + -- main.js + -- actions/ + -- chest.js + -- util.js -.. toctree:: - :hidden: +In any file of the given hierarchy, calling ``require("foo")`` will use the +parent *mygame* directory as origin. Otherwise, if a call to ``require`` starts +with *./* then it is relative to the current file being loaded. As such, in +*chest.js* you would typically use ``require("./util")`` to load *util.js* as +it is lives in the same directory as *chest.js*. + +On the other hand, in the *main.js* file you may invoke +``require("./actions/chest")`` or ``require("actions/util");`` as both will work +in that case. + +.. note:: Do not append *.js* extension in any require call, it is added + automatically from the code. + +Example of usage *main.js* + +:: - api-js-starter + var chest = require("./actions/chest"); + chest.create(); + +And in *actions/chest.js* + +:: + + var util = require("./util"); + + exports.create = function () { + print("Super cool exported function"); + }; + +.. tip:: We recommend that you use local requires using *./* for files that are + in the same directory or in a descendant directory and use "absolute" + paths when referring to a sibling or ancestor directory. Javascript modules ------------------ @@ -30,12 +87,5 @@ api-molko-util api-molko-window -Conformance ------------ - -The Javascript engine is powered by Duktape_, it supports ECMAScript 5.1 with -some extensions of newer specifications. See the official `features tables`_ for -more information. - .. _Duktape: http://duktape.org .. _features tables: https://wiki.duktape.org/postes5features
--- a/extern/libduktape/CMakeLists.txt Mon Nov 09 10:37:36 2020 +0100 +++ b/extern/libduktape/CMakeLists.txt Mon Nov 09 13:24:49 2020 +0100 @@ -23,6 +23,8 @@ FOLDER extern SOURCES ${libduktape_SOURCE_DIR}/duk_config.h + ${libduktape_SOURCE_DIR}/duk_module.c + ${libduktape_SOURCE_DIR}/duk_module.h ${libduktape_SOURCE_DIR}/duktape.c ${libduktape_SOURCE_DIR}/duktape.h PUBLIC_INCLUDES
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extern/libduktape/duk_module.c Mon Nov 09 13:24:49 2020 +0100 @@ -0,0 +1,471 @@ +/* + * Duktape 1.x compatible module loading framework + */ + +#include "duktape.h" +#include "duk_module.h" + +/* (v)snprintf() is missing before MSVC 2015. Note that _(v)snprintf() does + * NOT NUL terminate on truncation, but that's OK here. + * http://stackoverflow.com/questions/2915672/snprintf-and-visual-studio-2010 + */ +#if defined(_MSC_VER) && (_MSC_VER < 1900) +#define snprintf _snprintf +#endif + +#if 0 /* Enable manually */ +#define DUK__ASSERT(x) do { \ + if (!(x)) { \ + fprintf(stderr, "ASSERTION FAILED at %s:%d: " #x "\n", __FILE__, __LINE__); \ + fflush(stderr); \ + } \ + } while (0) +#define DUK__ASSERT_TOP(ctx,val) do { \ + DUK__ASSERT(duk_get_top((ctx)) == (val)); \ + } while (0) +#else +#define DUK__ASSERT(x) do { (void) (x); } while (0) +#define DUK__ASSERT_TOP(ctx,val) do { (void) ctx; (void) (val); } while (0) +#endif + +static void duk__resolve_module_id(duk_context *ctx, const char *req_id, const char *mod_id) { + duk_uint8_t buf[DUK_COMMONJS_MODULE_ID_LIMIT]; + duk_uint8_t *p; + duk_uint8_t *q; + duk_uint8_t *q_last; /* last component */ + duk_int_t int_rc; + + DUK__ASSERT(req_id != NULL); + /* mod_id may be NULL */ + + /* + * A few notes on the algorithm: + * + * - Terms are not allowed to begin with a period unless the term + * is either '.' or '..'. This simplifies implementation (and + * is within CommonJS modules specification). + * + * - There are few output bound checks here. This is on purpose: + * the resolution input is length checked and the output is never + * longer than the input. The resolved output is written directly + * over the input because it's never longer than the input at any + * point in the algorithm. + * + * - Non-ASCII characters are processed as individual bytes and + * need no special treatment. However, U+0000 terminates the + * algorithm; this is not an issue because U+0000 is not a + * desirable term character anyway. + */ + + /* + * Set up the resolution input which is the requested ID directly + * (if absolute or no current module path) or with current module + * ID prepended (if relative and current module path exists). + * + * Suppose current module is 'foo/bar' and relative path is './quux'. + * The 'bar' component must be replaced so the initial input here is + * 'foo/bar/.././quux'. + */ + + if (mod_id != NULL && req_id[0] == '.') { + int_rc = snprintf((char *) buf, sizeof(buf), "%s/../%s", mod_id, req_id); + } else { + int_rc = snprintf((char *) buf, sizeof(buf), "%s", req_id); + } + if (int_rc >= (duk_int_t) sizeof(buf) || int_rc < 0) { + /* Potentially truncated, NUL not guaranteed in any case. + * The (int_rc < 0) case should not occur in practice. + */ + goto resolve_error; + } + DUK__ASSERT(strlen((const char *) buf) < sizeof(buf)); /* at most sizeof(buf) - 1 */ + + /* + * Resolution loop. At the top of the loop we're expecting a valid + * term: '.', '..', or a non-empty identifier not starting with a period. + */ + + p = buf; + q = buf; + for (;;) { + duk_uint_fast8_t c; + + /* Here 'p' always points to the start of a term. + * + * We can also unconditionally reset q_last here: if this is + * the last (non-empty) term q_last will have the right value + * on loop exit. + */ + + DUK__ASSERT(p >= q); /* output is never longer than input during resolution */ + + q_last = q; + + c = *p++; + if (c == 0) { + goto resolve_error; + } else if (c == '.') { + c = *p++; + if (c == '/') { + /* Term was '.' and is eaten entirely (including dup slashes). */ + goto eat_dup_slashes; + } + if (c == '.' && *p == '/') { + /* Term was '..', backtrack resolved name by one component. + * q[-1] = previous slash (or beyond start of buffer) + * q[-2] = last char of previous component (or beyond start of buffer) + */ + p++; /* eat (first) input slash */ + DUK__ASSERT(q >= buf); + if (q == buf) { + goto resolve_error; + } + DUK__ASSERT(*(q - 1) == '/'); + q--; /* Backtrack to last output slash (dups already eliminated). */ + for (;;) { + /* Backtrack to previous slash or start of buffer. */ + DUK__ASSERT(q >= buf); + if (q == buf) { + break; + } + if (*(q - 1) == '/') { + break; + } + q--; + } + goto eat_dup_slashes; + } + goto resolve_error; + } else if (c == '/') { + /* e.g. require('/foo'), empty terms not allowed */ + goto resolve_error; + } else { + for (;;) { + /* Copy term name until end or '/'. */ + *q++ = c; + c = *p++; + if (c == 0) { + /* This was the last term, and q_last was + * updated to match this term at loop top. + */ + goto loop_done; + } else if (c == '/') { + *q++ = '/'; + break; + } else { + /* write on next loop */ + } + } + } + + eat_dup_slashes: + for (;;) { + /* eat dup slashes */ + c = *p; + if (c != '/') { + break; + } + p++; + } + } + loop_done: + /* Output #1: resolved absolute name. */ + DUK__ASSERT(q >= buf); + duk_push_lstring(ctx, (const char *) buf, (size_t) (q - buf)); + + /* Output #2: last component name. */ + DUK__ASSERT(q >= q_last); + DUK__ASSERT(q_last >= buf); + duk_push_lstring(ctx, (const char *) q_last, (size_t) (q - q_last)); + return; + + resolve_error: + (void) duk_type_error(ctx, "cannot resolve module id: %s", (const char *) req_id); +} + +/* Stack indices for better readability. */ +#define DUK__IDX_REQUESTED_ID 0 /* module id requested */ +#define DUK__IDX_REQUIRE 1 /* current require() function */ +#define DUK__IDX_REQUIRE_ID 2 /* the base ID of the current require() function, resolution base */ +#define DUK__IDX_RESOLVED_ID 3 /* resolved, normalized absolute module ID */ +#define DUK__IDX_LASTCOMP 4 /* last component name in resolved path */ +#define DUK__IDX_DUKTAPE 5 /* Duktape object */ +#define DUK__IDX_MODLOADED 6 /* Duktape.modLoaded[] module cache */ +#define DUK__IDX_UNDEFINED 7 /* 'undefined', artifact of lookup */ +#define DUK__IDX_FRESH_REQUIRE 8 /* new require() function for module, updated resolution base */ +#define DUK__IDX_EXPORTS 9 /* default exports table */ +#define DUK__IDX_MODULE 10 /* module object containing module.exports, etc */ + +static duk_ret_t duk__require(duk_context *ctx) { + const char *str_req_id; /* requested identifier */ + const char *str_mod_id; /* require.id of current module */ + duk_int_t pcall_rc; + + /* NOTE: we try to minimize code size by avoiding unnecessary pops, + * so the stack looks a bit cluttered in this function. DUK__ASSERT_TOP() + * assertions are used to ensure stack configuration is correct at each + * step. + */ + + /* + * Resolve module identifier into canonical absolute form. + */ + + str_req_id = duk_require_string(ctx, DUK__IDX_REQUESTED_ID); + duk_push_current_function(ctx); + duk_get_prop_string(ctx, -1, "id"); + str_mod_id = duk_get_string(ctx, DUK__IDX_REQUIRE_ID); /* ignore non-strings */ + duk__resolve_module_id(ctx, str_req_id, str_mod_id); + str_req_id = NULL; + str_mod_id = NULL; + + /* [ requested_id require require.id resolved_id last_comp ] */ + DUK__ASSERT_TOP(ctx, DUK__IDX_LASTCOMP + 1); + + /* + * Cached module check. + * + * If module has been loaded or its loading has already begun without + * finishing, return the same cached value (module.exports). The + * value is registered when module load starts so that circular + * references can be supported to some extent. + */ + + duk_push_global_stash(ctx); + duk_get_prop_string(ctx, -1, "\xff" "module:Duktape"); + duk_remove(ctx, -2); /* Lookup stashed, original 'Duktape' object. */ + duk_get_prop_string(ctx, DUK__IDX_DUKTAPE, "modLoaded"); /* Duktape.modLoaded */ + duk_require_type_mask(ctx, DUK__IDX_MODLOADED, DUK_TYPE_MASK_OBJECT); + DUK__ASSERT_TOP(ctx, DUK__IDX_MODLOADED + 1); + + duk_dup(ctx, DUK__IDX_RESOLVED_ID); + if (duk_get_prop(ctx, DUK__IDX_MODLOADED)) { + /* [ requested_id require require.id resolved_id last_comp Duktape Duktape.modLoaded Duktape.modLoaded[id] ] */ + duk_get_prop_string(ctx, -1, "exports"); /* return module.exports */ + return 1; + } + DUK__ASSERT_TOP(ctx, DUK__IDX_UNDEFINED + 1); + + /* [ requested_id require require.id resolved_id last_comp Duktape Duktape.modLoaded undefined ] */ + + /* + * Module not loaded (and loading not started previously). + * + * Create a new require() function with 'id' set to resolved ID + * of module being loaded. Also create 'exports' and 'module' + * tables but don't register exports to the loaded table yet. + * We don't want to do that unless the user module search callbacks + * succeeds in finding the module. + */ + + /* Fresh require: require.id is left configurable (but not writable) + * so that is not easy to accidentally tweak it, but it can still be + * done with Object.defineProperty(). + * + * XXX: require.id could also be just made non-configurable, as there + * is no practical reason to touch it (at least from ECMAScript code). + */ + duk_push_c_function(ctx, duk__require, 1 /*nargs*/); + duk_push_string(ctx, "name"); + duk_push_string(ctx, "require"); + duk_def_prop(ctx, DUK__IDX_FRESH_REQUIRE, DUK_DEFPROP_HAVE_VALUE); /* not writable, not enumerable, not configurable */ + duk_push_string(ctx, "id"); + duk_dup(ctx, DUK__IDX_RESOLVED_ID); + duk_def_prop(ctx, DUK__IDX_FRESH_REQUIRE, DUK_DEFPROP_HAVE_VALUE | DUK_DEFPROP_SET_CONFIGURABLE); /* a fresh require() with require.id = resolved target module id */ + + /* Module table: + * - module.exports: initial exports table (may be replaced by user) + * - module.id is non-writable and non-configurable, as the CommonJS + * spec suggests this if possible + * - module.filename: not set, defaults to resolved ID if not explicitly + * set by modSearch() (note capitalization, not .fileName, matches Node.js) + * - module.name: not set, defaults to last component of resolved ID if + * not explicitly set by modSearch() + */ + duk_push_object(ctx); /* exports */ + duk_push_object(ctx); /* module */ + duk_push_string(ctx, "exports"); + duk_dup(ctx, DUK__IDX_EXPORTS); + duk_def_prop(ctx, DUK__IDX_MODULE, DUK_DEFPROP_HAVE_VALUE | DUK_DEFPROP_SET_WRITABLE | DUK_DEFPROP_SET_CONFIGURABLE); /* module.exports = exports */ + duk_push_string(ctx, "id"); + duk_dup(ctx, DUK__IDX_RESOLVED_ID); /* resolved id: require(id) must return this same module */ + duk_def_prop(ctx, DUK__IDX_MODULE, DUK_DEFPROP_HAVE_VALUE); /* module.id = resolved_id; not writable, not enumerable, not configurable */ + duk_compact(ctx, DUK__IDX_MODULE); /* module table remains registered to modLoaded, minimize its size */ + DUK__ASSERT_TOP(ctx, DUK__IDX_MODULE + 1); + + /* [ requested_id require require.id resolved_id last_comp Duktape Duktape.modLoaded undefined fresh_require exports module ] */ + + /* Register the module table early to modLoaded[] so that we can + * support circular references even in modSearch(). If an error + * is thrown, we'll delete the reference. + */ + duk_dup(ctx, DUK__IDX_RESOLVED_ID); + duk_dup(ctx, DUK__IDX_MODULE); + duk_put_prop(ctx, DUK__IDX_MODLOADED); /* Duktape.modLoaded[resolved_id] = module */ + + /* + * Call user provided module search function and build the wrapped + * module source code (if necessary). The module search function + * can be used to implement pure Ecmacsript, pure C, and mixed + * ECMAScript/C modules. + * + * The module search function can operate on the exports table directly + * (e.g. DLL code can register values to it). It can also return a + * string which is interpreted as module source code (if a non-string + * is returned the module is assumed to be a pure C one). If a module + * cannot be found, an error must be thrown by the user callback. + * + * Because Duktape.modLoaded[] already contains the module being + * loaded, circular references for C modules should also work + * (although expected to be quite rare). + */ + + duk_push_string(ctx, "(function(require,exports,module){"); + + /* Duktape.modSearch(resolved_id, fresh_require, exports, module). */ + duk_get_prop_string(ctx, DUK__IDX_DUKTAPE, "modSearch"); /* Duktape.modSearch */ + duk_dup(ctx, DUK__IDX_RESOLVED_ID); + duk_dup(ctx, DUK__IDX_FRESH_REQUIRE); + duk_dup(ctx, DUK__IDX_EXPORTS); + duk_dup(ctx, DUK__IDX_MODULE); /* [ ... Duktape.modSearch resolved_id last_comp fresh_require exports module ] */ + pcall_rc = duk_pcall(ctx, 4 /*nargs*/); /* -> [ ... source ] */ + DUK__ASSERT_TOP(ctx, DUK__IDX_MODULE + 3); + + if (pcall_rc != DUK_EXEC_SUCCESS) { + /* Delete entry in Duktape.modLoaded[] and rethrow. */ + goto delete_rethrow; + } + + /* If user callback did not return source code, module loading + * is finished (user callback initialized exports table directly). + */ + if (!duk_is_string(ctx, -1)) { + /* User callback did not return source code, so module loading + * is finished: just update modLoaded with final module.exports + * and we're done. + */ + goto return_exports; + } + + /* Finish the wrapped module source. Force module.filename as the + * function .fileName so it gets set for functions defined within a + * module. This also ensures loggers created within the module get + * the module ID (or overridden filename) as their default logger name. + * (Note capitalization: .filename matches Node.js while .fileName is + * used elsewhere in Duktape.) + */ + duk_push_string(ctx, "\n})"); /* Newline allows module last line to contain a // comment. */ + duk_concat(ctx, 3); + if (!duk_get_prop_string(ctx, DUK__IDX_MODULE, "filename")) { + /* module.filename for .fileName, default to resolved ID if + * not present. + */ + duk_pop(ctx); + duk_dup(ctx, DUK__IDX_RESOLVED_ID); + } + pcall_rc = duk_pcompile(ctx, DUK_COMPILE_EVAL); + if (pcall_rc != DUK_EXEC_SUCCESS) { + goto delete_rethrow; + } + pcall_rc = duk_pcall(ctx, 0); /* -> eval'd function wrapper (not called yet) */ + if (pcall_rc != DUK_EXEC_SUCCESS) { + goto delete_rethrow; + } + + /* Module has now evaluated to a wrapped module function. Force its + * .name to match module.name (defaults to last component of resolved + * ID) so that it is shown in stack traces too. Note that we must not + * introduce an actual name binding into the function scope (which is + * usually the case with a named function) because it would affect the + * scope seen by the module and shadow accesses to globals of the same name. + * This is now done by compiling the function as anonymous and then forcing + * its .name without setting a "has name binding" flag. + */ + + duk_push_string(ctx, "name"); + if (!duk_get_prop_string(ctx, DUK__IDX_MODULE, "name")) { + /* module.name for .name, default to last component if + * not present. + */ + duk_pop(ctx); + duk_dup(ctx, DUK__IDX_LASTCOMP); + } + duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_VALUE | DUK_DEFPROP_FORCE); + + /* + * Call the wrapped module function. + * + * Use a protected call so that we can update Duktape.modLoaded[resolved_id] + * even if the module throws an error. + */ + + /* [ requested_id require require.id resolved_id last_comp Duktape Duktape.modLoaded undefined fresh_require exports module mod_func ] */ + DUK__ASSERT_TOP(ctx, DUK__IDX_MODULE + 2); + + duk_dup(ctx, DUK__IDX_EXPORTS); /* exports (this binding) */ + duk_dup(ctx, DUK__IDX_FRESH_REQUIRE); /* fresh require (argument) */ + duk_get_prop_string(ctx, DUK__IDX_MODULE, "exports"); /* relookup exports from module.exports in case it was changed by modSearch */ + duk_dup(ctx, DUK__IDX_MODULE); /* module (argument) */ + DUK__ASSERT_TOP(ctx, DUK__IDX_MODULE + 6); + + /* [ requested_id require require.id resolved_id last_comp Duktape Duktape.modLoaded undefined fresh_require exports module mod_func exports fresh_require exports module ] */ + + pcall_rc = duk_pcall_method(ctx, 3 /*nargs*/); + if (pcall_rc != DUK_EXEC_SUCCESS) { + /* Module loading failed. Node.js will forget the module + * registration so that another require() will try to load + * the module again. Mimic that behavior. + */ + goto delete_rethrow; + } + + /* [ requested_id require require.id resolved_id last_comp Duktape Duktape.modLoaded undefined fresh_require exports module result(ignored) ] */ + DUK__ASSERT_TOP(ctx, DUK__IDX_MODULE + 2); + + /* fall through */ + + return_exports: + duk_get_prop_string(ctx, DUK__IDX_MODULE, "exports"); + duk_compact(ctx, -1); /* compact the exports table */ + return 1; /* return module.exports */ + + delete_rethrow: + duk_dup(ctx, DUK__IDX_RESOLVED_ID); + duk_del_prop(ctx, DUK__IDX_MODLOADED); /* delete Duktape.modLoaded[resolved_id] */ + (void) duk_throw(ctx); /* rethrow original error */ + return 0; /* not reachable */ +} + +void duk_module_duktape_init(duk_context *ctx) { + /* Stash 'Duktape' in case it's modified. */ + duk_push_global_stash(ctx); + duk_get_global_string(ctx, "Duktape"); + duk_put_prop_string(ctx, -2, "\xff" "module:Duktape"); + duk_pop(ctx); + + /* Register `require` as a global function. */ + duk_eval_string(ctx, + "(function(req){" + "var D=Object.defineProperty;" + "D(req,'name',{value:'require'});" + "D(this,'require',{value:req,writable:true,configurable:true});" + "D(Duktape,'modLoaded',{value:Object.create(null),writable:true,configurable:true});" + "})"); + duk_push_c_function(ctx, duk__require, 1 /*nargs*/); + duk_call(ctx, 1); + duk_pop(ctx); +} + +#undef DUK__ASSERT +#undef DUK__ASSERT_TOP +#undef DUK__IDX_REQUESTED_ID +#undef DUK__IDX_REQUIRE +#undef DUK__IDX_REQUIRE_ID +#undef DUK__IDX_RESOLVED_ID +#undef DUK__IDX_LASTCOMP +#undef DUK__IDX_DUKTAPE +#undef DUK__IDX_MODLOADED +#undef DUK__IDX_UNDEFINED +#undef DUK__IDX_FRESH_REQUIRE +#undef DUK__IDX_EXPORTS +#undef DUK__IDX_MODULE
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extern/libduktape/duk_module.h Mon Nov 09 13:24:49 2020 +0100 @@ -0,0 +1,22 @@ +#if !defined(DUK_MODULE_DUKTAPE_H_INCLUDED) +#define DUK_MODULE_DUKTAPE_H_INCLUDED + +#include "duktape.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +/* Maximum length of CommonJS module identifier to resolve. Length includes + * both current module ID, requested (possibly relative) module ID, and a + * slash in between. + */ +#define DUK_COMMONJS_MODULE_ID_LIMIT 256 + +extern void duk_module_duktape_init(duk_context *ctx); + +#if defined(__cplusplus) +} +#endif /* end 'extern "C"' wrapper */ + +#endif /* DUK_MODULE_DUKTAPE_H_INCLUDED */
--- a/libcore/core/util.c Mon Nov 09 10:37:36 2020 +0100 +++ b/libcore/core/util.c Mon Nov 09 13:24:49 2020 +0100 @@ -16,11 +16,17 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include <sys/stat.h> #include <assert.h> +#include <errno.h> +#include <fcntl.h> #include <stdlib.h> +#include <unistd.h> #include <SDL.h> +#include "alloc.h" +#include "error.h" #include "util.h" void @@ -29,6 +35,33 @@ SDL_Delay(ms); } +char * +readall(const char *path) +{ + int fd; + struct stat st; + char *str = NULL; + ssize_t nr; + + if ((fd = open(path, O_RDONLY)) < 0 || fstat(fd, &st) < 0) + goto io_error; + if (!(str = alloc_zero(1, st.st_size + 1))) + goto alloc_error; + if ((nr = read(fd, str, st.st_size)) != st.st_size) + goto io_error; + + return str; + +io_error: + errorf("%s", strerror(errno)); + +alloc_error: + close(fd); + free(str); + + return NULL; +} + unsigned int nrand(unsigned int lower, unsigned int upper) {
--- a/libcore/core/util.h Mon Nov 09 10:37:36 2020 +0100 +++ b/libcore/core/util.h Mon Nov 09 13:24:49 2020 +0100 @@ -52,6 +52,18 @@ delay(unsigned int ms); /** + * Read a whole file dynamically. + * + * \pre path != NULL + * \param path the path to the file + * \return A pointer to a dynamically allocated string containing the file + * contents or NULL on failures. + * \note Don't forget to free the result. + */ +char * +readall(const char *path) PLAT_NODISCARD; + +/** * Generate a random number between lower and upper (included). * * \pre upper must be <= RAND_MAX
--- a/molko-js/src/js.c Mon Nov 09 10:37:36 2020 +0100 +++ b/molko-js/src/js.c Mon Nov 09 13:24:49 2020 +0100 @@ -16,12 +16,10 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -#include <sys/stat.h> #include <assert.h> -#include <errno.h> -#include <fcntl.h> +#include <libgen.h> +#include <limits.h> #include <string.h> -#include <unistd.h> #include <core/alloc.h> #include <core/error.h> @@ -29,6 +27,7 @@ #include <core/trace.h> #include <duktape.h> +#include <duk_module.h> #include "js.h" #include "js-clock.h" @@ -88,6 +87,76 @@ panicf("%s", msg); } +static duk_ret_t +search(duk_context *ctx) +{ + char path[PATH_MAX]; + char *ret; + + duk_push_global_stash(ctx); + duk_get_prop_string(ctx, -1, DUK_HIDDEN_SYMBOL("molko::base")); + snprintf(path, sizeof (path), "%s/%s.js", duk_to_string(ctx, -1), duk_require_string(ctx, 0)); + duk_pop_n(ctx, 2); + + tracef("opening module: %s", path); + + if (!(ret = readall(path))) + duk_error(ctx, DUK_ERR_ERROR, "%s", error()); + + duk_push_string(ctx, ret); + free(ret); + + return 1; +} + +static void +setup_module(struct js *js) +{ + duk_module_duktape_init(js->handle); + duk_get_global_string(js->handle, "Duktape"); + duk_push_c_function(js->handle, search, 4); + duk_put_prop_string(js->handle, -2, "modSearch"); + duk_pop(js->handle); +} + +static void +setup_global(struct js *js) +{ + /* Create global "Molko" property. */ + duk_push_global_object(js->handle); + duk_push_object(js->handle); + duk_put_prop_string(js->handle, -2, "Molko"); + duk_push_c_function(js->handle, js_print, 1); + duk_put_prop_string(js->handle, -2, "print"); + duk_push_c_function(js->handle, js_trace, 1); + duk_put_prop_string(js->handle, -2, "trace"); + duk_pop(js->handle); +} + +static void +setup_properties(struct js *js) +{ + /* Store a reference to this pointer. */ + duk_push_global_stash(js->handle); + duk_push_pointer(js->handle, js); + duk_put_prop_string(js->handle, -2, DUK_HIDDEN_SYMBOL("js.pointer")); + duk_pop(js->handle); +} + +static void +setup_base(struct js *js, const char *path) +{ + char base[PATH_MAX]; + + snprintf(base, sizeof (base), "%s", path); + snprintf(base, sizeof (base), "%s", dirname(base)); + + duk_push_global_stash(js->handle); + duk_push_string(js->handle, base); + duk_put_prop_string(js->handle, -2, DUK_HIDDEN_SYMBOL("molko::base")); + duk_pop(js->handle); +} + bool js_init(struct js *js) { @@ -99,56 +168,14 @@ if (!js->handle) panicf("could not create Javascript context"); - /* Create global "Molko" property. */ - duk_push_global_object(js->handle); - duk_push_object(js->handle); - duk_put_prop_string(js->handle, -2, "Molko"); - duk_push_c_function(js->handle, js_print, 1); - duk_put_prop_string(js->handle, -2, "print"); - duk_push_c_function(js->handle, js_trace, 1); - duk_put_prop_string(js->handle, -2, "trace"); - duk_pop(js->handle); - - /* Store a reference to this pointer. */ - duk_push_global_stash(js->handle); - duk_push_pointer(js->handle, js); - duk_put_prop_string(js->handle, -2, DUK_HIDDEN_SYMBOL("js.pointer")); - duk_pop(js->handle); - + /* Setup module. */ + setup_module(js); + setup_global(js); + setup_properties(js); + return true; } -static char * -readall(const char *path) -{ - int fd = -1; - struct stat st; - char *str = NULL; - ssize_t nr; - - if ((fd = open(path, O_RDONLY)) < 0) - goto failure; - if (fstat(fd, &st) < 0) - goto failure; - - if (!(str = alloc_zero(1, st.st_size + 1))) - goto failure; - if ((nr = read(fd, str, st.st_size)) != st.st_size) - goto failure; - - return str; - -failure: - errorf("%s", strerror(errno)); - - if (fd != -1) - close(fd); - - free(str); - - return NULL; -} - void js_core_load(struct js *js) { @@ -176,6 +203,8 @@ if (!text) return false; + setup_base(js, path); + if (duk_peval_string(js->handle, text) != 0) { int ln;