利用C/C++编写node.js原生模块的方法教程
前言
一直想了解一下使用c/c++编写nodejs原生模块,从网上找到的博客,大多都停留在如何搭建环境,然后一个hello world完事。连更多的参考资料也没有。于是就自己整理了一下,分享于此。
至于准备环境什么的,网上一抓一大把,就不再详述 。
主要参考两个地方:
其中第一个是nodejs的官方文档,里面介绍了几个不错的参考例子。
第二个是v8引擎的文档,c++的,编写c++模块主要看这个文档。
好了,我们开始几个例子,逐步的了解如何使用c++编写nodejs模块。
hello world
不能免俗,第一个先上来写个hello world吧,毕竟程序员认识的第一个程序就是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)
好了,这是最简单的一个helloworld,我们将文件命名为addon.cc,我们使用node-gyp编译一下,然后在我们的js文件中直接使用require引入模块,然后就可以调用了。
const myaddon = require('./build/release/addon') ; console.log(myaddon.hello());
如无意外,将会在终端打印hello world!。
我们简单来看一下代码,第一行#include <node.h>
是c++中引入node.h头文件的代码。头文件可理解为接口,我们在里面只定义了接口方法,并未实现,然后通过其他文件实现,c++链接器负责将这两个链接在一起。
然后定义了一个方法hello()
,没有返回值。方法参数通过const v8::functioncallbackinfo<v8::value> &args
传递,注意,这里我们加了v8::前缀注解,也可以直接在文件开始使用using v8;
这样就可以不用每次都使用这个注解了。
v8::isolate *isolate = args.getisolate();
这里,我们在函数中访问了javascript的作用域。
auto message = v8::string::newfromutf8(isolate, "hello world!");
我们创建了一个字符串类型的变量,赋值hello world!并将其绑定到作用域。
我们通过args.getreturnvalue()
获取了我们函数的返回值。
initialize()方法用于初始化模块方法,将方法和要导出的模块的方法名进行绑定。
最后node_module导出这个模块。
上面这个例子很简单,如果是js代码的话:
'use strict'; let hello = function hello() { let message = "hello world!"; return message; }; module.exports = { hello:hello };
好了,第一个helloworld就结束了。网上很多介绍nodejs c++模块的博客文章,到这里就结束了。看完之后,一脸懵逼,啥啊这是?我想再写个传参数,并对参数做简单操作的方法该怎么写?
sum(a,b)
好吧。那我们就再写一个sum(a,b)
函数,传递两个数字类型参数a,b,并求两个参数的和返回。
js中代码简单到下:
let sum = function(a,b){ if(typeof a == 'number' && typeof b == 'number'){ return a + b; }else{ throw new error('参数类型错误'); } }
那么,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()); }
首先判断两个参数是否是number类型,如果不是,直接抛出异常。如果是,则将返回值设置为两个参数的和。
这里我们并没有在参数列表中,直接使用a,b作为参数,而是直接使用 args 对象。 这和js是类似的,第一个参数是 args[0]
,第二个参数是 args[1]
。
调用isnumber()
来判断是否是数字类型。如果不是,抛出一个typeerror类型错误异常。
如果类型没问题,使用args[0]->numbervalue()
获取参数的数字值,然后相加,赋值给返回值。
可能你会问,args[0]
这是个啥?它的isnumber()
方法又是怎么来的?哪里有文档可以查阅呢?
这里其实是v8引擎内部类型,基本和js的内置对象是一一对应的。可以查阅v8类型说明文档。
上面这个图是不是很熟悉,和js的类型系统特别像。
js的array,date,function,string等等都是继承自object,而v8引擎内部,object和primitive都是继承自value类型。
这里的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等等。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。