nodejs之js调用c++初探
发布于 4 年前 作者 theanarkh 4843 次浏览 来自 分享

nodejs的很多功能都是通过c++或者通过c++层调用libuv层实现的,nodejs是如何在js层面调用c++的呢?在nodejs里,使用c++层的功能主要有两种方式,第一种就是调用全局变量process,Buffer等,第二种就是通过process.binding函数。

1 process、buffer等全局变量

首先来看第一个种。在nodejs启动的过程中。在Environment::Start函数中生成了process对象。

// 利用v8新建一个函数
  auto process_template = FunctionTemplate::New(isolate());
  // 设置函数名
  process_template->SetClassName(FIXED_ONE_BYTE_STRING(isolate(), "process"));
  // 利用函数new一个对象
  auto process_object = process_template->GetFunction()->NewInstance(context()).ToLocalChecked();
  // 设置env的一个属性,val是process_object
  set_process_object(process_object);
  // 设置process对象的属性
  SetupProcessObject(this, argc, argv, exec_argc, exec_argv);

以上代码生成了一个process对象并且保存在env里。然后继续执行LoadEnvironment函数。在该函数里会执行bootstrap_node.js。然后执行bootstrap_node.js导出的函数。

 Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(),"bootstrap_node.js");
 // 执行bootstrap_node.js
 Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
 Local<Function> f = Local<Function>::Cast(f_value);
 // 全局变量,我们访问全局变量的时候都是global的属性
 Local<Object> global = env->context()->Global()
 // js层的全局变量,类似浏览器的window
 global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global);
 Local<Value> arg = env->process_object();
 // 执行bootstrap_node.js 
 auto ret = f->Call(env->context(), Null(env->isolate()), 1, &arg);

在bootstrap_node.js的函数里执行setupGlobalVariables函数。

global.process = process;
global.Buffer = NativeModule.require('buffer').Buffer;
process.domain = null;
process._exiting = false;

以上代码设置了几个全局变量,我们访问process的时候就是访问global.process。当v8编译执行bootstrap_node.js的时候,传进来了一个c++的对象process,在js层调用global其实就是调用了c++层的global对象,执行global.process = process的时候,即在c++层面给c++层的global对象新增了一个属性process,他的值是传进来的c++对象process。所以当我们在js层面访问process的时候,v8会在c++层面的global对象里查找process属性,这时候就会找到传进来的c++对象。

2 process.binding

我们看一下我们在js里调用process.binding函数的时候,nodejs都做了什么,首先看一下process.binding的函数是怎么来的,我们知道在nodejs启动的时候新建了一个process对象,并且通过node.cc的SetupProcessObject函数挂载了一系列属性。其中设置了一个属性就是binding

env->SetMethod(process, "binding", Binding);

static void Binding(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);

  CHECK(args[0]->IsString());

  Local<String> module = args[0].As<String>();
  node::Utf8Value module_v(env->isolate(), module);

  node_module* mod = get_builtin_module(*module_v);
  Local<Object> exports;
  if (mod != nullptr) {
    exports = InitModule(env, mod, module);
  } else if (!strcmp(*module_v, "constants")) {
    exports = Object::New(env->isolate());
    CHECK(exports->SetPrototype(env->context(),
                                Null(env->isolate())).FromJust());
    DefineConstants(env->isolate(), exports);
  } else if (!strcmp(*module_v, "natives")) {
    exports = Object::New(env->isolate());
    DefineJavaScript(env, exports);
  } else {
    return ThrowIfNoSuchModule(env, *module_v);
  }

  args.GetReturnValue().Set(exports);
}

nodejs的启动流程文章中我们分析过binding函数就是在一个内置模块链表中找到对应的模块。然后执行该模块注册的时候,然后返回一个该模块导出的对象。这就是底层的binding函数做的功能,但是我们在调process.binding的时候,并不是直接执行了c++层的binding函数。在bootstrap_node.js中还封装了一层。

const bindingObj = Object.create(null);

