自前デバッグを考える(しかし、沸き上がる怠惰感情に負けて断念した)

実行中のプログラムをデバッグ調査したい(ソースを見ながらステップ実行してローカル変数を見たい)。これを Visual Studio 無しでやりたい。これができれば Visual Studio が入っていない Windows でも調査できる。

毎年1回くらいの頻度で、この衝動にかられるのです。謎の情熱。で、今回は作るのめんどくせ。ってなったので、構想だけ共有しておきます。

目次

きっかけと構想案

この方のブログを拝見しまして、おお!これだ!と思いました。

Roslyn の構文解析を使ってデバッガーを自作する | Do Design Space

ただし、このプログラム始まりで実行しなければいけないのがポイントで、自分がやりたいのは、すでに実行中のプログラムに介入して既存処理をごにょごにょいじって、デバッグ処理を埋め込んでデバッグ調査したいというものだったので、もう一歩の改良が必要です。

で、結論としては、実行中のプログラムをいじるのは大変すぎるので、オリジナルソースを複写して、デバッグ処理関連のクラスやメソッドをソースに追加して、ビルドしてもらって動かしてもらう。みたいな流れで妥協するかと考えました。

で、手動でやるのは面倒くさすぎるので、ヘルパープログラム作って、ボタンを押したらソース改変してもらって、それをビルドして実行してもらうことで下準備おっけーじゃんという段取りを考えました。

が、ここで情熱が冷めてしまい、なんまいだー。してぽしゃりました。また来年に期待です。

ソース改変を考える

以下のようなイメージでいじろうと思っていました。WinForms でも WPF でもいいんですけど、ターゲットexeの .NET Framework に合わせたかったため、ソースコードオンリーで画面作成して埋め込むことを考えていました。この時、プロジェクトファイルに関係する参照 dll が無かったらついでに追加もしておきます。いじるソースコードに using の名前空間省略も無い場合は追加します。

ちなみに、デバッグ調査するのは、任意のクラスにある任意のイベントハンドラ1つ分だけです。これは何となく。ヘルパープログラムで選択欄を設けておく必要もありますね。

以下のサンプルは、WinForms プロジェクトで、画面に Button を1つ置いて、クリックイベントを紐づけているだけの画面サンプルです。

オリジナルソース

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp12
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            var x = 12;

            for (var i = 0; i < x; i++)
            {
                Console.WriteLine(i);
            }
        }
    }
}

改変後ソース

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp12
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            DebugHelper.SourceCode = @"using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp12
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            var x = 12;

            for (var i = 0; i < x; i++)
            {
                Console.WriteLine(i);
            }
        }
    }
}
";


        }

        private async void button1_Click(object sender, EventArgs e)
        {
            DebugHelper.Show(this);

            var x = 12;
            await DebugHelper.NextBreakPoints(474 - 21, 11, $"x : {x.GetType().FullName} = {x}");

            for (var i = 0; i < x; i++)
            {
                Console.WriteLine(i);
                await DebugHelper.NextBreakPoints(561 - 25, 21, $"x : {x.GetType().FullName} = {x}", $"i : {i.GetType().FullName} = {i}");
            }
            DebugHelper.Close();
        }
    }

    public class DebugHelper
    {
        private static Form DebugForm;
        private static Action SetSourceCode;
        private static Action<int, int, string[]> ClickHandler;

        public static string SourceCode { get; set; }

        static DebugHelper()
        {
            DebugForm = new Form
            {
                Width = 800,
                Height = 600,
                Text = "簡易デバッガー",
                TopMost = true
            };

            var splitContainer1 = new SplitContainer() { Dock = DockStyle.Fill };
            var textBox1 = new RichTextBox() { Text = "test1", Dock = DockStyle.Fill };
            var textBox2 = new RichTextBox() { Text = "test2", Dock = DockStyle.Fill };
            textBox1.Font = new Font(textBox1.Font.FontFamily, 12);
            textBox2.Font = new Font(textBox1.Font.FontFamily, 12);

            var button1 = new Button() { Text = "次に進む", Dock = DockStyle.Top };
            button1.Click += (ss, ee) =>
            {
                _IsStop = false;
            };

            splitContainer1.Panel1.Controls.Add(textBox1);
            splitContainer1.Panel2.Controls.Add(textBox2);
            splitContainer1.Panel2.Controls.Add(button1);
            DebugForm.Controls.Add(splitContainer1);
            //splitContainer1.SplitterDistance = DebugForm.Width / 2;
            splitContainer1.SplitterDistance = (DebugForm.Width / 3) * 2;

            SourceCode = string.Empty;
            SetSourceCode = () => textBox1.Text = SourceCode;
            ClickHandler = (selectionStart, selectionLength, variables) =>
            {
                textBox2.Clear();
                foreach (var variable in variables)
                {
                    textBox2.AppendText($" {variable}\r\n");
                }

                textBox1.SelectionLength = 0;
                textBox1.SelectionStart = selectionStart;
                textBox1.SelectionLength = selectionLength;
                textBox1.Focus();
            };
        }

        private static bool _IsShown;

        public static void Show(Form owner = null)
        {
            if (_IsShown)
                return;

            if (DebugForm.IsDisposed)
                return;

            if (string.IsNullOrWhiteSpace(SourceCode))
                throw new InvalidOperationException("SourceCode Property is null");

            DebugForm.Show(owner);
            SetSourceCode();
            _IsShown = true;
        }

        public static void Close()
        {
            if (!_IsShown)
                return;

            DebugForm.Close();
            _IsShown = false;
        }

        private static bool _IsStop;

        public static async Task NextBreakPoints(int selectionStart, int selectionLength, params string[] variables)
        {
            // 2回目の起動対策
            if (DebugForm.IsDisposed)
                return;

            if (!_IsShown)
                Show();

            ClickHandler(selectionStart, selectionLength, variables);

            _IsStop = true;
            while (_IsStop)
            {
                await Task.Run(async () => await Task.Delay(100));
            }
        }
    }
}

動作イメージ

デバッグ対象の画面を開いて、デバッグ対象のコントロールのイベントを発生させます。このサンプルだとボタンのクリック。 f:id:sutefu7:20191220194348p:plain

デバッグ画面が表示されます。最初のローカル変数が表示されています。 f:id:sutefu7:20191220194406p:plain

「次に進む」ボタンをクリックすると、次の式に進みます。(と言いながら for ループは無視しているけど) f:id:sutefu7:20191220194416p:plain

さらに「次に進む」ボタンをクリックすると、次の式に進みます。 f:id:sutefu7:20191220194426p:plain

という感じで見ていきます。

変数がデータクラスの時は、プロパティメンバーも見たいので、もう一工夫が必要ですね。また、改変後ソースを作るには、CodeAnalysis でソース解析して、式シンタックスを見つけて、そこの文字位置を取得しないと、うまくステップ実行中の一行を選択状態にすることができません。難しいですね。例外エラーが飛んだ時のために、何らかの処理も組み込まないとですね。問題山積みやんけ。。。