Delphi2005 Learning Note 5 Net Interoperability: .NET ↔ Win32

xiaoxiao2021-03-06  45

The following content is reproduced

http://www.blong.com/conference/borconuk2002/interop1/win32anddotnetinterop.htm#pinvoke

.NET Interoperability: .NET ↔ Win32

Brian Long (www.blong.com)

Table of contents

Introduction .NET Clients Using Win32 DLL EXPORTS (P / Invoke)

Traditional Syntax Attribute Syntax Working Out The Parameter Types Win32 Errors Hresult Return Values ​​Performance Issues Win32 Clients Using .NET Assembly Exports (Inverse P / Invoke)

Creative Round Tripping

Round Tripping, Step 1: Disassembly Round Tripping, Step 2:? Reassembly Modifying A .NET Assembly Manifest Exporting .NET Methods A Maintenance Nightmare Summary References / Further Reading About Brian Long

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 USAFTER ANITIAL 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.

.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 (useing the win32 apis messagebeep and beep) or performing high accountcounter 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 DOSMETHING (I: Integer): bool; cdecl;

Function DOSMELSEA (MSG: PCHAR): BOOL; CDECL;

Function DOSMELSEW (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.

.

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 Und 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 DOSMETHING (I: Integer): Boolean; CDECL;

IMPLEMentation

Const

Win32DLL = 'Win32dll.dll';

Function DOSMETHING (I: integer): boolean;

External Win32dll Name 'DOSMETHING'; END.

THE CALLING CDECL DIRECTIVE IN THE DECLATION 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 Return Types The Declaration To Use The P / Invoke Attribute, Like this:

Function DOSMETHING (I: integer): boolean;

...

[DLLIMPORT (Win32DLL, CALLINGCONVENTION = CALLINGCONVENTION.CDECL)]

Function DOSMETHING (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 this the routine buys textual parameters or return type.

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 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 DOSMELSE (Const Msg: String): Boolean; CDECL;

...

Function Dosomethingelse (const msg: string): boolean;

External Win32DLL;

THIS I Same as Explicitly Writing The P / Invoke Attribute Like this: Function Dosomethingelse (const msg: string): boolean;

...

[DLLIMPORT (Win32DLL, EntryPoint = 'DOSMETHINGELSE', 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 = 'DOSMETHINGELSE', 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 DOSMELSEA (Const msg: string): boolean;

Function DOSMELSEW (Const Msg: WideString): Boolean;

...

[DLLIMPORT (Win32DLL, Charset = Charset.ansi, CallingConvention = CALLINGCONVENTION.CDECL)]

Function DOSMELSEA (Const msg: string): boolean;

EXTERNAL;

[DLLIMPORT (Win32DLL, Charset = charset.unicode, callingconvention = calingconvention.cdecl)]

Function DOSMELSEW (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 does not 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 ., iv,.

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 thesese routines can be found in borland.win32.windows.pas. The declarations in the interface:

Function GetUsername (LPBuffer: Stringbuilder; VAR NSIZE: DWORD): BOOL; stdcall;

Function getComputename (LPBUFFER: STRINGBUILDER; VAR NSIZE: DWORD): BOOL; stdcall;

Function getWindowsDirectory (LPBUFFER: STRINGBUILDER; UIZE: 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;

Userbufflen: dWord;

Begin

Userbuf: = StringBuilder.create (64);

Userbuflen: = Userbuf.capacity;

IF getUsername (Userbuf, Userbufflen) THEN

Messagebox.show (userbuf.tostring)

Else

// User name is for Than 64 Characters

END;

Procedure tfrmpinvoke.btncomputernameClick (Sender: Tobject; Args: Eventargs);

VAR

Computerbuf: StringBuilder;

Computerbuflen: DWORD;

Begin

// set max size buffer to ensure succes

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 SuccessGetWindowsDirectory;

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 succes

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 usteation section:

Function getcomputername; External kernel32;

HOWEVER THE DELPHIES THISSUE BY EXPLICITLY SETTING ITLASTERROR 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, Implement 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 unit of their scout. if you want to call the helper routine you will need to flulily qualify it, calling borland.delphi.system.getlasterror.

TO AID MOVING API-BASED CODE ACROS WE CAN IMPLEMENT .NET Equivalent 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: = 'unrecognized 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 ('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 dam

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;

Userbufflen: longword;

Begin

// Buffer Too Small, So We Will Get An Exception

Userbuf: = StringBuilder.create (2);

Userbuflen: = Userbuf.capacity;

Win32Check (getUserName (userbuf, userbuffle);

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

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 tries

One Way of Writing The P / Invoke Import Is Using The Standard Delphi Syntax:

Function CLSIDFROGID ([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 CLSIDFROGID (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 CLSIDFROGID ([Marshalas (UnmanagedType.lpwstr)] PPSZ: String; Out rclsid: guid);

EXTERNAL;

OR:

[DLLIMPORT (OLE32, Charset = charset.unicode, preservesig = false)]

Procedure CLSIDFROGID (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 (VARGEINTEGER): BOOL; stdcall;

External kernel32 name 'queryperformancecounter';

Function QueryperFormanceFrequency (VAR LPFREQUENCY: TLARGEINTEGER): Bool; stdcall;

External kernel32 name 'queryperformancefrequency'; The Logical Way of Translasing these Routines Would Be Like this:

Function QueryperFormanceCounter (VAR LPPERFORMANCUNT: 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 Effect 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), SuppressunmanageDcodeeSecurity]

Function QueryperFormanceCounter (OUT LPPERFORMANT: INT64): LongBool;

EXTERNAL;

[DLLIMPORT (kernel32), SuppressunmanageDcodeeSecurity]

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 can not 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 Permissions 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 mathal.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, us:

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 There 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 is 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..

Creative Round Tripping IS Used In There Scenarios:

CHANGING THE IL CODE generated by a compiler in a Way not permitted by the comport, such as to extra

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

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 DOSMETHING (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 Thought The 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 user:

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 everyhes back to a .net assembly you Use the il assembler, ilrosm.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

Alignment 512

.corflags 0x00000001

THE FIRST CHANGE IS To Define A V-Table Fixup Containing As Many Slots As There Wave We Have TW Methods To Export, So The Manifest Should Be Changed To this:

.Module DotNetassembly.dll

// mvid: {b865276c-a90f-4ca2-8af1-0BF42A04A451}

.imagebase 0x00400000

.subsystem 0x00000002

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

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

To Turn Them Into Unmanaged Exports The Y Y 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 doomething

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

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 IMPORT 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 = 'DOSMETHING'

Export ORD: 0002 = 'DOSMETHINGELSE'

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 ROM version:

Unit DotNetassemblyImport;

Interface

Procedure DOSMETHING (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 RUTINES APPLY TO NORMAL P / Invoke Routines. Since We Didn't Specify Any Marshaling Attributes in The Declarations of The Routines, The

String Parameter in

DOSMELSE 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 askESSEMBLY 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 .

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

.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 .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 will not ever need. 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 assects 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, 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 Jugning and Making Inflable Origami Paper Frogs.

转载请注明原文地址:https://www.9cbs.com/read-74633.html

New Post(0)