CLR SPY and Customer Debug Probes: The Marshaling Probe

As promised, here's my first entry with more details about Customer Debug Probes and CLR SPY.

The Marshaling probe is the easiest one to understand and great for experimentation since it's the only one that doesn't report on error/warning conditions.  It also fits best into the spy theme since it non-intrusively reports on work that is regularly done by just about any managed application.  In today's world, you'll be hard-pressed to find a managed application that doesn't marshal parameters to unmanaged code - especially if you count the .NET Framework APIs used by the application.

The Marshaling probe logs how parameters and return types get marshaled from managed to unmanaged code.  (The probe doesn't tell you about marshaling in the opposite direction, nor does it tell you about fields as they are marshaled.)  For a "Hello, World" C# application:

public class HelloWorld

{

  public static void Main()

  {

    System.Console.WriteLine("Hello, World!");

  }

}

I get the following output from CLR SPY (without the date, time, and probe prefix so it's easier to see):

  Marshaling from IntPtr to DWORD in method GetStdHandle.

  Marshaling from Int32 to DWORD in method GetStdHandle.

  Marshaling from Int32 to DWORD in method GetFileType.

  Marshaling from IntPtr to DWORD in method GetFileType.

  Marshaling from Int32 to DWORD in method WriteFile.

  Marshaling from IntPtr to DWORD in method WriteFile.

  Marshaling from Int32 to DWORD in method WriteFile.

  Marshaling from Int32 to DWORD in method WriteFile.

  Marshaling from Byte* to DWORD in method WriteFile.

  Marshaling from IntPtr to DWORD in method WriteFile.

This happens because Console.WriteLine internally makes PInvoke calls to the Win32 GetStdHandle, GetFileType, and WriteFile APIs.

The steps to do this yourself are:

  1. Run ClrSpy.exe, which you can get from here.
  2. Add HelloWorld.exe to the list of monitored applications, and ensure that "Marshaling" is checked and "Everything" is selected.
  3. Click "Options..." and check "Log messages to a file:" since the balloon notification messages flurry past quickly.
  4. Run HelloWorld.exe.

Look what happens if I use CLR SPY on an equivalent managed C++ "Hello, World" application:

  Marshaling from UInt32 to DWORD in method _mainCRTStartup.

  Marshaling from IntPtr to DWORD in method GetStdHandle.

  Marshaling from Int32 to DWORD in method GetStdHandle.

  Marshaling from Int32 to DWORD in method GetFileType.

  Marshaling from IntPtr to DWORD in method GetFileType.

  Marshaling from Int32 to DWORD in method WriteFile.

  Marshaling from IntPtr to DWORD in method WriteFile.

  Marshaling from Int32 to DWORD in method WriteFile.

  Marshaling from Int32 to DWORD in method WriteFile.

  Marshaling from Byte* to DWORD in method WriteFile.

  Marshaling from IntPtr to DWORD in method WriteFile.

This highlights a difference between the C++-generated code, which has special entry point logic, and the C#-generated code.

There are two things to be aware of when analyzing Marshaling probe messages.  For any method:

  1. The marshaling for the return type is reported first, followed by marshaling for the parameters in the reverse order from the method's parameter list.
  2. Messages are only shown the first time it is called.  So if I change the "Hello, World" program to print to the console multiple times, I'll get the same output from CLR SPY.

When parameters are user-defined types, they are qualified with their namespace.  However, the methods whose parameters are being marshaled are never displayed with their namespace or even class name, which can make examining the output a little tedious.

If you're using the Marshaling probe to debug problems in your own code, you probably want to suppress messages for marshaling done by the .NET Framework.  This is possible using the "Marshaling Filter" pane in CLR SPY.  When you click the Edit button, you can type a semicolon-delimited expression list in the text box that states the items for which you want to see messages.  The string you assign as the filter can be a semicolon-delimited expression list (with no spaces inbetween).  Here are the rules for each expression:

  • You can specify a method name to show messages specific to any methods with a matching name
  • You can specify a class name to show messages specific to methods inside any class with a matching name
  • You can specify a namespace name to show messages specific to classes inside that namespace
  • You can qualify a method name with its class name using the syntax ClassName::MethodName
  • You can qualify a class name with its namespace using the syntax Namespace.ClassName
  • You can qualify a method name with its class name and namespace using the syntax Namespace.ClassName::MethodName
  • All expressions are case-insensitive
  • You can't specify partial names.  Any namespace/class name/method name must be complete.  For example, filtering on the namespace "System" does not include the namespace "System.Reflection"
  • There can be no spaces between the semicolons

For the C++ "Hello, World" example, a filter of GetStdHandle;GetFileType;WriteFile would show all messages except the _mainCRTStartup one.  A filter of System.IO would only show the WriteFile messages (since that's the namespace containing the private PInvoke signature), and a filter of Microsoft.Win32.Win32Native would only show the GetStdHandle and GetFileType messages.