简介
v8js是一个特殊的php拓展, 其作用是将v8嵌入到php中, 使得用户可以在php中运行js代码. 同时, 经过作者的努力, 运行在php中的js可以无缝访问并php中的数据结构, 调用php內建的函数, 从而实现”1 + 1 > 2”的目标. 本文将跟随作者的, 领略v8js的风采.
功能介绍
使用一个工具是了解这个工具的最好方式, 笔者将在这一节中介绍v8js拓展的功能及使用.
首先介绍一下v8js的提供的接口, v8js的接口如下:
1 |
|
从上面接口的注释可以知道, 使用v8js拓展执行js代码时, 基本流程是先创建一个V8Js类的对象, 然后执行成员函数executeString, 函数返回的结果即是js的执行结果.
V8js的成员函数setTimeLimit和setMemoryLimit函数用于设置js执行的时间限制和内存限制.
使用例子后续补充.
架构设计
接着讲一讲v8js拓展的架构设计. v8js模块包含了一个计时器线程, 计时任务双线队列, 全局变量结构体. 在全局变量结构体中存储了v8的platform. v8js模块中实现了一个名为V8Js的类, 在这个类的构造函数中, v8js会出现一个isolate对象, 并创建一个全局上下文. 那么v8js模块中, 一个V8Js类的对象包含一个独立的isolate对象. 整体架构如下图所示:
详细设计
js代码执行
v8的源码中提供了一个hello的代码, 如下所示:
1 | // 初始化周边数据 |
从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作为形参传入到. 例如, 模块的代码如下:
1 | var x = 5; |
nodejs编译编译这个代码之前, 将这个模块的代码处理为:
1 | (function(module, exports, require) { |
然后, 创建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的使用