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

fix merge conflict

Patrick Reilly 12 éve
szülő
commit
6f44220328

+ 52 - 25
README.md

@@ -1,30 +1,29 @@
 V8Js
 ====
 
-This is a PHP extension for Google's V8 Javascript engine 
+V8Js is a PHP extension for Google's V8 Javascript engine.
+
+The extension allows you to execute Javascript code in a secure sandbox from PHP. The executed code can be restricted using a time limit and/or memory limit. This provides the possibility to execute untrusted code with confidence.
 
 
 Minimum requirements
 --------------------
 
-- V8 JavaScript Engine library version 2.5.8 <http://code.google.com/p/v8/> (trunk)
+- V8 Javascript Engine library (libv8) version 3.2.4 or above <http://code.google.com/p/v8/> (trunk)
 
-	V8 is Google's open source JavaScript engine.
+	V8 is Google's open source Javascript engine.
 	V8 is written in C++ and is used in Google Chrome, the open source browser from Google.
-	V8 implements ECMAScript as specified in ECMA-262, 5th edition, and runs on Windows (XP or newer), 
-	Mac OS X (10.5 or newer), and Linux systems that use IA-32, x64, or ARM processors.
-	V8 can run standalone, or can be embedded into any C++ application.
-	You can find more information here:
-	<http://code.google.com/p/v8/>
+	V8 implements ECMAScript as specified in ECMA-262, 5th edition.
+    This extension makes use of V8 isolates to ensure separation between multiple V8Js instances, hence the need for 3.2.4 or above.
+
+- PHP 5.3.3+
 
