对象封装


【Object wrap】

Node-API 提供了一种方式来“封装”C++类及其实例,以便可以从 JavaScript 调用类的构造函数和方法。

【Node-API offers a way to "wrap" C++ classes and instances so that the class constructor and methods can be called from JavaScript.】

  1. napi_define_class API 定义了一个 JavaScript 类,该类包含构造函数、静态属性和方法,以及与 C++ 类对应的实例属性和方法。
  2. 当 JavaScript 代码调用构造函数时,构造函数回调使用 napi_wrap 将新的 C++ 实例封装在 JavaScript 对象中,然后返回该封装对象。
  3. 当 JavaScript 代码调用类的方法或属性访问器时,会调用相应的 napi_callback C++ 函数。对于实例回调,napi_unwrap 获取作为调用目标的 C++ 实例。

对于封装的对象,区分在类原型上调用的函数和在类实例上调用的函数可能很困难。解决这个问题的常用模式是保存对类构造函数的持久引用,以便以后进行 instanceof 检查。

【For wrapped objects it may be difficult to distinguish between a function called on a class prototype and a function called on an instance of a class. A common pattern used to address this problem is to save a persistent reference to the class constructor for later instanceof checks.】

napi_value MyClass_constructor = NULL;
status = napi_get_reference_value(env, MyClass::es_constructor, &MyClass_constructor);
assert(napi_ok == status);
bool is_instance = false;
status = napi_instanceof(env, es_this, MyClass_constructor, &is_instance);
assert(napi_ok == status);
if (is_instance) {
  // napi_unwrap() ...
} else {
  // otherwise...
} 

一旦不再需要引用,就必须将其释放。

【The reference must be freed once it is no longer needed.】

有时 napi_instanceof() 无法确保某个 JavaScript 对象是特定原生类型的封装对象。尤其是当封装的 JavaScript 对象通过静态方法而不是作为原型方法的 this 值传回到插件时,就会出现这种情况。在这种情况下,它们可能会被错误地解包。

【There are occasions where napi_instanceof() is insufficient for ensuring that a JavaScript object is a wrapper for a certain native type. This is the case especially when wrapped JavaScript objects are passed back into the addon via static methods rather than as the this value of prototype methods. In such cases there is a chance that they may be unwrapped incorrectly.】

const myAddon = require('./build/Release/my_addon.node');

// `openDatabase()` returns a JavaScript object that wraps a native database
// handle.
const dbHandle = myAddon.openDatabase();

// `query()` returns a JavaScript object that wraps a native query handle.
const queryHandle = myAddon.query(dbHandle, 'Gimme ALL the things!');

// There is an accidental error in the line below. The first parameter to
// `myAddon.queryHasRecords()` should be the database handle (`dbHandle`), not
// the query handle (`query`), so the correct condition for the while-loop
// should be
//
// myAddon.queryHasRecords(dbHandle, queryHandle)
//
while (myAddon.queryHasRecords(queryHandle, dbHandle)) {
  // retrieve records
} 

在上面的例子中,myAddon.queryHasRecords() 是一个接受两个参数的方法。第一个参数是数据库句柄,第二个参数是查询句柄。在内部,它会解开第一个参数并将得到的指针转换为本地数据库句柄。然后,它会解开第二个参数并将得到的指针转换为查询句柄。如果参数顺序错误,类型转换仍然可以进行,但很有可能底层的数据库操作会失败,甚至可能导致非法内存访问。

【In the above example myAddon.queryHasRecords() is a method that accepts two arguments. The first is a database handle and the second is a query handle. Internally, it unwraps the first argument and casts the resulting pointer to a native database handle. It then unwraps the second argument and casts the resulting pointer to a query handle. If the arguments are passed in the wrong order, the casts will work, however, there is a good chance that the underlying database operation will fail, or will even cause an invalid memory access.】

为了确保从第一个参数获取的指针确实是指向数据库句柄的指针,以及从第二个参数获取的指针确实是指向查询句柄的指针,queryHasRecords() 的实现必须进行类型验证。在 napi_ref 中保留用于实例化数据库句柄的 JavaScript 类构造函数以及用于实例化查询句柄的构造函数会有所帮助,因为这样可以使用 napi_instanceof() 来确保传递给 queryHasRecords() 的实例确实是正确的类型。

【To ensure that the pointer retrieved from the first argument is indeed a pointer to a database handle and, similarly, that the pointer retrieved from the second argument is indeed a pointer to a query handle, the implementation of queryHasRecords() has to perform a type validation. Retaining the JavaScript class constructor from which the database handle was instantiated and the constructor from which the query handle was instantiated in napi_refs can help, because napi_instanceof() can then be used to ensure that the instances passed into queryHashRecords() are indeed of the correct type.】

