嵌入者指南
原文地址:http://code.google.com/intl/zh-CN/apis/v8/embed.html
嵌入者指南
如果你读了入门指南,那你对把V8当作一个独立的虚拟机,以及V8的句柄、作用域、context等概念应该很熟悉了。这篇文章将更深入地讨论这些概念,并介绍如何将V8嵌入到你的程序中。
V8 API提供了一些函数,用来编译并执行脚本,访问c++方法和数据结构,处理错误,以及安全检查。你可以在程序中像使用一个普通的c++库一样使用V8。你在代码中包含include/v8.h,就可以调用V8 API 了。
V8设计元素介绍了一些背景,它有助于你优化程序。
受众
这篇文章是给那些要在c++程序中使用V8的程序员看的。它告诉你如何构建用于JavaScript的c++对象和函数,和用于c++的JavaScript对象和函数。
句柄和垃圾回收(gc)
句柄是堆中JavaScript对象的引用。V8的gc会回收那些不再使用的JavaScript对象所占用的内存。在gc进行垃圾回收时,经常会把对象在堆中移到另一个位置。移动时,gc会把所有的句柄都指向新的位置。
如果一个对象,JavaScript已经不能访问到它了,而且也没有句柄引用它,它就会被认为是需要回收的垃圾。gc会不定时地(from time to time)将所有的垃圾从内存中释放掉。V8的gc机制是V8性能的关键。更多请参考V8设计元素。
有2个类型的句柄:
- 栈中的临时句柄,在它的析构被调用时释放。这些句柄的生命周期由句柄所在的作用域决定,这些作用域一般在函数刚被调用时创建。当离开作用域时,如果没有JavaScript或其他句柄引用,gc就会释放这些对象。入门指南中的例子,用的就是这种句柄。
临时句柄用Local<SomeType>类,也可以它的父类Handle<SomeType>。
注意:句柄栈不在c++调用栈中,但句柄作用域是在c++栈中。句柄作用域只能分配在栈中,不能用new来分配。 - 持久化句柄不在栈中,只有在手动移除它时才被释放。和临时句柄类似,持久化句柄指向堆中分配的对象。如果你要跨函数使用对象,或者句柄没有对应的c++作用域,那就应该用持久化句柄。比如,Google Chrome就使用持久化句柄来引用一个DOM节点。持久化句柄用Persistent::New生成,用Persistent::Dispose释放。用Persistent::MakeWeak来生成一个弱引用的持久化句柄,当一个持久化句柄只剩下弱引用时,gc会触发一个回调。
持久化句柄用Persistent<SomeType>类,也可以它的父类Handle<SomeType>。
当然,每次创建一个句柄,都会创建一个具有许多句柄的对象!句柄作用域放在哪是非常有用的。你可以把句柄作用域看成一个能容纳很多句柄的容器。当析构被调用时,作用域中所有句柄都会被从栈中移除。和你想的一样,堆中的句柄指向的对象也被gc用适当的方法释放了。
回到我们在入门指南中的那个简单的例子,下图中可以看到句柄栈和堆中分配的对象。注意Context::New()返回了一个持久化句柄,它并不在句柄栈中。

析构时,HandleScope::~HandleScope被调用,句柄作用域被删除。这个作用域中句柄引用的对象,如果没有其他句柄引用他们,那么他们将在下次gc时被移除。堆中的source_obj和script_obj对象,如果没有被其他句柄引用,也会被gc移除。因为context句柄是持久化句柄,所以离开句柄作用域时,它不会被移除。只有一个方法可以移除context句柄,就是调用它的Dispose方法。
注意:本文中提及的句柄是指临时句柄,持久化句柄是指全局句柄。
context
V8中,context是指用来运行一段独立JavaScript的V8实例。要运行一段JavaScript代码,必须明确指定一个context。
为什么这是必须的呢?因为JavaScript提供了一套内建的函数和对象,而它们是可以由JavaScript代码来改变的。比如,如果有2段完全不相干的JavaScript代码都改了同一个全局对象,那就会有些不和谐的事情发生了。
每次创建内建对象都创建一个执行context,看起来很浪费CPU和内存。但实际上,V8广泛运用了缓存机制,第一次创建context可能慢点,但后来的就很快了。因为第一个context需要创建内建对象,并解析内建的JavaScript代码,而后来的只要创建自己的内建对象就可以了。使用V8的快照特性(建议编译时使用snapshot=yes这个编译选项,这也是默认的),可以大大减少第一次创建context所花费的时间,因为快照包含了一系列的堆,而堆中包含已经编译好的JavaScript内建代码。和gc一样,V8的缓存机制也是V8性能的关键,更多信息请参看V8设计元素。
创建好一个context后,你可以随意进出多次。当你在contextA中时,你也可以进入另一个contextB,这时B代替A成为了当前context。当你退出B,A又重新成为当前context。如下图:

