Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V8引擎探索:如何注入全局变量 #102

Open
youngwind opened this issue Apr 7, 2017 · 3 comments
Open

V8引擎探索:如何注入全局变量 #102

youngwind opened this issue Apr 7, 2017 · 3 comments
Labels

Comments

@youngwind
Copy link
Owner

youngwind commented Apr 7, 2017

前言

最近花了一些时间研究 V8 引擎,收获良多。今天,我们一起来探索一番。

注:阅读本文需要一定 C++ 基础。

V8 与 d8

问题:V8 引擎是一个很复杂的东西,对它的研究,应该从哪里开始着手呢?
答案:从运行它开始。

那么,如何运行 V8 呢?这里有一些参考资料:

  1. 编译 V8 源码,By justjavac
  2. Building-from-Source, By 官方文档
  3. Installing V8 on a Mac,By kevincennis

这些资料讲得都很完备,我就不赘述了。直接给出运行结果示意图。

d8

至此,我们已经把 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 版本)

  1. git checkout -b 4.8 -t branch-heads/4.8
  2. make release
  3. 在 V8 项目根目录下新建 hello_world.cpp 文件,将这里的代码拷贝过去,保存。
  4. 执行命令: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
  5. 执行命令:cp out/x64.release/*.bin .
  6. 执行命令:./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();

把上述压缩成一行的字符串,放入上面的例子中,重新编译,执行结果如下图所示。

prototype

由此,我们已经证明:此 C++ 应用 hello_world 已经能够执行任意给定的 JS 代码。

到底是谁的 console

然而,当我想运行 console 语句的时候,意外的情况发生了。如下所示,给定 JS 代码为输出一个字符串。

Local<String> source =
                String::NewFromUtf8(isolate,
                                    "console.log('哈哈哈');",
                                    NewStringType::kNormal).ToLocalChecked();

执行结果如下图所示:
console

为什么程序无法识别 console?
不是说好的 V8 引擎能够执行 JS 代码?难道 console 不属于 ES 规范?
答案:console 还真不是 ES 规范中定义的,准确地说,console 不属于任何的规范,详见这里

由此,我有以下两点思考:

  1. console 不过是约定俗成的一个不成文规矩,浏览器和 NodeJS 都支持它。V8 作为 JS 执行引擎,只能执行符合 ES 规范的代码。因此,直接调用 console 会报错。
  2. 既然 console 不是 V8 提供的,那为什么在浏览器和 NodeJS 中都能使用呢?到底是谁提供的 console 呢?

带着这个疑问,我进行了以下的尝试:

print

从上图我们可以看出,hello_world、d8 和 NodeJS 的表现各不相同,为什么呢?
这个问题非常困扰我,直到我发现了这个概念:C++和JS 交互
由此,我发现 hello_world、d8、NodeJS 这三者与 v8 真正的关系,如下图所示(点击查看大图):
js-C++-bridge

由此我们可以得出结论: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++ 方法。

下面我们具体来看看注入的代码。

注入全局变量

关于如何注入,网上也有一些参考资料:

  1. JavaScript引擎研究与C、C++与互调用 ,By lwg2001s
  2. 使用 Google V8 引擎开发可定制的应用程序, By 邱俊涛
  3. V8引擎javascript与C++交互, By 心灵捕手
  4. 关于V8 JavaScript Engine的使用方法研究(转), By lcgg110

然而,这些资料大多年代久远,V8 的 API 也发生了变化,因此,其中的代码很难直接运行起来。后来我在 V8 的源码中直接找到了例子,参考这里。仔细观察 shell.cc ,我们能够发现注入全局变量的“三步走”方法:

  1. 声明函数
    void Print(const v8::FunctionCallbackInfo<v8::Value>& args);  // line 54
  2. 定义函数
    // 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);
    }
  3. 注入函数
    // 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 的探索甚是消耗时间,主要有两个难点要克服。

  1. 要有一定的 C++ 基础(虽然上大学的时候学过点皮毛,但是后来基本没用过,都忘光了,只能从头拾起)
  2. 要熟悉 V8 的概念和 API 。这里有个 V8 的 API 文档,仅供参考。

----------- EOF --------------

@52cik
Copy link

52cik commented Apr 7, 2017

我只知道是 宿主(HOST) 提供的对象/方法。。
不过还真不知道怎么注入的。
坐等更新。

@tonymiao2012
Copy link

请问下你的录屏用的什么软件?直接把图片上传到gitHub能显示吗。谢谢

@youngwind
Copy link
Owner Author

@tonymiao2012 licecap;可以。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants