Home > Article > Web Front-end > Detailed explanation of writing node.js native module in C/C++
This article mainly introduces you to the relevant information about using C/C++ to write node.js native modules. The steps of implementation are introduced in detail step by step. It has certain reference and learning value for everyone. It is needed Friends, let’s follow the editor and take a look.
Preface
I have always wanted to know how to use C/C++ to write nodejs native modules. Most of the blogs I found on the Internet were stuck on how to build them. environment, and then a Hello World is done. There aren't even any more reference materials. So I sorted it out myself and shared it here.
As for preparing the environment, there are a lot of them online, so I won’t go into details.
Mainly refer to two places:
nodejs official documentation
v8Document
The first one is the official document of nodejs, which introduces several good reference examples.
The second one is the documentation of the v8 engine, which is for C++. When writing a C++ module, you should mainly read this document.
Okay, let's start with a few examples and step by step understand how to use c++ to write nodejs modules.
Hello World
It cannot be avoided. Let’s write Hello World first. After all, programmers know it first. The first program is Hello World.
#include <node.h> void hello(const v8::FunctionCallbackInfo<v8::Value> &args) { v8::Isolate *isolate = args.GetIsolate(); auto message = v8::String::NewFromUtf8(isolate, "Hello World!"); args.GetReturnValue().Set(message); } void Initialize(v8::Local<v8::Object> exports) { NODE_SET_METHOD(exports, "hello", hello); } NODE_MODULE(module_name, Initialize)
Okay, this is the simplest HelloWorld. We name the file addon.cc, we use node-gyp to compile it, and then in our js file Directly use require to introduce the module, and then you can call it.
const myAddon = require('./build/Release/addon') ; console.log(myAddon.hello());
If nothing unexpected happens, Hello World! will be printed on the terminal.
Let’s take a brief look at the code. The first line #include 7f7d395e5ec8b32ec491af5a6adcd1ae
is the code that introduces the node.h header file in C++. The header file can be understood as an interface. We only define the interface methods in it, but do not implement them. They are then implemented through other files. The C++ linker is responsible for linking the two together.
Then a method hello()
is defined, which has no return value. Method parameters are passed through const v8::FunctionCallbackInfoefcceec664dd4e974e6278d25693ab6b &args
. Note that we have added the v8:: prefix annotation here. You can also use it directly at the beginning of the file using v8;
This way you don’t have to use this annotation every time.
v8::Isolate *isolate = args.GetIsolate();
Here, we access the scope of javascript in the function.
auto message = v8::String::NewFromUtf8(isolate, "Hello World!");
We created a string type variable, assigned the value Hello World! and Bind it to the scope.
We obtain the return value of our
function through args.GetReturnValue().
The Initialize() method is used to initialize the module method and bind the method to the method name of the module to be exported.
Finally NODE_MODULE exports this module.
The above example is very simple, if it is js code:
'use strict'; let hello = function hello() { let message = "Hello World!"; return message; }; module.exports = { hello:hello };
Okay, the first HelloWorld is over. There are many blog articles on the Internet introducing the nodejs C++ module, but they end here. After reading it, I was confused, what is this? I want to write another method to pass parameters and perform simple operations on the parameters. How should I write it?
sum(a,b)
Okay. Then we will write another sum(a,b)
function, pass two numeric type parameters a, b, and find the sum of the two parameters and return it. The code in
js is as simple as the following:
let sum = function(a,b){ if(typeof a == 'number' && typeof b == 'number'){ return a + b; }else{ throw new Error('参数类型错误'); } }
So, how to write C++:
void sum(const FunctionCallbackInfo<Value> &args) { Isolate *isolate = args.GetIsolate(); if(!args[0]->IsNumber()){ isolate->ThrowException(v8::Exception::TypeError( v8::String::NewFromUtf8(isolate, "args[0] must be a number"))); return ; } if(!args[1]->IsNumber()){ isolate->ThrowException(v8::Exception::TypeError( v8::String::NewFromUtf8(isolate, "args[1] must be a number"))); return ; } args.GetReturnValue().Set(args[0]->NumberValue() + args[1]->NumberValue()); }
First determine whether the two parameters are of type Number. If not, directly throw an exception. If so, the return value is set to the sum of the two parameters.
Here we do not directly use a and b as parameters in the parameter list, but directly use the args object. This is similar to js. The first parameter is args[0]
, and the second parameter is args[1]
.
Call IsNumber()
to determine whether it is a numeric type. If not, throw a TypeError exception.
If there is no problem with the type, use args[0]->NumberValue()
to get the numerical value of the parameter, then add it and assign it to the return value.
You may ask, args[0]
What is this? Where does its IsNumber()
method come from? Where can I find documentation?
This is actually the internal type of the v8 engine, which basically corresponds to the built-in objects of js one-to-one. You can check the v8 type documentation.
#Is the above picture very familiar? It is very similar to the js type system.
JS’s Array, Date, Function, String, etc. all inherit from Object, and inside the v8 engine, Object and Primitive all inherit from the Value type.
这里的IsNumber()
方法就是Value类型的方法。那么除了这个方法,还有什么方法呢?
上面这张图,我只是截了一小部分,全部的可以直接去查阅文档。看,这里有各种方法,判断是否是数字类型的IsNumber()
,判断是否是日期类型的IsDate()
,判断是否是数组的IsArray()
方法等等。
v8的接口实现的也很完善了,即使并不精通C++的开发者也可以照猫画虎的实现个简单的模块。
args[0]->NumberValue()
返回的是一个double的值,是的,这里是实打实的C++里的double类型,可以直接进行加减运算的。类似的还有BooleanValue()
方法等等,都是获取不同类型的值使用的方法。
第二个例子中,我们简单实现了一个sum()
方法,传递两个参数,求和。但是这里涉及到的只是整型的值,那如果有其他类型的值怎么办呢?比如数组。
sumOfArray(array)
下面将方法升级一下,传递一个数组,然后求数组中所有值的和。js的话:
let sumOfArray = function(array){ if(!Array.isArray(array)){ throw new Error('参数错误,必须为Array类型'); } let sum = 0; for(let item of array){ sum += item; } return sum; }
逻辑很简单,就是将传过来的数组进行遍历一遍,然后将所有项累加即可。C++也是如此:
void sumOfArray(const FunctionCallbackInfo<Value> &args){ Isolate *isolate = args.GetIsolate(); if(!args[0]->IsArray()){ isolate->ThrowException(v8::Exception::TypeError( v8::String::NewFromUtf8(isolate, "args[0] must be an Array"))); return ; } Local<Object> received_v8_obj = args[0]->ToObject(); Local<Array> keys = received_v8_obj->GetOwnPropertyNames(); int length = keys->Length(); double sum = 0; for(int i=0;i<length;i++){ sum += received_v8_obj->Get(keys->Get(i))->NumberValue(); } args.GetReturnValue().Set(sum); }
先判断是否是数组,没什么问题。
然后我们定义了一个Object类型的received_v8_obj属性,将其赋值为args[0]->ToObject()
。这里调用ToObject()方法将其转换为一个对象。
然后调用这个对象的GetOwnPropertyNames()
方法获取所有的键,然后根据键获取对象的值,进行累加。
为什么不直接将其转换为数组,然后进行遍历呢?
我们都知道,js中的数组并不是真正的数组,其实质还是对象。其内部都是键值对存储的。因此这里也是一样,Value类型并不提供直接转换为数组的ToArray()
方法,而是将其转换为Object对象,通过对象的形式进行操作。
那么对象有哪些操作呢,看文档。
但是你会发现,v8确实有个Array类,继承自Object类。那么Array有什么方法呢?
看文档就知道了,少的可怜:
所以,对数组的操作都将转换为对象操作。
createObj()
说到对象了,那么我们就来写一个创建对象的方法。传递两个参数,一个name,一个age,创建一个对象,表示一个人,名叫啥,多大年纪。
void createObj(const FunctionCallbackInfo<Value> &args){ Isolate *isolate = args.GetIsolate(); Local<Object> obj = Object::New(isolate); obj->Set(String::NewFromUtf8(isolate,"name"),args[0]->ToString()); obj->Set(String::NewFromUtf8(isolate,"age"),args[1]->ToNumber()); args.GetReturnValue().Set(obj); }
这个方法,参照文档,基本没啥可说的。
通过Object::New(isolate)
创建一个对象,然后设置两个属性name,age,将参数依次赋值给这两个属性,然后返回这个对象即可。
如果用js写:
let createObj = function(name,age){ let obj = {}; obj.name = name; obj.age = age; return obj; };
callback
上面说的,都没提到js中一个重要的东西,回调函数。如果参数中传一个回调函数,那么我们该如何执行呢?
来一个简单的例子。
let cb = function(a,b,fn){ if(typeof a !== 'number' || typeof b !== 'number'){ throw new Error('参数类型错误,只能是Number类型'); } if(typeof fn !== 'function'){ throw new Error('参数fn类型错误,只能是Function类型'); } fn(a,b); };
这个例子很简单,我们传两个数字类型参数a,b和一个回调函数fn,然后将a,b作为fn的参数调用fn回调函数。这里我们对a,b的操作转交给回调函数。回调函数里我们可以求和,也可以求积,随你。
这个例子中,暂时还没涉及到的是如何调用回调函数。
先上代码:
void cb(const FunctionCallbackInfo<Value> &args){ Isolate *isolate = args.GetIsolate(); if(!args[0]->IsNumber()){ isolate->ThrowException(v8::Exception::TypeError( v8::String::NewFromUtf8(isolate, "args[0] must be a Number"))); } if(!args[1]->IsNumber()){ isolate->ThrowException(v8::Exception::TypeError( v8::String::NewFromUtf8(isolate, "args[1] must be a Number"))); } if(!args[2]->IsFunction()){ isolate->ThrowException(v8::Exception::TypeError( v8::String::NewFromUtf8(isolate, "args[2] must be a Function"))); } Local<Function> jsfn = Local<Function>::Cast(args[2]); Local<Value> argv[2] = { Number::New(isolate,args[0]->NumberValue()),Number::New(isolate,args[1]->NumberValue())}; Local<Value> c = jsfn->Call(Null(isolate),2,argv); args.GetReturnValue().Set(c); }
上面三个判断参数类型,略过。
我们定义一个Function类型属性jsfn,将args[2]
强制转换为Function并赋值给jsfn。
然后定义一个具有两个值的参数argv,这两个值就是args[0]
, args[1]
的数字值。
然后通过jsfn->Call(Null(isolate),2,argv)
调用回调函数。
argv是一个数组,其个数我们在定义时指定,2个。
Call()
方法为函数类型的值进行调用的方法。
Local< Value > | Call (Handle< Value > recv, int argc, Handle< Value > argv[])
查阅文档,可以看出,Call()方法传3个参数,第一个参数是执行上下文,用于绑定代码执行时的this,第二个参数为参数个数,第三个为参数列表,数组形式。
上面几个例子,只是冰山一角,连一角都算不上。只为了解一下nodejs使用C/C++编写原生模块,如果要编写一个可用的,高性能的C模块,那么,要求程序员一定要精通C/C++,并且对js底层也很精通,包括v8和libuv等等。
The above is the detailed content of Detailed explanation of writing node.js native module in C/C++. For more information, please follow other related articles on the PHP Chinese website!