node/electron插件: 由監(jiān)聽(tīng) Windows 打印機(jī)狀態(tài)功能深入理解原生node插件編寫(xiě)過(guò)程

寫(xiě)在前面

這里說(shuō)的插件,其實(shí)是基于 node-addon-api 編寫(xiě)的插件。有人會(huì)說(shuō),其實(shí) github 上已經(jīng)有人開(kāi)源的打印機(jī)相關(guān)的組件。
但是,它不是本人要的。
本人需要的是:第一時(shí)間知道打印機(jī)的及打印任務(wù)的所有狀態(tài)!

最初實(shí)現(xiàn)

開(kāi)始寫(xiě)第一個(gè)版本時(shí),因?yàn)檫M(jìn)度需要,本人快速實(shí)現(xiàn)了一個(gè) dll 版本,然后在 electron 中通過(guò) ffi 組件調(diào)用本人的 dll 。它工作得很好,但是它調(diào)用鏈中增加了一層 ffi ,讓本人很是介意~有點(diǎn)強(qiáng)迫癥!??!

重寫(xiě)版本

第一個(gè)版本功能穩(wěn)定后,本人深入挖了一下 ffi 的功能實(shí)現(xiàn)(本人不是寫(xiě)前端的,node也是初次接觸),Get 到它本身也是 C/C++ 實(shí)現(xiàn)的組件,然后看了下 node 官方對(duì)組件開(kāi)發(fā)的相關(guān)介紹,決定繞過(guò) ffi 把本人的 dll 直接變成 node 的插件。

開(kāi)始填坑

為什么說(shuō)是開(kāi)始填坑?
因?yàn)楸救说墓δ苁?C/C++ & C# 混編的!這中間的坑只填過(guò)了,才知深淺。

坑1:項(xiàng)目配置 —— 托管 /clr

node 原生插件開(kāi)發(fā)使用了 gyp 配置,為了方便大家使用,官方提供了開(kāi)源配置項(xiàng)目 node-gyp,依葫蘆畫(huà)瓢,很快完成了 Hello World.,但是,咱怎么能忘記了混編呢?微軟對(duì)于 C/C++ & C# 混編的配置選項(xiàng)叫 /clr 。找到 MSVSSettings.py 中 /clr 注釋對(duì)應(yīng)的配置選項(xiàng)為 CompileAsManaged ,當(dāng)然也有人在 issue 里提了在 AdditionalOptions 里面增加 /clr ,本人不反對(duì),本人也沒(méi)有驗(yàn)證,而是選擇使用開(kāi)源代碼提供的 CompileAsManaged 選項(xiàng)。有過(guò)混編經(jīng)驗(yàn)的都知道,光改完 /clr 是遠(yuǎn)遠(yuǎn)不夠,還要改程序集等等一堆選項(xiàng)。這里有一個(gè)小技巧,就是可以依賴(lài) npm install 來(lái)處理,最終修改到的選項(xiàng)如下:

"RuntimeLibrary": 2, #MultiThreadedDLL /MD
"Optimization": 2,
"RuntimeTypeInfo": "true",
"CompileAsManaged": "true", # /clr
"DebugInformationFormat": 3, #ProgramDatabase /Zi
"ExceptionHandling": 0, #Async /EHa
"BasicRuntimeChecks": 0, #Default

坑2:項(xiàng)目配置 —— win_delay_load_hook

踩過(guò)坑1后,開(kāi)始寫(xiě)邏輯了,并且也順利的實(shí)現(xiàn)了功能,開(kāi)始調(diào)度時(shí)卻被告之:

正嘗試在 OS 加載程序鎖內(nèi)執(zhí)行托管代碼。不要嘗試在 DllMain 或映像初始化函數(shù)內(nèi)運(yùn)行托管代碼,這樣做會(huì)導(dǎo)致應(yīng)用程序掛起。

按第一版的實(shí)現(xiàn),本人知道要在 dll 注冊(cè)位置加上:

#pragma unmanaged

但是,這個(gè)位置具體在哪呢?第一反應(yīng)應(yīng)該就是 node 插件初始化的宏位置,但......
于是又重新翻看了 node addon 的文檔,找到了 win_delay_load_hook 這個(gè)配置,要設(shè)置成 true ,但其實(shí)它默認(rèn)就是 true。既然是默認(rèn)選項(xiàng),為何還是不行呢?仔細(xì)看此配置的功能,它其實(shí)是在項(xiàng)目中默認(rèn)增加了 win_delay_load_hook.cc 的文件,源文件位于 node-gyp/src 中,將其找出來(lái)看后才知道 dll 的入口在這,并且與 depend++ 查看 dll 的導(dǎo)出是一致的,在此文件中加上 #pragma unmanaged 后,程序能順利運(yùn)行了。

這里有個(gè)小技巧:win_delay_load_hook.cc 默認(rèn)在 node_modules 中,而且項(xiàng)目一般不會(huì)直接帶上這個(gè)文件夾,也就是說(shuō)如果每個(gè)開(kāi)發(fā)人員重新 npm install 時(shí)此文件會(huì)被覆蓋,我們其實(shí)可以在 gyp 配置中把 win_delay_load_hook 設(shè)置成 false ,同時(shí)把 win_delay_load_hook.cc 拷貝到項(xiàng)目的源文件中,編譯文件中加上這個(gè)文件即可。
最新修正:electron 的時(shí)候,win_delay_load_hook.cc 以上述操作會(huì)運(yùn)行不了,所以需要修改 win_delay_load_hook 設(shè)置為 true ,然后在 copies 中增加 源文件目錄中修改后的到 <(node_gyp_src)/src 中。

坑3:異步多次回調(diào)

node-addon-api 對(duì)異步工作有封裝,詳見(jiàn) Napi::AsyncWorker 的使用,但是對(duì)于多次回調(diào),這個(gè)類(lèi)并沒(méi)有支持得很好(也有可能是我使用不當(dāng)),為了解決這個(gè)問(wèn)題,本人翻了很多 github 上的項(xiàng)目,都沒(méi)有很好的解決,后來(lái)在 github 上找到了 node-addon-examples 找到了 node-addon 的 C 實(shí)現(xiàn) async_work_thread_safe_function 的 example 中有較好的實(shí)現(xiàn),對(duì)比了它和 Napi::AsyncWorker 的邏輯過(guò)程,發(fā)現(xiàn) Napi::AsyncWorker 應(yīng)該是不能很好的完成本人需要的功能,所以決定自己實(shí)現(xiàn),具體就是把 async_work_thread_safe_function 參照 Napi::AsyncWorker 改成了模板虛基類(lèi)。感興趣的可以聯(lián)系。

坑4:打印機(jī)監(jiān)控線程與回調(diào) JS 線程同步

其實(shí),多線程同步方式有很多,但是為了讓 js 線程和工作線程不是一直處于工作狀態(tài)中,而是有事件時(shí)才開(kāi)始工作和回調(diào),本人選擇了 event & critical_section 一起來(lái)完成本工作,event 用于打印機(jī)事件到達(dá)后通知 js 線程取數(shù)據(jù),而 critical_section 保證的是對(duì)于數(shù)據(jù)操作的唯一性。我相信大神們肯定有很多別的實(shí)現(xiàn)方式,比如說(shuō)管道等。希望大家提供各種意見(jiàn)吧。

關(guān)鍵實(shí)現(xiàn)

// 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);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容