nodejs的js调用c++以及c++调用libuv过程
发布于 3 年前 作者 theanarkh 2493 次浏览 来自 分享

​1 c++模块的注册和使用

我们知道nodejs是由js、c++、c组成的。我们来看一下他们是如何分工和合作的。本文以net模块为例进行分析。我们可以通过以下方式使用net模块。

const net = require('net');

net模块是原生的js模块。对应nodejs源码的net.js。他是对tcp和pipe的封装,我们这里只讲tcp的功能。我们可以通过以下代码创建一个tcp服务器。

const net = require('net');
net.createServer((socket) => {}).listen(80);

我们知道js里是没有网络功能的,这就意味着,网络功能是由nodejs中的c++模块实现的。所以这时候nodejs就会创建一个c++对象。

const {
  TCP,
} = internalBinding('tcp_wrap');
new TCP(TCPConstants.SERVER);

TCP对象封装了底层tcp的功能。他对应的是c++层的tcp_wrap模块。我们看看tcp_wrap模块的代码。在分析tcp_wrap之前我们先看看internalBinding做了什么事情。

let internalBinding;
{
  const bindingObj = ObjectCreate(null);
  internalBinding = function internalBinding(module) {
    let mod = bindingObj[module];
    if (typeof mod !== 'object') {
      mod = bindingObj[module] = getInternalBinding(module);
    }
    return mod;
  };
}

internalBinding是在getInternalBinding的基础上加了缓存处理。我们继续看getInternalBinding。

// 根据模块名查找对应的模块
void GetInternalBinding(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  // 模块名
  Local<String> module = args[0].As<String>();
  node::Utf8Value module_v(env->isolate(), module);
  Local<Object> exports;
  // 查找名字为module_v并且标记位NM_F_INTERNAL的模块
  node_module* mod = FindModule(modlist_internal, *module_v, NM_F_INTERNAL);
  exports = InitModule(env, mod, module);
  args.GetReturnValue().Set(exports);
}

getInternalBinding通过模块名从模块链表里找到对应的节点。然后执行对应的初始化函数。

// 初始化一个模块,即执行他里面的注册函数
static Local<Object> InitModule(Environment* env,
                                node_module* mod,
                                Local<String> module) {
  Local<Object> exports = Object::New(env->isolate());
  Local<Value> unused = Undefined(env->isolate());
  mod->nm_context_register_func(exports, unused, env->context(), mod->nm_priv);
  return exports;
}

那么链表中的模块是从哪里来的呢?这时候我们就可以回头分析tcp_wrap了。tcp_wrap.cc的最后一句是(Initialize函数即上面的nm_context_register_func属性的值)

NODE_MODULE_CONTEXT_AWARE_INTERNAL(tcp_wrap, node::TCPWrap::Initialize)

NODE_MODULE_CONTEXT_AWARE_INTERNAL是一个宏,宏展开如下

#define NODE_BUILTIN_MODULE_CONTEXT_AWARE(modname, regfunc)                   \    
  NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_BUILTIN)    
      
#define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags)          \      
  static node::node_module _module = { \    
      NODE_MODULE_VERSION,       \    
      flags,                    \    
      nullptr,                  \    
      __FILE__,                  \    
      nullptr,                  \    
      (node::addon_context_register_func) (regfunc),  \    
      NODE_STRINGIFY(modname),   \    
      priv,                      \    
      nullptr                   \    
    };                          \    
    void _register_ ## modname() {  \    
      node_module_register(&_module);  \    
  }    

我们看到,宏展开后,首先定义了一个node_module 结构体。然后定义了一个_register_ xxx的函数,对应tcp模块就是_register_ tcp_wrap。这个函数在Nodejs初始化的时候会被执行

1.  void RegisterBuiltinModules() {    
2.  // 宏展开后就是执行一系列的_register_xxx函数    
3.  #define V(modname) _register_##modname();    
4.    NODE_BUILTIN_MODULES(V)    
5.  #undef V    
6.  }

我们看到_register_ tcp_wrap函数被执行了,里面只有一句代码

node_module_register(&_module);  

node_module_register定义如下

extern "C" void node_module_register(void* m) {    
  struct node_module* mp = reinterpret_cast<struct node_module*>(m);    
  mp->nm_link = modlist_builtin;    
  modlist_builtin = mp;    
}    

就是把一个node_module加入到链表中。完成了模块的注册。这就是我们刚才通过GetInternalBinding访问的那个链表。至此,我们已经了解了c++模块的注册和在js中是如何调用c++模块的。

2 使用c++模块的功能

