From 90b69bf47cb05404cf305cc7a385499dabf34402 Mon Sep 17 00:00:00 2001 From: Sami Vaarala Date: Fri, 27 May 2016 19:03:23 +0300 Subject: [PATCH] Add 1.x compatible module loader as extra --- extras/module-duktape/Makefile | 13 + extras/module-duktape/README.rst | 31 ++ extras/module-duktape/duk_module_duktape.c | 463 +++++++++++++++++++++ extras/module-duktape/duk_module_duktape.h | 14 + extras/module-duktape/test.c | 55 +++ 5 files changed, 576 insertions(+) create mode 100644 extras/module-duktape/Makefile create mode 100644 extras/module-duktape/README.rst create mode 100644 extras/module-duktape/duk_module_duktape.c create mode 100644 extras/module-duktape/duk_module_duktape.h create mode 100644 extras/module-duktape/test.c diff --git a/extras/module-duktape/Makefile b/extras/module-duktape/Makefile new file mode 100644 index 00000000..5d9518bd --- /dev/null +++ b/extras/module-duktape/Makefile @@ -0,0 +1,13 @@ +# For manual testing; say 'make' in extras/module-duktape and run ./test. +# There's test coverage in tests/ecmascript, so tests here are very simple. + +.PHONY: test +test: + gcc -std=c99 -Wall -Wextra -o $@ -I../../src/ -I. ../../src/duktape.c duk_module_duktape.c test.c -lm + @printf '\n' + ./test 'assert(typeof require === "function");' + ./test 'assert(require.name === "require");' + ./test 'assert(typeof Duktape.modLoaded === "object");' + ./test 'assert(typeof Duktape.modSearch === "undefined");' + ./test 'Duktape.modSearch = function myModSearch(id) { return "exports.foo = 123;" }; assert(require("dummy").foo === 123);' + ./test 'Duktape.modSearch = function myModSearch(id) { return "exports.foo = 234;" }; delete Duktape; assert(typeof Duktape === "undefined"); assert(require("dummy").foo === 234);' diff --git a/extras/module-duktape/README.rst b/extras/module-duktape/README.rst new file mode 100644 index 00000000..20a2305f --- /dev/null +++ b/extras/module-duktape/README.rst @@ -0,0 +1,31 @@ +=============================================== +Duktape 1.x compatible module loading framework +=============================================== + +The default built-in module loading framework was removed in Duktape 2.x +because more flexibility was needed for module loading. This directory +contains a Duktape 1.x compatible module loading framework which you can +add to your build: + +* Add ``duk_module_duktape.c`` to list of C sources to compile. + +* Ensure ``duk_module_duktape.h`` is in the include path. + +* Include the extra header in calling code and initialize the bindings:: + + #include "duktape.h" + #include "duk_module_duktape.h" + + /* After initializing the Duktape heap or when creating a new + * thread with a new global environment: + */ + duk_module_duktape_init(ctx); + + Don't call ``duk_module_duktape_init()`` more than once for the same global + environment. + +* As usual in Duktape 1.x, you should define ``Duktape.modSearch()`` to provide + environment specific module lookups. + +* After these steps, ``require()`` will be registered to the global object and + the module system is ready to use. diff --git a/extras/module-duktape/duk_module_duktape.c b/extras/module-duktape/duk_module_duktape.c new file mode 100644 index 00000000..1b1c1876 --- /dev/null +++ b/extras/module-duktape/duk_module_duktape.c @@ -0,0 +1,463 @@ +/* + * Duktape 1.x compatible module loading framework + */ + +#include "duktape.h" +#include "duk_module_duktape.h" + +#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: + duk_error(ctx, DUK_ERR_TYPE_ERROR, "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, "})"); + 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] */ + 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:{},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 diff --git a/extras/module-duktape/duk_module_duktape.h b/extras/module-duktape/duk_module_duktape.h new file mode 100644 index 00000000..8c880810 --- /dev/null +++ b/extras/module-duktape/duk_module_duktape.h @@ -0,0 +1,14 @@ +#if !defined(DUK_MODULE_DUKTAPE_H_INCLUDED) +#define DUK_MODULE_DUKTAPE_H_INCLUDED + +#include "duktape.h" + +/* 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); + +#endif /* DUK_MODULE_DUKTAPE_H_INCLUDED */ diff --git a/extras/module-duktape/test.c b/extras/module-duktape/test.c new file mode 100644 index 00000000..44b982d2 --- /dev/null +++ b/extras/module-duktape/test.c @@ -0,0 +1,55 @@ +#include +#include +#include "duktape.h" +#include "duk_module_duktape.h" + +static duk_ret_t handle_print(duk_context *ctx) { + printf("%s\n", duk_safe_to_string(ctx, 0)); + return 0; +} + +static duk_ret_t handle_assert(duk_context *ctx) { + if (duk_to_boolean(ctx, 0)) { + return 0; + } + duk_error(ctx, DUK_ERR_ERROR, "assertion failed: %s", duk_safe_to_string(ctx, 1)); + return 0; +} + + +int main(int argc, char *argv[]) { + duk_context *ctx; + int i; + int exitcode = 0; + + ctx = duk_create_heap_default(); + if (!ctx) { + return 1; + } + + duk_push_c_function(ctx, handle_print, 1); + duk_put_global_string(ctx, "print"); + duk_push_c_function(ctx, handle_assert, 2); + duk_put_global_string(ctx, "assert"); + + duk_module_duktape_init(ctx); + printf("top after init: %ld\n", (long) duk_get_top(ctx)); + + for (i = 1; i < argc; i++) { + printf("Evaling: %s\n", argv[i]); + if (duk_peval_string(ctx, argv[i]) != 0) { + if (duk_get_prop_string(ctx, -1, "stack")) { + duk_replace(ctx, -2); + } else { + duk_pop(ctx); + } + exitcode = 1; + } + printf("--> %s\n", duk_safe_to_string(ctx, -1)); + duk_pop(ctx); + } + + printf("Done\n"); + duk_destroy_heap(ctx); + return exitcode; +}