I marked my parameter as [optional], so why do I get an RPC error when I pass NULL?


Consider the following interface declaration in an IDL file:

// Code in italics is wrong

interface IFoo : IUnknown
{
    HRESULT Cancel([in, optional, string] LPCWSTR pszReason);
};

The idea here is that you want to be able to call the Cancel method as pFoo->Cancel(NULL) if you don't want to provide a reason.

If you try this, you'll find that the call sometimes fails with error 0x800706F4, which decodes to HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER). What's going on here?

The optional attribute does not mean what you think it means. To a C or C++ programmer, an "optional" pointer parameter typically means that it is valid to pass NULL/nullptr as the parameter value. But that's not what it means to the IDL compiler.

To the IDL compiler, optional parameters are hints to the scripting engine that the parameter should be passed as VT_ERROR/DISP_E_PARAM­NOT­FOUND. The attribute is meaningful only when applied to parameters of type VARIANT or VARIANT*.

What you actually want is the unique attribute. This somewhat confusingly-named attribute means "The parameter is allowed to be a null pointer." Therefore, the interface should have been written as

interface IFoo : IUnknown
{
    HRESULT Cancel([in, unique, string] LPCWSTR pszReason);
};

At the lowest level in the marshaler, pointer parameters are marked as ref, unique, or ptr. ref parameters may not be null, whereas unique and ptr parameters are allowed to be null. Larry Osterman explained to me that the default for interface pointers (anything derived from IUnknown) is unique and the default for all other pointer types is ref. Therefore, if you want to say that NULL is a valid value for a non-interface pointer parameter, you must say so explicitly by annotating the parameter as [unique].

It's probably too late to change the behavior of MIDL to reject the [optional] tag on non-VARIANT parameters because in the decades since the attribute was introduced, it's probably being used incorrectly approximately twenty-five bazillion times, and making it an error would break a lot of code. (Even if you just made it a warning, that wouldn't help because a lot of people treat warnings as errors.)

Exercise: Why is the RPC_X_NULL_REF_POINTER error raised only sometimes?

Comments (14)
  1. VinDuv says:

    I’d guess the error is only raised sometimes because most of the time Cancel does nothing (because the operation is already completed), and does not uses the reason string.

    [RPC does not lazy-marshal parameters. (How would it do that? Set a read breakpoint on the memory where the string would have been to see if anybody tries to access it?) -Raymond]
  2. Adam Rosenfield says:

    I'm guessing that it only raises RPC_X_NULL_REF_POINTER when marshalling to a remote object (the R in RPC) in another process or machine; if the COM object is local to the process, then it doesn't raise that error?

  3. Joshua says:

    [Set a read breakpoint on the memory where the string would have been to see if anybody tries to access it?]

    Come to think of it, with all the address space in 64 bit, this no longer sounds absurd. It's still probably too slow.

    [Not only would it be slow, it would create deadlocks and reentrancy bugs. Who knew that reading a byte from memory would block the thread, send a message to another process, and pump messages waiting for the message reply? You would need the additional rules "You may not hold any locks while accessing memory passed as a parameter" and "Assume that accessing memory passed as a parameter can cause other methods of your object (or other objects) to be called" which are kind of hard to work with. -Raymond]
  4. Michael Kohne says:

    On the MIDL compiler warning - I don't know the userbase, but I bet you could get away with making it a warning. The sort of people who turn on warning-as-error would probably be happy to know about this.

  5. SI says:

    And speaking of warnings, the warning about nonOLE compliant parameters in IDispatch interfaces never seems to turn up until you set warnlevel to max, and then the build log is completely filled with warnings about the function name and parameter lengths of all the default interfaces.

  6. SI says:

    It already is a MIDL warning.

    1>....include<FILE>.idl(205): warning MIDL2400: for oleautomation, optional parameters should be VARIANT or VARIANT * : [optional] [ Parameter 'example' of Procedure 'LoadSettings' ( Interface 'IExample' ) ]

    1>....include<FILE>.idl(214): warning MIDL2400: for oleautomation, optional parameters should be VARIANT or VARIANT * : [optional] [ Parameter 'example' of Procedure 'SaveSettings' ( Interface 'IExample') ]

  7. Anonymous Coward says:

    > Even if you just made it a warning, that wouldn't help because a lot of people treat warnings as errors.

    That's like not changing the taskbar's shade of blue because some programs rely on the exact pixel values to determine how many programs are running. People who treat warnings as errors should already expect to get new errors each time they change their compiler.

    [Tell that to the people who complain about stupid warnings. -Raymond]
  8. Alex Cohn says:

    Larry Osterman [explains](blogs.msdn.com/.../142401.aspx) that this RPC_X_NULL_REF_POINTER happens when the COM object is created in a different apartment, or the COM object goes out-of-proc. I don't think I could answer this without internet search.

  9. cheong00 says:

    > Even if you just made it a warning, that wouldn't help because a lot of people treat warnings as errors.

    Yes. All warnings have to be treated somehow... either fix it if it's really creating potential bugs (like the warning caused by comparing string to object with == ), or mark it with suppression directives and comment to explain why it's not a problem and you want to keep it "as is".

  10. Anon says:

    Not fixing problems with the compiler because bad code might break is like not installing a fire suppression system in your munitions warehouse because it might accidentally go off when someone smokes under it.

    [You'd be surprised how many people say your product sucks because it broke bad code. -Raymond]
  11. Joker_vD says:

    @Anon: "Hey, we installed this new fancy (and power hungry machinery) in your house, and suddenly the wiring melted and started fire! Even though it shouldn't have happened did the previous electricians done their job properly! So it's their fault, not mine, blame them".

    Nope, you broke it, and the fact that the environment was not perfect is not an excuse. It's never perfect. It's rarely even good.

  12. Joshua says:

    [Tell that to the people who complain about stupid warnings. -Raymond]

    By linking to that particular example you defeated your argument.

  13. Marc K says:

    "By linking to that particular example you defeated your argument."

    Yes, please don't put in any backcompat hacks to support programmers like the one cited in that link.

  14. John Doe says:

    @Alex Cohn, perhaps you couldn't, if you're not familiar with COM.  But it's very basic COM: if it's in the same apartment, or if you have a raw object pointer (e.g. free-threaded marshaling objects in the same process), marshaling won't happen.

Comments are closed.

Skip to main content