How to pass SAFEARRAY to COM object through IDispa

2019-01-24 14:02发布

问题:

i am trying to call a method of COM object, where one of the documented parameters is an "array of bytes". The actual declartion depends on the per-language documentation you're looking at:

  • in C# language:

    byte[] TransformFinalBlock(
        byte[] inputBuffer,
        int inputOffset,
        int inputCount
    )
    
  • in C++ language;

    array<unsigned char>^ TransformFinalBlock(
        array<unsigned char>^ inputBuffer, 
        int inputOffset, 
        int inputCount
    )
    
  • in VB language:

    Function TransformFinalBlock ( _
        inputBuffer As Byte(), _
        inputOffset As Integer, _
        inputCount As Integer _
    ) As Byte()
    
  • in F# language:

    abstract TransformFinalBlock : 
            inputBuffer:byte[] * 
            inputOffset:int * 
            inputCount:int -> byte[] 
    

The object i'm using can also be accessed using COM. The object provides an early binding interface, ICryptoTransform, which declares the method as using SAFEARRAY.

From the type library:

  • using IDL syntax

    [
      odl,
      uuid(8ABAD867-F515-3CF6-BB62-5F0C88B3BB11),
      version(1.0),
      dual,
      oleautomation,
      custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "System.Security.Cryptography.ICryptoTransform")    
    ]
    interface ICryptoTransform : IDispatch {
       ...
       [id(0x60020005)]
       HRESULT TransformFinalBlock(
                    [in] SAFEARRAY(unsigned char) inputBuffer, 
                    [in] long inputOffset, 
                    [in] long inputCount, 
                    [out, retval] SAFEARRAY(unsigned char)* pRetVal);
    };
    
  • using object Pascal syntax:

    ICryptoTransform = interface(IDispatch)
         ['{8ABAD867-F515-3CF6-BB62-5F0C88B3BB11}']
         ...
         function TransformFinalBlock(inputBuffer: PSafeArray; inputOffset: Integer; inputCount: Integer): PSafeArray; safecall;
    end;
    

This means that when using early-binding you must pass the method a SAFEARRAY. The language i use has support for SafeArray APIs, can i can perform the call easily enough:

var
   inputBuffer: PSafeArray;
   xform: ICryptoTransform;
   ...
begin
   ...

   xform.TransformFinalBlock(inputBuffer, ...);
   ...
end;

Here's the same code in a java-like language:

PSafeArray inputBuffer;
ICryptoTransform xform;

...

xform.TransformFinalBlock(inputBuffer, ...);

And everything works fine; but that's not my question.

Note: i'm trying drive home the point that this is a language-agnostic question, as COM is a language agnostic technology. But at some point we have to actually use a language that we will demonstrate code in. Some people confuse a language with a technology. If i knew Knuth's invented language, i would have used that.

But what about late-binding IDispatch?

Now that we know we can pass a SAFEARRAY to a COM object (when using early-binding), i need to solve the problem of passing an array using late-binding.

Note: The question of how to pass a SAFEARRAY to a COM object through IDispatch is useful me to in circumstances besides ICryptoTransform.

  • passing an array of BSTR to MSHTML control
  • passing an array of byte to other COM objects

Some languages provide automatic mechanisms to invoke methods through an IDispatch interface at run-time (i.e. late-binding). In fact IDispatch late binding was invented for VBScript:

Dim xform = CreateObject("System.Security.Cryptography.SHA256Managed");
Dim buffer;
o.TransformFinalBlock(buffer, 0, 8);

And late-binding compiler auto-magic was added in .NET 4.0:

dynamic xform = Activator.CreateInstance(Type.GetTypeFromProgID("System.Security.Cryptography.SHA256Managed", true));
xform.TransformFinalBlock(buffer, 0, 8);

Late-binding compiler magic also existed in Delphi:

xform: OleVariant;
buffer: OleVariant;
xform.TransformFinalBlock(buffer, 0, 8);

i happen to be using Dephi, and this call fails.

But it's not really compiler magic

It's not really magic what VBScript, C# dynamic, and Delphi are doing. They're just calling IDispatch.Invoke:

IDispatch = interface(IUnknown)
   ['{00020400-0000-0000-C000-000000000046}']
   function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
         Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
end;

The mess is setting up these parameters:

xform.Invoke(
      1610743820,      //DispID
      IID_NULL,        //riid (reserved for future use, must be IID_NULL)
      0,               //locale id (lcid)
      DISPATCH_METHOD, //flags
      dispParams,      //Pointer to a DISPPARAMS structure
      null,            //Pointer to the location where the result is to be stored, or NULL if the caller expects no result
      exceptInfo,      //Pointer to a structure that contains exception information
      null);           //This argument can be set to null. 

