RAD Studio for Microsoft .NET |  |

Using Platform Invoke with Delphi for .NET
Calling Unmanaged Functions | Structures | Callback Functions | Passing Object References | Using COM Interfaces | See Also
Collapse All

This topic describes the basic techniques of using unmanaged APIs. Some of the common mistakes and pitfalls are pointed out, and a quick reference for translating Delphi data types is provided. This topic does not attempt to explain the basics of platform invoke or marshaling data. Please refer to the links at the end of this topic for more information on platform invoke and marshaling. Understanding attributes and how they are used is also highly recommended before reading this document.

The Win32 API is used for several examples. For further details on the API functions mentioned, please see the Windows Platform SDK documentation.

The following topics are discussed in this section:

- Calling unmanaged functions
- Structures
- Callback functions
- Passing Object References
- Using COM Interfaces

Calling Unmanaged Functions

When calling unmanaged functions, a managed declaration of the function must be created that represents the unmanaged types. In many cases functions take pointers to data that can be of variable types. One example of such a function is the Win32 API function SystemParametersInfo that is declared as follows:

Copy Code

    BOOL SystemParametersInfo(
      UINT uiAction,  // system parameter to retrieve or set
      UINT uiParam,   // depends on action to be taken
      PVOID pvParam,  // depends on action to be taken
      UINT fWinIni    // user profile update option
    );

Depending on the value of uiAction, pvParam can be one of dozens of different structures or simple data types. Since there is no way to represent this with one single managed declaration, multiple overloaded versions of the function must be declared (see Borland.Vcl.Windows.pas), where each overload covers one specific case. The parameter pvParam can also be given the generic declaration IntPtr. This places the burden of marshaling on the caller, rather than the built in marshaler. Note that the data types used in a managed declaration of an unmanaged function must be types that the default marshaler supports. Otherwise, the caller must declare the parameter as IntPtr and be responsible for marshaling the data.

Data Types

Most data types do not need to be changed, except for pointer and string types. The following table shows commonly used data types, and how to translate them for managed code:

Unmanaged Data Type | Managed Data Type |
| Input Parameter | Output Parameter
Pointer to string (PChar) | String | StringBuilder
Untyped parameter/buffer | TBytes | TBytes
Pointer to structure (PRect) | const TRect | var TRect
Pointer to simple type (PByte) | const Byte | var Byte
Pointer to array (PInteger) | array of Integer | array of Integer
Pointer to pointer type (^PInteger) | IntPtr | IntPtr

IntPtr can also represent all pointer and string types, in which case you need to manually marshal data using the Marshal class. When working with functions that receive a text buffer, the StringBuilder class provides the easiest solution. The following example shows how to use a StringBuilder to receive a text buffer:

Copy Code

    function GetText(Window: HWND; BufSize: Integer = 1024): string;
    var
      Buffer: StringBuilder;
    begin
      Buffer := StringBuilder.Create(BufSize);
      GetWindowText(Window, Buffer, Buffer.Capacity);
      Result := Buffer.ToString;
    end;

The StringBuilder class is automatically marshaled into an unmanaged buffer and back. In some cases it may not be practical, or possible, to use a StringBuilder. The following examples show how to marshal data to send and retrieve strings using SendMessage:

Copy Code

    procedure SetText(Window: HWND; Text: string);
    var
      Buffer: IntPtr;
    begin
      Buffer := Marshal.StringToHGlobalAuto(Text);
      try
        Result := SendMessage(Window, WM_SETTEXT, 0, Buffer);
      finally
        Marshal.FreeHGlobal(Buffer);
      end;
    end;

An unmanaged buffer is allocated, and the string copied into it by calling StringToHGlobalAuto. The buffer must be freed once it’s no longer needed. To marshal a pointer to a structure, use the Marshal. StructureToPtr method to copy the contents of the structure into the unmanaged memory buffer.

The following example shows how to receive a text buffer and marshal the data into a string:

