node/electron插件: 由监听 Windows 打印机
写在前面
这里说的插件,其实是基于 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);
}