Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5f7d2f5
Initial implementation of vs modules based on cppwinrtplus fork
DefaultRyan Apr 23, 2026
469c963
Rename WINRT_MODULE -> WINRT_IMPL_BUILD_MODULE; WINRT_CONSUME_MODULE …
DefaultRyan Apr 23, 2026
84da05a
Namespace modules are winrt.Namespace. Non-namespace modules are winr…
DefaultRyan Apr 23, 2026
ab72882
Bootstrap basic unit test
DefaultRyan Apr 24, 2026
464a75c
Fix coroutine export. A few more unit tests.
DefaultRyan Apr 24, 2026
b6d6186
module_include, module_exclude
DefaultRyan Apr 24, 2026
717754a
Rename test_module to test_cpp20_module
DefaultRyan Apr 24, 2026
d9ec817
Nuget test project with module include/exclude
DefaultRyan Apr 24, 2026
ca0090a
Refine generated files in component.
DefaultRyan Apr 24, 2026
1aceeed
Added a type deriving from DependencyObject
DefaultRyan Apr 24, 2026
05a845e
Basic module build/consume support
DefaultRyan Apr 24, 2026
6fffa1d
Prefix namespace ixx files with "winrt." for consistency with module …
DefaultRyan Apr 24, 2026
b8b9715
IFC needs better per-project scoping. Now it fully works end to end.
DefaultRyan Apr 25, 2026
25ce335
Minor fix to TestModuleComponent2
DefaultRyan Apr 25, 2026
ccfee65
Documentation and polish.
DefaultRyan Apr 25, 2026
f4073d3
Fix CI failures
DefaultRyan Apr 25, 2026
d5d1821
Try for better std hygiene
DefaultRyan Apr 25, 2026
4d91f75
Address some PR feedback
DefaultRyan Apr 25, 2026
79eb574
Better automation of AdditionalBMIDirectories
DefaultRyan Apr 25, 2026
b0d4656
Missed fallback definition of WINRT_IMPL_STD_EXPORT
DefaultRyan Apr 26, 2026
392a1c3
Wrap base.h and extern the handler pointers
DefaultRyan Apr 26, 2026
9207cf9
Uniform namespace filtering
DefaultRyan Apr 27, 2026
9a98166
Write trailing comments for #endif
DefaultRyan Apr 27, 2026
c4e1b06
Clarify language version requirement
DefaultRyan Apr 27, 2026
5889c9e
Add source_location test
DefaultRyan Apr 27, 2026
2c83d42
Refactor/cleanup some string writers
DefaultRyan Apr 27, 2026
cb62bc7
Add arm64 configs and replace bogus project guids with real guids.
DefaultRyan Apr 27, 2026
054fdf5
More cleanup of strings. Collected common macros into base_macros.h, …
DefaultRyan Apr 27, 2026
9322551
More strings cleanup. Emit a canonical winrt/base_macros.h
DefaultRyan Apr 28, 2026
805c89c
More clarifications about guidance when sharing pre-built modules
DefaultRyan Apr 28, 2026
6c852cc
Ensure structured bindings are exported for IKeyValuePair
DefaultRyan May 2, 2026
aed4a2d
Fix up module namespace exclude logic so it still generates import st…
DefaultRyan May 4, 2026
a7b1413
intrin.h and <cstddef> only needed by base, not namespace modules
DefaultRyan May 5, 2026
14eb2dc
Tidy up a few codegen bits
DefaultRyan May 6, 2026
b8b0cdd
Fix sln config on test_cpp20_module project
DefaultRyan May 6, 2026
d784e0e
Namespace modules now check version against imported winrt_base
DefaultRyan May 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/instructions/cppwinrt.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# C++/WinRT Codebase — Agent Instructions

## Repository Structure

