综合

img shidongdong

Delphi2005学习笔记5 NET Interoperability: .NET ↔ Win32

发表于2004/12/30 21:54:00  1318人阅读

分类: Delphi .Net

以下内容转载自http://www.blong.com/Conferences/BorConUK2002/Interop1/Win32AndDotNetInterop.htm#PInvoke
.NET

.NET Interoperability: .NET ↔ Win32

Brian Long (www.blong.com)

Table of Contents

Click here to download the files associated with this article.


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 to Borland Delphi for .NET, 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 marshaling is done for us after an initial process to set up the inter-platform calls.

Note: the information in the paper is based around the Delphi for .NET Preview compiler as shipped with Delphi 7 and Updates to it made available after Delphi 7's release. At the time of writing the Delphi for .NET Preview compiler represents the only .NET-enabled version of Delphi available. When Delphi 8 for .NET (codenamed Octane) becomes available, various steps in this paper that use the Delphi for .NET Preview compiler, as well as some of the code snippets, may require changes.

Note: an updated version of this article specific to Delphi 8 for .NET is now available online. Click here to read it.

.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).

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 that 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
  • 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
  • None: this is the same 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 that 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.

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.Win32.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, that 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.Win32.Windows.pas. The declarations in the interface section look like this:


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

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).

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). The Borland.Win32.Windows.pas unit does provide such a declaration.

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 that all the Win32 imports in Delphi for .NET Preview omit this field, meaning it defaults to False and so you will not get any OS error information without redeclaring the pertinent APIs. For example, this is the GetComputerName declaration from the Borland.Win32.Windows.pas implementation section:


function GetComputerName; external kernel32;

However the Delphi for .NET Preview Update remedies this issue by explicitly setting SetLastError to True for all its declarations, giving P/Invoke import declarations like:


const
  advapi32  = 'advapi32.dll';
  kernel32  = 'kernel32.dll';

[DllImport(advapi32, CharSet = CharSet.Auto, SetLastError = True, EntryPoint = 'GetUserName')]
function GetUserName; external;
[DllImport(kernel32, CharSet = CharSet.Auto, SetLastError = True, EntryPoint = 'GetComputerName')]
function GetComputerName; external;
[DllImport(kernel32, CharSet = CharSet.Auto,  SetLastError = True, EntryPoint = 'GetWindowsDirectory')]
function GetWindowsDirectory; external;
[DllImport(kernel32, CharSet = CharSet.Auto, SetLastError = True, EntryPoint = 'GetSystemDirectory')]
function GetSystemDirectory; external;
[DllImport(kernel32, CharSet = CharSet.Auto, SetLastError = True, EntryPoint = 'GetCurrentDirectory')]
function GetCurrentDirectory; external;

Also note that Delphi for .NET Preview Update introduces 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.Win32.Windows and call GetLastError in the same unit, the compiler will call the P/Invoke version in the import unit rather than the helper routine in the Borland.Delphi.System.pas, due to the order in which the units are used and the resultant priority of their scope. If you want to call the helper routine you will need to fully qualify it, calling Borland.Delphi.System.GetLastError.

To aid moving API-based code across we can implement .NET equivalents of the Delphi Win32 error support routines, using the Win32 routine FormatMessage, declared in Borland.Win32.Windows.pas.


function SysErrorMessage(ErrorCode: Integer): string;
var
  Msg: StringBuilder;
const
  FORMAT_MESSAGE_FROM_SYSTEM = $1000;
begin
  Msg := StringBuilder.Create(1024);
  if FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nil, ErrorCode, 0, Msg, Msg.Capacity, nil) <> 0 then
    Result := Msg.ToString
  else
    Result := 'Unrecognised error'
end;

type
  EWin32Error = class(Exception)
  public
    ErrorCode: Integer;
  end;

procedure RaiseLastWin32Error;
var
  LastError: Integer;
begin
  LastError := Marshal.GetLastWin32Error;
  if LastError <> 0 then
    raise EWin32Error.Create(Format('System Error.  Code: %d.'#13'%s', [LastError, SysErrorMessage(LastError)]))
  else
    raise EWin32Error.Create('A call to a Win32 function failed')
end;

function Win32Check(RetVal: Bool): Bool;
begin
  if not RetVal then
    RaiseLastWin32Error;
  Result := RetVal;
end;

The calls to the routines can now be written just as in normal Win32 Delphi applications, for example:


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;

Because Delphi for .NET Preview does not yet have the application-wide default exception handler, any exception generated here will by default produce a dialog 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; stdcall;
external kernel32;
function QueryPerformanceFrequency(var lpFrequency: Int64): Boolean; stdcall;
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; stdcall;
external kernel32;
function QueryPerformanceFrequency(out lpFrequency: Int64): LongBool; stdcall;
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, this really means they are part of the projectname.Unit namespace (dotNetApp.Unit in this case). So to prelink 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 prelink 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 prelink code, the first tests will indeed be a little slower than subsequent tests in the same program run. Prelinking the specified routines individually removes this first-hit delay, but curiouslyI found calling PrelinkAll makes each test in a given session noticeably quicker than with any of the previous tests.

Win32 Clients Using .NET Assembly 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 C++ With Managed Extensions (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 3).

Creative Round Tripping

Because programming tools do not cater for exporting .NET assembly methods we have to resort to some cunning trickery to take advantage of this technique, 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, 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:

  • changing the IL code generated by a compiler in a way not permitted by the compiler, such as to export .NET assembly methods
  • adding custom ILAsm code to your classes
  • merging several modules into one module

Clearly we will be focusing on the first area, since Delphi for .NET does not support exporting .NET methods. 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. Let's take an example assembly, dotNetAssembly.dll, which contains two standalone routines DoSomething and DoSomethingElse:


procedure DoSomething(I: Integer);
begin
  MessageBox.Show(IntToStr(I))
end;

procedure DoSomethingElse(const Msg: String); 
begin
  MessageBox.Show(Msg)
end;

Note that whilst these are written as standalone routines, Delphi for .NET will ensure they are implemented as static class 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 these routines end up acting as though they were declared something like:


type
  Unit = class(System.Object)
  public
    class static procedure DoSomething(I: Integer);
    class static procedure DoSomethingElse(const Msg: String); 
  end;

To disassemble the assembly use:


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). There will be references to other assemblies in the manifest as well as general module information:


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


.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. 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. To resolve the problem you must specify the COMIMAGE_FLAGS_32BITREQUIRED flag (whose value is 2):


.module dotNetAssembly.dll
// MVID: {B865276C-A90F-4CA2-8AF1-0BF42A04A451}
.imagebase 0x00400000
.subsystem 0x00000002
.file alignment 512
.corflags 0x00000002
.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.0.3705.0
Copyright (C) Microsoft Corporation 1998-2001. 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 that ilasm.exe is invoked with the /quiet command-line option to avoid seeing 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; external dotNETAssembly;
procedure DoSomethingElse(Msg: PChar); stdcall; external dotNETAssembly;

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 a 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. 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. Since leaving in 1995, Brian has been providing training and consultancy services to the Delphi and C++Builder communities, and more recently to the .NET community.

If you need training in these products, or need solutions to problems you have with them, please get in touch, or visit Brian's Web site.

Besides authoring a Borland Pascal problem-solving book published in 1994, 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.

In his spare time (and waiting for his C++ programs to compile) Brian has learnt the art of juggling and making inflatable origami paper frogs.

阅读全文
0 0

相关文章推荐

img
取 消
img