从嵌入V8开始(下)

从嵌入V8开始


原文链接:https://v8.dev/docs/embed
CSDN博客有字数限制,故将此篇翻译分为两部分。上部分链接:从嵌入V8开始(上)


访问器(Accessor)

访问器(accessor)是一个C++回调,用于当JavaScript脚本访问一个对象属性时计算和返回一个值。访问器可以通过对象模板来设置,使用SetAccessor方法来设置。这个方法获取关联属性的名称和两个回调函数,这两个回调函数分别在读和写相应属性时调用。

访问器的复杂性取决于操作的数据类型:

访问静态全局变量

假设有两个C++整型变量,xy。这两个变量在Context中都是暴露给JavaScript的全局变量。为了实现这一点,需要在脚本读或写这两个变量的时候调用C++访问器函数。访问器函数使用Integer::New将C++整型转换成JavaScript整型,使用Int32Value将JavaScript整型转换成C++整型。示例如下:

void XGetter(Local<String> property,
              const PropertyCallbackInfo<Value>& info) {
  info.GetReturnValue().Set(x);
}

void XSetter(Local<String> property, Local<Value> value,
             const PropertyCallbackInfo<void>& info) {
  x = value->Int32Value();
}

// YGetter/YSetter are so similar they are omitted for brevity

Local<ObjectTemplate> global_templ = ObjectTemplate::New(isolate);
global_templ->SetAccessor(String::NewFromUtf8(isolate, "x"), XGetter, XSetter);
global_templ->SetAccessor(String::NewFromUtf8(isolate, "y"), YGetter, YSetter);
Persistent<Context> context = Context::New(isolate, NULL, global_templ);

在上面的代码中,对象模板是与Context同时创建的。模板可以提前创建,并用于任意数量的Context

访问动态变量

在前面的例子中,变量是静态全局的。那么如果操作的数据是动态的情况下,比如浏览器中的DOM树,该怎么做呢?先假设xy都是C++类Point的对象域:

class Point {
 public:
  Point(int x, int y) : x_(x), y_(y) { }
  int x_, y_;
}

为了使任意数量的C++ point实例暴露给JavaScript,我们需要为每一个C++ point创建一个JavaScript对象,并且连接JavaScript对象和C++实例。例子中就是需要将外部值和内部对象域进行连接/
首先为point包装对象创建一个对象模板:

Local<ObjectTemplate> point_templ = ObjectTemplate::New(isolate);

每个JavaScript point都持有一个C++对象的引用,这个C++对象是一个内部域的包装。之所以被称为内部域,是因为他们无法被JavaScript访问,只能通过C++节点来访问。一个对象可以拥有任意数量的内部域。内部域数量的通过对象模板来设置:

point_templ->SetInternalFieldCount(1);

这里将内部域数量设置为1,这意味着这个对象拥有一个内部域,其索引为0,指向一个C++对象。
向模板中添加xy的访问器:

point_templ->SetAccessor(String::NewFromUtf8(isolate, "x"), GetPointX, SetPointX);
point_templ->SetAccessor(String::NewFromUtf8(isolate, "y"), GetPointY, SetPointY);

接下来,通过创建模板的实例来包装C++ point。然后将索引为0的内部域设置指向p的外部包装。

Point* p = ...;
Local<Object> obj = point_templ->NewInstance();
obj->SetInternalField(0, External::New(isolate, p));

这个外部对象是对void*的简单包装。外部对象只能用来保存内部域的引用。JavaScript对象不能直接拥有C++对象的引用,所以外部值被当作从JavaScript到C++的“桥梁”。从这个角度来看,外部值是句柄的“反义词”,因为句柄是让C++引用到JavaScript对象。

下面是xgetset访问器的定义。y的访问器定义只需要将x替换成y即可:

void GetPointX(Local<String> property,
               const PropertyCallbackInfo<Value>& info) {
  Local<Object> self = info.Holder();
  Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
  void* ptr = wrap->Value();
  int value = static_cast<Point*>(ptr)->x_;
  info.GetReturnValue().Set(value);
}

void SetPointX(Local<String> property, Local<Value> value,
               const PropertyCallbackInfo<void>& info) {
  Local<Object> self = info.Holder();
  Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
  void* ptr = wrap->Value();
  static_cast<Point*>(ptr)->x_ = value->Int32Value();
}

访问器提取point对象的引用。这个对象由JavaScript对象包装。之后访问器读写相应的域。通过这个方法,这些通用访问器可以被用于任意数量的被包装起来的point对象。

拦截器(Interceptor)

可以通过指定一个回调用于在脚本访问到任意对象属性时调用。这些回调被称为拦截器(interceptor)。为了效率,这里由两种类型的拦截器:

  • 名称属性拦截器(named property interceptors):当通过字符串形式的名称来访问属性时调用。
    例子:浏览器环境中,document.theFormName.elementName
  • 索引属性拦截器(indexed property interceptors):当访问索引形式的属性调用。
    例子:浏览器环境中,document.forms.elements[0]

示例process.cc中提供了V8源码,包括一个使用拦截器的例子。下面的代码片段SetNamedPropertyHandler吃定了MapGetMapSet两个拦截器:

Local<ObjectTemplate> result = ObjectTemplate::New(isolate);
result->SetNamedPropertyHandler(MapGet, MapSet);

MapGet拦截器如下:

void JsHttpRequestProcessor::MapGet(Local<String> name,
                                    const PropertyCallbackInfo<Value>& info) {
  // Fetch the map wrapped by this object.
  map<string, string> *obj = UnwrapMap(info.Holder());

  // Convert the JavaScript string to a std::string.
  string key = ObjectToString(name);

  // Look up the value if it exists using the standard STL idiom.
  map<string, string>::iterator iter = obj->find(key);

  // If the key is not present return an empty handle as signal.
  if (iter == obj->end()) return;

  // Otherwise fetch the value and wrap it in a JavaScript string.
  const string &value = (*iter).second;
  info.GetReturnValue().Set(
      String::NewFromUtf8(value.c_str(), String::kNormalString, value.length()));
}

通过拦截器,当一个属性被访问的时候,指定的回调会被调用。访问器和拦截器的区别在于拦截器处理所有的属性,而访问器只与一个指定的属性关联。

安全模型

“同源策略(same-origin policy)”保证了一个document或者脚本只能获取和修改同一个“源头”的document的属性。“源(origin)”是由域名(如:www.example.com)、协议(如:https)和端口这三者组合决定的。举个例子,www.example.com:81www.example.com不是同源。这三者必须全部匹配的两个页面才会被认为是同源。如果没有这层保护,那么一个恶意的网页有可能会对另一个网页的完整性造成影响。

在V8中,“源”则被定义为Context。默认情况下,跨上下文的访问时被禁止的。如果需要跨上下文访问,则需要使用安全口令或者安全回调。安全口令可以是任何值,不过通常情况下是一个标记,一个在其他地方不会出现的字符串。你可以在建立Context的时候通过SetSecurrityToken来设置安全口令。如果没有指定安全口令,那么V8会自动的为Context生成一个。

当试图访问一个全局变量时,V8安全系统会首先检查全局对象的安全口令与访问操作代码的安全口令是否匹配。如果口令匹配,那么访问则被允许。如果不匹配,那么V8则会执行一个回调,检查访问是否应该被允许。你可以通过设置安全回调来来指定访问一个对象是否被允许。安全回调使用对象模板的SetAccessCheckCallbacks方法来设置。V8安全系统获取被访问对象的安全回调,并调用它来查询其他Context是否由访问权限。需要访问的对象、被访问的属性名、访问的类型(如读取、写入、删除等)这些信息会传递给回调方法,返回的结果确定是否能够允许访问。

Google的Chrome实现了这个机制。这样如果安全口令没有匹配上,那么指定的回调会只允许访问如下:window.focus()window.blur()window.close()window.locationwindow.open()history.forward()history.back()history.go()

异常(Exception)

当出现错误的时候,V8会抛出异常——比如脚本或函数试图读取一个不存在的属性时,或者通过调用函数的方式调用了一个非函数。

在操作不成功时,V8会返回一个空句柄。因此,在仅需执行之前检查代码的返回值是否时空句柄是非常重要的。使用Local类的共有成员函数IsEmpty()可以检查一个句柄是否为空。
可以通过TryCatch来捕获异常,如下:

TryCatch trycatch(isolate);
Local<Value> v = script->Run();
if (v.IsEmpty()) {
  Local<Value> exception = trycatch.Exception();
  String::Utf8Value exception_str(exception);
  printf("Exception: %s\n", *exception_str);
  // ...
}

如果返回值为空句柄,且没有TryCatch,那么代码肯定就会出错。如果你使用了TryCatch,那么异常就可以被捕获,代码也可以继续执行了。

继承

JavaScript是一种没有类的面向对象语言,它使用原型继承而不是类继承。这会对很多习惯C++和Java的程序员产生困扰。

像C++和Java这样的基于类的面向对象语言是在类和实例两个区分的概念基础上建立的。而JavaScript是基于原型的语言,所以没有这种区分。它仅仅只有对象。JavaScript并不天生支持类继承的声明。不过,JavaScript的原型机制简化了向所有对象的实例中添加自定义属性和方法的过程。在JavaScript中,可以向对象中添加属性。例如:

// Create an object named `bicycle`.
function bicycle() {}
// Create an instance of `bicycle` called `roadbike`.
var roadbike = new bicycle();
// Define a custom property, `wheels`, on `roadbike`.
roadbike.wheels = 2;

通过这种方式添加的自定义属性只会在存在于这个实例中。如果创建了另一个实例bicycle(),假设名字较mountainbike,那么mountainbike.wheels则会返回undefined,除非wheels属性被明确的添加。

有时候的确需要对一个对象的所有实例中都添加自定义属性,比如wheels应该是所有bicycle都该有的属性。这是JavaScript的对象原型就非常有用了。通过对象的prototype来添加自定义属性,如下:

// First, create the “bicycle” object
function bicycle() {}
// Assign the wheels property to the object’s prototype
bicycle.prototype.wheels = 2;

这样,所有的bicycle()实例都拥有了wheels属性。

在V8中利用模板可以使用相同的方法。每个FunctionTemplate都有一个PrototypeTemplate方法,这个方法提供了这个函数模板的原型模板。你可以在PrototypeTemplate上设置属性,并将C++函数关联到这些属性上。这样相应的FunctionTemplate的所有实例都有这些属性。例如:

Local<FunctionTemplate> biketemplate = FunctionTemplate::New(isolate);
biketemplate->PrototypeTemplate().Set(
    String::NewFromUtf8(isolate, "wheels"),
    FunctionTemplate::New(isolate, MyWheelsMethodCallback)->GetFunction()
);

这会使所有的biketemplate的实例在原型链上都拥有wheels方法。当调用该方法时,会引起C++函数MyWheelsMethodCallback被调用。

V8的FunctionTemplate类提供了公共成员函数Inherit()。可以调用这个函数来使函数模板继承另外一个函数模板,如下:

void Inherit(Local<FunctionTemplate> parent);
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值