Why is System.Diagnostic.Debugger class compiled in retail?

There's a good reason that methods on a "System.Diagnostics.Debugger" class are still compiled in retail builds, although this occasionally surprises folks. Here's why...

 

Background:

The System.Diagnostics.Debugger class provides some useful low-level debugging APIs such as:

  • Debugger.Break() - causes the app to break under the debugger when executed. This is used for VB's "Stop" statement and other debugging hooks. If no debugger is attached, it pops up a fatal error dialog (like watson or prompt to attach a debugger).
  • Debugger.Log(...) - logs a string to the debugger. This is a nop if no debugger is attached.
  • Debugger.Launch - this launches a debugger (via managed jit-launch mechanisms) if one is not attached.
  • Debugger.IsAttached - this detects if a debugger is attached.

(I previously blogged about the contrast between these managed APIs to their native counterparts)

These are implemented in mscorlib in the BCL.  They exist in both debug and retail builds.  

Here are some answers to common followup questions:

Question #1: Why aren't they just nops when a debugger is not attached?

Debugger.Log is indeed a nop if no debugger is attached. But the others still do things when a debugger isn't attached: Debugger.Launch and Debugger.IsAttached obviously innately make sense to exist when a debugger is not attached.

Debugger.Break() pops up a fatal error dialog. An alternative design would be for Debugger.Break() to indeed by a nop when no debugger is attached. However, this has some subtle issues:
- If Debugger.Breaks() was a nop in retail builds, it would be easy to blindly sprinkle them throughout your app, but this could present significant noise when run under a debugger. (Imagine always stopping on every single 1st-chance exceptions).
- It would violate the semantics that Debugger.Break() notifies the user. Under a debugger, it stops right at the line of interest. Outside the debugger, it presents an escalation dialog for Watson / Jit-debuggers, etc. This can be useful for implementing a retail assert.

 

 

Question #2: Why are S.D.Debugger methods in retail BCL builds?

We don't want different API public surface area across different flavors of system libraries. (the Condtional attribute helps avoid that problem) The only code in the process that needs to be debuggable is the code you're debugging. So debug applications may still link against retail libraries like mscorlib or other 3rd-party libraries. (They certainly uses retail versions of the runtime and the OS!)

 

Question #3: Why don't they have a Conditional attribute?

We don't use a conditional attribute on S.D.Debugger methdos because Conditional is a compiler-specific policy issue.

These are low-level primitives and try to be generally useful without enforcing a specific policy. Debug vs. Retail is a policy. And every time the platform picks the policy, it's wrong for some users.

  1. Maybe you don't have retail vs. debug builds.  Sure, Debug vs. Retail is a common design pattern and VS greatly helps with this, but it's not something that we need to bake into the low level BCL layers and thus prevent alternative paradigms.
  2. These can provide value in retail builds.  For example, you may want a Retail assert. Or you may want retail stress logging.
  3. Users can write higher level functions on them which  enforce their desired policy. For example, the FX provides Debug.Assert and Debug.Write which are conditionally compiled and build on top of the low-level debugger primitives.

 

You could write your own functions around them that enforce the particular policy that you want. For example, here are 2 different policies for wrapping the Break() function:

         void MyBreak1() 
        { 
            if (Debugger.IsAttached) { Debugger.Break(); } 
        }

        [Conditional(DEBUG)]
        void MyBreak2() 
        { 
            Debugger.Break(); 
        }