Copy Code

    function GetText(Window: HWND; BufSize: Integer = 1024): string;
    var
      Buffer: IntPtr;
    begin
      Buffer := Marshal.AllocHGlobal(BufSize * Marshal.SystemDefaultCharSize);
      try
        SendMessage(Window, WM_GETTEXT, BufSize, Buffer);
        Result := Marshal.PtrToStringAuto(Buffer);
      finally
        Marshal.FreeHGlobal(Buffer);
      end;
    end;

It is important to ensure the buffer is large enough, and by using the SystemDefaultCharSize method, the buffer is guaranteed to hold BufSize characters on any system.

Advanced Techniques

When working with unmanaged API’s, it is common to pass parameters as either a pointer to something, or NULL. Since the managed API translations don’t use pointer types, it might be necessary to create an additional overloaded version of the function with the parameter that can be NULL declared as IntPtr.

Special Cases

There are cases where a StringBuilder and even the Marshal class will be unable to correctly handle the data that needs to be passed to an unmanaged function. An example of such a case is when the string you need to pass, or receive, contains multiple strings separated by NULL characters. Since the default marshaler will consider the first NULL to be the end of the string, the data will be truncated (this also applies to the StringToHGlobalXXX and PtrToStringXXX methods). In this situation TBytes can be used (using the PlatformStringOf and PlatformBytesOf functions in Borland.Delphi.System to convert the byte array to/from a string). Note that these utility functions do not add or remove terminating NULL characters.

When working with COM interfaces, the UnmanagedType enumeration (used by the MarshalAsAttribute class) has a special value, LPStruct. This is only valid in combination with a System.Guid class, causing the marshaler to convert the parameter into a Win32 GUID structure. The function CoCreateInstance that is declared in Delphi 7 as:

Copy Code

    function CoCreateInstance([MarshalAs(UnmanagedType.LPStruct)] clsid: TCLSID;
      [MarshalAs(UnmanagedType.IUnknown)] unkOuter: TObject;
      dwClsContext: Longint;
      [MarshalAs(UnmanagedType.LPStruct)] iid: TIID;
      [MarshalAs(UnmanagedType.Interface)] out pv
    ): HResult;

This is currently the only documented use for UnmanagedType.LPStruct.

Structures

The biggest difference between calling unmanaged functions and passing structures to unmanaged functions is that the default marshaler has some major restrictions when working with structures. The most important are that dynamic arrays, arrays of structures and the StringBuilder class cannot be used in structures. For these cases IntPtr is required (although in some cases string paired with various marshaling attributes can be used for strings).

Data Types

The following table shows commonly used data types, and how to “translate” them for managed code:

Unmanaged Data Type | Managed Data Type |
| Input Parameter | Output Parameter
Pointer to string (PChar) | String | IntPtr
Character array (array[a..b] of Char) | String | String
Array of value type (array[a..b] of Byte) | array[a..b] of Byte | array[a..b] of Byte
Dynamic array (array[0..0] of type) | IntPtr | IntPtr
Array of struct (array[1..2] of TRect) | IntPtr or flatten | IntPtr or flatten
Pointer to structure (PRect) | IntPtr | IntPtr
Pointer to simple type (PByte) | IntPtr | IntPtr
Pointer to array (PInteger) | IntPtr | IntPtr
Pointer to pointer type (^PInteger) | IntPtr | IntPtr

When working with arrays and strings in structures, the MarshalAs attribute is used to describe additional information to the default marshaler about the data type. A record declared in Delphi 7, for example:

Copy Code

    type
      TMyRecord = record
        IntBuffer: array[0..31] of Integer;
        CharBuffer: array[0..127] of Char;
        lpszInput: LPTSTR;
        lpszOutput: LPTSTR;
      end;

Would be declared as follows in RAD Studio:

Copy Code

    type
      [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
      TMyRecord = record
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
        IntBuffer: array[0..31] of Integer;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
        CharBuffer: string;
        [MarshalAs(UnmanagedType.LPTStr)]
        lpszInput: string;
        lpszOutput: IntPtr;
      end;

The above declarations assume that the strings contain platform dependant TChar’s (as commonly used by the Win32 API). It is important to note that in order to receive text in lpszOutput, the Marshal. AllocHGlobal method needs to be called before passing the structure to an API function.

A structure can contain structures, but not pointers to structures. For such cases an IntPtr must be declared, and the Marshal. StructureToPtr method used to move data from the managed structure into unmanaged memory. Note that StructureToPtr does not allocate the memory needed (this must be done separately). Be sure to use Marshal. SizeOf to determine the amount of memory required, as Delphi’s SizeOf is not aware of the MarshalAs attribute (in the example above, CharBuffer would be 4 bytes using Delphi’s SizeOf when it in fact should occupies 128 bytes on a single byte system). The following examples show how to send messages that pass pointers to a structure:

Copy Code

    procedure SetRect(Handle: HWND; const Rect: TRect);
    var
      Buffer: IntPtr;
    begin
      Buffer := Marshal.AllocHGlobal(Marshal.SizeOf(TypeOf(TRect)));
      try
        Marshal.StructureToPtr(TObject(Rect), Buffer, False);
        SendMessage(Handle, EM_SETRECT, 0, Buffer);
      finally
        Marshal.DestroyStructure(Buffer, TypeOf(TRect));
      end;
    end;

    procedure GetRect(Handle: HWND; var Rect: TRect);
    var
      Buffer: IntPtr;
    begin
      Buffer := Marshal.AllocHGlobal(Marshal.SizeOf(TypeOf(TRect)));
      try
        SendMessage(Handle, EM_GETRECT, 0, Buffer);
        Rect := TRect(Marshal.PtrToStructure(Buffer, TypeOf(TRect)));
      finally
        Marshal.DestroyStructure(Buffer, TypeOf(TRect));
      end;
    end;

It is important to call DestroyStructure rather than FreeHGlobal if the structure contains fields where the marshaling layer needs to free additional buffers (see the documentation for DestroyStructure for more details).

Advanced topics

Working with unmanaged API’s it is not uncommon to need to convert a byte array into a structure (or retrieve one or more fields from a structure held in a byte array), or vice versa. Although the Marshal class contains a method to retrieve the offset of a given field, it is extremely slow and should be avoided in most situations. Informal performance tests show that for a structure with eight or nine numeric fields, it is much faster to allocate a block of unmanaged memory, copy the byte array to the unmanaged memory and call PtrToStructure than finding the position of just one field using Marshal. OffsetOf and converting the data using the BitConverter class. Borland.Vcl.WinUtils contains helper functions to perform conversions between byte arrays and structures (see StructureToBytes and BytesToStructure).

Special cases

There are cases where custom processing is required, such as sending a message with a pointer to an array of integers. For situations like this, the Marshal class provides methods to copy data directly to the unmanaged buffer, at specified offsets (so you can construct an array of a custom data type after allocating a buffer). The following example shows how to send a message where the LParam is a pointer to an array of Integer:

Copy Code

    function SendArrayMessage(Handle: HWND; Msg: UINT; WParam: WPARAM;
      LParam: TIntegerDynArray): LRESULT;
    var
      Buffer: IntPtr;
    begin
      Buffer := Marshal.AllocHGlobal(Length(LParam) * SizeOf(Integer));
      try
        Marshal.Copy(LParam, 0, Buffer, Length(LParam));
        Result := SendMessage(Handle, Msg, WParam, Buffer);
      finally
        Marshal.FreeHGlobal(Buffer);
      end;
    end;

Callback Functions

When passing a function pointer for a managed function to an unmanaged API, a reference must be maintained to the delegate or it will be garbage collected. If you pass a pointer to your managed function directly, a temporary delegate will be created, and as soon as it goes out of scope (at the end of MyFunction in the example below), it is subject to garbage collection. Consider the following Delphi 7 code:

Copy Code

    function MyFunction: Integer;
    begin
      ...
      RegisterCallback(@MyCallback);
      ...
    end;

In order for this to work in a managed environment, the code needs to be changed to the following:

Copy Code

    const
      MyCallbackDelegate: TFNMyCallback = @MyCallback;

    function MyFunction: Integer;
    begin
      ...
      RegisterCallback(MyCallbackDelegate);
      ...
    end;

This will ensure that the callback can be called as long as MyCallbackDelegate is in scope.

Data types

The same rules apply for callbacks as any other unmanaged API function.

Special cases

Any parameters used in an asynchronous process must be declared as IntPtr. The marshaler will free any memory it has allocated for unmanaged types when it returns from the function call. When using an IntPtr, it is your responsibility to free any memory that has been allocated.

Passing Object References

When working with for example the Windows API, object references are sometimes passed to the API where they are stored and later passed back to the application for processing usually associated with a given event. This can still be accomplished in .NET, but special care needs to be taken to ensure a reference is kept to all objects (otherwise they can and will be garbage collected).

Data types

Unmanaged Data Types | Managed Data Type |
| Supply Data | Receive Data
Pointer (Object reference, user data) | GCHandle | GCHandle

The GCHandle provides the primary means of passing an object references to unmanaged code, and ensuring garbage collection does not happen. A GCHandle needs to be allocated, and later freed when no longer needed. There are several types of GCHandle, GCHandleType.Normal being the most useful when an unmanaged client holds the only reference. In order pass a GCHandle to an API function once it is allocated, type cast it to IntPtr (and optionally onwards to LongInt, depending on the unmanaged declaration). The IntPtr can later be cast back to a GCHandle. Note that IsAllocated must be called before accessing the Target property, as shown below:

Copy Code

    procedure MyProcedure;
    var
      Ptr: IntPtr;
      Handle: GCHandle;
    begin
      ...
      if Ptr <> nil then
      begin
        Handle := GCHandle(Ptr);
        if Handle.IsAllocated then
          DoSomething(Handle.Target);
      end;
      ...
    end;

Advanced techniques

The use of a GCHandle, although relatively easy, is fairly expensive in terms of performance. It also has the possibility of resource leaks if handles aren’t freed correctly. If object references are maintained in the managed code, it is possible to pass a unique index, for example the hash code returned by the GetHashCode method, to the unmanaged API instead of an object reference. A hash table can be maintained on the managed side to facilitate retrieving an object instance from a hash value if needed. An example of using this technique can be found in the TTreeNodes class (in Borland.Vcl.ComCtrls).

Using COM Interfaces

When using COM interfaces, a similar approach is taken as when using unmanaged API’s. The interface needs to be declared, using custom attributes to describe the type interface and the GUID. Next the methods are declared; using the same approach as for unmanaged API’s. The following example uses the IAutoComplete interface, defined as follows in Delphi 7:

Copy Code

    IAutoComplete = interface(IUnknown)
      ['{00bb2762-6a77-11d0-a535-00c04fd7d062}']
      function Init(hwndEdit: HWND; punkACL: IUnknown;
        pwszRegKeyPath: LPCWSTR; pwszQuickComplete: LPCWSTR): HRESULT; stdcall;
      function Enable(fEnable: BOOL): HRESULT; stdcall;
    end;

In RAD Studio it is declared as follows:

Copy Code

    [ComImport, GuidAttribute('00BB2762-6A77-11D0-A535-00C04FD7D062'), InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
    IAutoComplete = interface
      function Init(hwndEdit: HWND; punkACL: IEnumString;
        pwszRegKeyPath: IntPtr; pwszQuickComplete: IntPtr): HRESULT;
      function Enable(fEnable: BOOL): HRESULT;
    end;

Note the custom attributes used to describe the GUID and type of interface. It is also essential to use the ComImportAttribute class. There are some important notes when importing COM interfaces. You do not need to implement the IUnknown/IDispatch methods, and inheritance is not supported.

Data types

The same rules as unmanaged functions apply for most data types, with the following additions:

Unmanaged Data Type | Managed Data Type |
| Supply Data | Receive Data
GUID | System.Guid | System.Guid
IUnknown | TObject | TObject
IDispatch | TObject | TObject
Interface | TObject | TObject
Variant | TObject | TObject
SafeArray (of type) | array of <type> | array of <type>
BSTR | String | String

Using the MarshalAsAttribute custom attribute is required for some of the above uses of TObject, specifying the exact unmanaged type (such as UnmanagedType.IUnknown, UnmanagedType.IDispatch or UnmanagedType.Interface). This is also true for certain array types. An example of explicitly specifying the unmanaged type is the Next method of the IEnumString interface. The Win32 API declares Next as follows:

Copy Code

    HRESULT Next(
      ULONG celt,
      LPOLESTR * rgelt,
      ULONG * pceltFetched
    );

In RAD Studio the declaration would be:

Copy Code

    function Next(celt: Longint;
      [out, MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr, SizeParamIndex = 0)]
      rgelt: array of string;
      out pceltFetched: Longint
    ): Integer;

Advanced techniques

When working with safearrays, the marshal layer automatically converts (for example) an array of bytes into the corresponding safearray type. The marshal layer is very sensitive to type mismatches when converting safearrays. If the type of the safearray does not exactly match the type of the managed array, an exception is thrown. Some of the Win32 safearray API’s do not set the type of the safearray correctly when the array is created, which will lead to a type mismatch in the marshal layer when used from .NET. The solutions are to either ensure that the safearray is created correctly, or to bypass the marshal layer’s automatic conversion. The latter choice may be risky (but could be the only alternative if you don’t have the ability to change the COM server that is providing the data). Consider the following declaration:

Copy Code

    function AS_GetRecords(const ProviderName: WideString; Count: Integer;
      out RecsOut: Integer; Options: Integer; const CommandText: WideString;
      var Params: OleVariant; var OwnerData: OleVariant): OleVariant;

If the return value is known to always be a safearray (that doesn’t describe its type correctly) wrapped in a variant, we can change the declaration to the following:

Copy Code

    type
      TSafeByteArrayData = packed record
        VType: Word;
        Reserved1: Word;
        Reserved2: Word;
        Reserved3: Word;
        VArray: IntPtr;  { This is a pointer to the actual SafeArray }
      end;

    function AS_GetRecords(const ProviderName: WideString; Count: Integer;
      out RecsOut: Integer; Options: Integer; const CommandText: WideString;
      var Params: OleVariant; var OwnerData: OleVariant): TSafeByteArrayData;

Knowing that an OleVariant is a record, the TSafeByteArrayData record can be extracted from Delphi 7’s TVarData (equivalent to the case where the data type is varArray). The record will provide access to the raw pointer to the safearray, from which data can be extracted. By using a structure instead of an OleVariant, the marshal layer will not try to interpret the type of data in the array. You will however be burdened with extracting the data from the actual safearray.

Special cases

Although it is preferred to use Activator.CreateInstance when creating an instance, it is not fully compatible with CoCreateInstanceEx. When working with remote servers, CreateInstance will always try to invoke the server locally, before attempting to invoke the server on the remote machine. Currently the only known work-around is to use CoCreateInstanceEx.

Since inheritance isn’t supported, a descendant interface needs to declare the ancestor’s methods. Below is the IAutoComplete2 interface, which extends IAutoComplete.

Copy Code

    [ComImport, GuidAttribute('EAC04BC0-3791-11d2-BB95-0060977B464C'), InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
    IAutoComplete2 = interface(IAutoComplete)
      // IAutoComplete methods
      function Init(hwndEdit: HWND; punkACL: IEnumString;
        pwszRegKeyPath: IntPtr; pwszQuickComplete: IntPtr): HRESULT;
      function Enable(fEnable: BOOL): HRESULT;
      //
      function SetOptions(dwFlag: DWORD): HRESULT;
      function GetOptions(var dwFlag: DWORD): HRESULT;
    end;

See Also

Marshaling Data with Platform Invoke

Using COM Interop in Managed Applications

Virtual Library Interfaces

Copyright(C) 2008 CodeGear(TM). All Rights Reserved.
What do you think about this topic? Send feedback!