 Hello, my name is Cameron de Camera and I'm a member of the Visual C++ compiler team. I work on the C++ compiler front end and primarily in the area of implementing modules. Today I'm going to talk a little bit about how to leverage modules in projects and how to use them effectively. We'll also discuss some tooling aspects of a modules ecosystem towards the end. Without further ado, this is practical C++ modules. Let's start by breaking down the category of topics that this talk will not touch on. Please note that this list is not a comprehensive one. Right off the bat, we're not going to discuss how to integrate modules into existing projects using a specific build system. The techniques I'm going to share here are going to pertain to source level transformations that one can apply, which facilitate in organizing their code in order to take advantage of modules. The next thing we're not going to talk about is how to deploy prevailed modules. This kind of falls under the umbrella of packaging and is a topic that will always have a solution specific to the project that you're working on. So let's get right into it. We'll first discuss the modules machinery included in C++20 at a high level. Then I'll get into our first demo portion where we will use that machinery to help migrate a project. We'll gradually use more advanced techniques as we progress through that migration. And finally, we'll discuss tooling and the implications module has on the C++ ecosystem and beyond. So let's talk a little bit about C++20 exported translation units. I chose the wording of C++20 exported translation units very carefully. C++20 brings us some name module units, as a lot of people might know, but it also affords C++ with a more generalized notion of exported translation units, and I'll elaborate on that a little bit as we progress. So we kind of have module units, which have two very common partitions to them, which is primary module interfaces and module partitions. Finally we have header units, which is the last form of an exported translation unit that C++20 gives us. So let's first talk about name module units. These have clear boundaries of API surface area exposed through various declarations marked as exported. The primary module interface are what make up the entirety of the entry point to this exposed API surface area. So if we were to break down a primary module interface, an example of one could be this, where we have the primary module declaration at the top export module M, and we're exporting this MyType. We do not export the internal type, and then we export a function which accepts MyType and just uses the internal type in its implementation. So the consumer of this module might just import it, use MyType for some various operations, and then call the API that we exposed through the export declaration F. Now if you tried to use the internal type in the import side, that would result in a syntax error, because that name would just not be found by the compiler. It was not exported. The compiler on this side does not know about it. Module partitions are a specific type of interface which ultimately end up composing a single module interface. Module partitions provide the programmer with a new way of organizing their API in a logical way. These can be done for a multitude of reasons. One such reason possibly is to reduce compilation time in a rebuild scenario. Another reason might be to use partitions is to separate their interface into distinct pieces to help unglue certain dependencies, and I'll get into this a little bit later when we get into the demo portion of the talk. In this example, we took the original primary module interface from before and separated it into two partitions. So we actually have the same API surface as before, except that we've made it clear to any contributor to the primary module interface that functions always go in the function partition and types always go in the types partition. And so the use of module partitions is the same as before. Again as I mentioned, we had the same API surface, so to the end user, it looks exactly the same. They still import that module. It's just the composition of that module is different under the covers. Which brings us to the last form of C++20 exported translation unit, header units. Header units are really kind of just separately compiled header files, and they act more like a formalized PCH. Header units export everything in its translation unit, and they also have the added ability to export macros. So a use of this might look like this. And the syntax is actually very analogous to hash include where the argument name is a header name. So you can have the quoted form as I have here, or you can actually have the angle bracket form that you would see with a lot of the standard library headers. And let's say this m.h exports the same content, or it just has the same declarations as our original module interface, so we can use my type and we can use f. And if there were any macros defined in that header, we could also use them in this translation unit. Now that we know all the different types of exported translation units available to us, it's important to know which ones will be easier to integrate than others. And I'm going to just go through the list of the different types of exported translation units in order of integration difficulty for projects. So the very first one is header units. Header units are very easy to integrate into most projects, simply because they preserve all the macro state, they preserve all the declarations that were in that header. You can almost do like a one-to-one transformation of hash include to import header unit. So that one's fairly straightforward. The next one is primary module interfaces. Now because macro state doesn't come out of a module, a named module interface, these are a little bit more harder to define, especially because you now have the ability to restrict what API surface you offer all of your consumers. So there's some cost benefit there. And finally, we have module partitions. This is really just a refinement of the primary module interface. So as we go from one to three here, we're descending from the largest API surface to the most narrow as we descend into our dependency chain for the project. So let's finally jump into the demo portion of this talk. So the tools we're going to use for this demo portion are MSVC. This is an unreleased toolset. It's going to be sometime after 16.6. I'm going to be using VS Code as my main editor and Python 3 for the latter half of this demo, which is going to be really around tooling. Let's say that we've been provided this small library. In this library, we have collections of animals, collections of transports, some internal implementation details, and some algorithms that we can run on those types. The library author decided to use the stood variant vocabulary type to get a little bit of runtime polymorphism over dog, cat, and horse for the animal section. In the transport section, we use the same vocabulary type to group car, plane, and animal horse together. And it's relevant that we reuse this animal horse type here because when we go to modularize this later, we'll actually have to import this type from a separate module, which we're going to name animal. In here, we have some internal implementation details to the algorithm section. And these are not really meant to be used by the end user. It's really just used to implement details of the algorithms themselves. And getting into the algorithms, we have this one question we can ask, which is can this transport transport the given animal? So when we go to dispatch over this type, we have various cases. So a car cannot transport a horse. A horse is too big for a car. A plane cannot transport animals. And you can't put a horse on a horse. That's just silly. And finally, we have our catch all case for everything else, where this transport is capable of transporting the given animal. We finally have some two string methods, which do exactly what you would expect. It takes in a generic type, dispatches on it, and gives us a string-applied version of that type. Over here in our main section, let's ignore this function for now. This function really is going to use that algorithm that's provided to us for figuring out whether or not this transport is capable of transporting this animal and just print it out to the screen. So if we go to compile this, you'll see that we get some numbers here. We can compile this translation unit in about 1.5 seconds. And if we run it, it prints out all the various combinations that we have in our main here. So this is kind of C++ as we know and love it today. So let's say at some point later, the library author decides that they really don't want everything in one giant header file. And they think that splitting it up into multiple header files is a little bit more of a maintainable solution. So one thing you can do in C++ today is do exactly that. So if we go into our collections header, let's go ahead and split up all of these different header files and separate our concerns a little bit. So we have one for animal. We have one for transport. And we have one for the algorithms. This is the exact same program semantically as before, just that we have organized our code differently so that things are slightly more maintainable going forward. And in fact, we can compile this. And you'll notice that the output time is roughly the same. And it runs exactly the same as before. So we have the same program semantics. It's just we've organized our code a little bit better. So this is all well and good. But the problem is that all of the consumers of our library collections here are still going to be paying that copy-paste cost of all of those header files. So things like collection animal and stood variant, all that stuff is going to be compiled into every translation unit that depends on collections. So C++ 20 gives us the first mechanism we can use and leverage to move our code into a modular world. And the first thing that we're going to try to integrate is header units. So this is what integrating header units in your program might look like. You'll notice that the only discernible difference between this main and the last one is that we've changed the hash-include preprocessor directive into an import directive. Likewise, everything in our library has changed from hash-include to import directives. So collections.h, transport, and algorithms. So if we build this now, we can't just build the main by itself anymore. We have to build all of these header files separately. They're separately compiled translation units. So first we build all of the headers. And then we can actually build our main now. You might notice our time went down a little bit here. And in fact, if you were to benchmark where the time is being spent now, it's actually up here in this include iostream. Now we could move this into our collections.h, but we don't want to have all of iostream be included in collections.h. That seems like a pretty big hammer to impose on all of the consumers of our library. So for right now, we're just going to keep it outside of our header unit collection. And if we were to run this now, we do get the same output as before. So as I mentioned before, header units are kind of the easiest thing to start integrating in the projects right now. The barrier to entry is really low because they preserve all of the macro state that preserve every declaration that's in your header files. All you really have to do is make your header files a standalone translation unit. They have to compile. That's the bare minimum requirement for header units. So let's see if we could possibly move this thing into more of a named module unit. So this is kind of what our main program looks like after we've integrated named module units now. The only thing that's really changed is the import declaration this time. And it's now naming our named module interface. And if we look at the contents of this thing, the very first thing you might notice is this section up here. Between the module semicolon and the export module collections, this area is what's known as the global module fragment. And I like to think of this area as this is kind of where your third party code goes. STL headers, boost headers, that kind of stuff are all great candidates to put in this section. And anything in this section will not be exported by the module. It's just used really for implementing the details of your APIs. So as we take a look at this module interface now, you might notice that it has kind of a very similar shape to the original header file that contained all of this library in one file. And that is something we will touch on in just a little bit. But for now, let's focus on what is actually happening here. So you might see all of these export keywords here. These are what actually end up creating the API surface of this named module collections. Everything that has an export on it will be exported and observable from outside the module. You'll see that this collections internals is not exported. So if I tried to use this type on the import side, the compiler would complain and say that I cannot find it. And we'll see an example of that in just a bit. Everything else is exactly the same as it was before. So if we go back to our main now, the way we compile this is very similar to the header unit. We have to build the module first. And then we can build our main. And we actually get a very similar time to what the header unit had. And we're going to address that really quickly right now. So you may recall that before we didn't actually want to include Iostream in our original header unit because that means that anybody who included that header unit collections.h would also get Iostream. That's not something that we wanted to impose on any of our users. So with named module units, we actually get an opportunity to hide Iostream as an implementation detail of these two APIs. So what we can do here is we can actually take these two APIs and put them into our named module down here into the algorithm section. Add Iostream into the global module fragment. We can now delete it from up here. Since we have put those APIs in the algorithms namespace, we're going to have to add this. And we can rebuild our module, rebuild our translation unit. And now we have some really significant speed up. And in fact, the program runs exactly the same as it has before. But we can rebuild this in a 10th of the time that it took to build the original version. So you recall me mentioning that modules can be used to hide implementation details of our APIs. So if we tab back over here to our interface file and we scroll down back to this internal section, you'll see that this overloaded type is not exported. So in my main, if I try to use that, all this type does is really accept a series of lambdas and create an overload set object out of them. If I try to build this, the compiler will error out and say it doesn't know about this overloaded name. It was never exported from the interface. So the compiler simply doesn't know about it on this side. So now that we've talked a little bit about primary module interfaces, let's talk a little bit about what it looks like to integrate module partitions into a project. So this is what it looks like to integrate module partitions into a project. None of our main has actually changed. It's even the same. It can even be the same file. But underneath, how our module interface is composed has actually changed quite a bit. So if we look at what our interface looks like now, at the top level, all you see is the collections module interface declaration. And then we import, export all of the partitions that compose this API surface. So if we peek into animal, for example, you see it looks just like a module interface except that it has this new syntax, which tells the compiler that this is a module partition. And we export it as we did in the primary module interface. Similarly, transport has the same thing. But whenever we import another partition, if we're in the same module unit, we can actually just use this syntax to import a separate partition that's part of the same module. And we get access to animal horse in this way. Algorithms, since it uses both of these module partitions, imports them both. And algorithms is largely the same as before. And as before, we have moved these two functions into the algorithms. So now that we have these module partitions, we actually build it similarly to how we built all those header files before. We need to sequence them. So if we go through and we build the module, and the reason we have to build them in a specific order is because some of the interfaces depend on each other. And then we finally build the main as before. We get very similar speedups that we saw with just the primary module interface. And in fact, we can run this, and it has the same output. So we have not changed the program semantically from the very beginning. So you may be asking yourself, why would we go through the process of converting all of these modules into partitions? What does that actually gain us? Well, from an organizational point of view, you actually get the ability to very specifically tell users of your libraries and contributors where to find things and where to put things. But another thing, and I mentioned this before, is that it can actually contribute to build throughput wins in rebuild scenarios. So I'm gonna go ahead and go and add an extra API to just the algorithm's partition. And we're going to rebuild just that partition and see if we can get some output. Okay, so now that we have just that algorithm, we're going to include iostream up here again, and we're gonna try to use it. So now all we should be able to do is rebuild just the algorithm's header, or the interface, and build our primary main. And now we can run it, and we'll see the output of one here because a horse is a horse, of course. Now the reason this actually works in the MSVC compiler is because we adopted a by reference model for referencing partitions and other module units. So this means that whenever you import a module into another module, all of the declarations from the imported module don't actually get shoved into the module that you're creating. It actually references the other module on disk. So it loads them dynamically as it needs them. There is one last thing I wanted to mention before we depart from this example, and that is the inclusion of iostream up here. You might recall that in the algorithms partition, we actually included iostream in the global module up here, but that doesn't actually mean that it's iostreams are accessible from outside the module. They are still not considered to be part of the API surface of that module. So we put our global module up here again, we import our module, and now we can use stdc out here as it is used inside the module within this result API. So all of the techniques I've shown so far work really well with toy projects, but what happens if we try to apply them to a real world code scenario? So what I've done here is I've gone out and I've cloned the I am GUI repository from GitHub. And all I've done is set up a basic VS code experience so I can build the library and I can build their DirectX 12 example that they provide. So let's go ahead and do exactly that and build the application and we can actually run this. You can see this works exactly like you would expect. So our goal right now is to just see if we can apply header units to this project right now. And the very first thing I like to start by doing is creating what I like to call a bridge layer between the DirectX 12 headers and the rest of the application, just the library portion. So I'm gonna go ahead and do that right now. Okay, so this application should build exactly the same as before and we can run it again. One reason I really like these bridge files is that they kind of help you separate the parts of your API from the system API. I use them a lot when writing my own personal projects and they're really mostly personal preference and not necessary for this demo. But they kind of make grouping all of those APIs a little bit easier when we go to make a header unit out of those things. So let's actually go through the process of replacing all of these hash include pre-processor directives into import directives throughout just the parts of the application, not the library. Now it's important to note that when we actually go to build these headers, we have to build them in a specific order because as you can see, most things depend on DX bridge and DX bridge can be built all by itself and IAM GUI can be built all by itself. Essentially any of the library headers can be built by themselves. The ones that we need to actually depend on are the IAM GUI Imple Win32.h and the IAM GUI Imple DirectX 12.h, which both depend on IAM GUI.h. So we have to sequence the build in that way. So now that the build steps are in place to build the library, then build the headers and then build the application, we can go ahead and do exactly that. So if we build the library first, this build should be exactly the same as before. Then we can build our headers and now we can finally build our application and just to see, we can run it again and we get the same output. So that is kind of a very brief overview of how to translate something, translate a project just using header units. Now, if you wanted to use module implemented or module units, that process is slightly more involved. And as we've seen, it can take quite a bit of time to figure out exactly what API surface you want to expose to all of your customers or any consumer of your library. Just as another quick test, I set up an example where we are using the header units and a case where we're not using the header units, we're using traditional hash include. And we're just gonna see the compile time throughput difference between these two scenarios. So without modules, we can build this translation unit in about 1.2 seconds. With modules or with header units, I should say, we build this translation unit in about a 10th of the time. And that's pretty essential when you're talking about more of a REPL type development cycle and giving developers more power when they just want to iterate very quickly on a problem. And again, this promotes the idea in C++ that you pay for what you use. And in this case, in the case of header units and in the case of modules, you're paying a cost which is directly proportional to the number of names and to the names that you use inside of your module. Okay, so let's quickly hop back to our overview page and talk a little bit about tooling while we have some time. So back in 2017, Gabriel Dosre, he published a paper called, Modules Are a Tooling Opportunity. And in this paper, he discussed the idea of having a proper semantic representation of a C++ program. Now, since then, Microsoft's compiler, MSVC, we have adopted the IFC format, which is our module BMI on disk. And this is spiritually a successor of the IPR work that came out of Gabby and Bjarnay. And really the goal of this work was to create a proper semantic representation of a C++ program internally in the compiler. Now, what happens when we actually persist that semantic representation on disk and what does it look like? Well, in reality, it's just a graph. So you can take this left-hand side over here and persist it out to disk and you might get a graph that looks something like this. And you can see all the names that Invoker depends on. You can see a whole bunch of other rich information that you can get from our IFC format. So what can we actually do with it today? What I have here is a very basic example of using the SDL library. And SDL is a C-based graphical API that has been around for quite some time. It's pretty mature. And this particular version is actually no longer available, 1.2. But what I have done is I've created a module out of this thing, or I've created a header unit out of the SDL.h. And I just took one of their basic examples and made it compile. So let's see what the output of this thing looks like. Right, so you get a very basic image to the screen. Now, the most interesting thing here is that we end up with this artifact on disk, the header unit for SDL.h. And let me show you something that might be a little bit surprising. On the right-hand side here, we have some Python code. And you might even say that this Python code looks very similar to the SDL code that we have on the left-hand side. And that is by design. And what I have done here is I've created this FFI library, which will take and consume the IFC format that we have on disk, translate it to some FFI wrappers for SDL and invoke the SDL APIs. So let's go ahead and run this wrapper and just see what happens. So the easiest way to invoke this is to just run Python over it and you provide the IFC that you have on disk to it. And we get the same output as the C++ program because really it's just using the DLL underneath. And just to prove that I'm not pulling your leg, I'm gonna make this window super wide just to see what happens. So let's rerun it and we get an overly wide window. So the thing that really made what I just showed possible is the fact that we have a formalized IFC specification. And this specification is, again, derivative of IPR. So if you're familiar with IPR, the data model that supports it, you're gonna be familiar with the IFC specification. What all we did in the Python example is we traversed the BMI that was on disk, the IFC, and looked for functions that we could generate FFI wrappers in using Python C type library. So the other thing about this IFC that you should know is that it's all 32-bit addressing. So if you build an IFC on a 32-bit machine, you can actually take that IFC and put it on a 64-bit machine and compile some C++ translation unit using that IFC. So you can move it from machine to machine. It's independent in this way. And finally, the IFC spec will be open source. So the tooling opportunities will be available to the wider community, not just people who work from Microsoft, for example. So what kind of conclusions can we draw from all this module's information, all of the possible scenarios that we can apply modules in in the C++20 machinery that comes behind it? I think that one of the primary takeaways is that moving to modules is always gonna be an iterative process. It's not gonna be something that happens overnight. And the other thing to acknowledge is that built systems will catch up, I promise you. Modules are certainly worth the effort. And it's going to bring a whole new generation of tooling opportunities. And tooling opportunities that we probably couldn't even think of 10 years ago, five years ago even. And it's just, it's an exciting time for C++. It's an exciting time for compiler developers. And it's even an exciting time for developers to see what people are gonna come up with. I wanted to finish with some acknowledgments. I wanted to first thank Cody Miller, who initially came up with the idea of creating the FFI wrapper based on the IFC format. And I wanted to thank Gabby, who provided a lot of good guidance and input into good modules practices, which helped me ultimately form this talk. And I also wanted to thank everybody here. Thank you for joining me on this sort of small journey in exploring modules. And I would like to invite anybody in the YouTube comments section who's watching this live right now to ask me, ask my future self some questions. Challenge me. And thank you again for joining me.