Why can't I have variadic COM methods?


COM methods cannot be variadic. Why not?

Answer: Because the marshaler doesn't know when to stop.

Suppose variadic COM methods were possible. And then you wrote this code:

interface IVariadic
{
 HRESULT Mystery([in] int code, ...);
};

IVariadic *variadic = something;
uint32_t ipaddr;
HRESULT hr = variadic->Mystery(9, 192, 168, 1, 1, &ipaddr);

How would COM know how to marshal this function call? In other words, suppose that variadic is a pointer to a proxy that refers to an object in another process. The COM marshaler needs to take all the parameters to IVariadic::Mystery, package them up, send them to the other process, then unpack the parameters, and pass them to the implementation. And then when the implementation returns, it needs to take the return value and any output parameters, package them up, send them back to the originating process, where they are unpacked and applied to the original parameters.

Consider, for example,

interface IDyadic
{
 HRESULT Enigma([in] int a, [out] int *b);
};

IDyadic *dyadic = something;
int b;
HRESULT hr = dyadic->Enigma(1, &b);

If dyadic refers to an object in another process, the marshaler does this:

  • Allocate a block of memory containing the following information:

    • Information to identify the dyadic object in the other process,

    • the integer 1.
  • Transmit that block of memory to the other process.

The other process receives the block of memory and does the following:

  • Use the information in the memory block to identify the dyadic object.

  • Extract the parameter 1 from the memory block.

  • Allocate a local integer variable, call it x.
  • Call dyadic->Enigma(1, &x). Let's say that the function stores 42 into x, and it returns E_PENDING.

  • Allocate a block of memory containing the following information:

    • The value E_PENDING (the HRESULT returned by dyadic->Enigma),

    • The integer 42 (the value that dyadic->Enigma stored in the local variable x).
  • Transmit that block of memory to the originating process.

The originating process receives the block of memory and does the following:

  • Extracts the HRESULT E_PENDING.
  • Extracts the value 42.
  • Stores the value 42 into b.
  • Returns the value E_PENDING to the caller.

Note that in order for the marshaler to do its job, it needs to know every parameter to the method, whether that parameter is an input parameter (which is sent from the originating process to the remote process), an output parameter (which is sent from the remote process to the originating process), and how to send that parameter. In our case, the parameter is just an integer, so sending it is just copying the bits, but in the more general case, the parameter could be a more complicated data structure.

Now let's look at that variadic method again. How is the marshaler supposed to know what to do with the ...? It doesn't know how many parameters it needs to transfer. It doesn't know what types those parameters are. It doesn't know which ones are input parameters and which ones are output parameters.

In order to know that, it would have to reverse-engineer the implementation of the IVariadic::Mystery function and figure out that the first parameter, the number 9, is a code that means that the method takes four 8-bit integers as input and outputs a 32-bit integer.

This is a rather tall order for the client side of the marshaler, since it has to do its work without access to the other process. It would have to use its psychic powers to figure out how to package up the parameters, as well as how to unpack them afterward.

Therefore, COM says, "Sorry, you can't do that."

But what you can do is encode the parameters in a form that the marshaler understands. For example, you might use a counted array of VARIANTs or a SAFEARRAY. The COM folks already did the work to teach the marshaler how to, for example, decode the vt member of the VARIANT and understand that, "Oh, if the value is VT_I4, then the VARIANT contains a 32-bit signed integer."

Bonus chatter: But wait, there is a MIDL attribute called [vararg]. You said that COM doesn't support variadic methods, but there is a MIDL keyword that says variadic right on the tin!

Ah, but that [varargs] attribute is just a sleight of hand trick. Bceause when you say [varargs], what you're saying is, "The last parameter of this method is a SAFEARRAY of VARIANTs. A scripting language can expose this method to scripts as variadic, but what it actually does is take all the variadic parameters and store them into a SAFEARRAY, and then pass the SAFEARRAY."

In other words, it indicates that the last parameter of the method acts like the C# params keyword.

Comments (9)
  1. JamesJohnston says:

    Another reason might be that variadic functions aren't supported for stdcall calling convention...

  2. Logan says:

    msdn.microsoft.com/.../ff553183(v=vs.85).aspx

    You can have variadic functions in "lightweight COM" though!

  3. wqw says:

    So are these "scripting" [varargs] [in], [out] or [in/out] for the std marshaller?

    [[in] and [out] are not just for scripting. The standard marshaler uses those. But some of the attributes like [varargs] and [retval] and [optional] are purely for scripting since they affect how the method is exposed to scripting languages, not how it marshals. -Raymond]
  4. Alex Cohn says:

    For all practical purposes, SAFEARRAY of VARIANTs is equivalent to variadic parameters list. The overhead which may be important for printf() and her kin, is negligible when COM marshalling is involved.

  5. Cesar says:

    There are two variadic conventions which would be easy to support: counter (a specially-marked input parameter has the number of parameters in the "..." part) and sentinel (the parameters in the "..." part are all pointers, and the last one is always a NULL pointer). Of course, it would also need a way to specify the size every parameter in the "..." part must have (machine word, a pair of machine words, pointer, float, or double), and they would have to be all the same size.

    The other common variadic convention, which I'll call "printf", would be much harder to support in the marshaller; not only you have to parse the format string to know how many parameters there are, the format string also specifies the *size* of each parameter individually.

    (The gcc compiler, by the way, has a way to mark in the prototype of a variadic function that it uses a sentinel or that it uses a printf-style format string; it also knows several kinds of format strings. That information is used to warn the developer when a variadic function is misused.)

    [Also requires that all the parameters be of the same type (or if not, you need some way of telling the marshaller the type of each parameter. -Raymond]
  6. Ben Voigt says:

    @Cesar: It's not enough to know there is a list of pointers, that all pointers are the same size, and where the list ends.

    The content pointed-to must also be marshaled (potentially in both directions), and therein lies the problem.

  7. Deanna says:

    It's the same as VB6's "ParamArray ByRef xxx() As Variant"

  8. Axel Rietschin says:

    @Logan: only on [local] interfaces or methods, i.e. not marshaled.

  9. acq says:

    I just want to use this opportunity to thank Raymond for all his "highly technical" articles (like this one). I consider the amount of the insight and clarity in them unprecedented to any other source handling the same topics.

    A personal anecdote: Until up to a few days ago, I've never had to do anything with the "shell namespace" objects. As soon as I've discovered the OldNewThing articles I've had a good examples for the start.

    This article now I really, really respect for its depth:

    blogs.msdn.com/.../10483672.aspx

    Thanks Raymond.

Comments are closed.

Skip to main content