diff --git a/README.md b/README.md index 377ee3ee..f577cd9a 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,13 @@ class V8Js public function setAverageObjectSize($average_object_size) {} + /** + * Install a V8 inspector (client) on this V8Js object. + * This is an experimental feature. + * @return V8Inspector + */ + public function connectInspector(); + /** * Returns uncaught pending exception or null if there is no pending exception. * @return V8JsScriptException|null @@ -206,6 +213,30 @@ class V8Js {} } +final class V8Inspector +{ + /** + * Send a message to V8's inspector backend. + * @param string $message + */ + public function send($message) + {} + + /** + * Register a callback handler for responses from V8's inspector backend. + * @param callable $handler + */ + public function setResponseHandler($handler) + {} + + /** + * Register a callback handler for notifications from V8's inspector backend. + * @param callable $handler + */ + public function setNotificationHandler($handler) + {} +} + final class V8JsScriptException extends Exception { /** @@ -401,3 +432,56 @@ objects obeying the above rules and re-thrown in JavaScript context. If they are not caught by JavaScript code the execution stops and a `V8JsScriptException` is thrown, which has the original PHP exception accessible via `getPrevious` method. + +Inspector Client +================ + +Keep in mind that this is an experimental feature of php-v8js. + +The inspector client API, i.e. calling `V8Js::connectInspector()`, provides +low-level access to [V8's Inspector Protocol](https://v8.dev/docs/inspector). +This may be used to access V8's profiler. + +Theoretically this also provides access to V8's debugger. Yet keep in mind +that setting a breakpoint in the JavaScript code will also block PHP code +execution (due to php-v8js' call model). + +See [Chrome DevTool Protocol API documentation](https://chromedevtools.github.io/devtools-protocol/1-3/Profiler/) +for details on how to use the profiler. + +Usage example to collect call count information on function level: + +```php +connectInspector(); + +$i->setResponseHandler(function($res) { + $res = \json_decode($res); + + if ($res->id === 3) { + foreach($res->result->result[0]->functions as $info) { + printf("function '%s' was called %d times.\n", $info->functionName ?: '', $info->ranges[0]->count); + } + } +}); + +$i->send(json_encode([ 'id' => 1, 'method' => "Profiler.enable" ])); +$i->send(json_encode([ 'id' => 2, 'method' => "Profiler.startPreciseCoverage", 'params' => [ 'callCount' => true ] ])); + +$fn = $v8->executeString('(function foo() { const blarg = 42; })', 'multi-call-lambda'); + +$fn(); +$fn(); +$fn(); + +$i->send(json_encode([ 'id' => 3, 'method' => "Profiler.takePreciseCoverage" ])); +``` + +yields + +``` +function '' was called 1 times. +function 'foo' was called 3 times. +``` diff --git a/config.m4 b/config.m4 index 2e463b47..85014e30 100644 --- a/config.m4 +++ b/config.m4 @@ -217,6 +217,7 @@ int main () v8js_timer.cc \ v8js_v8.cc \ v8js_v8object_class.cc \ + v8js_v8inspector_class.cc \ v8js_variables.cc \ ], $ext_shared, , "$ac_cv_v8_narrowing -std="$ac_cv_v8_cstd) diff --git a/v8js_class.cc b/v8js_class.cc index 4e9be672..1501b964 100644 --- a/v8js_class.cc +++ b/v8js_class.cc @@ -25,6 +25,7 @@ #include "v8js_v8object_class.h" #include "v8js_object_export.h" #include "v8js_timer.h" +#include "v8js_v8inspector_class.h" extern "C" { #include "php.h" @@ -848,6 +849,22 @@ static PHP_METHOD(V8Js, clearPendingException) } /* }}} */ +/* {{{ proto void V8Js::connectInspector() + */ +static PHP_METHOD(V8Js, connectInspector) +{ + v8js_ctx *c; + + if (zend_parse_parameters_none() == FAILURE) { + return; + } + + c = Z_V8JS_CTX_OBJ_P(getThis()); + v8js_v8inspector_create(return_value, c); +} +/* }}} */ + + /* {{{ proto void V8Js::setModuleNormaliser(string base, string module_id) */ static PHP_METHOD(V8Js, setModuleNormaliser) @@ -1245,6 +1262,9 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_v8js_clearpendingexception, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO(arginfo_v8js_connectinspector, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_INFO_EX(arginfo_v8js_setmodulenormaliser, 0, 0, 2) ZEND_ARG_INFO(0, base) ZEND_ARG_INFO(0, module_id) @@ -1291,6 +1311,7 @@ const zend_function_entry v8js_methods[] = { /* {{{ */ PHP_ME(V8Js, checkString, arginfo_v8js_checkstring, ZEND_ACC_PUBLIC|ZEND_ACC_DEPRECATED) PHP_ME(V8Js, getPendingException, arginfo_v8js_getpendingexception, ZEND_ACC_PUBLIC|ZEND_ACC_DEPRECATED) PHP_ME(V8Js, clearPendingException, arginfo_v8js_clearpendingexception, ZEND_ACC_PUBLIC|ZEND_ACC_DEPRECATED) + PHP_ME(V8Js, connectInspector, arginfo_v8js_connectinspector, ZEND_ACC_PUBLIC) PHP_ME(V8Js, setModuleNormaliser, arginfo_v8js_setmodulenormaliser, ZEND_ACC_PUBLIC) PHP_ME(V8Js, setModuleLoader, arginfo_v8js_setmoduleloader, ZEND_ACC_PUBLIC) PHP_ME(V8Js, setTimeLimit, arginfo_v8js_settimelimit, ZEND_ACC_PUBLIC) diff --git a/v8js_main.cc b/v8js_main.cc index 66b58ef1..b3a5519b 100644 --- a/v8js_main.cc +++ b/v8js_main.cc @@ -27,6 +27,7 @@ extern "C" { #include "v8js_class.h" #include "v8js_exceptions.h" #include "v8js_v8object_class.h" +#include "v8js_v8inspector_class.h" ZEND_DECLARE_MODULE_GLOBALS(v8js) struct _v8js_process_globals v8js_process_globals; @@ -139,6 +140,7 @@ PHP_MINIT_FUNCTION(v8js) PHP_MINIT(v8js_class)(INIT_FUNC_ARGS_PASSTHRU); PHP_MINIT(v8js_exceptions)(INIT_FUNC_ARGS_PASSTHRU); PHP_MINIT(v8js_v8object_class)(INIT_FUNC_ARGS_PASSTHRU); + PHP_MINIT(v8js_v8inspector_class)(INIT_FUNC_ARGS_PASSTHRU); REGISTER_INI_ENTRIES(); diff --git a/v8js_v8inspector_class.cc b/v8js_v8inspector_class.cc new file mode 100644 index 00000000..c35b9162 --- /dev/null +++ b/v8js_v8inspector_class.cc @@ -0,0 +1,297 @@ +/* + +----------------------------------------------------------------------+ + | PHP Version 7 | + +----------------------------------------------------------------------+ + | Copyright (c) 2020 The PHP Group | + +----------------------------------------------------------------------+ + | http://www.opensource.org/licenses/mit-license.php MIT License | + +----------------------------------------------------------------------+ + | Author: Stefan Siegl | + +----------------------------------------------------------------------+ +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "php_v8js_macros.h" +#include "v8js_exceptions.h" +#include "v8js_v8inspector_class.h" + +#include + +extern "C" { +#include "zend_exceptions.h" +} + + +/* {{{ Class Entries */ +zend_class_entry *php_ce_v8inspector; +/* }}} */ + +/* {{{ Object Handlers */ +static zend_object_handlers v8js_v8inspector_handlers; +/* }}} */ + + + +class InspectorFrontend final : public v8_inspector::V8Inspector::Channel { + public: + explicit InspectorFrontend(v8::Local context, zval *response_handler, zval *notification_handler) { + isolate_ = context->GetIsolate(); + response_handler_ = response_handler; + notification_handler_ = notification_handler; + } + ~InspectorFrontend() override = default; + + private: + void sendResponse( + int callId, + std::unique_ptr message) override { + invokeHandler(message->string(), response_handler_); + } + void sendNotification( + std::unique_ptr message) override { + invokeHandler(message->string(), notification_handler_); + } + void flushProtocolNotifications() override {} + + + void invokeHandler(const v8_inspector::StringView& string, zval *handler) { + if (Z_TYPE_P(handler) == IS_NULL) { + return; + } + + v8::HandleScope handle_scope(isolate_); + int length = static_cast(string.length()); + v8::Local message = + (string.is8Bit() + ? v8::String::NewFromOneByte( + isolate_, + reinterpret_cast(string.characters8()), + v8::NewStringType::kNormal, length) + : v8::String::NewFromTwoByte( + isolate_, + reinterpret_cast(string.characters16()), + v8::NewStringType::kNormal, length)) + .ToLocalChecked(); + + v8::String::Utf8Value str(isolate_, message); + const char *cstr = ToCString(str); + + zval handler_result; + zval params[1]; + ZVAL_STRINGL(¶ms[0], cstr, str.length()); + call_user_function_ex(EG(function_table), NULL, handler, &handler_result, 1, params, 0, NULL); + } + + v8::Isolate* isolate_; + zval *response_handler_; + zval *notification_handler_; +}; + + +class InspectorClient : public v8_inspector::V8InspectorClient { + public: + InspectorClient(v8::Local context, zval *response_handler, zval *notification_handler) { + isolate_ = context->GetIsolate(); + channel_.reset(new InspectorFrontend(context, response_handler, notification_handler)); + inspector_ = v8_inspector::V8Inspector::create(isolate_, this); + session_ = inspector_->connect(1, channel_.get(), v8_inspector::StringView()); + inspector_->contextCreated(v8_inspector::V8ContextInfo( + context, kContextGroupId, v8_inspector::StringView())); + context_.Reset(isolate_, context); + } + + void send(const zend_string *message) { + v8::Locker locker(isolate_); + v8::HandleScope handle_scope(isolate_); + v8_inspector::StringView message_view((const uint8_t*) ZSTR_VAL(message), ZSTR_LEN(message)); + { + v8::SealHandleScope seal_handle_scope(isolate_); + session_->dispatchProtocolMessage(message_view); + } + } + + private: + static const int kContextGroupId = 1; + + std::unique_ptr inspector_; + std::unique_ptr session_; + std::unique_ptr channel_; + v8::Global context_; + v8::Isolate* isolate_; +}; + + + +static void v8js_v8inspector_free_storage(zend_object *object) /* {{{ */ +{ + v8js_v8inspector *c = v8js_v8inspector_fetch_object(object); + + delete c->client; + zval_ptr_dtor(&c->response_handler); + zval_ptr_dtor(&c->notification_handler); + + zend_object_std_dtor(&c->std); +} +/* }}} */ + +static zend_object *v8js_v8inspector_new(zend_class_entry *ce) /* {{{ */ +{ + v8js_v8inspector *c; + c = (v8js_v8inspector *) ecalloc(1, sizeof(v8js_v8inspector) + zend_object_properties_size(ce)); + + zend_object_std_init(&c->std, ce); + object_properties_init(&c->std, ce); + + c->std.handlers = &v8js_v8inspector_handlers; + + ZVAL_NULL(&c->response_handler); + ZVAL_NULL(&c->notification_handler); + + return &c->std; +} +/* }}} */ + + +/* {{{ proto V8Inspector::__construct() + */ +PHP_METHOD(V8Inspector,__construct) +{ + zend_throw_exception(php_ce_v8js_exception, + "Can't directly construct V8Inspector objects!", 0); + RETURN_FALSE; +} +/* }}} */ + +/* {{{ proto V8Inspector::__sleep() + */ +PHP_METHOD(V8Inspector, __sleep) +{ + zend_throw_exception(php_ce_v8js_exception, + "You cannot serialize or unserialize V8Inspector instances", 0); + RETURN_FALSE; +} +/* }}} */ + +/* {{{ proto V8Inspector::__wakeup() + */ +PHP_METHOD(V8Inspector, __wakeup) +{ + zend_throw_exception(php_ce_v8js_exception, + "You cannot serialize or unserialize V8Inspector instances", 0); + RETURN_FALSE; +} +/* }}} */ + +/* {{{ proto void V8Inspector::send(string message) + */ +static PHP_METHOD(V8Inspector, send) +{ + zend_string *message; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &message) == FAILURE) { + return; + } + + v8js_v8inspector *inspector = Z_V8JS_V8INSPECTOR_OBJ_P(getThis()); + inspector->client->send(message); + +} +/* }}} */ + +/* {{{ proto void V8Inspector::setResponseHandler(callable) + */ +static PHP_METHOD(V8Inspector, setResponseHandler) +{ + v8js_ctx *c; + zval *callable; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &callable) == FAILURE) { + return; + } + + v8js_v8inspector *inspector = Z_V8JS_V8INSPECTOR_OBJ_P(getThis()); + ZVAL_COPY(&inspector->response_handler, callable); +} +/* }}} */ + +/* {{{ proto void V8Inspector::setNotificationHandler(callable) + */ +static PHP_METHOD(V8Inspector, setNotificationHandler) +{ + v8js_ctx *c; + zval *callable; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &callable) == FAILURE) { + return; + } + + v8js_v8inspector *inspector = Z_V8JS_V8INSPECTOR_OBJ_P(getThis()); + ZVAL_COPY(&inspector->notification_handler, callable); +} +/* }}} */ + + +void v8js_v8inspector_create(zval *res, v8js_ctx *ctx) /* {{{ */ +{ + object_init_ex(res, php_ce_v8inspector); + v8js_v8inspector *inspector = Z_V8JS_V8INSPECTOR_OBJ_P(res); + + V8JS_CTX_PROLOGUE(ctx); + inspector->client = new InspectorClient(v8_context, + &inspector->response_handler, &inspector->notification_handler); + +} +/* }}} */ + +ZEND_BEGIN_ARG_INFO_EX(arginfo_v8inspector_send, 0, 0, 1) + ZEND_ARG_INFO(0, message) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_v8inspector_set_handler, 0, 0, 1) + ZEND_ARG_INFO(0, callable) +ZEND_END_ARG_INFO() + +static const zend_function_entry v8js_v8inspector_methods[] = { /* {{{ */ + PHP_ME(V8Inspector, __construct, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_CTOR) + PHP_ME(V8Inspector, __sleep, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL) + PHP_ME(V8Inspector, __wakeup, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL) + PHP_ME(V8Inspector, send, arginfo_v8inspector_send, ZEND_ACC_PUBLIC) + PHP_ME(V8Inspector, setResponseHandler, arginfo_v8inspector_set_handler, ZEND_ACC_PUBLIC) + PHP_ME(V8Inspector, setNotificationHandler, arginfo_v8inspector_set_handler, ZEND_ACC_PUBLIC) + {NULL, NULL, NULL} +}; +/* }}} */ + +PHP_MINIT_FUNCTION(v8js_v8inspector_class) /* {{{ */ +{ + zend_class_entry ce; + + /* V8Inspector Class */ + INIT_CLASS_ENTRY(ce, "V8Inspector", v8js_v8inspector_methods); + php_ce_v8inspector = zend_register_internal_class(&ce); + php_ce_v8inspector->ce_flags |= ZEND_ACC_FINAL; + php_ce_v8inspector->create_object = v8js_v8inspector_new; + + /* V8Inspector handlers */ + memcpy(&v8js_v8inspector_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers)); + v8js_v8inspector_handlers.clone_obj = NULL; + v8js_v8inspector_handlers.cast_object = NULL; + v8js_v8inspector_handlers.offset = XtOffsetOf(struct v8js_v8inspector, std); + v8js_v8inspector_handlers.free_obj = v8js_v8inspector_free_storage; + + return SUCCESS; +} /* }}} */ + + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * indent-tabs-mode: t + * End: + * vim600: noet sw=4 ts=4 fdm=marker + * vim<600: noet sw=4 ts=4 + */ diff --git a/v8js_v8inspector_class.h b/v8js_v8inspector_class.h new file mode 100644 index 00000000..8c50e098 --- /dev/null +++ b/v8js_v8inspector_class.h @@ -0,0 +1,53 @@ +/* + +----------------------------------------------------------------------+ + | PHP Version 7 | + +----------------------------------------------------------------------+ + | Copyright (c) 2020 The PHP Group | + +----------------------------------------------------------------------+ + | http://www.opensource.org/licenses/mit-license.php MIT License | + +----------------------------------------------------------------------+ + | Author: Stefan Siegl | + +----------------------------------------------------------------------+ +*/ + +#ifndef V8JS_V8INSPECTOR_CLASS_H +#define V8JS_V8INSPECTOR_CLASS_H + +class InspectorClient; + +/* {{{ Object container */ +struct v8js_v8inspector { + InspectorClient *client; + zval response_handler; + zval notification_handler; + zend_object std; +}; +/* }}} */ + +extern zend_class_entry *php_ce_v8inspector; + +/* Create PHP V8Inspector object */ +void v8js_v8inspector_create(zval *res, v8js_ctx *ctx); + +static inline v8js_v8inspector *v8js_v8inspector_fetch_object(zend_object *obj) { + return (v8js_v8inspector *)((char *)obj - XtOffsetOf(struct v8js_v8inspector, std)); +} + +#define Z_V8JS_V8INSPECTOR_OBJ_P(zv) v8js_v8inspector_fetch_object(Z_OBJ_P(zv)); + + + +PHP_MINIT_FUNCTION(v8js_v8inspector_class); + +#endif /* V8JS_V8INSPECTOR_CLASS_H */ + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * indent-tabs-mode: t + * End: + * vim600: noet sw=4 ts=4 fdm=marker + * vim<600: noet sw=4 ts=4 + */ +