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 |
There was a problem hiding this comment.
Pull request overview
Adds first-pass C++20 named module support to C++/WinRT so consumers can use import winrt; (with optional import std;) and improves MSBuild/NuGet integration plus CI/test coverage around module build/consume scenarios.
Changes:
- Extend the code generator and embedded base headers to support module builds (new
winrt.ixx, macro-onlybase_macros.h, and per-namespace include guarding viaWINRT_MODULE_NS_*). - Add NuGet/MSBuild targets and rules for module builder/consumer projects (
CppWinRTModuleBuild/CppWinRTModuleConsume) including cross-project IFC/OBJ resolution. - Introduce module-focused repo and NuGet test projects, CI wiring, and documentation for users and maintainers.
Reviewed changes
Copilot reviewed 100 out of 100 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/test/coro_threadpool.cpp | Switches test to import winrt; under module-test define |
| test/test_cpp23_module/test_cpp23_module.vcxproj | New C++23 consumer project wiring module IFC/OBJ + shared sources |
| test/test_cpp23_module_winrt/test_cpp23_module_winrt.vcxproj | New C++23 module builder project that generates SDK projection + compiles winrt.ixx |
| test/test_cpp20_module/test_cpp20_module.vcxproj | New C++20 module consumer unit test project |
| test/test_cpp20_module/pch.h | New PCH for module consumer tests (non-winrt headers) |
| test/test_cpp20_module/pch.cpp | PCH TU for module consumer tests |
| test/test_cpp20_module/main.cpp | Adds module-based Catch2 test runner + basic module usage tests |
| test/test_cpp20_module/interop.cpp | Adds module consumer interop tests (collections/errors/format/guid) |
| test/test_cpp20_module/component.cpp | Adds module + component header interaction test TU |
| test/test_cpp20_module/async.cpp | Adds coroutine-based module consumer tests |
| test/test_cpp20_module_winrt/test_cpp20_module_winrt.vcxproj | New C++20 module builder project generating SDK projection + compiling winrt.ixx |
| test/test_component_module/Toaster.h | New test WinRT component implementation header |
| test/test_component_module/Toaster.cpp | Component implementation compilation unit |
| test/test_component_module/test_component_module.vcxproj | New component DLL project consuming the winrt module |
| test/test_component_module/test_component_module.idl | IDL for component used to validate component authoring with modules |
| test/test_component_module/pch.h | Component PCH avoiding winrt headers in module mode |
| test/test_component_module/pch.cpp | Component PCH TU |
| test/test_component_module/module.cpp | DLL exports/activation entrypoints using module imports |
| test/test_component_module/exports.def | Exports definition for the component DLL |
| test/nuget/TestModuleSingleProject/TestModuleSingleProject.vcxproj | New NuGet test for single-project build+consume module scenario |
| test/nuget/TestModuleSingleProject/readme.txt | Explains single-project module test intent |
| test/nuget/TestModuleSingleProject/main.cpp | Single-project sample app using import winrt; |
| test/nuget/TestModuleConsumerApp/widget_test.cpp | NuGet consumer app TU validating component cross-namespace usage |
| test/nuget/TestModuleConsumerApp/TestModuleConsumerApp.vcxproj | NuGet consumer app configured for CppWinRTModuleConsume + ProjectReferences |
| test/nuget/TestModuleConsumerApp/readme.txt | Explains NuGet consumer module test intent |
| test/nuget/TestModuleConsumerApp/platform_test.cpp | NuGet consumer app TU validating platform types via module |
| test/nuget/TestModuleConsumerApp/main.cpp | NuGet consumer app entrypoint using module + component headers |
| test/nuget/TestModuleComponent/TestModuleComponentWidget.idl | Component IDL for Widgets namespace types |
| test/nuget/TestModuleComponent/TestModuleComponentWidget.h | Widgets runtimeclass implementation header |
| test/nuget/TestModuleComponent/TestModuleComponentWidget.cpp | Widgets runtimeclass implementation TU |
| test/nuget/TestModuleComponent/TestModuleComponentClass.idl | Root namespace component IDL referencing Widgets types |
| test/nuget/TestModuleComponent/TestModuleComponentClass.h | Root runtimeclass implementation header |
| test/nuget/TestModuleComponent/TestModuleComponentClass.cpp | Root runtimeclass implementation TU |
| test/nuget/TestModuleComponent/TestModuleComponent.vcxproj | NuGet component project consuming module + generating component projection |
| test/nuget/TestModuleComponent/TestModuleComponent.def | Exports definition for NuGet component DLL |
| test/nuget/TestModuleComponent/readme.txt | Explains NuGet component authoring test intent |
| test/nuget/TestModuleComponent/pch.h | Component PCH intentionally excluding winrt headers |
| test/nuget/TestModuleComponent/pch.cpp | Component PCH TU |
| test/nuget/TestModuleBuilder/TestModuleBuilder.vcxproj | NuGet module builder project (no sources; targets inject winrt.ixx) |
| test/nuget/TestModuleBuilder/readme.txt | Explains NuGet module builder test intent |
| test/nuget/TestModuleBuilder/PropertySheet.props | Placeholder props file for NuGet module builder test solution |
| test/nuget/NuGetTest.sln | Adds module builder/consumer/component/single-project tests to solution |
| strings/base_xaml_typename.h | Exports winrt::impl namespace for module visibility |
| strings/base_windows.h | Exports winrt::impl namespace for module visibility |
| strings/base_types.h | Exports winrt::impl namespace for module visibility |
| strings/base_string.h | Exports winrt::impl namespace for module visibility |
| strings/base_string_operators.h | Exports winrt::impl namespace for module visibility |
| strings/base_string_input.h | Exports winrt::impl namespace for module visibility |
| strings/base_std_includes.h | New split-out standard library include list for GMF usage |
| strings/base_std_hash.h | Exports winrt::impl namespace for module visibility |
| strings/base_security.h | Refactors access token guard to avoid module-related incomplete-type issues |
| strings/base_reference_produce.h | Exports winrt::impl namespace for module visibility |
| strings/base_natvis.h | Exports winrt::impl namespace for module visibility |
| strings/base_module_macros.h | New macro-only header content to bridge macros across module boundary |
| strings/base_meta.h | Exports winrt::impl namespace for module visibility |
| strings/base_marshaler.h | Exports winrt::impl namespace for module visibility |
| strings/base_macros.h | Moves core macro definitions out; adds modules-specific warning suppression |
| strings/base_iterator.h | Exports winrt::impl namespace for module visibility |
| strings/base_includes.h | Removes std includes from platform include set (split GMF includes) |
| strings/base_implements.h | Exports winrt::impl namespace for module visibility |
| strings/base_identity.h | Exports winrt::impl namespace for module visibility |
| strings/base_foundation.h | Exports winrt::impl namespace for module visibility |
| strings/base_extern.h | Exports selectany handler variables for correct linkage across module/header TUs |
| strings/base_events.h | Exports winrt::impl namespace for module visibility |
| strings/base_error.h | Exports winrt::impl namespace for module visibility |
| strings/base_delegate.h | Exports winrt::impl namespace for module visibility |
| strings/base_coroutine_threadpool.h | Exports winrt::impl namespace for module visibility |
| strings/base_coroutine_foundation.h | Exports winrt::impl namespace for module visibility |
| strings/base_composable.h | Exports winrt::impl namespace for module visibility |
| strings/base_com_ptr.h | Exports winrt::impl namespace for module visibility |
| strings/base_collections.h | Exports winrt::impl namespace for module visibility |
| strings/base_collections_vector.h | Exports winrt::impl namespace for module visibility |
| strings/base_collections_map.h | Exports winrt::impl namespace for module visibility |
| strings/base_collections_input_vector.h | Exports winrt::impl namespace for module visibility |
| strings/base_collections_input_vector_view.h | Exports winrt::impl namespace for module visibility |
| strings/base_collections_input_map.h | Exports winrt::impl namespace for module visibility |
| strings/base_collections_input_map_view.h | Exports winrt::impl namespace for module visibility |
| strings/base_collections_input_iterable.h | Exports winrt::impl namespace for module visibility |
| strings/base_collections_base.h | Exports winrt::impl namespace for module visibility |
| strings/base_array.h | Exports winrt::impl namespace for module visibility |
| strings/base_agile_ref.h | Exports winrt::impl namespace for module visibility |
| strings/base_activation.h | Exports winrt::impl namespace for module visibility |
| strings/base_abi.h | Exports winrt::impl namespace for module visibility |
| nuget/readme.txt | Adds pointer to modules documentation in package |
| nuget/readme.md | Documents new module build/consume properties and usage |
| nuget/Microsoft.Windows.CppWinRT.targets | Adds module build/consume targets and module output resolution |
| nuget/Microsoft.Windows.CppWinRT.nuspec | Includes docs/modules.md in NuGet package |
| nuget/CppWinrtRules.Project.xml | Adds VS property pages for module build/consume |
| natvis/pch.h | Updates natvis build to include new split include/macro fragments |
| docs/modules.md | New user-facing module guide and workflows |
| docs/modules-internals.md | New maintainer doc for codegen + macro flow and per-namespace guards |
| cppwinrt/type_writers.h | Adds guarded include emission for cross-namespace deps (WINRT_MODULE_NS_*) |
| cppwinrt/main.cpp | Generates winrt.ixx with GMF includes + module namespace macro header |
| cppwinrt/file_writers.h | Adds base_macros.h generation and module-aware namespace header behavior |
| cppwinrt/component_writers.h | Makes generated component stubs module-aware (WINRT_MODULE) |
| cppwinrt/code_writers.h | Updates version assert and exports winrt::impl in generated headers |
| cppwinrt.sln | Adds new module test projects to solution |
| .github/workflows/ci.yml | Adds module tests to CI matrix and runs NuGet module test apps |
| .github/instructions/modules.instructions.md | Adds module-specific AI assistance guidance for touched areas |
| .github/copilot-instructions.md | Adds repo-wide guidance including module architecture notes |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@DefaultRyan The two issues I mentioned in #1556 (comment) still exist, could you look into them? |
@YexuanXiao I'm still teasing out the details, but I think I see a couple different issues.
Also, that repro project is probably getting a bit out of date at this point. |
This comment was marked as outdated.
This comment was marked as outdated.
|
To achieve stable support for coexistence of |
…DULE_IMPORTED before including component headers (to skip duplicated base.h content)
I looked into the Instead, I've gone with a different approach: each generated namespace header now has a module guard that auto-imports the winrt module when So, coexistence "technically" works, by dodging the entire coexistence conundrum entirely. ;) |
|
This exposes you to arbitrary include/import order, which is a can of worms of its own. It is probably far easier to just use global linkage for things that are expected to be global between TUs (e.g. the function pointers) and let COMDAT folding deal with the rest. |
I encountered this issue myself. It seems to be a bug in MSVC. Marking all specializations as export and suppressing the warnings temporarily resolves these errors. |
|
Marking specializations as export is non-standard and becomes an error in MSVC 14.52 |
I already mentioned it's a workaround for a bug in MSVC. Of course, I know that specializations don't need to be exported, but adding export to the namespace that wraps the specialization does solve the problem. MSVC may generate some annoying warnings, but they can be suppressed. |
The state of things at commit 272113b certainly felt less brittle in that regard. The problem was that we had That's not the end of the world, as consumers opting into modules are generally going to import rather than include, but it's still not ideal, especially if somebody is trying to migrate to modules in a large project one file at a time. Even so, if that's the only way to get there, I can accept the state of things at that point. Maybe that's just a restriction of "v1 module support", and push raw include support out to v2. I'm still pretty new to using modules, personally, but I'm learning a lot these last couple weeks. The crux of the dilemma seems to be:
I'm going to tinker some more to see if I can reach a compromise by preventing non-module headers from including in-module headers, but top-level in-module headers are allowed to include other in-module headers. Either way, I value the input here... I'm fine with letting COMDAT folding merge stuff up, but that depends on getting a successful build first. I'm loathe to depend on non-standard behavior that's guaranteed to break. |
|
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. |
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. Not only does this bring in the nuts and bolts of generating a module and successfully consuming it, the NuGet package'sMicrosoft.Windows.CppWinRT.targetsfile gets improvements to help streamline developer's workflow.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.Per-namespace include guards. 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. The code generator emitswinrt/winrt_module_namespaces.halongsidewinrt.ixx, declaringWINRT_MODULE_NS_*macros for each namespace in the module. Cross-namespace#includedeps use#ifndef WINRT_MODULE_NS_<namespace>guards — only namespaces actually in the module are skipped.Auto-import via module guard. When
WINRT_MODULEis defined, each namespace header contains a "module guard" — a preprocessor block that automatically doesimport winrt;(guarded byWINRT_MODULE_IMPORTEDso it only happens once per TU), includesbase_macros.hfor macros, and loadswinrt_module_namespaces.hfor per-namespace guards. This means consumers can simply#include <winrt/Namespace.h>without needing an explicitimport winrt;— the header handles it transparently. The same headers work in both module and traditional header mode without regeneration. Generated component.g.hfiles contain no module-specific logic — they just include their namespace headers, which handle everything via their module guard.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, similar to how WinMD references are already resolved.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 (generated separately, not folded into the ixx) specializeimpltemplates likecategory<>,abi<>,guid_v<>, andname_v<>for their types. These specializations must see the primary templates, which requires them to be exported. This is not ideal —implis an implementation detail and exporting it exposes internal surface area. However, the alternative would require either folding all component projection headers into the ixx (defeating the build-time benefits for component authors) or restructuring the projection to separate the specialization-target templates from the rest ofimpl(significant churn across the entire codebase). A future update could introduce a smallerwinrt::impl::specializenamespace containing only the templates that external code needs to specialize, reducing the exported surface area without breaking the module/header dual-mode design.What's in this PR
Code generator (
cppwinrt/):.g.handmodule.g.cppfiles use#ifdef WINRT_MODULEtoimport winrt;(and optionallyimport std;) before including component namespace headerswinrt.ixxdefinesWINRT_BUILD_MODULEin its global module fragment;import std;is in the module purview (conditional onWINRT_IMPORT_STD)winrt_module_namespaces.hgenerated alongsidewinrt.ixxwith per-namespace macroswrite_module_guard()— boundary-based: checks whether the header's own namespace is in the module 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 depsNuGet targets (
nuget/):CppWinRTBuildModuletarget — addswinrt.ixxto compilation, definesWINRT_MODULECppWinRTGetModuleOutputstarget — 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 (TestModuleComponent+TestModuleComponent.Widgets), cross-namespace value type field, platformUrireturn typeTestModuleSingleProject— single project that builds and consumes its own moduleinclude_test.cpp— regression tests verifying that#include <winrt/...>works correctly whenWINRT_MODULEis defined, including namespaces that are in the module (traditional fallback path).Repo test projects (
test/):test_cpp20_moduleadded to CI test matrix (all architectures, MSVC only)Documentation (
docs/):modules.md— user-facing guide (Quick Start with IDE + XML instructions, MSBuild properties, source patterns, macros, architecture)modules-internals.md— maintainer guide (code generation pipeline, macro flow, per-namespace guards)nuget/readme.md— C++20 Modules section with quick example.github/instructions/modules.instructions.md— AI assistant instructions for module-related codeKey macros
WINRT_MODULE.g.h/.g.cpp/module.g.cppbehavior; used by module guard in namespace headers to determine include vs import pathsWINRT_BUILD_MODULEbase_macros.honlyWINRT_MODULE_NS_*!NS_dep || NS_self) to decide whether to include or skip dependenciesWINRT_IMPORT_STDimport std;in winrt.ixx (purview),.g.h, andmodule.g.cpp. Does NOT affect base.h (see design notes)Current limitations
import std;requires/std:c++lateston v143 toolset.import std;is NOT used insidebase.h. Platform headers (<intrin.h>) transitively include STL headers, making a subsequentimport std;in the same header unsafe. Instead,import std;is placed at the TU level: inwinrt.ixx(module purview),module.g.cpp, and.g.hfiles.#include "base.h"behavior, and cross-dep guards are bypassed.Future directions
CppWinRTModuleBuildto accept additional-inWinMDs beyond the platform SDK.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.