Search code examples
c++cominvokesafearrayidispatch

Calling COM from C++ using CoCreateInstance and IDispatch, with numerous parameters in DISPPARAMS incl. SAFEARRAY


I have a COM object which exposes a function. I would like to pass parameters to this function and receive a return value. I'm using C++ with CoCreateInstance(). The error I receive is:

hr = 0x8002000e : Invalid number of parameters.

I'm reasonably sure that I have the correct number of parameters, which I can view in OleView:

[id(0x68030001), propget]
double My_function(
                [in, out] double* PdblPrice, 
                [in, out] DATE* PdateStartDate, 
                [in, out] short* PintFlag, 
                [in, out] VARIANT_BOOL* PbolXP, 
                [in, out] SAFEARRAY(double)* PdblScale),
                [out, retval] double*);

A summary of my code is as follows, and it works up to the point marked below:

#include <windows.h>
#include <objbase.h>
#include <comutil.h>
#include <vector>
#include <atlcomcli.h>

int main()
{
    HRESULT hr;

    // Create an instance of COM object
    hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
    CLSID clsid;
    HRESULT nResult1 = CLSIDFromProgID(OLESTR("My_Library.clsMy_Library"), &clsid);

    IUnknown* pUnknown;
    hr = CoCreateInstance(clsid, NULL, CLSCTX_ALL, IID_IUnknown, (void**)&pUnknown);

    // Get the IDispatch interface
    IDispatch* pDispatch;
    hr = pUnknown->QueryInterface(IID_IDispatch, (void**)&pDispatch);

    // Call the Invoke method
    DISPID dispid;
    BSTR bstrFunction = SysAllocString(L"My_function");
    hr = pDispatch->GetIDsOfNames(IID_NULL, &bstrFunction, 1, LOCALE_USER_DEFAULT, &dispid);

    // ALL OF THE ABOVE WORKS.

    // Prepare the arguments for the method call
    // first convert a std::vector to SAFEARRAY
    std::vector<double> _PdblScale = { 0, 0.25, 0.5, 0.75, 1.0, 0, 0, 0.5, 1, 1 };
    SAFEARRAY* psa = SafeArrayCreateVector(VT_R8, 0, _PdblScale.size());
    int* pData;
    HRESULT hr_ = SafeArrayAccessData(psa, (void**)&pData);
    if (SUCCEEDED(hr_))
    {
        for (unsigned int i = 0; i < _PdblScale.size(); i++)
        {
            pData[i] = _PdblScale[i];
        }
        SafeArrayUnaccessData(psa);
    }

    DISPPARAMS dispparams;
    dispparams.cArgs = 5;
    dispparams.rgvarg = new VARIANT[5];
    dispparams.cNamedArgs = 5;


    VARIANT PdblPrice;
    PdblPrice.vt = VT_R8;
    PdblPrice.dblVal = 28.0;
    dispparams.rgvarg[0] = PdblPrice;

    VARIANT PdateStartDate;
    PdateStartDate.vt = VT_DATE;
    PdateStartDate.date = 41052;
    dispparams.rgvarg[1] = PdateStartDate;

    VARIANT PintFlag;
    PintFlag.vt = VT_I2;
    PintFlag.iVal = 1;
    dispparams.rgvarg[2] = PintFlag;

    VARIANT PbolXP;
    PbolXP.vt = VT_BOOL;
    PbolXP.boolVal = false;
    dispparams.rgvarg[3] = PbolXP;

    VARIANT PdblScale;
    PdblScale.vt = VT_SAFEARRAY;
    PdblScale.pvRecord = psa;
    dispparams.rgvarg[4] = PdblScale;

    VARIANT varResult;
    VariantInit(&varResult);
    EXCEPINFO excepinfo;
    memset(&excepinfo, 0, sizeof(excepinfo));
    UINT nArgErr = (UINT)-1;

    // Invoke the method  ## THIS IS WHERE hr returns 0x8002000e : Invalid number of parameters
    hr = pDispatch->Invoke(dispid, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &dispparams, &varResult, &excepinfo, &nArgErr);
    if (FAILED(hr))
    {
        printf("Failed to invoke method.");
        pDispatch->Release();
        pUnknown->Release();
        CoUninitialize();
        return 1;
    }

    // Print the result
    printf("Result: %d\n", varResult.intVal);

    // Clean up
    VariantClear(&varResult);
    pDispatch->Release();
    pUnknown->Release();
    CoUninitialize();

    return 0;
}

I see from IDispatch Invoke() returns Type mismatch that the arguments should be in reverse order. I have tried that, ie. used [4], [3], [2], etc instead of [0], [1], etc above, but this still gives the error.

Any suggestions as to where I might be going wrong?

By the way, the COM is from a 32bit DLL, and I am compiling my code to x86.


