Skip to content
Open
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
161 changes: 156 additions & 5 deletions src/debugger/variables.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,155 @@ struct VariableMember
VariableMember(const VariableMember &that) = delete;
};

static HRESULT FillValueAndType(VariableMember &member, Variable &var)
static bool IsTypeDisplayFallback(const std::string &value, const std::string &typeName)
{
return value.size() == typeName.size() + 2 &&
value.front() == '{' &&
value.back() == '}' &&
value.compare(1, typeName.size(), typeName) == 0;
}

static bool IsZeroArgStringToString(IMetaDataImport *pMD, mdTypeDef typeDef, mdMethodDef methodDef)
{
mdTypeDef memTypeDef;
ULONG nameLen;
WCHAR methodName[mdNameLen] = {0};
DWORD methodAttr = 0;
PCCOR_SIGNATURE pSig = nullptr;
ULONG cbSig = 0;
if (FAILED(pMD->GetMethodProps(methodDef, &memTypeDef,
methodName, _countof(methodName), &nameLen,
&methodAttr, &pSig, &cbSig, nullptr, nullptr)))
return false;

if (memTypeDef != typeDef || (methodAttr & mdStatic) || !(methodAttr & mdVirtual) ||
(methodAttr & mdNewSlot) || !str_equal(methodName, W("ToString")))
return false;

ULONG convFlags = 0;
ULONG elementSize = CorSigUncompressData(pSig, &convFlags);
pSig += elementSize;

if ((convFlags & IMAGE_CEE_CS_CALLCONV_MASK) == IMAGE_CEE_CS_CALLCONV_VARARG)
return false;

if (convFlags & IMAGE_CEE_CS_CALLCONV_GENERIC)
{
ULONG gParams = 0;
elementSize = CorSigUncompressData(pSig, &gParams);
pSig += elementSize;
}

ULONG cParams = 0;
elementSize = CorSigUncompressData(pSig, &cParams);
pSig += elementSize;

CorElementType returnType = CorSigUncompressElementType(pSig);
return cParams == 0 && returnType == ELEMENT_TYPE_STRING;
}

static bool TypeDeclaresToStringOverride(IMetaDataImport *pMD, mdTypeDef typeDef)
{
ULONG numMethods = 0;
HCORENUM hEnum = nullptr;
mdMethodDef methodDef = mdMethodDefNil;
while (SUCCEEDED(pMD->EnumMethodsWithName(&hEnum, typeDef, W("ToString"), &methodDef, 1, &numMethods)) &&
numMethods != 0)
{
if (IsZeroArgStringToString(pMD, typeDef, methodDef))
{
pMD->CloseEnum(hEnum);
return true;
}
}
pMD->CloseEnum(hEnum);
return false;
}

static bool HasOverriddenToString(ICorDebugValue *pInputValue)
{
BOOL isNull = TRUE;
ToRelease<ICorDebugValue> pValue;
if (FAILED(DereferenceAndUnboxValue(pInputValue, &pValue, &isNull)))
return false;
if (isNull)
return false;

ToRelease<ICorDebugValue2> pValue2;
ToRelease<ICorDebugType> pType;
if (FAILED(pValue->QueryInterface(IID_ICorDebugValue2, (LPVOID*) &pValue2)) ||
FAILED(pValue2->GetExactType(&pType)))
return false;

while (pType)
{
std::string typeName;
if (FAILED(TypePrinter::GetTypeOfValue(pType, typeName)))
return false;
// Exception.ToString() includes stack traces; keep existing exception variable display.
if (typeName == "System.Object" || typeName == "System.ValueType" || typeName == "System.Exception")
return false;

ToRelease<ICorDebugClass> pClass;
mdTypeDef typeDef = mdTypeDefNil;
ToRelease<ICorDebugModule> pModule;
ToRelease<IUnknown> pMDUnknown;
ToRelease<IMetaDataImport> pMD;
if (FAILED(pType->GetClass(&pClass)) ||
FAILED(pClass->GetToken(&typeDef)) ||
FAILED(pClass->GetModule(&pModule)) ||
FAILED(pModule->GetMetaDataInterface(IID_IMetaDataImport, &pMDUnknown)) ||
FAILED(pMDUnknown->QueryInterface(IID_IMetaDataImport, (LPVOID*) &pMD)))
{
return false;
}

if (TypeDeclaresToStringOverride(pMD, typeDef))
return true;

ToRelease<ICorDebugType> pBaseType;
if (FAILED(pType->GetBase(&pBaseType)) || pBaseType == nullptr)
return false;
pType = pBaseType.Detach();
}

return false;
}

