从嵌入V8开始(上)

从嵌入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。

这些概念的细节会在后面进阶指导中详细讲解。

运行示例

根据以下步骤,你可以运行起示例:

  1. 根据指导下载V8源码。
  2. 这个hello world示例的讲解基于V8 v7.1.11版本。你可以使用以下指令来check out这个分支:
git checkout refs/tags/7.1.11 -b sample -t
  1. 创建一个构建配置,使用一些脚本:
tools/dev/v8gen.py x64.release.sample
  1. 在Linux 64位系统上构建静态库:
ninja -C out.gn/x64.release.sample v8_monolith
  1. 编译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
  1. 对于更复杂的代码,V8需要一个ICU数据文件。将这个文件复制到你二进制文件保存的地方:
cp out.gn/x64.release.sample/icudtl.dat .
  1. 在命令行中运行hello_world可执行文件。例如,在Linux环境下,在V8目录,运行:
./hello_world
  1. 程序最终打印“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对象。它的使用开销很小,因为它不需要使用垃圾回收器来管理对象的生命周期。
    • PersistentUniquePersistent都是不可复制的,这导致他们与C++11之前的标准库容器不兼容。PersistentValueVectorPersistentValueMap提供了为Persistent值准备的容器类。C++11的嵌入者不需要这些,因为C++11的移动语义解决了这个问题。

当然,每创建一个对象就创建一个Local句柄会导致大量的句柄产生!这就是handle scope的优势之处。你可以将一个handle scope当作一个容器,里面存放着许多句柄。当handle scope的析构器被调用时,所有在范围内创建的句柄都会被移除出栈。和期望的一样,这会使得垃圾回收器将这些句柄指向的对象从堆中删除。

回到hello world例子中,下图展示了句柄栈和堆分配对象。注意Context::New()会返回一个Local句柄。我们基于它创建一个新的Persistent句柄来展示Persistent句柄的使用方式。

local-persist-handles-review

当析构器HandleScope::~HandleScope被调用时,这个handle scope会被删除。其中的句柄指向的对象如果没有被其他句柄引用,那么会在下一个垃圾回收过程中被移除。垃圾回收器同样会将source_objscript_obj对象从堆中移除。而context句柄是一个Persistent句柄,所以在退出handle scope时它不会被移除。唯一能够移除context句柄的方式时显式地调用Reset

要注意避开一个常见的陷阱:不可以在一个声明了handle scope的函数中直接返回Local句柄。如果这么做的话,Local句柄在函数返回前,会因为handle scope的析构函数调用而被立即删除。正确的方式应该是创建一个EscapableHandleScope代替HandleScope,并且调用handle scopeEscape方法来传递需要返回的值。下面是一个实际的例子:

// 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。这意味着将当前的ContextA替换为B。当你从B中退出后,A则恢复为当前Context。下图展示了相关内容:

intro-contexts

值得注意的是,每个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开始(下)

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值