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.
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.
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 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 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?
How to pass SAFEARRAY to COM object through IDispatch?
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.