最新如何用C++扩展NodeJS的能力?_c++编写nodejs扩展,2024年最新C C++程序员必备

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

也可以直接配置到package.json中。

安装完后要在binding.gyp中配置好nan的头文件依赖,修改如下:

{
  "targets": [
    {
      "target\_name": "meshtool",
      "sources": [ “./meshfilereader.cpp”,"./index.cpp"],
      "include\_dirs" : [
           "<!(node -e \"require('nan')\")"
     }]
}

之所以include_dirs要这么写是因为这样可以自适应nan模块安装在全局和安装在本地的情况。如果确定安装方式的话,也可以直接写路径。

至此万事俱备,只欠代码。

编写Addon的C++代码

写代码前的预备知识
写Addon之前,建议先要了解一下Google V8的API,至少要了解以下的一些概念:

JS 基本类型 对应的V8原生C++ 原生类型 :

Javascript	V8
Number	v8::Number, v8::Integer
String	v8::String
Array	v8::Array
Object	v8::Object
Function	v8::Function

JS基本类型和V8的原生类型之间实际是等价的,也就是C++层从JS层获取到的JS基本对象和返回JS层的结果,都是以v8的上述的原生类型形式。

JS句柄 v8::Handle 它相当于一个智能指针,所有上面的C++的原生类型都是由Handle来引用的,相当于JS中的那个var 变量,因此不管是从JS层获取到的原生类型对象还是在C++内部构造出的原生类型对象,都是以 v8::Handle 形式给出来的。
v8::Handle分为两种,v8::Local和v8::Persistant, 前者只在当前Scope中有效,后者是代表全局变量。
v8::Local 的Scope由 HandleScope管理,由离最近的HandleScope分配,并随HandleScope生命周期结束而结束。而v8::Persistant的生命周期由自己的New和Dispose方法管理。
生命周期结束的Handle,其指向的对象会随时被垃圾收集回收。

JS方法的C++表示

必须为为全局函数或静态方法,根据V8版本不同固定为如下的形式:

V8 3.11

v8::Handle<v8::Value>  AnyMethodName(v8::Argument args)
V8 3.28

void AnyMethodName(v8::Argument args)

JS方法的传入参数 v8::Argument

不管什么样的JS函数,其C++方法的传入参数都是一个v8::Argument对象,这是因为v8::Arugment本身就是一个list, 内含可变数量的实际参数,如果想取第i个传入参数,只需要使用args[i] 即可。另外还可以通过args.This()获取this对象。

了解完概念,我们试着写一个输出一个方法的Addon
和写普通的JS模块一样,Addon的代码需要确定模块的输出,这里就是借助Nan写输出一个叫parseMesh的JS方法的Addon:

NAN\_METHOD(ParseMesh)
{
    NanScope();
    if(args.Length() < 2 || !args[0]->IsString() || !args[1]->IsFunction())
    {
        return NanThrowError("Bad Arguments");
    }
    Handle<String> filename = args[0].As<String>();
    Handle<Function> callback = args[1].As<Function>();
    ...
    NanReturnUndefined();
}
 
void init(Handle<Object> exports)
{
    NODE\_SET\_METHOD(exports,"parseMesh",ParseMesh);
}
 
NODE\_MODULE(meshtool, init);

以上代码最后一行定义模块名称meshtool,和加载它的时候调用的初始化方法init.
而初始化方法中则设置了输出的函数名parseMesh, 而实际接受parseMesh调用的C++方法即ParseMesh。

再来看这个ParseMesh方法,由于前面所说,因为v8::Argument的存在,所有JS的函数在C++层的方法参数和返回值都是一致的,所以它可以被一个NAN_METHOD的宏来处理,该宏根据Node版本将方法展开成对应的形式。保证ParseMesh方法可以在初始化中注册为任意版本JS函数parseMesh的的实现。

最后的NanReturnUndefined()表示该方法返回undefined (没有返回值即返回undefined). Nan还有很多其他的Return形式可以使用。

到此我们已经可以用C++ Addon来输出简单的JS函数了,这对于大多数情况已经够用。然而NodeJS还提供了一些更高大上的东西,比如输出一个自定义的JS的类型,或者在C++中使用多线程,并异步执行回调等。

进阶

进阶1: 输出一个JS包装类型

前一篇只提到了如何输出一个JS方法,但有的时候如果我们想输出的是一个C++的对象呢,这种情况在想要包装一个现有的C++库到JS的时候出现的尤其频繁。
如果我们仅有输出C++方法成为JS函数一条路,那也有笨办法,用C++代码表示:

//C++
class Body
{
    Body();
    void Move();
};
 
NAN\_METHOD(CreateBody)
{
    NanScope();
    Body\* handle = new Body();
    NanReturnValue(NanNew<Integer>(reinterpret\_cast<int>(handle)));
}
 
NAN\_METHOD(BodyMove)
{
    //check arguments
      Body\* handle = reinterpret\_cast<Body\*>(args[0].As<Integer>()->intValue());
      handle->Move();
}
 
NAN\_METHOD(DestroyBody)
{
    //check arguments
     Body\* handle = reinterpret\_cast<Body\*>(args[0].As<Integer>()->intValue());
     delete handle;
}
void init(Handle<Object> exports) {
     NODE\_SET\_METHOD(exports,"createBody",CreateBody);
     NODE\_SET\_METHOD(exports,"bodyMove",BodyMove);
     NODE\_SET\_METHOD(exports,"destroyBody", DestroyBody);
}
NODE\_MODULE(native_body, init)

相应的使用native addon的JS代码:

var native=require("native\_body");
var handle = native.createBody();
native.bodyMove(handle);
native.bodyDestroy(handle);

其实就是将一个Body对象的指针作为JS的一个int变量让JS层持有,每当要操作该对象时,重新将该指针传回。
但是这样的实现有很多缺点:

  1. 所有的JS方法需要传入一个额外的由CreateBody得到的handle。
  2. 用完必须显式调用BodyDestroy, 否则会内存泄露。
  3. 不安全,如果黑客通过外部传入特定地址的handle, 内部也会将它当做Body指针而执行对应方法,轻则程序崩溃,重则程序行为被控制。(不过这个问题可以通过向外部提供‘间接’地址解决,不展开了)

NodeJS对这种需求提供了比较完美的解决方案- ObjectWrap , 通过自定义C++ class继承ObjectWrap,NodeJS可以输出和自定义JS类型等价的对象。上面的代码可以改成这样:

class BodyWrap : public ObjectWrap
{
    Body\* internalBody_;
 
public:
    BodyWrap():
    internalBody\_(new Body())
    {
    }
 
    ~BodyWrap()
    {
        delete internalBody_;
    }
 
    static NAN\_METHOD(New){
        NanScope();
        // arg check is omitted for brevity
        BodyWrap \*jsBody = new BodyWrap();
        jsBody->Wrap(args.This());
        NanReturnValue(args.This());
    }
 
    static NAN\_METHOD(Move)
    {
        //check arguments
        BodyWrap\* thisObj = ObjectWrap::Unwrap<BodyWrap>(args.This());
        thisObj->internalBody_->Move();
    }
};
 
void init(Handle<Object> exports) {
    NanScope();
    Local<FunctionTemplate> t = NanNew<FunctionTemplate>(BodyWrap::New);
    t->InstanceTemplate()->SetInternalFieldCount(1);
    t->SetClassName(NanNew<String>("Body"));
    NODE\_SET\_PROTOTYPE\_METHOD(t, "move", BodyWrap::Move);
    exports->Set(NanNew<String>("Body"), t->GetFunction());
}
 
NODE\_MODULE(native_body, init)

相应的JS代码:

var Body = require("native\_body").Body;
var b = new Body();
b.move();

从JS代码可以看到已经不存在什么handle了,需要Body实例的时候可以直接new 出来,在该实例上调用方法,对应的C++ Body类型的方法就会执行,这和普通的JS自定义class完全没什么区别。
另外还可以注意到一点,JS代码中没有执行任何类似于DestroyBody的方法。那C++的Body实例何时释放呢?-- 在上面这个代码中,当new出来的JS实例 b被垃圾回收时,C++ Body实例会被自然的析构。

进阶2: 使用多线程异步计算

通常使用C++ Addon的场景,都是计算密集的任务,另外从前面的一些实例代码可以看出,C++到JS之间的数据传递中,是有很多装箱/拆箱的消耗的(如从v8::Number 到double),因此我们为了避免这种损耗,通常希望在C++中做尽量多的事情,而不希望将任务过度切分,因此如果全部在主线程执行,无可避免的会对主线程造成阻塞。解决方案则是将主要的计算任务,放在另一个线程中执行,再将结果数据在主线程中通过回调交回给JS。
假设前面的Body对象多了一个计算量很高的 checkCollision方法检查是否与其他物体碰撞,并返回布尔值。如何将checkCollision放到其他线程,再将结果返回主线程呢?我们需要用到另一个NodeJS的基础库:libuv.
下面这个示例演示了如何创建一个线程来运行checkCollision,并在线程中使用uv_aync_send方法将结果带回到主线程回调给JS层。

class Body
{
    ...
    bool checkCollision();
};
 
struct BodyContext
{
    Body\* body;
    uv_async_t async;
    bool result;
    NanCallback \*callback;
    uv_thread_t tid;
};
 
void AfterCheckCollision(uv_async_t \*async)
{
    BodyContext\* ctx = async->data;
    Handle<Value> argv[] = {NanNew<Boolean>(ctx->result)};
    ctx->callback->Call(1,argv);
    uv_thread_t tid = ctx->tid;
    uv\_thread\_join(&tid);
    delete ctx->callback;
    delete ctx;
}
 
void RunCheckCollision(BodyContext\* ctx)
{
    ctx->result = ctx->body->CheckCollision();
    uv\_async\_init(uv\_default\_loop(), &ctx->async,AfterCheckCollision);
    ctx->async.data = ctx;
    uv\_async\_send(&ctx->async);
}
 
class BodyWrap: public ObjectWrap
{
...
static NAN\_METHOD(CheckCollision)
{
    //这里假设外部的this--也就是JS body对象是一直有引用持有的。否则要使用v8:Persistant进行保持不在异步执行过程中被GC.
    BodyWrap\* thisObj = ObjectWrap::Unwrap<BodyWrap>(args.This());
    Handle<Function> cb = args[0].As<Function>();
    NanCallback \*callback = new NanCallback(cb);
    BodyContext \*ctx = new BodyContext();
    ctx->body = thisObj->internalBody_;
    ctx->callback = callback;
    uv_thread_t tid;
    uv\_thread\_create(&tid,RunCheckCollision,ctx);
    ctx->tid = tid;
}
...
}

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!




[外链图片转存中...(img-uzTTauaZ-1715877891118)]
[外链图片转存中...(img-FRgIo4qo-1715877891118)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 18
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在 Node.js 中编写 C 模块,需要使用 Node.js 提供的 C++ API。下面是一个简单的示例,展示了如何编写一个 C++ 模块并将其导出到 Node.js 中: 首先,创建一个名为 `example.cpp` 的文件,其中包含以下代码: ```cpp #include <node.h> using namespace v8; // 定义一个函数,接受两个参数并返回它们的和 void Add(const FunctionCallbackInfo<Value>& args) { Isolate* isolate = args.GetIsolate(); if (args.Length() < 2) { isolate->ThrowException( Exception::TypeError(String::NewFromUtf8(isolate, "参数数量不正确"))); return; } if (!args[0]->IsNumber() || !args[1]->IsNumber()) { isolate->ThrowException( Exception::TypeError(String::NewFromUtf8(isolate, "参数必须是数字"))); return; } double value = args[0].As<Number>()->Value() + args[1].As<Number>()->Value(); Local<Number> num = Number::New(isolate, value); args.GetReturnValue().Set(num); } // 导出 Add 函数 void Init(Local<Object> exports) { NODE_SET_METHOD(exports, "add", Add); } // 声明初始化函数 NODE_MODULE(NODE_GYP_MODULE_NAME, Init) ``` 这个文件定义了一个名为 `Add` 的函数,该函数接受两个参数,并返回它们的和。该文件还定义了一个名为 `Init` 的初始化函数,该函数将 `Add` 导出到 Node.js 中。最后,该文件使用 `NODE_MODULE` 宏将 `Init` 与模块名关联起来。 接下来,使用 `node-gyp` 工具将 C++ 模块编译为 Node.js 可以加载的二进制文件。创建一个名为 `binding.gyp` 的文件,其中包含以下内容: ```json { "targets": [ { "target_name": "example", "sources": [ "example.cpp" ] } ] } ``` 该文件指定了编译目标 `example`,并将 `example.cpp` 文件作为源文件。使用以下命令将模块编译为 Node.js 可以加载的二进制文件: ``` $ node-gyp configure build ``` 最后,在 Node.js 中加载模块并使用它。创建一个名为 `app.js` 的文件,其中包含以下内容: ```js const addon = require('./build/Release/example.node'); console.log('1 + 2 =', addon.add(1, 2)); ``` 该文件加载 C++ 模块,并使用 `addon.add` 调用 `Add` 函数。运行以下命令运行应用程序: ``` $ node app.js ``` 输出应该显示 `1 + 2 = 3`。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值