March 24th, 2022

Behind C++/WinRT: How does C++/WinRT decide which interfaces are implemented?

Last time, we diagnosed a problem by realizing that the unkwn.h header had not been included prior to including any C++/WinRT headers, and that means that C++/WinRT did not activate its code that supports classic COM interfaces.

This is going to be the first in what will probably be a very sporadic series of looking into the C++/WinRT implementation, as reverse-engineered by me.¹ I’m writing it in part so that I’ll be able to refer back to this write-up the next time I have to debug this code. And in part so that there are more people who understand the insides of C++/WinRT and can help support it. (This is the selfish reason for many of the articles I write: I’m writing them in order to reduce my own workload.)

It all hangs on this definition of is_interface:

#ifdef WINRT_IMPL_IUNKNOWN_DEFINED

template <typename T>
struct is_interface : std::disjunction<
    std::is_base_of<Windows::Foundation::IInspectable, T>,
    std::conjunction<
        std::is_base_of<::IUnknown, T>,
        std::negation<is_implements<T>>>> {};

#else

template <typename T>
struct is_interface :
    std::is_base_of<Windows::Foundation::IInspectable, T> {};

#endif

The WINRT_IMPL_IUNKNOWN_DEFINED macro is an internal C++/WinRT macro that remembers whether unknwn.h has been included. If so, then ::IUnknown is defined, and C++/WinRT can activate classic COM support. Let’s translate the C++ type traits template meta-programming into something we’re more familiar with.

One of the main tools of the C++ type traits system is the std::integral_constant<T, v>. This is a type that wraps a constant value v of type T.

template<typename T, T v>
struct integral_constant
{
    static constexpr T value = v;
    ... other stuff not relevant here ...
};

For example, std::integral_constant<int, 42>::value is an integer constant whose value is 42.

This seems pointless, but it’s not. Template meta-programming doesn’t have variables; it operates on types. The std::integral_constant lets you treat a type as if it were a variable whose value is the integral_constant::value.

C++ comes with a number of pre-made integral constants. Relevant today are std::true_type and std::false_type, which wrap a Boolean true or false, respectively. And it also comes with some pre-made template types that manipulate them:

  • std::conjunction performs a logical and on its arguments.²
  • std::disjunction performs a logical or on its arguments.
  • std::negation performs a logical not on its argument.

Okay, now we can start taking apart the first expression.

template <typename T>
struct is_interface : std::disjunction<
    std::is_base_of<Windows::Foundation::IInspectable, T>,
    std::conjunction<
        std::is_base_of<::IUnknown, T>,
        std::negation<is_implements<T>>>> {};

We mentally convert the std::disjunction to ||, the std::conjunction to &&, and the std::negation to !.

template <typename T>
struct is_interface is true if
    std::is_base_of<Windows::Foundation::IInspectable, T> ||
    (
        std::is_base_of<::IUnknown, T> &&
        !is_implements<T>);

Now we can read out the logic. Something is considered an interface if either

  • It derives from winrt::Windows::Foundation::IInspectable, or
  • It derives from ::IUnknown and is not an implements.

The rejection of implements prevents is_interface from misdetecting implements as a itself being COM interface.

Onward to the #else: If unknwn.h was not included, then we use a simpler definition of is_interface that merely detects derivation from winrt::Windows::Foundation::IInspectable.

In order to detect classic COM interfaces, C++/WinRT needs ::IUnknown to have been defined. Otherwise, it has nothing to test as a base class.

So that’s the quick diagnosis of yesterday’s problem wherein C++/WinRT failed to recognize classic COM interfaces.

Next time, we’ll dig in deeper to how the is_interface definition is used to pick out the interfaces.

Bonus chatter: As I noted last time, the requirement that you include unknwn.h before including C++/WinRT is no longer present as of C++/WinRT version 2.0.210922.5. The trick is to forward-declare the ::IUnknown type so that you can talk about it without knowing what it is. The std::is_base_class template type requires only that the proposed derived class be complete. The base class (::IUnknown) doesn’t have to be complete.

Exercise: Why is it okay for std::is_base_class to accept an incomplete base class? How can it possibly detect whether something derives from a class which has no definition?

¹ A lot of learning comes from reverse-engineering. When doing debugging, you are pretty much forced into it.

² Actually, std::conjunction and std::disjunction behave more like their JavaScript equivalent operators && and || because they short-circuit and support “truthiness”.

Answer to exercise: If the base class is incomplete, then nothing can derive from it. You can’t derive from an incomplete type.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

0 comments

Discussion are closed.

Feedback