JCHub

  • Home
  • Category
    • A/V
    • WebRTC
    • Beauty of Programming
    • Linux
    • Windows
    • Moments of Life
    • Campus Life
  • Reference
    • API Reference
    • Utilities
    • AV Test
    • Doc
  • Message Board
  • About
JCHub
Code as My Sword, Lost in Obsession
  1. Main page
  2. Beauty of Programming
  3. Main content

Nodejs C++ addon开发简介

2020年9月25日 6hotness 0likes 0comments

目前OWT服务端使用了两种方式开发C++ addon:

  • 使用内部的V8、libuv 和 Node.js 库
  • 第三方NAN库

OWT服务端更新最频繁的数WebRTC agent代码了,目前WebRTC agent用的是NAN方式开发C++ addon,其他agent用的是内部的V8、libuv 和 Node.js 库开发C++ addon,开发起来会比较复杂,需要熟悉Nodejs各个组件的知识以及libuv、V8相关API调用。所以对于新开发的C++ addon,推荐使用NAN方式开发,简单,而且不用考虑Nodejs升级后的兼容性问题。

1. NAN简介

NAN的全称为**Native Abstraction for Node.js** , 其表现上是一个Node.js包。安装后,就得到一堆C++头文件,里面是一堆宏。它主要为Node.js和V8跨版本提供了封装的宏,使得开发者不用关心各个版本之间的API的差异。

NAN官方地址为:https://github.com/nodejs/nan。

NAN的优势在于可以屏蔽不同版本Node的API,使得C++ addon可以wirte once, compile anywhere,一份C++ addon可以适用于不同版本的Nodejs。

接下来我们以WebRTC agent中的代码简单讲解下如何使用NAN开发一个C++ addon。

2. NAN C++ addon开发

这里我们以WebRTC agent中的rtcConn addon作为示例,代码位于source/agent/webrtc/rtcConn中。

2.1 使用

安装NAN包:

1
npm install nan

编写binding.gyp,将NAN路径添加到binding.gyp中:

1
2
3
"include_dirs" : [
    "<!(node -e \"require('nan')\")"
]

NAN的路径会通过node -e "require('nan')"获取。

2.2 目录结构

如下是rtcConn addon的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── addon.cc
├── binding.gyp
├── conn_handler
│   ├── WoogeenHandler.cpp
│   └── WoogeenHandler.h
├── erizo
├── IOThreadPool.cc
├── IOThreadPool.h
├── MediaStream.cc
├── MediaStream.h
├── ThreadPool.cc
├── ThreadPool.h
├── WebRtcConnection.cc
└── WebRtcConnection.h

在每个C++ addon中都包含:binding.gyp,addon.cc。

  • binding.gyp。使用JSON风格,包含当前addon的编译配置,例如用到的文件以及库
  • addon.cc。类似于main函数入口,调用包含的各个C++类的Init接口

binding.gyp

如下是rtcConn addon中,binding.gyp部分内容。

