V8引擎探索:如何注入全局变量
youngwind opened this issue · 3 comments
前言
最近花了一些时间研究 V8 引擎,收获良多。今天,我们一起来探索一番。
注:阅读本文需要一定 C++ 基础。
V8 与 d8
问题:V8 引擎是一个很复杂的东西,对它的研究,应该从哪里开始着手呢?
答案:从运行它开始。
那么,如何运行 V8 呢?这里有一些参考资料:
- 编译 V8 源码,By justjavac
- Building-from-Source, By 官方文档
- Installing V8 on a Mac,By kevincennis
这些资料讲得都很完备,我就不赘述了。直接给出运行结果示意图。
至此,我们已经把 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 版本)
- git checkout -b 4.8 -t branch-heads/4.8
- make release
- 在 V8 项目根目录下新建 hello_world.cpp 文件,将这里的代码拷贝过去,保存。
- 执行命令:
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!'(如下面代码所示) ,这是一个表达式,此表达式执行返回的结果就是一个字符串。
Local<String> source =
String::NewFromUtf8(isolate,
"'Hello' + ' , World!'",
NewStringType::kNormal).ToLocalChecked();
ok,你可能会觉得这样的表达式太简单了,不足以证明其能够正确运行 JS 代码。
好,那我们尝试用复杂的原型链作为例子,如下所示。
// 这个例子够复杂了吧
function Person(name) {
this.name = name;
}
Person.prototype.hi = function () {
return this.name;
};
var p = new Person('youngwind');
p.hi();
把上述压缩成一行的字符串,放入上面的例子中,重新编译,执行结果如下图所示。
由此,我们已经证明:此 C++ 应用 hello_world 已经能够执行任意给定的 JS 代码。
到底是谁的 console
然而,当我想运行 console 语句的时候,意外的情况发生了。如下所示,给定 JS 代码为输出一个字符串。
Local<String> source =
String::NewFromUtf8(isolate,
"console.log('哈哈哈');",
NewStringType::kNormal).ToLocalChecked();
为什么程序无法识别 console?
不是说好的 V8 引擎能够执行 JS 代码?难道 console 不属于 ES 规范?
答案:console 还真不是 ES 规范中定义的,准确地说,console 不属于任何的规范,详见这里。
由此,我有以下两点思考:
- console 不过是约定俗成的一个不成文规矩,浏览器和 NodeJS 都支持它。V8 作为 JS 执行引擎,只能执行符合 ES 规范的代码。因此,直接调用 console 会报错。
- 既然 console 不是 V8 提供的,那为什么在浏览器和 NodeJS 中都能使用呢?到底是谁提供的 console 呢?
带着这个疑问,我进行了以下的尝试:
从上图我们可以看出,hello_world、d8 和 NodeJS 的表现各不相同,为什么呢?
这个问题非常困扰我,直到我发现了这个概念:C++和JS 交互。
由此,我发现 hello_world、d8、NodeJS 这三者与 v8 真正的关系,如下图所示(点击查看大图):
由此我们可以得出结论: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++ 方法。
下面我们具体来看看注入的代码。
注入全局变量
关于如何注入,网上也有一些参考资料:
- JavaScript引擎研究与C、C++与互调用 ,By lwg2001s
- 使用 Google V8 引擎开发可定制的应用程序, By 邱俊涛
- V8引擎javascript与C++交互, By 心灵捕手
- 关于V8 JavaScript Engine的使用方法研究(转), By lcgg110
然而,这些资料大多年代久远,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(4.8版本的),并且将一些所需要的头文件和中间过程生成的 .a 文件拷贝到一个新的仓库 fake-node 中,按照上面的步骤,便可以随意注入其他全局变量了。
后话
对 V8 的探索甚是消耗时间,主要有两个难点要克服。
- 要有一定的 C++ 基础(虽然上大学的时候学过点皮毛,但是后来基本没用过,都忘光了,只能从头拾起)
- 要熟悉 V8 的概念和 API 。这里有个 V8 的 API 文档,仅供参考。
----------- EOF --------------
我只知道是 宿主(HOST) 提供的对象/方法。。
不过还真不知道怎么注入的。
坐等更新。
请问下你的录屏用的什么软件?直接把图片上传到gitHub能显示吗。谢谢
@tonymiao2012 licecap;可以。