const getBinding = process.binding;
 process.binding = function binding(module) {
   module = String(module);
   let mod = bindingObj[module];
   if (typeof mod !== 'object') {
     mod = bindingObj[module] = getBinding(module);
     moduleLoadList.push(`Binding ${module}`);
   }
   return mod;
 };

nodejs在这加了一层缓存。下面我们以net.js调用tcp_wrap.cc为例看一下js是如何调用c++的功能的。 当我们执行以下代码时,

const { TCP } = process.binding('tcp_wrap');

v8首先编译js代码,然后在执行时访问process对象,根据1中的分析,这时候就会访问c++层的process对象,然后访问binding属性,即上面绑定的binding函数,该函数会调用C++层的binding函数,返回一个导出的对象exports。接下来我们执行TCP.a或者new TCP的时候,其实就类似于调用一个c++对象的属性或者在c++层面new一个对象一样。这个是由v8进行转换的。即v8在编译解析TCP这个字符串的时候他就会生成访问底层TCP类的代码。

理解js是如何调用c++的,不能把思路停留到静态,要结合v8是如何编译和执行js代码的。比如v8在编译这段代码。

const { TCP } = process.binding('tcp_wrap');
const tcp = new TCP();
tcp.listen();

转化成c++代码或者二进制代码反编译成c++后可能是

HashTable *hash = new HashTable();

object * binding(char *str) {
	if (hash[str]) {
		return hash[str];
	}
	return hash[str] = Binding(str);
}
object * Binding(char *str) {}

Object *process = new Object();
Object *Tcp_wrap = new Object();

process->binding = binding
Function *TCP = process.binding('tcp_wrap'); => Function *TCP = Tcp_wrap;
object* tcp = new Tcp_wrap();
tcp.listen();

在js里直接调用c++是不可以的,但是js最终是要编译成二进制代码的。在二进制的世界里,js代码和c++代码就可以通信了,因为nodejs定义的那些c++模块和c++变量都是基于v8的架构的,比如定义了一个process对象,或者Binding函数,都是利用了v8的规则和接口。所以在nodejs里,不管是v8内置的js函数,对象,还是nodejs额外提供的那些模块功能,他最后都是由v8去处理的。虽然无法在js里直接调用c++功能,但是可以在js被编译后使用c++功能。而nodejs的实现方案就是实现一个process对象和Binding函数。js里通过process.binding加载一个c++模块的时候,这段js在编译后执行,首先访问js层的process对象,v8知道js的process对象对应是c++的process对象,就像我们在js里定义一个函数或者对象,在编译后v8也知道应该调用的是c++哪些代码,因为我们怎么写脚本以及使用什么功能但是v8提供的,v8在编译执行我们的js脚本的时候,当我会知道需要执行底层哪些代码。所以v8知道需要执行的是c++层的process对象里的Binding函数,通过底层的Binding,就可以使用c++模块的功能了。 当我们使用非v8提供的内置函数时,nodejs里是通过在执行时查找对应模块的形式去实现的,而不是通过在v8的global变量里挂载新的函数。下面是在v8的global里挂载自定义对象的大致流程。

void say(){}
Handle<FunctionTemplate> a_template = FunctionTemplate::New(callbackWhenNewObject);
a_template ->SetClassName(String::New("a_template"));
Handle<ObjectTemplate> a_template _proto = a_template->PrototypeTemplate();
a_template _proto->Set(String::New("say"), FunctionTemplate::New(say));
// 挂载到全局变量,我们在js里就可以直接访问a_template 
global->Set(String::New("a_template "), a_template );

在js里调用

new a_template().say();

总得来说,js是调用c++功能是通过process.binding去实现的,首先是我们在js里调用js层的process.binding,通过v8提供的功能,js层的process.binding在执行的时候是对应c++层的某段代码的,所以js被编译执行时就可以使用c++提供的功能了,因为这时候c++代码和js代码都被编译成二进制代码,通过process.binding就把这两个层面的代码在底层联系起来。

回到顶部