If you've written or are writing a compiler for .NET, Visual Studio will use the C# Expression Evaluator (EE) by default if no custom EE exists. This might be acceptable for your purposes. However, if the C# experience isn't what you want/need, you have the option of replacing parts or all of the C# EE with your own implementation.
Microsoft has already implemented the Concord EE interface to support .NET languages (IDkmLanguageExpressionEvaluator
). You do have the option to replace this implementation with your own, but this approach requires an enormous amount of work and is not recommended. Instead, you can customize the existing .NET EE to understand your language. There is a sample .NET expression evaluator you can look at in this repo.
Here's a diagram of the .NET Expression Evaluator:
For simplicity sake, we are not showing the Concord Dispatcher, or the various components involved in communicating between the UI and EE components.
When the user evaluates an expression in the Watch window, for example a + 1
, the workflow is:
a + 1
. The evaluation request contains not only text, but also the target language, display radix, information about the current code location, and various other flags.IDkmLanguageExpressionEvaluator
.DkmClrValue
. This is the raw/unformatted value from the CLR. This needs to be formatted into the language being debugged. For example, if the language is C#, the value is a uint32 of number 12, and the display radix is 16, the string the user sees is "0x0000000C".The Watch window, Quick Watch window, Autos window, Immediate window, and DataTips all follow this same workflow. The Locals window uses a similar workflow (more information below...)
Adding support for new languagesGenerally, supporting a new language requires implementing the following interfaces (see below the table for more explanation about each interface:
Interface PurposeIDkmClrExpressionCompiler
Components implementing this interface are used to generate executable PE blobs using the compiler for the current language. IDkmClrFormatter
Components implementing this interface are used to convert DkmClrValues
into value and type strings appropriate for the current language. IDkmLanguageFrameDecoder
Components implementing this interface are used to format frames of the call stack into the strings that are visible for each row of the Call Stack window. IDkmClrExpressionCompiler
For the purpose of discussing this interface, assume we are stopped at the "return" statement in the following C# function:
private static int Func(int arg1) { int local1 = arg1 + 1; return local1; }
This method is used to convert a textual expression and context information into a PE blob that represents the compiled expression. This method is called when the user evaluates from the Watch, Quick Watch, Immediate windows, hovers over values in the editor (DataTips), or sets a conditional breakpoint.
To see how this method works, say the user types "local1 + 10" into the Watch window. Eventually the Dispatcher will call IDkmClrExpressionCompiler.CompileExpression
. The input is the expression (DkmLanguageExpression
) and the current instruction address (DkmClrInstructionAddress
). There is also some context information about the evaluation (DkmInspectionContext
). If CompileExpression is called to compile a breakpoint condition, this context information will be null. The output is an error message in case of compile error and a compiled inspection query (DkmCompiledClrInspectionQuery
) if the compilation was successful. One of the two output values should be set.
The inspection query contains a PE file with code like the following in it:
.class public QueryClass
{
.method public hidebysig static int32 QueryMethod(int32 arg1) cil managed
{
.locals init ([0]int32 local1)
ldloc.0
ldc.i4.s 10
add
ret
}
}
Note that the arguments of the query method match the arguments of the method "Func" we are evaluating in. In addition, the query method contains all of the locals of Func in the same order. If needed, the query method may contain additional temporary locals after the matching locals. The code above loads the value of local slot 0 "local1", adds 10 to it and returns it.
An inspection query can be created by calling DkmCompiledClrInspectionQuery.Create
. It takes the following parameters:
DkmClrCompilationResultFlags.BoolResult
and DkmClrCompilationResultFlags.PotentialSideEffect
. Setting BoolResult
allows the expression to be used for a "When True" breakpoint condition. Setting PotentialSideEffect
prevents the debugger from automatically evaluating an expression without user interaction. This should be set for expressions that may modify the state of the process.DkmEvaluationResultCategory.Data
. This value is used by the debugger to select the icon to show for the value in the inspection windows.public
, private
, protected
, etc. in C#). This value is used by the debugger to select the icon modifier in some cases.DkmClrCustomTypeInfo
.This method is similar to CompileExpression, but it is used when the user edits a value from one of the variable inspection windows. Using the same example C# code above, assume the user edits the value of "local1" in the Locals window and enters the text "99". Eventually the Dispatcher will call IDkmClrExpressionCompiler.CompileAssignment
. The input is the expression "99", the current instruction address, a DkmEvaluationResult (this is the result of the evaluation to get the value of "local1"). The output is an error message or compiled inspection query - same as CompileExpression.
The Expression should generate a PE file with code to assign the given expression to the previous evaluation result (The L-Value). Typically the compiler will use DkmEvaluationResult.FullName
to get an L-Value string and compile something to the effect of local1 = 99
. The PE file should end up with code equivalent to:
.class public QueryClass
{
.method public hidebysig static void QueryMethod(int32 arg1) cil managed
{
.locals init ([0]int32 local1)
ldc.i4.s 99
stloc.0
ret
}
}
The Expression Compiler should create a DkmClrInspectionQuery as described in the CompileExpression section.
This method is used when the user views the Locals window or an extension uses the DTE to get local variables or arguments. The input to this method is an inspection context, instruction address for the current location, and a boolean parameter to indicate if only arguments were requested. The return value is a 'DkmCompiledClrLocalsQuery'.
As an example, assume we are stopped in this code:
private static string Func(int arg1, int arg2) { int local1 = arg1 + arg2; string local2 = "Hello"; return local2 + local1.ToString(); }
In this case, the Expression Compiler will generate a PE file similar to:
.class public QueryClass
{
.method public hidebysig static int32 M1(int32 arg1, int32 arg2) cil managed
{
.locals init ([0]int32 local1, [1]string local2)
ldarg.0
ret
}
.method public hidebysig static int32 M2(int32 arg1, int32 arg2) cil managed
{
.locals init ([0]int32 local1, [1]string local2)
ldarg.1
ret
}
.method public hidebysig static int32 M3(int32 arg1, int32 arg2) cil managed
{
.locals init ([0]int32 local1, [1]string local2)
ldloc.0
ret
}
.method public hidebysig static string M4(int32 arg1, int32 arg2) cil managed
{
.locals init ([0]int32 local1, [1]string local2)
ldloc.1
ret
}
}
The parameters to DkmCompiledClrLocalsQuery.Create
are similar to the parameters for DkmCompiledClrInspectionQuery.Create
. The difference is that instead of a method name parameter, there is a read only collection of DkmClrLocalVariableInfo
. DkmClrLocalVariableInfo
is a pairing of a variable name along with the query method to get the value of the variable. It also contains custom type information, full name, and result category. These values should be the same as what CompileExpression
would set if evaluating the variable directly.
Debugger intrinsics are used to support expressions such as $exception
as well as the Make Object ID feature and pseudo-variables. This is another big topic and more information is available here.
Implementing this interface allows components to customize the way values are displayed in the variable inspection windows. C#'s implementation does the following:
The methods on IDkmClrFormatter
are GetValueString
, GetTypeName
, HasUnderlyingString
, and GetUnderlyingString
This method gets the string value to display to the user given a raw value. The parameters are:
DkmClrValue.HostObjectValue
will contain the marshalled value. For example if the value is an int32, DkmClrValue.HostObjectValue
will be a boxed int32. The debugger can marshal all primitive types and a few non-primitive types (like System.Decimal).DkmEvaluationFlags.NoQuotes
.If there was an error executing the query, DkmClrValue.ValueFlags
will have the DkmClrValueFlags.Error
bit set. In this case GetValueString
, should cast DkmClrValue.HostObjectValue
to a string and return that.
This method gets the string value to display given a raw type. The parameters are:
GetTypeName
call to the correct formatter.Information about DkmClrType:
DkmClrType contains the information needed to uniquely identify a type, but has very little functionality beyond that. If your Formatter is written in C++, you'll need access to a metadata reader (IMetadataImport will work). You can access the raw metadata blocks using DkmClrModuleInstance.GetMetadataBytesPtr()
and open the reader against the raw blocks.
If your Formatter is written in C#, you can use the debugger's metadata reader and type system. The debugger's type system is named LMR and lives in the namespace Microsoft.VisualStudio.Debugger.Metadata
. You can get the LMR type by calling DkmClrValue.GetLmrType()
. A LMR type is very similar to System.Type
and for the most part you can use LMR as you would use Reflection.
Gets the raw string to show in the string/xml/html visualizer. Most formatter implementations can delegate to the C# implementation by calling DkmClrValue.GetUnderlyingString(inspectionContext)
.
This method determines if a value has an underlying string. If this method returns true, the debugger will show a "Magnifying Glass" icon in the UI. Clicking it will allow the user to select a string visualizer. Most formatter implementations can delegate to the C# implementation by calling DkmClrValue.HasUnderlyingString(inspectionContext)
.
Implementing this interface allows components to customize the way stack frames are displayed in the debugger UI.
This method generates the string value to display in the Call Stack window (or other debugger UI) for a stack frame.
GetFrameName takes the following parameters:
DkmVariableInfoFlags
parameter has the DkmVariableInfoFlags.Values
bit set.GetFrameName
should return the name of the method this code location is within.GetFrameReturnType can be called from the debugger automation APIs and should return the name of the frame's return type. The parameters and usage are similar to GetFrameName.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4