簡體   English   中英

如何在C ++匯編中使用`#import`和`tlb`文件在C ++中實現COM回調接口?

[英]How to implement COM callback interface in C++, from C# assembly, using `#import` and `tlb` file?

以下虛擬和最小的例子解釋了這個問題。

您有一個C#/。NET庫,它通過COM導出接口以從C ++使用。

[C++ Application] --- is using ---> [via COM] [C#/.NET Library]

C#/ .NET端的庫看起來像這樣:

[Guid("example-0000-0000-0000-0000000000000")]
public class MyObj : IMyObj
{
    void SetLogger(ILogger logger);
    void DoSomething(string someArgument);
}

編譯程序集並導出名為MyObj.tlbtlb文件。 使用Visual-C ++中的#import語句在Application中導入此tlb文件:

#import "MyObj.tlb" named_guids auto_rename

void someFunc()
{
    MyObjPtr myObj;
    myObj.CreateInstance(CLSID_MyObj);
    // How to set the logger?
    myObj->DoSomething(_bstr_t(L"foo"));
    // ...
}

一切正常,但您希望為C#/ .NET庫啟用日志記錄。 C#/ .NET庫的內部應該能夠將日志消息發送回C ++應用程序,以便使用那里已有的日志記錄環境來編寫日志消息。

[C++ Logging System] <--- log message --- [C#/.NET Component]

您已在C#/ .NET組件中聲明了記錄器的接口。

[Guid("example-0000-0000-0000-0000000000000")]
public interface ILogger
{
    void WriteLine(string line); 
}

問題是:

使用ILogger接口為C ++應用程序實現Logger類的最簡單方法是什么?

(不使用ATL或MFC)

ILogger接口派生自IDispatch ,但C#interop層實際上並不使用IDispatch接口。 因此,只需要實現IUnknown接口。

請參閱下面的“使用IUnknown C#中的接口限制為早期綁定”部分,如何更改C#組件以完全避免使用IDispatch接口。

問題中描述的ILogger接口的實現如下所示:

#import "MyObj.tlb" named_guids auto_rename

class Logger : public ILogger
{
public:
    Logger(MyLogger log)
        : _log(log), _refCount(1)
    {
    }

    virtual ~Logger()
    {
    }

public: // Implement ILogger
    virtual HRESULT __stdcall raw_WriteLine(BSTR message) {
        // Convert BSTR and write to _log.
        return S_OK;
    }

public: // Implement IDispatch
    virtual HRESULT __stdcall GetTypeInfoCount(UINT *pctinfo)
    {
        return E_NOTIMPL;
    }

    virtual HRESULT __stdcall GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo)
    {
        return E_NOTIMPL;
    }

    virtual HRESULT __stdcall GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId)
    {
        return E_NOTIMPL;
    }

    virtual HRESULT __stdcall Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
    {
        return E_NOTIMPL;
    }

public: // Implement IUnknown
    virtual HRESULT __stdcall QueryInterface(REFIID riid, void **ppvObject)
    {
        if (riid == IID_IUnknown) {
            *ppvObject = static_cast<IUnknown*>(this); 
            AddRef();
            return S_OK;
        }
        if (riid == IID_IDispatch) {
            *ppvObject = static_cast<IDispatch*>(this); 
            AddRef();
            return S_OK;
        }
        if (riid == IID_ILogger) {
            *ppvObject = static_cast<ILogger*>(this) ;
            AddRef();
            return S_OK;
        }
        *ppvObject = nullptr;
        return E_NOINTERFACE;
    }

    virtual ULONG __stdcall AddRef()
    {
        return InterlockedIncrement(&_refCount);
    }

    virtual ULONG __stdcall Release()
    {
        return InterlockedDecrement(&_refCount);
    }

private:
    MyLogger _log;
    long _refCount;
}

請注意以下有關此實現的重要事項:

  • 引用計數實現為看起來像COM引用計數。 因為此對象將由C ++應用程序擁有,所以不需要實現對象的銷毀。
  • 所有IDispatch方法只返回錯誤代碼。 它們永遠不會被C#interop調用。 有關如何實現這些方法的詳細信息,請參閱“ IDispatch工作實現”一節。
  • 在實際項目中, tlb的導入類型都放在自己的命名空間中。
  • class聲明和實現應該在頭文件和實現文件中分開。

代碼在C ++應用程序中使用如下:

bool MyApp::start()
{
    try {
        HRESULT hresult;
        MyObjPtr myObj;
        hresult = myObj.CreateInstance(CLSID_MyObj);
        if (hresult != S_OK) { 
            return false;
        }
        // Create the logger object which acts as callback for the C# library
        _logger = new Logger(_myLogger);
        // Assign this logger
        myObj->SetLogger(_logger);
    } catch (const _com_error &comError) {
        return false;
    }
}

使用IUnknown C#中的接口限制為早期綁定