注意每个context都各自保存了一份内建函数和对象。创建context时,你可以选择设置一个安全令牌。参考安全模型可以获得更多信息。
V8使用context的目的,就是让浏览器中的每个window和iframe都有自己的JavaScript context。
模板
模板,就是一个context里的JavaScript函数和对象的设计图。你可以用模板把c++函数和数据结构封装起来成JavaScript对象,这样JavaScript脚本就可以方便地调用了。例如:Google Chrome使用模板把c++ DOM节点封装成JavaScript对象,还用模板来安装全局命名空间的函数。你可以创建一系列的模板,用在每一个你新创建的context中。你需要多少模板,就可以有多少模板。但在一个context中,每个模板只能有一个实例。
JavaScript中,函数和对象间有很强的二元性。要创建一个新的对象类型,在Jave或c++中,典型的方法是定义一个新类。而在JavaScript中,是创建一个新函数,用这个函数作为构造器创建一个实例。JavaScript对象的成员和函数与这个构造函数紧密相关。这反映了V8的模板是如何工作的。有2种类型的模板:
- 函数模板
函数模板是一个函数的设计图。你可以调用模板的GetFunction方法来创建一个模板实例,并传入你要运行在其中的context。你也可以给函数模板绑定一个c++回调,它在JavaScript函数实例执行后被调用。 - 对象模板
每个函数模板都有一个关联的对象模板。由该函数作为对象的构造器。你可以给对象模板绑定2种c++回调:- 访问器,它在某指定对象的属性被绑定到一个脚本时被调用。
- 拦截器,它在任意对象的属性被绑定到一个脚本时被调用。
- 访问器,它在某指定对象的属性被绑定到一个脚本时被调用。
访问器和拦截器在下文中讨论。
下面的例子代码展示了如何用模板来创建一个全局对象和设置内建全局函数。
// Create a template for the global object and set the
// built-in global functions.
Handle<ObjectTemplate> global = ObjectTemplate::New();
global->Set(String::New("log"), FunctionTemplate::New(LogCallback));
// Each processor gets its own context so different processors
// do not affect each other.
Persistent<Context> context = Context::New(NULL, global);
这段例子代码在process.cc文件JavaScriptHttpProcessor::Initializer函数中。
访问器
访问器是一个c++回调函数,当一个对象和一个JavaScript脚本绑定时,它用来计算并返回一个值。用对象模板的SetAccessor方法来绑定一个访问器。SetAccessor方法需要一个属性名参数和2个回调,分别在属性被读和写时调用。
访问起的复杂度,取决于你要操作的数据类型。
访问静态全局变量
假定需要2个c++变量x和y,他们都是一个context中可以给JavaScript访问的全局变量。要实现这个,需要在JavaScript读或写时调用c++访问器函数。访问器函数通过Integer::New将c++整型转换为JavaScript整型,通过Int32Value将JavaScript整型转换为c++整型。例子如下:
Handle<Value> XGetter(Local<String> property,
const AccessorInfo& info) {
return Integer::New(x);
}
void XSetter(Local<String> property, Local<Value> value,
const AccessorInfo& info) {
x = value->Int32Value();
}
// YGetter/YSetter are so similar they are omitted for brevity
Handle<ObjectTemplate> global_templ = ObjectTemplate::New();
global_templ->SetAccessor(String::New("x"), XGetter, XSetter);
global_templ->SetAccessor(String::New("y"), YGetter, YSetter);
Persistent<Context> context = Context::New(NULL, global_templ);
注意上面代码中的对象模板是和context一起创建的。你也可以用更高级的方法来创建,这样就可以用在多个context里。
访问动态变量
上面例子中的变量是全局静态的。那动态变量怎么办呢,比如浏览器的DOM树?我们假定x和y是c++类Point的成员:
class Point {
public:
Point(int x, int y) : x_(x), y_(y) { }
int x_, y_;
}
要是有多个c++ point实例需要给JavaScript用,那我们就得在JavaScript中也创建多个实例,与c++ point实例一一对应,然后把JavaScript实例和c++实例连接起来。这样就完成了外部值和对象成员的内部值。
首先给创建一个对象模板,把point封装起来:
Handle<ObjectTemplate> point_templ = ObjectTemplate::New();
每个JavaScript point对象都保存了一个c++成员封装类的引用。这些成员是没有名字的(原文是so named,怀疑应该是no named),因为他们不能在JavaScript里访问,而只能通过c++代码访问。一个对象可以有多个内部成员,内部成员的个数可以在对象模板中设置,如:
point_templ->SetInternalFieldCount(1);
例子中内部成员个数设置为1,表示这个对象有1个内部成员,下标从0开始,指向一个c++对象。
将x和y的访问器添加到模板中:
point_templ.SetAccessor(String::New("x"), GetPointX, SetPointX);
point_templ.SetAccessor(String::New("y"), GetPointY, SetPointY);
下一步,创建一个模板的实例,把第0个内部成员设置给point实例p的外部封装。
Point* p = ...;
Local<Object> obj = point_templ->NewInstance();
obj->SetInternalField(0, External::New(p));
外部对象就是简单的封装了一个void*。外部对象只能使用内部成员的引用。JavaScript对象不能直接引用c++对象,因此外部值需要通过一座"桥"来传到c++中。感觉外部值就像是个反过来的句柄,因为c++是通过句柄来引用JavaScript对象。
下面是x的get和set访问器,y的访问器只要把x换成y就行了。
Handle<Value> GetPointX(Local<String> property,
const AccessorInfo &info) {
Local<Object> self = info.Holder();
Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
void* ptr = wrap->Value();
int value = static_cast<Point*>(ptr)->x_;
return Integer::New(value);
}
void SetPointX(Local<String> property, Local<Value> value,
const AccessorInfo& info) {
Local<Object> self = info.Holder();
Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
void* ptr = wrap->Value();
static_cast<Point*>(ptr)->x_ = value->Int32Value();
}
访问器得到JavaScript封装的point的引用,然后就能读写绑定的成员了。这种访问器可以用在多个point的封装对象上。
拦截器
你可以指定一个回调,在脚本访问对象属性时做处理。这就叫拦截器。为了效率,有2种类型的拦截器:
- 命名属性拦截器 - 当通过属性的名字来访问时调用。
例如,浏览器环境里的document.theFormName.elementName。 - 下标属性拦截器 - 当通过属性的下标来访问时调用。
例如,浏览器环境里的document.forms.elements[0]。
V8的源码中有一个使用拦截器的例子,在process.cc中。下面的代码片段中,SetNamedPropertyHandler是MapGet和MapSet的拦截器:Handle<ObjectTemplate> result = ObjectTemplate::New(); result->SetNamedPropertyHandler(MapGet, MapSet);
下面是MapGet的拦截器:Handle<Value> JavaScriptHttpRequestProcessor::MapGet(Local<String> name, const AccessorInfo &info) { // Fetch the map wrapped by this object. map<string, string> *obj = UnwrapMap(info.Holder()); // Convert the JavaScript string to a std::string. string key = ObjectToString(name); // Look up the value if it exists using the standard STL idiom. map<string, string>::iterator iter = obj->find(key); // If the key is not present return an empty handle as signal. if (iter == obj->end()) return Handle<Value>(); // Otherwise fetch the value and wrap it in a JavaScript string. const string &value = (*iter).second; return String::New(value.c_str(), value.length()); }
就像访问器一样,指定的回调在属性被访问时执行。访问器和拦截器的区别是,拦截器能处理所有的属性,而访问器只处理绑定的属性。
安全模型
"同源原则"(在Netscape Navigator 2.0中第一次被提出)会保护从某个"源"载入的文档或脚本,不会被另一个"源"中的文档读取或设置。这里的术语"源",指的是由域名(www.example.com)、协议(http或https)、端口(www.example.com:81和www.example.com是不同的)3者组合而成的东西,也就是我们常说的url地址啦。这3者都匹配的2个页面,会被认为是来自相同的源。如果没有这个保护,那么恶意页面就会危害到正常页面。
在V8中,"源"被定义成一个context。默认情况下,除了调用你的那个context外,其他context是不允许你访问的。要想访问其他context,你需要使用安全令牌或安全回调。安全令牌可以是任意值,但通常是一个符号,一个唯一的标准字符串。在创建一个context时,你可以用SetSecurityToken来指定一个安全令牌。如果你没指定,V8会自动给你生成一个。当试图访问一个全局变量时,V8安全系统会首先检查与之绑定的安全令牌。如果匹配就允许访问。如果不匹配,V8就会调用一个回调来判断是否允许访问。你可以使用对象模板的SetAccessCheckCallbacks方法来给一个对象设置安全回调,在对象被访问时,回调会被调用。V8安全系统会得到与对象绑定的回调,调用它来询问是否允许另一个context访问这个对象。这个回调会被传入被访问的对象,被访问的属性名,访问的类型(读、写、删等)等参数,返回是否允许访问。
Google Chrome里实现了这个机制,这样当安全令牌不匹配时,可以通过指定的回调来判断下面的操作是否被允许:window.focus(), window.blur(), window.close(), window.location, window.open(), history.forward(), history.back(), and history.go()。
异常
如果发生错误,V8会抛出一个异常。例如,当一个脚本或函数试图读一个不存在的属性时,或一个函数被调用,而它实际上不是一个函数时。
如果一个操作不成功,V8会返回空句柄。所以一定要先检查返回的句柄是否为空,然后再继续执行。可以用Handle类的public成员函数IsEmpty()来检查句柄是否为空。
你可以用TryCatch来捕获异常,例如:
TryCatch trycatch;
Handle v = script->Run();
if (v.IsEmpty()) {
Handle<value> exception = trycatch.Exception();
String::AsciiValue exception_str(exception);
printf("Exception: %s\n", *exception_str);
// ...
}
如果返回的是空句柄,而你又没有使用TryCatch,那代码就会bail out。如果用了TryCatch,那你的代码就可以继续执行。
继承
JavaScript是一种无类型、面向对象的语言,就其本身而论,他使用原形(prototypal)继承,而不是经典继承。这会让使用C++、Java这种普通面向对象的程序员很迷惑。
基于类的面向对象的语言,比如Java和C++,有2个不同的概念:类和实例。而JavaScript是基于原型的语言,他没有这种区别:很简单,只有对象。JavaScript原生不支持类继承的声明,然而,JavaScript的原型机制可以很方便地给一个对象的所有实例添加自定义的属性和方法。例如这样给一个实例加自定义属性:
// Create an object "bicycle"
function bicycle(){
}
// Create an instance of bicycle called roadbike
var roadbike = new bicycle()
// Define a custom property, wheels, on roadbike
roadbike.wheels = 2
这样添加的属性只在这一个对象实例中有。如果再创建一个bicycle实例,比如叫mountainbike,mountainbike.wheels会返回undefined,除非也给它加一个wheels属性。
有时这就是我们需要的,但也有时我们希望能给所有实例都加上自定义属性 - 就是说,执行后所有的bicycles都有了wheels。这就是JavaScript的原型对象的用处了。使用原型对象,要在添加自定义属性前,使用关键字prototype:
// First, create the "bicycle" object
function bicycle(){
}
// Assign the wheels property to the object's prototype
bicycle.prototype.wheels = 2
现在所有的bicycle实例都有wheels属性了。
同样的方法可以用在V8的模板里。每个FunctionTemplate都有一个PrototypeTemplate方法,它会返回模板的函数原型。你可以在PrototypeTemplate上设置属性,并绑定C++函数,它会应用到对应FunctionTemplate的每个实例上。例如:
Handle<FunctionTemplate> biketemplate = FunctionTemplate::New();
biketemplate.PrototypeTemplate().Set(
String::New("wheels"),
FunctionTemplate::New(MyWheelsMethodCallback)
)
这会让所有biketemplate实例的原型链上,都有一个wheels方法,当它被调用时,C++函数MyWheelsMethodCallback就会被调用。
V8的FunctionTemplate类提供了一个public函数Inherit(),它能让你的函数模板继承另一个函数模板,例如:
void Inherit(Handle<FunctionTemplate> parent);
浙公网安备 33010602011771号