VSCode中C++和JS的混合调试。
效果图
二话不说先上图。
VSCode
必须先吹一波VSCode,巨硬少有的良心作,用来写前端代码速度快,有弹性,体位好,手感棒,谁用谁知道,尤其是对JS和TS的支持非常棒。
其本身也是巨硬用自家TS写的Electron跨平台编辑器,没有Visual Studio的笨重,虽然它不是IDE,但是耍起来堪比IDE。
下面进入正题。
Node.js C++ 原生模块
Node.js允许我们用C++来开发原生模块。理论上来说C++能做的事大部分JS都能做,为什么还要用C++来写Node.js,我认为有以下几点:
- 可以使用C++中JS所没有的特性,比如类的私有成员、多继承,大数运算等等
- 可以提高性能,编码解码、加密解密等运算量大的工作C++做起来的效率要比JS快得多
- 可以使用现有的C++轮子,没有必要用JS再造一遍
- 需要做的东西的的确确用JS不好写,用C++反而好写,又必须用上Node.js
- 瞎鸡脖玩
而我就是出于第5种原因。
其实让Node.js和C++搞起基来是非常容易的,我们先来点前戏。
准备
具体可以参考node-gyp文档
- Visual Studio Code 1.22+
- Visual Studio Code C/C++ 扩展(Microsoft官方的,打开VSCode扩展页搜一下第一个应该就是)
- Node.js 8+ (这里我使用8版本以上才有的NAPI来写)
- node-gyp (原生模块跨平台构建工具,通过npm安装)
- Python 2.7 (node-gyp需要,Python 3不行)
- .NET 4.5.1 (非Win7忽略这一项)
- Visual Studio 2015/2017,或单独安装MSBuild和VC++工具集140/141(非Windows系统忽略这一项)
- gcc (非Linux系统忽略这一项)
- Xcode (非Mac系统忽略这一项)
准备好了就开始搞起。拿Windows平台做个示范,全平台通用。
正片
package.json
新建个目录,创建package.json
,npm install
走起
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| { "scripts": { "build:debug": "node-gyp configure && node-gyp build --debug", "build:release": "node-gyp configure && node-gyp build", "clean": "node-gyp clean" }, "devDependencies": { "node-gyp": "^3.8.0" }, "dependencies": { "node-addon-api": "*" } }
|
build
脚本执行构建完会在根目录生成build目录,clean
用于清理生成,node-addon-api
这个库用C++封装了C风格的NAPI,写起来容易点。
binding.gyp
node-gyp
会根据根目录下的binding.gyp
配置来生成项目,binding.gyp
看起来是json格式,其实是Python的字典和列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| { "targets": [ { "target_name": "VscodeCppJsDebugDemo", "sources": [ "./src/main.cpp", "./src/DemoAsyncWorker.cpp" ], "include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"], "dependencies": ["<!(node -p \"require('node-addon-api').gyp\")"], "cflags!": [ "-fno-exceptions" ], "cflags_cc!": [ "-fno-exceptions" ], "xcode_settings": { "GCC_ENABLE_CPP_EXCEPTIONS": "YES", "CLANG_CXX_LIBRARY": "libc++", "MACOSX_DEPLOYMENT_TARGET": "10.7" }, "msvs_settings": { "VCCLCompilerTool": { "ExceptionHandling": 1 } }, "defines": ["NAPI_DISABLE_CPP_EXCEPTIONS", "NODE_ADDON_API_DISABLE_DEPRECATED"] } ] }
|
简单说,有了这个配置文件,node-gyp
就会分别对不同操作系统下的编译器做不需要我们关心的跨平台配置,编译src
目录下的两个源文件,最终生成VscodeCppJsDebugDemo.node
二进制文件,其实就是个动态链接库。
index.js
既然都要用到C++了,那么在大多数情况需求都应该是耗时的异步操作,如数据库的读写,音频流的处理等等,JS调用的时候应该是异步的,那就用C++写个异步函数吧,模仿耗时操作。
根目录下新建index.js
。
1 2 3 4 5 6 7 8
| const asyncDemo = require('./build/Debug/VscodeCppJsDebugDemo.node')
asyncDemo((str) => { const msg = 'asyncDemo() ' + str console.log(msg) })
console.log('[JavaScript] Call console.log()')
|
引入原生模块,原生模块暴露的是一个异步函数,执行它后回调函数将在1秒后执行并且不阻塞JS线程,回调函数的参数是一个字符串。为了更直观的调试效果,在这5行都打上断点。
src/main.cpp
在src
目录下新建main.cpp
,看注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include "./DemoAsyncWorker.h"
using namespace Napi;
static Value _asyncDemo(const CallbackInfo& info) { Env env = info.Env();
if (info.Length() != 1) { Error::New(env, "arguments.length !== 1").ThrowAsJavaScriptException(); return env.Undefined(); }
if (!info[0].IsFunction()) { Error::New(env, "typeof arguments[0] !== 'function'").ThrowAsJavaScriptException(); return env.Undefined(); }
Function callback = info[0].As<Function>(); DemoAsyncWorker *w = new DemoAsyncWorker(callback); w->Queue();
return env.Undefined(); }
Object init(Env env, Object exports) { Object global = env.Global(); Object console = global.Get("console").As<Object>(); Function log = console.Get("log").As<Function>(); log.Call(console, { String::New(env, "[C++] Call console.log()") }); Function module = Function::New(env, _asyncDemo); return module; }
NODE_API_MODULE(NODE_GYP_MODULE_NAME, init)
|
注意到21行,为什么在堆内存上new了一个Worker对象,在w->Queue()
完了以后不用delete w
,点进源码里面去看会发现,这个Worker对象在执行完异步操作后会自杀,即使不自杀,我们也不可以直接把它delete掉,因为排队等待是异步的,直接delete后回调根本不能执行,甚至由于指针操作不当程序会崩溃。附上对象自杀代码:
napi-inl.h1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| inline void AsyncWorker::OnWorkComplete(napi_env , napi_status status, void* this_pointer) { AsyncWorker* self = static_cast<AsyncWorker*>(this_pointer); if (status != napi_cancelled) { HandleScope scope(self->_env); details::WrapCallback([&] { if (self->_error.size() == 0) { self->OnOK(); } else { self->OnError(Error::New(self->_env, self->_error)); } return nullptr; }); } delete self; }
|
src/DemoAsyncWorker.h
在src
目录下新建DemoAsyncWorker.h
,声明DemoAsyncWorker类,看注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #ifndef __DEMO_ASYNC_WORKER_H__ #define __DEMO_ASYNC_WORKER_H__
#include <napi.h>
class DemoAsyncWorker : public Napi::AsyncWorker { public: DemoAsyncWorker(Napi::Function&); ~DemoAsyncWorker(); void Execute(); void OnOK(); };
#endif
|
src/DemoAsyncWorker.cpp
在src
目录下新建DemoAsyncWorker.cpp
,实现DemoAsyncWorker类,看注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include "./DemoAsyncWorker.h" #include <thread> #include <chrono>
using namespace Napi;
DemoAsyncWorker::DemoAsyncWorker(Function& callback): AsyncWorker(callback) {}
DemoAsyncWorker::~DemoAsyncWorker() {}
void DemoAsyncWorker::Execute() { std::this_thread::sleep_for(std::chrono::milliseconds(1000)); }
void DemoAsyncWorker::OnOK() { Callback().Call({ String::New(Env(), "callback.") }); }
|
.vscode/launch.json
万事预备,只差靓妹。接着就要配置VSCode的Debugger了。
在.vscode
目录下新建launch.json
,这个json文件就是VSCode Debugger的配置文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| { "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "JS Debug Build", "console": "integratedTerminal", "program": "${workspaceFolder}/index.js", "preLaunchTask": "npm: build:debug" }, { "name": "Windows Attach", "type": "cppvsdbg", "request": "attach", "processId": "${command:pickProcess}" } ] }
|
重点看configurations
数组,这里面的每一项,都是一个启动项。request
可以是launch
(启动)或attach
(附加),console
是终端选项,设置为integratedTerminal
则使用VSCode的内部集成终端显示调试结果,preLaunchTask
是启动前执行的任务,就是package.json
中的scripts['build:debug']
。先编译生成项目,再启动node调试进程,入口是根目录的index.js
。
启动起来以后,调试器会断在第一行,这时先不要急,切换到第二个启动项再点一下启动。关键就在这里,第二项配置的是Windows的C++调试器,其它平台也差不多,设置为附加到另一个调试进程,启动它后会弹出一个列表,输入node
就会出现刚刚启动的调试进程,选择它后就可以让C++代码也进入调试,可以看到调试器下拉菜单中多了一项,我们可以随时切换调试。效果演示GIF图在本文最开始。
到此为止,就实现了用VSCode混合调试C++和JS代码。
.vscode/tasks.json
设置快捷键Ctrl + Shift + B
为Release生成,只要在.vscode/tasks.json
做如下配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| { "version": "2.0.0", "tasks": [ { "type": "npm", "script": "build:release", "problemMatcher": [], "group": { "kind": "build", "isDefault": true } } ] }
|
Git仓库
本文只讲解了JS的调试法,还有TS、Electron的调试配置,具体可以来看这个仓库。
参考