Javascript里有个C:Part 1 - 基础
发布于 4年前 作者 fool 8275 次浏览 最后一次编辑是 3年前

这是一个系列文章,将从V8引擎的接口出发,从C++的角度解释JavaScript 。在最后,我们会学会如何用C++拓展V8引擎,了解node.js的整合机制,最终用C++做出一个node.js的模块。

Javascript里有个C 系列文章:


  1. Javascript里有个C:Part 1 – 基础

  2. Javascript里有个C:Part 2 – 对象

  3. Javascript里有个C:Part 3 - 深入对象

  4. Javascript里有个C:Part 4 - 异步

  5. Javascript里有个C:Part 5 - node.js

  6. 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>


using namespace v8;

int main(int argc, char
argv[]) {

  // 新建基于栈分配的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

7 回复

"如果你的代码只使用一个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 上, 请问是这样吗?

是这样的。HandleScope的作用就是一个用来放置Handle的容器,专门用来自动控制其后分配的Handle的生命周期。Context::Scope用来隐式地进行Context::Enter、Context::Exit。

谢谢!
看起来比较容易上手.
比起PHP扩展的写法, 大量魔法宏来说, V8的接口美丽多了.

[…] Javascript里有个C:Part 1 – 基础 […]

[…] Javascript里有个C:Part 1 – 基础 […]

回到顶部