Skip to content

Commit 28f0611

Browse files
committed
Merge pull request #184 from stesie/cust-module-normalisation
Allow custom module path normalisation
2 parents bf58fe6 + 67a9de0 commit 28f0611

8 files changed

+290
-15
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ class V8Js
7979
public function setModuleLoader(callable $loader)
8080
{}
8181

82+
/**
83+
* Provide a function or method to be used to normalise module paths. This can be any valid PHP callable.
84+
* This can be used in combination with setModuleLoader to influence normalisation of the module path (which
85+
* is normally done by V8Js itself but can be overriden this way).
86+
* The normaliser function will receive the base path of the current module (if any; otherwise an empty string)
87+
* and the literate string provided to the require method and should return an array of two strings (the new
88+
* module base path as well as the normalised name). Both are joined by a '/' and then passed on to the
89+
* module loader (unless the module was cached before).
90+
* @param callable $normaliser
91+
*/
92+
public function setModuleNormaliser(callable $normaliser)
93+
{}
94+
8295
/**
8396
* Compiles and executes script in object's context with optional identifier string.
8497
* A time limit (milliseconds) and/or memory limit (bytes) can be provided to restrict execution. These options will throw a V8JsTimeLimitException or V8JsMemoryLimitException.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
--TEST--
2+
Test V8Js::setModuleNormaliser : Custom normalisation #001
3+
--SKIPIF--
4+
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
8+
$JS = <<< EOT
9+
var foo = require("./test");
10+
EOT;
11+
12+
$v8 = new V8Js();
13+
14+
$v8->setModuleNormaliser(function($base, $module) {
15+
var_dump($base, $module);
16+
return [ "", "test" ];
17+
});
18+
19+
$v8->setModuleLoader(function($module) {
20+
print("setModuleLoader called for ".$module."\n");
21+
return 'exports.bar = 23;';
22+
});
23+
24+
$v8->executeString($JS, 'module.js');
25+
?>
26+
===EOF===
27+
--EXPECT--
28+
string(0) ""
29+
string(6) "./test"
30+
setModuleLoader called for test
31+
===EOF===
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
--TEST--
2+
Test V8Js::setModuleNormaliser : Custom normalisation #002
3+
--SKIPIF--
4+
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
8+
$JS = <<< EOT
9+
var foo = require("./test");
10+
EOT;
11+
12+
$v8 = new V8Js();
13+
14+
// setModuleNormaliser may redirect module requirement
15+
// to a different path (and even rename the module)
16+
$v8->setModuleNormaliser(function($base, $module) {
17+
var_dump($base, $module);
18+
return [ "path/to", "test-foo" ];
19+
});
20+
21+
$v8->setModuleLoader(function($module) {
22+
print("setModuleLoader called for ".$module."\n");
23+
return 'exports.bar = 23;';
24+
});
25+
26+
$v8->executeString($JS, 'module.js');
27+
?>
28+
===EOF===
29+
--EXPECT--
30+
string(0) ""
31+
string(6) "./test"
32+
setModuleLoader called for path/to/test-foo
33+
===EOF===
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
Test V8Js::setModuleNormaliser : Custom normalisation #003
3+
--SKIPIF--
4+
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
8+
$JS = <<< EOT
9+
var foo = require("./test");
10+
var bar = require("test");
11+
EOT;
12+
13+
$v8 = new V8Js();
14+
15+
// Caching is done based on the identifiers passed back
16+
// by the module normaliser. If it returns the same id
17+
// for multiple require calls, the module loader callback
18+
// will be called only once (as the others are cached)
19+
$v8->setModuleNormaliser(function($base, $module) {
20+
var_dump($base, $module);
21+
return [ "path/to", "test-foo" ];
22+
});
23+
24+
$v8->setModuleLoader(function($module) {
25+
print("setModuleLoader called for ".$module."\n");
26+
if($module != "path/to/test-foo") {
27+
throw new \Exception("module caching fails");
28+
}
29+
return 'exports.bar = 23;';
30+
});
31+
32+
$v8->executeString($JS, 'module.js');
33+
?>
34+
===EOF===
35+
--EXPECT--
36+
string(0) ""
37+
string(6) "./test"
38+
setModuleLoader called for path/to/test-foo
39+
string(0) ""
40+
string(4) "test"
41+
===EOF===
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
--TEST--
2+
Test V8Js::setModuleNormaliser : Custom normalisation #004
3+
--SKIPIF--
4+
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
8+
$JS = <<< EOT
9+
var foo = require("foo");
10+
EOT;
11+
12+
$v8 = new V8Js();
13+
14+
// If a module includes another module, $base must be set to the
15+
// path of the first module (on the second call)
16+
$v8->setModuleNormaliser(function($base, $module) {
17+
var_dump($base, $module);
18+
return [ "path/to", $module ];
19+
});
20+
21+
$v8->setModuleLoader(function($module) {
22+
print("setModuleLoader called for ".$module."\n");
23+
switch($module) {
24+
case "path/to/foo":
25+
return "require('bar');";
26+
27+
case "path/to/bar":
28+
return 'exports.bar = 23;';
29+
}
30+
});
31+
32+
$v8->executeString($JS, 'module.js');
33+
?>
34+
===EOF===
35+
--EXPECT--
36+
string(0) ""
37+
string(3) "foo"
38+
setModuleLoader called for path/to/foo
39+
string(7) "path/to"
40+
string(3) "bar"
41+
setModuleLoader called for path/to/bar
42+
===EOF===

v8js_class.cc

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ static void v8js_free_storage(void *object TSRMLS_DC) /* {{{ */
8989
zval_ptr_dtor(&c->pending_exception);
9090
}
9191

92+
if (c->module_normaliser) {
93+
zval_ptr_dtor(&c->module_normaliser);
94+
}
95+
9296
if (c->module_loader) {
9397
zval_ptr_dtor(&c->module_loader);
9498
}
@@ -362,6 +366,7 @@ static PHP_METHOD(V8Js, __construct)
362366
c->memory_limit = 0;
363367
c->memory_limit_hit = false;
364368

369+
c->module_normaliser = NULL;
365370
c->module_loader = NULL;
366371

367372
/* Include extensions used by this context */
@@ -687,6 +692,24 @@ static PHP_METHOD(V8Js, clearPendingException)
687692
}
688693
/* }}} */
689694

695+
/* {{{ proto void V8Js::setModuleNormaliser(string base, string module_id)
696+
*/
697+
static PHP_METHOD(V8Js, setModuleNormaliser)
698+
{
699+
v8js_ctx *c;
700+
zval *callable;
701+
702+
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &callable) == FAILURE) {
703+
return;
704+
}
705+
706+
c = (v8js_ctx *) zend_object_store_get_object(getThis() TSRMLS_CC);
707+
708+
c->module_normaliser = callable;
709+
Z_ADDREF_P(c->module_normaliser);
710+
}
711+
/* }}} */
712+
690713
/* {{{ proto void V8Js::setModuleLoader(string module)
691714
*/
692715
static PHP_METHOD(V8Js, setModuleLoader)
@@ -1005,6 +1028,11 @@ ZEND_END_ARG_INFO()
10051028
ZEND_BEGIN_ARG_INFO(arginfo_v8js_clearpendingexception, 0)
10061029
ZEND_END_ARG_INFO()
10071030

