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

这是一个系列文章,将从V8引擎的接口出发,从C++的角度解释JavaScript 。在最后,我们会学会如何用C++拓展V8引擎,了解node.js的整合机制,最终用C++做出一个node.js的模块。 <br/> <br/><em>Javascript里有个C </em>系列文章: <br/><ol> <br/> <li><a title=“编辑“Javascript里有个C:Part 1 – 基础”” href=“http://cnodejs.org/blog/?p=1621”>Javascript里有个C:Part 1 – 基础</a></li> <br/> <li><a href=“http://cnodejs.org/blog/?p=1804”>Javascript里有个C:Part 2 – 对象</a></li> <br/> <li><a href=“http://cnodejs.org/blog/?p=1833”>Javascript里有个C:Part 3 - 深入对象</a></li> <br/> <li>Javascript里有个C:Part 4 - 异步</li> <br/> <li>Javascript里有个C:Part 5 - node.js</li> <br/> <li>Javascript里有个C:Part 6 - 实战node.js Module</li> <br/></ol> <br/>在此之前,你需要了解<a href=“http://code.google.com/apis/v8/build.html”>如何编译V8引擎</a>,以及<a href=“http://code.google.com/apis/v8/get_started.html”>如何把自己的代码和V8引擎链接</a>。 <br/> <br/>本文将阐述JavaScript在V8里的基本概念。 <br/> <br/><!–more–> <br/><h4>Handle</h4> <br/>JavaScript里的对象都由垃圾回收器进行管理,对象间的赋值均是传递引用,与C++由用户管理内存和值语义的机制截然不同。那么C++如何表示JavaScript里的对象呢?通过一种GC管理的智能指针。 <br/> <br/>V8里使用Handle类型来托管 JavaScript对象,与C++的std::shared_pointer类似,Handle类型间的赋值均是直接传递对象引用,但不同的是,V8使用自己的GC来管理对象生命周期,而不是智能指针常用的引用计数。 <br/> <br/>JavaScript类型在C++中均有对应的自定义类型,如String、Integer、Object、Date、Array等,严格遵守在JavaScript中的继承关系。C++中使用这些类型时,必须使用Handle托管,以使用GC来管理它们的生命周期,而不使用原生栈和堆。 <br/> <br/>Handle的使用和智能指针相同,以Handle<T>的形式声明,通过operator->调用T的成员函数,通过operator解引用。建立新的对象时,需要使用T::New()来返回一个Handle<T>。 <br/> <br/>另外,Handle的生命周期和C++智能指针不同,并不是在C++语义的scope内生存(即{} 包围的部分),而需要通过HandleScope手动指定。 HandleScope只能分配在栈上, HandleScope对象声明后, 其后建立的Handle都由HandleScope来管理生命周期,HandleScope对象析构后,其管理的Handle将由GC判断是否回收。 <br/> <br/>下面是一个例子: <br/><pre escaped=“true” lang=“c”>  // We will be creating temporary handles so we use a handle scope. <br/>  HandleScope handle_scope; <br/> <br/>  // Create a new empty array. <br/>  Handle<Array> array = Array::New(3); <br/> <br/>  // Fill out the values <br/>  array->Set(0, Integer::New(x)); <br/>  array->Set(1, Integer::New(y)); <br/>  array->Set(2, Integer::New(z));</pre> <br/>例子很简单,我们首先声明handle_scope来限定之后Handle的scope,然后通过Array::New返回了一个新的Array对象,最后使用Array::Set来设定Array的数据。 <br/> <br/>那么,如果我们想通过一个函数来返回一个Handle呢?最直观的代码可能会是这样: <br/><pre escaped=“true” lang=“c”>// This function returns a new array with three elements, x, y, and z. <br/>Handle<Array> NewPointArray(int x, int y, int z) { <br/> <br/>  // We will be creating temporary handles so we use a handle scope. <br/>  HandleScope handle_scope; <br/> <br/>  // Create a new empty array. <br/>  Handle<Array> array = Array::New(3); <br/> <br/>  // Return an empty result if there was an error creating the array. <br/>  if (array.IsEmpty()) <br/>    return array(); <br/> <br/>  // Fill out the values <br/>  array->Set(0, Integer::New(x)); <br/>  array->Set(1, Integer::New(y)); <br/>  array->Set(2, Integer::New(z)); <br/> <br/>  return array; <br/>}</pre> <br/>但问题是,当函数返回时,handle_scope会被析构,其管理的Handle也都将被回收,那么此刻返回的array将不再有意义。V8的解决方案是HandleScope::Close (Handle<T> value),这个成员函数将关闭当前HandleScope并把参数中的Handle的转交给上一个scope,也就是进入这个函数之前所在的scope。下面是正确的代码: <br/><pre escaped=“true” lang=“c”>// This function returns a new array with three elements, x, y, and z. <br/>Handle<Array> NewPointArray(int x, int y, int z) { <br/> <br/>  // We will be creating temporary handles so we use a handle scope. <br/>  HandleScope handle_scope; <br/> <br/>  // Create a new empty array. <br/>  Handle<Array> array = Array::New(3); <br/> <br/>  // Return an empty result if there was an error creating the array. <br/>  if (array.IsEmpty()) <br/>    return Handle(); <br/> <br/>  // Fill out the values <br/>  array->Set(0, Integer::New(x)); <br/>  array->Set(1, Integer::New(y)); <br/>  array->Set(2, Integer::New(z)); <br/> <br/>  // Return the value through Close. <br/>  return handle_scope.Close(array); <br/>}</pre> <br/>此时类似于栈的Handle我们已了解完毕。但还有一种情况。JavaScript里还存在全局对象,我们怎么在C++里表示呢? <br/> <br/>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++在堆上分配对象。 <br/><h4>Context</h4> <br/>V8允许不同的JavaScript代码运行在完全不同的环境下,其运行环境称为Context。不同的Context下拥有自己的全局对象(PersistentHandle),运行代码时必须指定所在的Context。最典型的例子就是Chrome的标签,每个标签都拥有自己的Context。 <br/> <br/>Context拥有自己的全局代理对象(global proxy object),每个Context下的全局对象都是这个全局代理对象的属性。通过Context::Global ()可以得到这个全局代理对象。新建Context时你可以手动指定它的全局代理对象,这样每个Context都会自动拥有一些全局对象,比如DOM。 <br/> <br/>Context也是一种scope,通过Context::Enter ()和Context::Exit ()来进入、退出,或者使用类似于HandleScope的Context::Scope来隐式进入,其代码非常简单: <br/><pre escaped=“true” lang=“c”> /* <br/> * Stack-allocated class which sets the execution context for all <br/> * operations executed within a local scope. <br/> / <br/> class Scope { <br/> public: <br/> explicit inline Scope(Handle<Value> context) : context_(context) { <br/> context_->Enter(); <br/> } <br/> inline ~Scope() { context_->Exit(); } <br/> private: <br/> Handle context_; <br/> };</pre> <br/>进入Context的scope后,接下来新建的Handle都会建立在这个Context下。 <br/> <br/>下面的图可以表示Context间scope的关系: <br/> <br/><img src=“http://code.google.com/apis/v8/images/intro_contexts.png” alt="" /> <br/> <br/>当建立scopeA时,程序将自动进入contextA的scope,建立scopeB时,程序又将自动进入contextB的scope,最后scopeB析构时,程序又回到contextA。 <br/><h4>Script</h4> <br/>JavaScript代码的运行分为两个部分:编译和运行。这两个过程通过Script对象来完成。首先由Script::Compile (Handle<String> source)来由源码字符串进行编译并返回一个编译后的Script对象,然后再由Local<Value> Script::Run ()运行代码并返回结果。 <br/><h4>综述</h4> <br/>最终一段完整的代码如下所示,它包含了新建Context、运行代码等完整过程: <br/><pre escaped=“true” lang=“c”>#include <v8.h> <br/> <br/>using namespace v8; <br/> <br/>int main(int argc, char argv[]) { <br/> <br/>  // 新建基于栈分配的handle scope. <br/>  HandleScope handle_scope; <br/> <br/>  // 新建context. <br/>  Persistent<Context> context = Context::New(); <br/> <br/>  // 进入context,我们将在这个context内编译运行代码 <br/>  Context::Scope context_scope(context); <br/> <br/>  // 新建一个包含源代码的字符串. <br/>  Handle<String> source = String::New("‘Hello’ + ‘, World!’"); <br/> <br/>  // 编译代码. <br/>  Handle<Script> script = Script::Compile(source); <br/> <br/>  // 运行代码并获取结果. <br/>  Handle<Value> result = script->Run(); <br/> <br/>  // 因为context是Persistent Handle,所以要手动释放. <br/>  context.Dispose(); <br/> <br/>  // 将结果转换成ASCII字符串. <br/>  String::AsciiValue ascii(result); <br/>  printf("%s\n", *ascii); <br/>  return 0; <br/>}</pre> <br/>用一张图来表示这段代码Handle的生命周期如下: <br/> <br/><img src=“http://code.google.com/apis/v8/images/local_persist_handles_review.png” alt="" /> <br/> <br/>至此,我们已经了解了V8的大部分基础概念,下一篇文章里我们将讨论操作JavaScript里的对象。 <br/> <br/>文中使用的部分代码来自V8文档:<a href=“http://code.google.com/apis/v8/embed.html”>http://code.google.com/apis/v8/embed.html</a>

10 回复

"如果你的代码只使用一个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等完整后即文中的“完整的代码”

谢谢指正,摘抄范例代码时确实未实地运行过,该打!后面的文章我会非常注意的!

仔细看完文中的代码: <br/> HandleScope handle_scope; <br/> Persistent context = Context::New(); <br/> Context::Scope context_scope(context); <br/> Handle source = String::New("‘Hello’ + ‘, World!’"); <br/>是不是可以这样理解: <br/> HandleScope handle_scope; <br/> Context::Scope context_scope(context); <br/>这两句语句是有"副作用"的, 这两句后面的Handle source会自动分配在"最近"的HandleScope 和 Context::Scope 上, 请问是这样吗?

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

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

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

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

挖个坟~v8变化好快的说~ NAN都是用 crazy 来形容 v8 & node core

巨赞!

来自酷炫的 CNodeMD

回到顶部