和fibjs同居的日子(二) 之fibjs和v8的接口交互
一、V8引擎概览
V8是Google团队开发的高性能JavaScript开源解析引擎, 它采用c++开发并且被应用在开源的浏览器Google Chrome中。它实现了描述在-262当中的ECMAScript规范。它可以运行在WindowsXp及以上的系统, 不小于MacOsX10.5的系统, 和使用IA-32, ARM或者MIPS处理器的linux系统。V8可以独立运行,你也可以把它嵌入到你的c++应用当中去。
二、v8和自定义c++类的交互
1、基础概念
图 . V8 引擎基本概念关系图
-
handle
handle 是指向对象的指针,在 V8 中,所有的对象都通过 handle 来引用,handle 主要用于 V8 的垃圾回收机制。
在 V8 中,handle 分为两种:持久化 (Persistent)handle 和本地 (Local)handle,持久化 handle 存放在堆上,而本地 handle 存放在栈上。这个与 C/C++ 中的堆和栈的意义相同 ( 简而言之,堆上的空间需要开发人员自己申请,使用完成之后显式的释放;而栈上的为自动变量,在退出函数 / 方法之后自动被释放 )。持久化 handle 与本地 handle 都是 Handle 的子类。在 V8 中,所有数据访问均需要通过 handle。需要注意的是,使用持久化 handle 之后,需要显式的调用 Dispose() 来通知垃圾回收机制。
-
作用域 (scope)
scope 是 handle 的集合,可以包含若干个 handle,这样就无需将每个 handle 逐次释放,而是直接释放整个 scope。
在使用本地 handle 时,需要声明一个 HandleScope 的实例,scope 是 handle 的容器,使用 scope,则无需依次释放 handle。
HandleScope handle_scope;
Local<ObjectTemplate> temp;
-
上下文 (context)
context 是一个执行器环境,使用 context 可以将相互分离的 JavaScript 脚本在同一个 V8 实例中运行,而互不干涉。在运行 JavaScript 脚本是,需要显式的指定 context 对象。
关于javascript的handle句柄, handle scope, context等概念我就不介绍了,可以先google一下。
2、步骤
c++类暴露给javascript环境使用,一般包括以下几个步骤:
-
1、声明类
class className{
public:
className(){}
int getProperty(){}
void setProperty(int property) {}
private:
int property;
};
-
2、创建函数模板实例
Local<FunctionTemplate> classTemplate = FunctionTemplate::New(isolate, className);
-
3、设置类名
classTemplate.SetClassName(className);
-
4、 然后定义原型模板
v8::Local<v8::ObjectTemplate> protoTemplate = classTemplate ->PrototypeTemplate();
protoTemplate ->Set("getProperty", FunctionTemplate::New(ClassGetProperty));
protoTemplate ->Set("setProperty", FunctionTemplate::New(ClassSetProperty));
其中ClassGetProperty 这是函数是原型的访问器它用来包裹c++类的方法后面我们会讲到。这里的prototypetemplate对应于javascript的函数的prototype对象。所有该类创建的对象都会继承setProperty和getProperty方法。
例如var obj = new className();
obj.getProperty();
obj.setProperty();
如上代码是正确的。
-
5、设置实例模板:
v8::Local<v8::ObjectTemplate> instanceTemplate = _class->InstanceTemplate();
instanceTemplate.SetInternalFieldCount(1);
-
6、最后把函数模板实例放入对象
v8::Local<v8::Object> o = v8:: Object ::new(isolate);
o.set(className, classTemplate);
-
7、编写javascript代码
var class = new className();
class.setProperty();
class.getProperty();
3、参考资料
关于和c++的交互请参考V8 embedder's guide: https://developers.google.com/v8/embed
这一篇博文也很有帮助
使用 Google V8 引擎开发可定制的应用程序 http://www.ibm.com/developerworks/cn/opensource/os-cn-v8engine/ 这里使用的接口参数现在已经发生了变化, 但是对理解本文还是有很大帮助的。
三、fibjs中定义的c++类是如何暴露给javascript使用的
本文如无特殊声明, c++类特指需要暴露给javascript环境使用的c++类,如fs_base、http_base等。
1、fibjs初始化。
fibjs在开始运行时会创建一个fibjs运行实例:
// fibjs/src/base/fibjs.cpp
Isolate* isolate = new Isolate(fname);
它对v8实例进行了封装,里面创建了fiber来执行初始化工作:
// fibjs/src/base/Runtime.cpp
isolate->init();
紧接着就创建了一个SandBox对象并初始化根沙箱环境:
// fibjs/src/base/Runtime.cpp
m_topSandbox = new SandBox();
m_topSandbox->initRoot();
在initRoot方法里面我们定义的所有c++类都被安装了,即将c++类及其成员和方法暴露给javascript运行环境。
// fibjs/src/sandbox/SandBox.cpp
RootModule* RootModule::g_root = NULL;
void SandBox::initRoot()
{
Isolate* isolate = holder();
RootModule* pModule = RootModule::g_root;
while (pModule)
{
ClassInfo& ci = pModule->class_info();
InstallModule(ci.name(), ci.getFunction(isolate));
pModule = pModule->m_next;
}
InstallModule("expect", createV8Function("expect", isolate->m_isolate, test_base::s_expect));
}
通过RootModule来实现所有c++类的安装。
// fibjs/include/object.h
class RootModule
{
public:
RootModule()
{
m_next = g_root;
g_root = this;
}
public:
virtual ClassInfo &class_info() = 0;
public:
RootModule* m_next;
static RootModule* g_root;
};
fibjs在每一个c++类实现的地方定义了一个RootModule的子类并重写了class_info这个虚函数, 以fs_base为例,在fs.cpp中有这样一行:
DECLARE_MODULE(fs);
它是一个宏定义:
// fibjs/include/utils.h
#define DECLARE_MODULE(name) \
class RootModule_##name : public RootModule \
{ \
public: \
virtual ClassInfo &class_info() \
{ \
return name##_base::class_info(); \
} \
} s_RootModule_##name;
展开来如下所示:
class RootModule_fs : public RootModule
{
public:
virtual ClassInfo &class_info()
{
return name_fs::class_info();
}
} s_RootModule_fs;
这是一个类声明和定义, 在程序开始运行并初始化时就执行了父类的构方法。这样这个对象就被静态变量RootModule::g_root所指向了, 当main函数执行的时候, 这里就行成了一个单链表, RootModule::g_root声明为null, 通过成员m_next来指向, 使用头插法构造了一个单链表, 最终g_root指向表头。
在initRoot方法中,在while循环里取单链表里的内容执行class_info()。利用多态特性真正执行的是fs_base::class_info(), 返回了一个ClassInfo对象。
fs_base::class_info().getFunction返回了一个javascript实例也就是我们所说的c++类的v8包裹。通过InstallModule把这个实例注册到javascript对象上。
2、ClassInfo::getFunction()
CassInfo对象在每个c++类声明的时候创建:
// fibjs/include/ifs/fs.h
inline ClassInfo& fs_base::class_info()
{
static ClassData::ClassMethod s_method[] =
{
{"exists", s_exists, true},
{"unlink", s_unlink, true},
{"umask", s_umask, true},
{"openTextStream", s_openTextStream, true},
{"readFile", s_readFile, true},
{"readLines", s_readLines, true},
{"writeFile", s_writeFile, true}
};
static ClassData::ClassProperty s_property[] =
{
{"SEEK_SET", s_get_SEEK_SET, block_set, true},
{"SEEK_CUR", s_get_SEEK_CUR, block_set, true},
{"SEEK_END", s_get_SEEK_END, block_set, true}
};
static ClassData s_cd =
{
"fs", s__new, NULL,
15, s_method, 0, NULL, 3, s_property, NULL, NULL,
NULL
};
//创建ClassInfo对象
static ClassInfo s_ci(s_cd);
return s_ci;
}
其中ClassMethod、ClassProperty等类都是用来包裹c++类方法和类属性的。后面会讲到。在调用getFunction的时候把c++类的构造方法、成员变量和成员方法都按照v8提供的接口进行包装。返回javascript函数模板实例并设置到object上完成交互。
// fibjs/include/ClassInfo.h
v8::Local<v8::Function> getFunction(Isolate* isolate)
{
cache* _cache = _init(isolate);
return v8::Local<v8::Function>::New(isolate->m_isolate, _cache->m_function);
}
对照第二章的步骤基本可以理解fibjs的c++类和v8交互了。在ClassInfo::getFunction中是通过_init()这个方法实现的。下面讲解以注释的形式来。
cache* _init(Isolate* isolate)
{
if (m_id < 0)
m_id = (int32_t)isolate->m_classInfo.size();
void* p = NULL;
while ((int32_t)isolate->m_classInfo.size() < m_id + 1)
isolate->m_classInfo.append(p);
cache* _cache = (cache*)isolate->m_classInfo[m_id];
if (_cache)
return _cache;
isolate->m_classInfo[m_id] = _cache = new cache();
// 使用c++类构造方法创建函数模板, m_cd.cor是c++类的构造方法。
v8::Local<v8::FunctionTemplate> _class = v8::FunctionTemplate::New(
isolate->m_isolate, m_cd.cor);
_cache->m_class.Reset(isolate->m_isolate, _class);
// 给函数模板设置名称
_class->SetClassName(isolate->NewFromUtf8(m_cd.name));
if (m_cd.base)
{
// 递归查找c++的父类,并继承父类
cache* _cache1 = m_cd.base->_init(isolate);
_class->Inherit(
v8::Local<v8::FunctionTemplate>::New(isolate->m_isolate,
_cache1->m_class));
}
// 创建原型模板
v8::Local<v8::ObjectTemplate> pt = _class->PrototypeTemplate();
int32_t i;
// 给原型模板设置方法, 即把c++类方法包裹器注册到原型模板上。这里的invoker就是上面fs_base::s_exists或其他方法
for (i = 0; i < m_cd.mc; i++)
pt->Set(isolate->NewFromUtf8(m_cd.cms[i].name),
v8::FunctionTemplate::New(isolate->m_isolate, m_cd.cms[i].invoker),
(v8::PropertyAttribute)(v8::ReadOnly | v8::DontDelete));
// 给原型模板设置访问器, 这里是把其他c++类对象的包裹作为原型模板的对象属性。
for (i = 0; i < m_cd.oc; i++)
{
cache* _cache1 = m_cd.cos[i].invoker()._init(isolate);
pt->Set(isolate->NewFromUtf8(m_cd.cos[i].name),
v8::Local<v8::FunctionTemplate>::New(isolate->m_isolate, _cache1->m_class),
(v8::PropertyAttribute)(v8::ReadOnly | v8::DontDelete));
}
// 给原型设置属性,以及属性的访问器。这里的getter和setter就是上面的s_get_SEEK_SET和block_set。
for (i = 0; i < m_cd.pc; i++)
pt->SetAccessor(isolate->NewFromUtf8(m_cd.cps[i].name),
m_cd.cps[i].getter, m_cd.cps[i].setter,
v8::Local<v8::Value>(), v8::DEFAULT, v8::DontDelete);
// 创建实例模板
v8::Local<v8::ObjectTemplate> ot = _class->InstanceTemplate();
ot->SetInternalFieldCount(1);
ClassData *pcd;
pcd = &m_cd;
while (pcd && !pcd->cis)
pcd = pcd->base ? &pcd->base->m_cd : NULL;
// 给含有索引属性拦截器的实例模板设置拦截器,它会在访问索引属性时被调用
if (pcd)
ot->SetIndexedPropertyHandler(pcd->cis->getter, pcd->cis->setter);
pcd = &m_cd;
while (pcd && !pcd->cns)
pcd = pcd->base ? &pcd->base->m_cd : NULL;
// 给含有具名属性拦截器的实例模板设置拦截器,它会在访问名称为字符串的属性时被调用
if (pcd)
ot->SetNamedPropertyHandler(pcd->cns->getter, pcd->cns->setter,
NULL, pcd->cns->remover, pcd->cns->enumerator);
if (m_cd.caf)
ot->SetCallAsFunctionHandler(m_cd.caf);
// 调用模板的GetFunction 方法来创建一个模板的 JavaScript 实例
v8::Local<v8::Function> _function = _class->GetFunction();
// 设置实例的方法、属性、对象
Attach(isolate, _function);
_cache->m_function.Reset(isolate->m_isolate, _function);
if (m_cd.cor)
{
v8::Local<v8::Object> o = _function->NewInstance();
o->SetAlignedPointerInInternalField(0, 0);
_cache->m_cache.Reset(isolate->m_isolate, o);
}
// 返回c++类的包裹。
return _cache;
}
最后看InstallModule
// fibjs/src/sandbox/SandBox.cpp
void SandBox::InstallModule(exlib::string fname, v8::Local<v8::Value> o)
{
mods()->Set(holder()->NewFromUtf8(fname), o);
}
mods()
v8::Local<v8::Object> mods()
{
Isolate* isolate = holder();
v8::Local<v8::Value> v = isolate->GetPrivate(wrap(), "_mods");
v8::Local<v8::Object> o;
if (!v->IsUndefined())
o = v->ToObject();
else
{
o = v8::Object::New(isolate->m_isolate);
isolate->SetPrivate(wrap(), "_mods", o);
}
return o;
}
这返回的是一个v8::Local<v8::Object>对象,对应第二章步骤的第6步。完成了安装。
四、访问器对c++方法的包装
上面提到把访问器set到原型模板上。我这里以Buffer为例。解析它的slice方法是如何包裹的。
首先Buffer的用法是这样的
var buf = new Buffer('hello');
buf.slice(1)
从fibjs/include/ifs/buffer.h中可以看到ClassMethod、ClassProperty、ClassData这些类,它们把访问器封装起来用于统一注册到函数原型模板上。查看ClassMethod类和Buffer_base::classinfo()可以看到slice的访问器是s_slice也就是上文_init()代码中的m_cd.cms[i].invoker。它的定义是
inline void Buffer_base::s_slice(const v8::FunctionCallbackInfo<v8::Value>& args)
{
obj_ptr<Buffer_base> vr;
METHOD_INSTANCE(Buffer_base);
METHOD_ENTER(1, 0);
OPT_ARG(int32_t, 0, 0);
hr = pInst->slice(v0, vr);
METHOD_OVER(2, 2);
ARG(int32_t, 0);
ARG(int32_t, 1);
hr = pInst->slice(v0, v1, vr);
METHOD_RETURN();
}
我把宏定义展开得到
可以看到通过GetArgumentValue()获取c++函数参数, pInst->slice()执行c++函数,通过args.GetReturnValue().set(V8_RETURN(GetReturnValue(isolate, vr)))来设置返回值。
而最终要的就是pInst->slice这个方法。
实现这个方法就实现了整个c++类暴露给javascript运行环境的过程。
这个方法定义在fibjs/src/global/Buffer.cpp中。
五、总结
不要担心,不要觉得复杂,以上的代码都已经被集成到工具里面通过idl文件自动生成,当我们想自定义一个fibjs模块(c++类)的时候, 我们只需要关心上文中的最后一个步骤那就是slice是怎么实现的。下一篇文章就给大家介绍如何编写自己的fibjs模块。