-
-
Notifications
You must be signed in to change notification settings - Fork 645
Description
Modules Proposal
A short while ago I posted this issue asking for a guide on how to use C++-20 modules with premake projects.
From that inquiry I could gather that there had been no attempts yet to implement module support for the gmake generator. There is, however, a work in progress for support with Visual Studio. Microsoft has been an early adopter for modules, and they actually have a presentation at cppcon this year about their module implementation for MSVC and Visual Studio, which seems feature complete though lacking full support for module partitions. So naturally, it has been more easy to implement module support in premake for VS projects.
Since there existed no current effort for doing the same with gmake, I decided to implement it myself. Thus the following proposal.
Description
This document describes a proposal for an implementation which enables C++-20 modules support for the GCC toolset and GNU Makefile. We can assume that most users on Windows platform will be using MSVC, so *Unix will be the primary target of this discussion.
Challenges
I have written about here, and many people have been talking about, the benefits but also the challenges with modules. First of all, with header-inclusion we have a separation between symbol declaration and symbol definition in source code, allowing us to compile translation units in arbitrary order no matter their inter-dependencies. We can include header files freely, and then rely on the linker to provide us the implementation at a later stage.
But sinces a module is a binary representation of an abstract syntax tree, and we do not want to include header files in a module system, we must depend on precompiled binary module interfaces (BMI's). While we certainly can still use header files in a modularized build pipeline it provides us with little benefit. E.g. if our library interface is dependent upon standard library types such as std::vector or std::shared_pointer, then we still need to include those standard library headers in our headers, and in turn include those again in our module units.
So clearly, a build tool for modern C++-20 development must allow the programmer to omit header-inclusion while still providing a functional compilation.
Requirements
As always with C++, a core design goal is to provide the user with maximal freedom of expression. And this philosophy should be kept with a premake implementation. Microsoft has decided to standardize the file extension for module interface units as .ixx. Clang as well, has certain expectations. But, users should be allowed to specify files and project structure as they desire, and the build tool should aim to support their choices.
As for GCC, there is no expected file extension for module units, that is a compilation unit which declares a module. Instead, by enabling the flags -std=c++20 and -fmodules-ts, the compiler reads the module declaration and builds a list of dependencies, and checks for exisiting BMI's for each of those dependencies. If one of these BMI's cannot be found then the whole compilation fails. By default, GCC uses a module mapper which stores local BMI's in a gcm.cache/ directory at the invocation location.
For these reasons, a build tool supporting GCC with modules should read the module declarations of all referenced source files in a project, construct a dependency graph of all their dependencies, and then generate a sequence of build commands such that module units are built in the correct order. GCC does not differentiate between a module partition and a module interface unit - both are just C++ source files and treated with the same recognition. This is a fundamental difference vs. MSVC and Visual Studio which explicitly tags module files.
While we can still tag module files, it is not suffient to simply build module partitions first since they might depend on fully defined module units. Though I have not done any research on how the Visual Studio dev team has developed their module build system I suspect they have done a similar thing.
Parsing a module file
My early-stage module parser performs the actions described above. By reading through the cppreference (here) one may observe that module declarations must always be at the top of source files, which clearly supports the development of such tools. After all, we do not want to read through the entire file if we can retrieve what we need to know from the first ~10 lines. Consider this example:
module; // global module fragment
#include <memory>
export module mechanics; // module declaration
import <iostream>; // import declaration
export { // exported scope
class Robot {
public:
void SayHi() { std::cout << "Hi, I am a robot!\n"; }
};
}
// [...]The global module fragment tells us to enable the preprocessor for included headers placed immediately after. When we read the module declaration we know this file should be included in the module build pipeline. The import declaration enables translation units importing this module unit to use the Robot::SayHi() method without needing to #include or import iostream themselves. And finally, when we see about an exported symbol (signified by the export scope) we can safely ignore the rest of the file (which may be hundreds or thousands of lines) since no module imports or exports may appear after this point.
Now, imagine we have another source file (file names are not relevant here):
export module corporation : robotics_facility;
export import mechanics;
export {
Robot* construct_robot(/* ... */) {
// ...
}
}
// [...]If we have both files in our list of project source files, we always need to build the mechanics-module first, no matter the order in which they appear in the file list. Furthermore, if changes happen to the mechanics-module, we need to invalidate the depending module partition and build that again. Visual Studio will check these dependencies automatically, and with Makefile we can set target dependencies.
Creating build commands
With Makefiles, if we should create build targets for the above-mentioned two module units, they could look like this:
MODULE_FLAGS=-fmodules-ts
STD_HEADER=-xc++-system-header
iostream:
$(CXX) $(STD) $(MODULE_FLAGS) $(STD_HEADER) $@
mechanics: iostream
$(CXX) $(STD) $(MODULE_FLAGS) [...]
corporation-robotics_facility: mechanics
$(CXX) $(STD) $(MODULE_FLAGS) [...]Seeing that premake already has such a system for detecting source/header dependencies, I do not think building and parsing a module dependency graph will be necessary (whew).
Interface in premake.lua files
We need a new setting called cppmodules which can be se to "enabled", and by default be set to "disabled". When enabled this should add the -fmodules-ts flag to GCC, and the -fmodules flag to Clang (or similar), and the <EnableModules>true</EnableModules> to the generated Visual Studio project files.
We already have the compileas with options for "Module" and "ModulePartition". These should be recognized, but ignored, by gmake2.
We also need further options for toolset when using gmake2 with GCC. At the time of writing, the earliest compiler supporting C++-modules is g++-11, and since people might have earlier versions installed as well, this flag should support "gcc10", "gcc11", "g++-10", "g++-11", etc.