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

寫在前面

這里說的插件凿将,其實是基于 node-addon-api 編寫的插件校套。有人會說,其實 github 上已經(jīng)有人開源的打印機相關(guān)的組件牧抵。
但是笛匙,它不是本人要的。
本人需要的是:第一時間知道打印機的及打印任務(wù)的所有狀態(tài)犀变!

最初實現(xiàn)

開始寫第一個版本時妹孙,因為進度需要,本人快速實現(xiàn)了一個 dll 版本获枝,然后在 electron 中通過 ffi 組件調(diào)用本人的 dll 蠢正。它工作得很好,但是它調(diào)用鏈中增加了一層 ffi 省店,讓本人很是介意~有點強迫癥O浮!萨西!

重寫版本

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

開始填坑

為什么說是開始填坑娱俺?
因為本人的功能是 C/C++ & C# 混編的!這中間的坑只填過了废麻,才知深淺荠卷。

坑1:項目配置 —— 托管 /clr

node 原生插件開發(fā)使用了 gyp 配置,為了方便大家使用烛愧,官方提供了開源配置項目 node-gyp油宜,依葫蘆畫瓢,很快完成了 Hello World.怜姿,但是慎冤,咱怎么能忘記了混編呢?微軟對于 C/C++ & C# 混編的配置選項叫 /clr 沧卢。找到 MSVSSettings.py 中 /clr 注釋對應(yīng)的配置選項為 CompileAsManaged 蚁堤,當(dāng)然也有人在 issue 里提了在 AdditionalOptions 里面增加 /clr ,本人不反對但狭,本人也沒有驗證披诗,而是選擇使用開源代碼提供的 CompileAsManaged 選項撬即。有過混編經(jīng)驗的都知道,光改完 /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后才沧,開始寫邏輯了,并且也順利的實現(xiàn)了功能绍刮,開始調(diào)度時卻被告之:

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

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

#pragma unmanaged

但是得运,這個位置具體在哪呢膝蜈?第一反應(yīng)應(yīng)該就是 node 插件初始化的宏位置,但......
于是又重新翻看了 node addon 的文檔熔掺,找到了 win_delay_load_hook 這個配置饱搏,要設(shè)置成 true ,但其實它默認就是 true置逻。既然是默認選項推沸,為何還是不行呢?仔細看此配置的功能券坞,它其實是在項目中默認增加了 win_delay_load_hook.cc 的文件鬓催,源文件位于 node-gyp/src 中,將其找出來看后才知道 dll 的入口在這恨锚,并且與 depend++ 查看 dll 的導(dǎo)出是一致的宇驾,在此文件中加上 #pragma unmanaged 后,程序能順利運行了猴伶。

這里有個小技巧:win_delay_load_hook.cc 默認在 node_modules 中课舍,而且項目一般不會直接帶上這個文件夾,也就是說如果每個開發(fā)人員重新 npm install 時此文件會被覆蓋他挎,我們其實可以在 gyp 配置中把 win_delay_load_hook 設(shè)置成 false 筝尾,同時把 win_delay_load_hook.cc 拷貝到項目的源文件中,編譯文件中加上這個文件即可雇盖。
最新修正:electron 的時候忿等,win_delay_load_hook.cc 以上述操作會運行不了,所以需要修改 win_delay_load_hook 設(shè)置為 true 崔挖,然后在 copies 中增加 源文件目錄中修改后的到 <(node_gyp_src)/src 中贸街。

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

node-addon-api 對異步工作有封裝庵寞,詳見 Napi::AsyncWorker 的使用,但是對于多次回調(diào)薛匪,這個類并沒有支持得很好(也有可能是我使用不當(dāng))捐川,為了解決這個問題,本人翻了很多 github 上的項目逸尖,都沒有很好的解決古沥,后來在 github 上找到了 node-addon-examples 找到了 node-addon 的 C 實現(xiàn) async_work_thread_safe_function 的 example 中有較好的實現(xiàn),對比了它和 Napi::AsyncWorker 的邏輯過程娇跟,發(fā)現(xiàn) Napi::AsyncWorker 應(yīng)該是不能很好的完成本人需要的功能岩齿,所以決定自己實現(xiàn),具體就是把 async_work_thread_safe_function 參照 Napi::AsyncWorker 改成了模板虛基類苞俘。感興趣的可以聯(lián)系盹沈。

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

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

關(guān)鍵實現(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)容合作請聯(lián)系作者
  • 序言:七十年代末监徘,一起剝皮案震驚了整個濱河市悍缠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌耐量,老刑警劉巖飞蚓,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異廊蜒,居然都是意外死亡趴拧,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門山叮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來著榴,“玉大人,你說我怎么就攤上這事屁倔∧杂郑” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長问麸。 經(jīng)常有香客問我往衷,道長,這世上最難降的妖魔是什么严卖? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任席舍,我火速辦了婚禮,結(jié)果婚禮上哮笆,老公的妹妹穿的比我還像新娘来颤。我一直安慰自己,他們只是感情好稠肘,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布福铅。 她就那樣靜靜地躺著,像睡著了一般项阴。 火紅的嫁衣襯著肌膚如雪本讥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天鲁冯,我揣著相機與錄音,去河邊找鬼色查。 笑死薯演,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的秧了。 我是一名探鬼主播跨扮,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼验毡!你這毒婦竟也來了衡创?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤晶通,失蹤者是張志新(化名)和其女友劉穎璃氢,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狮辽,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡一也,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了喉脖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片椰苟。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖树叽,靈堂內(nèi)的尸體忽然破棺而出舆蝴,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布洁仗,位于F島的核電站层皱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏京痢。R本人自食惡果不足惜奶甘,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望祭椰。 院中可真熱鬧臭家,春花似錦、人聲如沸方淤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽携茂。三九已至你踩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間讳苦,已是汗流浹背带膜。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鸳谜,地道東北人膝藕。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像咐扭,于是被迫代替她去往敵國和親芭挽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容