上一章中提到了如何使用N-API进行Node.js Native模块的开发,介绍了使用napi_create_string_utf8方法创建一个UTF-8编码字符串并将其作为返回值返回。在本文中将继续这个话题介绍Node.js调用C/C++通过N-API实现的native方法时,获取其参数的操作。

实现目标

在本节中,将通过N-API实现一个add方法,其接受两个number类型的参数,并返回二者之和。add方法的实现等同于下列Javascript代码。

function add(a, b) {
  if (arguments.length < 2) {
    throw new TypeError('Wrong number of arguments');
  }

  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new TypeError('Wrong arguments');
  }

  return a + b;
}

实现方式

在上一章中我们已经提到过使用N-API定义方法时接受的参数为(napi_env, napi_callback_info),其中napi_callback_info为上下文的信息。我们可以通过napi_get_cb_info方法从napi_callback_info类型的参数中得到回调的参数等数据。napi_get_cb_info的函数原型如下:

napi_status napi_get_cb_info(
  napi_env env,
  napi_callback_info cbinfo, // 传入回调函数的回调信息
  size_t *argc, // 作为入参传入`argv`数组的大小,并将接收实际的参数个数
  napi_value *argv, // 存放参数的buffer
  napi_value *this_arg, // Javascript中的`this`
  void** data // 接收数据指针
);

需要注意的是,若参数的个数大于请求的数量argc,将只复制argc的值所指定数量的参数至argv中。例如在下列代码中将请求的参数个数argc的值设为0,后续调用napi_typeof时将得到napi_invalid_arg错误,原因是未复制参数至buffer中。若实际的参数个数小于请求的数量,将复制全部的参数并使用napi_value类型所表示的undefined值填充。

在了解了napi_get_cb_info方法后,我们就可以使用它来获取add方法的参数了。对于add方法,需要两个数值类型的参数,所以在调用napi_get_cb_info方法前,我们声明了size_t类型的argc变量用于存放我们需要的参数个数以及接收实际的参数个数,并声明了napi_value类型的数组argv用于存放参数的值,其长度为我们需要的参数个数的值。

size_t argc = 2;
napi_value argv[2];

status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL);

在调用napi_get_cb_info后,我们还需要对参数进行验证。在上文中Javascript实现中,我们对参数个数以及参数的类型进行了检查,下面我们就来看看如何在N-API中实现。

首先,我们需要判断实际传递的参数个数。在实际参数数量小于2个的情况下,我们需要使用napi_throw_type_error方法抛出一个TypeError错误。其代码实现如下:

if (argc < 2) {
  napi_throw_type_error(env, NULL, "Wrong number of arguments.");
  return NULL; // 不再继续执行后续代码
}

napi_throw_type_error方法将抛出一个TypeError类型的错误。该方法相当于使用throw new TypeError()语句。napi_throw_type_error的函数原型如下:

napi_status napi_throw_type_error(
  napi_env env,
  const char *code, // 可选的错误代码
  const char *msg // C语言类型的错误信息字符串
);

然后,我们需要验证传入的参数是否为我们需要的number类型。在N-API中,我们可以通过napi_typeof方法获取指定对象的类型,其函数原型如下:

napi_status napi_typeof(
  napi_env env,
  napi_value value, // 将要获取类型的Javascript值
  napi_valuetype *result // Javascript值的类型
);

在调用napi_typeof方法后,可以通过其出参result得到其napi_valuetype类型所表示的对象类型。napi_valuetype是一个用于描述napi_value所存放Javascript对象类型的枚举值,其定义为:

typedef enum {
  napi_undefined,
  napi_null,
  napi_boolean,
  napi_number,
  napi_string,
  napi_symbol,
  napi_object,
  napi_function,
  napi_external,
  napi_bigint
} napi_valuetype;

napi_valuetype对应了ECMAScript标准中定义的BooleanNullUndefinedNumberBigIntStringSymbolObject八种数据类型,以及函数对应的Function类型。另外,napi_valuetype还包括了一个napi_external类型,其表示没有任何属性也没有任何原型的对象。

napi_typeof与Javascript中的typeof操作符也有一些区别,例如当值为null时,typeof操作符将返回objectnapi_typeof将得到napi_null

