IDL has a size_is attribute (as well as length_is, first_is, last_is, max_is, ...) that decorates a C-style array with enough information for it to be marshaled. Without such an attribute, the array would not be distinguishable from a pointer to a single element. Here's an example of size_is being used on the IDL definition of ISequentialStream::Read:
[out, size_is(cb), length_is(*pcbRead)]
[in] ULONG cb,
[out] ULONG *pcbRead);
size_is contains an expression (usually just the name of another parameter) that, at run-time, evaluates to the number of elements in the array. In this example, the use of size_is and length_is together enables the transmission of a dynamically-sized slice of a dynamically-sized array. This is sometimes called a conformant varying array, or an "open array."
How does Interop handle size_is information? Does managed code have a size_is notion? The answers are "not well," and "sort of."
Unfortunately, type libraries are not as expressive as IDL. So if you use MIDL to create a type library from the IDL description of ISequentialStream, the type library will contain a definition of Read that's equivalent to the following:
[out] void *pv,
[in] ULONG cb,
[out] ULONG *pcbRead);
If you don't believe me, you can try this out by viewing such a type library with OLEVIEW! Because the type library importer uses a type library as input rather than an IDL file, it never sees any size_is markings. Therefore, it would effectively import the previous signature as follows (in C# syntax):
void Read(IntPtr pv, uint cb, out uint pcbRead);
In this case, the array parameter is still usable in managed code if you manipulate the IntPtr, but in many cases (like double* imported to "ref double") the imported signature is not usable as-is. There is a solution, however, if you want to modify the imported signature or manually define ISequentialStream in managed code.
MarshalAsAttribute has a named parameter called SizeParamIndex that's valid to use with UnmanagedType.LPArray. With this, you can specify which parameter contains the length of the array (expressed as the number of elements) at run-time, similar to size_is. However, rather than specifying the name of a parameter, you specify the zero-based index of the parameter, as follows:
byte  pv, // parameter 0
int cb, // parameter 1
IntPtr pcbRead // parameter 2
This is how mscorlib's declaration of IStream (System.Runtime.InteropServices.UCOMIStream) defines the Read method.
SizeParamIndex isn't as flexible and powerful as IDL's size_is attribute, but it's the closest thing we've got. It has several limitations that size_is doesn't have:
- It can only be used with by-value arrays
- The "size parameter" must be a by-value parameter
- It can only be used with single-dimensional arrays
- It can only be used with parameters (not fields)
- The size can't be specified using an arbitrary expression
Because managed arrays are self-describing, the marshaler doesn't pay attention to the SizeParamIndex when marshaling a managed array to unmanaged code. (It can just check the array's Length property.) SizeParamIndex is only honored when marshaling an unmanaged array, whose length would otherwise be unknown, to a managed array. But because SizeParamIndex can only be used with a by-value array and a by-value size parameter, it's only useful for calls originating from unmanaged code.
There's an extremely confusing aspect of SizeParamIndex that I must share: the parameter index isn't always zero-based! For methods marked with the PreserveSig pseudo custom attribute, it's a one-based index! This was not the intent of SizeParamIndex when it was designed, but this is the behavior we're stuck with. If you define ISequentialStream::Read with PreserveSig, it should look as follows:
[PreserveSig] int Read(
byte  pv, // parameter 1
int cb, // parameter 2
IntPtr pcbRead // parameter 3