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就把这两个层面的代码在底层联系起来。