Skip to content

Migrate native addon from nan to node-addon-api (N-API)#152

Merged
sebi2k1 merged 12 commits into
masterfrom
migrate/nan-to-node-addon-api
May 23, 2026
Merged

Migrate native addon from nan to node-addon-api (N-API)#152
sebi2k1 merged 12 commits into
masterfrom
migrate/nan-to-node-addon-api

Conversation

@sebi2k1
Copy link
Copy Markdown
Owner

@sebi2k1 sebi2k1 commented May 17, 2026

Summary

  • Replace nan with node-addon-api in both native source files (can.cc, signals.cc) and the build configuration (binding.gyp)
  • node-addon-api wraps 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 recompilation
  • Adds Dockerfile.build-test for reproducing the Linux build locally

Motivation

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

File Change
package.json nannode-addon-api ^8.0.0
binding.gyp Updated include path expression; added NAPI_DISABLE_CPP_EXCEPTIONS
native/can.cc Nan::ObjectWrapNapi::ObjectWrap<T>; static methods → instance methods; Nan::PersistentNapi::FunctionReference/ObjectReference; napi_env stored for uv_async callbacks
native/signals.cc NAN_METHODNapi::Value free functions; all Nan::* replaced with Napi::*
Dockerfile.build-test New — builds the package on node:22-bookworm-slim for verification

Test plan

  • Compiled cleanly on Node.js 22 / Linux arm64 (Debian bookworm) — zero warnings
  • Verify npm test passes on a machine with a real or virtual CAN interface (vcan0)
  • Smoke-test: load the binary built on Node.js 20 under Node.js 22 without rebuilding

🤖 Generated with Claude Code

@MyHomeMyData
Copy link
Copy Markdown

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, npm run build:ts fails because the declared devDependency typescript@4.8.4 is incompatible with the tsconfig base @tsconfig/node22, which requires TypeScript ≥ 5.4 for the es2024, ESNext.Collection and ESNext.Iterator lib targets.

Additionally, tsc inadvertently picks up .js files from the parent project directory (4 levels above the package). Adding an explicit include or rootDir to tsconfig.json would prevent this.

As a result, dist/socketcan.js is missing after install and the package cannot be loaded. Could you update the TypeScript devDependency and add a prepare script (or include dist/ in the repository) so the package is usable out of the box?

@sebi2k1
Copy link
Copy Markdown
Owner Author

sebi2k1 commented May 23, 2026

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, npm run build:ts fails because the declared devDependency typescript@4.8.4 is incompatible with the tsconfig base @tsconfig/node22, which requires TypeScript ≥ 5.4 for the es2024, ESNext.Collection and ESNext.Iterator lib targets.

Additionally, tsc inadvertently picks up .js files from the parent project directory (4 levels above the package). Adding an explicit include or rootDir to tsconfig.json would prevent this.

As a result, dist/socketcan.js is missing after install and the package cannot be loaded. Could you update the TypeScript devDependency and add a prepare script (or include dist/ in the repository) so the package is usable out of the box?

Fixed now and also dropped support for the old EOL node version.

sebi2k1 and others added 8 commits May 23, 2026 13:21
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>
@sebi2k1 sebi2k1 force-pushed the migrate/nan-to-node-addon-api branch from 400ee13 to 458a7a8 Compare May 23, 2026 11:22
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>
@sebi2k1 sebi2k1 linked an issue May 23, 2026 that may be closed by this pull request
sebi2k1 and others added 2 commits May 23, 2026 13:45
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>
@sebi2k1 sebi2k1 merged commit 9e6929c into master May 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate from nan to node-addon-api (N-API) for ABI stability across Node.js versions Not working with Worker thread.

2 participants