Migrate native addon from nan to node-addon-api (N-API)#152
Conversation
|
Thank you for implementing this so quickly — that's much appreciated! Tested on Node.js 22.22.3 / Linux x64. The native addon builds and loads correctly — ABI stability works as expected. However, Additionally, As a result, |
Fixed now and also dropped support for the old EOL node version. |
Replace nan with node-addon-api in both native source files and the build configuration. N-API provides ABI stability across Node.js major versions, so a binary compiled once continues to load on Node.js 20, 22, and 24 without recompilation. Changes: - binding.gyp: switch include path to node-addon-api, add NAPI_DISABLE_CPP_EXCEPTIONS define - native/can.cc: rewrite RawChannel to use Napi::ObjectWrap<T>, instance methods, Napi::FunctionReference/ObjectReference for persistent listener handles, and napi_env stored for uv_async callbacks - native/signals.cc: rewrite DecodeSignal/EncodeSignal as plain Napi::Value functions - package.json: replace nan dependency with node-addon-api ^8.0.0 - Dockerfile.build-test: new file for verifying compilation on Linux Verified: compiles cleanly on Node.js 22 / Linux arm64 (Debian bookworm) with no warnings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Run test-parsing and test-signal_conversion (the tests that exercise the migrated can_signals native module) inside the container. The two vcan- dependent test suites (test-raw_basic, test-signal_generation) require a real Linux kernel with the vcan module and cannot run in Docker Desktop. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Regenerate lock file after replacing nan with node-addon-api. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
uv_async callbacks run on the Node.js main thread but are not entered via NAPI, so no HandleScope is open when they fire. Accessing any Napi::Reference value (FunctionReference::Value()) without one causes a fatal: "Cannot create a handle without a HandleScope". Add Napi::HandleScope scope(env) at the top of async_channel_stopped() and async_receiver_ready(), matching the Nan::HandleScope that the previous NAN implementation had in the same two functions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Now that the native addon uses N-API, the same binary should load unchanged across all LTS releases. Adding 24.x to the matrix verifies this on every PR. Dockerfile.build-test reverted to build-only — its purpose is to quickly verify compilation on Linux without a CAN interface. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bump typescript devDep to ^5.4.0 (resolves incompatibility with @types/node v22) - Bump @types/node to ^22.0.0 to match supported runtime - Add prepare script so dist/ is built automatically on npm install - Add rootDir to tsconfig to prevent stray file pickup - Add engines field declaring node >=22.0.0 - CI matrix: drop 18.x and 20.x (both EOL); keep 22.x and 24.x - Publish workflow: update node-version from 18 to 22 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Regenerated lockfile incorporating dependabot security bumps from master (js-yaml 4.1.0, node-gyp 12.3.0, tar 7.5.15) alongside branch dependency updates (typescript 5.9.3, @types/node 22.19.19). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
400ee13 to
458a7a8
Compare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
uv_default_loop() always returns the main thread's event loop. When start() is called from a worker thread, the uv_async callbacks fired on the main loop but m_napi_env held the worker's environment — accessing a worker-thread V8 context from the wrong isolate caused a segfault on any received message. Fix: use napi_get_uv_event_loop to obtain the event loop that owns the current napi_env, so async callbacks always fire on the correct loop. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verifies that a RawChannel created and started inside a Worker thread can receive CAN frames without segfaulting. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
worker.terminate() force-kills the worker without closing the uv_async handles (that only happens via ch.stop()), causing Node to abort with "uv_loop_close() while having open handles". Instead, the worker calls ch.stop() via setImmediate after forwarding the first message. This runs async_channel_stopped() which closes both handles and Unref()s the channel, allowing the event loop to drain and the worker to exit naturally. The main thread waits on worker 'exit' before calling done(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
nanwithnode-addon-apiin both native source files (can.cc,signals.cc) and the build configuration (binding.gyp)node-addon-apiwraps the stable N-API layer, guaranteeing ABI compatibility across Node.js major versions — a binary compiled once for Node.js 20 continues to load on Node.js 22 and 24 without recompilationDockerfile.build-testfor reproducing the Linux build locallyMotivation
Users who upgrade Node.js (e.g. from v20 to v22) currently find the native addon silently broken until they manually rebuild. This is a common pain point for ioBroker adapters and other downstream consumers. N-API eliminates the need to recompile on major version upgrades and makes prebuilt binaries (e.g. via
prebuildify) feasible in the future.What changed
package.jsonnan→node-addon-api ^8.0.0binding.gypNAPI_DISABLE_CPP_EXCEPTIONSnative/can.ccNan::ObjectWrap→Napi::ObjectWrap<T>; static methods → instance methods;Nan::Persistent→Napi::FunctionReference/ObjectReference;napi_envstored foruv_asynccallbacksnative/signals.ccNAN_METHOD→Napi::Valuefree functions; allNan::*replaced withNapi::*Dockerfile.build-testnode:22-bookworm-slimfor verificationTest plan
npm testpasses on a machine with a real or virtual CAN interface (vcan0)🤖 Generated with Claude Code