Skip to content

C++20 module support v1 (import winrt;)#1556

Open
DefaultRyan wants to merge 43 commits intomasterfrom
feature/modules_v1
Open

C++20 module support v1 (import winrt;)#1556
DefaultRyan wants to merge 43 commits intomasterfrom
feature/modules_v1

Conversation

@DefaultRyan
Copy link
Copy Markdown
Member

@DefaultRyan DefaultRyan commented Mar 30, 2026

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 #include directives 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's Microsoft.Windows.CppWinRT.targets file gets improvements to help streamline developer's workflow.

Quick overview

Two new MSBuild properties control module support:

  • CppWinRTModuleBuild — Generates the platform SDK projection and compiles winrt.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 a ProjectReference to a builder. The NuGet targets automatically resolve the IFC, OBJ, and include paths.
import winrt;
using namespace winrt::Windows::Foundation;

// Component types from a project reference (not in the module)
#include <winrt/MyComponent.h>

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_MODULE guards emitted by the code generator. The NuGet targets define WINRT_MODULE as 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 #include reference/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 emits winrt/winrt_module_namespaces.h alongside winrt.ixx, declaring WINRT_MODULE_NS_* macros for each namespace in the module. Cross-namespace #include deps use #ifndef WINRT_MODULE_NS_<namespace> guards — only namespaces actually in the module are skipped.

Auto-import via module guard. When WINRT_MODULE is defined, each namespace header contains a "module guard" — a preprocessor block that automatically does import winrt; (guarded by WINRT_MODULE_IMPORTED so it only happens once per TU), includes base_macros.h for macros, and loads winrt_module_namespaces.h for per-namespace guards. This means consumers can simply #include <winrt/Namespace.h> without needing an explicit import winrt; — the header handles it transparently. The same headers work in both module and traditional header mode without regeneration. Generated component .g.h files contain no module-specific logic — they just include their namespace headers, which handle everything via their module guard.

Builder/consumer split. CppWinRTModuleBuild and CppWinRTModuleConsume are separate properties because in multi-project solutions, only one project should compile the expensive winrt.ixx. The CppWinRTGetModuleOutputs / CppWinRTResolveModuleReferences targets handle cross-project IFC/OBJ resolution via MSBuild's ProjectReference infrastructure, 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 existing BuildStlModules property. On v143 (VS 2022), import std; requires /std:c++latest; on v145 (VS 2026), /std:c++20 suffices.

Exported winrt::impl namespace. The winrt::impl namespace is exported from the module alongside winrt. This is necessary because component projection headers (generated separately, not folded into the ixx) specialize impl templates like category<>, abi<>, guid_v<>, and name_v<> for their types. These specializations must see the primary templates, which requires them to be exported. This is not ideal — impl is 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 of impl (significant churn across the entire codebase). A future update could introduce a smaller winrt::impl::specialize namespace 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/):

  • Generated component .g.h and module.g.cpp files use #ifdef WINRT_MODULE to import winrt; (and optionally import std;) before including component namespace headers
  • winrt.ixx defines WINRT_BUILD_MODULE in its global module fragment; import std; is in the module purview (conditional on WINRT_IMPORT_STD)
  • winrt_module_namespaces.h generated alongside winrt.ixx with per-namespace macros
  • write_module_guard() — boundary-based: checks whether the header's own namespace is in the module to decide between base_macros.h (import path) and base.h (traditional path)
  • write_root_include_guarded() — compound guard: !defined(NS_dep) || defined(NS_self) for cross-namespace deps
  • Generated headers include inline comments explaining the module guard and cross-namespace guards

NuGet targets (nuget/):

  • CppWinRTBuildModule target — adds winrt.ixx to compilation, defines WINRT_MODULE
  • CppWinRTGetModuleOutputs target — exports IFC/OBJ/GeneratedFilesDir for consumers
  • CppWinRTResolveModuleReferences target — resolves module from ProjectReference items
  • CppWinRTModuleBuild / CppWinRTModuleConsume properties in CppWinrtRules.Project.xml

