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

如何用纯C正确实现多个COM接口?附官方示例分析

纯C环境下实现多COM接口的方案

一、C++多继承COM类对应的C内存布局

你给出的C++类CDialogEventHandler通过多继承实现了IFileDialogEvents和IFileDialogControlEvents,其内存布局为:

[IFileDialogEvents子对象(含vtable指针)][IFileDialogControlEvents子对象(含vtable指针)][类自定义成员]

在纯C中需要完全模拟该布局,对应的结构体定义如下:

#include <shobjidl.h>

typedef struct CDialogEventHandler {
    // 对应C++第一个基类:IFileDialogEvents接口实例
    IFileDialogEvents fileDialogEvents;
    // 对应C++第二个基类:IFileDialogControlEvents接口实例
    IFileDialogControlEvents fileDialogControlEvents;
    // 自定义成员:引用计数
    ULONG refCount;
    // 其他业务所需自定义数据...
} CDialogEventHandler;

每个COM接口结构体(如IFileDialogEvents)的第一个成员都是指向其vtable的指针,上述C结构体完全匹配C++多继承的内存布局。

二、双接口的vTable实现

每个COM接口需要独立的vtable,包含该接口所有方法的函数指针,以下是具体实现:

1. 实现接口核心方法

先编写两个接口的所有方法(包括IUnknown的QueryInterface/AddRef/Release,以及各自的事件方法):

// 通用QueryInterface逻辑,处理所有IID请求
static HRESULT WINAPI DialogEventHandler_QueryInterface(IUnknown* pUnk, REFIID riid, void** ppv) {
    CDialogEventHandler* pThis = CONTAINING_RECORD(pUnk, CDialogEventHandler, fileDialogEvents);

    *ppv = NULL;
    // 处理IUnknown请求:返回第一个接口的IUnknown,满足COM同一性要求
    if (IsEqualIID(riid, &IID_IUnknown)) {
        *ppv = &pThis->fileDialogEvents;
    }
    // 处理IFileDialogEvents请求
    else if (IsEqualIID(riid, &IID_IFileDialogEvents)) {
        *ppv = &pThis->fileDialogEvents;
    }
    // 处理IFileDialogControlEvents请求
    else if (IsEqualIID(riid, &IID_IFileDialogControlEvents)) {
        *ppv = &pThis->fileDialogControlEvents;
    }
    else {
        return E_NOINTERFACE;
    }

    // 成功返回接口时增加引用计数
    ((IUnknown*)*ppv)->AddRef();
    return S_OK;
}

// IFileDialogEvents的QueryInterface包装(适配接口this指针)
static HRESULT WINAPI FDEvents_QueryInterface(IFileDialogEvents* pThis, REFIID riid, void** ppv) {
    return DialogEventHandler_QueryInterface((IUnknown*)pThis, riid, ppv);
}

// IFileDialogControlEvents的QueryInterface包装(调整指针到实例起始地址)
static HRESULT WINAPI FDControlEvents_QueryInterface(IFileDialogControlEvents* pThis, REFIID riid, void** ppv) {
    CDialogEventHandler* pInst = CONTAINING_RECORD(pThis, CDialogEventHandler, fileDialogControlEvents);
    return DialogEventHandler_QueryInterface((IUnknown*)&pInst->fileDialogEvents, riid, ppv);
}

// 通用AddRef:线程安全的引用计数增加
static ULONG WINAPI DialogEventHandler_AddRef(IUnknown* pUnk) {
    CDialogEventHandler* pThis = CONTAINING_RECORD(pUnk, CDialogEventHandler, fileDialogEvents);
    return InterlockedIncrement(&pThis->refCount);
}

// IFileDialogControlEvents的AddRef包装
static ULONG WINAPI FDControlEvents_AddRef(IFileDialogControlEvents* pThis) {
    CDialogEventHandler* pInst = CONTAINING_RECORD(pThis, CDialogEventHandler, fileDialogControlEvents);
    return DialogEventHandler_AddRef((IUnknown*)&pInst->fileDialogEvents);
}

