mirror of https://github.com/svaarala/duktape.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
413 lines
15 KiB
413 lines
15 KiB
=======
|
|
Modules
|
|
=======
|
|
|
|
Introduction
|
|
============
|
|
|
|
This document discusses the barebone CommonJS-based module framework
|
|
built into Duktape:
|
|
|
|
* Ecmascript modules are defined using the CommonJS module format:
|
|
|
|
- http://wiki.commonjs.org/wiki/Modules/1.1.1
|
|
|
|
* The user must provide a *module search function* which locates a module
|
|
corresponding to a resolved module ID, and can register module symbols
|
|
directly and/or return module source code as a string. To remain portable
|
|
and unaware of file systems and such, Duktape does not provide a default
|
|
module search function.
|
|
|
|
* C modules, static or DLL-based, can be implemented on top of the module
|
|
search function by user code. There is no built-in C module support in
|
|
the main Duktape library to avoid portability issues for exotic platforms.
|
|
However, there is a recommended convention which works on most platforms
|
|
and allows both static and DLL loading, see ``c-module-convention.rst``.
|
|
|
|
Using modules from Ecmascript code (require)
|
|
============================================
|
|
|
|
Duktape provides a global ``require()`` function which allows a module to be
|
|
loaded based on a module identifier::
|
|
|
|
var lib = require('package/lib')
|
|
|
|
The module identifier may contain forward slashes to provide some hierarchy.
|
|
There are both absolute and relative identifiers::
|
|
|
|
// absolute identifier, resolves to 'a/b' everywhere
|
|
var mod = require('a/b');
|
|
|
|
// relative identifier, resolves to 'package/c' inside package/lib
|
|
var mod = require('./c');
|
|
|
|
// relative identifier, resolves to 'foo/xyz' inside foo/bar/quux
|
|
var mod = require('../xyz');
|
|
|
|
Relative paths are resolved starting from the absolute root of the current
|
|
module, i.e. the module calling ``require()``, see separate section on the
|
|
resolution algorithm.
|
|
|
|
A module with a certain resolved identifier is loaded only once per a global
|
|
environment: ``Duktape.modLoaded`` keeps track of modules which are fully or
|
|
partially loaded, mapping a resolved identifier to the module's ``exports``
|
|
table. If a required module has already been (fully or partially) loaded,
|
|
the same ``exports`` value is returned without further action.
|
|
|
|
If a module has not yet been registered to ``Duktape.modLoaded``, Duktape
|
|
calls ``Duktape.modSearch()``, a module search function which must be
|
|
provided by the user (there is no default)::
|
|
|
|
Duktape.modSearch = function (id, require, exports, module) {
|
|
// ...
|
|
};
|
|
|
|
The identifier given to the ``modSearch()`` function is a fully resolved,
|
|
absolute identifier.
|
|
|
|
If the search function cannot locate a module based on its identifier, it is
|
|
expected to throw an error. If a module is found, the search function can
|
|
register symbols directly to 'exports' (this is used to implement C modules),
|
|
and can also return a string to be used as the module Ecmascript source code::
|
|
|
|
/* An actual implementation would usually scan and return module sources
|
|
* e.g. from the filesystem or a compressed source pack. This example
|
|
* illustrates the return value options.
|
|
*/
|
|
|
|
Duktape.modSearch = function (id, require, exports, module) {
|
|
if (id === 'foo') {
|
|
/* 'foo' is a native module, register symbols to 'exports' in a
|
|
* platform specific way.
|
|
*/
|
|
return; // undefined: no Ecmascript source
|
|
}
|
|
if (id === 'bar') {
|
|
/* 'bar' is a pure Ecmascript module. */
|
|
return 'exports.hello = function () { print("Hello world from bar!"); };';
|
|
}
|
|
if (id === 'quux') {
|
|
/* 'quux' is a mixed C/Ecmascript module. C code provides the
|
|
* exports.rawFunc() binding while Ecmascript code implements
|
|
* a safe variant on top of that.
|
|
*/
|
|
return 'exports.func = function () {\n' +
|
|
' try { exports.rawFunc(); } catch (e) { print(e); }\n' +
|
|
'};\n';
|
|
}
|
|
|
|
/* If a module is not found, an error must be thrown. */
|
|
throw new Error('cannot find module: ' + id);
|
|
};
|
|
|
|
If the search function returns without throwing an error, the exports value
|
|
is registered to ``Duktape.modLoaded`` before evaluating module source code
|
|
possibly returned by the search function. This is important so that circular
|
|
requires are properly supported (the current barebones mechanism does not
|
|
support circular references for C modules, though).
|
|
|
|
The user ``Duktape.modSearch()`` function encapsulates functionality such as
|
|
module search paths and file I/O. This is a nice division of labor as it
|
|
encapsulates practically all of the platform dependent parts of module
|
|
loading, while keeping the user callback unaware of almost every other
|
|
aspect of module loading. Using stub module search functions is also easy,
|
|
which is good for testing.
|
|
|
|
Ecmascript modules follow the CommonJS format, e.g. ``package/lib`` could
|
|
be represented by the source file::
|
|
|
|
exports.add = function (a, b) {
|
|
return a + b;
|
|
};
|
|
|
|
CommonJS instructs that modules should be evaluated with certain bindings
|
|
in force. Duktape currently implements the CommonJS requirements by simply
|
|
wrapping the module code inside some footer/header code::
|
|
|
|
(function (require, exports, module) {
|
|
/* module code here */
|
|
})
|
|
|
|
So the example module would become::
|
|
|
|
(function (require, exports, module) {
|
|
exports.add = function (a, b) {
|
|
return a + b;
|
|
};
|
|
// return value is ignored
|
|
})
|
|
|
|
When evaluated, the expression results in a function object (denoted ``F``)
|
|
which is then called (more or less) like::
|
|
|
|
var exports = {};
|
|
F.call(exports, /* exports also used as 'this' binding */
|
|
require, /* require method */
|
|
exports, /* exports */
|
|
{ id: 'package/lib' }); /* module */
|
|
|
|
A few notes:
|
|
|
|
* The return value of this call is ignored.
|
|
|
|
* The first argument is a new function object whose underlying native function
|
|
is the same as the global ``require()`` function. This fresh function is
|
|
needed to facilitate resolution of relative module identifiers: relative
|
|
identifers are resolved relative to the current module. The resolved
|
|
absolute identifier of the current module is tracked in ``require.id``.
|
|
Native code can then pick up the resolution path from the current function
|
|
object.
|
|
|
|
* The third argument provides the module with its own, resolved identifier.
|
|
The value in ``module.id`` is guaranteed to be in absolute form, and resolve
|
|
to the module itself if required from any other module. Duktape doesn't
|
|
currently support ``module.exports`` like NodeJS, as it is not required by
|
|
CommonJS.
|
|
|
|
CommonJS module identifier resolution
|
|
=====================================
|
|
|
|
CommonJS specifies that identifier terms must be "camelCase":
|
|
|
|
* http://wiki.commonjs.org/wiki/Modules/1.1#Module_Identifiers
|
|
|
|
Some interpret this to mean that e.g. a dash character is not allowed.
|
|
Such an interpretation seems counterproductive because e.g. filenames
|
|
often contain dashes, underscores, etc. Duktape allows terms to contain
|
|
any characters (including non-ASCII and white space) except that:
|
|
|
|
* A term must not begin with a period (``.``) to simplify resolution.
|
|
Such terms are rejected.
|
|
|
|
* A term cannot contain a forward slash, which (of course) gets
|
|
interpreted as a separator.
|
|
|
|
* A term cannot contain a U+0000 character. Such terms are currently
|
|
not rejected. Instead, they terminate the resolution as if the
|
|
requested identifier had ended.
|
|
|
|
If user code wishes to impose further limits, the module search function
|
|
can check a resolved identifier and throw an error if it is not of a
|
|
desirable form.
|
|
|
|
Logger names and tracebacks
|
|
===========================
|
|
|
|
Logger name defaulting uses the calling function's ``fileName`` property.
|
|
The ``fileName`` of the internal module wrapper function is set to the
|
|
resolved module identifier to make the logger default name come out right.
|
|
|
|
Tracebacks show both ``name`` and ``fileName`` of the internal wrapper
|
|
function. The ``name`` property is currently not set, so the wrapper
|
|
function appears anonymous. It could also be set to the module name.
|
|
|
|
module.exports
|
|
==============
|
|
|
|
NodeJS allows the default ``exports`` value to be changed by the module being
|
|
loaded; it can even be replaced e.g. by a function (it's normally an object
|
|
value). To change the value, the module must assign to ``module.exports``
|
|
which initially has the same value as ``exports``:
|
|
|
|
* http://timnew.github.io/blog/2012/04/20/exports_vs_module_exports_in_node_js/
|
|
|
|
Duktape doesn't currently support assignment to ``module.exports``.
|
|
|
|
C modules and DLLs
|
|
==================
|
|
|
|
Recommended convention
|
|
----------------------
|
|
|
|
``c-module-convention.rst`` describes a recommended convention for defining
|
|
an init function for a C module. The convention allows a C module to be
|
|
initialized manually when using static linking, or as part of loading the
|
|
module from a DLL.
|
|
|
|
The recommendation is in no way mandatory and you can easily write a module
|
|
loader with your own conventions (see below). However, modules following
|
|
the recommended convention will be easier to share between projects.
|
|
|
|
Implementing a C module / DLL loader
|
|
------------------------------------
|
|
|
|
The user provided module search function can be used to implement DLL support.
|
|
Simply load the DLL based on the module identifier, and call some kind of init
|
|
function in the DLL to register module symbols into the 'exports' table given
|
|
to the module loader.
|
|
|
|
Mixed C/Ecmascript modules are also possible by first registering symbols
|
|
provided by C code into the 'exports' table, and then returning the Ecmascript
|
|
part of the module. The Ecmascript part can access the symbols provided by C
|
|
code through the shared 'exports' table.
|
|
|
|
Limitations:
|
|
|
|
* Because the module is not yet registered into ``Duktape.modLoaded`` when the
|
|
module search function executes, circular requires are not handled correctly
|
|
for C modules.
|
|
|
|
* There is no automatic mechanism to know when a DLL can be unloaded from
|
|
memory. Tracking the reachability of the exports table of the module
|
|
(e.g. through a finalizer) is **not** enough because other modules can
|
|
copy references to individual exported values.
|
|
|
|
Background
|
|
==========
|
|
|
|
Module frameworks
|
|
-----------------
|
|
|
|
Ecmascript has not traditionally had a module mechanism. In browser
|
|
environments a web page can load multiple script files in a specific
|
|
order, each of them introducing more global symbols. This is not very
|
|
elegant because the order of loading must be correct in case any code
|
|
runs during loading. Several module mechanisms have since been created
|
|
for the browser environment to make writing modular Ecmascript easier.
|
|
Similar needs also exist in non-browser environments and several mechanisms
|
|
have been defined.
|
|
|
|
References summarizing several module frameworks:
|
|
|
|
* http://addyosmani.com/writing-modular-js/
|
|
|
|
* http://wiki.commonjs.org/wiki/Modules
|
|
|
|
Module loading APIs or "formats":
|
|
|
|
* Asynchronous Module Definition (AMD) API:
|
|
|
|
- https://github.com/amdjs/amdjs-api/wiki/AMD
|
|
|
|
* CommonJS:
|
|
|
|
- http://wiki.commonjs.org/wiki/Modules/1.1.1
|
|
|
|
- https://github.com/joyent/node/blob/master/lib/module.js
|
|
|
|
- https://github.com/commonjs/commonjs/tree/master/tests/modules
|
|
|
|
- http://requirejs.org/docs/commonjs.html
|
|
|
|
- http://dailyjs.com/2010/10/18/modules/
|
|
|
|
* NodeJS, more or less CommonJS:
|
|
|
|
- http://nodejs.org/docs/v0.11.13/api/modules.html
|
|
|
|
* ES6:
|
|
|
|
- https://people.mozilla.org/~jorendorff/es6-draft.html#sec-modules
|
|
|
|
AMD is optimized for the web client side, and requires callback based
|
|
asynchronous module loading. This model is not very convenient for
|
|
server side programming, or fully fledged application programming which
|
|
is more natural with Duktape.
|
|
|
|
CommonJS module format is a server side module mechanism which seems most
|
|
appropriate to be the default Duktape mechanism.
|
|
|
|
Some NodeJS tests
|
|
=================
|
|
|
|
This section illustrates some NodeJS module loader features, as it's nice
|
|
to align with NodeJS behavior when possible.
|
|
|
|
Assignments
|
|
-----------
|
|
|
|
Test module::
|
|
|
|
// test.js
|
|
var foo = 123; // not visible outside
|
|
bar = 234; // assigned to global object
|
|
this.quux = 345; // exported from module
|
|
exports.baz = 456; // exported from module
|
|
|
|
Test code::
|
|
|
|
> var t = require('./test');
|
|
undefined
|
|
> console.log(JSON.stringify(t));
|
|
{"quux":345,"baz":456}
|
|
undefined
|
|
> console.log(bar);
|
|
234
|
|
|
|
Future work
|
|
===========
|
|
|
|
module.exports
|
|
--------------
|
|
|
|
Could add support to ``module.exports``.
|
|
|
|
Ability to load modules from C code
|
|
-----------------------------------
|
|
|
|
For instance, implement something like::
|
|
|
|
// Pushes the 'exports' table of 'foo/bar' module to the stack.
|
|
duk_require_module(ctx, "foo/bar");
|
|
|
|
This is not a high priority thing as one can simply::
|
|
|
|
duk_eval_string(ctx, "require('foo/bar')");
|
|
|
|
Eval invokes the compiler which is not ideal, but modules are usually
|
|
imported during initialization so this should rarely matter.
|
|
|
|
Better C module support
|
|
-----------------------
|
|
|
|
Several ideas to improve the C module support:
|
|
|
|
* Allow C modules to participate in circular requires. Module search and
|
|
C module init need to be separated for this to be possible, so that the
|
|
``Duktape.modLoaded`` registration can be done in-between.
|
|
|
|
* Provide a default DLL loading helper for at least POSIX and Windows.
|
|
|
|
Module unloading support
|
|
------------------------
|
|
|
|
Currently modules cannot be unloaded: once loaded, they're registered to
|
|
``Duktape.modLoaded`` permanently, which keeps the exported object permanently
|
|
reachable (unless removed manually). Adding a finalizer to the exports table
|
|
is not a solution: another module might hold a reference to a specific symbol
|
|
within the module but not the exports table itself, e.g.::
|
|
|
|
var helloFunc = require('hello').func;
|
|
|
|
Collecting a module exports table and executing some unload code is not
|
|
trivial. Just removing an unused exports object probably requires weak
|
|
reference support.
|
|
|
|
Isolating a module from the global object
|
|
-----------------------------------------
|
|
|
|
Currently ``this`` is bound to ``exports`` so writes through ``this`` do
|
|
not pollute globals. Variable and function declarations also currently
|
|
go to the module wrapper function and do not pollute globals. However,
|
|
plain assignments do write to globals, and reads not matching identifiers
|
|
declared in scope are read from globals::
|
|
|
|
fooBar = 123; // if 'fooBar' not in scope, write to global
|
|
print(barFoo); // if 'barFoo' not in scope, read from global
|
|
|
|
Lua-like module loader
|
|
----------------------
|
|
|
|
The lowest level module mechanism could also be similar to what Lua does.
|
|
A module would be cached as in CommonJS so that it would only be loaded
|
|
once per global context. Modules could be loaded with a user callback
|
|
which takes a module ID and returns the loaded module object (same as the
|
|
``exports``) value to be registered into the module cache.
|
|
|
|
The upside of this approach is flexibility: most of the CommonJS module
|
|
mechanism can be implemented on top of this.
|
|
|
|
One downside is that the module loading mechanism would not be a common one
|
|
and most users would need to implement or borrow a standard module loader.
|
|
Another downside is that a Lua-like mechanism doesn't deal with circular
|
|
module loading while the CommonJS one does (to some extent).
|
|
|