本文分享一下nodejs中js调用c++模块的一些内容。js调用c++模块是v8提供的能力,nodejs是使用了这个能力。这样我们只需要面对js,剩下的事情交给nodejs就行。本文首先讲一下利用v8如何实现js调用c++,然后再讲一下nodejs是怎么做的。
1 js调用c++
首先介绍一下v8中两个非常核心的类FunctionTemplate和ObjectTemplate。顾名思义,这两个类是定义模板的,好比建房子时的设计图一样,通过设计图,我们就可以造出对应的房子。v8也是,定义某种模板,就可以通过这个模板创建出对应的实例。下面介绍一下这些概念(为了方便,下面都是伪代码)。
1.1 定义一个函数模板
Local<FunctionTemplate> exampleFunctionTemplate = v8::FunctionTemplate::New(isolate(), New);
// 定义函数的类名
exampleFunctionTemplate->SetClassName(‘TCP’)
首先定义一个FunctionTemplate对象。我们看到FunctionTemplate的第二个入参是一个函数,当我们执行由FunctionTemplate创建的函数时,v8就会执行New函数。当然我们也可以不传。
1.2 定义函数模板的prototype内容
prototype就是js里的function.prototype。如果你理解js里的知识,就很容易理解c++的代码。
v8::Local<v8::FunctionTemplate> t = v8::FunctionTemplate::New(isolate(), callback);
t->SetClassName('test');
// 在prototype上定义两个属性
exampleFunctionTemplate->PrototypeTemplate()->Set('name', t);
exampleFunctionTemplate->PrototypeTemplate()->Set('hello', 'world');
1.3 定义函数模板对应的实例模板的内容
实例模板就是一个ObjectTemplate对象。他定义了,当以new的方式执行由函数模板创建出来的函数时的返回值。
function A() {
this.a = 1;
this.b = 2;
}
new A();
实例模板类似上面代码中A函数里面的代码。我们看看在v8里怎么定义。
t->InstanceTemplate()->Set(key, val);
t->InstanceTemplate()->SetInternalFieldCount(1);
InstanceTemplate返回的是一个ObjectTemplate对象。SetInternalFieldCount这个函数比较特殊,也是比较重要的一个地方,我们知道对象就是一块内存,对象有他自己的内存布局,我们知道在c++里,我们定义一个类,也就定义了对象的布局。比如我们有以下定义。
class demo
{
private:
int a;
int b;
};
在内存中布局如下。 上面这种方式有个问题,就是类定义之后,内存布局就固定了。而v8是自己去控制对象的内存布局的。当我们在v8中定义一个类的时候,是没有任何属性的。我们看一下v8中HeapObject类的定义。
class HeapObject: public Object {
static const int kMapOffset = Object::kSize; // Object::kSize是0
static const int kSize = kMapOffset + kPointerSize;
};
这时候的内存布局如下。 然后我们再看一下HeapObject子类HeapNumber的定义。
class HeapNumber: public HeapObject {
// kSize之前的空间存储map对象的指针
static const int kValueOffset = HeapObject::kSize;
// kValueOffset - kSize之间存储数字的值
static const int kSize = kValueOffset + kDoubleSize;
};
内存布局如下
我们发现这些类只有几个类变量,类变量是不保存在对象内存空间的。这些类变量就是定义了对象每个域所占内存空间的信息,当我们定义一个HeapObject对象的时候,v8首先申请一块内存,然后把这块内存首地址强行转成对应对象的指针。然后通过类变量对属性的内存进行存取。我们看看在v8里如何申请一个HeapNumber对象
Object* Heap::AllocateHeapNumber(double value, PretenureFlag pretenure) {
// 在哪个空间分配内存,比如新生代,老生代
AllocationSpace space = (pretenure == TENURED) ? CODE_SPACE : NEW_SPACE;
// 在space上分配一个HeapNumber对象大小的内存
Object* result = AllocateRaw(HeapNumber::kSize, space);
/*
转成HeapObect,设置map属性,map属性是表示对象类型、大小等信息的
*/
HeapObject::cast(result)->set_map(heap_number_map());
// 转成HeapNumber对象
HeapNumber::cast(result)->set_value(value);
return result;
}
回到对象模板的问题。我们看一下对象模板的定义。
class TemplateInfo: public Struct {
static const int kTagOffset = HeapObject::kSize;
static const int kPropertyListOffset = kTagOffset + kPointerSize;
static const int kHeaderSize = kPropertyListOffset + kPointerSize;
};
class ObjectTemplateInfo: public TemplateInfo {
static const int kConstructorOffset = TemplateInfo::kHeaderSize;
static const int kInternalFieldCountOffset = kConstructorOffset + kPointerSize;
static const int kSize = kInternalFieldCountOffset + kHeaderSize;
};
内存布局如下
回到对象模板的问题,我们看看Set(key, val)做了什么。
void Template::Set(v8::Handle<String> name, v8::Handle<Data> value,
v8::PropertyAttribute attribute) {
// ...
i::Handle<i::Object> list(Utils::OpenHandle(this)->property_list());
NeanderArray array(list);
array.add(Utils::OpenHandle(*name));
array.add(Utils::OpenHandle(*value));
array.add(Utils::OpenHandle(*v8::Integer::New(attribute)));
}
上面的代码大致就是给一个list后面追加一些内容。我们看看这个list是怎么来的,即property_list函数的实现。
// 读取对象中某个属性的值
#define READ_FIELD(p, offset) (*reinterpret_cast<Object**>(FIELD_ADDR(p, offset))
static Object* cast(Object* value) {
return value;
}
Object* TemplateInfo::property_list() {
return Object::cast(READ_FIELD(this, kPropertyListOffset));
}
从上面代码中我们知道,内部布局如下。
根据内存布局,我们知道property_list的值是list指向的值。所以Set(key, val)操作的内存并不是对象本身的内存,对象利用一个指针指向一块内存保存Set(key, val)的值。SetInternalFieldCount函数就不一样了,他会影响(扩张)对象本身的内存。我们来看一下他的实现。
void ObjectTemplate::SetInternalFieldCount(int value) {
// 修改的是kInternalFieldCountOffset对应的内存的值
Utils::OpenHandle(this)->set_internal_field_count(i::Smi::FromInt(value));
}
我们看到SetInternalFieldCount函数的实现很简单,就是在对象本身的内存中保存一个数字。接下来我们看看这个字段的使用。后面会详细介绍他的用处。
Handle<JSFunction> Factory::CreateApiFunction(
Handle<FunctionTemplateInfo> obj,
bool is_global) {
int internal_field_count = 0;
if (!obj->instance_template()->IsUndefined()) {
// 获取函数模板的实例模板
Handle<ObjectTemplateInfo> instance_template = Handle<ObjectTemplateInfo>(ObjectTemplateInfo::cast(obj->instance_template()));
// 获取实例模板的internal_field_count字段的值(通过SetInternalFieldCount设置的那个值)
internal_field_count = Smi::cast(instance_template->internal_field_count())->value();
}
// 计算新建对象需要的空间,如果
int instance_size = kPointerSize * internal_field_count;
if (is_global) {
instance_size += JSGlobalObject::kSize;
} else {
instance_size += JSObject::kHeaderSize;
}
InstanceType type = is_global ? JS_GLOBAL_OBJECT_TYPE : JS_OBJECT_TYPE;
// 新建一个函数对象
Handle<JSFunction> result =
Factory::NewFunction(Factory::empty_symbol(), type, instance_size,
code, true);
}
我们看到internal_field_count的值的意义是,会扩张对象的内存,比如一个对象本身只有n字节,如果定义internal_field_count的值是1,对象的内存就会变成n+internal_field_count * 一个指针的字节数。内存布局如下。 1.4 通过函数模板创建一个函数
AFunctionTemplate->GetFunction();
global->Set('demo', AFunctionTemplate->GetFunction());
这样我们就可以在js里直接调用demo这个变量,然后对应的函数就会被执行。这就是js调用c++的原理。
2 nodejs是如何处理js调用c++问题的
nodejs没有给每个功能定义一个全局变量,而是通过另外一种方式实现js调用c++。我们以tcp模块为例。在tcp_wrap.cc文件最后有一句代码
NODE_BUILTIN_MODULE_CONTEXT_AWARE(tcp_wrap, node::TCPWrap::Initialize)
这是一个宏,展开后如下
#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的函数。这个函数在nodejs初始化的时候会执行
void RegisterBuiltinModules() {
// 宏展开后就是执行一系列的_register_xxx函数
#define V(modname) _register_##modname();
NODE_BUILTIN_MODULES(V)
#undef V
}
我们看到_register_ xxx函数执行了
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加入到链表中。完成了模块的注册。我们来看看如何使用这个模块。
constant { TCP } = process.binding('tcp_wrap');
new TCP(...);
我们看到nodejs是通过process.binding来实现c++模块的调用的。nodejs通过定义一个全局变量process统一处理c++模块的调用,而不是定义一堆全局对象。下面我们看process.binding的实现,跳过nodejs的缓存处理,直接看c++的实现。
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());
// 执行nm_context_register_func函数,就是tcp_wrap.cc的Initialize函数
mod->nm_context_register_func(exports,
unused,
env->context(),
mod->nm_priv);
return exports;
}
static void Binding(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
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 = InitModule(env, mod, module);
args.GetReturnValue().Set(exports);
}
v8中,js调用c++函数的规则是函数入参const FunctionCallbackInfo<Value>& args(拿到js传过来的内容)和设置返回值args.GetReturnValue().Set(给js返回的内容);binding函数的逻辑就是执行对应的模块钩子函数,并有一个exports变量传进去,然后钩子函数会修改exports的值,该exports的值就是js层能拿到的值。最后我们来看看tcp_wrap.cc的Initialize。
void TCPWrap::Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context) {
Environment* env = Environment::GetCurrent(context);
/*
new TCP时,v8会新建一个c++对象(根据InstanceTemplate()模板创建的对象),然后传进New函数,
然后执行New函数,New函数的入参args的args.This()就是该c++对象
*/
Local<FunctionTemplate> t = env->NewFunctionTemplate(New);
Local<String> tcpString = FIXED_ONE_BYTE_STRING(env->isolate(), "TCP");
t->SetClassName(tcpString);
t->InstanceTemplate()->SetInternalFieldCount(1);
t->InstanceTemplate()->Set(env->owner_string(), Null(env->isolate()));
// ...
// 在target(即exports对象)中注册该函数
target->Set(tcpString, t->GetFunction());
上面就定义了我们在js层可以拿到的值。今天就分析到这,后续再补充。 如果你对nodejs感兴趣欢迎关注我的公众号。