V8引擎嵌入指南

时间:2024-04-02 09:06:26

如果已读过V8编程入门那你已经熟悉了如句柄(handle)、作用域(scope)和上下文(context)之类的关键概念,以及如何将V8引擎作为一个独立的虚拟机来使用。本文将进一步讨论这些概念,并介绍其他有关V8引擎嵌入C++应用程序的关键概念。

V8引擎的API提供了编译执行脚本、访问C++方法和数据结构、处理错误、执行安全检查等功能。你的应用程序可以像使用其他C++库一样使用V8引擎。你可以通过在C++代码中包含头文件include/v8.h来访问V8引擎的API。

V8引擎设计理念这篇文章中提供的背景信息,可能会对您优化您的应用程序有所帮助。

句柄和垃圾收集

句柄是对保存在堆(heap)中的JavaScript对象的引用。V8垃圾收集器会回收不可访问对象所占用的内存。在垃圾回收的过程中,垃圾收集器常常移动堆中对象的位置。当垃圾收集器移动对象的时候的它也更新所有指向这些对象的句柄,使其指向对象新的位置。

如果一个对象在JavaScript代码中无法访问,且没有任何句柄指向这个对象(c++代码中),它就会被认为是垃圾。垃圾收集器不停的删除所有被视为垃圾的对象。V8引擎的垃圾收集机制,是V8引擎拥有高性能的关键。要了解更多相关信息,请参考V8引擎设计理念 。

有两种类型的句柄(handle):

    • 本地句柄保存在堆栈(stack)中,在其析构函数被调用的时候从堆栈中删除(自身)。这些句柄的生命周期是由句柄作用域(handle scope)决定的,作用域(scope)常常在调用函数时创建。当句柄作用域被删除时,垃圾收集器将释放这些句柄之前所引用的对象,因为他们将不能通过JavaScript或其他句柄访问。V8编程入门中的例子使用的就是这种类型的句柄。Local<SomeType>代表本地句柄,它也可以存储在父类Handle<SomeType>的实例中。

注意 :保存句柄的栈并不是C++的调用堆栈,但是句柄作用域却嵌入在C++堆栈中。句柄作用域对象只能是堆栈分配,不能使用new生成实例。

  • 持久句柄并不保存在堆栈中,并且仅当你专门删除它们时才会被删除。就像一个本地句柄,持久句柄也保存指向堆分配对象的引用。如果你需要在多个函数中访问同一个对象,或者句柄的生命周期和C++作用域并不相同时,你可以使用持久句柄。例如,Google Chrome使用持久句柄保存文档对象模型(DOM)节点的引用。使用Persistent::NewPersistent::Dispose创建和销毁一个持久句柄。持久句柄可以使用Persistent::MakeWeak将其标记为weak,这将在对一个对象的唯一引用是弱持久句柄(weak persistent)时触发垃圾收集器执行一个回调。Persistent<SomeType>代表持久句柄,它也可以存储在父类Handle<SomeType>的实例中。

当然,每创建一个对象都会创建一个句柄,着可能会导致出现大量的句柄!这时句柄作用域就非常有用了。你可以将句柄作用域当做一个可以保存很多句柄的容器。句柄作用域的析构函数被调用时会从堆栈中删除该作用域内所有的句柄。正如您所期望的,这会使所有被删除句柄所指向的对象被垃圾回收器标记为从堆中删除。

回到V8 编程入门中那个很简单的例子 ,在下面的图中你可以看到句柄堆栈(handle-stack)和在堆中分配的对象(heap-allocated objects)。请注意Context::New()返回的持久句柄并不在句柄堆栈中。

V8引擎嵌入指南

当析构函数HandleScope::~HandleScope被调用,即删除句柄作用域时,如果没有别的句柄引用,被删除作用域内的所有句柄所指向的对象都会在下次垃圾回收时被标记为删除。垃圾收集器还可以从堆中删除source_objscript_obj对象,因为他们不再被任何句柄引用且不能在JavaScript中访问。由于上下文句柄(context handle)是一个持久句柄,它不会在超出句柄作用于后被删除。删除上下文句柄唯一的方法是显式的调用它的Dispose方法。

 :在本文中“句柄(handle)”一词指的是本地句柄(local handle),当提及“持久句柄(persistent handle)”时,我们会使用持久句柄的全称。

上下文(context)

在V8中,上下文(context)是一个执行环境,允许JavaScript应用程序独立地、不相关地运行在一个V8引擎实例中。要执行任何JavaScript代码,您必须显式的指定其运行的上下文。

这为什么是必要的?由于JavaScript提供了一套内置的实用函数和对象,而它们可以被JavaScript代码修改。例如,当两个完全不相干的JavaScript函数都以同样的方式改变了全局对象,则很可能会产生意外结果。