static HRESULT PrintValueWithImplicitToString(ICorDebugValue *pValue, ICorDebugThread *pThread, FrameLevel frameLevel,
int evalFlags, const std::string &receiverExpression,
EvalStackMachine *pEvalStackMachine, std::string &output)
{
HRESULT Status;

IfFailRet(PrintValue(pValue, output, true));

if (!pThread || receiverExpression.empty() || !pEvalStackMachine || (evalFlags & EVAL_NOFUNCEVAL))
return S_OK;

std::string typeName;
if (FAILED(TypePrinter::GetTypeOfValue(pValue, typeName)))
return S_OK;
if (!IsTypeDisplayFallback(output, typeName) || !HasOverriddenToString(pValue))
return S_OK;

ToRelease<ICorDebugValue> pToStringValue;
std::string evalOutput;
if (SUCCEEDED(pEvalStackMachine->EvaluateExpression(pThread, frameLevel, evalFlags,
receiverExpression + ".ToString()",
&pToStringValue, evalOutput)) &&
pToStringValue != nullptr)
{
std::string display;
if (SUCCEEDED(PrintValue(pToStringValue, display, false)))
output = display;
}

return S_OK;
}

static HRESULT FillValueAndType(VariableMember &member, Variable &var, ICorDebugThread *pThread, FrameLevel frameLevel,
EvalStackMachine *pEvalStackMachine)
{
if (member.value == nullptr)
{
Expand All @@ -87,7 +235,8 @@ static HRESULT FillValueAndType(VariableMember &member, Variable &var)
}

TypePrinter::GetTypeOfValue(member.value, var.type);
return PrintValue(member.value, var.value, true);
return PrintValueWithImplicitToString(member.value, pThread, frameLevel, var.evalFlags, var.evaluateName,
pEvalStackMachine, var.value);
}

static HRESULT FetchFieldsAndProperties(Evaluator *pEvaluator, ICorDebugValue *pInputValue, ICorDebugThread *pThread,
Expand Down Expand Up @@ -258,7 +407,8 @@ HRESULT Variables::GetStackVariables(
ToRelease<ICorDebugValue> iCorValue;
IfFailRet(getValue(&iCorValue, var.evalFlags));
IfFailRet(TypePrinter::GetTypeOfValue(iCorValue, var.type));
IfFailRet(PrintValue(iCorValue, var.value));
IfFailRet(PrintValueWithImplicitToString(iCorValue, pThread, frameId.getLevel(), var.evalFlags, var.evaluateName,
m_sharedEvalStackMachine.get(), var.value));

IfFailRet(AddVariableReference(var, frameId, iCorValue, ValueIsVariable));
variables.push_back(var);
Expand Down Expand Up @@ -354,7 +504,7 @@ HRESULT Variables::GetChildren(
bool isIndex = !it.name.empty() && it.name.at(0) == '[';
if (var.name.find('(') == std::string::npos) // expression evaluator does not support typecasts
var.evaluateName = ref.evaluateName + (isIndex ? "" : ".") + var.name;
IfFailRet(FillValueAndType(it, var));
IfFailRet(FillValueAndType(it, var, pThread, ref.frameId.getLevel(), m_sharedEvalStackMachine.get()));
IfFailRet(AddVariableReference(var, ref.frameId, it.value, ValueIsVariable));
variables.push_back(var);
}
Expand Down Expand Up @@ -404,7 +554,8 @@ HRESULT Variables::Evaluate(

variable.evaluateName = expression;
IfFailRet(TypePrinter::GetTypeOfValue(pResultValue, variable.type));
IfFailRet(PrintValue(pResultValue, variable.value));
IfFailRet(PrintValueWithImplicitToString(pResultValue, pThread, frameLevel, variable.evalFlags, variable.evaluateName,
m_sharedEvalStackMachine.get(), variable.value));

return AddVariableReference(variable, frameId, pResultValue, ValueIsVariable);
}
Expand Down
78 changes: 77 additions & 1 deletion test-suite/MITestVariables/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,50 @@ public override string ToString()
}
}

public class ToStringDisplayClass
{
public int Value = 42;

public override string ToString()
{
return "class:" + Value.ToString();
}
}