- `cppwinrt/` — The cppwinrt.exe code generator (C++ source)
- `main.cpp` — CLI parsing, namespace iteration, SCC detection, .ixx orchestration
- `file_writers.h` — All file generation functions (headers, .ixx modules, component stubs)
- `code_writers.h` — Code-level writing utilities (guards, namespace wrappers, type writers)
- `type_writers.h` — Type formatting (ABI signatures, names, GUIDs)
- `component_writers.h` — Component authoring code generation
- `helpers.h` — Metadata reading helpers
- `settings.h` — Global settings populated from CLI args
- `text_writer.h` — Core text writer infrastructure
- `strings/` — String literal `.h` files embedded by the prebuild step. Changes require: delete prebuild.exe → rebuild solution
- `nuget/` — MSBuild targets, props, and NuGet packaging
- `Microsoft.Windows.CppWinRT.targets` — Main MSBuild integration (projections, module support)
- `test/` — Test projects
- `test/test_cpp20_module/` — Standalone module test (in main solution)
- `test/nuget/` — NuGet integration tests (multi-project module chain)
- `docs/` — Documentation
- `natvis/` — Visual Studio debug visualizer (includes strings/*.h in its pch.h — add new files there too)

## Build Process

- Use VS Developer Shell for correct toolset environment
- `cmake --build build --config Release --target cppwinrt` for cppwinrt.exe (or MSBuild: `msbuild cppwinrt\cppwinrt.vcxproj /p:Configuration=Release /p:Platform=x64`)
- NuGet tests: `msbuild test\nuget\NuGetTest.sln /p:Configuration=Release /p:Platform=x64`
- Module test projects require v145 toolset (VS 2026). Directory.Build.Props sets v143 by default — override with `<PlatformToolset>v145</PlatformToolset>` in Configuration PropertyGroup

## Key Patterns

### Prebuild Embedding
The `strings/*.h` files are embedded as string literals by the prebuild step. If you modify any `strings/*.h` file, you must delete `prebuild.exe` and rebuild the entire solution for changes to take effect.

### Module Guard Macros
- `WINRT_IMPL_BUILD_MODULE` — Defined in .ixx global fragment. Makes `WINRT_EXPORT` expand to `export extern "C++"` and suppresses `#include` of dependencies
- `WINRT_IMPORT_MODULE` — Defined by consumers who import modules. Makes namespace headers no-op (types come from module import)
- `WINRT_EXPORT` — Empty in header mode, `export extern "C++"` in module mode

### Generated Header Structure
Each namespace produces four header files:
- `impl/<ns>.0.h` — Forward declarations, ABIs, GUIDs, categories
- `impl/<ns>.1.h` — Interface definitions
- `impl/<ns>.2.h` — Delegates, structs, class implementations
- `<ns>.h` — Public API surface (consume definitions, class wrappers, operators)

### Dependency Collection
When generating headers with `-modules`, writer.depends is inspected after each header to build a namespace dependency graph. This graph drives SCC detection and module import lists.

## Common Gotchas

- Module IFCs are NOT compatible across toolset versions — always clean rebuild when switching
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this reminds me - do we still have the #pragma detect mismatch protection to prevent mixing cppwinrt versions across a build? that was a snag I ran into in early experiments (due do it being macro-based). do we have a test case to validate?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

TLDR; Good point. Fix incoming.

Full version:
I had to think about this one for a sec, and I think there might be a small gap, which I'll try to test for.
But at the same time, modules offer stronger built-in protection for some cases.

To refresh my memory and review what we have, here is what I believe works right now:

  • The #pragma detect_mismatch and check_version are still present in base.h, and therefore the winrt_base module.
  • "header mode" still works the same, defends against "multiple version of base.h" as well as "base.h is a different version than this namespace header".
  • Headers and winrt_base having different versions should still hit the #pragma detect_mismatch
  • Importing multiple versions of winrt_base into a binary should still hit the #pragma detect_mismatch
  • It is probably quite hard to import multiple versions of winrt_base into a translation unit, even if they were the same CPPWINRT_VERSION, but if you manage to pull it off, I'd still expect #pragma detect_mismatch to catch it.

The gap is:

  • Namespace module imports winrt_base of a different version. (We still have a single version of winrt_base in the binary, but a namespace module of a different version is importing it)

We're probably covered on many of the "scary" cases, like silent ODR violations of base types causing undefined behavior, but not all of it. The interesting machinery types are all owned by winrt_base, and the various namespace modules mostly build on it to create the WinRT projections. ABI-breaking changes to types in base would probably cause a build error, e.g. "wrong number of parameters in winrt::some_function" or " type bar doesn't exist in namespace winrt". But an update could violate assumptions on how those building blocks are supposed to be used, and do so in a way that doesn't break the build.

When compiling the namespace modules, we're importing winrt_base instead of #including base.h. Both base.h and all the modules #include base_macros.h, but CPPWINRT_VERSION-related stuff is directly in base.h, so the namespace modules don't see it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've got a fix up now. winrt_base now exports the version string, and namespace modules check their locally-generated version against the version they import from winrt_base.

- PCH and modules can coexist but PCH must NOT contain imports from WinRT headers when using modules, and winrt imports are preferred over textual inclusion
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Given the sharp edges of combining modules with pches (which strikes me as basically an anti-pattern anyway), should we MSBuild warn a user about mixing & matching?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm not sure that we can detect an import statement from inside a PCH. If a user tries to do it, they're going to hit a compiler ICE. Hopefully, the MSVC folks are working on turning that into an actual diagnostic.

- `/ifcSearchDir` works for the module dependency scanner to find IFCs, but cross-component modules may need explicit `/reference "name=path.ifc"` flags
- `import std;` requires `BuildStlModules=true`
- Component modules (-opt) encode direct instantiation — they cannot be shared across DLL boundaries
41 changes: 41 additions & 0 deletions .github/instructions/modules.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# C++/WinRT Modules — Agent Instructions

## Module Architecture (v2 — Per-Namespace)

Each WinRT namespace gets its own C++20 named module (`winrt.<Namespace>`). Base infrastructure is in `winrt_base` and `winrt_numerics`.

### Code Generator Flow

1. `-modules` flag enables .ixx generation in cppwinrt.exe
2. `-module_include`/`-module_exclude` filter which namespaces get modules
3. Headers are generated with dependency tracking (deps_ptr parameter)
4. Tarjan's SCC algorithm detects cyclic namespace groups
5. Standalone namespaces get individual .ixx; cyclic groups get consolidated SCC owner + re-export stubs

### MSBuild Flow

1. `CppWinRTBuildModule=true` adds `-modules` to cppwinrt.exe invocations
2. `CppWinRTAddModuleInterfaces` discovers `$(GeneratedFilesDir)winrt\*.ixx` and adds to ClCompile
3. `CppWinRTConsumeModule` metadata on ProjectReference controls per-reference IFC sharing
4. `CppWinRTResolveModuleReferences` calls `CppWinRTGetModuleOutputs` on tagged references
5. Platform projection suppresses `-modules` when consuming pre-built IFCs

### Critical Invariants

- Module guards are unconditional in codegen — `-modules` only controls .ixx generation
- Component modules use `-opt` (direct instantiation) — NEVER share across projects
- Reference and platform projection modules DON'T use `-opt` (activation factory) — safe for cross-project consumption
- SCC owner is alphabetically first namespace in the cycle
- All .ixx filenames use `winrt` prefix: `winrt.Windows.Foundation.ixx`, `winrt_base.ixx`

### Testing Changes

After modifying cppwinrt.exe code:
1. Rebuild cppwinrt.exe: `msbuild cppwinrt\cppwinrt.vcxproj /p:Configuration=Release /p:Platform=x64`
2. Run standalone test: build `test_cpp20_module` in main solution
3. Run NuGet tests: `msbuild test\nuget\NuGetTest.sln /p:Configuration=Release /p:Platform=x64`

After modifying targets:
1. Clean NuGet test obj dirs
2. Build with `/v:normal` and check "Module providers:" diagnostic messages
3. Inspect `.rsp` files in `obj/` to verify correct `-modules` flag placement
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,15 @@ jobs:
$target_platform = "${{ matrix.arch }}"
& "_build\$target_platform\$target_configuration\cppwinrt.exe" -in local -out _build\$target_platform\$target_configuration -verbose

- name: Remove module test projects on v143
if: matrix.toolchain.platform_toolset == 'v143'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we detect/enforce v145 requirement? is there a graceful degradation path to v143, et al? is that not worthwhile?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

There are fundemental limitations in the v143 toolset. One particular pain point is that v143 modules don't like the auto return types. This breaks the "quality of life" improvements we made that turn missing headers into compile erros instead of link errors.

Here's a CI run when it aattempted to use v143: https://github.com/microsoft/cppwinrt/actions/runs/24921696485/job/72984385782

Even after working around that by wrapping the return type like WINRT_IMPL_AUTO(FooReturnType), there was still a bit of pain. I'm not sure how worthwhile it is to try and light it up for v143, especially when I look at the list of bugfixes that have been going into v145. (See https://devblogs.microsoft.com/cppblog/c-language-updates-in-msvc-build-tools-v14-50/#c++-modules and https://devblogs.microsoft.com/cppblog/msvc-build-tools-version-14-51-release-candidate-now-available/#tickets-fixed)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

However, rather than relying on a cryptic build error to leave a user wondering if they held it wrong, we could issue a #warning or #error if we detect trying to build a module using _MSC_VER less than 1950.

I'm leaning towards a warning like "This probably won't work on this version", rather than forbidding it. I bet winrt_base still (mostly) works on v143, as it doesn't deal with splitting decls and defs across namespace impl headers and all that. Thoughts?

run: |
# Module test projects require v145 toolset
mv test\nuget\NugetTest.sln test\nuget\NugetTest.sln.orig
Comment thread
dmachaj marked this conversation as resolved.
Get-Content test\nuget\NugetTest.sln.orig |
Where-Object { -not ($_ -match 'TestModule') } |
Set-Content test\nuget\NugetTest.sln

- name: Run nuget test
run: |
cmd /c "$env:VSDevCmd" "&" msbuild /m /clp:ForceConsoleColor "$env:msbuild_config_props" test\nuget\NugetTest.sln
Expand Down
14 changes: 14 additions & 0 deletions cppwinrt.sln
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_nocoro", "test\test_no
{D613FB39-5035-4043-91E2-BAB323908AF4} = {D613FB39-5035-4043-91E2-BAB323908AF4}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_cpp20_module", "test\test_cpp20_module\test_cpp20_module.vcxproj", "{B8E3A5CE-4E91-4F27-9B02-E0CAF7E10D72}"
ProjectSection(ProjectDependencies) = postProject
{D613FB39-5035-4043-91E2-BAB323908AF4} = {D613FB39-5035-4043-91E2-BAB323908AF4}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D15C8430-A7CD-4616-BD84-243B26A9F1C2}"
ProjectSection(SolutionItems) = preProject
build_nuget.cmd = build_nuget.cmd
Expand Down Expand Up @@ -411,6 +416,14 @@ Global
{9E392830-805A-4AAF-932D-C493143EFACA}.Release|x64.Build.0 = Release|x64
{9E392830-805A-4AAF-932D-C493143EFACA}.Release|x86.ActiveCfg = Release|Win32
{9E392830-805A-4AAF-932D-C493143EFACA}.Release|x86.Build.0 = Release|Win32
{B8E3A5CE-4E91-4F27-9B02-E0CAF7E10D72}.Debug|ARM64.ActiveCfg = Debug|x64
{B8E3A5CE-4E91-4F27-9B02-E0CAF7E10D72}.Debug|x64.ActiveCfg = Debug|x64
{B8E3A5CE-4E91-4F27-9B02-E0CAF7E10D72}.Debug|x64.Build.0 = Debug|x64
{B8E3A5CE-4E91-4F27-9B02-E0CAF7E10D72}.Debug|x86.ActiveCfg = Debug|x64
{B8E3A5CE-4E91-4F27-9B02-E0CAF7E10D72}.Release|ARM64.ActiveCfg = Release|x64
{B8E3A5CE-4E91-4F27-9B02-E0CAF7E10D72}.Release|x64.ActiveCfg = Release|x64
{B8E3A5CE-4E91-4F27-9B02-E0CAF7E10D72}.Release|x64.Build.0 = Release|x64
{B8E3A5CE-4E91-4F27-9B02-E0CAF7E10D72}.Release|x86.ActiveCfg = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -435,6 +448,7 @@ Global
{5FF6CD6C-515A-4D55-97B6-62AD9BCB77EA} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0}
{D4C8F881-84D5-4A7B-8BDE-AB4E34A05374} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0}
{9E392830-805A-4AAF-932D-C493143EFACA} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0}
{B8E3A5CE-4E91-4F27-9B02-E0CAF7E10D72} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2783B8FD-EA3B-4D6B-9F81-662D289E02AA}
Expand Down
32 changes: 23 additions & 9 deletions cppwinrt/code_writers.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,28 @@ namespace cppwinrt
}
}

static void write_endif(writer& w)
{
w.write("#endif\n");
Comment thread
DefaultRyan marked this conversation as resolved.
Outdated
}

// When modules are enabled, wraps a block of #include directives in
// #ifndef WINRT_IMPL_BUILD_MODULE ... #endif so that in module builds (where
// WINRT_IMPL_BUILD_MODULE is defined in the global module fragment), textual
// includes are suppressed — dependencies come via import instead.
[[nodiscard]] static finish_with wrap_module_aware_includes_guard(writer& w, bool modules_enabled)
{
if (modules_enabled)
{
w.write("#ifndef WINRT_IMPL_BUILD_MODULE\n");
return { w, write_endif };
}
else
{
return { w, write_nothing };
}
}

static void write_version_assert(writer& w)
{
w.write_root_include("base");
Expand All @@ -52,14 +74,6 @@ namespace cppwinrt
w.write(format);
}

static void write_endif(writer& w)
{
auto format = R"(#endif
)";

w.write(format);
}

static void write_close_file_guard(writer& w)
{
write_endif(w);
Expand Down Expand Up @@ -166,7 +180,7 @@ namespace cppwinrt

[[nodiscard]] static finish_with wrap_impl_namespace(writer& w)
{
auto format = R"(namespace winrt::impl
auto format = R"(WINRT_EXPORT namespace winrt::impl
{
)";

Expand Down
5 changes: 4 additions & 1 deletion cppwinrt/component_writers.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ namespace cppwinrt

static void write_module_g_cpp(writer& w, std::vector<TypeDef> const& classes)
{
w.write_root_include("base");
if (!settings.modules)
{
w.write_root_include("base");
}
auto format = R"(%
bool __stdcall %_can_unload_now() noexcept
{
Expand Down
Loading