不幸的是,napi_instanceof() 并不能防止原型被篡改。例如,数据库句柄实例的原型可以被设置为查询句柄实例构造函数的原型。在这种情况下,数据库句柄实例可能会表现得像一个查询句柄实例,并且它将通过针对查询句柄实例的 napi_instanceof() 检测,但仍然包含指向数据库句柄的指针。

【Unfortunately, napi_instanceof() does not protect against prototype manipulation. For example, the prototype of the database handle instance can be set to the prototype of the constructor for query handle instances. In this case, the database handle instance can appear as a query handle instance, and it will pass the napi_instanceof() test for a query handle instance, while still containing a pointer to a database handle.】

为此,Node-API 提供了类型标记功能。

【To this end, Node-API provides type-tagging capabilities.】

类型标记是一个对于插件唯一的 128 位整数。Node-API 提供了 napi_type_tag 结构用于存储类型标记。当将该值与 JavaScript 对象或存储在 napi_value 中的 外部 一起传递给 napi_type_tag_object() 时,JavaScript 对象将被“标记”类型标记。这种“标记”在 JavaScript 端是不可见的。当一个 JavaScript 对象传入本地绑定时,可以使用 napi_check_object_type_tag() 结合原始类型标记来判断该 JavaScript 对象是否之前已经被“标记”了类型标记。这就创建了一种比 napi_instanceof() 更高精度的类型检查能力,因为这种类型标记可以在原型操作和插件卸载/重载后仍然保持。

【A type tag is a 128-bit integer unique to the addon. Node-API provides the napi_type_tag structure for storing a type tag. When such a value is passed along with a JavaScript object or external stored in a napi_value to napi_type_tag_object(), the JavaScript object will be "marked" with the type tag. The "mark" is invisible on the JavaScript side. When a JavaScript object arrives into a native binding, napi_check_object_type_tag() can be used along with the original type tag to determine whether the JavaScript object was previously "marked" with the type tag. This creates a type-checking capability of a higher fidelity than napi_instanceof() can provide, because such type- tagging survives prototype manipulation and addon unloading/reloading.】

继续上述示例,下面的基础插件实现演示了如何使用 napi_type_tag_object()napi_check_object_type_tag()

【Continuing the above example, the following skeleton addon implementation illustrates the use of napi_type_tag_object() and napi_check_object_type_tag().】

// This value is the type tag for a database handle. The command
//
//   uuidgen | sed -r -e 's/-//g' -e 's/(.{16})(.*)/0x\1, 0x\2/'
//
// can be used to obtain the two values with which to initialize the structure.
static const napi_type_tag DatabaseHandleTypeTag = {
  0x1edf75a38336451d, 0xa5ed9ce2e4c00c38
};

// This value is the type tag for a query handle.
static const napi_type_tag QueryHandleTypeTag = {
  0x9c73317f9fad44a3, 0x93c3920bf3b0ad6a
};

static napi_value
openDatabase(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value result;

  // Perform the underlying action which results in a database handle.
  DatabaseHandle* dbHandle = open_database();

  // Create a new, empty JS object.
  status = napi_create_object(env, &result);
  if (status != napi_ok) return NULL;

  // Tag the object to indicate that it holds a pointer to a `DatabaseHandle`.
  status = napi_type_tag_object(env, result, &DatabaseHandleTypeTag);
  if (status != napi_ok) return NULL;

  // Store the pointer to the `DatabaseHandle` structure inside the JS object.
  status = napi_wrap(env, result, dbHandle, NULL, NULL, NULL);
  if (status != napi_ok) return NULL;

  return result;
}

// Later when we receive a JavaScript object purporting to be a database handle
// we can use `napi_check_object_type_tag()` to ensure that it is indeed such a
// handle.

static napi_value
query(napi_env env, napi_callback_info info) {
  napi_status status;
  size_t argc = 2;
  napi_value argv[2];
  bool is_db_handle;

  status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
  if (status != napi_ok) return NULL;

  // Check that the object passed as the first parameter has the previously
  // applied tag.
  status = napi_check_object_type_tag(env,
                                      argv[0],
                                      &DatabaseHandleTypeTag,
                                      &is_db_handle);
  if (status != napi_ok) return NULL;

  // Throw a `TypeError` if it doesn't.
  if (!is_db_handle) {
    // Throw a TypeError.
    return NULL;
  }
}