C# and OutputDebugString

Thursday 3 March 2005

I've taken over responsibility at work for a UI written in C#. This is challenging on a number of levels: I haven't done a lot of UI, I haven't done a lot of C#, and the previous owner was (shall we say) not the most disciplined developer.

Debugging UI code is a pain because if your debugger obscures the UI, then exposing the UI causes window messages, which change the behavior of the code, or trip more breakpoints, and so on. So I'm using printf-style debugging: printed messages in key points to understand what's going on. Here's some tips on how to do it.

First, use the .NET Debug.WriteLine() method. This calls the Win32 OutputDebugString() function. The good thing about this technique is that because it is a native Windows facility, there are tools available to help. Most developers know that these methods write strings to your debugger's Output window. That's great if you're running the program in the debugger.

But did you also know that you can see these messages even without a debugger? Sysinternals provides the kick-ass DebugView utility that will display all the traffic through the OutputDebugString function. This provides unexpected benefits, because it will display the chatter from everything on your system, not just the program you happen to be looking at.

Second, write yourself a utility function to beef up the Debug.WriteLine() functionality. For example, this method will automatically put the calling function's name into the message:

static public void DebugOut(string msg)
{
    StackTrace st = new StackTrace(false);
    string caller = st.GetFrame(1).GetMethod().Name;
    Debug.WriteLine(caller + ": " + msg);
}

Now you can write this:

public void MyBuggyFunction()
{
    DebugOut("hey there");
    //...
}

and see a message like this:

MyBuggyFunction: hey there

In thornier debugging cases, I want to know the full call stack to the point of the message. So I wrote another helper:

// The namespaces that ShortenType will remove from a type name.
static private string[] assumedPrefixes =
    new string[] {
                     "System.Windows.",
                     "System."
                 };

// Returns a short name for the given type.
static private string ShortTypeName(System.Type type)
{
    string typeName = type.ToString();
    foreach (string pref in assumedPrefixes)
    {
        if (typeName.StartsWith(pref))
        {
            return typeName.Substring(pref.Length);
        }
    }

    return typeName;
}

// Return a string description of the stack, with parameter types.
static public string GetDetailedStack(int skip, string prefix)
{
    string s = "";
    StackTrace st = new StackTrace(true);

    for (int i = skip; i < st.FrameCount; i++)
    {
        StackFrame sf = st.GetFrame(i);
        MethodBase meth = sf.GetMethod();
        string method = ShortTypeName(meth.DeclaringType) + "." + meth.Name + "(";
        bool first = true;
        foreach (ParameterInfo p in meth.GetParameters())
        {
            if (!first)
            {
                method += ", ";
            }
            method += ShortTypeName(p.ParameterType);
            first = false;
        }
        method += ")";
        s += prefix + method + "\n";
    }

    return s;
}

// Write a debug message, with the full stack.
static public void DebugOutStack(string msg)
{
    StackTrace st = new StackTrace(false);

    // The real caller is one frame up the stack.
    string caller = st.GetFrame(1).GetMethod().Name;

    Debug.WriteLine(caller + ": " + msg + "\n" + GetDetailedStack(2, "   "));
}

Now when I call DebugOutStack, I get a line for each frame on the stack:

MyUICtrl_Layout: called again!
   UI.MyUICtrl.MyUICtrl_Layout(Object, Forms.LayoutEventArgs)
   UI.MyUICtrl.splitter1_SplitterMoved(Object, Forms.SplitterEventArgs)
   Forms.Splitter.OnSplitterMoved(Forms.SplitterEventArgs)
   Forms.Splitter.set_SplitPosition(Int32)
   Forms.Splitter.ApplySplitPosition()
   Forms.Splitter.SplitEnd(Boolean)
   Forms.Splitter.OnMouseUp(Forms.MouseEventArgs)
   Forms.Control.WmMouseUp(Forms.Message&, Forms.MouseButtons, Int32)
   Forms.Control.WndProc(Forms.Message&)
   Forms.Control+ControlNativeWindow.OnMessage(Forms.Message&)
   Forms.Control+ControlNativeWindow.WndProc(Forms.Message&)
   Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr)

Having these stack traces available in my output messages has really helped me to pinpoint curious behavior.

I haven't found a way to get the actual values on the stack (for example, the type of the this pointers would be very helpful).

Comments

[gravatar]
Julian M Bucknall 4:43 PM on 3 Mar 2005

Ned

Although your pointers are invaluable (I really like the simple one that prints the function name), an easier way is to use a dual monitor setup to debug UI code. The debugger doesn't then obscure/reveal the debuggee.

Cheers, Julian

[gravatar]
Ned Batchelder 4:53 PM on 3 Mar 2005

You are right: a dual monitors are ideal. As it happens, I often work on just my laptop, and have adapted...

[gravatar]
Jake P 5:09 PM on 3 Mar 2005

Ned,

Why not use a logging package such as log4net instead of rolling your own? It already has built-in functionality for writing to the Debug window and has additional functionality that may work even better for your situation. If you have a second computer, you can even have the messages sent remotely across the network.

The package is based on log4j and if you've used the Python logging package, you won't have any problems getting around.

[gravatar]
Ned Batchelder 5:17 PM on 3 Mar 2005

log4net sounds like a great idea. As I said, I'm a newbie in this environment, so I'm working my way up the toolsets...

[gravatar]
Jeff Atwood 10:58 PM on 3 Mar 2005

> As it happens, I often work on just my laptop, and have adapted.

Yes, but most laptops support dual displays: hook up an external monitor to the VGA port and you can use it as a 2nd display device in Windows, alongside the built in LCD of your laptop.

[gravatar]
Kristopher Kailden 1:52 PM on 1 Jun 2005

Good thoughts. Thanks!

Add a comment:

name
email
Ignore this:
not displayed and no spam.
Leave this empty:
www
not searched.
 
Name and either email or www are required.
Don't put anything here:
Leave this empty:
URLs auto-link and some tags are allowed: <a><b><i><p><br><pre>.