This article has been provided courtesy of MSDN.
Summary
Learn how to use the Platform Invoke (P/Invoke) feature of the .NET Compact Framework.
Contents
Introduction
In order to realize the vision of access to information anywhere, any time, and on any device, developers need robust and feature-rich tools. With the release of the Microsoft .NET Compact Framework and Smart Device Projects in Visual Studio .NET 2003, Microsoft has made it easier for developers to extend their applications to smart devices by leveraging their existing knowledge of the Microsoft .NET Framework.
The .NET Compact Framework targets Pocket PC 2000, 2002, and embedded Microsoft® Windows® CE .NET 4.1 devices, and has been developed as a subset of the desktop Framework. The Compact Framework, coupled with the Smart Device Project support in Visual Studio .NET 2003, allows developers to write managed code in Visual Basic or C# that is executed in a common language runtime analogous to that in the full .NET Framework.
However, as a subset of the .NET Framework, the .NET Compact Framework supports approximately 25% of the types across the entire range of namespaces, in addition to several .NET Compact Framework-specific types for dealing with user input, messaging, and Microsoft® SQL™ Server 2000 Windows CE Edition 2.0. For developers, this means that there will be some functionality that you can only access by dropping down to the operating system (Windows CE) APIs. For example, the notification services included in the Pocket PC 2002 platform have no managed equivalents in the .NET Compact Framework. In addition, there are a variety of third-party and custom DLLs that you may require access to: for example, the APIs to make phone calls and retrieve the call log using a Pocket PC 2002 Phone Edition device.
Luckily for you, the .NET Compact Framework (like its desktop cousin) does support the Platform Invoke (P/Invoke) service. This service allows managed code to invoke unmanaged functions residing in DLLs. Although the .NET Compact Framework supports P/Invoke, it does so a little differently than the full .NET Framework. In this whitepaper and the one to follow, we'll explore using P/Invoke in the Compact Framework, with a focus on the similarities and differences you'll encounter. This paper assumes that you're familiar at a basic level with the full .NET Framework.
Using P/Invoke
Just as in the .NET Framework, using the P/Invoke service in the .NET Compact Framework includes three primary steps: declaration, invocation, and error handling. After describing these steps, we'll take a look at the differences between the P/Invoke service in the .NET Compact Framework and the full .NET Framework.
Declaration
To begin, you must, at design time, tell the .NET Compact Framework which unmanaged function you intend to call. You'll need to include the DLL name (also called the module), the function name (also called the entry point) and the calling convention to use. For example, in order to call the SHGetSpecialFolderPath
function to return the path to various system folders on the Windows CE device, you would declare the function using the DllImportAttribute
; or in VB with either DllImportAttribute
or the Declare
statement.
[VB]
Private Declare Function SHGetSpecialFolderPath Lib "coredll.dll" ( _
ByVal hwndOwner As Integer, _
ByVal lpszPath As String, _
ByVal nFolder As ceFolders, _
ByVal fCreate As Boolean) As Boolean
[C#]
[DllImport("coredll.dll", SetLastError=true)]
private static extern bool SHGetSpecialFolderPath(
int hwndOwner,
string lpszPath,
ceFolders nFolder,
bool fCreate);
In both cases, you'll notice that the declaration includes the name of the DLL in which the function resides (coredll.dll, in which many of the Windows CE APIs reside, analogous to kernel32.dll and user32.dll in the Win32 API) and the name of the function (SHGetSpecialFolderPath
). In addition, the function in VB is marked as Private
, so that it can be called only from within the class in which it is declared. It could also be marked as Public
or Friend
, depending on how your application will call it.
Note: As with full .NET Framework-based applications, it is a best practice to group unmanaged API declarations in a class within an appropriate namespace, such as Quilogy.CeApi
, and expose their functions via shared wrapper methods. This class can be packaged in its own assembly and distributed to all of the developers on your team or in your organization.
In the C# declaration, the static
and extern
keywords are required to indicate that the function is implemented externally and can be called without creating an instance of the class (although the function is also marked as private
so it remains hidden).
You'll also notice that the function requires an argument of type ceFolders
. In actuality, the function requires a CSIDL
value that is a 32-bit integer, and maps to one of several Windows CE API constants. In these cases, it is a best practice to expose the constant values (found in the API documentation on msdn.microsoft.com in an enumeration), as shown in the following snippet.
[VB]
Private Enum ceFolders As Integer
PROGRAMS = 2 PERSONAL = 5 STARTUP = 7 STARTMENU = &HB FONTS = &H14 FAVORITES = &H16 End Enum
Just as with the .NET Framework, the Declare
statement supports the Alias
clause that allows you to specify that the function has a different name in the DLL. This is useful when the function name in the DLL conflicts with a keyword or another function already defined in your code. The DllImportAttribute
attribute supports this feature as well, through the EntryPoint
property that can be added to the attribute declaration, as follows.
[DllImport("coredll.dll", EntryPoint="SHGetSpecialFolderPath")]
static extern bool GetFolderPath(
Note: The Alias
clause or EntryPoint
property can alternatively contain the ordinal number of the function in the DLL, in the format "#num", as in "#155".
Finally, you'll notice that the SetLastError
property is set to true
in the C# declaration (it is false
by default). This specifies that the common language runtime calls Windows CE GetLastError
function to cache the error value returned, so that other functions don't override the value. You can then safely retrieve the error using Marshal.GetLastWin32Error
. In the Declare
statement, true
is assumed for this value, as it is when using DllImportAttribute
from VB.
Invocation
Once you've correctly declared the function to call (typically in a utility class), you can wrap the function call in a method of that class. The normal technique used is to declare the wrapper function as a Public Shared
(or static
in C#) method of the class, as shown here.
Namespace Quilogy.CeApi
Public Class FileSystem
Private Sub New()
End Sub
Public Shared Function GetSpecialFolderPath( _
ByVal folder As ceFolders) As String
Dim sPath As String = New String(" "c, MAX_PATH)
Dim i As Integer
Dim ret As Boolean
Try
ret = SHGetSpecialFolderPath(0, sPath, folder, False)
Catch ex As Exception
HandleCeError(ex, "GetSpecialFolderPath")
Return Nothing
End Try
If Not ret Then
Dim errorNum As Integer = Marshal.GetLastWin32Error()
HandleCeError(New WinCeException( _
"SHGetSpecialFolderPath returned False, " & _
"likely an invalid constant", errorNum), _
"GetSpecialFolderPath")
End If
Return sPath
End Function
End Class
End Namespace
In this case, the call to SHGetSpecialFolderPath
is wrapped in the GetSpecialFolderPath
method of the Quilogy.CeApi.FileSystem
class, and is responsible for passing the correct parameters to the DLL function and handling any errors. As in this example, it is a best practice to expose only those arguments in the wrapper that the client needs to supply. The remainder can be defaulted to appropriate values. You'll notice that the class also contains a private constructor to prevent the creation of instances of the class.
The method can then be invoked by a caller like so:
Imports Quilogy
...
Dim docs As String
docs = CeApi.FileSystem.GetSpecialFolderPath(ceApi.ceFolders.PERSONAL)
When this code is just-in-time (JIT) compiled at runtime, the P/Invoke service of the common language runtime will extract the Declare
or DllImportAttribute
definition from the metadata in the assembly, locate and load the DLL containing the function into memory, and then retrieve the address of the function using the entry point information. If all goes well, the function will then be invoked, its arguments marshaled, and any return values passed back to the caller.
Handling Errors
Although developers never expect their code to generate runtime errors, it is important to remember that functions invoked on DLLs using P/Invoke can generate two different kinds of errors.
The first is an exception generated by the PInvoke service itself. This occurs if the arguments passed to the method contain invalid data, or if the function itself is declared with improper arguments. In this case, a NotSupportedException
will be thrown. If this occurs, you should re-examine your declaration to determine if it matches the actual DLL definition. Alternatively, P/Invoke may throw a MissingMethodException
, which, as the name implies, is produced if the entry point cannot be found. In the GetSpecialFolderPath
method, these exception are trapped in a Try Catch
block and then passed to another custom method called HandleCeError
. This method inspects the type of exception passed, and then throws a custom exception of type WinCeException
, derived from ApplicationException
, with the appropriate message, both of which are shown in the following listing. This technique of exception wrapping is effective in the .NET Compact Framework, since it centralizes error handling and allows custom members (such as the Windows CE error number) to be added to the custom exception classes.
Private Shared Sub HandleCeError(ByVal ex As Exception, _
ByVal method As String)
If Not ExceptionsEnabled Then
Return
End If
If TypeOf ex Is NotSupportedException Then
Throw New WinCeException( _
"Bad arguments or incorrect declaration in " & method, 0, ex)
End If
If TypeOf ex Is MissingMethodException Then
Throw New WinCeException( _
"Entry point not found in " & method, 0, ex)
End If
If TypeOf ex Is WinCeException Then
Throw ex
End If
Throw New WinCeException( _
"Miscellaneous exception in " & method, 0, ex)
End Sub
Public Class WinCeException : Inherits ApplicationException
Public Sub New()
End Sub
Public Sub New(ByVal message As String)
MyBase.New(message)
End Sub
Public Sub New(ByVal message As String, ByVal apiError As Integer)
MyBase.New(message)
Me.APIErrorNumber = apiError
End Sub
Public Sub New(ByVal message As String, _
ByVal apiError As Integer, ByVal innerexception As Exception)
MyBase.New(message, innerexception)
Me.APIErrorNumber = apiError
End Sub
Public APIErrorNumber As Integer = 0
End Class
You'll notice that the HandleCeError
method first inspects the shared field, ExceptionsEnabled
, and, if false
, simply returns from the method. This shared field allows callers to specify whether exceptions will be thrown from methods in the class.
Note: The error strings shown in the previous code listing can alternatively be placed in a resource file, packaged in a satellite assembly, and retrieved dynamically using ResourceManager
class, as described in the VS .NET help.
The second type of error that might be produced is an error returned from the DLL function itself. In the case of the GetSpecialFolderPath
method, this occurs when SHGetSpecialFolderPath
returns False
(if the enumeration contained an invalid or unsupported constant for example). If this is the case, the method retrieves the error number using GetLastWin32Error
and passes the error number to the HandleCeError
method through its overloaded constructor.
.NET Compact Framework Differences
Although the declarations shown above are the same in the .NET Compact Framework as in the full .NET Framework (with the exception of the module name), there are several subtle differences.
- All Unicode, All the Time. On the full .NET Framework, the default character set, which controls the marshaling behavior of string parameters and the exact entry point name to use (whether P/Invoke appends an "A" for ANSI or a "W" for Unicode, depending on the
ExactSpelling
property) can be set using the Ansi
, Auto
, or Unicode
clause in the Declare
statement, and the CharSet
property in DllImportAttribute
. While in the full .NET Framework, this defaults to Ansi
, the .NET Compact Framework only supports Unicode
, and consequently only includes the CharSet.Unicode
(and CharSet.Auto
which equals Unicode
) value, and does not support any of the clauses of the Declare
statement. This means that the ExactSpelling
property is also not supported. As a result, if your DLL function expects an ANSI string, you'll need to perform the conversion in the DLL, or convert the string to a byte array using the overloaded GetBytes
method of the ASCIIEncoding
class, before calling the function, since the .NET Compact Framework will always pass a pointer to the Unicode string.
- One Calling Convention. The full .NET Framework supports three different calling conventions (which determine issues such as the order in which arguments are passed to the function, and who is responsible for cleaning the stack), using the
CallingConvention
enumeration used in the CallingConvention
property of DllImportAttribute
. With the .NET Compact Framework, however, only the Winapi
value (the default platform convention) is supported, which defaults to the calling convention for C and C++ referred to as Cdecl.
- Unidirectional. Although parameters can be passed to a DLL function by value or by reference, allowing the DLL function to return data to .NET Compact Framework application, P/Invoke on the .NET Compact Framework does not support callbacks as does the full .NET Framework. On the full .NET Framework, callbacks are supported through the use of delegates (object-oriented function pointers) passed to the DLL function. The DLL function then calls the managed application at the address of the delegate with the results of the function. A typical example of using callbacks is the
EnumWindows
API function, that can be used to enumerate all the top-level windows and pass their handles to a callback function.
- Different Exceptions. On the full .NET Framework, the
EntryPointNotFoundException
and ExecutionEngineException
(usually) will be thrown if the function cannot be located, or if the function was declared incorrectly, respectively. As mentioned earlier, on the .NET Compact Framework, the MissingMethodException
and NotSupportedException
types are thrown.
- Processing Windows Messages. Often, when dealing with operating system APIs, it becomes necessary to pass the handle (
hwnd
) to a window to a function, or to add custom processing for messages sent by the operating system. On the full .NET Framework, the Form
class exposes a Handle
property to satisfy the former requirement, and the ability to override the DefWndProc
method to handle the latter. In fact, these members are exposed by the Control
base class, and so this capability is available to all classes, including ListBox
, Button
, and so on, that derive from Control
. The Form
class in the .NET Compact Framework contains neither, but does include MessageWindow
and Message
classes in the Microsoft.WindowsCE.Forms
namespace. The MessageWindow
class can be inherited, and its WndProc
method overridden, to catch specific messages of type Message
. .NET Compact Framework-based applications can even send messages to other windows using the SendMessage
and PostMessage
methods of the MessageWindow
class. For example, PostMessage
can be used to broadcast a custom message to all top-level windows, when checking to determine if the .NET Compact Framework-based application is already running, as in the example code posted by Jonathan Wells on smartdevices.microsoftdev.com.
Marshaling Data
During the invocation process, the P/Invoke service is responsible for marshaling the parameter values passed to the DLL function. This component of P/Invoke is often referred to as the marshaler. In this section, we'll discuss the work of the marshaler in dealing with common types, strings, structures, non-integral types and a few other issues.
Blittable Types
Fortunately, many of the types you'll use to call DLL functions will have a common representation in both the .NET Compact Framework and unmanaged code. These types are referred to as blittable types, and can be seen in the following table.
Compact Framework type |
Visual Basic keyword |
C# keyword |
System.Byte |
Byte |
byte |
System.SByte |
n/a |
sbyte |
System.Int16 |
Short |
short |
System.UInt16 |
n/a |
ushort |
System.Int32 |
Integer |
int |
System.Int64 |
Long (only ByRef ) |
long (only ref ) |
System.UInt64 |
n/a |
ulong |
System.IntPtr |
n/a |
* using unsafe |
In other words, the marshaler doesn't need to perform any special handling of parameters defined with these types in order to convert them between managed and unmanaged code. In fact, by extension, the marshaler needn't convert one-dimensional arrays of these types, or even structures and classes that contain only these types.
While this behavior is the same on the full .NET Framework, the .NET Compact Framework also includes System.Char
(Char
in VB, char
in C#), System.String
(String
in VB, string
in C#), and System.Boolean
(Boolean
in VB, bool
in C#) as blittable types. In the case of System.Char
and System.String
, this is due to the fact that, as mentioned previously, the .NET Compact Framework only supports Unicode, and so the marshaler always marshals the former as a 2-byte Unicode char and the latter as a Unicode array. In the case of System.Boolean
, the .NET Compact Framework marshaler uses a 1-byte integer value, whereas the full .NET framework uses a 4-byte integer value.
Warning: Although System.String
is a blittable type in the .NET Compact Framework, it is not blittable when used inside a complex object, such as a structure or class. This situation will be explored in detail in a later paper.
Obviously, sticking to these types in your .NET Compact Framework applications leads to simpler coding on your part. Another simple example of using blittable types is calling the CeRunAppAtEvent
function, to allow a .NET Compact Framework application to be started when ActiveSync data synchronization finishes.
Public Enum ceEvents
NOTIFICATION_EVENT_NONE = 0
NOTIFICATION_EVENT_TIME_CHANGE = 1
NOTIFICATION_EVENT_SYNC_END = 2
NOTIFICATION_EVENT_DEVICE_CHANGE = 7
NOTIFICATION_EVENT_RS232_DETECTED = 9
NOTIFICATION_EVENT_RESTORE_END = 10
NOTIFICATION_EVENT_WAKEUP = 11
NOTIFICATION_EVENT_TZ_CHANGE = 12
End Enum
Public Class Environment
Private Sub New()
End Sub
<DllImport("coredll.dll", SetLastError:=True)> _
Private Shared Function CeRunAppAtEvent(ByVal appName As String, _
ByVal whichEvent As ceEvents) As Boolean
End Function
Public Shared Function ActivateAfterSync() As Boolean
Dim ret As Boolean
Try
Dim app As String
app = _
System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase
ret = CeRunAppAtEvent(app, ceEvents.NOTIFICATION_EVENT_SYNC_END)
If Not ret Then
Dim errorNum As Integer = Marshal.GetLastWin32Error()
HandleCeError(New WinCeException( _
"CeRunAppAtEvent returned false", errorNum), _
"ActivateAfterSync")
End If
Return ret
Catch ex As Exception
HandleCeError(ex, "ActivateAfterSync")
Return False
End Try
End Function
Public Shared Function DeactivateAfterSync() As Boolean
Dim ret As Boolean = False
Try
Dim app As String
app = _
System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase
ret = CeRunAppAtEvent(app, ceEvents.NOTIFICATION_EVENT_NONE)
If Not ret Then
Dim errorNum As Integer = Marshal.GetLastWin32Error()
HandleCeError(New WinCeException( _
"CeRunAppAtEvent returned false", errorNum), _
"DeactivateAfterSync")
End If
Return ret
Catch ex As Exception
HandleCeError(ex, "DeactivateAfterSync")
Return False
End Try
End Function
End Class
Note: After the ActivateAfterSync
method is called, an instance of the application will be automatically started with a specific command-line parameter after the ActiveSync synchronization ends. Additional code is typically added to a smart device project in order to detect the command-line and activate an existing instance of the application.
In this example, as in SHGetSpecialFolderPath
, all of the arguments and the return value are blittable types, so no extra work on your part or the marshaler's needs be done. You'll notice that in this case, the ceEvents
enumeration is marshaled as the underlying value type, the blittable System.Int32
by default. It should be noted, however, that the marshaler can only work with return types that are 32 bits or less.
Passing Strings
As just mentioned, strings in the .NET Compact Framework are blittable types, and are represented to unmanaged functions as null-terminated arrays of Unicode characters. At invocation time, since System.String
is a reference type (it is allocated on the managed heap, and its address is stored in the reference variable), and even though it is being passed by value, the marshaler passes a pointer to the string to the unmanaged function, as was done in the case of both SHGetSpecialFolder
and CeRunAppAtEvent
.
Note: The .NET Compact Framework always passes a pointer to a reference type and does not support passing reference types by reference (ByRef
in VB, ref
in C#).
As you have no doubt noticed, however, SHGetSpecialFolder
expects a fixed-length string buffer, which it can fill with the path, while CeRunAppAtEvent
simply reads the string. In the former case, the fixed-length is declared as follows (the c
denotes a conversion to a System.Char
).
Dim sPath As String = New String(" "c, MAX_PATH)
Since a pointer to the string is passed to the unmanaged function, the unmanaged code sees the string as WCHAR *
(or TCHAR *
, LPTSTR
, or possibly LPSTR
), and can manipulate the string using the pointer. When the function completes, the caller can inspect the string as normal.
This behavior is quite different from the full .NET Framework, where the marshaler must take the character set into consideration. As a result, the full .NET Framework does not support passing strings by value or by reference into unmanaged functions and allowing the unmanaged function to modify the contents of the buffer.
To solve this problem (since many of the Win32 APIs expect string buffers) in the full .NET Framework, you can, instead, pass a System.Text.StringBuilder
object; a pointer will be passed by the marshaler into the unmanaged function that can be manipulated. The only caveat is that the StringBuilder
must be allocated enough space for the return value, or the text will overflow, causing an exception to be thrown by P/Invoke.
It turns out that StringBuilder
can also be used in the exact same way on the .NET Compact Framework, so the declaration of SHGetSpecialFolderPath
could be changed to:
Private Declare Function SHGetSpecialFolderPath Lib "coredll.dll" ( _
ByVal hwndOwner As Integer, _
ByVal lpszPath As StringBuilder, _
ByVal nFolder As ceFolders, _
ByVal fCreate As Boolean) As Boolean
And the calling syntax to:
Dim sPath As New StringBuilder(MAX_PATH)
ret = SHGetSpecialFolderPath(0, sPath, folder, False)
In fact, in this specific case, it is more efficient to change to a StringBuilder
, since, when using the Declare
statement in VB, the ByVal String
parameter will be marshaled as an out
parameter. Using an out
parameter forces the common language runtime to create a new String
object when the function returns before returning the new reference. The DllImportAttribute
does not cause this behavior.
In any case, it is recommended that you use StringBuilder
when dealing with fixed-length string buffers, since they are easier to initialize and more consistent with the full .NET Framework.
Passing Structures
As mentioned previously, you can pass structures to unmanaged functions without worrying, as long as the structure contains blittable types. For example, the GlobalMemoryStatus
Windows CE API is passed a pointer to a MEMORYSTATUS
structure that it populates, to return information on the physical and virtual memory of the device. Since the structure contains only blittable types (unmanaged DWORD
values that translate to unsigned 32 bit integers, System.UInt32
, in the .NET Compact Framework), the function can be called easily, as shown in the following snippet.
Private Structure MEMORY_STATUS
Public dwLength As UInt32
Public dwMemoryLoad As UInt32
Public dwTotalPhys As UInt32
Public dwAvailPhys As Integer
Public dwTotalPageFile As UInt32
Public dwAvailPageFile As UInt32
Public dwTotalVirtual As UInt32
Public dwAvailVirtual As UInt32
End Structure
<DllImport("coredll.dll", SetLastError:=True)> _
Private Shared Sub GlobalMemoryStatus(ByRef ms As MEMORY_STATUS)
End Sub
Public Shared Function GetAvailablePhysicalMemory() As String
Dim ms As New MEMORY_STATUS
Try
GlobalMemoryStatus(ms)
Dim avail As Double = CType(ms.dwAvailPhys, Double) / 1048.576
Dim sAvail As String = String.Format("{0:###,##}", avail)
Return sAvail
Catch ex As Exception
HandleCeError(ex, "GetAvailablePhysicalMemory")
Return Nothing
End Try
End Function
You'll notice that, in this case, the GetAvailablePhysicalMemory
function instantiates a MEMORY_STATUS
structure and passes it the GlobalMemoryStatus
function. Since the function requires a pointer to the structure (defined as LPMEMORYSTATUS
in Windows CE), the declaration of GlobalMemoryStatus
indicates that the ms
parameter is ByRef
. In C#, both the declaration and the invocation would require the use of the ref
keyword.
Alternatively, this function could have been called by declaring MEMORY_STATUS
as a class rather than a structure. Because the class is a reference type, you wouldn't then need to declare the parameter as by reference, and instead could rely on the fact that the marshaler always passes a 4-byte pointer to the reference type to the unmanaged function.
It is also important to note that reference types are always marshaled in the order in which they appear in managed code. This means that the fields will be laid out in memory, as expected by the unmanaged function. As a result, you don't need to decorate the structure with the StructLayoutAttribute
and LayoutKind.Sequential
, although it is supported, as on the full .NET Framework.
While you can pass structures (and classes) to unmanaged functions, the .NET Compact Framework marshaler does not support marshaling a pointer to a structure returned from an unmanaged function. In these cases, you will need to marshal the structure manually, using the PtrToStructure
method of the Marshal
class.
Finally, one of the major differences between the marshaler in the .NET Compact Framework and that in the full .NET Framework is that the .NET Compact Framework marshaler cannot marshal complex objects (reference types) within structures. This means that if any fields in a structure has types other than those listed in the table shown previously (including strings or arrays of strings), the structure cannot be fully marshaled. This is due to the fact that the .NET Compact Framework does not support the MarshalAsAttribute
used in the full .NET Framework to supply explicit instructions to the marshaler as to how to marshal the data. However, in a later paper, we'll explore how reference types, such as strings embedded in structures, can be passed and returned from unmanaged functions.
Passing Non-Integral Types
You'll notice in the table of blittable types that no mention is made of floating point variables. These types are non-integral (not representing a whole number), and cannot be marshaled by value by the .NET Compact Framework. However, they can be marshaled by reference, and passed as pointers to an unmanaged function that acts as a wrapper or shim.
Note: This is also the case with 64 bit integers (Long
in VB, long
in C#) that are marshaled as a pointer to INT64.
For example, if the unmanaged function accepts two parameters of type double
and returns a double
defined in C as:
double DoSomeWork(double a, double b) { }
then, in order to call the function from the .NET Compact Framework, you would first need to create an unmanaged function, using eMbedded Visual C, that accepts both arguments by reference (as pointers), and returns the result as an output parameter by calling the original function, like so:
void DoSomeWorkShim(double *pa, double *pb, double *pret)
{
*pret = DoSomeWork(pa, pb);
}
You would then declare the DoSomeWorkShim
function in the Compact Framework, using the DllImportAttribute
statement as follows:
<DllImport("ShimFunction.dll")> _
Private Shared Sub DoSomeWorkShim(ByRef a As Double, _
ByRef b As Double, ByRef ret As Double)
End Sub
Finally, you could wrap the call to DoSomeWorkShim
in an additional managed wrapper function that maintains the original calling signature of DoSomeWork
.
Public Shared Function DoSomeWork(ByVal a As Double, _
ByVal b As Double) As Double
Dim ret As Double
DoSomeWorkShim(a, b, ret)
Return ret
End Function
Other Issues
One of the issues that developers worry about when thinking about P/Invoke is its interoperation with the garbage collector (GC) in the common language runtime. Since the garbage collector in both the .NET Compact Framework and the full .NET Framework can rearrange objects on the managed heap when a collection occurs, this could be a problem for an unmanaged function attempting to manipulate memory using a pointer passed to it. Fortunately, the .NET Compact Framework marshaler automatically pins (locks the object in its current memory location) any reference type passed to an unmanaged function for the duration of the call.
A second issue that developers often encounter is the use of the unsafe
and fixed
keywords in C#, in relation to P/Invoke. While this topic will be discussed more thoroughly in a later paper, essentially the unsafe
keyword allows C# code to manipulate memory directly through the use of pointers, just as in C. While this allows for a greater degree of flexibility, it also disables the code verification feature of the common language runtime. This is the feature that allows the common language runtime, during the JIT process, to ensure that the code it executes only reads allocated memory, and that all methods are called with the proper number and type of arguments.
Note: In addition to creating unverifiable code, on the full .NET Framework, unsafe code can only be executed in a trusted environment, although, in version 1.0 of the .NET Compact Framework, code access security (CAS) was not included, and so this is not presently an issue.
The fixed
keyword is used in conjunction with unsafe
to pin a managed object, usually a value type or an array of value types, such as an array of System.Char
, so that the GC cannot move it while it is being used by the unmanaged function.
Conclusion
As you can see, the Platform Invoke feature of the .NET Compact Framework provides a solid subset of the features available on the full .NET Framework. With it, you should be able to fairly easily make the majority of the calls to unmanaged DLLs that your applications require.
For situations that are more complex, such as passing strings embedded in structures, you may need to resort to pointers and memory allocation, the topic of a later whitepaper.
Links