diff --git a/cppwinrt/helpers.h b/cppwinrt/helpers.h index 22854c48e..b2d783d61 100644 --- a/cppwinrt/helpers.h +++ b/cppwinrt/helpers.h @@ -80,7 +80,7 @@ namespace cppwinrt // by the caller and callee. The exception to this rule is property setters where the callee may simply store a // reference to the collection. The collection thus becomes async in the sense that it is expected to remain // valid beyond the duration of the call. - + if (is_put_overload(m_method)) { return true; @@ -148,10 +148,54 @@ namespace cppwinrt return static_cast(get_attribute(row, type_namespace, type_name)); } + namespace impl + { + template + struct variant_index; + + template + struct variant_index + { + static constexpr bool found = std::is_same_v; + static constexpr std::size_t value = std::conditional_t, + variant_index>::value + (found ? 0 : 1); + }; + } + + template + struct variant_index; + + template + struct variant_index, T> : impl::variant_index + { + }; + + template + constexpr std::size_t variant_index_v = variant_index::value; + + template + auto get_integer_attribute(FixedArgSig const& signature) + { + auto variant = std::get(signature.value).value; + switch (variant.index()) + { + case variant_index_v>: return static_cast(std::get>(variant)); + case variant_index_v>: return static_cast(std::get>(variant)); + default: return std::get(variant); // Likely throws, but that's intentional + } + } + + template + auto get_attribute_value(FixedArgSig const& signature) + { + return std::get(std::get(signature.value).value); + } + template auto get_attribute_value(CustomAttribute const& attribute, uint32_t const arg) { - return std::get(std::get(attribute.Value().FixedArgs()[arg].value).value); + return get_attribute_value(attribute.Value().FixedArgs()[arg]); } static auto get_abi_name(MethodDef const& method) @@ -283,33 +327,200 @@ namespace cppwinrt return bases; } - static std::pair get_version(TypeDef const& type) + struct contract_version + { + std::string_view name; + uint32_t version; + }; + + struct previous_contract + { + std::string_view contract_from; + std::string_view contract_to; + uint32_t version_low; + uint32_t version_high; + }; + + struct contract_history + { + contract_version current_contract; + + // Sorted such that the first entry is the first contract the type was introduced in + std::vector previous_contracts; + }; + + static contract_version decode_contract_version_attribute(CustomAttribute const& attribute) + { + // ContractVersionAttribute has three constructors, but only two we care about here: + // .ctor(string contract, uint32 version) + // .ctor(System.Type contract, uint32 version) + auto signature = attribute.Value(); + auto& args = signature.FixedArgs(); + assert(args.size() == 2); + + contract_version result{}; + result.version = get_integer_attribute(args[1]); + call(std::get(args[0].value).value, + [&](ElemSig::SystemType t) + { + result.name = t.name; + }, + [&](std::string_view name) + { + result.name = name; + }, + [](auto&&) + { + assert(false); + }); + + return result; + } + + static previous_contract decode_previous_contract_attribute(CustomAttribute const& attribute) { - uint32_t version{}; + // PreviousContractVersionAttribute has two constructors: + // .ctor(string fromContract, uint32 versionLow, uint32 versionHigh) + // .ctor(string fromContract, uint32 versionLow, uint32 versionHigh, string contractTo) + auto signature = attribute.Value(); + auto& args = signature.FixedArgs(); + assert(args.size() >= 3); + + previous_contract result{}; + result.contract_from = get_attribute_value(args[0]); + result.version_low = get_integer_attribute(args[1]); + result.version_high = get_integer_attribute(args[2]); + if (args.size() == 4) + { + result.contract_to = get_attribute_value(args[3]); + } + + return result; + } + static contract_version get_initial_contract_version(TypeDef const& type) + { + // Most types don't have previous contracts, so optimize for that scenario to avoid unnecessary allocations + contract_version current_contract{}; + + // The initial contract, assuming the type has moved contracts, is the only contract name that doesn't appear as + // a "to contract" argument to a PreviousContractVersionAttribute. Note that this assumes that a type does not + // "return" to a prior contract, however this is a restriction enforced by midlrt + std::vector previous_contracts; + std::vector to_contracts; for (auto&& attribute : type.CustomAttribute()) { - auto name = attribute.TypeNamespaceAndName(); + auto [ns, name] = attribute.TypeNamespaceAndName(); + if (ns != "Windows.Foundation.Metadata") + { + continue; + } - if (name.first != "Windows.Foundation.Metadata") + if (name == "ContractVersionAttribute") + { + assert(current_contract.name.empty()); + current_contract = decode_contract_version_attribute(attribute); + } + else if (name == "PreviousContractVersionAttribute") + { + auto prev = decode_previous_contract_attribute(attribute); + + // If this contract was the target of an earlier contract change, we know this isn't the initial one + if (std::find(to_contracts.begin(), to_contracts.end(), prev.contract_from) == to_contracts.end()) + { + previous_contracts.push_back(contract_version{ prev.contract_from, prev.version_low }); + } + + if (!prev.contract_to.empty()) + { + auto itr = std::find_if(previous_contracts.begin(), previous_contracts.end(), [&](auto const& ver) + { + return ver.name == prev.contract_to; + }); + if (itr != previous_contracts.end()) + { + *itr = previous_contracts.back(); + previous_contracts.pop_back(); + } + + to_contracts.push_back(prev.contract_to); + } + } + else if (name == "VersionAttribute") + { + // Prefer contract versioning, if present. Otherwise, use an empty contract name to indicate that this + // is not a contract version + if (current_contract.name.empty()) + { + current_contract.version = get_attribute_value(attribute, 0); + } + } + } + + if (!previous_contracts.empty()) + { + assert(previous_contracts.size() == 1); + return previous_contracts[0]; + } + + return current_contract; + } + + static contract_history get_contract_history(TypeDef const& type) + { + contract_history result{}; + for (auto&& attribute : type.CustomAttribute()) + { + auto [ns, name] = attribute.TypeNamespaceAndName(); + if (ns != "Windows.Foundation.Metadata") { continue; } - if (name.second == "ContractVersionAttribute") + if (name == "ContractVersionAttribute") { - version = get_attribute_value(attribute, 1); - break; + assert(result.current_contract.name.empty()); + result.current_contract = decode_contract_version_attribute(attribute); } + else if (name == "PreviousContractVersionAttribute") + { + result.previous_contracts.push_back(decode_previous_contract_attribute(attribute)); + } + // We could report the version that the type was introduced if the type is not contract versioned, however + // that information is not useful to us anywhere, so just skip it + } + + if (result.previous_contracts.empty()) + { + return result; + } + assert(!result.current_contract.name.empty()); - if (name.second == "VersionAttribute") + // There's no guarantee that the contract history will be sorted in metadata (in fact it's unlikely to be) + for (auto& prev : result.previous_contracts) + { + if (prev.contract_to.empty() || (prev.contract_to == result.current_contract.name)) { - version = get_attribute_value(attribute, 0); + // No 'to' contract indicates that this was the last contract before the current one + prev.contract_to = result.current_contract.name; + std::swap(prev, result.previous_contracts.back()); break; } } + assert(result.previous_contracts.back().contract_to == result.current_contract.name); + + for (size_t size = result.previous_contracts.size() - 1; size; --size) + { + auto& last = result.previous_contracts[size]; + auto itr = std::find_if(result.previous_contracts.begin(), result.previous_contracts.begin() + size, [&](auto const& prev) + { + return prev.contract_to == last.contract_from; + }); + assert(itr != result.previous_contracts.end()); + std::swap(*itr, result.previous_contracts[size - 1]); + } - return { HIWORD(version), LOWORD(version) }; + return result; } struct interface_info @@ -321,7 +532,11 @@ namespace cppwinrt bool base{}; bool exclusive{}; bool fastabi{}; - std::pair version{}; + // A pair of (relativeContract, version) where 'relativeContract' is the contract the interface was introduced + // in relative to the contract history of the class. E.g. if a class goes from contract 'A' to 'B' to 'C', + // 'relativeContract' would be '0' for an interface introduced in contract 'A', '1' for an interface introduced + // in contract 'B', etc. This is only set/valid for 'fastabi' interfaces + std::pair relative_version{}; std::vector> generic_param_stack{}; }; @@ -421,7 +636,6 @@ namespace cppwinrt } info.exclusive = has_attribute(info.type, "Windows.Foundation.Metadata", "ExclusiveToAttribute"); - info.version = get_version(info.type); get_interfaces_impl(w, result, info.defaulted, info.overridable, base, info.generic_param_stack, info.type.InterfaceImpl()); insert_or_assign(result, name, std::move(info)); } @@ -443,10 +657,32 @@ namespace cppwinrt return result; } - auto count = std::count_if(result.begin(), result.end(), [](auto&& pair) + auto history = get_contract_history(type); + size_t count = 0; + for (auto& pair : result) { - return pair.second.exclusive && !pair.second.base && !pair.second.overridable; - }); + if (pair.second.exclusive && !pair.second.base && !pair.second.overridable) + { + ++count; + + auto introduced = get_initial_contract_version(pair.second.type); + pair.second.relative_version.second = introduced.version; + + auto itr = std::find_if(history.previous_contracts.begin(), history.previous_contracts.end(), [&](previous_contract const& prev) + { + return prev.contract_from == introduced.name; + }); + if (itr != history.previous_contracts.end()) + { + pair.second.relative_version.first = static_cast(itr - history.previous_contracts.begin()); + } + else + { + assert(history.current_contract.name == introduced.name); + pair.second.relative_version.first = static_cast(history.previous_contracts.size()); + } + } + } std::partial_sort(result.begin(), result.begin() + count, result.end(), [](auto&& left_pair, auto&& right_pair) { @@ -482,9 +718,9 @@ namespace cppwinrt return left_enabled; } - if (left.version != right.version) + if (left.relative_version != right.relative_version) { - return left.version < right.version; + return left.relative_version < right.relative_version; } return left_pair.first < right_pair.first; diff --git a/test/test_component_fast/Nomadic.cpp b/test/test_component_fast/Nomadic.cpp new file mode 100644 index 000000000..e2d5c8af5 --- /dev/null +++ b/test/test_component_fast/Nomadic.cpp @@ -0,0 +1,35 @@ +#include "pch.h" +#include "Nomadic.h" +#include "Nomadic.g.cpp" + +namespace winrt::test_component_fast::implementation +{ + hstring Nomadic::FirstMethod() + { + return L"FirstMethod"; + } + hstring Nomadic::SecondMethod() + { + return L"SecondMethod"; + } + hstring Nomadic::ThirdMethod() + { + return L"ThirdMethod"; + } + hstring Nomadic::FourthMethod() + { + return L"FourthMethod"; + } + hstring Nomadic::FifthMethod() + { + return L"FifthMethod"; + } + hstring Nomadic::SixthMethod() + { + return L"SixthMethod"; + } + hstring Nomadic::SeventhMethod() + { + return L"SeventhMethod"; + } +} diff --git a/test/test_component_fast/Nomadic.h b/test/test_component_fast/Nomadic.h new file mode 100644 index 000000000..65448b967 --- /dev/null +++ b/test/test_component_fast/Nomadic.h @@ -0,0 +1,24 @@ +#pragma once +#include "Nomadic.g.h" + +namespace winrt::test_component_fast::implementation +{ + struct Nomadic : NomadicT + { + Nomadic() = default; + + hstring FirstMethod(); + hstring SecondMethod(); + hstring ThirdMethod(); + hstring FourthMethod(); + hstring FifthMethod(); + hstring SixthMethod(); + hstring SeventhMethod(); + }; +} +namespace winrt::test_component_fast::factory_implementation +{ + struct Nomadic : NomadicT + { + }; +} diff --git a/test/test_component_fast/test_component_fast.idl b/test/test_component_fast/test_component_fast.idl index ad7f89ce4..915eec49d 100644 --- a/test/test_component_fast/test_component_fast.idl +++ b/test/test_component_fast/test_component_fast.idl @@ -42,6 +42,80 @@ namespace test_component_fast } } + [contractversion(10)] + apicontract FirstContract{}; + + [contractversion(10)] + apicontract SecondContract{}; + + [contractversion(10)] + apicontract ThirdContract{}; + + [contractversion(10)] + apicontract FourthContract{}; + + [fastabi2(ThirdContract, 3)] + [from_contract(FirstContract, range(1, 10), SecondContract)] + [from_contract(SecondContract, range(1, 10), ThirdContract)] + [from_contract(ThirdContract, range(1, 10))] + [contract(FourthContract, 1)] + runtimeclass Nomadic + { + Nomadic(); + + /* NOTE: There seems to be a bug in midlrt where 'INomadicSeventh' and 'INomadicEighth' are both versioned to + 1.0 (when the class was added to the contract) in metadata. The interface impl list has the correct + contract versions, however this trips us up since we only look at the version of the interface + [interface_name("INomadicEighth")] + [contract(FourthContract, 2.1)] + { + String EighthMethod(); + } + */ + + [interface_name("INomadicSeventh")] + [contract(FourthContract, 1.2)] + { + String SeventhMethod(); + } + + [interface_name("INomadicSixth")] + [contract(ThirdContract, 4.3)] + { + String SixthMethod(); + } + + [interface_name("INomadicFifth")] + [contract(ThirdContract, 3.4)] + { + String FifthMethod(); + } + + [interface_name("INomadicFourth")] + [contract(SecondContract, 6.5)] + { + String FourthMethod(); + } + + [interface_name("INomadicThird")] + [contract(SecondContract, 5.6)] + { + String ThirdMethod(); + } + + [interface_name("INomadicSecond")] + [contract(FirstContract, 8.7)] + { + String SecondMethod(); + } + + [interface_name("INomadicFirst")] + [contract(FirstContract, 7.8)] + { + String FirstMethod(); + } + } + namespace Composition { [contractversion(4)] diff --git a/test/test_component_fast/test_component_fast.vcxproj b/test/test_component_fast/test_component_fast.vcxproj index 9f42be8e3..8cc4739b1 100644 --- a/test/test_component_fast/test_component_fast.vcxproj +++ b/test/test_component_fast/test_component_fast.vcxproj @@ -617,6 +617,7 @@ + @@ -628,6 +629,7 @@ + diff --git a/test/test_fast/Nomadic.cpp b/test/test_fast/Nomadic.cpp new file mode 100644 index 000000000..98753c06d --- /dev/null +++ b/test/test_fast/Nomadic.cpp @@ -0,0 +1,36 @@ +#include "pch.h" +#include "winrt/test_component_fast.h" +#include + +using namespace winrt; +using namespace test_component_fast; + +hstring invoke_by_interface_vtable_offset(Nomadic const& nomadic, ptrdiff_t offset) +{ + // NOTE: Behavior guaranteed by Windows ABI; see the "C style interface" for WinRT/COM types for more info. Note + // that IInspectable has 6 functions in total (including those inherited from IUnknown) + auto insp = static_cast<::IInspectable*>(get_abi(nomadic)); + auto vtable = *reinterpret_cast(insp); + auto fn_ptr = static_cast(vtable[6 + offset]); + + HSTRING hstr; + check_hresult(fn_ptr(insp, &hstr)); + + hstring result; + attach_abi(result, hstr); + return result; +} + +TEST_CASE("Nomadic") +{ + impl::get_diagnostics_info().detach(); + + Nomadic n; + REQUIRE(invoke_by_interface_vtable_offset(n, 0) == L"FirstMethod"); + REQUIRE(invoke_by_interface_vtable_offset(n, 1) == L"SecondMethod"); + REQUIRE(invoke_by_interface_vtable_offset(n, 2) == L"ThirdMethod"); + REQUIRE(invoke_by_interface_vtable_offset(n, 3) == L"FourthMethod"); + REQUIRE(invoke_by_interface_vtable_offset(n, 4) == L"FifthMethod"); + REQUIRE(invoke_by_interface_vtable_offset(n, 5) == L"SixthMethod"); + REQUIRE(invoke_by_interface_vtable_offset(n, 6) == L"SeventhMethod"); +} diff --git a/test/test_fast/test_fast.vcxproj b/test/test_fast/test_fast.vcxproj index dbf3d9b8b..c256cad6b 100644 --- a/test/test_fast/test_fast.vcxproj +++ b/test/test_fast/test_fast.vcxproj @@ -283,6 +283,7 @@ NotUsing + Create