从CPU时间和内存的角度看来,创建一个新的执行环境并建立必须的对象,可能是非常昂贵的开销。不过,V8引擎中广泛存在的缓存特性可以保证,第一次创建上下文是比较昂贵的,但其后创建上下文的花费会少很多。这是因为第一个上下文(context)需求创建内置对象并解析内置的JavaScript代码(built-in JavaScript code),但随后创建的上下文只需要创建自己的内置对象即可。随着V8引擎快照功能(通过构建参数snapshot=yes来激活,默认处于激活状态)的出现,第一次创建上下文所花的时间将会被很大程度的优化。因为快照功能包含一个经过序列化的堆,其中已经包括编译好的JavaScript内置对象的代码。与垃圾收集机制一样,V8引擎中广泛存在的缓存也是V8引擎高性能的关键,更多信息请参阅V8引擎设计理念 。

当您创建了一个上下文之后,可以任意次数的进入和退出该上下文。当在上下文A中时,您也可以进入一个不同的上下文B,这意味着您使用B上下文取代A成为当前上下文。当您从B中退出时,A恢复成为当前上下文。如下所示:

V8引擎嵌入指南

请注意,内置的实用函数和对象在各上下文中是独立的。当创建上下文时,您也可以设置一个可选的安全令牌。更多信息请参见安全模型章节。

V8引擎使用上下文的初衷是,使每个窗口和浏览器的iframe可以有它们自己的JavaScript环境。

模板

在上下文中,模板是JavaScript函数和对象的框架。您可以使用模板将C++函数和数据结构包装在JavaScript对象中,使他们可以通过JavaScript脚本来操作。例如,Google Chrome使用模板将C++ DOM节点包装成JavaScript对象,并将其安装在全局命名空间中。您可以创建一套模板,在多个上下文中使用。你可以创建任意多的模板,但在一个上下文中,每个模版只能有一个实例存在。

在JavaScript中,函数和对象之间有强烈的两重性。在Java或C++中要创建一个新类型的实例,通常情况下你会首先定义一个新类。在JavaScript中你会创建一个新的函数,并使用这个函数作为构造函数创建新的实例。一个JavaScript对象的结构和功能是和它的构造函数紧密相关的。这将在V8模版的工作方式中体现。V8有两种类型的模板:

  • 函数模板
    函数模板是一个函数的框架。你可以在想要生成JavaScript函数的上下文中调用模板的”GetFunction”方法来创建一个该模版的JavaScript实例。您还可以将一个c++回调函数和一个函数模版相关联,这个回调函数将在JavaScript函数执行时被调用。
  • 对象模板
    每个函数模板都有一个与之关联的对象模板。它用来配置使用这个函数作为构造函数所创建的对象。对于对象模板,你可以将两种C++回调函数与之关联:
    -“存取回调(accessor callbacks)”在对象的指定属性被脚本访问时调用。
    -“拦截回调(interceptor callbacks)”在对象的任意属性被脚本访问时调用。
    存取器(Accessors)和拦截器(interceptors)将在下文中讨论。

下面的演示代码创建全局对象模板,并设置内置的全局函数。

// 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中的JsHttpProcessor::Initializer函数。

存取器(accessors)

存取器(accessors)是一个C++回调函数,在JavaScript脚本存取对象属性时计算并返回该属性的值。存取器可以通过对象模板的SetAccessor方法来设置。此方法接受一个属性名称和两个回调函数分别用于读取和写入属性的值。

一个存取器的复杂程度取决于它们操作的数据类型:

  • 存取静态全局变量
  • 存取动态变量

存取静态全局变量

比方说,有两个C++整形变量xy,将要提供给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);

请注意上述代码中的对象模板和上下文是同时创建的。模板可以事先创建,并可在任意多个上下文中使用。

返回页首

存取动态变量

前面例子中的变量是静态的全局变量,如果数据是动态的,比如说是浏览器中的DOM树。让我们想象xy是c++类Point的对象字段:

 class Point {
   public:
    Point(int x, int y) : x_(x), y_(y) { }
    int x_, y_;
  }

为了使任意数量的C++point类的实例都可用于JavaScript,我们需要为每个C++point的实例都创建一个与之对应的JavaScript对象 ,并在JavaScript对象和C++实例之间建立联系。这个操作通过外部对象(external values)和内部字段(internal object fields)来完成。

首先创建一个对象模板(object template)包装point对象:

Handle<ObjectTemplate> point_templ = ObjectTemplate::New();

每个JavaScript的point对象保留一个对C++对象的引用,这是包含内部字段(internal field)的包装(wrapper)。这些字段被称为内部字段是因为他们不能在JavaScript中访问,它们只能从C++代码中访问。一个对象可以有任意数量的内部字段,使用如下方式设置内部字段的数量:

point_templ->SetInternalFieldCount(1);

在这里,内部字段计数被设置为1 ,这意味着该对象有一个内部字段指向一个C++对象,其索引为0

添加xy的存取器:

  point_templ.SetAccessor(String::New("x"), GetPointX, SetPointX);
  point_templ.SetAccessor(String::New("y"), GetPointY, SetPointY);

