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 拷贝到项目的源文件中,编译文件中加上这个文件即可。
最新修正:electron 的时候,win_delay_load_hook.cc 以上述操作会运行不了,所以需要修改 win_delay_load_hook 设置为 true ,然后在 copies 中增加 源文件目录中修改后的到 <(node_gyp_src)/src 中。

坑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);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容