v8 extension 机制


发布于 2021-09-17


背景说明

V8 JavaScript 引擎支持一种v8::Extension的机制,可以让 C++ 往 JavaScript 运行环境注入创建新的 JavaScript 对象。

v8::Extension相比使用v8::ObjectTemplate来注入创建新的 JavaScript 对象,更加简单直接。

使用 v8::Extension

假如我们要为 JavaScript 创建一个 Demo 对象:

  1. 创建一个 DemoExtension 类,继承自v8::Extension。在 DemoExtension 类中实现对象的具体内容。
  2. 通过v8::RegisterExtension函数把我们的 DemoExtension 类实例注册到 V8 中。
  3. 创建一个v8::ExtensionConfiguration变量,创建的v8::Context时用于配置注入的哪些v8::Extension生效。

下面我们注入 dog 和 cat 两个JavaScript对象,具体代码如下:

class DogExtension : public v8::Extension {
 public:
  DogExtension(const std::string& name)
      : v8::Extension("dog", R"(
                      dog = {};
                      dog.age = 1;

                      dog.eat = function() {
                          native function Eat();
                          return Eat();
                      };
)") {}

 private:
  v8::Local<v8::FunctionTemplate> GetNativeFunctionTemplate(
      v8::Isolate* isolate,
      v8::Local<v8::String> name) override {
    if (name->StringEquals(v8::String::NewFromUtf8(
                               isolate, "Eat", v8::NewStringType::kInternalized)
                               .ToLocalChecked())) {
      return v8::FunctionTemplate::New(isolate, Eat);
    }

    return v8::Local<v8::FunctionTemplate>();
  }

  static void Eat(const v8::FunctionCallbackInfo<v8::Value>& args) {
    args.GetReturnValue().Set(StringToV8Value(args.GetIsolate(), "dog eat!"));
  }
};

class CatExtension : public v8::Extension {
 public:
  CatExtension(const std::string& name)
      : v8::Extension("cat", R"(
                      cat = {};
                      cat.age = 2;

                      cat.eat = function() {
                          native function Eat();
                          return Eat();
                      };

                      cat.jump = function() {
                          native function Jump();
                          return Jump();
                      };
)") {}

 private:
  v8::Local<v8::FunctionTemplate> GetNativeFunctionTemplate(
      v8::Isolate* isolate,
      v8::Local<v8::String> name) override {
    if (name->StringEquals(v8::String::NewFromUtf8(
                               isolate, "Eat", v8::NewStringType::kInternalized)
                               .ToLocalChecked())) {
      return v8::FunctionTemplate::New(isolate, Eat);
    } else if (name->StringEquals(
                   v8::String::NewFromUtf8(isolate, "Jump",
                                           v8::NewStringType::kInternalized)
                       .ToLocalChecked())) {
      return v8::FunctionTemplate::New(isolate, Jump);
    }

    return v8::Local<v8::FunctionTemplate>();
  }

  static void Eat(const v8::FunctionCallbackInfo<v8::Value>& args) {
    args.GetReturnValue().Set(StringToV8Value(args.GetIsolate(), "cat eat!"));
  }

  static void Jump(const v8::FunctionCallbackInfo<v8::Value>& args) {
    args.GetReturnValue().Set(StringToV8Value(args.GetIsolate(), "cat jump!"));
  }
};



    std::unique_ptr<v8::Extension> dog = std::make_unique<DogExtension>("dog");
    v8::RegisterExtension(std::move(dog));

    std::unique_ptr<v8::Extension> cat = std::make_unique<CatExtension>("cat");
    v8::RegisterExtension(std::move(cat));

    const char* names[2] = {};
    names[0] = "dog";
    names[1] = "cat";
    v8::ExtensionConfiguration extension_configuration(2, names);

    v8::Local<v8::Context> context =
        v8::Context::New(isolate, &extension_configuration);

v8::Extension构造的时候需要两个参数:

  • name,扩展的名字。后续这个字符串需要放到v8::ExtensionConfiguration里面。
  • source,实现扩展的代码字符串。

注意:name 和 source 字符串的生命周期确保不比对应的v8::Extension短。

其实 source 里面的代码其实是 JavaScript 语法的超集。实现函数方法略有差异,比如:

dog.eat = function() {
  native function Eat();
  return Eat();
};

实现 dog 对象的 eat 方法,通过 native 关键字告诉 v8 该方法实际上是 C++ 实现的Eat()函数。后续 v8 在解析编译代码的时候,会回调GetNativeFunctionTemplate去映射方法名。所以那时候我们会根据GetNativeFunctionTemplate里的 name 字符串参数来返回 C++ Eat()函数绑定好的v8::FunctionTemplate对象。后续在 JavaScript 环境里调用dog.eat()实际就调用到了 C++ Eat()函数。

实现了我们的DogExtensionCatExtension类后,在创建v8::Context前,先通过v8::RegisterExtension注册到 v8 中。然后再创建一个v8::ExtensionConfiguration,设定我们需要在v8::Context里面激活的v8::Extension的名字。然后创建v8::Context的时候,把v8::ExtensionConfiguration传递进去。

最后展示在浏览器开发者工具 console 里的效果如下图所示:

v8 extension demo

控制权限

我们注入的对象都是默认的权限,实际上可以跟JavaScript对象一样,设置相应的 enumerable、configurable、writable 权限,如下代码:

persion = {};

Object.defineProperty(this, 'persion', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: persion
});

Object.defineProperty(persion, 'age', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: 20
});

Object.defineProperty(persion, 'run', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: function() {
    native function Run();
    return Run();
  }
});
  • enumerable,属性是否能够被枚举到
  • configurable,属性是否能够被删除
  • writable,属性是否能够被写入

Chromium 中使用 v8::Extension

RenderThreadImpl提供了一个RegisterExtension方法,让上层业务可以往网页的v8::Context中注入v8::Extension。最终调用到了 blink 层的ScriptController类中。ScriptController直接调用v8::RegisterExtension注册v8::Extension

在blink创建网页环境时,LocalWindowProxy::CreateContext里会调用ScriptController::ExtensionsFor构造对应的v8::ExtensionConfiguration来创建网页的v8::Context