v8 extension 机制
发布于 2021-09-17
背景说明
V8 JavaScript 引擎支持一种v8::Extension
的机制,可以让 C++ 往 JavaScript 运行环境注入创建新的 JavaScript 对象。
v8::Extension
相比使用v8::ObjectTemplate
来注入创建新的 JavaScript 对象,更加简单直接。
使用 v8::Extension
假如我们要为 JavaScript 创建一个 Demo 对象:
- 创建一个 DemoExtension 类,继承自
v8::Extension
。在 DemoExtension 类中实现对象的具体内容。 - 通过
v8::RegisterExtension
函数把我们的 DemoExtension 类实例注册到 V8 中。 - 创建一个
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()
函数。
实现了我们的DogExtension
、CatExtension
类后,在创建v8::Context
前,先通过v8::RegisterExtension
注册到 v8 中。然后再创建一个v8::ExtensionConfiguration
,设定我们需要在v8::Context
里面激活的v8::Extension
的名字。然后创建v8::Context
的时候,把v8::ExtensionConfiguration
传递进去。
最后展示在浏览器开发者工具 console 里的效果如下图所示:
控制权限
我们注入的对象都是默认的权限,实际上可以跟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
。