You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
}

原因分析

  1. 线程模型冲突:ffi.Callback的回调函数运行在Windows的系统消息线程上,而Node.js的异步操作(包括Promise、async/await)依赖自身的事件循环(Event Loop)。系统消息线程被GetMessageA的同步循环完全占据,没有任何机会让Node.js事件循环处理异步任务的完成回调,导致psList()返回的Promise永远无法触发resolve。
  2. 同步阻塞的消息循环:代码中的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 13:01:07