这是一个系列文章,将从V8引擎的接口出发,从C++的角度解释JavaScript 。在最后,我们会学会如何用C++拓展V8引擎,了解node.js的整合机制,最终用C++做出一个node.js的模块。 Javascript里有个C 系列文章: Javascript里有个C:Part 1 – 基础 Javascript里有个C:Part 2 – 对象 Javascript里有个C:Part 3 - 深入对象 Javascript里有个C:Part 4 - 异步 Javascript里有个C:Part 5 - node.js Javascript里有个C:Part 6 - 实战node.js Module 在此之前,你需要了解如何编译V8引擎,以及如何把自己的代码和V8引擎链接。 本文将阐述JavaScript在V8里的基本概念。 Handle JavaScript里的对象都由垃圾回收器进行管理,对象间的赋值均是传递引用,与C++由用户管理内存和值语义的机制截然不同。那么C++如何表示JavaScript里的对象呢?通过一种GC管理的智能指针。 V8里使用Handle类型来托管 JavaScript对象,与C++的std::shared_pointer类似,Handle类型间的赋值均是直接传递对象引用,但不同的是,V8使用自己的GC来管理对象生命周期,而不是智能指针常用的引用计数。 JavaScript类型在C++中均有对应的自定义类型,如String、Integer、Object、Date、Array等,严格遵守在JavaScript中的继承关系。C++中使用这些类型时,必须使用Handle托管,以使用GC来管理它们的生命周期,而不使用原生栈和堆。 Handle的使用和智能指针相同,以Handle的形式声明,通过operator->调用T的成员函数,通过operator*解引用。建立新的对象时,需要使用T::New()来返回一个Handle。 另外,Handle的生命周期和C++智能指针不同,并不是在C++语义的scope内生存(即{} 包围的部分),而需要通过HandleScope手动指定。 HandleScope只能分配在栈上, HandleScope对象声明后, 其后建立的Handle都由HandleScope来管理生命周期,HandleScope对象析构后,其管理的Handle将由GC判断是否回收。 下面是一个例子: // We will be creating temporary handles so we use a handle scope. HandleScope handle_scope; // Create a new empty array. Handle array = Array::New(3); // Fill out the values array->Set(0, Integer::New(x)); array->Set(1, Integer::New(y)); array->Set(2, Integer::New(z)); 例子很简单,我们首先声明handle_scope来限定之后Handle的scope,然后通过Array::New返回了一个新的Array对象,最后使用Array::Set来设定Array的数据。 那么,如果我们想通过一个函数来返回一个Handle呢?最直观的代码可能会是这样: // This function returns a new array with three elements, x, y, and z. Handle NewPointArray(int x, int y, int z) { // We will be creating temporary handles so we use a handle scope. HandleScope handle_scope; // Create a new empty array. Handle array = Array::New(3); // Return an empty result if there was an error creating the array. if (array.IsEmpty()) return array(); // Fill out the values array->Set(0, Integer::New(x)); array->Set(1, Integer::New(y)); array->Set(2, Integer::New(z)); return array; } 但问题是,当函数返回时,handle_scope会被析构,其管理的Handle也都将被回收,那么此刻返回的array将不再有意义。V8的解决方案是HandleScope::Close (Handle value),这个成员函数将关闭当前HandleScope并把参数中的Handle的转交给上一个scope,也就是进入这个函数之前所在的scope。下面是正确的代码: // This function returns a new array with three elements, x, y, and z. Handle NewPointArray(int x, int y, int z) { // We will be creating temporary handles so we use a handle scope. HandleScope handle_scope; // Create a new empty array. Handle array = Array::New(3); // Return an empty result if there was an error creating the array. if (array.IsEmpty()) return Handle(); // Fill out the values array->Set(0, Integer::New(x)); array->Set(1, Integer::New(y)); array->Set(2, Integer::New(z)); // Return the value through Close. return handle_scope.Close(array); } 此时类似于栈的Handle我们已了解完毕。但还有一种情况。JavaScript里还存在全局对象,我们怎么在C++里表示呢? Handle有两种类型,Local Handle和Persistent Handle,类型分别是Local : Handle和Persistent : Handle,前者和Handle没有区别,生存周期都在scope内。而后者的生命周期脱离scope,你需要手动调用Persistent::Dispose结束其生命周期。也就是说Local Handle相当于在C++在栈上分配对象,而Persistent Handle相当于C++在堆上分配对象。 Context V8允许不同的JavaScript代码运行在完全不同的环境下,其运行环境称为Context。不同的Context下拥有自己的全局对象(PersistentHandle),运行代码时必须指定所在的Context。最典型的例子就是Chrome的标签,每个标签都拥有自己的Context。 Context拥有自己的全局代理对象(global proxy object),每个Context下的全局对象都是这个全局代理对象的属性。通过Context::Global ()可以得到这个全局代理对象。新建Context时你可以手动指定它的全局代理对象,这样每个Context都会自动拥有一些全局对象,比如DOM。 Context也是一种scope,通过Context::Enter ()和Context::Exit ()来进入、退出,或者使用类似于HandleScope的Context::Scope来隐式进入,其代码非常简单: /** * Stack-allocated class which sets the execution context for all * operations executed within a local scope. / class Scope { public: explicit inline Scope(Handle context) : context_(context) { context_->Enter(); } inline ~Scope() { context_->Exit(); } private: Handle context_; }; 进入Context的scope后,接下来新建的Handle都会建立在这个Context下。 下面的图可以表示Context间scope的关系: 当建立scopeA时,程序将自动进入contextA的scope,建立scopeB时,程序又将自动进入contextB的scope,最后scopeB析构时,程序又回到contextA。 Script JavaScript代码的运行分为两个部分:编译和运行。这两个过程通过Script对象来完成。首先由Script::Compile (Handle source)来由源码字符串进行编译并返回一个编译后的Script对象,然后再由Local Script::Run ()运行代码并返回结果。 综述 最终一段完整的代码如下所示,它包含了新建Context、运行代码等完整过程: #include <v8.h> using namespace v8; int main(int argc, char argv[]) { // 新建基于栈分配的handle scope. HandleScope handle_scope; // 新建context. Persistent context = Context::New(); // 进入context,我们将在这个context内编译运行代码 Context::Scope context_scope(context); // 新建一个包含源代码的字符串. Handle source = String::New("'Hello' + ', World!'"); // 编译代码. Handle script = Script::Compile(source); // 运行代码并获取结果. Handle result = script->Run(); // 因为context是Persistent Handle,所以要手动释放. context.Dispose(); // 将结果转换成ASCII字符串. String::AsciiValue ascii(result); printf("%s\n", *ascii); return 0; } 用一张图来表示这段代码Handle的生命周期如下: 至此,我们已经了解了V8的大部分基础概念,下一篇文章里我们将讨论操作JavaScript里的对象。 文中使用的部分代码来自V8文档:http://code.google.com/apis/v8/embed.html
回复 (10)
谢谢指正,摘抄范例代码时确实未实地运行过,该打!后面的文章我会非常注意的!
是这样的。HandleScope的作用就是一个用来放置Handle的容器,专门用来自动控制其后分配的Handle的生命周期。Context::Scope用来隐式地进行Context::Enter、Context::Exit。
"如果你的代码只使用一个Context的话,可以免去新建Context的过程"这一节有误。String::New,Script::Compile等函数返回的均是Local(Handle),而左值是Value。不知道作者有无测试该代码,以我使用经验Context,HandleScope,Context::Scope三者不能缺一。又搜了下,原来是摘自V8官网的Hello World,http://code.google.com/apis/v8/get_started.html#intro,但作者没注意到下一行字 “To actually run this example in V8, you also need to add handles, a handle scope, and a context:”,添加Context,HandleScope等完整后即文中的“完整的代码”
[...] Javascript里有个C:Part 1 – 基础 [...]
[...] Javascript里有个C:Part 1 – 基础 [...]
谢谢! 看起来比较容易上手. 比起PHP扩展的写法, 大量魔法宏来说, V8的接口美丽多了.
仔细看完文中的代码: HandleScope handle_scope; Persistent context = Context::New(); Context::Scope context_scope(context); Handle source = String::New("'Hello' + ', World!'"); 是不是可以这样理解: HandleScope handle_scope; Context::Scope context_scope(context); 这两句语句是有"副作用"的, 这两句后面的Handle source会自动分配在"最近"的HandleScope 和 Context::Scope 上, 请问是这样吗?
挖个坟v8变化好快的说 NAN都是用 crazy 来形容 v8 & node core
mark