SoFunction
Updated on 2025-03-10

C++ writes non-invasive interfaces

Finally I wrote about the non-invasive interface of C++, excited, happy, disappointed, relieved,... After doing so much object-oriented science, I have become impatient, so I don’t want to explain too much.

Although I knew how to develop a non-invasive interface in C++ for a long time, I finally felt satisfied after refactoring the entire framework code more than a dozen times. It supports adding interfaces to basic types, such as int, char, const char*, double; supports generics, such as vector, list; supports inheritance, the interface implemented by the base class means that the subclass also inherits the implementation of the interface, and the subclass can also reject the interfaces of the base class, such as a duck refuses the base class bird to "fly", and reports an error during compilation; supports interface combination;..., but, here, it is only briefly introduced, and does not involve the handling of various abnormal details in C++. In C++, if you have to do a little serious work, you have to face endless details.

Let’s take a look at its usage examples:

1. Naturally, define an interface: taken from real code snippets

  struct IFormatble
  {
    static TypeInfo* GetTypeInfo();
    virtual void Format(TextWriter& stream, const FormatInfo& info) = 0;
    virtual bool Parse(TextReader& stream, const FormatInfo& info)
    {
      PPNotImplement();
    }
  };

2. Interface implementation class. Assuming that the interface implementation of IFormatble is added to int, the actual code will definitely not write the code of the implementation class for each basic type. This is just for example. Just give it a name for a class.

  struct ImpIntIFormatble : IFormatble
  {
    int* mThis;  //This line is the key    virtual void Format(TextWriter& stream, const FormatInfo& info)override
    {}

    virtual bool Parse(TextReader& stream, const FormatInfo& info)override
    {}
  };


The key here is that the fields of the implementation class are specified to be dead, and can only contain 3 pointer member fields at most, and the first field must be the destination type pointer, the second is the type information object (used for generics), and the third is the extra parameters, and the order cannot be messed up. If the member field does not need to use the second and third member field data, it can be omitted and not written, like here. All interface implementation classes must comply with such memory layout;

3. Assembly, assemble the interface implementation class onto an existing class to tell the compiler that the class implements a certain interface (here is IFormatble), using the implementation class ImpIntIFormatble in step 2;

PPInterfaceOf(IFormatble, int, ImpIntIFormatble)

4. Register the implementation class into the interface implementation list of type information. This step can be omitted, just for the runtime interface query, which is equivalent to IUnknown's Query. This line of code is executed in the constructor of the global object and placed in the cpp source file.

RegisterInterfaceImp<IFormatble, int>();

Then you can use the interface happily, for example

      int aa = 20;
      TextWriter stream();
      FormatInfo info();
      TInterface&lt;IFormatble&gt; formatable(aa); //The name TInterface is so ugly, there is nothing I can do about it      formatable-&gt;Format(stream, info);
      double dd = 3.14;
      formatable = TInterface&lt;IFormatble&gt;(dd);  //Assuming double also implements IFormatble      formatable-&gt;Format(stream, info);

Is it a bit magical? Actually, it’s nothing, it’s just about trait and memory layout, which is just using type operations tricks. To examine the memory layout of ImpIntIFormatble, for the general C++ compiler, the virtual function table pointers of the object (if it exists) are placed at the object's starting address, followed by the member data field of the object itself. Therefore, the memory layout of ImpIntIFormatble is equivalent to,

struct ImpIntIFormatble
{
  void* vtbl;
  int* mThis;
};
 

Note that there is no inheritance here. This is the memory representation of the ImpIntIFormatble object that implements the IFormatble interface. Therefore, it is conceivable that the memory layout of all interface implementation classes is mandatory in the following form:

  struct InterfaceLayout
  {
    const void* mVtbl;
    const void* mThis;      //Object itself    const TypeInfo* mTypeInfo;  //Type Information    const void* mParam;  //Supplementary parameters are usually rarely used  };



Of course, if the compiler's virtual function table pointer is not placed in the object's start address, it will not be possible to play like this, and then non-invasive interfaces cannot be started. Then, it is TInterface, inherited from InterfaceLayout

  template<typename IT>
  struct TInterface : public InterfaceLayout
  {
    typedef IT interface_type;
    static_assert(is_abstract<IT>::value, "interface must have pure function");
    static_assert(sizeof(IT) == sizeof(void*), "Can't have data");
  public:
    interface_type* operator->()const
    {
      interface_type* result = (interface_type*)(void*)this;
      return result;
    }
    
  };



No matter what, the memory layout of the TInterface object is consistent with the memory layout of the interface implementation class. Therefore, operator->overloaded functions can be successfully completed with rough type conversion. Then when constructing the TInterface object, you forcefully obtain the virtual function table of the ImpIntIFormatble object (that is, the pointer data of its starting address) pointer to the mVtbl of InterfaceLayout, and then place the pointer of the actual object on mThis in turn, and place the type information object in mTypeInfo. If it is necessary to deal with mParam, assign the value accordingly.

Then, there is the use of template<typename Interface, typename Object>struct InterfaceOf, which is not worth mentioning.

Since C++ ABI does not have a unified standard, and the C++ standard does not stipulate that the compiler must use virtual function tables to implement polymorphism, the strange tricks here cannot be guaranteed to be established on all platforms. However, non-invasive interfaces are really convenient and are already the core tool for writing C++ code. Everything revolves around non-invasive interfaces.

I originally planned to make a long speech, but I could only end it in a hurry. After that, I will be liberated and will temporarily leave cppblog for a long time. The alternative implementation of the planned content, message sending, virtual template functions, strings, input and output, formatting, serialization, locale, global variables, template expressions, combinator sub-parser, allocator, smart pointer, program runtime, abstract factory visitors and other modes, in order to show the power of C++ from a new perspective, it can only be interrupted.