v8嵌入式开发--v8js篇
简介
v8js是一个特殊的php拓展, 其作用是将v8嵌入到php中, 使得用户可以在php中运行js代码. 同时, 经过作者的努力, 运行在php中的js可以无缝访问并php中的数据结构, 调用php內建的函数, 从而实现”1 + 1 > 2”的目标. 本文将跟随作者的, 领略v8js的风采.
功能介绍
使用一个工具是了解这个工具的最好方式, 笔者将在这一节中介绍v8js拓展的功能及使用.
首先介绍一下v8js的提供的接口, v8js的接口如下:
<?php
class V8Js
{
/* Constants */
const V8_VERSION = '';
const FLAG_NONE = 1;
const FLAG_FORCE_ARRAY = 2;
const FLAG_PROPAGATE_PHP_EXCEPTIONS = 4;
/* Methods */
/**
* Initializes and starts V8 engine and returns new V8Js object with it's own V8 context.
* @param string $object_name
* @param array $variables
* @param array $extensions
* @param bool $report_uncaught_exceptions
* @param string $snapshot_blob
*/
public function __construct($object_name = "PHP", array $variables = [], array $extensions = [], $report_uncaught_exceptions = TRUE, $snapshot_blob = NULL)
{}
/**
* 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.
* @param callable $loader
*/
public function setModuleLoader(callable $loader)
{}
/**
* Provide a function or method to be used to normalise module paths. This can be any valid PHP callable.
* This can be used in combination with setModuleLoader to influence normalisation of the module path (which
* is normally done by V8Js itself but can be overriden this way).
* The normaliser function will receive the base path of the current module (if any; otherwise an empty string)
* and the literate string provided to the require method and should return an array of two strings (the new
* module base path as well as the normalised name). Both are joined by a '/' and then passed on to the
* module loader (unless the module was cached before).
* @param callable $normaliser
*/
public function setModuleNormaliser(callable $normaliser)
{}
/**
* Compiles and executes script in object's context with optional identifier string.
* A time limit (milliseconds) and/or memory limit (bytes) can be provided to restrict execution. These options will throw a V8JsTimeLimitException or V8JsMemoryLimitException.
* @param string $script
* @param string $identifier
* @param int $flags
* @param int $time_limit in milliseconds
* @param int $memory_limit in bytes
* @return mixed
*/
public function executeString($script, $identifier = '', $flags = V8Js::FLAG_NONE, $time_limit = 0, $memory_limit = 0)
{}
/**
* Compiles a script in object's context with optional identifier string.
* @param $script
* @param string $identifier
* @return resource
*/
public function compileString($script, $identifier = '')
{}
/**
* Executes a precompiled script in object's context.
* A time limit (milliseconds) and/or memory limit (bytes) can be provided to restrict execution. These options will throw a V8JsTimeLimitException or V8JsMemoryLimitException.
* @param resource $script
* @param int $flags
* @param int $time_limit
* @param int $memory_limit
*/
public function executeScript($script, $flags = V8Js::FLAG_NONE, $time_limit = 0 , $memory_limit = 0)
{}
/**
* Set the time limit (in milliseconds) for this V8Js object
* works similar to the set_time_limit php
* @param int $limit
*/
public function setTimeLimit($limit)
{}
/**
* Set the memory limit (in bytes) for this V8Js object
* @param int $limit
*/
public function setMemoryLimit($limit)
{}
/**
* Set the average object size (in bytes) for this V8Js object.
* V8's "amount of external memory" is adjusted by this value for every exported object. V8 triggers a garbage collection once this totals to 192 MB.
* @param int $average_object_size
*/
public function setAverageObjectSize($average_object_size)
{}
/**
* Returns uncaught pending exception or null if there is no pending exception.
* @return V8JsScriptException|null
*/
public function getPendingException()
{}
/**
* Clears the uncaught pending exception
*/
public function clearPendingException()
{}
/** 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!
* @param string $extension_name
* @param string $code
* @param array $dependencies
* @param bool $auto_enable
* @return bool
*/
public static function registerExtension($extension_name, $code, array $dependencies, $auto_enable = FALSE)
{}
/**
* Returns extensions successfully registered with V8Js::registerExtension().
* @return array|string[]
*/
public static function getExtensions()
{}
/**
* Creates a custom V8 heap snapshot with the provided JavaScript source embedded.
* Snapshots are supported by V8 4.3.7 and higher. For older versions of V8 this
* extension doesn't provide this method.
* @param string $embed_source
* @return string|false
*/
public static function createSnapshot($embed_source)
{}
}
final class V8JsScriptException extends Exception
{
/**
* @return string
*/
final public function getJsFileName( ) {}
/**
* @return int
*/
final public function getJsLineNumber( ) {}
/**
* @return int
*/
final public function getJsStartColumn( ) {}
/**
* @return int
*/
final public function getJsEndColumn( ) {}
/**
* @return string
*/
final public function getJsSourceLine( ) {}
/**
* @return string
*/
final public function getJsTrace( ) {}
}
final class V8JsTimeLimitException extends Exception
{
}
final class V8JsMemoryLimitException extends Exception
{
}
从上面接口的注释可以知道, 使用v8js拓展执行js代码时, 基本流程是先创建一个V8Js类的对象, 然后执行成员函数executeString, 函数返回的结果即是js的执行结果.
V8js的成员函数setTimeLimit和setMemoryLimit函数用于设置js执行的时间限制和内存限制.
使用例子后续补充.
架构设计
接着讲一讲v8js拓展的架构设计. v8js模块包含了一个计时器线程, 计时任务双线队列, 全局变量结构体. 在全局变量结构体中存储了v8的platform. v8js模块中实现了一个名为V8Js的类, 在这个类的构造函数中, v8js会出现一个isolate对象, 并创建一个全局上下文. 那么v8js模块中, 一个V8Js类的对象包含一个独立的isolate对象. 整体架构如下图所示:
详细设计
js代码执行
v8的源码中提供了一个hello的代码, 如下所示:
// 初始化周边数据
v8::V8::InitializeICUDefaultLocation(argv[0]);
v8::V8::InitializeExternalStartupData(argv[0]);
// 创建platform
std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
// 初始化V8环境
v8::V8::InitializePlatform(platform.get());
v8::V8::Initialize();
// 构造参数
v8::Isolate::CreateParams create_params;
create_params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator();
// 创建isolate对象(v8虚拟机实例)
v8::Isolate* isolate = v8::Isolate::New(create_params);
v8::Isolate::Scope isolate_scope(isolate);
v8::HandleScope handle_scope(isolate);
// 创建上下文
v8::Local<v8::Context> context = v8::Context::New(isolate);
v8::Context::Scope context_scope(context);
// 将普通字符串转化为V8的字符串
v8::Local<v8::String> source = v8::String::NewFromUtf8(isolate, "'Hello' + ', World!'", v8::NewStringType::kNormal).ToLocalChecked();
// 编译
v8::Local<v8::Script> script = v8::Script::Compile(context, source).ToLocalChecked();
// 执行
v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
// 执行结果转化为普通字符串
v8::String::Utf8Value utf8(isolate, result);
printf("%s\n", *utf8);
// 销毁isolate
isolate->Dispose();
// 销毁V8环境和platform
v8::V8::Dispose();
v8::V8::ShutdownPlatform();
delete create_params.array_buffer_allocator;
从demo可以看到, 运行一个js脚本需要的操作包括:
- 创建platform
- 创建isolate
- 创建context
- 字符串编译成script
- 运行script
- 取出结果
- 销毁环境
v8js拓展将platform创建放在了第一个V8Js类的构造函数中; isolate和context的创建放在了V8Js的构造函数. executeString中执行了编译和运行script的操作. 对象析构的时候销毁isolate对象. 模块退出的时候销毁platform等操作.
內建函数实现
在这个部分需要介绍一下v8的template机制以及v8各个元素之间的关系. 下面是v8的常用名词:
- platform 平台, 一个进程中可以有多个, 但是只有一个生效
- isolate v8虚拟机, 可以存在多个, 一个isolate同一时间只允许一个线程访问
- context 运行上下文, 可以有多个, 可以嵌套, 从属于isolate.
- scope 作用域, isolate和context都有scope, 用于垃圾回收处理
- template 模板, 用于创建函数和对象, 从属于isolate
在v8中, 对象和函数从属于context, 而context在isolate的scope销毁时会被一同销毁, 那么这些对象和函数需要在context创建的时候被不停的创建, 为了省去这部分工作量, v8引入了template, 用于创建对象和函数.
內建函数的实现就是基于template实现的. 首先V8Js的的构造函数中会创建一个template对象, 然后再这个对象中添加functionTemplate对象, 而这些functionTemplate对象封装了內建函数. 在contenxt创建的时候, 将这个template对象作为实参传入, 那么创建出来的context就包含了內建函数.
template包含ObjectTemplate和FunctionTemplate两种, 前者创建对象, 后者创建函数. 如果用户想创建一个类呢? 由于js早期并不存在class关键词, 创建一个类的对象都是通过函数实现的. 所以创建一个类需要使用的是FunctionTemplate.
v8js拓展的內建函数的具体实现在文件”v8js_methods.cc”中, 在v8js_register_methods函数中将这些內建函数添加到template对象中. 这个函数在V8Js类的构造函数中被调用, 位置在contenxt被创建之前.
commonjs模块实现
首先讲一下什么是commonjs模块规范. “规范”认为, 每个文件是一个模块(module), 并拥有其自己的作用域. 在这个作用域内声明的变量和函数都是这个作用域私有的. 模块信息存储在module对象中, module提供exports对象用于暴露作用域内的函数/变量. 模块之间通过require函数加载其他的模块.
为了实现上面规范, 在每个文件的作用域内, 需要提前声明module, exports对象和require函数. 并且module和exports对象存在于模块中, 模块间的module和exports都不同. 参考上文中的template的功能, module和exports可以实现为两个ObjectTemplate, 并注册到主ObjectTemplate. 同样, require也可以类似实现. 这时基于主ObjectTemplate创建context, 并在这个context运行指定文件, 并提取exports, 即可暴露指定的接口(变量和函数).
考虑到重新创建context成本较大, v8的context支持基于老context创建新的context, 但是基于这种方式就无法使用到template了. nodejs的解决方案是, 创建一个函数, 在函数体中添加模块的代码, 将module/exports/require作为形参传入到. 例如, 模块的代码如下:
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
nodejs编译编译这个代码之前, 将这个模块的代码处理为:
(function(module, exports, require) {
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
});
然后, 创建module对象, exports对象, reuqire函数, 将这三个作为实参传入到生成的函数中去, 最后提取module和exports对象缓存起来. 当其他的模块reuqire这个模块时, 先查缓存, 命中缓存则直接返回exports对象.
v8js借鉴了nodejs的方式实现了一个commonjs模块.
由于reuqire一个模块时, 可以指定模块的相对路径, 这就要求reuqire在被调用时能够知道调用reuqire的文件的绝对路径. v8创建函数时支持给函数传递一个meta数据, v8js利用这个特性给require传递了路径数据. “v8js_commonjs.cc”中实现了一套相对路径推算绝对路径的代码, 使用c++17或者boost的话, 可以使用filesystem的canonical函数解决.
超限功能实现
前文提到v8js拓展包含一个计时器线程和计时任务队列, v8js依靠它们实现了超限功能.
V8Js类的对象在执行js脚本时, v8js会基于isolate/context/超时配置/内存限制创建一个超限任务, 并添加到计时任务队列中.
计时器线程会取出队列中的任务, 检查是否超时, 内存是否超限. 对超限的任务, 超时线程将终止任务运行. 在判断内存超限时, 计时器线程需要先解锁isolate, 然后获取堆统计信息, 如果超限就手工gc并终止当前任务.
js中调用php数据实现
这部分略
改进与优化
isolate归属问题
在v8js中isolate是V8Js对象级别的, 事实上isolate和全局context的创建成本较大, 并不适合放在对象级别. 基于php单线程运行的特点, isolate和全局context可以存放在线程级别中. 每次执行脚本时, 基于全局context创建新的context.
学习总结
- 学习了js脚本运行的流程
- 学习了v8中各个名词之间的关系
- 学习了多isolate的使用