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

Merged CommonJS modules functionality.

Simon Best 12 éve
szülő
commit
93df3411d9
3 módosított fájl, 284 hozzáadás és 20 törlés
  1. 19 3
      php_v8js_macros.h
  2. 25 16
      v8js.cc
  3. 240 1
      v8js_methods.cc

+ 19 - 3
php_v8js_macros.h

@@ -91,12 +91,28 @@ v8::Handle<v8::Value> zval_to_v8js(zval * 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>);
-
 /* Register accessors into passed object */
 void php_v8js_register_accessors(v8::Local<v8::ObjectTemplate>, zval * 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;
+};
+/* }}} */
+
+/* Register builtin methods into passed object */
+void php_v8js_register_methods(v8::Handle<v8::ObjectTemplate>, php_v8js_ctx *c);
+
 #endif	/* PHP_V8JS_MACROS_H */
 
 /*

+ 25 - 16
v8js.cc

@@ -46,21 +46,6 @@ extern "C" {
 static void php_v8js_throw_script_exception(v8::TryCatch * TSRMLS_DC);
 static void php_v8js_create_script_exception(zval *, v8::TryCatch * 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;
-};
-/* }}} */
-
 // Timer context
 struct php_v8js_timer_ctx
 {
@@ -610,6 +595,7 @@ static PHP_METHOD(V8Js, __construct)
 	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);
@@ -642,7 +628,7 @@ static PHP_METHOD(V8Js, __construct)
 	c->global_template->SetClassName(V8JS_SYM("V8Js"));
 
 	/* Register builtin methods */
-	php_v8js_register_methods(c->global_template->InstanceTemplate());
+	php_v8js_register_methods(c->global_template->InstanceTemplate(), c);
 
 	/* Create context */
 	c->context = v8::Context::New(&extension_conf, c->global_template->InstanceTemplate());
@@ -923,6 +909,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;
@@ -1084,6 +1088,10 @@ 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)
@@ -1102,6 +1110,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}

+ 240 - 1
v8js_methods.cc

@@ -25,10 +25,13 @@
 
 extern "C" {
 #include "php.h"
+#include "zend_exceptions.h"
 }
 
 #include "php_v8js_macros.h"
 #include <v8.h>
+#include <map>
+#include <vector>
 
 /* global.exit - terminate execution */
 V8JS_METHOD(exit) /* {{{ */
@@ -167,12 +170,248 @@ V8JS_METHOD(var_dump) /* {{{ */
 }
 /* }}} */
 
-void php_v8js_register_methods(v8::Handle<v8::ObjectTemplate> global) /* {{{ */
+// TODO: Put this in php_v8js_context
+std::map<char *, v8::Handle<v8::Object> > modules_loaded;
+std::vector<char *> modules_stack;
+std::vector<char *> modules_base;
+
+void 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 normalize_identifier(char *base, char *identifier, char *normalised_path, char *module_name)
+{
+	std::vector<char *> id_terms, terms;
+	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(), "..")) {
+		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, "..")) {
+			normalised_terms.pop_back();
+		} else if (strcmp(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);
+	}
+}
+
+V8JS_METHOD(require)
+{
+	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 = strdup(ToCString(module_id_v8));
+	char *normalised_path = (char *)malloc(PATH_MAX);
+	char *module_name = (char *)malloc(PATH_MAX);
+
+	normalize_identifier(modules_base.back(), module_id, normalised_path, module_name);
+
+	char *normalised_module_id = (char *)malloc(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);
+
+	// Check for module cyclic dependencies
+	for (std::vector<char *>::iterator it = modules_stack.begin(); it != modules_stack.end(); ++it)
+    {
+    	if (!strcmp(*it, normalised_module_id)) {
+    		return v8::ThrowException(v8::String::New("Module cyclic dependency"));
+    	}
+    }
+
+    // If we have already loaded and cached this module then use it
+	if (modules_loaded.count(normalised_module_id) > 0) {
+		return modules_loaded[normalised_module_id];
+	}
+
+	// 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"));
+	}
+
+	// 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)) {
+		return v8::ThrowException(v8::String::New("Module loader callback failed"));
+	}
+
+	// Check if an exception was thrown
+	if (EG(exception)) {
+		// 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))) {
+		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;
+
+	// Enter the module context
+	v8::Context::Scope scope(context);
+
+	v8::HandleScope handle_scope;
+
+	// 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()) {
+		return v8::ThrowException(v8::String::New("Module script compile failed"));
+	}
+
+	// Add this module and path to the stack
+	modules_stack.push_back(normalised_module_id);
+	modules_base.push_back(normalised_path);
+
+	// Run script
+	v8::Local<v8::Value> result = script->Run();
+
+	// Remove this module and path from the stack
+	modules_stack.pop_back();
+	modules_base.pop_back();
+
+	// 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...
+		modules_loaded[normalised_module_id] = handle_scope.Close(module->Get(v8::String::New("exports"))->ToObject());
+	} else {
+		// ...otherwise we cache the exports object itself
+		modules_loaded[normalised_module_id] = handle_scope.Close(exports);		
+	}
+
+	return 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);
+
+	modules_base.push_back("");
+	global->Set(V8JS_SYM("require"), v8::FunctionTemplate::New(V8JS_MN(require), v8::External::New(c)), v8::ReadOnly);
 }
 /* }}} */