专栏首页原创分享nodejs之js调用c++初探

nodejs之js调用c++初探

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

本文分享自微信公众号 - 编程杂技(theanarkh),作者:theanarkh

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-03-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • nodejs之next_tick源码分析

    next_tick函数是process对象的一个属性。他是在bootstrap_node.js中设置的。

    theanarkh
  • nodejs框架express路由的大致原理

    昨晚准备洗澡的时候,突然想实现一下express的路由逻辑,但时间有限,只能先写这么多。这个不完全是express的路由原理,只是提供一点思路,具体逻辑可以参考...

    theanarkh
  • js引擎v8源码分析之Handle(基于v8 0.1.5)

    Handle是使用v8的时候很重要的一个概念和类。他本质是堆对象的封装。我们通过Handle管理真正的对象,而不是直接操作对象。Handle在v8中有两个实现。...

    theanarkh
  • nodejs如何利用libuv实现事件循环和异步

    但nodejs不是给每个功能拓展一个对象,而是拓展一个process对象,再通过process.binding拓展js功能。Nodejs定义了一个js对象pro...

    标子
  • 代码管理| 创建自己的私有Cocopods库

    iOS组件化的实现基本基于cocoapods,如何使用cocoapods创建自己的组件库,是实现组件化的第一要素,下面就创建自己的私有Cocopods库展开实战...

    進无尽
  • 100个linux命令(3)-权限管理

    文件权限附属在文件所有者 u,文件所属组 g 和其他用户o 上。使用字符修改权限需要指明操作谁的权限,另外还要使用"+"或"-"指定增加权限还是删除权限,也可以...

    懒人的小脑
  • Python实现PPPOE攻击工具,秒杀拨号上网

    七夜安全博客
  • Easyui 让DataGrid适应浏览器宽度

    DataGrid有100%宽度的设置,但是有时不是很让人满意,比如你你放大或者拉放你的浏览器,那么DataGrid只维持第一次加载的宽高,非常难看 $('#L...

    用户1149182
  • wordpress专用腾讯云服务器99元够你玩一年

    一般来说新手初次接触做站都比较习惯wordpress,织梦cms,帝国cms等,那么花钱买空间就是个头疼的事,腾讯云最新做了活动AMD1C1G云服务器,国内的服...

    主机优惠教程
  • 实例演绎Unix/Linux的"一切皆文件"思想

    我们知道,在Unix/Linux系统中“一切皆文件”,socket也被认为是一种文件,socket被表示成文件描述符。

    Linux阅码场

扫码关注云+社区

领取腾讯云代金券