diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d660400a0..97ac3d41e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: run: | $target_configuration = "${{ matrix.config }}" $target_platform = "${{ matrix.arch }}" - & "_build\$target_platform\$target_configuration\cppwinrt.exe" -in local -out _build\$target_platform\$target_configuration -verbose + & "_build\$target_platform\$target_configuration\cppwinrt.exe" -in local -out _build\$target_platform\$target_configuration -verbose -flatten_classes test-msvc-cppwinrt-test: name: '${{ matrix.compiler }}: Test [${{ matrix.test_exe }}] (${{ matrix.arch }}, ${{ matrix.config }}, ${{ matrix.toolchain.platform_toolset }})' @@ -377,7 +377,7 @@ jobs: run: | $target_configuration = "${{ matrix.config }}" $target_platform = "${{ matrix.arch }}" - & "_build\$target_platform\$target_configuration\cppwinrt.exe" -in local -out _build\$target_platform\$target_configuration -verbose + & "_build\$target_platform\$target_configuration\cppwinrt.exe" -in local -out _build\$target_platform\$target_configuration -verbose -flatten_classes - name: Run nuget test run: | diff --git a/.gitignore b/.gitignore index 9493fdad5..1bb24d891 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ test*.xml test*_results.txt test_failures.txt build +_build packages Debug Release diff --git a/.pipelines/jobs/OneBranchTest.yml b/.pipelines/jobs/OneBranchTest.yml index 2fb3a4bd1..98a4831e1 100644 --- a/.pipelines/jobs/OneBranchTest.yml +++ b/.pipelines/jobs/OneBranchTest.yml @@ -107,7 +107,7 @@ jobs: - task: CmdLine@2 displayName: Run cppwinrt to build projection inputs: - script: $(BuildPath)\cppwinrt.exe -in local -out $(BuildPath) -verbose + script: $(BuildPath)\cppwinrt.exe -in local -out $(BuildPath) -verbose -flatten_classes - task: VSBuild@1 displayName: Build test diff --git a/build-dir/winmd-prefix/src/winmd b/build-dir/winmd-prefix/src/winmd new file mode 160000 index 000000000..0f1eae3bf --- /dev/null +++ b/build-dir/winmd-prefix/src/winmd @@ -0,0 +1 @@ +Subproject commit 0f1eae3bfa63fa2ba3c2912cbfe72a01db94cc5a diff --git a/build_nuget.cmd b/build_nuget.cmd index 99e193311..5fc4a0809 100644 --- a/build_nuget.cmd +++ b/build_nuget.cmd @@ -3,10 +3,10 @@ rem @echo off set target_version=%1 if "%target_version%"=="" set target_version=999.999.999.999 -call msbuild /m /p:Configuration=Release,Platform=x86,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:fast_fwd -call msbuild /m /p:Configuration=Release,Platform=x64,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:fast_fwd -call msbuild /m /p:Configuration=Release,Platform=arm64,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:fast_fwd +call msbuild /m /p:Configuration=Release,Platform=x86,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:fast_fwd;cached_thunks +call msbuild /m /p:Configuration=Release,Platform=x64,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:fast_fwd;cached_thunks +call msbuild /m /p:Configuration=Release,Platform=arm64,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:fast_fwd;cached_thunks call msbuild /m /p:Configuration=Release,Platform=x86,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:cppwinrt -nuget pack nuget\Microsoft.Windows.CppWinRT.nuspec -Properties target_version=%target_version%;cppwinrt_exe=%cd%\_build\x86\Release\cppwinrt.exe;cppwinrt_fast_fwd_x86=%cd%\_build\x86\Release\cppwinrt_fast_forwarder.lib;cppwinrt_fast_fwd_x64=%cd%\_build\x64\Release\cppwinrt_fast_forwarder.lib;cppwinrt_fast_fwd_arm64=%cd%\_build\arm64\Release\cppwinrt_fast_forwarder.lib +nuget pack nuget\Microsoft.Windows.CppWinRT.nuspec -Properties target_version=%target_version%;cppwinrt_exe=%cd%\_build\x86\Release\cppwinrt.exe;cppwinrt_fast_fwd_x86=%cd%\_build\x86\Release\cppwinrt_fast_forwarder.lib;cppwinrt_fast_fwd_x64=%cd%\_build\x64\Release\cppwinrt_fast_forwarder.lib;cppwinrt_fast_fwd_arm64=%cd%\_build\arm64\Release\cppwinrt_fast_forwarder.lib;cppwinrt_cached_thunks_x86=%cd%\_build\x86\Release\cppwinrt_cached_thunks.lib;cppwinrt_cached_thunks_x64=%cd%\_build\x64\Release\cppwinrt_cached_thunks.lib;cppwinrt_cached_thunks_arm64=%cd%\_build\arm64\Release\cppwinrt_cached_thunks.lib diff --git a/build_projection.cmd b/build_projection.cmd index 140e6c7f8..3a9fe21df 100644 --- a/build_projection.cmd +++ b/build_projection.cmd @@ -34,5 +34,5 @@ if not exist "%cppwinrt_exe%" ( ) echo Building projection into %target_platform% %target_configuration% -%cppwinrt_exe% -in local -out %~p0\_build\%target_platform%\%target_configuration% -verbose +%cppwinrt_exe% -in local -out %~p0\_build\%target_platform%\%target_configuration% -verbose -flatten_classes echo. diff --git a/build_test_all.cmd b/build_test_all.cmd index c9beb852c..a6e487956 100644 --- a/build_test_all.cmd +++ b/build_test_all.cmd @@ -12,30 +12,36 @@ if "%target_version%"=="" set target_version=999.999.999.999 if not exist ".\.nuget" mkdir ".\.nuget" if not exist ".\.nuget\nuget.exe" powershell -Command "$ProgressPreference = 'SilentlyContinue' ; Invoke-WebRequest https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile .\.nuget\nuget.exe" -call .nuget\nuget.exe restore cppwinrt.sln" +call .nuget\nuget.exe restore cppwinrt.sln call .nuget\nuget.exe restore natvis\cppwinrtvisualizer.sln call .nuget\nuget.exe restore test\nuget\NugetTest.sln -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:fast_fwd +call msbuild %additional_msbuild_args% /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:fast_fwd;cached_thunks +if errorlevel 1 exit /b 1 -call msbuild /p:Configuration=%target_configuration%,Platform=%target_platform%,Deployment=Component;CppWinRTBuildVersion=%target_version% natvis\cppwinrtvisualizer.sln -call msbuild /p:Configuration=%target_configuration%,Platform=%target_platform%,Deployment=Standalone;CppWinRTBuildVersion=%target_version% natvis\cppwinrtvisualizer.sln +call msbuild %additional_msbuild_args% /p:Configuration=%target_configuration%,Platform=%target_platform%,Deployment=Component;CppWinRTBuildVersion=%target_version% natvis\cppwinrtvisualizer.sln +if errorlevel 1 exit /b 1 +call msbuild %additional_msbuild_args% /p:Configuration=%target_configuration%,Platform=%target_platform%,Deployment=Standalone;CppWinRTBuildVersion=%target_version% natvis\cppwinrtvisualizer.sln +if errorlevel 1 exit /b 1 if "%target_platform%"=="arm64" goto :eof -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:cppwinrt +set build_targets=cppwinrt +set build_targets=%build_targets%;test\test +set build_targets=%build_targets%;test\test_nocoro +set build_targets=%build_targets%;test\test_cpp20 +set build_targets=%build_targets%;test\test_cpp20_no_sourcelocation +set build_targets=%build_targets%;test\test_fast +set build_targets=%build_targets%;test\test_slow +set build_targets=%build_targets%;test\test_module_lock_custom +set build_targets=%build_targets%;test\test_module_lock_none +set build_targets=%build_targets%;test\old_tests\test_old -call msbuild /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% test\nuget\NugetTest.sln +call msbuild %additional_msbuild_args% /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln "/t:%build_targets%" +if errorlevel 1 exit /b 1 -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_nocoro -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_cpp20 -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_cpp20_no_sourcelocation -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_fast -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_slow -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_module_lock_custom -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_module_lock_none -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_module_lock_none -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\old_tests\test_old +call msbuild %additional_msbuild_args% /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% test\nuget\NugetTest.sln +if errorlevel 1 exit /b 1 call run_tests.cmd %target_platform% %target_configuration% +if errorlevel 1 exit /b 1 diff --git a/cached_thunks/cached_thunks.vcxproj b/cached_thunks/cached_thunks.vcxproj new file mode 100644 index 000000000..71aac1a18 --- /dev/null +++ b/cached_thunks/cached_thunks.vcxproj @@ -0,0 +1,112 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + 16.0 + {B3F2B53D-5E38-4B75-9C07-E2B1A5C2E0D7} + Win32Proj + cached_thunks + + + + StaticLibrary + true + Unicode + + + StaticLibrary + false + true + Unicode + + + + + + + + + + + + false + + + true + + + + $(OutDir)$(TargetName)$(TargetExt) + + + + true + false + !$(Platform_Arm) + cppwinrt_cached_thunks + + + + + + + + + + + + + + + Document + + + + + Document + true + + + + + false + Document + + + false + Document + + + + + + + + + + diff --git a/cppwinrt.sln b/cppwinrt.sln index 3bcfb33bc..9a4f1e542 100644 --- a/cppwinrt.sln +++ b/cppwinrt.sln @@ -87,6 +87,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_fast_fwd", "test\test_ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "fast_fwd", "fast_fwd\fast_fwd.vcxproj", "{A63B3AD1-AB7B-461E-9FFF-2447F5BCD459}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cached_thunks", "cached_thunks\cached_thunks.vcxproj", "{B3F2B53D-5E38-4B75-9C07-E2B1A5C2E0D7}" +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "scratch", "scratch\scratch.vcxproj", "{E893622C-47DE-4F83-B422-0A26711590A4}" ProjectSection(ProjectDependencies) = postProject {D613FB39-5035-4043-91E2-BAB323908AF4} = {D613FB39-5035-4043-91E2-BAB323908AF4} @@ -339,6 +341,18 @@ Global {A63B3AD1-AB7B-461E-9FFF-2447F5BCD459}.Release|x64.Build.0 = Release|x64 {A63B3AD1-AB7B-461E-9FFF-2447F5BCD459}.Release|x86.ActiveCfg = Release|Win32 {A63B3AD1-AB7B-461E-9FFF-2447F5BCD459}.Release|x86.Build.0 = Release|Win32 + {B3F2B53D-5E38-4B75-9C07-E2B1A5C2E0D7}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {B3F2B53D-5E38-4B75-9C07-E2B1A5C2E0D7}.Debug|ARM64.Build.0 = Debug|ARM64 + {B3F2B53D-5E38-4B75-9C07-E2B1A5C2E0D7}.Debug|x64.ActiveCfg = Debug|x64 + {B3F2B53D-5E38-4B75-9C07-E2B1A5C2E0D7}.Debug|x64.Build.0 = Debug|x64 + {B3F2B53D-5E38-4B75-9C07-E2B1A5C2E0D7}.Debug|x86.ActiveCfg = Debug|Win32 + {B3F2B53D-5E38-4B75-9C07-E2B1A5C2E0D7}.Debug|x86.Build.0 = Debug|Win32 + {B3F2B53D-5E38-4B75-9C07-E2B1A5C2E0D7}.Release|ARM64.ActiveCfg = Release|ARM64 + {B3F2B53D-5E38-4B75-9C07-E2B1A5C2E0D7}.Release|ARM64.Build.0 = Release|ARM64 + {B3F2B53D-5E38-4B75-9C07-E2B1A5C2E0D7}.Release|x64.ActiveCfg = Release|x64 + {B3F2B53D-5E38-4B75-9C07-E2B1A5C2E0D7}.Release|x64.Build.0 = Release|x64 + {B3F2B53D-5E38-4B75-9C07-E2B1A5C2E0D7}.Release|x86.ActiveCfg = Release|Win32 + {B3F2B53D-5E38-4B75-9C07-E2B1A5C2E0D7}.Release|x86.Build.0 = Release|Win32 {E893622C-47DE-4F83-B422-0A26711590A4}.Debug|ARM64.ActiveCfg = Debug|ARM64 {E893622C-47DE-4F83-B422-0A26711590A4}.Debug|ARM64.Build.0 = Debug|ARM64 {E893622C-47DE-4F83-B422-0A26711590A4}.Debug|x64.ActiveCfg = Debug|x64 diff --git a/cppwinrt/code_writers.h b/cppwinrt/code_writers.h index fc5081bef..d7015af80 100644 --- a/cppwinrt/code_writers.h +++ b/cppwinrt/code_writers.h @@ -1381,7 +1381,7 @@ namespace cppwinrt if constexpr (std::is_base_of_v) { V result{ nullptr }; - impl::check_hresult_allow_bounds(WINRT_IMPL_SHIM(Windows::Foundation::Collections::IMapView)->Lookup(get_abi(key), put_abi(result))); + impl::check_hresult_allow_bounds(impl::consume_general_nothrow>(static_cast(this), &abi_t>::Lookup, get_abi(key), put_abi(result))); return result; } else @@ -1389,7 +1389,7 @@ namespace cppwinrt std::optional result; V value{ empty_value() }; - if (0 == impl::check_hresult_allow_bounds(WINRT_IMPL_SHIM(Windows::Foundation::Collections::IMapView)->Lookup(get_abi(key), put_abi(value)))) + if (0 == impl::check_hresult_allow_bounds(impl::consume_general_nothrow>(static_cast(this), &abi_t>::Lookup, get_abi(key), put_abi(value)))) { result = std::move(value); } @@ -1407,7 +1407,7 @@ namespace cppwinrt if constexpr (std::is_base_of_v) { V result{ nullptr }; - impl::check_hresult_allow_bounds(WINRT_IMPL_SHIM(Windows::Foundation::Collections::IMap)->Lookup(get_abi(key), put_abi(result))); + impl::check_hresult_allow_bounds(impl::consume_general_nothrow>(static_cast(this), &abi_t>::Lookup, get_abi(key), put_abi(result))); return result; } else @@ -1415,7 +1415,7 @@ namespace cppwinrt std::optional result; V value{ empty_value() }; - if (0 == impl::check_hresult_allow_bounds(WINRT_IMPL_SHIM(Windows::Foundation::Collections::IMap)->Lookup(get_abi(key), put_abi(value)))) + if (0 == impl::check_hresult_allow_bounds(impl::consume_general_nothrow>(static_cast(this), &abi_t>::Lookup, get_abi(key), put_abi(value)))) { result = std::move(value); } @@ -1426,7 +1426,7 @@ namespace cppwinrt auto TryRemove(param_type const& key) const { - return 0 == impl::check_hresult_allow_bounds(WINRT_IMPL_SHIM(Windows::Foundation::Collections::IMap)->Remove(get_abi(key))); + return 0 == impl::check_hresult_allow_bounds(impl::consume_general_nothrow>(static_cast(this), &abi_t>::Remove, get_abi(key))); } )"); } @@ -1765,7 +1765,13 @@ namespace cppwinrt if (param.Flags().In()) { - if (category != param_category::fundamental_type) + if (category == param_category::object_type && is_object_class(param_signature->Type())) + { + w.write("impl::produce_borrowed_ref<%>(%)", + param_type, + param_name); + } + else if (category != param_category::fundamental_type) { w.write("*reinterpret_cast<% const*>(&%)", param_type, @@ -3334,6 +3340,142 @@ struct WINRT_IMPL_EMPTY_BASES produce_dispatch_to_overridable bind_each(factories, type)); } + static void write_thunked_class_base(writer& w, TypeDef const& type, coded_index const& default_interface) + { + w.write("impl::thunked_runtimeclass<%", default_interface); + + for (auto&& [interface_name, info] : get_interfaces(w, type)) + { + if (!info.is_default && !info.is_protected && !info.overridable) + { + w.write(", %", interface_name); + } + } + + w.write('>'); + } + + static void write_thunked_class_requires(writer& w, TypeDef const& type) + { + bool first = true; + + for (auto&& [interface_name, info] : get_interfaces(w, type)) + { + if (!info.is_protected && !info.overridable) + { + if (first) + { + first = false; + w.write(",\n impl::require<%", type.TypeName()); + } + + w.write(", %", interface_name); + } + } + + if (!first) + { + w.write('>'); + } + } + + static void write_thunked_class_usings(writer& w, TypeDef const& type) + { + auto type_name = type.TypeName(); + std::map> method_usage; + + for (auto&& [interface_name, info] : get_interfaces(w, type)) + { + if (!info.is_protected && !info.overridable) + { + for (auto&& method : info.type.MethodList()) + { + method_usage[get_name(method)].insert(interface_name); + } + } + } + + for (auto&& [method_name, interfaces] : method_usage) + { + if (interfaces.size() <= 1) + { + continue; + } + + for (auto&& interface_name : interfaces) + { + w.write(" using impl::consume_t<%, %>::%;\n", + type_name, + interface_name, + method_name); + } + } + } + + static bool has_secondary_interfaces(writer& w, TypeDef const& type) + { + for (auto&& [interface_name, info] : get_interfaces(w, type)) + { + if (!info.is_default && !info.is_protected && !info.overridable) + { + return true; + } + } + return false; + } + + static bool has_async_default_interface(coded_index const& default_interface) + { + std::string_view ns; + std::string_view n; + + if (default_interface.type() == TypeDefOrRef::TypeSpec) + { + auto sig = default_interface.TypeSpec().Signature().GenericTypeInst(); + auto const& [type_namespace, type_name_val] = get_type_namespace_and_name(sig.GenericType()); + ns = type_namespace; + n = type_name_val; + } + else + { + auto tn = type_name(default_interface); + ns = tn.name_space; + n = tn.name; + } + + if (ns != "Windows.Foundation") + { + return false; + } + return n == "IAsyncAction" || + n == "IAsyncOperation`1" || + n == "IAsyncActionWithProgress`1" || + n == "IAsyncOperationWithProgress`2"; + } + + static void write_thunked_class(writer& w, TypeDef const& type, coded_index const& default_interface) + { + auto type_name = type.TypeName(); + auto factories = get_factories(w, type); + + auto format = R"( struct WINRT_IMPL_EMPTY_BASES % : %% + { + %(std::nullptr_t) noexcept : thunked_runtimeclass(nullptr) {} + %(void* ptr, take_ownership_from_abi_t) noexcept : thunked_runtimeclass(ptr, take_ownership_from_abi) {} +%%% }; +)"; + + w.write(format, + type_name, + bind(type, default_interface), + bind(type), + type_name, + type_name, + bind(type, factories), + bind(type), + bind_each(factories, type)); + } + static void write_fast_class(writer& w, TypeDef const& type, coded_index const& base_type) { auto type_name = type.TypeName(); @@ -3383,6 +3525,10 @@ struct WINRT_IMPL_EMPTY_BASES produce_dispatch_to_overridable { write_fast_class(w, type, default_interface); } + else if (settings.flatten_classes && get_bases(type).empty() && has_secondary_interfaces(w, type) && !has_async_default_interface(default_interface)) + { + write_thunked_class(w, type, default_interface); + } else { write_slow_class(w, type, default_interface); diff --git a/cppwinrt/cppwinrt.vcxproj b/cppwinrt/cppwinrt.vcxproj index b8beed890..8cf827962 100644 --- a/cppwinrt/cppwinrt.vcxproj +++ b/cppwinrt/cppwinrt.vcxproj @@ -77,6 +77,7 @@ + diff --git a/cppwinrt/file_writers.h b/cppwinrt/file_writers.h index ed9386b4e..0509db3a0 100644 --- a/cppwinrt/file_writers.h +++ b/cppwinrt/file_writers.h @@ -33,6 +33,7 @@ namespace cppwinrt w.write(strings::base_events); w.write(strings::base_activation); w.write(strings::base_implements); + w.write(strings::base_thunked_runtimeclass); w.write(strings::base_composable); w.write(strings::base_foundation); w.write(strings::base_chrono); diff --git a/cppwinrt/helpers.h b/cppwinrt/helpers.h index 2dc152a74..84c8246c5 100644 --- a/cppwinrt/helpers.h +++ b/cppwinrt/helpers.h @@ -992,6 +992,27 @@ namespace cppwinrt return object; } + static bool is_object_class(TypeSig const& signature) + { + bool result{}; + + call(signature.Type(), + [](ElementType) {}, + [&](coded_index const& type) + { + TypeDef type_def; + if (type.type() == TypeDefOrRef::TypeDef) + type_def = type.TypeDef(); + else if (type.type() == TypeDefOrRef::TypeRef) + type_def = find_required(type.TypeRef()); + if (type_def && get_category(type_def) == category::class_type) + result = true; + }, + [](auto&&) {}); + + return result; + } + static auto get_delegate_method(TypeDef const& type) { auto methods = type.MethodList(); diff --git a/cppwinrt/main.cpp b/cppwinrt/main.cpp index 70a55b076..73fe1e2c3 100644 --- a/cppwinrt/main.cpp +++ b/cppwinrt/main.cpp @@ -37,6 +37,7 @@ namespace cppwinrt { "license", 0, 1, "[]", "Generate license comment from template file" }, { "brackets", 0, 0 }, // Use angle brackets for #includes (defaults to quotes) { "fastabi", 0, 0 }, // Enable support for the Fast ABI + { "flatten_classes", 0, 0 }, // Emit flattened runtimeclass projections with cached interface dispatch { "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 }; @@ -85,6 +86,7 @@ R"( local Local ^%WinDir^%\System32\WinMetadata folder { settings.verbose = args.exists("verbose"); settings.fastabi = args.exists("fastabi"); + settings.flatten_classes = args.exists("flatten_classes"); settings.input = args.files("input", database::is_database); settings.reference = args.files("reference", database::is_database); diff --git a/cppwinrt/settings.h b/cppwinrt/settings.h index e07df4ea2..c563bf819 100644 --- a/cppwinrt/settings.h +++ b/cppwinrt/settings.h @@ -30,6 +30,7 @@ namespace cppwinrt winmd::reader::filter component_filter; bool fastabi{}; + bool flatten_classes{}; std::map fastabi_cache; }; diff --git a/docs/plan-cached-interface-dispatch.md b/docs/plan-cached-interface-dispatch.md new file mode 100644 index 000000000..253511c2c --- /dev/null +++ b/docs/plan-cached-interface-dispatch.md @@ -0,0 +1,833 @@ +# Plan: Thunk-Based Interface Caching for Runtimeclasses + +**Status: Implementation complete.** All phases done, all tests pass. Gated behind +`-flatten_classes` CLI flag (MSBuild: `$(CppWinRTFlattenClasses)`). + +## Rules + +- **NEVER modify existing test source files.** Fixes go in `strings/` or `cppwinrt/`. +- **NEVER poll** waiting for builds. Use async terminal, wait for notification. +- **Use sub-agents for error triage.** Don't read 100K+ line build logs manually. +- **cppwinrt.exe output** must go under `build/` — never into source trees. +- **the compiler and linker are right**. Before you blame them for a runtime or compiler bug, you MUST prove they work. +- **debug to collect dump.** When there's a crash, debuggers are in c:\debuggers\cdb.exe for you to use. Dumps should go under build/ and not into the source tree. +- **disassemble with the debugger** Dumpbin does not work; use `c:\debuggers\cdb.exe -logo nul -z thefile.exe -c "uf binaryname!symbolname ; q"` - the space around the `;` is important. You can use `-c "x binaryname!*symbol*pattern ; q"` to get precise addresses of functions, then use that with `uf` instead if needed. Always use `-logo nul` to suppress the debugger banner. +- **reduce noisy output.** Use `-logo nul` with cdb, `/v:m` with msbuild, `-Verbosity quiet` with nuget. Pipe verbose output to log files and read only the relevant parts. Consider writing helper scripts under `scripts/` to wrap common operations with clean output. +- **commit at reasonable chunks.** Don't create lots of little commits, but don't create huge commits either. Commit when something is observed to work or when you want to experiment and use diffs. + + +## Goal + +Eliminate per-call `QueryInterface`/`Release` overhead when calling non-default interface +methods on projected runtimeclasses. Today every call to e.g. `PropertySet::Insert()` does a +QI for `IMap`, a vtable call, and a Release. The new design uses ASM thunk stubs that +masquerade as COM objects and self-resolve on first vtable call, so the QI cost is paid +once per interface per object, with zero per-call overhead afterward. + +Keep detailed notes on your progress in this file, in the section "Detailed Notes". + +## Approach: Three-way branch in `consume_general` + +The runtimeclass **does not inherit from its default interface**. Instead it inherits from +`impl::thunked_runtimeclass`, which holds: + +- An `atomic default_cache` (the default interface pointer) +- Per-secondary-interface `cache_and_thunk` pairs (cache slot + `interface_thunk`) + +Each `interface_thunk` is 16 bytes with a vtable pointer into a shared table of 256 +architecture-specific ASM stubs. On first method call through any interface, the stub +calls `winrt_cached_resolve_thunk()` which QIs the default interface, atomically replaces +the cache slot with the real pointer, and tail-jumps to the real method. All subsequent +calls dispatch directly — the thunk is never touched again. + +The hot path is `consume_general`, which gets a three-way `if constexpr` branch: + +```cpp +template +void consume_general(Derive const* d, MemberPointer mptr, Args&&... args) +{ + if constexpr (std::is_same_v) + { + // D is the interface itself — direct dispatch + auto const abi = *(abi_t**)d; + check_hresult((abi->*mptr)(std::forward(args)...)); + } + else if constexpr (has_thunked_interface_v) + { + // D has a thunked cache slot for Base. Read the cache slot directly. + // If still a thunk, the vtable dispatch self-resolves via ASM stub. + // If already resolved, direct vtable call. Zero refcount overhead. + auto const abi = *(abi_t**)(&d->template thunk_cache_slot()); + check_hresult((abi->*mptr)(std::forward(args)...)); + } + else + { + // No cache — full QI fallback (same as today). + hresult code; + auto const result = try_as_with_reason(d, code); + check_hresult(code); + auto const abi = *(abi_t**)&result; + check_hresult((abi->*mptr)(std::forward(args)...)); + } +} +``` + +Same three-way split for `consume_noexcept` and `consume_noexcept_remove_overload`. + +The thunk branch reads the cache slot (an `atomic`) as an ABI pointer. If the +slot still holds the thunk, the vtable dispatch goes through the ASM stub which resolves +it. If already resolved, it's a direct vtable call. **No `if(null)` check at the call +site** — the thunk IS the null-state handler, encoded in the ASM. + +### Why NOT `require_one::operator I()` as the intercept point + +`require_one::operator I()` returns by value. Returning a copy of the cache slot pointer +would cause the `I` destructor to Release it — either a no-op (thunk) or incorrectly +releasing the cached real interface. To fix that you'd need to AddRef before returning, +adding per-call interlocked overhead. The `consume_general` three-way branch avoids this +entirely — it reads the cache slot as a raw pointer with zero refcount traffic. + +`require_one::operator I()` is only used for explicit conversion (`IMap map = ps`). +For thunked types it can AddRef the cache slot value (or resolve the thunk first), which +is acceptable for the uncommon conversion path. The hot method-call path never touches it. + +--- + +## Hazard Audit + +### P0: `get_abi(IUnknown const&)` and friends — `*(void**)(&object)` + +**Location:** `strings/base_windows.h` lines 338–375 + +```cpp +inline void* get_abi(IUnknown const& object) noexcept { return *(void**)(&object); } +inline void** put_abi(IUnknown& object) noexcept { ... reinterpret_cast(&object); } +inline void* detach_abi(IUnknown& object) noexcept { ... *(void**)(&object); ... } +inline void attach_abi(IUnknown& object, void* value) noexcept { ... } +``` + +These take `IUnknown const&`/`IUnknown&`. In the thunk design the runtimeclass no longer +inherits from `IUnknown` — it inherits from `impl::thunked_runtimeclass`, +whose first data member is `thunked_runtimeclass_header` (containing `default_cache` then +`iid_table`). With `default_cache` first, `*(void**)(&object)` correctly reads the COM +interface pointer. + +**Mitigation:** Add SFINAE-guarded template overloads (C++17-compatible) that match any +thunked type and delegate to member accessors: + +```cpp +template , int> = 0> +void* get_abi(T const& object) noexcept +{ + return object.get_default_abi(); +} +``` + +These are more constrained than `get_abi(IUnknown const&)` and win overload resolution. +Same pattern for `put_abi`, `detach_abi`, `attach_abi`, `copy_from_abi`, `copy_to_abi`. +**One definition covers all thunked runtimeclasses — no per-class generation.** + +Note: `has_thunked_cache_v` is detected via `std::void_t` +— no C++20 `requires` needed. cppwinrt's language floor is C++17. + +### P0: `copy_from_abi` / `copy_to_abi` + +**Location:** `strings/base_windows.h` lines 370–385 + +Same `*(void**)(&object)` aliasing hazard as `get_abi`. Need the same SFINAE-guarded +template overloads: + +```cpp +template , int> = 0> +void copy_from_abi(T& object, void* value) noexcept { object.copy_from_default_abi(value); } + +template , int> = 0> +void copy_to_abi(T const& object, void*& value) noexcept { object.copy_to_default_abi(value); } +``` + +### P0: `write_abi_args` — `*(void**)(¶m)` for `object_type` IN params + +**Location:** `cppwinrt/code_writers.h` line 645 + +```cpp +case param_category::object_type: +case param_category::string_type: + w.write("*(void**)(&%)", param_name); + break; +``` + +`param_category::object_type` includes `class_type` (runtimeclasses), `interface_type`, +and `delegate_type`. WinRT metadata **can** have method parameters typed as runtimeclasses. + +**Recommendation:** Replace `*(void**)(¶m)` with `get_abi(param)` for `object_type`. +The SFINAE-guarded template overload handles thunked runtimeclasses; the existing +`IUnknown const&` overload handles interfaces and delegates. + +### P0: `bind_out::operator void**()` — `(void**)(&object)` + +**Location:** `strings/base_string.h` lines 511–544 + +**Analysis:** OUT params in WinRT ABI are always interface-typed. `T` in `bind_out` +is always an interface type for COM out-params. **Safe**, but add a `static_assert`: + +```cpp +static_assert(sizeof(T) == sizeof(void*), + "bind_out requires sizeof(T) == sizeof(void*); use put_abi() for larger types"); +``` + +### P0: `WINRT_IMPL_SHIM` macro + +**Location:** `strings/base_macros.h` line 16 + +```cpp +(*(abi_t<__VA_ARGS__>**)&static_cast<__VA_ARGS__ const&>(static_cast(*this))) +``` + +**BREAKS for thunked types.** `static_cast(*this)` requires `*this` to +inherit from `IMap`. Thunked types don't — they inherit from `thunked_runtimeclass`. +The `static_cast` would fail to compile. + +`WINRT_IMPL_SHIM` is only used in hand-written `IMap`/`IMapView` `Lookup`/`Remove` +overloads in `code_writers.h` (5 call sites). **Fix:** Change these call sites to use +`consume_general` (which handles the thunked path) instead of bypassing it via the macro. +The `check_hresult_allow_bounds` behavior can be preserved by adding a variant of +`consume_general` that returns the HRESULT instead of throwing: + +```cpp +template +hresult consume_general_nothrow(Derive const* d, MemberPointer mptr, Args&&... args) noexcept +{ + // same three-way branch, but returns (abi->*mptr)(...) instead of check_hresult +} +``` + +Then the hand-written map overloads call `consume_general_nothrow` and apply +`check_hresult_allow_bounds` on the result. + +### P1: Coroutine `when_any` — `*(unknown_abi**)&sender` + +**Location:** `strings/base_coroutine_foundation.h` lines 863–865 + +`T` is constrained to async interface types. **SAFE — no change needed.** + +### P1: `consume_general` `D == Base` branch + +`*(abi_t**)d` where `d` is `Derive const*` with `Derive == Base` — always an +interface type. **SAFE.** + +--- + +## Thunk vtable: no-op IUnknown slots + +The thunk vtable's first 3 entries (slots 0/1/2 = QI/AddRef/Release) use dedicated +no-op functions instead of the generic resolve stubs: + +```asm +winrt_cached_thunk_qi proc + mov dword ptr [r8], 0 ; *ppv = nullptr + mov eax, 80004002h ; E_NOINTERFACE + ret +winrt_cached_thunk_qi endp + +winrt_cached_thunk_addref proc + mov eax, 1 + ret +winrt_cached_thunk_addref endp + +winrt_cached_thunk_release proc + mov eax, 1 + ret +winrt_cached_thunk_release endp +``` + +The vtable array uses these for slots 0–2, regular stubs for slots 3–255: + +```asm +winrt_cached_thunk_vtable label qword + dq winrt_cached_thunk_qi ; slot 0 + dq winrt_cached_thunk_addref ; slot 1 + dq winrt_cached_thunk_release ; slot 2 + vtable_entry %3 ; slot 3+ + ... +``` + +**Why this matters:** With no-op Release on the thunk, lifecycle code becomes simpler: + +- **Destructor:** Unconditionally Release every cache slot. If the slot holds a thunk, + Release is a no-op. If it holds a real interface, Release decrements the refcount. + No "is it a thunk?" check needed. +- **Copy:** Cannot blindly AddRef all slots — thunk pointers are embedded in the *source* + object's storage. Copying a thunk pointer would create a dangling reference to the + source. Copy must: for resolved slots, AddRef + copy the real pointer; for unresolved + slots, initialize a fresh thunk in the destination. +- **Move:** Steal all cache slots wholesale (thunk or real, doesn't matter), then + reinitialize thunks in the destination pointing to the destination's header. + +--- + +## Runtimeclass Categories + +### Cacheable (thunked) + +Non-composable, non-fastabi, non-static runtimeclasses with ≥1 secondary interface +and a non-async default interface. Enabled by the `-flatten_classes` cppwinrt.exe flag. +Examples: `PropertySet`, `StringMap`, `StorageFile`, `MediaCapture`. + +Includes types with generic default interfaces (`StringMap` defaults to +`IMap`, not a named interface). The `get_default_interface()` in the +code generator returns `coded_index` which handles both cases uniformly. + +### Excluded (initial implementation) + +| Category | Reason | +|----------|--------| +| Composable runtimeclasses | Complex base class chains, `impl::base<>`, override machinery | +| Fast ABI runtimeclasses | Already optimized, `[FastAbi]` attribute, separate code path | +| Static-only runtimeclasses | No instances (`write_static_class`) | +| Single-interface runtimeclasses | No secondaries to cache (e.g. `Deferral`) | +| Runtimeclasses with async default interface | Async types have special lifetime semantics (`DataWriterStoreOperation` etc.) | +| Async types | `IAsyncAction` etc. are interfaces, not runtimeclasses | +| Component-authored types | Use `implements<>`, not the projected runtimeclass | + +--- + +## Thunk-Based Design + +### Architecture overview + +The prototype is in `jonwis.github.io/code/cppwinrt-proj/thunk_experiment.h`. + +``` +thunked_runtimeclass layout: + +┌─ thunked_runtimeclass_header (16 bytes) ───────────────────────┐ +│ default_cache: atomic → IPropertySet ABI ptr │ +│ iid_table: guid const* const* → static iids array │ +├─ pairs[0]: cache_and_thunk_tagged (24 bytes) ──────────────────┤ +│ cache: atomic → initially &thunk, then real IMap* │ +│ thunk: interface_thunk → { vtable → g_thunk_vtable, payload } │ +├─ pairs[1]: cache_and_thunk_tagged (24 bytes) ──────────────────┤ +│ cache: atomic → initially &thunk, then real IIterable*│ +│ thunk: interface_thunk → { vtable → g_thunk_vtable, payload } │ +├─ pairs[2]: cache_and_thunk_tagged (24 bytes) ──────────────────┤ +│ cache: atomic → IObservableMap* │ +│ thunk: interface_thunk → { vtable → g_thunk_vtable, payload } │ +└────────────────────────────────────────────────────────────────┘ + +Total: 16 + 3×24 = 88 bytes (N=3, tagged mode) +``` + +Each `interface_thunk` (16 bytes) masquerades as a COM object. Its vtable points to a +shared table of 256 ASM stubs. Slots 0–2 are no-op QI/AddRef/Release (see above). +Each method stub (10 bytes on x64): + +```asm +winrt_cached_thunk_stub_N: + mov eax, N ; slot index + jmp CachedResolveAndDispatch +``` + +`CachedResolveAndDispatch` (~80 bytes, shared): +1. Saves caller's `rdx`/`r8`/`r9` and slot index via `push` (with `NESTED_ENTRY` unwind info) +2. Calls `winrt_cached_resolve_thunk(rcx)` — rcx is `interface_thunk*` +3. `resolve()` atomically replaces the cache slot with the real interface via QI +4. Loads `real_vtable[slot_index]` into `rax`, validates via `__guard_check_icall_fptr` (CFG) +5. Restores caller's args, tail-jumps to the real method via `rex_jmp_reg rax` + +After resolution, the cache slot holds the real COM pointer directly. All subsequent +calls dispatch through the real vtable — zero overhead. + +### `as()` / `try_as()` on thunked types + +Since the thunked runtimeclass does not inherit from `IUnknown`, it provides its own +`as()` and `try_as()` that QI through the default interface: + +```cpp +// In thunked_runtimeclass_base: +template auto as() const +{ + return reinterpret_cast(&default_cache)->as(); +} + +template auto try_as() const noexcept +{ + return reinterpret_cast(&default_cache)->try_as(); +} + +template auto try_as_with_reason(hresult& code) const noexcept +{ + return reinterpret_cast(&default_cache)->try_as_with_reason(code); +} +``` + +`default_cache` is `sizeof(void*)` — reinterpreting it as `IInspectable const*` is valid +(same aliasing as projected interfaces). This provides `ps.as()` etc. + +These are found by unqualified lookup because `PropertySet` inherits from +`thunked_runtimeclass_base` which defines them. + +### Trait: `has_thunked_interface_v` + +Uses a `thunked_interfaces` type alias inherited from the base class: + +```cpp +// In thunked_runtimeclass: +using thunked_interfaces = std::tuple; +``` + +Detected via `std::void_t` (C++17-compatible): + +```cpp +template +inline constexpr bool has_thunked_cache_v = false; + +template +inline constexpr bool has_thunked_cache_v> = true; + +template +inline constexpr bool has_thunked_interface_v = false; + +template +inline constexpr bool has_thunked_interface_v> = + tuple_contains_v; +``` + +No per-class specializations needed. The trait is derived from the base class template. + +### `thunk_cache_slot()` accessor + +```cpp +// In thunked_runtimeclass: +template +std::atomic const& thunk_cache_slot() const +{ + constexpr size_t idx = type_index::value; + static_assert(idx < sizeof...(I), "Interface not in thunked list"); + return pairs[idx].cache; +} +``` + +### ABI overloads via SFINAE template (C++17) + +```cpp +// In winrt namespace (or winrt::impl): +template , int> = 0> +void* get_abi(T const& object) noexcept { return object.get_default_abi(); } + +template , int> = 0> +void** put_abi(T& object) noexcept { object.clear_thunked(); return object.put_default_abi(); } + +template , int> = 0> +void* detach_abi(T& object) noexcept { return object.detach_default_abi(); } + +template , int> = 0> +void attach_abi(T& object, void* value) noexcept { object.attach_default_abi(value); } + +template , int> = 0> +void copy_from_abi(T& object, void* value) noexcept { object.copy_from_default_abi(value); } + +template , int> = 0> +void copy_to_abi(T const& object, void*& value) noexcept { object.copy_to_default_abi(value); } +``` + +One definition per function covers all thunked types. The `get_default_abi()`, +`put_default_abi()`, etc. are members of `thunked_runtimeclass_base`. + +### `write_abi_args` change + +Replace `*(void**)(¶m)` with `get_abi(param)` for `object_type` IN params: + +```cpp +case param_category::object_type: + w.write("get_abi(%)", param_name); + break; +case param_category::string_type: + w.write("*(void**)(&%)", param_name); // hstring unchanged + break; +``` + +--- + +## Runtimeclass generated shape + +### Before (current): + +```cpp +struct WINRT_IMPL_EMPTY_BASES PropertySet : IPropertySet, + impl::require, + IIterable>, + IObservableMap> +{ + PropertySet(std::nullptr_t) noexcept {} + PropertySet(void* ptr, take_ownership_from_abi_t) noexcept + : IPropertySet(ptr, take_ownership_from_abi) {} + PropertySet(); +}; +``` + +`sizeof(PropertySet) == sizeof(void*)`. Secondary interfaces QI'd on every method call. + +### After (thunked): + +```cpp +struct WINRT_IMPL_EMPTY_BASES PropertySet : + impl::thunked_runtimeclass, + IIterable>, + IObservableMap>, + impl::require, + IIterable>, + IObservableMap> +{ + PropertySet(std::nullptr_t) noexcept : thunked_runtimeclass(nullptr) {} + PropertySet(void* ptr, take_ownership_from_abi_t) noexcept + : thunked_runtimeclass(ptr) {} + PropertySet(); // calls factory, delegates to take_ownership_from_abi ctor +}; +``` + +`sizeof(PropertySet) == 88 bytes` (header 16 + 3×24 pairs, tagged mode). +The `require<>` CRTP still provides `consume_t` methods. `consume_general` uses the +thunk branch for known interfaces, QI fallback for unknown ones. + +### Factory constructor wiring + +The default constructor calls through the factory machinery: + +```cpp +inline PropertySet::PropertySet() : + PropertySet(impl::call_factory_cast( + [](IActivationFactory const& f) { return f.template ActivateInstance(); })) +{ } +``` + +`ActivateInstance()` returns `PropertySet(void*, take_ownership_from_abi)`, +which passes the raw ABI pointer to `thunked_runtimeclass(ptr)`. The base class stores +it in `default_cache` and initializes all thunk pairs. **No change to factory machinery.** + +### StringMap (generic default interface): + +```cpp +struct WINRT_IMPL_EMPTY_BASES StringMap : + impl::thunked_runtimeclass, + IIterable>, + IObservableMap>, + impl::require>, + IObservableMap> +{ ... }; +``` + +### Header file placement + +The thunked class is generated in the same `.2.h` file as today. `write_slow_class` +in `code_writers.h` changes the base class for cacheable types; the output file path +is unchanged. + +### Include ordering for `base_thunked_runtimeclass.h` + +Include in `write_base_h()` (`file_writers.h`) after `base_implements.h` and before +the generated projection headers. This ensures `thunked_runtimeclass` is defined +before any runtimeclass type that inherits from it. + +```cpp +// In write_base_h(): +// ... existing includes ... +write(strings::base_implements); +write(strings::base_thunked_runtimeclass); // NEW +// ... then generated headers ... +``` + +--- + +## ASM stubs + +### Shared across all thunked types + +| File | Architecture | Size | +|------|-------------|------| +| `strings/cached_thunks_x64.asm` | x64 (MASM) | ~4.7 KB | +| `strings/cached_thunks_x86.asm` | x86 (MASM) | ~2.9 KB | +| `strings/cached_thunks_arm64.asm` | ARM64 (armasm64) | ~4.2 KB | +| `strings/cached_thunks_arm64ec.asm` | ARM64EC (armasm64) | ~4.2 KB | + +256 stubs × 10 bytes each + common dispatch + no-op IUnknown slots + vtable array. + +### Extern declarations + +```cpp +extern "C" inline void* winrt_cached_resolve_thunk(interface_thunk const* thunk) +{ + return thunk->resolve(); +} +// Force the compiler to emit the inline function body so the ASM thunk stubs can find it. +extern "C" __declspec(selectany) void* (*winrt_resolve_thunk_forcelink_)(interface_thunk const*) = winrt_cached_resolve_thunk; +extern "C" const void* winrt_cached_thunk_vtable[256]; +``` + +`winrt_cached_resolve_thunk` is defined inline in `base_thunked_runtimeclass.h` (emitted +into `winrt/base.h`). The `selectany` function-pointer variable forces the compiler to +emit the inline function body as an externally-visible COMDAT symbol, which the ASM stubs +reference. Projects that don't include `winrt/base.h` (e.g. C++/CX) must not link the +ASM stubs. + +### Build integration + +The ASM files compile into a static library (`cppwinrt_thunks` or similar) that links +into any binary using thunked runtimeclasses. The NuGet package includes pre-compiled +`.obj` files per architecture. + +--- + +## Thread safety + +`interface_thunk::resolve()` uses `compare_exchange_strong` on the cache slot: +- Two threads racing to resolve the same interface both QI successfully +- The loser's `compare_exchange` fails; it releases its result and uses the winner's pointer +- No locks, no spinwaits + +After resolution, the cache slot holds a raw pointer and all reads are `memory_order_acquire` +loads — standard lock-free pattern. + +--- + +## Implementation Plan + +### Phase 1: Runtime infrastructure (`strings/`) + +1. **`base_thunked_runtimeclass.h`** — new file containing: + - `thunked_runtimeclass_header` (default_cache + iid_table) + - `interface_thunk` (16 bytes, `resolve()` logic) + - `cache_and_thunk_tagged` / `cache_and_thunk_full` pair types + - `thunked_runtimeclass_base` (clear, attach, copy, move — non-template) + - `thunked_runtimeclass` typed template + - `type_index` compile-time helper + - `has_thunked_cache_v` / `has_thunked_interface_v` traits via `thunked_interfaces` + - `get_default_abi()`, `put_default_abi()`, `detach_default_abi()`, `attach_default_abi()` + - `copy_from_default_abi()`, `copy_to_default_abi()` + - `as()`, `try_as()`, `try_as_with_reason()` members + +2. **SFINAE-guarded ABI overloads** (`base_thunked_runtimeclass.h`): + - `get_abi`, `put_abi`, `detach_abi`, `attach_abi` + - `copy_from_abi`, `copy_to_abi` + - All use `std::enable_if_t>` (C++17) + +3. **Modify `consume_general`** (`base_windows.h`): + - Add `has_thunked_interface_v` branch + - Same for `consume_noexcept` and `consume_noexcept_remove_overload` + - Add `consume_general_nothrow` variant for map Lookup/Remove overloads + +4. **Fix `WINRT_IMPL_SHIM` call sites** (`code_writers.h`): + - Replace 5 hand-written `WINRT_IMPL_SHIM` calls with `consume_general_nothrow` + - Or keep `WINRT_IMPL_SHIM` for the `D == Base` case and add a thunked alternative + +5. **ASM stubs** — copy from prototype, add no-op QI/AddRef/Release: + - `strings/cached_thunks_x64.asm` + - `strings/cached_thunks_x86.asm` + - `strings/cached_thunks_arm64.asm` + - `strings/cached_thunks_arm64ec.asm` + +6. **`bind_out` static_assert** (`base_string.h`) + +7. **Include in `write_base_h()`** (`file_writers.h`): after `base_implements.h` + +8. **`write_abi_args`** (`code_writers.h`): + - `*(void**)(¶m)` → `get_abi(param)` for `object_type` IN params + - Safe before Phase 2: for non-thunked types, `get_abi(param)` resolves to the + existing `get_abi(IUnknown const&)` which does the same `*(void**)(&object)` + +### Phase 2: Code generator (`cppwinrt/`) + +9. **`write_slow_class`** (`code_writers.h`): + - For cacheable types: inherit from `impl::thunked_runtimeclass` + instead of the default interface directly + - Keep `impl::require<>` inheritance for consume CRTP methods + - Generate constructors that delegate to `thunked_runtimeclass` + - Class goes in the same `.2.h` file as today + +10. **`write_default_interface`** / `default_interface` trait: + - Must still work — `thunked_runtimeclass` stores `IDefault` in + the template args; the trait maps `PropertySet → IPropertySet` + +11. **Build system** (`CMakeLists.txt`): + - New `cppwinrt_thunks` static library target for ASM stubs + - Per-architecture assembly rules + +### Phase 3: Validation + +12. **All existing tests must pass unchanged.** No test file modifications allowed. + +13. **New tests:** + - Thunk resolution correctness (first call QIs, subsequent calls skip) + - Copy semantics (fresh thunks, lazy re-resolve) + - Move semantics (steal default + reinit thunks) + - Thread safety (8+ threads racing to resolve same interface) + - `get_abi`/`put_abi`/`detach_abi`/`attach_abi` on thunked types + - `copy_from_abi`/`copy_to_abi` on thunked types + - `as()`/`try_as()` on thunked types + - Types with generic default interface (StringMap) + - Types with many secondaries (>8 → full mode with explicit IID storage) + +--- + +## Risks + +| Risk | Mitigation | +|------|-----------| +| Per-instance size (88 bytes for N=3 vs 8) | QI elimination justifies it for hot types | +| ASM stubs per architecture | 4 files, ~4 KB each, shared across all types | +| MinGW/Clang: no MASM | GAS equivalents needed; or C trampoline fallback | +| C++17 floor | Use `std::enable_if_t` / `std::void_t` instead of `requires` | +| 256-slot vtable limit | WinRT interfaces rarely exceed ~30 methods; `static_assert` | +| `consume_general` branch prediction | `if constexpr` — resolved at compile time | + +--- + +## Open questions + +1. **NuGet packaging:** ASM stubs need compilation. Options: pre-compiled `.obj` per arch, + or MSBuild targets that assemble from source. + +2. **`operator=(nullptr)`:** Must clear default + all cache slots, release resolved ones. + +3. **Interaction with `base<>` / composable types:** Deferred to a future phase. + +--- + +## Tooling: Build & Validate + +### Scripts + +| Script | Purpose | +|--------|---------| +| `scripts/build_and_test.ps1` | Parallel build + test runner | +| `scripts/run_cppwinrt.ps1` | Run `cppwinrt.exe` with output under `build/` | + +### `scripts/build_and_test.ps1` + +Default: builds only `test\test` (the main test target and its dependencies). This is +the fastest feedback loop for iterating on `strings/` and `cppwinrt/` changes. + +``` +.\scripts\build_and_test.ps1 # build test\test only (x64 Release) +.\scripts\build_and_test.ps1 -Test # build test\test + run it +.\scripts\build_and_test.ps1 -BuildAll # build ALL test targets +.\scripts\build_and_test.ps1 -BuildAll -Test # build all + run all +.\scripts\build_and_test.ps1 -BinLog # produce binary log +.\scripts\build_and_test.ps1 -Clean # git clean -dfx . then build +``` + +- Default (no flags) — build-only for `test\test` and its deps (prebuild, cppwinrt, + test_component, test_component_no_pch). +- **`-BuildAll`** — builds all 9 test targets in a single `msbuild /m /v:m` invocation. +- **`-Test`** — after building, runs whichever test executables were built. +- **`-BinLog`** — produces `_build\build.binlog` for structured error analysis. +- **`-Clean`** — runs `git clean -dfx .` before building. Wipes all build artifacts. + +### `scripts/run_cppwinrt.ps1` + +Runs locally-built `cppwinrt.exe`. Output goes under `build/` (gitignored). + +``` +.\scripts\run_cppwinrt.ps1 # build\projection\x64\Release +.\scripts\run_cppwinrt.ps1 -OutputDir build\projection\custom # custom output +``` + +### Agent workflow for build-fix iterations + +1. **Make the code change** (edit `strings/*.h`, `cppwinrt/code_writers.h`, etc.) + +2. **Run the build** via terminal in async mode: + ``` + .\scripts\build_and_test.ps1 -BinLog + ``` + Do **NOT** poll or sleep. Wait for terminal completion notification. + +3. **On build failure**, dispatch a sub-agent (e.g. `Explore` with GPT-5.3-Codex) to read + `_build\build_output.log`, extract `error C####` lines, group by file, return a report. + If `-BinLog` was used, the agent can use `binlog_lm_errors` instead. + +4. **Review the report** and fix. Repeat from step 1. + +5. **On build success**, run tests: + ``` + .\scripts\build_and_test.ps1 -Test + ``` + +6. **On test failure**, read `_build\x64\Release\_results.txt`. + +7. **To inspect generated headers**: + ``` + .\scripts\run_cppwinrt.ps1 + ``` + +## Development Notes + +### Key architectural decisions + +1. **`default_cache` is the first member** of `thunked_runtimeclass_header`, so + `*(void**)(&object)` reads the COM pointer — matching the `IUnknown` layout that + `get_abi`, `bind_in`, and other `reinterpret_cast` patterns rely on. + +2. **Thunked traits in `base_meta.h`**, not `base_thunked_runtimeclass.h`. The traits + (`has_thunked_cache_v`, `has_thunked_interface_v`) must be visible before + `consume_general` in `base_windows.h`. + +3. **ABI overloads in `base_windows.h`**, not the thunked header. `detach_from` in + `base_activation.h` couldn't see them when they were in the later-included header. + +4. **`resolve()` returns nullptr on QI failure** — no exceptions through ASM frames. + The ASM stub checks the return value; if null, returns `E_NOINTERFACE` directly. + `consume_general`'s `check_hresult` on the method result throws in a proper C++ frame. + +5. **`produce_borrowed_ref`** wraps ABI `void*` parameters in produce stubs. For + thunked types (sizeof > 8), a `reinterpret_cast` would overread the stack. The wrapper + constructs a proper thunked temporary via `T{nullptr}` + `attach_abi`, then `detach_abi` + on destruction to prevent Release (caller owns the reference). + +6. **`extern "C" inline` + `selectany` forcelink** for `winrt_cached_resolve_thunk` in the + header. MSVC won't emit `extern "C" inline` functions as externally-linkable symbols + unless something takes the address. The `selectany` function-pointer variable forces + emission. Projects not including `winrt/base.h` (C++/CX, proxy/stub) must not link + the ASM stubs. + +7. **`-flatten_classes` opt-in flag** gates thunked runtimeclass generation, matching the + `-fastabi` pattern. MSBuild property: `$(CppWinRTFlattenClasses)`. + +### Async exclusion + +Runtimeclasses whose default interface is `IAsyncAction`, `IAsyncOperation`, +`IAsyncActionWithProgress`, or `IAsyncOperationWithProgress` are excluded. +Thunked types don't inherit the async interface, losing `await_resume`/`operator co_await`. +Detected via `has_async_default_interface()` in `code_writers.h`. + +### COM identity for thunked types + +Thunked types add `has_thunked_cache_v` to ~15 trait checks that previously used +`is_base_of`: `arg`, `com_ref`, `empty_value`, `is_com_interface`, +`box_value`/`unbox_value`, and all ABI overloads. Hidden-friend `operator==`/`!=` on +`thunked_runtimeclass_base` implements COM identity via three-tier comparison: +pointer equality → `default_cache` match → QI for IUnknown. + +### Commits + +| Hash | Description | +|------|-------------| +| `64b8e32f` | Plan doc + build/test scripts | +| `85d475f0` | Plan review fixes | +| `5a3fa1e1` | Move write_abi_args to Phase 1 | +| `93ef473d` | Rules: -logo nul, noisy output | +| `0cb57ec8` | Phase 1: thunked infrastructure | +| `2600e332` | Phase 2: code generator emits thunked runtimeclasses | +| `40dac14b` | Fix thunked type identity (arg, is_com_interface, empty_value, box/unbox) | +| `59740cef` | thunked_dispatch test + disassembly verification | +| `722d83a5` | Phase 3: copy/move, ABI interop, as/try_as, threading tests | +| `63e8c490` | Phase 3: generic default + full mode tests | +| `9edcf17d` | Phase 3 complete | +| `b175ad26` | E2E: async exclusion, operator==, delegate ABI fix | +| `6bdf12d0` | Inline resolve thunk into base.h | +| `94f6aad4` | Gate behind -flatten_classes flag | +| `ee7371aa` | Doc fixes for PR readiness | \ No newline at end of file diff --git a/docs/runtimeclass-caching.md b/docs/runtimeclass-caching.md new file mode 100644 index 000000000..488494b82 --- /dev/null +++ b/docs/runtimeclass-caching.md @@ -0,0 +1,209 @@ +# Runtimeclass Interface Caching + +## What changed + +Projected runtimeclass types that have secondary interfaces (e.g., `PropertySet`, +`StringMap`, `Uri`) now cache resolved interface pointers instead of calling +`QueryInterface` on every method call. The projected type's in-memory layout changed +from a single `void*` (the default interface pointer) to a struct containing the +default pointer plus per-interface cache slots backed by self-resolving ASM thunks. + +This feature is opt-in via the `-flatten_classes` flag to cppwinrt.exe, or by setting +`$(CppWinRTFlattenClasses)` to `true` in MSBuild projects using the C++/WinRT NuGet package. + +## Impact on consumers + +**None.** The API surface is identical. Existing code that uses projected runtimeclass +types compiles and runs without modification. The change is purely in the generated +type layout and internal dispatch mechanism. + +## Which types are affected + +A runtimeclass is cached if all of the following are true: +- The `-flatten_classes` flag is passed to cppwinrt.exe +- It has a default interface (not a static-only class) +- It is not marked `[FastAbi]` +- It has no base class (not composable) +- It has at least one secondary interface +- Its default interface is not an async type (`IAsyncAction`, `IAsyncOperation`, etc.) + +Examples: `PropertySet`, `StringMap`, `Uri`, `XmlDocument`, `Package`. + +Types that remain unchanged: single-interface runtimeclasses (e.g., `Deferral` if it +only had `IDeferral`), composable types, `[FastAbi]` types, static classes, all +interfaces, delegates, and structs. + +## Generated code: before and after + +### Old: `DisplayInformation` (master) + +```cpp +struct DisplayInformation : IDisplayInformation, + impl::require +{ + DisplayInformation(std::nullptr_t) noexcept {} + DisplayInformation(void* ptr, take_ownership_from_abi_t) noexcept + : IDisplayInformation(ptr, take_ownership_from_abi) {} + static auto GetForCurrentView(); + // ...statics... +}; +``` + +`sizeof(DisplayInformation) == 8` (one `void*` — the `IDisplayInformation` COM pointer). + +### New: `DisplayInformation` (this branch) + +```cpp +struct DisplayInformation : + impl::thunked_runtimeclass, + impl::require +{ + DisplayInformation(std::nullptr_t) noexcept : thunked_runtimeclass(nullptr) {} + DisplayInformation(void* ptr, take_ownership_from_abi_t) noexcept + : thunked_runtimeclass(ptr, take_ownership_from_abi) {} + static auto GetForCurrentView(); + // ...statics... +}; +``` + +`sizeof(DisplayInformation) == 112` (header 16 + 4 × 24 cache/thunk pairs). + +## Dispatch comparison + +Consider: + +```cpp +auto info = DisplayInformation::GetForCurrentView(); +auto dpi = info.LogicalDpi(); // IDisplayInformation (default) +auto rawDpi = info.RawDpiX(); // IDisplayInformation (default) +auto brightness = info.ScreenBrightness(); // IDisplayInformation4 (secondary) +auto hz = info.RefreshRate(); // IDisplayInformation5 (secondary) +``` + +`LogicalDpi` and `RawDpiX` are on the default interface `IDisplayInformation` — these +dispatch directly through `default_cache` in both old and new code (no QI in either +case). + +`ScreenBrightness` is on `IDisplayInformation4` and `RefreshRate` is on +`IDisplayInformation5` — secondary interfaces. These go through +`consume_general`. + +### Old dispatch (per call to a secondary interface) + +``` +consume_general is called with Derive=DisplayInformation, Base=IDisplayInformation4. +Derive != Base, so: + 1. try_as_with_reason(info) → QueryInterface + AddRef + 2. *(abi_t**)&result → load vtable from QI result + 3. (abi->*mptr)(args...) → call ScreenBrightness via vtable + 4. ~com_ref() → Release on the QI result +``` + +Every call does QI + Release — two interlocked refcount operations and a COM +apartment check, even though the same object always returns the same interface. + +### New dispatch (per call to a secondary interface) + +``` +consume_general is called with Derive=DisplayInformation, Base=IDisplayInformation4. +has_thunked_interface_v is true, so: + 1. d->thunk_cache_slot() → address of cache slot (compile-time offset) + 2. *(abi_t**)(&slot) → load pointer from cache slot + 3. (abi->*mptr)(args...) → call ScreenBrightness via vtable +``` + +No QI, no Release, no refcount traffic. The cache slot holds either a thunk (first +call) or the real interface pointer (all subsequent calls). + +## The proxy-replacement mechanism + +Each cache slot is initialized to point at an `interface_thunk` — a 16-byte struct +that masquerades as a COM object. Its vtable points to a shared table of 256 ASM +stubs. + +``` +cache_and_thunk layout: + +┌─ cache: atomic ──── initially points to ──┐ +├─ thunk: interface_thunk ◄────────────────────────┘ +│ vtable → g_thunk_vtable[256] +│ payload → (header pointer + interface index) +└────────────────────────────────────────────── +``` + +### First call through a cache slot + +The cache holds a pointer to the thunk. The vtable dispatch enters the ASM stub: + +```asm +winrt_cached_thunk_stub_N: + mov eax, N ; vtable slot index + jmp CachedResolveAndDispatch ; save registers, call resolve, tail-jump +``` + +`CachedResolveAndDispatch` calls `winrt_cached_resolve_thunk(thunk*)`, which: + +1. Reads `thunk->payload` to find the default interface pointer and the IID +2. Calls `QueryInterface(iid, &real)` +3. Atomically replaces the cache slot: `cache.compare_exchange(&thunk, real)` +4. Returns the real interface pointer + +The stub then tail-jumps to `real_vtable[slot_index]`, completing the original +method call with the real COM object. + +### All subsequent calls + +The cache slot now holds the real `IMap*` pointer. The vtable dispatch goes directly +to the real COM object's vtable — zero overhead vs a raw interface call. + +### Why this is safe + +**Thread safety.** Two threads racing to resolve the same slot both QI successfully. +The winner's `compare_exchange` stores the real pointer; the loser's CAS fails, it +Releases its duplicate result and uses the winner's pointer. All reads after +resolution are `memory_order_acquire` loads — standard lock-free pattern. + +**Lifetime safety.** The thunk's IUnknown slots are no-ops: `QueryInterface` returns +`E_NOINTERFACE`, `AddRef` and `Release` return 1. This means: + +- **Destructor** can unconditionally Release every cache slot. Thunk → no-op Release. + Real pointer → normal Release. No "is it a thunk?" check needed. +- **Copy** AddRefs the default interface and re-initializes fresh thunks in the + destination (lazy re-resolve on first use). +- **Move** steals the default pointer and all cache slots wholesale, then + re-initializes thunks in the source. + +**ABI compatibility.** The thunk is layout-compatible with a COM object (`void**` +pointing to a vtable). Any code that reads `*(void**)&cache_slot` gets a valid +vtable pointer — either the thunk's or the real object's. The `consume_general` +hot path doesn't distinguish between them. + +**Produce stubs.** WinRT component produce stubs (server-side ABI implementations) +receive `void*` parameters and forward them to the C++ implementation as projected +types. The old code used `*reinterpret_cast(¶m)` to view the `void*` +as `T const&` — valid when `sizeof(T) == sizeof(void*)`. With thunked types being +larger, this overreads the stack. The fix: `produce_borrowed_ref` constructs a +proper thunked wrapper from the `void*` (via `T{nullptr}` + `attach_abi`) and +detaches on destruction to prevent Release. This is only emitted for `class_type` +parameters; interface and delegate types remain pointer-sized and use the zero-cost +reinterpret pattern. + +## Summary of changes from the prior model + +| Aspect | Before | After | +|--------|--------|-------| +| Runtimeclass base class | Default interface (e.g., `IDisplayInformation`) | `impl::thunked_runtimeclass` | +| `sizeof(DisplayInformation)` | 8 bytes | 112 bytes (N=4 secondaries) | +| Secondary interface call | QI + vtable call + Release (per call) | Cache slot load + vtable call (per call) | +| First secondary call cost | QI + Release | QI + atomic CAS (one-time) | +| Subsequent call cost | QI + Release | Direct vtable dispatch (zero overhead) | +| Refcount operations per call | 2 (AddRef in QI, Release after) | 0 | +| Thread safety | N/A (stateless) | Lock-free compare-exchange on resolve | +| Consumer API changes | — | None | diff --git a/nuget/Microsoft.Windows.CppWinRT.nuspec b/nuget/Microsoft.Windows.CppWinRT.nuspec index a63e08bf9..65c701dfa 100644 --- a/nuget/Microsoft.Windows.CppWinRT.nuspec +++ b/nuget/Microsoft.Windows.CppWinRT.nuspec @@ -21,6 +21,9 @@ + + + diff --git a/nuget/Microsoft.Windows.CppWinRT.targets b/nuget/Microsoft.Windows.CppWinRT.targets index 188e56835..31e6c020c 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) -flatten_classes "$(CppWinRTPackageDir)bin\" "$(CppWinRTPackageDir)" @@ -892,6 +893,7 @@ $(XamlMetaDataProviderPch) %(AdditionalDependencies);WindowsApp.lib %(AdditionalDependencies);$(CppWinRTPackageDir)build\native\lib\$(Platform)\cppwinrt_fast_forwarder.lib + %(AdditionalDependencies);$(CppWinRTPackageDir)build\native\lib\$(Platform)\cppwinrt_cached_thunks.lib diff --git a/run_tests.cmd b/run_tests.cmd index 58e3c5524..9554a45c2 100644 --- a/run_tests.cmd +++ b/run_tests.cmd @@ -7,6 +7,7 @@ set target_version=%3 if "%target_platform%"=="" set target_platform=x64 if "%target_configuration%"=="" set target_configuration=Debug +set any_failed=false call :run_test test call :run_test test_nocoro @@ -17,6 +18,7 @@ call :run_test test_slow call :run_test test_old call :run_test test_module_lock_custom call :run_test test_module_lock_none +if "%any_failed%"=="true" exit /b 1 goto :eof :run_test @@ -28,5 +30,6 @@ if %ERRORLEVEL% EQU 0 ( ) else ( type %1_results.txt >&2 echo %1 >> test_failures.txt + set any_failed=true ) goto :eof diff --git a/scripts/build_and_test.ps1 b/scripts/build_and_test.ps1 new file mode 100644 index 000000000..898a7695f --- /dev/null +++ b/scripts/build_and_test.ps1 @@ -0,0 +1,112 @@ +# build_and_test.ps1 — Parallel build and test for cppwinrt +# +# Default: builds only test\test (and its deps: prebuild, cppwinrt, test_component, etc.) +# -BuildAll: builds all 9 test targets +# -Test: after building, runs whichever test executables were built +# -Clean: git clean -dfx . before building (wipes all build artifacts) +# -BinLog: produce _build\build.binlog for structured error analysis +# +# Usage: +# .\scripts\build_and_test.ps1 # build test\test only +# .\scripts\build_and_test.ps1 -Test # build test\test + run it +# .\scripts\build_and_test.ps1 -BuildAll # build all test targets +# .\scripts\build_and_test.ps1 -BuildAll -Test # build all + run all +# .\scripts\build_and_test.ps1 -Clean -BuildAll # clean + build all +param( + [string]$Platform = "x64", + [string]$Configuration = "Release", + [switch]$BuildAll, + [switch]$Test, + [switch]$Clean, + [switch]$BinLog +) + +$ErrorActionPreference = "Stop" +$root = git -C $PSScriptRoot rev-parse --show-toplevel + +if ($Clean) { + Write-Host "Cleaning workspace (git clean -dfx) ..." -ForegroundColor Yellow + git -C $root clean -dfx . +} + +# Ensure NuGet +if (-not (Test-Path "$root\.nuget\nuget.exe")) { + New-Item -ItemType Directory -Path "$root\.nuget" -Force | Out-Null + $ProgressPreference = 'SilentlyContinue' + Invoke-WebRequest "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" ` + -OutFile "$root\.nuget\nuget.exe" +} +& "$root\.nuget\nuget.exe" restore "$root\cppwinrt.sln" -Verbosity quiet + +# Select targets. Default: just test\test (pulls in prebuild, cppwinrt, test_component, etc.) +if ($BuildAll) { + $targets = @( + "test\test", + "test\test_nocoro", + "test\test_cpp20", + "test\test_cpp20_no_sourcelocation", + "test\test_fast", + "test\test_slow", + "test\test_module_lock_custom", + "test\test_module_lock_none", + "test\old_tests\test_old" + ) -join ";" +} else { + $targets = "test\test" +} + +$buildDir = "$root\_build" +New-Item -ItemType Directory -Path $buildDir -Force | Out-Null + +$msbuildArgs = @( + "$root\cppwinrt.sln", + "/v:m", "/m", + "/p:Configuration=$Configuration", + "/p:Platform=$Platform", + "/t:$targets" +) +if ($BinLog) { + $msbuildArgs += "/bl:$buildDir\build.binlog" +} + +Write-Host "Building: $targets" -ForegroundColor Cyan +$buildLog = "$buildDir\build_output.log" +& msbuild @msbuildArgs 2>&1 | Tee-Object -FilePath $buildLog +$buildExitCode = $LASTEXITCODE + +if ($buildExitCode -ne 0) { + Write-Host "BUILD FAILED (exit code $buildExitCode)" -ForegroundColor Red + Write-Host "Full log: $buildLog" + exit $buildExitCode +} + +Write-Host "BUILD SUCCEEDED" -ForegroundColor Green + +if (-not $Test) { exit 0 } + +# Run tests — only executables that exist (covers both default and -BuildAll) +$testDir = "$buildDir\$Platform\$Configuration" +$testExes = @( + "test", "test_nocoro", "test_cpp20", "test_cpp20_no_sourcelocation", + "test_fast", "test_slow", "test_old", + "test_module_lock_custom", "test_module_lock_none" +) +$failures = @() +foreach ($t in $testExes) { + $exe = "$testDir\$t.exe" + if (-not (Test-Path $exe)) { continue } + Write-Host "RUN $t" -ForegroundColor Cyan -NoNewline + & $exe > "$testDir\${t}_results.txt" 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host " FAIL" -ForegroundColor Red + $failures += $t + } else { + Write-Host " PASS" -ForegroundColor Green + } +} + +if ($failures.Count -gt 0) { + Write-Host "`nFAILED TESTS: $($failures -join ', ')" -ForegroundColor Red + exit 1 +} +Write-Host "`nALL TESTS PASSED" -ForegroundColor Green diff --git a/scripts/run_cppwinrt.ps1 b/scripts/run_cppwinrt.ps1 new file mode 100644 index 000000000..bfa8311e6 --- /dev/null +++ b/scripts/run_cppwinrt.ps1 @@ -0,0 +1,43 @@ +# run_cppwinrt.ps1 — Run cppwinrt.exe to generate projection headers +# Usage: .\scripts\run_cppwinrt.ps1 [-Platform x64] [-Configuration Release] [-OutputDir build\projection] +# +# Generates projection headers from local winmd into a directory under build/. +# Use this when you need to regenerate headers after changing strings/ or cppwinrt/ code. +param( + [string]$Platform = "x64", + [string]$Configuration = "Release", + [string]$OutputDir = "" +) + +$ErrorActionPreference = "Stop" +$root = git -C $PSScriptRoot rev-parse --show-toplevel + +$cppwinrtExe = "$root\_build\$Platform\$Configuration\cppwinrt.exe" +if (-not (Test-Path $cppwinrtExe)) { + Write-Error "cppwinrt.exe not found at $cppwinrtExe. Run build_and_test.ps1 -BuildOnly first." + exit 1 +} + +if (-not $OutputDir) { + $OutputDir = "$root\build\projection\$Platform\$Configuration" +} +# Ensure output is under build/ (relative to repo root) +if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { + $OutputDir = Join-Path $root $OutputDir +} + +New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null + +Write-Host "Running cppwinrt.exe:" -ForegroundColor Cyan +Write-Host " Input: local" -ForegroundColor Gray +Write-Host " Output: $OutputDir" -ForegroundColor Gray + +& $cppwinrtExe -in local -out $OutputDir +$exitCode = $LASTEXITCODE + +if ($exitCode -ne 0) { + Write-Host "cppwinrt.exe FAILED (exit code $exitCode)" -ForegroundColor Red + exit $exitCode +} + +Write-Host "Projection generated at: $OutputDir" -ForegroundColor Green diff --git a/strings/base_activation.h b/strings/base_activation.h index 1a195d865..ce2e32901 100644 --- a/strings/base_activation.h +++ b/strings/base_activation.h @@ -540,9 +540,18 @@ WINRT_EXPORT namespace winrt template T ActivateInstance() const { - IInspectable instance; - check_hresult((*(impl::abi_t**)this)->ActivateInstance(put_abi(instance))); - return instance.try_as(); + if constexpr (impl::has_thunked_cache_v) + { + void* result{}; + check_hresult((*(impl::abi_t**)this)->ActivateInstance(&result)); + return{ result, take_ownership_from_abi }; + } + else + { + IInspectable instance; + check_hresult((*(impl::abi_t**)this)->ActivateInstance(put_abi(instance))); + return instance.try_as(); + } } }; } diff --git a/strings/base_implements.h b/strings/base_implements.h index 7edf32149..997bbf817 100644 --- a/strings/base_implements.h +++ b/strings/base_implements.h @@ -45,7 +45,7 @@ namespace winrt::impl using tuple_if = typename tuple_if_base::type; template - struct is_interface : std::disjunction, is_classic_com_interface> {}; + struct is_interface : std::disjunction, is_classic_com_interface, std::bool_constant>> {}; template struct is_marker : std::disjunction, std::is_void> {}; diff --git a/strings/base_meta.h b/strings/base_meta.h index 7dbb4c386..7f7b6767a 100644 --- a/strings/base_meta.h +++ b/strings/base_meta.h @@ -184,10 +184,50 @@ namespace winrt::impl struct WINRT_IMPL_EMPTY_BASES base : base_one... {}; + // ======================================================================== + // Traits for detecting thunked runtimeclasses (C++17-compatible) + // ======================================================================== + + template + inline constexpr bool has_thunked_cache_v = false; + + template + inline constexpr bool has_thunked_cache_v> = true; + + template + struct tuple_contains : std::disjunction...> {}; + + template + struct tuple_contains_tuple; + + template + struct tuple_contains_tuple> : tuple_contains {}; + + template + inline constexpr bool has_thunked_interface_v = false; + + template + inline constexpr bool has_thunked_interface_v> = + tuple_contains_tuple::value; + + // ======================================================================== + // Compile-time type index helper + // ======================================================================== + + template + struct type_index; + + template + struct type_index : std::integral_constant ? 0 : 1 + type_index::value> {}; + + template + struct type_index : std::integral_constant {}; + template T empty_value() noexcept { - if constexpr (std::is_base_of_v) + if constexpr (std::is_base_of_v || has_thunked_cache_v) { return nullptr; } @@ -223,7 +263,7 @@ namespace winrt::impl }; template - struct arg>> + struct arg || has_thunked_cache_v>> { using in = void*; }; diff --git a/strings/base_reference_produce.h b/strings/base_reference_produce.h index 2820aff50..6e9ea6d4d 100644 --- a/strings/base_reference_produce.h +++ b/strings/base_reference_produce.h @@ -518,7 +518,7 @@ WINRT_EXPORT namespace winrt template , int> = 0> Windows::Foundation::IInspectable box_value(T const& value) { - if constexpr (std::is_base_of_v) + if constexpr (std::is_base_of_v || impl::has_thunked_cache_v) { return value; } @@ -531,7 +531,7 @@ WINRT_EXPORT namespace winrt template T unbox_value(Windows::Foundation::IInspectable const& value) { - if constexpr (std::is_base_of_v) + if constexpr (std::is_base_of_v || impl::has_thunked_cache_v) { return value.as(); } @@ -560,7 +560,7 @@ WINRT_EXPORT namespace winrt { if (value) { - if constexpr (std::is_base_of_v) + if constexpr (std::is_base_of_v || impl::has_thunked_cache_v) { if (auto temp = value.try_as()) { diff --git a/strings/base_thunked_runtimeclass.h b/strings/base_thunked_runtimeclass.h new file mode 100644 index 000000000..3138b7ac9 --- /dev/null +++ b/strings/base_thunked_runtimeclass.h @@ -0,0 +1,436 @@ + +WINRT_EXPORT namespace winrt::impl +{ + // ======================================================================== + // Thunk data structures + // ======================================================================== + + struct alignas(16) thunked_runtimeclass_header + { + // default_cache MUST be the first member so that *(void**)&object reads + // the COM pointer — matching the layout of IUnknown-derived types and + // ensuring reinterpret_cast(&void_ptr) works in produce stubs. + mutable std::atomic default_cache{}; + guid const* const* iid_table{}; + }; + + struct interface_thunk + { + void const* const* vtable; + uintptr_t payload; + + std::atomic* cache_slot() const + { + return reinterpret_cast*>( + const_cast(reinterpret_cast(this)) - sizeof(std::atomic)); + } + + __declspec(noinline) void* resolve() const + { + auto* slot = cache_slot(); + void* current = slot->load(std::memory_order_acquire); + if (current != static_cast(this)) + return current; + + void* default_abi; + guid const* iid; + + if (payload & 1) + { + auto* hdr = reinterpret_cast(payload & ~uintptr_t(0xF)); + default_abi = hdr->default_cache.load(std::memory_order_relaxed); + iid = hdr->iid_table[(payload >> 1) & 7]; + } + else + { + default_abi = reinterpret_cast(payload); + iid = *reinterpret_cast( + reinterpret_cast(this) + sizeof(interface_thunk)); + } + + void* real = nullptr; + if (static_cast(default_abi)->QueryInterface(*iid, &real) < 0) + return nullptr; + + void* expected = const_cast(this); + if (!slot->compare_exchange_strong(expected, real, std::memory_order_release, std::memory_order_acquire)) + { + static_cast(real)->Release(); + return expected; + } + return real; + } + }; + static_assert(sizeof(interface_thunk) == 16); + + extern "C" inline void* winrt_cached_resolve_thunk(interface_thunk const* thunk) + { + return thunk->resolve(); + } + // Force the compiler to emit the inline function body so the ASM thunk stubs can find it. + extern "C" __declspec(selectany) void* (*winrt_resolve_thunk_forcelink_)(interface_thunk const*) = winrt_cached_resolve_thunk; + extern "C" const void* winrt_cached_thunk_vtable[]; + + struct cache_and_thunk_tagged + { + mutable std::atomic cache{}; + mutable interface_thunk thunk{}; + }; + static_assert(sizeof(cache_and_thunk_tagged) == 24); + static_assert(offsetof(cache_and_thunk_tagged, thunk) == sizeof(std::atomic)); + + struct cache_and_thunk_full + { + mutable std::atomic cache{}; + mutable interface_thunk thunk{}; + mutable guid const* iid{}; + }; + static_assert(sizeof(cache_and_thunk_full) == 32); + static_assert(offsetof(cache_and_thunk_full, thunk) == sizeof(std::atomic)); + static_assert(offsetof(cache_and_thunk_full, iid) == offsetof(cache_and_thunk_full, thunk) + sizeof(interface_thunk)); + + inline void init_pair_tagged(cache_and_thunk_tagged& p, size_t index, thunked_runtimeclass_header* header) + { + p.cache.store(&p.thunk, std::memory_order_relaxed); + p.thunk.vtable = reinterpret_cast(winrt_cached_thunk_vtable); + p.thunk.payload = reinterpret_cast(header) | (index << 1) | 1; + } + + inline void init_pair_full(cache_and_thunk_full& p, void* default_abi, guid const* iid) + { + p.cache.store(&p.thunk, std::memory_order_relaxed); + p.thunk.vtable = reinterpret_cast(winrt_cached_thunk_vtable); + p.thunk.payload = reinterpret_cast(default_abi); + p.iid = iid; + } + + template + using cache_and_thunk_t = std::conditional_t; + + // ======================================================================== + // Non-template base: all COM lifecycle operations via (pointer, count, stride) + // ======================================================================== + + struct thunked_runtimeclass_base : thunked_runtimeclass_header + { + protected: + __declspec(noinline) void clear_impl(void* pairs_begin, size_t count, size_t stride) + { + if (auto p = default_cache.exchange(nullptr, std::memory_order_acquire)) + static_cast(p)->Release(); + + auto* base = static_cast(pairs_begin); + for (size_t i = 0; i < count; ++i, base += stride) + { + auto& cache = *reinterpret_cast*>(base); + auto* thunk = reinterpret_cast(base + sizeof(std::atomic)); + auto p = cache.exchange(nullptr, std::memory_order_acquire); + if (p && p != thunk) + static_cast(p)->Release(); + } + } + + __declspec(noinline) void attach_impl(void* default_abi, void* pairs_begin, size_t count, size_t stride, bool tagged) + { + default_cache.store(default_abi, std::memory_order_relaxed); + auto* base = static_cast(pairs_begin); + if (tagged) + { + for (size_t i = 0; i < count; ++i, base += stride) + init_pair_tagged(*reinterpret_cast(base), i, this); + } + else + { + for (size_t i = 0; i < count; ++i, base += stride) + init_pair_full(*reinterpret_cast(base), default_abi, iid_table[i]); + } + } + + __declspec(noinline) void copy_from(thunked_runtimeclass_base const& other, void* pairs_begin, size_t count, size_t stride, bool tagged) + { + if (auto p = other.default_cache.load(std::memory_order_relaxed)) + { + static_cast(p)->AddRef(); + attach_impl(p, pairs_begin, count, stride, tagged); + } + } + + __declspec(noinline) void move_from(thunked_runtimeclass_base& other, void* my_pairs, void* other_pairs, size_t count, size_t stride, bool tagged) + { + auto p = other.default_cache.exchange(nullptr, std::memory_order_acquire); + if (p) attach_impl(p, my_pairs, count, stride, tagged); + other.clear_impl(other_pairs, count, stride); + } + + __declspec(noinline) void assign_copy_impl(thunked_runtimeclass_base const& other, void* pairs_begin, size_t count, size_t stride, bool tagged) + { + if (this != &other) + { + clear_impl(pairs_begin, count, stride); + copy_from(other, pairs_begin, count, stride, tagged); + } + } + + __declspec(noinline) void assign_move_impl(thunked_runtimeclass_base& other, void* my_pairs, void* other_pairs, size_t count, size_t stride, bool tagged) + { + if (this != &other) + { + clear_impl(my_pairs, count, stride); + move_from(other, my_pairs, other_pairs, count, stride, tagged); + } + } + + public: + template auto as() const + { + return reinterpret_cast(&default_cache)->as(); + } + + template auto try_as() const noexcept + { + return reinterpret_cast(&default_cache)->try_as(); + } + + template auto try_as(hresult& code) const noexcept + { + return try_as_with_reason(reinterpret_cast(&default_cache), code); + } + + explicit operator bool() const noexcept + { + return default_cache.load(std::memory_order_relaxed) != nullptr; + } + + operator Windows::Foundation::IUnknown() const noexcept + { + Windows::Foundation::IUnknown result{ nullptr }; + copy_from_abi(result, default_cache.load(std::memory_order_relaxed)); + return result; + } + + operator Windows::Foundation::IInspectable() const noexcept + { + Windows::Foundation::IInspectable result{ nullptr }; + copy_from_abi(result, default_cache.load(std::memory_order_relaxed)); + return result; + } + + friend bool operator==(thunked_runtimeclass_base const& left, thunked_runtimeclass_base const& right) noexcept + { + if (&left == &right) + { + return true; + } + auto left_abi = left.default_cache.load(std::memory_order_relaxed); + auto right_abi = right.default_cache.load(std::memory_order_relaxed); + if (left_abi == right_abi) + { + return true; + } + if (!left_abi || !right_abi) + { + return false; + } + return get_abi(left.try_as()) == get_abi(right.try_as()); + } + + friend bool operator!=(thunked_runtimeclass_base const& left, thunked_runtimeclass_base const& right) noexcept + { + return !(left == right); + } + + friend bool operator==(thunked_runtimeclass_base const& left, std::nullptr_t) noexcept + { + return left.default_cache.load(std::memory_order_relaxed) == nullptr; + } + + friend bool operator!=(thunked_runtimeclass_base const& left, std::nullptr_t) noexcept + { + return left.default_cache.load(std::memory_order_relaxed) != nullptr; + } + + friend bool operator==(std::nullptr_t, thunked_runtimeclass_base const& right) noexcept + { + return right.default_cache.load(std::memory_order_relaxed) == nullptr; + } + + friend bool operator!=(std::nullptr_t, thunked_runtimeclass_base const& right) noexcept + { + return right.default_cache.load(std::memory_order_relaxed) != nullptr; + } + + void* get_default_abi() const noexcept + { + return default_cache.load(std::memory_order_relaxed); + } + + void** put_default_abi() noexcept + { + return reinterpret_cast(&default_cache); + } + + void* detach_default_abi() noexcept + { + return default_cache.exchange(nullptr, std::memory_order_relaxed); + } + + void attach_default_abi(void* value) noexcept + { + default_cache.store(value, std::memory_order_relaxed); + } + + void copy_from_default_abi(void* value) noexcept + { + if (value) + { + static_cast(value)->AddRef(); + } + default_cache.store(value, std::memory_order_relaxed); + } + + void copy_to_default_abi(void*& value) const noexcept + { + WINRT_ASSERT(value == nullptr); + value = default_cache.load(std::memory_order_relaxed); + if (value) + { + static_cast(value)->AddRef(); + } + } + }; + + // ======================================================================== + // Typed template: adds interface accessors, wires up lifecycle + // ======================================================================== + + template + struct thunked_runtimeclass : thunked_runtimeclass_base + { + static constexpr size_t N = sizeof...(I); + static constexpr bool use_tagged = N <= 8; + using pair_type = cache_and_thunk_t; + static constexpr size_t pair_stride = sizeof(pair_type); + + using thunked_interfaces = std::tuple; + + inline static const std::array iids{ &guid_of()... }; + mutable std::array pairs{}; + + protected: + thunked_runtimeclass(std::nullptr_t) noexcept + { + iid_table = iids.data(); + } + + thunked_runtimeclass(void* default_abi, take_ownership_from_abi_t) noexcept + { + iid_table = iids.data(); + attach_impl(default_abi, pairs.data(), N, pair_stride, use_tagged); + } + + thunked_runtimeclass() noexcept + { + iid_table = iids.data(); + } + + public: + ~thunked_runtimeclass() + { + clear_impl(pairs.data(), N, pair_stride); + } + + thunked_runtimeclass(thunked_runtimeclass const& other) + { + iid_table = iids.data(); + copy_from(other, pairs.data(), N, pair_stride, use_tagged); + } + + thunked_runtimeclass(thunked_runtimeclass&& other) noexcept + { + iid_table = iids.data(); + move_from(other, pairs.data(), other.pairs.data(), N, pair_stride, use_tagged); + } + + thunked_runtimeclass& operator=(thunked_runtimeclass const& other) + { + assign_copy_impl(other, pairs.data(), N, pair_stride, use_tagged); + return *this; + } + + thunked_runtimeclass& operator=(thunked_runtimeclass&& other) noexcept + { + assign_move_impl(other, pairs.data(), other.pairs.data(), N, pair_stride, use_tagged); + return *this; + } + + thunked_runtimeclass& operator=(std::nullptr_t) noexcept + { + clear_impl(pairs.data(), N, pair_stride); + return *this; + } + + using thunked_runtimeclass_base::operator bool; + using thunked_runtimeclass_base::as; + using thunked_runtimeclass_base::try_as; + using thunked_runtimeclass_base::get_default_abi; + using thunked_runtimeclass_base::put_default_abi; + using thunked_runtimeclass_base::detach_default_abi; + using thunked_runtimeclass_base::attach_default_abi; + using thunked_runtimeclass_base::copy_from_default_abi; + using thunked_runtimeclass_base::copy_to_default_abi; + + template + std::atomic const& thunk_cache_slot() const noexcept + { + if constexpr (std::is_same_v) + { + return default_cache; + } + else + { + constexpr size_t idx = type_index::value; + static_assert(idx < sizeof...(I), "Interface not in thunked list"); + return pairs[idx].cache; + } + } + + void clear_thunked() noexcept + { + clear_impl(pairs.data(), N, pair_stride); + } + + void reset_thunked(void* new_default_abi) noexcept + { + clear_impl(pairs.data(), N, pair_stride); + if (new_default_abi) + { + attach_impl(new_default_abi, pairs.data(), N, pair_stride, use_tagged); + } + } + }; + + // Non-owning wrapper for produce stub IN parameters. Constructs a proper + // thunked runtimeclass from a void* ABI pointer (borrowed reference) and + // detaches on destruction to prevent Release. For non-thunked types, just + // reinterpret-casts (zero overhead, same as before). + template + struct produce_borrowed_ref + { + T value{ nullptr }; + + produce_borrowed_ref(void* abi) noexcept + { + attach_abi(value, abi); + } + + ~produce_borrowed_ref() + { + detach_abi(value); + } + + produce_borrowed_ref(produce_borrowed_ref const&) = delete; + produce_borrowed_ref& operator=(produce_borrowed_ref const&) = delete; + + operator T const&() const noexcept { return value; } + }; +} diff --git a/strings/base_windows.h b/strings/base_windows.h index 21c4163e5..3bf77d5cc 100644 --- a/strings/base_windows.h +++ b/strings/base_windows.h @@ -67,7 +67,7 @@ namespace winrt::impl #endif template - using com_ref = std::conditional_t, T, com_ptr>; + using com_ref = std::conditional_t || has_thunked_cache_v, T, com_ptr>; template , int> = 0> com_ref wrap_as_result(void* result) @@ -85,7 +85,7 @@ namespace winrt::impl struct is_classic_com_interface : std::conjunction, std::negation>> {}; template - struct is_com_interface : std::disjunction, std::is_base_of, is_implements, is_classic_com_interface> {}; + struct is_com_interface : std::disjunction, std::is_base_of, is_implements, is_classic_com_interface, std::bool_constant>> {}; template inline constexpr bool is_com_interface_v = is_com_interface::value; @@ -296,13 +296,13 @@ WINRT_EXPORT namespace winrt::Windows::Foundation WINRT_EXPORT namespace winrt { - template , int> = 0> + template && !impl::has_thunked_cache_v, int> = 0> auto get_abi(T const& object) noexcept { return reinterpret_cast const&>(object); } - template , int> = 0> + template && !impl::has_thunked_cache_v, int> = 0> auto put_abi(T& object) noexcept { if constexpr (!std::is_trivially_destructible_v) @@ -313,19 +313,19 @@ WINRT_EXPORT namespace winrt return reinterpret_cast*>(&object); } - template , int> = 0> + template && !impl::has_thunked_cache_v, int> = 0> void copy_from_abi(T& object, V&& value) { object = reinterpret_cast(value); } - template , int> = 0> + template && !impl::has_thunked_cache_v, int> = 0> void copy_to_abi(T const& object, V& value) { reinterpret_cast(value) = object; } - template > && !std::is_convertible_v, int> = 0> + template > && !impl::has_thunked_cache_v> && !std::is_convertible_v, int> = 0> auto detach_abi(T&& object) { impl::abi_t result{}; @@ -369,6 +369,53 @@ WINRT_EXPORT namespace winrt return nullptr; } + template , int> = 0> + void* get_abi(T const& object) noexcept + { + return object.get_default_abi(); + } + + template , int> = 0> + void** put_abi(T& object) noexcept + { + object.clear_thunked(); + return object.put_default_abi(); + } + + template , int> = 0> + void* detach_abi(T& object) noexcept + { + return object.detach_default_abi(); + } + + template >, int> = 0> + void* detach_abi(T&& object) noexcept + { + return object.detach_default_abi(); + } + + template , int> = 0> + void attach_abi(T& object, void* value) noexcept + { + object.reset_thunked(value); + } + + template , int> = 0> + void copy_from_abi(T& object, void* value) noexcept + { + if (value) + { + static_cast(value)->AddRef(); + } + object.reset_thunked(value); + } + + template , int> = 0> + void copy_to_abi(T const& object, void*& value) noexcept + { + object.copy_to_default_abi(value); + } + inline void copy_from_abi(Windows::Foundation::IUnknown& object, void* value) noexcept { object = nullptr; @@ -457,17 +504,22 @@ WINRT_EXPORT namespace winrt::impl template void consume_noexcept_remove_overload(Derive const* d, MemberPointer mptr, Args&&... args) noexcept { - if constexpr (!std::is_same_v) + if constexpr (std::is_same_v) { - winrt::hresult _winrt_cast_result_code; - auto const _winrt_casted_result = try_as_with_reason(d, _winrt_cast_result_code); - check_hresult(_winrt_cast_result_code); - auto const _winrt_abi_type = *(abi_t**)&_winrt_casted_result; + auto const _winrt_abi_type = *(abi_t**)d; + (_winrt_abi_type->*mptr)(std::forward(args)...); + } + else if constexpr (has_thunked_interface_v) + { + auto const _winrt_abi_type = *(abi_t**)(&d->template thunk_cache_slot()); (_winrt_abi_type->*mptr)(std::forward(args)...); } else { - auto const _winrt_abi_type = *(abi_t**)d; + winrt::hresult _winrt_cast_result_code; + auto const _winrt_casted_result = try_as_with_reason(d, _winrt_cast_result_code); + check_hresult(_winrt_cast_result_code); + auto const _winrt_abi_type = *(abi_t**)&_winrt_casted_result; (_winrt_abi_type->*mptr)(std::forward(args)...); } } @@ -475,17 +527,22 @@ WINRT_EXPORT namespace winrt::impl template void consume_noexcept(Derive const* d, MemberPointer mptr, Args&&... args) noexcept { - if constexpr (!std::is_same_v) + if constexpr (std::is_same_v) { - winrt::hresult _winrt_cast_result_code; - auto const _winrt_casted_result = try_as_with_reason(d, _winrt_cast_result_code); - check_hresult(_winrt_cast_result_code); - auto const _winrt_abi_type = *(abi_t**)&_winrt_casted_result; + auto const _winrt_abi_type = *(abi_t**)d; + WINRT_VERIFY_(0, (_winrt_abi_type->*mptr)(std::forward(args)...)); + } + else if constexpr (has_thunked_interface_v) + { + auto const _winrt_abi_type = *(abi_t**)(&d->template thunk_cache_slot()); WINRT_VERIFY_(0, (_winrt_abi_type->*mptr)(std::forward(args)...)); } else { - auto const _winrt_abi_type = *(abi_t**)d; + winrt::hresult _winrt_cast_result_code; + auto const _winrt_casted_result = try_as_with_reason(d, _winrt_cast_result_code); + check_hresult(_winrt_cast_result_code); + auto const _winrt_abi_type = *(abi_t**)&_winrt_casted_result; WINRT_VERIFY_(0, (_winrt_abi_type->*mptr)(std::forward(args)...)); } } @@ -493,7 +550,17 @@ WINRT_EXPORT namespace winrt::impl template void consume_general(Derive const* d, MemberPointer mptr, Args&&... args) { - if constexpr (!std::is_same_v) + if constexpr (std::is_same_v) + { + auto const _winrt_abi_type = *(abi_t**)d; + check_hresult((_winrt_abi_type->*mptr)(std::forward(args)...)); + } + else if constexpr (has_thunked_interface_v) + { + auto const _winrt_abi_type = *(abi_t**)(&d->template thunk_cache_slot()); + check_hresult((_winrt_abi_type->*mptr)(std::forward(args)...)); + } + else { winrt::hresult _winrt_cast_result_code; auto const _winrt_casted_result = try_as_with_reason(d, _winrt_cast_result_code); @@ -501,10 +568,28 @@ WINRT_EXPORT namespace winrt::impl auto const _winrt_abi_type = *(abi_t**)&_winrt_casted_result; check_hresult((_winrt_abi_type->*mptr)(std::forward(args)...)); } - else + } + + template + hresult consume_general_nothrow(Derive const* d, MemberPointer mptr, Args&&... args) noexcept + { + if constexpr (std::is_same_v) { auto const _winrt_abi_type = *(abi_t**)d; - check_hresult((_winrt_abi_type->*mptr)(std::forward(args)...)); + return (_winrt_abi_type->*mptr)(std::forward(args)...); + } + else if constexpr (has_thunked_interface_v) + { + auto const _winrt_abi_type = *(abi_t**)(&d->template thunk_cache_slot()); + return (_winrt_abi_type->*mptr)(std::forward(args)...); + } + else + { + winrt::hresult _winrt_cast_result_code; + auto const _winrt_casted_result = try_as_with_reason(d, _winrt_cast_result_code); + check_hresult(_winrt_cast_result_code); + auto const _winrt_abi_type = *(abi_t**)&_winrt_casted_result; + return (_winrt_abi_type->*mptr)(std::forward(args)...); } } } diff --git a/strings/cached_thunks_arm64.asm b/strings/cached_thunks_arm64.asm new file mode 100644 index 000000000..298c16eb0 --- /dev/null +++ b/strings/cached_thunks_arm64.asm @@ -0,0 +1,139 @@ +; cached_thunks_arm64.asm - ARM64 cached interface dispatch stubs +; +; Calling convention: https://docs.microsoft.com/en-us/cpp/build/arm64-windows-abi-conventions +; +; Each leaf stub loads a slot index into w10 and branches to CachedResolveAndDispatch. +; CachedResolveAndDispatch calls winrt_cached_resolve_thunk(x0) which returns the +; resolved IFoo* (or nullptr). On success, replaces x0 with the resolved pointer, +; validates via CFG, and tail-jumps to vtable[slot]. On failure, returns E_NOINTERFACE. + +#include "ksarm64.h" + + IMPORT winrt_cached_resolve_thunk + IMPORT __guard_check_icall_fptr + + TEXTAREA + +; ============================================================================ +; No-op IUnknown slots for thunk objects +; The thunk is not a real COM object; QI fails, AddRef/Release return 1. +; ============================================================================ + + LEAF_ENTRY winrt_cached_thunk_qi + str xzr, [x2] ; *ppv = nullptr + mov w0, #0x4002 + movk w0, #0x8000, lsl #16 ; E_NOINTERFACE + ret + LEAF_END winrt_cached_thunk_qi + + LEAF_ENTRY winrt_cached_thunk_addref + mov w0, #1 + ret + LEAF_END winrt_cached_thunk_addref + + LEAF_ENTRY winrt_cached_thunk_release + mov w0, #1 + ret + LEAF_END winrt_cached_thunk_release + +; ============================================================================ +; CachedResolveAndDispatch +; +; Entry: w10 = vtable slot index, x0 = interface_thunk* +; x1-x7 = caller's args (preserved across resolve) +; +; Calls winrt_cached_resolve_thunk(x0) -> x0 = resolved IFoo* or nullptr. +; On success: validates target via CFG, tail-jumps to vtable[slot]. +; On failure: returns E_NOINTERFACE (0x80004002). +; ============================================================================ + + CFG_ALIGN + NESTED_ENTRY CachedResolveAndDispatch + + ; Save frame, link, caller's args, and slot index + PROLOG_SAVE_REG_PAIR fp, lr, #-80! + PROLOG_NOP stp x1, x2, [sp, #16] + PROLOG_NOP stp x3, x4, [sp, #32] + PROLOG_NOP stp x5, x6, [sp, #48] + PROLOG_NOP stp x7, x10, [sp, #64] + + ; x0 = interface_thunk* (already in place) + bl winrt_cached_resolve_thunk + + ; Restore slot index (x10) and caller's args + ldp x7, x10, [sp, #64] + ldp x5, x6, [sp, #48] + ldp x3, x4, [sp, #32] + ldp x1, x2, [sp, #16] + + cbz x0, resolve_failed + + ; x0 = resolved IFoo*. Load method ptr and validate via CFG. + ldr x9, [x0] ; x9 = resolved vtable + ldr x15, [x9, x10, lsl #3] ; x15 = method at vtable[slot] + + ; Verify indirect call target (CFG: target in x15, validator in x12) + adrp x12, __guard_check_icall_fptr + ldr x12, [x12, __guard_check_icall_fptr] + blr x12 + + ; Restore frame and tail-jump to validated method + EPILOG_RESTORE_REG_PAIR fp, lr, #80! + EPILOG_NOP br x15 + +resolve_failed + mov w0, #0x4002 + movk w0, #0x8000, lsl #16 ; E_NOINTERFACE + EPILOG_RESTORE_REG_PAIR fp, lr, #80! + EPILOG_RETURN + + NESTED_END CachedResolveAndDispatch + +; ============================================================================ +; Leaf stub macro — each loads a slot index and branches to the shared dispatcher +; movz w10, #imm16 = 0x5280000A | (imm16 << 5) +; ============================================================================ + + MACRO + WINRT_CACHED_THUNK $idx + LEAF_ENTRY winrt_cached_thunk_stub_$idx + DCD (0x5280000A :OR: ($idx:SHL:5)) + b CachedResolveAndDispatch + LEAF_END winrt_cached_thunk_stub_$idx + MEND + +; Emit 256 stubs (slots 0-255) + GBLA StubCtr +StubCtr SETA 0 + WHILE StubCtr < 256 + WINRT_CACHED_THUNK $StubCtr +StubCtr SETA StubCtr + 1 + WEND + +; ============================================================================ +; Vtable array: slots 0-2 are no-op IUnknown, slots 3-255 are resolve stubs +; Read-only data. +; ============================================================================ + + AREA |.rdata|, DATA, READONLY, ALIGN=3 + + EXPORT winrt_cached_thunk_vtable +winrt_cached_thunk_vtable + + DCQ winrt_cached_thunk_qi + DCQ winrt_cached_thunk_addref + DCQ winrt_cached_thunk_release + + MACRO + VtableEntry $idx + DCQ winrt_cached_thunk_stub_$idx + MEND + + GBLA VtblCtr +VtblCtr SETA 3 + WHILE VtblCtr < 256 + VtableEntry $VtblCtr +VtblCtr SETA VtblCtr + 1 + WEND + + END diff --git a/strings/cached_thunks_arm64ec.asm b/strings/cached_thunks_arm64ec.asm new file mode 100644 index 000000000..c496205cf --- /dev/null +++ b/strings/cached_thunks_arm64ec.asm @@ -0,0 +1,135 @@ +; cached_thunks_arm64ec.asm - ARM64EC cached interface dispatch stubs +; +; ARM64EC uses ARM64 instructions with x64-compatible calling convention. +; Logic is identical to ARM64. Fast Forward Sequences handle transitions. + +#include "ksarm64.h" + + IMPORT winrt_cached_resolve_thunk + IMPORT __guard_check_icall_fptr + + TEXTAREA + +; ============================================================================ +; No-op IUnknown slots for thunk objects +; The thunk is not a real COM object; QI fails, AddRef/Release return 1. +; ============================================================================ + + LEAF_ENTRY winrt_cached_thunk_qi + str xzr, [x2] ; *ppv = nullptr + mov w0, #0x4002 + movk w0, #0x8000, lsl #16 ; E_NOINTERFACE + ret + LEAF_END winrt_cached_thunk_qi + + LEAF_ENTRY winrt_cached_thunk_addref + mov w0, #1 + ret + LEAF_END winrt_cached_thunk_addref + + LEAF_ENTRY winrt_cached_thunk_release + mov w0, #1 + ret + LEAF_END winrt_cached_thunk_release + +; ============================================================================ +; CachedResolveAndDispatch +; +; Entry: w10 = vtable slot index, x0 = interface_thunk* +; x1-x7 = caller's args (preserved across resolve) +; +; Calls winrt_cached_resolve_thunk(x0) -> x0 = resolved IFoo* or nullptr. +; On success: validates target via CFG, tail-jumps to vtable[slot]. +; On failure: returns E_NOINTERFACE (0x80004002). +; ============================================================================ + + CFG_ALIGN + NESTED_ENTRY CachedResolveAndDispatch + + ; Save frame, link, caller's args, and slot index + PROLOG_SAVE_REG_PAIR fp, lr, #-80! + PROLOG_NOP stp x1, x2, [sp, #16] + PROLOG_NOP stp x3, x4, [sp, #32] + PROLOG_NOP stp x5, x6, [sp, #48] + PROLOG_NOP stp x7, x10, [sp, #64] + + ; x0 = interface_thunk* (already in place) + bl winrt_cached_resolve_thunk + + ; Restore slot index (x10) and caller's args + ldp x7, x10, [sp, #64] + ldp x5, x6, [sp, #48] + ldp x3, x4, [sp, #32] + ldp x1, x2, [sp, #16] + + cbz x0, resolve_failed + + ; x0 = resolved IFoo*. Load method ptr and validate via CFG. + ldr x9, [x0] ; x9 = resolved vtable + ldr x15, [x9, x10, lsl #3] ; x15 = method at vtable[slot] + + ; Verify indirect call target (CFG: target in x15, validator in x12) + adrp x12, __guard_check_icall_fptr + ldr x12, [x12, __guard_check_icall_fptr] + blr x12 + + ; Restore frame and tail-jump to validated method + EPILOG_RESTORE_REG_PAIR fp, lr, #80! + EPILOG_NOP br x15 + +resolve_failed + mov w0, #0x4002 + movk w0, #0x8000, lsl #16 ; E_NOINTERFACE + EPILOG_RESTORE_REG_PAIR fp, lr, #80! + EPILOG_RETURN + + NESTED_END CachedResolveAndDispatch + +; ============================================================================ +; Leaf stub macro — each loads a slot index and branches to the shared dispatcher +; movz w10, #imm16 = 0x5280000A | (imm16 << 5) +; ============================================================================ + + MACRO + WINRT_CACHED_THUNK $idx + LEAF_ENTRY winrt_cached_thunk_stub_$idx + DCD (0x5280000A :OR: ($idx:SHL:5)) + b CachedResolveAndDispatch + LEAF_END winrt_cached_thunk_stub_$idx + MEND + +; Emit 256 stubs (slots 0-255) + GBLA StubCtr +StubCtr SETA 0 + WHILE StubCtr < 256 + WINRT_CACHED_THUNK $StubCtr +StubCtr SETA StubCtr + 1 + WEND + +; ============================================================================ +; Vtable array: slots 0-2 are no-op IUnknown, slots 3-255 are resolve stubs +; Read-only data. +; ============================================================================ + + AREA |.rdata|, DATA, READONLY, ALIGN=3 + + EXPORT winrt_cached_thunk_vtable +winrt_cached_thunk_vtable + + DCQ winrt_cached_thunk_qi + DCQ winrt_cached_thunk_addref + DCQ winrt_cached_thunk_release + + MACRO + VtableEntry $idx + DCQ winrt_cached_thunk_stub_$idx + MEND + + GBLA VtblCtr +VtblCtr SETA 3 + WHILE VtblCtr < 256 + VtableEntry $VtblCtr +VtblCtr SETA VtblCtr + 1 + WEND + + END diff --git a/strings/cached_thunks_x64.asm b/strings/cached_thunks_x64.asm new file mode 100644 index 000000000..615fd598d --- /dev/null +++ b/strings/cached_thunks_x64.asm @@ -0,0 +1,136 @@ +; cached_thunks_x64.asm - x64 cached interface dispatch stubs +; +; Calling convention: https://docs.microsoft.com/en-us/cpp/build/calling-convention +; +; Each leaf stub loads a slot index into eax and jumps to CachedResolveAndDispatch. +; CachedResolveAndDispatch calls winrt_cached_resolve_thunk(rcx) which returns +; the resolved IFoo* (or nullptr). On success, replaces rcx with the resolved +; pointer and tail-jumps to vtable[slot]. On failure, returns E_NOINTERFACE. + +include ksamd64.inc + +extern winrt_cached_resolve_thunk:proc +extern __guard_check_icall_fptr:QWORD + +; ============================================================================ +; No-op IUnknown slots for thunk objects +; The thunk is not a real COM object; QI fails, AddRef/Release return 1. +; ============================================================================ + +LEAF_ENTRY winrt_cached_thunk_qi, _TEXT, NoPad + mov dword ptr [r8], 0 ; *ppv = nullptr + mov eax, 80004002h ; E_NOINTERFACE + ret +LEAF_END winrt_cached_thunk_qi, _TEXT + +LEAF_ENTRY winrt_cached_thunk_addref, _TEXT, NoPad + mov eax, 1 + ret +LEAF_END winrt_cached_thunk_addref, _TEXT + +LEAF_ENTRY winrt_cached_thunk_release, _TEXT, NoPad + mov eax, 1 + ret +LEAF_END winrt_cached_thunk_release, _TEXT + +; ============================================================================ +; CachedResolveAndDispatch +; +; Entry: eax = vtable slot index, rcx = interface_thunk* +; rdx, r8, r9 = caller's args (preserved across resolve) +; +; Calls winrt_cached_resolve_thunk(rcx) → rax = resolved IFoo* or nullptr. +; On success: rcx = rax, tail-jumps to [rcx]->vtable[slot]. +; On failure: returns E_NOINTERFACE (0x80004002). +; ============================================================================ + +NESTED_ENTRY CachedResolveAndDispatch, _TEXT + + ; Save caller's enregistered args and slot index + push r9 + push r8 + push rdx + push rax ; slot index + + END_PROLOGUE + + sub rsp, 4 * 8 ; shadow space for callee + + ; rcx = interface_thunk* (already in place from caller) + call winrt_cached_resolve_thunk + + add rsp, 4 * 8 ; remove shadow space + + pop r10 ; r10 = slot index + + test rax, rax + jz resolve_failed + + ; rax = resolved IFoo*. Load method ptr into rax for CFG validation. + mov rcx, rax ; rcx = new this (resolved IFoo*) + mov r11, [rax] ; r11 = resolved vtable + mov rax, [r11 + r10 * 8] ; rax = method at vtable[slot] + + ; Verify indirect call target (preserves all GPRs except rax/flags) + call [__guard_check_icall_fptr] + + ; Restore caller's args after CFG check + pop rdx + pop r8 + pop r9 + + rex_jmp_reg rax + +resolve_failed: + pop rdx ; balance the stack + pop r8 + pop r9 + mov eax, 80004002h ; E_NOINTERFACE + ret + +NESTED_END CachedResolveAndDispatch, _TEXT + +; ============================================================================ +; Leaf stub macro — each loads a slot index and jumps to the shared dispatcher +; ============================================================================ + +WINRT_CACHED_THUNK MACRO idx +LEAF_ENTRY winrt_cached_thunk_stub_&idx, _TEXT, NoPad + mov eax, idx + jmp CachedResolveAndDispatch +LEAF_END winrt_cached_thunk_stub_&idx, _TEXT +ENDM + +; Emit 256 stubs (slots 0-255) +counter = 0 +REPT 256 + WINRT_CACHED_THUNK %counter + counter = counter + 1 +ENDM + +; ============================================================================ +; Vtable array: slots 0-2 are no-op IUnknown, slots 3-255 are resolve stubs +; ============================================================================ + +vtable_entry MACRO idx + dq winrt_cached_thunk_stub_&idx +ENDM + +CONST segment + + public winrt_cached_thunk_vtable + winrt_cached_thunk_vtable label qword + + dq winrt_cached_thunk_qi + dq winrt_cached_thunk_addref + dq winrt_cached_thunk_release + + counter2 = 3 + REPT 253 + vtable_entry %counter2 + counter2 = counter2 + 1 + ENDM + +CONST ends + +END diff --git a/strings/cached_thunks_x86.asm b/strings/cached_thunks_x86.asm new file mode 100644 index 000000000..6231bdf43 --- /dev/null +++ b/strings/cached_thunks_x86.asm @@ -0,0 +1,122 @@ +; cached_thunks_x86.asm - x86 cached interface dispatch stubs +; +; Calling convention: https://docs.microsoft.com/en-us/cpp/cpp/stdcall +; +; x86 COM uses __stdcall: args on stack, callee cleans. Thunks tail-jump +; to the real method which does the stdcall cleanup. +; +; Each stub loads a slot index into eax and jumps to CachedResolveAndDispatch. + +.686 +.MODEL FLAT + + extrn _winrt_cached_resolve_thunk:proc + extrn ___guard_check_icall_fptr:DWORD + +.CODE + +; ============================================================================ +; No-op IUnknown slots for thunk objects +; The thunk is not a real COM object; QI fails, AddRef/Release return 1. +; ============================================================================ + +winrt_cached_thunk_qi PROC + mov eax, [esp+12] ; ppv (3rd arg) + mov dword ptr [eax], 0 ; *ppv = nullptr + mov eax, 80004002h ; E_NOINTERFACE + ret 12 ; stdcall: 3 args (this, riid, ppv) +winrt_cached_thunk_qi ENDP + +winrt_cached_thunk_addref PROC + mov eax, 1 + ret 4 ; stdcall: 1 arg (this) +winrt_cached_thunk_addref ENDP + +winrt_cached_thunk_release PROC + mov eax, 1 + ret 4 +winrt_cached_thunk_release ENDP + +; ============================================================================ +; CachedResolveAndDispatch +; +; Entry: eax = vtable slot index +; Stack: [esp]=ret_addr [esp+4]=this(thunk*) [esp+8]=arg1 ... +; +; Calls winrt_cached_resolve_thunk(this) → eax = resolved IFoo* or nullptr. +; On success: replaces this on stack, validates via CFG, tail-jumps to method. +; On failure: returns E_NOINTERFACE (0x80004002). +; ============================================================================ + + ALIGN 16 +CachedResolveAndDispatch PROC + push eax ; save slot index + + ; Call resolve: cdecl, pass thunk* as arg + push dword ptr [esp+8] ; push 'this' (thunk*) as arg + call _winrt_cached_resolve_thunk + add esp, 4 ; clean cdecl arg + + pop ecx ; ecx = slot index + + test eax, eax + jz resolve_failed + + mov [esp+4], eax ; replace 'this' on stack with resolved IFoo* + mov edx, [eax] ; edx = resolved vtable + mov eax, [edx + ecx*4] ; eax = method at vtable[slot] + + ; Verify indirect call target (CFG) + call [___guard_check_icall_fptr] + + ; Jump to method (stdcall: callee will clean stack including 'this') + jmp eax + +resolve_failed: + mov eax, 80004002h ; E_NOINTERFACE + ret +CachedResolveAndDispatch ENDP + +; ============================================================================ +; Leaf stub macro — each loads a slot index and jumps to the shared dispatcher +; ============================================================================ + +WINRT_CACHED_THUNK MACRO idx + ALIGN 2 + winrt_cached_thunk_stub_&idx& PROC + mov eax, idx + jmp CachedResolveAndDispatch + winrt_cached_thunk_stub_&idx& ENDP +ENDM + +; Emit 256 stubs (slots 0-255) +counter = 0 +REPT 256 + WINRT_CACHED_THUNK %counter + counter = counter + 1 +ENDM + +; ============================================================================ +; Vtable array: slots 0-2 are no-op IUnknown, slots 3-255 are resolve stubs +; ============================================================================ + +vtable_entry MACRO idx + dd winrt_cached_thunk_stub_&idx& +ENDM + +.CONST + +PUBLIC _winrt_cached_thunk_vtable +_winrt_cached_thunk_vtable LABEL DWORD + +dd winrt_cached_thunk_qi +dd winrt_cached_thunk_addref +dd winrt_cached_thunk_release + +counter2 = 3 +REPT 253 + vtable_entry %counter2 + counter2 = counter2 + 1 +ENDM + +END diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e5ca6e0fb..f52942c5a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -74,7 +74,7 @@ else() add_custom_command( OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/cppwinrt/winrt/base.h" - COMMAND cppwinrt -input local -output "${CPPWINRT_PROJECTION_INCLUDE_DIR}" -verbose + COMMAND cppwinrt -input local -output "${CPPWINRT_PROJECTION_INCLUDE_DIR}" -verbose -flatten_classes DEPENDS cppwinrt VERBATIM diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets new file mode 100644 index 000000000..5148f1067 --- /dev/null +++ b/test/Directory.Build.targets @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + true + + + + + + diff --git a/test/nuget/TestProxyStub/Directory.Build.targets b/test/nuget/TestProxyStub/Directory.Build.targets new file mode 100644 index 000000000..06dfaa64b --- /dev/null +++ b/test/nuget/TestProxyStub/Directory.Build.targets @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/nuget/TestRuntimeComponentCX/Directory.Build.targets b/test/nuget/TestRuntimeComponentCX/Directory.Build.targets new file mode 100644 index 000000000..08c6f9f36 --- /dev/null +++ b/test/nuget/TestRuntimeComponentCX/Directory.Build.targets @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/test/async_propagate_cancel.cpp b/test/test/async_propagate_cancel.cpp index 9e0ab8ee9..28141b75f 100644 --- a/test/test/async_propagate_cancel.cpp +++ b/test/test/async_propagate_cancel.cpp @@ -126,7 +126,7 @@ namespace async.Cancel(); // Wait indefinitely if a debugger is present, to make it easier to debug this test. - REQUIRE(WaitForSingleObject(completed.get(), IsDebuggerPresent() ? INFINITE : 1000) == WAIT_OBJECT_0); + REQUIRE(WaitForSingleObject(completed.get(), IsDebuggerPresent() ? INFINITE : 1000 * 30) == WAIT_OBJECT_0); REQUIRE(async.Status() == AsyncStatus::Canceled); REQUIRE(async.ErrorCode() == HRESULT_FROM_WIN32(ERROR_CANCELLED)); diff --git a/test/test/test.vcxproj b/test/test/test.vcxproj index 43928dab2..a6166e9fa 100644 --- a/test/test/test.vcxproj +++ b/test/test/test.vcxproj @@ -358,6 +358,7 @@ + diff --git a/test/test/thunked_dispatch.cpp b/test/test/thunked_dispatch.cpp new file mode 100644 index 000000000..8c2d71587 --- /dev/null +++ b/test/test/thunked_dispatch.cpp @@ -0,0 +1,290 @@ +#include "pch.h" +#include "catch.hpp" + +// Additional headers for thunked type testing (pch.h uses WINRT_LEAN_AND_MEAN). +#undef WINRT_LEAN_AND_MEAN +#include + +using namespace winrt; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; + +// These functions exercise thunked runtimeclass consumer-side dispatch. +// PropertySet inherits from thunked_runtimeclass. +// Calls to Insert/Lookup/Size go through the thunked cache slots rather than QI. +// +// Build x64 Release, then disassemble with: +// cdb -logo nul -z test.exe -c "uf test!thunked_propertyset_insert ; q" +// Verify: load cache slot -> load vtable -> call method, NO QueryInterface. + +// Prevent inlining so the function is visible in the disassembly. +__declspec(noinline) void thunked_propertyset_insert(PropertySet const& ps, hstring const& key, IInspectable const& value) +{ + ps.Insert(key, value); +} + +__declspec(noinline) IInspectable thunked_propertyset_lookup(PropertySet const& ps, hstring const& key) +{ + return ps.Lookup(key); +} + +__declspec(noinline) uint32_t thunked_propertyset_size(PropertySet const& ps) +{ + return ps.Size(); +} + +__declspec(noinline) bool thunked_propertyset_haskey(PropertySet const& ps, hstring const& key) +{ + return ps.HasKey(key); +} + +TEST_CASE("thunked_dispatch") +{ + PropertySet ps; + REQUIRE(ps); + + // Exercise IMap methods through the thunked runtimeclass. + ps.Insert(L"one", box_value(1)); + ps.Insert(L"two", box_value(2)); + ps.Insert(L"three", box_value(3)); + REQUIRE(ps.Size() == 3); + + auto val = ps.Lookup(L"two"); + REQUIRE(unbox_value(val) == 2); + + REQUIRE(ps.HasKey(L"one")); + REQUIRE(!ps.HasKey(L"four")); + + ps.Remove(L"one"); + REQUIRE(ps.Size() == 2); + + // Exercise via the noinline helpers (for disassembly). + thunked_propertyset_insert(ps, L"four", box_value(4)); + REQUIRE(thunked_propertyset_size(ps) == 3); + REQUIRE(unbox_value(thunked_propertyset_lookup(ps, L"four")) == 4); + REQUIRE(thunked_propertyset_haskey(ps, L"four")); + + ps.Clear(); + REQUIRE(ps.Size() == 0); +} + +TEST_CASE("thunked_copy_move") +{ + // Populate a PropertySet and resolve a cache slot via Insert. + PropertySet ps1; + ps1.Insert(L"key", box_value(42)); + REQUIRE(ps1.Size() == 1); + + // Copy construction: new object, independent lifetime. + PropertySet ps2 = ps1; + REQUIRE(ps2); + REQUIRE(ps2.Size() == 1); + REQUIRE(unbox_value(ps2.Lookup(L"key")) == 42); + + // Mutations on the copy are visible (same underlying COM object via AddRef). + ps2.Insert(L"other", box_value(99)); + REQUIRE(ps1.Size() == 2); // same COM object + + // Copy assignment. + PropertySet ps3; + ps3 = ps1; + REQUIRE(ps3); + REQUIRE(ps3.Size() == 2); + + // Move construction: source becomes null. + PropertySet ps4 = std::move(ps1); + REQUIRE(ps4); + REQUIRE(!ps1); + REQUIRE(ps4.Size() == 2); + + // Move assignment. + PropertySet ps5; + ps5.Insert(L"temp", box_value(0)); + ps5 = std::move(ps4); + REQUIRE(ps5); + REQUIRE(!ps4); + REQUIRE(ps5.Size() == 2); + + // Assign nullptr. + ps5 = nullptr; + REQUIRE(!ps5); +} + +TEST_CASE("thunked_abi_interop") +{ + PropertySet ps; + ps.Insert(L"x", box_value(1)); + + // get_abi returns the default interface pointer. + void* abi = get_abi(ps); + REQUIRE(abi != nullptr); + + // copy_to_abi / copy_from_abi round-trip. + void* copy = nullptr; + copy_to_abi(ps, copy); + REQUIRE(copy != nullptr); + + PropertySet ps2{ nullptr }; + copy_from_abi(ps2, copy); + static_cast<::IUnknown*>(copy)->Release(); // balance the AddRef from copy_to_abi + REQUIRE(ps2); + REQUIRE(ps2.Size() == 1); + + // detach_abi / attach_abi round-trip. + void* detached = detach_abi(ps); + REQUIRE(!ps); + REQUIRE(detached != nullptr); + + attach_abi(ps, detached); + REQUIRE(ps); + REQUIRE(ps.Size() == 1); + + // put_abi clears and returns slot address. + void** slot = put_abi(ps); + REQUIRE(slot != nullptr); + REQUIRE(!ps); // cleared by put_abi +} + +TEST_CASE("thunked_as_try_as") +{ + PropertySet ps; + ps.Insert(L"a", box_value(1)); + + // as for interfaces the object implements. + auto map = ps.as>(); + REQUIRE(map.Size() == 1); + + auto iterable = ps.as>>(); + REQUIRE(iterable); + + auto inspectable = ps.as(); + REQUIRE(inspectable); + + // try_as returns non-empty for implemented interfaces. + auto maybe_map = ps.try_as>(); + REQUIRE(maybe_map); + REQUIRE(maybe_map.Size() == 1); + + // try_as returns empty for non-implemented interfaces. + auto maybe_closable = ps.try_as(); + REQUIRE(!maybe_closable); + + // Implicit conversion to IInspectable. + IInspectable as_inspectable = ps; + REQUIRE(as_inspectable); + + // Implicit conversion to IUnknown. + Windows::Foundation::IUnknown as_unknown = ps; + REQUIRE(as_unknown); +} + +TEST_CASE("thunked_threading") +{ + // Create a PropertySet and hammer it from multiple threads to verify + // concurrent thunk resolution doesn't crash or leak. + PropertySet ps; + + static constexpr int thread_count = 8; + static constexpr int iterations = 100; + + std::vector threads; + std::atomic ready{ 0 }; + std::atomic errors{ 0 }; + + for (int t = 0; t < thread_count; ++t) + { + threads.emplace_back([&, t]() + { + ready++; + while (ready < thread_count) {} // spin until all threads are ready + + for (int i = 0; i < iterations; ++i) + { + try + { + // Each call through a secondary interface triggers thunk resolution + // on first use. Multiple threads racing to resolve should be safe. + auto key = std::to_wstring(t * iterations + i); + ps.Insert(hstring(key), box_value(i)); + (void)ps.HasKey(hstring(key)); + (void)ps.Size(); + } + catch (...) + { + errors++; + } + } + }); + } + + for (auto& th : threads) + { + th.join(); + } + + REQUIRE(errors == 0); + REQUIRE(ps.Size() > 0); +} + +TEST_CASE("thunked_generic_default") +{ + // StringMap has a generic default interface: IMap. + // Verifies thunking works when the default isn't a named interface. + Collections::StringMap sm; + REQUIRE(sm); + + sm.Insert(L"hello", L"world"); + sm.Insert(L"foo", L"bar"); + REQUIRE(sm.Size() == 2); + REQUIRE(sm.Lookup(L"hello") == L"world"); + REQUIRE(sm.HasKey(L"foo")); + + // IObservableMap is a secondary thunked interface. + auto observable = sm.as>(); + REQUIRE(observable); + + // IIterable is another secondary. + auto iterable = sm.as>>(); + REQUIRE(iterable); + + int count = 0; + for (auto&& kv : sm) + { + (void)kv.Key(); + (void)kv.Value(); + count++; + } + REQUIRE(count == 2); + + sm.Clear(); + REQUIRE(sm.Size() == 0); +} + +TEST_CASE("thunked_full_mode") +{ + // Windows.ApplicationModel.Package has 9 secondary interfaces (>8), + // which triggers full mode (cache_and_thunk_full with explicit IID storage + // instead of tagged payload). We can't activate Package from a console app, + // but we can verify the type compiles, constructs as null, and exercises + // the full-mode template path. + using Windows::ApplicationModel::Package; + + // Null construction. + Package pkg{ nullptr }; + REQUIRE(!pkg); + + // Copy/move of null. + Package pkg2 = pkg; + REQUIRE(!pkg2); + + Package pkg3 = std::move(pkg); + REQUIRE(!pkg3); + REQUIRE(!pkg); + + // Verify the type's thunked_interfaces tuple has >8 entries, + // confirming full mode is active. + static_assert(std::tuple_size_v > 8, + "Package should have >8 thunked interfaces (full mode)"); + static_assert(!Package::use_tagged, + "Package should use full mode, not tagged mode"); +} diff --git a/test/test_component/test_component.vcxproj b/test/test_component/test_component.vcxproj index 3ffdb8f97..acbc33137 100644 --- a/test/test_component/test_component.vcxproj +++ b/test/test_component/test_component.vcxproj @@ -377,7 +377,7 @@ - $(CppWinRTDir)cppwinrt -in local -out $(OutputPath) -verbose + $(CppWinRTDir)cppwinrt -in local -out $(OutputPath) -verbose -flatten_classes $(CppWinRTDir)cppwinrt -input $(OutputPath)test_component.winmd -comp "$(ProjectDir)Generated Files" -out "$(ProjectDir)Generated Files" -include test_component -ref sdk -verbose -prefix -opt -lib test -fastabi -overwrite -name test_component Projecting Windows and component metadata into $(OutputPath)