Debugging the Managed and Native Code during a P/Invoke call on NetCF v2 using Visual Studio 2005

During my recent MEDC session, I demonstrated how to debug both the managed and native code sides of a P/Invoke. Today's topic is based on that demo. I will use Visual Studio 2005 Beta 2 to demonstrate the steps required to debug a P/Invoke in an application running on the Beta 2 release of the .NET Compact Framework v2.

Before we get started, please take a quick read of the posts listed below. They provide background information that will be used throughout today's discussion.

Sample managed application
The example, below, is for a simple battery status application. It checks to see if the device is connected to an external power source and the power level of the internal battery. The application uses a custom native library (described below) to query the battery state.

To use this example, create a Visual Basic .NET Pocket PC Device Application project. You will need to add the following controls to your form, and name them as specified in the list below.

Control Name------- ----Button goButtonLabel statusLabelLabel chargeStatusLabel
Once the controls are added, create a click event handler for the goButton and insert the following code.

Private Sub goButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles goButton.Click    Dim result As Integer = 0    Dim myBattery As MyBatteryInfo = New MyBatteryInfo    myBattery.ChargeStatus = BatteryChargeStatus.Unknown    myBattery.PluggedIn = AcPowerStatus.Unknown    ' Debug Build ONLY    ' stop the process to allow for re-attaching    ' the managed code debugger    System.Diagnostics.Debug.Assert(False, "Attach Native Debugger Now")    ' P/Invoke to get battery information    result = GetBatteryInfo(myBattery)    ' Debug Build ONLY    ' stop the process to allow for re-attaching    ' the managed code debugger    System.Diagnostics.Debug.Assert(False, "Re-attach Managed Debugger Now")    If (result = 0) Then        MessageBox.Show("Failed to read battery status", "Error")    End If    ' update the form    Dim statusText As String    Select Case myBattery.PluggedIn        Case AcPowerStatus.Online            statusText = "Yes"        Case AcPowerStatus.Offline            statusText = "No"        Case AcPowerStatus.Unknown            statusText = "Unknown"        Case Else            statusText = "Unrecognized"    End Select    statusLabel.Text = statusText    If (myBattery.ChargeStatus = BatteryChargeStatus.Unknown) Then        statusText = "Unknown"    ElseIf (myBattery.ChargeStatus = BatteryChargeStatus.NoBattery) Then        statusText = "No battery"    Else        Dim bcs As BatteryChargeStatus = 0        bcs = myBattery.ChargeStatus And (BatteryChargeStatus.High _                                    Or BatteryChargeStatus.Low _                                    Or BatteryChargeStatus.Critical)        Select Case bcs            Case BatteryChargeStatus.High                statusText = "High"            Case BatteryChargeStatus.Low                statusText = "Low"            Case BatteryChargeStatus.Critical                statusText = "Critical"        End Select        bcs = myBattery.ChargeStatus And BatteryChargeStatus.Charging        If (bcs = BatteryChargeStatus.Charging) Then            statusText = statusText + " (Charging)"        End If    End If    chargeStatusLabel.Text = statusTextEnd Sub
You will also need to add this structure.

Public Structure MyBatteryInfo    Dim PluggedIn As AcPowerStatus    Dim ChargeStatus As BatteryChargeStatusEnd Structure
The structure utilizes a couple of custom enumerations.

Public Enum AcPowerStatus As Byte    Offline = 0    Online = 1    Unknown = 255    End Enum<FlagsAttribute()> _Public Enum BatteryChargeStatus As Byte    High = 1    Low = 2    Critical = 4    Charging = 8    NoBattery = 128    Unknown = 255End Enum
Add the following P/Invoke signature to your class.
Declare Function GetBatteryInfo Lib "Native.dll" _    (ByRef batteryInfo As MyBatteryInfo) As Integer
And do not forget to import the interop services namespace
Imports System.Runtime.InteropServices
Sample native library
The native library provides a simplified interface to the battery status API (GetSystemPowerStatusEx2) and returns only the information used by the application (connected to AC power source, battery charge level). To use this example, create a Visual C++ Win32 Smart Device Project (DLL) called GetBatteryInfo, in the application solution. To make deployment simpler, set the DLL project settings to deploy the binary into the same device folder as the application project. Once your project is created, add the function below.

