C++20 module support v1 (import winrt;)#1556
Conversation
sylveon
left a comment
There was a problem hiding this comment.
winrt_to_hresult_handler and friends needs WINRT_EXPORT extern "C++" to work properly when mixing non-modules and modules code in the same final DLL/EXE
… on exported handlers
This comment has been minimized.
This comment has been minimized.
You'll need to provide more info on how you configured your test project. I'm not hitting this in the test_cpp20_module project, even after adding some use of |
This comment has been minimized.
This comment has been minimized.
|
Idk about exporting the impl namespace, feel like we should find a better solution. |
|
If exporting impl is unavoidable, I still hope to avoid a single winrt module. A slightly cleaner approach is to provide winrt.impl and winrt.base, where winrt.base imports winrt.impl but only re-exports non-impl declarations: export module winrt.base;
import winrt.impl; // Not re-exported
export namespace winrt {
using winrt::hstring;
...
}winrt.impl is intended for use only by files generated by cppwinrt, while user code should use winrt.base. And generate independent modules for each root namespace, such as winrt.Windows. |
|
My idea would be to generate the things required to produce as part of the module too, but that would eliminate the ability to share winrt.ifc between projects. |
|
Damaging the development experience just to hide the implementation is completely not worth it in my opinion, especially since users can already access them through headers anyway. Considering only the development experience in Visual Studio, we could request the MSVC and IntelliSense teams to support an attribute such as |
|
OK, that compromise approach has worked! Update incoming...
|
|
I have to say, adding export to the outer scope of the partial specialization of std::coroutine_traits is indeed useful. I have successfully built a realistic WinUI3 project example that uses both include and import. I will publish it later. |
|
I have published it at https://github.com/YexuanXiao/Authenticator/compare/module. It can be built using MSVC 14.51 and 14.52. It is not fully modularized yet, but it is sufficient to demonstrate feasibility. |
|
@sylveon Why not considerate @YexuanXiao 's modularized impletation. I hope we can accelerate modular process! And @YexuanXiao Why not give us a PR sample! |
I have fully modularized the project. All .cpp files that require manual writing (including xaml.cpp) can be built entirely using modules without relying on header files. The commit with the message "step1" mixes include and imports, while the commit with the message "step2" makes the project fully modularized (except for the cpp files generated by xamlc and module.g.cpp). |
I'm curious to see a specific example of what you mean. Could you please share more details? |
|
It is hard for me to give an explanation right now. Today I tried adding individual |
|
I reported a new bug in MSVC. MSVC generates warning C4499 for a full specialization declaration with "export "C++": 'extern': an explicit specialization cannot have a storage class (ignored)". This issue can be reproduced without modules, and it is definitely a bug in MSVC because "export "C++"" is language linkage, but MSVC reports it as a storage class. |
|
@DefaultRyan I found that hstring_reference_flag is missing the inline modifier, which causes a redefinition error. I discovered this issue in the CI. #1571 |
I did some work starting to port a real-world project C++/WinRT project to use modules, and we absolutely need some level of mixing, as there are widely-used dependencies that include cppwinrt headers. For example, WIL's cppwinrt.h includes <winrt/base.h>. But good news, I went deeper on STL's usage of So now, you just need to ensure those inclusions happen before the module import in a TU. This should be easy for most people, as those sorts of dependencies are typically wrapped up into a PCH, while the individual module imports should be happening in cpp files. If we start getting too many headers that themselves import instead of include, that's where things get brittle and bad, quickly.
What is extern "C++" {
export namespace winrt {
}
}By doing this, I've gotten to the same level of compat as STL, as mentioned above. |
…winrt into feature/modules_v1
|
I tried the latest commit, and I think we have reached a consensus on most issues. However, I still have some questions and suggestions. First, Currently, the generated winrt module both includes headers and imports Second, is Then, as @sylveon mentioned, template specializations should not be exported, so each declaration that needs to be exported should be marked with Finally, the |
|
You can include directxmath.h in the global fragment, and then include |
I haven't verified whether |
|
Additionally, |
If my only concern was being able to tolerate including winrt headers, followed by importing the module, then I would have no worries. But... Remember component authoring. Especially when we have a large solution, with many idl files creating winmds, scattered across multiple projects. These types are not going into the winrt module, so we are still including those headers. But those component types generally do depend on the platform types that are in the module, so they are going to be processed after Put another way: // winrt/component.h
// My component's type has dependencies on, say, Windows.UI.Xaml.
#include <winrt/impl/Windows.UI.Xaml.2.h>If I include The alternative is just doing "all modules, everywhere", where the component projections also create all their own modules, and we generate them so they import the modules they need. Nobody ever includes winrt headers! There is some appeal to this idea, but it means dealing with the following:
|
|
Regarding the solution to the "import-then-include" problem, I implemented a solution in my own fork, because XAMLC currently only supports this mode. My approach is to set a general guard macro, This approach is simple enough, because the import already includes everything needed, so there's no need to include anything else. We can only control the winrt headers, and this method is sufficient to handle them. This means that if someone decides to use the winrt module, they can simply define this macro on the first line of their .cpp file without overthinking it, and then the winrt module will take effect without any other winrt headers bothering them. When XAMLC supports modules in the future, this macro can also be leveraged to change the generated code to the following form: #ifdef WINRT_CONSUME_MODULE
import winrt;
#else
#include ...
#endifThis concept is very general and simple. |
I had something like that early on, but I think it was clashing with some other parts of the approach I was taking. Now that I've gone down the road of modularizing almost half of the Terminal project, I'm thinking of going back that way. In fact, as I'm thinking of how to resolve some of the other pain points I'm hitting in the Terminal project, I keep coming back to "maybe the component projections should also be modules", which leads to "if we have multiple modules, we need to split out base to re-export" as well as "how do we name the different modules, and maybe we should be less monolithic". Which leads to some of the design decisions in your fork. Some parts still give me pause - I don't fully see what the user-experience is around the cycle-breaking SCCs. Can I import Windows.Foo.Bar, or do I need to import all of Windows.Foo? Which namespace modules can I actually import? If more APIs are added that change the namespace graph, will the SCC partition namespaces differently and cause customer code to break because now they are importing the wrong namespace partitions? But I was encouraged by the overall reduced friction of using it in your Authenticator repo. |
My original idea was to increase parallelism, reduce recompilation, and achieve a C#-like experience, and implementing a separate module for each namespace was the most straightforward approach I could think of. I also considered that changes to dependencies might break user code, but since I've already implemented it, so be it. I now think converting top-level namespaces to lowercase and adding a winrt prefix is also a good choice, and in the future we can further consider using module partitions to implement each module. I will continue to investigate how to make include and import coexist, just like in the STL. |
The MSVC Preview has fixed this issue, and the |
|
I'm going to put this PR on hold for a little bit. What I'm doing instead is adapting the "all-in on modules" approach used in @YexuanXiao 's cppwinrtplus fork, and will open up a fresh v2 PR using this approach. I've been kicking the tires on it and the per-namespace module approach seems pretty sound. I have a basic version working already, and am in the process of adding tests and trying it out in multi-project solutions. |
C++20 Module Support (
import winrt;)This PR adds C++20 named module support to C++/WinRT, allowing consumers to
write
import winrt;instead of using#includedirectives and precompiled headers. In multi-project solutions, a shared module builder project compiles the platform projection once; consumer projects reference the pre-built module for significantly faster builds.Quick overview
Two new MSBuild properties control module support:
CppWinRTModuleBuild— Generates the platform SDK projection and compileswinrt.ixx. Set on a dedicated static library project (the "module builder"), or on a standalone project for single-project scenarios.CppWinRTModuleConsume— Consumes a pre-built module from aProjectReferenceto a builder. The NuGet targets automatically resolve the IFC, OBJ, and include paths.Design choices
Macro-driven, not flag-driven. Module-aware behavior in generated component files (
.g.h,.g.cpp,module.g.cpp) is controlled by#ifdef WINRT_MODULEguards emitted by the code generator. The NuGet targets defineWINRT_MODULEas a preprocessor definition — no special cppwinrt.exe command-line flag is needed. This means the same generated files work in both module and header mode without regeneration.Boundary-based module guard. Each namespace header contains a "module guard" — a preprocessor block that checks whether the header's own namespace is in the module (via
WINRT_MODULE_NS_<self>). If it IS in the module, the TU is doing a traditional#include(not via import), so the header falls through to#include "base.h"and all cross-dep guards are bypassed. If it is NOT in the module (e.g., a component header), the TU must have already doneimport winrt;, so the header usesbase_macros.honly and cross-dep guards skip module namespaces. This means the same headers work in both module and traditional header mode without regeneration.Per-namespace include guards with compound conditions. After
import winrt;, consumers textually#includereference/component projection headers. Platform namespace deps (already in the module) must be skipped to avoid MSVC redeclaration errors, but component cross-namespace deps must NOT be skipped. Cross-namespace#includedeps use compound guards —#if !defined(WINRT_MODULE_NS_<dep>) || defined(WINRT_MODULE_NS_<self>)— so deps in the module are skipped only when the including header is NOT itself in the module.extern "C++"wrapping for include-then-import compatibility. The winrt.ixx module purview wraps all#includedirectives inextern "C++" { ... }, andnamespace stdblocks useWINRT_IMPL_EXTERN_CXX(which expands toextern "C++"in the ixx, empty in header mode). This gives declarations external C++ linkage so MSVC can merge textually-included declarations with module-exported declarations. This is critical for real-world codebases where 3rd-party headers (e.g., WIL's cppwinrt.h)#includewinrt headers before user code doesimport winrt;.Scoped
import std;.import std;is NOT placed insidebase.hbecause platform headers (<intrin.h>) transitively include STL headers, making a subsequentimport std;unsafe. Instead,import std;is emitted at the TU level — inwinrt.ixx(module purview),module.g.cpp, and.g.hfiles — controlled by theWINRT_IMPORT_STDmacro.Builder/consumer split.
CppWinRTModuleBuildandCppWinRTModuleConsumeare separate properties because in multi-project solutions, only one project should compile the expensivewinrt.ixx. TheCppWinRTGetModuleOutputs/CppWinRTResolveModuleReferencestargets handle cross-project IFC/OBJ resolution via MSBuild'sProjectReferenceinfrastructure.import std;is orthogonal.import winrt;works with C++20.import std;is optional and independently controlled by the existingBuildStlModulesproperty. On v143 (VS 2022),import std;requires/std:c++latest; on v145 (VS 2026),/std:c++20suffices.Exported
winrt::implnamespace. Thewinrt::implnamespace is exported from the module alongsidewinrt. This is necessary because component projection headers specializeimpltemplates likecategory<>,abi<>,guid_v<>, andname_v<>for their types.What's in this PR
Code generator (
cppwinrt/):.g.hfiles use#ifdef WINRT_MODULEtoimport winrt;(and optionallyimport std;) before including component namespace headers.g.cppandmodule.g.cppuse#ifdef WINRT_MODULE/#ifndef WINRT_MODULEfor import vs include pathswinrt.ixxwraps the module purview inextern "C++" { ... }, definesWINRT_IMPL_EXTERN_CXXasextern "C++", and conditionally doesimport std;winrt_module_namespaces.hgenerated alongsidewinrt.ixxwith per-namespace macroswrite_module_guard()— boundary-based: checksWINRT_MODULE_NS_<self>to decide betweenbase_macros.h(import path) andbase.h(traditional path)write_root_include_guarded()— compound guard:!defined(NS_dep) || defined(NS_self)for cross-namespace depsWINRT_IMPL_EXTERN_CXXapplied to allnamespace stdblocks for module linkage compatibilityimport std;removed frombase.h— only emitted in ixx/module.g.cpp/.g.hNuGet targets (
nuget/):CppWinRTBuildModuletarget — addswinrt.ixxto compilation, definesWINRT_MODULEandWINRT_IMPORT_STDCppWinRTGetModuleOutputstarget — exports IFC/OBJ/GeneratedFilesDir for consumersCppWinRTResolveModuleReferencestarget — resolves module fromProjectReferenceitemsCppWinRTModuleBuild/CppWinRTModuleConsumeproperties inCppWinrtRules.Project.xmlNuGet test projects (
test/nuget/):TestModuleBuilder— static lib, builds the moduleTestModuleConsumerApp— console app consuming the module + a component reference (multi-namespace, cross-namespace struct fields, platform type returns)TestModuleComponent— DLL with IDL, two namespaces, cross-namespace value type field, platformUrireturn typeTestModuleSingleProject— single project that builds and consumes its own moduleinclude_test.cpp— regression tests for include-with-WINRT_MODULERepo test projects (
test/):test_cpp20_module/test_cpp20_module_winrt— C++20 module consumer/builder (noimport std;)test_cpp23_module/test_cpp23_module_winrt— C++23 module consumer/builder (withimport std;)test_component_module— component DLL with modulesinclude_before_import_test.cpp— verifies#includebeforeimport winrt;works (extern "C++" coexistence)test_cpp20_moduleadded to CI test matrix (all architectures, MSVC only)Documentation (
docs/):modules.md— user-facing guide (Quick Start, properties, macros, architecture)modules-internals.md— maintainer guide (codegen pipeline, macro flow, extern "C++", boundary-based guards)nuget/readme.md— C++20 Modules section.github/instructions/modules.instructions.md— AI assistant instructionsKey macros
WINRT_MODULE.g.h/.g.cpp/module.g.cppbehavior; activates boundary-based module guard in namespace headersWINRT_BUILD_MODULEbase_macros.honlyWINRT_MODULE_NS_*WINRT_IMPL_EXTERN_CXXextern "C++"in module, empty in header mode. Applied tonamespace stdblocksWINRT_IMPORT_STDimport std;in winrt.ixx,.g.h, andmodule.g.cpp. Does NOT affect base.hCurrent limitations
CppWinRTSDKReferencesproperty on the builder project.import std;requires/std:c++lateston v143 toolset.import std;is NOT used insidebase.h— platform headers transitively include STL headers first. Instead,import std;is at the TU level.#include "base.h"behavior.import winrt;works thanks toextern "C++"wrapping.Future directions
CppWinRTModuleInputitem group.Documentation
See docs/modules.md for the full user guide and docs/modules-internals.md for code generation internals.
Acknowledgements
Credit to @sylveon and @YexuanXiao for the trailblazing they've done in their forks, as well as their early feedback while this was in draft. Also @zadjii-msft and @Scottj1s for their earlier attempts and showing the potential build improvements.