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

C DLL与Python通过ZeroMQ实现IPC遇阻:回调消息无法传至Python日志

问题分析与修复方案

你遇到的问题主要是DLL端ZeroMQ资源管理错误、消息发送方式不对,加上Windows钩子回调的特殊限制导致IPC无法正常工作。咱们一步步拆解问题并修复:

核心问题点

1. ZeroMQ上下文与Socket的变量作用域冲突

你的DLL里定义了全局的requester和context,但在ConnectServer函数里又重新定义了同名的静态变量:

static void* context = zmq_ctx_new();
static void* requester = zmq_socket(context, ZMQ_REQ);

这就导致全局变量根本没被赋值,后续钩子回调里用的是未初始化的requester,自然发不出消息。

2. 错误的消息发送方式

你直接把LPMSG指针传给zmq_send:

zmq_send(requester, data, 5, 0);

这里有两个问题:

  • 你发送的是内存地址,而不是MSG结构体的实际数据,Python端收到的是无效地址值,不是消息内容
  • 指定的发送长度是5,但MSG结构体的大小远大于5,这会导致数据截断

3. 钩子回调里的阻塞操作

ZeroMQ的REQ/REP模式是请求-响应成对的,你在钩子回调里写了10次循环的请求-响应,而Windows钩子回调不能长时间阻塞,否则会导致系统卡顿甚至钩子失效。而且回调是在目标进程的线程里执行的,长时间占用会出问题。

4. Python端的线程问题

你在Python里把加载DLL、设置钩子和消息循环放在同一个线程,而pythoncom.PumpMessages()会阻塞当前线程,这会导致ZeroMQ的接收循环根本没机会运行(你定义了message_msg_loop但没启动它)。


修复后的代码

修正后的DLL代码

// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <zmq.h>

HHOOK tHook;
HMODULE hinstDLL;
// 全局变量,确保整个DLL内共享ZeroMQ资源
void* g_requester = nullptr;
void* g_context = nullptr;

// 辅助函数:发送MSG结构体数据到Python,避免回调内冗余代码
void SendMsgToPython(const MSG* msg) {
    if (!g_requester) return;

    // 发送MSG结构体的完整数据,指定正确的结构体大小
    int send_len = zmq_send(g_requester, msg, sizeof(MSG), ZMQ_DONTWAIT);
    if (send_len == -1) {
        // 打印错误信息方便调试
        printf("zmq_send failed: %d\n", zmq_errno());
    }
}

LRESULT CALLBACK meconnect(int code, WPARAM wParam, LPARAM lParam) {
    if (code == HC_ACTION) {
        LPMSG data = (LPMSG)lParam;
        UINT message = data->message;
        switch (message) {
        case WM_POINTERUPDATE:
            if (!IS_POINTER_INCONTACT_WPARAM(wParam)) break;
        case WM_POINTERDOWN:
        case WM_POINTERUP:
            // 只发送一次消息,不在回调里做循环/阻塞操作
            SendMsgToPython(data);
            break;
        }
    }
    return CallNextHookEx(tHook, code, wParam, lParam);
}

extern "C" __declspec(dllexport) BOOL ConnectServer() {
    printf("Connecting to hello world server…\n");
    if (g_context) {
        // 避免重复初始化资源
        return TRUE;
    }
    g_context = zmq_ctx_new();
    if (!g_context) {
        printf("zmq_ctx_new failed\n");
        return FALSE;
    }
    // 改用PUSH模式,不需要响应,避免钩子回调阻塞
    g_requester = zmq_socket(g_context, ZMQ_PUSH);
    if (!g_requester) {
        printf("zmq_socket failed\n");
        zmq_ctx_destroy(g_context);
        g_context = nullptr;
        return FALSE;
    }
    // 使用非阻塞连接,避免阻塞DLL加载流程
    int rc = zmq_connect(g_requester, "tcp://127.0.0.1:5555");
    if (rc != 0) {
        printf("zmq_connect failed: %d\n", zmq_errno());
        zmq_close(g_requester);
        zmq_ctx_destroy(g_context);
        g_requester = nullptr;
        g_context = nullptr;
        return FALSE;
    }
    printf("connected\n");
    return TRUE;
}

extern "C" __declspec(dllexport) BOOL DisconnectServer() {
    if (g_requester) {
        zmq_close(g_requester);
        g_requester = nullptr;
    }
    if (g_context) {
        zmq_ctx_destroy(g_context);
        g_context = nullptr;
    }
    return TRUE;
}

