当前位置: 首页 > 后端技术 > Node.js

如何写一个简单的node.jsc++扩展

时间:2023-04-03 17:20:04 Node.js

node是用c++写的,node的核心模块也是用c++代码实现的,所以node也开放给用户写c++扩展来实现一些操作窗口。如果你对require函数的描述还有印象,你会记得如果不写文件后缀,它有一个特定的匹配规则:LOAD_AS_FILE(X)1。如果X是文件,则加载X作为其文件扩展名格式.STOP2。如果X.js是文件,则将X.js作为JavaScript文本加载。STOP3。如果X.json是文件,则将X.json解析为JavaScript对象。第四步。如果X.node是一个文件,加载X.node作为二进制插件。STOP可以看到,最后会匹配到一个.node,下面的描述也说明该后缀的文件是二进制资源。而这个.node文件一般会是我们编译好的C++扩展。为什么写c++扩展可以简单理解为,如果你想写一些代码,基于node做一些事情,那么有几种选择:写一段JS代码,然后require执行写一段c++代码,编译完成后需要执行打开node源码,写自己想要的代码,然后重新编译。其实日常开发只需要第一项就够了。我们用自己熟悉的语言,写一段熟悉的代码,然后发布到npm等平台上。其他有相同需求的人可以下载我们上传的包,然后在TA的项目中使用。但是有的时候,单纯的写JS可能并不能满足我们的需求,可能是工期跟不上,可能是执行效率不理想,也可能是语言限制。所以我们会直接写一些c++代码来创建一个c++扩展供node加载和执行。而且,如果已经有C++版本的轮子,我们用扩展的方式调用执行,而不是从头实现一套,这也是避免重新造轮子的一种方式。举个简单的例子,如果你接触过webpack,用过sass,那么在安装过程中可能会遇到各种报错,可能会看到关键字gyp。其实原因是sass有使用一些c++扩展来辅助完成一些操作,而gyp是编译c++扩展的工具。https://github.com/sass/node-sass当然还有上面说的第三种操作方式,我们可以直接修改node源码,但是如果只是想写一些原生的js,实现是不行的这么好的模块,那就不用修改源码了。毕竟修改后需要编译。如果其他人需要使用你的逻辑,他们需要安装你编译的特殊版本。这种操作不容易传播。如果要使用sass,需要安装一个sass版本的node。就像下载专门的优酷看星战一样——。简单总结一下,写C++的扩展有几个好处:可以复用node的模块管理机制,执行效率比JS更高效,C++版本的轮子更多可以使用如何写一个简单的扩展呢node出来已经11年了。通过早期资料、博客等各种信息渠道可以看出,之前开发一个C++扩展并不容易。努力吧,我们写一个c++扩展还是比较容易的。这里开门见山,今天放出一个关键的工具:node-addon-api模块,下面是官方提供的各种简单的demo,让大家知道它是个什么样的工具:node-addon-examples一点到注意的是,demo目录会分成三个子目录,readme中也有写,是三种不同的c++扩展的写法(基于不同的工具)。我们这次介绍的是在node-addon-api目录下,是三者中最容易使用的。首先是我们比较熟悉的package.json文件。我们需要依赖两个组件来完成开发,分别是bindings和node-addon-api。然后我们还需要简单了解一下gyp的用法,因为编译一个c++扩展需要用到它。就像helloworld例子中的binding.gyp文件例子:{"targets":[{//导出的文件名"target_name":"hello",//编译标志的定义禁用了异常机制(注意感叹号表示排除过滤)"cflags!":["-fno-exceptions"],//c++编译标志的定义禁用异常机制(注意感叹号表示排除过滤,即c++编译器会removetheflag)"cflags_cc!":["-fno-exceptions"],//源代码入口文件"sources":["hello.cc"],//源代码包含目录"include_dirs":[//这里表示一段shell操作,用于获取node-addon-api的一些参数,有兴趣的老手可以使用node-p"require('node-addon-api').include"查看效果"//DefineAddfunctionNapi::ValueAdd(constNapi::CallbackInfo&info){Napi::Envenv=info.Env();//接收第一个参数doublearg0=info[0].As().DoubleValue();//接收第二个参数doublearg1=info[1].As().DoubleValue();//添加两个参数并返回Napi::Numbernum=Napi::Number::New(env,arg0+arg1);returnnum;}//注册我们的函数、对象等的入口函数Napi::ObjectInit(Napi::Envenv,Napi::Objectexports){//挂载一个名为add的函数到exportsexports.Set(Napi::String::New(env,"add"),Napi::Function::New(env,Add));returnexports;}//固定宏使用NODE_API_MODULE(addon,Init)来使用node-是时候使用gyp了。建议全局安装node-gyp,避免项目中有多个node_modules目录时使用npx时出现一些不可预知的问题:>npmi-gnode-gyp#generatebuildfiles>node-gypconfigure#Build>node-gypbuild这个时候你会发现在项目目录下生成了一个名为add.node的文件,就是我们在binding.gyp中设置的target_name的值。最后,我们需要写一段JS代码来调用生成的.node文件:const{add}=require('bindings')('add.node')console.log(add(1,2))//3console.log(add(0.1,0.2))//熟悉的0.3XXXXX实现了一个函数柯里化。接下来,我们玩得开心,实现一道前端高频测试题,如何实现一个函数柯里化,定义如下:add(1)(2)(3)//=>6add(1,2,3)//=>6源码位置:https://github.com/Jiasm/node...一些我们会用到的技术要点:如何在c++函数中返回一个函数供JS调用如何制作返回值既支持函数调用又支持值操作如何处理参数个数不固定的数组)不再描述binding.gyp和package.json的配置,直接上c++代码:#include//用于覆盖valueOfGetValue(constNapi::CallbackInfo&info)实现的函数Napi::Value){Napi::Envenv=info.Env();//获取我们在创建valueOf函数时传入的结果double*storageData=reinterpret_cast(info.Data());//避免空指针Caseif(storageData==NULL){returnNapi::Number::New(env,0);}else{returnNapi::Number::New(env,*storageData);}}Napi::FunctionCurryAdd(constNapi::CallbackInfo&info){Napi::Envenv=info.Env();//获取下面创建curryAdd函数时传入的结果double*storageData=reinterpret_cast(info.Data());双*结果=新的双;//遍历传入的所有参数longlen,index;对于(len=info.Length(),index=0;index().DoubleValue();*结果+=参数;}//用于多次计算if(storageData!=NULL){*result+=*storageData;}//为函数的返回值创建一个新函数Napi::Functionfn=Napi::Function::New(env,CurryAdd,"curryAdd",result);//修改valueOf方法输出结果fn.Set("valueOf",Napi::Function::New(env,GetValue,"valueOf",result));returnfn;}Napi::ObjectInit(Napi::Envenv,Napi::Objectexports){Napi::Functionfn=Napi::Function::New(env,CurryAdd,"curryAdd");出口。设置(Napi::String::New(env,"curryAdd"),fn);returnexports;}NODE_API_MODULE(curryadd,Init)编译完成后,写一段简单的JS代码调用校验结果:const{curryAdd}=require('bindings')('curry-add');constfn=curryAdd(1,2,3);常量fn2=fn(4);console.log(fn.valueOf())//=>6console.log(fn2.valueOf())//=>10console.log(fn2(5).valueOf())//=>15然后就可以说说如何解决上面列出的三个技术点:如何在c++函数中返回AfunctionforJS调用通过Napi::Function::New创建一个新的函数,并将计算结果存放在一个可以获取该函数的地方,以备下次使用。如何通过fn使返回值既支持函数调用又支持值操作。set篡改valueOf函数返回结果如何处理参数个数不固定(其实这个很简单,从上面可以看出,它本身就是一个数组)通过获取info的Length遍历获取并与JS对比当然,例如currying等功能在JS中实现起来非常简单,用reduce函数写五行就可以了。那我们折腾这么多干什么?这就又回到了开头提到的优点:冒泡排序是用来比较执行效率的。为了证明效率上的差异,我们选择用一种排序算法来验证,使用最简单易懂的冒泡排序。一、JS版:源码位置:https://github.com/Jiasm/node...functionbubble(arr){for(leti=0,len=arr.length;ivoidbubbleSort(double*arr,intlen){doubletemp;诠释我,j;for(i=0;i();intlen=array.Length(),i;//返回值Napi::Arrayarr=Napi::Array::New(env,len);双倍的*list=newdouble[len];//将Array转换为双精度数组,可以很容易地被c++使用for(i=0;iMath.random()*1e6|0)console.time('c++')consta=bubble(arr)console.timeEnd('c++')functionbubbleJS(arr){for(让i=0,len=arr.length;i