Is P/Invoke Dead?
A reader recently asked whether P/Invoke is dead since you can use C++ Interop and avoid re-declaring all the functions and structures that you might already have in system header files.
Firstly, P/Invoke is still extremely useful for languages like C# that can’t consume header files, but then that should be obvious. Secondly, P/Invoke can also be useful for calling functions for which you don’t readily have header and lib files for and avoids resorting to calling LoadLibrary followed by GetProcAddress.
What may not be as obvious is that P/Invoke can still be very useful from C++ for other reasons. Although you can avoid P/Invoke entirely in your C++ projects there are times when the code can actually be simpler if you just use P/Invoke instead. Let’s look at a few examples.
Lets say you’re mixing native (HWND) and managed (Windows.Forms.Control and derived classes) windows in a project. You might be using the Form class as the main window for your application and HWNDs (window handles) to represent windows in another process. So the challenge is to get the window text from an HWND and display it in a Windows.Forms control. Essentially the problem boils down to implementing the following function:
String^ GetWindowText(HWND windowHandle);
Given a window handle, we would like to get the window text as a managed string. As a C++ programmer you might naturally write the following implementation:
String^ GetWindowText(HWND windowHandle)
{
String^ result = String::Empty;
int textLength = ::GetWindowTextLength(windowHandle);
if (0 < textLength)
{
std::vector<wchar_t> buffer(textLength + 1);
if (0 == ::GetWindowText(windowHandle,
&buffer[0],
textLength + 1))
{
throw gcnew ComponentModel::Win32Exception(::GetLastError(),
"The GetWindowText User32 function failed.");
}
result = Runtime::InteropServices::Marshal::PtrToStringUni(IntPtr(&buffer[0]));
}
return result;
}
We start by getting the text length with a call to the GetWindowTextLength function. If the window has any text, we use the indispensable vector class to manage the buffer that the GetWindowText function will use to write the text to. 1 is added to the length of the buffer as the GetWindowText function null-terminates the text written to the buffer. If you neglect this step the text will be truncated. Finally, the PtrToStringUni helper method from the Marshal class is used to convert the null-terminated string into a managed string.
Another solution is to use the marshalling services provided by the CLR for P/Invoke to avoid the native string management and conversion. Consider the following solution:
using namespace Runtime::InteropServices;
[DllImport("User32.dll", CharSet=CharSet::Unicode, SetLastError=true)]
int GetWindowText(HWND windowHandle,
Text::StringBuilder% text,
int count);
String^ GetWindowText(HWND windowHandle)
{
String^ result = String::Empty;
int textLength = ::GetWindowTextLength(windowHandle);
if (0 < textLength)
{
Text::StringBuilder buffer(textLength + 1);
if (0 == GetWindowText(windowHandle,
buffer,
buffer.Capacity))
{
throw gcnew ComponentModel::Win32Exception(::GetLastError(),
"The GetWindowText User32 function failed.");
}
result = buffer.ToString();
}
return result;
}
Here we use the DllImport attribute to indicate to the CLR that the GetWindowText function is implemented by the User32 DLL. The function is prototyped with the .NET Framework’s StringBuilder class to represent the text buffer and a tracking reference is used so we can use stack semantics for the StringBuilder object when calling the function. As you can see, the GetWindowText implementation is very similar to the original. We still use the GetWindowTextLength directly as there is no benefit in using P/Invoke for it. Of course the solution using P/Invoke is 7 lines longer than the first solution and requires that you translate the GetWindowText function into a P/Invoke declaration for the CLR. About the only thing good about this solution is that you’re not using native memory which could become a performance problem in some rare cases. Of course you’d want to profile this to actually make such a conclusion.
Here’s a more compelling example. For a project I was working on a few months back I needed to allow the user to pick computers from the network. I decided to use the Active Directory Object Picker dialog box. If you’ve never had the pleasure of using this little gem, its exposed through the IDsObjectPicker COM interface and can be a challenge to get just right even from C++. Needless to say using P/Invoke from C# would just be nightmare. To make life simpler I wrapped the functionally we needed in a DLL written in native C++ and exposed it with the following function:
HRESULT __stdcall BrowseForComputers(HWND parentWindow,
bool multiSelect,
SAFEARRAY** computers,
bool* dialogResult);
I’ll spare you the details of its implementation. For this function it would be very useful to be able to use P/Invoke since there is quite a bit of overhead involved in calling this function. Firstly we need to deal with COM safe arrays which are really not fun to work with. Secondly we need to check the HRESULT error code for failure. And finally we need to check the dialogResult pointer (if you remember MIDL then just think [out,retval]) to check whether the user pressed OK or Cancel. This is where P/Invoke and managed code really makes me smile. Consider the following example of using this function:
[DllImport("Native.dll", PreserveSig=false, CharSet=CharSet::Unicode)]
bool BrowseForComputers(IntPtr parentWindow,
bool multiSelect,
[MarshalAs(UnmanagedType::SafeArray, SafeArraySubType=VarEnum::VT_VARIANT)]
array<String^>^% computers);
void main()
{
array<String^>^ computers = nullptr;
if (BrowseForComputers(IntPtr::Zero, // parent
true, // multi-select
computers))
{
for each (String^ computer in computers)
{
Console::WriteLine(computer);
}
}
}
Although the declaration of the BrowseForComputers function can take a few moments to get just right, using it is very simple and elegant. The CLR takes care of all the marshalling and error translation.
© 2005 Kenny Kerr