The Draconic Engine native layer is written in C++ and its native libraries are structured using modules. Modules allow grouping of C++ code in a standard encapsulated way.
Modules provide a new compilation mechanism where the compiler maps groups of symbols to names. Existing legacy C++ code remains compatible, but as an implementation detail. Modules are therefore best used when wrapping legacy source/header code.
There are three types of translation units when one includes modules:
- Regular translation unit (the source/header kind)
- Module interface unit
- Module implementation unit
Module interfaces export the public declaration for the module. They play a role which is the equivalent to that of headers in the traditional C++ translation unit.
Module implementation units do not export publicly, although all the code in a module implementation is visible to the linker, thus allowing to split the interface declaration from the implementation.
Declare module interfaces in files using the .cppm extension
Implement modules in files using the .cpp extension
A module interface or implementation unit has a well-defined structure. It contains:
- Global module fragment
- Named module fragment
The global module fragment is the portion of the unit where global module symbols may be included (via headers), declared and/or defined therein. Those symbols are said to be in the global module purview and may be used in the entire unit, but will not be exported themselves unless an explicit export declaration is made in the named module fragment.
As an example, one may #include <vector> in the global module fragment, and this means
that std::vector will now be in the global module purview and usable across the entire module
unit, making it also possible to export declarations with it. However, the std::vector template
itself will not be exported through the named module. Any client code that imported this module
will still not know any symbols for std::vector unless it also does #include <vector>
alongside the import. And the same goes for macros.
For this reason, it is heavily recommended that headers are wrapped in a module which is as low in the hierarchy as possible, in order to avoid obfuscated header dependencies creeping up at the top. In particular, one should avoid exporting declarations containing STL header symbols that are not properly wrapped.
Do not leak global symbols or preprocessor macros through a named module interface
The named module fragment is the portion of the unit where the module declaration is exported
and/or implemented. Exported symbols are prefixed by the export keyword. Entire structs, classes
or namespaces can be exported in one go.
Example of a module interface unit
// Global module fragment starts here
module;
#include <cstdint>
// ... all traditional source/header C++ code goes in here
// Named module fragment starts here
export module example;
// transitive import: expose everything from module "stuff" through "example"
export import stuff;
// foobar() is part of the example module, and exported
export void foobar();
export namespace foo {
// All declarations inside this namespace are exported
using uint32 = unsigned int;
}Example of a module implementation unit
// Global module fragment starts here
module;
#include <cstdint>
// ... all traditional source/header C++ code goes in here
// Named module fragment starts here (note: not exported)
module example;
import counterexample; // use anything from module "counterexample" only in here
// private_foobar() is not exported
void private_foobar()
{ /* */ }
// foobar() is implemented in here
void foobar()
{ /* */ }Our CMake project contains scripts that make it easy to add modules to the source tree.
Simply add your library containing the modules to a subdirectory
and use the add_modules_library function.
In this example, the library is called my_modules.
It exports module_1 and module_2, which in turn use subcomponent_1
and subcomponent_2 modules as well.
Most of these modules have an internal implementation unit as discussed above.
We also provide two unit test programs for the my_modules library.
Files that end in .test.cpp are automatically added to CTest.
my_modules/
├── subcomponent_1/
| ├── subcomponent_1.cppm
| └── subcomponent_1.cpp
├── subcomponent_2/
| ├── subcomponent_2.cppm
| └── subcomponent_2.cpp
├── module_1.cppm
├── module_1.test.cpp
├── module_2.cppm
├── module_2.cpp
├── module_2.test.cpp
└── CMakeLists.txt
The CMakeLists.txt in my_modules needs to refer to the modules libraries
in its subdirectories.
add_modules_library(subcomponent_1)
add_modules_library(subcomponent_2)From the parent directory of my_modules, simply do
add_modules_library(my_modules)
target_link_libraries(my_modules PUBLIC subcomponent_1 subcomponent_2)This will add all the exported modules and unit test programs to the build.