The real trick is the dispParams structure, that contains the arguments.

The argument will be an variant

The arguments that get passed through DISPPARAMS are all variants:

typedef struct tagDISPPARAMS {
  VARIANTARG *rgvarg;
  DISPID     *rgdispidNamedArgs;
  UINT       cArgs;
  UINT       cNamedArgs;
} DISPPARAMS;

So no matter what happens, my "array of bytes" is going to be a variant.

A VARIANT, in Win32, is simply a union that contains:

  • VARTYPE vt: The type of data in the union.
  • the appropriate union member, e.g.:

    BYTE        bVal;
    IDispatch  *pdispVal;
    SAFEARRAY  *parray;
    BYTE       *pbVal;
    IDispatch  *ppdispVal;
    SAFEARRAY  *pparray;
    VARIANT    *pvarVal;
    PVOID       byref;
    CHAR        cVal;
    

Up to now i have been passing a variant of type:

vt = VT_ARRAY | VT_UI1

MSDN documents what you must do when you want to use the parray union with VT_ARRAY | *:

Value: VT_ARRAY | <anything>

Description: An array of data type was passed. VT_EMPTY and VT_NULL are invalid types to combine with VT_ARRAY. The pointer in pbyrefVal points to an array descriptor, which describes the dimensions, size, and in-memory location of the array.

What this means is that using the parray member:

    SAFEARRAY  *parray;

You need to set parray member to a pointer to a SAFEARRAY structure:

typedef struct tagSAFEARRAY {
  USHORT         cDims;
  USHORT         fFeatures;
  ULONG          cbElements;
  ULONG          cLocks;
  PVOID          pvData;
  SAFEARRAYBOUND rgsabound[1];
} SAFEARRAY, *LPSAFEARRAY;

In my case, my array of bytes is actually a SAFEARRAY, which is then being stored in a variant:

VARIANT *inputBuffer;
SAFEARRAY *safeArray;

//Setup our SAFEARRAY of data
safeArray.cDims = 1;
safeArray.fFeatures = FADF_HAVEVARTYPE;
safeArray.cbElements = 1;
safeArray.cbLocks = 0;
safeArray.pvData = pMyData;
safeArray.rgsabound[0].ElementCount = 1;
safeArray.rgsabound[0].LowBound = 0;

//Wrap the safearray in a variant
inputBuffer.vt = VT_ARRAY | VT_UI1; //$2011
vt.parray = safeArray;

Note: Of course i'm not crazy enough to have created this safearray myself; i'm using the SafeArrayCreate api function. i'm just demonstrating that it's all knowable, and not magic.

In other words i pass an array variant of bytes

In other words i am passing an array of bytes, wrapped in a variant, as all calls to:

dispatch.Invoke(...);

must be. Except that the late-binding call throws an error:

The parameter is incorrect.

So what am i possibly doing wrong?

How does one pass an array of byte to a late-bound IDispatch call?

My Question

How to pass SAFEARRAY to COM object through IDispatch?

回答1:

This should give you some insight:

On the caller side, C# code:

Foo foo = new Foo();
byte[] input = new byte[] { 1, 2, 3, 4 };
byte[] output = foo.Bar(input);
byte[] referenceOutput = new byte[] { 4, 3, 2, 1 };
Debug.Assert(Enumerable.SequenceEqual(output, referenceOutput));

The Foo.Bar IDL:

interface IFoo : IDispatch
{
    [id(1)] HRESULT Bar([in] VARIANT vInput, [out, retval] VARIANT* pvOutput);
};

And C++ (ATL) server implementation with safe arrays:

// IFoo
    STDMETHOD(Bar)(VARIANT vInput, VARIANT* pvOutput) throw()
    {
        _ATLTRY
        {
            ATLENSURE_THROW(vInput.vt == (VT_ARRAY | VT_UI1), E_INVALIDARG);
            CComSafeArray<BYTE> pInputArray(vInput.parray);
            ATLASSERT(pInputArray.GetDimensions() == 1);
            const ULONG nCount = pInputArray.GetCount();
            CComSafeArray<BYTE> pOutputArray;
            ATLENSURE_SUCCEEDED(pOutputArray.Create(nCount));
            for(ULONG nIndex = 0; nIndex < nCount; nIndex++)
                pOutputArray[(INT) nIndex] = pInputArray[(INT) ((nCount - 1) - nIndex)];
            ATLASSERT(pvOutput);
            VariantInit(pvOutput);
            CComVariant vOutput(pOutputArray.Detach());
            ATLVERIFY(SUCCEEDED(vOutput.Detach(pvOutput)));
        }
        _ATLCATCH(Exception)
        {
            return Exception;
        }
        return S_OK;
    }

Source: Trac, Subversion - beware Visual Studio 2012.