Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An Introduction to P/Invoke and Marshaling on the Microsoft .NET Compact Framework

0.00/5 (No votes)
19 Jan 2004 1  
An introduction to P/Invoke and Marshaling on the Microsoft .NET Compact Framework.

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        ' \Windows\Start Menu\Programs
    PERSONAL = 5        ' \My Documents
    STARTUP = 7         ' \Windows\StartUp
    STARTMENU = &HB     ' \Windows\Start Menu
    FONTS = &H14        ' \Windows\Fonts
    FAVORITES = &H16    ' \Windows\Favorites
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( //the rest of the declaration
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()
      ' Prevents creation of the class
    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
        ' API Error so retrieve the error number
        Dim errorNum As Integer = Marshal.GetLastWin32Error()
        HandleCeError(New WinCeException( _
         "SHGetSpecialFolderPath returned False, " & _
         "likely an invalid constant", errorNum), _
         "GetSpecialFolderPath")
      End If

      Return sPath
    End Function
    ' Other methods
  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)

   ' Do any logging here

  ' Swallow the exception if asked
  If Not ExceptionsEnabled Then
    Return
  End If

  If TypeOf ex Is NotSupportedException Then
    ' Bad arguments or incorrectly declared
    Throw New WinCeException( _
     "Bad arguments or incorrect declaration in " & method, 0, ex)
  End If

  If TypeOf ex Is MissingMethodException Then
    ' Entry point not found
    Throw New WinCeException( _
     "Entry point not found in " & method, 0, ex)
  End If

  If TypeOf ex Is WinCeException Then
    Throw ex
  End If

  ' All other exceptions
  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
    ' other stuff
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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here