// 通用Release:线程安全的引用计数减少,计数为0时释放实例
static ULONG WINAPI DialogEventHandler_Release(IUnknown* pUnk) {
    CDialogEventHandler* pThis = CONTAINING_RECORD(pUnk, CDialogEventHandler, fileDialogEvents);
    ULONG newCount = InterlockedDecrement(&pThis->refCount);
    if (newCount == 0) {
        HeapFree(GetProcessHeap(), 0, pThis);
    }
    return newCount;
}

// IFileDialogControlEvents的Release包装
static ULONG WINAPI FDControlEvents_Release(IFileDialogControlEvents* pThis) {
    CDialogEventHandler* pInst = CONTAINING_RECORD(pThis, CDialogEventHandler, fileDialogControlEvents);
    return DialogEventHandler_Release((IUnknown*)&pInst->fileDialogEvents);
}

// IFileDialogEvents的OnFileOk方法示例
static HRESULT WINAPI FDEvents_OnFileOk(IFileDialogEvents* pThis, IFileDialog* pfd) {
    CDialogEventHandler* pInst = CONTAINING_RECORD(pThis, CDialogEventHandler, fileDialogEvents);
    // 自定义业务逻辑
    return S_OK;
}

// IFileDialogControlEvents的OnItemSelected方法示例
static HRESULT WINAPI FDControlEvents_OnItemSelected(IFileDialogControlEvents* pThis, IFileDialogCustomize* pfdc, DWORD dwIDCtl, DWORD dwIDItem) {
    CDialogEventHandler* pInst = CONTAINING_RECORD(pThis, CDialogEventHandler, fileDialogControlEvents);
    // 自定义业务逻辑
    return S_OK;
}

// 根据shobjidl.h定义,补充两个接口的剩余方法实现

2. 组装vTable结构体

将上述方法指针组装为两个接口的vtable:

// IFileDialogEvents的vTable
static const IFileDialogEventsVtbl g_FDEventsVtbl = {
    FDEvents_QueryInterface,
    DialogEventHandler_AddRef,
    DialogEventHandler_Release,
    FDEvents_OnFileOk,
    // 补充IFileDialogEvents的其他方法指针
};

// IFileDialogControlEvents的vTable
static const IFileDialogControlEventsVtbl g_FDControlEventsVtbl = {
    FDControlEvents_QueryInterface,
    FDControlEvents_AddRef,
    FDControlEvents_Release,
    FDControlEvents_OnItemSelected,
    // 补充IFileDialogControlEvents的其他方法指针
};

三、实例创建与初始化

编写实例创建函数,初始化接口vtable指针和引用计数:

CDialogEventHandler* CreateDialogEventHandler() {
    CDialogEventHandler* pInst = (CDialogEventHandler*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(CDialogEventHandler));
    if (!pInst) {
        return NULL;
    }

    // 绑定两个接口的vTable
    pInst->fileDialogEvents.lpVtbl = &g_FDEventsVtbl;
    pInst->fileDialogControlEvents.lpVtbl = &g_FDControlEventsVtbl;
    // 初始引用计数设为1
    pInst->refCount = 1;

    return pInst;
}

关键注意事项

  • 指针调整:通过CONTAINING_RECORD宏将接口指针转换为实例指针,这是C中处理多接口布局的核心,确保无论通过哪个接口调用方法,都能正确访问实例的自定义成员。
  • IUnknown同一性:COM要求对象所有接口返回的IUnknown必须是同一个,因此统一返回第一个接口的IUnknown实现。
  • 线程安全:引用计数操作必须使用InterlockedIncrement和InterlockedDecrement保证多线程环境下的正确性。

内容的提问来源于stack exchange,提问作者Ivan Venkov

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 16:45:44