对象封装


¥Object wrap

Node-API 为 "wrap" 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++ 类对应的构造函数、静态属性和方法以及实例属性和方法。

    ¥The napi_define_class API defines a JavaScript class with constructor, static properties and methods, and instance properties and methods that correspond to the C++ class.

  2. 当 JavaScript 代码调用构造函数时,构造函数回调使用 napi_wrap 将新的 C++ 实例封装在 JavaScript 对象中,然后返回封装对象。

    ¥When JavaScript code invokes the constructor, the constructor callback uses napi_wrap to wrap a new C++ instance in a JavaScript object, then returns the wrapper object.

  3. 当 JavaScript 代码调用类上的方法或属性访问器时,将调用相应的 napi_callback C++ 函数。对于实例回调,napi_unwrap 获取作为调用目标的 C++ 实例。

    ¥When JavaScript code invokes a method or property accessor on the class, the corresponding napi_callback C++ function is invoked. For an instance callback, napi_unwrap obtains the C++ instance that is the target of the call.

对于封装对象,可能很难区分在类原型上调用的函数和在类实例上调用的函数。用于解决此问题的一种常见模式是保存对类构造函数的持久引用,以供以后进行 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() 然后可用于确保传递到 queryHashRecords() 的实例确实是正确的类型。

¥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 结构。当这样的值与存储在 napi_valuenapi_type_tag_object() 中的 JavaScript 对象或 外部的 一起传递时,JavaScript 对象将是带有类型标记的 "marked"。"mark" 在 JavaScript 端是不可见的。当 JavaScript 对象到达原生绑定时,可以使用 napi_check_object_type_tag() 和原始类型标签来确定 JavaScript 对象之前是否是带有类型标签的 "marked"。这创建了比 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;
  }
}