从嵌入V8开始
原文链接:https://v8.dev/docs/embed
CSDN博客有字数限制,故将此篇翻译分为两部分。下部分链接:从嵌入V8开始(下)
本文也发布于本人的知乎专栏:https://zhuanlan.zhihu.com/p/394547532
这篇文档介绍了V8的一些关键概念,提供了一个hello world示例来帮助你开始V8代码。
读者
这篇文档适用于想要将V8引擎嵌入到C++程序中的C++程序员。这篇文档介绍了如何使你应用中的C++对象和方法暴露给JavaScript使用,以及如何将JavaScript对象和方法暴露给你的C++应用。
Hello world
让我们看看Hello world示例。这个示例将一个JavaScritp表达式作为一个字符串参数,并将其作为JavaScript代码进行执行,最终通过标准输出打印其结果。
首先,这里有一些关键概念:
- 一个isolate是一个VM实例,其拥有自己的堆。
- 一个local handle是指向一个对象的引用。由于V8的垃圾收集的工作机制,所有的V8对象都必须通过handle来访问。
- 一个handle scope可以被看作若干handle的容器。当你准备清除这些handle的时候,你可以不用挨个删除这个handle,而是删除他们的scope即可。
- 一个context是一个JavaScript的执行环境。它可以让独立不相关的JavaScript代码运行在单独的V8实例中。你必须明确的指定将要运行的JavaScript代码的context。
这些概念的细节会在后面进阶指导中详细讲解。
运行示例
根据以下步骤,你可以运行起示例:
- 根据指导下载V8源码。
- 这个hello world示例的讲解基于V8 v7.1.11版本。你可以使用以下指令来check out这个分支:
git checkout refs/tags/7.1.11 -b sample -t
- 创建一个构建配置,使用一些脚本:
tools/dev/v8gen.py x64.release.sample
- 在Linux 64位系统上构建静态库:
ninja -C out.gn/x64.release.sample v8_monolith
- 编译hello-world.cc,链接到构建过程中创建的静态库。例如,在64位的Linux下使用GNU编译器:
g++ -I. -Iinclude samples/hello-world.cc -o hello_world -lv8_monolith -Lout.gn/x64.release.sample/obj/ -pthread -std=c++0x
- 对于更复杂的代码,V8需要一个ICU数据文件。将这个文件复制到你二进制文件保存的地方:
cp out.gn/x64.release.sample/icudtl.dat .
- 在命令行中运行hello_world可执行文件。例如,在Linux环境下,在V8目录,运行:
./hello_world
- 程序最终打印“Hello, World!”。
如果你想要一个与主干同步的示例,那么就check out hello-world.cc
文件。这个例子非常简单。你肯定想要更多,而不仅仅是以字符串的形式运行脚本。以下的进阶指导中为V8嵌入者提供了更多的信息。
更多的示例代码
下面的这些示例包含于下载的源码中。
process.cc
这个示例提供了扩展假想HTTP请求处理应用的必要代码。该应用可以作为一个网站服务器的一部分。它使用JavaScript脚本作为参数,该脚本必须提供一个被称为Process的方法。这个Process方法可以被用于,如收集这个虚拟网站每个页面被点击次数之类的信息。
shell.cc
这个示例将文件名称作为参数,读取、执行其内容。包括一个在输入待执行JavaScript代码时的命令提示。在这个示例中,例如print
等附加方法通过对象模板和函数模板添加到JavaScript中。
进阶指导
现在你已经对如何单独使用V8作为虚拟机熟悉了,且对一些V8关键概念有了了解。下面我们将进一步讨论这些概念,并且介绍一些在嵌入V8时需要用到的其他相关概念。
V8的API提供了许多函数以用于编译执行脚本、访问C++方法和数据结构、处理错误、开启安全检查等。你的应用可以像使用其他的C++库那样来使用V8。C++代码通过V8的API来访问V8,需要包含头文件include/v8.h
。
句柄(Handle)和垃圾回收
句柄(handle)提供了指向JavaScript对象在堆中位置的引用。V8垃圾收集器会回收哪些不会再被访问到的对象的内存。再垃圾回收过程中,垃圾回收期会经常性的将对象移动到堆内不同的位置。当垃圾回收器移动对象的时候,会更新所有的指向这个对象的句柄。
当一个对象处于JavaScript不可访问到的状态,且没有任何句柄指向它,此时这个对象会被认为时垃圾。垃圾回收器会不时地移除所有被认为是垃圾的对象。V8的垃圾回收机制是V8性能的关键。
句柄有以下几种类型:
Local
句柄被持有在栈上。当适当的析构器被调用时,Local
会被删除。这些句柄的生命周期是由handle scope
决定的。handle scope
通常在函数调用的开头创建。当handle scope
被删除时,其中的句柄指向的对象会被认为无法再被JavaScript或其他句柄访问到,所以垃圾回收器会清理这些对象。这种类型的句柄在hello world例子中被运用到。
Local
句柄的类是Local<SomeType>
。
注意:句柄栈并不是C++调用栈的一部分,但是handle scope
是嵌入在C++栈上的。handle scope
只能用于栈分配,不能通过new
来分配。Persistent
句柄提供了指向堆上分配的JavaScript对象的引用。Persistent
句柄有两种,区别在于他们的引用的生命周期管理。当准备在多个函数中保持一个引用的时候,或者句柄生命周期不与C++范围相关的时候,就需要使用到Persistent
句柄。举个例子,Google Chrome使用Persistent
句柄来引用DOM节点。一个Persisitent
句柄可以通过使用PersistentBase::SetWeak
来成为弱引用,当所有的指向一个对象的所有句柄都是弱引用时,以此来触发来自垃圾回收器的回调。UniquePersistent<SomeType>
句柄依赖于C++构造器和析构器来管理相应对象的生命周期。Persistent<SomeType>
句柄可以由其自己的构造器来构造,但是必须明确地使用Persistent::Reset
来清除。
- 还有其他比较少用到的句柄类型,这里简单的介绍一下:
Eternal
是一个Persistent
句柄,用于指向永远不会被删除的 JavaScript对象。它的使用开销很小,因为它不需要使用垃圾回收器来管理对象的生命周期。Persistent
和UniquePersistent
都是不可复制的,这导致他们与C++11之前的标准库容器不兼容。PersistentValueVector
和PersistentValueMap
提供了为Persistent
值准备的容器类。C++11的嵌入者不需要这些,因为C++11的移动语义解决了这个问题。
当然,每创建一个对象就创建一个Local
句柄会导致大量的句柄产生!这就是handle scope
的优势之处。你可以将一个handle scope
当作一个容器,里面存放着许多句柄。当handle scope
的析构器被调用时,所有在范围内创建的句柄都会被移除出栈。和期望的一样,这会使得垃圾回收器将这些句柄指向的对象从堆中删除。
回到hello world例子中,下图展示了句柄栈和堆分配对象。注意Context::New()
会返回一个Local
句柄。我们基于它创建一个新的Persistent
句柄来展示Persistent
句柄的使用方式。
![local-persist-handles-review](https://i-blog.csdnimg.cn/blog_migrate/5b1bd73258afb15494a7aa905ad48de7.png)
当析构器HandleScope::~HandleScope
被调用时,这个handle scope
会被删除。其中的句柄指向的对象如果没有被其他句柄引用,那么会在下一个垃圾回收过程中被移除。垃圾回收器同样会将source_obj
和script_obj
对象从堆中移除。而context
句柄是一个Persistent
句柄,所以在退出handle scope
时它不会被移除。唯一能够移除context
句柄的方式时显式地调用Reset
。
要注意避开一个常见的陷阱:不可以在一个声明了handle scope
的函数中直接返回Local
句柄。如果这么做的话,Local
句柄在函数返回前,会因为handle scope
的析构函数调用而被立即删除。正确的方式应该是创建一个EscapableHandleScope
代替HandleScope
,并且调用handle scope
的Escape
方法来传递需要返回的值。下面是一个实际的例子:
// This function returns a new array with three elements, x, y, and z.
Local<Array> NewPointArray(int x, int y, int z) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
// We will be creating temporary handles so we use a handle scope.
EscapableHandleScope handle_scope(isolate);
// Create a new empty array.
Local<Array> array = Array::New(isolate, 3);
// Return an empty result if there was an error creating the array.
if (array.IsEmpty())
return Local<Array>();
// Fill out the values
array->Set(0, Integer::New(isolate, x));
array->Set(1, Integer::New(isolate, y));
array->Set(2, Integer::New(isolate, z));
// Return the value through Escape.
return handle_scope.Escape(array);
}
Escape
方法将其参数的值复制到封闭的scope
,删除其所有的Local
句柄,并且返回一个新的句柄,这个句柄可以安全的作为函数的返回值。
上下文(Context)
在V8中,上下文(context)是一个执行环境,允许独立的不相干的JavaScript应用在一个单独的V8实例中运行。必须明确的指定用于运行JavaScript代码的Context
。
为什么这个是必要的?因为JavaScript提供了一系列内置工具函数,且对象会被JavaScript代码改变。举个例子,如果两个完全不相关的JavaScript函数都改变了一个全局对象,那么就会产生一些未知的不期望出现的结果。
就CPU时间和内存而言,创建新的Context
是一个非常耗费的操作,因为其包含大量的内置对象。不过,V8的大规模缓存保证了除了第一个创建的Context
外,后续的创建操作会变得更低耗。这是因为第一个创建的Context
需要创建内置对象并且解析内置的JavaScript代码,而后续创建的Context
则只需要创建内置对象即可。通过V8的快照(snapshot)机制(构建选项设置为snapshot=yes
开启,默认即为yes
)创建第一个Context
所消耗的时间也大幅度的优化了,因为快照包含了一个序列化的堆,其中包含了编译后的内置JavaScript代码。大规模缓存和垃圾回收机制一样,都是V8高性能的关键。
当创建好一个Context
后,可以进入退出任意次。当你在context A
的时候,你同样可以进入另一个不同的context B
。这意味着将当前的Context
从A
替换为B
。当你从B
中退出后,A
则恢复为当前Context
。下图展示了相关内容:
![intro-contexts](https://i-blog.csdnimg.cn/blog_migrate/c97d50ff92d7f0af6e96a33e522b50f8.png)
值得注意的是,每个Context
中的内置工具函数和对象都是保持独立的。在创建一个Context
时,可选择性地设置一个安全token。可查看安全模型来获取更多的信息。
通过使用Context
,可以使浏览器中每个窗口和iframe
都能有一个“干净”的JavaScript环境。
模板(Template)
模板是Context
中JavaScript函数和对象的的蓝图。可以通过模板使用JavaScript对象来包装C++函数和数据结构,这样就可以在JavaScript脚本中操作他们。Google的Chrome就是使用模板将C++ DOM包装成JavaScript对象。你可以创建一系列模板,并且可以在每个新的Context
中使用他们。你可以创建任意数量的模板,但是在每个指定的Context
中,任意一个模板都只能有一个实例。
在JavaScript中,函数和对象有很强的二义性。在Java或者C++中创建一个新类型的对象,一般需要先定义一个类。然而在JavaScript中,是创建一个新的函数,并使用这个函数作为构造器创建实例。一个JavaScript对象的内部构造和功能取决于构造它的函数。这些在V8模板工作方式中有反应。模板有以下两种类型:
- 函数模板(Function Template)
函数模板是一个单独函数的蓝图。在需要实例化JavaScript函数的Context
中,通过调用模板的GetFunction
方法来创建这个模板的JavaScript实例。还可以关联一个C++回调方法,用于在JavaScript函数实例被调用时回调。 - 对象模板(Object Template)
每个函数模板都有一个相关联的对象模板。对象模板用于配置这个函数创建出的对象。对象模板可以与两种C++回调相关联:accessor
回调时机:一个特定的对象属性被脚本访问时intercepter
回调时机:任意的对象属性被脚本访问时
访问器(accessor)和拦截器(interceptor)在后续中详细讨论。
下面的代码实例展示的是为全局对象设置模板和设置内置全局函数:
// Create a template for the global object and set the
// built-in global functions.
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
global->Set(String::NewFromUtf8(isolate, "log"),
FunctionTemplate::New(isolate, LogCallback));
// Each processor gets its own context so different processors
// do not affect each other.
Persistent<Context> context = Context::New(isolate, NULL, global);
这个示例代码取自process.cc
中的JsHttpProcessor::Initializer
。
下部分链接:从嵌入V8开始(下)