首先通过target_name定义了该C++ addon名称,这样会在build目录下生成target_name.node文件。接着包含相关源码文件以及链接用到的第三方库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
  'targets': [{
    'target_name': 'rtcConn',
    'variables': {
      'source_rel_dir': '../../..', # relative source dir path
      'source_abs_dir%': '<(module_root_dir)/../../..', # absolute source dir path
    },
    'sources': [
      'addon.cc',
      'WebRtcConnection.cc',
      'ThreadPool.cc',
      'IOThreadPool.cc',
      "MediaStream.cc",
      'conn_handler/WoogeenHandler.cpp',
      'erizo/src/erizo/DtlsTransport.cpp',
      ........................
    ],
    'cflags_cc': ['-DWEBRTC_POSIX', '-DWEBRTC_LINUX', '-DLINUX', '-DNOLINUXIF', '-DNO_REG_RPC=1', '-DHAVE_VFPRINTF=1', '-DRETSIGTYPE=void', '-DNEW_STDIO', '-DHAVE_STRDUP=1'],
    'include_dirs': [
      "<!(node -e \"require('nan')\")",
      'conn_handler',
      'erizo/src/erizo',
      'erizo/src/erizo/lib',
      '<(source_rel_dir)/../build/libdeps/build/include',
      ...........................
    ],
    'libraries': [
      '-L<(source_abs_dir)/../build/libdeps/build/lib',
      '-lsrtp2',
      '-lnice',
      ...........................
    ],
}

addon.cc

调用用到的各个C++类的Init方法,这些C++类都是按照NAN规定的方式进行wrap,这样我们就可以将这些C++类导出,以addon.cppclass方式调用各个C++类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// addon.cc
#include "WebRtcConnection.h"
#include "ThreadPool.h"
#include "IOThreadPool.h"
#include "MediaStream.h"
 
#include <dtls/DtlsSocket.h>
 
#include <node.h>
 
using namespace v8;
 
void InitAll(Handle<Object> exports) {
  dtls::DtlsSocketContext::Init();
  WebRtcConnection::Init(exports);
  MediaStream::Init(exports);
  ThreadPool::Init(exports);
  IOThreadPool::Init(exports);
}
 
NODE_MODULE(addon, InitAll)

目录中除了binding.gyp,addon.cc,剩下的都是用到的各个C++类NAN方式wrap后的文件。

OWT服务端不同agent代码文件命名,以及代码规范比较乱。这里规定新代码中wrap后的文件名最好加上Wrapper修饰,便于区分。可以参照WebRTC agent中最新的rtcFrame addon(基于WebRTC SDK 实现的一个C++ addon)。

2.3 NAN wrap C++类

我们的C++类都要按照NAN规定的方式wrap,这样Nodejs层才可以调用。

我们有个licode中的erizo::WebRtcConnection(third_party/licode/erizo/src/erizo/WebRtcConnection.h)类,用于处理WebRTC连接相关的配置信息,我们要将它提供给Nodejs层调用。我们可以通过如下方式进行wrap,这里看下wrap后的类头文件大概内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// WebRtcConnection.h
class WebRtcConnection : public Nan::ObjectWrap, public erizo::WebRtcConnectionEventListener {
public:
    static NAN_MODULE_INIT(Init);
    
    // 真正执行相关业务的类
    std::shared_ptr<erizo::WebRtcConnection> me;
    int eventSt;
    // 定义一个事件队列,存储回调的各个事件
    std::queue<int> eventSts;
    std::queue<std::pair<std::string, std::string>> eventMsgs;
 
    boost::mutex mutex;
 
private:
    WebRtcConnection();
    ~WebRtcConnection();
 
    Nan::Callback *eventCallback_;
    // libuv相关
    uv_async_t async_;
    uv_async_t asyncStats_;
    bool hasCallback_;
    static NAN_METHOD(New);
    static NAN_METHOD(stop);
    static NAN_METHOD(close);
    static NAN_METHOD(init);
    static NAN_METHOD(createOffer);
    static NAN_METHOD(setRemoteSdp);
    static NAN_METHOD(getLocalSdp);
    static Nan::Persistent<v8::Function> constructor;
    static NAUV_WORK_CB(eventsCallback);
    virtual void notifyEvent(erizo::WebRTCEvent event,
                             const std::string& message = "",
                             const std::string& stream_id = "");
};
 
#endif  // WEBRTCCONNECTIONWRAPPER_H

对于C++类要导出的接口使用NAN_METHOD进行修饰。我们要wrap的类作为me成员。

2.3.1 NAN_MODULE_INIT

用于定义该模块的入口函数。在这里注册要使用的C++类接口,这样Nodejs层才可以调用。当Nodejs层执行require('path/xxx.node')的时候,就会执行该函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// WebRtcConnection.cc
NAN_MODULE_INIT(WebRtcConnection::Init) {
  // Prepare constructor template
  Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(New);
  tpl->SetClassName(Nan::New("WebRtcConnection").ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);
 
  // Prototype
  // 如下接口都是要导出的,提供Nodejs层调用,第二个参数是Nodejs层使用的函数名
  Nan::SetPrototypeMethod(tpl, "stop", stop);
  Nan::SetPrototypeMethod(tpl, "close", close);
  Nan::SetPrototypeMethod(tpl, "init", init);
  Nan::SetPrototypeMethod(tpl, "setRemoteSdp", setRemoteSdp);
  Nan::SetPrototypeMethod(tpl, "getLocalSdp", getLocalSdp);
  Nan::SetPrototypeMethod(tpl, "getCurrentState", getCurrentState);
  Nan::SetPrototypeMethod(tpl, "createOffer", createOffer);
 
  constructor.Reset(tpl->GetFunction());
  Nan::Set(target, Nan::New("WebRtcConnection").ToLocalChecked(), Nan::GetFunction(tpl).ToLocalChecked());
}

2.3.2 NAN_METHOD(New)

构造函数入口,在Nodejs层New一个该C++ wrap类时,就会进入这里。在这里可通过判断Nodejs层传入的参数个数,调用不同的构造函数。

我们通过info.Length()判断传入的参数个数。

2.3.3 NAN_METHOD(setRemoteSdp)

通过这个函数讲下怎么传递参数到C++。这个函数主要传递Nodejs层的字符串参数到C++底层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// WebRtcConnection.cc
NAN_METHOD(WebRtcConnection::setRemoteSdp) {
  // 这个obj是我们通过NAN方式wrap后的类
  WebRtcConnection* obj = Nan::ObjectWrap::Unwrap<WebRtcConnection>(info.Holder());
  // 这个obj->me是wrap前的
  std::shared_ptr<erizo::WebRtcConnection> me = obj->me;
  if (!me) {
    return;
  }
  // info[0]代表Nodejs函数的第一个参数
  // 要先转为v8::String::Utf8Value才能转为C++字符串
  v8::String::Utf8Value param(Nan::To<v8::String>(info[0]).ToLocalChecked());
  std::string sdp = std::string(*param);
  
  // info[1]代表Nodejs函数的第二个参数
  v8::String::Utf8Value stream_id_param(Nan::To<v8::String>(info[1]).ToLocalChecked());
  std::string stream_id = std::string(*stream_id_param);
 
  // 通过info.Length() > 2判断Nodejs函数参数是否大于2,
  // 获取Nodejs函数的第三个参数,为bool类型
  bool need_resend_sdp = info.Length() > 2 ? info[2]->BooleanValue() : false;
  // 获取到Nodejs传递的各个参数后,执行实际业务
  bool r = me->setRemoteSdp(sdp, stream_id, need_resend_sdp);
  
  // GetReturnValue方式将返回值传递到Nodejs层
  info.GetReturnValue().Set(Nan::New(r));
}

2.3.4 NAUV_WORK_CB

通过这个函数讲下怎么将C++底层的数据回调到Nodejs层。大概函数调用层次如下:

1
2
3
4
5
6
7
8
9
erizo::WebRtcConnection::maybeNotifyWebRtcConnectionEvent
               ↓
WebRtcConnectionEventListener::notifyEvent
               ↓
WebRtcConnection::notifyEvent
               ↓
WebRtcConnection::eventsCallback
               ↓
Nan::MakeCallback

首先按NAN方式定义一个回调

1
static NAUV_WORK_CB(eventsCallback)

定义一个抽象类,在这里定义回调接口,其实就是常见的观察者模式

1
2
3
4
5
6
class WebRtcConnectionEventListener {
public:
    virtual ~WebRtcConnectionEventListener() {
    }
    virtual void notifyEvent(WebRTCEvent newEvent, const std::string& message, const std::string &stream_id = "") = 0;
};

NAN方式wrap后的类继承前一步定义的抽象类

1
2
3
4
class WebRtcConnection : public Nan::ObjectWrap,
                         public erizo::WebRtcConnectionEventListener {
public:
    static NAN_MODULE_INIT(Init);

实现抽象类的回调接口

1
2
3
4
5
6
7
8
9
10
void WebRtcConnection::notifyEvent(erizo::WebRTCEvent event, const std::string& message, const std::string& stream_id) {
  boost::mutex::scoped_lock lock(mutex);
  // 将回调的事件放到队列中
  this->eventSts.push(event);
  this->eventMsgs.emplace(message, stream_id);
  async_.data = this;
  // 通过libuv的uv_async_send异步执行我们的回调处理:eventsCallback
  // uv_async_send与eventsCallback的绑定见后面分析
  uv_async_send(&async_);
}

NAN_METHOD(New)中构造对象时,将wrap的类对象传递进去

1
2
3
4
5
WebRtcConnection* obj = new WebRtcConnection();
// obj传递给erizo::WebRtcConnection的构造函数
// 这样在erizo::WebRtcConnection中就可以调用NAN wrap这一层的回调接口
obj->me = std::make_shared<erizo::WebRtcConnection>(worker, io_worker, wrtcId, iceConfig,
                                                    rtp_mappings, ext_mappings, obj);

看下erizo::WebRtcConnection中的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 定义的抽象类,定义回调接口
class WebRtcConnectionEventListener {
public:
    virtual ~WebRtcConnectionEventListener() {
    }
    virtual void notifyEvent(WebRTCEvent newEvent, const std::string& message, const std::string &stream_id = "") = 0;
};
// erizo::WebRtcConnection
class WebRtcConnection: public TransportListener, public LogContext,
                        public std::enable_shared_from_this<WebRtcConnection> {
  DECLARE_LOGGER();
 
public:
  typedef typename Handler::Context Context;
  // WebRtcConnectionEventListener即为外部NAN这一层wrap的类对象obj
  WebRtcConnection(std::shared_ptr<Worker> worker, std::shared_ptr<IOWorker> io_worker,
      const std::string& connection_id, const IceConfig& ice_config,
      const std::vector<RtpMap> rtp_mappings, const std::vector<erizo::ExtMap> ext_mappings,
      WebRtcConnectionEventListener* listener);
private:
  // 外部NAN方式wrap的类对象obj
  WebRtcConnectionEventListener* conn_event_listener_;

通过conn_event_listener_进行各种事件回调:

1
2
3
4
5
6
7
8
9
void WebRtcConnection::maybeNotifyWebRtcConnectionEvent(const WebRTCEvent& event,
    const std::string& message,
    const std::string& stream_id) {
  boost::mutex::scoped_lock lock(event_listener_mutex_);
  if (!conn_event_listener_) {
      return;
  }
  conn_event_listener_->notifyEvent(event, message, stream_id);
}

这样回调的信息就会传到NAN这一层,NAN这一层再调用Nodejs层的回调函数,即可将信息回调到Nodejs层。

Nodejs层回调

函数这里我们通过调用定义的init接口获取Nodejs层的回调函数

1
2
3
4
5
6
7
8
9
10
NAN_METHOD(WebRtcConnection::init) {
  WebRtcConnection* obj = Nan::ObjectWrap::Unwrap<WebRtcConnection>(info.Holder());
  std::shared_ptr<erizo::WebRtcConnection> me = obj->me;
  // Nodejs init函数第一个参数为回调函数
  // 存到obj->eventCallback_中
  obj->eventCallback_ = new Nan::Callback(info[0].As<Function>());
  bool r = me->init();
 
  info.GetReturnValue().Set(Nan::New(r));
}

这里看下Nodejs层代码调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 回调函数里包含newStatus, mess, streamId三个参数
// 对应前面的回调接口定义
this.wrtc.init((newStatus, mess, streamId) => {
      log.info('message: WebRtcConnection status update, ' +
               'id: ' + this.id + 'streamId:', streamId, ', status: ' + newStatus +
                ', mess:' + logger.objectToLog(this.metadata) + mess);
      switch(newStatus) {
        case CONN_INITIAL:
          this.emit('status_event', {type: 'started'}, newStatus);
          break;
 
        case CONN_SDP_PROCESSED:
          log.info(' messege CONN_SDP_PROCESSED');
          this.isProcessingRemoteSdp = false;
          // this.latestSdp = mess;
          // this._maybeSendAnswer(newStatus, streamId);
          break;

回调事件队列处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
NAUV_WORK_CB(WebRtcConnection::eventsCallback) {
  Nan::HandleScope scope;
  WebRtcConnection* obj = reinterpret_cast<WebRtcConnection*>(async->data);
  if (!obj || obj->me == NULL)
    return;
  boost::mutex::scoped_lock lock(obj->mutex);
  // 队列中事件一一回调到Nodejs层
  while (!obj->eventSts.empty()) {
    Local<Value> args[] = {
      Nan::New(obj->eventSts.front()),
      Nan::New(obj->eventMsgs.front().first.c_str()).ToLocalChecked(),
      Nan::New(obj->eventMsgs.front().second.c_str()).ToLocalChecked()
    };
    // Nan::MakeCallback方式回调,这里我们回调了三个参数数据
    // 调用Nodejs层回调函数obj->eventCallback_回调到Nodejs层
    Nan::MakeCallback(Nan::GetCurrentContext()->Global(), obj->eventCallback_->GetFunction(), 3, args);
    obj->eventMsgs.pop();
    obj->eventSts.pop();
  }
}

eventsCallback与libuv的绑定在NAN_METHOD(WebRtcConnection::New)中:

1
uv_async_init(uv_default_loop(), &obj->async_, &WebRtcConnection::eventsCallback);

这样调用uv_async_send(&async_)就会异步执行eventsCallback。

退出时关闭处理

1
2
3
4
5
6
7
8
9
10
11
NAN_METHOD(WebRtcConnection::close) {
  WebRtcConnection* obj = Nan::ObjectWrap::Unwrap<WebRtcConnection>(info.Holder());
  obj->me->setWebRtcConnectionEventListener(NULL);
  obj->me->close();
  obj->me.reset();
  obj->hasCallback_ = false;
  // 调用libuv的接口,关闭处理,取消回调绑定
  if (!uv_is_closing(reinterpret_cast<uv_handle_t*>(&obj->async_))) {
    uv_close(reinterpret_cast<uv_handle_t*>(&obj->async_), NULL);
  }
}

2.4 Nodejs层调用

编译后如下方式引入:

1
const addon = require('../rtcConn/build/Release/rtcConn');

构造:

1
var wrtc = new addon.WebRtcConnection()

传入回调函数:

1
2
3
wrtc.init((newStatus, mess, streamId) => {
  // 各种处理
}

相关接口调用:

1
wrtc.setRemoteSdp(latestSdp, streamId, needResendSdp)

3. 总结

本文简单介绍了目前服务端Nodejs c++ addon开发,通过列举的参数传递,回调例子,对着相关代码过一遍,基本能快速上手。

This article is licensed with Creative Commons Attribution-NonCommercial-No Derivatives 4.0 International License
Tag: C++
Last updated:2026年3月26日

Jeff

管理员——代码为剑,如痴如醉

Tip the author Like
< Last article
Next article >

Comments

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
Cancel

This site uses Akismet to reduce spam. Learn how your comment data is processed.

文章目录
  • 1. NAN简介
  • 2. NAN C++ addon开发
    • 2.1 使用
    • 2.2 目录结构
    • 2.3 NAN wrap C++类
    • 2.4 Nodejs层调用
  • 3. 总结
Related Posts
  • 网络字节转换到本地字节的函数模板
  • MFC自绘带背景颜色标题栏
  • VC++获取本机IP地址
  • FLTK程序编译错误
  • C++实现windows重启
Categories

COPYRIGHT © 2026 jianchihu.net. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang