Platform Invoke for calling native code from C# or VB.NET category 'KB', language C# and VB.NET, created 20-Sep-2009, version V1.2 (17-Dec-2011), by Luc Pattyn |
|
License: The author hereby grants you a worldwide, non-exclusive license to use and redistribute the files and the source code in the article in any way you see fit, provided you keep the copyright notice in place; when code modifications are applied, the notice must reflect that. The author retains copyright to the article, you may not republish or otherwise make available the article, in whole or in part, without the prior written consent of the author. Disclaimer: This work is provided |
In the Microsoft .NET Framework, Platform Invoke or P/Invoke is the name of the technology one would use to call unmanaged code (e.g. native C) from managed code languages such as C# and VB.NET. This article will attempt to provide the basic information on this very vast subject. It will not cover C++/CLI, the .NET managed C++ dialect, as I am unfamiliar with it, and its way of interacting with native code is or can be quite different.
This article is not complete; new information gets added when a need arises, however completeness will never be achieved since the subject is vast.
There are two typical situations where the managed/unmanaged boundary needs crossing:
The overall approach is "Keep it Simple". Simple interop jobs can be solved with a few keywords, without ressorting to special tricks, such as the use of "unsafe code". Nevertheless we will have to touch a lot of keywords, attributes and flags to cover some of the more complex jobs.
Here is the official MSDN P/Invoke tutorial.
Note: a lot of hyperlinks are included; the web pages or web sites they refer to may have various quality levels. Don't shoot the messenger.
Calling the Win32 function GetDiskFreeSpaceEx which takes one input parameter and provides three output values:
// this is the C API, copied from the documentation
BOOL WINAPI GetDiskFreeSpaceEx(
__in_opt LPCTSTR lpDirectoryName,
__out_opt PULARGE_INTEGER lpFreeBytesAvailable,
__out_opt PULARGE_INTEGER lpTotalNumberOfBytes,
__out_opt PULARGE_INTEGER lpTotalNumberOfFreeBytes
);
// managed code (C#)
using System.Runtime.InteropServices; // in C# this statement is always required for P/Invoke
class Test {
public void Test1() {
// getting disk space information for C:
string name=@"C:\";
long lpFreeBytesAvailable;
long lpTotalNumberOfBytes;
long lpTotalNumberOfFreeBytes;
GetDiskFreeSpaceEx(name, out lpFreeBytesAvailable, out lpTotalNumberOfBytes, out lpTotalNumberOfFreeBytes);
Console.WriteLine("lpFreeBytesAvailable = "+lpFreeBytesAvailable+" bytes");
}
// this is the prototype
[DllImport("kernel32.dll")]
public static extern int GetDiskFreeSpaceEx(string rootPathName,
out long lpFreeBytesAvailable, out long lpTotalNumberOfBytes, out long lpTotalNumberOfFreeBytes);
}
' managed code (VB.NET)
class Test
Public Sub Test1()
Dim name As String = "C:\"
Dim lpFreeBytesAvailable As Long
Dim lpTotalNumberOfBytes As Long
Dim lpTotalNumberOfFreeBytes As Long
GetDiskFreeSpaceEx(name, lpFreeBytesAvailable, lpTotalNumberOfBytes, lpTotalNumberOfFreeBytes)
Console.WriteLine("lpFreeBytesAvailable = " & lpFreeBytesAvailable & " bytes")
End Sub
' this is the prototype
<DllImport("kernel32.dll")> _
Public Shared Sub GetDiskFreeSpaceEx(ByVal rootPathName As String, _
ByRef lpFreeBytesAvailable As Long, ByRef lpTotalNumberOfBytes As Long, ByRef lpTotalNumberOfFreeBytes As Long)
End Sub
' this is an alternative syntax for the same prototype
Declare Sub GetDiskFreeSpaceEx Lib "kernel32.dll" (ByVal rootPathName As String, _
ByRef lpFreeBytesAvailable As Long, ByRef lpTotalNumberOfBytes As Long, ByRef lpTotalNumberOfFreeBytes As Long)
End Class
The prototype acts like a declaration, it adds the method to the current class without needing an implementation;
so it is similar to what a function declaration in a .h file would be in a C/C++ environment.
In C# the DllImport
attribute and the keywords static
and extern
are essential;
in VB.NET there are two ways to provide prototype information, they are based on some combination of the
DllImport
attribute and the keywords Declare
, Lib
and Shared
; the
long form (with an empty Sub or Function) is very similar to the C# one, the short form is available only for simple cases;
we will use the shorter form when it fits.
The parameter list, the return type, and the scope attribute can be modified as required or appropriate.
The data types on both sides should match sufficiently; their size should match perfectly, and reals versus integers should be correct; however signed versus unsigned integer may be unimportant.
Here are the most important type relations for numeric data (mostly copied from here):
Native code (e.g. C) | Managed code (e.g. C#) | Description |
LONGLONG, long long | System.Int64, long | 64-bit signed integer |
ULONGLONG, unsigned long long | System.UInt64, ulong | 64-bit unsigned integer |
INT, LONG, BOOL, int, long | System.Int32, int | 32-bit signed integer |
UINT, ULONG, DWORD, | System.UInt32, uint | 32-bit unsigned integer |
SHORT, short | System.Int16, short | 16-bit signed integer |
WORD, unsigned short | System.UInt16, ushort | 16-bit uncsigned integer |
BYTE, unsigned char | System.Byte, byte | 8-bit unsigned integer |
CHAR, char | System.Char, char | 8-bit signed integer, needs CharSet.Ansi |
DOUBLE, double | System.Double, double | 64-bit real |
FLOAT, float | System.Single, float | 32-bit real |
Warning: some keywords have a different meaning on both sides; some examples:
Pointers to a simple value type can be handled quite easily by applying the proper keyword: ref
or out
in C#, ByRef
in VB.NET
Structs will be the topic of part 2.
One should not pass class instances (i.e. objects) to the other side; object code and data is understood well on the side where it belongs, and not on the other side. With two exception: strings and arrays.
Here are the most important type relations for string data:
Native code (e.g. C) | Managed code (e.g. C#) | Description |
LPSTR, char* | System.StringBuilder | needs CharSet.Ansi |
LPCSTR, const char* | System.String | needs CharSet.Ansi |
LPWSTR, wchar_t* | System.StringBuilder | needs CharSet.Unicode |
LPCWSTR, const wchar_t* | System.String | needs CharSet.Unicode |
A writeable string parameter (char*
in C) should be passed as a StringBuilder with sufficient capacity, see
this GetVolumeInformation example:
// get the file system name
public static string GetFileSystemName(string rootPathName) {
StringBuilder volName=new StringBuilder(256);
StringBuilder fsName=new StringBuilder(256);
int OK=GetVolumeInformation(rootPathName, volName, volName.Capacity,
0, 0, 0, fsName, fsName.Capacity);
if (OK!=0) return fsName.ToString();
return null;
}
[DllImport("kernel32.dll")]
public static extern int GetVolumeInformation( // BOOL
string rootPathName, // LPCTSTR root directory
StringBuilder lpVolumeNameBuffer, // LPTSTR volume name buffer
int nVolumeNameSize, // DWORD length of name buffer
int lpVolumeSerialNumber, // LPDWORD volume serial number
int lpMaximumComponentLength, // LPDWORD maximum file name length
int lpFileSystemFlags, // LPDWORD file system options
StringBuilder lpFileSystemNameBuffer, // LPTSTR file system name buffer
int nFileSystemNameSize); // DWORD length of file system name buffer
' get the file system name
Private Function GetFileSystemName(ByVal name As String) As String
Dim volName As StringBuilder = New StringBuilder(256)
Dim fsName As StringBuilder = New StringBuilder(256)
Dim OK As Boolean = Kernel32.GetVolumeInformation(name, volName, volName.Capacity, _
0, 0, 0, fsName, fsName.Capacity)
If (OK) Return fsName.ToString()
Return Nothing
End Sub
Declare Auto Function GetVolumeInformation Lib "kernel32.dll" _
(ByVal rootPathName As String, _
ByVal lpVolumeNameBuffer As StringBuilder, _
ByVal nVolumeNameSize As Integer, _
ByVal lpVolumeSerialNumber As Integer, _
ByVal lpMaximumComponentLength As Integer, _
ByVal lpFileSystemFlags As Integer, _
ByVal lpFileSystemNameBuffer As StringBuilder, _
ByVal nFileSystemNameSize As Integer) As Boolean
Strings marshalling is controlled by the DllImport CharSet attribute in C#, and by the
Auto
, ANSI
or Unicode
keywords in VB.NET; it takes care of the
ANSI-Unicode divide.
I'm only considering one-dimensional arrays of some elementary value type allocated by managed code. Higher-dimensioned arrays, and arrays of objects, are to be avoided; and in a mixed world I strongly recommend objects get created by managed code, this avoids the need for unmanaged objects to be upgraded to managed objects.
The main concern here is not to copy the data, but really passing a pointer, and doing so in a safe way; remember, the managed world has a garbage collector which may run at any time, and might move objects around. We will present up to three ways for passing arrays. The first one pins the array automatically, the other two (only one in VB.NET) do it explicitly:
public int ArrayAutomatic() {
int dim=1000;
int[] numbers=new int[dim];
...
int sum=SumArray(numbers, dim);
return sum;
}
[DllImport("native.dll")]
unsafe public static extern int SumArray(int[] numbers, int count);
Public Function ArrayAutomatic() As Integer
Dim dime As Integer = 1000
Dim numbers(dime) As Integer
...
Dim sum As Integer = SumArray(numbers, dime)
Return sum
End Function
Declare Auto Function SumArray Lib "NativeC.dll" (ByVal numbers As Integer(), ByVal count As Integer) As Integer
fixed
keyword (C# only): it requires the unsafe
keyword, and the "allow unsafe code" compiler switch;
it fixes the object, and allows you to get its pointer.
The advantage is the pointer can be modified before being passed on, so it is possible to pass less than the full array to the native world.
Here is a C# example (as VB.NET does not have pointers, this approach is not available in VB.NET):
unsafe public int ArrayFixed() {
int dim=1000;
int[] numbers=new int[dim];
...
int sum;
fixed (int* pNumbers=&numbers[0]) {
sum=SumArray(pNumbers, dim);
}
return sum;
}
[DllImport("native.dll")]
unsafe public static extern int SumArray(int* pNumbers, int count);
GCHandle
class: it does not require any unsafe stuff; one needs to pin the object, get the pointer, and when done
to unpin the object; the advantage here is the GCHandle can be kept around much longer, e.g. when the native world wants to keep the
pointer alive and use it after the function has returned, maybe to perform some asynchronous input/output. Here is a C# example:
public int ArrayHandle() {
int sum=0;
int dim=10000;
int[] numbers=new int[dim];
...
GCHandle handle=GCHandle.Alloc(numbers, GCHandleType.Pinned);
sum=SumArray(handle.AddrOfPinnedObject(), dim);
handle.Free();
return sum;
}
[DllImport("nativeC.dll")]
public static extern int SumArray(IntPtr pNumbers, int count);
Public Function ArrayHandle() As Integer
Dim dime As Integer = 1000
Dim numbers(dime) As Integer
...
Dim handle As GCHandle = GCHandle.Alloc(numbers, GCHandleType.Pinned)
Dim sum As Integer = SumArray(handle.AddrOfPinnedObject(), dime)
handle.Free()
Return sum
End Function
Declare Auto Function SumArray Lib "NativeC.dll" (ByVal pNumbers As IntPtr, ByVal count As Integer) As Integer
Note how the parameter types are different in each case. By the way, you can overload prototypes just like any other methods in
.NET, so it is perfectly legal to provide more than one prototype for any given native function (the examples above will be calling
the appropriate overload of one and the same function, where the first parameter is either an array, a pointer, or an IntPtr
.
I tend to prefer the GCHandle method, as it does not require any unsafe keyword and switch; it also has the advantage one can keep the object pinned for as long as the pointer is in use in the native world (it may store the pointer for future reference, e.g. when asynchronous input/output is going to be performed). There obviously is a risk of forgetting to free the handle when done, which would limit the freedom the garbage collector has. For both methods: when too many objects get pinned down, your app may run into memory problems as there would no longer be a way to reshuffle the objects to consolidate the small free memory blocks in one large one.
Once in a while you want the native code to call a managed method; well, you can pass a delegate and pick it up and use it as a function pointer. The two-side logging chapter offers one example. And here is another typical one, using the Win32 EnumWindows function which requires a "callback function":
BOOL EnumWindows(
WNDENUMPROC lpEnumFunc,
LPARAM lParam
);
BOOL CALLBACK EnumWindowsProc(
HWND hwnd,
LPARAM lParam
);
BOOL IsWindowVisible(
HWND hWnd
);
class DelegateExample {
private List<IntPtr> windowList;
public List<IntPtr> GetVisibleWindowHandles() {
windowList=new List<IntPtr>();
EnumWindows(new EnumWindowsProc(CollectVisibleWindows), IntPtr.Zero);
Console.WriteLine("There are {0} visible windows", windowList.Count);
return windowList;
}
private bool CollectVisibleWindows(IntPtr hWnd, IntPtr lParam) {
if(IsWindowVisible(hWnd)) windowList.Add(hWnd);
return true; // please continue enumeration !
}
[DllImport("user32.dll")]
private static extern int EnumWindows(LP_EnumWindowsProc ewp, IntPtr lParam);
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(IntPtr hWnd);
}
Class DelegateExample
Dim windowList As List(Of IntPtr)
Public Function GetVisibleWindowHandles() As List(Of IntPtr)
windowList = New List(Of IntPtr)
EnumWindows(New EnumWindowsProc(AddressOf CollectVisibleWindows), IntPtr.Zero)
Console.WriteLine("There are {0} visible windows", windowList.Count)
Return windowList
End Function
Public Function CollectVisibleWindows(ByVal hWnd As IntPtr, ByVal lParam As IntPtr) As Boolean
If IsWindowVisible(hWnd) Then windowList.Add(hWnd)
Return True ' please continue enumeration !
End Function
Public Delegate Function EnumWindowsProc(ByVal hWnd As IntPtr, ByVal lParam As IntPtr) As Boolean
Declare Auto Function EnumWindows Lib "user32.dll" (ByVal ewp As User32.EnumWindowsProc, _
ByVal lParam As IntPtr) As Integer
Declare Auto Function IsWindowVisible Lib "user32.dll" (ByVal hWnd As IntPtr) As Boolean
End Class
Some comments, repeating statements made elsewhere in the article:
IntPtr
;lParam
parameter wasn't used.Return values mostly follow the same guidelines as function parameters. However I do not recommend some return types:
IntPtr
for every occurrence.string s=stringBuilder.ToString()
to it. If that is not practical,
consider using Marshal.PtrToStringAnsi()
or one of its siblings; be aware that these methods create a managed copy
but don't free the original unmanaged string.In C# all prototypes need a DllImport
attribute specifying the name of the DLL file;
in VB.NET all prototypes can use a similar approach (however there is an alternative for simple cases).
One normally specifies a simple filename without path; the extension is ".DLL" by default.
DLL files are searched in the standard Windows way, i.e. in the directory of the EXE
first, and in all the folders specified in the environment variable "PATH" next. It would be unwise to specify a path in the
DllImport
statement as that would hamper the usage of the code on another system.
Note 1: P/Invoke can only work on native code DLLs, so the GAC (Global Assembly Cache) is not involved whatsoever.
Note 2: the native DLL files are not used or checked during managed code compilation; at run-time, any DLL file not found,
and any function not found, will result in an Exception being thrown.
Besides the mandatory file name, a prototype may contain more information, however all of that is optional. It gets explained here and here. I'll reproduce the essentials:
CharSet
: controls how strings get marshalled and also how the function name
get mangled (appending an 'A' or 'W').
The default value is CharSet.Ansi
which means all strings automatically get marshalled from managed Unicode to native ANSI,
which is fine if the native code specifies char*
or const char*
. Note: for non-ANSI characters an exception
would be thrown. Use CharSet.Unicode
to get the wide version of a Win32 function, as in
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern MessageBoxResult MessageBox(IntPtr hWnd, string text, string caption, MessageBoxOptions options);
Declare Unicode Function MessageBox Lib "user32.dll" (ByVal hWnd as IntPtr, ByVal text As String, _
ByVal caption As String, ByVal options As MessageBoxOptions) As MessageBoxResult
SetLastError
: is described below.CallingConvention
: chooses how parameters get passed around, and who is responsible for cleaning up the stack;
see here;
the default value is CallingConvention.StdCall
which is fine for Win32, and for native C code. While earlier
versions of .NET show some tolerance to mistakes, starting with .NET 4.0 they'd better be declared accurately.EntryPoint
: specifies the exact name of the function inside the native DLL; this isn't really useful for
a C-coded DLL, but it could be very useful to hide the name mangling that goes on in a C++ DLL, so you could use unmangled
names in the managed world, and apply the mangling through the EntryPoint attribute.There are some more DllImport
options, but I have never felt a need for them.
VB.NET supports a simpler syntax (without DllImport
) which is adequate when all that is required is the
DLL file name and the charset information. It consists of the Declare
and Lib
keywords, optionally
augmented by a Ansi
, Auto
or Unicode
keyword.
Most VB.NET examples in this article use the short version.
Obviously the first thing you should do is read the documentation on the functions you plan on using. MSDN or other sites should be helpful here.
It is a pity Microsoft never decided to provide all the Win32 prototypes in some class. The primary sources of information would be www.pinvoke.net; this site lists lots of Win32 functions and data structures; it is not complete, and there are some errors, so please take it with a grain of salt, and check it against the general principles as laid out in this article.
Popular Win32 functions are
SendMessage and
PostMessage,
which send or post a window message to some window.
They have two special parameters, known as wParam and lParam, which have different meaning depending on the message ID.
When they are pointers, they should be modeled as IntPtr, and not as int or long; the reason is Win32 and Win64
use 4 and 8 bytes respectively, and IntPtr adapts to that automatically, whereas integer types don't. As wParam and/or lParam
sometimes have numeric meaning, one can use IntPtr.Zero
and new IntPtr(intValue)
,
however it makes perfect sense to have several prototypes:
LRESULT SendMessage(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);
private const uint WM_COMMAND = 0x0111;
private const uint LV_VIEW_DETAILS=0x702c;
SendMessage(listviewHandle, WM_COMMAND, new IntPtr(LV_VIEW_DETAILS), IntPtr.Zero);
SendMessage(listviewHandle, WM_COMMAND, LV_VIEW_DETAILS, 0);
// general prototype, can be used for all messages
[DllImport("user32.dll")]
public static extern IntPtr SendMessage(IntPtr Hdc, uint Msg, IntPtr wParam, IntPtr lParam);
// specialised prototype, could be used for int-int messages, such as WM_COMMAND
[DllImport("user32.dll")]
public static extern IntPtr SendMessage(IntPtr Hdc, uint Msg, int wParam, int lParam);
TBD
This example shows one way to use the last error functionality.
Windows keeps an error status for each thread;
most Win32 functions either don't touch it, or set it when something goes wrong.
The error code can be set (normally cleared) by calling SetLastError, and be read by calling GetLastError.
The marshaling logic itself uses these functions, and offers its own error code handling; so managed code should rely on
Marshal.GetLastWin32Error. It is essential to flag the Win32 functions that provide such error status
, by adding SetLastError=true
(BTW: in VB.NET the Declare
keyword does this implicitly). And it seems
LastWin32Error does not accumulate, i.e. Marshal.GetLastWin32Error
returns the error of the last Win32 call,
and not any previous ones.
private void GetPrinterInfo(string name) {
IntPtr hPrinter;
int sizeNeeded=0;
bool OK=OpenPrinter(name, out hPrinter, 0);
if (OK) GetPrinter(hPrinter, infoNumber, IntPtr.Zero, 0, ref sizeNeeded);
if (OK) OK=GetPrinter(hPrinter, infoNumber, pAddr, sizeNeeded, ref sizeNeeded);
if (OK) info2=(PRINTER_INFO_2)Marshal.PtrToStructure(pAddr, typeof(PRINTER_INFO_2));
if (OK) ClosePrinter(hPrinter);
if (!OK) {
int errorCode=Marshal.GetLastWin32Error();
log(FormatMessage(errorCode));
}
}
[DllImport("winspool.drv", SetLastError=true)]
private static extern bool OpenPrinter(string printerName, out IntPtr hPrinter, int printerDefaults);
[DllImport("winspool.drv", SetLastError=true)]
private static extern bool ClosePrinter(IntPtr hPrinter);
[DllImport("winspool.drv", SetLastError=true)]
private static extern bool GetPrinter(IntPtr hPrinter, int level, IntPtr pi2,
int bufSize, ref int sizeNeeded);
// translate an error code to an error message
public static string FormatMessage(int errorCode) {
int capacity=512;
int FORMAT_MESSAGE_FROM_SYSTEM=0x00001000;
StringBuilder sb=new StringBuilder(capacity);
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, IntPtr.Zero, errorCode, 0,
sb, sb.Capacity, IntPtr.Zero);
int i=sb.Length;
if (i>0 && sb[i - 1]==10) i--; // remove final LF character
if (i>0 && sb[i - 1]==13) i--; // remove final CR character
sb.Length=i;
return sb.ToString();
}
[DllImport("kernel32.dll")]
public static extern int FormatMessage(int dwFlags, IntPtr lpSource, int dwMessageId,
int dwLanguageId, StringBuilder lpBuffer, int nSize, IntPtr Arguments);
TBD
When in charge of the native and the managed code, the first thing I do is implement a single logging mechanism available on both sides. This is how it works:
log(string)
or log(int, string)
method which takes a one-line string
(and an optional thread ID) and appends it to the log;
this log can be a text file, or a ListBox, or whatever you choose to store and/or visualize a number of chronological messages for
viewing and analyzing while debugging the application.log(string)
function, which offers the same functionality; technically the only
thing it does is pass its parameter to the managed log method, by using a delegate. That is how I manage to intertwine native and
managed log messages.This could be a log output, the first and last lines are from the managed code (showing thread ID=09), the middle three (thread ID faked to zero) are from the native C code:
00:40:05.547 [09] going to call C function that logs
00:40:05.547 [00] NativeLoggingDemo message #12
00:40:05.547 [00] NativeLoggingDemo message #13
00:40:05.547 [00] NativeLoggingDemo message #14
00:40:05.547 [09] returned from C function that logs
This is the actual code that would do so:
// native code (C example)
// WARNING: the __stdcall keyword is essential here
typedef void (__stdcall* LOGHANDLER) (int threadID, const char *msg);
LOGHANDLER logHandler; // this function pointer will hold the C# delegate
// store a delegate for future use
__declspec(dllexport) void NativeSetLogHandler(LOGHANDLER handler) {
logHandler=handler;
}
// log one string using the managed delegate
void log(const char* msg) {
if (logHandler!=0) logHandler(0, msg); // we choose to always use threadID zero for native code
}
__declspec(dllexport) void NativeLoggingDemo(int i) {
char buf[80];
sprintf_s(buf, 80, "NativeLoggingDemo message #%d", i);
log(s);
}
// managed code (C# example)
class LogDemoForm : Form {
// define the logging delegate
public delegate void LogHandler(int threadID, string s);
// keep the delegate alive by storing it here
LogHandler logHandler=new LogHandler(log);
// pass the log delegate to the native code, so it too can use our logging mechanism
protected void initialize() {
NativeSetLogHandler(logHandler);
}
protected void demo() {
log("going to call C function that logs");
for(int i=12; i<15; i++) NativeLoggingDemo(i);
log("returned from C function that logs");
}
// log one string; automatically include the ID of the current thread
protected void log(string s) {
log(Thread.CurrentThread.ManagedThreadId, s);
}
// log one string; insert current time and threadID
// we use the Control.Invoke pattern so the bulk of it always executes on the main thread and can access GUI Controls
protected void log(int threadID, string s) {
if (this.InvokeRequired) {
this.Invoke(logHandler, new object[]{threadID, s});
} else {
threadID=threadID & 0xFF;
s=DateTime.Now.ToString("HH:mm:ss.fff")+" ["+threadID.ToString("X2")+"] "+s;
Console.WriteLine(s);
if (logFile!=null) logFile.WriteLine(s);
if (listBox!=null) listbox.Items.Add(s);
}
}
// prototype of the native function that stores our logging delegate
[DllImport("nativeCode.dll")]
private static extern void NativeSetLogHandler(LogHandler logHandler);
// prototype of the native function that stores our logging delegate
[DllImport("nativeCode.dll")]
private static extern void NativeLoggingDemo(int i);
}
// managed code (C# example)
Class LogDemoForm
Implements Form
' define the logging delegate
Public Delegate Sub LogHandler(ByVal threadID As Integer, ByVal s As String)
' keep the delegate alive by storing it here
Dim logHandler As NativeC.LogHandler
' pass the log delegate to the native code, so it too can use our logging mechanism
protected Sub initialize()
logHandler = New LogHandler(AddressOf log)
NativeSetLogHandler(logHandler)
End Sub
protected Sub demo()
log("going to call C function that logs")
For i As Integer = 12 to 14
NativeLoggingDemo(i)
Next
log("returned from C function that logs")
End Sub
' log one string; automatically include the ID of the current thread
Protected Sub log(string s)
log(Thread.CurrentThread.ManagedThreadId, s)
End Sub
' log one string; insert current time and threadID
' we use the Control.Invoke pattern so the bulk of it always executes on the main thread and can access GUI Controls
Private Sub log(ByVal threadID As Integer, ByVal s As String)
If Me.InvokeRequired Then
Me.Invoke(logHandler, New Object() {threadID, s})
Else
If s.Length > 0 Then
threadID = threadID And 255
Dim threadString As String = " [" & threadID.ToString("X2") & "] "
s = DateTime.Now.ToString("HH:mm:ss.fff") & threadString & s
End If
Console.WriteLine(s)
If logFile IsNot Nothing Then logFile.WriteLine(s)
If listBox IsNot Nothing Then listbox.Items.Add(s)
End If
End Sub
' prototype of the native function that stores our logging delegate
Declare Auto Sub NativeSetLogHandler Lib "NativeC.dll" (ByVal logHandler As LogHandler)
' prototype of the native function that stores our logging delegate
Declare Auto Sub NativeLoggingDemo Lib "NativeC.dll" (ByVal i As Integer)
End Class
Of course more elaborate schemes could be devised (e.g. using different colors on the Console) but the essence is to get a single list of all the messages from both the managed and native side.
.NET uses exceptions to signal most problems that may occur, such as a NullReferenceException when a reference gets used before being assigned a value. When using P/Invoke, some additional problems may occur; they too generate exceptions, and the most popular ones are:
DllNotFoundException
: while refering to a native function in a DLL file, the file can't be found; I expect this
also to show up when the DLL was found but requires another DLL that can't be found.EntryPointNotFoundException
: while refering to a native function in a DLL file, the file was found but didn't
contain the named function; possible causes: name mangling due to string type (append 'A' or 'W'), or
C++ name mangling (C++ normally appends the parameter list signature). You could use a utility such as DUMPBIN to watch
the list of exported functions.AccessViolationException
: a native code memory access has failed, probably because a pointer value was wrong.
Lots of mistakes can cause this, e.g. parameter misalignment (e.g. confusing managed long and native long).When the managed side needs to get some insight in what goes wrong on the native side, then
Marshal.GetExceptionPointers
might come to the rescue; for more information
this could be a good
starting place.
Still in the works: I have prepared a demo application, which is (not yet) available here. It consists of
Both executables have the same functionality; they demonstrate calls to some Win32 functions and calls to the functions provided by NativeC.dll; the three projects have been kept separate and can be built using the Express versions of Visual Studio (C++, C# and VB.NET respectively).
Managed code can call unmanaged code; this may be easy or not so easy depending on the data structures and classes involved. And it may be extremely performant when done properly; the best results can be obtained when you can freely choose the interface specifications, as opposed to being told exactly what the API and data types would be.
Perceler |
Copyright © 2012, Luc Pattyn |
Last Modified 23-Dec-2024 |