这是一个系列文章,将从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<T>的形式声明,通过operator->调用T的成员函数,通过operator解引用。建立新的对象时,需要使用T::New()来返回一个Handle<T>。
另外,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 = 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<Array> 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 = 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<T> value),这个成员函数将关闭当前HandleScope并把参数中的Handle的转交给上一个scope,也就是进入这个函数之前所在的scope。下面是正确的代码:
// This function returns a new array with three elements, x, y, and z.
Handle<Array> 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 = 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<T> : Handle<T>和Persistent<T> : Handle<T>,前者和Handle<T>没有区别,生存周期都在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<Value> 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<String> source)来由源码字符串进行编译并返回一个编译后的Script对象,然后再由Local<Value> Script::Run ()运行代码并返回结果。
综述
最终一段完整的代码如下所示,它包含了新建Context、运行代码等完整过程:
#include <v8.h>argv[]) {
using namespace v8;
int main(int argc, char
// 新建基于栈分配的handle scope.
HandleScope handle_scope;
// 新建context.
Persistent<Context> context = Context::New();
// 进入context,我们将在这个context内编译运行代码
Context::Scope context_scope(context);
// 新建一个包含源代码的字符串.
Handle<String> source = String::New(“’Hello’ + ', World!’”);
// 编译代码.
Handle<Script> script = Script::Compile(source);
// 运行代码并获取结果.
Handle<Value> 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
"如果你的代码只使用一个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等完整后即文中的“完整的代码”
仔细看完文中的代码:
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 上, 请问是这样吗?