Using C++ Modules in MSVC from the Command Line Part 1: Primary Module Interfaces

Cameron

In this three-part series we will explore how to build modules and header units from the command line as well as how to use/reference them.

The goal of this post is to serve as a brief tour of compiling and using primary module interfaces from the command line and the options we use.

Note: This tutorial will focus primarily on dealing with IFC and object file output. Other types of compiler output such as PDB info are not mentioned.

Overview

Summary of C++ modules options

Option Brief Description
/interface Tells the compiler that the input file is a module interface unit.
/internalPartition Tells the compiler that the input file is an internal partition unit.
/reference Provides the compiler with an IFC file to reference for the nominated module interface name.
/ifcSearchDir When the compiler falls back to implicit module interface search, directories specified by this option will be used.
/ifcOutput Tells the compiler where the IFC resulting from compilation should go. If that destination is a directory the compiler will generate a name based on the interface name or the header unit name.
/ifcOnly Instructs the compiler to only produce an IFC as the result of compilation. No other outputs will be produced as the result of compilation even if other options are specified.
/exportHeader Instructs the compiler to create a header unit from the input.
/headerName Tells the compiler that the input designates the name of a header.
/translateInclude Instructs the compiler to perform #include -> import translation if the header-name nominates an importable header.
/showResolvedHeader When building a header unit, show the fully resolved path to that header unit after compilation.
/validateIfcChecksum[-] Off by default. Specifying this switch will enforce an extra security check using the stored content hash in the IFC.

Basics of building a module interface

For the content in this section, we will assume that you have an appropriate compiler environment command prompt set up and that you have navigated to the directory with your test files.

Let’s look at the most basic scenario we can for starters:

m.ixx:

export module MyModule;

export
void f() { }

main.cpp:

import MyModule;

int main() {
  f();
}

The simplest way to build this sample is the following:

$ cl /c /std:c++latest m.ixx
$ cl /std:c++latest /reference MyModule=MyModule.ifc main.cpp m.obj
$ .\main.exe

One quick note about the name of file m.ixx above, the .ixx extension is the default module interface extension for MSVC. If you wish to use a different extension then you must use /interface along with /TP in order to compile the input as both C++ and as a module interface. Here’s a quick example of compiling the module interface if the name were my-module.cppm:

$ cl /c /std:c++latest /interface /TP my-module.cppm

In the first line we compile the module interface, and two things happen implicitly:

  1. The compiler will derive a name for the resulting object file based on the base name of the input file. The resulting object file in this case is derived from m.ixx transformed into m.obj.
  2. The compiler will derive a name for the resulting IFC file based on the module interface name. The resulting IFC in this case is derived from the module name MyModule transformed into MyModule.ifc. Note that the name of the input file has no bearing on the exported module interface name, they are completely orthogonal to each other so if this file were named foobar.ixx the generated IFC name would still be MyModule.ifc.

If we take away the two implicit points above, we will end up with a command line which looks like this:

$ cl /c /std:c++latest m.ixx /ifcOutput MyModule.ifc /Fom.obj

On the import side we could take advantage of the compiler’s implicit lookup behavior to find the module interface:

$ cl /std:c++latest main.cpp m.obj
$ .\main.exe

Whoa! Hold on there! What happened? Well, in MSVC the compiler implements a well-coordinated lookup to find the module interface implicitly. Because the compiler generates a module interface IFC based on the module name it can safely be assumed that if there is no direct /reference option on the command line then there could be an IFC somewhere on disk which is named after the module interface name. In the scenario above we are trying to import a module interface named MyModule so there might be a MyModule.ifc on disk, and indeed there is! It is worth pointing out that this implicit lookup behavior will search the current directory along with any directory added using /ifcSearchDir.

Let’s consider a scenario where the destination for the resulting IFC is not in the immediate directory. Consider the following directory structure:

./
├─ src/
│  ├─ m.ixx
│  ├─ main.cpp
├─ bin/

And let’s assume that our compiler command prompt is rooted at ./ and that we want all output to go into the bin\ folder. Here’s what the fully explicit command lines look like:

$ cl /c /std:c++latest src\m.ixx /Fobin\m.obj /ifcOutput bin\MyModule.ifc
$ cl /std:c++latest /reference MyModule=bin\MyModule.ifc src\main.cpp /Fobin\m.obj /Febin\main.exe bin\m.obj

There are a lot of things going on so let’s narrow the scope of noise to just the command line options required to compile main.cpp and not link it.

$ cl /c /std:c++latest /reference MyModule=bin\MyModule.ifc src\main.cpp /Fobin\m.obj

Note: The /Fo tells the compiler where to put the resulting object file. Further, in order to ensure that the compiler can properly detect that the destination is a directory, please append the trailing ‘\‘ at the end of the argument.

If we wanted to take advantage of the compiler’s implicit naming mechanisms the command lines would be the following:

$ cl /c /std:c++latest src\m.ixx /Fobin\ /ifcOutput bin\
$ cl /std:c++latest /ifcSearchDir bin\ src\main.cpp /Fobin\ /Febin\ bin\m.obj

