module extension: add support for canonical name resolution (#2013)

Before attempting to load a module, each provided resolver must be given an
opportunity to examine the name of the requested module without actually
loading it so as to canonicalize it, in case a module can be referred to by
multiple names.

Then, modules are loaded and cached by their canonical name.

JerryScript-DCO-1.0-Signed-off-by: Gabriel Schulhof gabriel.schulhof@intel.com
This commit is contained in:
Gabriel "_|Nix|_" Schulhof
2017-09-29 13:02:34 +03:00
committed by Zoltan Herczeg
parent e527e41bac
commit 6d53931055
6 changed files with 329 additions and 77 deletions
+97 -30
View File
@@ -2,17 +2,33 @@
This is a JerryScript extension that provides a means of loading modules. Fundamentally, a module is a name (stored as
a string) that resolves to a `jerry_value_t`. This extension provides the function `jerryx_module_resolve()` which
accepts the name of the module being requested as well as an array of so-called "resolvers" - functions which satisfy
the signature `jerryx_module_resolver_t`. The resolvers in the list are called in sequence until one of them returns
`true` and a `jerry_value_t` in its out parameter. The value is cached if it is not an error, so subsequent requests
for the same name will not result in additional calls to the resolvers.
accepts the name of the module being requested as well as an array of so-called "resolvers" - structures containing two
function pointers: one for a function which computes a canonical name for the requested module or returns a reference
to the requested name, and one that converts a canonical name to a `jerry_value_t`, thus "resolving" or "loading" the
requested module.
The resolvers are first called in sequence to each compute the canonical name of the requested module. This is
accomplished by calling the `get_canonical_name` function pointer they provide. If the function pointer is `NULL`, the
requested module name is assumed to be what the resolver considers to be its canonical name. `jerryx_module_resolve`
searches its cache of loaded modules for each canonical name as returned by a `get_canonical_name` function pointer. If
one of the loaded modules in the cache corresponds to a canonical name, it is returned.
If no cached module is found, `jerryx_module_resolve` calls each resolver's `resolve` function pointer, passing it its
previously computed interpretation of the requested module's canonical name. If the resolver successfully creates the
`jerry_value_t` that represents the loaded module, it returns `true` and the `jerry_value_t` in its out parameter.
When `jerryx_module_resolve` receives a value of `true` from a resolver, it stops iterating over the remaining
resolvers in the sequence and, if the `jerry_value_t` returned from the resolver's `resolve` does not have the error
flag set, it will add the `jerry_value_t` to its cache under the module's canonical name and return it. Thus, on
subsequent calls to `jerryx_module_resolve` with a module name whose canonical name is associated with the
`jerry_value_t`, no `resolve` callback need be called again.
The purpose of having resolvers is to be able to account for the fact that different types of modules may be structured
differently and thus, for each type of module a module resolver must be supplied at the point where an instance of that
type of module is requested.
Additionally, this extension provides a means of easily defining so-called "native" JerryScript modules which can be
resolved using the JerryScript native module resolver `jerryx_module_native_resolver()`, which can be passed to
resolved using the native JerryScript module resolver `jerryx_module_native_resolver`, which can be passed to
`jerryx_module_resolve()`. Native modules are registered during application startup and by calling `dlopen()` by means
of library constructors, support for which can be turned on using the `FEATURE_INIT_FINI` build flag. In the absence of
such a flag, the module registration and unregistration functions are exposed as global symbols which can be called
@@ -24,10 +40,17 @@ explicitly.
Load a copy of a module into the current context or return one that was already loaded if it is found.
Each function in `resolvers_p` will be called in sequence until one returns `true` and fills out its out-parameter with
the `jerry_value_t` representing the requested module. If the `jerry_value_t` does not have the error flag set it will
be cached. Thus, on subsequent calls with the same value for `name`, none of the functions in `resolvers_p` will be
called.
For each resolver passed in via `resolvers_p`, its `get_canonical_name` function pointer gets called in order to
establish the resolver's interpretation of what the canonical name for the module should be. If `get_canonical_name` is
`NULL`, it is assumed that the requested module's name as passed in is its canonical name.
Then, for each resolver passed in via `resolvers_p`, its `resolve` function pointer gets called with its interpretation
of what the module's canonical name should be, as computed in the previous step.
If the resolver's `resolve` function pointer returns `true`, the `jerry_value_t` returned in its out-parameter will be
returned by `jerryx_module_resolve` as the result of the request. If no error flag is set on the `jerry_value_t` it
will be cached under its canonical name so as to avoid loading the same module twice in the event of a subsequent call
to `jerryx_module_resolve` with a module name whose canonical name matches an already loaded module.
**Prototype**
@@ -48,56 +71,79 @@ jerryx_module_resolve (const jerry_char_t *name,
**Summary**
The resolver for native JerryScript modules. A pointer to this function can be passed in the second parameter to
`jerryx_module_resolve` to search for the module among the native JerryScript modules loaded so far.
The resolver for native JerryScript modules. A pointer to this structure can be passed in the second parameter to
`jerryx_module_resolve` to search for the module among the native JerryScript modules built into the binary. This
function is available only if the preprocessor directive `JERRYX_NATIVE_MODULES_SUPPORTED` is defined.
**Prototype**
```c
bool
jerryx_module_native_resolver (const jerry_char_t *name,
jerry_value_t *result)
extern jerry_module_resolver_t jerryx_native_module_resolver;
```
- `name` - the name of the module to find
- `result` - out - place where to store the resulting module instance
- return value - `true` if the module was found and stored in `result`, and `false` otherwise
# Module data types
## jerryx_native_module_on_resolve_t
## jerryx_module_get_canonical_name_t
**Summary**
Function pointer type for a function that will create an instance of a native module.
The function pointer type for converting a module's requested name to its canonical name.
**Prototype**
```c
typedef jerry_value_t (*jerryx_native_module_on_resolve_t) (void);
typedef jerry_value_t (*jerryx_module_get_canonical_name_t) (const jerry_value_t name);
```
## jerryx_module_resolve_t
**Summary**
Function pointer type for module resolution.
**Prototype**
```c
typedef bool (*jerryx_module_resolve_t) (const jerry_value_t canonical_name,
jerry_value_t *result);
```
## jerryx_module_resolver_t
**Summary**
Function pointer type for a module resolver
Structure defining a module resolver.
**Prototype**
```c
typedef bool (*jerryx_module_resolver_t) (const jerry_char_t *name, jerry_value_t *result);
typedef struct
{
jerryx_module_get_canonical_name_t get_canonical_name_p;
jerryx_module_resolve_t resolve_p;
} jerryx_module_resolver_t;
```
- `get_canonical_name_p` - function pointer to be called when the canonical name corresponding to the requested name
of a module must be established.
- `resolve_p` - function pointer to be called when a module with the given canonical name needs to be converted to the
`jerry_value_t` that will become the loaded module.
**Example**
```c
bool
load_and_evaluate_js_file (const jerry_char_t *name, jerry_value_t *result)
static bool
load_and_evaluate_js_file (const jerry_value_t name, jerry_value_t *result)
{
bool return_value = false;
char *js_file_contents = NULL;
int file_size = 0;
FILE *js_file = fopen (name, "r");
jerry_size_t name_size = jerry_get_utf8_string_size (name);
jerry_char_t name_string[name_size + 1];
jerry_string_to_utf8_char_buffer (name, name_string, name_size);
name_string[name_size] = 0;
FILE *js_file = fopen (name_string, "r");
if (js_file)
{
@@ -124,23 +170,44 @@ load_and_evaluate_js_file (const jerry_char_t *name, jerry_value_t *result)
return return_value;
}
static jerry_value_t
canonicalize_file_path (const jerry_value_t name)
{
jerry_value_t absolute_path;
/**
* Since a file on the file system can be referred to by multiple relative paths, but only by one absolute path, the
* absolute path becomes the canonical name for the module. Thus, to establish this canonical name, we must search
* name for "./" and "../", follow symlinks, etc., then create absolute_path via jerry_create_string () and return
* it, because it is the canonical name for this module. Thus, we avoid loading the same JavaScript file twice.
*/
return absolute_path;
}
static jerryx_module_resolver_t js_file_loader
{
canonicalize_file_path,
load_and_evaluate_js_file
};
```
We can now load JavaScript files:
```c
static const jerryx_module_resolver_t resolvers[] =
static const jerryx_module_resolver_t *resolvers[] =
{
/*
* Consult the JerryScript native module resolver first, in case the requested module is a native JerryScript
* Consult the resolver for native JerryScript modules first, in case the requested module is a native JerryScript
* module.
*/
jerryx_module_native_resolver,
&jerryx_module_native_resolver,
/*
* If the requested module is not a native JerryScript module, assume it is a JavaScript file on disk and use the
* above-defined JavaScript file loader to load it.
*/
load_and_evaluate_js_file
&js_file_loader
};
jerry_value_t js_module = jerryx_module_resolve (requested_module, resolvers, 2);
```