node-ffi-napi:ffi.Callback内Promise未resolved的原因及解决方法
ffi.Callback内异步函数Promise无法resolve的原因与解决方法
在Windows系统运行以下代码时,发现ffi.Callback中的自执行异步函数内,await psList()的Promise始终无法resolved,但预期应该能正常resolved。请问该Promise无法在ffi.Callback内resolved的原因是什么?有没有解决方法?
相关代码
import ref from "ref-napi"; import wchar from "ref-wchar-napi"; import psList from "ps-list"; let pidA = 0o1; if (process.platform === "win32") { const intPtr = ref.refType(ref.types.int32); const msgType = ref.types.void; const msgPtr = ref.refType(msgType); const EVENT_SYSTEM_FOREGROUND = 3; const WINEVENT_OUTOFCONTEXT = 0; const WINEVENT_SKIPOWNPROCESS = 2; const user32 = ffi.Library("user32", { GetForegroundWindow: ["int", []], GetWindowThreadProcessId: ["int", [ref.types.int32, intPtr]], SetWinEventHook: ["int", ["int", "int", "pointer", "pointer", "int", "int", "int"]], GetMessageA: ["bool", [msgPtr, "int", "uint", "uint"]], GetWindowTextW: ["int", ["pointer", "pointer", "int"]], GetWindowTextLengthW: ["int", ["pointer"]], }); function getMessage() { return user32.GetMessageA(ref.alloc(msgPtr), null, 0, 0); } const pfnWinEventProc = ffi.Callback( "void", ["pointer", "int", "pointer", "long", "long", "int", "int"], function (hWinEventHook, event, hwnd, idObject, idChild, idEventThread, dwmsEventTime) { const hwnd2 = user32.GetForegroundWindow(); const pid = ref.alloc(ref.types.int32); const result = user32.GetWindowThreadProcessId(hwnd2, pid); const windowTitleLength = user32.GetWindowTextLengthW(hwnd); const bufferSize = windowTitleLength * 2 + 4; const titleBuffer = Buffer.alloc(bufferSize); user32.GetWindowTextW(hwnd, titleBuffer, bufferSize); const titleText = ref.reinterpretUntilZeros(titleBuffer, wchar.size); const finallyWindowTitle = wchar.toString(titleText); pidA = pid.deref(); (async () => { let list = await psList(); list.forEach((data) => { console.log(data); data.list.forEach((item) => { if (item.pid === pidA) { console.log(`Foreground window ${JSON.stringify(item)}`); } }); }); })().catch(err => { console.error(err); }); console.log(`timestamp {finallyWindowTitle}, result:${result}, pid: ${pid.deref()}`); }); user32.SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, null, pfnWinEventProc, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS); let res = getMessage(); while (res != 0) { switch (res) { case -1: console.log("Invalid GetMessageA arguments or something!"); break; default: console.log("Got a message!"); } res = getMessage(); } } else { throw new Error("Plat not supported: " + process.platform); }
原因分析
- 线程模型冲突:ffi.Callback的回调函数运行在Windows的系统消息线程上,而Node.js的异步操作(包括Promise、async/await)依赖自身的事件循环(Event Loop)。系统消息线程被
GetMessageA的同步循环完全占据,没有任何机会让Node.js事件循环处理异步任务的完成回调,导致psList()返回的Promise永远无法触发resolve。 - 同步阻塞的消息循环:代码中的
while (res != 0)循环持续调用getMessage(),这是一个阻塞式的Win32 API,会死死卡住当前线程,Node.js事件循环根本无法推进,异步任务的完成信号得不到处理。
解决方法
方法1:将异步任务转移到Node.js事件循环队列
使用setImmediate或process.nextTick,把异步操作放到事件循环队列中,让主线程在消息循环的间隙处理:
// 修改ffi.Callback内的自执行异步函数部分 setImmediate(async () => { const currentPid = pid.deref(); // 捕获当前pid,避免闭包引用问题 let list = await psList(); list.forEach((data) => { console.log(data); data.list.forEach((item) => { if (item.pid === currentPid) { console.log(`Foreground window ${JSON.stringify(item)}`); } }); }); }).catch(err => { console.error(err); });
方法2:使用Worker Threads隔离异步任务
通过Node.js的worker_threads模块,把psList()这类异步操作放到单独的工作线程中执行,完全避开系统消息线程的阻塞:
// 先引入worker_threads模块 import { Worker } from 'worker_threads'; // 在ffi.Callback内替换原异步代码 const currentPid = pid.deref(); const worker = new Worker(` import psList from 'ps-list'; psList().then(list => { list.forEach(data => { data.list.forEach(item => { if (item.pid === ${currentPid}) { console.log(\`Foreground window \${JSON.stringify(item)}\`); } }); }); process.exit(); }).catch(err => { console.error(err); process.exit(); }); `, { eval: true });
方法3:改用非阻塞的消息处理方式
把同步的GetMessageA循环替换为非阻塞的PeekMessageA,给Node.js事件循环留出运行空间:
// 替换原getMessage函数和while循环 function processMessages() { const msg = ref.alloc(msgPtr); // 非阻塞获取并处理消息 while (user32.PeekMessageA(msg, null, 0, 0, 1)) { user32.TranslateMessage(msg); user32.DispatchMessageA(msg); } // 递归调用,让事件循环有机会执行其他任务 setImmediate(processMessages); } // 启动非阻塞消息处理 processMessages();
内容的提问来源于stack exchange,提问作者Ella
相关产品推荐
相关产品推荐

