// 这个例子够复杂了吧
function Person(name) {
this.name = name;
}
Person.prototype.hi = function () {
return this.name;
};
var p = new Person('youngwind');
p.hi();
然而,这些资料大多年代久远,V8 的 API 也发生了变化,因此,其中的代码很难直接运行起来。后来我在 V8 的源码中直接找到了例子,参考这里。仔细观察 shell.cc ,我们能够发现注入全局变量的“三步走”方法:
声明函数
void Print(const v8::FunctionCallbackInfo<v8::Value>& args); // line 54
定义函数
// The callback that is invoked by v8 whenever the JavaScript 'print'
// function is called. Prints its arguments on stdout separated by
// spaces and ending with a newline.
void Print(const v8::FunctionCallbackInfo<v8::Value>& args) {
bool first = true;
for (int i = 0; i < args.Length(); i++) {
v8::HandleScope handle_scope(args.GetIsolate());
if (first) {
first = false;
} else {
printf(" ");
}
v8::String::Utf8Value str(args[i]);
const char* cstr = ToCString(str);
printf("%s", cstr);
}
printf("\n");
fflush(stdout);
}
注入函数
// Creates a new execution environment containing the built-in
// functions.
v8::Local<v8::Context> CreateShellContext(v8::Isolate* isolate) {
// Create a template for the global object.
v8::Local<v8::ObjectTemplate> global = v8::ObjectTemplate::New(isolate);
// Bind the global 'print' function to the C++ Print callback.
global->Set(
v8::String::NewFromUtf8(isolate, "print", v8::NewStringType::kNormal)
.ToLocalChecked(),
v8::FunctionTemplate::New(isolate, Print));
return v8::Context::New(isolate, NULL, global);
}
前言
最近花了一些时间研究 V8 引擎,收获良多。今天,我们一起来探索一番。
注:阅读本文需要一定 C++ 基础。
V8 与 d8
问题:V8 引擎是一个很复杂的东西,对它的研究,应该从哪里开始着手呢? 答案:从运行它开始。
那么,如何运行 V8 呢?这里有一些参考资料:
这些资料讲得都很完备,我就不赘述了。直接给出运行结果示意图。
至此,我们已经把 V8 的 Demo d8 跑起来,并且可以让其执行任意的 JS 代码。 但是,我们仔细想想:V8 和 d8 是一个概念吗? 不是的,V8 和 d8 不是一个概念。V8 是一个 C++ 库,d8 是一个 C++ 应用,其中内嵌了 V8 库,所以,d8 才能执行 JS 代码(因为它本质上将输入的 JS 代码交给 V8 处理了)。
那么,我们能不能模仿 d8,自己写一个 C++ 应用,来执行指定的 JS 代码呢?
内嵌 V8
官方给出了一个内嵌 V8 的 demo,按照该文档进行操作,便可以自己实现这样的一个 C++ 应用。
请注意,之前我看这文档的时候还是对应 V8 的 4.8 版本,目前该文档已经升级到 5.8 版本,操作步骤有些不同。我在这里当初当时我操作 4.8 版本的步骤,仅供参考。(本文后面所有的探索都是基于 4.8 版本)
clang++ -stdlib=libstdc++ -std=c++11 -I. hello_world.cpp -o hello_world out/x64.release/libv8_base.a out/x64.release/libv8_libbase.a out/x64.release/libicudata.a out/x64.release/libicuuc.a out/x64.release/libicui18n.a out/x64.release/libv8_base.a out/x64.release/libv8_external_snapshot.a out/x64.release/libv8_libplatform.a
cp out/x64.release/*.bin .
./hello_world
,屏幕会打印出 “Hello, World!" 字样为什么屏幕会输出 ”Hello, World!" 呢? 因为在此 demo 中,给定执行的 JS 语句为 'Hello' + ' , World!'(如下面代码所示) ,这是一个表达式,此表达式执行返回的结果就是一个字符串。
ok,你可能会觉得这样的表达式太简单了,不足以证明其能够正确运行 JS 代码。 好,那我们尝试用复杂的原型链作为例子,如下所示。
把上述压缩成一行的字符串,放入上面的例子中,重新编译,执行结果如下图所示。
由此,我们已经证明:此 C++ 应用 hello_world 已经能够执行任意给定的 JS 代码。
到底是谁的 console
然而,当我想运行 console 语句的时候,意外的情况发生了。如下所示,给定 JS 代码为输出一个字符串。
执行结果如下图所示:![console](https://cloud.githubusercontent.com/assets/8401872/24783442/f32bd754-1b7e-11e7-9da4-b00ec1deb9c7.png)
为什么程序无法识别 console? 不是说好的 V8 引擎能够执行 JS 代码?难道 console 不属于 ES 规范? 答案:console 还真不是 ES 规范中定义的,准确地说,console 不属于任何的规范,详见这里。
由此,我有以下两点思考:
带着这个疑问,我进行了以下的尝试:
从上图我们可以看出,hello_world、d8 和 NodeJS 的表现各不相同,为什么呢? 这个问题非常困扰我,直到我发现了这个概念:C++和JS 交互。 由此,我发现 hello_world、d8、NodeJS 这三者与 v8 真正的关系,如下图所示(点击查看大图):![js-C++-bridge](https://cloud.githubusercontent.com/assets/8401872/24784085/d089ccf2-1b82-11e7-8a29-03def2a13c8d.png)
由此我们可以得出结论:hello_world、d8、NodeJS和浏览器内核,都是一个 C++ 应用,其中内嵌 V8 引擎,用于执行 JS 代码。但是,它们会 V8 在外边包裹一层 Bridge,通过这一层 Bridge,实现 JS 和 C++ 之间的相互调用,以达到扩展 JS 的目的。
举个例子:为什么 d8 能够运行语句
print("哈哈哈");
呢?因为 d8 里面有一个 C++ 方法 Print,通过某种方式,将此方法注入到 V8 的全局环境中,对应到全局变量 print上。所以,当 V8 在执行该 “JS” 代码 print 的时候,其实本质上是在调用 Print 这个 C++ 方法。下面我们具体来看看注入的代码。
注入全局变量
关于如何注入,网上也有一些参考资料:
然而,这些资料大多年代久远,V8 的 API 也发生了变化,因此,其中的代码很难直接运行起来。后来我在 V8 的源码中直接找到了例子,参考这里。仔细观察 shell.cc ,我们能够发现注入全局变量的“三步走”方法:
至此,我们终于能搞明白如何注入全局变量了。 为了方便后续的调试,我提前编译好了 V8(4.8版本的),并且将一些所需要的头文件和中间过程生成的 .a 文件拷贝到一个新的仓库 fake-node 中,按照上面的步骤,便可以随意注入其他全局变量了。
后话
对 V8 的探索甚是消耗时间,主要有两个难点要克服。
----------- EOF --------------