本小节我们来看看,可以访问c++模块之后,又是如何使用c++模块的功能的。首先我们看看在js层执行new TCP的时候,c++做了什么事情。我们看一下TCP的定义。

void TCPWrap::Initialize(Local<Object> target,
                         Local<Value> unused,
                         Local<Context> context,
                         void* priv) {
  Environment* env = Environment::GetCurrent(context);
  // 以New为回调新建一个函数
  Local<FunctionTemplate> t = env->NewFunctionTemplate(New);
  Local<String> tcpString = FIXED_ONE_BYTE_STRING(env->isolate(), "TCP");
  // 函数名
  t->SetClassName(tcpString);
  t->InstanceTemplate()
    ->SetInternalFieldCount(StreamBase::kStreamBaseFieldCount);
​
  // 设置t的原型方法,即TCP.prototype的属性
  env->SetProtoMethod(t, "open", Open);
  env->SetProtoMethod(t, "bind", Bind);
  env->SetProtoMethod(t, "listen", Listen);
 
  // target为导出的对象,设置对象的TCP属性
  target->Set(env->context(),
              tcpString,
              t->GetFunction(env->context()).ToLocalChecked()).Check();
}
​

我们看以上代码似乎有点复杂,主要是v8的一些知识,翻译成js大概如下。

function FunctionTemplate(cb) {    
   function Tmp() {  
    Object.assign(this, map);  
    cb(this);  
   }  
   const map = {};  
   return {  
    PrototypeTemplate: function() {  
        return {  
            set: function(k, v) {  
                Tmp.prototype[k] = v;  
            }  
        }  
    },  
    InstanceTemplate: function() {  
        return {  
            set: function(k, v) {  
                map[k] = v;  
            }  
        }  
    },  
    GetFunction() {  
        return Tmp;  
    }  
   }   
  
}    
  
const TCPFunctionTemplate = FunctionTemplate((target) => { target[0] = new TCPWrap(); })    
TCPFunctionTemplate.PrototypeTemplate().set('connect', TCPWrap.Connect);  
TCPFunctionTemplate.InstanceTemplate().set('name', 'hi');  
const TCP = TCPFunctionTemplate.GetFunction();  

我们看到当在js层new TCP的时候,首先会new一个c++层的对象。然后执行一个函数,对应tcp_wrap.cc就是New。

void TCPWrap::New(const FunctionCallbackInfo<Value>& args) {
  // 忽略一些c参数处理
  new TCPWrap(env, args.This(), provider);
}

New中创建了一个TCPWrap对象。

TCPWrap::TCPWrap(Environment* env, Local<Object> object, ProviderType provider)
    : ConnectionWrap(env, object, provider) {
  // 初始化一个tcp handle
  int r = uv_tcp_init(env->event_loop(), &handle_);
}

这时候关系图如下。 这看起来很简单,但是其实有很多细节。这要从c++模块的基类说起。TCPWrap继承于BaseObject。BaseObject的构造函数里做了一些非常关键的操作(object就是刚才new TCP时对应的c++层对象,而不是new TCPWrap对应的对象,this对应的是new TCPWrap对象)。

// 把对象存储到persistent_handle_中,必要的时候通过object()取出来
BaseObject::BaseObject(Environment* env, v8::Local<v8::Object> object)
    : persistent_handle_(env->isolate(), object), env_(env) {
  // 把this存到object中
  object->SetAlignedPointerInInternalField(0, static_cast<void*>(this));
}

所以TCPWrap初始化后得到的关系图如下 这时候我们完成了new TCP的初始化工作,回到文章开始的代码,当我们创建一个tcp服务器的时候,会调用listen函数启动服务器。我们看看listen函数的调用过程。js层调用listen函数时,会执行c++层的Listen函数。

void TCPWrap::Listen(const FunctionCallbackInfo<Value>& args) {
  TCPWrap* wrap;
  // 把TCPWrap解包出来存到wrap中
  ASSIGN_OR_RETURN_UNWRAP(&wrap,
                          args.Holder(),
                          args.GetReturnValue().Set(UV_EBADF));
  Environment* env = wrap->env();
  int backlog;
  if (!args[0]->Int32Value(env->context()).To(&backlog)) return;
  // OnConnection为有新建立的连接时触发的回调(已完成三次握手) 
  int err = uv_listen(reinterpret_cast<uv_stream_t*>(&wrap->handle_),
                      backlog,
                      OnConnection);
  args.GetReturnValue().Set(err);
}

ASSIGN_OR_RETURN_UNWRAP这个是关键代码,new TCP的时候,我们已经知道js层和c++对象的关系。当js层调用listen函数时,他关联的对象是new TCP。ASSIGN_OR_RETURN_UNWRAP的作用就是把new TCP对应关联的new TCPWrap对象解包出来使用。这就是js层调用c++层功能的过程。

