Forráskód Böngészése

Allow custom module normalisation

Stefan Siegl 9 éve
szülő
commit
67a9de01bd

+ 13 - 0
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.

+ 31 - 0
tests/commonjs_cust_normalise_001.phpt

@@ -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 - 0
tests/commonjs_cust_normalise_002.phpt

@@ -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 - 0
tests/commonjs_cust_normalise_003.phpt

@@ -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 - 0
tests/commonjs_cust_normalise_004.phpt

@@ -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 - 0
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)

+ 1 - 0
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<char *> modules_stack;
   std::vector<char *> modules_base;

+ 99 - 4
v8js_methods.cc

@@ -8,6 +8,7 @@
   +----------------------------------------------------------------------+
   | Author: Jani Taskinen <[email protected]>                         |
   | Author: Patrick Reilly <[email protected]>                             |
+  | Author: Stefan Siegl <[email protected]>                          |
   +----------------------------------------------------------------------+
 */
 
@@ -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();
+
+			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;
+		}
 
-	v8js_commonjs_normalise_identifier(c->modules_base.back(), module_id, normalised_path, module_name);
+		// 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;