public struct ToStringDisplayStruct
{
public int Value;

public ToStringDisplayStruct(int value)
{
Value = value;
}

public override string ToString()
{
return "struct:" + Value.ToString();
}
}

public class ToStringDisplayBase
{
public override string ToString()
{
return "base:7";
}
}

public class ToStringDisplayInherited : ToStringDisplayBase
{
public int Value = 7;
}

public class ToStringNoOverrideClass
{
public int Value = 123;
public Guid Id = new Guid("11111111-2222-3333-4444-555555555555");
}

public struct TestSetVarStruct
{
public static int static_field_i;
Expand Down Expand Up @@ -548,6 +592,7 @@ static void Main(string[] args)
Context.EnableBreakpoint(@"__FILE__:__LINE__", "BREAK5");
Context.EnableBreakpoint(@"__FILE__:__LINE__", "BREAK6");
Context.EnableBreakpoint(@"__FILE__:__LINE__", "BREAK7");
Context.EnableBreakpoint(@"__FILE__:__LINE__", "BREAK8");
Context.EnableBreakpoint(@"__FILE__:__LINE__", "BREAK_GETTER");
Context.EnableBreakpoint(@"__FILE__:__LINE__", "bp_func1");
Context.EnableBreakpoint(@"__FILE__:__LINE__", "bp_func2");
Expand Down Expand Up @@ -1250,7 +1295,7 @@ static void Main(string[] args)

int dummy7 = 7; Label.Breakpoint("BREAK7");

Label.Checkpoint("test_eval_with_exception", "finish", (Object context) => {
Label.Checkpoint("test_eval_with_exception", "test_to_string_display", (Object context) => {
Context Context = (Context)context;
Context.WasBreakpointHit(@"__FILE__:__LINE__", "BREAK7");

Expand All @@ -1262,6 +1307,37 @@ static void Main(string[] args)
Context.Continue(@"__FILE__:__LINE__");
});

Guid toStringGuid = new Guid("11111111-2222-3333-4444-555555555555");
Version toStringVersion = new Version(1, 2, 3, 4);
TimeSpan toStringTimeSpan = new TimeSpan(1, 2, 3);
ToStringDisplayClass toStringClass = new ToStringDisplayClass();
ToStringDisplayStruct toStringStruct = new ToStringDisplayStruct(99);
ToStringDisplayInherited toStringInherited = new ToStringDisplayInherited();
ToStringNoOverrideClass toStringNoOverride = new ToStringNoOverrideClass();

int dummy8 = 8; Label.Breakpoint("BREAK8");

Label.Checkpoint("test_to_string_display", "finish", (Object context) => {
Context Context = (Context)context;
Context.WasBreakpointHit(@"__FILE__:__LINE__", "BREAK8");

Context.CreateAndCompareVar(@"__FILE__:__LINE__", "toStringGuid", "11111111-2222-3333-4444-555555555555");
Context.CreateAndCompareVar(@"__FILE__:__LINE__", "toStringVersion", "1.2.3.4");
Context.CreateAndCompareVar(@"__FILE__:__LINE__", "toStringTimeSpan", "01:02:03");
Context.CreateAndCompareVar(@"__FILE__:__LINE__", "toStringClass", "class:42");
Context.CreateAndCompareVar(@"__FILE__:__LINE__", "toStringStruct", "struct:99");
Context.CreateAndCompareVar(@"__FILE__:__LINE__", "toStringInherited", "base:7");
Context.CreateAndCompareVar(@"__FILE__:__LINE__", "toStringNoOverride", "{MITestVariables.ToStringNoOverrideClass}");
Context.GetAndCheckChildValue(@"__FILE__:__LINE__", "123", "toStringNoOverride", 0, false, 0);
Context.GetAndCheckChildValue(@"__FILE__:__LINE__", "11111111-2222-3333-4444-555555555555", "toStringNoOverride", 1, false, 0);

var res = Context.MIDebugger.Request(String.Format("-var-create - * \"toStringGuid\" --evalFlags {0}", (int)Context.enum_EVALFLAGS.EVAL_NOFUNCEVAL));
Assert.Equal(MIResultClass.Done, res.Class, @"__FILE__:__LINE__");
Assert.Equal("{System.Guid}", ((MIConst)res["value"]).CString, @"__FILE__:__LINE__");

Context.Continue(@"__FILE__:__LINE__");
});

Label.Checkpoint("finish", "", (Object context) => {
Context Context = (Context)context;
Context.WasExit(@"__FILE__:__LINE__");
Expand Down
Loading