Interoperability 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:

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:

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; varnSize:
                  DWORD): BOOL; stdcall;
                  
                
              
function
                  
                
                   GetUserName(lpBuffer:
                  PChar; varnSize:
                  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; varnSize:
                  DWORD): BOOL;
                  
                
              
function
                  
                
                   GetComputerName(lpBuffer:
                  StringBuilder; varnSize:
                  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; outrclsid:
                  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; outrclsid:
                  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; outrclsid:
                  Guid);
                  
                
              
external
                  
                ;
                
              

or:

 
                
              
[DllImport(ole32,
                  CharSet = CharSet.Unicode, PreserveSig = False)]
                
              
procedure
                  
                
                   CLSIDFromProgID(ppsz: String; outrclsid:
                  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(varlpPerformanceCount:
                  TLargeInteger): BOOL; stdcall;
                  
                
              
external
                  
                
                   kernel32 name 'QueryPerformanceCounter';
                  
                
              
function
                  
                
                   QueryPerformanceFrequency(varlpFrequency:
                  TLargeInteger): BOOL; stdcall;
                  
                
              
external
                  
                
                   kernel32 name 'QueryPerformanceFrequency';
                  
                
              

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

 
                
              
function
                  
                
                   QueryPerformanceCounter(varlpPerformanceCount:
                  Int64): Boolean;
                  
                
              
external
                  
                
                   kernel32;
                
              
function
                  
                
                   QueryPerformanceFrequency(varlpFrequency:
                  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(outlpPerformanceCount:
                  Int64): LongBool;
                  
                
              
external
                  
                
                   kernel32;
                
              
function
                  
                
                   QueryPerformanceFrequency(outlpFrequency:
                  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(outlpPerformanceCount:
                  Int64): LongBool;
                  
                
              
external
                  
                ;
                
              
[DllImport(kernel32),
                  SuppressUnmanagedCodeSecurity]
                
              
function
                  
                
                   QueryPerformanceFrequency(outlpFrequency:
                  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 LoadLibrary call helps that work.

·         Additionally you can take the trouble to dynamically manufacture the appropriate binding details through Reflection.Emit (a bit more complicated but you can find the details here).

·         In .NET 2.0 additional support for this Dynamic PInvoke issue will be added to allow the return value of a call to the Win32 GetProcAddress routine to be turned into an executable entity through Marshal.GetDelegateForFunctionPointer .

However here we shall look at what Delphi "Diamondback" offers in this regard.

The basic premise of the support is to make a DLL look and feel a little like a COM object that implements an interface, where the interface methods correspond to the DLL exports. You define an arbitrary interface and add to it methods that correspond to all the DLL exports of interest. A new version of the Supports function implemented in the Borland.Vcl.Win32 unit does all the behind-the-scenes magic for you.

Let’s look at an example. Consider a simple Win32 DLL that exports some routines that you wish to call:

 
                
              
function
                  
                DoSomething(I:
                  Integer): Bool; stdcall;
                  
                
              
function
                  
                DoSomethingElseW(Msg:
                  PWideChar): Bool; stdcall;
                  
                
              

To access this through Dynamic PInvoke you define an interface to represent these routines:

 
                
              
type
                      
                    
                  
                
              
  ISomething
                   = interface
                    
                  
                
              
    functionDoSomething(I:
                  Integer): Boolean;
                    
                  
                
              
    functionDoSomethingElseW(const
                   Msg: String): Boolean;
                    
                  
                
              
  end;
                      
                    
                  
                
              

Then to access these routines, you use the new version of Supports that takes the DLL name as a parameter. If the DLL is not in the standard directories then you will need to put in a fully qualified path to ensure it can be found:

 
                
              
uses
                      
                    
                  
                
              
  Borland.Vcl.Win32; 
                  
                //can
                    be shortened to just Win32
                  
                
                    
                  
                
              
...
                    
                  
                
              
var
                      
                    
                  
                
              
  Something: ISomething;
                    
                  
                
              
...
                    
                  
                
              
  if
                    not Supports('Win32DLL.dll', TypeOf(ISomething),
                  Something) then
                    
                  
                
              
    MessageBox.Show('DLL
                  routines not found for some reason');
                    
                  
                
              
  Something.DoSomething(123456789);
                    
                  
                
              
  Something.DoSomethingElseW('I
                    am a Unicode string');
                    
                  
                
              

Note : there are strict limitations to this support thanks to some defaults applied in the P/Invoke stubs that are dynamically created behind the scenes (in the Borland.Vcl.Win32 unit):

·         The calling convention is assumed to be the Windows standard (i.e. stdcall) thanks to the use of an equivalent to the attribute value CallingConvention.WinApi .

·         Textual parameters in the DLL have to be PWideChar on NT-based platforms (Windows 2000/XP/2003) and PChar (PAnsiChar) on Windows 95/98/Me, thanks to the use of an equivalent to the attribute value CharSet.Auto.

·         Other parameters must be data types that .NET can marshal without any intervention. There is no point adding any marshaling attributes to any parameters that will not be marshaled correctly as they are ignored in the dynamic stub creation.

If any of these issues prove too restrictive then you should resort to one of the alternative forms of Dynamic PInvoke listed at the start of this section.

Native Object Creation

Additional functionality added in Delphi "Diamondback" includes support for accessing unmanaged classes implemented in Delphi or C++Builder run-time packages. As long as the class follows some rules, then it will be accessible. This allows code that could be tricky to port over to .NET (perhaps due to pointer use or other .NET unfriendly activities) to be used almost as-is, with only a little modification. There are limitations on this process (perhaps too harsh) but the principal works just fine. Here are the restrictions on the class:

·         The calling convention of every method you wish to call must be stdcall.

·         Parameter data types must be .NET-compatible: Char, Shortint, Integer, Longint , Single and Double.

·         Textual parameters must be PWideChar on the Win32 side and String on the .NET side

So let’s take a simple class TSimpleClass , implemented in SimpleClass.pas that is contained in a Win32 package called Win32Package.bpl. The following class definition follows the rules above, having been massaged from its original definition to cater for the requirements Native Object Creation:

 
                
              
type
                      
                    
                  
                
              
  TSimpleClass
                   = class
                    
                  
                
              
  private
                      
                    
                  
                
              
    FI:
                  Integer;
                    
                  
                
              
  public
                      
                    
                  
                
              
    constructor
                   Create;
                    
                  
                
              
    destructor
                   Destroy; override;
                    
                  
                
              
    procedureSampleProc(I:
                  Integer; Msg: PWideChar); stdcall;
                    
                  
                
              
    functionSampleFunc:
                  Integer; stdcall;
                    
                  
                
              
  end;
                      
                    
                  
                
              

The next requirement is to add in two routines to the package whose job is to create and destroy an instance of TSimpleClass , but dealing with it through a typeless pointer. These routines need to be exported from the package using the traditional DLL exports clause. There is no need to declare the routines in the interface section of the unit; they can remain private to the implementation section.

 
                
              
function
                  
                CreateSimpleClass:
                  Pointer;
                    
                  
                
              
begin
                  
                
                    
                  
                
              
  Result
                  := TSimpleClass.Create
                    
                  
                
              
end
                  
                ;
                    
                  
                
              
 
                  
                
              
{$WARN
                  UNSAFE_CAST OFF}
                    
                  
                
              
procedure
                  
                FreeSimpleClass(SimpleClassInstance:
                  Pointer);
                    
                  
                
              
begin
                  
                
                    
                  
                
              
  TSimpleClass(SimpleClassInstance).Free
                    
                  
                
              
end
                  
                ;
                    
                  
                
              
 
                  
                
              
exports
                  
                
                    
                  
                
              
  CreateSimpleClass,
                    
                  
                
              
  FreeSimpleClass;
                  
                
              

In your .NET application you must now define an interface that corresponds to the public members you wish to access, but attach a custom attribute to it that provides information allowing the appropriate connections to be made. ClassImportAttribute is implemented in the Borland.Vcl.Win32 unit and needs to be told the name of the unit that implements the class, the class name and the names of the constructor/destructor routines exported from the package. Having defined the interface you can declare a variable of that type and assign to it the result of calling the static class method UnmanagedPackage.CreateInstance , which is passed the actual binary package name. Once done you can then use the interface to call the unmanaged class methods.

Here is some code that shows the idea:

 
                
              
uses
                      
                    
                  
                
              
  Borland.Vcl.Win32; 
                  
                //can
                    be shortened to just Win32
                  
                
                    
                  
                
              
...
                    
                  
                
              
type
                  
                
                    
                  
                
              
  [ClassImport('Simpleclassimpl'
                    , 'TSimpleClass'
                    , 'CreateSimpleClass'
                    , 'FreeSimpleClass'
                    )]
                    
                  
                
              
  ISimpleClass
                   = interface
                    
                  
                
              
    procedureSampleProc(I:
                  Integer; Msg: String);
                    
                  
                
              
    functionSampleFunc:
                  Integer;
                    
                  
                
              
  end;
                    
                  
                
              
...
                    
                  
                
              
var
                  
                
                    
                  
                
              
  SimpleClass: ISimpleClass;
                    
                  
                
              
...
                    
                  
                
              
  if
                    notUnmanagedPackage.CreateInstance(
                    
                  
                
              
           'Win32Package.bpl', TypeOf(ISimpleClass), SimpleClass) then
                    
                  
                
              
    MessageBox.Show('Packaged
                    class not accessible for some reason');
                    
                  
                
              
  SimpleClass.SampleProc(99, 'Hello
                    world - this a .NET message');
                    
                  
                
              
  MessageBox.Show(System.String.Format(
                    
                  
                
              
    'The
                    function returned a value of {0}', SimpleClass.SampleFunc));
                      
                    
                  
                
              

Note : This information was based on a pre-release version of Delphi "Diamondback". This feature may not even make it into the shipping product, or it may be enhanced from the description provided here. An updated version of this article will appear on the Articles page of my Web site containing corrected information about this new feature shortly after Delphi "Diamondback" is released.

Win32 Clients Using Unmanaged Exports (Inverse P/Invoke)

The CCW aspect of COM Interop (see Reference 1) permits a COM client to access a .NET object just like any other COM object, but sometimes a Win32 application can benefit from being given access to just a handful of routines in a .NET assembly. This technique is catered for by the .NET platform; the CLR has an inherent capability to expose any .NET method to the outside (Win32) world using a mechanism that is the exact reverse of that used by P/Invoke, hence the term Inverse P/Invoke. However only Delphi for .NET, C++ With Managed Extensions (aka Managed C++) and the underlying Intermediate Language (IL) support this ability directly. There is also little printed coverage of this technique, other than in Chapter 15 of Inside Microsoft .NET IL Assembler (see Reference 5) and Chapter 16 of Delphi for .NET Developer’s Guide (see Reference 4).

Exporting Global Routines

Delphi for .NET supports the Inverse P/Invoke mechanism using the same syntax as Delphi for Win32 supports exporting routines from DLLs, via the exports clause. This ability is unique in the more popular .NET languages (specifically C# and Visual Basic.NET do not support it) and is referred to as unmanaged exports. The syntax is limited to exporting Delphi global routines (implemented as class static methods of the unit wrapper class Unit that Delphi generates behind the scenes). Regular class static methods cannot be exported.

A sample assembly project source file is shown below. Notice that it is required to mark the project source as unsafe for the exports clause to compile. You can do this either with the compiler directive shown, or if compiling with the command-line compiler you can pass the --unsafe+ switch. Listing a routine for export is considered making the routine unsafe.

 
                
              

library dotNetAssembly;

 

{$UNSAFECODE ON}

 

{%DelphiDotNetAssemblyCompiler ' $(SystemRoot)\microsoft.net\framework\v1.1.4322\System.Windows.Forms.dll'}

 

uses

  System.Windows.Forms;

 

procedure DoSomething(I: Integer);

begin

  MessageBox.Show(I.ToString)

end ;

 

procedure DoSomethingElse(const Msg: String);

begin

  MessageBox.Show(Msg)

end ;

 

exports

  DoSomething,

  DoSomethingElse;

 

begin

end
                  
                .
                
              
 
        
      

Limitations

As mentioned, the Delphi for .NET support for unmanaged exports is (unnecessarily) restricted to exporting global routines. You are not permitted to export an arbitrary class static method, even though global routines are implemented as class static methods as mentioned above. To explain, whilst we can write global routines as we also have been able to, Delphi for .NET will ensure they are implemented as class static methods, since all CLS-compliant routines in .NET must be methods. Every source file in a Delphi for .NET project has an implicit class, called Unit, containing everything in the source file. So the routines DoSomething and DoSomethingElse in the previous listing end up acting as though they were declared something like this:

 
                
              
type
                  
                
              
 
                  Unit = class(&Object)
                  
                
              
  public
                  
                
              
    class
                    procedureDoSomething(I:
                  Integer); static;
                  
                
              
    class
                    procedureDoSomethingElse(const
                   Msg: String); static;
                  
                
              
  end;
                  
                
              

As an example of regular class static methods not being exportable the library project file listed below contains a sample class that declares a couple of class methods. There is no way to export these methods to unmanaged code using the unmanaged exports feature of Delphi "Diamondback" for .NET.

 
                
              

library dotNetAssembly;

 

{%DelphiDotNetAssemblyCompiler ' $(SystemRoot)\microsoft.net\framework\v1.1.4322\System.Windows.Forms.dll'}

 

uses

  System.Windows.Forms;

 

type

  SampleClass = class

  public

    class procedure DoSomething(I: Integer); static;

    class procedureDoSomethingElse(const Msg: String); static;

  end;

 

class procedure DoSomething(I: Integer);

begin

  MessageBox.Show(I.ToString)

end ;

 

class procedure DoSomethingElse(const Msg: String);

begin

  MessageBox.Show(Msg)

end ;

 

//This part won’t compile

//exports

//  SampleClass.DoSomething,

//  SampleClass.DoSomethingElse;

 

begin

end
                  
                .
                
              

This shows up a limitation of Delphi for .NET unmanaged exports, but of course in C# and Visual Basic.NET the limitation is harsher. You cannot export static methods for use from unmanaged code at all. Fortunately the next section shows us how we can overcome these limitations if we wish to.

Removing Limitations With Creative Round Tripping

High level .NET programming languages do not take full advantage of all the features of the CLR. The CLR feature set, accessible through IL is very rich, attempting to cater for as many requirements of different and disparate languages as possible. High level languages use whatever subset is needed to implement the language features and ignore the rest as immaterial. Sometimes developers might want to employ a feature or two of IL that is not accessible through their language of choice. In this case we would be looking for the mechanism to export arbitrary class static methods. We can achieve this by resorting to some cunning trickery, namely creative round tripping.

Round tripping is a term describing a two step process that involves taking a managed Win32 file (a .NET assembly) and disassembling it to the corresponding IL source code and metadata (and any managed or unmanaged resources), and then reassembling the IL code, metadata and resources into an equivalent .NET binary.

Because of the rich, descriptive nature of the metadata in managed PE (Portable Executable, the Win32 file format) files this round-tripping process is very reliable. A few select things do not survive the process, but these are not things that are supported by the Delphi for .NET compiler, for example, data on data (data that contains the address of another data constant) and embedded native, non managed code. Also, local variable names will be lost if there is no PDB file available since they are only defined in debug information and not in metadata.

Round tripping in itself is only useful to prove that you get a working executable back after a disassembly/reassembly process. The term creative round tripping is used to describe a round tripping job with an extra step. After disassembling the assembly into IL code, you modify the IL code before reassembly.

Creative round tripping is used in these scenarios:

Clearly we will be focusing on the first item in the list in order to overcome Delphi for .NET’s unmanaged exports limitation, which will apply equally to the general unmanaged export limitation in C# or Visual Basic.NET. Let's first look at the two steps involved in round tripping to get the gist of things before looking at the details of creative round tripping.

Round Tripping, Step 1: Disassembly

To disassemble a .NET assembly you use the .NET Framework IL Disassembler, ildasm.exe, which comes with the Framework SDK. You will need to ensure the PATH has been set appropriately to allow you to run it without entering a fully-qualified path for the utility each time. You can do this globally using the Environment Variables button in the System Properties dialog or by running the handy batch file, SDKVars.bat, supplied with the .NET Framework SDK. You can find this batch file in the C:\Program Files\Microsoft.NET\SDK\v1.1\bin directory, assuming a default installation.

Note : the batch file will only execute the command prompt session that it is executed within and will not have a global effect and so it can be handy to set up a shortcut on the desktop or in the Start menu somewhere that executes this command line to invoke a command-prompt session and execute the batch file within it:

cmd /K "C:\Program Files\Microsoft.NET\SDK\v1.1\bin\SDKVars.bat"

Let's take an example assembly, dotNetAssembly.dll, made from the project file above. To disassemble the assembly use this command:

 
                
              
ildasm
                  
                
                   dotNetAssembly.dll /linenum
                   /out:dotNetAssembly.il
                  
                
              

This produces the named IL file and will also store any unmanaged resources in a file called dotNetAssembly.res and any managed resources in files with the names that are specified in the assembly metadata. The /linenum option will cause the IL file to include references to the original source lines, assuming debug information is available in a PDB file.

Round Tripping, Step 2: Reassembly

To re-assemble everything back to a .NET assembly you use the IL Assembler, ilasm.exe, which comes as part of the .NET Framework:

 
                
              
ilasm
                  
                
                   /dll dotNetAssembly.il
                   /out:dotNetAssembly.dll /res:dotNetAssembly.res
                   /quiet
                  
                
              

Modifying A .NET Assembly Manifest

To expose methods from an assembly you must make some changes to the assembly manifest (found at the top of the IL file before the class declarations) before reassembly. There will be references to other assemblies in the manifest as well as general module information:

 
                
              
.assembly
                  extern System.Windows.Forms
                  
                
              
{
                
              
 
                  .publickeytoken
                   = (B7 7A 5C 56 19 34 E0 89
                  )                        
                  // .z\V.4..
                  
                
              
 
                  .ver
                   1:0:5000:0
                  
                
              
}
                
              
.assembly dotNetAssembly
                  
                
              
{
                
              
 
                  // --- The following custom attribute is added automatically,
                  do not uncomment -------
                
              
 
                  //  .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(bool,
                  
                
              
 
                  //                                                                                bool)
                  = ( 01 00 00
                   01 00 00
                   ) 
                  
                
              
  .hash
                  algorithm 0x00008004
                
              
 
                  .ver
                   0:0:0:0
                  
                
              
}
                
              
.module
                  dotNetAssembly.dll
                
              
//
                  MVID: {B865276C-A90F-4CA2-8AF1-0BF42A04A451}
                
              
.imagebase
                   0x00400000
                  
                
              
.subsystem
                  0x00000002
                
              
.file
                  alignment 512
                
              
.corflags
                   0x00000001
                  
                
              

The first change is to define a v-table fixup containing as many slots as there are methods to export. In our case we have two methods to export, so the manifest should be changed to this:

 
                
              
.assembly
                  extern System.Windows.Forms
                  
                
              
{
                
              
 
                  .publickeytoken
                   = (B7 7A 5C 56 19 34 E0 89
                  )                        
                  // .z\V.4..
                  
                
              
 
                  .ver
                   1:0:5000:0
                  
                
              
}
                
              
.assembly dotNetAssembly
                  
                
              
{
                
              
 
                  // --- The following custom attribute is added automatically,
                  do not uncomment -------
                
              
 
                  //  .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(bool,
                  
                
              
 
                  //                                                                        
                         bool)
                  = ( 01 00 00
                   01 00 00
                   ) 
                  
                
              
  .hash
                  algorithm 0x00008004
                
              
 
                  .ver
                   0:0:0:0
                  
                
              
}
                
              
.module
                  dotNetAssembly.dll
                
              
//
                  MVID: {B865276C-A90F-4CA2-8AF1-0BF42A04A451}
                
              
.imagebase
                   0x00400000
                  
                
              
.subsystem
                  0x00000002
                
              
.file
                  alignment 512
                
              
.corflags
                   0x00000001
                  
                
              
.data
                    VT_01 = int32[2]
                  
                
              
.vtfixup
                     [2] int32 fromunmanaged
                     at VT_01
                    
                  
                
              

Notice that the number of methods is specified twice, once in the integer array (which is data space used for the v-table fixup, each slot being 32-bits in size) and once in the v-table fixup definition (which is defined to contain two slots and is mapped over the integer array).

Before leaving the manifest there is one other change that must be made if you want your assembly to operate on Windows XP and later. By default the .corflags directive, which sets the runtime header flags, specifies a value of 1, which equates to the COMIMAGE_FLAGS_ILONLY flag (defined in the CorHdr.h include file in the .NET Framework SDK). If this flag is set, the XP loader ignores the key section of the assembly file and the fixups are not fixed up. This causes fatal errors when trying to use the assembly exports. The reason this issue occurs on XP is because this was the first version of Windows to incorporate awareness of .NET. The executable file loader subsystem can recognize a .NET binary and cater for it directly. To resolve the problem you must specify the COMIMAGE_FLAGS_32BITREQUIRED flag (whose value is 2) to get the correct behaviour out of the updated loader:

 
                
              
.assembly
                  extern System.Windows.Forms
                  
                
              
{
                
              
 
                  .publickeytoken
                   = (B7 7A 5C 56 19 34 E0 89
                  )                        
                  // .z\V.4..
                  
                
              
 
                  .ver
                   1:0:5000:0
                  
                
              
}
                
              
.assembly dotNetAssembly
                  
                
              
{
                
              
 
                  // --- The following custom attribute is added automatically,
                  do not uncomment -------
                
              
 
                  //  .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(bool,
                  
                
              
 
                  //                                                                          
                       bool)
                  = ( 01 00 00
                   01 00 00
                   ) 
                  
                
              
  .hash
                  algorithm 0x00008004
                
              
 
                  .ver
                   0:0:0:0
                  
                
              
}
                
              
.module
                  dotNetAssembly.dll
                
              
//
                  MVID: {B865276C-A90F-4CA2-8AF1-0BF42A04A451}
                
              
.imagebase
                   0x00400000
                  
                
              
.subsystem
                  0x00000002
                
              
.file
                  alignment 512
                
              
.corflags
                     0x0000000
                    
                  
                2
                  
                
              
.data
                  VT_01 = int32[2]
                
              
.vtfixup
                   [2] int32 fromunmanaged
                   at VT_01
                  
                
              

Exporting .NET Methods

Now that we have a v-table fixup for the exported methods, we must get each method to appear as an entry in it. The IL representation of the methods currently looks like this:

 
                
              
.method
                  public static void  DoSomething(int32
                  I) cil
                   managed
                  
                
              
{
                
              
 
                  // Code size       13 (0xd)
                
              
 
                  .maxstack 
                  1
                  
                
              
 
                  .line 32:0 'dotNetAssembly.dpr'
                  
                
              
 
                  IL_0000:  ldarg.0
                
              
 
                  IL_0001:  call       string Borland.Delphi.System.Unit::IntToStr(int32)
                  
                
              
 
                  IL_0006:  call       valuetype
                   [System.Windows.Forms]System.Windows.Forms.DialogResult
                  
                
              
           
                               [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
                  
                
              
 
                  IL_000b:  pop
                
              
 
                  .line 33:0
                
              
 
                  IL_000c:  ret
                
              
}
                  // end of method Unit::DoSomething
                  
                
              
 
                
              
.method
                  public static void  DoSomethingElse([in]
                  string Msg) cil
                   managed
                  
                
              
{
                
              
 
                  // Code size       8 (0x8)
                
              
 
                  .maxstack 
                  1
                  
                
              
 
                  .line 37:0
                
              
 
                  IL_0000:  ldarg.0
                
              
 
                  IL_0001:  call       valuetype
                   [System.Windows.Forms]System.Windows.Forms.DialogResult
                  
                
              
                        
                  [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
                  
                
              
 
                  IL_0006:  pop
                
              
 
                  .line 38:0
                
              
 
                  IL_0007:  ret
                
              
}
                  // end of method Unit::DoSomethingElse
                  
                
              

To turn them into unmanaged exports they should be changed to:

 
                
              
.method
                  public static void  DoSomething(int32
                  I) cil
                   managed
                  
                
              
{
                
              
 
                  // Code size       13 (0xd)
                
              
 
                  .maxstack 
                  1
                  
                
              
 
                  .line 32:0 'dotNetAssembly.dpr'
                  
                
              
 
                    .vtentry
                     1:
                    
                  
                1
                  
                
              
 
                    .export [
                  
                1
                  
                ]
                    as DoSomething
                    
                  
                
              
 
                  IL_0000:  ldarg.0
                
              
 
                  IL_0001:  call       string Borland.Delphi.System.Unit::IntToStr(int32)
                  
                
              
 
                  IL_0006:  call       valuetype
                   [System.Windows.Forms]System.Windows.Forms.DialogResult
                  
                
              
     
                                     [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
                  
                
              
 
                  IL_000b:  pop
                
              
 
                  .line 33:0
                
              
 
                  IL_000c:  ret
                
              
}
                  // end of method Unit::DoSomething
                  
                
              
 
                
              
.method
                  public static void  DoSomethingElse([in]
                  string Msg) cil
                   managed
                  
                
              
{
                
              
 
                  // Code size       8 (0x8)
                
              
 
                  .maxstack 
                  1
                  
                
              
 
                  .line 37:0
                
              
 
                    .vtentry
                     1:
                    
                  
                2
                  
                
              
 
                    .export [
                  
                2
                  
                ]
                    as DoSomethingElse
                    
                  
                
              
 
                  IL_0000:  ldarg.0
                
              
 
                  IL_0001:  call       valuetype
                   [System.Windows.Forms]System.Windows.Forms.DialogResult
                  
                
              
                        
                  [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
                  
                
              
 
                  IL_0006:  pop
                
              
 
                  .line 38:0
                
              
 
                  IL_0007:  ret
                
              
}
                  // end of method Unit::DoSomethingElse
                  
                
              

Each method requires a .vtentry directive to link it to the v-table fixup (the red font shows the slot number being specified) and an .export directive to indicate the exported name.

Assembling the file with the appropriate command-line produces:

 
                
              
C:\Temp> ilasm
                   /dll dotNetAssembly.il
                   /out:dotNetAssembly.dll /res:dotNetAssembly.res
                   /quiet
                  
                
              
Microsoft
                  (R) .NET Framework IL Assembler.  Version
                  1.1.4322.573
                    
                  
                
              
Copyright
                  (C) Microsoft Corporation 1998-2002. All rights reserved.
                    
                  
                
              
Assembling
                  'dotNetAssembly.il'
                  , no listing file, to DLL --> 'dotNetAssembly.dll'
                  
                
              
Source
                  file is ANSI
                
              
 
                
              
EmitExportStub
                  
                : dwVTFSlotRVA=0x00000000
                  
                
              
EmitExportStub
                  
                : dwVTFSlotRVA=0x00000004
                  
                
              
                
                  
                    Writing
                    
                  
                  
                    PE
                    
                  
                
                   file
                
              
Operation
                  completed successfully
                
              

The important information is that two export stubs are emitted. Indeed, using Delphi 's TDump utility with the -ee command-line switch (to list the exported routines) proves the point:

 
                
              
C:\Temp>tdump -ee
                   dotnetassembly.dll
                  
                
              
Turbo
                  Dump  Version 5.0.16.12 Copyright (c) 1988, 2000 Inprise
                  Corporation
                
              
               
                  Display of File DOTNETASSEMBLY.DLL
                
              
 
                
              
EXPORT
                  ord:0001='DoSomething'
                  
                
              
EXPORT
                  ord:0002='DoSomethingElse'
                  
                
              

Note : ilasm.exe is invoked with the /quiet command-line option which causes the omission of the usual output of every single method listed out as it gets assembled, followed by details of how many members were emitted in the PE file for each class.

These routines can now be called just like any other DLL routine from a Win32 application:

 
                
              
unit
                  
                dotNetAssemblyImport;
                  
                
              
 
                
              
interface
                  
                
              
 
                
              
procedure
                  
                DoSomething(I:
                  Integer); stdcall;
                  
                
              
procedure
                  
                DoSomethingElse(Msg:
                  PChar); stdcall;
                  
                
              
 
                
              
implementation
                  
                
              
 
                
              
const
                  
                
              
  dotNETAssembly
                   = 'dotNETAssembly.dll';
                  
                
              
 
                
              
procedure
                  
                DoSomething(I:
                  Integer); stdcall; externaldotNETAssembly;
                  
                
              
procedure
                  
                DoSomethingElse(Msg:
                  PChar); stdcall; externaldotNETAssembly;
                  
                
              
 
                
              
end
                  
                .
                
              

 

The same data marshaling rules apply with Inverse P/Invoke routines as apply to normal P/Invoke routines. Since we didn't specify any marshaling attributes in the declarations of the routines, the String parameter in DoSomethingElse will be marshaled as an ANSI string and so map onto a Win32 PChar (this is taken account of in the Win32 import unit above).

A Maintenance Nightmare?

The main issue developers see with Inverse P/Invoke is the maintenance problem. If you have to modify the compiler-generated assembly through creative round tripping then what happens when you recompile the DLL with your compiler? It would appear you have to go back and manually update the assembly again to export the routines.

Whilst this argument is valid, there is nothing stopping you from writing a utility that automates this post-compilation phase. Such a utility could then be incorporated into the build process and always be executed after the assembly is produced by the compiler.

Such a utility would be best written as a command-line application, however a GUI project that shows the idea accompanies this paper, called mme.dpr (Managed Method Exporter).

Summary

This paper has looked at the mechanisms that facilitate building Windows systems out of Win32 and .NET code. This will continue to be a useful technique whilst .NET is still at an early stage of its life and Win32 dominates in terms of existing systems and developer skills. Indeed, due to the nature of legacy code this may continue long after .NET programming dominates the Windows arena.

The coverage of interoperability mechanism has been intended to be complete enough to get you started without having too many unanswered questions. However it is inevitable in a paper of this size that much information has been omitted. The references below should provide much of the information that could not be fitted into this paper.

References

  1. .NET Interoperability: COM Interop by Brian Long.
    This paper looks at the issues involved in .NET code using Win32 COM objects and also Win32 COM client applications accessing.NET objects, using the COM Interop mechanism.
  2. .NET and COM, The Complete Interoperability Guide by Adam Nathan (of Microsoft), SAMS.
    This covers everything you will need to know about interoperability between .NET and COM, plus lots more you won't ever need.
  3. PINVOKE.NET, managed by Adam Nathan.
    This is an online repository of C# P/Invoke signatures for Win32 APIs.
  4. Delphi for .NET Developer’ Guide by Xavier Pacheco.
    There is plenty of additional detail to the subjects covered in this paper in my chapter contributed to this book: Chapter 16, Interoperability – COM Interop and the Platform Invocation Service.
  5. Inside Microsoft .NET IL Assembler by Serge Lidin (of Microsoft), Microsoft Press.
    This book describes the CIL (Common Intermediate Language) in detail and is the only text I've seen that shows how to export .NET assembly methods for Win32 clients. The author was responsible for developing the IL Disassembler and Assembler and various other aspects of the .NET Framework.

About Brian Long

Brian Long used to work at Borland UK , performing a number of duties including Technical Support on all the programming tools. Now he is a trouble-shooter, trainer and technical writer on all things .NET as well as a malware (spyware, adware, rootkits etc.) remover.

Besides authoring a Borland Pascal problem-solving book published in 1994 and contributing towards a Delphi for .NET book in 2004, Brian is a regular columnist in The Delphi Magazine and has had numerous articles published in Developer's Review, Computing, Delphi Developer's Journal and EXE Magazine. He was nominated for the Spirit of Delphi 2000 award and was voted Best Speaker at Borland's BorCon 2002 conference in Anaheim, California by the conference delegates.

There are a growing number of conference papers and articles available on Brian's Web site, so feel free to have a browse.