DWORD GetBatteryInfo(BATTERYINFO* pBatteryInfo){    // set default return value    DWORD result = 0;    // check incoming pointer    if(NULL == pBatteryInfo)    {        return 0;    }    SYSTEM_POWER_STATUS_EX2 sps;    // request the power status    result = GetSystemPowerStatusEx2(&sps, sizeof(sps), TRUE);    // only update the caller if the previous call succeeded    if(0 != result)    {        pBatteryInfo->acStatus = sps.ACLineStatus;        pBatteryInfo->chargeStatus = sps.BatteryFlag;    }    return result;}
And the data structure that will be used to return the desired battery state information.

typedef struct{    BYTE acStatus;    BYTE chargeStatus;} BATTERYINFO;

You may have noticed that the application code contains two Debug.Assert statements -- one before and one after the call to GetBatteryInfo. It is important
to remember that detaching a debugger resumes the application which was being debugged. Because we will be using two separate debugger engines (one for
mananaged code and one for native code), we will need a way to instruct the application to pause and allow us to connect the appropriate debugger engine.
Prior to Visual Studio 2005, you needed two separate products to debug managed (Visual Studio .NET 2003) and native (Embedded Visual C++) code. With the
Beta 2 release of Visual Studio 2005, these steps are accompished within one instance of the Visual Studio product.

I find using Debug.Assert to be the most useful means of pausing an application for the following reasons: the call is safe to leave in the code (it does
nothing in release builds) and it allows me to display a message (I like to have reminders of where I am in relation to the P/Invoke call). The snippet
below shows the Debug.Assert calls that I used in the above managed code.

' Debug Build ONLY' stop the process to allow for re-attaching' the managed code debuggerSystem.Diagnostics.Debug.Assert(False, "Attach Native Debugger Now")' P/Invoke to get battery informationresult = GetBatteryInfo(myBattery)' Debug Build ONLY' stop the process to allow for re-attaching' the managed code debuggerSystem.Diagnostics.Debug.Assert(False, "Re-attach Managed Debugger Now")
At this point, it is important to point out that to be able to re-attach to your application using the managed debugger, you will need to have enabled attach to process support prior to starting the application.

Debugging Steps
Now that we have our solution containing the managed application and native library projects, it is time to deploy and get started debugging.

  1. Enable Attach to Process support on your device.
    Since we will be re-attaching to the application once we are finished debugging the native library, this is a required step.
     

  2. Build, deploy and debug the managed application
    Since we have a very specific part of the code we wish to debug, I recommend using the Run to Cursor feature of Visual Studio 2005.
     

    • Locate the first line of our goButton_Click method
    • Right-click in the line and select Run to Cursor

    The application will compile, deploy and get launched on your device

    While I, personally, find using Run to Cursor to be a very efficient means of getting to the desired location in my code, there is another important reason
    to use this technique. In the Beta 2 release of Visual Studio 2005, there is a known issue involving attaching the native device debugger while breakpoints
    are active in managed code. If you have any managed breakpoints, please be sure to disable them before detaching the native debugger. When you re-attach
    the managed debugger, you can re-enable the breakpoints. This issue will not occur in the final release of Visual Studio 2005.
     

  3. Click the Go button
    Since the Run to Cursor feature creates a one time breakpoint in your code, you will stop in the debugger at the first line of the getStarted_Click method.
     

  4. Debug the managed code
    You can now step through the goButton_Click method, examine variables using the Autos window, etc.
     

  5. Tell Visual Studio 2005 where to find the native DLL symbols
    The simplest way to do this is to use the Modules window while debugging the managed application.
     

    • On the Debug menu, select Windows and then Modules
    • In the Modules window, right-click and select Symbol Settings
    • Set the path to the native library's symbol (pdb) file
       
  6. Detach the managed debugger engine by selecting Detach All on the Debug menu
    At this point, you may see a message stating that a "fatal" debugger error has occurred, as shown below.

    A fatal error has occurred and debugging needs to be terminated. For more details, please see the Microsoft Help and Support web site. HRESULT=0x80072746. ErrorCode=0x0.
    This message is a known issue in the Beta 2 release of Visual Studio 2005 / .NET Compact Framework v2 and will not be present in the final release. You can
    safely click Ok and continue.

    Your device will now display the "Attach Native Debugger Now" message.
     

  7. Attach the native debugger engine
    The steps to attach the native debugger are very similar to those I describe in this post.

    • On the Tools menu, select Attach to Process
    • Set the Transport to "Smart Device"
    • Set the Qualifier to your device type (ex: "Pocket PC 2003 SE Emulator")
    • Select the Native debugger
      • Click the Select button
      • In the Select Code Type dialog, select Debug these code types and select Native (Smart Device)
      • Click OK
    • Highlight your application in the Available Processes list
    • Click the Attach button

    Since you can only have one debugger attached to a process, selecting the Native debugger will automatically disable debugging managed code.
     

  8. Set a breakpoint in the GetBatteryState function
     

  9. Continue the application by clicking the Continue button in the assert dialog (on your device)
    The application will continue and call the GetBatteryInfo function and the debugger will stop in your native code.
     

  10. Debug the native code
    You can now step through the GetBatteryState function, examine variabled using the Autos window, etc.
     

  11. Detach the native debugger engine by selecting Detach All on the Debug menu
     

  12. Attach the managed debugger engine
     

  13. Resume debugging the managed application by clicking the Debug button in the assert dialog (on your device)
    You can now step through your managed code, set breakpoints, examine variable contents, etc.
    Note: If the Continue button is clicked, the application will resume and will not stop in the debugger.
     

  14. Disable Attach to Process Support
    Once you are finished debugging, I recommend disabling Attach to Process support. Please see this post for details.

As you can see, the steps to debug both the managed and native code in a P/Invoke call are pretty straight forward. Debugging calls to methods on native COM
objects is done in the same way.

Take care,
-- DK

[Edit: fix links]

Disclaimer(s):
This posting is provided "AS IS" with no warranties, and confers no rights.
Some of the information contained within this post may be in relation to beta software. Any and all details are subject to change.

Comments