想要开发一个 Node 的 C++ 扩展,首推死月大神的《Node.js:来一打 C++ 扩展》。钻研了一个多月,记录一些心得如下:
开发环境
首先自然是安装 Node,推荐安装 nvm,这样就可以随时切换不同的 Node 版本。另外,为了编译后的扩展在 32 位系统也能使用,最好是安装 32 bit 的 Node。nvm 的指令可以指定,如:nvm install 10.15.1 32
。
然后就是安装 node-gyp,根据其安装教程,Windows 下除了安装 node-gyp :npm i -g node-gyp
,还需要在管理员权限下安装 windows-build-tools :npm install --global --production windows-build-tools
。
安装完成之后,应该启动时(在 CMD 执行 node-gyp)可能会报错误:
if not defined npm_config_node_gyp (node “……\node_modules\node-gyp\bin\node-gyp.js” ) else (node “” )
internal/modules/cjs/loader.js:583
throw err;
^Error: Cannot find module ‘……\npm\node_modules\node_modules\node-gyp\bin\node-gyp.js’
这个错误只要设置一下 npm_config_node_gyp
的环境变量就可以了。值就是node-gyp.js
的路径,通常在 Node 安装目录下的 \node_modules\npm\node_modules\node-gyp\bin\node-gyp.js
。到 Windows 的环境变量新增一下就行。
开发流程
一个 Node C++ 扩展,包含三个部分:
- 实际功能代码;
- 扩展入口代码(addon.cc);
- 编译配置(binding.gyp);
这里我们以 ObjectWrap
的方式开发一个 Windows ini C++ 扩展为例子作为说明。下面为目录结构:
├─inc
| ├─win-ini.h
│ └─win-ini.cpp
├─src
| ├─ini.h
| ├─ini.cc
│ └─addon.cc
└─binding.gyp
相关代码可以查看 Github
1. 编写 binding.gyp
binding.gyp
相当于一个 makefile,用于定义项目的依赖项,编译的 C++ 文件等等。详细文档可以查看 GYP Document。
在 C++ Node 扩展的项目里,必须设置 target_name
(代表编译后的文件名),sources
(代表需包含编译的文件)。此外还可以设置 include_dirs
头文件的包含文件夹。如果有需要包含动态链接库,还需要设置 library_dirs
。
那么,对于我们的示例项目,可以如下配置:
{
"targets": [{
"target_name": "ini",
"sources": [ "src/addon.cc", "src/ini.cc", "inc/win-ini.cpp", "inc/common.cpp" ],
"include_dirs": [
"inc",
]
}]
}
所有的 .cc
和 .cpp
文件都需要加入到 sources
里。
2. 编写转接类
假设你已经有一个 C++ 的 Class,例如 Ini。那么我们要将其转为一个 Node 的 C++ 扩展,最简单的方式是做一个类型的转接。将 v8 的类型转为 C++ 的类型。以如下 Class 为例:
class Ini
{
public:
Ini(const TCHAR* pszIniFile/*full path*/);
void SetFile(const TCHAR* pszIniFile/*full path*/);
bool GetAllKeys(TCHAR* pszAppName, TCHAR* pszKeys[], int& nCount);
bool GetAllApps(TCHAR* pszAppName[], int& nCount);
// read ini
bool Read(TCHAR* pszAppName, TCHAR* pszKeyName, TCHAR* pszValue, int nMaxSize=1024);
// write ini
bool Write(TCHAR* pszAppName, TCHAR* pszKeyName, TCHAR* pszValue);
bool Remove(TCHAR* pszAppName, TCHAR* pszKeyName);
bool Remove(TCHAR* pszAppName);
private:
TCHAR m_szIni[MAX_PATH];
};
那么这里主要有三个地方要注意,1. 构造函数;2. 函数形参;3. 返回值。把这三者搞定就可以了。这里我们基于 ObjectWrap
来写这个类。
class IniNode : public ObjectWrap {
public:
// 初始化类
static void Init(Isolate* isolate);
// 类入口
static void NewInstance(const FunctionCallbackInfo<Value>& args);
private:
// 构造函数
explicit IniNode(Local<String> value);
~IniNode();
// 构造函数转化
static void New(const FunctionCallbackInfo<Value>& args);
// 类模板
static Persistent<Function> constructor;
// ini 路径
String::Utf8Value m_szIni;
// 非 Node C++ 扩展 ini 类实例
Ini m_ini;
}
首先我们把构造函数写一下,基本上把变量初始化,然后给到我们自己的 ini 类直接初始化即可:
IniNode::IniNode(Local<String> value)
: m_szIni(value), m_ini(*m_szIni)
{
}
然后,我们要编写初始化函数,用来设定类模板,此部分会影响到 C++ 扩展的对外接口:
void IniNode::Init(Isolate* isolate)
{
// 创建类模板
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
// 设置类名
tpl->SetClassName(String::NewFromUtf8(isolate, "IniNode"));
// 设置类对外暴露的函数或成员计数
tpl->InstanceTemplate()->SetInternalFieldCount(1);
// 设置对外函数,名字为 getPath
NODE_SET_PROTOTYPE_METHOD(tpl, "setPath", setPath);
// 设置类模板
constructor.Reset(isolate, tpl->GetFunction());
}
New
是 Node 实际调用的接口,通过 New
我们生成一个类实例。如果构造函数有参数,我们还需要取得参数。返回参数就是实例对象。
void IniNode::New(const FunctionCallbackInfo<Value>& args)
{
// 获取参数
Local<String> str = args[0]->IsString() ? Local<String>::Cast(args[0]) : String::NewFromUtf8(Isolate::GetCurrent(), "");
// 实例化对象
IniNode* obj = new IniNode(str);
// 将对象存入 this
obj->Wrap(args.This());
// 返回 this
args.GetReturnValue().Set(args.This());
}
这样当我们在 Node.js 里面引用扩展的似乎,就可以用一个变量接收实例了:
const ini = require('ini')
let ini_obj = ini('your_ini_path')
这样基础的结构就搭出来了,然后我们要实现一下 Init
里的 setPath
接口。我们需要定义两个 setPath
的函数。
const char* setPath(const char* szPath);
private:
static void setPath(const FunctionCallbackInfo<Value>& args);
私有的 setPath
就是 Init
里实际设置的接口,Node 调用函数时,会实际调用这个函数。FunctionCallbackInfo
这是包含了函数调用的一些上下文信息,包含入参和返回值。
void IniNode::setPath(const FunctionCallbackInfo<Value>& args)
{
Isolate* isolate = args.GetIsolate();
// 获取第一个入参
String::Utf8Value path(Local<String>::Cast(args[0]));
// 获取对象指针
IniNode* obj = ObjectWrap::Unwrap<IniNode>(args.Holder());
// 调用实际的 setPath 并返回执行结果,返回值类型为 String
args.GetReturnValue().Set(String::NewFromUtf8(isolate, obj->setPath(*path)));
}
而实际的 setPath
这只要转接一下即可。
const char* IniNode::setPath(const char* szPath)
{
m_ini.SetFile(szPath);
return szPath;
}
其他的函数也是类似的做法。
3. 编写入口 addon.cc
编写完成类之后,就可以写入口了。编写一个入口初始化函数:
void InitAll(Local<Object> exports, Local<Object> module)
{
// 初始化类模板
IniNode::Init(exports->GetIsolate());
// 导出扩展入口
NODE_SET_METHOD(module, "exports", IniNode::NewInstance);
}
然后用宏定义 NODE_MODULE
设置即可:
NODE_MODULE(addon1, InitAll)
如此,就完成了一个 Node C++ 扩展的编写。
4. 编译生成与调用
依次执行两条指令:
# 生成对应平台的工程项目
node-gyp configure
# 编译生成 node C++ 扩展
node-gyp build
编译成功后,就可以在 build/Release
里找到一个 .node
的后缀名的文件。这就是 Node C++ 扩展,可以用相对路径的方式 require
之后使用。例如:
const ini = require('ini')
let ini_obj = ini('your_ini_path')
let ini_path = ini_obj.setPath('new_ini_path')
console.log(ini_path) // new_ini_path