napi_threadsafe_function cleanup requires release or abort
When destroying an N-API addon, call napi_release_threadsafe_function with napi_tsfn_release to release the threadsafe function, or napi_tsfn_abort to abort pending calls. This must be done before the addon object is destroyed.
Convert timestamps to milliseconds for JavaScript Date objects
When passing dates from native code to JavaScript, convert timestamps to milliseconds since epoch by multiplying the timeIntervalSince1970 value by 1000. JavaScript Date objects expect millisecond precision, not second precision.
JSON encoding with custom date strategy in Swift
When encoding Swift objects to JSON for JavaScript, use JSONEncoder with custom dateEncodingStrategy. Encode dates as milliseconds since 1970 (matching JavaScript Date behavior): encoder.dateEncodingStrategy = .custom { date, encoder in let milliseconds = Int64(date.timeIntervalSince1970 * 1000); var container = encoder.singleValueContainer(); try container.encode(milliseconds) }
Convert NSString to C++ std::string in Objective-C++
To convert NSString to C++ std::string in Objective-C++: use [nsString UTF8String] to get const char* pointer, then create std::string from it. Example: std::string cppString([nsString UTF8String]). To convert back to NSString: use [NSString stringWithUTF8String:cppString.c_str()].
Swift compilation actions in binding.gyp
Swift code must be compiled before linking with Node.js addon code. In binding.gyp, use two actions: (1) build_swift action that runs 'swiftc src/SwiftCode.swift -emit-objc-header-path ./build_swift/swift_addon-Swift.h -emit-library -o ./build_swift/libSwiftCode.a -emit-module -module-name swift_addon -module-link-name SwiftCode' to compile Swift code into a static library and generate an Objective-C header. (2) copy_swift_lib action that copies the compiled library from build_swift/ to PRODUCT_DIR and uses install_name_tool to set the correct install name for runtime linking.
binding.gyp configuration for Swift on macOS
For Swift addons on macOS, binding.gyp requires: OS condition checking for 'mac', sources including .mm, .m, and .swift files, include_dirs for node-addon-api, and xcode_settings. Key xcode_settings: GCC_ENABLE_CPP_EXCEPTIONS=YES enables C++ exception handling, CLANG_ENABLE_OBJC_ARC=YES enables Automatic Reference Counting, SWIFT_OBJC_BRIDGING_HEADER points to the bridging header, SWIFT_VERSION=5.0 sets Swift language version, SWIFT_OBJC_INTERFACE_HEADER_NAME names the generated Swift-to-Objective-C header (e.g., swift_addon-Swift.h), MACOSX_DEPLOYMENT_TARGET=11.0 sets minimum macOS version. OTHER_CFLAGS should include -ObjC++ and -fobjc-arc. OTHER_LDFLAGS should include -Wl,-rpath,@loader_path and -Wl,-install_name,@rpath/libSwiftCode.a for runtime library linking.
Swift addon package structure
A typical Swift addon for Electron has this structure: binding.gyp (build configuration), include/ with SwiftBridge.h (Objective-C header), js/index.js (JavaScript interface), package.json (configuration), and src/ containing SwiftCode.swift (Swift implementation), SwiftBridge.m (Objective-C bridge), and swift_addon.mm (Node.js addon implementation).
Swift cannot be used directly with Node.js N-API
Swift cannot be used directly with Node.js N-API as used by Electron. Instead, you must create a bridge using Objective-C++ to connect Swift with JavaScript in your Electron application.
Two-step compilation process for Swift in Electron addons
When building Swift addons for Electron, use a two-step compilation process: first compile Swift code separately into a static library (.a file), then create an Objective-C bridge that exposes Swift functionality, link the compiled Swift library with the Node.js addon, and manage Swift runtime dependencies. This ensures Swift's advanced language features and runtime are properly handled while exposing functionality to JavaScript through Node.js's native addon system.
Objective-C bridge header pattern for Swift
Create an Objective-C header (SwiftBridge.h) that declares class methods exposed to C++/Node.js. Use @interface for the class name and declare methods with +() class method syntax. Methods that take callbacks should use void(^)(NSString*) syntax for Objective-C blocks. This header bridges between the Swift implementation and the Node.js addon.
Objective-C bridge implementation forwards to Swift
In the Objective-C bridge implementation (.m file), import both the Objective-C header and the Swift-generated header (swift_addon-Swift.h). Implement each class method by forwarding calls to corresponding methods in the Swift class. For callbacks, store them in static variables to persist throughout the application lifecycle, allowing callbacks to be invoked at any time when events occur in Swift code.
@objc attribute exposes Swift to Objective-C
In Swift code, use the @objc attribute on classes and methods to make them accessible from Objective-C. Classes must inherit from NSObject to be compatible with @objc. This enables the Objective-C bridge to call Swift code.
NSHostingView bridges SwiftUI to AppKit
NSHostingView is a crucial bridging component that allows SwiftUI views to be used in AppKit applications. It acts as a container that wraps SwiftUI views and handles translation between SwiftUI's declarative UI system and AppKit's imperative UI system, enabling use of modern SwiftUI while integrating with traditional macOS window management.
Objective-C++ files (.mm) for Node.js addon bridge
Use .mm extension for files that bridge Objective-C and C++. Include both @import Foundation and #include <napi.h> headers. Create a C++ class inheriting from Napi::ObjectWrap to wrap native functionality. Define static Init() method to register the class with Node.js, and implement instance methods that call Objective-C code.
Napi::ObjectWrap for exposing classes to JavaScript
Create a C++ class inheriting from Napi::ObjectWrap<ClassName> to expose a class to JavaScript. Implement a static Init(Napi::Env env, Napi::Object exports) method that calls DefineClass with the environment, class name, and method descriptors. Store a Napi::FunctionReference to the constructor and set it on exports to make the class available to JavaScript.
InstanceMethod for Napi method descriptors
In Napi::ObjectWrap::DefineClass, use InstanceMethod(name, &ClassName::MethodName) to define methods callable from JavaScript. Each method takes Napi::CallbackInfo with environment and arguments, and returns Napi::Value or void.
Converting between JavaScript and C++ string types in Napi
To convert JavaScript strings to C++: use info[index].As<Napi::String>() to get Napi string, then .Utf8Value() or cast to std::string. To convert C++ strings to JavaScript: use Napi::String::New(env, cppString) to create a Napi string that can be returned to JavaScript.
Persistent references in Napi for callback storage
Use Napi::ObjectReference with Napi::Persistent() to store persistent references to JavaScript callbacks and objects. Create with Napi::Persistent(obj), access with .Value(), and reset with .Reset() when done. Persistent references prevent garbage collection of referenced objects during addon lifetime.
JavaScript wrapper extends EventEmitter for Swift addon
Create a JavaScript wrapper that extends EventEmitter. Load the native addon with require('bindings')('module_name'). Set up event listeners on the native addon and re-emit them through the EventEmitter. Provide convenience methods that call native addon methods. Provide a destroy() method to release native resources before app quit.
Must call destroy() on Swift addon before app quit
Before the app quits, you must call destroy() on the Swift addon (in will-quit or before-quit event handler). Without this, persistent references to callbacks and the threadsafe function will prevent the native addon's destructor from running, causing Electron to hang on quit.
Swift addon only works on macOS
Swift native addons for Electron are macOS-only. Check process.platform === 'darwin' in the JavaScript wrapper and throw an error if running on other platforms.
Cannot call build script from Node.js directly for Electron native addons
You cannot call the native addon build script (npm run build) from Node.js directly, since Node.js does not set up an 'app' in the eyes of macOS. Electron does set up an app, so test code by requiring and calling it from Electron instead.
Use cases for native modules in Electron
Native modules in Electron enable: accessing native platform APIs not available in JavaScript (any macOS, Windows, or Linux OS API), creating UI components that interact with native desktop frameworks, integrating with existing native libraries, and implementing performance-critical code that runs faster than JavaScript.
Native Node.js Addons in Electron
Electron supports Native Node.js Addons, which are dynamically-linked shared objects on Unix-like systems or DLL files on Windows. These addons can be loaded into Node.js or Electron using require() or import functions and behave like regular JavaScript modules while providing an interface to code written in C++, Rust, or other compiled languages.
Building native addons requires platform-specific tools
Building native Node.js addons requires node-gyp (a cross-platform command-line tool) and platform-specific build tools: Visual Studio on Windows, Xcode or command-line tools on macOS, or GCC/similar compilers on Linux. The node-gyp tool compiles native addon modules using these platform-specific tools behind the scenes.
macOS requirements for native addon development
To build native Node.js addons on macOS, install the Xcode Command Line Tools which provide compilers and build tools including clang, clang++, and make. Run 'xcode-select --install' to prompt installation if not already installed.
Windows requirements for native addon development
The official Node.js installer offers optional installation of 'Tools for Native Modules' which installs Python 3 and the 'Visual Studio Desktop development with C++' workload. Alternatively, use chocolatey, winget, or the Windows Store.
Linux requirements for native addon development
Linux native addon development requires a supported version of Python, make, and a proper C/C++ compiler toolchain such as GCC.
Essential npm packages for native addon development
Two essential packages for native Node.js addon development are: node-addon-api (a C++ wrapper for the low-level Node.js API that makes building addons easier with an object-oriented API) and bindings (a helper module that simplifies loading compiled native addons by handling finding the compiled .node file automatically).
binding.gyp configuration file structure
The binding.gyp file is a JSON-like configuration that tells node-gyp how to build a native addon. Key fields include: target_name (determines the compiled module filename), sources (list of source files to compile), include_dirs (directories to search for header files), dependencies (node-addon-api dependency configuration), defines (preprocessor definitions), and platform-specific settings (cflags for Unix, xcode_settings for macOS, msvs_settings for Windows).
npm build scripts for native addons
Typical npm scripts for native addon development: 'clean' script removes the build directory with 'node -e "require("fs").rmSync("build", { recursive: true, force: true })"' for a fresh build, and 'build' script runs 'node-gyp configure && node-gyp build' to compile the addon.
Node-addon-api exception handling configuration
To enable C++ exceptions in a native addon using node-addon-api, include 'NODE_ADDON_API_CPP_EXCEPTIONS' in the defines array, disable -fno-exceptions flag with cflags! and cflags_cc!, enable GCC_ENABLE_CPP_EXCEPTIONS in xcode_settings, and set ExceptionHandling to 1 in msvs_settings.
Using native-addon-api with ObjectWrap pattern
The Napi::ObjectWrap<MyAddon> pattern creates a wrapper class that bridges C++ code with JavaScript. The Init static method defines a JavaScript class with instance methods using DefineClass, creates a persistent reference to prevent garbage collection, and exports the class constructor. Instance methods receive Napi::CallbackInfo containing environment, arguments, and return values.
Example: Hello World native addon implementation
Header file (include/cpp_code.h):
#pragma once
#include <string>
namespace cpp_code {
std::string hello_world(const std::string& input);
}
Implementation (src/cpp_code.cc):
#include <string>
#include "../include/cpp_code.h"
namespace cpp_code {
std::string hello_world(const std::string& input) {
return "Hello from C++! You said: " + input;
}
}
Addon bridge (src/my_addon.cc):
#include <napi.h>
#include <string>
#include "../include/cpp_code.h"
class MyAddon : public Napi::ObjectWrap<MyAddon> {
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
Napi::Function func = DefineClass(env, "MyAddon", {
InstanceMethod("helloWorld", &MyAddon::HelloWorld)
});
Napi::FunctionReference* constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData(constructor);
exports.Set("MyAddon", func);
return exports;
}
MyAddon(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<MyAddon>(info) {}
private:
Napi::Value HelloWorld(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 1 || !info[0].IsString()) {
Napi::TypeError::New(env, "Expected string argument").ThrowAsJavaScriptException();
return env.Null();
}
std::string input = info[0].As<Napi::String>();
std::string result = cpp_code::hello_world(input);
return Napi::String::New(env, result);
}
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
return MyAddon::Init(env, exports);
}
NODE_API_MODULE(my_addon, Init)
JavaScript wrapper for native addon
A typical JavaScript wrapper uses the bindings module to load the compiled .node file, creates a class extending EventEmitter, instantiates the native C++ class, wraps native methods with input validation, and exports a singleton instance. It should handle unsupported platforms gracefully by providing fallback behavior.
Using native addons in Electron applications
To use a native addon in an Electron application: include it as a dependency, build it targeting the specific Electron version (electron-forge handles this automatically), and import it like any other module in a process that has Node.js enabled. The addon can be used in the main process or renderer processes with Node.js enabled.
Alternative languages for native addons
Native addons can be written in languages beyond C++: Rust can be used with napi-rs, neon, or node-bindgen; Objective-C and Swift can be used through Objective-C++ on macOS.
Platform-specific APIs and frameworks for native addons
Implementation details for native code differ significantly by platform. Windows offers Win32 API, COM components, and UWP/WinRT. macOS offers Cocoa, AppKit, and Objective-C runtime. Developers should reference platform-specific documentation alongside N-API documentation for complex structures like asynchronous thread-safe function calls or JavaScript-native objects.
Rebuilding native modules against custom Electron builds
To compile native Node modules against a custom build of Electron that doesn't match a public release, instruct npm to use the Node version bundled with the custom build by running: `npm rebuild --nodedir=/path/to/src/out/Default/gen/node_headers`.
Windows native module linking requirements
When building native modules for Windows in Electron 4.x and higher, link against node.lib from Electron (not Node.js), and include the /DELAYLOAD:node.exe flag in your link.exe invocation. The delay-load hook object file must be linked directly into the final DLL, not into a dependent DLL. Example invocation: `link.exe /OUT:"foo.node" "...\node.lib" delayimp.lib /DELAYLOAD:node.exe /DLL "my_addon.obj" "win_delay_load_hook.obj"`
Prebuild package for prebuilt native module binaries
Prebuild is a package that provides a way to publish native Node modules with prebuilt binaries for multiple versions of Node and Electron. When using a prebuild-powered module that provides Electron-specific binaries, omit the --build-from-source flag and npm_config_build_from_source environment variable to take full advantage of the prebuilt binaries.
node-pre-gyp for native modules with prebuilt binaries
The node-pre-gyp tool provides a way to deploy native Node modules with prebuilt binaries, and many popular modules use it. When Electron-specific binaries are not available, modules may need to be built from source. It is recommended to use @electron/rebuild for these modules. When installing via npm, pass --build-from-source or set npm_config_build_from_source environment variable.
Troubleshooting native module compatibility
When a native module doesn't work after installation, verify: (1) run @electron/rebuild first, (2) ensure the native module is compatible with the target platform and architecture for your Electron app, (3) ensure win_delay_load_hook is not set to false in the module's binding.gyp, (4) rebuild modules after upgrading Electron.
Windows win_delay_load_hook requirement for native modules
On Windows in Electron 4.x and higher, native modules must use a delay-load hook because symbols needed by native modules are exported by electron.exe, not node.dll. By default, node-gyp links against node.dll, so setting 'win_delay_load_hook': 'true' in binding.gyp is required to redirect the node.dll reference to the loading executable. Without this, errors like 'Module did not self-register' or 'The specified procedure could not be found' may occur.
@electron/rebuild tool for rebuilding native modules
@electron/rebuild is a package that automatically determines the Electron version and handles downloading headers and rebuilding native modules for your app. It can be installed as a dev dependency with `npm install --save-dev @electron/rebuild` and run with `./node_modules/.bin/electron-rebuild`. Electron Forge uses this tool automatically in both development mode and when making distributables.
Installing native modules using npm environment variables
To install native modules for Electron using npm, set these environment variables: npm_config_target (Electron version, e.g. 1.2.3), npm_config_arch (machine architecture, e.g. x64), npm_config_target_arch (target architecture, e.g. x64), npm_config_disturl (https://electronjs.org/headers), npm_config_runtime (electron), npm_config_build_from_source (true). Then run `HOME=~/.electron-gyp npm install` to install dependencies.
Native modules require recompilation for Electron ABI
Native Node.js modules must be recompiled for Electron because Electron has a different application binary interface (ABI) than Node.js due to differences such as using Chromium's BoringSSL instead of OpenSSL. Without recompilation, attempting to use a native module will result in an error stating the module was compiled against a different Node.js version with an incompatible NODE_MODULE_VERSION.
Manually rebuilding native modules with node-gyp for Electron
To manually rebuild a native module for Electron using node-gyp, run: `cd /path-to-module/` followed by `HOME=~/.electron-gyp node-gyp rebuild --target=1.2.3 --arch=x64 --dist-url=https://electronjs.org/headers`. The HOME variable specifies where to find development headers, --target specifies the Electron version, --dist-url specifies where to download headers, and --arch specifies the target system architecture.
Node.js version for ARM development
Node.js v12.9.0 or later is recommended for Windows ARM development. Alternatively, update npm's bundled node-gyp to version 5.0.2 or later to compile native modules for ARM.
Visual Studio 2017 ARM components installation
Install Visual Studio 2017 (any edition) for cross-compiling native modules. Run this command to add ARM-specific components: vs_installer.exe --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Component.VC.ATLMFC --add Microsoft.VisualStudio.Component.VC.Tools.ARM64 --add Microsoft.VisualStudio.Component.VC.MFC.ARM64 --includeRecommended
Debugging ARM64 native modules
Debug ARM64 native modules using Visual Studio 2017 on the development machine with Visual Studio Remote Debugger on the target ARM64 device. Launch the app exe on target device with --inspect-brk flag, attach from VS 2017 via Debug > Attach to Process, configure symbol paths under Debug > Options > Debugging > Symbols, set breakpoints, and resume execution using Chrome's remote tools for Node.
Cross-compiling native modules for ARM64
To cross-compile native modules for ARM64: open the cross-compilation command prompt, run 'set npm_config_arch=arm64', then use 'npm install' as normal. If modules were previously compiled for another architecture, remove node_modules to force recompilation.
Downloading ARM64 node.lib for Electron
By default, node-gyp does not download the arm64 version of node.lib. Download it manually from https://electronjs.org/headers/v{version}/win-arm64/node.lib and move it to %APPDATA%\..\ Local\node-gyp\Cache\{version}\arm64\node.lib, substituting the Electron version number.
Architecture-specific code pitfall
Windows-specific code that uses if...else logic to select between x64 or x86 architectures will typically select the wrong architecture for arm64 targets. Use the npm_config_arch environment variable in build and packaging scripts instead of relying on process.arch.
Creating cross-compilation command prompt for ARM64
To create a cross-compilation command prompt for ARM64: 1) Duplicate the 'x64_x86 Cross Tools Command Prompt for VS 2017' shortcut, 2) Right-click and choose Properties, 3) Change the Target field to end with 'vcvarsamd64_arm64.bat' instead of 'vcvarsamd64_x86.bat'. For development on ARM device with x86 emulation, use vcvarsx86_arm64.bat instead.
Architecture detection example for Windows
Common architecture-specific code pattern: if (process.arch === 'x64') { // Do 64-bit thing... } else { // Do 32-bit thing... }. This pattern fails for ARM64 and should be replaced with environment variable checks.
Native modules ARM64 compilation requirements
Native modules for Windows ARM must compile against v142 of the MSVC compiler provided in Visual Studio 2017. Any pre-built .dll or .lib files must be available for Windows on ARM.