How to pass SAFEARRAY to COM object through IDispatch?
Asked Answered
U

1

6

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.

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?

Unintentional answered 15/8, 2012 at 21:44 Comment(7)
I am usually using VARIANT with VT_ARRAY | VT_UI1 and it works fine.Copyright
@RomanR. Oh, that was my typo in the question. i typed VT_BYTE (i.e. 0x10), but really i meant VT_I1) (i.e. 0x10).Unintentional
So what about VT_UI1 == 0x11?Copyright
@RomanR. i screwed it up again. The constant i had in my sample code (0x2011) was correct. i was then transcoding that into Delphi varArray or varByte into C-style VT_ARRAY | VT_BYTE. But VT_BYTE wasn't the right constant, so i changed it to VT_I1. At my glance of your code i thought that's what it was. Now i see i screwed it up again, and 0x11 is VT_UI1. Which is what the code already uses (0x2011). Sorry. For the record i've tried array of VT_I1 and array of VT_UI1. My next step will be to step into the assembly code of .Invoke(...)Unintentional
My point is that if your code is on both side of marhsaling layer, the caller and the callee, VT_ARRAY | VT_UI1 variants are travelling through just fine. If they don't (error like E_INVALIDARG for no other reason) I would suppose the variant itself is broken, e.g. incorrectly initialized SAFEARRAY.Copyright
@RomanR. Granted i didn't initialize the SafeArray structure myself (i used a Windows API for that), i checked the members of the structure. The contents of the SAFEARRAY you can see towards the end of the question; and they look reasonable to me.Unintentional
I see you are at loss, I made a simple project that works (see answer).Copyright
C
2

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.

Copyright answered 17/8, 2012 at 18:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.