diff --git a/README.md b/README.md index 8cf45a3..18ce894 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/tests/commonjs_cust_normalise_001.phpt b/tests/commonjs_cust_normalise_001.phpt new file mode 100644 index 0000000..c9b9edf --- /dev/null +++ b/tests/commonjs_cust_normalise_001.phpt @@ -0,0 +1,31 @@ +--TEST-- +Test V8Js::setModuleNormaliser : Custom normalisation #001 +--SKIPIF-- + +--FILE-- +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=== diff --git a/tests/commonjs_cust_normalise_002.phpt b/tests/commonjs_cust_normalise_002.phpt new file mode 100644 index 0000000..792ebaa --- /dev/null +++ b/tests/commonjs_cust_normalise_002.phpt @@ -0,0 +1,33 @@ +--TEST-- +Test V8Js::setModuleNormaliser : Custom normalisation #002 +--SKIPIF-- + +--FILE-- +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=== diff --git a/tests/commonjs_cust_normalise_003.phpt b/tests/commonjs_cust_normalise_003.phpt new file mode 100644 index 0000000..c417eb6 --- /dev/null +++ b/tests/commonjs_cust_normalise_003.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test V8Js::setModuleNormaliser : Custom normalisation #003 +--SKIPIF-- + +--FILE-- +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=== diff --git a/tests/commonjs_cust_normalise_004.phpt b/tests/commonjs_cust_normalise_004.phpt new file mode 100644 index 0000000..3086396 --- /dev/null +++ b/tests/commonjs_cust_normalise_004.phpt @@ -0,0 +1,42 @@ +--TEST-- +Test V8Js::setModuleNormaliser : Custom normalisation #004 +--SKIPIF-- + +--FILE-- +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=== diff --git a/v8js_class.cc b/v8js_class.cc index 8faec51..f538a82 100644 --- a/v8js_class.cc +++ b/v8js_class.cc @@ -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); } @@ -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 */ @@ -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) @@ -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() @@ -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) diff --git a/v8js_class.h b/v8js_class.h index 873591a..ab7b7aa 100644 --- a/v8js_class.h +++ b/v8js_class.h @@ -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 modules_stack; std::vector modules_base; diff --git a/v8js_methods.cc b/v8js_methods.cc index 7757c0e..8832df2 100644 --- a/v8js_methods.cc +++ b/v8js_methods.cc @@ -8,6 +8,7 @@ +----------------------------------------------------------------------+ | Author: Jani Taskinen | | Author: Patrick Reilly | + | Author: Stefan Siegl | +----------------------------------------------------------------------+ */ @@ -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; - v8js_commonjs_normalise_identifier(c->modules_base.back(), module_id, 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(); + + 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;