Node.js 基于 GYP(Generate Your Projects)构建 C/C++ 插件,叫 node-gyp。这次介绍如何写一个简单的 hello 插件,并且能 debug C/C++ 代码。

1 创建一个可以被 node-gyp 编译的项目

1.1 安装全局 node-gyp 命令

npm install -g node-gyp

1.2 创建 package.json 环境

新建目录 hello, 并 npm init -y

1.3 创建 binding.gyp 文件,用来配置 node-gyp

// binding.gyp
{
    "targets": [
        {
            "target_name": "hello",
            "sources": [
                "./src/hello.cc",
            ]
        }
    ]
}

1.4 添加 C++ 源代码

// ./src/hello.cc
#include <node.h>

namespace demo
{

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Object;
using v8::String;
using v8::Value;

void Method(const FunctionCallbackInfo<Value> &args)
{
    Isolate *isolate = args.GetIsolate();
    args.GetReturnValue().Set(String::NewFromUtf8(
                                  isolate, "world", NewStringType::kNormal)
                                  .ToLocalChecked());
}

void Initialize(Local<Object> exports)
{
    NODE_SET_METHOD(exports, "hello", Method);
}

// 这里的 NODE_GYP_MODULE_NAME 宏就是 前面 binding.gyp 的指定的 target_name
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

} // namespace demo

例子来自: https://nodejs.org/api/addons.html#addons_hello_world

1.5 编译 addon

node-gyp rebuild

会生成 build/Release/hello.node 文件

1.6 添加 Task,不必每次输入命令

package.json 的 "scripts" 字段中,添加:

"scripts": {
    "start": "node index.js",
    "build:dev": "node-gyp -j 16 build --debug",
    "build": "node-gyp -j 16 build",
    "clean": "node-gyp clean"
}

之后在 Visual Studio Code 中就可以直接执行 Task 了。

执行 Task 的方式是:通过快捷键 Command+P 在弹出的框中,输入 task build:dev 就可以执行对应的命令了。

1.6 创建 index.js 并在里面添加如下:

const helloAddon = require('./build/Release/hello.node')

console.log('output:', helloAddon.hello('fakeArgs'))

这里的 helloAddon 就是编译的结果,等同于 js 代码 module.exports.hello = () => 'world'

接着执行 node index.js 即会输出 output: world 结果。

2 Debug C/C++ 代码

2.1 让 node-gyp 编译出 Debug 版本的 hello.node

node-gyp rebuild --debug

这样会重新编译 hello.cc ,并且生成 ./build/Debug/hello.node

2.2 添加 vscode 配置项

  • 安装 C/C++ 的扩展 ms-vscode.cpptools
  • 安装 LLDB 的 Debugger 扩展 vadimcn.vscode-lldb
  • 项目中的 <node.h> 是红线,因为没有找到对应的头文件
  • 在项目中 Command+Shift+P 弹出的下拉框输入,C/C++ configuration (UI)
  • 在界面中添加 include 头文件,包括 nodejs 的源代码的源文件和 v8 的源文件

添加头文件路径可以参考我的,这样代码里面的头文件 node.h 就不会影响

"/Users/xxx/node-investigation/n1/node/src/**",
"/Users/xxx/node-investigation/n1/node/deps/v8/include/**"

2.3 添加断点

接着把先前 index.js 的代码 Release 改成 Debug

const helloAddon = require('./build/Debug/hello.node')

2.4 配置 Debugger 的启动配置

在项目中 Command+Shift+P 弹出的下拉框输入 open launch.json ,会显示这个打开 launch.json 的选项,然后添加:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug C/C++ Addon",
            "type": "lldb",
            "request": "launch",
            "preLaunchTask": "npm: build:dev",
            "program": "node",
            "args": ["${workspaceFolder}/index.js"]
        }
    ]
}

这样下次开始 Start Debugging 的时候,会依据 launch.json 执行,当给 hello.cc 标记断点后,程序会在断点出断点。

注意:只有装了 lldb 插件后,才能使用 type: "lldb" 选项值。lldb 是 llvm 的一个类似 gdb 的实现。这个插件用来支持 llvm 编译器的。如果在 Windows 中,可以用 vscode 自带的 cppvsdbg

3 混合调试 C/C++ 和 NodeJS

就是先启动 NodeJS 调试后,接着启动 C/C++ 调试器,这样在深入 js 代码的过程中,还可以进入到 Addons 中去。有了上面介绍的基础,实现混合调试简单了很多。

3.1 调试 JS 代码

在 launch.json 中的 configurations 数组中,继续添加如下配置:

{
    "name": "JS Debug Build",
    "type": "node",
    "request": "launch",
    "console": "integratedTerminal",
    "program": "${workspaceFolder}/index.js",
    "preLaunchTask": "npm: build:dev"
}

这样,就可以在 Start Debugging 的时候,选择 JS Debug Build 可以调试断点 JS 代码了。

注意:调试 JS 代码,需要让 Debugger 面板切换到 JS Debug Build ,如下图

3.2 混合调试 Addons

继续在 launch.json 中添加如下配置:

{
    "name": "Attach C/C++ Addon",
    "type": "lldb",
    "request": "attach",
    "pid": "${command:pickMyProcess}"
}

把 Debug 面板的 Debugger 切换到 Attach C/C++ Addon, 接着再启动即可。这里的 request: "attach" 是让 lldb 链接到 nodejs 进程中去 Debug。所以下面的 pid 字段就是启动后,会弹出下拉框,然后选择要 链接的进程。一旦链接成功,就可以对 C/C++ 代码断点了。

4 接下来

如果没有成功可以参考我弄的一个 demo 仓库: https://github.com/yantze/demo-nodejs-addon-debug

注意,里面的 c_cpp_properties.json 需要自己修改 include 路径。

如果还想弄 Node Addon API 的 NAPI 接口,配置算是类似的,就不再赘述了。不过,如果使用的是 NAPI 的代码,就不需要 node 和 v8 的头文件了,并且官方推荐的也是 N-API 实现插件。