Skip to content

Allow custom module path normalisation #184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 6, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ class V8Js
public function setModuleLoader(callable $loader)
{}

/**
* Provide a function or method to be used to normalise module paths. This can be any valid PHP callable.
* This can be used in combination with setModuleLoader to influence normalisation of the module path (which
* is normally done by V8Js itself but can be overriden this way).
* The normaliser function will receive the base path of the current module (if any; otherwise an empty string)
* and the literate string provided to the require method and should return an array of two strings (the new
* module base path as well as the normalised name). Both are joined by a '/' and then passed on to the
* module loader (unless the module was cached before).
* @param callable $normaliser
*/
public function setModuleNormaliser(callable $normaliser)
{}

/**
* Compiles and executes script in object's context with optional identifier string.
* A time limit (milliseconds) and/or memory limit (bytes) can be provided to restrict execution. These options will throw a V8JsTimeLimitException or V8JsMemoryLimitException.
Expand Down
31 changes: 31 additions & 0 deletions tests/commonjs_cust_normalise_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
Test V8Js::setModuleNormaliser : Custom normalisation #001
--SKIPIF--
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
--FILE--
<?php

$JS = <<< EOT
var foo = require("./test");
EOT;

$v8 = new V8Js();

$v8->setModuleNormaliser(function($base, $module) {
var_dump($base, $module);
return [ "", "test" ];
});

$v8->setModuleLoader(function($module) {
print("setModuleLoader called for ".$module."\n");
return 'exports.bar = 23;';
});

$v8->executeString($JS, 'module.js');
?>
===EOF===
--EXPECT--
string(0) ""
string(6) "./test"
setModuleLoader called for test
===EOF===
33 changes: 33 additions & 0 deletions tests/commonjs_cust_normalise_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
--TEST--
Test V8Js::setModuleNormaliser : Custom normalisation #002
--SKIPIF--
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
--FILE--
<?php

$JS = <<< EOT
var foo = require("./test");
EOT;

$v8 = new V8Js();

// setModuleNormaliser may redirect module requirement
// to a different path (and even rename the module)
$v8->setModuleNormaliser(function($base, $module) {
var_dump($base, $module);
return [ "path/to", "test-foo" ];
});

$v8->setModuleLoader(function($module) {
print("setModuleLoader called for ".$module."\n");
return 'exports.bar = 23;';
});

$v8->executeString($JS, 'module.js');
?>
===EOF===
--EXPECT--
string(0) ""
string(6) "./test"
setModuleLoader called for path/to/test-foo
===EOF===
41 changes: 41 additions & 0 deletions tests/commonjs_cust_normalise_003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
--TEST--
Test V8Js::setModuleNormaliser : Custom normalisation #003
--SKIPIF--
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
--FILE--
<?php

$JS = <<< EOT
var foo = require("./test");
var bar = require("test");
EOT;

$v8 = new V8Js();

// Caching is done based on the identifiers passed back
// by the module normaliser. If it returns the same id
// for multiple require calls, the module loader callback
// will be called only once (as the others are cached)
$v8->setModuleNormaliser(function($base, $module) {
var_dump($base, $module);
return [ "path/to", "test-foo" ];
});

$v8->setModuleLoader(function($module) {
print("setModuleLoader called for ".$module."\n");
if($module != "path/to/test-foo") {
throw new \Exception("module caching fails");
}
return 'exports.bar = 23;';
});

$v8->executeString($JS, 'module.js');
?>
===EOF===
--EXPECT--
string(0) ""
string(6) "./test"
setModuleLoader called for path/to/test-foo
string(0) ""
string(4) "test"
===EOF===
42 changes: 42 additions & 0 deletions tests/commonjs_cust_normalise_004.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
--TEST--
Test V8Js::setModuleNormaliser : Custom normalisation #004
--SKIPIF--
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
--FILE--
<?php

$JS = <<< EOT
var foo = require("foo");
EOT;

$v8 = new V8Js();

// If a module includes another module, $base must be set to the
// path of the first module (on the second call)
$v8->setModuleNormaliser(function($base, $module) {
var_dump($base, $module);
return [ "path/to", $module ];
});

$v8->setModuleLoader(function($module) {
print("setModuleLoader called for ".$module."\n");
switch($module) {
case "path/to/foo":
return "require('bar');";

case "path/to/bar":
return 'exports.bar = 23;';
}
});

$v8->executeString($JS, 'module.js');
?>
===EOF===
--EXPECT--
string(0) ""
string(3) "foo"
setModuleLoader called for path/to/foo
string(7) "path/to"
string(3) "bar"
setModuleLoader called for path/to/bar
===EOF===
29 changes: 29 additions & 0 deletions v8js_class.cc
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ static void v8js_free_storage(void *object TSRMLS_DC) /* {{{ */
zval_ptr_dtor(&c->pending_exception);
}

