寫在前面
這里說的插件凿将,其實是基于 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);
}