extern "C" __declspec(dllexport) BOOL SetHook() {
    if (tHook) {
        return TRUE;
    }
    tHook = SetWindowsHookEx(WH_GETMESSAGE, meconnect, hinstDLL, 0);
    return tHook != NULL;
}

extern "C" __declspec(dllexport) BOOL UnHook() {
    if (tHook) {
        BOOL res = UnhookWindowsHookEx(tHook);
        tHook = nullptr;
        return res;
    }
    return TRUE;
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) {
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
        hinstDLL = hModule;
        // 禁用DLL_THREAD_ATTACH/DETACH通知,提升性能
        DisableThreadLibraryCalls(hModule);
        break;
    case DLL_PROCESS_DETACH:
        // 进程卸载时自动清理资源,避免内存泄漏
        DisconnectServer();
        UnHook();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;
    }
    return TRUE;
}

修正后的Python代码

import zmq
import time
import ctypes
import pythoncom
import threading

def message_msg_loop():
    context = zmq.Context()
    socket = context.socket(zmq.PULL)  # 和DLL的PUSH模式配对,单向接收
    socket.bind("tcp://127.0.0.1:5555")
    print("Python server ready to receive messages...")
    
    # 定义和C端对应的MSG结构体类,用于解析二进制数据
    class MSG(ctypes.Structure):
        _fields_ = [
            ("hwnd", ctypes.c_void_p),
            ("message", ctypes.c_uint),
            ("wParam", ctypes.c_ulong),
            ("lParam", ctypes.c_longlong),
            ("time", ctypes.c_uint),
            ("pt", ctypes.c_long * 2)
        ]

    while True:
        # 接收完整的MSG结构体二进制数据
        message_data = socket.recv()
        msg = MSG.from_buffer_copy(message_data)
        # 打印解析后的消息,可以替换为写入日志的逻辑
        print(f"Received MSG: message=0x{msg.message:X}, wParam=0x{msg.wParam:X}, lParam=0x{msg.lParam:X}")
        # 示例:写入日志文件
        # with open("pointer_log.txt", "a", encoding="utf-8") as f:
        #     log_line = f"{time.ctime()} | Message: 0x{msg.message:X} | Position: ({msg.pt[0]}, {msg.pt[1]})\n"
        #     f.write(log_line)

def pointer_msg_loop():
    lib = ctypes.cdll.LoadLibrary(r'C:\Users\Braun\Documents\BA_Thesis\ba-oliver-braun-logging-tool-code\MessagesDll\x64\Release\HOOKDLL.dll')
    print(f"Loaded DLL: {lib}")
    
    # 初始化ZeroMQ连接
    res = lib.ConnectServer()
    print(f"ConnectServer result: {res}")
    
    # 设置全局钩子
    res = lib.SetHook()
    print(f"SetHook result: {res}")
    
    # 运行Windows消息循环,维持钩子存活
    pythoncom.PumpMessages()
    
    # 退出时清理资源
    res = lib.UnHook()
    print(f"UnHook result: {res}")
    res = lib.DisconnectServer()
    print(f"DisconnectServer result: {res}")

if __name__ == "__main__":
    # 启动ZeroMQ接收线程,和钩子消息循环分离,避免互相阻塞
    recv_thread = threading.Thread(target=message_msg_loop, daemon=True)
    recv_thread.start()
    
    # 启动钩子管理线程
    pointer_msg_loop()

关键改进说明

  1. 变量作用域修复:把ZeroMQ的上下文和Socket改成全局变量,确保ConnectServer初始化后,钩子回调能正确访问资源。
  2. 改用PUSH/PULL模式:REQ/REP需要严格的请求-响应配对,不适合钩子这种不能阻塞的场景。PUSH/PULL是单向发送,不需要响应,彻底避免回调阻塞问题。
  3. 正确发送结构体数据:发送MSG结构体的完整字节数据,Python端通过ctypes.Structure解析二进制数据,确保消息内容准确。
  4. 线程分离:Python端把ZeroMQ接收和钩子消息循环放在不同线程,避免互相阻塞,保证两边都能正常运行。
  5. 资源自动清理:DLL的DLL_PROCESS_DETACH里自动清理ZeroMQ资源和钩子,避免内存泄漏。
  6. 错误处理:增加了ZeroMQ操作的错误打印,方便调试定位问题。

内容的提问来源于stack exchange,提问作者Oliver Braun

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.06 17:22:34