Notice that the difference here is we simply provide a directory as the argument to each of our command line options.

Modules with interface dependencies

Often, we don’t want to build a single module interface and call it a day, it is frequently the case that sufficiently large projects will be composed of many module interfaces which describe various parts of the system. In this section we’ll explore how to build translation units which depend on one or more interfaces.

Let’s consider a slightly more sophisticated directory layout:

./
├─ src/
│  ├─ types/
│  │  ├─ types.ixx
│  ├─ util/
│  │  ├─ util.ixx
│  ├─ shop/
│  │  ├─ shop.ixx
│  │  ├─ shop-unit.cpp
│  ├─ main.cpp
├─ bin/

The code for these files can be found here.

As you explore the code you will find that many of these modules/source files contain references to module interfaces and those interfaces may reference yet another interface. At its core, the most basic dependency graph looks like the following:

   types.ixx
   /       \
util.ixx  shop.ixx
  \        /
shop-unit.cpp
      |
   main.cpp

Without further ado, here are the explicit command lines in all their glory:

$ cl /c /EHsc /std:c++latest src\types\types.ixx /Fobin\types.obj /ifcOutput bin\types.ifc
$ cl /c /EHsc /std:c++latest /reference types=bin\types.ifc src\util\util.ixx /Fobin\util.obj /ifcOutput bin\util.ifc
$ cl /c /EHsc /std:c++latest /reference types=bin\types.ifc src\shop\shop.ixx /Fobin\shop.obj /ifcOutput bin\shop.ifc
$ cl /c /EHsc /std:c++latest /reference types=bin\types.ifc /reference util=bin\util.ifc /reference shop=bin\shop.ifc src\shop\shop-unit.cpp /Fobin\shop-unit.obj
$ cl /EHsc /std:c++latest /reference shop=bin\shop.ifc /reference types=bin\types.ifc src\main.cpp /Fobin\main.obj /Febin\main.exe bin\types.obj bin\util.obj bin\shop.obj bin\shop-unit.obj

That is quite a mouthful. One thing you might notice is that when we built src\shop\shop-unit.cpp we needed a reference to both types and shop even though there’s no explicit import of either interface. The reason for this is because util has an implicit dependency on types to resolve Product properly and because it is a module unit the line module shop; implicitly imports the module interface shop, this behavior is defined by the C++ standard.

Applying some techniques learned above we can drastically reduce the noise by using implicit naming/lookup:

$ cl /c /EHsc /std:c++latest src\types\types.ixx /Fobin\ /ifcOutput bin\
$ cl /c /EHsc /std:c++latest /ifcSearchDir bin\ src\util\util.ixx /Fobin\ /ifcOutput bin\
$ cl /c /EHsc /std:c++latest /ifcSearchDir bin\ src\shop\shop.ixx /Fobin\ /ifcOutput bin\
$ cl /c /EHsc /std:c++latest /ifcSearchDir bin\ src\shop\shop-unit.cpp /Fobin\
$ cl /EHsc /std:c++latest /ifcSearchDir bin\ src\main.cpp /Fobin\ /Febin\ bin\types.obj bin\util.obj bin\shop.obj bin\shop-unit.obj

This is looking much better. We can take it a step further by taking advantage of the fact that cl.exe will process each source file in a linear sequence:

$ cl /EHsc /std:c++latest /ifcSearchDir bin\ src\types\types.ixx src\util\util.ixx src\shop\shop.ixx src\shop\shop-unit.cpp src\main.cpp /Fobin\ /Febin\main.exe /ifcOutput bin\

The command above uses implicit naming/lookup along with cl.exe‘s linear source processing behavior.

Note: the above command line will not work if the option /MP is used (compiling multiple inputs in parallel).

Just to be complete, we could also use explicit naming for our module interfaces in the single command line above:

$ cl /EHsc /std:c++latest /reference shop=bin\shop.ifc /reference types=bin\types.ifc /reference util=bin\util.ifc src\types\types.ixx src\util\util.ixx src\shop\shop.ixx src\shop\shop-unit.cpp src\main.cpp /Fobin\ /Febin\main.exe /ifcOutput bin\

The reason either of these command lines work is that the compiler will not try to do anything special with a /reference option unless the name designating the IFC is used and there is no extra cost to add /reference options for a command line if you know the module will be generated at some point in the input sequence.

Closing

In part 2 we will cover how to handle module interface partitions. Finally, in part 3 we will cover how to handle header units.

We urge you to go out and try using Visual Studio 2019/2022 with Modules. Both Visual Studio 2019 and Visual Studio 2022 Preview are available through the Visual Studio downloads page!

As always, we welcome your feedback. Feel free to send any comments through e-mail at visualcpp@microsoft.com or through Twitter @visualc. Also, feel free to follow me on Twitter @starfreakclone.

If you encounter other problems with MSVC in VS 2019/2022 please let us know via the Report a Problem option, either from the installer or the Visual Studio IDE itself. For suggestions or bug reports, let us know through DevComm.

3 comments

Leave a comment