Solution

  • There are many problems with your code:

    • lack of error handling.

    • when creating the COM object, you don't need to get its IUnknown just to immediately query it for IDispatch. You can get its IDispatch directly.

    • memory leaks on bstrFunction and dispparams.rgvarg.

    • you are creating a SAFEARRAY of VT_R8 (double) elements, but you are using an int* pointer to populate its values. You need to use a double* pointer instead.

    • you are not populating the DISPPARAMS or the VARIANTs correctly. The parameters have to be stored in the DISPPARAMS in revere order. And the VARIANTs need to use the VT_BYREF flag, which means they need to point at external variables that hold the actual values, rather than storing the values inside the VARIANTs themselves.

    • calling IDispatch::Invoke() with the wrong flag. You need to use DISPATCH_PROPERTYGET instead of DISPATCH_METHOD since the method is marked as propget in the IDL.

    • not using VARIANT_BOOL correctly for the bolXP parameter.

    • after invoking the method, you are printing out the wrong field of varResult. The method is declared as returning a double in the IDL, not an int.

    With all of that said, try something more like this:

    #include <windows.h>
    #include <objbase.h>
    #include <comutil.h>
    #include <comdef.h>
    #include <atlcomcli.h>
    #include <vector>
    
    // tweaked from https://devblogs.microsoft.com/oldnewthing/20040520-00/?p=39243
    class CCoInitializeEx {
      HRESULT m_hr;
    public:
      CCoInitializeEx() : m_hr(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)) { }
      ~CCoInitializeEx() { if (SUCCEEDED(m_hr)) CoUninitialize(); }
      operator HRESULT() const { return m_hr; }
    };
    
    int main()
    {
        HRESULT hr;
    
        // Initialize COM
    
        CCoInitializeEx init;
        hr = init;
        if (FAILED(hr)) {
            printf("Failed to init COM.");
            return -1;
        }
    
        // Create an instance of COM object and get its IDispatch interface
    
        CLSID clsid;
        hr = CLSIDFromProgID(OLESTR("My_Library.clsMy_Library"), &clsid);
        if (FAILED(hr)) {
            printf("Failed to get CLSID.");
            return -1;
        }
    
        IDispatchPtr pDispatch;
        hr = pDispatch.CreateInstance(clsid);
        if (FAILED(hr)) {
            printf("Failed to create COM object.");
            return -1;
        }
    
        // Call the Invoke method
    
        DISPID dispid;
        _bstr_t bstrFunction = OLESTR("My_function");
        LPOLESTR pbstrFunction = bstrFunction;
        hr = pDispatch->GetIDsOfNames(IID_NULL, &pbstrFunction, 1, LOCALE_USER_DEFAULT, &dispid);
        if (FAILED(hr)) {
            printf("Failed to get DispID.");
            return -1;
        }
    
        // ...
    
        // first convert a std::vector to SAFEARRAY
        // TODO: wrap the SAFEARRAY inside a RAII class...
    
        std::vector<double> vecDblScale = { 0, 0.25, 0.5, 0.75, 1.0, 0, 0, 0.5, 1, 1 };
        SAFEARRAY* psa = SafeArrayCreateVector(VT_R8, 0, vecDblScale.size());
        if (!psa) {
            printf("Failed to allocate SAFEARRAY.");
            return -1;
        }
    
        double* pData;
        hr = SafeArrayAccessData(psa, reinterpret_cast<void**>(&pData));
        if (FAILED(hr))
            printf("Failed to access SAFEARRAY data.");
            SafeArrayDestroy(psa);
            return -1;
        }
    
        for (size_t i = 0; i < vecDblScale.size(); ++i)
        {
            pData[i] = vecDblScale[i];
        }
        // alternatively:
        //
        // #include <algorithm>
        // std::copy(vecDblScale.begin(), vecDblScale.end(), pData);
    
        SafeArrayUnaccessData(psa);
    
        // Prepare the arguments for the method call
    
        std::vector<VARIANT> vecRgvarg(5);
    
        DOUBLE dblPrice = 28.0;
        vecRgvarg[4].vt = VT_R8 | VT_BYREF;
        vecRgvarg[4].pdblVal = &dblPrice;
    
        DATE dateStartDate = 41052;
        vecRgvarg[3].vt = VT_DATE | VT_BYREF;
        vecRgvarg[3].pdate = &dateStartDate;
    
        short intFlag = 1;
        vecRgvarg[2].vt = VT_I2 | VT_BYREF;
        vecRgvarg[2].piVal = &intFlag;
    
        VARIANT_BOOL bolXP = VARIANT_FALSE;
        vecRgvarg[1].vt = VT_BOOL | VT_BYREF;
        vecRgvarg[1].pboolVal = &bolXP;
    
        vecRgvarg[0].vt = VT_R8 | VT_ARRAY | VT_BYREF;
        vecRgvarg[0].pparray = &psa;
    
        DISPPARAMS dispparams = {};
        dispparams.cArgs = vecRgvarg.size();
        dispparams.rgvarg = vecRgvarg.data();
    
        // Invoke the method
    
        _variant_t varResult;
        EXCEPINFO excepinfo = {};
        UINT nArgErr = (UINT)-1;
    
        hr = pDispatch->Invoke(dispid, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYGET, &dispparams, &varResult, &excepinfo, &nArgErr);
        if (FAILED(hr)) {
            printf("Failed to invoke method.");
            SafeArrayDestroy(psa);
            return -1;
        }
    
        // Print the result
        printf("Result: %f\n", varResult.dblVal);
    
        // Clean up
        SafeArrayDestroy(psa);
    
        return 0;
    }