Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow visualization of properties on generic types #1472

Merged
merged 16 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 125 additions & 6 deletions natvis/cppwinrt_visualizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,39 @@ using namespace std::filesystem;
using namespace winrt;
using namespace winmd::reader;

std::vector<std::string> db_files;
std::unique_ptr<cache> db_cache;
namespace
{
std::vector<std::string> db_files;
std::unique_ptr<cache> db_cache;
coded_index<TypeDefOrRef> guid_TypeRef{};
}

coded_index<TypeDefOrRef> FindGuidType()
{
if (!guid_TypeRef)
{
// There is no definitive TypeDef for System.Guid. But there are a variety of TypeRefs scattered about
// This one should be relatively quick to find
auto pv = db_cache->find("Windows.Foundation", "IPropertyValue");
for (auto&& method : pv.MethodList())
{
if (method.Name() == "GetGuid")
{
auto const& sig = method.Signature();
auto const& type = sig.ReturnType().Type().Type();
XLANG_ASSERT(std::holds_alternative<coded_index<TypeDefOrRef>>(type));
if (std::holds_alternative<coded_index<TypeDefOrRef>>(type))
{
guid_TypeRef = std::get<coded_index<TypeDefOrRef>>(type);
XLANG_ASSERT(guid_TypeRef.type() == TypeDefOrRef::TypeRef);
XLANG_ASSERT(guid_TypeRef.TypeRef().TypeNamespace() == "System");
XLANG_ASSERT(guid_TypeRef.TypeRef().TypeName() == "Guid");
}
}
}
}
return guid_TypeRef;
}

void MetadataDiagnostic(DkmProcess* process, std::wstring const& status, std::filesystem::path const& path)
{
Expand Down Expand Up @@ -118,8 +149,9 @@ void LoadMetadata(DkmProcess* process, WCHAR const* processPath, std::string_vie
}
}

