Skip to content

Commit ccfee65

Browse files
DefaultRyanYexuanXiaoCopilot
committed
Documentation and polish.
Co-authored-by: YexuanXiao <bizwen@nykz.org> Co-authored-by: Copilot <copilot@github.com>
1 parent 25ce335 commit ccfee65

21 files changed

Lines changed: 573 additions & 46 deletions
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# C++/WinRT Codebase — Agent Instructions
2+
3+
## Repository Structure
4+
5+
- `cppwinrt/` — The cppwinrt.exe code generator (C++ source)
6+
- `main.cpp` — CLI parsing, namespace iteration, SCC detection, .ixx orchestration
7+
- `file_writers.h` — All file generation functions (headers, .ixx modules, component stubs)
8+
- `code_writers.h` — Code-level writing utilities (guards, namespace wrappers, type writers)
9+
- `type_writers.h` — Type formatting (ABI signatures, names, GUIDs)
10+
- `component_writers.h` — Component authoring code generation
11+
- `helpers.h` — Metadata reading helpers
12+
- `settings.h` — Global settings populated from CLI args
13+
- `text_writer.h` — Core text writer infrastructure
14+
- `strings/` — String literal `.h` files embedded by the prebuild step. Changes require: delete prebuild.exe → rebuild solution
15+
- `nuget/` — MSBuild targets, props, and NuGet packaging
16+
- `Microsoft.Windows.CppWinRT.targets` — Main MSBuild integration (projections, module support)
17+
- `test/` — Test projects
18+
- `test/test_cpp20_module/` — Standalone module test (in main solution)
19+
- `test/nuget/` — NuGet integration tests (multi-project module chain)
20+
- `docs/` — Documentation
21+
- `natvis/` — Visual Studio debug visualizer (includes strings/*.h in its pch.h — add new files there too)
22+
23+
## Build Process
24+
25+
- Use VS Developer Shell for correct toolset environment
26+
- `cmake --build build --config Release --target cppwinrt` for cppwinrt.exe (or MSBuild: `msbuild cppwinrt\cppwinrt.vcxproj /p:Configuration=Release /p:Platform=x64`)
27+
- NuGet tests: `msbuild test\nuget\NuGetTest.sln /p:Configuration=Release /p:Platform=x64`
28+
- Module test projects require v145 toolset (VS 2026). Directory.Build.Props sets v143 by default — override with `<PlatformToolset>v145</PlatformToolset>` in Configuration PropertyGroup
29+
30+
## Key Patterns
31+
32+
### Prebuild Embedding
33+
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.
34+
35+
### Module Guard Macros
36+
- `WINRT_IMPL_BUILD_MODULE` — Defined in .ixx global fragment. Makes `WINRT_EXPORT` expand to `export extern "C++"` and suppresses `#include` of dependencies
37+
- `WINRT_IMPORT_MODULE` — Defined by consumers who import modules. Makes namespace headers no-op (types come from module import)
38+
- `WINRT_EXPORT` — Empty in header mode, `export extern "C++"` in module mode
39+
40+
### Generated Header Structure
41+
Each namespace produces four header files:
42+
- `impl/<ns>.0.h` — Forward declarations, ABIs, GUIDs, categories
43+
- `impl/<ns>.1.h` — Interface definitions
44+
- `impl/<ns>.2.h` — Delegates, structs, class implementations
45+
- `<ns>.h` — Public API surface (consume definitions, class wrappers, operators)
46+
47+
### Dependency Collection
48+
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.
49+
50+
## Common Gotchas
51+
52+
- Module IFCs are NOT compatible across toolset versions — always clean rebuild when switching
53+
- 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
54+
- `/ifcSearchDir` works for the module dependency scanner to find IFCs, but cross-component modules may need explicit `/reference "name=path.ifc"` flags
55+
- `import std;` requires `BuildStlModules=true`
56+
- Component modules (-opt) encode direct instantiation — they cannot be shared across DLL boundaries
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# C++/WinRT Modules — Agent Instructions
2+
3+
## Module Architecture (v2 — Per-Namespace)
4+
5+
Each WinRT namespace gets its own C++20 named module (`winrt.<Namespace>`). Base infrastructure is in `winrt_base` and `winrt_numerics`.
6+
7+
### Code Generator Flow
8+
9+
1. `-modules` flag enables .ixx generation in cppwinrt.exe
10+
2. `-module_include`/`-module_exclude` filter which namespaces get modules
11+
3. Headers are generated with dependency tracking (deps_ptr parameter)
12+
4. Tarjan's SCC algorithm detects cyclic namespace groups
13+
5. Standalone namespaces get individual .ixx; cyclic groups get consolidated SCC owner + re-export stubs
14+
15+
### MSBuild Flow
16+
17+
1. `CppWinRTBuildModule=true` adds `-modules` to cppwinrt.exe invocations
18+
2. `CppWinRTAddModuleInterfaces` discovers `$(GeneratedFilesDir)winrt\*.ixx` and adds to ClCompile
19+
3. `CppWinRTConsumeModule` metadata on ProjectReference controls per-reference IFC sharing
20+
4. `CppWinRTResolveModuleReferences` calls `CppWinRTGetModuleOutputs` on tagged references
21+
5. Platform projection suppresses `-modules` when consuming pre-built IFCs
22+
23+
### Critical Invariants
24+
25+
- Module guards are unconditional in codegen — `-modules` only controls .ixx generation
26+
- Component modules use `-opt` (direct instantiation) — NEVER share across projects
27+
- Reference and platform projection modules DON'T use `-opt` (activation factory) — safe for cross-project consumption
28+
- SCC owner is alphabetically first namespace in the cycle
29+
- All .ixx filenames use `winrt` prefix: `winrt.Windows.Foundation.ixx`, `winrt_base.ixx`
30+
31+
### Testing Changes
32+
33+
After modifying cppwinrt.exe code:
34+
1. Rebuild cppwinrt.exe: `msbuild cppwinrt\cppwinrt.vcxproj /p:Configuration=Release /p:Platform=x64`
35+
2. Run standalone test: build `test_cpp20_module` in main solution
36+
3. Run NuGet tests: `msbuild test\nuget\NuGetTest.sln /p:Configuration=Release /p:Platform=x64`
37+
38+
After modifying targets:
39+
1. Clean NuGet test obj dirs
40+
2. Build with `/v:normal` and check "Module providers:" diagnostic messages
41+
3. Inspect `.rsp` files in `obj/` to verify correct `-modules` flag placement

cppwinrt/component_writers.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,10 @@ namespace cppwinrt
136136

137137
static void write_module_g_cpp(writer& w, std::vector<TypeDef> const& classes)
138138
{
139-
w.write_root_include("base");
139+
if (!settings.modules)
140+
{
141+
w.write_root_include("base");
142+
}
140143
auto format = R"(%
141144
bool __stdcall %_can_unload_now() noexcept
142145
{

cppwinrt/file_writers.h

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,26 @@ namespace cppwinrt
262262
writer w;
263263
write_preamble(w);
264264
write_pch(w);
265+
266+
if (settings.modules)
267+
{
268+
// In module builds, import winrt_base instead of #include "winrt/base.h".
269+
// The component's namespace module(s) provide type visibility.
270+
w.write("\nimport winrt_base;\n");
271+
272+
// Collect all unique namespaces from the component classes
273+
std::set<std::string> namespaces;
274+
for (auto&& type : classes)
275+
{
276+
namespaces.insert(std::string(type.TypeNamespace()));
277+
}
278+
for (auto&& ns : namespaces)
279+
{
280+
w.write("import winrt.%;\n", ns);
281+
}
282+
w.write("\n");
283+
}
284+
265285
write_module_g_cpp(w, classes);
266286
w.flush_to_file(settings.output_folder + "module.g.cpp");
267287
}
@@ -353,15 +373,15 @@ namespace cppwinrt
353373
writer w;
354374
write_pch(w);
355375

356-
// The .g.h handles its own imports, but the implementation .h
357-
// needs the types available in scope, so we import them here.
376+
if (settings.modules)
358377
{
378+
// The .g.h handles its own imports, but the implementation .h
379+
// needs the types available in scope, so we import them here.
359380
writer dep_scanner;
360381
dep_scanner.add_depends(type);
361382
write_component_g_h(dep_scanner, type);
362383

363384
w.write("\n#define WINRT_IMPORT_MODULE\n");
364-
w.write("import winrt_base;\n");
365385
for (auto&& depends : dep_scanner.depends)
366386
{
367387
w.write("import winrt.%;\n", depends.first);
@@ -375,6 +395,11 @@ namespace cppwinrt
375395

376396
// --- Per-namespace C++20 module interface unit (.ixx) writers ---
377397

398+
// Emits the common global module fragment used by all generated .ixx files.
399+
// Defines WINRT_IMPL_BUILD_MODULE so generated headers switch WINRT_EXPORT
400+
// to 'export extern "C++"' and suppress textual #includes of dependencies
401+
// (dependencies arrive via module imports instead).
402+
// Includes minimal headers needed for macros, intrinsics, and debug assertions.
378403
static void write_module_preamble(writer& w)
379404
{
380405
write_preamble(w);
@@ -391,6 +416,15 @@ namespace cppwinrt
391416
w.write(format);
392417
}
393418

419+
// Emits $(out)/winrt/module.h
420+
// This header provides macros that are needed inside module interface units
421+
// but cannot cross module boundaries via 'import'. Each .ixx file includes
422+
// this in its global module fragment. It provides:
423+
// - WINRT_ASSERT/WINRT_VERIFY macros
424+
// - WINRT_IMPL_COROUTINES detection
425+
// - WINRT_IMPL_SHIM macro
426+
// - WINRT_EXPORT (switches between empty and 'export extern "C++"')
427+
// - MSVC warning suppressions for module-related diagnostics
394428
static void write_module_h()
395429
{
396430
writer w;
@@ -572,6 +606,17 @@ export module winrt_numerics;
572606
w.flush_to_file(settings.output_folder + "winrt/winrt_numerics.ixx");
573607
}
574608

609+
// Emits a per-namespace module interface unit for namespaces that are NOT
610+
// part of a dependency cycle (standalone module).
611+
// Output: $(out)/winrt/winrt.<ns>.ixx (export module winrt.<ns>;)
612+
//
613+
// The generated .ixx:
614+
// 1. Starts with the global module fragment (WINRT_IMPL_BUILD_MODULE, minimal includes)
615+
// 2. Declares 'export module winrt.<ns>;'
616+
// 3. Imports std and re-exports winrt_base
617+
// 4. Imports each dependent namespace module (computed from type references in headers)
618+
// 5. Includes the impl headers (*.0.h, *.1.h, *.2.h) and public header (<ns>.h)
619+
// in the module purview, where WINRT_EXPORT causes declarations to be exported
575620
static void write_namespace_ixx(
576621
std::string_view const& ns,
577622
std::set<std::string> const& deps,
@@ -583,6 +628,26 @@ export module winrt_numerics;
583628
// Module declaration
584629
w.write("export module winrt.%;\n\n", ns);
585630

631+
// Document dependencies
632+
w.write("// Module dependencies:\n");
633+
w.write("// - std\n");
634+
w.write("// - winrt_base (re-exported)\n");
635+
if (deps.empty())
636+
{
637+
w.write("// - (no additional namespace imports)\n");
638+
}
639+
else
640+
{
641+
for (auto& dep : deps)
642+
{
643+
if (module_namespaces.count(dep) || module_namespaces.empty())
644+
{
645+
w.write("// - winrt.%\n", dep);
646+
}
647+
}
648+
}
649+
w.write("\n");
650+
586651
// Import std and base
587652
w.write("import std;\n");
588653
w.write("export import winrt_base;\n");
@@ -607,6 +672,22 @@ export module winrt_numerics;
607672
w.flush_to_file(settings.output_folder + "winrt/winrt." + std::string(ns) + ".ixx");
608673
}
609674

675+
// Emits the SCC (Strongly Connected Component) owner module interface unit.
676+
// When multiple namespaces form a dependency cycle, they cannot each have their
677+
// own independent module (circular imports are illegal in C++20 modules).
678+
// Instead, one namespace is chosen as the "owner" (alphabetically first in the SCC),
679+
// and ALL cyclic namespaces' declarations are consolidated into this single module.
680+
// The other namespaces in the SCC get thin re-export stubs (see write_namespace_reexport_ixx).
681+
//
682+
// Output: $(out)/winrt/winrt.<owner>.ixx (export module winrt.<owner>;)
683+
//
684+
// The owner module:
685+
// 1. Imports external dependencies (deps outside the SCC)
686+
// 2. Forward-declares all projected types for ALL SCC namespaces before any
687+
// impl headers — this breaks the type reference cycles
688+
// 3. Includes impl headers in stable phase order: all *.0.h, then all *.1.h,
689+
// then all *.2.h, then all public headers — preserving the original header
690+
// layering while keeping SCC compilation deterministic
610691
static void write_namespace_scc_owner_ixx(
611692
cache const& c,
612693
std::string_view const& owner,
@@ -618,6 +699,13 @@ export module winrt_numerics;
618699
write_module_preamble(w);
619700

620701
// Module declaration (owner namespace)
702+
w.write("// This module is an SCC owner (cycle breaker). The following namespaces\n");
703+
w.write("// form a dependency cycle and are consolidated into this single module:\n");
704+
for (auto& ns : scc_members)
705+
{
706+
w.write("// - %\n", ns);
707+
}
708+
w.write("// Other SCC namespaces are emitted as re-export stubs.\n\n");
621709
w.write("export module winrt.%;\n\n", owner);
622710

623711
// Import std and base
@@ -635,7 +723,10 @@ export module winrt_numerics;
635723

636724
w.write("\n");
637725

638-
// Forward declarations for all SCC members (exported at module purview scope)
726+
// Forward declarations for all projected types in this SCC.
727+
// This is required because SCC members have cyclic type references,
728+
// and generated headers suppress dependent #includes when WINRT_IMPL_BUILD_MODULE
729+
// is defined. Forward declarations provide the names needed before definitions.
639730
for (auto& ns : scc_members)
640731
{
641732
auto found = c.namespaces().find(ns);
@@ -654,7 +745,10 @@ export module winrt_numerics;
654745
w.write_each<write_forward>(members.contracts);
655746
}
656747

657-
// Include all SCC members' headers in phase order
748+
// Include all SCC members' headers in stable phase order.
749+
// All *.0.h (forward decls + ABIs), then all *.1.h (interfaces),
750+
// then all *.2.h (delegates/structs/classes), then all public headers.
751+
// This preserves the original header layering while keeping compilation deterministic.
658752
for (auto& ns : scc_members)
659753
{
660754
w.write("#include \"winrt/impl/%.0.h\"\n", ns);
@@ -675,6 +769,10 @@ export module winrt_numerics;
675769
w.flush_to_file(settings.output_folder + "winrt/winrt." + std::string(owner) + ".ixx");
676770
}
677771

772+
// Emits a thin re-export stub module for SCC non-owner namespaces.
773+
// This allows 'import winrt.<ns>;' to work even though the actual declarations
774+
// live in the SCC owner module. The stub simply re-exports the owner.
775+
// Output: $(out)/winrt/winrt.<ns>.ixx (export module winrt.<ns>; export import winrt.<owner>;)
678776
static void write_namespace_reexport_ixx(
679777
std::string_view const& ns,
680778
std::string_view const& owner)

cppwinrt/main.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,15 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder
525525

526526
group.get();
527527

528-
// Generate per-namespace module interface files (v2)
528+
// Generate per-namespace module interface files (.ixx)
529+
//
530+
// Each projected namespace gets its own C++20 named module (winrt.<Namespace>).
531+
// Namespaces that form dependency cycles are detected using Tarjan's SCC algorithm
532+
// and consolidated: one namespace "owns" the SCC module (containing all declarations),
533+
// while others get thin re-export stubs so 'import winrt.<ns>;' always works.
534+
//
535+
// Base infrastructure modules (winrt_base, winrt_numerics) are only generated
536+
// for platform projection builds (-base flag).
529537
if (settings.modules)
530538
{
531539
if (settings.base)

0 commit comments

Comments
 (0)