以 DateTime 为目标的调试可视化工具不会传输值

Debugging visualizer which targets DateTime doesn't transfer the value

我写了一篇Visual Studio debugging visualizer which targets DateTime (repo). My issue is that the debugger side only passes the target value to the debuggee side if the target expression is of object, not of DateTime (issue).

我发布了一个包含 MCVE that reproduces the problem 的 GH 存储库。调试器端看起来像这样:

protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider) {
    var response = objectProvider.TransferObject(5);

    var msg = response switch {
        string s => s,
        IEnumerable e => string.Join(", ", e.Cast<object>()),
        _ => "Unhandled type"
    };

    MessageBox.Show(msg);
}

被调试端看起来像这样:

public override void TransferData(object target, Stream incomingData, Stream outgoingData) {
    int? repetitions = Deserialize(incomingData) switch {
        int i when i > 0 => i,
        string s when int.TryParse(s, out int i) && i > 0 => i,
        _ => null
    };

    object toSerialize =
        repetitions is null ? $"Invalid value for repetitions" :
        target switch {
            DateTime dt => Repeat(dt, repetitions.Value).ToArray(),
            null => $"{nameof(target)} is null",
            _ => $"Not implemented for target of type {target.GetType().FullName}" as object
        };

    Serialize(outgoingData, toSerialize);
}

构建并安装可视化工具后,开始调试以下代码:

var dte = DateTime.UtcNow;
object o = dte;

如果我将鼠标悬停在 o 上并触发可视化工具,目标 DateTime 将传递到调试对象端,并且 return 是一个 DateTime 数组。但是,如果我在 dte 上触发可视化工具,我会返回字符串 target is null,这意味着调试端已在 target 参数中收到 null

这可能是什么原因造成的?我该如何解决?


一些随机笔记


类似地针对 int

时异常命中的堆栈跟踪

响应,可视化一个int时的错误信息如下:

The target process exited with code -1073740791 (0xC0000409) while evaluating the function 'Microsoft.VisualStudio.DebuggerVisualizers.DebuggeeSide.Impl.ClrCustomVisualizerDebuggeeHost.TransferData'.

If the problem happens regularly, consider disabling the Tools->Options setting "Debugging->General->Enable property evaluation and other implicit function calls" or debugging the cause by evaluating the expression from the Immediate window. See help for information on doing this.

随后是另一条消息:

Could not load the custom viewer.

目标进程崩溃,调试会话结束。

我尝试使用代码断点 (Debugger.Break()) 附加调试器但未成功。如果我 return 来自可视化工具 (new System.Diagnostics.StackTrace().ToString()) 的调用堆栈并且可视化工具成功运行,我将得到以下信息:

at SimpleValueTypeVisualizer.Debuggee.VisualizerObjectSource.TransferData(Object target, Stream incomingData, Stream outgoingData)

at Microsoft.VisualStudio.DebuggerVisualizers.DebuggeeSide.Impl.ClrCustomVisualizerDebuggeeHost.TransferData(Object visualizedObject, Byte[] uiSideData)

at TestNoRef.Program.Main(String[] args)

这似乎意味着 Microsoft.VisualStudio.DebuggerVisualizers.DebuggeeSide.Impl.ClrCustomVisualizerDebuggeeHost.TransferData.

有一些例外

当我使用 ILSpy 打开 DebuggerVisualizers.dll 时,相关的 TransferData 方法如下所示:

// Microsoft.VisualStudio.DebuggerVisualizers.DebuggeeSide.Impl.ClrCustomVisualizerDebuggeeHost
using System.IO;

public byte[] TransferData(object visualizedObject, byte[] uiSideData)
{
    MemoryStream memoryStream = new MemoryStream();
    MemoryStream incomingData = ((uiSideData != null) ? new MemoryStream(uiSideData) : null);
    m_debuggeeSideVisualizerObject.TransferData(visualizedObject, incomingData, memoryStream);
    return memoryStream.ToArray();
}

我猜异常是在方法的第三行 (MemoryStream incomingData = ...)。但我仍然不清楚异常的细节,特别是为什么问题只出现在未装箱的值上,而不是装箱的值。


事件日志详细信息

根据 ,我包含了在 int:

类型的表达式上打开可视化工具时创建的事件日志中的数据
Log Name:      Application
Source:        Application Error
Date:          22/04/2021 12:14:36
Event ID:      1000
Task Category: (100)
Level:         Error
Keywords:      Classic
User:          N/A
Computer:      LAPTOP-7O43T4OO
Description:
Faulting application name: TestNoRef.exe, version: 1.0.0.0, time stamp: 0xd9f9e12d
Faulting module name: clr.dll, version: 4.8.4341.0, time stamp: 0x6023024f
Exception code: 0xc0000409
Fault offset: 0x00574845
Faulting process ID: 0x94c4
Faulting application start time: 0x01d73757c33e87c0
Faulting application path: ***********
Faulting module path: C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll
Report ID: 1dcf070b-71ff-4279-be71-822698cc6168
Faulting package full name: 
Faulting package-relative application ID: 
Event Xml:
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
  <System>
    <Provider Name="Application Error" />
    <EventID Qualifiers="0">1000</EventID>
    <Version>0</Version>
    <Level>2</Level>
    <Task>100</Task>
    <Opcode>0</Opcode>
    <Keywords>0x80000000000000</Keywords>
    <TimeCreated SystemTime="2021-04-22T09:14:36.4507272Z" />
    <EventRecordID>1180760705</EventRecordID>
    <Correlation />
    <Execution ProcessID="0" ThreadID="0" />
    <Channel>Application</Channel>
    <Computer>LAPTOP-7O43T4OO</Computer>
    <Security />
  </System>
  <EventData>
    <Data>TestNoRef.exe</Data>
    <Data>1.0.0.0</Data>
    <Data>d9f9e12d</Data>
    <Data>clr.dll</Data>
    <Data>4.8.4341.0</Data>
    <Data>6023024f</Data>
    <Data>c0000409</Data>
    <Data>00574845</Data>
    <Data>94c4</Data>
    <Data>01d73757c33e87c0</Data>
    <Data>***********</Data>
    <Data>C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll</Data>
    <Data>1dcf070b-71ff-4279-be71-822698cc6168</Data>
    <Data>
    </Data>
    <Data>
    </Data>
  </EventData>
</Event>

我找不到合适的解决方案。它可能只是最新版本之一中引入的错误,到目前为止还没有人遇到过值类型的这个问题。其实我试过了

DateTime dte = DateTime.UtcNow;
ValueType vt = dte;

它同样适用于 vt 但不适用于 dte。为了以防万一,我向 net48 添加了一个明确的目标,但它没有任何改变。

我能想出的最好办法是一种与我猜 Zev Spitz 使用的非常相似的解决方法,但尽量不要浪费 GetData 重写来获取目标值。恐怕这不是一个很好的解决方案。

假设您想使用 GetData 检索不同的值,但它将在您的 DialogDebuggerVisualizer.Show 覆盖中使用,您可以在调用 GetData 时将您的值存储在 VisualizerObjectSource 对象中,并在调用时检索它调用 TransferData,但实际上并未将其从 Debuggee 转移到 Debugger。

 public class VisualizerObjectSource : Microsoft.VisualStudio.DebuggerVisualizers.VisualizerObjectSource 
    {
       /*static*/ DateTime? _lastDatetime=null;
        public override void TransferData(object target, Stream incomingData, Stream outgoingData) 
        {
            target = _lastDatetime;

            //Calculate here the output value        
            object toSerialize = " is null = " + (target==null).ToString();
       
            Serialize(outgoingData, toSerialize);
        }

        public override void GetData(object target, Stream outgoingData)
        {
            _lastDatetime = (DateTime)target;
            
            //Calculate here what you want to be returned by GetData
            base.GetData(" The stuff you want to return ", outgoingData);
        }   

    }

并且在您的调试器端,确保在调用 TransferObject()

之前调用 GetObject/GetData

       protected override void Show(IDialogVisualizerService windowService, 
IVisualizerObjectProvider objectProvider)
        {            
            object MyCustomStuff =objectProvider.GetObject();
            var response = objectProvider.TransferObject(5);

           //[...]          

             string msg =  response .ToString();

            MessageBox.Show(msg);
        }

正如我在评论中提到的,TransferData 实际上不是必需的。此外,可以避免整个 BinaryFormatter 序列化(不幸的是,只有在将数据传输到调试器可视化工具时,而不是在替换编辑值时,但还有另一个技巧)。

首先,考虑以下设置:

[assembly: DebuggerVisualizer(typeof(DateTimeVisualizer), typeof(DateTimeSerializer),
    Target = typeof(DateTime),
    Description = "DateTime Debugger Visualizer")]

其中序列化器如下:

// Note that TransferData is not overridden, and we do not call base.GetData so we can 
// avoid using BinaryFormatter (which often has issues when debugging a .NET Core or newer project)
internal class DateTimeSerializer : VisualizerObjectSource
{
    public override void GetData(object target, Stream outgoingData)
    {
       var dateTime = (DateTime)target;

       // Note: do not dispose the writer so the outgoingData remains open.
       // If targeting newer frameworks you can use the leaveOpen parameter, too.
       var writer = new BinaryWriter(outgoingData);

       // What a tiny payload compared to the default BinaryFormatter result...
       writer.Write(dateTime.Ticks);
       writer.Write((int)dateTime.Kind);
    }
}

并且在可视化工具本身中,重要的是不要调用 GetObject,它会尝试通过 BinaryFormatter 反序列化您的流。相反,使用 GetData,其中 return 是原始流:

internal class DateTimeDebuggerVisualizer : DialogDebuggerVisualizer
{
    protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
    {
        // GetObject would fail here as we have a custom written stream
        var reader = new BinaryReader(objectProvider.GetData());

        var dateTime = new DateTime(reader.ReadInt64(), (DateTimeKind)reader.ReadInt32());

        // Show the debugger [...]
    }

如果您的调试器可以编辑数据,那么替换值是另一个问题。不幸的是,在这种情况下无法避免 BinaryFormatter 序列化。如果您的对象不能在每个目标中序列化(例如在 .NET Core 及更高版本中),这可能是一个问题。

当然,这在 DateTime 的情况下不是问题,所以作为旁注:在这种情况下,技巧可以是将自定义可序列化数据放入 ReplaceObject 中实现IObjectReference 这样您就可以 return 在 IObjectReference.GetRealObject 中定制结果。 Here 是我的序列化程序中的一个这样的例子。