2.0 Help

HeaderCheck Tool

Types of Header Files

The code in Plasma Engine differentiates between two types of header files:

  • Public Header Files: Public header files are header files that can be included by third party. These header files should not leak any implementation details like platform headers. A third party is any library or executable outside of the currently compiled library / executable. For example when plFoundation is compiled, everything else is considered a third party.

  • Internal Header Files: Internal header files may include platform headers and leak implementation detail, but can only be used within a subcomponent of Plasma Engine (for example only inside plFoundation). Using them from outside of the component will cause a compiler error.

To mark up a header file as a internal header file, first include the component's internal.h file and then use the component specific macro. The component's internal header file is called ComponentInternal.h and the macro is called PL_COMPONENT_INTERNAL_HEADER.

The following example shows how to mark a header file as internal for plFoundation:

#include <Foundation/FoundationInternal.h> PL_FOUNDATION_INTERNAL_HEADER

The Header Checker Tool

The header checker tool will automatically be run by the continues integration to check for leakage of implementation detail. If a leak is found the build will fail. Usually you will see an error message such as:

Including 'wrl/wrappers/corewrappers.h' in Plasma/Code/Engine/Foundation/Strings/StringConversion.h:9 leaks underlying implementation detail. Including system or thirdparty headers in public Plasma header files is not allowed. Please use an interface, factory or pimpl to hide the implementation and avoid the include.

In this example including wrl/wrappers/corewrappers.h is illegal. This header file is included from Plasma/Code/Engine/Foundation/Strings/StringConversion.h at line 9. To fix these issues follow one of the techniques below to hide implementation details.

Hiding Implementation Detail

To consider the different options of hiding implementation detail have a look at the following example

#include <d3d11.h> class plTexture2D { public: void Bind(); private: ID3D11Texture2D* m_ptr; };

If a user includes this header file, the underlying implementation detail is leaked as the user will need the d3d11.h header in order to compile the code. Furthermore the user might need exactly the same version of the d3d11.h file in order for the code to compile. This is a leaky abstraction. Ideally classes that wrap functionality should not leak any of their implementation details to the user. The following techniques can be used to hide implementation detail.

Forward Declarations

Forward declarations can be used to remove the need to include a header file, therefor removing the leaky abstraction. Consider the following fixed version of the plTexture2D class:

struct ID3D11Texture2D; // Forward declare ID3D11Texture2D class plTexture2D { public: void Bind(); private: ID3D11Texture2D* m_ptr; };

This header is no longer a leaky abstraction as the user is no longer required to have a copy of d3d11.h.

Forward declarations can be made for:

  • Class or struct members if they are pointers or references.

  • All types used as arguments to functions.

  • Template arguments if the usage follows the two above rules.

Forward declarations can't be made for:

  • Class or struct members that are 'inline' because the compiler needs to know the size and alignment.

  • Base classes.

Enums can be forward declared if they are given an explicit storage type. So ideally to make enums forward declarable always manually specify a storage type.

enum MyEnum : int; // Forward declaration enum MyEnum : int // declaration { One, Two };

Nested types can never be forward declared. A nested type is a type that is inside a class or struct.

// does not work // struct Outer::Inner; struct Outer { struct Inner { int i; }; };

So prefer to put nested types into namespaces instead of structs or classes:

// Forward declaration namespace Outer { struct Inner; } // Declaration namespace Outer { struct Inner { int i; }; }

Templates can also be forward declared:

// forward declaration template<typename> struct Example; // Usage of forward declaration void bar(const Example<int>& arg); // declaration template<typename T> struct Example { T t; };

Advantages:

  • No runtime overhead

Disadvantages:

  • Forward declarations and actual declaration have to be kept in sync.

Moving Implementation Details Out Of Templates

Consider the following example which leaks implementation details:

// Application.h #include <roapi.h> template <typename AppClass> void RunApplication(AppClass& app) { RoInitialize(RO_INIT_MULTITHREADED); app.Init(); while(!app.Run()) {} app.DeInit(); RoUninitialize(); }

The two functions RoInitialize and RoUninitialize are platform specific functions and require the include roapi.h. We can't move the function into a .cpp because the implementation for templates needs to be known when using them. As a result this template leaks its implementation detail.

To fix this issue we need to wrap the leaking function calls into separate functions and forward declare these functions.

// Application.h void InitPlatform(); void DeInitPlatform(); template <typename AppClass> void RunApplication(AppClass& app) { InitPlatform(); app.Init(); while(!app.Run()) {} app.DeInit(); DeInitPlatform(); }
// Application.cpp #include "Application.h" #include <roapi.h> void InitPlatform() { RoInitialize(RO_INIT_MULTITHREADED); } void DeInitPlatform() { RoUninitialize(); }

