node/electron外掛: 由監聽 Windows 印表機狀態功能深入理解原生node外掛編寫過程
這裡說的外掛,其實是基於 node-addon-api 編寫的外掛。有人會說,其實 github 上已經有人開源的印表機相關的元件。
但是,它不是本人要的。
本人需要的是:第一時間知道印表機的及列印任務的所有狀態!
最初實現
開始寫第一個版本時,因為進度需要,本人快速實現了一個 dll 版本,然後在 electron 中通過 ffi 元件呼叫本人的 dll 。它工作得很好,但是它呼叫鏈中增加了一層 ffi ,讓本人很是介意~有點強迫症!!!
重寫版本
第一個版本功能穩定後,本人深入挖了一下 ffi 的功能實現(本人不是寫前端的,node也是初次接觸),Get 到它本身也是 C/C++ 實現的元件,然後看了下 node 官方對元件開發的相關介紹,決定繞過 ffi 把本人的 dll 直接變成 node 的外掛。
開始填坑
為什麼說是開始填坑?
因為本人的功能是 C/C++ & C# 混編的!這中間的坑只填過了,才知深淺。
坑1:專案配置 —— 託管 /clr
node 原生外掛開發使用了 gyp 配置,為了方便大家使用,官方提供了開源配置專案 node-gyp ,依葫蘆畫瓢,很快完成了 Hello World. ,但是,咱怎麼能忘記了混編呢?微軟對於 C/C++ & C# 混編的配置選項叫 /clr 。找到 MSVSSettings.py 中 /clr 註釋對應的配置選項為 CompileAsManaged ,當然也有人在 issue 裡提了在 AdditionalOptions 裡面增加 /clr ,本人不反對,本人也沒有驗證,而是選擇使用開原始碼提供的 CompileAsManaged 選項。有過混編經驗的都知道,光改完 /clr 是遠遠不夠,還要改程式集等等一堆選項。這裡有一個小技巧,就是可以依賴 npm install 來處理,最終修改到的選項如下:
"RuntimeLibrary": 2, #MultiThreadedDLL /MD "Optimization": 2, "RuntimeTypeInfo": "true", "CompileAsManaged": "true", # /clr "DebugInformationFormat": 3, #ProgramDatabase /Zi "ExceptionHandling": 0, #Async /EHa "BasicRuntimeChecks": 0, #Default
坑2:專案配置 —— win_delay_load_hook
踩過坑1後,開始寫邏輯了,並且也順利的實現了功能,開始排程時卻被告之:
正嘗試在 OS 載入程式鎖內執行託管程式碼。不要嘗試在 DllMain 或映像初始化函式內執行託管程式碼,這樣做會導致應用程式掛起。
按第一版的實現,本人知道要在 dll 註冊位置加上:
#pragma unmanaged
但是,這個位置具體在哪呢?第一反應應該就是 node 外掛初始化的巨集位置,但……
於是又重新翻看了 node addon 的文件,找到了 win_delay_load_hook 這個配置,要設定成 true ,但其實它預設就是 true。既然是預設選項,為何還是不行呢?仔細看此配置的功能,它其實是在專案中預設增加了 win_delay_load_hook.cc 的檔案,原始檔位於 node-gyp/src 中,將其找出來看後才知道 dll 的入口在這,並且與 depend++ 檢視 dll 的匯出是一致的,在此檔案中加上 #pragma unmanaged 後,程式能順利運行了。
這裡有個小技巧:win_delay_load_hook.cc 預設在 node_modules 中,而且專案一般不會直接帶上這個資料夾,也就是說如果每個開發人員重新 npm install 時此檔案會被覆蓋,我們其實可以在 gyp 配置中把 win_delay_load_hook 設定成 false ,同時把 win_delay_load_hook.cc 拷貝到專案的原始檔中,編譯檔案中加上這個檔案即可。
坑3:非同步多次回撥
node-addon-api 對非同步工作有封裝,詳見 Napi::AsyncWorker 的使用,但是對於多次回撥,這個類並沒有支援得很好(也有可能是我使用不當),為了解決這個問題,本人翻了很多 github 上的專案,都沒有很好的解決,後來在 github 上找到了 node-addon-examples 找到了 node-addon 的 C 實現 async_work_thread_safe_function 的 example 中有較好的實現,對比了它和 Napi::AsyncWorker 的邏輯過程,發現 Napi::AsyncWorker 應該是不能很好的完成本人需要的功能,所以決定自己實現,具體就是把 async_work_thread_safe_function 參照 Napi::AsyncWorker 改成了模板虛基類。感興趣的可以聯絡。
坑4:印表機監控執行緒與回撥 JS 執行緒同步
其實,多執行緒同步方式有很多,但是為了讓 js 執行緒和工作執行緒不是一直處於工作狀態中,而是有事件時才開始工作和回撥,本人選擇了 event & critical_section 一起來完成本工作,event 用於印表機事件到達後通知 js 執行緒取資料,而 critical_section 保證的是對於資料操作的唯一性。我相信大神們肯定有很多別的實現方式,比如說管道等。希望大家提供各種意見吧。
關鍵實現
// safe_async_worker.h template <typename T> class SafeAsyncWorker : public Napi::ObjectWrap<T> { public: SafeAsyncWorker(const Napi::CallbackInfo &info); protected: virtual void Execute() = 0; virtual Napi::Value Parse(napi_env env, void *data) = 0; virtual void Free(void *data) = 0; // Create a thread-safe function and an async queue work item. We pass the // thread-safe function to the async queue work item so the latter might have a // chance to call into JavaScript from the worker thread on which the // ExecuteWork callback runs. Napi::Value CreateAsyncWork(const Napi::CallbackInfo &cb); // This function runs on a worker thread. It has no access to the JavaScript // environment except through the thread-safe function. static void OnExecuteWork(napi_env env, void *data); // This function runs on the main thread after `ExecuteWork` exits. static void OnWorkComplete(napi_env env, napi_status status, void *data); // This function is responsible for converting data coming in from the worker // thread to napi_value items that can be passed into JavaScript, and for // calling the JavaScript function. static void OnCallJavaScript(napi_env env, napi_value js_cb, void *context, void *data); void SubmitWork(void *data); static Napi::FunctionReference constructor; private: napi_async_work work; napi_threadsafe_function tsfn; };
// safe_async_worker.inl template <typename T> Napi::FunctionReference SafeAsyncWorker<T>::constructor; template <typename T> inline SafeAsyncWorker<T>::SafeAsyncWorker(const Napi::CallbackInfo &info) : Napi::ObjectWrap<T>(info) { } template <typename T> void printer::SafeAsyncWorker<T>::SubmitWork(void *data) { // Initiate the call into JavaScript. The call into JavaScript will not // have happened when this function returns, but it will be queued. assert(napi_call_threadsafe_function(tsfn, data, napi_tsfn_blocking) == napi_ok); } template <typename T> Napi::Value SafeAsyncWorker<T>::CreateAsyncWork(const Napi::CallbackInfo &cb) { Napi::Env env = cb.Env(); napi_value work_name; // Create a string to describe this asynchronous operation. assert(napi_create_string_utf8(env, typeid(T).name(), NAPI_AUTO_LENGTH, &work_name) == napi_ok); // Convert the callback retrieved from JavaScript into a thread-safe function // which we can call from a worker thread. assert(napi_create_threadsafe_function(env, cb[0], NULL, work_name, 0, 1, NULL, NULL, this, OnCallJavaScript, &(tsfn)) == napi_ok); // Create an async work item, passing in the addon data, which will give the // worker thread access to the above-created thread-safe function. assert(napi_create_async_work(env, NULL, work_name, OnExecuteWork, OnWorkComplete, this, &(work)) == napi_ok); // Queue the work item for execution. assert(napi_queue_async_work(env, work) == napi_ok); // This causes `undefined` to be returned to JavaScript. return env.Undefined(); } template <typename T> void SafeAsyncWorker<T>::OnExecuteWork(napi_env /*env*/, void *this_pointer) { T *self = static_cast<T *>(this_pointer); // We bracket the use of the thread-safe function by this thread by a call to // napi_acquire_threadsafe_function() here, and by a call to // napi_release_threadsafe_function() immediately prior to thread exit. assert(napi_acquire_threadsafe_function(self->tsfn) == napi_ok); #ifdef NAPI_CPP_EXCEPTIONS try { self->Execute(); } catch (const std::exception &e) { // TODO } #else// NAPI_CPP_EXCEPTIONS self->Execute(); #endif // NAPI_CPP_EXCEPTIONS // Indicate that this thread will make no further use of the thread-safe function. assert(napi_release_threadsafe_function(self->tsfn, napi_tsfn_release) == napi_ok); } template <typename T> void SafeAsyncWorker<T>::OnWorkComplete(napi_env env, napi_status status, void *this_pointer) { T *self = (T *)this_pointer; // Clean up the thread-safe function and the work item associated with this // run. assert(napi_release_threadsafe_function(self->tsfn, napi_tsfn_release) == napi_ok); assert(napi_delete_async_work(env, self->work) == napi_ok); // Set both values to NULL so JavaScript can order a new run of the thread. self->work = NULL; self->tsfn = NULL; } template <typename T> void SafeAsyncWorker<T>::OnCallJavaScript(napi_env env, napi_value js_cb, void *this_pointer, void *data) { T *self = static_cast<T *>(this_pointer); if (env != NULL) { napi_value undefined; #ifdef NAPI_CPP_EXCEPTIONS try { napi_value js_value = self->Parse(env, data); } catch (const std::exception &e) { // TODO } #else// NAPI_CPP_EXCEPTIONS napi_value js_value = self->Parse(env, data); #endif // NAPI_CPP_EXCEPTIONS // Retrieve the JavaScript `undefined` value so we can use it as the `this` // value of the JavaScript function call. assert(napi_get_undefined(env, &undefined) == napi_ok); // Call the JavaScript function and pass it the prime that the secondary // thread found. assert(napi_call_function(env, undefined, js_cb, 1, &js_value, NULL) == napi_ok); } self->Free(data); }
