diff --git a/docs/tutorial/native-code-and-electron-cpp-linux.md b/docs/tutorial/native-code-and-electron-cpp-linux.md index 6abf021272..f9790466ec 100644 --- a/docs/tutorial/native-code-and-electron-cpp-linux.md +++ b/docs/tutorial/native-code-and-electron-cpp-linux.md @@ -1097,7 +1097,8 @@ public: Napi::Function func = DefineClass(env, "CppLinuxAddon", { InstanceMethod("helloWorld", &CppAddon::HelloWorld), InstanceMethod("helloGui", &CppAddon::HelloGui), - InstanceMethod("on", &CppAddon::On) + InstanceMethod("on", &CppAddon::On), + InstanceMethod("destroy", &CppAddon::Destroy) }); Napi::FunctionReference *constructor = new Napi::FunctionReference(); @@ -1139,11 +1140,12 @@ private: Here, we create a C++ class that inherits from `Napi::ObjectWrap`: -`static Napi::Object Init` defines our JavaScript interface with three methods: +`static Napi::Object Init` defines our JavaScript interface with four methods: * `helloWorld`: A simple function to test the bridge * `helloGui`: The function to launch our GTK3 UI * `on`: A method to register event callbacks +* `destroy`: A method to release all persistent references before app quit The constructor initializes: @@ -1354,7 +1356,8 @@ public: Napi::Function func = DefineClass(env, "CppLinuxAddon", { InstanceMethod("helloWorld", &CppAddon::HelloWorld), InstanceMethod("helloGui", &CppAddon::HelloGui), - InstanceMethod("on", &CppAddon::On) + InstanceMethod("on", &CppAddon::On), + InstanceMethod("destroy", &CppAddon::Destroy) }); Napi::FunctionReference *constructor = new Napi::FunctionReference(); @@ -1497,6 +1500,20 @@ private: callbacks.Value().Set(info[0].As(), info[1].As()); return env.Undefined(); } + + Napi::Value Destroy(const Napi::CallbackInfo &info) + { + callbacks.Reset(); + emitter.Reset(); + + if (tsfn_ != nullptr) + { + napi_release_threadsafe_function(tsfn_, napi_tsfn_abort); + tsfn_ = nullptr; + } + + return info.Env().Undefined(); + } }; Napi::Object Init(Napi::Env env, Napi::Object exports) @@ -1547,6 +1564,10 @@ class CppLinuxAddon extends EventEmitter { return this.addon.helloGui() } + destroy() { + this.addon.destroy() + } + // Parse JSON and convert date to JavaScript Date object parse(payload) { const parsed = JSON.parse(payload) @@ -1569,8 +1590,12 @@ This wrapper: * Only loads on Linux platforms * Forwards events from C++ to JavaScript * Provides clean methods to call into C++ +* Provides a `destroy()` method to release native resources * Converts JSON data into proper JavaScript objects +> [!IMPORTANT] +> You must call `destroy()` before the app quits (e.g. in the `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. + ## 7) Building and testing the addon With all files in place, you can build the addon: diff --git a/docs/tutorial/native-code-and-electron-cpp-win32.md b/docs/tutorial/native-code-and-electron-cpp-win32.md index 8c063edc88..337e9b7757 100644 --- a/docs/tutorial/native-code-and-electron-cpp-win32.md +++ b/docs/tutorial/native-code-and-electron-cpp-win32.md @@ -1099,7 +1099,8 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) { Napi::Function func = DefineClass(env, "CppWin32Addon", { InstanceMethod("helloWorld", &CppAddon::HelloWorld), InstanceMethod("helloGui", &CppAddon::HelloGui), - InstanceMethod("on", &CppAddon::On) + InstanceMethod("on", &CppAddon::On), + InstanceMethod("destroy", &CppAddon::Destroy) }); // ... rest of Init function @@ -1117,9 +1118,21 @@ Napi::Value On(const Napi::CallbackInfo& info) { callbacks.Value().Set(info[0].As(), info[1].As()); return env.Undefined(); } + +Napi::Value Destroy(const Napi::CallbackInfo& info) { + callbacks.Reset(); + emitter.Reset(); + + if (tsfn_ != nullptr) { + napi_release_threadsafe_function(tsfn_, napi_tsfn_abort); + tsfn_ = nullptr; + } + + return info.Env().Undefined(); +} ``` -This allows JavaScript to register callbacks for specific event types. +This allows JavaScript to register callbacks for specific event types. The `Destroy` method releases all persistent references and aborts the threadsafe function, which must be called before the app quits to prevent the process from hanging. ### Putting the bridge together @@ -1261,6 +1274,18 @@ private: callbacks.Value().Set(info[0].As(), info[1].As()); return env.Undefined(); } + + Napi::Value Destroy(const Napi::CallbackInfo& info) { + callbacks.Reset(); + emitter.Reset(); + + if (tsfn_ != nullptr) { + napi_release_threadsafe_function(tsfn_, napi_tsfn_abort); + tsfn_ = nullptr; + } + + return info.Env().Undefined(); + } }; Napi::Object Init(Napi::Env env, Napi::Object exports) { @@ -1309,6 +1334,10 @@ class CppWin32Addon extends EventEmitter { this.addon.helloGui() } + destroy() { + this.addon.destroy() + } + #parse(payload) { const parsed = JSON.parse(payload) @@ -1323,6 +1352,9 @@ if (process.platform === 'win32') { } ``` +> [!IMPORTANT] +> You must call `destroy()` before the app quits (e.g. in the `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. + ## 7) Building and Testing the Addon With all files in place, you can build the addon: diff --git a/docs/tutorial/native-code-and-electron-objc-macos.md b/docs/tutorial/native-code-and-electron-objc-macos.md index 5572a05bf6..13175ef7f7 100644 --- a/docs/tutorial/native-code-and-electron-objc-macos.md +++ b/docs/tutorial/native-code-and-electron-objc-macos.md @@ -753,7 +753,8 @@ public: Napi::Function func = DefineClass(env, "ObjcMacosAddon", { InstanceMethod("helloWorld", &ObjcAddon::HelloWorld), InstanceMethod("helloGui", &ObjcAddon::HelloGui), - InstanceMethod("on", &ObjcAddon::On) + InstanceMethod("on", &ObjcAddon::On), + InstanceMethod("destroy", &ObjcAddon::Destroy) }); Napi::FunctionReference* constructor = new Napi::FunctionReference(); @@ -915,6 +916,18 @@ Napi::Value On(const Napi::CallbackInfo& info) { callbacks.Value().Set(info[0].As(), info[1].As()); return env.Undefined(); } + +Napi::Value Destroy(const Napi::CallbackInfo& info) { + callbacks.Reset(); + emitter.Reset(); + + if (tsfn_ != nullptr) { + napi_release_threadsafe_function(tsfn_, napi_tsfn_abort); + tsfn_ = nullptr; + } + + return info.Env().Undefined(); +} ``` Let's take a look at what we've added in this step: @@ -922,10 +935,11 @@ Let's take a look at what we've added in this step: * `HelloWorld()`: Takes a string input, calls our Objective-C function, and returns the result * `HelloGui()`: A simple wrapper around the Objective-C `hello_gui` function * `On`: Allows JavaScript to register event listeners that will be called when native events occur +* `Destroy`: Releases all persistent references (callbacks and emitter) and aborts the threadsafe function, allowing the addon to be properly cleaned up on quit The `On` method is particularly important as it creates the event system that our JavaScript code will use to receive notifications from the native UI. -Together, these three components form a complete bridge between our Objective-C code and the JavaScript world, allowing bidirectional communication. Here's what the finished file should look like: +Together, these four components form a complete bridge between our Objective-C code and the JavaScript world, allowing bidirectional communication. Here's what the finished file should look like: ```objc title='src/objc_addon.mm' #include @@ -938,7 +952,8 @@ public: Napi::Function func = DefineClass(env, "ObjcMacosAddon", { InstanceMethod("helloWorld", &ObjcAddon::HelloWorld), InstanceMethod("helloGui", &ObjcAddon::HelloGui), - InstanceMethod("on", &ObjcAddon::On) + InstanceMethod("on", &ObjcAddon::On), + InstanceMethod("destroy", &ObjcAddon::Destroy) }); Napi::FunctionReference* constructor = new Napi::FunctionReference(); @@ -1061,6 +1076,18 @@ private: callbacks.Value().Set(info[0].As(), info[1].As()); return env.Undefined(); } + + Napi::Value Destroy(const Napi::CallbackInfo& info) { + callbacks.Reset(); + emitter.Reset(); + + if (tsfn_ != nullptr) { + napi_release_threadsafe_function(tsfn_, napi_tsfn_abort); + tsfn_ = nullptr; + } + + return info.Env().Undefined(); + } }; Napi::Object Init(Napi::Env env, Napi::Object exports) { @@ -1101,6 +1128,10 @@ class ObjcMacosAddon extends EventEmitter { this.addon.helloGui() } + destroy () { + this.addon.destroy() + } + parse (payload) { const parsed = JSON.parse(payload) @@ -1122,7 +1153,11 @@ This wrapper: 3. Loads the native addon 4. Sets up event listeners and forwards them 5. Provides a clean API for our functions -6. Parses JSON payloads and converts timestamps to JavaScript Date objects +6. Provides a `destroy()` method to release native resources +7. Parses JSON payloads and converts timestamps to JavaScript Date objects + +> [!IMPORTANT] +> You must call `destroy()` before the app quits (e.g. in the `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. ## 7) Building and Testing the Addon diff --git a/docs/tutorial/native-code-and-electron-swift-macos.md b/docs/tutorial/native-code-and-electron-swift-macos.md index 228260652f..b675c61e81 100644 --- a/docs/tutorial/native-code-and-electron-swift-macos.md +++ b/docs/tutorial/native-code-and-electron-swift-macos.md @@ -752,7 +752,8 @@ public: Napi::Function func = DefineClass(env, "SwiftAddon", { InstanceMethod("helloWorld", &SwiftAddon::HelloWorld), InstanceMethod("helloGui", &SwiftAddon::HelloGui), - InstanceMethod("on", &SwiftAddon::On) + InstanceMethod("on", &SwiftAddon::On), + InstanceMethod("destroy", &SwiftAddon::Destroy) }); Napi::FunctionReference* constructor = new Napi::FunctionReference(); @@ -770,7 +771,7 @@ This first part: 1. Defines a C++ class that inherits from `Napi::ObjectWrap` 2. Creates a static `Init` method to register our class with Node.js -3. Defines three methods: `helloWorld`, `helloGui`, and `on` +3. Defines four methods: `helloWorld`, `helloGui`, `on`, and `destroy` ### Callback Mechanism @@ -919,6 +920,18 @@ private: callbacks.Value().Set(info[0].As(), info[1].As()); return env.Undefined(); } + + Napi::Value Destroy(const Napi::CallbackInfo& info) { + callbacks.Reset(); + emitter.Reset(); + + if (tsfn_ != nullptr) { + napi_release_threadsafe_function(tsfn_, napi_tsfn_abort); + tsfn_ = nullptr; + } + + return info.Env().Undefined(); + } }; Napi::Object Init(Napi::Env env, Napi::Object exports) { @@ -934,7 +947,8 @@ This final part does multiple things: 2. The HelloWorld method implementation takes a string input from JavaScript, passes it to the Swift code, and returns the processed result back to the JavaScript environment. 3. The `HelloGui` method implementation provides a simple wrapper that calls the Swift UI creation function to display the native macOS window. 4. The `On` method implementation allows JavaScript code to register callback functions that will be invoked when specific events occur in the native Swift code. -5. The code sets up the module initialization process that registers the addon with Node.js and makes its functionality available to JavaScript. +5. The `Destroy` method releases all persistent references (callbacks and emitter) and aborts the threadsafe function. This must be called before the app quits to allow the destructor to run and prevent the process from hanging. +6. The code sets up the module initialization process that registers the addon with Node.js and makes its functionality available to JavaScript. The final and full `src/swift_addon.mm` should look like: @@ -949,7 +963,8 @@ public: Napi::Function func = DefineClass(env, "SwiftAddon", { InstanceMethod("helloWorld", &SwiftAddon::HelloWorld), InstanceMethod("helloGui", &SwiftAddon::HelloGui), - InstanceMethod("on", &SwiftAddon::On) + InstanceMethod("on", &SwiftAddon::On), + InstanceMethod("destroy", &SwiftAddon::Destroy) }); Napi::FunctionReference* constructor = new Napi::FunctionReference(); @@ -1074,6 +1089,18 @@ private: callbacks.Value().Set(info[0].As(), info[1].As()); return env.Undefined(); } + + Napi::Value Destroy(const Napi::CallbackInfo& info) { + callbacks.Reset(); + emitter.Reset(); + + if (tsfn_ != nullptr) { + napi_release_threadsafe_function(tsfn_, napi_tsfn_abort); + tsfn_ = nullptr; + } + + return info.Env().Undefined(); + } }; Napi::Object Init(Napi::Env env, Napi::Object exports) { @@ -1122,6 +1149,10 @@ class SwiftAddon extends EventEmitter { this.addon.helloGui() } + destroy () { + this.addon.destroy() + } + parse (payload) { const parsed = JSON.parse(payload) @@ -1143,7 +1174,11 @@ This wrapper: 3. Loads the native addon 4. Sets up event listeners and forwards them 5. Provides a clean API for our functions -6. Parses JSON payloads and converts timestamps to JavaScript Date objects +6. Provides a `destroy()` method to release native resources +7. Parses JSON payloads and converts timestamps to JavaScript Date objects + +> [!IMPORTANT] +> You must call `destroy()` before the app quits (e.g. in the `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. ## 7) Building and Testing the Addon