As you can see we removed the include to roapi.h from the header file and moved it into the cpp file. This way our header no longer leaks underlying implementation details, as the user won't see the cpp file when using our library. If considerable parts of the template don't depend on the template arguments this pattern can also be used to reduce code bloat by moving the non dependent parts out into non-templated functions.

Pimpl Light

The pattern that I call "Pimpl light" can be used to hide implementation detail at the cost of an additional allocation:

Consider our original plTexture2D example it would be modified like this:

// Texture2D.h class plTexture2D { public: plTexture2D(); ~plTexture2D(); void Bind(); private: struct Impl; // forward declaration plUniquePtr<Impl> m_pImpl; };
// Texture2D.cpp #include "Texture2D.h" #include <d3d11.h> // Declaration of plTexture2D::Impl struct struct plTexture2D::Impl { ID3D11Texture2D* m_ptr; }; plTexture2D::plTexture2D() : m_pImpl(PL_DEFAULT_NEW(Impl)) { } // all constructors / destructors / assignment operators must be in .cpp file otherwise forward declaration will not work. plTexture2D::~plTexture2D() { } plTexture2D::Bind() { // Use the implementation detail m_pImpl->m_ptr->Bind(); }

This is an easy pattern to hide implementation details.

Advantages:

  • Simple to implement, hides nasty implementation details well

Disadvantages:

  • Additional allocation

  • Additional indirection

Pimpl Inheritance

The Pimpl pattern can also be implemented by using inheritance instead of a forward declared struct. For our plTexture2D example it would look like this:

// Texture2D.h class plTexture2D { public: plUniquePtr<plTexture2D> Make(); // factory function, could also return a shared ptr. virtual ~plTexture2D(); void Bind(); private: plTexture2D(); // All constructors must be private friend class plTexture2DImpl; // This is the only class allowed to derive from plTexture2D };
// Texture2D.cpp #include "Texture2D.h" #include <d3d11.h> // Actual implementation class plTexture2DImpl : public plTexture2D { public: plTexture2DImpl() : plTexture2D() {} ~plTexture2DImpl(){} ID3D11Texture2D* m_ptr; }; plTexture2D::plTexture2D() {} plTexture2D::~plTexture2D() {} plUniquePtr<plTexture2D> plTexture2D::Make() { return plUniquePtr<plTexture2D>(PL_DEFAULT_NEW(plTexture2DImpl)); } plTexture2D::Bind() { // Use the implementation detail reinterpret_cast<plTexture2DImpl*>(this)->m_ptr->Bind(); }

As you see this version of pimpl hides the implementation detail similar to pimpl light.

Advantages:

  • No additional indirection (compared to pimpl light)

Disadvantages:

  • Additional allocation

  • Can no longer inherit from plTexture2D

  • plTexture2D can't be final

Opaque array of bytes

We can also place an opaque array of bytes large enough to store our implementation detail. Considering our plTexture2D example it would look like this:

// plTexture2D.h class plTexture2D { public: void Bind(); private: #if PL_ENABLED(PL_PLATFORM_32BIT) struct PL_ALIGN(Impl, 4) { plUInt8 m_Data[4]; }; #else struct PL_ALIGN(Impl, 8) { plUInt8 m_Data[8]; }; #endif Impl m_impl; };
// plTexture2D.cpp #include "Texture2D.h" struct plTexture2DImpl { D3D11Texture2D* m_ptr; }; static_assert(sizeof(plTexture2D::Impl) == sizeof(plTexture2DImpl), "plTexture2D::Impl has incorrect size"); static_assert(alignof(plTexture2D::Impl) == alignof(plTexture2DImpl), "plTexture2D::Impl has incorrect alignment"); void plTexture2D::Bind() { // Use implementation detail reinterpret_cast<plTexture2DImpl*>(&m_impl)->m_ptr->Bind(); }

This again hides the implementation details in the header file.

Advantages:

  • No runtime overhead

Disadvantages:

  • High maintenance burden. Especially if implementation detail size varies on different platforms.

Ignore the problem

You can choose to ignore the leaky abstraction issue and tell the header checker tool to ignore a certain file to be included or give a certain file the permission to include anything.

Each module in PlasmaEngine that uses the header checker has a headerCkeckerIgnore.json file where you can add ignores. It looks like this:

{ "includeTarget" : { "byName" : [ "a.h" ] }, "includeSource" : { "byName" : [ "b.h" ] } }
  • In the above file every time a.h is included and would generate an error in the header checker tool, that error will be ignored.

  • Every time b.h includes a header file that would cause an error, this error will also be ignored.

Advantages:

  • Less work

Disadvantages:

  • Longer compile times

  • Conflicts due to global namespace pollution

  • Requires users to have all header files for implementation details available

See Also

Last modified: 03 July 2024