if (c->module_normaliser) {
zval_ptr_dtor(&c->module_normaliser);
}

if (c->module_loader) {
zval_ptr_dtor(&c->module_loader);
}
Expand Down Expand Up @@ -362,6 +366,7 @@ static PHP_METHOD(V8Js, __construct)
c->memory_limit = 0;
c->memory_limit_hit = false;

c->module_normaliser = NULL;
c->module_loader = NULL;

/* Include extensions used by this context */
Expand Down Expand Up @@ -687,6 +692,24 @@ static PHP_METHOD(V8Js, clearPendingException)
}
/* }}} */

/* {{{ proto void V8Js::setModuleNormaliser(string base, string module_id)
*/
static PHP_METHOD(V8Js, setModuleNormaliser)
{
v8js_ctx *c;
zval *callable;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &callable) == FAILURE) {
return;
}

c = (v8js_ctx *) zend_object_store_get_object(getThis() TSRMLS_CC);

c->module_normaliser = callable;
Z_ADDREF_P(c->module_normaliser);
}
/* }}} */

/* {{{ proto void V8Js::setModuleLoader(string module)
*/
static PHP_METHOD(V8Js, setModuleLoader)
Expand Down Expand Up @@ -1005,6 +1028,11 @@ ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO(arginfo_v8js_clearpendingexception, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_v8js_setmodulenormaliser, 0, 0, 2)
ZEND_ARG_INFO(0, base)
ZEND_ARG_INFO(0, module_id)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_v8js_setmoduleloader, 0, 0, 1)
ZEND_ARG_INFO(0, callable)
ZEND_END_ARG_INFO()
Expand Down Expand Up @@ -1038,6 +1066,7 @@ static const zend_function_entry v8js_methods[] = { /* {{{ */
PHP_ME(V8Js, checkString, arginfo_v8js_checkstring, ZEND_ACC_PUBLIC|ZEND_ACC_DEPRECATED)
PHP_ME(V8Js, getPendingException, arginfo_v8js_getpendingexception, ZEND_ACC_PUBLIC)
PHP_ME(V8Js, clearPendingException, arginfo_v8js_clearpendingexception, ZEND_ACC_PUBLIC)
PHP_ME(V8Js, setModuleNormaliser, arginfo_v8js_setmodulenormaliser, ZEND_ACC_PUBLIC)
PHP_ME(V8Js, setModuleLoader, arginfo_v8js_setmoduleloader, ZEND_ACC_PUBLIC)
PHP_ME(V8Js, registerExtension, arginfo_v8js_registerextension, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
PHP_ME(V8Js, getExtensions, arginfo_v8js_getextensions, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
Expand Down
1 change: 1 addition & 0 deletions v8js_class.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ struct v8js_ctx {
v8js_tmpl_t global_template;
v8js_tmpl_t array_tmpl;

zval *module_normaliser;
zval *module_loader;
std::vector<char *> modules_stack;
std::vector<char *> modules_base;
Expand Down
115 changes: 100 additions & 15 deletions v8js_methods.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
+----------------------------------------------------------------------+
| Author: Jani Taskinen <jani.taskinen@iki.fi> |
| Author: Patrick Reilly <preilly@php.net> |
| Author: Stefan Siegl <stesie@brokenpipe.de> |
+----------------------------------------------------------------------+
*/

Expand Down Expand Up @@ -207,12 +208,106 @@ V8JS_METHOD(require)
}

v8::String::Utf8Value module_id_v8(info[0]);

const char *module_id = ToCString(module_id_v8);
char *normalised_path = (char *)emalloc(PATH_MAX);
char *module_name = (char *)emalloc(PATH_MAX);
char *normalised_path, *module_name;

if (c->module_normaliser == NULL) {
// No custom normalisation routine registered, use internal one
normalised_path = (char *)emalloc(PATH_MAX);
module_name = (char *)emalloc(PATH_MAX);

v8js_commonjs_normalise_identifier(c->modules_base.back(), module_id, normalised_path, module_name);
}
else {
// Call custom normaliser
int call_result;
zval *z_base, *z_module_id, *normaliser_result;

MAKE_STD_ZVAL(z_base);
MAKE_STD_ZVAL(z_module_id);

zend_try {
{
isolate->Exit();
v8::Unlocker unlocker(isolate);

ZVAL_STRING(z_base, c->modules_base.back(), 1);
ZVAL_STRING(z_module_id, module_id, 1);

zval **params[2] = {&z_base, &z_module_id};
call_result = call_user_function_ex(EG(function_table), NULL, c->module_normaliser,
&normaliser_result, 2, params, 0, NULL TSRMLS_CC);
}

isolate->Enter();

v8js_commonjs_normalise_identifier(c->modules_base.back(), module_id, normalised_path, module_name);
if (call_result == FAILURE) {
info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module normaliser callback failed")));
}
}
zend_catch {
v8js_terminate_execution(isolate);
V8JSG(fatal_error_abort) = 1;
call_result = FAILURE;
}
zend_end_try();

zval_ptr_dtor(&z_base);
zval_ptr_dtor(&z_module_id);

if(call_result == FAILURE) {
return;
}

// Check if an exception was thrown
if (EG(exception)) {
// Clear the PHP exception and throw it in V8 instead
zend_clear_exception(TSRMLS_C);
info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module normaliser callback exception")));
return;
}

if (Z_TYPE_P(normaliser_result) != IS_ARRAY) {
zval_ptr_dtor(&normaliser_result);
info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module normaliser didn't return an array")));
return;
}

HashTable *ht = HASH_OF(normaliser_result);
int num_elements = zend_hash_num_elements(ht);

if(num_elements != 2) {
zval_ptr_dtor(&normaliser_result);
info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module normaliser expected to return array of 2 strings")));
return;
}

zval **data;
ulong index = 0;
HashPosition pos;

for (zend_hash_internal_pointer_reset_ex(ht, &pos);
SUCCESS == zend_hash_get_current_data_ex(ht, (void **) &data, &pos);
zend_hash_move_forward_ex(ht, &pos)
) {

if (Z_TYPE_P(*data) != IS_STRING) {
convert_to_string(*data);
}

switch(index++) {
case 0: // normalised path
normalised_path = estrndup(Z_STRVAL_PP(data), Z_STRLEN_PP(data));
break;

case 1: // normalised module id
module_name = estrndup(Z_STRVAL_PP(data), Z_STRLEN_PP(data));
break;
}
}

zval_ptr_dtor(&normaliser_result);
}

char *normalised_module_id = (char *)emalloc(strlen(normalised_path)+1+strlen(module_name)+1);
*normalised_module_id = 0;
Expand Down Expand Up @@ -305,16 +400,6 @@ V8JS_METHOD(require)
convert_to_string(module_code);
}

// Check that some code has been returned
if (Z_STRLEN_P(module_code)==0) {
zval_ptr_dtor(&module_code);
efree(normalised_module_id);
efree(normalised_path);

info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module loader callback did not return code")));
return;
}

// Create a template for the global object and set the built-in global functions
v8::Handle<v8::ObjectTemplate> global = v8::ObjectTemplate::New();
global->Set(V8JS_SYM("print"), v8::FunctionTemplate::New(isolate, V8JS_MN(print)), v8::ReadOnly);
Expand Down Expand Up @@ -347,7 +432,7 @@ V8JS_METHOD(require)
// Enter the module context
v8::Context::Scope scope(context);
// Set script identifier
v8::Local<v8::String> sname = V8JS_SYM(normalised_module_id);
v8::Local<v8::String> sname = V8JS_STR(normalised_module_id);

v8::Local<v8::String> source = V8JS_STRL(Z_STRVAL_P(module_code), Z_STRLEN_P(module_code));
zval_ptr_dtor(&module_code);
Expand Down