使用C++开发Node原生插件

场景:之前做了一个弹幕姬,其中有一个选择字体的功能,但有个不太舒服的地方是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可以通过内存地址去访问这些创建好了的【对象/类/变量/方法】,由此获取插件实现的功能。

总结

  1. 创建插件,并将它编译为动态链接库
  2. 使用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服务

来回想一下,如果我们要调用一个函数,需要知道些什么?

  1. 函数名
  2. 形参
  3. 返回值类型

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
在原文的基础上加上了一个自己的理解,删去了一些我认为没太必要讲得太详细的内容,读全英文的文章还是有点头疼,但收获也是相当大的

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容