TypeDef FindType(DkmProcess* process, std::string_view const& typeName)
TypeDef FindSimpleType(DkmProcess* process, std::string_view const& typeName)
{
XLANG_ASSERT(typeName.find('<') == std::string_view::npos);
auto type = db_cache->find(typeName);
if (!type)
{
Expand All @@ -135,19 +167,104 @@ TypeDef FindType(DkmProcess* process, std::string_view const& typeName)
return type;
}

TypeDef FindType(DkmProcess* process, std::string_view const& typeNamespace, std::string_view const& typeName)
TypeDef FindSimpleType(DkmProcess* process, std::string_view const& typeNamespace, std::string_view const& typeName)
{
XLANG_ASSERT(typeName.find('<') == std::string_view::npos);
auto type = db_cache->find(typeNamespace, typeName);
if (!type)
{
std::string fullName(typeNamespace);
fullName.append(".");
fullName.append(typeName);
FindType(process, fullName);
FindSimpleType(process, fullName);
}
return type;
}

std::vector<std::string> ParseTypeName(std::string_view name)
{
DWORD count;
HSTRING* parts;
auto wide_name = winrt::to_hstring(name);
winrt::check_hresult(::RoParseTypeName(static_cast<HSTRING>(get_abi(wide_name)), &count, &parts));

winrt::com_array<winrt::hstring> wide_parts{ parts, count, winrt::take_ownership_from_abi };
std::vector<std::string> result;
for (auto&& part : wide_parts)
{
result.push_back(winrt::to_string(part));
}
return result;
}

template <std::input_iterator iter, std::sentinel_for<iter> sent>
TypeSig ResolveGenericTypePart(DkmProcess* process, iter& it, sent const& end)
{
constexpr std::pair<std::string_view, ElementType> elementNames[] = {
{"Boolean", ElementType::Boolean},
{"Int8", ElementType::I1},
{"Int16", ElementType::I2},
{"Int32", ElementType::I4},
{"Int64", ElementType::I8},
{"UInt8", ElementType::U1},
{"UInt16", ElementType::U2},
{"UInt32", ElementType::U4},
{"UInt64", ElementType::U8},
{"Single", ElementType::R4},
{"Double", ElementType::R8},
{"String", ElementType::String},
{"Char16", ElementType::Char},
{"Object", ElementType::Object}
};
std::string_view partName = *it;
auto basic_type_pos = std::find_if(std::begin(elementNames), std::end(elementNames), [&partName](auto&& elem) { return elem.first == partName; });
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this need to be case insensitive?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a good question, and I had to really think about this answer. From everything I've learned about WinRT over the years, the exact casing of the string returned by GetRuntimeClassName is not prescribed by the type system, just that "In particular, IInspectable.GetRuntimeClassName enables an object's client to retrieve a WinRT typename that can be resolved in metadata to enable language projection." (source https://learn.microsoft.com/en-us/uwp/winrt-cref/winrt-type-system)

That same WinRT type system doc linked above also calls out the names of these fundamental types, so these exact strings are standard, with the exception of Object. What isn't strictly specified in the standard is that an implementation's internal type for a generic must return a particular string from GetRuntimeClassName.

With a concrete type that implements generic interfaces, like Windows.Foundation.Collections.ValueSet, it only needs to return that string. After looking up that string in the winmd, the relationship of the runtime class to the various generic interfaces it implements is all captured in the ECMA metadata.

But with these implementation-supplied objects, the class name is more of a convention, rather than a dictate of the type system, as far as I can tell. But what I do know is that I checked 3 of the big implementations that actually generate matching pinterface GUIDs and runtime class names, and they all use these same names. So, I think we're good here.

Copy link
Member Author

@DefaultRyan DefaultRyan Feb 1, 2025

Choose a reason for hiding this comment

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

I did just notice that I need to validate Char16 and Guid. I'm also going to ping the CsWinRT owners to verify they also return consistent strings for these cases.

if (basic_type_pos != std::end(elementNames))
{
return TypeSig{ basic_type_pos->second };
}

if (partName == "Guid")
{
return TypeSig{ FindGuidType() };
}

TypeDef type = FindSimpleType(process, partName);
auto tickPos = partName.rfind('`');
if (tickPos == partName.npos)
{
return TypeSig{ type.coded_index<TypeDefOrRef>() };
}

int paramCount = 0;
std::from_chars(partName.data() + tickPos + 1, partName.data() + partName.size(), paramCount);
std::vector<TypeSig> genericArgs;
for (int i = 0; i < paramCount; ++i)
{
genericArgs.push_back(ResolveGenericTypePart(process, ++it, end));
}
return TypeSig{ GenericTypeInstSig{ type.coded_index<TypeDefOrRef>(), std::move(genericArgs) } };
}

TypeSig ResolveGenericType(DkmProcess* process, std::string_view genericName)
{
auto parts = ParseTypeName(genericName);
auto begin = parts.begin();
return ResolveGenericTypePart(process, begin, parts.end());
}

TypeSig FindType(DkmProcess* process, std::string_view const& typeName)
{
auto paramIndex = typeName.find('<');
if (paramIndex == std::string_view::npos)
{
return TypeSig{ FindSimpleType(process, typeName).coded_index<TypeDefOrRef>() };
}
else
{
return ResolveGenericType(process, typeName);
}
}

cppwinrt_visualizer::cppwinrt_visualizer()
{
try
Expand Down Expand Up @@ -187,13 +304,14 @@ cppwinrt_visualizer::cppwinrt_visualizer()
cppwinrt_visualizer::~cppwinrt_visualizer()
{
ClearTypeResolver();
guid_TypeRef = {};
db_files.clear();
db_cache.reset();
}

HRESULT cppwinrt_visualizer::EvaluateVisualizedExpression(
_In_ DkmVisualizedExpression* pVisualizedExpression,
_Deref_out_ DkmEvaluationResult** ppResultObject
_COM_Outptr_result_maybenull_ DkmEvaluationResult** ppResultObject
)
{
try
Expand Down Expand Up @@ -233,6 +351,7 @@ HRESULT cppwinrt_visualizer::EvaluateVisualizedExpression(
// unrecognized type
NatvisDiagnostic(pVisualizedExpression,
std::wstring(L"Unrecognized type: ") + (LPWSTR)bstrTypeName, NatvisDiagnosticLevel::Error);
*ppResultObject = nullptr;
return S_OK;
}

Expand Down
2 changes: 1 addition & 1 deletion natvis/cppwinrt_visualizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct cppwinrt_visualizer : winrt::implements<cppwinrt_visualizer,

STDMETHOD(EvaluateVisualizedExpression)(
_In_ Microsoft::VisualStudio::Debugger::Evaluation::DkmVisualizedExpression* pVisualizedExpression,
_Deref_out_ Microsoft::VisualStudio::Debugger::Evaluation::DkmEvaluationResult** ppResultObject
_COM_Outptr_result_maybenull_ Microsoft::VisualStudio::Debugger::Evaluation::DkmEvaluationResult** ppResultObject
);
STDMETHOD(UseDefaultEvaluationBehavior)(
_In_ Microsoft::VisualStudio::Debugger::Evaluation::DkmVisualizedExpression* pVisualizedExpression,
Expand Down
4 changes: 2 additions & 2 deletions natvis/cppwinrtvisualizer.vcxproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="packages\Microsoft.Windows.WinMD.1.0.210629.2\build\native\Microsoft.Windows.WinMD.props" Condition="Exists('packages\Microsoft.Windows.WinMD.1.0.210629.2\build\native\Microsoft.Windows.WinMD.props')" />
<Import Project="packages\Microsoft.Windows.WinMD.1.0.250131.1\build\native\Microsoft.Windows.WinMD.props" Condition="Exists('packages\Microsoft.Windows.WinMD.1.0.250131.1\build\native\Microsoft.Windows.WinMD.props')" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
Expand Down Expand Up @@ -321,6 +321,6 @@
</PropertyGroup>
<Error Condition="!Exists('packages\Microsoft.VSSDK.Debugger.VSDConfigTool.16.0.2012201-preview\build\Microsoft.VSSDK.Debugger.VSDConfigTool.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.VSSDK.Debugger.VSDConfigTool.16.0.2012201-preview\build\Microsoft.VSSDK.Debugger.VSDConfigTool.targets'))" />
<Error Condition="!Exists('packages\Microsoft.VSSDK.Debugger.VSDebugEng.16.0.2012201-preview\Microsoft.VSSDK.Debugger.VSDebugEng.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.VSSDK.Debugger.VSDebugEng.16.0.2012201-preview\Microsoft.VSSDK.Debugger.VSDebugEng.targets'))" />
<Error Condition="!Exists('packages\Microsoft.Windows.WinMD.1.0.210629.2\build\native\Microsoft.Windows.WinMD.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Windows.WinMD.1.0.210629.2\build\native\Microsoft.Windows.WinMD.props'))" />
<Error Condition="!Exists('packages\Microsoft.Windows.WinMD.1.0.250131.1\build\native\Microsoft.Windows.WinMD.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Windows.WinMD.1.0.250131.1\build\native\Microsoft.Windows.WinMD.props'))" />
</Target>
</Project>
Loading