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.
- Enabling Attach to Process support in NetCF v2
- Attaching the Managed Debugger to a Running NetCF v2 Application using Visual Studio 2005
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.
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.
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.
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.
Debug the managed code
You can now step through the goButton_Click method, examine variables using the Autos window, etc.
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
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.
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.
Set a breakpoint in the GetBatteryState function
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.
Debug the native code
You can now step through the GetBatteryState function, examine variabled using the Autos window, etc.
Detach the native debugger engine by selecting Detach All on the Debug menu
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.
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.