场景:之前做了一个弹幕姬,其中有一个选择字体的功能,但有个不太舒服的地方是js如果要获取可用字体只能一个一个试,这种方法不可能做到完美地列出所有可用字体,而且性能不佳,所以我就需要找到一个可以直接获取系统可用字体的方法,这里选择了使用更底层的语言C++来实现这个功能。
开始
首先我们了解一下Node的原生插件Node Native Addon,它像一群.js文件一样,是一个包装了这些文件的集合,也就是说,我们可以把我们所以要的逻辑包含在这个集合中,但是我们知道,C++和JS的代码原本是不兼容的,互用是不可能的,但是它们所实现的功能在更底层是共通的,比如当C++编译为.exe文件以后,它就仅仅只是一个逻辑的二进制表示,JS在执行的过程中有点类似(?),总之,在更底层,我们可以通过内存地址让二者通信,这给了我们一个实现用C++为Node编写插件的思路。
这里还有个概念——动态链接库(Dynamically Linked Library),也称DLL,我们经常会在Windows系统中看到扩展名为.dll的文件,这就是动态链接库,动态链接库会在程序运行时被载入,它包含了编译后的C/C++原生代码和API,可以实现与程序本体的通信。
那么Node是如何工作的呢?
Node使用Google开源的v8引擎作为默认JS引擎,为了实现事件循环、异步输入I/O操作,它使用了 Libuv 库。
这和我们要编写的插件有什么关系?
如果我们点到这两个库里去,会发现他们一个使用C++编写,一个使用C编写,暂且不提V8,Libuv作为Node使用的一个库,他竟然是使用C编写的?这意味着Node可以使用C编写的库。
具体一点怎么实现?
动态链接库是程序运行时被动态载入的,我们的插件也有这个特点,它作为我们程序本体外的一个存在,本身和我们的程序是完全分离的,所以我们需要在程序运行的时候使用Node提供的接口动态载入我们的插件。
动态载入插件,然后呢?
这里需要提到一个概念,Application Binary Interface (ABI),平时我们说的都是API,这个ABI是什么玩意?其实和API很像,只是ABI通过内存地址实现通讯,这在开头提过了,这是我们实现插件的手段。具体一点,当插件在内存中创建了【对象/类/变量/方法】之后,Node可以通过内存地址去访问这些创建好了的【对象/类/变量/方法】,由此获取插件实现的功能。
总结
- 创建插件,并将它编译为动态链接库
- 使用Node与动态链接库通信,获取其实现的逻辑或变量
那么问题来了,与JS不同,编译后的C++源码并不会保留函数名、变量名、类名这类东西,如果我们要在JS中使用一个在C++中实现的sayHello方法该怎么整?
不要方,Node给了我们实现这个的api,它是一堆C++的库
所以呢,我们需要先安装一下这个玩意,首先创建项目文件夹,并初始化项目。
打开终端
mkdir greet
cd ./greet
npm init -y
安装Napi,这是Node提供给我们的一个库
npm install -S node-addon-api
创建源文件文件夹
mkdir src
cd ./src
创建源文件 greet.h greet.cpp,如下
//greet.h
#include<string>
std::string helloUser( std::string name );
//greet.cpp
#include"greet.h"
#include<iostream>
std::string helloUser(std::string name){
return "Hello " + name + "!";
}
定睛一看这不就是个平平无奇的C++代码嘛?
是的,但我们的目的是让它能为Node服务
来回想一下,如果我们要调用一个函数,需要知道些什么?
- 函数名
- 形参
- 返回值类型
Napi所做的正是定义这些,让我们再创建一个文件index.cpp,我们在这个文件中定义这些
#include<napi.h>
#include<string>
#include"greet.h"
Napi::String greetHello(const Napi::CallbackInfo& info){
Napi::Env env = info.Env();
std::string name = (std::string)info[0].ToString();
std::string result = helloUser(name);
return Napi::String::New(env, result);
}
Napi::Object Init(Napi::Env env, Napi::Object exports){
exports.Set(
Napi::String::New(env, "greet"),
Napi::Function::New(env, greetHello)
);
return exports;
}
NODE_API_MODULE(greet, Init);
这个看起来有点多,但实质上还是很简单的,首先我们使用Napi的数据类型定义了一个greetHello方法,它的形参是一个info,这个里面有我们调用方法时传进来的参数,所以可以看到
std::string name = (std::string)info[0].ToString();
这一行代码我们获取了传进来的参数——用户名,info[*]会返回info中的参数列表中的某一个参数,我们把它转换为std::string类型,再讲其传入我们一开始定义的函数helloUser中
std::string result = helloUser(name);
获取了处理结果,最后再返回结果。
好了,只是把我们的逻辑使用Napi提供的数据类型再处理一遍,这样可以避免杂七杂八的数据类型,使得其更加规范。
再看下一个,Init,这是啥玩意?
顾名思义,我们通过Init来初始化插件,里面做的就是我们上面提到的,提供函数名和逻辑
exports.Set(
Napi::String::New(env, "greet"),
Napi::Function::New(env, greetHello)
);
这是最关键的,它告诉了Node,我们这个是个函数,它的名字是greet,它通过greetHello实现逻辑。现在,我们完美解决了上面的疑惑:怎么在JS中调用C++的方法
最后,我们通过一个宏函数
NODE_API_MODULE(greet, Init);
定义了一个Node模块greet,它的初始化方法是Init
预备工作
回到根目录下
cd ..
这里我们需要提前安装python解释器与vc++构建工具,这些大家自行搜索安装吧,装好后,我们再安装node-gyp,这是node提供给我们的编译插件用的脚手架
npm install -g node-gyp
配置
像package.json一样,我们的插件也得有个放自己配置的文件,它叫binding.gyp,内部如下
{
"targets": [
{
"target_name": "greet",
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"sources": [
"./src/greet.cpp",
"./src/index.cpp"
],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
}
]
}
targets就是编译目标,我们这里就一个greet模块
target_name就是模块名,这需要与宏函数中的模块名一致
sources是源文件列表
include_dirs是给node用的,node需要require相应模块,我们这里用了node-addon-api模块,那就安排上
其他的就不具体解释了,自行搜索就好
随后,我们使用node-gyp创建配置模板
node-gyp configure
会发现多出来一个文件夹 ./build,这是node-gyp生成的模板,不用管了
编译
node-gyp build
这么简单暴力的一行命令后,会多出来一个文件夹
./build/Release
这里头就是编译后的文件了,我们需要的是以.node为扩展名的那个
我们建一个index.js用来测试
const greetModule = require('./build/Release/greet.node');
console.log('module:', greetModule);
console.log('hello:', greetModule.greet('Yeuoly'));
发现输出
module: { greet: [Function],
path:
'I:\\H\\Shared\\Lib\\YeuolyDanmuNodeModules\\build\\Release\\greet.node' }
hello: Hello Yeuoly!
成功完成了我们用C++实现的逻辑,但是这里有个问题,路径问题,不同电脑上这个地方会出大问题,所以我们需要一个用来避免这个的东西,那就是bindings,这是个插件
npm install -S bindings
装完之后,更改index.js
const greetModule = require('bindings')('greet');
console.log('module:', greetModule);
console.log('hello:', greetModule.greet('Yeuoly'));
再次执行,发现结果一样,很完美
接下来考虑发布的问题,我们在使用这个模块的时候不止于还要require这么复杂吧?我们肯定希望简单一点,所以我们封装一下index.js
const greetModule = require('bindings')('greet');
export default{
greet : greetModule.greet
}
于是我们就可以在js中愉快地使用这个插件了
参考文章:A simple guide to load C/C++ code into Node.js JavaScript Applications
在原文的基础上加上了一个自己的理解,删去了一些我认为没太必要讲得太详细的内容,读全英文的文章还是有点头疼,但收获也是相当大的