1031+
ZEND_BEGIN_ARG_INFO_EX(arginfo_v8js_setmodulenormaliser, 0, 0, 2)
1032+
ZEND_ARG_INFO(0, base)
1033+
ZEND_ARG_INFO(0, module_id)
1034+
ZEND_END_ARG_INFO()
1035+
10081036
ZEND_BEGIN_ARG_INFO_EX(arginfo_v8js_setmoduleloader, 0, 0, 1)
10091037
ZEND_ARG_INFO(0, callable)
10101038
ZEND_END_ARG_INFO()
@@ -1038,6 +1066,7 @@ static const zend_function_entry v8js_methods[] = { /* {{{ */
10381066
PHP_ME(V8Js, checkString, arginfo_v8js_checkstring, ZEND_ACC_PUBLIC|ZEND_ACC_DEPRECATED)
10391067
PHP_ME(V8Js, getPendingException, arginfo_v8js_getpendingexception, ZEND_ACC_PUBLIC)
10401068
PHP_ME(V8Js, clearPendingException, arginfo_v8js_clearpendingexception, ZEND_ACC_PUBLIC)
1069+
PHP_ME(V8Js, setModuleNormaliser, arginfo_v8js_setmodulenormaliser, ZEND_ACC_PUBLIC)
10411070
PHP_ME(V8Js, setModuleLoader, arginfo_v8js_setmoduleloader, ZEND_ACC_PUBLIC)
10421071
PHP_ME(V8Js, registerExtension, arginfo_v8js_registerextension, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
10431072
PHP_ME(V8Js, getExtensions, arginfo_v8js_getextensions, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)

v8js_class.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ struct v8js_ctx {
5050
v8js_tmpl_t global_template;
5151
v8js_tmpl_t array_tmpl;
5252

53+
zval *module_normaliser;
5354
zval *module_loader;
5455
std::vector<char *> modules_stack;
5556
std::vector<char *> modules_base;

v8js_methods.cc

Lines changed: 100 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
+----------------------------------------------------------------------+
99
| Author: Jani Taskinen <jani.taskinen@iki.fi> |
1010
| Author: Patrick Reilly <preilly@php.net> |
11+
| Author: Stefan Siegl <stesie@brokenpipe.de> |
1112
+----------------------------------------------------------------------+
1213
*/
1314

@@ -207,12 +208,106 @@ V8JS_METHOD(require)
207208
}
208209

209210
v8::String::Utf8Value module_id_v8(info[0]);
210-
211211
const char *module_id = ToCString(module_id_v8);
212-
char *normalised_path = (char *)emalloc(PATH_MAX);
213-
char *module_name = (char *)emalloc(PATH_MAX);
212+
char *normalised_path, *module_name;
213+
214+
if (c->module_normaliser == NULL) {
215+
// No custom normalisation routine registered, use internal one
216+
normalised_path = (char *)emalloc(PATH_MAX);
217+
module_name = (char *)emalloc(PATH_MAX);
218+
219+
v8js_commonjs_normalise_identifier(c->modules_base.back(), module_id, normalised_path, module_name);
220+
}
221+
else {
222+
// Call custom normaliser
223+
int call_result;
224+
zval *z_base, *z_module_id, *normaliser_result;
225+
226+
MAKE_STD_ZVAL(z_base);
227+
MAKE_STD_ZVAL(z_module_id);
228+
229+
zend_try {
230+
{
231+
isolate->Exit();
232+
v8::Unlocker unlocker(isolate);
233+
234+
ZVAL_STRING(z_base, c->modules_base.back(), 1);
235+
ZVAL_STRING(z_module_id, module_id, 1);
236+
237+
zval **params[2] = {&z_base, &z_module_id};
238+
call_result = call_user_function_ex(EG(function_table), NULL, c->module_normaliser,
239+
&normaliser_result, 2, params, 0, NULL TSRMLS_CC);
240+
}
241+
242+
isolate->Enter();
214243

215-
v8js_commonjs_normalise_identifier(c->modules_base.back(), module_id, normalised_path, module_name);
244+
if (call_result == FAILURE) {
245+
info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module normaliser callback failed")));
246+
}
247+
}
248+
zend_catch {
249+
v8js_terminate_execution(isolate);
250+
V8JSG(fatal_error_abort) = 1;
251+
call_result = FAILURE;
252+
}
253+
zend_end_try();
254+
255+
zval_ptr_dtor(&z_base);
256+
zval_ptr_dtor(&z_module_id);
257+
258+
if(call_result == FAILURE) {
259+
return;
260+
}
261+
262+
// Check if an exception was thrown
263+
if (EG(exception)) {
264+
// Clear the PHP exception and throw it in V8 instead
265+
zend_clear_exception(TSRMLS_C);
266+
info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module normaliser callback exception")));
267+
return;
268+
}
269+
270+
if (Z_TYPE_P(normaliser_result) != IS_ARRAY) {
271+
zval_ptr_dtor(&normaliser_result);
272+
info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module normaliser didn't return an array")));
273+
return;
274+
}
275+
276+
HashTable *ht = HASH_OF(normaliser_result);
277+
int num_elements = zend_hash_num_elements(ht);
278+
279+
if(num_elements != 2) {
280+
zval_ptr_dtor(&normaliser_result);
281+
info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module normaliser expected to return array of 2 strings")));
282+
return;
283+
}
284+
285+
zval **data;
286+
ulong index = 0;
287+
HashPosition pos;
288+
289+
for (zend_hash_internal_pointer_reset_ex(ht, &pos);
290+
SUCCESS == zend_hash_get_current_data_ex(ht, (void **) &data, &pos);
291+
zend_hash_move_forward_ex(ht, &pos)
292+
) {
293+
294+
if (Z_TYPE_P(*data) != IS_STRING) {
295+
convert_to_string(*data);
296+
}
297+
298+
switch(index++) {
299+
case 0: // normalised path
300+
normalised_path = estrndup(Z_STRVAL_PP(data), Z_STRLEN_PP(data));
301+
break;
302+
303+
case 1: // normalised module id
304+
module_name = estrndup(Z_STRVAL_PP(data), Z_STRLEN_PP(data));
305+
break;
306+
}
307+
}
308+
309+
zval_ptr_dtor(&normaliser_result);
310+
}
216311

217312
char *normalised_module_id = (char *)emalloc(strlen(normalised_path)+1+strlen(module_name)+1);
218313
*normalised_module_id = 0;
@@ -305,16 +400,6 @@ V8JS_METHOD(require)
305400
convert_to_string(module_code);
306401
}
307402

308-
// Check that some code has been returned
309-
if (Z_STRLEN_P(module_code)==0) {
310-
zval_ptr_dtor(&module_code);
311-
efree(normalised_module_id);
312-
efree(normalised_path);
313-
314-
info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module loader callback did not return code")));
315-
return;
316-
}
317-
318403
// Create a template for the global object and set the built-in global functions
319404
v8::Handle<v8::ObjectTemplate> global = v8::ObjectTemplate::New();
320405
global->Set(V8JS_SYM("print"), v8::FunctionTemplate::New(isolate, V8JS_MN(print)), v8::ReadOnly);
@@ -347,7 +432,7 @@ V8JS_METHOD(require)
347432
// Enter the module context
348433
v8::Context::Scope scope(context);
349434
// Set script identifier
350-
v8::Local<v8::String> sname = V8JS_SYM(normalised_module_id);
435+
v8::Local<v8::String> sname = V8JS_STR(normalised_module_id);
351436

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

0 commit comments

Comments
 (0)