From 689582d660dd692998dc360ea403ac3c71c40333 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Thu, 26 Mar 2026 14:24:17 -0700 Subject: [PATCH 01/34] Initial rough crack at monolithic module consumption --- cppwinrt.sln | 23 +++- nuget/CppWinrtRules.Project.xml | 5 + nuget/Microsoft.Windows.CppWinRT.targets | 19 +++ strings/base_macros.h | 2 + test/test_cpp20_module/async.cpp | 39 ++++++ test/test_cpp20_module/main.cpp | 54 ++++++++ .../test_cpp20_module.vcxproj | 129 ++++++++++++++++++ 7 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 test/test_cpp20_module/async.cpp create mode 100644 test/test_cpp20_module/main.cpp create mode 100644 test/test_cpp20_module/test_cpp20_module.vcxproj diff --git a/cppwinrt.sln b/cppwinrt.sln index 3bcfb33bc..93cb5612b 100644 --- a/cppwinrt.sln +++ b/cppwinrt.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.6.33829.357 +# Visual Studio Version 18 +VisualStudioVersion = 18.6.11620.209 main MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cppwinrt", "cppwinrt\cppwinrt.vcxproj", "{D613FB39-5035-4043-91E2-BAB323908AF4}" ProjectSection(ProjectDependencies) = postProject @@ -124,6 +124,12 @@ 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", "{B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}" + ProjectSection(ProjectDependencies) = postProject + {A91B8BF3-28E4-4D9E-8DBA-64B70E4F0270} = {A91B8BF3-28E4-4D9E-8DBA-64B70E4F0270} + {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 @@ -411,6 +417,18 @@ 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 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|ARM64.Build.0 = Debug|ARM64 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|x64.ActiveCfg = Debug|x64 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|x64.Build.0 = Debug|x64 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|x86.ActiveCfg = Debug|Win32 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|x86.Build.0 = Debug|Win32 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|ARM64.ActiveCfg = Release|ARM64 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|ARM64.Build.0 = Release|ARM64 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|x64.ActiveCfg = Release|x64 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|x64.Build.0 = Release|x64 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|x86.ActiveCfg = Release|Win32 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -435,6 +453,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} + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2783B8FD-EA3B-4D6B-9F81-662D289E02AA} diff --git a/nuget/CppWinrtRules.Project.xml b/nuget/CppWinrtRules.Project.xml index 73f32c6d3..5f4b7501c 100644 --- a/nuget/CppWinrtRules.Project.xml +++ b/nuget/CppWinrtRules.Project.xml @@ -86,4 +86,9 @@ Description="Enables the /await:strict compiler option" Category="General" /> + + diff --git a/nuget/Microsoft.Windows.CppWinRT.targets b/nuget/Microsoft.Windows.CppWinRT.targets index 188e56835..7bf5044d3 100644 --- a/nuget/Microsoft.Windows.CppWinRT.targets +++ b/nuget/Microsoft.Windows.CppWinRT.targets @@ -878,6 +878,25 @@ $(XamlMetaDataProviderPch) + + + + + + NotUsing + WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + + + + diff --git a/strings/base_macros.h b/strings/base_macros.h index 3dc01fa2d..91bd0a3a1 100644 --- a/strings/base_macros.h +++ b/strings/base_macros.h @@ -35,6 +35,8 @@ #define _WINDOWS_NUMERICS_NAMESPACE_ winrt::Windows::Foundation::Numerics #define _WINDOWS_NUMERICS_BEGIN_NAMESPACE_ WINRT_EXPORT namespace winrt::Windows::Foundation::Numerics #define _WINDOWS_NUMERICS_END_NAMESPACE_ +// The include in the purview of a module is intentional: we need to export the numerics types. +#pragma warning(suppress: 5244) #include #undef _WINDOWS_NUMERICS_NAMESPACE_ #undef _WINDOWS_NUMERICS_BEGIN_NAMESPACE_ diff --git a/test/test_cpp20_module/async.cpp b/test/test_cpp20_module/async.cpp new file mode 100644 index 000000000..fa1503b94 --- /dev/null +++ b/test/test_cpp20_module/async.cpp @@ -0,0 +1,39 @@ +// catch.hpp must come before 'import winrt' to avoid redefinition of std types. +// The catch header pulls in standard library headers textually; placing it first +// ensures those headers are already included before the module import. +#include "catch.hpp" +#include + +import winrt; + +using namespace winrt; +using namespace winrt::Windows::Foundation; + +// Verify coroutines work through the module by exercising IAsyncOperation +static IAsyncOperation AddAsync(int a, int b) +{ + co_return a + b; +} + +static IAsyncAction DoNothingAsync() +{ + co_return; +} + +TEST_CASE("module_async_operation") +{ + auto result = AddAsync(3, 4).get(); + REQUIRE(result == 7); +} + +TEST_CASE("module_async_action") +{ + DoNothingAsync().get(); +} + +TEST_CASE("module_async_await") +{ + auto op = AddAsync(10, 20); + auto result = op.get(); + REQUIRE(result == 30); +} diff --git a/test/test_cpp20_module/main.cpp b/test/test_cpp20_module/main.cpp new file mode 100644 index 000000000..6a44a8bab --- /dev/null +++ b/test/test_cpp20_module/main.cpp @@ -0,0 +1,54 @@ +#include +#include + +#define CATCH_CONFIG_RUNNER +#define CATCH_CONFIG_WINDOWS_SEH + +#include "catch.hpp" + +import winrt; + +using namespace winrt; +using namespace winrt::Windows::Foundation; + +int main(int const argc, char** argv) +{ + init_apartment(); + std::set_terminate([] { reportFatal("Abnormal termination"); std::abort(); }); + _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE); + (void)_CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR); + _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE); + (void)_CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR); + return Catch::Session().run(argc, argv); +} + +CATCH_TRANSLATE_EXCEPTION(hresult_error const& e) +{ + return to_string(e.message()); +} + +TEST_CASE("module_hstring") +{ + hstring str = L"Hello, C++/WinRT Modules!"; + REQUIRE(str == L"Hello, C++/WinRT Modules!"); + REQUIRE(str.size() == 25); + + std::wstring_view view = str; + REQUIRE(view.length() == 25); +} + +TEST_CASE("module_uri") +{ + Uri uri(L"https://example.com/path?query=1"); + REQUIRE(uri.Domain() == L"example.com"); + REQUIRE(uri.Path() == L"/path"); + REQUIRE(uri.SchemeName() == L"https"); +} + +TEST_CASE("module_collections") +{ + Collections::PropertySet props; + props.Insert(L"key", box_value(L"value")); + REQUIRE(unbox_value(props.Lookup(L"key")) == L"value"); + REQUIRE(props.Size() == 1); +} diff --git a/test/test_cpp20_module/test_cpp20_module.vcxproj b/test/test_cpp20_module/test_cpp20_module.vcxproj new file mode 100644 index 000000000..9d0dcfd0b --- /dev/null +++ b/test/test_cpp20_module/test_cpp20_module.vcxproj @@ -0,0 +1,129 @@ + + + + + Debug + ARM64 + + + Debug + Win32 + + + Release + ARM64 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE} + unittests + test_cpp20_module + 20 + + + + Application + true + + + Application + true + + + Application + false + true + + + Application + false + true + + + Application + true + + + Application + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(OutputPath);..\;%(AdditionalIncludeDirectories) + NOMINMAX;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + NotUsing + + + Console + + + + + Disabled + MultiThreadedDebug + + + + + MaxSpeed + true + true + MultiThreaded + + + true + true + + + + + + + + + + + From 9a29dd1178ef6e8d2ffd5f02ae71bd7f23acc379 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Thu, 26 Mar 2026 15:34:03 -0700 Subject: [PATCH 02/34] Try import std in tests. Requires PlatformToolset=v145 --- test/test_cpp20_module/async.cpp | 7 +- test/test_cpp20_module/interop.cpp | 133 ++++++++++++++++++ test/test_cpp20_module/main.cpp | 2 +- .../test_cpp20_module.vcxproj | 8 ++ 4 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 test/test_cpp20_module/interop.cpp diff --git a/test/test_cpp20_module/async.cpp b/test/test_cpp20_module/async.cpp index fa1503b94..d29e64f53 100644 --- a/test/test_cpp20_module/async.cpp +++ b/test/test_cpp20_module/async.cpp @@ -1,9 +1,8 @@ -// catch.hpp must come before 'import winrt' to avoid redefinition of std types. -// The catch header pulls in standard library headers textually; placing it first -// ensures those headers are already included before the module import. +// All #include directives must come before module imports to avoid +// redefinition conflicts with types already in the module. #include "catch.hpp" -#include +import std; import winrt; using namespace winrt; diff --git a/test/test_cpp20_module/interop.cpp b/test/test_cpp20_module/interop.cpp new file mode 100644 index 000000000..368befe30 --- /dev/null +++ b/test/test_cpp20_module/interop.cpp @@ -0,0 +1,133 @@ +#include "catch.hpp" +#include + +import std; +import winrt; + +using namespace winrt; +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Foundation::Collections; + +// ---- Standard library interop ---- + +TEST_CASE("module_std_wstring_interop") +{ + // Construct hstring from std::wstring + std::wstring ws = L"hello from std::wstring"; + hstring h{ ws }; + REQUIRE(h == L"hello from std::wstring"); + + // Convert back + std::wstring_view view = h; + REQUIRE(view == L"hello from std::wstring"); + + // Concatenation + std::wstring combined = std::wstring(h) + L" world"; + REQUIRE(combined == L"hello from std::wstring world"); +} + +TEST_CASE("module_box_std_wstring") +{ + // box_value with a wide string literal + auto boxed = box_value(L"boxed literal"); + REQUIRE(unbox_value(boxed) == L"boxed literal"); + + // box_value with hstring + hstring hs = L"boxed hstring"; + auto boxed2 = box_value(hs); + REQUIRE(unbox_value(boxed2) == L"boxed hstring"); +} + +TEST_CASE("module_box_numeric_types") +{ + auto boxed_int = box_value(42); + REQUIRE(unbox_value(boxed_int) == 42); + + auto boxed_double = box_value(3.14); + REQUIRE(unbox_value(boxed_double) == 3.14); + + auto boxed_bool = box_value(true); + REQUIRE(unbox_value(boxed_bool) == true); +} + +// ---- Collections ---- + +TEST_CASE("module_vector_view") +{ + auto vec = single_threaded_vector({ L"one", L"two", L"three" }); + REQUIRE(vec.Size() == 3); + REQUIRE(vec.GetAt(0) == L"one"); + REQUIRE(vec.GetAt(2) == L"three"); + + // Iterate + int count = 0; + for (auto const& item : vec) + { + count++; + REQUIRE(!item.empty()); + } + REQUIRE(count == 3); +} + +TEST_CASE("module_map") +{ + auto map = single_threaded_map(); + map.Insert(L"a", 1); + map.Insert(L"b", 2); + map.Insert(L"c", 3); + + REQUIRE(map.Size() == 3); + REQUIRE(map.Lookup(L"b") == 2); + REQUIRE(map.HasKey(L"c")); + REQUIRE(!map.HasKey(L"d")); +} + +TEST_CASE("module_observable_vector") +{ + auto vec = single_threaded_observable_vector(); + int callback_count = 0; + vec.VectorChanged([&](auto&&, auto&&) { callback_count++; }); + + vec.Append(L"first"); + vec.Append(L"second"); + REQUIRE(vec.Size() == 2); + REQUIRE(callback_count == 2); +} + +// ---- Errors and HRESULT ---- + +TEST_CASE("module_hresult_error") +{ + try + { + throw hresult_invalid_argument(L"test error message"); + } + catch (hresult_error const& e) + { + REQUIRE(e.code() == E_INVALIDARG); + REQUIRE(to_string(e.message()).find("test error message") != std::string::npos); + } +} + +// ---- GUIDs ---- + +TEST_CASE("module_guid") +{ + auto g = guid_of(); + REQUIRE(g != guid{}); + + // Round-trip through string + auto str = to_hstring(g); + REQUIRE(!str.empty()); +} + +// ---- 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"); +} +#endif diff --git a/test/test_cpp20_module/main.cpp b/test/test_cpp20_module/main.cpp index 6a44a8bab..b2f044a3e 100644 --- a/test/test_cpp20_module/main.cpp +++ b/test/test_cpp20_module/main.cpp @@ -1,11 +1,11 @@ #include -#include #define CATCH_CONFIG_RUNNER #define CATCH_CONFIG_WINDOWS_SEH #include "catch.hpp" +import std; import winrt; using namespace winrt; diff --git a/test/test_cpp20_module/test_cpp20_module.vcxproj b/test/test_cpp20_module/test_cpp20_module.vcxproj index 9d0dcfd0b..b7e568c73 100644 --- a/test/test_cpp20_module/test_cpp20_module.vcxproj +++ b/test/test_cpp20_module/test_cpp20_module.vcxproj @@ -37,29 +37,35 @@ Application true + v145 Application true + v145 Application false true + v145 Application false true + v145 Application true + v145 Application false true + v145 @@ -95,6 +101,7 @@ $(OutputPath);..\;%(AdditionalIncludeDirectories) NOMINMAX;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) NotUsing + true Console @@ -121,6 +128,7 @@ + From 1652e6b64d645e206dda0f11d9246c6a2ee42c53 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Thu, 26 Mar 2026 15:40:27 -0700 Subject: [PATCH 03/34] Exclude v145-specific project from batch builds. --- cppwinrt.sln | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cppwinrt.sln b/cppwinrt.sln index 93cb5612b..2395b9102 100644 --- a/cppwinrt.sln +++ b/cppwinrt.sln @@ -418,17 +418,11 @@ Global {9E392830-805A-4AAF-932D-C493143EFACA}.Release|x86.ActiveCfg = Release|Win32 {9E392830-805A-4AAF-932D-C493143EFACA}.Release|x86.Build.0 = Release|Win32 {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|ARM64.Build.0 = Debug|ARM64 {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|x64.ActiveCfg = Debug|x64 - {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|x64.Build.0 = Debug|x64 {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|x86.ActiveCfg = Debug|Win32 - {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|x86.Build.0 = Debug|Win32 {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|ARM64.ActiveCfg = Release|ARM64 - {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|ARM64.Build.0 = Release|ARM64 {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|x64.ActiveCfg = Release|x64 - {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|x64.Build.0 = Release|x64 {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|x86.ActiveCfg = Release|Win32 - {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 79d8003d3218e2a19a39b74992a20a9282370f2e Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Thu, 26 Mar 2026 20:33:55 -0700 Subject: [PATCH 04/34] Attempt detecting ability to import std inside winrt --- cppwinrt/main.cpp | 4 +++ nuget/Microsoft.Windows.CppWinRT.targets | 7 +++++ strings/base_includes.h | 31 +++++++++++++------ .../test_cpp20_module.vcxproj | 2 +- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/cppwinrt/main.cpp b/cppwinrt/main.cpp index 70a55b076..0ae668710 100644 --- a/cppwinrt/main.cpp +++ b/cppwinrt/main.cpp @@ -345,7 +345,11 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder writer ixx; write_preamble(ixx); ixx.write("module;\n"); + // In the global module fragment, 'import' is not allowed. + // Suppress the 'import std;' path in base_includes so we get raw #includes. + ixx.write("#define WINRT_IMPL_GLOBAL_MODULE_FRAGMENT\n"); ixx.write(strings::base_includes); + ixx.write("#undef WINRT_IMPL_GLOBAL_MODULE_FRAGMENT\n"); ixx.write("\nexport module winrt;\n#define WINRT_EXPORT export\n\n"); for (auto&&[ns, members] : c.namespaces()) diff --git a/nuget/Microsoft.Windows.CppWinRT.targets b/nuget/Microsoft.Windows.CppWinRT.targets index 7bf5044d3..04bd3b105 100644 --- a/nuget/Microsoft.Windows.CppWinRT.targets +++ b/nuget/Microsoft.Windows.CppWinRT.targets @@ -895,6 +895,13 @@ $(XamlMetaDataProviderPch) WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + + + + WINRT_IMPORT_STD;%(PreprocessorDefinitions) + + diff --git a/strings/base_includes.h b/strings/base_includes.h index d6808792b..6e51dcf25 100644 --- a/strings/base_includes.h +++ b/strings/base_includes.h @@ -1,5 +1,21 @@ #include + +#if __has_include() +#include +#endif + +// When WINRT_IMPORT_STD is defined, use 'import std;' instead of individual +// standard library #include directives. This requires C++20 or later with +// 'import std;' support enabled in the build system (e.g. BuildStlModules=true +// in MSBuild, or equivalent). Not used in the winrt.ixx global module fragment +// where 'import' is not permitted. +#if defined(__cpp_lib_modules) && defined(WINRT_IMPORT_STD) && !defined(WINRT_IMPL_GLOBAL_MODULE_FRAGMENT) +#define WINRT_IMPL_IMPORT_STD +import std; +#endif + +#ifndef WINRT_IMPL_IMPORT_STD #include #include #include @@ -27,15 +43,6 @@ #include #include -#if __has_include() -#include -#endif - -#if __has_include() -#define WINRT_IMPL_NUMERICS -#include -#endif - #ifndef WINRT_LEAN_AND_MEAN #include #endif @@ -57,3 +64,9 @@ #elif defined(_RESUMABLE_FUNCTIONS_SUPPORTED) #error "C++/WinRT no longer supports pre-standardization coroutines. If you use co_await, switch to /await:strict or upgrade to C++20. If you do not, remove /await from the compiler flags." #endif +#endif // !WINRT_IMPL_IMPORT_STD + +#if __has_include() +#define WINRT_IMPL_NUMERICS +#include +#endif diff --git a/test/test_cpp20_module/test_cpp20_module.vcxproj b/test/test_cpp20_module/test_cpp20_module.vcxproj index b7e568c73..3958057f0 100644 --- a/test/test_cpp20_module/test_cpp20_module.vcxproj +++ b/test/test_cpp20_module/test_cpp20_module.vcxproj @@ -99,7 +99,7 @@ $(OutputPath);..\;%(AdditionalIncludeDirectories) - NOMINMAX;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + NOMINMAX;WINRT_LEAN_AND_MEAN;WINRT_IMPORT_STD;%(PreprocessorDefinitions) NotUsing true From 546491d04f569815cbee4fb74fdea607481c2a93 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Mon, 30 Mar 2026 10:07:19 -0700 Subject: [PATCH 05/34] Test component project is building. Checkpoint with some documentation added. --- cppwinrt/code_writers.h | 14 +- cppwinrt/component_writers.h | 17 +- cppwinrt/file_writers.h | 74 +++- cppwinrt/main.cpp | 9 + cppwinrt/settings.h | 2 + cppwinrt/type_writers.h | 25 ++ docs/modules-internals.md | 336 +++++++++++++++++ docs/modules.md | 340 ++++++++++++++++++ nuget/Microsoft.Windows.CppWinRT.nuspec | 1 + nuget/Microsoft.Windows.CppWinRT.targets | 12 +- test/test_component_module/Toaster.cpp | 4 + test/test_component_module/Toaster.h | 34 ++ test/test_component_module/exports.def | 3 + test/test_component_module/module.cpp | 30 ++ test/test_component_module/module_wrapper.cpp | 9 + test/test_component_module/pch.cpp | 1 + test/test_component_module/pch.h | 3 + .../test_component_module.idl | 14 + .../test_component_module.vcxproj | 165 +++++++++ test/test_cpp20_module/component.cpp | 49 +++ 20 files changed, 1134 insertions(+), 8 deletions(-) create mode 100644 docs/modules-internals.md create mode 100644 docs/modules.md create mode 100644 test/test_component_module/Toaster.cpp create mode 100644 test/test_component_module/Toaster.h create mode 100644 test/test_component_module/exports.def create mode 100644 test/test_component_module/module.cpp create mode 100644 test/test_component_module/module_wrapper.cpp create mode 100644 test/test_component_module/pch.cpp create mode 100644 test/test_component_module/pch.h create mode 100644 test/test_component_module/test_component_module.idl create mode 100644 test/test_component_module/test_component_module.vcxproj create mode 100644 test/test_cpp20_module/component.cpp diff --git a/cppwinrt/code_writers.h b/cppwinrt/code_writers.h index fc5081bef..adce461b6 100644 --- a/cppwinrt/code_writers.h +++ b/cppwinrt/code_writers.h @@ -37,7 +37,19 @@ namespace cppwinrt static void write_version_assert(writer& w) { + // When WINRT_IMPL_SKIP_INCLUDES is defined, base.h is already available + // from the imported winrt module. However, macros don't cross module + // boundaries, so we include a lightweight header with just the macros. + auto format_guard = R"(#ifdef WINRT_IMPL_SKIP_INCLUDES +#include "winrt/base_macros.h" +#else +)"; + auto format_end = R"(#endif +)"; + w.write(format_guard); w.write_root_include("base"); + w.write(format_end); + auto format = R"(static_assert(winrt::check_version(CPPWINRT_VERSION, "%"), "Mismatched C++/WinRT headers."); #define CPPWINRT_VERSION "%" )"; @@ -137,7 +149,7 @@ namespace cppwinrt if (found != c.namespaces().end() && has_projected_types(found->second)) { - w.write_root_include(parent); + w.write_root_include_guarded(parent); } else { diff --git a/cppwinrt/component_writers.h b/cppwinrt/component_writers.h index 3966cad6c..b0b1a98de 100644 --- a/cppwinrt/component_writers.h +++ b/cppwinrt/component_writers.h @@ -136,7 +136,14 @@ namespace cppwinrt static void write_module_g_cpp(writer& w, std::vector const& classes) { - w.write_root_include("base"); + if (settings.component_module) + { + w.write("import std;\nimport winrt;\n"); + } + else + { + w.write_root_include("base"); + } auto format = R"(% bool __stdcall %_can_unload_now() noexcept { @@ -397,6 +404,14 @@ catch (...) { return winrt::to_hresult(); } return; } + // In module mode, the constructor/static definitions are already exported + // from the winrt module (via the namespace header folded into winrt.ixx). + // Only the winrt_make_* factory function above is needed. + if (settings.component_module) + { + return; + } + auto wrap_type = wrap_type_namespace(w, type_namespace); for (auto&&[factory_name, factory] : get_factories(w, type)) diff --git a/cppwinrt/file_writers.h b/cppwinrt/file_writers.h index ed9386b4e..31433dda0 100644 --- a/cppwinrt/file_writers.h +++ b/cppwinrt/file_writers.h @@ -46,6 +46,57 @@ namespace cppwinrt w.flush_to_file(settings.output_folder + "winrt/base.h"); } + // Lightweight header containing only the preprocessor macros needed by + // generated namespace headers. Used when WINRT_IMPL_SKIP_INCLUDES is + // defined (i.e., consuming component headers after 'import winrt;'). + // Macros don't cross module boundaries, so this provides the macros + // that base.h would normally supply. + static void write_base_macros_h() + { + writer w; + write_preamble(w); + w.write(R"(#pragma once + +// This header provides the preprocessor macros needed by generated C++/WinRT +// headers when the full base.h is not #included (e.g., when using 'import winrt;'). +// Macros are not exported across C++20 module boundaries, so this lightweight +// header must be included textually. + +#ifndef CPPWINRT_VERSION +#define CPPWINRT_VERSION "%" +#endif + +#ifndef WINRT_EXPORT +#define WINRT_EXPORT +#endif + +#if defined(_MSC_VER) +#define WINRT_IMPL_EMPTY_BASES __declspec(empty_bases) +#else +#define WINRT_IMPL_EMPTY_BASES +#endif + +#if defined(_MSC_VER) +#define WINRT_IMPL_NOVTABLE __declspec(novtable) +#else +#define WINRT_IMPL_NOVTABLE +#endif + +#if defined(__clang__) && defined(__has_attribute) +#if __has_attribute(__lto_visibility_public__) +#define WINRT_IMPL_PUBLIC __attribute__((lto_visibility_public)) +#else +#define WINRT_IMPL_PUBLIC +#endif +#else +#define WINRT_IMPL_PUBLIC +#endif + +#define WINRT_IMPL_ABI_DECL WINRT_IMPL_NOVTABLE WINRT_IMPL_PUBLIC +)", CPPWINRT_VERSION_STRING); + w.flush_to_file(settings.output_folder + "winrt/base_macros.h"); + } + static void write_fast_forward_h(std::vector const& classes) { writer w; @@ -139,7 +190,7 @@ namespace cppwinrt for (auto&& depends : w.depends) { - w.write_depends(depends.first, '0'); + w.write_depends_guarded(depends.first, '0'); } w.write_depends(w.type_namespace, '0'); @@ -169,7 +220,7 @@ namespace cppwinrt for (auto&& depends : w.depends) { - w.write_depends(depends.first, impl); + w.write_depends_guarded(depends.first, impl); } w.write_depends(w.type_namespace, '1'); @@ -224,7 +275,7 @@ namespace cppwinrt for (auto&& depends : w.depends) { - w.write_depends(depends.first, '2'); + w.write_depends_guarded(depends.first, '2'); } w.write_depends(w.type_namespace, '2'); @@ -250,9 +301,22 @@ namespace cppwinrt write_preamble(w); write_include_guard(w); - for (auto&& depends : w.depends) + if (settings.component_module) + { + // In module mode, both SDK and component types are available from + // 'import winrt;' (the component projection is folded into the + // winrt.ixx module). Only macros need a textual include since + // they don't cross module boundaries. + w.write("#include \"winrt/base_macros.h\"\n"); + w.write("import std;\n"); + w.write("import winrt;\n"); + } + else { - w.write_depends(depends.first); + for (auto&& depends : w.depends) + { + w.write_depends(depends.first); + } } auto filename = settings.output_folder + get_generated_component_filename(type) + ".g.h"; diff --git a/cppwinrt/main.cpp b/cppwinrt/main.cpp index 0ae668710..a04c50b8d 100644 --- a/cppwinrt/main.cpp +++ b/cppwinrt/main.cpp @@ -39,6 +39,7 @@ namespace cppwinrt { "fastabi", 0, 0 }, // Enable support for the Fast ABI { "ignore_velocity", 0, 0 }, // Ignore feature staging metadata and always include implementations { "synchronous", 0, 0 }, // Instructs cppwinrt to run on a single thread to avoid file system issues in batch builds + { "module", 0, 0, {}, "Generate component files using 'import winrt;' instead of #include" }, }; static void print_usage(writer& w) @@ -172,6 +173,13 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder settings.component_lib = args.value("library", "winrt"); settings.component_opt = args.exists("optimize"); settings.component_ignore_velocity = args.exists("ignore_velocity"); + settings.component_module = args.exists("module"); + + if (settings.component_module) + { + // Module mode disables the PCH include in generated files + settings.component_pch.clear(); + } if (settings.component_pch == ".") { @@ -373,6 +381,7 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder if (settings.base) { write_base_h(); + write_base_macros_h(); ixx.flush_to_file(settings.output_folder + "winrt/winrt.ixx"); } diff --git a/cppwinrt/settings.h b/cppwinrt/settings.h index e07df4ea2..0785ae423 100644 --- a/cppwinrt/settings.h +++ b/cppwinrt/settings.h @@ -31,6 +31,8 @@ namespace cppwinrt bool fastabi{}; std::map fastabi_cache; + + bool component_module{}; // Generate component files using 'import winrt;' instead of #include }; extern settings_type settings; diff --git a/cppwinrt/type_writers.h b/cppwinrt/type_writers.h index df17450f1..d057c0f9e 100644 --- a/cppwinrt/type_writers.h +++ b/cppwinrt/type_writers.h @@ -564,6 +564,19 @@ namespace cppwinrt settings.brackets ? '>' : '\"'); } + void write_root_include_guarded(std::string_view const& include) + { + auto format = R"(#ifndef WINRT_IMPL_SKIP_INCLUDES +#include %winrt/%.h% +#endif +)"; + + write(format, + settings.brackets ? '<' : '\"', + include, + settings.brackets ? '>' : '\"'); + } + void write_depends(std::string_view const& ns, char impl = 0) { if (impl) @@ -576,6 +589,18 @@ namespace cppwinrt } } + void write_depends_guarded(std::string_view const& ns, char impl = 0) + { + if (impl) + { + write_root_include_guarded(write_temp("impl/%.%", ns, impl)); + } + else + { + write_root_include_guarded(ns); + } + } + void save_header(char impl = 0) { auto filename{ settings.output_folder + "winrt/" }; diff --git a/docs/modules-internals.md b/docs/modules-internals.md new file mode 100644 index 000000000..f3ce7e5f4 --- /dev/null +++ b/docs/modules-internals.md @@ -0,0 +1,336 @@ +# C++/WinRT Code Generator Internals: Module Support + +This document is for maintainers of the cppwinrt code generator. It explains the +internal mechanisms that enable C++20 module support, the design decisions behind +them, and the interactions between the various moving pieces. + +## Table of Contents + +- [File Generation Pipeline](#file-generation-pipeline) +- [winrt.ixx Generation](#winrtixx-generation) +- [The Global Module Fragment Problem](#the-global-module-fragment-problem) +- [base_macros.h: Why Macros Need Special Handling](#base_macrosh-why-macros-need-special-handling) +- [WINRT_IMPL_SKIP_INCLUDES: Guarded Cross-Namespace Includes](#winrt_impl_skip_includes-guarded-cross-namespace-includes) +- [The -module Flag: Component Code Generation](#the--module-flag-component-code-generation) +- [The Combined-ixx Approach](#the-combined-ixx-approach) +- [The windowsnumerics.impl.h Warning](#the-windowsnumericsh-warning) +- [import std Integration](#import-std-integration) +- [Code Paths Reference](#code-paths-reference) + +## File Generation Pipeline + +The code generator produces these files (per namespace `NS`): + +``` +write_namespace_0_h(NS) → winrt/impl/NS.0.h (forward decls, enums, ABI, consume structs) +write_namespace_1_h(NS) → winrt/impl/NS.1.h (interface type definitions) +write_namespace_2_h(NS) → winrt/impl/NS.2.h (class/struct/delegate definitions) +write_namespace_h(NS) → winrt/NS.h (consume definitions, produce, std::hash) +write_base_h() → winrt/base.h (core library: types, COM, error handling, etc.) +write_base_macros_h() → winrt/base_macros.h (lightweight macro-only header for module mode) +``` + +For the ixx, `main.cpp` writes directly to a `writer ixx` object. + +For component generation, additional files: +``` +write_component_g_h(type) → Generated Files/.g.h (implementation base template) +write_component_g_cpp(type) → Generated Files/.g.cpp (factory function, opt constructors) +write_module_g_cpp(classes) → Generated Files/module.g.cpp (DLL activation factory) +``` + +## winrt.ixx Generation + +**Source**: `main.cpp`, lines ~345-378 + +The ixx is built by writing directly to a `writer ixx` object: + +```cpp +writer ixx; +write_preamble(ixx); +ixx.write("module;\n"); // global module fragment +ixx.write("#define WINRT_IMPL_GLOBAL_MODULE_FRAGMENT\n"); +ixx.write(strings::base_includes); // standard library #includes +ixx.write("#undef WINRT_IMPL_GLOBAL_MODULE_FRAGMENT\n"); +ixx.write("\nexport module winrt;\n#define WINRT_EXPORT export\n\n"); + +for (auto ns : namespaces) + ixx.write("#include \"winrt/NS.h\"\n"); // all namespace headers + +ixx.flush_to_file("winrt/winrt.ixx"); +``` + +Key points: +- `strings::base_includes` is the *literal content* of `base_includes.h` (embedded + at compile time by the prebuild step). It's not an `#include` directive — it's + the raw text of the standard library includes. +- `WINRT_EXPORT` is defined as `export` inside the module purview. Every namespace + declaration in the generated headers uses `WINRT_EXPORT namespace winrt::...` so + types are properly exported. +- The namespace headers are `#include`d inside the module purview, making all their + content (types, template specializations) part of the module. + +## The Global Module Fragment Problem + +**Problem**: The C++ standard prohibits `import` declarations in the global module +fragment (the section between `module;` and `export module winrt;`). + +**Context**: `base_includes.h` conditionally emits `import std;` when +`WINRT_IMPORT_STD` is defined. If this triggers in the global module fragment, +compilation fails. + +**Solution**: The ixx writer wraps the base_includes content with a guard macro: + +```cpp +ixx.write("#define WINRT_IMPL_GLOBAL_MODULE_FRAGMENT\n"); +ixx.write(strings::base_includes); // import std; is suppressed +ixx.write("#undef WINRT_IMPL_GLOBAL_MODULE_FRAGMENT\n"); +``` + +Inside `base_includes.h`: +```cpp +#if defined(__cpp_lib_modules) && defined(WINRT_IMPORT_STD) \ + && !defined(WINRT_IMPL_GLOBAL_MODULE_FRAGMENT) +import std; +#else +// ... individual #include directives +#endif +``` + +The three-way condition ensures `import std;` only fires when: +1. The compiler supports it (`__cpp_lib_modules`) +2. The user/build opted in (`WINRT_IMPORT_STD`) +3. We're not in the global module fragment (`!WINRT_IMPL_GLOBAL_MODULE_FRAGMENT`) + +## base_macros.h: Why Macros Need Special Handling + +**Problem**: C++20 modules do not export preprocessor macros. When consumer code +does `import winrt;`, macros like `WINRT_EXPORT`, `WINRT_IMPL_EMPTY_BASES`, +`CPPWINRT_VERSION`, and `WINRT_IMPL_ABI_DECL` are not available. + +**Who needs them**: Generated component files (`.g.h`) reference these macros. +In header mode, they come from `#include "winrt/base.h"`. In module mode, +`base.h` is not `#include`d — its content comes from the module. + +**Solution**: `base_macros.h` is a lightweight header containing ONLY the +preprocessor definitions needed by generated code. It's safe to `#include` +alongside `import winrt;` because it has no type declarations that could +conflict with module-exported symbols. + +**Generated by**: `write_base_macros_h()` in `file_writers.h` + +**Used by**: `.g.h` files in module mode: +```cpp +#include "winrt/base_macros.h" +import std; +import winrt; +// ... template code using WINRT_IMPL_EMPTY_BASES etc. +``` + +## WINRT_IMPL_SKIP_INCLUDES: Guarded Cross-Namespace Includes + +**Problem**: When consuming component headers after `import winrt;`, the +component's namespace header chain includes SDK headers (e.g., +`#include "winrt/impl/Windows.Foundation.2.h"`) that would conflict with +the module-imported versions. + +**Solution**: Generated namespace headers wrap their cross-namespace `#include` +dependencies with `#ifndef WINRT_IMPL_SKIP_INCLUDES` guards: + +```cpp +// In generated Windows.Foundation.h: +#ifndef WINRT_IMPL_SKIP_INCLUDES +#include "winrt/base.h" +#endif +// ... (base.h include guarded, cross-namespace deps guarded) +#include "winrt/impl/Windows.Foundation.2.h" // self-namespace: NOT guarded +``` + +**Implementation**: `write_root_include_guarded()` in `type_writers.h` wraps the +`#include` with the guard. Used by: +- `write_version_assert()` for `base.h` include +- `write_depends_guarded()` for cross-namespace impl includes +- `write_parent_depends()` for parent namespace includes + +The component's own impl headers are always included (via `write_depends()`, +unguarded) since they contain the component-specific type definitions. + +**Note**: With the combined-ixx approach, `WINRT_IMPL_SKIP_INCLUDES` is not +needed for component authoring (the component types are in the module). It +remains useful for the scenario of consuming a component's projection header +separately after `import winrt;`, should MSVC add support for cross-module +template specialization in the future. + +## The -module Flag: Component Code Generation + +**Source**: `settings.h` (`component_module`), `main.cpp` (arg parsing) + +When `-module` is passed to cppwinrt.exe, the following changes apply to +generated component files: + +### module.g.cpp + +**Source**: `write_module_g_cpp()` in `component_writers.h` + +| Header mode | Module mode | +|-------------|-------------| +| `#include "pch.h"` | (omitted — pch cleared) | +| `#include "winrt/base.h"` | `import std;` + `import winrt;` | + +### Toaster.g.h (component base template) + +**Source**: `write_component_g_h()` in `file_writers.h` + +| Header mode | Module mode | +|-------------|-------------| +| `#include "winrt/test_component.h"` etc. | `#include "winrt/base_macros.h"` + `import std;` + `import winrt;` | + +### Toaster.g.cpp (factory + optional optimized constructors) + +**Source**: `write_component_g_cpp()` in `component_writers.h` + +| Header mode | Module mode | +|-------------|-------------| +| `winrt_make_*` + constructor/static overrides | `winrt_make_*` only | + +In module mode, the constructor and static method definitions are omitted from +`.g.cpp` because they're already exported from the `winrt` module (via the +component's namespace header folded into `winrt.ixx`). The `winrt_make_*` +factory function is always emitted since it's needed by `module.g.cpp` for +activation lookup. + +## The Combined-ixx Approach + +**The key insight**: MSVC cannot specialize module-imported class templates from +textual `#include` code. The component's `.0.h` files specialize templates like +`category<>`, `abi<>`, `guid_v<>` from `winrt::impl` namespace. + +**The solution**: Append the component's namespace header includes to the end of +`winrt.ixx`, so the specializations happen inside the module purview: + +``` +export module winrt; +#define WINRT_EXPORT export + +#include "winrt/Windows.Foundation.h" +... +#include "winrt/Windows.Web.UI.Interop.h" +#include "winrt/test_component.h" ← appended by build system +``` + +This is done by the NuGet targets (`CppWinRTAddModuleSource`) or manually in +the project's CustomBuildStep. The target scans `$(GeneratedFilesDir)winrt/` for +`.h` files not already in `winrt.ixx` and appends them. + +## The windowsnumerics.impl.h Warning + +**Source**: `base_macros.h` (strings) + +The `` SDK header is intentionally included in the +module purview (not the global module fragment) because it defines the +`winrt::Windows::Foundation::Numerics` types using `WINRT_EXPORT`, which must +expand to `export` for the types to be exported from the module. + +MSVC warning C5244 flags this as "appears erroneous." We suppress it with +`#pragma warning(suppress: 5244)` on the single line before the include, +matching the approach from the sylveon fork. + +```cpp +#ifdef WINRT_IMPL_NUMERICS +#define _WINDOWS_NUMERICS_BEGIN_NAMESPACE_ WINRT_EXPORT namespace ... +// The include in the purview of a module is intentional. +#pragma warning(suppress: 5244) +#include +... +#endif +``` + +## import std Integration + +**Source**: `base_includes.h` + +The standard library `#include` directives in `base_includes.h` are conditionally +replaced with `import std;`. The conditions are: + +```cpp +#if defined(__cpp_lib_modules) && defined(WINRT_IMPORT_STD) \ + && !defined(WINRT_IMPL_GLOBAL_MODULE_FRAGMENT) +import std; +#else +#include +#include +... // 25+ individual includes +#endif +``` + +`WINRT_IMPORT_STD` is: +- Defined automatically by NuGet targets when `CppWinRTModule=true` and + `BuildStlModules=true` +- Can be defined manually by users as a preprocessor definition + +`__cpp_lib_modules` is defined in `` by MSVC when the STL supports +modules. However, it does NOT guarantee `import std;` will work — that also +requires the build system to compile `std.ixx` (controlled by `BuildStlModules`). +This is why `WINRT_IMPORT_STD` is a separate opt-in: `__cpp_lib_modules` alone +would break existing users whose build systems don't compile the std module. + +## Code Paths Reference + +### Key source files + +| File | Responsibility | +|------|---------------| +| `main.cpp` | CLI parsing, ixx generation, orchestration | +| `settings.h` | `component_module` flag storage | +| `file_writers.h` | File-level writers: `write_base_h`, `write_base_macros_h`, `write_component_g_h`, `write_namespace_h` | +| `component_writers.h` | Component template writers: `write_module_g_cpp`, `write_component_g_cpp` | +| `code_writers.h` | Low-level writers: `write_version_assert`, `write_parent_depends`, `write_open_file_guard` | +| `type_writers.h` | Writer class: `write_root_include`, `write_root_include_guarded`, `write_depends`, `write_depends_guarded` | +| `strings/base_includes.h` | Standard library includes / `import std;` conditional | +| `strings/base_macros.h` | `WINRT_EXPORT`, `windowsnumerics.impl.h` suppress | + +### Key NuGet targets + +| Target | File | Role | +|--------|------|------| +| `CppWinRTAddModuleSource` | `Microsoft.Windows.CppWinRT.targets` | Folds component headers into ixx, adds ixx to compilation, defines `WINRT_IMPORT_STD` | +| `CppWinRTMakeProjections` | `Microsoft.Windows.CppWinRT.targets` | Orchestrates platform/reference/component projection generation | +| `CppWinRTMakeComponentProjection` | `Microsoft.Windows.CppWinRT.targets` | Runs cppwinrt.exe for component stubs (passes `-module` via `CppWinRTParameters`) | + +### Macro flow diagram + +``` +User sets CppWinRTModule=true + BuildStlModules=true + │ + ├── NuGet targets define WINRT_IMPORT_STD on all ClCompile items + ├── NuGet targets pass -module to cppwinrt.exe + ├── NuGet targets append component headers to winrt.ixx + │ + ▼ +winrt.ixx compilation: + module; + #define WINRT_IMPL_GLOBAL_MODULE_FRAGMENT ← blocks import std; + ← falls through to #include path + #undef WINRT_IMPL_GLOBAL_MODULE_FRAGMENT + export module winrt; + #define WINRT_EXPORT export + #include "winrt/base.h" ← WINRT_IMPORT_STD not checked (pragma once) + #include "winrt/Windows.Foundation.h" ← SDK types + specializations + ... + #include "winrt/MyComponent.h" ← component specializations in purview + +Consumer .cpp compilation: + import std; ← std module (compiled by BuildStlModules) + import winrt; ← winrt module (SDK + component types) + #include "Toaster.h" + └── #include "Toaster.g.h" + └── #include "winrt/base_macros.h" ← macros only (WINRT_EXPORT etc.) + └── import std; ← harmless duplicate + └── import winrt; ← harmless duplicate + +module.g.cpp compilation: + import std; ← from -module code gen + import winrt; ← from -module code gen + void* winrt_make_MyComponent_Toaster() ← factory function + bool __stdcall ..._can_unload_now() ← DLL entry points +``` diff --git a/docs/modules.md b/docs/modules.md new file mode 100644 index 000000000..555bb1f2c --- /dev/null +++ b/docs/modules.md @@ -0,0 +1,340 @@ +# C++20 Modules Support in C++/WinRT + +## Overview + +C++/WinRT can generate a C++20 named module (`winrt.ixx`) that allows consumers +to write `import winrt;` instead of using traditional `#include` directives and +precompiled headers. This can significantly improve build times and simplify the +developer experience. + +## Quick Start + +### Pure SDK consumption (app that calls WinRT APIs) + +Set these MSBuild properties in your `.vcxproj`: + +```xml + + true + + + + + true + stdcpp20 + + +``` + +Then in your source files: + +```cpp +import std; +import winrt; + +using namespace winrt; +using namespace winrt::Windows::Foundation; + +int main() +{ + init_apartment(); + Uri uri(L"https://example.com"); + // ... +} +``` + +### Component authoring (DLL with IDL) + +Same project properties as above, then in your implementation files: + +```cpp +// MyComponent.cpp +import std; +import winrt; +#include "MyComponent.h" // includes MyComponent.g.h +#include "MyComponent.g.cpp" // factory function only in module mode +``` + +The NuGet targets automatically: +1. Pass `-module` to cppwinrt.exe so generated `.g.h`/`.g.cpp` files use + `import winrt;` instead of `#include` directives +2. Fold the component's projection headers into `winrt.ixx` so template + specializations occur within the module purview +3. Define `WINRT_IMPORT_STD` when `BuildStlModules` is enabled + +## Requirements + +- **Visual Studio 2026** (v145 toolset) or later — the v180 MSBuild targets + support `import std;` with C++20. Earlier toolsets (v143) require C++23 for + `import std;` support. +- **C++20 or later** language standard (`/std:c++20` or `/std:c++latest`) +- **`BuildStlModules=true`** in `` to enable + the standard library module compilation + +## MSBuild Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `CppWinRTModule` | bool | `false` | Enable C++20 module mode. Adds `winrt.ixx` to compilation, folds component projections into the module, and passes `-module` to cppwinrt.exe for component generation. | +| `BuildStlModules` | ClCompile metadata | `false` | Enables building `std.ixx`/`std.compat.ixx` so `import std;` works. Set inside ``. | + +When `CppWinRTModule=true`, the NuGet targets also automatically: +- Define `WINRT_IMPORT_STD` as a preprocessor definition when `BuildStlModules` + is enabled, so `base.h` uses `import std;` instead of individual `#include`s +- Define `WINRT_LEAN_AND_MEAN` on the `winrt.ixx` compilation unit + +## cppwinrt.exe Command-Line Options + +### Module-related options + +| Option | Description | +|--------|-------------| +| `-module` | Generate component files (`.g.h`, `.g.cpp`, `module.g.cpp`) using `import winrt;` instead of `#include` directives. The `.g.h` files emit `import std;`, `import winrt;`, and `#include "winrt/base_macros.h"`. The `.g.cpp` files emit only the `winrt_make_*` factory function (constructor/static optimizations are omitted since those definitions come from the module). | + +### Other commonly used options + +| Option | Description | +|--------|-------------| +| `-input ` | Windows metadata (.winmd) to include in projection | +| `-reference ` | Windows metadata to reference from projection | +| `-output ` | Location of generated projection and component templates | +| `-component []` | Generate component templates, and optional implementation | +| `-name ` | Specify explicit name for component files | +| `-optimize` | Generate component projection with unified construction support | +| `-base` | Generate base.h unconditionally | +| `-prefix` | Use dotted namespace convention for component files | +| `-pch ` | Specify PCH file name (use `.` to disable) | +| `-library ` | Specify library prefix (defaults to `winrt`) | +| `-fastabi` | Enable Fast ABI support | +| `-verbose` | Show detailed progress information | +| `-overwrite` | Overwrite generated component files | + +## Source File Patterns + +### Include ordering rules + +When using `import winrt;`, all `#include` directives must appear **before** +the `import` declarations. This is because textual `#include`s and module +`import`s can conflict if headers define types that the module also exports. + +```cpp +// CORRECT: #includes before imports +#include "catch.hpp" // third-party headers first +#include // Windows SDK headers (non-standard-library) + +import std; // standard library module +import winrt; // C++/WinRT module + +// Your code here... +``` + +### Consumer app (SDK types only) + +```cpp +import std; +import winrt; + +using namespace winrt; +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Web::Syndication; + +winrt::Windows::Foundation::IAsyncAction FetchFeedAsync() +{ + Uri uri(L"https://blogs.windows.com/feed"); + SyndicationClient client; + auto feed = co_await client.RetrieveFeedAsync(uri); + for (auto const& item : feed.Items()) + { + std::wcout << item.Title().Text().c_str() << std::endl; + } +} +``` + +### Component implementation + +```cpp +// Toaster.cpp +import std; +import winrt; +#include "Toaster.h" // your implementation header +#include "Toaster.g.cpp" // generated factory function + +namespace winrt::MyComponent::implementation +{ + void Toaster::MakeToast(hstring const& message) + { + // implementation... + } +} +``` + +### Component implementation header + +```cpp +// Toaster.h +#pragma once +#include "Toaster.g.h" // generated base class template + +namespace winrt::MyComponent::implementation +{ + struct Toaster : ToasterT + { + Toaster() = default; + void MakeToast(hstring const& message); + }; +} + +namespace winrt::MyComponent::factory_implementation +{ + struct Toaster : ToasterT {}; +} +``` + +## Preprocessor Macros Reference + +### User-facing macros + +| Macro | Purpose | +|-------|---------| +| `WINRT_IMPORT_STD` | When defined alongside `__cpp_lib_modules`, causes `base.h` to emit `import std;` instead of individual standard library `#include` directives. Automatically defined by the NuGet targets when both `CppWinRTModule` and `BuildStlModules` are enabled. | +| `WINRT_LEAN_AND_MEAN` | Suppresses `#include ` and `std::hash`/`std::formatter` specializations from generated headers. Reduces header weight. | + +### Internal implementation macros + +These are used by the code generator and should not be set directly by users: + +| Macro | Purpose | +|-------|---------| +| `WINRT_IMPL_GLOBAL_MODULE_FRAGMENT` | Set by the generated `winrt.ixx` around the `base_includes` content in the global module fragment (before `export module winrt;`). Prevents `import std;` from being emitted in the global module fragment where `import` declarations are not permitted by the C++ standard. | +| `WINRT_IMPL_IMPORT_STD` | Internal flag set when `WINRT_IMPORT_STD`, `__cpp_lib_modules`, and not-in-global-fragment conditions are all met. Guards the `#include` fallback path. | +| `WINRT_IMPL_SKIP_INCLUDES` | When defined, generated namespace headers skip their cross-namespace `#include` dependencies (e.g., `#include "winrt/base.h"`, `#include "winrt/impl/Windows.Foundation.2.h"`). Used when those dependencies are already available from the `winrt` module. The component's own impl headers are never skipped. | +| `WINRT_EXPORT` | Expands to `export` inside `winrt.ixx` (module purview), empty in header mode. Used on namespace declarations so types are properly exported from the module. Defined in `base_macros.h` (as empty) for use in generated component files that operate alongside the module. | +| `WINRT_IMPL_EMPTY_BASES` | MSVC `__declspec(empty_bases)` optimization. Defined in both `base.h` and `base_macros.h`. | +| `WINRT_IMPL_ABI_DECL` | Combines `WINRT_IMPL_NOVTABLE` and `WINRT_IMPL_PUBLIC` for ABI interface declarations. | +| `CPPWINRT_VERSION` | Version string for header compatibility checking. Defined in `base.h` and `base_macros.h`. | + +## Architecture: How the Module is Built + +### Generated file structure + +``` +winrt/ +├── base.h # Core C++/WinRT library (types, helpers, COM support) +├── base_macros.h # Lightweight macro-only header for module consumers +├── winrt.ixx # C++20 module interface unit +├── Windows.Foundation.h # SDK namespace header (consume definitions) +├── impl/ +│ ├── Windows.Foundation.0.h # Forward decls, enums, ABI, consume structs +│ ├── Windows.Foundation.1.h # Interface definitions +│ └── Windows.Foundation.2.h # Class/struct/delegate definitions +├── test_component.h # Component namespace header (if component) +└── impl/ + ├── test_component.0.h # Component forward decls + template specializations + ├── test_component.1.h # Component interface definitions + └── test_component.2.h # Component class definitions +``` + +### winrt.ixx structure + +``` +module; ← Global module fragment +#define WINRT_IMPL_GLOBAL_MODULE_FRAGMENT + ← Raw #includes (import not allowed here) + ← Platform headers +#undef WINRT_IMPL_GLOBAL_MODULE_FRAGMENT + +export module winrt; ← Module purview begins +#define WINRT_EXPORT export + +#include "winrt/Windows.Foundation.h" ← SDK namespace headers +#include "winrt/Windows.Foundation.Collections.h" +... ← (all SDK namespaces) +#include "winrt/MyComponent.h" ← Component headers (appended by build) +``` + +### Why component headers are folded into winrt.ixx + +The component's `.0.h` impl headers contain explicit template specializations: + +```cpp +// In test_component.0.h: +template <> struct category + { using type = interface_category; }; +template <> struct abi + { struct type : inspectable_abi { ... }; }; +template <> inline constexpr guid guid_v{ ... }; +``` + +These specialize primary templates (`category`, `abi`, `guid_v`) defined in +`base.h`. If these specializations occur in textual code that imported the +primary templates from a module, MSVC currently reports: + +``` +error C3856: 'category': symbol is not a class template +``` + +By folding the component headers into `winrt.ixx`, the specializations happen +inside the same module purview as the primary templates, avoiding this issue +entirely. + +### Generated file differences in module mode (`-module` flag) + +| File | Header mode | Module mode (`-module`) | +|------|-------------|------------------------| +| `Toaster.g.h` | `#include "winrt/test_component.h"` | `#include "winrt/base_macros.h"` + `import std;` + `import winrt;` | +| `Toaster.g.cpp` | `winrt_make_*` + constructor/static overrides | `winrt_make_*` only (constructors come from module) | +| `module.g.cpp` | `#include "pch.h"` + `#include "winrt/base.h"` | `import std;` + `import winrt;` | + +### base_macros.h + +Macros are not exported across C++20 module boundaries. When a `.g.h` file does +`import winrt;`, macros like `WINRT_EXPORT`, `WINRT_IMPL_EMPTY_BASES`, and +`CPPWINRT_VERSION` are not available. The generated `base_macros.h` is a +lightweight textual header that provides just these macros. It is always safe to +`#include` alongside `import winrt;` because it contains only preprocessor +definitions — no type/function declarations that could conflict with the module. + +## NuGet Targets Integration + +The `CppWinRTAddModuleSource` target in `Microsoft.Windows.CppWinRT.targets` +performs these steps when `CppWinRTModule=true`: + +1. **Scans** `$(GeneratedFilesDir)winrt/` for namespace headers (`.h` files) + that are not already listed in `winrt.ixx` +2. **Appends** `#include` directives for those headers to the end of `winrt.ixx` + (this folds component projection headers into the module) +3. **Adds** `winrt.ixx` as a `ClCompile` item with `PrecompiledHeader=NotUsing` +4. **Defines** `WINRT_IMPORT_STD` when `BuildStlModules=true` is detected + +The `-module` flag is automatically appended to `$(CppWinRTParameters)` when +`CppWinRTModule=true`, so it flows to all cppwinrt.exe invocations including +the component projection generation. + +## Known Limitations + +1. **Toolset requirement**: The v145 toolset (VS 2026) or later is needed for + `import std;` with C++20. Earlier toolsets define `__cpp_lib_modules` but + their MSBuild targets only enable `StdModulesSupported` for C++23+. + +2. **Include ordering**: Standard library `#include` directives and third-party + headers must come before `import std;` and `import winrt;` to avoid + redefinition errors. + +3. **No PCH with modules**: Module interface units cannot use precompiled + headers. Component source files using `import winrt;` should set + `NotUsing`. + +4. **WINRT_LEAN_AND_MEAN for consumers**: Pure consumer apps (no component + authoring) should define `WINRT_LEAN_AND_MEAN` to avoid pulling `` + and `produce<>` templates into the module. Component authors should NOT + define it since they need `produce<>`. + +5. **Cross-module template specialization**: MSVC does not currently support + specializing module-imported class templates from textual `#include` code. + The combined-ixx approach (folding component headers into `winrt.ixx`) + works around this by keeping specializations within the module purview. + If you need to include a component's projection header separately (outside + the module), this will fail with: + ``` + error C3856: 'category': symbol is not a class template + ``` diff --git a/nuget/Microsoft.Windows.CppWinRT.nuspec b/nuget/Microsoft.Windows.CppWinRT.nuspec index a63e08bf9..afe9067c3 100644 --- a/nuget/Microsoft.Windows.CppWinRT.nuspec +++ b/nuget/Microsoft.Windows.CppWinRT.nuspec @@ -25,5 +25,6 @@ + diff --git a/nuget/Microsoft.Windows.CppWinRT.targets b/nuget/Microsoft.Windows.CppWinRT.targets index 04bd3b105..ae925b346 100644 --- a/nuget/Microsoft.Windows.CppWinRT.targets +++ b/nuget/Microsoft.Windows.CppWinRT.targets @@ -26,6 +26,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. $([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)))..\..\ $([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory))) $(CppWinRTParameters) -fastabi + $(CppWinRTParameters) -module "$(CppWinRTPackageDir)bin\" "$(CppWinRTPackageDir)" @@ -889,10 +890,19 @@ $(XamlMetaDataProviderPch) BeforeTargets="ClCompile" Condition="'$(CppWinRTModule)' == 'true'"> + + + <_CppWinRTModuleHeaders Include="$(GeneratedFilesDir)winrt\*.h" Exclude="$(GeneratedFilesDir)winrt\base.h;$(GeneratedFilesDir)winrt\base_macros.h;$(GeneratedFilesDir)winrt\fast_forward.h" /> + <_CppWinRTIxxExistingIncludes Include="$([System.IO.File]::ReadAllText('$(GeneratedFilesDir)winrt\winrt.ixx'))" /> + + NotUsing - WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + + Midl + + + + .;$(OutputPath);Generated Files + NOMINMAX;%(PreprocessorDefinitions) + NotUsing + true + + + exports.def + + + true + $(OutputPath)test_component_module.winmd + $(OutputPath)test_component_module.h + /nomidl %(AdditionalOptions) + C:\Windows\System32\WinMetadata + true + + + + $(CppWinRTDir)cppwinrt -in local -out $(OutputPath) -verbose + $(CppWinRTDir)cppwinrt -input $(OutputPath)test_component_module.winmd -out $(OutputPath) -ref sdk -verbose + $(CppWinRTDir)cppwinrt -input $(OutputPath)test_component_module.winmd -comp "$(ProjectDir)Generated Files" -out "$(ProjectDir)Generated Files" -include test_component_module -ref sdk -verbose -prefix -opt -lib test_module -overwrite -name test_component_module -module + echo #include "winrt/test_component_module.h">> "$(OutputPath)winrt\winrt.ixx" + + Projecting Windows and component metadata + $(OutputPath)\winrt\base.h;Generated Files\module.g.cpp + $(CppWinRTDir)cppwinrt.exe;$(OutputPath)test_component_module.winmd + + + + + Disabled + MultiThreadedDebug + + + + + MaxSpeed + true + true + MultiThreaded + + + true + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test_cpp20_module/component.cpp b/test/test_cpp20_module/component.cpp new file mode 100644 index 000000000..78524cabf --- /dev/null +++ b/test/test_cpp20_module/component.cpp @@ -0,0 +1,49 @@ +// Test consuming a custom WinRT component through a module. +// The component projection header is included AFTER importing winrt, +// which tests the interaction between module-imported base types and +// #include-based component projection headers. +#include "catch.hpp" +#include + +import std; +import winrt; + +// When consuming component headers alongside 'import winrt;', define +// WINRT_IMPL_SKIP_INCLUDES to skip #include directives for base.h and SDK +// headers that are already available from the module. +#define WINRT_IMPL_SKIP_INCLUDES +#include "winrt/test_component_module.h" + +using namespace winrt; +using namespace winrt::test_component_module; + +TEST_CASE("module_component_create") +{ + auto toaster = Toaster(); + REQUIRE(toaster.Name() == L"Unnamed"); +} + +TEST_CASE("module_component_create_with_name") +{ + auto toaster = Toaster(L"My Toaster"); + REQUIRE(toaster.Name() == L"My Toaster"); +} + +TEST_CASE("module_component_property") +{ + auto toaster = Toaster(); + toaster.Name(L"Updated"); + REQUIRE(toaster.Name() == L"Updated"); +} + +TEST_CASE("module_component_static") +{ + auto toaster = Toaster::CreateDefault(); + REQUIRE(toaster.Name() == L"Default Toaster"); +} + +TEST_CASE("module_component_async") +{ + auto toaster = Toaster(); + toaster.ToastAsync().get(); +} From 01f5f0380e7911339d947e55d5a41421bb834714 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Mon, 30 Mar 2026 13:57:18 -0700 Subject: [PATCH 06/34] PR feedback: split includes into std and non-std, add base_module_macros.h, export extern handlers --- cppwinrt/file_writers.h | 47 ++++--------- cppwinrt/main.cpp | 5 +- docs/modules-internals.md | 128 +++++++++++++++++++++++------------ docs/modules.md | 12 ++-- strings/base_extern.h | 8 +-- strings/base_includes.h | 61 ----------------- strings/base_module_macros.h | 33 +++++++++ strings/base_std_includes.h | 49 ++++++++++++++ 8 files changed, 186 insertions(+), 157 deletions(-) create mode 100644 strings/base_module_macros.h create mode 100644 strings/base_std_includes.h diff --git a/cppwinrt/file_writers.h b/cppwinrt/file_writers.h index 31433dda0..e3ed4c628 100644 --- a/cppwinrt/file_writers.h +++ b/cppwinrt/file_writers.h @@ -11,6 +11,14 @@ namespace cppwinrt auto wrap_file_guard = wrap_open_file_guard(w, "BASE"); w.write(strings::base_includes); + w.write(R"( +#if defined(__cpp_lib_modules) && defined(WINRT_IMPORT_STD) +import std; +#else +)"); + w.write(strings::base_std_includes); + w.write(R"(#endif +)"); w.write(strings::base_macros); w.write(strings::base_types); w.write(strings::base_extern); @@ -55,45 +63,14 @@ namespace cppwinrt { writer w; write_preamble(w); - w.write(R"(#pragma once - -// This header provides the preprocessor macros needed by generated C++/WinRT -// headers when the full base.h is not #included (e.g., when using 'import winrt;'). -// Macros are not exported across C++20 module boundaries, so this lightweight -// header must be included textually. + auto format = R"(#pragma once #ifndef CPPWINRT_VERSION #define CPPWINRT_VERSION "%" #endif - -#ifndef WINRT_EXPORT -#define WINRT_EXPORT -#endif - -#if defined(_MSC_VER) -#define WINRT_IMPL_EMPTY_BASES __declspec(empty_bases) -#else -#define WINRT_IMPL_EMPTY_BASES -#endif - -#if defined(_MSC_VER) -#define WINRT_IMPL_NOVTABLE __declspec(novtable) -#else -#define WINRT_IMPL_NOVTABLE -#endif - -#if defined(__clang__) && defined(__has_attribute) -#if __has_attribute(__lto_visibility_public__) -#define WINRT_IMPL_PUBLIC __attribute__((lto_visibility_public)) -#else -#define WINRT_IMPL_PUBLIC -#endif -#else -#define WINRT_IMPL_PUBLIC -#endif - -#define WINRT_IMPL_ABI_DECL WINRT_IMPL_NOVTABLE WINRT_IMPL_PUBLIC -)", CPPWINRT_VERSION_STRING); +)"; + w.write(format, CPPWINRT_VERSION_STRING); + w.write(strings::base_module_macros); w.flush_to_file(settings.output_folder + "winrt/base_macros.h"); } diff --git a/cppwinrt/main.cpp b/cppwinrt/main.cpp index a04c50b8d..c3142d320 100644 --- a/cppwinrt/main.cpp +++ b/cppwinrt/main.cpp @@ -353,11 +353,8 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder writer ixx; write_preamble(ixx); ixx.write("module;\n"); - // In the global module fragment, 'import' is not allowed. - // Suppress the 'import std;' path in base_includes so we get raw #includes. - ixx.write("#define WINRT_IMPL_GLOBAL_MODULE_FRAGMENT\n"); ixx.write(strings::base_includes); - ixx.write("#undef WINRT_IMPL_GLOBAL_MODULE_FRAGMENT\n"); + ixx.write(strings::base_std_includes); ixx.write("\nexport module winrt;\n#define WINRT_EXPORT export\n\n"); for (auto&&[ns, members] : c.namespaces()) diff --git a/docs/modules-internals.md b/docs/modules-internals.md index f3ce7e5f4..66ce8529d 100644 --- a/docs/modules-internals.md +++ b/docs/modules-internals.md @@ -8,9 +8,10 @@ them, and the interactions between the various moving pieces. - [File Generation Pipeline](#file-generation-pipeline) - [winrt.ixx Generation](#winrtixx-generation) -- [The Global Module Fragment Problem](#the-global-module-fragment-problem) +- [Split Standard Library Includes](#split-standard-library-includes) - [base_macros.h: Why Macros Need Special Handling](#base_macrosh-why-macros-need-special-handling) - [WINRT_IMPL_SKIP_INCLUDES: Guarded Cross-Namespace Includes](#winrt_impl_skip_includes-guarded-cross-namespace-includes) +- [WINRT_EXPORT on Extern Handlers](#winrt_export-on-extern-handlers) - [The -module Flag: Component Code Generation](#the--module-flag-component-code-generation) - [The Combined-ixx Approach](#the-combined-ixx-approach) - [The windowsnumerics.impl.h Warning](#the-windowsnumericsh-warning) @@ -49,9 +50,8 @@ The ixx is built by writing directly to a `writer ixx` object: writer ixx; write_preamble(ixx); ixx.write("module;\n"); // global module fragment -ixx.write("#define WINRT_IMPL_GLOBAL_MODULE_FRAGMENT\n"); -ixx.write(strings::base_includes); // standard library #includes -ixx.write("#undef WINRT_IMPL_GLOBAL_MODULE_FRAGMENT\n"); +ixx.write(strings::base_includes); // platform includes (intrin.h, version, directxmath) +ixx.write(strings::base_std_includes); // standard library #includes ixx.write("\nexport module winrt;\n#define WINRT_EXPORT export\n\n"); for (auto ns : namespaces) @@ -61,46 +61,51 @@ ixx.flush_to_file("winrt/winrt.ixx"); ``` Key points: -- `strings::base_includes` is the *literal content* of `base_includes.h` (embedded - at compile time by the prebuild step). It's not an `#include` directive — it's - the raw text of the standard library includes. -- `WINRT_EXPORT` is defined as `export` inside the module purview. Every namespace - declaration in the generated headers uses `WINRT_EXPORT namespace winrt::...` so - types are properly exported. -- The namespace headers are `#include`d inside the module purview, making all their - content (types, template specializations) part of the module. +- `strings::base_includes` contains platform-specific headers (``, + ``, ``) — NOT standard library headers. +- `strings::base_std_includes` contains all standard library `#include` + directives (``, ``, etc.). These are always written + as raw `#include`s in the global module fragment where `import` is prohibited. +- `WINRT_EXPORT` is defined as `export` inside the module purview. +- The namespace headers are `#include`d inside the module purview, making all + their content (types, template specializations) part of the module. -## The Global Module Fragment Problem +## Split Standard Library Includes -**Problem**: The C++ standard prohibits `import` declarations in the global module -fragment (the section between `module;` and `export module winrt;`). +**Problem**: The C++ standard prohibits `import` declarations in the global +module fragment (the section between `module;` and `export module winrt;`). +The ixx needs raw `#include` directives there, but `base.h` consumers may +want `import std;` instead. -**Context**: `base_includes.h` conditionally emits `import std;` when -`WINRT_IMPORT_STD` is defined. If this triggers in the global module fragment, -compilation fails. +**Solution**: The standard library includes are split into a separate file: -**Solution**: The ixx writer wraps the base_includes content with a guard macro: +| File | Content | Used by | +|------|---------|--------| +| `strings/base_includes.h` | Platform headers: ``, ``, `` | Both ixx and base.h | +| `strings/base_std_includes.h` | Standard library: ``, ``, ``, etc. | ixx (always), base.h (conditional) | -```cpp -ixx.write("#define WINRT_IMPL_GLOBAL_MODULE_FRAGMENT\n"); -ixx.write(strings::base_includes); // import std; is suppressed -ixx.write("#undef WINRT_IMPL_GLOBAL_MODULE_FRAGMENT\n"); -``` +In `write_base_h()`, the std includes are written conditionally: -Inside `base_includes.h`: ```cpp -#if defined(__cpp_lib_modules) && defined(WINRT_IMPORT_STD) \ - && !defined(WINRT_IMPL_GLOBAL_MODULE_FRAGMENT) +w.write(strings::base_includes); // platform includes (always) +w.write(R"( +#if defined(__cpp_lib_modules) && defined(WINRT_IMPORT_STD) import std; #else -// ... individual #include directives -#endif +)"); +w.write(strings::base_std_includes); // std includes (fallback) +w.write(R"(#endif +)"); ``` -The three-way condition ensures `import std;` only fires when: -1. The compiler supports it (`__cpp_lib_modules`) -2. The user/build opted in (`WINRT_IMPORT_STD`) -3. We're not in the global module fragment (`!WINRT_IMPL_GLOBAL_MODULE_FRAGMENT`) +In the ixx writer, both are written unconditionally as raw includes: + +```cpp +ixx.write(strings::base_includes); // platform +ixx.write(strings::base_std_includes); // std (always #include in GMF) +``` + +This eliminates the need for any guard macros like `WINRT_IMPL_GLOBAL_MODULE_FRAGMENT`. ## base_macros.h: Why Macros Need Special Handling @@ -117,7 +122,8 @@ preprocessor definitions needed by generated code. It's safe to `#include` alongside `import winrt;` because it has no type declarations that could conflict with module-exported symbols. -**Generated by**: `write_base_macros_h()` in `file_writers.h` +**Generated by**: `write_base_macros_h()` in `file_writers.h`, using content from +`strings/base_module_macros.h` **Used by**: `.g.h` files in module mode: ```cpp @@ -161,6 +167,26 @@ remains useful for the scenario of consuming a component's projection header separately after `import winrt;`, should MSVC add support for cross-module template specialization in the future. +## WINRT_EXPORT on Extern Handlers + +**Source**: `strings/base_extern.h` + +The global function pointer variables used for error handling customization +(`winrt_to_hresult_handler`, `winrt_to_message_handler`, +`winrt_throw_hresult_handler`, `winrt_activation_handler`) are prefixed with +`WINRT_EXPORT`: + +```cpp +WINRT_EXPORT __declspec(selectany) std::int32_t(__stdcall* winrt_to_hresult_handler)(...) noexcept {}; +WINRT_EXPORT __declspec(selectany) winrt::hstring(__stdcall* winrt_to_message_handler)(...) {}; +// etc. +``` + +This ensures correct linkage when mixing modules and non-modules translation +units in the same DLL/EXE. Without `WINRT_EXPORT`, the module-compiled TUs +and the header-compiled TUs would see these as separate symbols, leading to +one side's customizations being invisible to the other. + ## The -module Flag: Component Code Generation **Source**: `settings.h` (`component_module`), `main.cpp` (arg parsing) @@ -247,14 +273,21 @@ matching the approach from the sylveon fork. ## import std Integration -**Source**: `base_includes.h` +**Source**: `strings/base_includes.h`, `strings/base_std_includes.h`, +`file_writers.h` (`write_base_h`) + +The standard library includes are split into two files: -The standard library `#include` directives in `base_includes.h` are conditionally -replaced with `import std;`. The conditions are: +- **`strings/base_includes.h`**: Platform headers (``, ``, + ``). Always written as `#include`. +- **`strings/base_std_includes.h`**: Standard library headers (``, + ``, ``, etc.). + +In `write_base_h()`, the std includes are conditional: ```cpp -#if defined(__cpp_lib_modules) && defined(WINRT_IMPORT_STD) \ - && !defined(WINRT_IMPL_GLOBAL_MODULE_FRAGMENT) +// In generated base.h: +#if defined(__cpp_lib_modules) && defined(WINRT_IMPORT_STD) import std; #else #include @@ -263,6 +296,9 @@ import std; #endif ``` +In the ixx global module fragment, both files are written as raw `#include`s +with no conditional — `import` is not permitted there. + `WINRT_IMPORT_STD` is: - Defined automatically by NuGet targets when `CppWinRTModule=true` and `BuildStlModules=true` @@ -286,8 +322,11 @@ would break existing users whose build systems don't compile the std module. | `component_writers.h` | Component template writers: `write_module_g_cpp`, `write_component_g_cpp` | | `code_writers.h` | Low-level writers: `write_version_assert`, `write_parent_depends`, `write_open_file_guard` | | `type_writers.h` | Writer class: `write_root_include`, `write_root_include_guarded`, `write_depends`, `write_depends_guarded` | -| `strings/base_includes.h` | Standard library includes / `import std;` conditional | -| `strings/base_macros.h` | `WINRT_EXPORT`, `windowsnumerics.impl.h` suppress | +| `strings/base_includes.h` | Platform includes (``, ``, ``) | +| `strings/base_std_includes.h` | Standard library includes (``, ``, ``, etc.) | +| `strings/base_macros.h` | `WINRT_EXPORT` definition, `windowsnumerics.impl.h` suppress | +| `strings/base_module_macros.h` | Lightweight macros for module consumers (`WINRT_EXPORT`, `WINRT_IMPL_EMPTY_BASES`, etc.) | +| `strings/base_extern.h` | `WINRT_EXPORT`-decorated extern handler variables | ### Key NuGet targets @@ -309,12 +348,11 @@ User sets CppWinRTModule=true + BuildStlModules=true ▼ winrt.ixx compilation: module; - #define WINRT_IMPL_GLOBAL_MODULE_FRAGMENT ← blocks import std; - ← falls through to #include path - #undef WINRT_IMPL_GLOBAL_MODULE_FRAGMENT + ← platform includes (base_includes) + ← std library includes (base_std_includes) export module winrt; #define WINRT_EXPORT export - #include "winrt/base.h" ← WINRT_IMPORT_STD not checked (pragma once) + #include "winrt/base.h" ← base.h (import std; not checked, pragma once) #include "winrt/Windows.Foundation.h" ← SDK types + specializations ... #include "winrt/MyComponent.h" ← component specializations in purview diff --git a/docs/modules.md b/docs/modules.md index 555bb1f2c..b020a854f 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -76,7 +76,7 @@ The NuGet targets automatically: | Property | Type | Default | Description | |----------|------|---------|-------------| | `CppWinRTModule` | bool | `false` | Enable C++20 module mode. Adds `winrt.ixx` to compilation, folds component projections into the module, and passes `-module` to cppwinrt.exe for component generation. | -| `BuildStlModules` | ClCompile metadata | `false` | Enables building `std.ixx`/`std.compat.ixx` so `import std;` works. Set inside ``. | +| `BuildStlModules` | ClCompile metadata | `false` | Enables building `std.ixx`/`std.compat.ixx` so `import std;` works. Set inside ``. This is the same project property set by "Build ISO C++23 Standard Library Modules" (https://learn.microsoft.com/en-us/cpp/build/reference/c-cpp-prop-page?view=msvc-180#cc-language-properties)| When `CppWinRTModule=true`, the NuGet targets also automatically: - Define `WINRT_IMPORT_STD` as a preprocessor definition when `BuildStlModules` @@ -205,10 +205,8 @@ These are used by the code generator and should not be set directly by users: | Macro | Purpose | |-------|---------| -| `WINRT_IMPL_GLOBAL_MODULE_FRAGMENT` | Set by the generated `winrt.ixx` around the `base_includes` content in the global module fragment (before `export module winrt;`). Prevents `import std;` from being emitted in the global module fragment where `import` declarations are not permitted by the C++ standard. | -| `WINRT_IMPL_IMPORT_STD` | Internal flag set when `WINRT_IMPORT_STD`, `__cpp_lib_modules`, and not-in-global-fragment conditions are all met. Guards the `#include` fallback path. | | `WINRT_IMPL_SKIP_INCLUDES` | When defined, generated namespace headers skip their cross-namespace `#include` dependencies (e.g., `#include "winrt/base.h"`, `#include "winrt/impl/Windows.Foundation.2.h"`). Used when those dependencies are already available from the `winrt` module. The component's own impl headers are never skipped. | -| `WINRT_EXPORT` | Expands to `export` inside `winrt.ixx` (module purview), empty in header mode. Used on namespace declarations so types are properly exported from the module. Defined in `base_macros.h` (as empty) for use in generated component files that operate alongside the module. | +| `WINRT_EXPORT` | Expands to `export` inside `winrt.ixx` (module purview), empty in header mode. Used on namespace declarations so types are properly exported from the module. Also applied to `winrt_to_hresult_handler` and related extern handler variables in `base_extern.h` for correct linkage when mixing modules and non-modules code in the same binary. Defined in `base_macros.h` (as empty) for use in generated component files that operate alongside the module. | | `WINRT_IMPL_EMPTY_BASES` | MSVC `__declspec(empty_bases)` optimization. Defined in both `base.h` and `base_macros.h`. | | `WINRT_IMPL_ABI_DECL` | Combines `WINRT_IMPL_NOVTABLE` and `WINRT_IMPL_PUBLIC` for ABI interface declarations. | | `CPPWINRT_VERSION` | Version string for header compatibility checking. Defined in `base.h` and `base_macros.h`. | @@ -238,10 +236,8 @@ winrt/ ``` module; ← Global module fragment -#define WINRT_IMPL_GLOBAL_MODULE_FRAGMENT - ← Raw #includes (import not allowed here) - ← Platform headers -#undef WINRT_IMPL_GLOBAL_MODULE_FRAGMENT + ← Platform includes (base_includes) + ← Standard library includes (base_std_includes) export module winrt; ← Module purview begins #define WINRT_EXPORT export diff --git a/strings/base_extern.h b/strings/base_extern.h index 84e2943c4..25c25e9d2 100644 --- a/strings/base_extern.h +++ b/strings/base_extern.h @@ -1,8 +1,8 @@ -__declspec(selectany) std::int32_t(__stdcall* winrt_to_hresult_handler)(void* address) noexcept {}; -__declspec(selectany) winrt::hstring(__stdcall* winrt_to_message_handler)(void* address) {}; -__declspec(selectany) void(__stdcall* winrt_throw_hresult_handler)(std::uint32_t lineNumber, char const* fileName, char const* functionName, void* returnAddress, winrt::hresult const result) noexcept {}; -__declspec(selectany) std::int32_t(__stdcall* winrt_activation_handler)(void* classId, winrt::guid const& iid, void** factory) noexcept {}; +WINRT_EXPORT __declspec(selectany) std::int32_t(__stdcall* winrt_to_hresult_handler)(void* address) noexcept {}; +WINRT_EXPORT __declspec(selectany) winrt::hstring(__stdcall* winrt_to_message_handler)(void* address) {}; +WINRT_EXPORT __declspec(selectany) void(__stdcall* winrt_throw_hresult_handler)(std::uint32_t lineNumber, char const* fileName, char const* functionName, void* returnAddress, winrt::hresult const result) noexcept {}; +WINRT_EXPORT __declspec(selectany) std::int32_t(__stdcall* winrt_activation_handler)(void* classId, winrt::guid const& iid, void** factory) noexcept {}; #if defined(_MSC_VER) #ifdef _M_HYBRID diff --git a/strings/base_includes.h b/strings/base_includes.h index 6e51dcf25..614cc4c84 100644 --- a/strings/base_includes.h +++ b/strings/base_includes.h @@ -5,67 +5,6 @@ #include #endif -// When WINRT_IMPORT_STD is defined, use 'import std;' instead of individual -// standard library #include directives. This requires C++20 or later with -// 'import std;' support enabled in the build system (e.g. BuildStlModules=true -// in MSBuild, or equivalent). Not used in the winrt.ixx global module fragment -// where 'import' is not permitted. -#if defined(__cpp_lib_modules) && defined(WINRT_IMPORT_STD) && !defined(WINRT_IMPL_GLOBAL_MODULE_FRAGMENT) -#define WINRT_IMPL_IMPORT_STD -import std; -#endif - -#ifndef WINRT_IMPL_IMPORT_STD -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef WINRT_LEAN_AND_MEAN -#include -#endif - -#ifdef __cpp_lib_span -#include -#endif - -#ifdef __cpp_lib_format -#include -#endif - -#ifdef __cpp_lib_source_location -#include -#endif - -#ifdef __cpp_lib_coroutine -#include -#elif defined(_RESUMABLE_FUNCTIONS_SUPPORTED) -#error "C++/WinRT no longer supports pre-standardization coroutines. If you use co_await, switch to /await:strict or upgrade to C++20. If you do not, remove /await from the compiler flags." -#endif -#endif // !WINRT_IMPL_IMPORT_STD - #if __has_include() #define WINRT_IMPL_NUMERICS #include diff --git a/strings/base_module_macros.h b/strings/base_module_macros.h new file mode 100644 index 000000000..2c9babfad --- /dev/null +++ b/strings/base_module_macros.h @@ -0,0 +1,33 @@ + +// This header provides the preprocessor macros needed by generated C++/WinRT +// headers when the full base.h is not #included (e.g., when using 'import winrt;'). +// Macros are not exported across C++20 module boundaries, so this lightweight +// header must be included textually. + +#ifndef WINRT_EXPORT +#define WINRT_EXPORT +#endif + +#if defined(_MSC_VER) +#define WINRT_IMPL_EMPTY_BASES __declspec(empty_bases) +#else +#define WINRT_IMPL_EMPTY_BASES +#endif + +#if defined(_MSC_VER) +#define WINRT_IMPL_NOVTABLE __declspec(novtable) +#else +#define WINRT_IMPL_NOVTABLE +#endif + +#if defined(__clang__) && defined(__has_attribute) +#if __has_attribute(__lto_visibility_public__) +#define WINRT_IMPL_PUBLIC __attribute__((lto_visibility_public)) +#else +#define WINRT_IMPL_PUBLIC +#endif +#else +#define WINRT_IMPL_PUBLIC +#endif + +#define WINRT_IMPL_ABI_DECL WINRT_IMPL_NOVTABLE WINRT_IMPL_PUBLIC diff --git a/strings/base_std_includes.h b/strings/base_std_includes.h new file mode 100644 index 000000000..19a69b9fc --- /dev/null +++ b/strings/base_std_includes.h @@ -0,0 +1,49 @@ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef WINRT_LEAN_AND_MEAN +#include +#endif + +#ifdef __cpp_lib_span +#include +#endif + +#ifdef __cpp_lib_format +#include +#endif + +#ifdef __cpp_lib_source_location +#include +#endif + +#ifdef __cpp_lib_coroutine +#include +#elif defined(_RESUMABLE_FUNCTIONS_SUPPORTED) +#error "C++/WinRT no longer supports pre-standardization coroutines. If you use co_await, switch to /await:strict or upgrade to C++20. If you do not, remove /await from the compiler flags." +#endif From 797fea75883590b0976e9cf9c40f90e64b01437a Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Mon, 30 Mar 2026 14:06:28 -0700 Subject: [PATCH 07/34] Explicitly include header in base_std_includes.h --- strings/base_std_includes.h | 1 + 1 file changed, 1 insertion(+) diff --git a/strings/base_std_includes.h b/strings/base_std_includes.h index 19a69b9fc..4da15b0c3 100644 --- a/strings/base_std_includes.h +++ b/strings/base_std_includes.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include From 5cddeb3a93a25f0e0097b85cd0ec6ad0e4e5b4ac Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Mon, 30 Mar 2026 14:54:43 -0700 Subject: [PATCH 08/34] Add base_std_includes.h to natvis project --- natvis/pch.h | 1 + 1 file changed, 1 insertion(+) diff --git a/natvis/pch.h b/natvis/pch.h index 95e561971..e47817d79 100644 --- a/natvis/pch.h +++ b/natvis/pch.h @@ -11,6 +11,7 @@ #include #include #include "base_includes.h" +#include "base_std_includes.h" #include "base_macros.h" #include "base_types.h" #include "base_extern.h" From da640ca54d51145d7ab6e2e988088195652ed1f0 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Mon, 30 Mar 2026 18:16:27 -0700 Subject: [PATCH 09/34] PR feedback: Reduce code duplication in base_macros.h, extern linkage on exported handlers --- cppwinrt/file_writers.h | 1 + strings/base_extern.h | 8 ++++---- strings/base_macros.h | 28 ---------------------------- 3 files changed, 5 insertions(+), 32 deletions(-) diff --git a/cppwinrt/file_writers.h b/cppwinrt/file_writers.h index e3ed4c628..27aa55942 100644 --- a/cppwinrt/file_writers.h +++ b/cppwinrt/file_writers.h @@ -19,6 +19,7 @@ import std; w.write(strings::base_std_includes); w.write(R"(#endif )"); + w.write(strings::base_module_macros); w.write(strings::base_macros); w.write(strings::base_types); w.write(strings::base_extern); diff --git a/strings/base_extern.h b/strings/base_extern.h index 25c25e9d2..5c5c9e2cf 100644 --- a/strings/base_extern.h +++ b/strings/base_extern.h @@ -1,8 +1,8 @@ -WINRT_EXPORT __declspec(selectany) std::int32_t(__stdcall* winrt_to_hresult_handler)(void* address) noexcept {}; -WINRT_EXPORT __declspec(selectany) winrt::hstring(__stdcall* winrt_to_message_handler)(void* address) {}; -WINRT_EXPORT __declspec(selectany) void(__stdcall* winrt_throw_hresult_handler)(std::uint32_t lineNumber, char const* fileName, char const* functionName, void* returnAddress, winrt::hresult const result) noexcept {}; -WINRT_EXPORT __declspec(selectany) std::int32_t(__stdcall* winrt_activation_handler)(void* classId, winrt::guid const& iid, void** factory) noexcept {}; +WINRT_EXPORT extern "C++" __declspec(selectany) std::int32_t(__stdcall* winrt_to_hresult_handler)(void* address) noexcept {}; +WINRT_EXPORT extern "C++" __declspec(selectany) winrt::hstring(__stdcall* winrt_to_message_handler)(void* address) {}; +WINRT_EXPORT extern "C++" __declspec(selectany) void(__stdcall* winrt_throw_hresult_handler)(std::uint32_t lineNumber, char const* fileName, char const* functionName, void* returnAddress, winrt::hresult const result) noexcept {}; +WINRT_EXPORT extern "C++" __declspec(selectany) std::int32_t(__stdcall* winrt_activation_handler)(void* classId, winrt::guid const& iid, void** factory) noexcept {}; #if defined(_MSC_VER) #ifdef _M_HYBRID diff --git a/strings/base_macros.h b/strings/base_macros.h index 91bd0a3a1..051e6fcfd 100644 --- a/strings/base_macros.h +++ b/strings/base_macros.h @@ -27,10 +27,6 @@ #define WINRT_IMPL_COROUTINES #endif -#ifndef WINRT_EXPORT -#define WINRT_EXPORT -#endif - #ifdef WINRT_IMPL_NUMERICS #define _WINDOWS_NUMERICS_NAMESPACE_ winrt::Windows::Foundation::Numerics #define _WINDOWS_NUMERICS_BEGIN_NAMESPACE_ WINRT_EXPORT namespace winrt::Windows::Foundation::Numerics @@ -51,30 +47,6 @@ #define WINRT_IMPL_NOINLINE #endif -#if defined(_MSC_VER) -#define WINRT_IMPL_EMPTY_BASES __declspec(empty_bases) -#else -#define WINRT_IMPL_EMPTY_BASES -#endif - -#if defined(_MSC_VER) -#define WINRT_IMPL_NOVTABLE __declspec(novtable) -#else -#define WINRT_IMPL_NOVTABLE -#endif - -#if defined(__clang__) && defined(__has_attribute) -#if __has_attribute(__lto_visibility_public__) -#define WINRT_IMPL_PUBLIC __attribute__((lto_visibility_public)) -#else -#define WINRT_IMPL_PUBLIC -#endif // __has_attribute(__lto_visibility_public__) -#else -#define WINRT_IMPL_PUBLIC -#endif - -#define WINRT_IMPL_ABI_DECL WINRT_IMPL_NOVTABLE WINRT_IMPL_PUBLIC - #if defined(__clang__) #define WINRT_IMPL_HAS_DECLSPEC_UUID __has_declspec_attribute(uuid) #elif defined(_MSC_VER) From f9b0871bd2c925c6d6700bfaeb19f4a2f9bfdbdf Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Tue, 31 Mar 2026 12:06:13 -0700 Subject: [PATCH 10/34] Add base_module_macros.h to natvis pch --- natvis/pch.h | 1 + 1 file changed, 1 insertion(+) diff --git a/natvis/pch.h b/natvis/pch.h index e47817d79..bda5dde50 100644 --- a/natvis/pch.h +++ b/natvis/pch.h @@ -12,6 +12,7 @@ #include #include "base_includes.h" #include "base_std_includes.h" +#include "base_module_macros.h" #include "base_macros.h" #include "base_types.h" #include "base_extern.h" From 21eb38c2cebaaae34bd294f9566ac7b26cb3e1f2 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Tue, 31 Mar 2026 14:30:35 -0700 Subject: [PATCH 11/34] Workaround access_token issue. More tests. --- strings/base_security.h | 51 +++++++++++-------- test/old_tests/UnitTests/Security.cpp | 8 +++ test/test/await_adapter.cpp | 7 +++ test/test/coro_threadpool.cpp | 4 ++ .../test_cpp20_module.vcxproj | 7 ++- 5 files changed, 53 insertions(+), 24 deletions(-) diff --git a/strings/base_security.h b/strings/base_security.h index a912487e4..799ec3e36 100644 --- a/strings/base_security.h +++ b/strings/base_security.h @@ -69,31 +69,38 @@ WINRT_EXPORT namespace winrt check_bool(WINRT_IMPL_SetThreadToken(nullptr, get())); } - auto operator()() const - { - struct guard - { - guard(access_token&& previous) noexcept : m_previous(std::move(previous)) - { - } - - ~guard() - { - m_previous.revert(); - } - - guard(guard const&) - { - // A Visual C++ compiler bug (550631) requires the copy constructor even though it is never called. - WINRT_ASSERT(false); - } + auto operator()() const; + }; +} - private: +WINRT_EXPORT namespace winrt +{ + struct access_token_guard + { + access_token_guard(handle&& previous) noexcept : m_previous(std::move(previous)) + { + } - access_token const m_previous; - }; + ~access_token_guard() + { + check_bool(WINRT_IMPL_SetThreadToken(nullptr, m_previous.get())); + } - return guard(impersonate()); + access_token_guard(access_token_guard const&) + { + // A Visual C++ compiler bug (550631) requires the copy constructor even though it is never called. + WINRT_ASSERT(false); } + + private: + // Work around modules bug that claims access_token is undefined/incomplete here. + handle m_previous; }; + + inline auto access_token::operator()() const + { + auto previous = thread(); + check_bool(WINRT_IMPL_SetThreadToken(nullptr, get())); + return access_token_guard(std::move(previous)); + } } diff --git a/test/old_tests/UnitTests/Security.cpp b/test/old_tests/UnitTests/Security.cpp index 6f11f32b2..69a5c9d14 100644 --- a/test/old_tests/UnitTests/Security.cpp +++ b/test/old_tests/UnitTests/Security.cpp @@ -1,6 +1,14 @@ +#ifndef WINRT_TEST_MODULES #include "pch.h" +#endif #include "catch.hpp" +#ifdef WINRT_TEST_MODULES +#include +import std; +import winrt; +#endif + using namespace winrt; using namespace Windows::Foundation; diff --git a/test/test/await_adapter.cpp b/test/test/await_adapter.cpp index dccb9ce3d..3f719f2dc 100644 --- a/test/test/await_adapter.cpp +++ b/test/test/await_adapter.cpp @@ -1,5 +1,12 @@ +#ifdef WINRT_TEST_MODULES +#include +#include +import std; +import winrt; +#else #include "pch.h" #include "winrt/Windows.System.h" +#endif using namespace std::literals; using namespace winrt; diff --git a/test/test/coro_threadpool.cpp b/test/test/coro_threadpool.cpp index 4c31f8a03..1f2e90ef6 100644 --- a/test/test/coro_threadpool.cpp +++ b/test/test/coro_threadpool.cpp @@ -1,8 +1,12 @@ // Intentionally not using pch... #include "catch.hpp" +#ifdef WINRT_TEST_MODULES +import winrt; +#else // Only need winrt/base.h for coroutine thread pool support. #include "winrt/base.h" +#endif using namespace winrt; diff --git a/test/test_cpp20_module/test_cpp20_module.vcxproj b/test/test_cpp20_module/test_cpp20_module.vcxproj index 3958057f0..f6f7a78be 100644 --- a/test/test_cpp20_module/test_cpp20_module.vcxproj +++ b/test/test_cpp20_module/test_cpp20_module.vcxproj @@ -99,7 +99,7 @@ $(OutputPath);..\;%(AdditionalIncludeDirectories) - NOMINMAX;WINRT_LEAN_AND_MEAN;WINRT_IMPORT_STD;%(PreprocessorDefinitions) + NOMINMAX;WINRT_LEAN_AND_MEAN;WINRT_IMPORT_STD;WINRT_TEST_MODULES;%(PreprocessorDefinitions) NotUsing true @@ -127,6 +127,9 @@ + + + @@ -134,4 +137,4 @@ - + \ No newline at end of file From 3ea319bccf54828b5bf8197a8f0415d3649d823d Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Tue, 31 Mar 2026 14:50:19 -0700 Subject: [PATCH 12/34] Revert dual-build of await_adapter.cpp --- test/test/await_adapter.cpp | 7 ------- test/test_cpp20_module/test_cpp20_module.vcxproj | 1 - 2 files changed, 8 deletions(-) diff --git a/test/test/await_adapter.cpp b/test/test/await_adapter.cpp index 3f719f2dc..dccb9ce3d 100644 --- a/test/test/await_adapter.cpp +++ b/test/test/await_adapter.cpp @@ -1,12 +1,5 @@ -#ifdef WINRT_TEST_MODULES -#include -#include -import std; -import winrt; -#else #include "pch.h" #include "winrt/Windows.System.h" -#endif using namespace std::literals; using namespace winrt; diff --git a/test/test_cpp20_module/test_cpp20_module.vcxproj b/test/test_cpp20_module/test_cpp20_module.vcxproj index f6f7a78be..ec56dfcb1 100644 --- a/test/test_cpp20_module/test_cpp20_module.vcxproj +++ b/test/test_cpp20_module/test_cpp20_module.vcxproj @@ -128,7 +128,6 @@ - From 9c021d23441a195c8749a2ae6d43203cca74c669 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Tue, 31 Mar 2026 15:19:49 -0700 Subject: [PATCH 13/34] Fix some more test pch issues --- test/old_tests/UnitTests/Security.cpp | 8 ---- test/test/coro_threadpool.cpp | 2 +- test/test_cpp20_module/async.cpp | 2 +- test/test_cpp20_module/interop.cpp | 3 +- test/test_cpp20_module/pch.cpp | 1 + test/test_cpp20_module/pch.h | 4 ++ .../test_cpp20_module.vcxproj | 41 ++++++++++++++++--- 7 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 test/test_cpp20_module/pch.cpp create mode 100644 test/test_cpp20_module/pch.h diff --git a/test/old_tests/UnitTests/Security.cpp b/test/old_tests/UnitTests/Security.cpp index 69a5c9d14..6f11f32b2 100644 --- a/test/old_tests/UnitTests/Security.cpp +++ b/test/old_tests/UnitTests/Security.cpp @@ -1,14 +1,6 @@ -#ifndef WINRT_TEST_MODULES #include "pch.h" -#endif #include "catch.hpp" -#ifdef WINRT_TEST_MODULES -#include -import std; -import winrt; -#endif - using namespace winrt; using namespace Windows::Foundation; diff --git a/test/test/coro_threadpool.cpp b/test/test/coro_threadpool.cpp index 1f2e90ef6..d41377f69 100644 --- a/test/test/coro_threadpool.cpp +++ b/test/test/coro_threadpool.cpp @@ -1,5 +1,5 @@ // Intentionally not using pch... -#include "catch.hpp" +#include #ifdef WINRT_TEST_MODULES import winrt; diff --git a/test/test_cpp20_module/async.cpp b/test/test_cpp20_module/async.cpp index d29e64f53..6c1493968 100644 --- a/test/test_cpp20_module/async.cpp +++ b/test/test_cpp20_module/async.cpp @@ -1,6 +1,6 @@ // All #include directives must come before module imports to avoid // redefinition conflicts with types already in the module. -#include "catch.hpp" +#include "pch.h" import std; import winrt; diff --git a/test/test_cpp20_module/interop.cpp b/test/test_cpp20_module/interop.cpp index 368befe30..0d6d30ab3 100644 --- a/test/test_cpp20_module/interop.cpp +++ b/test/test_cpp20_module/interop.cpp @@ -1,5 +1,4 @@ -#include "catch.hpp" -#include +#include "pch.h" import std; import winrt; diff --git a/test/test_cpp20_module/pch.cpp b/test/test_cpp20_module/pch.cpp new file mode 100644 index 000000000..1d9f38c57 --- /dev/null +++ b/test/test_cpp20_module/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/test/test_cpp20_module/pch.h b/test/test_cpp20_module/pch.h new file mode 100644 index 000000000..a619a47eb --- /dev/null +++ b/test/test_cpp20_module/pch.h @@ -0,0 +1,4 @@ +#pragma once + +#include +#include diff --git a/test/test_cpp20_module/test_cpp20_module.vcxproj b/test/test_cpp20_module/test_cpp20_module.vcxproj index ec56dfcb1..79676b537 100644 --- a/test/test_cpp20_module/test_cpp20_module.vcxproj +++ b/test/test_cpp20_module/test_cpp20_module.vcxproj @@ -100,7 +100,7 @@ $(OutputPath);..\;%(AdditionalIncludeDirectories) NOMINMAX;WINRT_LEAN_AND_MEAN;WINRT_IMPORT_STD;WINRT_TEST_MODULES;%(PreprocessorDefinitions) - NotUsing + Use true @@ -126,12 +126,43 @@ - - - + + NotUsing + NotUsing + NotUsing + NotUsing + NotUsing + NotUsing + + + NotUsing + NotUsing + NotUsing + NotUsing + NotUsing + NotUsing + - + + NotUsing + NotUsing + NotUsing + NotUsing + NotUsing + NotUsing + + + Create + Create + Create + Create + Create + Create + + + + From 2ea8a9a47ed5812fc461904e260b857337d6432c Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Tue, 31 Mar 2026 15:32:20 -0700 Subject: [PATCH 14/34] Update module limitation docs --- docs/modules.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/docs/modules.md b/docs/modules.md index b020a854f..03ad82ba0 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -316,21 +316,31 @@ the component projection generation. headers must come before `import std;` and `import winrt;` to avoid redefinition errors. -3. **No PCH with modules**: Module interface units cannot use precompiled - headers. Component source files using `import winrt;` should set - `NotUsing`. +3. **PCH compatibility**: The module interface unit (`winrt.ixx`) cannot use a + precompiled header. Regular source files that `import winrt;` CAN use a PCH, + but the PCH must not include any `winrt/` headers (e.g., `winrt/base.h`, + `winrt/Windows.Foundation.h`) since those types come from the module. A PCH + containing third-party headers, Windows SDK headers, or other project headers + is fine. 4. **WINRT_LEAN_AND_MEAN for consumers**: Pure consumer apps (no component authoring) should define `WINRT_LEAN_AND_MEAN` to avoid pulling `` and `produce<>` templates into the module. Component authors should NOT define it since they need `produce<>`. -5. **Cross-module template specialization**: MSVC does not currently support - specializing module-imported class templates from textual `#include` code. - The combined-ixx approach (folding component headers into `winrt.ixx`) - works around this by keeping specializations within the module purview. - If you need to include a component's projection header separately (outside - the module), this will fail with: +5. **Cross-module template specialization**: Component projection headers + (`.0.h` files) specialize templates like `winrt::impl::category`, + `winrt::impl::abi`, `winrt::impl::guid_v`, etc. These templates are in the + `winrt::impl` namespace, which is currently not exported from the module + (it uses module linkage). When a textual `#include` of a component header + tries to specialize these after `import winrt;`, the primary templates are + not visible: ``` error C3856: 'category': symbol is not a class template ``` + The combined-ixx approach (folding component headers into `winrt.ixx`) + works around this by keeping specializations within the module purview, + where the `impl` templates are directly visible. A future improvement could + export the specific `impl` templates that need external specialization, + which would allow component headers to be included separately after + `import winrt;` without folding them into the module. From afe92afff8e113cb696d84958133496ea9764b88 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Tue, 31 Mar 2026 18:53:03 -0700 Subject: [PATCH 15/34] Export impl namespace to get component authoring to work. --- cppwinrt.sln | 26 +++- cppwinrt/code_writers.h | 2 +- cppwinrt/file_writers.h | 13 +- strings/base_abi.h | 2 +- strings/base_activation.h | 6 +- strings/base_agile_ref.h | 2 +- strings/base_array.h | 2 +- strings/base_collections.h | 2 +- strings/base_collections_base.h | 2 +- strings/base_collections_input_iterable.h | 2 +- strings/base_collections_input_map.h | 2 +- strings/base_collections_input_map_view.h | 2 +- strings/base_collections_input_vector.h | 2 +- strings/base_collections_input_vector_view.h | 2 +- strings/base_collections_map.h | 2 +- strings/base_collections_vector.h | 2 +- strings/base_com_ptr.h | 4 +- strings/base_composable.h | 2 +- strings/base_coroutine_foundation.h | 4 +- strings/base_coroutine_threadpool.h | 4 +- strings/base_delegate.h | 2 +- strings/base_error.h | 4 +- strings/base_events.h | 2 +- strings/base_fast_forward.h | 2 +- strings/base_foundation.h | 2 +- strings/base_identity.h | 2 +- strings/base_implements.h | 6 +- strings/base_iterator.h | 2 +- strings/base_macros.h | 2 +- strings/base_marshaler.h | 2 +- strings/base_meta.h | 2 +- strings/base_natvis.h | 2 +- strings/base_reference_produce.h | 4 +- strings/base_std_hash.h | 2 +- strings/base_string.h | 4 +- strings/base_string_input.h | 2 +- strings/base_string_operators.h | 2 +- strings/base_types.h | 4 +- strings/base_windows.h | 2 +- strings/base_xaml_typename.h | 2 +- .../test_component_module.vcxproj | 10 +- .../test_cpp20_module.vcxproj | 12 +- .../test_module_winrt.vcxproj | 133 ++++++++++++++++++ 43 files changed, 224 insertions(+), 68 deletions(-) create mode 100644 test/test_module_winrt/test_module_winrt.vcxproj diff --git a/cppwinrt.sln b/cppwinrt.sln index 2395b9102..d92318aa9 100644 --- a/cppwinrt.sln +++ b/cppwinrt.sln @@ -126,7 +126,17 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_nocoro", "test\test_no EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_cpp20_module", "test\test_cpp20_module\test_cpp20_module.vcxproj", "{B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}" ProjectSection(ProjectDependencies) = postProject - {A91B8BF3-28E4-4D9E-8DBA-64B70E4F0270} = {A91B8BF3-28E4-4D9E-8DBA-64B70E4F0270} + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C} = {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_module_winrt", "test\test_module_winrt\test_module_winrt.vcxproj", "{F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}" + ProjectSection(ProjectDependencies) = postProject + {D613FB39-5035-4043-91E2-BAB323908AF4} = {D613FB39-5035-4043-91E2-BAB323908AF4} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_component_module", "test\test_component_module\test_component_module.vcxproj", "{E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}" + ProjectSection(ProjectDependencies) = postProject + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C} = {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C} {D613FB39-5035-4043-91E2-BAB323908AF4} = {D613FB39-5035-4043-91E2-BAB323908AF4} EndProjectSection EndProject @@ -423,6 +433,18 @@ Global {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|ARM64.ActiveCfg = Release|ARM64 {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|x64.ActiveCfg = Release|x64 {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|x86.ActiveCfg = Release|Win32 + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Debug|x64.ActiveCfg = Debug|x64 + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Debug|x86.ActiveCfg = Debug|Win32 + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Release|ARM64.ActiveCfg = Release|ARM64 + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Release|x64.ActiveCfg = Release|x64 + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Release|x86.ActiveCfg = Release|Win32 + {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Debug|x64.ActiveCfg = Debug|x64 + {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Debug|x86.ActiveCfg = Debug|Win32 + {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Release|ARM64.ActiveCfg = Release|ARM64 + {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Release|x64.ActiveCfg = Release|x64 + {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Release|x86.ActiveCfg = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -448,6 +470,8 @@ Global {D4C8F881-84D5-4A7B-8BDE-AB4E34A05374} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0} {9E392830-805A-4AAF-932D-C493143EFACA} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0} {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0} + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0} + {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2783B8FD-EA3B-4D6B-9F81-662D289E02AA} diff --git a/cppwinrt/code_writers.h b/cppwinrt/code_writers.h index adce461b6..98ed795f5 100644 --- a/cppwinrt/code_writers.h +++ b/cppwinrt/code_writers.h @@ -178,7 +178,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 { )"; diff --git a/cppwinrt/file_writers.h b/cppwinrt/file_writers.h index 27aa55942..cfdd83513 100644 --- a/cppwinrt/file_writers.h +++ b/cppwinrt/file_writers.h @@ -281,13 +281,18 @@ import std; if (settings.component_module) { - // In module mode, both SDK and component types are available from - // 'import winrt;' (the component projection is folded into the - // winrt.ixx module). Only macros need a textual include since - // they don't cross module boundaries. + // In module mode, SDK types and exported impl templates come from + // 'import winrt;'. Component-specific types come from the component's + // own projection headers, included with WINRT_IMPL_SKIP_INCLUDES to + // skip SDK dependencies that are already in the module. w.write("#include \"winrt/base_macros.h\"\n"); w.write("import std;\n"); w.write("import winrt;\n"); + w.write("#define WINRT_IMPL_SKIP_INCLUDES\n"); + for (auto&& depends : w.depends) + { + w.write_depends(depends.first); + } } else { diff --git a/strings/base_abi.h b/strings/base_abi.h index 72946a3fa..4b7b8f77f 100644 --- a/strings/base_abi.h +++ b/strings/base_abi.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template <> struct abi { diff --git a/strings/base_activation.h b/strings/base_activation.h index 1a195d865..c657c692d 100644 --- a/strings/base_activation.h +++ b/strings/base_activation.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { struct library_traits { @@ -125,7 +125,7 @@ WINRT_EXPORT namespace winrt #define WINRT_IMPL_INTERLOCKED_READ_MEMORY_BARRIER (__dmb(_ARM64_BARRIER_ISH)); #endif -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { inline std::int32_t interlocked_read_32(std::int32_t const volatile* target) noexcept { @@ -548,7 +548,7 @@ WINRT_EXPORT namespace winrt } } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template T fast_activate(Windows::Foundation::IActivationFactory const& factory) diff --git a/strings/base_agile_ref.h b/strings/base_agile_ref.h index 88fbea065..14447706a 100644 --- a/strings/base_agile_ref.h +++ b/strings/base_agile_ref.h @@ -47,7 +47,7 @@ WINRT_EXPORT namespace winrt #endif } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct module_lock_updater; diff --git a/strings/base_array.h b/strings/base_array.h index 9544dcf8c..c5dca1b90 100644 --- a/strings/base_array.h +++ b/strings/base_array.h @@ -483,7 +483,7 @@ WINRT_EXPORT namespace winrt } } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct array_size_proxy diff --git a/strings/base_collections.h b/strings/base_collections.h index 4e1af51eb..7d8dc2e77 100644 --- a/strings/base_collections.h +++ b/strings/base_collections.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { namespace wfc = Windows::Foundation::Collections; diff --git a/strings/base_collections_base.h b/strings/base_collections_base.h index 6fe10fa64..d299cc0c8 100644 --- a/strings/base_collections_base.h +++ b/strings/base_collections_base.h @@ -1,4 +1,4 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { struct nop_lock_guard {}; diff --git a/strings/base_collections_input_iterable.h b/strings/base_collections_input_iterable.h index e75211c30..e9d3af251 100644 --- a/strings/base_collections_input_iterable.h +++ b/strings/base_collections_input_iterable.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct input_iterable : diff --git a/strings/base_collections_input_map.h b/strings/base_collections_input_map.h index b33975fe6..3fe146bf6 100644 --- a/strings/base_collections_input_map.h +++ b/strings/base_collections_input_map.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct map_impl : diff --git a/strings/base_collections_input_map_view.h b/strings/base_collections_input_map_view.h index bfd8d82a9..d79eed61d 100644 --- a/strings/base_collections_input_map_view.h +++ b/strings/base_collections_input_map_view.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct input_map_view : diff --git a/strings/base_collections_input_vector.h b/strings/base_collections_input_vector.h index b5b76de38..a06e73b33 100644 --- a/strings/base_collections_input_vector.h +++ b/strings/base_collections_input_vector.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct vector_impl : diff --git a/strings/base_collections_input_vector_view.h b/strings/base_collections_input_vector_view.h index 3793e239a..30768c18e 100644 --- a/strings/base_collections_input_vector_view.h +++ b/strings/base_collections_input_vector_view.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct input_vector_view : diff --git a/strings/base_collections_map.h b/strings/base_collections_map.h index fa769fb88..b40247dab 100644 --- a/strings/base_collections_map.h +++ b/strings/base_collections_map.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template using multi_threaded_map = map_impl; diff --git a/strings/base_collections_vector.h b/strings/base_collections_vector.h index 3e9c1b254..3388806af 100644 --- a/strings/base_collections_vector.h +++ b/strings/base_collections_vector.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template using multi_threaded_vector = vector_impl; diff --git a/strings/base_com_ptr.h b/strings/base_com_ptr.h index 27496789a..0f02fabeb 100644 --- a/strings/base_com_ptr.h +++ b/strings/base_com_ptr.h @@ -5,7 +5,7 @@ WINRT_EXPORT namespace winrt struct com_ptr; } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { struct capture_decay { @@ -349,7 +349,7 @@ WINRT_EXPORT namespace winrt } } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template std::int32_t capture_to(void** result, com_ptr const& object, M method, Args&& ...args) diff --git a/strings/base_composable.h b/strings/base_composable.h index e606d1292..5a7712ef7 100644 --- a/strings/base_composable.h +++ b/strings/base_composable.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct composable_factory diff --git a/strings/base_coroutine_foundation.h b/strings/base_coroutine_foundation.h index 4f1c8d0b6..cde752aa3 100644 --- a/strings/base_coroutine_foundation.h +++ b/strings/base_coroutine_foundation.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct async_completed_handler; @@ -312,7 +312,7 @@ WINRT_EXPORT namespace winrt } } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct cancellation_token diff --git a/strings/base_coroutine_threadpool.h b/strings/base_coroutine_threadpool.h index 6748906ca..3ffcf7002 100644 --- a/strings/base_coroutine_threadpool.h +++ b/strings/base_coroutine_threadpool.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { #ifdef WINRT_IMPL_COROUTINES inline auto submit_threadpool_callback(void(__stdcall* callback)(void*, void* context), void* context) @@ -321,7 +321,7 @@ WINRT_EXPORT namespace winrt }; } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { struct apartment_awaiter { diff --git a/strings/base_delegate.h b/strings/base_delegate.h index 1cfe58710..1fe00ecd1 100644 --- a/strings/base_delegate.h +++ b/strings/base_delegate.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { #if defined(_MSC_VER) #pragma warning(push) diff --git a/strings/base_error.h b/strings/base_error.h index c58635e5d..d42ca516b 100644 --- a/strings/base_error.h +++ b/strings/base_error.h @@ -7,7 +7,7 @@ #define WINRT_IMPL_RETURNADDRESS() nullptr #endif -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { struct heap_traits { @@ -536,7 +536,7 @@ WINRT_EXPORT namespace winrt } } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { inline hresult check_hresult_allow_bounds(hresult const result, winrt::impl::slim_source_location const& sourceInformation = winrt::impl::slim_source_location::current()) { diff --git a/strings/base_events.h b/strings/base_events.h index f7e2e6976..c21124e9c 100644 --- a/strings/base_events.h +++ b/strings/base_events.h @@ -130,7 +130,7 @@ WINRT_EXPORT namespace winrt }; } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct event_revoker diff --git a/strings/base_fast_forward.h b/strings/base_fast_forward.h index dc89fe6ce..c7cf6c3be 100644 --- a/strings/base_fast_forward.h +++ b/strings/base_fast_forward.h @@ -30,7 +30,7 @@ static_assert(WINRT_FAST_ABI_SIZE >= %); #pragma detect_mismatch("WINRT_FAST_ABI_SIZE", WINRT_IMPL_STRING(WINRT_FAST_ABI_SIZE)) -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { // Thunk definitions are in arch-specific assembly sources % diff --git a/strings/base_foundation.h b/strings/base_foundation.h index ea8881edc..083252753 100644 --- a/strings/base_foundation.h +++ b/strings/base_foundation.h @@ -100,7 +100,7 @@ WINRT_EXPORT namespace winrt::Windows::Foundation } } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template <> inline constexpr auto& name_v = L"Windows.Foundation.Point"; template <> inline constexpr auto& name_v = L"Windows.Foundation.Size"; diff --git a/strings/base_identity.h b/strings/base_identity.h index 30830bc5a..7c61c83e2 100644 --- a/strings/base_identity.h +++ b/strings/base_identity.h @@ -17,7 +17,7 @@ WINRT_EXPORT namespace winrt } } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template constexpr std::array to_array(T const* value, std::index_sequence const) noexcept diff --git a/strings/base_implements.h b/strings/base_implements.h index 7edf32149..0eb8db0bd 100644 --- a/strings/base_implements.h +++ b/strings/base_implements.h @@ -6,7 +6,7 @@ #endif #endif -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { struct marker { @@ -30,7 +30,7 @@ WINRT_EXPORT namespace winrt struct implements; } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template using tuple_cat_t = decltype(std::tuple_cat(std::declval()...)); @@ -267,7 +267,7 @@ WINRT_EXPORT namespace winrt } } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct interface_list; diff --git a/strings/base_iterator.h b/strings/base_iterator.h index acb0ebd42..46c0c6b63 100644 --- a/strings/base_iterator.h +++ b/strings/base_iterator.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct fast_iterator diff --git a/strings/base_macros.h b/strings/base_macros.h index 051e6fcfd..ccb47fda9 100644 --- a/strings/base_macros.h +++ b/strings/base_macros.h @@ -114,7 +114,7 @@ typedef struct _GUID GUID; #define WINRT_IMPL_BUILTIN_FUNCTION nullptr #endif -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { // This struct is intended to be highly similar to std::source_location. The key difference is // that function_name is NOT included. Function names do not fold to identical strings and can diff --git a/strings/base_marshaler.h b/strings/base_marshaler.h index 526f4d4c3..9be6959c9 100644 --- a/strings/base_marshaler.h +++ b/strings/base_marshaler.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { inline std::int32_t make_marshaler(unknown_abi* outer, void** result) noexcept { diff --git a/strings/base_meta.h b/strings/base_meta.h index 7dbb4c386..6b28640ef 100644 --- a/strings/base_meta.h +++ b/strings/base_meta.h @@ -48,7 +48,7 @@ WINRT_EXPORT namespace winrt } } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { using namespace std::literals; diff --git a/strings/base_natvis.h b/strings/base_natvis.h index 60c5f5548..9e78563cb 100644 --- a/strings/base_natvis.h +++ b/strings/base_natvis.h @@ -5,7 +5,7 @@ #ifdef WINRT_NATVIS -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { struct natvis { diff --git a/strings/base_reference_produce.h b/strings/base_reference_produce.h index 570ba42e0..c341e8d20 100644 --- a/strings/base_reference_produce.h +++ b/strings/base_reference_produce.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct reference : implements, Windows::Foundation::IReference, Windows::Foundation::IPropertyValue> @@ -420,7 +420,7 @@ WINRT_EXPORT namespace winrt::Windows::Foundation } } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template T unbox_value_type(From&& value) diff --git a/strings/base_std_hash.h b/strings/base_std_hash.h index 864c31b13..cdffb13c6 100644 --- a/strings/base_std_hash.h +++ b/strings/base_std_hash.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { inline std::size_t hash_data(void const* ptr, std::size_t const bytes) noexcept { diff --git a/strings/base_string.h b/strings/base_string.h index c2ba6c742..92e2ffc6d 100644 --- a/strings/base_string.h +++ b/strings/base_string.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { struct atomic_ref_count { @@ -442,7 +442,7 @@ template<> struct std::formatter : std::formatter {}; #endif -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template <> struct abi { diff --git a/strings/base_string_input.h b/strings/base_string_input.h index 5ac0221f6..71cd5f3c0 100644 --- a/strings/base_string_input.h +++ b/strings/base_string_input.h @@ -65,7 +65,7 @@ WINRT_EXPORT namespace winrt::param } } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template using param_type = std::conditional_t, param::hstring, T>; diff --git a/strings/base_string_operators.h b/strings/base_string_operators.h index 25e2eccce..223769d01 100644 --- a/strings/base_string_operators.h +++ b/strings/base_string_operators.h @@ -94,7 +94,7 @@ WINRT_EXPORT namespace winrt bool operator>=(std::nullptr_t left, hstring const& right) = delete; } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { inline hstring concat_hstring(std::wstring_view const& left, std::wstring_view const& right) { diff --git a/strings/base_types.h b/strings/base_types.h index 18529e116..dc2b13632 100644 --- a/strings/base_types.h +++ b/strings/base_types.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { using ptp_io = struct tp_io*; using ptp_timer = struct tp_timer*; @@ -208,7 +208,7 @@ WINRT_EXPORT namespace winrt::Windows::Foundation using DateTime = std::chrono::time_point; } -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { #ifdef WINRT_IMPL_IUNKNOWN_DEFINED using hresult_type = long; diff --git a/strings/base_windows.h b/strings/base_windows.h index 21c4163e5..dcd164574 100644 --- a/strings/base_windows.h +++ b/strings/base_windows.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { #ifdef WINRT_DIAGNOSTICS diff --git a/strings/base_xaml_typename.h b/strings/base_xaml_typename.h index b7b3a954a..2cc45b0bb 100644 --- a/strings/base_xaml_typename.h +++ b/strings/base_xaml_typename.h @@ -1,5 +1,5 @@ -namespace winrt::impl +WINRT_EXPORT namespace winrt::impl { template struct xaml_typename_name diff --git a/test/test_component_module/test_component_module.vcxproj b/test/test_component_module/test_component_module.vcxproj index e6e8887bd..7c1bd442f 100644 --- a/test/test_component_module/test_component_module.vcxproj +++ b/test/test_component_module/test_component_module.vcxproj @@ -97,13 +97,15 @@ - .;$(OutputPath);Generated Files + .;$(ProjectDir)Generated Files;$(SolutionDir)test\test_module_winrt\Generated Files;Generated Files NOMINMAX;%(PreprocessorDefinitions) NotUsing true + $(IntDir)..\test_module_winrt\winrt.ixx.ifc;%(AdditionalModuleDependencies) exports.def + $(IntDir)..\test_module_winrt\winrt.ixx.obj;%(AdditionalDependencies) true @@ -115,10 +117,9 @@ - $(CppWinRTDir)cppwinrt -in local -out $(OutputPath) -verbose - $(CppWinRTDir)cppwinrt -input $(OutputPath)test_component_module.winmd -out $(OutputPath) -ref sdk -verbose + $(CppWinRTDir)cppwinrt -in local -out "$(ProjectDir)Generated Files" -verbose + $(CppWinRTDir)cppwinrt -input $(OutputPath)test_component_module.winmd -out "$(ProjectDir)Generated Files" -ref sdk -verbose $(CppWinRTDir)cppwinrt -input $(OutputPath)test_component_module.winmd -comp "$(ProjectDir)Generated Files" -out "$(ProjectDir)Generated Files" -include test_component_module -ref sdk -verbose -prefix -opt -lib test_module -overwrite -name test_component_module -module - echo #include "winrt/test_component_module.h">> "$(OutputPath)winrt\winrt.ixx" Projecting Windows and component metadata $(OutputPath)\winrt\base.h;Generated Files\module.g.cpp @@ -144,7 +145,6 @@ - diff --git a/test/test_cpp20_module/test_cpp20_module.vcxproj b/test/test_cpp20_module/test_cpp20_module.vcxproj index 79676b537..cce09ab8d 100644 --- a/test/test_cpp20_module/test_cpp20_module.vcxproj +++ b/test/test_cpp20_module/test_cpp20_module.vcxproj @@ -98,13 +98,15 @@ --> - $(OutputPath);..\;%(AdditionalIncludeDirectories) + $(SolutionDir)test\test_module_winrt\Generated Files;..\;%(AdditionalIncludeDirectories) NOMINMAX;WINRT_LEAN_AND_MEAN;WINRT_IMPORT_STD;WINRT_TEST_MODULES;%(PreprocessorDefinitions) Use true + $(IntDir)..\test_module_winrt\winrt.ixx.ifc;%(AdditionalModuleDependencies) Console + $(IntDir)..\test_module_winrt\winrt.ixx.obj;%(AdditionalDependencies) @@ -126,14 +128,6 @@ - - NotUsing - NotUsing - NotUsing - NotUsing - NotUsing - NotUsing - NotUsing NotUsing diff --git a/test/test_module_winrt/test_module_winrt.vcxproj b/test/test_module_winrt/test_module_winrt.vcxproj new file mode 100644 index 000000000..5e427ffc9 --- /dev/null +++ b/test/test_module_winrt/test_module_winrt.vcxproj @@ -0,0 +1,133 @@ + + + + + Debug + ARM64 + + + Debug + Win32 + + + Release + ARM64 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C} + test_module_winrt + test_module_winrt + 20 + + + + StaticLibrary + true + v145 + + + StaticLibrary + true + v145 + + + StaticLibrary + false + true + v145 + + + StaticLibrary + false + true + v145 + + + StaticLibrary + true + v145 + + + StaticLibrary + false + true + v145 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(ProjectDir)Generated Files;%(AdditionalIncludeDirectories) + NotUsing + true + + + $(CppWinRTDir)cppwinrt -in sdk -out "$(ProjectDir)Generated Files" -verbose + Generating C++/WinRT SDK projection + + + + + Disabled + MultiThreadedDebug + + + + + MaxSpeed + true + true + MultiThreaded + + + + + + + + + From 8abb336f1d6f979f85709511344b9540e728ecb9 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Wed, 1 Apr 2026 11:42:32 -0700 Subject: [PATCH 16/34] Enable VS2022 support! --- test/test/coro_threadpool.cpp | 1 + .../test_component_module.vcxproj | 12 ++++++------ test/test_cpp20_module/async.cpp | 1 - test/test_cpp20_module/interop.cpp | 1 - test/test_cpp20_module/main.cpp | 1 - test/test_cpp20_module/pch.h | 6 ++++++ test/test_cpp20_module/test_cpp20_module.vcxproj | 12 ++++++------ test/test_module_winrt/test_module_winrt.vcxproj | 12 ++++++------ 8 files changed, 25 insertions(+), 21 deletions(-) diff --git a/test/test/coro_threadpool.cpp b/test/test/coro_threadpool.cpp index d41377f69..42e6fb234 100644 --- a/test/test/coro_threadpool.cpp +++ b/test/test/coro_threadpool.cpp @@ -1,5 +1,6 @@ // Intentionally not using pch... #include +#include #ifdef WINRT_TEST_MODULES import winrt; diff --git a/test/test_component_module/test_component_module.vcxproj b/test/test_component_module/test_component_module.vcxproj index 7c1bd442f..7adf3a1ab 100644 --- a/test/test_component_module/test_component_module.vcxproj +++ b/test/test_component_module/test_component_module.vcxproj @@ -37,35 +37,35 @@ DynamicLibrary true - v145 + v143 DynamicLibrary true - v145 + v143 DynamicLibrary false true - v145 + v143 DynamicLibrary false true - v145 + v143 DynamicLibrary true - v145 + v143 DynamicLibrary false true - v145 + v143 diff --git a/test/test_cpp20_module/async.cpp b/test/test_cpp20_module/async.cpp index 6c1493968..285fc2030 100644 --- a/test/test_cpp20_module/async.cpp +++ b/test/test_cpp20_module/async.cpp @@ -2,7 +2,6 @@ // redefinition conflicts with types already in the module. #include "pch.h" -import std; import winrt; using namespace winrt; diff --git a/test/test_cpp20_module/interop.cpp b/test/test_cpp20_module/interop.cpp index 0d6d30ab3..e265bd800 100644 --- a/test/test_cpp20_module/interop.cpp +++ b/test/test_cpp20_module/interop.cpp @@ -1,6 +1,5 @@ #include "pch.h" -import std; import winrt; using namespace winrt; diff --git a/test/test_cpp20_module/main.cpp b/test/test_cpp20_module/main.cpp index b2f044a3e..98794553b 100644 --- a/test/test_cpp20_module/main.cpp +++ b/test/test_cpp20_module/main.cpp @@ -5,7 +5,6 @@ #include "catch.hpp" -import std; import winrt; using namespace winrt; diff --git a/test/test_cpp20_module/pch.h b/test/test_cpp20_module/pch.h index a619a47eb..005155af0 100644 --- a/test/test_cpp20_module/pch.h +++ b/test/test_cpp20_module/pch.h @@ -2,3 +2,9 @@ #include #include + +#include +#include +#include +#include +#include diff --git a/test/test_cpp20_module/test_cpp20_module.vcxproj b/test/test_cpp20_module/test_cpp20_module.vcxproj index cce09ab8d..e2f128d08 100644 --- a/test/test_cpp20_module/test_cpp20_module.vcxproj +++ b/test/test_cpp20_module/test_cpp20_module.vcxproj @@ -37,35 +37,35 @@ Application true - v145 + v143 Application true - v145 + v143 Application false true - v145 + v143 Application false true - v145 + v143 Application true - v145 + v143 Application false true - v145 + v143 diff --git a/test/test_module_winrt/test_module_winrt.vcxproj b/test/test_module_winrt/test_module_winrt.vcxproj index 5e427ffc9..09e11705d 100644 --- a/test/test_module_winrt/test_module_winrt.vcxproj +++ b/test/test_module_winrt/test_module_winrt.vcxproj @@ -37,35 +37,35 @@ StaticLibrary true - v145 + v143 StaticLibrary true - v145 + v143 StaticLibrary false true - v145 + v143 StaticLibrary false true - v145 + v143 StaticLibrary true - v145 + v143 StaticLibrary false true - v145 + v143 From abae82cf7c0d374650a424304cf52e41dfaaed39 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Wed, 1 Apr 2026 11:50:50 -0700 Subject: [PATCH 17/34] For some reason, the project dependencies got clobbered --- cppwinrt.sln | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cppwinrt.sln b/cppwinrt.sln index d92318aa9..6b15ad5d9 100644 --- a/cppwinrt.sln +++ b/cppwinrt.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.6.11620.209 main +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33829.357 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cppwinrt", "cppwinrt\cppwinrt.vcxproj", "{D613FB39-5035-4043-91E2-BAB323908AF4}" ProjectSection(ProjectDependencies) = postProject @@ -137,7 +137,6 @@ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_component_module", "test\test_component_module\test_component_module.vcxproj", "{E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}" ProjectSection(ProjectDependencies) = postProject {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C} = {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C} - {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}" From 92ea1063cb0f5658744b1033fad83d9b045373db Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Wed, 1 Apr 2026 12:21:48 -0700 Subject: [PATCH 18/34] import std in components should be conditional --- cppwinrt/component_writers.h | 2 +- cppwinrt/file_writers.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cppwinrt/component_writers.h b/cppwinrt/component_writers.h index b0b1a98de..bb185e89d 100644 --- a/cppwinrt/component_writers.h +++ b/cppwinrt/component_writers.h @@ -138,7 +138,7 @@ namespace cppwinrt { if (settings.component_module) { - w.write("import std;\nimport winrt;\n"); + w.write("#ifdef WINRT_IMPORT_STD\nimport std;\n#endif\nimport winrt;\n"); } else { diff --git a/cppwinrt/file_writers.h b/cppwinrt/file_writers.h index cfdd83513..f490be407 100644 --- a/cppwinrt/file_writers.h +++ b/cppwinrt/file_writers.h @@ -286,7 +286,7 @@ import std; // own projection headers, included with WINRT_IMPL_SKIP_INCLUDES to // skip SDK dependencies that are already in the module. w.write("#include \"winrt/base_macros.h\"\n"); - w.write("import std;\n"); + w.write("#ifdef WINRT_IMPORT_STD\nimport std;\n#endif\n"); w.write("import winrt;\n"); w.write("#define WINRT_IMPL_SKIP_INCLUDES\n"); for (auto&& depends : w.depends) From c78fc14e1c84db1095dcc7760993bd0cf65ca1a7 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Wed, 1 Apr 2026 12:25:22 -0700 Subject: [PATCH 19/34] Enable building module test projects --- cppwinrt.sln | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cppwinrt.sln b/cppwinrt.sln index 6b15ad5d9..01b7a78a9 100644 --- a/cppwinrt.sln +++ b/cppwinrt.sln @@ -427,23 +427,41 @@ Global {9E392830-805A-4AAF-932D-C493143EFACA}.Release|x86.ActiveCfg = Release|Win32 {9E392830-805A-4AAF-932D-C493143EFACA}.Release|x86.Build.0 = Release|Win32 {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|ARM64.Build.0 = Debug|ARM64 {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|x64.ActiveCfg = Debug|x64 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|x64.Build.0 = Debug|x64 {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|x86.ActiveCfg = Debug|Win32 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Debug|x86.Build.0 = Debug|Win32 {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|ARM64.ActiveCfg = Release|ARM64 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|ARM64.Build.0 = Release|ARM64 {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|x64.ActiveCfg = Release|x64 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|x64.Build.0 = Release|x64 {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|x86.ActiveCfg = Release|Win32 + {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE}.Release|x86.Build.0 = Release|Win32 {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Debug|ARM64.Build.0 = Debug|ARM64 {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Debug|x64.ActiveCfg = Debug|x64 + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Debug|x64.Build.0 = Debug|x64 {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Debug|x86.ActiveCfg = Debug|Win32 + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Debug|x86.Build.0 = Debug|Win32 {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Release|ARM64.ActiveCfg = Release|ARM64 + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Release|ARM64.Build.0 = Release|ARM64 {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Release|x64.ActiveCfg = Release|x64 + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Release|x64.Build.0 = Release|x64 {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Release|x86.ActiveCfg = Release|Win32 + {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}.Release|x86.Build.0 = Release|Win32 {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Debug|ARM64.Build.0 = Debug|ARM64 {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Debug|x64.ActiveCfg = Debug|x64 + {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Debug|x64.Build.0 = Debug|x64 {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Debug|x86.ActiveCfg = Debug|Win32 + {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Debug|x86.Build.0 = Debug|Win32 {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Release|ARM64.ActiveCfg = Release|ARM64 + {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Release|ARM64.Build.0 = Release|ARM64 {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Release|x64.ActiveCfg = Release|x64 + {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Release|x64.Build.0 = Release|x64 {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Release|x86.ActiveCfg = Release|Win32 + {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 22c67d8790b4b71f7928abd5a762149728be0cb2 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Wed, 1 Apr 2026 12:58:49 -0700 Subject: [PATCH 20/34] Don't export impl from fast_forward --- strings/base_fast_forward.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strings/base_fast_forward.h b/strings/base_fast_forward.h index c7cf6c3be..dc89fe6ce 100644 --- a/strings/base_fast_forward.h +++ b/strings/base_fast_forward.h @@ -30,7 +30,7 @@ static_assert(WINRT_FAST_ABI_SIZE >= %); #pragma detect_mismatch("WINRT_FAST_ABI_SIZE", WINRT_IMPL_STRING(WINRT_FAST_ABI_SIZE)) -WINRT_EXPORT namespace winrt::impl +namespace winrt::impl { // Thunk definitions are in arch-specific assembly sources % From 9250b45898bc93f4d4d890ee9cf68c94180edef7 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Wed, 1 Apr 2026 14:32:20 -0700 Subject: [PATCH 21/34] Fix stuff around pch inclusion and component files. Updated docs. --- cppwinrt/main.cpp | 6 - docs/modules-internals.md | 72 +++++---- docs/modules.md | 142 +++++++++--------- test/test_component_module/Toaster.cpp | 4 +- test/test_component_module/module.cpp | 4 + test/test_component_module/module_wrapper.cpp | 9 -- test/test_component_module/pch.h | 6 +- .../test_component_module.vcxproj | 16 +- 8 files changed, 126 insertions(+), 133 deletions(-) delete mode 100644 test/test_component_module/module_wrapper.cpp diff --git a/cppwinrt/main.cpp b/cppwinrt/main.cpp index c3142d320..693ccfb44 100644 --- a/cppwinrt/main.cpp +++ b/cppwinrt/main.cpp @@ -175,12 +175,6 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder settings.component_ignore_velocity = args.exists("ignore_velocity"); settings.component_module = args.exists("module"); - if (settings.component_module) - { - // Module mode disables the PCH include in generated files - settings.component_pch.clear(); - } - if (settings.component_pch == ".") { settings.component_pch.clear(); diff --git a/docs/modules-internals.md b/docs/modules-internals.md index 66ce8529d..586a4ec8c 100644 --- a/docs/modules-internals.md +++ b/docs/modules-internals.md @@ -200,8 +200,8 @@ generated component files: | Header mode | Module mode | |-------------|-------------| -| `#include "pch.h"` | (omitted — pch cleared) | -| `#include "winrt/base.h"` | `import std;` + `import winrt;` | +| `#include "pch.h"` | `#include "pch.h"` (preserved) | +| `#include "winrt/base.h"` | `#ifdef WINRT_IMPORT_STD` / `import std;` / `#endif` + `import winrt;` | ### Toaster.g.h (component base template) @@ -209,7 +209,7 @@ generated component files: | Header mode | Module mode | |-------------|-------------| -| `#include "winrt/test_component.h"` etc. | `#include "winrt/base_macros.h"` + `import std;` + `import winrt;` | +| `#include "winrt/test_component.h"` etc. | `#include "winrt/base_macros.h"` + conditional `import std;` + `import winrt;` + `#define WINRT_IMPL_SKIP_INCLUDES` + component headers | ### Toaster.g.cpp (factory + optional optimized constructors) @@ -220,33 +220,27 @@ generated component files: | `winrt_make_*` + constructor/static overrides | `winrt_make_*` only | In module mode, the constructor and static method definitions are omitted from -`.g.cpp` because they're already exported from the `winrt` module (via the -component's namespace header folded into `winrt.ixx`). The `winrt_make_*` +`.g.cpp` because they're already available from the component's projection +header (included by the `.g.h` after `import winrt;`). The `winrt_make_*` factory function is always emitted since it's needed by `module.g.cpp` for activation lookup. -## The Combined-ixx Approach +## Exported `winrt::impl` Namespace -**The key insight**: MSVC cannot specialize module-imported class templates from -textual `#include` code. The component's `.0.h` files specialize templates like -`category<>`, `abi<>`, `guid_v<>` from `winrt::impl` namespace. +**The key insight**: Component projection headers (`.0.h` files) specialize +templates like `category<>`, `abi<>`, `guid_v<>` from `winrt::impl` namespace. +For these specializations to work after `import winrt;`, the primary templates +must be exported from the module. -**The solution**: Append the component's namespace header includes to the end of -`winrt.ixx`, so the specializations happen inside the module purview: +**The solution**: All `namespace winrt::impl` blocks in the `strings/base_*.h` +files and the generated namespace headers use `WINRT_EXPORT namespace winrt::impl`. +In module mode, `WINRT_EXPORT` expands to `export`, making the `impl` templates +part of the module's public interface. In header mode, `WINRT_EXPORT` is empty, +so there's no change to the existing behavior. -``` -export module winrt; -#define WINRT_EXPORT export - -#include "winrt/Windows.Foundation.h" -... -#include "winrt/Windows.Web.UI.Interop.h" -#include "winrt/test_component.h" ← appended by build system -``` - -This is done by the NuGet targets (`CppWinRTAddModuleSource`) or manually in -the project's CustomBuildStep. The target scans `$(GeneratedFilesDir)winrt/` for -`.h` files not already in `winrt.ixx` and appends them. +This allows component projection headers to be `#include`d separately after +`import winrt;` — they can specialize the exported `impl` templates without +needing to be folded into `winrt.ixx`. ## The windowsnumerics.impl.h Warning @@ -332,7 +326,7 @@ would break existing users whose build systems don't compile the std module. | Target | File | Role | |--------|------|------| -| `CppWinRTAddModuleSource` | `Microsoft.Windows.CppWinRT.targets` | Folds component headers into ixx, adds ixx to compilation, defines `WINRT_IMPORT_STD` | +| `CppWinRTAddModuleSource` | `Microsoft.Windows.CppWinRT.targets` | Adds ixx to compilation, defines `WINRT_IMPORT_STD` | | `CppWinRTMakeProjections` | `Microsoft.Windows.CppWinRT.targets` | Orchestrates platform/reference/component projection generation | | `CppWinRTMakeComponentProjection` | `Microsoft.Windows.CppWinRT.targets` | Runs cppwinrt.exe for component stubs (passes `-module` via `CppWinRTParameters`) | @@ -343,7 +337,6 @@ User sets CppWinRTModule=true + BuildStlModules=true │ ├── NuGet targets define WINRT_IMPORT_STD on all ClCompile items ├── NuGet targets pass -module to cppwinrt.exe - ├── NuGet targets append component headers to winrt.ixx │ ▼ winrt.ixx compilation: @@ -352,22 +345,27 @@ winrt.ixx compilation: ← std library includes (base_std_includes) export module winrt; #define WINRT_EXPORT export - #include "winrt/base.h" ← base.h (import std; not checked, pragma once) + #include "winrt/base.h" ← base.h (pragma once) #include "winrt/Windows.Foundation.h" ← SDK types + specializations - ... - #include "winrt/MyComponent.h" ← component specializations in purview + ... ← winrt::impl is exported Consumer .cpp compilation: - import std; ← std module (compiled by BuildStlModules) - import winrt; ← winrt module (SDK + component types) - #include "Toaster.h" - └── #include "Toaster.g.h" - └── #include "winrt/base_macros.h" ← macros only (WINRT_EXPORT etc.) - └── import std; ← harmless duplicate - └── import winrt; ← harmless duplicate + #include "pch.h" ← PCH (no winrt/ headers) + import std; ← std module (optional, needs BuildStlModules) + import winrt; ← winrt module (SDK types + exported impl) + +Component .g.h: + #include "winrt/base_macros.h" ← macros only (WINRT_EXPORT etc.) + #ifdef WINRT_IMPORT_STD + import std; ← conditional on WINRT_IMPORT_STD + #endif + import winrt; ← SDK types + exported impl templates + #define WINRT_IMPL_SKIP_INCLUDES ← skip SDK #include deps + #include "winrt/MyComponent.h" ← component projection (specializes impl) module.g.cpp compilation: - import std; ← from -module code gen + #include "pch.h" ← PCH preserved + #ifdef WINRT_IMPORT_STD / import std; ← conditional import winrt; ← from -module code gen void* winrt_make_MyComponent_Toaster() ← factory function bool __stdcall ..._can_unload_now() ← DLL entry points diff --git a/docs/modules.md b/docs/modules.md index 03ad82ba0..c9482ab83 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -9,9 +9,14 @@ developer experience. ## Quick Start -### Pure SDK consumption (app that calls WinRT APIs) +### Consuming the platform projection (app that calls WinRT APIs) -Set these MSBuild properties in your `.vcxproj`: +In Visual Studio, set the following in your project property pages: +- **C/C++ > General > C++ Language Standard**: `ISO C++20` or `Preview` +- **C/C++ > General > Build ISO C++23 Standard Library Modules**: `Yes` +- **C++/WinRT > General > C++20 Module**: `true` + +Or equivalently, set these MSBuild properties in your `.vcxproj`: ```xml @@ -49,7 +54,7 @@ Same project properties as above, then in your implementation files: ```cpp // MyComponent.cpp -import std; +#include "pch.h" // PCH is fine — just don't include winrt/ headers in it import winrt; #include "MyComponent.h" // includes MyComponent.g.h #include "MyComponent.g.cpp" // factory function only in module mode @@ -57,19 +62,20 @@ import winrt; The NuGet targets automatically: 1. Pass `-module` to cppwinrt.exe so generated `.g.h`/`.g.cpp` files use - `import winrt;` instead of `#include` directives -2. Fold the component's projection headers into `winrt.ixx` so template - specializations occur within the module purview + `import winrt;` instead of `#include` directives for platform types +2. Generate component projection headers with `WINRT_IMPL_SKIP_INCLUDES` so + they skip platform `#include` directives already available from the module 3. Define `WINRT_IMPORT_STD` when `BuildStlModules` is enabled ## Requirements -- **Visual Studio 2026** (v145 toolset) or later — the v180 MSBuild targets - support `import std;` with C++20. Earlier toolsets (v143) require C++23 for - `import std;` support. +- **Visual Studio 2022** (v143 toolset) or later for `import winrt;` - **C++20 or later** language standard (`/std:c++20` or `/std:c++latest`) -- **`BuildStlModules=true`** in `` to enable - the standard library module compilation +- For `import std;` alongside `import winrt;`: + - **v143 toolset**: requires `/std:c++latest` (C++23 mode) + `BuildStlModules=true` + - **v145 toolset** (VS 2026): works with `/std:c++20` + `BuildStlModules=true` +- **`BuildStlModules=true`** (optional) in `` + to enable the standard library module compilation for `import std;` ## MSBuild Properties @@ -89,7 +95,7 @@ When `CppWinRTModule=true`, the NuGet targets also automatically: | Option | Description | |--------|-------------| -| `-module` | Generate component files (`.g.h`, `.g.cpp`, `module.g.cpp`) using `import winrt;` instead of `#include` directives. The `.g.h` files emit `import std;`, `import winrt;`, and `#include "winrt/base_macros.h"`. The `.g.cpp` files emit only the `winrt_make_*` factory function (constructor/static optimizations are omitted since those definitions come from the module). | +| `-module` | Generate component files (`.g.h`, `.g.cpp`, `module.g.cpp`) using `import winrt;` instead of `#include` directives for platform projection types. The `.g.h` files emit `import winrt;`, include the component's own projection headers with `WINRT_IMPL_SKIP_INCLUDES`, and `#include "winrt/base_macros.h"` for macros. The `.g.cpp` files emit only the `winrt_make_*` factory function (constructor/static optimizations are omitted since those definitions come from the component's projection header included after `import winrt;`). `import std;` is conditional on `WINRT_IMPORT_STD`. PCH includes are preserved. | ### Other commonly used options @@ -128,7 +134,7 @@ import winrt; // C++/WinRT module // Your code here... ``` -### Consumer app (SDK types only) +### Consumer app (platform types only) ```cpp import std; @@ -220,7 +226,7 @@ winrt/ ├── base.h # Core C++/WinRT library (types, helpers, COM support) ├── base_macros.h # Lightweight macro-only header for module consumers ├── winrt.ixx # C++20 module interface unit -├── Windows.Foundation.h # SDK namespace header (consume definitions) +├── Windows.Foundation.h # Platform namespace header (consume definitions) ├── impl/ │ ├── Windows.Foundation.0.h # Forward decls, enums, ABI, consume structs │ ├── Windows.Foundation.1.h # Interface definitions @@ -236,50 +242,44 @@ winrt/ ``` module; ← Global module fragment - ← Platform includes (base_includes) - ← Standard library includes (base_std_includes) + ← Platform includes (base_includes) + ← Standard library includes (base_std_includes) export module winrt; ← Module purview begins #define WINRT_EXPORT export -#include "winrt/Windows.Foundation.h" ← SDK namespace headers +#include "winrt/Windows.Foundation.h" ← Platform namespace headers #include "winrt/Windows.Foundation.Collections.h" -... ← (all SDK namespaces) -#include "winrt/MyComponent.h" ← Component headers (appended by build) +... ← (all platform namespaces) ``` -### Why component headers are folded into winrt.ixx - -The component's `.0.h` impl headers contain explicit template specializations: - -```cpp -// In test_component.0.h: -template <> struct category - { using type = interface_category; }; -template <> struct abi - { struct type : inspectable_abi { ... }; }; -template <> inline constexpr guid guid_v{ ... }; -``` +The `winrt::impl` namespace is exported alongside `winrt`, so component +projection headers can specialize `impl` templates (like `category`, `abi`, +`guid_v`) after `import winrt;` without needing to be folded into the ixx. -These specialize primary templates (`category`, `abi`, `guid_v`) defined in -`base.h`. If these specializations occur in textual code that imported the -primary templates from a module, MSVC currently reports: +### How component projection works with modules -``` -error C3856: 'category': symbol is not a class template -``` +The `winrt::impl` namespace is exported from the module, which means the +primary templates (`category`, `abi`, `guid_v`, `name_v`, `default_interface`, +`consume`) are visible to external code. Component projection headers +(`.0.h` files) specialize these templates for the component's types. -By folding the component headers into `winrt.ixx`, the specializations happen -inside the same module purview as the primary templates, avoiding this issue -entirely. +When a component's `.g.h` does `import winrt;` and then includes the +component's projection header with `WINRT_IMPL_SKIP_INCLUDES`: +- Platform projection dependencies (`base.h`, `Windows.Foundation.h`, etc.) are + skipped — they're already available from the module +- The component's own impl headers are included normally — they contain + the template specializations that register the component's types +- The specializations succeed because the primary templates are exported + from the module and visible to the textual include ### Generated file differences in module mode (`-module` flag) | File | Header mode | Module mode (`-module`) | |------|-------------|------------------------| -| `Toaster.g.h` | `#include "winrt/test_component.h"` | `#include "winrt/base_macros.h"` + `import std;` + `import winrt;` | -| `Toaster.g.cpp` | `winrt_make_*` + constructor/static overrides | `winrt_make_*` only (constructors come from module) | -| `module.g.cpp` | `#include "pch.h"` + `#include "winrt/base.h"` | `import std;` + `import winrt;` | +| `Toaster.g.h` | `#include "winrt/test_component.h"` | `#include "winrt/base_macros.h"` + `import winrt;` + component headers with `WINRT_IMPL_SKIP_INCLUDES` | +| `Toaster.g.cpp` | `winrt_make_*` + constructor/static overrides | `winrt_make_*` only (constructors come from projection header) | +| `module.g.cpp` | `#include "pch.h"` + `#include "winrt/base.h"` | `#include "pch.h"` + `import winrt;` (`import std;` conditional on `WINRT_IMPORT_STD`) | ### base_macros.h @@ -295,22 +295,19 @@ definitions — no type/function declarations that could conflict with the modul The `CppWinRTAddModuleSource` target in `Microsoft.Windows.CppWinRT.targets` performs these steps when `CppWinRTModule=true`: -1. **Scans** `$(GeneratedFilesDir)winrt/` for namespace headers (`.h` files) - that are not already listed in `winrt.ixx` -2. **Appends** `#include` directives for those headers to the end of `winrt.ixx` - (this folds component projection headers into the module) -3. **Adds** `winrt.ixx` as a `ClCompile` item with `PrecompiledHeader=NotUsing` -4. **Defines** `WINRT_IMPORT_STD` when `BuildStlModules=true` is detected - -The `-module` flag is automatically appended to `$(CppWinRTParameters)` when -`CppWinRTModule=true`, so it flows to all cppwinrt.exe invocations including -the component projection generation. +1. **Adds** `winrt.ixx` as a `ClCompile` item with `PrecompiledHeader=NotUsing` +2. **Defines** `WINRT_IMPORT_STD` when `BuildStlModules=true` is detected +3. **Passes** `-module` to cppwinrt.exe via `$(CppWinRTParameters)` so + component generation emits `import winrt;` in `.g.h` and `module.g.cpp` ## Known Limitations -1. **Toolset requirement**: The v145 toolset (VS 2026) or later is needed for - `import std;` with C++20. Earlier toolsets define `__cpp_lib_modules` but - their MSBuild targets only enable `StdModulesSupported` for C++23+. +1. **`import std;` requires C++23 or later on v143**: The v143 toolset (VS 2022) + enables `StdModulesSupported` only when the language standard is C++23 or + `/std:c++latest` (`CppLanguageStandardNumber >= 23`). The v145 toolset + (VS 2026) relaxes this to C++20. On v143 with `/std:c++20`, `import std;` + is not available — use `import winrt;` with textual `#include`s for standard + library types. 2. **Include ordering**: Standard library `#include` directives and third-party headers must come before `import std;` and `import winrt;` to avoid @@ -323,24 +320,23 @@ the component projection generation. containing third-party headers, Windows SDK headers, or other project headers is fine. -4. **WINRT_LEAN_AND_MEAN for consumers**: Pure consumer apps (no component +4. **Standard library headers not included transitively**: When using traditional + `#include "winrt/base.h"`, you implicitly get standard library headers like + ``, ``, ``, etc. With `import winrt;`, these are + NOT automatically available. You must either: + - `import std;` (if your toolset/language standard supports it), or + - Explicitly `#include` the standard library headers you need, or + - Include them in your PCH + +5. **WINRT_LEAN_AND_MEAN for consumers**: Pure consumer apps (no component authoring) should define `WINRT_LEAN_AND_MEAN` to avoid pulling `` and `produce<>` templates into the module. Component authors should NOT define it since they need `produce<>`. -5. **Cross-module template specialization**: Component projection headers - (`.0.h` files) specialize templates like `winrt::impl::category`, - `winrt::impl::abi`, `winrt::impl::guid_v`, etc. These templates are in the - `winrt::impl` namespace, which is currently not exported from the module - (it uses module linkage). When a textual `#include` of a component header - tries to specialize these after `import winrt;`, the primary templates are - not visible: - ``` - error C3856: 'category': symbol is not a class template - ``` - The combined-ixx approach (folding component headers into `winrt.ixx`) - works around this by keeping specializations within the module purview, - where the `impl` templates are directly visible. A future improvement could - export the specific `impl` templates that need external specialization, - which would allow component headers to be included separately after - `import winrt;` without folding them into the module. +6. **Exported `winrt::impl` namespace**: The `winrt::impl` namespace is exported + from the module so that component projection headers can specialize `impl` + templates (`category`, `abi`, `guid_v`, etc.) after `import winrt;`. This + means component headers can be `#include`d separately without being folded + into `winrt.ixx`. The `WINRT_IMPL_SKIP_INCLUDES` macro is used by generated + `.g.h` files to skip platform `#include` dependencies that are already available + from the module. diff --git a/test/test_component_module/Toaster.cpp b/test/test_component_module/Toaster.cpp index 479d8e176..e56300784 100644 --- a/test/test_component_module/Toaster.cpp +++ b/test/test_component_module/Toaster.cpp @@ -1,4 +1,4 @@ -import std; -import winrt; +#include "pch.h" + #include "Toaster.h" #include "Toaster.g.cpp" diff --git a/test/test_component_module/module.cpp b/test/test_component_module/module.cpp index 3719ada6e..a685bc824 100644 --- a/test/test_component_module/module.cpp +++ b/test/test_component_module/module.cpp @@ -1,4 +1,8 @@ +#include "pch.h" + +#ifdef WINRT_IMPORT_STD import std; +#endif import winrt; bool __stdcall test_module_can_unload_now() noexcept; diff --git a/test/test_component_module/module_wrapper.cpp b/test/test_component_module/module_wrapper.cpp deleted file mode 100644 index 8d70a96e7..000000000 --- a/test/test_component_module/module_wrapper.cpp +++ /dev/null @@ -1,9 +0,0 @@ -// Wrapper for the generated module.g.cpp that provides module imports -// instead of the PCH and base.h includes that the generated file expects. -import std; -import winrt; - -// WINRT_IMPL_SKIP_INCLUDES is defined project-wide for component source files, -// so the generated module.g.cpp's #include "winrt/base.h" will be skipped. -// The import winrt; above provides all needed symbols. -#include "module.g.cpp" diff --git a/test/test_component_module/pch.h b/test/test_component_module/pch.h index cd816aac7..c757a4cd9 100644 --- a/test/test_component_module/pch.h +++ b/test/test_component_module/pch.h @@ -1,3 +1,7 @@ #pragma once -#include "winrt/Windows.Foundation.h" +#ifndef WINRT_IMPORT_STD +#include +#include +#include +#endif diff --git a/test/test_component_module/test_component_module.vcxproj b/test/test_component_module/test_component_module.vcxproj index 7adf3a1ab..e84d229b2 100644 --- a/test/test_component_module/test_component_module.vcxproj +++ b/test/test_component_module/test_component_module.vcxproj @@ -99,7 +99,7 @@ .;$(ProjectDir)Generated Files;$(SolutionDir)test\test_module_winrt\Generated Files;Generated Files NOMINMAX;%(PreprocessorDefinitions) - NotUsing + Use true $(IntDir)..\test_module_winrt\winrt.ixx.ifc;%(AdditionalModuleDependencies) @@ -117,12 +117,10 @@ - $(CppWinRTDir)cppwinrt -in local -out "$(ProjectDir)Generated Files" -verbose - $(CppWinRTDir)cppwinrt -input $(OutputPath)test_component_module.winmd -out "$(ProjectDir)Generated Files" -ref sdk -verbose $(CppWinRTDir)cppwinrt -input $(OutputPath)test_component_module.winmd -comp "$(ProjectDir)Generated Files" -out "$(ProjectDir)Generated Files" -include test_component_module -ref sdk -verbose -prefix -opt -lib test_module -overwrite -name test_component_module -module Projecting Windows and component metadata - $(OutputPath)\winrt\base.h;Generated Files\module.g.cpp + Generated Files\module.g.cpp $(CppWinRTDir)cppwinrt.exe;$(OutputPath)test_component_module.winmd @@ -145,6 +143,14 @@ + + Create + Create + Create + Create + Create + Create + @@ -162,4 +168,4 @@ - + \ No newline at end of file From eb562473ac9fb3e670452df43e33977a24cee1f6 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Wed, 1 Apr 2026 17:01:57 -0700 Subject: [PATCH 22/34] Add some Copilot instructions for future sessions. --- .github/copilot-instructions.md | 102 +++++++++++++++++++ .github/instructions/modules.instructions.md | 66 ++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/instructions/modules.instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..ecfa8f944 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,102 @@ +# C++/WinRT Repository - Copilot Instructions + +## Project Overview + +C++/WinRT is a C++ language projection for Windows Runtime APIs. The `cppwinrt.exe` +code generator reads WinRT metadata (.winmd) and produces C++ headers and a C++20 +module interface unit (winrt.ixx). + +## Repository Structure + +- `cppwinrt/` — The code generator source (cppwinrt.exe) +- `strings/` — Handwritten C++ fragments embedded into generated output by the prebuild step +- `prebuild/` — Embeds strings/*.h content into strings.h/strings.cpp as string literals +- `nuget/` — NuGet package files (props, targets, rules) for customer integration +- `natvis/` — Debugger visualizer (directly includes strings/*.h, not generated output) +- `test/` — Test projects +- `_build/` — Build output +- `docs/` — Documentation including modules.md and modules-internals.md + +## Build System + +- Uses MSBuild with Visual Studio. Build from a VS Developer PowerShell. +- `Directory.Build.Props` sets shared properties (toolset, output paths, language standard). +- `$(OutDir)` = `$(SolutionDir)_build\$(Platform)\$(Configuration)\` +- `$(IntDir)` = `$(SolutionDir)_build\$(Platform)\$(Configuration)\temp\$(ProjectName)\` +- The prebuild step must run before cppwinrt can compile (it generates strings.h/strings.cpp). + +### Key Build Commands + +```powershell +# Build just the code generator +msbuild cppwinrt.sln /t:cppwinrt /p:Configuration=Debug /p:Platform=x64 /v:minimal + +# Build a test project (pass SolutionDir for path resolution) +msbuild test\test_cpp20\test_cpp20.vcxproj /p:Configuration=Debug /p:Platform=x64 /p:SolutionDir=d:\Repos\GitHub\cppwinrt\ + +# Generate SDK projection +.\_build\x64\Debug\cppwinrt.exe -in sdk -out .\_build\x64\Debug\ +``` + +## Code Generator Architecture + +### String Fragment System + +The `strings/base_*.h` files are NOT headers that get #included at build time. +They are embedded as string literals by the prebuild step into `strings.cpp`. +The code generator writes them into generated output files (base.h, winrt.ixx, etc.). + +When modifying strings/*.h files: +1. Rebuild `prebuild` and then `cppwinrt` to pick up changes +2. Regenerate the projection (`cppwinrt.exe -in sdk -out ...`) +3. Then rebuild test projects + +### Text Writer System + +- `text_writer.h` — Base writer with `%` placeholder substitution +- `type_writers.h` — The `writer` class with `write_root_include`, `write_depends`, etc. +- `code_writers.h` — High-level writers: `write_version_assert`, `write_open_file_guard`, etc. +- `file_writers.h` — File-level orchestration: `write_base_h`, `write_namespace_h`, etc. +- `component_writers.h` — Component stub generation: `write_module_g_cpp`, `write_component_g_h`, etc. + +### Generated File Layers (per namespace) + +- `.0.h` — Forward declarations, enums, ABI, consume structs, template specializations +- `.1.h` — Interface type definitions +- `.2.h` — Class/struct/delegate type definitions +- Main `.h` — Consume definitions, produce implementations, std::hash specializations + +## C++20 Modules + +See `docs/modules.md` for user-facing documentation and `docs/modules-internals.md` +for maintainer details. + +### Key Design Decisions + +- `winrt::impl` is exported from the module so component headers can specialize + templates (`category`, `abi`, `guid_v`, etc.) after `import winrt;` +- `base_includes.h` has platform headers only; `base_std_includes.h` has std library + headers. The ixx global module fragment uses both as raw #includes. base.h uses + the std includes conditionally (import std vs #include). +- `base_module_macros.h` provides macros that don't cross module boundaries + (WINRT_EXPORT, WINRT_IMPL_EMPTY_BASES, etc.). It's the single source of truth; + `base_macros.h` has `#ifndef` fallbacks for direct consumers like natvis. +- Generated component files (`.g.h`, `module.g.cpp`) use `import winrt;` when + `-module` flag is passed. `import std;` is conditional on `WINRT_IMPORT_STD`. +- The natvis project includes strings/*.h directly — any new strings/ files must + be added to natvis/pch.h too. + +### Common Pitfalls + +- `` transitively includes STL headers on newer MSVC. Can't use + `import std;` in the ixx purview because it conflicts with these transitive includes. +- `#pragma once` prevents re-inclusion but macros from included headers DO persist + from the global module fragment into the purview. +- PCH + conditional compilation: `#include "pch.h"` must be the FIRST non-comment + line in a file. It cannot be inside `#ifdef` blocks. +- `PlatformToolset` in `Directory.Build.Props` overrides per-project settings in + the Globals PropertyGroup. To override, set it in Configuration PropertyGroups. +- `WINRT_EXPORT` must be defined before any code that uses it. When strings/ files + are included directly (natvis), `base_module_macros.h` must come before `base_macros.h`. +- `extern "C++"` is needed on exported `__declspec(selectany)` variables + (`winrt_to_hresult_handler` etc.) to prevent module linkage. diff --git a/.github/instructions/modules.instructions.md b/.github/instructions/modules.instructions.md new file mode 100644 index 000000000..a07cfde86 --- /dev/null +++ b/.github/instructions/modules.instructions.md @@ -0,0 +1,66 @@ +--- +applyTo: "strings/**,cppwinrt/**,nuget/**,test/test_module_winrt/**,test/test_cpp20_module/**,test/test_component_module/**" +--- + +# C++20 Module Code Generation + +## When Modifying strings/*.h Files + +These files are embedded as string literals by the prebuild step. They are NOT +normal headers: + +1. Changes require rebuilding prebuild → cppwinrt → regenerating projection +2. The natvis project includes these files directly via its pch.h — add any new + files there too +3. `WINRT_EXPORT` prefixes `namespace winrt` (public) AND `namespace winrt::impl` + (for component template specialization). Both expansions are needed. +4. `base_module_macros.h` is the single source of truth for macros shared between + module and header modes. Don't duplicate definitions — use `#ifndef` guards. + +## Module-Aware Code Generation (-module flag) + +When `settings.component_module` is true: + +### module.g.cpp +- Keeps `#include "pch.h"` (PCH is compatible with modules) +- Replaces `#include "winrt/base.h"` with `import winrt;` +- `import std;` is conditional: `#ifdef WINRT_IMPORT_STD` + +### .g.h (component base template) +- Emits `#include "winrt/base_macros.h"` for macros +- Emits conditional `import std;` + unconditional `import winrt;` +- Emits `#define WINRT_IMPL_SKIP_INCLUDES` then includes component's own headers +- Does NOT include platform namespace headers (they come from the module) + +### .g.cpp (factory + optimized constructors) +- In module mode, emits ONLY the `winrt_make_*` factory function +- Constructor/static overrides are omitted (they come from the component's + projection header, which is included by the .g.h after import winrt) + +## WINRT_IMPL_SKIP_INCLUDES + +Generated namespace headers guard cross-namespace `#include` dependencies with: +```cpp +#ifndef WINRT_IMPL_SKIP_INCLUDES +#include "winrt/base.h" +#endif +``` + +- Cross-namespace dependencies (other namespaces' impl headers): GUARDED + (`write_depends_guarded` / `write_root_include_guarded`) +- Self-namespace dependencies (own impl headers): NOT guarded + (`write_depends` / `write_root_include`) +- base.h include in version assert: GUARDED with base_macros.h fallback + +## Test Project Architecture + +- `test_module_winrt` — Static lib that builds winrt.ixx. Generates projection + to its own "Generated Files" directory. Other module projects reference its + IFC and OBJ. +- `test_cpp20_module` — Consumer test app. References test_module_winrt's IFC/OBJ. + Uses PCH for non-winrt headers (catch.hpp, Windows.h). +- `test_component_module` — Component DLL. References test_module_winrt's IFC/OBJ. + Has its own MIDL/cppwinrt steps for component projection. + +All module test projects use v143 toolset. `import std;` requires `BuildStlModules=true` +and either v145 toolset or `/std:c++latest` on v143. From d82e886414d09956607650f3d971a4a519a188bb Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Thu, 2 Apr 2026 00:21:56 -0700 Subject: [PATCH 23/34] Remove stale ixx component folding code. --- nuget/Microsoft.Windows.CppWinRT.targets | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/nuget/Microsoft.Windows.CppWinRT.targets b/nuget/Microsoft.Windows.CppWinRT.targets index ae925b346..e3b291016 100644 --- a/nuget/Microsoft.Windows.CppWinRT.targets +++ b/nuget/Microsoft.Windows.CppWinRT.targets @@ -890,16 +890,6 @@ $(XamlMetaDataProviderPch) BeforeTargets="ClCompile" Condition="'$(CppWinRTModule)' == 'true'"> - - - <_CppWinRTModuleHeaders Include="$(GeneratedFilesDir)winrt\*.h" Exclude="$(GeneratedFilesDir)winrt\base.h;$(GeneratedFilesDir)winrt\base_macros.h;$(GeneratedFilesDir)winrt\fast_forward.h" /> - <_CppWinRTIxxExistingIncludes Include="$([System.IO.File]::ReadAllText('$(GeneratedFilesDir)winrt\winrt.ixx'))" /> - - NotUsing From 78c7344c8faa93dcf97f917542c33346c036a21e Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Thu, 2 Apr 2026 16:25:42 -0700 Subject: [PATCH 24/34] Clean up some duplication in generated files. --- cppwinrt/file_writers.h | 6 +++++- cppwinrt/main.cpp | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cppwinrt/file_writers.h b/cppwinrt/file_writers.h index f490be407..c39562891 100644 --- a/cppwinrt/file_writers.h +++ b/cppwinrt/file_writers.h @@ -10,6 +10,9 @@ namespace cppwinrt { auto wrap_file_guard = wrap_open_file_guard(w, "BASE"); + w.write(R"( +#ifndef WINRT_IMPL_INCLUDES_HANDLED +)"); w.write(strings::base_includes); w.write(R"( #if defined(__cpp_lib_modules) && defined(WINRT_IMPORT_STD) @@ -18,8 +21,9 @@ import std; )"); w.write(strings::base_std_includes); w.write(R"(#endif +#endif )"); - w.write(strings::base_module_macros); + w.write_root_include("base_macros"); w.write(strings::base_macros); w.write(strings::base_types); w.write(strings::base_extern); diff --git a/cppwinrt/main.cpp b/cppwinrt/main.cpp index 693ccfb44..68828066a 100644 --- a/cppwinrt/main.cpp +++ b/cppwinrt/main.cpp @@ -349,7 +349,7 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder ixx.write("module;\n"); ixx.write(strings::base_includes); ixx.write(strings::base_std_includes); - ixx.write("\nexport module winrt;\n#define WINRT_EXPORT export\n\n"); + ixx.write("\nexport module winrt;\n#define WINRT_EXPORT export\n#define WINRT_IMPL_INCLUDES_HANDLED\n\n"); for (auto&&[ns, members] : c.namespaces()) { From 322a103676fb6b3f0c12da0aa801c487d07d4282 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Thu, 2 Apr 2026 17:22:21 -0700 Subject: [PATCH 25/34] Add some #endif comments --- cppwinrt/file_writers.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cppwinrt/file_writers.h b/cppwinrt/file_writers.h index c39562891..d94502140 100644 --- a/cppwinrt/file_writers.h +++ b/cppwinrt/file_writers.h @@ -20,8 +20,8 @@ import std; #else )"); w.write(strings::base_std_includes); - w.write(R"(#endif -#endif + w.write(R"(#endif // __cpp_lib_modules && WINRT_IMPORT_STD +#endif // WINRT_IMPL_INCLUDES_HANDLED )"); w.write_root_include("base_macros"); w.write(strings::base_macros); From 5c8939cdb9637fed3159e5e090450da78b70213f Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Thu, 2 Apr 2026 17:24:10 -0700 Subject: [PATCH 26/34] Add tests to ensure cpp23 with import std work. --- .github/instructions/modules.instructions.md | 2 +- cppwinrt.sln | 26 ++- .../test_component_module.vcxproj | 6 +- .../test_cpp20_module.vcxproj | 6 +- .../test_cpp20_module_winrt.vcxproj} | 4 +- .../test_cpp23_module.vcxproj | 151 ++++++++++++++++++ .../test_cpp23_module_winrt.vcxproj | 131 +++++++++++++++ 7 files changed, 316 insertions(+), 10 deletions(-) rename test/{test_module_winrt/test_module_winrt.vcxproj => test_cpp20_module_winrt/test_cpp20_module_winrt.vcxproj} (98%) create mode 100644 test/test_cpp23_module/test_cpp23_module.vcxproj create mode 100644 test/test_cpp23_module_winrt/test_cpp23_module_winrt.vcxproj diff --git a/.github/instructions/modules.instructions.md b/.github/instructions/modules.instructions.md index a07cfde86..4ad84a622 100644 --- a/.github/instructions/modules.instructions.md +++ b/.github/instructions/modules.instructions.md @@ -1,5 +1,5 @@ --- -applyTo: "strings/**,cppwinrt/**,nuget/**,test/test_module_winrt/**,test/test_cpp20_module/**,test/test_component_module/**" +applyTo: "strings/**,cppwinrt/**,nuget/**,test/test_cpp20_module_winrt/**,test/test_cpp23_module_winrt/**,test/test_cpp20_module/**,test/test_component_module/**" --- # C++20 Module Code Generation diff --git a/cppwinrt.sln b/cppwinrt.sln index 01b7a78a9..34baed7c9 100644 --- a/cppwinrt.sln +++ b/cppwinrt.sln @@ -129,7 +129,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_cpp20_module", "test\t {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C} = {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C} EndProjectSection EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_module_winrt", "test\test_module_winrt\test_module_winrt.vcxproj", "{F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_cpp20_module_winrt", "test\test_cpp20_module_winrt\test_cpp20_module_winrt.vcxproj", "{F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C}" ProjectSection(ProjectDependencies) = postProject {D613FB39-5035-4043-91E2-BAB323908AF4} = {D613FB39-5035-4043-91E2-BAB323908AF4} EndProjectSection @@ -139,6 +139,16 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_component_module", "te {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C} = {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C} EndProjectSection EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_cpp23_module_winrt", "test\test_cpp23_module_winrt\test_cpp23_module_winrt.vcxproj", "{A7C3B845-6F1D-4E3A-9B82-7D4F5E6C8A12}" + ProjectSection(ProjectDependencies) = postProject + {D613FB39-5035-4043-91E2-BAB323908AF4} = {D613FB39-5035-4043-91E2-BAB323908AF4} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_cpp23_module", "test\\test_cpp23_module\\test_cpp23_module.vcxproj", "{C8D4E5F6-7A8B-4C9D-BE0F-1A2B3C4D5E6F}" + ProjectSection(ProjectDependencies) = postProject + {A7C3B845-6F1D-4E3A-9B82-7D4F5E6C8A12} = {A7C3B845-6F1D-4E3A-9B82-7D4F5E6C8A12} + 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 @@ -462,6 +472,18 @@ Global {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Release|x64.Build.0 = Release|x64 {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Release|x86.ActiveCfg = Release|Win32 {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1}.Release|x86.Build.0 = Release|Win32 + {A7C3B845-6F1D-4E3A-9B82-7D4F5E6C8A12}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {A7C3B845-6F1D-4E3A-9B82-7D4F5E6C8A12}.Debug|x64.ActiveCfg = Debug|x64 + {A7C3B845-6F1D-4E3A-9B82-7D4F5E6C8A12}.Debug|x86.ActiveCfg = Debug|Win32 + {A7C3B845-6F1D-4E3A-9B82-7D4F5E6C8A12}.Release|ARM64.ActiveCfg = Release|ARM64 + {A7C3B845-6F1D-4E3A-9B82-7D4F5E6C8A12}.Release|x64.ActiveCfg = Release|x64 + {A7C3B845-6F1D-4E3A-9B82-7D4F5E6C8A12}.Release|x86.ActiveCfg = Release|Win32 + {C8D4E5F6-7A8B-4C9D-BE0F-1A2B3C4D5E6F}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {C8D4E5F6-7A8B-4C9D-BE0F-1A2B3C4D5E6F}.Debug|x64.ActiveCfg = Debug|x64 + {C8D4E5F6-7A8B-4C9D-BE0F-1A2B3C4D5E6F}.Debug|x86.ActiveCfg = Debug|Win32 + {C8D4E5F6-7A8B-4C9D-BE0F-1A2B3C4D5E6F}.Release|ARM64.ActiveCfg = Release|ARM64 + {C8D4E5F6-7A8B-4C9D-BE0F-1A2B3C4D5E6F}.Release|x64.ActiveCfg = Release|x64 + {C8D4E5F6-7A8B-4C9D-BE0F-1A2B3C4D5E6F}.Release|x86.ActiveCfg = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -489,6 +511,8 @@ Global {B85004D5-2C57-4A53-BA7F-FE0B614EA1DE} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0} {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0} {E1D3004A-7F92-4C2B-B25A-3D3A29E316A1} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0} + {A7C3B845-6F1D-4E3A-9B82-7D4F5E6C8A12} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0} + {C8D4E5F6-7A8B-4C9D-BE0F-1A2B3C4D5E6F} = {3C7EA5F8-6E8C-4376-B499-2CAF596384B0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2783B8FD-EA3B-4D6B-9F81-662D289E02AA} diff --git a/test/test_component_module/test_component_module.vcxproj b/test/test_component_module/test_component_module.vcxproj index e84d229b2..1f7526ca2 100644 --- a/test/test_component_module/test_component_module.vcxproj +++ b/test/test_component_module/test_component_module.vcxproj @@ -97,15 +97,15 @@ - .;$(ProjectDir)Generated Files;$(SolutionDir)test\test_module_winrt\Generated Files;Generated Files + .;$(ProjectDir)Generated Files;$(SolutionDir)test\test_cpp20_module_winrt\Generated Files;Generated Files NOMINMAX;%(PreprocessorDefinitions) Use true - $(IntDir)..\test_module_winrt\winrt.ixx.ifc;%(AdditionalModuleDependencies) + $(IntDir)..\test_cpp20_module_winrt\winrt.ixx.ifc;%(AdditionalModuleDependencies) exports.def - $(IntDir)..\test_module_winrt\winrt.ixx.obj;%(AdditionalDependencies) + $(IntDir)..\test_cpp20_module_winrt\winrt.ixx.obj;%(AdditionalDependencies) true diff --git a/test/test_cpp20_module/test_cpp20_module.vcxproj b/test/test_cpp20_module/test_cpp20_module.vcxproj index e2f128d08..c9b1fed5d 100644 --- a/test/test_cpp20_module/test_cpp20_module.vcxproj +++ b/test/test_cpp20_module/test_cpp20_module.vcxproj @@ -98,15 +98,15 @@ --> - $(SolutionDir)test\test_module_winrt\Generated Files;..\;%(AdditionalIncludeDirectories) + $(SolutionDir)test\test_cpp20_module_winrt\Generated Files;..\;%(AdditionalIncludeDirectories) NOMINMAX;WINRT_LEAN_AND_MEAN;WINRT_IMPORT_STD;WINRT_TEST_MODULES;%(PreprocessorDefinitions) Use true - $(IntDir)..\test_module_winrt\winrt.ixx.ifc;%(AdditionalModuleDependencies) + $(IntDir)..\test_cpp20_module_winrt\winrt.ixx.ifc;%(AdditionalModuleDependencies) Console - $(IntDir)..\test_module_winrt\winrt.ixx.obj;%(AdditionalDependencies) + $(IntDir)..\test_cpp20_module_winrt\winrt.ixx.obj;%(AdditionalDependencies) diff --git a/test/test_module_winrt/test_module_winrt.vcxproj b/test/test_cpp20_module_winrt/test_cpp20_module_winrt.vcxproj similarity index 98% rename from test/test_module_winrt/test_module_winrt.vcxproj rename to test/test_cpp20_module_winrt/test_cpp20_module_winrt.vcxproj index 09e11705d..e1b6170ce 100644 --- a/test/test_module_winrt/test_module_winrt.vcxproj +++ b/test/test_cpp20_module_winrt/test_cpp20_module_winrt.vcxproj @@ -29,8 +29,8 @@ 16.0 {F39B6BA2-3E78-4A01-BF72-D52C39AC3D8C} - test_module_winrt - test_module_winrt + test_cpp20_module_winrt + test_cpp20_module_winrt 20 diff --git a/test/test_cpp23_module/test_cpp23_module.vcxproj b/test/test_cpp23_module/test_cpp23_module.vcxproj new file mode 100644 index 000000000..b146edc59 --- /dev/null +++ b/test/test_cpp23_module/test_cpp23_module.vcxproj @@ -0,0 +1,151 @@ + + + + + Debug + ARM64 + + + Debug + Win32 + + + Release + ARM64 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + {C8D4E5F6-7A8B-4C9D-BE0F-1A2B3C4D5E6F} + test_cpp23_module + test_cpp23_module + + + + Application + true + v143 + + + Application + true + v143 + + + Application + false + true + v143 + + + Application + false + true + v143 + + + Application + true + v143 + + + Application + false + true + v143 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(SolutionDir)test\test_cpp23_module_winrt\Generated Files;..\;%(AdditionalIncludeDirectories) + NOMINMAX;WINRT_LEAN_AND_MEAN;WINRT_IMPORT_STD;WINRT_TEST_MODULES;%(PreprocessorDefinitions) + Use + true + stdcpplatest + $(IntDir)..\test_cpp23_module_winrt\winrt.ixx.ifc;%(AdditionalModuleDependencies) + + + Console + $(IntDir)..\test_cpp23_module_winrt\winrt.ixx.obj;%(AdditionalDependencies) + + + + + Disabled + MultiThreadedDebug + + + + + MaxSpeed + true + true + MultiThreaded + + + true + true + + + + + + NotUsing + + + + + NotUsing + + + Create + + + + + + + + + diff --git a/test/test_cpp23_module_winrt/test_cpp23_module_winrt.vcxproj b/test/test_cpp23_module_winrt/test_cpp23_module_winrt.vcxproj new file mode 100644 index 000000000..ef2e94a58 --- /dev/null +++ b/test/test_cpp23_module_winrt/test_cpp23_module_winrt.vcxproj @@ -0,0 +1,131 @@ + + + + + Debug + ARM64 + + + Debug + Win32 + + + Release + ARM64 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + {A7C3B845-6F1D-4E3A-9B82-7D4F5E6C8A12} + test_cpp23_module_winrt + test_cpp23_module_winrt + + + + StaticLibrary + true + v143 + + + StaticLibrary + true + v143 + + + StaticLibrary + false + true + v143 + + + StaticLibrary + false + true + v143 + + + StaticLibrary + true + v143 + + + StaticLibrary + false + true + v143 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(ProjectDir)Generated Files;%(AdditionalIncludeDirectories) + NotUsing + true + stdcpplatest + WINRT_IMPORT_STD;%(PreprocessorDefinitions) + + + $(CppWinRTDir)cppwinrt -in sdk -out "$(ProjectDir)Generated Files" -verbose + Generating C++/WinRT SDK projection + + + + + Disabled + MultiThreadedDebug + + + + + MaxSpeed + true + true + MultiThreaded + + + + + + + + + From 851c745775c4dc3d80949d3ef626b5d46be8517d Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Fri, 3 Apr 2026 14:56:02 -0700 Subject: [PATCH 27/34] Add nuget test projects. Replace cppwinrt -module with component #ifdefs. Updated docs. --- .github/instructions/modules.instructions.md | 50 +++-- cppwinrt/component_writers.h | 29 ++- cppwinrt/file_writers.h | 32 ++- cppwinrt/main.cpp | 2 - cppwinrt/settings.h | 2 - docs/modules-internals.md | 84 +++++--- docs/modules.md | 201 ++++++++++++++---- nuget/CppWinrtRules.Project.xml | 11 +- nuget/Microsoft.Windows.CppWinRT.targets | 97 ++++++++- nuget/readme.md | 44 ++++ nuget/readme.txt | 3 + test/nuget/NuGetTest.sln | 56 +++++ .../TestModuleBuilder/PropertySheet.props | 1 + .../TestModuleBuilder.vcxproj | 94 ++++++++ test/nuget/TestModuleBuilder/readme.txt | 9 + .../TestModuleComponent.def | 3 + .../TestModuleComponent.vcxproj | 123 +++++++++++ .../TestModuleComponentClass.cpp | 10 + .../TestModuleComponentClass.h | 17 ++ .../TestModuleComponentClass.idl | 9 + test/nuget/TestModuleComponent/pch.cpp | 1 + test/nuget/TestModuleComponent/pch.h | 4 + test/nuget/TestModuleComponent/readme.txt | 9 + .../TestModuleConsumerApp.vcxproj | 114 ++++++++++ test/nuget/TestModuleConsumerApp/main.cpp | 12 ++ test/nuget/TestModuleConsumerApp/readme.txt | 10 + .../TestModuleSingleProject.vcxproj | 111 ++++++++++ test/nuget/TestModuleSingleProject/main.cpp | 12 ++ test/nuget/TestModuleSingleProject/readme.txt | 9 + .../test_component_module.vcxproj | 4 +- 30 files changed, 1029 insertions(+), 134 deletions(-) create mode 100644 test/nuget/TestModuleBuilder/PropertySheet.props create mode 100644 test/nuget/TestModuleBuilder/TestModuleBuilder.vcxproj create mode 100644 test/nuget/TestModuleBuilder/readme.txt create mode 100644 test/nuget/TestModuleComponent/TestModuleComponent.def create mode 100644 test/nuget/TestModuleComponent/TestModuleComponent.vcxproj create mode 100644 test/nuget/TestModuleComponent/TestModuleComponentClass.cpp create mode 100644 test/nuget/TestModuleComponent/TestModuleComponentClass.h create mode 100644 test/nuget/TestModuleComponent/TestModuleComponentClass.idl create mode 100644 test/nuget/TestModuleComponent/pch.cpp create mode 100644 test/nuget/TestModuleComponent/pch.h create mode 100644 test/nuget/TestModuleComponent/readme.txt create mode 100644 test/nuget/TestModuleConsumerApp/TestModuleConsumerApp.vcxproj create mode 100644 test/nuget/TestModuleConsumerApp/main.cpp create mode 100644 test/nuget/TestModuleConsumerApp/readme.txt create mode 100644 test/nuget/TestModuleSingleProject/TestModuleSingleProject.vcxproj create mode 100644 test/nuget/TestModuleSingleProject/main.cpp create mode 100644 test/nuget/TestModuleSingleProject/readme.txt diff --git a/.github/instructions/modules.instructions.md b/.github/instructions/modules.instructions.md index 4ad84a622..0a14cc63a 100644 --- a/.github/instructions/modules.instructions.md +++ b/.github/instructions/modules.instructions.md @@ -1,5 +1,5 @@ --- -applyTo: "strings/**,cppwinrt/**,nuget/**,test/test_cpp20_module_winrt/**,test/test_cpp23_module_winrt/**,test/test_cpp20_module/**,test/test_component_module/**" +applyTo: "strings/**,cppwinrt/**,nuget/**,test/test_cpp20_module_winrt/**,test/test_cpp23_module_winrt/**,test/test_cpp20_module/**,test/test_component_module/**,test/nuget/TestModule*/**" --- # C++20 Module Code Generation @@ -17,25 +17,27 @@ normal headers: 4. `base_module_macros.h` is the single source of truth for macros shared between module and header modes. Don't duplicate definitions — use `#ifndef` guards. -## Module-Aware Code Generation (-module flag) +## Module-Aware Component Code Generation (WINRT_MODULE macro) -When `settings.component_module` is true: +Generated component files use `#ifdef WINRT_MODULE` guards to support both +module and header modes. No special cppwinrt.exe flag is needed — the +`WINRT_MODULE` preprocessor macro is set at the project level by the NuGet +targets (or manually for non-NuGet projects). ### module.g.cpp - Keeps `#include "pch.h"` (PCH is compatible with modules) -- Replaces `#include "winrt/base.h"` with `import winrt;` -- `import std;` is conditional: `#ifdef WINRT_IMPORT_STD` +- `#ifdef WINRT_MODULE`: uses `import winrt;` (and conditional `import std;`) +- `#else`: uses `#include "winrt/base.h"` ### .g.h (component base template) -- Emits `#include "winrt/base_macros.h"` for macros -- Emits conditional `import std;` + unconditional `import winrt;` -- Emits `#define WINRT_IMPL_SKIP_INCLUDES` then includes component's own headers -- Does NOT include platform namespace headers (they come from the module) +- `#ifdef WINRT_MODULE`: emits `#include "winrt/base_macros.h"` for macros, + conditional `import std;`, `import winrt;`, `#define WINRT_IMPL_SKIP_INCLUDES` +- Always includes component's own headers (which skip SDK deps via the guard) ### .g.cpp (factory + optimized constructors) -- In module mode, emits ONLY the `winrt_make_*` factory function -- Constructor/static overrides are omitted (they come from the component's - projection header, which is included by the .g.h after import winrt) +- Always emits the `winrt_make_*` factory function +- Constructor/static overrides are guarded by `#ifndef WINRT_MODULE` + (they come from the projection header via the module in module mode) ## WINRT_IMPL_SKIP_INCLUDES @@ -64,3 +66,27 @@ Generated namespace headers guard cross-namespace `#include` dependencies with: All module test projects use v143 toolset. `import std;` requires `BuildStlModules=true` and either v145 toolset or `/std:c++latest` on v143. + +## NuGet Module Integration (test/nuget/) + +The NuGet targets split module support into two properties: + +- `CppWinRTModuleBuild=true` — Project generates the SDK projection and compiles + winrt.ixx. Exports IFC/OBJ/include paths via `CppWinRTGetModuleOutputs` target. +- `CppWinRTModuleConsume=true` — Project consumes a pre-built module from a + `ProjectReference` to a builder. `CppWinRTResolveModuleReferences` target + resolves paths via `MSBuild` task calling `CppWinRTGetModuleOutputs` on refs. + +Both properties define `WINRT_MODULE` so generated component files use `import winrt;`. +`CppWinRTModuleConsume` auto-sets `CppWinRTEnablePlatformProjection=false`. + +### NuGet Module Test Projects + +- `test/nuget/TestModuleBuilder` — StaticLib, `CppWinRTModuleBuild=true`. Builds + the shared winrt module. No source files — NuGet targets add winrt.ixx. +- `test/nuget/TestModuleConsumerApp` — Console app, `CppWinRTModuleConsume=true`. + References TestModuleBuilder. Uses `import std; import winrt;`. +- `test/nuget/TestModuleComponent` — DLL with IDL, `CppWinRTModuleConsume=true`. + References TestModuleBuilder. Component .g.h files use `import winrt;`. +- `test/nuget/TestModuleSingleProject` — Console app, `CppWinRTModuleBuild=true`. + Builds and consumes module in one project (no separate builder). diff --git a/cppwinrt/component_writers.h b/cppwinrt/component_writers.h index bb185e89d..097bc11c4 100644 --- a/cppwinrt/component_writers.h +++ b/cppwinrt/component_writers.h @@ -136,14 +136,11 @@ namespace cppwinrt static void write_module_g_cpp(writer& w, std::vector const& classes) { - if (settings.component_module) - { - w.write("#ifdef WINRT_IMPORT_STD\nimport std;\n#endif\nimport winrt;\n"); - } - else - { - w.write_root_include("base"); - } + w.write("#ifdef WINRT_MODULE\n"); + w.write("#ifdef WINRT_IMPORT_STD\nimport std;\n#endif\nimport winrt;\n"); + w.write("#else\n"); + w.write_root_include("base"); + w.write("#endif\n"); auto format = R"(% bool __stdcall %_can_unload_now() noexcept { @@ -404,17 +401,14 @@ catch (...) { return winrt::to_hresult(); } return; } - // In module mode, the constructor/static definitions are already exported + // In module mode, the constructor/static definitions are already available // from the winrt module (via the namespace header folded into winrt.ixx). - // Only the winrt_make_* factory function above is needed. - if (settings.component_module) + // Guard them so they're only emitted in header mode. { - return; - } - - auto wrap_type = wrap_type_namespace(w, type_namespace); + auto wrap_type = wrap_type_namespace(w, type_namespace); + w.write("#ifndef WINRT_MODULE\n"); - for (auto&&[factory_name, factory] : get_factories(w, type)) + for (auto&&[factory_name, factory] : get_factories(w, type)) { if (factory.type && is_always_disabled(factory.type)) { @@ -561,6 +555,9 @@ catch (...) { return winrt::to_hresult(); } } } } + + w.write("#endif\n"); + } } static void write_component_override_dispatch_base(writer& w, TypeDef const& type) diff --git a/cppwinrt/file_writers.h b/cppwinrt/file_writers.h index d94502140..104cc8c0d 100644 --- a/cppwinrt/file_writers.h +++ b/cppwinrt/file_writers.h @@ -283,27 +283,19 @@ import std; write_preamble(w); write_include_guard(w); - if (settings.component_module) - { - // In module mode, SDK types and exported impl templates come from - // 'import winrt;'. Component-specific types come from the component's - // own projection headers, included with WINRT_IMPL_SKIP_INCLUDES to - // skip SDK dependencies that are already in the module. - w.write("#include \"winrt/base_macros.h\"\n"); - w.write("#ifdef WINRT_IMPORT_STD\nimport std;\n#endif\n"); - w.write("import winrt;\n"); - w.write("#define WINRT_IMPL_SKIP_INCLUDES\n"); - for (auto&& depends : w.depends) - { - w.write_depends(depends.first); - } - } - else + // In module mode (WINRT_MODULE defined), SDK types and exported impl + // templates come from 'import winrt;'. Component-specific types come + // from the component's own projection headers, included with + // WINRT_IMPL_SKIP_INCLUDES to skip SDK dependencies already in the module. + w.write("#ifdef WINRT_MODULE\n"); + w.write("#include \"winrt/base_macros.h\"\n"); + w.write("#ifdef WINRT_IMPORT_STD\nimport std;\n#endif\n"); + w.write("import winrt;\n"); + w.write("#define WINRT_IMPL_SKIP_INCLUDES\n"); + w.write("#endif\n"); + for (auto&& depends : w.depends) { - for (auto&& depends : w.depends) - { - w.write_depends(depends.first); - } + w.write_depends(depends.first); } auto filename = settings.output_folder + get_generated_component_filename(type) + ".g.h"; diff --git a/cppwinrt/main.cpp b/cppwinrt/main.cpp index 68828066a..501ce2c6a 100644 --- a/cppwinrt/main.cpp +++ b/cppwinrt/main.cpp @@ -39,7 +39,6 @@ namespace cppwinrt { "fastabi", 0, 0 }, // Enable support for the Fast ABI { "ignore_velocity", 0, 0 }, // Ignore feature staging metadata and always include implementations { "synchronous", 0, 0 }, // Instructs cppwinrt to run on a single thread to avoid file system issues in batch builds - { "module", 0, 0, {}, "Generate component files using 'import winrt;' instead of #include" }, }; static void print_usage(writer& w) @@ -173,7 +172,6 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder settings.component_lib = args.value("library", "winrt"); settings.component_opt = args.exists("optimize"); settings.component_ignore_velocity = args.exists("ignore_velocity"); - settings.component_module = args.exists("module"); if (settings.component_pch == ".") { diff --git a/cppwinrt/settings.h b/cppwinrt/settings.h index 0785ae423..e07df4ea2 100644 --- a/cppwinrt/settings.h +++ b/cppwinrt/settings.h @@ -31,8 +31,6 @@ namespace cppwinrt bool fastabi{}; std::map fastabi_cache; - - bool component_module{}; // Generate component files using 'import winrt;' instead of #include }; extern settings_type settings; diff --git a/docs/modules-internals.md b/docs/modules-internals.md index 586a4ec8c..8afede18f 100644 --- a/docs/modules-internals.md +++ b/docs/modules-internals.md @@ -187,43 +187,73 @@ units in the same DLL/EXE. Without `WINRT_EXPORT`, the module-compiled TUs and the header-compiled TUs would see these as separate symbols, leading to one side's customizations being invisible to the other. -## The -module Flag: Component Code Generation +## The WINRT_MODULE Macro: Component Code Generation -**Source**: `settings.h` (`component_module`), `main.cpp` (arg parsing) +**Source**: `component_writers.h` (`write_module_g_cpp`, `write_component_g_cpp`), +`file_writers.h` (`write_component_g_h`) -When `-module` is passed to cppwinrt.exe, the following changes apply to -generated component files: +Module-aware component code generation is controlled entirely by the +`WINRT_MODULE` preprocessor macro, defined at the project level (by the NuGet +targets or manually). The code generator always emits both code paths with +`#ifdef WINRT_MODULE` guards — no special cppwinrt.exe flags are needed. ### module.g.cpp **Source**: `write_module_g_cpp()` in `component_writers.h` -| Header mode | Module mode | -|-------------|-------------| -| `#include "pch.h"` | `#include "pch.h"` (preserved) | -| `#include "winrt/base.h"` | `#ifdef WINRT_IMPORT_STD` / `import std;` / `#endif` + `import winrt;` | +The generated code uses `#ifdef WINRT_MODULE` to select between import and include: + +```cpp +#ifdef WINRT_MODULE +#ifdef WINRT_IMPORT_STD +import std; +#endif +import winrt; +#else +#include "winrt/base.h" +#endif +``` ### Toaster.g.h (component base template) **Source**: `write_component_g_h()` in `file_writers.h` -| Header mode | Module mode | -|-------------|-------------| -| `#include "winrt/test_component.h"` etc. | `#include "winrt/base_macros.h"` + conditional `import std;` + `import winrt;` + `#define WINRT_IMPL_SKIP_INCLUDES` + component headers | +```cpp +#ifdef WINRT_MODULE +#include "winrt/base_macros.h" +#ifdef WINRT_IMPORT_STD +import std; +#endif +import winrt; +#define WINRT_IMPL_SKIP_INCLUDES +#endif +#include "winrt/test_component.h" // always emitted +``` + +When `WINRT_MODULE` is defined, `WINRT_IMPL_SKIP_INCLUDES` causes the component +headers to skip cross-namespace platform `#include` deps (already in the module). +When not defined, the headers pull in `base.h` transitively as normal. ### Toaster.g.cpp (factory + optional optimized constructors) **Source**: `write_component_g_cpp()` in `component_writers.h` -| Header mode | Module mode | -|-------------|-------------| -| `winrt_make_*` + constructor/static overrides | `winrt_make_*` only | +```cpp +void* winrt_make_MyComponent_Toaster() { ... } // always emitted + +WINRT_EXPORT namespace winrt::MyComponent +{ +#ifndef WINRT_MODULE + Toaster::Toaster() : Toaster(make()) {} + // ... other constructors/statics +#endif +} +``` -In module mode, the constructor and static method definitions are omitted from -`.g.cpp` because they're already available from the component's projection -header (included by the `.g.h` after `import winrt;`). The `winrt_make_*` -factory function is always emitted since it's needed by `module.g.cpp` for -activation lookup. +The constructor/static overrides are guarded by `#ifndef WINRT_MODULE` because +they're already available from the component's projection header (included by +the `.g.h` after `import winrt;`). The `winrt_make_*` factory function is +always emitted since it's needed by `module.g.cpp` for activation lookup. ## Exported `winrt::impl` Namespace @@ -294,7 +324,8 @@ In the ixx global module fragment, both files are written as raw `#include`s with no conditional — `import` is not permitted there. `WINRT_IMPORT_STD` is: -- Defined automatically by NuGet targets when `CppWinRTModule=true` and +- Defined automatically by NuGet targets when `CppWinRTModuleBuild` or + `CppWinRTModuleConsume` is true and `BuildStlModules=true` - Can be defined manually by users as a preprocessor definition @@ -311,7 +342,6 @@ would break existing users whose build systems don't compile the std module. | File | Responsibility | |------|---------------| | `main.cpp` | CLI parsing, ixx generation, orchestration | -| `settings.h` | `component_module` flag storage | | `file_writers.h` | File-level writers: `write_base_h`, `write_base_macros_h`, `write_component_g_h`, `write_namespace_h` | | `component_writers.h` | Component template writers: `write_module_g_cpp`, `write_component_g_cpp` | | `code_writers.h` | Low-level writers: `write_version_assert`, `write_parent_depends`, `write_open_file_guard` | @@ -326,17 +356,19 @@ would break existing users whose build systems don't compile the std module. | Target | File | Role | |--------|------|------| -| `CppWinRTAddModuleSource` | `Microsoft.Windows.CppWinRT.targets` | Adds ixx to compilation, defines `WINRT_IMPORT_STD` | +| `CppWinRTBuildModule` | `Microsoft.Windows.CppWinRT.targets` | Adds ixx to compilation, defines `WINRT_MODULE` and `WINRT_IMPORT_STD` | +| `CppWinRTGetModuleOutputs` | `Microsoft.Windows.CppWinRT.targets` | Exports IFC/OBJ/GeneratedFilesDir for consuming projects | +| `CppWinRTResolveModuleReferences` | `Microsoft.Windows.CppWinRT.targets` | Resolves IFC/OBJ from ProjectReferences, defines `WINRT_MODULE` and `WINRT_IMPORT_STD` | | `CppWinRTMakeProjections` | `Microsoft.Windows.CppWinRT.targets` | Orchestrates platform/reference/component projection generation | -| `CppWinRTMakeComponentProjection` | `Microsoft.Windows.CppWinRT.targets` | Runs cppwinrt.exe for component stubs (passes `-module` via `CppWinRTParameters`) | +| `CppWinRTMakeComponentProjection` | `Microsoft.Windows.CppWinRT.targets` | Runs cppwinrt.exe for component stubs | ### Macro flow diagram ``` -User sets CppWinRTModule=true + BuildStlModules=true +User sets CppWinRTModuleBuild=true (or CppWinRTModuleConsume=true) + BuildStlModules=true │ + ├── NuGet targets define WINRT_MODULE on all ClCompile items ├── NuGet targets define WINRT_IMPORT_STD on all ClCompile items - ├── NuGet targets pass -module to cppwinrt.exe │ ▼ winrt.ixx compilation: @@ -354,7 +386,7 @@ Consumer .cpp compilation: import std; ← std module (optional, needs BuildStlModules) import winrt; ← winrt module (SDK types + exported impl) -Component .g.h: +Component .g.h (WINRT_MODULE defined by NuGet targets): #include "winrt/base_macros.h" ← macros only (WINRT_EXPORT etc.) #ifdef WINRT_IMPORT_STD import std; ← conditional on WINRT_IMPORT_STD diff --git a/docs/modules.md b/docs/modules.md index c9482ab83..36f926eee 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -11,30 +11,122 @@ developer experience. ### Consuming the platform projection (app that calls WinRT APIs) -In Visual Studio, set the following in your project property pages: -- **C/C++ > General > C++ Language Standard**: `ISO C++20` or `Preview` -- **C/C++ > General > Build ISO C++23 Standard Library Modules**: `Yes` -- **C++/WinRT > General > C++20 Module**: `true` +C++/WinRT module support uses two properties that separate building the module +from consuming it: -Or equivalently, set these MSBuild properties in your `.vcxproj`: +- **`CppWinRTModuleBuild`** — Generate the platform projection and compile + `winrt.ixx` as a C++20 module interface unit. Set this on one project in your + solution (typically a dedicated static library). Other projects reference this + one to share the pre-built module. + +- **`CppWinRTModuleConsume`** — Consume a pre-built winrt module from a + `ProjectReference` that sets `CppWinRTModuleBuild`. The NuGet targets + automatically resolve the module IFC, OBJ, and include paths. + +For a single-project scenario (no shared builder), just set `CppWinRTModuleBuild` +on your project — it builds and consumes the module in-place. + +#### Multi-project setup (recommended for solutions with multiple consumers) + +**Builder project** (static library, no source files needed): + +In Visual Studio: +- Right-click project > **Properties** +- **Configuration Properties > General > Configuration Type**: `Static Library (.lib)` +- **Configuration Properties > C/C++ > General > C++ Language Standard**: `ISO C++20 (/std:c++20)` or later +- **Configuration Properties > C++/WinRT > General > Build C++20 Module**: `Yes` + +Or equivalently in your `.vcxproj`: ```xml - true + true + + + + + stdcpp20 + + +``` + +**Consumer project** (app or component): + +In Visual Studio: +- Right-click project > **Properties** +- **Configuration Properties > C/C++ > General > C++ Language Standard**: `ISO C++20 (/std:c++20)` or later +- **Configuration Properties > C++/WinRT > General > Consume C++20 Module**: `Yes` +- Add a **Project Reference** to the builder project + +Or equivalently in your `.vcxproj`: + +```xml + + true + + + + + stdcpp20 + + + + + + +``` + +#### Single-project setup + +In Visual Studio: +- Right-click project > **Properties** +- **Configuration Properties > C/C++ > General > C++ Language Standard**: `ISO C++20 (/std:c++20)` or later +- **Configuration Properties > C++/WinRT > General > Build C++20 Module**: `Yes` + +Or equivalently in your `.vcxproj`: + +```xml + + true - true stdcpp20 ``` +#### Optionally enabling `import std;` + +`import winrt;` works independently of `import std;`. If you also want to use +`import std;`, enable it separately: + +In Visual Studio: +- **Configuration Properties > C/C++ > General > Build ISO C++23 Standard Library Modules**: `Yes` + +Or in your `.vcxproj`: + +```xml + + + true + + +``` + +> **Note**: On the v143 toolset (VS 2022), `import std;` requires +> `/std:c++latest` — set **C++ Language Standard** to `Preview` in the IDE. +> On the v145 toolset (VS 2026), `import std;` works with `/std:c++20`. + +When `BuildStlModules` is enabled, the NuGet targets automatically define +`WINRT_IMPORT_STD` so that `base.h` uses `import std;` instead of individual +standard library `#include` directives. + Then in your source files: ```cpp -import std; +import std; // optional — only if BuildStlModules is enabled import winrt; using namespace winrt; @@ -48,9 +140,14 @@ int main() } ``` +If you don't enable `import std;`, you'll need to `#include` standard library +headers you use (e.g., ``, ``) or include them in your PCH. + ### Component authoring (DLL with IDL) -Same project properties as above, then in your implementation files: +Same C++/WinRT module properties as above (`CppWinRTModuleConsume=true` with a +`ProjectReference` to a builder, or `CppWinRTModuleBuild=true` for +single-project), then in your implementation files: ```cpp // MyComponent.cpp @@ -61,44 +158,43 @@ import winrt; ``` The NuGet targets automatically: -1. Pass `-module` to cppwinrt.exe so generated `.g.h`/`.g.cpp` files use +1. Define `WINRT_MODULE` so generated `.g.h`/`.g.cpp` files use `import winrt;` instead of `#include` directives for platform types -2. Generate component projection headers with `WINRT_IMPL_SKIP_INCLUDES` so - they skip platform `#include` directives already available from the module -3. Define `WINRT_IMPORT_STD` when `BuildStlModules` is enabled +2. Define `WINRT_IMPORT_STD` when `BuildStlModules` is enabled +3. Resolve the module IFC, OBJ, and include paths from the builder project + (when `CppWinRTModuleConsume` is used) ## Requirements -- **Visual Studio 2022** (v143 toolset) or later for `import winrt;` +- **Visual Studio 2022** (v143 toolset) or later - **C++20 or later** language standard (`/std:c++20` or `/std:c++latest`) -- For `import std;` alongside `import winrt;`: - - **v143 toolset**: requires `/std:c++latest` (C++23 mode) + `BuildStlModules=true` +- For `import std;` (optional, orthogonal to `import winrt;`): + - **v143 toolset**: requires `/std:c++latest` + `BuildStlModules=true` - **v145 toolset** (VS 2026): works with `/std:c++20` + `BuildStlModules=true` -- **`BuildStlModules=true`** (optional) in `` - to enable the standard library module compilation for `import std;` +- **`BuildStlModules=true`** (optional) in `` metadata enables the + standard library module compilation. This is the "Build ISO C++23 Standard + Library Modules" property in the Visual Studio IDE. Note: on v143, the STL + modules infrastructure (`StdModulesSupported`) is only active when the language + standard is C++23 or later, so setting this property with `/std:c++20` on v143 + has no effect. ## MSBuild Properties | Property | Type | Default | Description | |----------|------|---------|-------------| -| `CppWinRTModule` | bool | `false` | Enable C++20 module mode. Adds `winrt.ixx` to compilation, folds component projections into the module, and passes `-module` to cppwinrt.exe for component generation. | -| `BuildStlModules` | ClCompile metadata | `false` | Enables building `std.ixx`/`std.compat.ixx` so `import std;` works. Set inside ``. This is the same project property set by "Build ISO C++23 Standard Library Modules" (https://learn.microsoft.com/en-us/cpp/build/reference/c-cpp-prop-page?view=msvc-180#cc-language-properties)| +| `CppWinRTModuleBuild` | bool | `false` | Generate the platform projection and compile `winrt.ixx` as a C++20 module interface unit. The compiled module IFC and OBJ are exported for consumption by projects that set `CppWinRTModuleConsume`. Defines `WINRT_MODULE` for generated component code. | +| `CppWinRTModuleConsume` | bool | `false` | Consume a pre-built winrt module from a `ProjectReference` that sets `CppWinRTModuleBuild`. Automatically resolves the IFC (for `AdditionalModuleDependencies`), OBJ (for linker `AdditionalDependencies`), and the builder's `GeneratedFilesDir` (for `AdditionalIncludeDirectories`). Skips regenerating the platform projection by default. Defines `WINRT_MODULE` for generated component code. | +| `BuildStlModules` | ClCompile metadata | `false` | Enables building `std.ixx`/`std.compat.ixx` so `import std;` works. This is orthogonal to `import winrt;` — you can use `import winrt;` without `import std;`. Set via **C/C++ > General > Build ISO C++23 Standard Library Modules** in the IDE, or `true` in `` metadata. On v143, the underlying `StdModulesSupported` infrastructure requires `/std:c++latest`; on v145, `/std:c++20` suffices. | -When `CppWinRTModule=true`, the NuGet targets also automatically: +When `CppWinRTModuleBuild` or `CppWinRTModuleConsume` is true, the NuGet targets also automatically: +- Define `WINRT_MODULE` so generated component files (`.g.h`, `.g.cpp`, + `module.g.cpp`) use `import winrt;` instead of `#include` directives - Define `WINRT_IMPORT_STD` as a preprocessor definition when `BuildStlModules` is enabled, so `base.h` uses `import std;` instead of individual `#include`s -- Define `WINRT_LEAN_AND_MEAN` on the `winrt.ixx` compilation unit +- Define `WINRT_LEAN_AND_MEAN` on the `winrt.ixx` compilation unit (builder only) ## cppwinrt.exe Command-Line Options -### Module-related options - -| Option | Description | -|--------|-------------| -| `-module` | Generate component files (`.g.h`, `.g.cpp`, `module.g.cpp`) using `import winrt;` instead of `#include` directives for platform projection types. The `.g.h` files emit `import winrt;`, include the component's own projection headers with `WINRT_IMPL_SKIP_INCLUDES`, and `#include "winrt/base_macros.h"` for macros. The `.g.cpp` files emit only the `winrt_make_*` factory function (constructor/static optimizations are omitted since those definitions come from the component's projection header included after `import winrt;`). `import std;` is conditional on `WINRT_IMPORT_STD`. PCH includes are preserved. | - -### Other commonly used options - | Option | Description | |--------|-------------| | `-input ` | Windows metadata (.winmd) to include in projection | @@ -202,8 +298,9 @@ namespace winrt::MyComponent::factory_implementation | Macro | Purpose | |-------|---------| -| `WINRT_IMPORT_STD` | When defined alongside `__cpp_lib_modules`, causes `base.h` to emit `import std;` instead of individual standard library `#include` directives. Automatically defined by the NuGet targets when both `CppWinRTModule` and `BuildStlModules` are enabled. | +| `WINRT_IMPORT_STD` | When defined alongside `__cpp_lib_modules`, causes `base.h` to emit `import std;` instead of individual standard library `#include` directives. Automatically defined by the NuGet targets when `BuildStlModules` is enabled alongside `CppWinRTModuleBuild` or `CppWinRTModuleConsume`. | | `WINRT_LEAN_AND_MEAN` | Suppresses `#include ` and `std::hash`/`std::formatter` specializations from generated headers. Reduces header weight. | +| `WINRT_MODULE` | When defined, generated component files (`.g.h`, `.g.cpp`, `module.g.cpp`) use `import winrt;` instead of `#include` directives for platform types. Automatically defined by the NuGet targets when `CppWinRTModuleBuild` or `CppWinRTModuleConsume` is set. Can also be defined manually for non-NuGet projects. | ### Internal implementation macros @@ -273,13 +370,16 @@ component's projection header with `WINRT_IMPL_SKIP_INCLUDES`: - The specializations succeed because the primary templates are exported from the module and visible to the textual include -### Generated file differences in module mode (`-module` flag) +### Generated file differences when `WINRT_MODULE` is defined + +Generated component files contain `#ifdef WINRT_MODULE` guards. The code generator +always emits both paths — the preprocessor selects at compile time: -| File | Header mode | Module mode (`-module`) | -|------|-------------|------------------------| -| `Toaster.g.h` | `#include "winrt/test_component.h"` | `#include "winrt/base_macros.h"` + `import winrt;` + component headers with `WINRT_IMPL_SKIP_INCLUDES` | -| `Toaster.g.cpp` | `winrt_make_*` + constructor/static overrides | `winrt_make_*` only (constructors come from projection header) | -| `module.g.cpp` | `#include "pch.h"` + `#include "winrt/base.h"` | `#include "pch.h"` + `import winrt;` (`import std;` conditional on `WINRT_IMPORT_STD`) | +| File | Without `WINRT_MODULE` | With `WINRT_MODULE` | +|------|------------------------|---------------------| +| `Toaster.g.h` | `#include "winrt/test_component.h"` (pulls in `base.h` transitively) | `#include "winrt/base_macros.h"` + `import winrt;` + `WINRT_IMPL_SKIP_INCLUDES` + component headers | +| `Toaster.g.cpp` | `winrt_make_*` + constructor/static overrides | `winrt_make_*` only (constructors guarded by `#ifndef WINRT_MODULE`) | +| `module.g.cpp` | `#include "winrt/base.h"` | `import winrt;` (`import std;` conditional on `WINRT_IMPORT_STD`) | ### base_macros.h @@ -292,12 +392,31 @@ definitions — no type/function declarations that could conflict with the modul ## NuGet Targets Integration -The `CppWinRTAddModuleSource` target in `Microsoft.Windows.CppWinRT.targets` -performs these steps when `CppWinRTModule=true`: +### CppWinRTBuildModule target + +Runs when `CppWinRTModuleBuild=true`. After the platform projection is generated: 1. **Adds** `winrt.ixx` as a `ClCompile` item with `PrecompiledHeader=NotUsing` -2. **Defines** `WINRT_IMPORT_STD` when `BuildStlModules=true` is detected -3. **Passes** `-module` to cppwinrt.exe via `$(CppWinRTParameters)` so +2. **Defines** `WINRT_MODULE` so generated component files use `import winrt;` +3. **Defines** `WINRT_IMPORT_STD` when `BuildStlModules=true` is detected + +### CppWinRTGetModuleOutputs target + +Defined in every project that imports CppWinRT.targets, but only returns data +when `CppWinRTModuleBuild=true`. Returns the IFC, OBJ, and GeneratedFilesDir +paths. Called by consuming projects via the `MSBuild` task on `ProjectReference` +items. + +### CppWinRTResolveModuleReferences target + +Runs when `CppWinRTModuleConsume=true`. After `ResolveProjectReferences`: + +1. **Calls** `CppWinRTGetModuleOutputs` on each `ProjectReference` +2. **Adds** the resolved IFC to `AdditionalModuleDependencies` +3. **Adds** the resolved OBJ to linker `AdditionalDependencies` +4. **Adds** the builder's `GeneratedFilesDir` to `AdditionalIncludeDirectories` +5. **Defines** `WINRT_IMPORT_STD` when `BuildStlModules=true` is detected +6. **Errors** if no project references with `CppWinRTModuleBuild` are found component generation emits `import winrt;` in `.g.h` and `module.g.cpp` ## Known Limitations diff --git a/nuget/CppWinrtRules.Project.xml b/nuget/CppWinrtRules.Project.xml index 5f4b7501c..66469c270 100644 --- a/nuget/CppWinrtRules.Project.xml +++ b/nuget/CppWinrtRules.Project.xml @@ -86,9 +86,14 @@ Description="Enables the /await:strict compiler option" Category="General" /> - + + diff --git a/nuget/Microsoft.Windows.CppWinRT.targets b/nuget/Microsoft.Windows.CppWinRT.targets index e3b291016..2d9466e02 100644 --- a/nuget/Microsoft.Windows.CppWinRT.targets +++ b/nuget/Microsoft.Windows.CppWinRT.targets @@ -26,7 +26,8 @@ Copyright (C) Microsoft Corporation. All rights reserved. $([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)))..\..\ $([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory))) $(CppWinRTParameters) -fastabi - $(CppWinRTParameters) -module + + false "$(CppWinRTPackageDir)bin\" "$(CppWinRTPackageDir)" @@ -880,23 +881,99 @@ $(XamlMetaDataProviderPch) - - + Condition="'$(CppWinRTModuleBuild)' == 'true'"> + NotUsing - + + + + WINRT_MODULE;%(PreprocessorDefinitions) + + + + + + WINRT_IMPORT_STD;%(PreprocessorDefinitions) + + + + + + + + + $(IntDir)winrt.ixx.ifc + $(IntDir)winrt.ixx.obj + $(GeneratedFilesDir) + + + + + + + + + + + + + + + + + + + + @(_CppWinRTResolvedModuleRefs->'%(ModuleIfc)', ';');%(ClCompile.AdditionalModuleDependencies) + @(_CppWinRTResolvedModuleRefs->'%(ModuleGeneratedFilesDir)', ';');%(ClCompile.AdditionalIncludeDirectories) + + + @(_CppWinRTResolvedModuleRefs->'%(ModuleObj)', ';');%(Link.AdditionalDependencies) + + + + + + + WINRT_MODULE;%(PreprocessorDefinitions) + + + + WINRT_IMPORT_STD;%(PreprocessorDefinitions) diff --git a/nuget/readme.md b/nuget/readme.md index 9379aa716..259136523 100644 --- a/nuget/readme.md +++ b/nuget/readme.md @@ -70,6 +70,8 @@ C++/WinRT behavior can be customized with these project properties: | CppWinRTOptimized | true \| *false | Enables component projection [optimization features](https://kennykerr.ca/2019/06/07/cppwinrt-optimizing-components/) | | CppWinRTGenerateWindowsMetadata | true \| *false | Indicates whether this project produces Windows Metadata | | CppWinRTEnableDefaultPrivateFalse | true \| *false | Indicates whether this project uses C++/WinRT optimized default for copying binaries to the output directory | +| CppWinRTModuleBuild | true \| *false | Generates the platform projection and compiles winrt.ixx as a C++20 module. See [C++20 Modules](#c20-modules) | +| CppWinRTModuleConsume | true \| *false | Consumes a pre-built winrt module from a ProjectReference. See [C++20 Modules](#c20-modules) | \*Default value To customize common C++/WinRT project properties: @@ -132,6 +134,48 @@ void DerivedPage::InitializeComponent() } ``` +## C++20 Modules + +C++/WinRT can generate a C++20 named module (`winrt.ixx`) that allows consumers to write `import winrt;` instead of `#include` directives. This can significantly improve build throughput in multi-project solutions. + +Two properties control module support: + +- **`CppWinRTModuleBuild=true`** — Generates the platform projection and compiles `winrt.ixx`. Set this on a dedicated static library project (the "module builder"). Other projects reference it to share the pre-built module. Can also be set on a standalone app project for single-project scenarios. + +- **`CppWinRTModuleConsume=true`** — Consumes a pre-built winrt module via `ProjectReference` to a builder project. The NuGet targets automatically resolve the module IFC, OBJ, and include paths. Skips regenerating the platform projection. + +Both properties define `WINRT_MODULE` so generated component `.g.h` files use `import winrt;`. + +### Quick example (multi-project) + +**Builder project** (static library, no source files needed): + +In Visual Studio, set **C++/WinRT > Build C++20 Module** to `Yes`, or: +```xml +true +``` + +**Consumer project** (app or component DLL): + +In Visual Studio, set **C++/WinRT > Consume C++20 Module** to `Yes` and add a Project Reference to the builder, or: +```xml +true + +``` + +```cpp +// main.cpp +import winrt; +using namespace winrt::Windows::Foundation; +``` + +### Requirements +- C++20 or later (**C++ Language Standard** set to `ISO C++20` or later in the IDE) +- `import std;` is optional and orthogonal — enable **Build ISO C++23 Standard Library Modules** in the IDE if desired +- On v143 (VS 2022), `import std;` requires **C++ Language Standard** set to `Preview (/std:c++latest)`; on v145 (VS 2026), `ISO C++20` suffices + +For full documentation including component authoring, source file patterns, preprocessor macros, and architecture details, see [docs/modules.md](https://github.com/Microsoft/cppwinrt/blob/master/docs/modules.md) (also included in this NuGet package under the `docs` folder). + ## Troubleshooting The msbuild verbosity level maps to msbuild message importance as follows: diff --git a/nuget/readme.txt b/nuget/readme.txt index 049ab13b8..48b533cc4 100644 --- a/nuget/readme.txt +++ b/nuget/readme.txt @@ -16,6 +16,9 @@ Client code can simply #include these headers, which are created in the generate For any IDL file contained in the project, C++/WinRT creates component (producing) projection headers. In addition, C++/WinRT generates templates and skeleton implementations for each runtime class, under the Generated Files directory. +======================================================================== +For C++20 module support (import winrt;), see docs/modules.md in this package, +or visit: https://github.com/Microsoft/cppwinrt/blob/master/docs/modules.md ======================================================================== For more information, visit: https://github.com/Microsoft/cppwinrt/tree/master/nuget diff --git a/test/nuget/NuGetTest.sln b/test/nuget/NuGetTest.sln index 310b0f252..b2d5c0c1d 100644 --- a/test/nuget/NuGetTest.sln +++ b/test/nuget/NuGetTest.sln @@ -47,6 +47,14 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ConsoleApplication1", "Cons EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TestProxyStub", "TestProxyStub\TestProxyStub.vcxproj", "{98E28FC8-2EB7-4544-9B6A-941462C6D3E2}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TestModuleBuilder", "TestModuleBuilder\TestModuleBuilder.vcxproj", "{A1B2C3D4-1111-2222-3333-444455556666}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TestModuleConsumerApp", "TestModuleConsumerApp\TestModuleConsumerApp.vcxproj", "{A1B2C3D4-5555-6666-7777-888899990000}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TestModuleComponent", "TestModuleComponent\TestModuleComponent.vcxproj", "{A1B2C3D4-AAAA-BBBB-CCCC-DDDDEEEE0000}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TestModuleSingleProject", "TestModuleSingleProject\TestModuleSingleProject.vcxproj", "{A1B2C3D4-FFFF-0000-1111-222233334444}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -279,6 +287,54 @@ Global {98E28FC8-2EB7-4544-9B6A-941462C6D3E2}.Release|x64.Build.0 = Release|x64 {98E28FC8-2EB7-4544-9B6A-941462C6D3E2}.Release|x86.ActiveCfg = Release|Win32 {98E28FC8-2EB7-4544-9B6A-941462C6D3E2}.Release|x86.Build.0 = Release|Win32 + {A1B2C3D4-1111-2222-3333-444455556666}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {A1B2C3D4-1111-2222-3333-444455556666}.Debug|ARM64.Build.0 = Debug|ARM64 + {A1B2C3D4-1111-2222-3333-444455556666}.Debug|x64.ActiveCfg = Debug|x64 + {A1B2C3D4-1111-2222-3333-444455556666}.Debug|x64.Build.0 = Debug|x64 + {A1B2C3D4-1111-2222-3333-444455556666}.Debug|x86.ActiveCfg = Debug|Win32 + {A1B2C3D4-1111-2222-3333-444455556666}.Debug|x86.Build.0 = Debug|Win32 + {A1B2C3D4-1111-2222-3333-444455556666}.Release|ARM64.ActiveCfg = Release|ARM64 + {A1B2C3D4-1111-2222-3333-444455556666}.Release|ARM64.Build.0 = Release|ARM64 + {A1B2C3D4-1111-2222-3333-444455556666}.Release|x64.ActiveCfg = Release|x64 + {A1B2C3D4-1111-2222-3333-444455556666}.Release|x64.Build.0 = Release|x64 + {A1B2C3D4-1111-2222-3333-444455556666}.Release|x86.ActiveCfg = Release|Win32 + {A1B2C3D4-1111-2222-3333-444455556666}.Release|x86.Build.0 = Release|Win32 + {A1B2C3D4-5555-6666-7777-888899990000}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {A1B2C3D4-5555-6666-7777-888899990000}.Debug|ARM64.Build.0 = Debug|ARM64 + {A1B2C3D4-5555-6666-7777-888899990000}.Debug|x64.ActiveCfg = Debug|x64 + {A1B2C3D4-5555-6666-7777-888899990000}.Debug|x64.Build.0 = Debug|x64 + {A1B2C3D4-5555-6666-7777-888899990000}.Debug|x86.ActiveCfg = Debug|Win32 + {A1B2C3D4-5555-6666-7777-888899990000}.Debug|x86.Build.0 = Debug|Win32 + {A1B2C3D4-5555-6666-7777-888899990000}.Release|ARM64.ActiveCfg = Release|ARM64 + {A1B2C3D4-5555-6666-7777-888899990000}.Release|ARM64.Build.0 = Release|ARM64 + {A1B2C3D4-5555-6666-7777-888899990000}.Release|x64.ActiveCfg = Release|x64 + {A1B2C3D4-5555-6666-7777-888899990000}.Release|x64.Build.0 = Release|x64 + {A1B2C3D4-5555-6666-7777-888899990000}.Release|x86.ActiveCfg = Release|Win32 + {A1B2C3D4-5555-6666-7777-888899990000}.Release|x86.Build.0 = Release|Win32 + {A1B2C3D4-AAAA-BBBB-CCCC-DDDDEEEE0000}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {A1B2C3D4-AAAA-BBBB-CCCC-DDDDEEEE0000}.Debug|ARM64.Build.0 = Debug|ARM64 + {A1B2C3D4-AAAA-BBBB-CCCC-DDDDEEEE0000}.Debug|x64.ActiveCfg = Debug|x64 + {A1B2C3D4-AAAA-BBBB-CCCC-DDDDEEEE0000}.Debug|x64.Build.0 = Debug|x64 + {A1B2C3D4-AAAA-BBBB-CCCC-DDDDEEEE0000}.Debug|x86.ActiveCfg = Debug|Win32 + {A1B2C3D4-AAAA-BBBB-CCCC-DDDDEEEE0000}.Debug|x86.Build.0 = Debug|Win32 + {A1B2C3D4-AAAA-BBBB-CCCC-DDDDEEEE0000}.Release|ARM64.ActiveCfg = Release|ARM64 + {A1B2C3D4-AAAA-BBBB-CCCC-DDDDEEEE0000}.Release|ARM64.Build.0 = Release|ARM64 + {A1B2C3D4-AAAA-BBBB-CCCC-DDDDEEEE0000}.Release|x64.ActiveCfg = Release|x64 + {A1B2C3D4-AAAA-BBBB-CCCC-DDDDEEEE0000}.Release|x64.Build.0 = Release|x64 + {A1B2C3D4-AAAA-BBBB-CCCC-DDDDEEEE0000}.Release|x86.ActiveCfg = Release|Win32 + {A1B2C3D4-AAAA-BBBB-CCCC-DDDDEEEE0000}.Release|x86.Build.0 = Release|Win32 + {A1B2C3D4-FFFF-0000-1111-222233334444}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {A1B2C3D4-FFFF-0000-1111-222233334444}.Debug|ARM64.Build.0 = Debug|ARM64 + {A1B2C3D4-FFFF-0000-1111-222233334444}.Debug|x64.ActiveCfg = Debug|x64 + {A1B2C3D4-FFFF-0000-1111-222233334444}.Debug|x64.Build.0 = Debug|x64 + {A1B2C3D4-FFFF-0000-1111-222233334444}.Debug|x86.ActiveCfg = Debug|Win32 + {A1B2C3D4-FFFF-0000-1111-222233334444}.Debug|x86.Build.0 = Debug|Win32 + {A1B2C3D4-FFFF-0000-1111-222233334444}.Release|ARM64.ActiveCfg = Release|ARM64 + {A1B2C3D4-FFFF-0000-1111-222233334444}.Release|ARM64.Build.0 = Release|ARM64 + {A1B2C3D4-FFFF-0000-1111-222233334444}.Release|x64.ActiveCfg = Release|x64 + {A1B2C3D4-FFFF-0000-1111-222233334444}.Release|x64.Build.0 = Release|x64 + {A1B2C3D4-FFFF-0000-1111-222233334444}.Release|x86.ActiveCfg = Release|Win32 + {A1B2C3D4-FFFF-0000-1111-222233334444}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/test/nuget/TestModuleBuilder/PropertySheet.props b/test/nuget/TestModuleBuilder/PropertySheet.props new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/nuget/TestModuleBuilder/PropertySheet.props @@ -0,0 +1 @@ + diff --git a/test/nuget/TestModuleBuilder/TestModuleBuilder.vcxproj b/test/nuget/TestModuleBuilder/TestModuleBuilder.vcxproj new file mode 100644 index 000000000..3b5f351c0 --- /dev/null +++ b/test/nuget/TestModuleBuilder/TestModuleBuilder.vcxproj @@ -0,0 +1,94 @@ + + + + + true + true + true + true + 15.0 + {A1B2C3D4-1111-2222-3333-444455556666} + Win32Proj + TestModuleBuilder + + + + + Debug + ARM64 + + + Debug + Win32 + + + Release + ARM64 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + StaticLibrary + Unicode + + + true + + + false + true + + + + + + + + + + + + + stdcpplatest + NOMINMAX;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + Level4 + true + %(AdditionalOptions) /permissive- /bigobj + true + NotUsing + + + + + Disabled + _DEBUG;%(PreprocessorDefinitions) + + + + + WIN32;%(PreprocessorDefinitions) + + + + + MaxSpeed + true + true + NDEBUG;%(PreprocessorDefinitions) + + + + + + diff --git a/test/nuget/TestModuleBuilder/readme.txt b/test/nuget/TestModuleBuilder/readme.txt new file mode 100644 index 000000000..e5c36fd68 --- /dev/null +++ b/test/nuget/TestModuleBuilder/readme.txt @@ -0,0 +1,9 @@ +This test project builds the winrt C++20 module (winrt.ixx) via the NuGet targets. + +It sets CppWinRTModuleBuild=true, which causes the NuGet targets to: +1. Generate the platform SDK projection headers +2. Compile winrt.ixx as a C++20 module interface unit +3. Export the module IFC/OBJ paths via CppWinRTGetModuleOutputs + +Other projects reference this one and set CppWinRTModuleConsume=true +to automatically pick up the IFC, OBJ, and include paths. diff --git a/test/nuget/TestModuleComponent/TestModuleComponent.def b/test/nuget/TestModuleComponent/TestModuleComponent.def new file mode 100644 index 000000000..d04a02ea8 --- /dev/null +++ b/test/nuget/TestModuleComponent/TestModuleComponent.def @@ -0,0 +1,3 @@ +EXPORTS +DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE +DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE diff --git a/test/nuget/TestModuleComponent/TestModuleComponent.vcxproj b/test/nuget/TestModuleComponent/TestModuleComponent.vcxproj new file mode 100644 index 000000000..0126a8d30 --- /dev/null +++ b/test/nuget/TestModuleComponent/TestModuleComponent.vcxproj @@ -0,0 +1,123 @@ + + + + + true + true + true + true + 15.0 + {A1B2C3D4-AAAA-BBBB-CCCC-DDDDEEEE0000} + Win32Proj + TestModuleComponent + + + + + Debug + ARM64 + + + Debug + Win32 + + + Release + ARM64 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + DynamicLibrary + Unicode + + + true + + + false + true + + + + + + + + + + + + + stdcpplatest + Use + pch.h + _WINRT_DLL;NOMINMAX;%(PreprocessorDefinitions) + Level4 + true + %(AdditionalOptions) /permissive- /bigobj + true + + + TestModuleComponent.def + + + + + Disabled + _DEBUG;%(PreprocessorDefinitions) + + + + + WIN32;%(PreprocessorDefinitions) + + + + + MaxSpeed + true + true + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + + + + + + Create + + + + + + + + Designer + + + + + + + + diff --git a/test/nuget/TestModuleComponent/TestModuleComponentClass.cpp b/test/nuget/TestModuleComponent/TestModuleComponentClass.cpp new file mode 100644 index 000000000..d178cec45 --- /dev/null +++ b/test/nuget/TestModuleComponent/TestModuleComponentClass.cpp @@ -0,0 +1,10 @@ +#include "pch.h" +#include "TestModuleComponentClass.h" +#include "TestModuleComponentClass.g.cpp" + +namespace winrt::TestModuleComponent::implementation +{ + void TestModuleComponentClass::Test() + { + } +} diff --git a/test/nuget/TestModuleComponent/TestModuleComponentClass.h b/test/nuget/TestModuleComponent/TestModuleComponentClass.h new file mode 100644 index 000000000..a554a180d --- /dev/null +++ b/test/nuget/TestModuleComponent/TestModuleComponentClass.h @@ -0,0 +1,17 @@ +#pragma once +#include "TestModuleComponentClass.g.h" + +namespace winrt::TestModuleComponent::implementation +{ + struct TestModuleComponentClass : TestModuleComponentClassT + { + TestModuleComponentClass() = default; + void Test(); + }; +} +namespace winrt::TestModuleComponent::factory_implementation +{ + struct TestModuleComponentClass : TestModuleComponentClassT + { + }; +} diff --git a/test/nuget/TestModuleComponent/TestModuleComponentClass.idl b/test/nuget/TestModuleComponent/TestModuleComponentClass.idl new file mode 100644 index 000000000..93aa4d7a0 --- /dev/null +++ b/test/nuget/TestModuleComponent/TestModuleComponentClass.idl @@ -0,0 +1,9 @@ +namespace TestModuleComponent +{ + [default_interface] + runtimeclass TestModuleComponentClass + { + TestModuleComponentClass(); + void Test(); + } +} diff --git a/test/nuget/TestModuleComponent/pch.cpp b/test/nuget/TestModuleComponent/pch.cpp new file mode 100644 index 000000000..1d9f38c57 --- /dev/null +++ b/test/nuget/TestModuleComponent/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/test/nuget/TestModuleComponent/pch.h b/test/nuget/TestModuleComponent/pch.h new file mode 100644 index 000000000..c1a1a1446 --- /dev/null +++ b/test/nuget/TestModuleComponent/pch.h @@ -0,0 +1,4 @@ +#pragma once +// PCH for a module-consuming component: do NOT include winrt/ headers here. +// Platform types come from 'import winrt;' in the .g.h files. +#include diff --git a/test/nuget/TestModuleComponent/readme.txt b/test/nuget/TestModuleComponent/readme.txt new file mode 100644 index 000000000..0b0468db9 --- /dev/null +++ b/test/nuget/TestModuleComponent/readme.txt @@ -0,0 +1,9 @@ +This test project is a WinRT component (DLL) that consumes a pre-built winrt +C++20 module from TestModuleBuilder. + +It sets CppWinRTModuleConsume=true and has IDL files, which exercises the +component authoring workflow with modules: +- The NuGet targets pass -module to cppwinrt.exe +- Generated .g.h files use 'import winrt;' instead of #include directives +- The PCH does NOT include winrt/ headers (they come from the module) +- Component projection is still generated for the project's own types diff --git a/test/nuget/TestModuleConsumerApp/TestModuleConsumerApp.vcxproj b/test/nuget/TestModuleConsumerApp/TestModuleConsumerApp.vcxproj new file mode 100644 index 000000000..69d21b696 --- /dev/null +++ b/test/nuget/TestModuleConsumerApp/TestModuleConsumerApp.vcxproj @@ -0,0 +1,114 @@ + + + + + true + true + true + true + 15.0 + {A1B2C3D4-5555-6666-7777-888899990000} + Win32Proj + TestModuleConsumerApp + + + + + Debug + ARM64 + + + Debug + Win32 + + + Release + ARM64 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + Application + Unicode + + + true + true + + + false + true + false + + + + + + + + + + + + + stdcpplatest + _CONSOLE;NOMINMAX;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + Level4 + true + %(AdditionalOptions) /permissive- /bigobj + true + NotUsing + + + Console + false + + + + + Disabled + _DEBUG;%(PreprocessorDefinitions) + + + + + WIN32;%(PreprocessorDefinitions) + + + + + MaxSpeed + true + true + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + + + false + + + + + + + + diff --git a/test/nuget/TestModuleConsumerApp/main.cpp b/test/nuget/TestModuleConsumerApp/main.cpp new file mode 100644 index 000000000..f399aa59b --- /dev/null +++ b/test/nuget/TestModuleConsumerApp/main.cpp @@ -0,0 +1,12 @@ +import std; +import winrt; + +using namespace winrt; +using namespace winrt::Windows::Foundation; + +int main() +{ + init_apartment(); + Uri uri(L"http://aka.ms/cppwinrt"); + std::printf("Hello, %ls!\n", uri.AbsoluteUri().c_str()); +} diff --git a/test/nuget/TestModuleConsumerApp/readme.txt b/test/nuget/TestModuleConsumerApp/readme.txt new file mode 100644 index 000000000..f199ccfe8 --- /dev/null +++ b/test/nuget/TestModuleConsumerApp/readme.txt @@ -0,0 +1,10 @@ +This test project consumes a pre-built winrt C++20 module from TestModuleBuilder. + +It sets CppWinRTModuleConsume=true, which causes the NuGet targets to: +1. Resolve the IFC/OBJ from the TestModuleBuilder project reference +2. Add the IFC to AdditionalModuleDependencies +3. Add the OBJ to linker AdditionalDependencies +4. Add the builder's GeneratedFilesDir to AdditionalIncludeDirectories +5. Skip regenerating the platform projection (CppWinRTEnablePlatformProjection=false) + +Source files use 'import winrt;' instead of #include directives. diff --git a/test/nuget/TestModuleSingleProject/TestModuleSingleProject.vcxproj b/test/nuget/TestModuleSingleProject/TestModuleSingleProject.vcxproj new file mode 100644 index 000000000..3c456f948 --- /dev/null +++ b/test/nuget/TestModuleSingleProject/TestModuleSingleProject.vcxproj @@ -0,0 +1,111 @@ + + + + + true + true + true + true + 15.0 + {A1B2C3D4-FFFF-0000-1111-222233334444} + Win32Proj + TestModuleSingleProject + + + + + Debug + ARM64 + + + Debug + Win32 + + + Release + ARM64 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + Application + Unicode + + + true + true + + + false + true + false + + + + + + + + + + + + + stdcpplatest + _CONSOLE;NOMINMAX;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + Level4 + true + %(AdditionalOptions) /permissive- /bigobj + true + NotUsing + + + Console + false + + + + + Disabled + _DEBUG;%(PreprocessorDefinitions) + + + + + WIN32;%(PreprocessorDefinitions) + + + + + MaxSpeed + true + true + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + + + false + + + + + diff --git a/test/nuget/TestModuleSingleProject/main.cpp b/test/nuget/TestModuleSingleProject/main.cpp new file mode 100644 index 000000000..f399aa59b --- /dev/null +++ b/test/nuget/TestModuleSingleProject/main.cpp @@ -0,0 +1,12 @@ +import std; +import winrt; + +using namespace winrt; +using namespace winrt::Windows::Foundation; + +int main() +{ + init_apartment(); + Uri uri(L"http://aka.ms/cppwinrt"); + std::printf("Hello, %ls!\n", uri.AbsoluteUri().c_str()); +} diff --git a/test/nuget/TestModuleSingleProject/readme.txt b/test/nuget/TestModuleSingleProject/readme.txt new file mode 100644 index 000000000..ec2f23236 --- /dev/null +++ b/test/nuget/TestModuleSingleProject/readme.txt @@ -0,0 +1,9 @@ +This test project both builds and consumes the winrt C++20 module in a single project. + +It sets CppWinRTModuleBuild=true only (no CppWinRTModuleConsume), which causes +the NuGet targets to: +1. Generate the platform SDK projection headers +2. Compile winrt.ixx as a C++20 module interface unit +3. Make the module available for import within the same project + +This validates the single-project scenario where there is no separate builder. diff --git a/test/test_component_module/test_component_module.vcxproj b/test/test_component_module/test_component_module.vcxproj index 1f7526ca2..97ec03b6d 100644 --- a/test/test_component_module/test_component_module.vcxproj +++ b/test/test_component_module/test_component_module.vcxproj @@ -98,7 +98,7 @@ .;$(ProjectDir)Generated Files;$(SolutionDir)test\test_cpp20_module_winrt\Generated Files;Generated Files - NOMINMAX;%(PreprocessorDefinitions) + NOMINMAX;WINRT_MODULE;%(PreprocessorDefinitions) Use true $(IntDir)..\test_cpp20_module_winrt\winrt.ixx.ifc;%(AdditionalModuleDependencies) @@ -117,7 +117,7 @@ - $(CppWinRTDir)cppwinrt -input $(OutputPath)test_component_module.winmd -comp "$(ProjectDir)Generated Files" -out "$(ProjectDir)Generated Files" -include test_component_module -ref sdk -verbose -prefix -opt -lib test_module -overwrite -name test_component_module -module + $(CppWinRTDir)cppwinrt -input $(OutputPath)test_component_module.winmd -comp "$(ProjectDir)Generated Files" -out "$(ProjectDir)Generated Files" -include test_component_module -ref sdk -verbose -prefix -opt -lib test_module -overwrite -name test_component_module Projecting Windows and component metadata Generated Files\module.g.cpp From 86c210f671a2002331226ebfe41c0a79dcb8bb1b Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Fri, 3 Apr 2026 15:08:02 -0700 Subject: [PATCH 28/34] Test modules in CI --- .github/workflows/ci.yml | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb8cafd2b..771595d3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,7 +92,7 @@ jobs: compiler: [MSVC, clang-cl] arch: [x86, x64, arm64] config: [Debug, Release] - test_exe: [test, test_nocoro, test_cpp20, test_cpp20_no_sourcelocation, test_fast, test_slow, test_old, test_module_lock_custom, test_module_lock_none] + test_exe: [test, test_nocoro, test_cpp20, test_cpp20_no_sourcelocation, test_fast, test_slow, test_old, test_module_lock_custom, test_module_lock_none, test_cpp20_module] exclude: - arch: arm64 config: Debug @@ -100,6 +100,9 @@ jobs: arch: arm64 - compiler: clang-cl config: Release + # Module tests require MSVC (clang-cl doesn't support C++20 modules) + - test_exe: test_cpp20_module + compiler: clang-cl runs-on: windows-latest steps: - uses: actions/checkout@v6 @@ -364,6 +367,30 @@ jobs: run: | cmd /c "$env:VSDevCmd" "&" msbuild /m /clp:ForceConsoleColor "$env:msbuild_config_props" test\nuget\NugetTest.sln + - 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" + } + build-nuget: name: Build nuget package with MSVC runs-on: windows-latest From 9ef679297d473b6a555db4b4b00601a2c32ca7a9 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Fri, 3 Apr 2026 16:52:57 -0700 Subject: [PATCH 29/34] TestModuleComponent now has multiple namespaces to further check WINRT_IMPL_SKIP_INCLUDES --- .github/instructions/modules.instructions.md | 35 ++++++++++++-- cppwinrt/code_writers.h | 10 ++-- cppwinrt/file_writers.h | 4 +- cppwinrt/main.cpp | 3 +- docs/modules-internals.md | 46 +++++++++++++------ docs/modules.md | 46 +++++++++++++------ nuget/Microsoft.Windows.CppWinRT.targets | 10 +++- .../TestModuleComponent.vcxproj | 5 ++ .../TestModuleComponentClass.cpp | 6 +++ .../TestModuleComponentClass.h | 1 + .../TestModuleComponentClass.idl | 3 ++ .../TestModuleComponentWidget.cpp | 3 ++ .../TestModuleComponentWidget.h | 19 ++++++++ .../TestModuleComponentWidget.idl | 10 ++++ .../TestModuleConsumerApp.vcxproj | 3 ++ test/nuget/TestModuleConsumerApp/main.cpp | 23 ++++++++++ .../TestModuleConsumerApp/platform_test.cpp | 13 ++++++ .../TestModuleConsumerApp/widget_test.cpp | 17 +++++++ 18 files changed, 214 insertions(+), 43 deletions(-) create mode 100644 test/nuget/TestModuleComponent/TestModuleComponentWidget.cpp create mode 100644 test/nuget/TestModuleComponent/TestModuleComponentWidget.h create mode 100644 test/nuget/TestModuleComponent/TestModuleComponentWidget.idl create mode 100644 test/nuget/TestModuleConsumerApp/platform_test.cpp create mode 100644 test/nuget/TestModuleConsumerApp/widget_test.cpp diff --git a/.github/instructions/modules.instructions.md b/.github/instructions/modules.instructions.md index 0a14cc63a..b95dc8216 100644 --- a/.github/instructions/modules.instructions.md +++ b/.github/instructions/modules.instructions.md @@ -31,28 +31,53 @@ targets (or manually for non-NuGet projects). ### .g.h (component base template) - `#ifdef WINRT_MODULE`: emits `#include "winrt/base_macros.h"` for macros, - conditional `import std;`, `import winrt;`, `#define WINRT_IMPL_SKIP_INCLUDES` -- Always includes component's own headers (which skip SDK deps via the guard) + conditional `import std;`, `import winrt;`, locally sets `WINRT_IMPL_SKIP_INCLUDES` +- Always includes component's own headers (platform SDK deps skipped by the + local `WINRT_IMPL_SKIP_INCLUDES`, but cross-namespace component deps are NOT) ### .g.cpp (factory + optimized constructors) - Always emits the `winrt_make_*` factory function - Constructor/static overrides are guarded by `#ifndef WINRT_MODULE` (they come from the projection header via the module in module mode) +## Macro Scoping + +Three macros with distinct scopes control module behavior: + +- `WINRT_BUILD_MODULE` — Defined by cppwinrt inside winrt.ixx's global module + fragment. Controls base.h skip in version assert. Also `#undef`s + `WINRT_IMPL_SKIP_INCLUDES` so cross-namespace deps work inside the ixx. + Never set by users or NuGet targets. +- `WINRT_MODULE` — Defined project-wide by NuGet targets. Controls .g.h/.g.cpp + behavior AND version assert base.h skip. Does NOT suppress cross-namespace + deps between component namespaces. +- `WINRT_IMPL_SKIP_INCLUDES` — Set locally inside .g.h files only (under + `#ifdef WINRT_MODULE`). Suppresses cross-namespace platform SDK deps that + are already in the module. NOT defined project-wide. + ## WINRT_IMPL_SKIP_INCLUDES Generated namespace headers guard cross-namespace `#include` dependencies with: ```cpp #ifndef WINRT_IMPL_SKIP_INCLUDES +#include "winrt/impl/OtherNamespace.0.h" +#endif +``` + +The version assert at the top of each namespace header checks all three macros: +```cpp +#if defined(WINRT_BUILD_MODULE) || defined(WINRT_MODULE) || defined(WINRT_IMPL_SKIP_INCLUDES) +#include "winrt/base_macros.h" +#else #include "winrt/base.h" #endif ``` -- Cross-namespace dependencies (other namespaces' impl headers): GUARDED - (`write_depends_guarded` / `write_root_include_guarded`) +- Cross-namespace dependencies (other namespaces' impl headers): GUARDED by + `WINRT_IMPL_SKIP_INCLUDES` (`write_depends_guarded` / `write_root_include_guarded`) - Self-namespace dependencies (own impl headers): NOT guarded (`write_depends` / `write_root_include`) -- base.h include in version assert: GUARDED with base_macros.h fallback +- base.h include in version assert: GUARDED by all three macros ## Test Project Architecture diff --git a/cppwinrt/code_writers.h b/cppwinrt/code_writers.h index 98ed795f5..39f305af0 100644 --- a/cppwinrt/code_writers.h +++ b/cppwinrt/code_writers.h @@ -37,10 +37,12 @@ namespace cppwinrt static void write_version_assert(writer& w) { - // When WINRT_IMPL_SKIP_INCLUDES is defined, base.h is already available - // from the imported winrt module. However, macros don't cross module - // boundaries, so we include a lightweight header with just the macros. - auto format_guard = R"(#ifdef WINRT_IMPL_SKIP_INCLUDES + // When building the module (WINRT_BUILD_MODULE, inside winrt.ixx) or + // consuming the module (WINRT_MODULE, project-wide), base.h types are + // already available. Macros don't cross module boundaries, so include + // base_macros.h for the version check and other macros. + // WINRT_IMPL_SKIP_INCLUDES also triggers this (used locally in .g.h files). + auto format_guard = R"(#if defined(WINRT_BUILD_MODULE) || defined(WINRT_MODULE) || defined(WINRT_IMPL_SKIP_INCLUDES) #include "winrt/base_macros.h" #else )"; diff --git a/cppwinrt/file_writers.h b/cppwinrt/file_writers.h index 104cc8c0d..c7b6e9c6a 100644 --- a/cppwinrt/file_writers.h +++ b/cppwinrt/file_writers.h @@ -287,11 +287,13 @@ import std; // templates come from 'import winrt;'. Component-specific types come // from the component's own projection headers, included with // WINRT_IMPL_SKIP_INCLUDES to skip SDK dependencies already in the module. + // WINRT_IMPL_SKIP_INCLUDES may already be defined project-wide by the + // NuGet targets, so use #ifndef to avoid redefinition warnings. w.write("#ifdef WINRT_MODULE\n"); w.write("#include \"winrt/base_macros.h\"\n"); w.write("#ifdef WINRT_IMPORT_STD\nimport std;\n#endif\n"); w.write("import winrt;\n"); - w.write("#define WINRT_IMPL_SKIP_INCLUDES\n"); + w.write("#ifndef WINRT_IMPL_SKIP_INCLUDES\n#define WINRT_IMPL_SKIP_INCLUDES\n#endif\n"); w.write("#endif\n"); for (auto&& depends : w.depends) { diff --git a/cppwinrt/main.cpp b/cppwinrt/main.cpp index 501ce2c6a..b722e7155 100644 --- a/cppwinrt/main.cpp +++ b/cppwinrt/main.cpp @@ -344,10 +344,11 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder group.synchronous(args.exists("synchronous")); writer ixx; write_preamble(ixx); - ixx.write("module;\n"); + ixx.write("module;\n#define WINRT_BUILD_MODULE\n#undef WINRT_IMPL_SKIP_INCLUDES\n"); ixx.write(strings::base_includes); ixx.write(strings::base_std_includes); ixx.write("\nexport module winrt;\n#define WINRT_EXPORT export\n#define WINRT_IMPL_INCLUDES_HANDLED\n\n"); + ixx.write("#include \"winrt/base.h\"\n\n"); for (auto&&[ns, members] : c.namespaces()) { diff --git a/docs/modules-internals.md b/docs/modules-internals.md index 8afede18f..cf05124c9 100644 --- a/docs/modules-internals.md +++ b/docs/modules-internals.md @@ -135,38 +135,54 @@ import winrt; ## WINRT_IMPL_SKIP_INCLUDES: Guarded Cross-Namespace Includes -**Problem**: When consuming component headers after `import winrt;`, the -component's namespace header chain includes SDK headers (e.g., -`#include "winrt/impl/Windows.Foundation.2.h"`) that would conflict with -the module-imported versions. +**Problem**: Inside generated `.g.h` files, after `import winrt;`, the +component's own projection headers are included. Those headers have +cross-namespace `#include` deps for platform SDK types (e.g., +`Windows.Foundation`) that are already available from the module. Re-including +platform headers works (MSVC handles the re-declarations gracefully), but +skipping them in this narrow scope avoids redundant processing. -**Solution**: Generated namespace headers wrap their cross-namespace `#include` -dependencies with `#ifndef WINRT_IMPL_SKIP_INCLUDES` guards: +**Solution**: Generated `.g.h` files set `WINRT_IMPL_SKIP_INCLUDES` locally +(under `#ifdef WINRT_MODULE`) before including the component's own headers. +Generated namespace headers wrap their cross-namespace deps with guards: ```cpp // In generated Windows.Foundation.h: #ifndef WINRT_IMPL_SKIP_INCLUDES #include "winrt/base.h" #endif -// ... (base.h include guarded, cross-namespace deps guarded) +// ... (cross-namespace deps guarded) #include "winrt/impl/Windows.Foundation.2.h" // self-namespace: NOT guarded ``` +**Critical scoping**: `WINRT_IMPL_SKIP_INCLUDES` is NOT defined project-wide. +It is only defined locally inside `.g.h` files. This ensures that consumer +source files can `#include ` after `import winrt;` +and the cross-namespace deps between component namespaces resolve normally. + +**WINRT_MODULE vs WINRT_IMPL_SKIP_INCLUDES in the version assert**: The version +assert at the top of every namespace header checks `WINRT_BUILD_MODULE`, +`WINRT_MODULE`, or `WINRT_IMPL_SKIP_INCLUDES` to decide whether to skip +`#include "base.h"` and use `base_macros.h` instead. This three-way check +covers: (a) inside winrt.ixx, (b) consumer source files, (c) inside .g.h files. + +**WINRT_BUILD_MODULE in winrt.ixx**: The ixx defines `WINRT_BUILD_MODULE` +and `#undef`s `WINRT_IMPL_SKIP_INCLUDES` in its global module fragment. This +ensures the version assert uses `base_macros.h` (since base.h is already +explicitly included), but cross-namespace deps are included normally so +namespace headers can reference each other's types inside the ixx. + **Implementation**: `write_root_include_guarded()` in `type_writers.h` wraps the -`#include` with the guard. Used by: -- `write_version_assert()` for `base.h` include +`#include` with `#ifndef WINRT_IMPL_SKIP_INCLUDES`. Used by: - `write_depends_guarded()` for cross-namespace impl includes - `write_parent_depends()` for parent namespace includes +`write_version_assert()` in `code_writers.h` checks `WINRT_BUILD_MODULE || +WINRT_MODULE || WINRT_IMPL_SKIP_INCLUDES` for the base.h skip. + The component's own impl headers are always included (via `write_depends()`, unguarded) since they contain the component-specific type definitions. -**Note**: With the combined-ixx approach, `WINRT_IMPL_SKIP_INCLUDES` is not -needed for component authoring (the component types are in the module). It -remains useful for the scenario of consuming a component's projection header -separately after `import winrt;`, should MSVC add support for cross-module -template specialization in the future. - ## WINRT_EXPORT on Extern Handlers **Source**: `strings/base_extern.h` diff --git a/docs/modules.md b/docs/modules.md index 36f926eee..9e18d4477 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -300,15 +300,16 @@ namespace winrt::MyComponent::factory_implementation |-------|---------| | `WINRT_IMPORT_STD` | When defined alongside `__cpp_lib_modules`, causes `base.h` to emit `import std;` instead of individual standard library `#include` directives. Automatically defined by the NuGet targets when `BuildStlModules` is enabled alongside `CppWinRTModuleBuild` or `CppWinRTModuleConsume`. | | `WINRT_LEAN_AND_MEAN` | Suppresses `#include ` and `std::hash`/`std::formatter` specializations from generated headers. Reduces header weight. | -| `WINRT_MODULE` | When defined, generated component files (`.g.h`, `.g.cpp`, `module.g.cpp`) use `import winrt;` instead of `#include` directives for platform types. Automatically defined by the NuGet targets when `CppWinRTModuleBuild` or `CppWinRTModuleConsume` is set. Can also be defined manually for non-NuGet projects. | +| `WINRT_MODULE` | When defined (project-wide), generated component files (`.g.h`, `.g.cpp`, `module.g.cpp`) use `import winrt;` instead of `#include` directives for platform types. Also causes generated namespace headers to skip `#include "base.h"` in their version assert (using `base_macros.h` instead), since base.h types are available from the module. Automatically defined by the NuGet targets when `CppWinRTModuleBuild` or `CppWinRTModuleConsume` is set. Can also be defined manually for non-NuGet projects. Does NOT suppress cross-namespace `#include` deps between component namespaces — only `WINRT_IMPL_SKIP_INCLUDES` (set locally in `.g.h` files) does that. | ### Internal implementation macros These are used by the code generator and should not be set directly by users: | Macro | Purpose | -|-------|---------| -| `WINRT_IMPL_SKIP_INCLUDES` | When defined, generated namespace headers skip their cross-namespace `#include` dependencies (e.g., `#include "winrt/base.h"`, `#include "winrt/impl/Windows.Foundation.2.h"`). Used when those dependencies are already available from the `winrt` module. The component's own impl headers are never skipped. | +|-------|--------| +| `WINRT_BUILD_MODULE` | Defined by cppwinrt in the winrt.ixx global module fragment. Tells namespace headers to use `base_macros.h` instead of `#include "base.h"` (since base.h is already included explicitly in the ixx). Also `#undef`s `WINRT_IMPL_SKIP_INCLUDES` so cross-namespace deps are included normally inside the ixx. | +| `WINRT_IMPL_SKIP_INCLUDES` | When defined, generated namespace headers skip their cross-namespace `#include` dependencies (e.g., `#include "winrt/impl/Windows.Foundation.2.h"`). Set locally inside generated `.g.h` files under `#ifdef WINRT_MODULE` — NOT defined project-wide. This ensures component-to-component cross-namespace deps still work while platform SDK deps (available from the module) are skipped in the narrow .g.h scope. | | `WINRT_EXPORT` | Expands to `export` inside `winrt.ixx` (module purview), empty in header mode. Used on namespace declarations so types are properly exported from the module. Also applied to `winrt_to_hresult_handler` and related extern handler variables in `base_extern.h` for correct linkage when mixing modules and non-modules code in the same binary. Defined in `base_macros.h` (as empty) for use in generated component files that operate alongside the module. | | `WINRT_IMPL_EMPTY_BASES` | MSVC `__declspec(empty_bases)` optimization. Defined in both `base.h` and `base_macros.h`. | | `WINRT_IMPL_ABI_DECL` | Combines `WINRT_IMPL_NOVTABLE` and `WINRT_IMPL_PUBLIC` for ABI interface declarations. | @@ -339,11 +340,14 @@ winrt/ ``` module; ← Global module fragment +#define WINRT_BUILD_MODULE ← Defined here, controls header behavior +#undef WINRT_IMPL_SKIP_INCLUDES ← Ensure cross-namespace deps work inside ixx ← Platform includes (base_includes) ← Standard library includes (base_std_includes) export module winrt; ← Module purview begins #define WINRT_EXPORT export +#include "winrt/base.h" ← Core library (explicit include) #include "winrt/Windows.Foundation.h" ← Platform namespace headers #include "winrt/Windows.Foundation.Collections.h" @@ -354,21 +358,33 @@ The `winrt::impl` namespace is exported alongside `winrt`, so component projection headers can specialize `impl` templates (like `category`, `abi`, `guid_v`) after `import winrt;` without needing to be folded into the ixx. -### How component projection works with modules +### How component and reference projection headers work with modules The `winrt::impl` namespace is exported from the module, which means the primary templates (`category`, `abi`, `guid_v`, `name_v`, `default_interface`, `consume`) are visible to external code. Component projection headers (`.0.h` files) specialize these templates for the component's types. -When a component's `.g.h` does `import winrt;` and then includes the -component's projection header with `WINRT_IMPL_SKIP_INCLUDES`: -- Platform projection dependencies (`base.h`, `Windows.Foundation.h`, etc.) are - skipped — they're already available from the module -- The component's own impl headers are included normally — they contain - the template specializations that register the component's types -- The specializations succeed because the primary templates are exported - from the module and visible to the textual include +**Consumer experience**: After `import winrt;`, consumers can textually `#include` +reference and component projection headers. `WINRT_MODULE` (defined project-wide +by the NuGet targets) tells each namespace header to skip `#include "base.h"` +and use `base_macros.h` instead — base.h types are already available from the +module. Cross-namespace `#include` deps between component namespaces are NOT +skipped, so multi-namespace components work correctly: + +```cpp +import winrt; // platform types from module +#include // root component namespace +#include // sub-namespace — works! + +auto widget = component.CreateWidget(); // returns MyComponent.Widgets.Widget +auto name = widget.Name(); // calls method on cross-namespace type +``` + +Inside generated `.g.h` files, `WINRT_IMPL_SKIP_INCLUDES` is set locally (under +`#ifdef WINRT_MODULE`) to skip platform SDK deps that are already in the module. +This is scoped to just the `.g.h` — it does not affect other headers included +by consumer source files. ### Generated file differences when `WINRT_MODULE` is defined @@ -456,6 +472,6 @@ Runs when `CppWinRTModuleConsume=true`. After `ResolveProjectReferences`: from the module so that component projection headers can specialize `impl` templates (`category`, `abi`, `guid_v`, etc.) after `import winrt;`. This means component headers can be `#include`d separately without being folded - into `winrt.ixx`. The `WINRT_IMPL_SKIP_INCLUDES` macro is used by generated - `.g.h` files to skip platform `#include` dependencies that are already available - from the module. + into `winrt.ixx`. Inside generated `.g.h` files, `WINRT_IMPL_SKIP_INCLUDES` + is set locally to skip platform `#include` dependencies that come from the + module. Cross-namespace deps between component namespaces are not affected. diff --git a/nuget/Microsoft.Windows.CppWinRT.targets b/nuget/Microsoft.Windows.CppWinRT.targets index 2d9466e02..8be9b25be 100644 --- a/nuget/Microsoft.Windows.CppWinRT.targets +++ b/nuget/Microsoft.Windows.CppWinRT.targets @@ -896,7 +896,12 @@ $(XamlMetaDataProviderPch) NotUsing - + WINRT_MODULE;%(PreprocessorDefinitions) @@ -966,7 +971,8 @@ $(XamlMetaDataProviderPch) - + WINRT_MODULE;%(PreprocessorDefinitions) diff --git a/test/nuget/TestModuleComponent/TestModuleComponent.vcxproj b/test/nuget/TestModuleComponent/TestModuleComponent.vcxproj index 0126a8d30..a1e617d20 100644 --- a/test/nuget/TestModuleComponent/TestModuleComponent.vcxproj +++ b/test/nuget/TestModuleComponent/TestModuleComponent.vcxproj @@ -98,10 +98,12 @@ + + Create @@ -114,6 +116,9 @@ Designer + + Designer + diff --git a/test/nuget/TestModuleComponent/TestModuleComponentClass.cpp b/test/nuget/TestModuleComponent/TestModuleComponentClass.cpp index d178cec45..0162436d0 100644 --- a/test/nuget/TestModuleComponent/TestModuleComponentClass.cpp +++ b/test/nuget/TestModuleComponent/TestModuleComponentClass.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include "TestModuleComponentClass.h" +#include "TestModuleComponentWidget.h" #include "TestModuleComponentClass.g.cpp" namespace winrt::TestModuleComponent::implementation @@ -7,4 +8,9 @@ namespace winrt::TestModuleComponent::implementation void TestModuleComponentClass::Test() { } + + winrt::TestModuleComponent::Widgets::Widget TestModuleComponentClass::CreateWidget() + { + return winrt::make(L"FromComponent"); + } } diff --git a/test/nuget/TestModuleComponent/TestModuleComponentClass.h b/test/nuget/TestModuleComponent/TestModuleComponentClass.h index a554a180d..5dd2d81b0 100644 --- a/test/nuget/TestModuleComponent/TestModuleComponentClass.h +++ b/test/nuget/TestModuleComponent/TestModuleComponentClass.h @@ -7,6 +7,7 @@ namespace winrt::TestModuleComponent::implementation { TestModuleComponentClass() = default; void Test(); + winrt::TestModuleComponent::Widgets::Widget CreateWidget(); }; } namespace winrt::TestModuleComponent::factory_implementation diff --git a/test/nuget/TestModuleComponent/TestModuleComponentClass.idl b/test/nuget/TestModuleComponent/TestModuleComponentClass.idl index 93aa4d7a0..34ada57f8 100644 --- a/test/nuget/TestModuleComponent/TestModuleComponentClass.idl +++ b/test/nuget/TestModuleComponent/TestModuleComponentClass.idl @@ -1,3 +1,5 @@ +import "TestModuleComponentWidget.idl"; + namespace TestModuleComponent { [default_interface] @@ -5,5 +7,6 @@ namespace TestModuleComponent { TestModuleComponentClass(); void Test(); + TestModuleComponent.Widgets.Widget CreateWidget(); } } diff --git a/test/nuget/TestModuleComponent/TestModuleComponentWidget.cpp b/test/nuget/TestModuleComponent/TestModuleComponentWidget.cpp new file mode 100644 index 000000000..f1d3fcde6 --- /dev/null +++ b/test/nuget/TestModuleComponent/TestModuleComponentWidget.cpp @@ -0,0 +1,3 @@ +#include "pch.h" +#include "TestModuleComponentWidget.h" +#include "Widgets.Widget.g.cpp" diff --git a/test/nuget/TestModuleComponent/TestModuleComponentWidget.h b/test/nuget/TestModuleComponent/TestModuleComponentWidget.h new file mode 100644 index 000000000..6ea3b8674 --- /dev/null +++ b/test/nuget/TestModuleComponent/TestModuleComponentWidget.h @@ -0,0 +1,19 @@ +#pragma once +#include "Widgets.Widget.g.h" + +namespace winrt::TestModuleComponent::Widgets::implementation +{ + struct Widget : WidgetT + { + Widget() = default; + Widget(hstring const& name) : m_name(name) {} + hstring Name() const { return m_name; } + + private: + hstring m_name{ L"DefaultWidget" }; + }; +} +namespace winrt::TestModuleComponent::Widgets::factory_implementation +{ + struct Widget : WidgetT {}; +} diff --git a/test/nuget/TestModuleComponent/TestModuleComponentWidget.idl b/test/nuget/TestModuleComponent/TestModuleComponentWidget.idl new file mode 100644 index 000000000..d30ed42be --- /dev/null +++ b/test/nuget/TestModuleComponent/TestModuleComponentWidget.idl @@ -0,0 +1,10 @@ +namespace TestModuleComponent.Widgets +{ + [default_interface] + runtimeclass Widget + { + Widget(); + Widget(String name); + String Name{ get; }; + } +} diff --git a/test/nuget/TestModuleConsumerApp/TestModuleConsumerApp.vcxproj b/test/nuget/TestModuleConsumerApp/TestModuleConsumerApp.vcxproj index 69d21b696..bfab231ad 100644 --- a/test/nuget/TestModuleConsumerApp/TestModuleConsumerApp.vcxproj +++ b/test/nuget/TestModuleConsumerApp/TestModuleConsumerApp.vcxproj @@ -100,6 +100,8 @@ + + @@ -108,6 +110,7 @@ + diff --git a/test/nuget/TestModuleConsumerApp/main.cpp b/test/nuget/TestModuleConsumerApp/main.cpp index f399aa59b..aac02868f 100644 --- a/test/nuget/TestModuleConsumerApp/main.cpp +++ b/test/nuget/TestModuleConsumerApp/main.cpp @@ -1,12 +1,35 @@ import std; import winrt; +// Include just the root component namespace header — NOT the Widgets namespace. +// CreateWidget() returns a Widgets::Widget, but we can call it and hold the +// result without including the Widgets header, as long as we don't call +// methods on the Widget object itself. +#include + using namespace winrt; using namespace winrt::Windows::Foundation; +void test_platform_types(); +void test_widget_usage(); + int main() { init_apartment(); + + // Platform types (from the winrt module) Uri uri(L"http://aka.ms/cppwinrt"); std::printf("Hello, %ls!\n", uri.AbsoluteUri().c_str()); + + // Component types — call a method that returns a cross-namespace type + // without including the Widgets namespace header. + winrt::TestModuleComponent::TestModuleComponentClass component; + component.Test(); + auto widget = component.CreateWidget(); // Returns Widgets::Widget + // We can hold 'widget' but to call Widget.Name() we'd need the Widgets header. + std::printf("Component test passed.\n"); + + // Delegate to other TUs that test more scenarios + test_platform_types(); + test_widget_usage(); } diff --git a/test/nuget/TestModuleConsumerApp/platform_test.cpp b/test/nuget/TestModuleConsumerApp/platform_test.cpp new file mode 100644 index 000000000..ffad075ec --- /dev/null +++ b/test/nuget/TestModuleConsumerApp/platform_test.cpp @@ -0,0 +1,13 @@ +import std; +import winrt; + +// Test using platform types from the module in a separate TU. +// No component headers needed here. +using namespace winrt; +using namespace winrt::Windows::Foundation; + +void test_platform_types() +{ + Uri uri(L"https://example.com"); + std::printf("Platform types in separate TU: %ls\n", uri.Domain().c_str()); +} diff --git a/test/nuget/TestModuleConsumerApp/widget_test.cpp b/test/nuget/TestModuleConsumerApp/widget_test.cpp new file mode 100644 index 000000000..9992d6769 --- /dev/null +++ b/test/nuget/TestModuleConsumerApp/widget_test.cpp @@ -0,0 +1,17 @@ +import std; +import winrt; + +// This TU includes BOTH component namespace headers so it can call methods +// on the Widget type returned by CreateWidget(). +#include +#include + +void test_widget_usage() +{ + winrt::TestModuleComponent::TestModuleComponentClass component; + auto widget = component.CreateWidget(); + + // Now we CAN call Widget methods because we included the Widgets header. + auto name = widget.Name(); + std::printf("Widget name: %ls\n", name.c_str()); +} From 4ff8c7bd0231ec68bc69bbe30edc400ce2e63205 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Fri, 3 Apr 2026 19:29:59 -0700 Subject: [PATCH 30/34] Explicitly generate list of module namespaces and use that for header exclusion. --- cppwinrt/code_writers.h | 13 ++++++--- cppwinrt/file_writers.h | 11 ++++---- cppwinrt/main.cpp | 27 ++++++++++++++++++- cppwinrt/type_writers.h | 20 +++++++++++++- nuget/Microsoft.Windows.CppWinRT.targets | 13 ++++----- .../TestModuleComponentClass.cpp | 11 ++++++++ .../TestModuleComponentClass.h | 2 ++ .../TestModuleComponentClass.idl | 8 ++++++ .../TestModuleComponentWidget.h | 1 + .../TestModuleComponentWidget.idl | 7 +++++ .../TestModuleConsumerApp/widget_test.cpp | 10 +++++++ 11 files changed, 107 insertions(+), 16 deletions(-) diff --git a/cppwinrt/code_writers.h b/cppwinrt/code_writers.h index 39f305af0..1fa8edd5b 100644 --- a/cppwinrt/code_writers.h +++ b/cppwinrt/code_writers.h @@ -41,10 +41,17 @@ namespace cppwinrt // consuming the module (WINRT_MODULE, project-wide), base.h types are // already available. Macros don't cross module boundaries, so include // base_macros.h for the version check and other macros. - // WINRT_IMPL_SKIP_INCLUDES also triggers this (used locally in .g.h files). - auto format_guard = R"(#if defined(WINRT_BUILD_MODULE) || defined(WINRT_MODULE) || defined(WINRT_IMPL_SKIP_INCLUDES) + // When WINRT_MODULE is defined, also include winrt_module_namespaces.h + // which declares per-namespace macros (WINRT_MODULE_NS_*) so that + // cross-namespace #include guards can precisely skip only the namespaces + // that are in the module. + auto format_guard = R"(#if defined(WINRT_BUILD_MODULE) || defined(WINRT_MODULE) #include "winrt/base_macros.h" -#else +#endif +#if defined(WINRT_MODULE) && !defined(WINRT_BUILD_MODULE) +#include "winrt/winrt_module_namespaces.h" +#endif +#if !defined(WINRT_BUILD_MODULE) && !defined(WINRT_MODULE) )"; auto format_end = R"(#endif )"; diff --git a/cppwinrt/file_writers.h b/cppwinrt/file_writers.h index c7b6e9c6a..023f5f861 100644 --- a/cppwinrt/file_writers.h +++ b/cppwinrt/file_writers.h @@ -285,15 +285,16 @@ import std; // In module mode (WINRT_MODULE defined), SDK types and exported impl // templates come from 'import winrt;'. Component-specific types come - // from the component's own projection headers, included with - // WINRT_IMPL_SKIP_INCLUDES to skip SDK dependencies already in the module. - // WINRT_IMPL_SKIP_INCLUDES may already be defined project-wide by the - // NuGet targets, so use #ifndef to avoid redefinition warnings. + // from the component's own projection headers. The version assert in + // each header includes winrt_module_namespaces.h (when WINRT_MODULE is + // defined), which provides per-namespace WINRT_MODULE_NS_* macros. + // Cross-namespace deps use these per-namespace guards: platform deps + // (in the module) are skipped, component deps (not in the module) are + // included normally. w.write("#ifdef WINRT_MODULE\n"); w.write("#include \"winrt/base_macros.h\"\n"); w.write("#ifdef WINRT_IMPORT_STD\nimport std;\n#endif\n"); w.write("import winrt;\n"); - w.write("#ifndef WINRT_IMPL_SKIP_INCLUDES\n#define WINRT_IMPL_SKIP_INCLUDES\n#endif\n"); w.write("#endif\n"); for (auto&& depends : w.depends) { diff --git a/cppwinrt/main.cpp b/cppwinrt/main.cpp index b722e7155..5d0352268 100644 --- a/cppwinrt/main.cpp +++ b/cppwinrt/main.cpp @@ -344,7 +344,7 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder group.synchronous(args.exists("synchronous")); writer ixx; write_preamble(ixx); - ixx.write("module;\n#define WINRT_BUILD_MODULE\n#undef WINRT_IMPL_SKIP_INCLUDES\n"); + ixx.write("module;\n#define WINRT_BUILD_MODULE\n"); ixx.write(strings::base_includes); ixx.write(strings::base_std_includes); ixx.write("\nexport module winrt;\n#define WINRT_EXPORT export\n#define WINRT_IMPL_INCLUDES_HANDLED\n\n"); @@ -373,6 +373,31 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder write_base_h(); write_base_macros_h(); ixx.flush_to_file(settings.output_folder + "winrt/winrt.ixx"); + + // Generate a companion header that declares which namespaces are + // in the module. Consumer projects include this (via WINRT_MODULE) + // so that cross-namespace #include guards can precisely skip only + // namespaces available from the module, while still including + // component and other non-module namespace deps. + // Only generated alongside the ixx (not for component projections + // which may also set -base but shouldn't produce module metadata). + if (!settings.component) + { + writer module_ns; + write_preamble(module_ns); + module_ns.write("#pragma once\n"); + for (auto&&[ns, members] : c.namespaces()) + { + if (!has_projected_types(members) || !settings.projection_filter.includes(members)) + { + continue; + } + std::string ns_macro{ ns }; + std::replace(ns_macro.begin(), ns_macro.end(), '.', '_'); + module_ns.write("#define WINRT_MODULE_NS_%\n", ns_macro); + } + module_ns.flush_to_file(settings.output_folder + "winrt/winrt_module_namespaces.h"); + } } if (settings.component) diff --git a/cppwinrt/type_writers.h b/cppwinrt/type_writers.h index d057c0f9e..84df25bf3 100644 --- a/cppwinrt/type_writers.h +++ b/cppwinrt/type_writers.h @@ -566,12 +566,30 @@ namespace cppwinrt void write_root_include_guarded(std::string_view const& include) { - auto format = R"(#ifndef WINRT_IMPL_SKIP_INCLUDES + // Extract the namespace from the include path. The include is either + // "NS" (for main headers) or "impl/NS.N" (for impl headers). + // The per-namespace guard macro is WINRT_MODULE_NS_. + std::string ns_part; + if (include.size() > 5 && include.substr(0, 5) == "impl/") + { + auto remainder = include.substr(5); // skip "impl/" + auto dot = remainder.rfind('.'); + ns_part = std::string(remainder.substr(0, dot)); + } + else + { + ns_part = std::string(include); + } + std::string ns_macro = ns_part; + std::replace(ns_macro.begin(), ns_macro.end(), '.', '_'); + + auto format = R"(#ifndef WINRT_MODULE_NS_% #include %winrt/%.h% #endif )"; write(format, + ns_macro, settings.brackets ? '<' : '\"', include, settings.brackets ? '>' : '\"'); diff --git a/nuget/Microsoft.Windows.CppWinRT.targets b/nuget/Microsoft.Windows.CppWinRT.targets index 8be9b25be..879b5bb04 100644 --- a/nuget/Microsoft.Windows.CppWinRT.targets +++ b/nuget/Microsoft.Windows.CppWinRT.targets @@ -897,11 +897,12 @@ $(XamlMetaDataProviderPch) + import winrt;. When WINRT_MODULE is defined, namespace headers include + winrt_module_namespaces.h which declares per-namespace WINRT_MODULE_NS_* + macros. Cross-namespace deps are guarded by these per-namespace macros, + precisely skipping only namespaces in the module while including + component and other non-module deps normally. + winrt.ixx defines WINRT_BUILD_MODULE in its global module fragment. --> WINRT_MODULE;%(PreprocessorDefinitions) @@ -972,7 +973,7 @@ $(XamlMetaDataProviderPch) + import winrt;, and so namespace headers use per-namespace guards. --> WINRT_MODULE;%(PreprocessorDefinitions) diff --git a/test/nuget/TestModuleComponent/TestModuleComponentClass.cpp b/test/nuget/TestModuleComponent/TestModuleComponentClass.cpp index 0162436d0..7abd3a528 100644 --- a/test/nuget/TestModuleComponent/TestModuleComponentClass.cpp +++ b/test/nuget/TestModuleComponent/TestModuleComponentClass.cpp @@ -13,4 +13,15 @@ namespace winrt::TestModuleComponent::implementation { return winrt::make(L"FromComponent"); } + + winrt::TestModuleComponent::WidgetInfo TestModuleComponentClass::GetWidgetInfo() + { + auto widget = CreateWidget(); + return { widget.Size(), L"A test widget" }; + } + + winrt::Windows::Foundation::Uri TestModuleComponentClass::GetUri() + { + return winrt::Windows::Foundation::Uri(L"http://aka.ms/cppwinrt"); + } } diff --git a/test/nuget/TestModuleComponent/TestModuleComponentClass.h b/test/nuget/TestModuleComponent/TestModuleComponentClass.h index 5dd2d81b0..313a384fe 100644 --- a/test/nuget/TestModuleComponent/TestModuleComponentClass.h +++ b/test/nuget/TestModuleComponent/TestModuleComponentClass.h @@ -8,6 +8,8 @@ namespace winrt::TestModuleComponent::implementation TestModuleComponentClass() = default; void Test(); winrt::TestModuleComponent::Widgets::Widget CreateWidget(); + winrt::TestModuleComponent::WidgetInfo GetWidgetInfo(); + winrt::Windows::Foundation::Uri GetUri(); }; } namespace winrt::TestModuleComponent::factory_implementation diff --git a/test/nuget/TestModuleComponent/TestModuleComponentClass.idl b/test/nuget/TestModuleComponent/TestModuleComponentClass.idl index 34ada57f8..993b37213 100644 --- a/test/nuget/TestModuleComponent/TestModuleComponentClass.idl +++ b/test/nuget/TestModuleComponent/TestModuleComponentClass.idl @@ -2,11 +2,19 @@ import "TestModuleComponentWidget.idl"; namespace TestModuleComponent { + struct WidgetInfo + { + TestModuleComponent.Widgets.WidgetSize Size; + String Description; + }; + [default_interface] runtimeclass TestModuleComponentClass { TestModuleComponentClass(); void Test(); TestModuleComponent.Widgets.Widget CreateWidget(); + WidgetInfo GetWidgetInfo(); + Windows.Foundation.Uri GetUri(); } } diff --git a/test/nuget/TestModuleComponent/TestModuleComponentWidget.h b/test/nuget/TestModuleComponent/TestModuleComponentWidget.h index 6ea3b8674..f2234c80d 100644 --- a/test/nuget/TestModuleComponent/TestModuleComponentWidget.h +++ b/test/nuget/TestModuleComponent/TestModuleComponentWidget.h @@ -8,6 +8,7 @@ namespace winrt::TestModuleComponent::Widgets::implementation Widget() = default; Widget(hstring const& name) : m_name(name) {} hstring Name() const { return m_name; } + winrt::TestModuleComponent::Widgets::WidgetSize Size() const { return { 100.0f, 50.0f }; } private: hstring m_name{ L"DefaultWidget" }; diff --git a/test/nuget/TestModuleComponent/TestModuleComponentWidget.idl b/test/nuget/TestModuleComponent/TestModuleComponentWidget.idl index d30ed42be..7a1814f7e 100644 --- a/test/nuget/TestModuleComponent/TestModuleComponentWidget.idl +++ b/test/nuget/TestModuleComponent/TestModuleComponentWidget.idl @@ -1,10 +1,17 @@ namespace TestModuleComponent.Widgets { + struct WidgetSize + { + Single Width; + Single Height; + }; + [default_interface] runtimeclass Widget { Widget(); Widget(String name); String Name{ get; }; + WidgetSize Size{ get; }; } } diff --git a/test/nuget/TestModuleConsumerApp/widget_test.cpp b/test/nuget/TestModuleConsumerApp/widget_test.cpp index 9992d6769..8da4c962c 100644 --- a/test/nuget/TestModuleConsumerApp/widget_test.cpp +++ b/test/nuget/TestModuleConsumerApp/widget_test.cpp @@ -14,4 +14,14 @@ void test_widget_usage() // Now we CAN call Widget methods because we included the Widgets header. auto name = widget.Name(); std::printf("Widget name: %ls\n", name.c_str()); + + // Test the cross-namespace struct (WidgetSize field from Widgets namespace + // in a struct defined in the root TestModuleComponent namespace). + auto info = component.GetWidgetInfo(); + std::printf("Widget info: %ls (%.0fx%.0f)\n", info.Description.c_str(), info.Size.Width, info.Size.Height); + + // Test calling a method that returns a platform type (Windows.Foundation.Uri). + // The platform type comes from the module, the component type doesn't. + auto uri = component.GetUri(); + std::printf("Component URI: %ls\n", uri.AbsoluteUri().c_str()); } From e25fb9a8eecb2cb8bbc43e987c4cc50eccbfa767 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Fri, 3 Apr 2026 19:40:11 -0700 Subject: [PATCH 31/34] WINRT_IMPL_SKIP_INCLUDES is finally gone --- .github/instructions/modules.instructions.md | 50 ++++---- cppwinrt/file_writers.h | 8 +- docs/modules-internals.md | 114 +++++++++++------- docs/modules.md | 23 ++-- test/test_cpp20_module/component.cpp | 7 +- .../test_cpp20_module.vcxproj | 2 +- 6 files changed, 115 insertions(+), 89 deletions(-) diff --git a/.github/instructions/modules.instructions.md b/.github/instructions/modules.instructions.md index b95dc8216..c4675f0a2 100644 --- a/.github/instructions/modules.instructions.md +++ b/.github/instructions/modules.instructions.md @@ -31,9 +31,10 @@ targets (or manually for non-NuGet projects). ### .g.h (component base template) - `#ifdef WINRT_MODULE`: emits `#include "winrt/base_macros.h"` for macros, - conditional `import std;`, `import winrt;`, locally sets `WINRT_IMPL_SKIP_INCLUDES` -- Always includes component's own headers (platform SDK deps skipped by the - local `WINRT_IMPL_SKIP_INCLUDES`, but cross-namespace component deps are NOT) + conditional `import std;`, `import winrt;` +- Always includes component's own headers. Platform deps are skipped by + per-namespace `WINRT_MODULE_NS_*` guards; component cross-namespace deps + are included normally. ### .g.cpp (factory + optimized constructors) - Always emits the `winrt_make_*` factory function @@ -42,42 +43,47 @@ targets (or manually for non-NuGet projects). ## Macro Scoping -Three macros with distinct scopes control module behavior: +Two project-level macros and one set of per-namespace macros control behavior: - `WINRT_BUILD_MODULE` — Defined by cppwinrt inside winrt.ixx's global module - fragment. Controls base.h skip in version assert. Also `#undef`s - `WINRT_IMPL_SKIP_INCLUDES` so cross-namespace deps work inside the ixx. + fragment. Controls base.h skip in version assert. Does NOT include + `winrt_module_namespaces.h` (inside the ixx, all deps must resolve). Never set by users or NuGet targets. - `WINRT_MODULE` — Defined project-wide by NuGet targets. Controls .g.h/.g.cpp - behavior AND version assert base.h skip. Does NOT suppress cross-namespace - deps between component namespaces. -- `WINRT_IMPL_SKIP_INCLUDES` — Set locally inside .g.h files only (under - `#ifdef WINRT_MODULE`). Suppresses cross-namespace platform SDK deps that - are already in the module. NOT defined project-wide. + behavior, version assert base.h skip, AND triggers inclusion of + `winrt_module_namespaces.h` for per-namespace guards. +- `WINRT_MODULE_NS_*` — Per-namespace macros (e.g. `WINRT_MODULE_NS_Windows_Foundation`) + defined in the generated `winrt_module_namespaces.h`. Each cross-namespace + `#include` dep is guarded by `#ifndef WINRT_MODULE_NS_`. Only + namespaces in the module are skipped; component and other deps always resolve. -## WINRT_IMPL_SKIP_INCLUDES +## Per-Namespace Include Guards -Generated namespace headers guard cross-namespace `#include` dependencies with: +Generated namespace headers use per-namespace guards for cross-namespace deps: ```cpp -#ifndef WINRT_IMPL_SKIP_INCLUDES -#include "winrt/impl/OtherNamespace.0.h" +#ifndef WINRT_MODULE_NS_Windows_Foundation +#include "winrt/impl/Windows.Foundation.0.h" #endif ``` -The version assert at the top of each namespace header checks all three macros: +The version assert at the top of each namespace header: ```cpp -#if defined(WINRT_BUILD_MODULE) || defined(WINRT_MODULE) || defined(WINRT_IMPL_SKIP_INCLUDES) +#if defined(WINRT_BUILD_MODULE) || defined(WINRT_MODULE) #include "winrt/base_macros.h" -#else +#endif +#if defined(WINRT_MODULE) && !defined(WINRT_BUILD_MODULE) +#include "winrt/winrt_module_namespaces.h" +#endif +#if !defined(WINRT_BUILD_MODULE) && !defined(WINRT_MODULE) #include "winrt/base.h" #endif ``` -- Cross-namespace dependencies (other namespaces' impl headers): GUARDED by - `WINRT_IMPL_SKIP_INCLUDES` (`write_depends_guarded` / `write_root_include_guarded`) -- Self-namespace dependencies (own impl headers): NOT guarded +- Cross-namespace dependencies: GUARDED by `WINRT_MODULE_NS_*` + (`write_depends_guarded` / `write_root_include_guarded`) +- Self-namespace dependencies: NOT guarded (`write_depends` / `write_root_include`) -- base.h include in version assert: GUARDED by all three macros +- base.h include: GUARDED by `WINRT_BUILD_MODULE || WINRT_MODULE` ## Test Project Architecture diff --git a/cppwinrt/file_writers.h b/cppwinrt/file_writers.h index 023f5f861..d08538514 100644 --- a/cppwinrt/file_writers.h +++ b/cppwinrt/file_writers.h @@ -60,10 +60,10 @@ import std; } // Lightweight header containing only the preprocessor macros needed by - // generated namespace headers. Used when WINRT_IMPL_SKIP_INCLUDES is - // defined (i.e., consuming component headers after 'import winrt;'). - // Macros don't cross module boundaries, so this provides the macros - // that base.h would normally supply. + // generated namespace headers. Used when WINRT_MODULE or WINRT_BUILD_MODULE + // is defined (i.e., consuming or building the winrt module). Macros don't + // cross module boundaries, so this provides the macros that base.h would + // normally supply. static void write_base_macros_h() { writer w; diff --git a/docs/modules-internals.md b/docs/modules-internals.md index cf05124c9..245daaba5 100644 --- a/docs/modules-internals.md +++ b/docs/modules-internals.md @@ -10,7 +10,7 @@ them, and the interactions between the various moving pieces. - [winrt.ixx Generation](#winrtixx-generation) - [Split Standard Library Includes](#split-standard-library-includes) - [base_macros.h: Why Macros Need Special Handling](#base_macrosh-why-macros-need-special-handling) -- [WINRT_IMPL_SKIP_INCLUDES: Guarded Cross-Namespace Includes](#winrt_impl_skip_includes-guarded-cross-namespace-includes) +- [Per-Namespace Include Guards (WINRT_MODULE_NS_*)](#per-namespace-include-guards-winrt_module_ns) - [WINRT_EXPORT on Extern Handlers](#winrt_export-on-extern-handlers) - [The -module Flag: Component Code Generation](#the--module-flag-component-code-generation) - [The Combined-ixx Approach](#the-combined-ixx-approach) @@ -133,52 +133,66 @@ import winrt; // ... template code using WINRT_IMPL_EMPTY_BASES etc. ``` -## WINRT_IMPL_SKIP_INCLUDES: Guarded Cross-Namespace Includes +## Per-Namespace Include Guards (WINRT_MODULE_NS_*) -**Problem**: Inside generated `.g.h` files, after `import winrt;`, the -component's own projection headers are included. Those headers have -cross-namespace `#include` deps for platform SDK types (e.g., -`Windows.Foundation`) that are already available from the module. Re-including -platform headers works (MSVC handles the re-declarations gracefully), but -skipping them in this narrow scope avoids redundant processing. +**Problem**: After `import winrt;`, consumers textually `#include` component +and reference projection headers. Those headers have cross-namespace `#include` +deps. Platform namespace deps (already in the module) must be skipped to avoid +MSVC redeclaration errors on `inline constexpr` variable template specializations +like `name_v`. But component-to-component cross-namespace deps must NOT be +skipped, or multi-namespace components break. -**Solution**: Generated `.g.h` files set `WINRT_IMPL_SKIP_INCLUDES` locally -(under `#ifdef WINRT_MODULE`) before including the component's own headers. -Generated namespace headers wrap their cross-namespace deps with guards: +**Solution**: cppwinrt generates `winrt/winrt_module_namespaces.h` alongside +`winrt.ixx`. This header defines one macro per namespace in the module: ```cpp -// In generated Windows.Foundation.h: -#ifndef WINRT_IMPL_SKIP_INCLUDES -#include "winrt/base.h" +// Generated winrt/winrt_module_namespaces.h: +#pragma once +#define WINRT_MODULE_NS_Windows_Foundation +#define WINRT_MODULE_NS_Windows_Foundation_Collections +// ... one per namespace in the module +``` + +Generated namespace headers use per-namespace guards for cross-namespace deps: + +```cpp +// In generated TestModuleComponent.h: +#ifndef WINRT_MODULE_NS_Windows_Foundation +#include "winrt/impl/Windows.Foundation.2.h" #endif -// ... (cross-namespace deps guarded) -#include "winrt/impl/Windows.Foundation.2.h" // self-namespace: NOT guarded +#include "winrt/impl/TestModuleComponent.Widgets.2.h" // NOT guarded (not in module) +#include "winrt/impl/TestModuleComponent.2.h" // self-namespace: never guarded ``` -**Critical scoping**: `WINRT_IMPL_SKIP_INCLUDES` is NOT defined project-wide. -It is only defined locally inside `.g.h` files. This ensures that consumer -source files can `#include ` after `import winrt;` -and the cross-namespace deps between component namespaces resolve normally. - -**WINRT_MODULE vs WINRT_IMPL_SKIP_INCLUDES in the version assert**: The version -assert at the top of every namespace header checks `WINRT_BUILD_MODULE`, -`WINRT_MODULE`, or `WINRT_IMPL_SKIP_INCLUDES` to decide whether to skip -`#include "base.h"` and use `base_macros.h` instead. This three-way check -covers: (a) inside winrt.ixx, (b) consumer source files, (c) inside .g.h files. - -**WINRT_BUILD_MODULE in winrt.ixx**: The ixx defines `WINRT_BUILD_MODULE` -and `#undef`s `WINRT_IMPL_SKIP_INCLUDES` in its global module fragment. This -ensures the version assert uses `base_macros.h` (since base.h is already -explicitly included), but cross-namespace deps are included normally so -namespace headers can reference each other's types inside the ixx. - -**Implementation**: `write_root_include_guarded()` in `type_writers.h` wraps the -`#include` with `#ifndef WINRT_IMPL_SKIP_INCLUDES`. Used by: +When `WINRT_MODULE` is defined, the version assert at the top of each namespace +header includes `winrt_module_namespaces.h`, making the `WINRT_MODULE_NS_*` +macros available for the guards. When `WINRT_BUILD_MODULE` is defined (inside +winrt.ixx), `winrt_module_namespaces.h` is NOT included, so all cross-namespace +deps resolve normally — the ixx needs them to build the module. + +**Implementation**: `write_root_include_guarded()` in `type_writers.h` extracts +the namespace from the include path and emits `#ifndef WINRT_MODULE_NS_`. +Used by: - `write_depends_guarded()` for cross-namespace impl includes - `write_parent_depends()` for parent namespace includes -`write_version_assert()` in `code_writers.h` checks `WINRT_BUILD_MODULE || -WINRT_MODULE || WINRT_IMPL_SKIP_INCLUDES` for the base.h skip. +`write_version_assert()` in `code_writers.h`: +```cpp +#if defined(WINRT_BUILD_MODULE) || defined(WINRT_MODULE) +#include "winrt/base_macros.h" +#endif +#if defined(WINRT_MODULE) && !defined(WINRT_BUILD_MODULE) +#include "winrt/winrt_module_namespaces.h" +#endif +#if !defined(WINRT_BUILD_MODULE) && !defined(WINRT_MODULE) +#include "winrt/base.h" +#endif +``` + +**winrt_module_namespaces.h generation**: In `main.cpp`, emitted alongside +`winrt.ixx` when `settings.base` is true and `settings.component` is false. +This prevents component projections (which may also use `-base`) from generating +a stale version that would mask the module builder's file on the include path. The component's own impl headers are always included (via `write_depends()`, unguarded) since they contain the component-specific type definitions. @@ -241,14 +255,15 @@ import winrt; import std; #endif import winrt; -#define WINRT_IMPL_SKIP_INCLUDES #endif #include "winrt/test_component.h" // always emitted ``` -When `WINRT_MODULE` is defined, `WINRT_IMPL_SKIP_INCLUDES` causes the component -headers to skip cross-namespace platform `#include` deps (already in the module). -When not defined, the headers pull in `base.h` transitively as normal. +When `WINRT_MODULE` is defined, the version assert in each included header +pulls in `winrt_module_namespaces.h`, which provides per-namespace +`WINRT_MODULE_NS_*` macros. Platform namespace deps are skipped by these +guards; component cross-namespace deps are included normally. +When `WINRT_MODULE` is not defined, the headers pull in `base.h` transitively. ### Toaster.g.cpp (factory + optional optimized constructors) @@ -387,20 +402,26 @@ User sets CppWinRTModuleBuild=true (or CppWinRTModuleConsume=true) + BuildStlMod ├── NuGet targets define WINRT_IMPORT_STD on all ClCompile items │ ▼ -winrt.ixx compilation: +winrt.ixx compilation (WINRT_BUILD_MODULE defined in global module fragment): module; + #define WINRT_BUILD_MODULE ← platform includes (base_includes) ← std library includes (base_std_includes) export module winrt; #define WINRT_EXPORT export - #include "winrt/base.h" ← base.h (pragma once) + #include "winrt/base.h" ← base.h (explicit include) #include "winrt/Windows.Foundation.h" ← SDK types + specializations ... ← winrt::impl is exported + Also generates: winrt/winrt_module_namespaces.h (per-namespace macros) -Consumer .cpp compilation: +Consumer .cpp compilation (WINRT_MODULE defined by NuGet targets): #include "pch.h" ← PCH (no winrt/ headers) import std; ← std module (optional, needs BuildStlModules) import winrt; ← winrt module (SDK types + exported impl) + #include "winrt/MyComponent.h" ← reference projection header + → version assert includes winrt_module_namespaces.h (via WINRT_MODULE) + → platform deps guarded by WINRT_MODULE_NS_* (skipped) + → component cross-namespace deps NOT guarded (included normally) Component .g.h (WINRT_MODULE defined by NuGet targets): #include "winrt/base_macros.h" ← macros only (WINRT_EXPORT etc.) @@ -408,13 +429,14 @@ Component .g.h (WINRT_MODULE defined by NuGet targets): import std; ← conditional on WINRT_IMPORT_STD #endif import winrt; ← SDK types + exported impl templates - #define WINRT_IMPL_SKIP_INCLUDES ← skip SDK #include deps #include "winrt/MyComponent.h" ← component projection (specializes impl) + → platform deps skipped by WINRT_MODULE_NS_* guards + → cross-namespace component deps included normally module.g.cpp compilation: #include "pch.h" ← PCH preserved #ifdef WINRT_IMPORT_STD / import std; ← conditional - import winrt; ← from -module code gen + import winrt; ← module import void* winrt_make_MyComponent_Toaster() ← factory function bool __stdcall ..._can_unload_now() ← DLL entry points ``` diff --git a/docs/modules.md b/docs/modules.md index 9e18d4477..2e31cb4c9 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -300,7 +300,7 @@ namespace winrt::MyComponent::factory_implementation |-------|---------| | `WINRT_IMPORT_STD` | When defined alongside `__cpp_lib_modules`, causes `base.h` to emit `import std;` instead of individual standard library `#include` directives. Automatically defined by the NuGet targets when `BuildStlModules` is enabled alongside `CppWinRTModuleBuild` or `CppWinRTModuleConsume`. | | `WINRT_LEAN_AND_MEAN` | Suppresses `#include ` and `std::hash`/`std::formatter` specializations from generated headers. Reduces header weight. | -| `WINRT_MODULE` | When defined (project-wide), generated component files (`.g.h`, `.g.cpp`, `module.g.cpp`) use `import winrt;` instead of `#include` directives for platform types. Also causes generated namespace headers to skip `#include "base.h"` in their version assert (using `base_macros.h` instead), since base.h types are available from the module. Automatically defined by the NuGet targets when `CppWinRTModuleBuild` or `CppWinRTModuleConsume` is set. Can also be defined manually for non-NuGet projects. Does NOT suppress cross-namespace `#include` deps between component namespaces — only `WINRT_IMPL_SKIP_INCLUDES` (set locally in `.g.h` files) does that. | +| `WINRT_MODULE` | When defined (project-wide), generated component files (`.g.h`, `.g.cpp`, `module.g.cpp`) use `import winrt;` instead of `#include` directives for platform types. Also causes generated namespace headers to skip `#include "base.h"` in their version assert (using `base_macros.h` instead) and to include `winrt_module_namespaces.h` which provides per-namespace `WINRT_MODULE_NS_*` macros for precise cross-namespace include guarding. Automatically defined by the NuGet targets when `CppWinRTModuleBuild` or `CppWinRTModuleConsume` is set. Can also be defined manually for non-NuGet projects. | ### Internal implementation macros @@ -308,8 +308,8 @@ These are used by the code generator and should not be set directly by users: | Macro | Purpose | |-------|--------| -| `WINRT_BUILD_MODULE` | Defined by cppwinrt in the winrt.ixx global module fragment. Tells namespace headers to use `base_macros.h` instead of `#include "base.h"` (since base.h is already included explicitly in the ixx). Also `#undef`s `WINRT_IMPL_SKIP_INCLUDES` so cross-namespace deps are included normally inside the ixx. | -| `WINRT_IMPL_SKIP_INCLUDES` | When defined, generated namespace headers skip their cross-namespace `#include` dependencies (e.g., `#include "winrt/impl/Windows.Foundation.2.h"`). Set locally inside generated `.g.h` files under `#ifdef WINRT_MODULE` — NOT defined project-wide. This ensures component-to-component cross-namespace deps still work while platform SDK deps (available from the module) are skipped in the narrow .g.h scope. | +| `WINRT_BUILD_MODULE` | Defined by cppwinrt in the winrt.ixx global module fragment. Tells namespace headers to use `base_macros.h` instead of `#include "base.h"` (since base.h is already included explicitly in the ixx). Does NOT include `winrt_module_namespaces.h` — inside the ixx, all cross-namespace deps must resolve normally. | +| `WINRT_MODULE_NS_*` | Per-namespace macros (e.g., `WINRT_MODULE_NS_Windows_Foundation`) defined in the generated `winrt_module_namespaces.h` companion header. Each cross-namespace `#include` dep is guarded by `#ifndef WINRT_MODULE_NS_`, so only namespaces actually in the module are skipped. Component and other non-module namespace deps are always included. | | `WINRT_EXPORT` | Expands to `export` inside `winrt.ixx` (module purview), empty in header mode. Used on namespace declarations so types are properly exported from the module. Also applied to `winrt_to_hresult_handler` and related extern handler variables in `base_extern.h` for correct linkage when mixing modules and non-modules code in the same binary. Defined in `base_macros.h` (as empty) for use in generated component files that operate alongside the module. | | `WINRT_IMPL_EMPTY_BASES` | MSVC `__declspec(empty_bases)` optimization. Defined in both `base.h` and `base_macros.h`. | | `WINRT_IMPL_ABI_DECL` | Combines `WINRT_IMPL_NOVTABLE` and `WINRT_IMPL_PUBLIC` for ABI interface declarations. | @@ -341,7 +341,6 @@ winrt/ ``` module; ← Global module fragment #define WINRT_BUILD_MODULE ← Defined here, controls header behavior -#undef WINRT_IMPL_SKIP_INCLUDES ← Ensure cross-namespace deps work inside ixx ← Platform includes (base_includes) ← Standard library includes (base_std_includes) @@ -381,10 +380,10 @@ auto widget = component.CreateWidget(); // returns MyComponent.Widgets auto name = widget.Name(); // calls method on cross-namespace type ``` -Inside generated `.g.h` files, `WINRT_IMPL_SKIP_INCLUDES` is set locally (under -`#ifdef WINRT_MODULE`) to skip platform SDK deps that are already in the module. -This is scoped to just the `.g.h` — it does not affect other headers included -by consumer source files. +Inside generated `.g.h` files, `import winrt;` makes platform types available. +The component's own projection headers are included normally — cross-namespace +deps between component namespaces resolve via unguarded includes, while platform +namespace deps are skipped by per-namespace `WINRT_MODULE_NS_*` guards. ### Generated file differences when `WINRT_MODULE` is defined @@ -393,7 +392,7 @@ always emits both paths — the preprocessor selects at compile time: | File | Without `WINRT_MODULE` | With `WINRT_MODULE` | |------|------------------------|---------------------| -| `Toaster.g.h` | `#include "winrt/test_component.h"` (pulls in `base.h` transitively) | `#include "winrt/base_macros.h"` + `import winrt;` + `WINRT_IMPL_SKIP_INCLUDES` + component headers | +| `Toaster.g.h` | `#include "winrt/test_component.h"` (pulls in `base.h` transitively) | `#include "winrt/base_macros.h"` + `import winrt;` + component headers (platform deps skipped by `WINRT_MODULE_NS_*` guards) | | `Toaster.g.cpp` | `winrt_make_*` + constructor/static overrides | `winrt_make_*` only (constructors guarded by `#ifndef WINRT_MODULE`) | | `module.g.cpp` | `#include "winrt/base.h"` | `import winrt;` (`import std;` conditional on `WINRT_IMPORT_STD`) | @@ -472,6 +471,6 @@ Runs when `CppWinRTModuleConsume=true`. After `ResolveProjectReferences`: from the module so that component projection headers can specialize `impl` templates (`category`, `abi`, `guid_v`, etc.) after `import winrt;`. This means component headers can be `#include`d separately without being folded - into `winrt.ixx`. Inside generated `.g.h` files, `WINRT_IMPL_SKIP_INCLUDES` - is set locally to skip platform `#include` dependencies that come from the - module. Cross-namespace deps between component namespaces are not affected. + into `winrt.ixx`. Per-namespace `WINRT_MODULE_NS_*` guards ensure that + platform namespace deps (already in the module) are skipped, while component + and other non-module namespace deps are included normally. diff --git a/test/test_cpp20_module/component.cpp b/test/test_cpp20_module/component.cpp index 78524cabf..3f4b5f4c3 100644 --- a/test/test_cpp20_module/component.cpp +++ b/test/test_cpp20_module/component.cpp @@ -2,16 +2,15 @@ // The component projection header is included AFTER importing winrt, // which tests the interaction between module-imported base types and // #include-based component projection headers. +// WINRT_MODULE (defined project-wide) causes namespace headers to use +// per-namespace WINRT_MODULE_NS_* guards so platform deps are skipped +// while component deps are included normally. #include "catch.hpp" #include import std; import winrt; -// When consuming component headers alongside 'import winrt;', define -// WINRT_IMPL_SKIP_INCLUDES to skip #include directives for base.h and SDK -// headers that are already available from the module. -#define WINRT_IMPL_SKIP_INCLUDES #include "winrt/test_component_module.h" using namespace winrt; diff --git a/test/test_cpp20_module/test_cpp20_module.vcxproj b/test/test_cpp20_module/test_cpp20_module.vcxproj index c9b1fed5d..4324c972c 100644 --- a/test/test_cpp20_module/test_cpp20_module.vcxproj +++ b/test/test_cpp20_module/test_cpp20_module.vcxproj @@ -99,7 +99,7 @@ $(SolutionDir)test\test_cpp20_module_winrt\Generated Files;..\;%(AdditionalIncludeDirectories) - NOMINMAX;WINRT_LEAN_AND_MEAN;WINRT_IMPORT_STD;WINRT_TEST_MODULES;%(PreprocessorDefinitions) + NOMINMAX;WINRT_LEAN_AND_MEAN;WINRT_IMPORT_STD;WINRT_MODULE;WINRT_TEST_MODULES;%(PreprocessorDefinitions) Use true $(IntDir)..\test_cpp20_module_winrt\winrt.ixx.ifc;%(AdditionalModuleDependencies) From a6eb07eacedcc583d64c70c7a34800c20874fc15 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Mon, 6 Apr 2026 12:58:35 -0700 Subject: [PATCH 32/34] Define missinf WINRT_MODULE for test_cpp23_module project --- test/test_cpp23_module/test_cpp23_module.vcxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_cpp23_module/test_cpp23_module.vcxproj b/test/test_cpp23_module/test_cpp23_module.vcxproj index b146edc59..b098ebbac 100644 --- a/test/test_cpp23_module/test_cpp23_module.vcxproj +++ b/test/test_cpp23_module/test_cpp23_module.vcxproj @@ -99,7 +99,7 @@ $(SolutionDir)test\test_cpp23_module_winrt\Generated Files;..\;%(AdditionalIncludeDirectories) - NOMINMAX;WINRT_LEAN_AND_MEAN;WINRT_IMPORT_STD;WINRT_TEST_MODULES;%(PreprocessorDefinitions) + NOMINMAX;WINRT_LEAN_AND_MEAN;WINRT_IMPORT_STD;WINRT_MODULE;WINRT_TEST_MODULES;%(PreprocessorDefinitions) Use true stdcpplatest From b0f66656f1dabfd247c7b98ae30d7bf2d291bf70 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Mon, 6 Apr 2026 15:15:15 -0700 Subject: [PATCH 33/34] Add module namespace filtering --- cppwinrt/main.cpp | 35 +++++++++++++++++++ cppwinrt/settings.h | 2 ++ docs/modules-internals.md | 6 ++++ docs/modules.md | 2 ++ nuget/CppWinrtRules.Project.xml | 5 +++ nuget/Microsoft.Windows.CppWinRT.targets | 1 + nuget/readme.md | 3 +- .../TestModuleBuilder.vcxproj | 1 + .../TestModuleSingleProject.vcxproj | 1 + .../test_cpp20_module_winrt.vcxproj | 4 +-- .../test_cpp23_module_winrt.vcxproj | 2 +- 11 files changed, 57 insertions(+), 5 deletions(-) diff --git a/cppwinrt/main.cpp b/cppwinrt/main.cpp index 5d0352268..f9939f0ff 100644 --- a/cppwinrt/main.cpp +++ b/cppwinrt/main.cpp @@ -39,6 +39,7 @@ namespace cppwinrt { "fastabi", 0, 0 }, // Enable support for the Fast ABI { "ignore_velocity", 0, 0 }, // Ignore feature staging metadata and always include implementations { "synchronous", 0, 0 }, // Instructs cppwinrt to run on a single thread to avoid file system issues in batch builds + { "module_filter", 0, option::no_max, "", "Filter which namespaces are included in winrt.ixx (headers are still generated for all)" }, }; static void print_usage(writer& w) @@ -115,6 +116,11 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder settings.exclude.insert(exclude); } + for (auto && ns : args.values("module_filter")) + { + settings.module_filter_include.insert(ns); + } + if (settings.license) { std::string license_arg = args.value("license"); @@ -199,6 +205,14 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder static void build_filters(cache const& c) { + // Build ixx_filter from -module-filter args. + // This allows filtering which namespaces go into winrt.ixx + // without affecting which headers are generated. + if (!settings.module_filter_include.empty()) + { + settings.ixx_filter = { settings.module_filter_include, {} }; + } + if (settings.reference.empty()) { return; @@ -357,6 +371,23 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder continue; } + // When -include/-exclude is specified, further filter which + // namespaces go into the ixx and winrt_module_namespaces.h. + // Headers are still generated for all namespaces (so consumers + // can #include them), but only filtered namespaces are in the module. + if (!settings.ixx_filter.empty() && !settings.ixx_filter.includes(members)) + { + // Still generate the headers, just don't add to ixx + group.add([&, &ns = ns, &members = members] + { + write_namespace_0_h(ns, members); + write_namespace_1_h(ns, members); + write_namespace_2_h(ns, members); + write_namespace_h(c, ns, members); + }); + continue; + } + ixx.write("#include \"winrt/%.h\"\n", ns); group.add([&, &ns = ns, &members = members] @@ -392,6 +423,10 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder { continue; } + if (!settings.ixx_filter.empty() && !settings.ixx_filter.includes(members)) + { + continue; + } std::string ns_macro{ ns }; std::replace(ns_macro.begin(), ns_macro.end(), '.', '_'); module_ns.write("#define WINRT_MODULE_NS_%\n", ns_macro); diff --git a/cppwinrt/settings.h b/cppwinrt/settings.h index e07df4ea2..9ae251a4c 100644 --- a/cppwinrt/settings.h +++ b/cppwinrt/settings.h @@ -28,6 +28,8 @@ namespace cppwinrt winmd::reader::filter projection_filter; winmd::reader::filter component_filter; + winmd::reader::filter ixx_filter; // Filters which namespaces go into winrt.ixx (from -module-filter) + std::set module_filter_include; // Raw -module-filter values bool fastabi{}; std::map fastabi_cache; diff --git a/docs/modules-internals.md b/docs/modules-internals.md index 245daaba5..ddf103fbd 100644 --- a/docs/modules-internals.md +++ b/docs/modules-internals.md @@ -194,6 +194,12 @@ Used by: This prevents component projections (which may also use `-base`) from generating a stale version that would mask the module builder's file on the include path. +**Module namespace filtering** (`-module_filter`): When specified, only matching +namespaces are included in `winrt.ixx` and `winrt_module_namespaces.h`. All +namespace headers are still generated for textual inclusion. This is implemented +via `settings.ixx_filter` (built from `-module_filter` args in `build_filters`). +The NuGet property `CppWinRTModuleFilter` passes this to cppwinrt.exe. + The component's own impl headers are always included (via `write_depends()`, unguarded) since they contain the component-specific type definitions. diff --git a/docs/modules.md b/docs/modules.md index 2e31cb4c9..eab29c4f2 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -184,6 +184,7 @@ The NuGet targets automatically: |----------|------|---------|-------------| | `CppWinRTModuleBuild` | bool | `false` | Generate the platform projection and compile `winrt.ixx` as a C++20 module interface unit. The compiled module IFC and OBJ are exported for consumption by projects that set `CppWinRTModuleConsume`. Defines `WINRT_MODULE` for generated component code. | | `CppWinRTModuleConsume` | bool | `false` | Consume a pre-built winrt module from a `ProjectReference` that sets `CppWinRTModuleBuild`. Automatically resolves the IFC (for `AdditionalModuleDependencies`), OBJ (for linker `AdditionalDependencies`), and the builder's `GeneratedFilesDir` (for `AdditionalIncludeDirectories`). Skips regenerating the platform projection by default. Defines `WINRT_MODULE` for generated component code. | +| `CppWinRTModuleFilter` | string | (empty) | Space-separated namespace prefixes to include in the winrt module. When set, only matching namespaces are compiled into `winrt.ixx` and listed in `winrt_module_namespaces.h`. All namespace headers are still generated for textual inclusion. Example: `Windows.Foundation Windows.Storage` would include only those namespace families in the module. Passed as `-module_filter` to cppwinrt.exe. | | `BuildStlModules` | ClCompile metadata | `false` | Enables building `std.ixx`/`std.compat.ixx` so `import std;` works. This is orthogonal to `import winrt;` — you can use `import winrt;` without `import std;`. Set via **C/C++ > General > Build ISO C++23 Standard Library Modules** in the IDE, or `true` in `` metadata. On v143, the underlying `StdModulesSupported` infrastructure requires `/std:c++latest`; on v145, `/std:c++20` suffices. | When `CppWinRTModuleBuild` or `CppWinRTModuleConsume` is true, the NuGet targets also automatically: @@ -210,6 +211,7 @@ When `CppWinRTModuleBuild` or `CppWinRTModuleConsume` is true, the NuGet targets | `-fastabi` | Enable Fast ABI support | | `-verbose` | Show detailed progress information | | `-overwrite` | Overwrite generated component files | +| `-module_filter ` | Filter which namespaces are included in `winrt.ixx`. Can be specified multiple times. All namespace headers are still generated; only the module contents and `winrt_module_namespaces.h` are filtered. Example: `-module_filter Windows.Foundation` includes `Windows.Foundation` and all sub-namespaces. | ## Source File Patterns diff --git a/nuget/CppWinrtRules.Project.xml b/nuget/CppWinrtRules.Project.xml index 66469c270..421c9cc64 100644 --- a/nuget/CppWinrtRules.Project.xml +++ b/nuget/CppWinrtRules.Project.xml @@ -96,4 +96,9 @@ Description="Consumes a pre-built winrt module from a project reference that sets CppWinRTModuleBuild. The module IFC, OBJ, and include paths are resolved automatically. Consumer source files should use 'import winrt;' instead of #include directives for C++/WinRT headers." Category="General" /> + + diff --git a/nuget/Microsoft.Windows.CppWinRT.targets b/nuget/Microsoft.Windows.CppWinRT.targets index 879b5bb04..9f10cc2fe 100644 --- a/nuget/Microsoft.Windows.CppWinRT.targets +++ b/nuget/Microsoft.Windows.CppWinRT.targets @@ -654,6 +654,7 @@ $(XamlMetaDataProviderPch) <_CppwinrtParameters>$(CppWinRTCommandVerbosity) $(CppWinRTParameters) <_CppwinrtParameters>$(_CppwinrtParameters) @(_CppwinrtInputs->'-in "%(WinMDPath)"', ' ') + <_CppwinrtParameters Condition="'$(CppWinRTModuleFilter)' != ''">$(_CppwinrtParameters) -module_filter $(CppWinRTModuleFilter) <_CppwinrtParameters>$(_CppwinrtParameters) -out "$(GeneratedFilesDir)." diff --git a/nuget/readme.md b/nuget/readme.md index 259136523..baed3d673 100644 --- a/nuget/readme.md +++ b/nuget/readme.md @@ -71,8 +71,7 @@ C++/WinRT behavior can be customized with these project properties: | CppWinRTGenerateWindowsMetadata | true \| *false | Indicates whether this project produces Windows Metadata | | CppWinRTEnableDefaultPrivateFalse | true \| *false | Indicates whether this project uses C++/WinRT optimized default for copying binaries to the output directory | | CppWinRTModuleBuild | true \| *false | Generates the platform projection and compiles winrt.ixx as a C++20 module. See [C++20 Modules](#c20-modules) | -| CppWinRTModuleConsume | true \| *false | Consumes a pre-built winrt module from a ProjectReference. See [C++20 Modules](#c20-modules) | -\*Default value +| CppWinRTModuleConsume | true \| *false | Consumes a pre-built winrt module from a ProjectReference. See [C++20 Modules](#c20-modules) || CppWinRTModuleFilter | *empty | Namespace prefixes to include in the module (e.g. `Windows.Foundation`). When empty, all platform namespaces are included. |\*Default value To customize common C++/WinRT project properties: * right-click the project node diff --git a/test/nuget/TestModuleBuilder/TestModuleBuilder.vcxproj b/test/nuget/TestModuleBuilder/TestModuleBuilder.vcxproj index 3b5f351c0..2aa196293 100644 --- a/test/nuget/TestModuleBuilder/TestModuleBuilder.vcxproj +++ b/test/nuget/TestModuleBuilder/TestModuleBuilder.vcxproj @@ -5,6 +5,7 @@ true true true + Windows.Foundation true 15.0 {A1B2C3D4-1111-2222-3333-444455556666} diff --git a/test/nuget/TestModuleSingleProject/TestModuleSingleProject.vcxproj b/test/nuget/TestModuleSingleProject/TestModuleSingleProject.vcxproj index 3c456f948..53f2b9afe 100644 --- a/test/nuget/TestModuleSingleProject/TestModuleSingleProject.vcxproj +++ b/test/nuget/TestModuleSingleProject/TestModuleSingleProject.vcxproj @@ -5,6 +5,7 @@ true true true + Windows.Foundation true 15.0 {A1B2C3D4-FFFF-0000-1111-222233334444} diff --git a/test/test_cpp20_module_winrt/test_cpp20_module_winrt.vcxproj b/test/test_cpp20_module_winrt/test_cpp20_module_winrt.vcxproj index e1b6170ce..31160eda5 100644 --- a/test/test_cpp20_module_winrt/test_cpp20_module_winrt.vcxproj +++ b/test/test_cpp20_module_winrt/test_cpp20_module_winrt.vcxproj @@ -106,8 +106,8 @@ true - $(CppWinRTDir)cppwinrt -in sdk -out "$(ProjectDir)Generated Files" -verbose - Generating C++/WinRT SDK projection + $(CppWinRTDir)cppwinrt -in sdk -out "$(ProjectDir)Generated Files" -module_filter Windows.Foundation -verbose + Generating C++/WinRT SDK projection (filtered: Windows.Foundation) diff --git a/test/test_cpp23_module_winrt/test_cpp23_module_winrt.vcxproj b/test/test_cpp23_module_winrt/test_cpp23_module_winrt.vcxproj index ef2e94a58..de6e7f22c 100644 --- a/test/test_cpp23_module_winrt/test_cpp23_module_winrt.vcxproj +++ b/test/test_cpp23_module_winrt/test_cpp23_module_winrt.vcxproj @@ -104,7 +104,7 @@ WINRT_IMPORT_STD;%(PreprocessorDefinitions) - $(CppWinRTDir)cppwinrt -in sdk -out "$(ProjectDir)Generated Files" -verbose + $(CppWinRTDir)cppwinrt -in sdk -out "$(ProjectDir)Generated Files" -module_filter Windows.Foundation -verbose Generating C++/WinRT SDK projection From 272113b97921eccb0673004f7093aa614d269297 Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Mon, 6 Apr 2026 18:17:58 -0700 Subject: [PATCH 34/34] Add module_filter to modules.instructions.md --- .github/instructions/modules.instructions.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/instructions/modules.instructions.md b/.github/instructions/modules.instructions.md index c4675f0a2..4041a2618 100644 --- a/.github/instructions/modules.instructions.md +++ b/.github/instructions/modules.instructions.md @@ -98,6 +98,18 @@ The version assert at the top of each namespace header: All module test projects use v143 toolset. `import std;` requires `BuildStlModules=true` and either v145 toolset or `/std:c++latest` on v143. +## Module Namespace Filtering (-module_filter) + +The `-module_filter` cppwinrt.exe arg controls which namespaces go into +`winrt.ixx` and `winrt_module_namespaces.h`. All namespace headers are still +generated for textual inclusion — only the module contents are filtered. + +- NuGet property: `CppWinRTModuleFilter` (space-separated prefixes) +- cppwinrt.exe arg: `-module_filter ` (can be repeated) +- Implementation: `settings.ixx_filter` in `main.cpp`/`settings.h` +- The repo test builders use `-module_filter Windows.Foundation` to speed up + the inner loop (5 namespaces instead of 339). + ## NuGet Module Integration (test/nuget/) The NuGet targets split module support into two properties: