3、第一个原生模块:创建项目结构、编写binding.gyp文件、实现简单的C++函数、编译与测试
好,咱们正式开始动手了。
前面两章聊了那么多概念和背景,我知道你肯定手痒了。今天我们就来写一个真正的原生模块——从零开始,一个不落。
我个人习惯是,学任何新技术,第一个模块都别搞太复杂。能跑起来,能看到效果,比什么都重要。所以今天的目标很简单:写一个C++函数,能在Node.js里调用它,让它返回一个字符串。
3.1 项目结构:别小看这个文件夹
先创建项目目录。我建议你跟着我一步步来,别跳步。
mkdir my-first-addon
cd my-first-addon
npm init -y
嗯,这里要注意:npm init -y 会生成一个 package.json。但原生模块的 package.json 和普通Node包有点不一样。你需要手动加一个字段:
{
"name": "my-first-addon",
"version": "1.0.0",
"description": "我的第一个原生模块",
"main": "build/Release/addon.node",
"scripts": {
"build": "node-gyp rebuild",
"test": "node test.js"
},
"gypfile": true
}
看到 "gypfile": true 了吗?这个告诉Node.js,我这个包是用node-gyp编译的。我在项目中遇到过有人忘了加这个,结果 npm install 的时候死活不编译,排查了半天。
接下来创建源码目录:
mkdir src
touch src/addon.cpp
touch binding.gyp
touch test.js
最终的项目结构长这样:
my-first-addon/
├── binding.gyp
├── package.json
├── src/
│ └── addon.cpp
└── test.js
简单吧?但别小看这个结构。我见过很多新手把C++文件和binding.gyp放在同一级目录,结果编译的时候各种路径报错。养成好习惯,源码放 src/,干净利落。
3.2 编写binding.gyp文件:这是你的“施工图纸”
binding.gyp 是node-gyp的配置文件。说白了,它就是告诉编译器:你要编译哪些源文件,用哪些头文件路径,链接哪些库。
来看一个最简版本:
{
"targets": [
{
"target_name": "addon",
"sources": [ "src/addon.cpp" ],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"defines": [ "NAPI_VERSION=8" ],
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ]
}
]
}
我来拆解一下每个字段的意思:
| 字段 | 含义 | 我的经验 |
|---|---|---|
target_name |
编译产物的名字,最终会生成 addon.node |
这个名字要和 require() 里的名字一致 |
sources |
要编译的C++源文件列表 | 多个文件用逗号隔开,路径相对于binding.gyp |
include_dirs |
头文件搜索路径 | 这里用了node-addon-api的路径,省得自己配 |
defines |
预定义宏 | NAPI_VERSION=8 表示用N-API第8版 |
cflags! / cflags_cc! |
编译选项,! 表示覆盖默认值 |
关掉异常处理,能减小生成的二进制体积 |
"msvs_settings" 来配置Visual Studio的编译选项。不过咱们今天先跑通Linux/macOS,Windows的坑后面章节再填。
3.3 实现简单的C++函数:Hello from C++!
终于到写代码的环节了。打开 src/addon.cpp,输入以下内容:
#include <napi.h>
Napi::String Hello(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::String::New(env, "Hello from C++! 这是你的第一个原生模块。");
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(
Napi::String::New(env, "hello"),
Napi::Function::New(env, Hello)
);
return exports;
}
NODE_API_MODULE(addon, Init)
这段代码干了三件事:
- 定义了一个C++函数
Hello,它接收CallbackInfo(调用信息),返回一个Napi::String。 - 写了一个初始化函数
Init,把Hello函数注册到exports对象上,这样JS那边就能通过addon.hello调用它。 - 用宏
NODE_API_MODULE把模块注册到Node.js的模块系统里。
你可能会问:为什么不用 node-addon-api 的 Napi::ObjectWrap?嗯,那是面向对象的写法,咱们今天先搞个简单的函数式写法,后面章节再深入。
Hello、Add。这样在C++代码里一眼就能看出哪些是暴露给JS的函数,哪些是内部辅助函数。
3.4 编译与测试:见证奇迹的时刻
代码写完了,接下来编译。确保你已经安装了 node-gyp:
npm install -g node-gyp
然后执行编译:
node-gyp configure
node-gyp build
或者一步到位:
node-gyp rebuild
编译成功后,你会看到 build/Release/addon.node 这个文件。这就是你的原生模块二进制文件。
现在写个测试脚本 test.js:
const addon = require('./build/Release/addon.node');
console.log(addon.hello());
运行它:
node test.js
如果一切顺利,你会看到:
Hello from C++! 这是你的第一个原生模块。
🎉 恭喜!你成功创建了第一个原生模块!
从这一刻起,你已经不再是普通的Node.js开发者了。你拥有了调用C++能力、突破JavaScript性能瓶颈的钥匙。
3.5 避坑指南:我踩过的那些坑
编译过程中可能会遇到各种问题。我把常见的坑列出来,你遇到了直接对照排查:
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
gyp: No Xcode or CLT version detected! |
macOS上没装Xcode命令行工具 | xcode-select --install |
fatal error: 'napi.h' file not found |
没装 node-addon-api |
npm install node-addon-api --save |
MSB4019: The imported project "Microsoft.Cpp.Default.props" was not found |
Windows上没装Visual Studio Build Tools | 安装 windows-build-tools 或VS 2022 |
Error: Module version mismatch. Expected 108, got 115 |
Node.js版本和编译时的ABI版本不一致 | 重新编译:node-gyp rebuild |
我曾经在一个项目里,因为Node.js从16升级到了18,忘了重新编译,结果模块加载时报版本不匹配。排查了整整两个小时才发现是这么个低级错误。所以记住一条铁律:换了Node.js版本,一定要重新编译原生模块。
3.6 小结与思考
今天的内容其实不多,但每一步都很关键。你学会了:
- 创建原生模块的标准项目结构
- 编写
binding.gyp配置文件 - 用N-API写一个简单的C++函数
- 编译并测试原生模块
下节课,我们会深入 binding.gyp 的更多配置选项,比如链接第三方库、条件编译、多平台支持等。但今天先消化这些,动手把第一个模块跑起来。
记住:能跑起来的代码,才是好代码。别光看不练,去敲一遍。