3 c++层调用libuv

接下来我们分析c++层是如何调用libuv的。上一节分析到拿到了TCPWrap对象,然后会执行以下代码

int err = uv_listen(reinterpret_cast<uv_stream_t*>(&wrap->handle_),
                      backlog,
                      OnConnection);

那么&wrap->handle_是什么呢?我们来看看TCPWrap的定义。TCPWrap继承于ConnectionWrap。

class TCPWrap : public ConnectionWrap<TCPWrap, uv_tcp_t>

ConnectionWrap是一个模板类。

// WrapType是c++层的类,UVType是libuv的类型
template <typename WrapType, typename UVType>
class ConnectionWrap : public LibuvStreamWrap {
 public:
  static void OnConnection(uv_stream_t* handle, int status);
  static void AfterConnect(uv_connect_t* req, int status);
​
 protected:
  ConnectionWrap(Environment* env,
                 v8::Local<v8::Object> object,
                 ProviderType provider);
​
  UVType handle_;
};

我们看到&wrap->handle_的值是uv_tcp_t。该handle_在初始化的时候和TCPWrap对象会关联起来(TCPWrap继承HandleWrap)。

HandleWrap::HandleWrap(Environment* env,
                       Local<Object> object,
                       uv_handle_t* handle,
                       AsyncWrap::ProviderType provider)
    : AsyncWrap(env, object, provider),
      state_(kInitialized),
      handle_(handle) {
  // 保存Libuv handle和c++对象的关系
  handle_->data = this;
​
​
}

后面我们会看到这个用处。这时候关系图如下 回调listen的代码

 int err = uv_listen(reinterpret_cast<uv_stream_t*>(&wrap->handle_),
                      backlog,
                      OnConnection);

这时候我们就知道传给libuv的结构体是什么了。当listen结束后,就会回调OnConnection函数。

template <typename WrapType, typename UVType>
void ConnectionWrap<WrapType, UVType>::OnConnection(uv_stream_t* handle,
                                                    int status) {
  // 拿到Libuv结构体对应的c++层TCPWrap对象                                                    
  WrapType* wrap_data = static_cast<WrapType*>(handle->data);
  // 回调js,client_handle相当于在js层执行new TCP
  Local<Value> argv[] = { Integer::New(env->isolate(), status), client_handle };
  wrap_data->MakeCallback(env->onconnection_string(), arraysize(argv), argv);
}

我们看到在OnConnection里,首先根据通过handle->data拿到对应的c++层对象TCPWrap(这就是HandleWrap里关联这两个数据结构的用处,为了保持上下文)。然后调用TCPWrap的MakeCallback函数(onconnection_string是字符串“onconnection”)。MakeCallback在AsyncWrap中定义(TCPWrap继承AsyncWrap)。

inline v8::MaybeLocal<v8::Value> AsyncWrap::MakeCallback(
    const v8::Local<v8::Name> symbol,
    int argc,
    v8::Local<v8::Value>* argv) {
  v8::Local<v8::Value> cb_v;
  // 根据字符串表示的属性值,从对象中取出该属性对应的值。是个函数
  if (!object()->Get(env()->context(), symbol).ToLocal(&cb_v))
    return v8::MaybeLocal<v8::Value>();
  // 要是个函数
  if (!cb_v->IsFunction()) {
    // TODO(addaleax): We should throw an error here to fulfill the
    // `MaybeLocal<>` API contract.
    return v8::MaybeLocal<v8::Value>();
  }
  // 回调,见async_wrap.cc
  return MakeCallback(cb_v.As<v8::Function>(), argc, argv);
}

object()就是js层new TCP对应的对象,获取该对象的onconnection属性的值(该属性在js层设置),该值是一个函数,然后继续调用MakeCallback。

MaybeLocal<Value> AsyncWrap::MakeCallback(const Local<Function> cb,
                                          int argc,
                                          Local<Value>* argv) {
 MaybeLocal<Value> ret = InternalMakeCallback(
      env(), object(), cb, argc, argv, context);
  return ret;
}

MakeCallback继续调用InternalMakeCallback,InternalMakeCallback中会调用v8的接口执行函数,从而回调js层。

  ret = callback->Call(env->context(), recv, argc, argv)

4 总结

这就是nodejs中js、c++、libuv的大致交互过程,也是通用的流程。

4 回复

好文没人看系列。。 不过也巧了,我也正在写nodejs源码的系列文章。。不过还没写完,所以还没发布XD

回到顶部