From 90ed152f407394e161045ece094b8fc53f4883e5 Mon Sep 17 00:00:00 2001 From: Stefan Siegl Date: Wed, 12 Jun 2013 09:11:30 +0200 Subject: [PATCH 1/3] Cache and re-use v8::FunctionTemplate on object wrapping --- v8js_convert.cc | 147 ++++++++++++++++++++++++++++-------------------- 1 file changed, 85 insertions(+), 62 deletions(-) diff --git a/v8js_convert.cc b/v8js_convert.cc index 5784c84..00e6c2d 100644 --- a/v8js_convert.cc +++ b/v8js_convert.cc @@ -32,6 +32,8 @@ extern "C" { #include "php_v8js_macros.h" #include +#include +#include /* Callback for PHP methods and functions */ static v8::Handle php_v8js_php_callback(const v8::Arguments &args) /* {{{ */ @@ -278,7 +280,6 @@ static v8::Handle php_v8js_property_query(v8::Local pro static v8::Handle php_v8js_hash_to_jsobj(zval *value, v8::Isolate *isolate TSRMLS_DC) /* {{{ */ { - v8::Local new_tpl = v8::FunctionTemplate::New(); v8::Local newobj; int i; char *key = NULL; @@ -303,75 +304,95 @@ static v8::Handle php_v8js_hash_to_jsobj(zval *value, v8::Isolate *is /* Object methods */ if (ce) { - new_tpl->SetClassName(V8JS_STRL(ce->name, ce->name_length)); - new_tpl->InstanceTemplate()->SetInternalFieldCount(2); + v8::Handle new_tpl; + static std::map > tpl_map; + std::map >::iterator it; - if (ce == zend_ce_closure) { - new_tpl->InstanceTemplate()->SetCallAsFunctionHandler(php_v8js_php_callback); - newobj = new_tpl->InstanceTemplate()->NewInstance(); - } else { - zend_hash_internal_pointer_reset_ex(&ce->function_table, &pos); - for (;; zend_hash_move_forward_ex(&ce->function_table, &pos)) { - if (zend_hash_get_current_key_ex(&ce->function_table, &key, &key_len, &index, 0, &pos) != HASH_KEY_IS_STRING || - zend_hash_get_current_data_ex(&ce->function_table, (void **) &method_ptr, &pos) == FAILURE - ) { - break; - } + try { + new_tpl = tpl_map.at(ce->name); + } + catch (const std::out_of_range &) { + /* No cached v8::FunctionTemplate available as of yet, create one. */ + new_tpl = v8::FunctionTemplate::New(); - if ((method_ptr->common.fn_flags & ZEND_ACC_PUBLIC) && /* Allow only public methods */ - (method_ptr->common.fn_flags & ZEND_ACC_CTOR) == 0 && /* ..and no __construct() */ - (method_ptr->common.fn_flags & ZEND_ACC_DTOR) == 0 && /* ..or __destruct() */ - (method_ptr->common.fn_flags & ZEND_ACC_CLONE) == 0 /* ..or __clone() functions */ - ) { - /* Override native toString() with __tostring() if it is set in passed object */ - if (IS_MAGIC_FUNC(ZEND_TOSTRING_FUNC_NAME)) { - new_tpl->InstanceTemplate()->Set(V8JS_SYM("toString"), PHP_V8JS_CALLBACK(method_ptr)); - /* TODO: __set(), __unset() disabled as JS is not allowed to modify the passed PHP object yet. - * __sleep(), __wakeup(), __set_state() are always ignored */ - } else if ( - IS_MAGIC_FUNC(ZEND_CALLSTATIC_FUNC_NAME)|| /* TODO */ - IS_MAGIC_FUNC(ZEND_SLEEP_FUNC_NAME) || - IS_MAGIC_FUNC(ZEND_WAKEUP_FUNC_NAME) || - IS_MAGIC_FUNC(ZEND_SET_STATE_FUNC_NAME) || - IS_MAGIC_FUNC(ZEND_SET_FUNC_NAME) || - IS_MAGIC_FUNC(ZEND_UNSET_FUNC_NAME) + new_tpl->SetClassName(V8JS_STRL(ce->name, ce->name_length)); + new_tpl->InstanceTemplate()->SetInternalFieldCount(2); + + if (ce == zend_ce_closure) { + /* Got a closure, mustn't cache ... */ + new_tpl->InstanceTemplate()->SetCallAsFunctionHandler(php_v8js_php_callback); + } else { + /* Add new v8::FunctionTemplate to tpl_map, as long as it is not a closure. */ + tpl_map[ce->name] = v8::Persistent::New(isolate, new_tpl); + + /* Attach object methods to the instance template. */ + zend_hash_internal_pointer_reset_ex(&ce->function_table, &pos); + for (;; zend_hash_move_forward_ex(&ce->function_table, &pos)) { + if (zend_hash_get_current_key_ex(&ce->function_table, &key, &key_len, &index, 0, &pos) != HASH_KEY_IS_STRING || + zend_hash_get_current_data_ex(&ce->function_table, (void **) &method_ptr, &pos) == FAILURE ) { - /* Register all magic function as hidden with lowercase name */ - } else if (IS_MAGIC_FUNC(ZEND_GET_FUNC_NAME)) { - get_ptr = method_ptr; - } else if (IS_MAGIC_FUNC(ZEND_CALL_FUNC_NAME)) { - call_ptr = method_ptr; - } else if (IS_MAGIC_FUNC(ZEND_INVOKE_FUNC_NAME)) { - invoke_ptr = method_ptr; - } else if (IS_MAGIC_FUNC(ZEND_ISSET_FUNC_NAME)) { - isset_ptr = method_ptr; - } else { - new_tpl->InstanceTemplate()->Set(V8JS_STR(method_ptr->common.function_name), PHP_V8JS_CALLBACK(method_ptr), v8::ReadOnly); + break; + } + + if ((method_ptr->common.fn_flags & ZEND_ACC_PUBLIC) && /* Allow only public methods */ + (method_ptr->common.fn_flags & ZEND_ACC_CTOR) == 0 && /* ..and no __construct() */ + (method_ptr->common.fn_flags & ZEND_ACC_DTOR) == 0 && /* ..or __destruct() */ + (method_ptr->common.fn_flags & ZEND_ACC_CLONE) == 0 /* ..or __clone() functions */ + ) { + /* Override native toString() with __tostring() if it is set in passed object */ + if (IS_MAGIC_FUNC(ZEND_TOSTRING_FUNC_NAME)) { + new_tpl->InstanceTemplate()->Set(V8JS_SYM("toString"), PHP_V8JS_CALLBACK(method_ptr)); + /* TODO: __set(), __unset() disabled as JS is not allowed to modify the passed PHP object yet. + * __sleep(), __wakeup(), __set_state() are always ignored */ + } else if ( + IS_MAGIC_FUNC(ZEND_CALLSTATIC_FUNC_NAME)|| /* TODO */ + IS_MAGIC_FUNC(ZEND_SLEEP_FUNC_NAME) || + IS_MAGIC_FUNC(ZEND_WAKEUP_FUNC_NAME) || + IS_MAGIC_FUNC(ZEND_SET_STATE_FUNC_NAME) || + IS_MAGIC_FUNC(ZEND_SET_FUNC_NAME) || + IS_MAGIC_FUNC(ZEND_UNSET_FUNC_NAME) + ) { + /* Register all magic function as hidden with lowercase name */ + } else if (IS_MAGIC_FUNC(ZEND_GET_FUNC_NAME)) { + get_ptr = method_ptr; + } else if (IS_MAGIC_FUNC(ZEND_CALL_FUNC_NAME)) { + call_ptr = method_ptr; + } else if (IS_MAGIC_FUNC(ZEND_INVOKE_FUNC_NAME)) { + invoke_ptr = method_ptr; + } else if (IS_MAGIC_FUNC(ZEND_ISSET_FUNC_NAME)) { + isset_ptr = method_ptr; + } else { + new_tpl->InstanceTemplate()->Set(V8JS_STR(method_ptr->common.function_name), PHP_V8JS_CALLBACK(method_ptr), v8::ReadOnly); + } } } + + + /* Only register getter, etc. when they're set in PHP side */ + if (call_ptr || get_ptr || isset_ptr) + { + /* Set __get() handler which acts also as __call() proxy */ + new_tpl->InstanceTemplate()->SetNamedPropertyHandler( + php_v8js_property_getter, /* getter */ + 0, /* setter */ + isset_ptr ? php_v8js_property_query : 0, /* query */ + 0, /* deleter */ + 0, /* enumerator */ + V8JS_BOOL(call_ptr ? true : false) + ); + } + + + /* __invoke() handler */ + if (invoke_ptr) { + new_tpl->InstanceTemplate()->SetCallAsFunctionHandler(php_v8js_property_caller, V8JS_SYM(ZEND_INVOKE_FUNC_NAME)); + } } + } - /* Only register getter, etc. when they're set in PHP side */ - if (call_ptr || get_ptr || isset_ptr) - { - /* Set __get() handler which acts also as __call() proxy */ - new_tpl->InstanceTemplate()->SetNamedPropertyHandler( - php_v8js_property_getter, /* getter */ - 0, /* setter */ - isset_ptr ? php_v8js_property_query : 0, /* query */ - 0, /* deleter */ - 0, /* enumerator */ - V8JS_BOOL(call_ptr ? true : false) - ); - } - - /* __invoke() handler */ - if (invoke_ptr) { - new_tpl->InstanceTemplate()->SetCallAsFunctionHandler(php_v8js_property_caller, V8JS_SYM(ZEND_INVOKE_FUNC_NAME)); - } - - newobj = new_tpl->InstanceTemplate()->NewInstance(); + newobj = new_tpl->InstanceTemplate()->NewInstance(); + if (ce != zend_ce_closure) { // @fixme all of those get lost, when using cached templates if (call_ptr) { newobj->SetHiddenValue(V8JS_SYM(ZEND_CALL_FUNC_NAME), PHP_V8JS_CALLBACK(call_ptr)); } @@ -392,6 +413,8 @@ static v8::Handle php_v8js_hash_to_jsobj(zval *value, v8::Isolate *is newobj->SetAlignedPointerInInternalField(0, (void *) value); newobj->SetAlignedPointerInInternalField(1, (void *) isolate); } else { + v8::Local new_tpl = v8::FunctionTemplate::New(); // @todo re-use template likewise + new_tpl->SetClassName(V8JS_SYM("Array")); newobj = new_tpl->InstanceTemplate()->NewInstance(); } From bc1cf7c5bb254dad9ddb81f2778ed3c8971554e0 Mon Sep 17 00:00:00 2001 From: Stefan Siegl Date: Wed, 12 Jun 2013 13:13:47 +0200 Subject: [PATCH 2/3] Use weak persistent handles and DELREF zval. --- v8js_convert.cc | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/v8js_convert.cc b/v8js_convert.cc index 00e6c2d..3ce9dfa 100644 --- a/v8js_convert.cc +++ b/v8js_convert.cc @@ -278,9 +278,23 @@ static v8::Handle php_v8js_property_query(v8::Local pro #define PHP_V8JS_CALLBACK(mptr) \ v8::FunctionTemplate::New(php_v8js_php_callback, v8::External::New(mptr))->GetFunction() + +static void php_v8js_weak_object_callback(v8::Isolate *isolate, v8::Persistent *object, zval *value) +{ + if (READY_TO_DESTROY(value)) { + zval_dtor(value); + FREE_ZVAL(value); + } else { + Z_DELREF_P(value); + } + + v8::V8::AdjustAmountOfExternalAllocatedMemory(-1024); + object->Dispose(); +} + static v8::Handle php_v8js_hash_to_jsobj(zval *value, v8::Isolate *isolate TSRMLS_DC) /* {{{ */ { - v8::Local newobj; + v8::Handle newobj; int i; char *key = NULL; ulong index; @@ -390,7 +404,17 @@ static v8::Handle php_v8js_hash_to_jsobj(zval *value, v8::Isolate *is } } - newobj = new_tpl->InstanceTemplate()->NewInstance(); + // Since we got to decrease the reference count again, in case v8 garbage collector + // decides to dispose the JS object, we add a weak persistent handle and register + // a callback function that removes the reference. + v8::Persistent persist_newobj = v8::Persistent::New(isolate, new_tpl->GetFunction()->NewInstance()); + persist_newobj.MakeWeak(value, php_v8js_weak_object_callback); + + // Just tell v8 that we're allocating some external memory + // (for the moment we just always tell 1k instead of trying to find out actual values) + v8::V8::AdjustAmountOfExternalAllocatedMemory(1024); + + newobj = persist_newobj; if (ce != zend_ce_closure) { // @fixme all of those get lost, when using cached templates if (call_ptr) { From ee659bdf416948f0411521845df417d3cb026d08 Mon Sep 17 00:00:00 2001 From: Stefan Siegl Date: Wed, 12 Jun 2013 14:46:39 +0200 Subject: [PATCH 3/3] Correctly attach hidden values to objects from cached templates. --- v8js_convert.cc | 92 ++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/v8js_convert.cc b/v8js_convert.cc index 3ce9dfa..6999382 100644 --- a/v8js_convert.cc +++ b/v8js_convert.cc @@ -319,13 +319,15 @@ static v8::Handle php_v8js_hash_to_jsobj(zval *value, v8::Isolate *is /* Object methods */ if (ce) { v8::Handle new_tpl; + bool cached_tpl = true; static std::map > tpl_map; - std::map >::iterator it; try { new_tpl = tpl_map.at(ce->name); } catch (const std::out_of_range &) { + cached_tpl = false; + /* No cached v8::FunctionTemplate available as of yet, create one. */ new_tpl = v8::FunctionTemplate::New(); @@ -338,50 +340,53 @@ static v8::Handle php_v8js_hash_to_jsobj(zval *value, v8::Isolate *is } else { /* Add new v8::FunctionTemplate to tpl_map, as long as it is not a closure. */ tpl_map[ce->name] = v8::Persistent::New(isolate, new_tpl); + } + } - /* Attach object methods to the instance template. */ - zend_hash_internal_pointer_reset_ex(&ce->function_table, &pos); - for (;; zend_hash_move_forward_ex(&ce->function_table, &pos)) { - if (zend_hash_get_current_key_ex(&ce->function_table, &key, &key_len, &index, 0, &pos) != HASH_KEY_IS_STRING || - zend_hash_get_current_data_ex(&ce->function_table, (void **) &method_ptr, &pos) == FAILURE - ) { - break; - } - - if ((method_ptr->common.fn_flags & ZEND_ACC_PUBLIC) && /* Allow only public methods */ - (method_ptr->common.fn_flags & ZEND_ACC_CTOR) == 0 && /* ..and no __construct() */ - (method_ptr->common.fn_flags & ZEND_ACC_DTOR) == 0 && /* ..or __destruct() */ - (method_ptr->common.fn_flags & ZEND_ACC_CLONE) == 0 /* ..or __clone() functions */ - ) { - /* Override native toString() with __tostring() if it is set in passed object */ - if (IS_MAGIC_FUNC(ZEND_TOSTRING_FUNC_NAME)) { - new_tpl->InstanceTemplate()->Set(V8JS_SYM("toString"), PHP_V8JS_CALLBACK(method_ptr)); - /* TODO: __set(), __unset() disabled as JS is not allowed to modify the passed PHP object yet. - * __sleep(), __wakeup(), __set_state() are always ignored */ - } else if ( - IS_MAGIC_FUNC(ZEND_CALLSTATIC_FUNC_NAME)|| /* TODO */ - IS_MAGIC_FUNC(ZEND_SLEEP_FUNC_NAME) || - IS_MAGIC_FUNC(ZEND_WAKEUP_FUNC_NAME) || - IS_MAGIC_FUNC(ZEND_SET_STATE_FUNC_NAME) || - IS_MAGIC_FUNC(ZEND_SET_FUNC_NAME) || - IS_MAGIC_FUNC(ZEND_UNSET_FUNC_NAME) - ) { - /* Register all magic function as hidden with lowercase name */ - } else if (IS_MAGIC_FUNC(ZEND_GET_FUNC_NAME)) { - get_ptr = method_ptr; - } else if (IS_MAGIC_FUNC(ZEND_CALL_FUNC_NAME)) { - call_ptr = method_ptr; - } else if (IS_MAGIC_FUNC(ZEND_INVOKE_FUNC_NAME)) { - invoke_ptr = method_ptr; - } else if (IS_MAGIC_FUNC(ZEND_ISSET_FUNC_NAME)) { - isset_ptr = method_ptr; - } else { - new_tpl->InstanceTemplate()->Set(V8JS_STR(method_ptr->common.function_name), PHP_V8JS_CALLBACK(method_ptr), v8::ReadOnly); - } - } + if (ce != zend_ce_closure) { + /* Attach object methods to the instance template. */ + zend_hash_internal_pointer_reset_ex(&ce->function_table, &pos); + for (;; zend_hash_move_forward_ex(&ce->function_table, &pos)) { + if (zend_hash_get_current_key_ex(&ce->function_table, &key, &key_len, &index, 0, &pos) != HASH_KEY_IS_STRING || + zend_hash_get_current_data_ex(&ce->function_table, (void **) &method_ptr, &pos) == FAILURE + ) { + break; } + if ((method_ptr->common.fn_flags & ZEND_ACC_PUBLIC) && /* Allow only public methods */ + (method_ptr->common.fn_flags & ZEND_ACC_CTOR) == 0 && /* ..and no __construct() */ + (method_ptr->common.fn_flags & ZEND_ACC_DTOR) == 0 && /* ..or __destruct() */ + (method_ptr->common.fn_flags & ZEND_ACC_CLONE) == 0 /* ..or __clone() functions */ + ) { + /* Override native toString() with __tostring() if it is set in passed object */ + if (!cached_tpl && IS_MAGIC_FUNC(ZEND_TOSTRING_FUNC_NAME)) { + new_tpl->InstanceTemplate()->Set(V8JS_SYM("toString"), PHP_V8JS_CALLBACK(method_ptr)); + /* TODO: __set(), __unset() disabled as JS is not allowed to modify the passed PHP object yet. + * __sleep(), __wakeup(), __set_state() are always ignored */ + } else if ( + IS_MAGIC_FUNC(ZEND_CALLSTATIC_FUNC_NAME)|| /* TODO */ + IS_MAGIC_FUNC(ZEND_SLEEP_FUNC_NAME) || + IS_MAGIC_FUNC(ZEND_WAKEUP_FUNC_NAME) || + IS_MAGIC_FUNC(ZEND_SET_STATE_FUNC_NAME) || + IS_MAGIC_FUNC(ZEND_SET_FUNC_NAME) || + IS_MAGIC_FUNC(ZEND_UNSET_FUNC_NAME) + ) { + /* Register all magic function as hidden with lowercase name */ + } else if (IS_MAGIC_FUNC(ZEND_GET_FUNC_NAME)) { + get_ptr = method_ptr; + } else if (IS_MAGIC_FUNC(ZEND_CALL_FUNC_NAME)) { + call_ptr = method_ptr; + } else if (IS_MAGIC_FUNC(ZEND_INVOKE_FUNC_NAME)) { + invoke_ptr = method_ptr; + } else if (IS_MAGIC_FUNC(ZEND_ISSET_FUNC_NAME)) { + isset_ptr = method_ptr; + } else if (!cached_tpl) { + new_tpl->InstanceTemplate()->Set(V8JS_STR(method_ptr->common.function_name), PHP_V8JS_CALLBACK(method_ptr), v8::ReadOnly); + } + } + } + if (!cached_tpl) { /* Only register getter, etc. when they're set in PHP side */ if (call_ptr || get_ptr || isset_ptr) { @@ -396,7 +401,6 @@ static v8::Handle php_v8js_hash_to_jsobj(zval *value, v8::Isolate *is ); } - /* __invoke() handler */ if (invoke_ptr) { new_tpl->InstanceTemplate()->SetCallAsFunctionHandler(php_v8js_property_caller, V8JS_SYM(ZEND_INVOKE_FUNC_NAME)); @@ -416,7 +420,9 @@ static v8::Handle php_v8js_hash_to_jsobj(zval *value, v8::Isolate *is newobj = persist_newobj; - if (ce != zend_ce_closure) { // @fixme all of those get lost, when using cached templates + if (ce != zend_ce_closure) { + // These unfortunately cannot be attached to the template, hence we have to put them + // on each and every object instance manually. if (call_ptr) { newobj->SetHiddenValue(V8JS_SYM(ZEND_CALL_FUNC_NAME), PHP_V8JS_CALLBACK(call_ptr)); }