NuGet test projects (test/nuget/):

  • TestModuleBuilder — static lib, builds the module
  • TestModuleConsumerApp — 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, platform Uri return type
  • TestModuleSingleProject — single project that builds and consumes its own module
  • All module test projects include include_test.cpp — regression tests verifying that #include <winrt/...> works correctly when WINRT_MODULE is defined, including namespaces that are in the module (traditional fallback path).

Repo test projects (test/):

  • test_cpp20_module added to CI test matrix (all architectures, MSVC only)
  • NuGet test consumer apps run as CI validation steps

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 code

Key macros

Macro Scope Purpose
WINRT_MODULE Project-wide Controls .g.h/.g.cpp/module.g.cpp behavior; used by module guard in namespace headers to determine include vs import paths
WINRT_BUILD_MODULE winrt.ixx only Set in ixx global module fragment; tells namespace headers to use base_macros.h only
WINRT_MODULE_NS_* winrt_module_namespaces.h Per-namespace macros. Module guard and cross-dep guards use compound conditions (!NS_dep || NS_self) to decide whether to include or skip dependencies
WINRT_IMPORT_STD Project-wide (optional) Enables import std; in winrt.ixx (purview), .g.h, and module.g.cpp. Does NOT affect base.h (see design notes)

Current limitations

  • The winrt module contains only platform SDK namespaces. Reference projection namespaces (from NuGet WinMD packages or project references) are included as textual headers alongside the module.
  • import std; requires /std:c++latest on v143 toolset.
  • import std; is NOT used inside base.h. Platform headers (<intrin.h>) transitively include STL headers, making a subsequent import std; in the same header unsafe. Instead, import std; is placed at the TU level: in winrt.ixx (module purview), module.g.cpp, and .g.h files.
  • Including a namespace header that IS in the module works — the module guard falls through to traditional #include "base.h" behavior, and cross-dep guards are bypassed.
  • Generated headers include inline comments explaining the module guard and cross-namespace guards.

Future directions

  • Investigate whether large WinMD references (e.g., WinUI) could optionally be folded into the module by extending CppWinRTModuleBuild to accept additional -in WinMDs beyond the platform SDK.
  • Evaluate build time impact across real-world solutions.

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.

Copy link
Copy Markdown
Contributor

@sylveon sylveon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread cppwinrt/file_writers.h Outdated
Comment thread docs/modules-internals.md Outdated
Comment thread cppwinrt/file_writers.h Outdated
Comment thread cppwinrt/main.cpp
Comment thread strings/base_extern.h Outdated
Comment thread strings/base_module_macros.h
Comment thread strings/base_module_macros.h
@YexuanXiao

This comment has been minimized.

@DefaultRyan
Copy link
Copy Markdown
Member Author

I tested it locally. After enabling CppWinRTModule, compiling winrt.ixx.obj failed, showing that the symbol for CoGetCallContext could not be found. Manually linking Ole32.lib resolved the issue, but the header mode does not have this problem. I'm not sure what the cause is. Additionally, if both CppWinRTModule and BuildStlModule are enabled, it will break code that uses only header files.

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 winrt::access_token.

@YexuanXiao

This comment has been minimized.

@sylveon
Copy link
Copy Markdown
Contributor

sylveon commented Apr 1, 2026

Idk about exporting the impl namespace, feel like we should find a better solution.

@YexuanXiao
Copy link
Copy Markdown
Contributor

YexuanXiao commented Apr 1, 2026

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.

@sylveon
Copy link
Copy Markdown
Contributor

sylveon commented Apr 1, 2026

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.

@YexuanXiao
Copy link
Copy Markdown
Contributor

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 [[msvc::internal]] to prevent IntelliSense from showing it.

@DefaultRyan DefaultRyan changed the title Module support v1 C++20 module support v1 (import winrt;) Apr 4, 2026
@DefaultRyan DefaultRyan marked this pull request as ready for review April 4, 2026 03:01
@DefaultRyan DefaultRyan requested review from Scottj1s and Copilot April 4, 2026 04:04
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-only base_macros.h, and per-namespace include guarding via WINRT_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.

Comment on lines +8 to +13
bool __stdcall test_module_can_unload_now() noexcept;
void* __stdcall test_module_get_activation_factory(std::wstring_view const& name);

