69

node/electron插件: 由监听 Windows 打印机状态功能深入理解原生node插件编写过程

 5 years ago
source link: http://blog.isnap.cn/2019/03/24/node-electron插件-由监听-Windows-打印机状态功能深入理解原生node插件编写过程/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

这里说的插件,其实是基于 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);
}
bUBnima.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK