简单的加法

前面的 Hello World 的例子很简单,接下来看看如何处理参数传递。我们来写个 sum 函数,也就是简单的加法。这里核心的关键就是 args 参数就是入参数组,就类似 JavaScript 里函数的 arguments 对象,我们需要做的就是把参数做一下类型转换,转成数值型求和就好了:

  1. // sum.cc
  2. #include <node.h>
  3. namespace demo {
  4. using v8::Number;
  5. using v8::Local;
  6. void Sum(const v8::FunctionCallbackInfo<v8::Value> &args) {
  7. v8::Isolate *isolate = args.GetIsolate();
  8. Local<v8::Context> context = isolate->GetCurrentContext();
  9. Local<Number> a = (args[0]->ToNumber(context)).ToLocalChecked();
  10. Local<Number> b = (args[1]->ToNumber(context)).ToLocalChecked();
  11. args.GetReturnValue().Set(a->Value() + b->Value());
  12. }
  13. void Init(v8::Local<v8::Object> exports) {
  14. NODE_SET_METHOD(exports, "sum", Sum);
  15. }
  16. // 这里的 NODE_GYP_MODULE_NAME 宏就是 前面 binding.gyp 的指定的 target_name
  17. NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
  18. } // namespace demo

这里为了方便,使用了 C++ 的 using 语法引入 v8 里的命名空间,省得重复写 v8::Local 等。
核心就是 12-16 行了,简单看一下:

  • 12 行,从 isolate 的 API 获取当前实例的上下文,后面获取数值的本地句柄方法需要用到;
  • 14 & 15 行,从 args[0]args[1] 调用 toNumber 方法将 JS 传入的参数转为 v8::Number 的数据类型。这里不熟悉 C++ 的同学可能会问,->. 是什么关系?什么时候用 -> 什么时候用 .-> 是 C++ 提供的一种简化语法,方便对指针所指向对象的成员的快捷访问,p->hello() 就等价于 (*p).hello() ,其实相当于是解引用与获取对象成员的合并操作。具体来说这个 case 里,args[0] 本身是一个 v8::Value 类型的本地句柄,也就是 v8::Local<v8::Value> 类型,调用 . 操作符时只能调用句柄本身的 API,就包括前文提到的 NewClear 等 API,用于句柄本身比如类型转换、检查是否句柄为空、清理句柄等。
    • image.png
    • 那调用 args[0]-> 时是访问什么对象的 API 呢?其实就是这个本地句柄指向的实际的对象,也就是 v8::Value 对象的成员方法了。前文我们看过,v8::Valuev8 里针对 JS 数据类型封装的绝大多数类型的超类,基于它可以进一步对参数做类型转换获得我们的场景下需要的数据类型;
    • 注意这里的 ToNumber 返回的也是一个待实本地句柄,所以也需要调用 ToLocalChecked() 方法转为本地句柄。除了 ToNumber 之外,还有很多各种转换为其他 V8 数据类型的 API,如 ToStringToBooleanToObject 等等;
    • 这样通过转换,我们就拿到了两个入参对应的数值类型的本地句柄了。
  • 最后是 16 行,将 a、b 这两个本地句柄调用 toValue() 方法(注意这里已经是 v8::Number 类的 toValue 方法了)拿到实际的 C++ double 类型的数值,求和之后设置返回值即可。

OK,我们编译一下,还是像前文所述,配置一下 binding.gyp,执行一下 build:

  1. > node-gyp rebuild
  2. CXX(target) Release/obj.target/sum/sum.o
  3. SOLINK_MODULE(target) Release/sum.node

写个简单的 JS 代码跑一下:

  1. const { sum } = require('./build/Release/sum.node');
  2. console.log(sum(1, 2));

运行一下:

  1. » node ./sum.js [14:30:39]
  2. 3

OK,这个简单的例子就写好了。

不定参数求和

接下来的一个问题是,这个函数是固定了 2 个参数,如果传过来是多个不固定的数量的数据怎么改写呢?思路也简单,必然需要用到循环遍历了:

  1. // sum.cc
  2. #include <node.h>
  3. namespace demo {
  4. using v8::Number;
  5. using v8::Local;
  6. using v8::FunctionCallbackInfo;
  7. using v8::Value;
  8. void Add(const FunctionCallbackInfo<Value> &args) {
  9. v8::Isolate *isolate = args.GetIsolate();
  10. Local<v8::Context> context = isolate->GetCurrentContext();
  11. Local<Number> a = (args[0]->ToNumber(context)).ToLocalChecked();
  12. Local<Number> b = (args[1]->ToNumber(context)).ToLocalChecked();
  13. args.GetReturnValue().Set(a->Value() + b->Value());
  14. }
  15. void Sum(const FunctionCallbackInfo<Value> &args) {
  16. v8::Isolate *isolate = args.GetIsolate();
  17. Local<v8::Context> context = isolate->GetCurrentContext();
  18. double sum = 0;
  19. for (int i = 0;i < args.Length();i++) {
  20. sum += (args[i]->ToNumber(context)).ToLocalChecked()->Value();
  21. }
  22. args.GetReturnValue().Set(sum);
  23. }
  24. void Init(v8::Local<v8::Object> exports) {
  25. NODE_SET_METHOD(exports, "add", Add);
  26. NODE_SET_METHOD(exports, "sum", Sum);
  27. }
  28. // 这里的 NODE_GYP_MODULE_NAME 宏就是 前面 binding.gyp 的指定的 target_name
  29. NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
  30. } // namespace demo

我们把之前的 Sum 函数重命名为 Add,新加一个 Sum 函数,这里唯一的知识点就是 args.Length() 可以获得入参的数量,所以写个 for 循环累加起来就好了。然后 33-34 行我们为两个函数分别定义两个导出,在 JS 侧:

  1. const { add, sum } = require('./build/Release/sum.node');
  2. console.log(add(1, 2));
  3. console.log(sum(1, 2, 3, 4));

编译后执行一下:

  1. dickeylthgithub/node-addon-playground/1-sum(main✗)» tnpm run build [14:30:41]
  2. > 0-hello-world@1.0.0 build /Users/dickeylth/dev/github/node-addon-playground/1-sum
  3. > node-gyp rebuild
  4. CXX(target) Release/obj.target/sum/sum.o
  5. SOLINK_MODULE(target) Release/sum.node
  6. dickeylthgithub/node-addon-playground/1-sum(main✗)» node ./sum.js [15:25:49]
  7. 3
  8. 10

是不是非常简单?