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

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

  • Comments 2

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 goButton
Label statusLabel
Label 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 = statusText

End Sub

You will also need to add this structure.

Public Structure MyBatteryInfo
    Dim PluggedIn As AcPowerStatus
    Dim ChargeStatus As BatteryChargeStatus
End 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 = 255
End 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 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")

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. 

Page 1 of 1 (2 items)