node程序员

node/electron插件: 由监听 Windows 打印机

2019-03-24  本文已影响0人  三岁于辛

写在前面

这里说的插件,其实是基于 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);
}
上一篇下一篇

猜你喜欢

热点阅读