-- PHP 5.3.3+ (non-ZTS build preferred)
-  Note: V8 engine is not natively thread safe and this extension
-  has not been designed to work around it either yet and might or
-  might not work properly with ZTS enabled PHP. :)
+  This embedded implementation of the V8 engine uses thread locking so it should work with ZTS enabled.
+  However, this has not been tested yet.
 
 
-API
-===
+PHP API
+=======
 
     class V8Js
     {
@@ -37,26 +36,31 @@ API
         /* Methods */
 
         // Initializes and starts V8 engine and Returns new V8Js object with it's own V8 context.
-        public __construct ( [string object_name = "PHP" [, array variables = NULL [, array extensions = NULL [, bool report_uncaught_exceptions = TRUE]]] )
+        public __construct ( [ string $object_name = "PHP" [, array $variables = NULL [, array $extensions = NULL [, bool $report_uncaught_exceptions = TRUE ] ] ] )
+
+        // Provide a function or method to be used to load required modules. This can be any valid PHP callable.
+        // The loader function will receive the normalised module path and should return Javascript code to be executed.
+        public setModuleLoader ( callable $loader )
 
         // Compiles and executes script in object's context with optional identifier string.
-        public mixed V8Js::executeString( string script [, string identifier [, int flags = V8Js::FLAG_NONE]])
+        // A time limit (milliseconds) and/or memory limit (bytes) can be provided to restrict execution. These options will throw a V8JsTimeLimitException or V8JsMemoryLimitException.
+        public mixed V8Js::executeString( string $script [, string $identifier [, int $flags = V8Js::FLAG_NONE [, int $time_limit = 0 [, int $memory_limit = 0]]]])
 
         // Returns uncaught pending exception or null if there is no pending exception.
-        public V8JsException V8Js::getPendingException( void )
+        public V8JsScriptException V8Js::getPendingException( )
 
         /** Static methods **/
 
         // Registers persistent context independent global Javascript extension.
         // NOTE! These extensions exist until PHP is shutdown and they need to be registered before V8 is initialized. 
         // For best performance V8 is initialized only once per process thus this call has to be done before any V8Js objects are created!
-        public static bool V8Js::registerExtension(string ext_name, string script [, array deps [, bool auto_enable = FALSE]])
+        public static bool V8Js::registerExtension( string $extension_name, string $code [, array $dependenciess [, bool $auto_enable = FALSE ] ] )
 
         // Returns extensions successfully registered with V8Js::registerExtension().
-        public static array V8Js::getExtensions( void )
+        public static array V8Js::getExtensions( )
     }
 
-    final class V8JsException extends Exception
+    final class V8JsScriptException extends Exception
     {
         /* Properties */
         protected string JsFileName = NULL;
@@ -65,10 +69,33 @@ API
         protected string JsTrace = NULL;
         
         /* Methods */
-        final public string getJsFileName( void )
-        final public int getJsLineNumber( void )
-        final public string getJsSourceLine( void )
-        final public string getJsTrace( void )
+        final public string getJsFileName( )
+        final public int getJsLineNumber( )
+        final public string getJsSourceLine( )
+        final public string getJsTrace( )
     }
-    
+
+    final class V8JsTimeLimitException extends Exception
+    {
+    }
+
+    final class V8JsMemoryLimitException extends Exception
+    {
+    }
+
+Javascript API
+==============
+
+    // Print a string.
+    print(string);
+
+    // Dump the contents of a variable.
+    var_dump(value);
+
+    // Terminate Javascript execution immediately.
+    exit();
+
+    // CommonJS Module support to require external code.
+    // This makes use of the PHP module loader provided via V8Js::setModuleLoader (see PHP API above).
+    require("path/to/module");
 

+ 1 - 1
config.m4

@@ -67,7 +67,7 @@ LDFLAGS=$old_LDFLAGS
     AC_DEFINE_UNQUOTED([PHP_V8_VERSION], "$ac_cv_v8_version", [ ])
   fi
   
-  PHP_NEW_EXTENSION(v8js, v8js.cc v8js_convert.cc v8js_methods.cc v8js_variables.cc, $ext_shared)
+  PHP_NEW_EXTENSION(v8js, v8js.cc v8js_convert.cc v8js_methods.cc v8js_variables.cc v8js_commonjs.cc, $ext_shared, , "-std=c++0x")
 
   PHP_ADD_MAKEFILE_FRAGMENT
 fi

+ 74 - 8
php_v8js_macros.h

@@ -22,8 +22,20 @@
 #ifndef PHP_V8JS_MACROS_H
 #define PHP_V8JS_MACROS_H
 
+extern "C" {
+#include "php.h"
+#include "php_v8js.h"
+}
+
 #include <v8.h>
 
+#include <chrono>
+#include <stack>
+#include <thread>
+
+#include <map>
+#include <vector>
+
 /* V8Js Version */
 #define V8JS_VERSION "0.1.3"
 
@@ -80,22 +92,76 @@ extern zend_class_entry *php_ce_v8_object;
 extern zend_class_entry *php_ce_v8_function;
 
 /* Create PHP V8 object */
-void php_v8js_create_v8(zval *, v8::Handle<v8::Value>, int TSRMLS_DC);
+void php_v8js_create_v8(zval *, v8::Handle<v8::Value>, int, v8::Isolate * TSRMLS_DC);
 
 /* Fetch V8 object properties */
-int php_v8js_v8_get_properties_hash(v8::Handle<v8::Value>, HashTable *, int TSRMLS_DC);
+int php_v8js_v8_get_properties_hash(v8::Handle<v8::Value>, HashTable *, int, v8::Isolate * TSRMLS_DC);
 
 /* Convert zval into V8 value */
-v8::Handle<v8::Value> zval_to_v8js(zval * TSRMLS_DC);
+v8::Handle<v8::Value> zval_to_v8js(zval *, v8::Isolate * TSRMLS_DC);
 
 /* Convert V8 value into zval */
-int v8js_to_zval(v8::Handle<v8::Value>, zval *, int TSRMLS_DC);
-
-/* Register builtin methods into passed object */
-void php_v8js_register_methods(v8::Handle<v8::ObjectTemplate>);
+int v8js_to_zval(v8::Handle<v8::Value>, zval *, int, v8::Isolate * TSRMLS_DC);
 
 /* Register accessors into passed object */
-void php_v8js_register_accessors(v8::Local<v8::ObjectTemplate>, zval * TSRMLS_DC);
+void php_v8js_register_accessors(v8::Local<v8::ObjectTemplate>, zval *, v8::Isolate * TSRMLS_DC);
+
+/* {{{ Context container */
+struct php_v8js_ctx {
+  zend_object std;
+  v8::Persistent<v8::String> object_name;
+  v8::Persistent<v8::Context> context;
+  zend_bool report_uncaught;
+  zval *pending_exception;
+  int in_execution;
+  v8::Isolate *isolate;
+  bool time_limit_hit;
+  bool memory_limit_hit;
+  v8::Persistent<v8::FunctionTemplate> global_template;
+  zval *module_loader;
+  std::vector<char *> modules_stack;
+  std::vector<char *> modules_base;
+};
+/* }}} */
+
+// Timer context
+struct php_v8js_timer_ctx
+{
+  long time_limit;
+  long memory_limit;
+  std::chrono::time_point<std::chrono::high_resolution_clock> time_point;
+  php_v8js_ctx *v8js_ctx;
+};
+
+/* Module globals */
+ZEND_BEGIN_MODULE_GLOBALS(v8js)
+  int v8_initialized;
+  HashTable *extensions;
+  int disposed_contexts; /* Disposed contexts since last time V8 did GC */
+
+  /* Ini globals */
+  char *v8_flags; /* V8 command line flags */
+  int max_disposed_contexts; /* Max disposed context allowed before forcing V8 GC */
+
+  // Timer thread globals
+  std::stack<php_v8js_timer_ctx *> timer_stack;
+  std::thread *timer_thread;
+  std::mutex timer_mutex;
+  bool timer_stop;
+
+  std::map<char *, v8::Handle<v8::Object> > modules_loaded;
+ZEND_END_MODULE_GLOBALS(v8js)
+
+extern zend_v8js_globals v8js_globals;
+
+#ifdef ZTS
+# define V8JSG(v) TSRMG(v8js_globals_id, zend_v8js_globals *, v)
+#else
+# define V8JSG(v) (v8js_globals.v)
+#endif
+
+/* Register builtin methods into passed object */
+void php_v8js_register_methods(v8::Handle<v8::ObjectTemplate>, php_v8js_ctx *c);
 
 #endif	/* PHP_V8JS_MACROS_H */
 

+ 1 - 1
samples/test_call.php

@@ -69,6 +69,6 @@ try {
   $blaa->executeString('PHP.obj.foo(1,2,3);', "call_test1 #8.js");
   echo "------------\n";
 
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
   echo $e->getMessage(), "\n";
 }

+ 1 - 1
samples/test_closure.php

@@ -6,6 +6,6 @@ $a->func = function ($a) { echo "Closure..\n"; };
 try {
   $a->executeString("print(PHP.func); PHP.func(1);", "closure_test.js");
   $a->executeString("print(PHP.func); PHP.func(1);", "closure_test.js");
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
   echo $e->getMessage(), "\n";
 }

+ 1 - 1
samples/test_date.php

@@ -4,6 +4,6 @@ $a = new V8Js();
 
 try {
 	var_dump($a->executeString("date = new Date('September 8, 1975 09:00:00'); print(date + '\\n'); date;", "test.js"));
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	echo $e->getMessage(), "\n";
 }

+ 1 - 1
samples/test_dumper.php

@@ -54,6 +54,6 @@ try {
   $a->executeString("var patt1=/[^a-h]/g; var_dump(patt1);", "call_test5.js");
   $a->executeString("var_dump(Math.PI, Infinity, null, undefined);", "call_test6.js");
 //  $a->executeString($JS);
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
   echo $e->getMessage(), "\n";
 }

+ 1 - 1
samples/test_exception.php

@@ -19,7 +19,7 @@
     
     try {
       $foo = new Foo();
-    } catch (V8JsException $e) {
+    } catch (V8JsScriptException $e) {
       echo "PHP Exception: ", $e->getMessage(), "\n"; //var_dump($e);
     }
 

+ 1 - 1
samples/test_exception2.php

@@ -27,7 +27,7 @@
     
     try {
       $foo = new Foo();
-    } catch (V8JsException $e) {
+    } catch (V8JsScriptException $e) {
       echo "PHP Exception: ", $e->getMessage(), "\n";
     }
 

+ 1 - 1
samples/test_extend.php

@@ -18,6 +18,6 @@ echo $a;
 
 try {
 	$a->executeString("PHP.mytest(PHP.foo, PHP.my_private, PHP.my_protected);", "test7.js");
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }

+ 4 - 4
samples/test_method.php

@@ -23,13 +23,13 @@ $a->executeString("PHP.myobj.mytest(3.14, 42, null);", "test3.js");
 // Invalid parameters
 try {
 	$a->executeString("PHP.myobj.mytest();", "test4.js");
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	echo $e->getMessage(), "\n";
 }
 
 try {
 	$a->executeString("PHP.myobj.mytest('arg1', 'arg2', 'arg3', 'extra_arg');", "test5.js");
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	echo $e->getMessage(), "\n";
 }
 
@@ -37,12 +37,12 @@ try {
 try {
 //	date_default_timezone_set("UTC");
 	$a->executeString("date = new Date('September 8, 1975 09:00:00'); PHP.print(date); PHP.myobj.mytest(date, PHP.myobj, new Array(1,2,3));", "test6.js");
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }
 
 try {
 	$a->executeString("PHP.myobj.mytest(PHP.myobj, new Array(1,2,3), new Array('foo', 'bar', PHP.myobj));", "test7.js");
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }

+ 2 - 2
test.php

@@ -139,7 +139,7 @@ $a->executeString("bigarray()", "test1.js");
 try {
   echo($a->executeString($jstparser, "test2.js")), "\n";
   var_dump($a->executeString($jsontemplate, "test1.js"));
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
   echo $e->getMessage();
 }
 
@@ -168,6 +168,6 @@ var_dump($b->executeString("print('foobar\\n');"));
 
 try {
   $b->executeString("foobar; foo();", "extest.js");
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
   var_dump($e, $e->getJsFileName(), $e->getJsLineNumber(), $e->getJsSourceLine(), $e->getJsTrace());
 }

+ 1 - 1
tests/basic.phpt

@@ -14,7 +14,7 @@ $v8 = new V8Js();
 
 try {
 	var_dump($v8->executeString($JS, 'basic.js'));
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }
 ?>

+ 1 - 1
tests/closures_basic.phpt

@@ -10,7 +10,7 @@ $a->func = function ($arg) { echo "Hello {$arg}, I'm Closure!\n"; };
 
 try {
   $a->executeString('print(PHP.func + "\n"); PHP.func("foobar");', "closure_test.js");
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
   echo $e->getMessage(), "\n";
 }
 ?>

+ 1 - 1
tests/closures_dynamic.phpt

@@ -19,7 +19,7 @@ $a->func = function ($arg) use ($b) { return call_user_func($b, $arg); };
 
 try {
   $a->executeString('print(PHP.func + "\n"); print(PHP.func("world") + "\n");', "closure_test.js");
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
   echo $e->getMessage(), "\n";
 }
 ?>

+ 33 - 0
tests/commonjs_modules.phpt

@@ -0,0 +1,33 @@
+--TEST--
+Test V8Js::setModuleLoader : CommonJS modules
+--SKIPIF--
+<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
+--FILE--
+<?php
+
+$JS = <<< EOT
+require("path/to/module1");
+EOT;
+
+$v8 = new V8Js();
+$v8->setModuleLoader(function($module) {
+  switch ($module) {
+    case 'path/to/module1':
+      return 'print(' . json_encode($module . PHP_EOL) . ');require("./module2");';
+
+    case 'path/to/module2':
+      return 'print(' . json_encode($module . PHP_EOL) . ');require("../../module3");';
+
+    default:
+      return 'print(' . json_encode($module . PHP_EOL) . ');';
+  }
+});
+
+$v8->executeString($JS, 'module.js');
+?>
+===EOF===
+--EXPECT--
+path/to/module1
+path/to/module2
+module3
+===EOF===

+ 1 - 1
tests/construct.phpt

@@ -14,7 +14,7 @@ $v8->__construct();
 
 try {
 	$v8->executeString($JS, 'basic.js');
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }
 ?>

+ 5 - 5
tests/context_preserving.phpt

@@ -27,7 +27,7 @@ $a->ctx = '#1';
 try {
 	echo '1. ';
 	$a->executeString($JS_set, 'set.js');
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }
 
@@ -38,7 +38,7 @@ $b->ctx = '#2';
 try {
 	echo '2. ';
 	$b->executeString($JS_change, 'change.js');
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }
 
@@ -46,7 +46,7 @@ try {
 try {
 	echo '3. ';
 	$a->executeString($JS_read, 'read.js');
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }
 
@@ -54,7 +54,7 @@ try {
 try {
 	echo '4. ';
 	$a->executeString($JS_change, 'change.js');
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }
 
@@ -62,7 +62,7 @@ try {
 try {
 	echo '5. ';
 	$a->executeString($JS_read, 'read.js');
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }
 ?>

+ 2 - 2
tests/context_separation.phpt

@@ -14,7 +14,7 @@ $a->foo = 'from first.js';
 
 try {
 	$a->executeString($JS, 'first.js');
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }
 
@@ -25,7 +25,7 @@ $b->foo = 'from second.js';
 
 try {
 	$b->executeString($JS, 'second.js');
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }
 

+ 3 - 3
tests/exception.phpt

@@ -1,5 +1,5 @@
 --TEST--
-Test V8::executeString() : V8JsException
+Test V8::executeString() : V8JsScriptException
 --SKIPIF--
 <?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
 --FILE--
@@ -13,13 +13,13 @@ $v8 = new V8Js();
 
 try {
 	$v8->executeString($JS, 'exception.js');
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }
 ?>
 ===EOF===
 --EXPECTF--
-object(V8JsException)#2 (11) {
+object(V8JsScriptException)#2 (11) {
   ["message":protected]=>
   string(75) "exception.js:1: ReferenceError: this_function_does_not_exist is not defined"
   ["string":"Exception":private]=>

+ 1 - 1
tests/exception_propagation_1.phpt

@@ -27,7 +27,7 @@ class Foo {
     
 try {
 	$foo = new Foo();
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	echo "PHP Exception: ", $e->getMessage(), "\n"; //var_dump($e);
 }
 ?>

+ 2 - 2
tests/exception_propagation_2.phpt

@@ -27,13 +27,13 @@ class Foo {
     
 try {
 	$foo = new Foo();
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	echo "PHP Exception: ", $e->getMessage(), "\n"; //var_dump($e);
 }
 ?>
 ===EOF===
 --EXPECTF--
-object(V8JsException)#3 (11) {
+object(V8JsScriptException)#3 (11) {
   ["message":protected]=>
   string(49) "throw_0:1: ReferenceError: fooobar is not defined"
   ["string":"Exception":private]=>

+ 1 - 1
tests/exception_propagation_3.phpt

@@ -26,7 +26,7 @@ class Foo {
     
 try {
 	$foo = new Foo();
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	echo "PHP Exception: ", $e->getMessage(), "\n";
 }
 ?>

+ 1 - 1
tests/execute_flags.phpt

@@ -15,7 +15,7 @@ try {
 	var_dump($v8->executeString($js, 'assoc_no_flags.js'));
 	echo "---\n";
 	var_dump($v8->executeString($js, 'assoc_force_to_array.js', V8Js::FLAG_FORCE_ARRAY));
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }
 ?>

+ 1 - 1
tests/execute_flags_args.phpt

@@ -17,7 +17,7 @@ try {
 	$v8->executeString($js, 'no_flags.js');
 	echo "---\n";
 	$v8->executeString($js, 'force_to_array.js', V8Js::FLAG_FORCE_ARRAY);
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }
 ?>

+ 72 - 0
tests/memory_limit.phpt

@@ -0,0 +1,72 @@
+--TEST--
+Test V8::executeString() : Time limit
+--SKIPIF--
+<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
+--FILE--
+<?php
+$JS = <<< EOT
+var text = "abcdefghijklmnopqrstuvwyxz0123456789";
+var memory = "";
+for (var i = 0; i < 1000000; ++i) {
+    memory += text;
+}
+EOT;
+
+$v8 = new V8Js();
+
+try {
+    var_dump($v8->executeString($JS, 'basic.js', V8Js::FLAG_NONE, 0, 10000000));
+} catch (V8JsMemoryLimitException $e) {
+    var_dump($e);
+}
+?>
+===EOF===
+--EXPECT--
+object(V8JsMemoryLimitException)#2 (7) {
+  ["message":protected]=>
+  string(46) "Script memory limit of 10000000 bytes exceeded"
+  ["string":"Exception":private]=>
+  string(0) ""
+  ["code":protected]=>
+  int(0)
+  ["file":protected]=>
+  string(36) "/var/www/v8js/tests/memory_limit.php"
+  ["line":protected]=>
+  int(13)
+  ["trace":"Exception":private]=>
+  array(1) {
+    [0]=>
+    array(6) {
+      ["file"]=>
+      string(36) "/var/www/v8js/tests/memory_limit.php"
+      ["line"]=>
+      int(13)
+      ["function"]=>
+      string(13) "executeString"
+      ["class"]=>
+      string(4) "V8Js"
+      ["type"]=>
+      string(2) "->"
+      ["args"]=>
+      array(5) {
+        [0]=>
+        string(125) "var text = "abcdefghijklmnopqrstuvwyxz0123456789";
+var memory = "";
+for (var i = 0; i < 1000000; ++i) {
+    memory += text;
+}"
+        [1]=>
+        string(8) "basic.js"
+        [2]=>
+        int(1)
+        [3]=>
+        int(0)
+        [4]=>
+        int(10000000)
+      }
+    }
+  }
+  ["previous":"Exception":private]=>
+  NULL
+}
+===EOF===

+ 4 - 4
tests/object_method_call.phpt

@@ -36,13 +36,13 @@ $a->executeString("PHP.myobj.mytest(3.14, 42, null);", "test3.js");
 // Invalid parameters
 try {
 	$a->executeString("PHP.myobj.mytest();", "test4.js");
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	echo $e->getMessage(), "\n";
 }
 
 try {
 	$a->executeString("PHP.myobj.mytest('arg1', 'arg2', 'arg3', 'extra_arg');", "test5.js");
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	echo $e->getMessage(), "\n";
 }
 
@@ -50,14 +50,14 @@ try {
 	echo "\nTEST: Javascript Date -> PHP DateTime\n";
 	echo "======================================\n";
 	$a->executeString("date = new Date('September 8, 1975 09:00:00 GMT'); print(date.toUTCString() + '\\n'); PHP.myobj.mydatetest(date, 'foo');", "test6.js");
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	echo $e->getMessage(), "\n";
 }
 
 // Array / Object
 try {
 	$a->executeString("PHP.myobj.mytest(PHP.myobj, new Array(1,2,3), new Array('foo', 'bar', PHP.myobj));", "test7.js");
-} catch (V8JsException $e) {
+} catch (V8JsScriptException $e) {
 	var_dump($e);
 }
 

+ 69 - 0
tests/time_limit.phpt

@@ -0,0 +1,69 @@
+--TEST--
+Test V8::executeString() : Time limit
+--SKIPIF--
+<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
+--FILE--
+<?php
+
+$JS = <<< EOT
+var text = "abcdefghijklmnopqrstuvwyxz0123456789";
+for (var i = 0; i < 10000000; ++i) {
+    var encoded = encodeURI(text);
+}
+EOT;
+
+$v8 = new V8Js();
+
+try {
+    var_dump($v8->executeString($JS, 'basic.js', V8Js::FLAG_NONE, 1000));
+} catch (V8JsTimeLimitException $e) {
+    var_dump($e);
+}
+?>
+===EOF===
+--EXPECT--
+object(V8JsTimeLimitException)#2 (7) {
+  ["message":protected]=>
+  string(47) "Script time limit of 1000 milliseconds exceeded"
+  ["string":"Exception":private]=>
+  string(0) ""
+  ["code":protected]=>
+  int(0)
+  ["file":protected]=>
+  string(34) "/var/www/v8js/tests/time_limit.php"
+  ["line":protected]=>
+  int(13)
+  ["trace":"Exception":private]=>
+  array(1) {
+    [0]=>
+    array(6) {
+      ["file"]=>
+      string(34) "/var/www/v8js/tests/time_limit.php"
+      ["line"]=>
+      int(13)
+      ["function"]=>
+      string(13) "executeString"
+      ["class"]=>
+      string(4) "V8Js"
+      ["type"]=>
+      string(2) "->"
+      ["args"]=>
+      array(4) {
+        [0]=>
+        string(124) "var text = "abcdefghijklmnopqrstuvwyxz0123456789";
+for (var i = 0; i < 10000000; ++i) {
+    var encoded = encodeURI(text);
+}"
+        [1]=>
+        string(8) "basic.js"
+        [2]=>
+        int(1)
+        [3]=>
+        int(1000)
+      }
+    }
+  }
+  ["previous":"Exception":private]=>
+  NULL
+}
+===EOF===

+ 268 - 102
v8js.cc

@@ -25,40 +25,19 @@
 #include "config.h"
 #endif
 
+#include "php_v8js_macros.h"
+
 extern "C" {
-#include "php.h"
 #include "php_ini.h"
 #include "ext/standard/info.h"
 #include "ext/standard/php_string.h"
 #include "ext/standard/php_smart_str.h"
 #include "zend_exceptions.h"
-#include "php_v8js.h"
 }
 
-#include <v8.h>
-#include "php_v8js_macros.h"
-
 /* Forward declarations */
-static void php_v8js_throw_exception(v8::TryCatch * TSRMLS_DC);
-static void php_v8js_create_exception(zval *, v8::TryCatch * TSRMLS_DC);
-
-/* Module globals */
-ZEND_BEGIN_MODULE_GLOBALS(v8js)
-	int v8_initialized;
-	HashTable *extensions;
-	v8::Persistent<v8::FunctionTemplate> global_template;
-	int disposed_contexts; /* Disposed contexts since last time V8 did GC */
-
-	/* Ini globals */
-	char *v8_flags; /* V8 command line flags */
-	int max_disposed_contexts; /* Max disposed context allowed before forcing V8 GC */
-ZEND_END_MODULE_GLOBALS(v8js)
-
-#ifdef ZTS
-# define V8JSG(v) TSRMG(v8js_globals_id, zend_v8js_globals *, v)
-#else
-# define V8JSG(v) (v8js_globals.v)
-#endif
+static void php_v8js_throw_script_exception(v8::TryCatch * TSRMLS_DC);
+static void php_v8js_create_script_exception(zval *, v8::TryCatch * TSRMLS_DC);
 
 ZEND_DECLARE_MODULE_GLOBALS(v8js)
 
@@ -107,7 +86,9 @@ ZEND_INI_END()
 zend_class_entry *php_ce_v8_object;
 zend_class_entry *php_ce_v8_function;
 static zend_class_entry *php_ce_v8js;
-static zend_class_entry *php_ce_v8js_exception;
+static zend_class_entry *php_ce_v8js_script_exception;
+static zend_class_entry *php_ce_v8js_time_limit_exception;
+static zend_class_entry *php_ce_v8js_memory_limit_exception;
 /* }}} */
 
 /* {{{ Object Handlers */
@@ -126,22 +107,12 @@ struct php_v8js_jsext {
 };
 /* }}} */
 
-/* {{{ Context container */
-struct php_v8js_ctx {
-	zend_object std;
-	v8::Persistent<v8::String> object_name;
-	v8::Persistent<v8::Context> context;
-	zend_bool report_uncaught;
-	zval *pending_exception;
-	int in_execution;
-};
-/* }}} */
-
 /* {{{ Object container */
 struct php_v8js_object {
 	zend_object std;
 	v8::Persistent<v8::Value> v8obj;
 	int flags;
+	v8::Isolate *isolate;
 };
 /* }}} */
 
@@ -177,7 +148,7 @@ static zval *php_v8js_v8_read_property(zval *object, zval *member, int type ZEND
 				MAKE_STD_ZVAL(retval);
 			}
 
-			if (v8js_to_zval(jsVal, retval, obj->flags TSRMLS_CC) == SUCCESS) {
+			if (v8js_to_zval(jsVal, retval, obj->flags, obj->isolate TSRMLS_CC) == SUCCESS) {
 				return retval;
 			}
 		}
@@ -194,7 +165,7 @@ static void php_v8js_v8_write_property(zval *object, zval *member, zval *value Z
 	php_v8js_object *obj = (php_v8js_object *) zend_object_store_get_object(object TSRMLS_CC);
 
 	if (obj->v8obj->IsObject() && !obj->v8obj->IsFunction()) {
-		obj->v8obj->ToObject()->ForceSet(V8JS_SYML(Z_STRVAL_P(member), Z_STRLEN_P(member)), zval_to_v8js(value TSRMLS_CC));
+		obj->v8obj->ToObject()->ForceSet(V8JS_SYML(Z_STRVAL_P(member), Z_STRLEN_P(member)), zval_to_v8js(value, obj->isolate TSRMLS_CC));
 	}
 }
 /* }}} */
@@ -209,7 +180,7 @@ static void php_v8js_v8_unset_property(zval *object, zval *member ZEND_HASH_KEY_
 }
 /* }}} */
 
-int php_v8js_v8_get_properties_hash(v8::Handle<v8::Value> jsValue, HashTable *retval, int flags TSRMLS_DC) /* {{{ */
+int php_v8js_v8_get_properties_hash(v8::Handle<v8::Value> jsValue, HashTable *retval, int flags, v8::Isolate *isolate TSRMLS_DC) /* {{{ */
 {
 	v8::Local<v8::Object> jsObj = jsValue->ToObject();
 
@@ -232,7 +203,7 @@ int php_v8js_v8_get_properties_hash(v8::Handle<v8::Value> jsValue, HashTable *re
 
 			MAKE_STD_ZVAL(value);
 
-			if (v8js_to_zval(jsVal, value, flags TSRMLS_CC) == FAILURE) {
+			if (v8js_to_zval(jsVal, value, flags, isolate TSRMLS_CC) == FAILURE) {
 				zval_ptr_dtor(&value);
 				return FAILURE;
 			}
@@ -253,15 +224,17 @@ static HashTable *php_v8js_v8_get_properties(zval *object TSRMLS_DC) /* {{{ */
 {
 	php_v8js_object *obj = (php_v8js_object *) zend_object_store_get_object(object TSRMLS_CC);
 	HashTable *retval;
-	
+
 	ALLOC_HASHTABLE(retval);
 	zend_hash_init(retval, 0, NULL, ZVAL_PTR_DTOR, 0);
 
-	v8::HandleScope local_scope;
+	v8::Locker locker(obj->isolate);
+	v8::Isolate::Scope isolate_scope(obj->isolate);
+	v8::HandleScope local_scope(obj->isolate);
 	v8::Handle<v8::Context> temp_context = v8::Context::New();
 	v8::Context::Scope temp_scope(temp_context);
 
-	if (php_v8js_v8_get_properties_hash(obj->v8obj, retval, obj->flags TSRMLS_CC) == SUCCESS) {
+	if (php_v8js_v8_get_properties_hash(obj->v8obj, retval, obj->flags, obj->isolate TSRMLS_CC) == SUCCESS) {
 		return retval;
 	}
 	return NULL;
@@ -332,7 +305,7 @@ static int php_v8js_v8_call_method(char *method, INTERNAL_FUNCTION_PARAMETERS) /
 	v8::Local<v8::Value> js_retval;
 
 	for (i = 0; i < argc; i++) {
-		jsArgv[i] = v8::Local<v8::Value>::New(zval_to_v8js(*argv[i] TSRMLS_CC));
+		jsArgv[i] = v8::Local<v8::Value>::New(zval_to_v8js(*argv[i], obj->isolate TSRMLS_CC));
 	}
 
 	js_retval = cb->Call(V8JS_GLOBAL, argc, jsArgv);
@@ -344,7 +317,7 @@ static int php_v8js_v8_call_method(char *method, INTERNAL_FUNCTION_PARAMETERS) /
 	}
 
 	if (return_value_used) {
-		return v8js_to_zval(js_retval, return_value, obj->flags TSRMLS_CC);
+		return v8js_to_zval(js_retval, return_value, obj->flags, obj->isolate TSRMLS_CC);
 	}
 
 	return SUCCESS;
@@ -407,7 +380,7 @@ static zend_object_value php_v8js_v8_new(zend_class_entry *ce TSRMLS_DC) /* {{{
 }
 /* }}} */
 
-void php_v8js_create_v8(zval *res, v8::Handle<v8::Value> value, int flags TSRMLS_DC) /* {{{ */
+void php_v8js_create_v8(zval *res, v8::Handle<v8::Value> value, int flags, v8::Isolate *isolate TSRMLS_DC) /* {{{ */
 {
 	php_v8js_object *c;
 
@@ -417,6 +390,7 @@ void php_v8js_create_v8(zval *res, v8::Handle<v8::Value> value, int flags TSRMLS
 
 	c->v8obj = v8::Persistent<v8::Value>::New(value);
 	c->flags = flags;
+	c->isolate = isolate;
 }
 /* }}} */
 
@@ -544,9 +518,6 @@ static void php_v8js_init(TSRMLS_D) /* {{{ */
 	/* Initialize V8 */
 	v8::V8::Initialize();
 
-	/* Redirect fatal errors to PHP error handler */
-	v8::V8::SetFatalErrorHandler(php_v8js_fatal_error_handler);
-
 	/* Run only once */
 	V8JSG(v8_initialized) = 1;
 }
@@ -579,6 +550,10 @@ static PHP_METHOD(V8Js, __construct)
 	c->report_uncaught = report_uncaught;
 	c->pending_exception = NULL;
 	c->in_execution = 0;
+	c->isolate = v8::Isolate::New();
+	c->time_limit_hit = false;
+	c->memory_limit_hit = false;
+	c->module_loader = NULL;
 
 	/* Initialize V8 */
 	php_v8js_init(TSRMLS_C);
@@ -597,20 +572,29 @@ static PHP_METHOD(V8Js, __construct)
 	/* Declare configuration for extensions */
 	v8::ExtensionConfiguration extension_conf(exts_count, exts);
 
+	// Isolate execution
+	v8::Locker locker(c->isolate);
+	v8::Isolate::Scope isolate_scope(c->isolate);
+
 	/* Handle scope */
-	v8::HandleScope handle_scope;
+	v8::HandleScope handle_scope(c->isolate);
+
+	/* Redirect fatal errors to PHP error handler */
+	// This needs to be done within the context isolate
+	v8::V8::SetFatalErrorHandler(php_v8js_fatal_error_handler);
 
 	/* Create global template for global object */
-	if (V8JSG(global_template).IsEmpty()) {
-		V8JSG(global_template) = v8::Persistent<v8::FunctionTemplate>::New(v8::FunctionTemplate::New());
-		V8JSG(global_template)->SetClassName(V8JS_SYM("V8Js"));
+	// Now we are using multiple isolates this needs to be created for every context
 
-		/* Register builtin methods */
-		php_v8js_register_methods(V8JSG(global_template)->InstanceTemplate());
-	}
+	c->global_template = v8::Persistent<v8::FunctionTemplate>::New(v8::FunctionTemplate::New());
+	c->global_template->SetClassName(V8JS_SYM("V8Js"));
+
+	/* Register builtin methods */
+	php_v8js_register_methods(c->global_template->InstanceTemplate(), c);
 
 	/* Create context */
-	c->context = v8::Context::New(&extension_conf, V8JSG(global_template)->InstanceTemplate());
+	c->context = v8::Context::New(&extension_conf, c->global_template->InstanceTemplate());
+	c->context->SetAlignedPointerInEmbedderData(1, c);
 
 	if (exts) {
 		_php_v8js_free_ext_strarr(exts, exts_count);
@@ -640,10 +624,10 @@ static PHP_METHOD(V8Js, __construct)
 	if (free) {
 		efree(class_name);
 	}
-	
+
 	/* Register Get accessor for passed variables */
 	if (vars_arr && zend_hash_num_elements(Z_ARRVAL_P(vars_arr)) > 0) {
-		php_v8js_register_accessors(php_obj_t->InstanceTemplate(), vars_arr TSRMLS_CC);
+		php_v8js_register_accessors(php_obj_t->InstanceTemplate(), vars_arr, c->isolate TSRMLS_CC);
 	}
 
 	/* Set name for the PHP JS object */
@@ -663,26 +647,118 @@ static PHP_METHOD(V8Js, __construct)
 	} \
 	\
 	(ctx) = (php_v8js_ctx *) zend_object_store_get_object(object TSRMLS_CC); \
+	v8::Locker locker((ctx)->isolate); \
+	v8::Isolate::Scope isolate_scope((ctx)->isolate); \
 	v8::Context::Scope context_scope((ctx)->context);
 
+static void php_v8js_timer_push(long time_limit, long memory_limit, php_v8js_ctx *c)
+{
+	V8JSG(timer_mutex).lock();
+
+	// Create context for this timer
+	php_v8js_timer_ctx *timer_ctx = (php_v8js_timer_ctx *)emalloc(sizeof(php_v8js_timer_ctx));
+
+	// Calculate the time point when the time limit is exceeded
+	std::chrono::milliseconds duration(time_limit);
+	std::chrono::time_point<std::chrono::high_resolution_clock> from = std::chrono::high_resolution_clock::now();
+
+	// Push the timer context
+	timer_ctx->time_limit = time_limit;
+	timer_ctx->memory_limit = memory_limit;
+	timer_ctx->time_point = from + duration;
+	timer_ctx->v8js_ctx = c;
+	V8JSG(timer_stack).push(timer_ctx);
+
+	V8JSG(timer_mutex).unlock();
+}
+
+static void php_v8js_timer_pop()
+{
+	V8JSG(timer_mutex).lock();
+
+	if (V8JSG(timer_stack).size()) {
+		// Free the timer context memory
+		php_v8js_timer_ctx *timer_ctx = V8JSG(timer_stack).top();
+		efree(timer_ctx);
+
+		// Remove the timer context from the stack
+		V8JSG(timer_stack).pop();
+	}
+
+	V8JSG(timer_mutex).unlock();
+}
+
+static void php_v8js_terminate_execution(php_v8js_ctx *c)
+{
+	// Forcefully terminate the current thread of V8 execution in the isolate
+	v8::V8::TerminateExecution(c->isolate);
+
+	// Remove this timer from the stack
+	php_v8js_timer_pop();
+}
+
+static void php_v8js_timer_thread()
+{
+	while (!V8JSG(timer_stop)) {
+		v8::Locker locker;
+
+		std::chrono::time_point<std::chrono::high_resolution_clock> now = std::chrono::high_resolution_clock::now();
+		v8::HeapStatistics hs;
+
+		if (V8JSG(timer_stack).size()) {
+			// Get the current timer context
+			php_v8js_timer_ctx *timer_ctx = V8JSG(timer_stack).top();
+			php_v8js_ctx *c = timer_ctx->v8js_ctx;
+
+			// Get memory usage statistics for the isolate
+			c->isolate->GetHeapStatistics(&hs);
+
+			if (timer_ctx->time_limit > 0 && now > timer_ctx->time_point) {
+				php_v8js_terminate_execution(c);
+
+				V8JSG(timer_mutex).lock();
+				c->time_limit_hit = true;
+				V8JSG(timer_mutex).unlock();
+			}
+
+			if (timer_ctx->memory_limit > 0 && hs.used_heap_size() > timer_ctx->memory_limit) {
+				php_v8js_terminate_execution(c);
+
+				V8JSG(timer_mutex).lock();
+				c->memory_limit_hit = true;
+				V8JSG(timer_mutex).unlock();
+			}
+		}
+
+		// Sleep for 10ms
+		std::chrono::milliseconds duration(10);
+		std::this_thread::sleep_for(duration);
+	}
+}
+
 /* {{{ proto mixed V8Js::executeString(string script [, string identifier [, int flags]])
  */
 static PHP_METHOD(V8Js, executeString)
 {
 	char *str = NULL, *identifier = NULL;
 	int str_len = 0, identifier_len = 0;
-	long flags = V8JS_FLAG_NONE;
+	long flags = V8JS_FLAG_NONE, time_limit = 0, memory_limit = 0;
 
-	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|sl", &str, &str_len, &identifier, &identifier_len, &flags) == FAILURE) {
+	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|slll", &str, &str_len, &identifier, &identifier_len, &flags, &time_limit, &memory_limit) == FAILURE) {
 		return;
 	}
 
 	V8JS_BEGIN_CTX(c, getThis())
 
+	V8JSG(timer_mutex).lock();
+	c->time_limit_hit = false;
+	c->memory_limit_hit = false;
+	V8JSG(timer_mutex).unlock();
+
 	/* Catch JS exceptions */
 	v8::TryCatch try_catch;
 
-	v8::HandleScope handle_scope;
+	v8::HandleScope handle_scope(c->isolate);
 
 	/* Set script identifier */
 	v8::Local<v8::String> sname = identifier_len ? V8JS_SYML(identifier, identifier_len) : V8JS_SYM("V8Js::executeString()");
@@ -693,21 +769,50 @@ static PHP_METHOD(V8Js, executeString)
 
 	/* Compile errors? */
 	if (script.IsEmpty()) {
-		php_v8js_throw_exception(&try_catch TSRMLS_CC);
+		php_v8js_throw_script_exception(&try_catch TSRMLS_CC);
 		return;
 	}
 
 	/* Set flags for runtime use */
 	V8JS_GLOBAL_SET_FLAGS(flags);
 
+	if (time_limit > 0 || memory_limit > 0) {
+		// If timer thread is not running then start it
+		if (!V8JSG(timer_thread)) {
+			// If not, start timer thread
+			V8JSG(timer_thread) = new std::thread(php_v8js_timer_thread);
+		}
+
+		php_v8js_timer_push(time_limit, memory_limit, c);
+	}
+
 	/* Execute script */
 	c->in_execution++;
 	v8::Local<v8::Value> result = script->Run();
 	c->in_execution--;
 
-	/* Script possibly terminated, return immediately */
+	if (time_limit > 0) {
+		php_v8js_timer_pop();
+	}
+
+	char exception_string[64];
+
+	if (c->time_limit_hit) {
+		// Execution has been terminated due to time limit
+		sprintf(exception_string, "Script time limit of %lu milliseconds exceeded", time_limit);
+		zend_throw_exception(php_ce_v8js_time_limit_exception, exception_string, 0 TSRMLS_CC);
+		return;
+	}
+
+	if (c->memory_limit_hit) {
+		// Execution has been terminated due to memory limit
+		sprintf(exception_string, "Script memory limit of %lu bytes exceeded", memory_limit);
+		zend_throw_exception(php_ce_v8js_memory_limit_exception, exception_string, 0 TSRMLS_CC);
+		return;
+	}
+
 	if (!try_catch.CanContinue()) {
-		/* TODO: throw PHP exception here? */
+		// At this point we can't re-throw the exception
 		return;
 	}
 
@@ -725,14 +830,14 @@ static PHP_METHOD(V8Js, executeString)
 
 			/* Report immediately if report_uncaught is true */
 			if (c->report_uncaught) {
-				php_v8js_throw_exception(&try_catch TSRMLS_CC);
+				php_v8js_throw_script_exception(&try_catch TSRMLS_CC);
 				return;
 			}
 
 			/* Exception thrown from JS, preserve it for future execution */
 			if (result.IsEmpty()) {
 				MAKE_STD_ZVAL(c->pending_exception);
-				php_v8js_create_exception(c->pending_exception, &try_catch TSRMLS_CC);
+				php_v8js_create_script_exception(c->pending_exception, &try_catch TSRMLS_CC);
 			}
 		}
 
@@ -743,7 +848,7 @@ static PHP_METHOD(V8Js, executeString)
 
 	/* Convert V8 value to PHP value */
 	if (!result.IsEmpty()) {
-		v8js_to_zval(result, return_value, flags TSRMLS_CC);
+		v8js_to_zval(result, return_value, flags, c->isolate TSRMLS_CC);
 	}
 }
 /* }}} */
@@ -766,6 +871,24 @@ static PHP_METHOD(V8Js, getPendingException)
 }
 /* }}} */
 
+/* {{{ proto void V8Js::setModuleLoader(string module)
+ */
+static PHP_METHOD(V8Js, setModuleLoader)
+{
+	php_v8js_ctx *c;
+	zval *callable;
+
+	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &callable) == FAILURE) {
+		return;
+	}
+
+	c = (php_v8js_ctx *) zend_object_store_get_object(getThis() TSRMLS_CC);
+
+	c->module_loader = callable;
+	Z_ADDREF_P(c->module_loader);
+}
+/* }}} */
+
 static void php_v8js_persistent_zval_ctor(zval **p) /* {{{ */
 {
 	zval *orig_ptr = *p;
@@ -920,11 +1043,17 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_v8js_executestring, 0, 0, 1)
 	ZEND_ARG_INFO(0, script)
 	ZEND_ARG_INFO(0, identifier)
 	ZEND_ARG_INFO(0, flags)
+	ZEND_ARG_INFO(0, time_limit)
+	ZEND_ARG_INFO(0, memory_limit)
 ZEND_END_ARG_INFO()
 
 ZEND_BEGIN_ARG_INFO(arginfo_v8js_getpendingexception, 0)
 ZEND_END_ARG_INFO()
 
+ZEND_BEGIN_ARG_INFO_EX(arginfo_v8js_setmoduleloader, 0, 0, 1)
+	ZEND_ARG_INFO(0, callable)
+ZEND_END_ARG_INFO()
+
 ZEND_BEGIN_ARG_INFO_EX(arginfo_v8js_registerextension, 0, 0, 2)
 	ZEND_ARG_INFO(0, extension_name)
 	ZEND_ARG_INFO(0, script)
@@ -935,7 +1064,7 @@ ZEND_END_ARG_INFO()
 ZEND_BEGIN_ARG_INFO(arginfo_v8js_getextensions, 0)
 ZEND_END_ARG_INFO()
 
-ZEND_BEGIN_ARG_INFO(arginfo_v8jsexception_no_args, 0)
+ZEND_BEGIN_ARG_INFO(arginfo_v8jsscriptexception_no_args, 0)
 ZEND_END_ARG_INFO()
 /* }}} */
 
@@ -943,6 +1072,7 @@ static const zend_function_entry v8js_methods[] = { /* {{{ */
 	PHP_ME(V8Js,	__construct,			arginfo_v8js_construct,				ZEND_ACC_PUBLIC|ZEND_ACC_CTOR)
 	PHP_ME(V8Js,	executeString,			arginfo_v8js_executestring,			ZEND_ACC_PUBLIC)
 	PHP_ME(V8Js,	getPendingException,	arginfo_v8js_getpendingexception,	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)
 	{NULL, NULL, NULL}
@@ -955,13 +1085,13 @@ static void php_v8js_write_property(zval *object, zval *member, zval *value ZEND
 {
 	V8JS_BEGIN_CTX(c, object)
 
-	v8::HandleScope handle_scope;
+	v8::HandleScope handle_scope(c->isolate);
 
 	/* Global PHP JS object */
 	v8::Local<v8::Object> jsobj = V8JS_GLOBAL->Get(c->object_name)->ToObject();
 
 	/* Write value to PHP JS object */
-	jsobj->ForceSet(V8JS_SYML(Z_STRVAL_P(member), Z_STRLEN_P(member)), zval_to_v8js(value TSRMLS_CC), v8::ReadOnly);
+	jsobj->ForceSet(V8JS_SYML(Z_STRVAL_P(member), Z_STRLEN_P(member)), zval_to_v8js(value, c->isolate TSRMLS_CC), v8::ReadOnly);
 
 	/* Write value to PHP object */
 	std_object_handlers.write_property(object, member, value ZEND_HASH_KEY_CC TSRMLS_CC);
@@ -972,7 +1102,7 @@ static void php_v8js_unset_property(zval *object, zval *member ZEND_HASH_KEY_DC
 {
 	V8JS_BEGIN_CTX(c, object)
 
-	v8::HandleScope handle_scope;
+	v8::HandleScope handle_scope(c->isolate);
 
 	/* Global PHP JS object */
 	v8::Local<v8::Object> jsobj = V8JS_GLOBAL->Get(c->object_name)->ToObject();
@@ -987,9 +1117,9 @@ static void php_v8js_unset_property(zval *object, zval *member ZEND_HASH_KEY_DC
 
 /* }}} V8Js */
 
-/* {{{ Class: V8JsException */
+/* {{{ Class: V8JsScriptException */
 
-static void php_v8js_create_exception(zval *return_value, v8::TryCatch *try_catch TSRMLS_DC) /* {{{ */
+static void php_v8js_create_script_exception(zval *return_value, v8::TryCatch *try_catch TSRMLS_DC) /* {{{ */
 {
 	v8::String::Utf8Value exception(try_catch->Exception());
 	const char *exception_string = ToCString(exception);
@@ -998,10 +1128,10 @@ static void php_v8js_create_exception(zval *return_value, v8::TryCatch *try_catc
 	char *message_string;
 	int linenum, message_len;
 
-	object_init_ex(return_value, php_ce_v8js_exception);
+	object_init_ex(return_value, php_ce_v8js_script_exception);
 
 #define PHPV8_EXPROP(type, name, value) \
-	zend_update_property##type(php_ce_v8js_exception, return_value, #name, sizeof(#name) - 1, value TSRMLS_CC);
+	zend_update_property##type(php_ce_v8js_script_exception, return_value, #name, sizeof(#name) - 1, value TSRMLS_CC);
 
 	if (tc_message.IsEmpty()) {
 		message_len = spprintf(&message_string, 0, "%s", exception_string);
@@ -1034,66 +1164,86 @@ static void php_v8js_create_exception(zval *return_value, v8::TryCatch *try_catc
 }
 /* }}} */
 
-static void php_v8js_throw_exception(v8::TryCatch *try_catch TSRMLS_DC) /* {{{ */
+static void php_v8js_throw_script_exception(v8::TryCatch *try_catch TSRMLS_DC) /* {{{ */
 {
 	v8::String::Utf8Value exception(try_catch->Exception());
 	const char *exception_string = ToCString(exception);
 	zval *zexception = NULL;
 
 	if (try_catch->Message().IsEmpty()) {
-		zend_throw_exception(php_ce_v8js_exception, (char *) exception_string, 0 TSRMLS_CC);
+		zend_throw_exception(php_ce_v8js_script_exception, (char *) exception_string, 0 TSRMLS_CC);
 	} else {
 		MAKE_STD_ZVAL(zexception);
-		php_v8js_create_exception(zexception, try_catch TSRMLS_CC);
+		php_v8js_create_script_exception(zexception, try_catch TSRMLS_CC);
 		zend_throw_exception_object(zexception TSRMLS_CC);
 	}
 }
 /* }}} */
 
 #define V8JS_EXCEPTION_METHOD(property) \
-	static PHP_METHOD(V8JsException, get##property) \
+	static PHP_METHOD(V8JsScriptException, get##property) \
 	{ \
 		zval *value; \
 		\
 		if (zend_parse_parameters_none() == FAILURE) { \
 			return; \
 		} \
-		value = zend_read_property(php_ce_v8js_exception, getThis(), #property, sizeof(#property) - 1, 0 TSRMLS_CC); \
+		value = zend_read_property(php_ce_v8js_script_exception, getThis(), #property, sizeof(#property) - 1, 0 TSRMLS_CC); \
 		*return_value = *value; \
 		zval_copy_ctor(return_value); \
 		INIT_PZVAL(return_value); \
 	}
 
-/* {{{ proto string V8JsException::getJsFileName()
+/* {{{ proto string V8JsEScriptxception::getJsFileName()
  */
 V8JS_EXCEPTION_METHOD(JsFileName);
 /* }}} */
 
-/* {{{ proto string V8JsException::getJsLineNumber()
+/* {{{ proto string V8JsScriptException::getJsLineNumber()
  */
 V8JS_EXCEPTION_METHOD(JsLineNumber);
 /* }}} */
 
-/* {{{ proto string V8JsException::getJsSourceLine()
+/* {{{ proto string V8JsScriptException::getJsSourceLine()
  */
 V8JS_EXCEPTION_METHOD(JsSourceLine);
 /* }}} */
 
-/* {{{ proto string V8JsException::getJsTrace()
+/* {{{ proto string V8JsScriptException::getJsTrace()
  */
 V8JS_EXCEPTION_METHOD(JsTrace);	
 /* }}} */
 
-static const zend_function_entry v8js_exception_methods[] = { /* {{{ */
-	PHP_ME(V8JsException,	getJsFileName,		arginfo_v8jsexception_no_args,	ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
-	PHP_ME(V8JsException,	getJsLineNumber,	arginfo_v8jsexception_no_args,	ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
-	PHP_ME(V8JsException,	getJsSourceLine,	arginfo_v8jsexception_no_args,	ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
-	PHP_ME(V8JsException,	getJsTrace,			arginfo_v8jsexception_no_args,	ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
+static const zend_function_entry v8js_script_exception_methods[] = { /* {{{ */
+	PHP_ME(V8JsScriptException,	getJsFileName,		arginfo_v8jsscriptexception_no_args,	ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
+	PHP_ME(V8JsScriptException,	getJsLineNumber,	arginfo_v8jsscriptexception_no_args,	ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
+	PHP_ME(V8JsScriptException,	getJsSourceLine,	arginfo_v8jsscriptexception_no_args,	ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
+	PHP_ME(V8JsScriptException,	getJsTrace,			arginfo_v8jsscriptexception_no_args,	ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
 	{NULL, NULL, NULL}
 };
 /* }}} */
 
-/* }}} V8JsException */
+/* }}} V8JsScriptException */
+
+/* {{{ Class: V8JsTimeLimitException */
+
+static const zend_function_entry v8js_time_limit_exception_methods[] = { /* {{{ */
+	{NULL, NULL, NULL}
+};
+
+/* }}} */
+
+/* }}} V8JsTimeLimitException */
+
+/* {{{ Class: V8JsMemoryLimitException */
+
+static const zend_function_entry v8js_memory_limit_exception_methods[] = { /* {{{ */
+	{NULL, NULL, NULL}
+};
+
+/* }}} */
+
+/* }}} V8JsMemoryLimitException */
 
 /* {{{ PHP_MINIT_FUNCTION
  */
@@ -1145,16 +1295,26 @@ static PHP_MINIT_FUNCTION(v8js)
 	zend_declare_class_constant_long(php_ce_v8js, ZEND_STRL("FLAG_NONE"),			V8JS_FLAG_NONE			TSRMLS_CC);
 	zend_declare_class_constant_long(php_ce_v8js, ZEND_STRL("FLAG_FORCE_ARRAY"),	V8JS_FLAG_FORCE_ARRAY	TSRMLS_CC);
 
-	/* V8JsException Class */
-	INIT_CLASS_ENTRY(ce, "V8JsException", v8js_exception_methods);
-	php_ce_v8js_exception = zend_register_internal_class_ex(&ce, zend_exception_get_default(TSRMLS_C), NULL TSRMLS_CC);
-	php_ce_v8js_exception->ce_flags |= ZEND_ACC_FINAL;
+	/* V8JsScriptException Class */
+	INIT_CLASS_ENTRY(ce, "V8JsScriptException", v8js_script_exception_methods);
+	php_ce_v8js_script_exception = zend_register_internal_class_ex(&ce, zend_exception_get_default(TSRMLS_C), NULL TSRMLS_CC);
+	php_ce_v8js_script_exception->ce_flags |= ZEND_ACC_FINAL;
 
 	/* Add custom JS specific properties */
-	zend_declare_property_null(php_ce_v8js_exception, ZEND_STRL("JsFileName"),		ZEND_ACC_PROTECTED TSRMLS_CC);
-	zend_declare_property_null(php_ce_v8js_exception, ZEND_STRL("JsLineNumber"),	ZEND_ACC_PROTECTED TSRMLS_CC);
-	zend_declare_property_null(php_ce_v8js_exception, ZEND_STRL("JsSourceLine"),	ZEND_ACC_PROTECTED TSRMLS_CC);
-	zend_declare_property_null(php_ce_v8js_exception, ZEND_STRL("JsTrace"),			ZEND_ACC_PROTECTED TSRMLS_CC);
+	zend_declare_property_null(php_ce_v8js_script_exception, ZEND_STRL("JsFileName"),		ZEND_ACC_PROTECTED TSRMLS_CC);
+	zend_declare_property_null(php_ce_v8js_script_exception, ZEND_STRL("JsLineNumber"),	ZEND_ACC_PROTECTED TSRMLS_CC);
+	zend_declare_property_null(php_ce_v8js_script_exception, ZEND_STRL("JsSourceLine"),	ZEND_ACC_PROTECTED TSRMLS_CC);
+	zend_declare_property_null(php_ce_v8js_script_exception, ZEND_STRL("JsTrace"),			ZEND_ACC_PROTECTED TSRMLS_CC);
+
+	/* V8JsTimeLimitException Class */
+	INIT_CLASS_ENTRY(ce, "V8JsTimeLimitException", v8js_time_limit_exception_methods);
+	php_ce_v8js_time_limit_exception = zend_register_internal_class_ex(&ce, zend_exception_get_default(TSRMLS_C), NULL TSRMLS_CC);
+	php_ce_v8js_time_limit_exception->ce_flags |= ZEND_ACC_FINAL;
+
+	/* V8JsMemoryLimitException Class */
+	INIT_CLASS_ENTRY(ce, "V8JsMemoryLimitException", v8js_memory_limit_exception_methods);
+	php_ce_v8js_memory_limit_exception = zend_register_internal_class_ex(&ce, zend_exception_get_default(TSRMLS_C), NULL TSRMLS_CC);
+	php_ce_v8js_memory_limit_exception->ce_flags |= ZEND_ACC_FINAL;
 
 	REGISTER_INI_ENTRIES();
 
@@ -1190,8 +1350,6 @@ static PHP_MSHUTDOWN_FUNCTION(v8js)
 {
 	UNREGISTER_INI_ENTRIES();
 
-	v8::V8::Dispose();
-
 	if (V8JSG(extensions)) {
 		zend_hash_destroy(V8JSG(extensions));
 		free(V8JSG(extensions));
@@ -1211,6 +1369,12 @@ static PHP_MSHUTDOWN_FUNCTION(v8js)
  */
 static PHP_RSHUTDOWN_FUNCTION(v8js)
 {
+	// If the timer thread is running then stop it
+	if (V8JSG(timer_thread)) {
+		V8JSG(timer_stop) = true;
+		V8JSG(timer_thread)->join();
+	}
+
 #if V8JS_DEBUG
 	v8::HeapStatistics stats;
 	v8::V8::GetHeapStatistics(&stats);
@@ -1254,6 +1418,8 @@ static PHP_GINIT_FUNCTION(v8js)
 	v8js_globals->disposed_contexts = 0;
 	v8js_globals->v8_initialized = 0;
 	v8js_globals->v8_flags = NULL;
+	v8js_globals->timer_thread = NULL;
+	v8js_globals->timer_stop = false;
 }
 /* }}} */
 

+ 111 - 0
v8js_commonjs.cc

@@ -0,0 +1,111 @@
+/*
+  +----------------------------------------------------------------------+
+  | PHP Version 5                                                        |
+  +----------------------------------------------------------------------+
+  | Copyright (c) 1997-2012 The PHP Group                                |
+  +----------------------------------------------------------------------+
+  | This source file is subject to version 3.01 of the PHP license,      |
+  | that is bundled with this package in the file LICENSE, and is        |
+  | available through the world-wide-web at the following url:           |
+  | http://www.php.net/license/3_01.txt                                  |
+  | If you did not receive a copy of the PHP license and are unable to   |
+  | obtain it through the world-wide-web, please send a note to          |
+  | [email protected] so we can mail you a copy immediately.               |
+  +----------------------------------------------------------------------+
+  | Author: Simon Best <[email protected]>                            |
+  +----------------------------------------------------------------------+
+*/
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+extern "C" {
+#include "php.h"
+#include "zend_exceptions.h"
+}
+
+#include "php_v8js_macros.h"
+
+void php_v8js_commonjs_split_terms(char *identifier, std::vector<char *> &terms)
+{
+    char *term = (char *)malloc(PATH_MAX), *ptr = term;
+
+    // Initialise the term string
+    *term = 0;
+
+    while (*identifier > 0) {
+        if (*identifier == '/') {
+            if (strlen(term) > 0) {
+                // Terminate term string and add to terms vector
+                *ptr++ = 0;
+                terms.push_back(strdup(term));
+
+                // Reset term string
+                memset(term, 0, strlen(term));
+                ptr = term;
+            }
+        } else {
+            *ptr++ = *identifier;
+        }
+
+        identifier++;
+    }
+
+    if (strlen(term) > 0) {
+        // Terminate term string and add to terms vector
+        *ptr++ = 0;
+        terms.push_back(strdup(term));
+    }
+
+    if (term > 0) {
+        free(term);
+    }
+}
+
+void php_v8js_commonjs_normalise_identifier(char *base, char *identifier, char *normalised_path, char *module_name)
+{
+    std::vector<char *> id_terms, terms;
+    php_v8js_commonjs_split_terms(identifier, id_terms);
+
+    // If we have a relative module identifier then include the base terms
+    if (!strcmp(id_terms.front(), ".") || !strcmp(id_terms.front(), "..")) {
+        php_v8js_commonjs_split_terms(base, terms);
+    }
+
+    terms.insert(terms.end(), id_terms.begin(), id_terms.end());
+
+    std::vector<char *> normalised_terms;
+
+    for (std::vector<char *>::iterator it = terms.begin(); it != terms.end(); it++) {
+        char *term = *it;
+
+        if (!strcmp(term, "..")) {
+            // Ignore parent term (..) if it's the first normalised term
+            if (normalised_terms.size() > 0) {
+                // Remove the parent normalized term
+                normalised_terms.pop_back();
+            }
+        } else if (strcmp(term, ".")) {
+            // Add the term if it's not the current term (.)
+            normalised_terms.push_back(term);
+        }
+    }
+
+    // Initialise the normalised path string
+    *normalised_path = 0;
+    *module_name = 0;
+
+    strcat(module_name, normalised_terms.back());
+    normalised_terms.pop_back();
+
+    for (std::vector<char *>::iterator it = normalised_terms.begin(); it != normalised_terms.end(); it++) {
+        char *term = *it;
+
+        if (strlen(normalised_path) > 0) {
+            strcat(normalised_path, "/");
+        }
+
+        strcat(normalised_path, term);
+    }
+}

+ 18 - 16
v8js_convert.cc

@@ -38,6 +38,7 @@ static v8::Handle<v8::Value> php_v8js_php_callback(const v8::Arguments &args) /*
 {
 	v8::Handle<v8::Value> return_value;
 	zval *value = reinterpret_cast<zval *>(args.This()->GetAlignedPointerFromInternalField(0));
+	v8::Isolate *isolate = reinterpret_cast<v8::Isolate *>(args.This()->GetAlignedPointerFromInternalField(1));
 	zend_function *method_ptr;
 	zend_fcall_info fci;
 	zend_fcall_info_cache fcc;
@@ -99,7 +100,7 @@ static v8::Handle<v8::Value> php_v8js_php_callback(const v8::Arguments &args) /*
 		argv = (zval **) safe_emalloc(argc, sizeof(zval *), 0);
 		for (i = 0; i < argc; i++) {
 			MAKE_STD_ZVAL(argv[i]);
-			if (v8js_to_zval(args[i], argv[i], flags TSRMLS_CC) == FAILURE) {
+			if (v8js_to_zval(args[i], argv[i], flags, isolate TSRMLS_CC) == FAILURE) {
 				fci.param_count++;
 				error_len = spprintf(&error, 0, "converting parameter #%d passed to %s() failed", i + 1, method_ptr->common.function_name);
 				return_value = V8JS_THROW(Error, error, error_len);
@@ -134,7 +135,7 @@ failure:
 	}
 
 	if (retval_ptr != NULL) {
-		return_value = zval_to_v8js(retval_ptr TSRMLS_CC);
+		return_value = zval_to_v8js(retval_ptr, isolate TSRMLS_CC);
 		zval_ptr_dtor(&retval_ptr);
 	} else {
 		return_value = V8JS_NULL;
@@ -275,7 +276,7 @@ static v8::Handle<v8::Integer> php_v8js_property_query(v8::Local<v8::String> pro
 #define PHP_V8JS_CALLBACK(mptr) \
 	v8::FunctionTemplate::New(php_v8js_php_callback, v8::External::New(mptr))->GetFunction()
 
-static v8::Handle<v8::Value> php_v8js_hash_to_jsobj(zval *value TSRMLS_DC) /* {{{ */
+static v8::Handle<v8::Value> php_v8js_hash_to_jsobj(zval *value, v8::Isolate *isolate TSRMLS_DC) /* {{{ */
 {
 	v8::Local<v8::FunctionTemplate> new_tpl = v8::FunctionTemplate::New();
 	v8::Local<v8::Object> newobj;
@@ -303,7 +304,7 @@ static v8::Handle<v8::Value> php_v8js_hash_to_jsobj(zval *value TSRMLS_DC) /* {{
 	/* Object methods */
 	if (ce) {
 		new_tpl->SetClassName(V8JS_STRL(ce->name, ce->name_length));
-		new_tpl->InstanceTemplate()->SetInternalFieldCount(1);
+		new_tpl->InstanceTemplate()->SetInternalFieldCount(2);
 
 		if (ce == zend_ce_closure) {
 			new_tpl->InstanceTemplate()->SetCallAsFunctionHandler(php_v8js_php_callback);
@@ -387,8 +388,9 @@ static v8::Handle<v8::Value> php_v8js_hash_to_jsobj(zval *value TSRMLS_DC) /* {{
 			// Increase the reference count of this value because we're storing it internally for use later
 			// See https://github.com/preillyme/v8js/issues/6
 			Z_ADDREF_P(value);
-			
+
 		newobj->SetAlignedPointerInInternalField(0, (void *) value);
+		newobj->SetAlignedPointerInInternalField(1, (void *) isolate);
 	} else {
 		new_tpl->SetClassName(V8JS_SYM("Array"));
 		newobj = new_tpl->InstanceTemplate()->NewInstance();
@@ -425,9 +427,9 @@ static v8::Handle<v8::Value> php_v8js_hash_to_jsobj(zval *value TSRMLS_DC) /* {{
 						}
 						continue;
 					}
-					newobj->Set(V8JS_STRL(key, key_len - 1), zval_to_v8js(*data TSRMLS_CC), v8::ReadOnly);
+					newobj->Set(V8JS_STRL(key, key_len - 1), zval_to_v8js(*data, isolate TSRMLS_CC), v8::ReadOnly);
 				} else {
-					newobj->Set(index, zval_to_v8js(*data TSRMLS_CC));
+					newobj->Set(index, zval_to_v8js(*data, isolate TSRMLS_CC));
 				}
 
 				if (tmp_ht) {
@@ -440,14 +442,14 @@ static v8::Handle<v8::Value> php_v8js_hash_to_jsobj(zval *value TSRMLS_DC) /* {{
 }
 /* }}} */
 
-static v8::Handle<v8::Value> php_v8js_hash_to_jsarr(zval *value TSRMLS_DC) /* {{{ */
+static v8::Handle<v8::Value> php_v8js_hash_to_jsarr(zval *value, v8::Isolate *isolate TSRMLS_DC) /* {{{ */
 {
 	HashTable *myht = HASH_OF(value);
 	int i = myht ? zend_hash_num_elements(myht) : 0;
 
 	/* Return object if dealing with assoc array */
 	if (i > 0 && _php_v8js_is_assoc_array(myht TSRMLS_CC)) {
-		return php_v8js_hash_to_jsobj(value TSRMLS_CC);
+		return php_v8js_hash_to_jsobj(value, isolate TSRMLS_CC);
 	}
 
 	v8::Local<v8::Array> newarr;
@@ -476,7 +478,7 @@ static v8::Handle<v8::Value> php_v8js_hash_to_jsarr(zval *value TSRMLS_DC) /* {{
 				tmp_ht->nApplyCount++;
 			}
 
-			newarr->Set(index++, zval_to_v8js(*data TSRMLS_CC));
+			newarr->Set(index++, zval_to_v8js(*data, isolate TSRMLS_CC));
 
 			if (tmp_ht) {
 				tmp_ht->nApplyCount--;
@@ -487,18 +489,18 @@ static v8::Handle<v8::Value> php_v8js_hash_to_jsarr(zval *value TSRMLS_DC) /* {{
 }
 /* }}} */
 
-v8::Handle<v8::Value> zval_to_v8js(zval *value TSRMLS_DC) /* {{{ */
+v8::Handle<v8::Value> zval_to_v8js(zval *value, v8::Isolate *isolate TSRMLS_DC) /* {{{ */
 {
 	v8::Handle<v8::Value> jsValue;
 
 	switch (Z_TYPE_P(value))
 	{
 		case IS_ARRAY:
-			jsValue = php_v8js_hash_to_jsarr(value TSRMLS_CC);
+			jsValue = php_v8js_hash_to_jsarr(value, isolate TSRMLS_CC);
 			break;
 
 		case IS_OBJECT:
-			jsValue = php_v8js_hash_to_jsobj(value TSRMLS_CC);
+			jsValue = php_v8js_hash_to_jsobj(value, isolate TSRMLS_CC);
 			break;
 
 		case IS_STRING:
@@ -526,7 +528,7 @@ v8::Handle<v8::Value> zval_to_v8js(zval *value TSRMLS_DC) /* {{{ */
 }
 /* }}} */
 
-int v8js_to_zval(v8::Handle<v8::Value> jsValue, zval *return_value, int flags TSRMLS_DC) /* {{{ */
+int v8js_to_zval(v8::Handle<v8::Value> jsValue, zval *return_value, int flags, v8::Isolate *isolate TSRMLS_DC) /* {{{ */
 {
 	if (jsValue->IsString())
 	{
@@ -575,9 +577,9 @@ int v8js_to_zval(v8::Handle<v8::Value> jsValue, zval *return_value, int flags TS
 	{
 		if ((flags & V8JS_FLAG_FORCE_ARRAY) || jsValue->IsArray()) {
 			array_init(return_value);
-			return php_v8js_v8_get_properties_hash(jsValue, Z_ARRVAL_P(return_value), flags TSRMLS_CC);
+			return php_v8js_v8_get_properties_hash(jsValue, Z_ARRVAL_P(return_value), flags, isolate TSRMLS_CC);
 		} else {
-			php_v8js_create_v8(return_value, jsValue, flags TSRMLS_CC);
+			php_v8js_create_v8(return_value, jsValue, flags, isolate TSRMLS_CC);
 			return SUCCESS;
 		}
 	}

+ 177 - 5
v8js_methods.cc

@@ -23,12 +23,14 @@
 #include "config.h"
 #endif
 
+#include "php_v8js_macros.h"
+
 extern "C" {
-#include "php.h"
+#include "zend_exceptions.h"
 }
 
-#include "php_v8js_macros.h"
-#include <v8.h>
+/* Forward declarations */
+void php_v8js_commonjs_normalise_identifier(char *base, char *identifier, char *normalised_path, char *module_name);
 
 /* global.exit - terminate execution */
 V8JS_METHOD(exit) /* {{{ */
@@ -162,17 +164,187 @@ V8JS_METHOD(var_dump) /* {{{ */
 	for (int i = 0; i < args.Length(); i++) {
 		_php_v8js_dumper(args[i], 1 TSRMLS_CC);
 	}
-	
+
 	return V8JS_NULL;
 }
 /* }}} */
 
-void php_v8js_register_methods(v8::Handle<v8::ObjectTemplate> global) /* {{{ */
+V8JS_METHOD(require)
+{
+	// Get the extension context
+	v8::Handle<v8::External> data = v8::Handle<v8::External>::Cast(args.Data());
+	php_v8js_ctx *c = static_cast<php_v8js_ctx*>(data->Value());
+
+	// Check that we have a module loader
+	if (c->module_loader == NULL) {
+		return v8::ThrowException(v8::String::New("No module loader"));
+	}
+
+	v8::String::Utf8Value module_id_v8(args[0]);
+
+	// Make sure to duplicate the module name string so it doesn't get freed by the V8 garbage collector
+	char *module_id = estrdup(ToCString(module_id_v8));
+	char *normalised_path = (char *)emalloc(PATH_MAX);
+	char *module_name = (char *)emalloc(PATH_MAX);
+
+	php_v8js_commonjs_normalise_identifier(c->modules_base.back(), module_id, normalised_path, module_name);
+	efree(module_id);
+
+	char *normalised_module_id = (char *)emalloc(strlen(module_id));
+	*normalised_module_id = 0;
+
+	if (strlen(normalised_path) > 0) {
+		strcat(normalised_module_id, normalised_path);
+		strcat(normalised_module_id, "/");
+	}
+
+	strcat(normalised_module_id, module_name);
+	efree(module_name);
+
+	// Check for module cyclic dependencies
+	for (std::vector<char *>::iterator it = c->modules_stack.begin(); it != c->modules_stack.end(); ++it)
+    {
+    	if (!strcmp(*it, normalised_module_id)) {
+    		efree(normalised_path);
+
+    		return v8::ThrowException(v8::String::New("Module cyclic dependency"));
+    	}
+    }
+
+    // If we have already loaded and cached this module then use it
+	if (V8JSG(modules_loaded).count(normalised_module_id) > 0) {
+		efree(normalised_path);
+
+		return V8JSG(modules_loaded)[normalised_module_id];
+	}
+
+	// Callback to PHP to load the module code
+
+	zval module_code;
+	zval *normalised_path_zend;
+
+	MAKE_STD_ZVAL(normalised_path_zend);
+	ZVAL_STRING(normalised_path_zend, normalised_module_id, 1);
+	zval* params[] = { normalised_path_zend };
+
+	if (FAILURE == call_user_function(EG(function_table), NULL, c->module_loader, &module_code, 1, params TSRMLS_CC)) {
+		efree(normalised_path);
+
+		return v8::ThrowException(v8::String::New("Module loader callback failed"));
+	}
+
+	// Check if an exception was thrown
+	if (EG(exception)) {
+		efree(normalised_path);
+
+		// Clear the PHP exception and throw it in V8 instead
+		zend_clear_exception(TSRMLS_CC);
+		return v8::ThrowException(v8::String::New("Module loader callback exception"));
+	}
+
+	// Convert the return value to string
+	if (Z_TYPE(module_code) != IS_STRING) {
+    	convert_to_string(&module_code);
+	}
+
+	// Check that some code has been returned
+	if (!strlen(Z_STRVAL(module_code))) {
+		efree(normalised_path);
+
+		return v8::ThrowException(v8::String::New("Module loader callback did not return code"));
+	}
+
+	// Create a template for the global object and set the built-in global functions
+	v8::Handle<v8::ObjectTemplate> global = v8::ObjectTemplate::New();
+	global->Set(v8::String::New("print"), v8::FunctionTemplate::New(V8JS_MN(print)), v8::ReadOnly);
+	global->Set(V8JS_SYM("sleep"), v8::FunctionTemplate::New(V8JS_MN(sleep)), v8::ReadOnly);
+	global->Set(v8::String::New("require"), v8::FunctionTemplate::New(V8JS_MN(require), v8::External::New(c)), v8::ReadOnly);
+
+	// Add the exports object in which the module can return its API
+	v8::Handle<v8::ObjectTemplate> exports_template = v8::ObjectTemplate::New();
+	v8::Handle<v8::Object> exports = exports_template->NewInstance();
+	global->Set(v8::String::New("exports"), exports);
+
+	// Add the module object in which the module can have more fine-grained control over what it can return
+	v8::Handle<v8::ObjectTemplate> module_template = v8::ObjectTemplate::New();
+	v8::Handle<v8::Object> module = module_template->NewInstance();
+	module->Set(v8::String::New("id"), v8::String::New(normalised_module_id));
+	global->Set(v8::String::New("module"), module);
+
+	// Each module gets its own context so different modules do not affect each other
+	v8::Persistent<v8::Context> context = v8::Context::New(NULL, global);
+
+	// Catch JS exceptions
+	v8::TryCatch try_catch;
+
+	v8::Locker locker(c->isolate);
+	v8::Isolate::Scope isolate_scope(c->isolate);
+
+	v8::HandleScope handle_scope(c->isolate);
+
+	// Enter the module context
+	v8::Context::Scope scope(context);
+	// Set script identifier
+	v8::Local<v8::String> sname = V8JS_SYM("require");
+
+	v8::Local<v8::String> source = v8::String::New(Z_STRVAL(module_code));
+
+	// Create and compile script
+	v8::Local<v8::Script> script = v8::Script::New(source, sname);
+
+	// The script will be empty if there are compile errors
+	if (script.IsEmpty()) {
+		efree(normalised_path);
+		return v8::ThrowException(v8::String::New("Module script compile failed"));
+	}
+
+	// Add this module and path to the stack
+	c->modules_stack.push_back(normalised_module_id);
+
+	c->modules_base.push_back(normalised_path);
+
+	// Run script
+	v8::Local<v8::Value> result = script->Run();
+
+	// Remove this module and path from the stack
+	c->modules_stack.pop_back();
+	c->modules_base.pop_back();
+
+	efree(normalised_path);
+
+	// Script possibly terminated, return immediately
+	if (!try_catch.CanContinue()) {
+		return v8::ThrowException(v8::String::New("Module script compile failed"));
+	}
+
+	// Handle runtime JS exceptions
+	if (try_catch.HasCaught()) {
+		// Rethrow the exception back to JS
+		return try_catch.ReThrow();
+	}
+
+	// Cache the module so it doesn't need to be compiled and run again
+	// Ensure compatibility with CommonJS implementations such as NodeJS by playing nicely with module.exports and exports
+	if (module->Has(v8::String::New("exports")) && !module->Get(v8::String::New("exports"))->IsUndefined()) {
+		// If module.exports has been set then we cache this arbitrary value...
+		V8JSG(modules_loaded)[normalised_module_id] = handle_scope.Close(module->Get(v8::String::New("exports"))->ToObject());
+	} else {
+		// ...otherwise we cache the exports object itself
+		V8JSG(modules_loaded)[normalised_module_id] = handle_scope.Close(exports);
+	}
+
+	return V8JSG(modules_loaded)[normalised_module_id];
+}
+
+void php_v8js_register_methods(v8::Handle<v8::ObjectTemplate> global, php_v8js_ctx *c) /* {{{ */
 {
 	global->Set(V8JS_SYM("exit"), v8::FunctionTemplate::New(V8JS_MN(exit)), v8::ReadOnly);
 	global->Set(V8JS_SYM("sleep"), v8::FunctionTemplate::New(V8JS_MN(sleep)), v8::ReadOnly);
 	global->Set(V8JS_SYM("print"), v8::FunctionTemplate::New(V8JS_MN(print)), v8::ReadOnly);
 	global->Set(V8JS_SYM("var_dump"), v8::FunctionTemplate::New(V8JS_MN(var_dump)), v8::ReadOnly);
+
+	c->modules_base.push_back("");
+	global->Set(V8JS_SYM("require"), v8::FunctionTemplate::New(V8JS_MN(require), v8::External::New(c)), v8::ReadOnly);
 }
 /* }}} */
 

+ 22 - 8
v8js_variables.cc

@@ -29,26 +29,34 @@ extern "C" {
 
 #include "php_v8js_macros.h"
 #include <v8.h>
+#include <string>
+
+struct php_v8js_accessor_ctx
+{
+    char *variable_name_string;
+    uint variable_name_string_len;
+    v8::Isolate *isolate;
+};
 
 static v8::Handle<v8::Value> php_v8js_fetch_php_variable(v8::Local<v8::String> name, const v8::AccessorInfo &info) /* {{{ */
 {
-	v8::String::Utf8Value variable_name(info.Data()->ToString());
-	const char *variable_name_string = ToCString(variable_name);
-	uint variable_name_string_len = strlen(variable_name_string);
+    v8::Handle<v8::External> data = v8::Handle<v8::External>::Cast(info.Data());
+    php_v8js_accessor_ctx *ctx = static_cast<php_v8js_accessor_ctx *>(data->Value());
 	zval **variable;
 
 	TSRMLS_FETCH();
 
-	zend_is_auto_global(variable_name_string, variable_name_string_len TSRMLS_CC);
+	zend_is_auto_global(ctx->variable_name_string, ctx->variable_name_string_len TSRMLS_CC);
 
-	if (zend_hash_find(&EG(symbol_table), variable_name_string, variable_name_string_len + 1, (void **) &variable) == SUCCESS) {
-		return zval_to_v8js(*variable TSRMLS_CC);
+	if (zend_hash_find(&EG(symbol_table), ctx->variable_name_string, ctx->variable_name_string_len + 1, (void **) &variable) == SUCCESS) {
+		return zval_to_v8js(*variable, ctx->isolate TSRMLS_CC);
 	}
+
 	return v8::Undefined();
 }
 /* }}} */
 
-void php_v8js_register_accessors(v8::Local<v8::ObjectTemplate> php_obj, zval *array TSRMLS_DC) /* {{{ */
+void php_v8js_register_accessors(v8::Local<v8::ObjectTemplate> php_obj, zval *array, v8::Isolate *isolate TSRMLS_DC) /* {{{ */
 {
 	char *property_name;
 	uint property_name_len;
@@ -76,8 +84,14 @@ void php_v8js_register_accessors(v8::Local<v8::ObjectTemplate> php_obj, zval *ar
 			continue; /* Ignore invalid property names */
 		}
 
+        // Create context to store accessor data
+        php_v8js_accessor_ctx *ctx = (php_v8js_accessor_ctx *)emalloc(sizeof(php_v8js_accessor_ctx));
+        ctx->variable_name_string = estrdup(Z_STRVAL_PP(item));
+        ctx->variable_name_string_len = Z_STRLEN_PP(item);
+        ctx->isolate = isolate;
+
 		/* Set the variable fetch callback for given symbol on named property */
-		php_obj->SetAccessor(V8JS_STRL(property_name, property_name_len - 1), php_v8js_fetch_php_variable, NULL, V8JS_STRL(Z_STRVAL_PP(item), Z_STRLEN_PP(item)), v8::PROHIBITS_OVERWRITING, v8::ReadOnly);
+		php_obj->SetAccessor(V8JS_STRL(property_name, property_name_len - 1), php_v8js_fetch_php_variable, NULL, v8::External::New(ctx), v8::PROHIBITS_OVERWRITING, v8::ReadOnly);
 	}
 }
 /* }}} */