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:
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:
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
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:
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.
Nested types can never be forward declared. A nested type is a type that is inside a class or struct.
So prefer to put nested types into namespaces instead of structs or classes:
Templates can also be forward declared:
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:
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.
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:
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:
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 befinal
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:
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:
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