浏览代码

Merge remote-tracking branch 'origin/master'

This primarily ports features introduced with V8Js 0.4.0
to the PHP7 branch.
Stefan Siegl 9 年之前
父节点
当前提交
2d34adb91f

+ 13 - 0
README.md

@@ -81,6 +81,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===

+ 17 - 0
tests/commonjs_fatal_error.phpt

@@ -0,0 +1,17 @@
+--TEST--
+Test V8Js::setModuleLoader : Handle fatal errors gracefully
+--SKIPIF--
+<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
+--FILE--
+<?php
+$v8 = new V8Js();
+
+$v8->setModuleLoader(function() {
+    trigger_error('some fatal error', E_USER_ERROR);
+});
+
+$v8->executeString(' require("foo"); ');
+?>
+===EOF===
+--EXPECTF--
+Fatal error: some fatal error in %s%ecommonjs_fatal_error.php on line 5

+ 27 - 0
tests/commonjs_source_naming.phpt

@@ -0,0 +1,27 @@
+--TEST--
+Test V8Js::setModuleLoader : Module source naming
+--SKIPIF--
+<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
+--FILE--
+<?php
+
+$JS = <<< EOT
+require('./foo//bar');
+EOT;
+
+$v8 = new V8Js();
+$v8->setModuleLoader(function($module) {
+    // return code with syntax errors to provoke script exception
+    return "foo(blar);";
+});
+
+try {
+    $v8->executeString($JS, 'commonjs_source_naming.js');
+} catch (V8JsScriptException $e) {
+    var_dump($e->getJsFileName());
+}
+?>
+===EOF===
+--EXPECT--
+string(7) "foo/bar"
+===EOF===

+ 36 - 0
tests/issue_185_001.phpt

@@ -0,0 +1,36 @@
+--TEST--
+Test V8::executeString() : Issue #185 this on direct invocation of method
+--SKIPIF--
+<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
+--FILE--
+<?php
+
+$v8 = new V8Js();
+
+$JS = <<<EOT
+
+function Bar(i) {
+    this.theValue = i;
+}
+Bar.prototype.tell = function() {
+    var_dump(this.theValue);
+    var_dump(typeof this.exit);
+};
+var inst = new Bar(23);
+var fn = inst.tell;
+fn();
+EOT;
+
+$v8->executeString($JS);
+
+// now fetch `inst` from V8 and call method from PHP
+$fn = $v8->executeString('(inst.tell)');
+$fn();
+?>
+===EOF===
+--EXPECT--
+NULL
+string(8) "function"
+NULL
+string(8) "function"
+===EOF===

+ 28 - 0
tests/issue_185_002.phpt

@@ -0,0 +1,28 @@
+--TEST--
+Test V8::executeString() : Issue #185 this on function invocation
+--SKIPIF--
+<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
+--FILE--
+<?php
+
+$v8 = new V8Js();
+
+$JS = <<<EOT
+
+function fn() {
+    var_dump(typeof this.exit);
+};
+fn();
+EOT;
+
+$v8->executeString($JS);
+
+// now fetch `inst` from V8 and call method from PHP
+$fn = $v8->executeString('(fn)');
+$fn();
+?>
+===EOF===
+--EXPECT--
+string(8) "function"
+string(8) "function"
+===EOF===

+ 32 - 0
tests/issue_185_basic.phpt

@@ -0,0 +1,32 @@
+--TEST--
+Test V8::executeString() : Issue #185 Wrong this on V8Object method invocation
+--SKIPIF--
+<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
+--FILE--
+<?php
+
+$v8 = new V8Js();
+
+$JS = <<<EOT
+
+function Bar(i) {
+    this.theValue = i;
+}
+Bar.prototype.tell = function() {
+    var_dump(this.theValue);
+};
+var inst = new Bar(23);
+inst.tell();
+EOT;
+
+$v8->executeString($JS);
+
+// now fetch `inst` from V8 and call method from PHP
+$inst = $v8->executeString('(inst)');
+$inst->tell();
+?>
+===EOF===
+--EXPECT--
+int(23)
+int(23)
+===EOF===

+ 26 - 1
v8js_class.cc

@@ -85,7 +85,9 @@ static void v8js_free_storage(zend_object *object TSRMLS_DC) /* {{{ */
 	v8js_ctx *c = v8js_ctx_fetch_object(object);
 
 	zend_object_std_dtor(&c->std TSRMLS_CC);
+
 	zval_dtor(&c->pending_exception);
+	zval_dtor(&c->module_normaliser);
 	zval_dtor(&c->module_loader);
 
 	/* Delete PHP global object from JavaScript */