然后,新建一个模版的实例来包装c++ point对象,并设置索引为0的内部字段指向point实例p的外部包装器(external wrapper)。

  Point* p = ...;
  Local<Object> obj = point_templ->NewInstance();
  obj->SetInternalField(0, External::New(p));

外部对象(external object,即上面提到的外部包装器)仅仅是一个void*的包装。外部对象只能用于在内部字段中保存引用值。JavaScript对象不能直接引用C++对象,所以使用外部对象作为连接JavaScript和C++的“桥梁”。从这个意义上,外部对象的作用和句柄是相反的,因为句柄允许C++保留JavaScript对象的引用。

下面是xgetset存取器的定义,y存取器和x完全相同:

 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对象。

拦截器(Interceptors)

您还可以指定一个在脚本访问对象的任意属性时都会触发的回调。这些回调被称为拦截器(interceptors)。为了提高效率,V8中有两种类型的拦截器:

  • 命名属性拦截器 -使用属性名访问属性时被调用。
    例如,在浏览器环境中,使用document.theFormName.elementName
  • 索引属性拦截器 -使用索引访问属性时被调用。例如,在浏览器环境中,使用document.forms.elements[0]

V8引擎源代码中的示例process.cc,提供了一个使用拦截器的例子。在下面的代码片段中SetNamedPropertyHandler函数设置了MapGetMapSet拦截器:

Handle<ObjectTemplate> result = ObjectTemplate::New();
result->SetNamedPropertyHandler(MapGet, MapSet);

MapGet拦截器代码如下:

Handle<Value> JsHttpRequestProcessor::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是不同的)。所有这三个必须匹配,才被视为同源。如果没有这种保护,恶意网页可能会影响其他正常的网页。

在V8中“源”被定义为一个上下文。默认情况下,不能在一个作用域中访问其他作用域。要访问和当前作用域不同的作用域,你需要使用安全令牌(security token)或安全回调(security callbacks)。一个安全令牌可以是任何值,但通常是一个符号或一个唯一的字符串。当定义上下文时,您可以通过SetSecurityToken指定安全令牌。如果你不指定安全令牌V8引擎将自动为新建的上下文生成令牌。

当试图将访问一个全局变量时的V8引擎安全系统首先检查被访问全局对象的安全令牌和访问代码的安全令牌。如果令牌匹配则授予访问权限。如果不匹配V8引擎会执行一个回调,用以判定是否应该允许访问。通过使用模板对象的SetAccessCheckCallbacks方法,你可以设置一个安全回调,这个回调函数决定是否允许对该对象的访问。V8引擎安全系统就可以获取被访问对象的安全回调,并执行它来决定是否允许来自另一个上下文的访问。这个回调函数的参数中包含被访问的对象,被访问属性的名称,访问的类型(如读、写、删除等)并返回是否允许访问。

Google Chrome使用了的这种机制,如果安全令牌不匹配,浏览器将使用特殊的安全回调决定是否可以访问如下对象: window.focus() window.blur() window.close() window.location ,window.open() history.forward() history.back()history.go()

异常

V8引擎在发生错误时会抛出异常,例如-当一个脚本或函数试图读取一个不存在的属性,或者如果一个不是函数的对象被当作函数调用。

如果操作没有成功V8引擎将返回一个空的句柄。因此,在继续执行操作之前,检查代码返回值是不是空句柄非常重要。你可以使用Handle类的公共成员函数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,那程序将会退出。如果有一个TryCatch捕获异常,则程序可以继续执行。

继承

JavaScript是一无类型的面向对象语言,而且它使用原型继承而非类继承。对于从传统面向对象语言如C++和Java转换而来的程序员而言,这相当令人费解。

基于类的面向对象语言,如Java和C++,是建立在两个不同的实体的概念:类和实例之上的。JavaScript是一种基于原型的语言,因此不会作出这种区别:它只有对象。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,除非显式的添加wheels属性,否则mountainbike.wheels将返回undefined

有时候这正是我们所需要的,但有时候将自定义属性添加到一个对象的所有实例会也许会更有帮助,毕竟所有自行车都有*。这是JavaScript的prototype对象非常有用的原因。要使用原型对象,在添加自定义属性之前使用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++的函数与之关联,随后该函数模板的所有实例都将拥有该属性。例如:

 Handle<FunctionTemplate> biketemplate = FunctionTemplate::New();
 biketemplate.PrototypeTemplate().Set(
     String::New("wheels"),
     FunctionTemplate::New(MyWheelsMethodCallback)
 )

这将使biketemplate的所有实例都从原型链继承wheels方法,并在wheels方法被调用的时候执行C++函数MyWheelsMethodCallback

V8的FunctionTemplate类提供了公共成员函数Inherit(),使用它可以使一个函数模板继承另一个函数模板。如下所示:

void Inherit(Handle<FunctionTemplate> parent);
翻译自:https://developers.google.com/v8/embed?csw=1