对于我们上文中需要实现的验证功能,我们可以使用下面的方式实现:

napi_valuetype valuetype;

napi_typeof(env, argv[0], &valuetype);

if (valuetype != napi_number) {
  // ...
  // 处理非数值类型的情况
}

接下来,我们需要获取参数的值,并计算得到二者之和。因为从传入的参数是Javascript值类型,为了能够进行计算,我们需要获取其对应在C/C++中的类型的值。N-API提供了包括napi_get_value_int32napi_get_value_doublenapi_get_value_boolnapi_get_value_string_utf8等在内的方法以便获取不同类型的值。在add方法中传入的参数为number类型,以及为了可以处理浮点数的情况,所以在这里我们主要使用napi_get_value_double方法获取参数对应double类型的值。napi_get_value_double方法的函数原型如下:

napi_status napi_get_value_double(
  napi_env env,
  napi_value value, // 获取的Javascript值
  double *result // 用于保存对应的double类型值
);

在上文中我们已经通过napi_typeof方法获取并验证参数是否为number类型,若未进行验证直接调用napi_get_value_double且Javascript值不为number类型时,它将返回napi_number_expected状态表示错误。我们可以使用下面的代码实现获取double类型值的操作:

double value1, value2;

napi_get_value_double(env, argv[0], &value1);
napi_get_value_double(env, argv[1], &value2);

最后,我们只需要计算出两个double类型值之和,创建其对应的Javascript值并返回即可。我们使用的是double类型进行计算,所以在创建Javascript值时需要使用napi_create_double方法。napi_create_double对应的函数原型如下:

napi_status napi_create_double(
  napi_env env,
  double value, // double类型的值
  napi_value *result // 保存创建的Javascript值
);

若在计算的时候不是将其转换为double类型,N-API也提供了napi_create_uint32napi_create_int64napi_create_bigint_int64等方法,可以根据具体的需求选择不同的类型。另外,在前文中我们也提到过使用napi_create_string_utf8创建UTF-8编码的字符串。除此之外,N-API还有napi_create_objectnapi_create_array等用于创建对象、数组等类型的方法,大家可以到N-API文档中查看并选择需要的方法。

napi_value sum;

napi_create_double(env, value1 + value2, sum);

// return sum;

在完成上面的所有操作后,我们只需按照上一章所说的完成exports的定义以及模块的注册后,便可以在js代码中使用定义的add方法。

完整的add方法代码

napi_value add(napi_env env, napi_callback_info info) {
  napi_status status;

  size_t argc = 2;
  napi_value argv[2];

  status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
  assert(status == napi_ok);

  if (argc < 2) {
    napi_throw_type_error(env, NULL, "Wrong number of arguments.");
    return NULL;
  }

  napi_valuetype valueType1, valueType2;

  status = napi_typeof(env, argv[0], &valueType1);
  assert(status == napi_ok);

  status = napi_typeof(env, argv[1], &valueType2);
  assert(status == napi_ok);

  if (valueType1 != napi_number || valueType2 != napi_number) {
    napi_throw_type_error(env, NULL, "Wrong arguments");
    return NULL;
  }

  double value1, value2;

  status = napi_get_value_double(env, argv[0], &value1);
  assert(status == napi_ok);

  status = napi_get_value_double(env, argv[1], &value2);
  assert(status == napi_ok);

  napi_value sum;
  status = napi_create_double(env, value1 + value2, &sum);
  assert(status == napi_ok);

  return sum;
}

// 模块注册等

结束语

同样,我们需要按之前章节中所提到的创建构建所需的binding.gyp文件,并运行node-gyp configure build命令编译。在编译之后,在Javascript代码中引入得到的.node文件并调用定义的add方法即可:

const { add } = require('./build/Release/add.node');

console.log(add(1, 2));
// 3

在本文中,我们介绍了如何在C/C++中获取从Javascript传递来的参数,以及获取参数对应在C/C++中类型的值。在后续的文章中我们将会继续介绍如何在C/C++中调用通过参数传递的Javascript方法,向其传递参数并接收其返回值。

参考资料