@@ -358,6 +360,7 @@ static PHP_METHOD(V8Js, __construct)
 	c->memory_limit = 0;
 	c->memory_limit_hit = false;
 
+	ZVAL_NULL(&c->module_normaliser);
 	ZVAL_NULL(&c->module_loader);
 
 	/* Include extensions used by this context */
@@ -671,6 +674,22 @@ 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 = Z_V8JS_CTX_OBJ_P(getThis());
+	ZVAL_COPY(&c->module_normaliser, callable);
+}
+/* }}} */
+
 /* {{{ proto void V8Js::setModuleLoader(string module)
  */
 static PHP_METHOD(V8Js, setModuleLoader)
@@ -964,6 +983,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()
@@ -997,6 +1021,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)
@@ -1039,7 +1064,7 @@ static void v8js_unset_property(zval *object, zval *member, void **cache_slot TS
 	/* Global PHP JS object */
 	v8::Local<v8::String> object_name_js = v8::Local<v8::String>::New(isolate, c->object_name);
 	v8::Local<v8::Object> jsobj = V8JS_GLOBAL(isolate)->Get(object_name_js)->ToObject();
-	
+
 	/* Delete value from PHP JS object */
 	jsobj->Delete(V8JS_SYML(Z_STRVAL_P(member), Z_STRLEN_P(member)));
 

+ 2 - 0
v8js_class.h

@@ -49,7 +49,9 @@ 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;
   std::map<char *, v8js_persistent_obj_t, cmp_str> modules_loaded;

+ 135 - 26
v8js_methods.cc

@@ -8,6 +8,7 @@
   +----------------------------------------------------------------------+
   | Author: Jani Taskinen <[email protected]>                         |
   | Author: Patrick Reilly <[email protected]>                             |
+  | Author: Stefan Siegl <[email protected]>                          |
   +----------------------------------------------------------------------+
 */
 
@@ -91,10 +92,20 @@ static void v8js_dumper(v8::Isolate *isolate, v8::Local<v8::Value> var, int leve
 	}
 
 	v8::TryCatch try_catch; /* object.toString() can throw an exception */
-	v8::Local<v8::String> details = var->ToDetailString();
-	if (try_catch.HasCaught()) {
-		details = V8JS_SYM("<toString threw exception>");
+	v8::Local<v8::String> details;
+
+	if(var->IsRegExp()) {
+		v8::RegExp *re = v8::RegExp::Cast(*var);
+		details = re->GetSource();
+	}
+	else {
+		details = var->ToDetailString();
+
+		if (try_catch.HasCaught()) {
+			details = V8JS_SYM("<toString threw exception>");
+		}
 	}
+
 	v8::String::Utf8Value str(details);
 	const char *valstr = ToCString(str);
 	size_t valstr_len = details->ToString()->Utf8Length();
@@ -112,7 +123,7 @@ static void v8js_dumper(v8::Isolate *isolate, v8::Local<v8::Value> var, int leve
 	}
 	else if (var->IsRegExp())
 	{
-		php_printf("regexp(%s)\n", valstr);
+		php_printf("regexp(/%s/)\n", valstr);
 	}
 	else if (var->IsArray())
 	{
@@ -207,12 +218,100 @@ 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 (Z_TYPE(c->module_normaliser) == IS_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);
+		v8js_commonjs_normalise_identifier(c->modules_base.back(), module_id, normalised_path, module_name);
+	}
+	else {
+		// Call custom normaliser
+		int call_result;
+		zval params[2];
+		zval normaliser_result;
+
+		zend_try {
+			{
+				isolate->Exit();
+				v8::Unlocker unlocker(isolate);
+
+				ZVAL_STRING(&params[0], c->modules_base.back());
+				ZVAL_STRING(&params[1], 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_dtor(&params[0]);
+		zval_dtor(&params[1]);
+
+		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(normaliser_result) != IS_ARRAY) {
+			zval_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_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;
+
+		ZEND_HASH_FOREACH_VAL(ht, data) {
+			if (Z_TYPE_P(data) != IS_STRING) {
+				convert_to_string(data);
+			}
+
+			switch(index++) {
+			case 0: // normalised path
+				normalised_path = estrndup(Z_STRVAL_P(data), Z_STRLEN_P(data));
+				break;
+
+			case 1: // normalised module id
+				module_name = estrndup(Z_STRVAL_P(data), Z_STRLEN_P(data));
+				break;
+			}
+		}
+		ZEND_HASH_FOREACH_END();
+
+		zval_dtor(&normaliser_result);
+	}
 
 	char *normalised_module_id = (char *)emalloc(strlen(normalised_path)+1+strlen(module_name)+1);
 	*normalised_module_id = 0;
@@ -252,19 +351,38 @@ V8JS_METHOD(require)
 	// Callback to PHP to load the module code
 
 	zval module_code;
-
+	int call_result;
 	zval params[1];
-	ZVAL_STRING(&params[0], normalised_module_id);
 
-	if (FAILURE == call_user_function_ex(EG(function_table), NULL, &c->module_loader, &module_code, 1, params, 0, NULL TSRMLS_CC)) {
-		zval_dtor(&params[0]);
+	zend_try {
+		{
+			isolate->Exit();
+			v8::Unlocker unlocker(isolate);
+
+			ZVAL_STRING(&params[0], normalised_module_id);
+			call_result = call_user_function_ex(EG(function_table), NULL, &c->module_loader, &module_code, 1, params, 0, NULL TSRMLS_CC);
+		}
+
+		isolate->Enter();
+
+		if (call_result == FAILURE) {
+			info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module loader callback failed")));
+		}
+	}
+	zend_catch {
+		v8js_terminate_execution(isolate);
+		V8JSG(fatal_error_abort) = 1;
+		call_result = FAILURE;
+	}
+	zend_end_try();
+
+	zval_dtor(&params[0]);
+
+	if (call_result == FAILURE) {
 		efree(normalised_module_id);
 		efree(normalised_path);
-
-		info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module loader callback failed")));
 		return;
 	}
-	zval_dtor(&params[0]);
 
 	// Check if an exception was thrown
 	if (EG(exception)) {
@@ -282,19 +400,10 @@ V8JS_METHOD(require)
 		convert_to_string(&module_code);
 	}
 
-	// Check that some code has been returned
-	if (Z_STRLEN(module_code) == 0) {
-		zval_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);
+	global->Set(V8JS_SYM("var_dump"), v8::FunctionTemplate::New(isolate, V8JS_MN(var_dump)), v8::ReadOnly);
 	global->Set(V8JS_SYM("sleep"), v8::FunctionTemplate::New(isolate, V8JS_MN(sleep)), v8::ReadOnly);
 	global->Set(V8JS_SYM("require"), v8::FunctionTemplate::New(isolate, V8JS_MN(require), v8::External::New(isolate, c)), v8::ReadOnly);
 
@@ -323,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("require");
+	v8::Local<v8::String> sname = V8JS_STR(normalised_module_id);
 
 	v8::Local<v8::String> source = V8JS_ZSTR(Z_STR(module_code));
 	zval_ptr_dtor(&module_code);

+ 11 - 1
v8js_v8object_class.cc

@@ -286,6 +286,7 @@ static int v8js_v8object_call_method(zend_string *method, zend_object *object, I
 
 		v8::Local<v8::String> method_name = V8JS_ZSYM(method);
 		v8::Local<v8::Object> v8obj = v8::Local<v8::Value>::New(isolate, obj->v8obj)->ToObject();
+		v8::Local<v8::Object> thisObj;
 		v8::Local<v8::Function> cb;
 
 		if (method_name->Equals(V8JS_SYM(V8JS_V8_INVOKE_FUNC_NAME))) {
@@ -294,6 +295,15 @@ static int v8js_v8object_call_method(zend_string *method, zend_object *object, I
 			cb = v8::Local<v8::Function>::Cast(v8obj->Get(method_name));
 		}
 
+		// If a method is invoked on V8Object, then set the object itself as
+		// "this" on JS side.  Otherwise fall back to global object.
+		if (obj->std.ce == php_ce_v8object) {
+			thisObj = v8obj;
+		}
+		else {
+			thisObj = V8JS_GLOBAL(isolate);
+		}
+
 		v8::Local<v8::Value> *jsArgv = static_cast<v8::Local<v8::Value> *>(alloca(sizeof(v8::Local<v8::Value>) * argc));
 		v8::Local<v8::Value> js_retval;
 
@@ -302,7 +312,7 @@ static int v8js_v8object_call_method(zend_string *method, zend_object *object, I
 			jsArgv[i] = v8::Local<v8::Value>::New(isolate, zval_to_v8js(&argv[i], isolate TSRMLS_CC));
 		}
 
-		return cb->Call(V8JS_GLOBAL(isolate), argc, jsArgv);
+		return cb->Call(thisObj, argc, jsArgv);
 	};
 
 	v8js_v8_call(obj->ctx, &return_value, obj->flags, obj->ctx->time_limit, obj->ctx->memory_limit, v8_call TSRMLS_CC);