std::int32_t __stdcall DllCanUnloadNow() noexcept
{
return test_module_can_unload_now() ? 0 : 1;
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DllCanUnloadNow/DllGetActivationFactory are declared as std::int32_t but this TU doesn’t include <cstdint> (and it conditionally avoids import std;). import winrt; won’t make standard library typedefs visible, so this may fail to compile depending on transitive includes. Include <cstdint> before the imports, or use a Windows type like HRESULT/long consistently here.

Copilot uses AI. Check for mistakes.
Comment on lines +122 to +130
// ---- Format (C++20) ----

#ifdef __cpp_lib_format
TEST_CASE("module_format")
{
hstring str = L"World";
auto result = std::format(L"Hello {}", str);
REQUIRE(result == L"Hello World");
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::format is used here but this TU neither import std; nor includes <format>. The __cpp_lib_format feature-test macro can be defined via other standard headers (e.g., MSVC’s <yvals_core.h>), so this block can compile in and then fail because std::format isn’t declared. Add #include <format> (or import std;) in the same conditional block before using std::format.

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci.yml
Comment on lines +370 to +392
- name: Run module consumer app
if: matrix.arch == 'x64'
run: |
$target_configuration = "${{ matrix.config }}"
$platform_dir = "x64"
$app = "test\nuget\bin\$target_configuration\$platform_dir\TestModuleConsumerApp\TestModuleConsumerApp.exe"
if (Test-Path $app) {
& $app
} else {
echo "::warning::TestModuleConsumerApp not found at $app"
}

- name: Run module single-project app
if: matrix.arch == 'x64'
run: |
$target_configuration = "${{ matrix.config }}"
$platform_dir = "x64"
$app = "test\nuget\bin\$target_configuration\$platform_dir\TestModuleSingleProject\TestModuleSingleProject.exe"
if (Test-Path $app) {
& $app
} else {
echo "::warning::TestModuleSingleProject not found at $app"
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These run steps only emit a GitHub Actions warning when the built EXE isn’t found, which can let the workflow pass even if the output path changes or the build didn’t actually produce the executable. Consider failing the step (e.g., throw/exit 1) when the expected binary is missing so the run validation is enforced.

Copilot uses AI. Check for mistakes.
@YexuanXiao
Copy link
Copy Markdown
Contributor

@DefaultRyan The two issues I mentioned in #1556 (comment) still exist, could you look into them?

@DefaultRyan
Copy link
Copy Markdown
Member Author

DefaultRyan commented Apr 7, 2026

@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.

  • The modules workaround for winrt::access_token has caused the client() method to more eagerly instantiate its code, resulting in a new dependency on CoGetCallContext. That method is not in WindowsApp.lib, but I didn't hit this in test code because it also brings in onecore.lib. I'll see if I can find a compromise that keeps modules happy but doesn't drag in that dependency until actually called. Worst-case, I'll update the NuGet to add onecore.lib to the CppWinRTLibs next to WindowsApp.lib until a better long-term solution presents itself.
  • I'm still tracking down exactly what is happening if BuildStlModules is set along with enabling winrt module support, and then we include the winrt headers instead of importing them. I do know that we hit some difficulties if you import winrt and also include the headers that are in the module. I've smoothed over some of it, but part of that also included guards to avoid including headers if their contents are already in the winrt module. I'm considering adding a warning message if a header is included along the lines of "This namespace header is already in the module. Did you mean import winrt; instead?".

Also, that repro project is probably getting a bit out of date at this point. CppWinRTModule has been replaced by CppWinRTModuleBuild (and CppWinRTModuleConsume for projects that want to consume a shared module built by another project). Not to mention the many changes I've made to the codegen since then.

@YexuanXiao

This comment was marked as outdated.

@YexuanXiao
Copy link
Copy Markdown
Contributor

YexuanXiao commented Apr 7, 2026

To achieve stable support for coexistence of #include and import, it may be necessary to change #define WINRT_EXPORT export to #define WINRT_EXPORT export extern "C++" (also need to handle windowsnumerics.impl.h), just like the STL does.

…DULE_IMPORTED before including component headers (to skip duplicated base.h content)
@DefaultRyan
Copy link
Copy Markdown
Member Author

To achieve stable support for coexistence of #include and import, it may be necessary to change #define WINRT_EXPORT export to #define WINRT_EXPORT export extern "C++" (also need to handle windowsnumerics.impl.h), just like the STL does.

To achieve stable support for coexistence of #include and import, it may be necessary to change #define WINRT_EXPORT export to #define WINRT_EXPORT export extern "C++" (also need to handle windowsnumerics.impl.h), just like the STL does.

I looked into the extern "C++" approach, and didn't end up with anything that was sufficient on its own. I kept hitting errors with MSVC encountering duplicate enum definitions and template specializations (e.g. name_v) between the import and the include. I don't know enough about C++20 module corner cases to know if this is a fundamental issue, or an MSVC shortcoming. After this PR is complete, I'll see if I can boil down a simplified repro case to give to Visual Studio.

Instead, I've gone with a different approach: each generated namespace header now has a module guard that auto-imports the winrt module when WINRT_MODULE is defined. This is guarded by WINRT_MODULE_IMPORTED so we don't spam a ton of duplicate imports per translation unit. After the auto-import, we use the WINRT_MODULE_NS_* namespace guards to skip the entirety of the header body if this namespace is already in the module.

So, coexistence "technically" works, by dodging the entire coexistence conundrum entirely. ;)

@sylveon
Copy link
Copy Markdown
Contributor

sylveon commented Apr 18, 2026

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.

@YexuanXiao
Copy link
Copy Markdown
Contributor

I kept hitting errors with MSVC encountering duplicate enum definitions and template specializations (e.g. name_v) between the import and the include.

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.

@sylveon
Copy link
Copy Markdown
Contributor

sylveon commented Apr 18, 2026

Marking specializations as export is non-standard and becomes an error in MSVC 14.52

@YexuanXiao
Copy link
Copy Markdown
Contributor

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.

@DefaultRyan
Copy link
Copy Markdown
Member Author

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 module linkage for things that are expected to be global between TUs (e.g. the function pointers) and let COMDAT folding deal with the rest.

The state of things at commit 272113b certainly felt less brittle in that regard. The problem was that we had WINRT_MODULE guards to avoid re-including module headers from non-module headers (e.g. component-generated headers attempting to re-include stuff that's already in the module, like base.h). Unfortunately, since we can't detect at preprocessor time whether a module has been imported, those guards caused the include chain to dead-end, so going #include <winrt/Windows.Foundation.h> in the raw would break when WINRT_MODULE was defined at the project level, because it wouldn't pick up the .2.h or base.h headers.

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 want to avoid the build-time cost of including headers from the platform projection (Windows.* namespace) repeatedly, by putting those into a module that can be shared. Even if including that header was benign, I still don't want to pay the cost of processing it, because it can be significant.
  • Additionally, AFAIK, not even the std module was able to solve the "import-then-include" problem using extern "C++" - that only helped with "include-then-import". So anything that re-includes base.h after import winrt is in trouble.
  • I also want to avoid churning the platform projection module by inserting component namespaces into it. So, component authoring will be including winrt headers that have dependencies on platform/base stuff. See above, that we want to import winrt and not include base.h. Already, we're having a little exposure to ordering - who is responsible for importing winrt so that component headers can resolve the needed types - Component.cpp? component.g.h? winrt/Component.h? I went with component.g.h originally, and put in guards to not include base.h.
  • But now, those same guards prevent directly including base.h.

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.

@DefaultRyan
Copy link
Copy Markdown
Member Author

OK, that compromise approach has worked! Update incoming...

  • No more auto-import winrt; from namespace headers. We still write import winrt; for you in module.g.cpp and component.g.h.
  • No more import std; from base.h. That now only happens in places where the TU is more controlled: the module purview in winrt.ixx, module.g.cpp, and component.g.h
  • Top-level winrt headers that are present in the module, plus base.h can be included, even though it's preferred to import them from winrt. If you try to manually include them in a TU that has already imported the module, you'll almost certainly hit errors.
  • Top-level winrt headers from outside the module (e.g. component projection) assume you've already imported the module if WINRT_MODULE is defined, and will not re-include stuff that's already in winrt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants