.NETInteroperability Between .NET & Win32

Brian Long (www.blong.com)

Table of Contents

Click here to download the files associated with this article.

If you find this article useful then please consider making a donation. It will be appreciated however big or small it might be and will encourage Brian to continue researching and writing about interesting subjects in the future.


Introduction

.NET is a new programming platform representing the future of Windows programming. Developers are moving across to it and learning the new .NET oriented languages and frameworks, but new systems do not appear overnight. It is a lengthy process moving entire applications across to a new platform and Microsoft is very much aware of this.

To this end .NET supports a number of interoperability mechanisms that allow applications to be moved across from the Win32 platform to .NET piece by piece, allowing developers to still build complete applications, but which comprise of Win32 portions and some .NET portions, of varying amounts.

When building new .NET applications, there are provisions for using existing Win32 DLL exports (both custom DLL routines and standard Win32 API routines) as well as COM objects (which then act like any other .NET object).

When building Win32 applications there is a process that allows you to access individual routines in .NET assemblies. When building Win32 COM client applications, there is a mechanism that lets you use .NET objects as if they were normal COM objects.

This paper investigates the interoperability options that do not involve COM:

  • .NET assemblies accessing Win32 DLL exports (including Win32 APIs)
  • Win32 applications/DLLs accessing routines in .NET assemblies

The accompanying paper, .NET Interoperability: COM Interop (see Reference 1), looks at interoperability between .NET and COM (termed COM Interop).

The coverage will have a specific bias towards a developer moving code from Borland Delphi (for Win32) to Borland Delphi for .NET (either Delphi 8 for .NET or Delphi "Diamondback"), however the principles apply to any other development tools. Clearly the Delphi-specific details will not apply to other languages but the high-level information will still be relevant.

Because of the different data types available on the two platforms (such as PChar in Win32 and the new Unicode String type on .NET), inter-platform calls will inevitably require some form of marshaling process to transform parameters and return values between the data types at either end of the call. Fortunately, as we shall see, the marshalling is done for us after an initial process to set up the inter-platform calls.

.NET Clients Using Win32 DLL Exports (P/Invoke)

This is the most common form of interoperability, which is why we are looking at it first. Whilst the .NET Framework is large and wide ranging, there are still things that you can do using Win32 APIs that are not possible using just the .NET framework. Simple examples include producing noises (using the Win32 APIs MessageBeep and Beep) or performing high accuracy timing (with QueryPerformanceCounter and QueryPerformanceFrequency).

Note: the first example cited here, making noises, will be rectified in .NET 2.0 as support is to be added for this.

In cases like this, where it would be helpful, or indeed necessary, to make use of a Win32 DLL routine from a .NET application you use a mechanism called the Platform Invocation Service, which is usually referred to as Platform Invoke or simply P/Invoke (or PInvoke). This service operates through a custom attribute, DllImportAttribute, defined in the System.Runtime.InteropServices namespace. The attribute allows the name of the implementing DLL (and other necessary information) to be associated with a procedure or function declaration, thereby allowing the DLL routine to be called.

The marshaling of parameters and return values between the managed .NET world and the unmanaged Win32 world is automatically performed by the Interop Marshaler used by the COM Interop support.

For an example, consider a Win32 DLL that exports three routines with the following signatures:

 
function DoSomething(I: Integer): Bool; cdecl;
function DoSomethingElseA(Msg: PChar): Bool; cdecl;
function DoSomethingElseW(Msg: PWideChar): Bool; cdecl;

As you can see, they all use the C calling convention, rather than the usual Win32 standard calling convention. The first routine takes an integer and returns a Boolean value, though using the standard Windows type Bool (a 32-bit Boolean value, equivalent to LongBool). The second routine takes a pointer to an ANSI character string (PChar is the same as PAnsiChar) and the last takes a pointer to a Unicode string.

To construct an appropriate import declaration that uses the P/Invoke mechanism you have two options, using the traditional Delphi DLL import syntax or using the custom P/Invoke attribute.

Traditional Syntax

You can use historic Delphi import declaration syntax and completely ignore the custom attribute, although there are caveats to this. We must understand the implications of leaving out the custom attribute in doing this. In point of fact, Delphi for .NET will create a custom attribute behind the scenes and the key thing is to understand what values the attribute fields will take.

The first exported routine can be declared in a .NET import unit like this:

 
unit Win32DLLImport;
 
interface
 
function DoSomething(I: Integer): Boolean; cdecl;
 
implementation
 
const
  Win32DLL = 'Win32DLL.dll';
 
function DoSomething(I: Integer): Boolean;
external Win32DLL name 'DoSomething';
 
end.

The calling convention is specified using the standard cdecl directive in the declaration part, and the DLL name and optional real DLL export name are specified in the implementation part.

This works fine for routines that do not have textual parameter types or return types. Internally, the compiler massages the declaration to use the P/Invoke attribute, like this:

 
function DoSomething(I: Integer): Boolean;
...
[DllImport(Win32DLL, CallingConvention = CallingConvention.Cdecl)]
function DoSomething(I: Integer): Boolean;
external;

Note: as well as the attribute constructor parameter (the DLL name) the attribute also has a CallingConvention field (attribute fields are often called parameters) that is set to specify the C calling convention. Indeed there are other parameters available in the attribute, which assume default values, and that is where problems can arise when the routine uses textual parameters or return types.

Attribute Syntax

The alternative to using traditional syntax is to explicitly specify the P/Invoke attribute in the import declaration. This will often be necessary when the routine takes textual parameters due to the default value of the attribute's CharSet parameter.

CharSet can take these values:

  • Ansi: textual parameters are treated as ANSI strings.
    This is the default value if you declare a DllImport attribute, but omit the
    CharSet parameter.
  • Auto: textual parameters are treated as ANSI strings on Win9x platforms and Unicode strings on NT-based systems (WinNT/2K/XP). This is the default setting generated by the compiler when it silently manufactures the whole DllImport attribute on your behalf.
  • None: this is considered obsolete but has the same effect as Ansi
  • Unicode: textual parameters are treated as Unicode strings

It is common for custom DLL routines to be implemented to take a fixed string type (either ANSI or Unicode), typically ANSI as Unicode is not implemented on Win9x systems. The same DLL will be deployed on any Windows system.

On the other hand, Win32 APIs that take string parameters are implemented twice; one implementation has an A suffix (the ANSI one) and one has a W suffix (the Unicode one). On Win9x systems the Unicode implementation is stubbed out as Unicode is not supported.

If the CharSet field is set to Ansi and the routine you are declaring is called Foo, at runtime the P/Invoke system will look for Foo in the DLL and use it if found; if it is not found it will look for FooA. However if CharSet is set to Unicode then FooW will be sought first, and if it is not found Foo will be used, if present.

The Auto value for CharSet means that on Win9x systems, string parameters will be turned into ANSI strings and the ANSI entry point search semantics will be used. On NT-based systems the parameters will be turned to Unicode and the Unicode search semantics will be used. For normal Win32 APIs this is just fine, but for most custom DLL routines a specific CharSet value must be specified.

In the case of the sample DLL exports shown above, we have two implementations of a routine, one that takes an ANSI parameter and one that takes a Unicode parameter. These are named following the Win32 conventions and so we could define a single .NET import like this, which would work fine on all Windows systems, calling DoSomethingElseA or DoSomethingElseW based on the Windows system type:

 
function DoSomethingElse(const Msg: String): Boolean; cdecl;
...
function DoSomethingElse(const Msg: String): Boolean;
external Win32DLL;

This is the same as explicitly writing the P/Invoke attribute like this:

 
function DoSomethingElse(const Msg: String): Boolean;
...
[DllImport(Win32DLL, EntryPoint = 'DoSomethingElse', CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
function DoSomethingElse(const Msg: String): Boolean;
external;

If we didn't have both an ANSI and Unicode implementation, and instead had just an ANSI version, then the single import declaration would look something like this:

 
function DoSomethingElse(const Msg: String): Boolean;
...
[DllImport(Win32DLL, EntryPoint = 'DoSomethingElse', CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
function DoSomethingElse(const Msg: String): Boolean;
external;

We can now define two specific .NET imports for the ANSI and Unicode versions of the routine as follows:

 
function DoSomethingElseA(const Msg: String): Boolean;
function DoSomethingElseW(const Msg: WideString): Boolean;
...
[DllImport(Win32DLL, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
function DoSomethingElseA(const Msg: String): Boolean;
external;
 
[DllImport(Win32DLL, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
function DoSomethingElseW(const Msg: String): Boolean;
external;

Note: the default value of the CallingConvention parameter is StdCall when you use DllImportAttribute and omit it. However when you use traditional Delphi syntax and do not specify a calling convention, Delphi specifies a calling convention of WinApi, which is equivalent to StdCall on Windows, but equivalent to Cdecl on Windows CE (which is not supported as a target CLR version by Delphi 8 for .NET, but is supported, though not for GUI designing, by Delphi "Diamondback").

Working Out The Parameter Types

In this case the .NET equivalent of the original data types was quite straightforward: PChar, PAnsiChar and PWideChar become String, Bool becomes Boolean and Integer becomes Integer. In other cases, the corresponding types to use in the .NET import declaration may not be so clear, particularly if the original declaration was written in C.

In many cases the appropriate information can be obtained by finding a normal Win32 API that uses the same parameter type in the same way and looking up the declaration in the Win32 import unit supplied with Delphi for .NET, Borland.Vcl.Windows.pas (in Delphi for .NET's Source\rtl directory). This unit contains import declarations for the majority of the standard Win32 APIs.

For example, consider an existing API or two that we can test the theory out with: GetComputerName and GetUserName. If these were part of some third party DLL, targeted at C/C++ programmers, which we were using in our Win32 applications, then we may well want to use them in a .NET application. The C declarations of these routines look like:

 
BOOL GetComputerName(
    LPTSTR lpBuffer,   // address of name buffer 
    LPDWORD nSize      // address of size of name buffer 
   );
BOOL GetUserName(
    LPTSTR lpBuffer,   // address of name buffer 
    LPDWORD nSize      // address of size of name buffer 
   );

Since you have already used them in your Win32 applications you will already have Delphi translations of these (which in this example's case we can get from the Delphi 7 Windows.pas import unit):

 
function GetComputerName(lpBuffer: PChar; var nSize: DWORD): BOOL; stdcall;
function GetUserName(lpBuffer: PChar; var nSize: DWORD): BOOL; stdcall;

In both cases the lpBuffer parameter is an out parameter that points to a buffer (a zero-based Char array) that receives the string with the computer's or user's name. The nSize parameter is an in/out parameter that specifies how large the buffer is, so the routine doesn't write past the end of it. If the buffer is too small, the routine returns False, otherwise it returns how many characters were written to the buffer.

If the documentation for the routine tells you the maximum size of the returned string you can easily make the buffer that large, otherwise you will have to check the return value; if it fails try a larger buffer.

There are many Win32 routines that take parameters that work this or a similar way, such as GetWindowsDirectory, GetSystemDirectory and GetCurrentDirectory. Sometimes the routine returns the number of characters (either that it wrote, or that it requires) and the buffer size parameter is passed by value (as in the routines just referred to), other times the function returns a Boolean value and the buffer size parameter is passed by reference. Win32 import declarations for these last three routines look like this:

 
function GetWindowsDirectory(lpBuffer: PChar; uSize: UINT): UINT; stdcall;
function GetSystemDirectory(lpBuffer: PChar; uSize: UINT): UINT; stdcall;
function GetCurrentDirectory(nBufferLength: DWORD; lpBuffer: PChar): DWORD; stdcall;

The corresponding Delphi for .NET declarations for all these routines can be found in Borland.Vcl.Windows.pas. The declarations in the interface section look like this:

 
function GetUserName(lpBuffer: StringBuilder; var nSize: DWORD): BOOL;
function GetComputerName(lpBuffer: StringBuilder; var nSize: DWORD): BOOL;
function GetWindowsDirectory(lpBuffer: StringBuilder; uSize: UINT): UINT;
function GetSystemDirectory(lpBuffer: StringBuilder; uSize: UINT): UINT;
function GetCurrentDirectory(nBufferLength: DWORD; lpBuffer: StringBuilder): DWORD;

As you can see, these types of string parameters are best represented using StringBuilder objects. StringBuilder is an appropriate type when the underlying Win32 routine will modify the string buffer, whereas String can be used when the routine will not modify its content (.NET String objects are immutable).

StringBuilder objects must have their capacity set to your desired size and that capacity can then be passed as the buffer size. The following five event handlers show how each of these APIs can be called from Delphi for .NET through P/Invoke.

 
procedure TfrmPInvoke.btnUserNameClick(Sender: TObject; Args: EventArgs);
var
  UserBuf: StringBuilder;
  UserBufLen: DWord;
begin
  UserBuf := StringBuilder.Create(64);
  UserBufLen := UserBuf.Capacity;
  if GetUserName(UserBuf, UserBufLen) then
    MessageBox.Show(UserBuf.ToString)
  else
    //User name is longer than 64 characters
end;
 
procedure TfrmPInvoke.btnComputerNameClick(Sender: TObject; Args: EventArgs);
var
  ComputerBuf: StringBuilder;
  ComputerBufLen: DWord;
begin
  //Set max size buffer to ensure success
  ComputerBuf := StringBuilder.Create(MAX_COMPUTERNAME_LENGTH);
  ComputerBufLen := ComputerBuf.Capacity;
  if GetComputerName(ComputerBuf, ComputerBufLen) then
    MessageBox.Show(ComputerBuf.ToString)
end;
 
procedure TfrmPInvoke.btnWindowsDirClick(Sender: TObject; Args: EventArgs);
var
  WinDirBuf: StringBuilder;
begin
  WinDirBuf := StringBuilder.Create(MAX_PATH); //Set max size buffer to ensure success
  GetWindowsDirectory(WinDirBuf, WinDirBuf.Capacity);
  MessageBox.Show(WinDirBuf.ToString)
end;
 
procedure TfrmPInvoke.btnSystemDirClick(Sender: TObject; Args: EventArgs);
var
  SysDirBuf: StringBuilder;
begin
  SysDirBuf := StringBuilder.Create(MAX_PATH); //Set max size buffer to ensure success
  GetSystemDirectory(SysDirBuf, SysDirBuf.Capacity);
  MessageBox.Show(SysDirBuf.ToString)
end;
 
procedure TfrmPInvoke.btnCurrentDirClick(Sender: TObject; Args: EventArgs);
var
  CurrDirBuf: StringBuilder;
begin
  CurrDirBuf := StringBuilder.Create(MAX_PATH); //Set max size buffer to ensure success
  GetCurrentDirectory(CurrDirBuf.Capacity, CurrDirBuf);
  MessageBox.Show(CurrDirBuf.ToString)
end;

In addition to the Delphi Win32 import unit you can also find C# P/Invoke declarations for much of the common Win32 API in Appendix E of .NET and COM (see Reference 2) and also in a dedicated online repository (see Reference 3).

Win32 Errors

Win32 routines often return False or 0 to indicate they failed (the documentation clarifies whether this is the case), leaving the programmer to call GetLastError to find the numeric error code. Delphi programmers can call SysErrorMessage to turn the error number into an error message string to do with as they will or call RaiseLastWin32Error or RaiseLastOSError to raise an exception with the message set to the error message for the last error code. Additionally Delphi offers the Win32Check routine that can take a Win32 API Boolean return value; this calls RaiseLastOSError if the parameter is False.

It is important that when calling Win32 routines from .NET you do not declare or use a P/Invoke declaration for the Win32 GetLastError API as it is unreliable (due to the interaction between .NET and the underlying OS). Instead you should use Marshal.GetLastWin32Error from the System.Runtime.InteropServices namespace. This routine relies on another DllImportAttribute field being specified. The SetLastError field defaults to False meaning the error code is ignored. If set to True the runtime marshaler will call GetLastError and cache the value for GetLastWin32Error to return.

Note: all the Win32 imports in Delphi for .NET specify this field with the value of True. For example, this is the GetComputerName declaration from the Borland.Vcl.Windows.pas implementation section:

 
const
  kernel32  = 'kernel32.dll';
 
[DllImport(kernel32, CharSet = CharSet.Auto, SetLastError = True, EntryPoint = 'GetComputerName')]
function GetComputerName; external;

Note: Delphi for .NET defines a routine GetLastError in the implicitly used Borland.Delphi.System unit, implemented simply as:

 
function GetLastError: Integer;
begin
  Result := System.Runtime.InteropServices.Marshal.GetLastWin32Error;
end;

However, if you use Borland.Vcl.Windows and call GetLastError in the same unit, the compiler will bind the call to the version of GetLastError defined in that unit. However thankfully it is not a P/Invoke declaration to the Win32 routine of that name. Instead it is a simple function that calls Borland.Delphi.System.GetLastError.

 
function GetLastError: DWORD;
begin
  Result := Borland.Delphi.System.GetLastError;
end;

To aid moving API-based code across, Borland.Vcl.SysUtils contains ported versions of the Delphi Win32 error support routines, such as SysErrorMessage, RaiseLastWin32Error, RaiseLastOSError and Win32Check. So just as in regular Win32 Delphi applications you can write code like this:

 
procedure TfrmPInvoke.btnUserNameClick(Sender: TObject; Args: EventArgs);
var
  UserBuf: StringBuilder;
  UserBufLen: LongWord;
begin
  //Buffer too small, so we will get an exception
  UserBuf := StringBuilder.Create(2);
  UserBufLen := UserBuf.Capacity;
  Win32Check(GetUserName(UserBuf, UserBufLen));
  MessageBox.Show(UserBuf.ToString)
end;
 
procedure TfrmPInvoke.btnWindowsDirClick(Sender: TObject; Args: EventArgs);
var
  WinDirBuf: StringBuilder;
begin
  WinDirBuf := StringBuilder.Create(MAX_PATH); //Set max size buffer to ensure success
  Win32Check(Bool(GetWindowsDirectory(WinDirBuf, WinDirBuf.Capacity)));
  MessageBox.Show(WinDirBuf.ToString)
end;

If you are running this code in a WinForms application then the exceptions generated to represent the error will be picked up by the default WinForms unhandled exception handler. This takes the form of a dialog showing you details of the exception and offering you the chance to terminate or continue the application. You can safely continue from these exceptions.

If the Details button on this dialog is pressed you get a useful stack trace pointing you to the execution path that led to the exception.

HRESULT Return Values

As you may be aware, various COM/OLE related Win32 APIs return HResult values. These values return various bits of status information such as success, failure and also error codes. These APIs can be declared using the P/Invoke mechanism as well as any other method (HResults are represented as integers in .NET). For example, let's take the CLSIDFromProgID API, which is declared in Win32 terms as:

 
function CLSIDFromProgID(pszProgID: POleStr; out clsid: TCLSID): HResult; stdcall;
external ole32 name 'CLSIDFromProgID'

The first parameter is a POleStr (or PWideChar), meaning it is a Unicode string on all Windows platforms. In C terms the parameter type is LPCOLESTR, where the C implies the routine considers the string constant and will not change it (we can use a String parameter instead of a StringBuilder in the P/Invoke definition thanks to this fact).

One way of writing the P/Invoke import is using the standard Delphi syntax:

 
function CLSIDFromProgID([MarshalAs(UnmanagedType.LPWStr)] ppsz: String; out rclsid: Guid): Integer; stdcall;
external ole32;

Notice in this case that the String parameter needs an attribute of its own to ensure it will always be marshaled correctly on all Windows platforms. By default, a String parameter in this type of declaration will be marshaled as Unicode on NT platforms and ANSI on Win9x platforms. An alternative would be to specify the API uses the Unicode character set:

 
[DllImport(ole32, CharSet = CharSet.Unicode)]
function CLSIDFromProgID(ppsz: String; out rclsid: Guid): Integer;
external;

Delphi programmers may be familiar with the safecall calling convention that allows HResults to be ignored by the developer. Instead, safecall methods automatically raise exceptions if the HResult returned indicates failure.

P/Invoke supports a similar mechanism with yet another DllImportAttribute field, PreserveSig. This field defaults to True, meaning that the API signature will be preserved, thereby returning a HResult. If you set PreserveSig to False you can remove the HResult return value and a failure HResult will automatically raise an exception. The above declarations could be rewritten as:

 
[DllImport(ole32, PreserveSig = False)]
procedure CLSIDFromProgID([MarshalAs(UnmanagedType.LPWStr)] ppsz: String; out rclsid: Guid);
external;

or:

 
[DllImport(ole32, CharSet = CharSet.Unicode, PreserveSig = False)]
procedure CLSIDFromProgID(ppsz: String; out rclsid: Guid);
external;

Performance Issues

Sometimes there may be various ways to express a DLL routine in .NET and indeed the marshaling system will do its best to cope with the way you express the routine signature. However some representations (data mappings between types) are more efficient than others. Take the high accuracy timing routines for example. These are declared in Delphi 7 like this, where TLargeInteger is a variant record containing an Int64 (whose high and low parts can be accessed through other fields):

 
const
  kernel32  = 'kernel32.dll';
 
function QueryPerformanceCounter(var lpPerformanceCount: TLargeInteger): BOOL; stdcall;
external kernel32 name 'QueryPerformanceCounter';
function QueryPerformanceFrequency(var lpFrequency: TLargeInteger): BOOL; stdcall;
external kernel32 name 'QueryPerformanceFrequency';

The logical way of translating these routines would be like this:

 
function QueryPerformanceCounter(var lpPerformanceCount: Int64): Boolean;
external kernel32;
function QueryPerformanceFrequency(var lpFrequency: Int64): Boolean;
external kernel32;

This requires the marshaler to translate from a BOOL (which is the same a LongBool, a 32-bit Boolean value where all bits are significant) to a Boolean object. It would be more efficient to choose a data type that was the same size and can have the value passed straight through. Also, since the documentation for these APIs specifies that they will write a value to the reference parameter, and are not interested in any value passed in, we can replace the var declaration with out to imply this fact. So a more accurate and more efficient pair of declarations would look like this:

 
function QueryPerformanceCounter(out lpPerformanceCount: Int64): LongBool;
external kernel32;
function QueryPerformanceFrequency(out lpFrequency: Int64): LongBool;
external kernel32;

In a case like this where we are calling high performance timers you can go one step further to remove overheads by using the SuppressUnmanagedCodeSecurityAttribute from the System.Security namespace:

 
[DllImport(kernel32), SuppressUnmanagedCodeSecurity]
function QueryPerformanceCounter(out lpPerformanceCount: Int64): LongBool;
external;
[DllImport(kernel32), SuppressUnmanagedCodeSecurity]
function QueryPerformanceFrequency(out lpFrequency: Int64): LongBool;
external;

This makes the calls to the routines a little more efficient at the expense of normal security checks and thereby means the reported times from the routines will be slightly more accurate. A simple test shows that the logical versus accurate declaration have little to distinguish them at runtime, but the declaration with security disabled is a little quicker.

SuppressUnmanagedCodeSecurityAttribute should only be used on routines that cannot be used maliciously because, as the name suggests, it bypasses the normal runtime security check for calling unmanaged code. A routine marked with this attribute can be called by .NET code that does not have permission to run unmanaged code (such as code running via a Web browser page).

One additional performance benefit you can achieve, according to the available information on the subject, is to cause the P/Invoke signature/metadata validation, DLL loading and routine location to execute in advance of any of the P/Invoke routine calls. By default the first time a P/Invoke routine from a given DLL is called, that is when the DLL is loaded. Similarly, the signature metadata is validated the first time the P/Invoke routine is called. You can do this in advance by calling the Marshal.Prelink method (for a single P/Invoke routine) or the Marshal.PrelinkAll (for all P/Invoke routines defined in a class or unit). Both these come from the System.Runtime.InteropServices namespace.

The two timing routines are declared as standalone routines in the unit, but to fit into the .NET model of having everything defined as a method, this really means they are part of the projectname.Unit namespace (dotNetApp.Unit in this case). So to pre-link the two timing routines from a form constructor you could use:

 
Marshal.Prelink(GetType.Module.GetType('dotNetApp.Unit').GetMethod('QueryPerformanceFrequency'));
Marshal.Prelink(GetType.Module.GetType('dotNetApp.Unit').GetMethod('QueryPerformanceCounter'));

To pre-link all P/Invoke routines in the unit from the form constructor, use:

 
Marshal.PrelinkAll(GetType.Module.GetType('dotNetApp.Unit'));

Simple tests indicate that without the pre-link code, the first tests will indeed be a little slower than subsequent tests in the same program run. Pre-linking the specified routines individually removes this first-hit delay, but curiously I found calling PrelinkAll makes each test in a given session noticeably quicker than with any of the previous tests.

Virtual Library Interfaces (VLI) aka Dynamic PInvoke

Delphi "Diamondback" introduces another approach to accessing P/Invoke routines in DLLs. It seems to be primarily oriented towards accessing routines in DLLs that are not immediately accessible using the normal Win32 DLL location rules, for example DLLs stored in an arbitrary custom directory. This facility has been tagged Virtual Library Interfaces (VLI). It is also referred to as Dynamic PInvoke, although this is a term that is in use in non-Delphi circles as well.

Accessing an unmanaged routine in a DLL not in the usual places (on the path, in the Windows or Windows system directories, in the current directory or the application directory) is not an insurmountable problem outside Delphi "Diamondback":

·         Preceding any call to a P/Invoke routine with a call to the Win32 routine LoadLibrary passing in the fully qualified DLL name. Only when you call one of the routines identified as being from the DLL does the Interop layer try and bind the routine to the DLL, so the