User Astrotrain指出了刪除IDispatch的簡化:如果您的組件剛剛被此C ++應用程序使用,因此不需要使用IDispatch接口進行后期綁定,則可以將InterfaceType屬性添加到接口以刪除一個綁定。

在我們的示例中,這將是這樣的:

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("example-0000-0000-0000-0000000000000")]
public interface ILogger
{
    void WriteLine(string line); 
}

ILogger接口現在直接來自IUnknown 現在您可以省略IDispatch接口的所有空實現。

缺點是您的組件只能用於支持早期綁定的語言。 如果此C ++應用程序是組件的唯一用戶,則這沒有問題。

IDispatch工作實現

用戶Paulo Madeira提供了一個完整的示例,說明如何使用ITypeInfo實現所有IDispatch方法。 以下示例顯示了Logger類,省略了本答案開頭示例中顯示的所有方法。 請務必閱讀以下注釋。

class Logger : public ILogger
{
    // ctor, dtor, ILogger and IUnknown implementation

public: // Implement IDispatch
    virtual HRESULT __stdcall GetTypeInfoCount(UINT *pctinfo)
    {
        if (pctinfo == nullptr) {
            return E_POINTER;
        }
        *pctinfo = (getTypeInfo() != nullptr) ? 1 : 0;
        return S_OK;
    }

    virtual HRESULT __stdcall GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo)
    {
        if (ppTInfo == nullptr) {
            return E_POINTER;
        }
        *ppTInfo = nullptr;
        if (iTInfo != 0) {
            return DISP_E_BADINDEX;
        }
        ITypeInfoPtr typeInfo(getTypeInfo());
        if (typeInfo == nullptr) {
            return E_NOTIMPL;
        }
        *ppTInfo = typeInfo.Detach();
        return S_OK;
    }

    virtual HRESULT __stdcall GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId)
    {
        if (rgDispId == nullptr) {
            return E_POINTER;
        }
        *rgDispId = 0;
        if (!IsEqualIID(riid, IID_NULL)) {
            return E_INVALIDARG;
        }
        ITypeInfoPtr typeInfo(getTypeInfo());
        if (typeInfo == nullptr) {
            return E_NOTIMPL;
        }
        if (cNames == 0) {
            return E_INVALIDARG;
        }
        return typeInfo->GetIDsOfNames(rgszNames, cNames, rgDispId);
    }

    virtual HRESULT __stdcall Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
    {
        if (pDispParams == nullptr) {
            return E_POINTER;
        }
        // IDispatch and ITypeInfo allows this to be NULL
        if (pVarResult != nullptr) {
            VariantInit(pVarResult);
        }
        // IDispatch and ITypeInfo allows this to be NULL
        if (pExcepInfo != nullptr) {
            ZeroMemory(pExcepInfo, sizeof(EXCEPINFO));
        }
        // IDispatch allows this to be NULL, ITypeInfo does not
        UINT argErr;
        if (puArgErr == nullptr) {
            puArgErr = &argErr;
        }
        *puArgErr = 0;
        if (!IsEqualIID(riid, IID_NULL)) {
            return E_INVALIDARG;
        }
        ITypeInfoPtr pTypeInfo(getTypeInfo());
        if (pTypeInfo == nullptr) {
            return E_NOTIMPL;
        }
        return pTypeInfo->Invoke(
            static_cast<ILogger*>(this),
            dispIdMember,
            wFlags,
            pDispParams,
            pVarResult,
            pExcepInfo,
            puArgErr);
    }

private:
    static ITypeInfo* getTypeInfo()
    {
        if (!_hasTypeLib) {
            ITypeLibPtr typeLib;
            if (SUCCEEDED(LoadRegTypeLib(LIBID_MyObj, 1, 0, 0, &typeLib))) {
                ITypeInfoPtr typeInfo;
                if (SUCCEEDED(typeLib->GetTypeInfoOfGuid(IID_IDispatch, &typeInfo))) {
                    if (!InterlockedCompareExchange(&_hasTypeLib, 1, 0)) {
                        _typeInfo.Attach(typeInfo.Detach());
                    }
                }
            }
        }
        return _typeInfo.GetInterfacePtr();
    }

private:
    static LONG volatile _hasTypeLib;
    static ITypeInfoPtr _typeInfo;
    // other variables
};

// Static definitions in cpp file:
LONG volatile Logger::_hasTypeLib;
ITypeInfoPtr Logger::_typeInfo;

請注意以上關於此示例的以下內容:

  • 您必須將LIBID_MyObj替換為庫的庫標識符。 它有類似的命名。
  • 只有在COM組件需要后期綁定時才需要完整實現。
  • class聲明和實現應該在頭文件和實現文件中分開。
  • static_cast<ILogger*>(this)強制轉換是確保指針(作為void*指針傳遞)指向正確的vtable所必需的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM