12.3 Programming in Delphi/Kylix and HLA
Delphi is a marvelous language for writing Win32 GUI-based applications. Kylix is the companion product that runs under Linux. Their support for Rapid Application Design (RAD) and visual programming is superior to almost every other Windows or Linux programming approach available. However, being Pascal-based, there are some things that just cannot be done in Delphi/Kylix and many things that cannot be done as efficiently in Delphi/Kylix as in assembly language. Fortunately, Delphi/Kylix lets you call assembly language procedures and functions so you can overcome Delphi's limitations.
Delphi provides two ways to use assembly language in the Pascal code: via a built-in assembler (BASM) or by linking in separately compiled assembly language modules. The built-in "Borland Assembler" (BASM) is a very weak Intel-syntax assembler. It is suitable for injecting a few instructions into your Pascal source code or perhaps writing a very short assembly language function or procedure. It is not suitable for serious assembly language programming. If you know Intel syntax and you only need to execute a few machine instructions, then BASM is perfect. However, since this is a text on assembly language programming, the assumption here is that you want to write some serious assembly code to link with your Pascal/Delphi code. To do that, you will need to write the assembly code and compile it with a different assembler (e.g., HLA) and link the code into your Delphi application. That is the approach this section will concentrate on. For more information about BASM, check out the Delphi documentation.
Before we get started discussing how to write HLA modules for your Delphi programs, you must understand two very important facts:
- HLA's exception handling facilities are not directly compatible with Delphi's. This means that you cannot use the TRY..ENDTRY and RAISE statements in the HLA code you intend to link to a Delphi program. This also means that you cannot call library functions that contain such statements. Since the HLA Standard Library modules use exception handling statements all over the place, this effectively prevents you from calling HLA Standard Library routines from the code you intend to link with Delphi1.
- Although you can write console applications with Delphi, 99% of Delphi applications are GUI applications. You cannot call console-related functions (e.g., stdin.xxxx or stdout.xxxx) from a GUI application. Even if HLA's console and standard input/output routines didn't use exception handling, you wouldn't be able to call them from a standard Delphi application.
Given the rich set of language features that Delphi supports, it should come as no surprise that the interface between Delphi's Object Pascal language and assembly language is somewhat complex. Fortunately there are two facts that reduce this problem. First, HLA uses many of the same calling conventions as Pascal; so much of the complexity is hidden from sight by HLA. Second, the other complex stuff you won't use very often, so you may not have to bother with it.
- Note: the following sections assume you are already familiar with Delphi programming. They make no attempt to explain Delphi syntax or features other than as needed to explain the Delphi assembly language interface. If you're not familiar with Delphi, you will probably want to skip this section.
12.3.1 Linking HLA Modules With Delphi Programs
The basic unit of interface between a Delphi program and assembly code is the procedure or function. That is, to combine code between the two languages you will write procedures in HLA (that correspond to procedures or functions in Delphi) and call these procedures from the Delphi program. Of course, there are a few mechanical details you've got to worry about, this section will cover those.
To begin with, when writing HLA code to link with a Delphi program you've got to place your HLA code in an HLA UNIT. An HLA PROGRAM module contains start up code and other information that the operating system uses to determine where to begin program execution when it loads an executable file from disk. However, the Delphi program also supplies this information and specifying two starting addresses confuses the linker, therefore, you must place all your HLA code in a UNIT rather than a PROGRAM module.
Within the HLA UNIT you must create EXTERNAL procedure prototypes for each procedure you wish to call from Delphi. If you prefer, you can put these prototype declarations in a header file and #INCLUDE them in the HLA code, but since you'll probably only reference these declarations from this single file, it's okay to put the EXTERNAL prototype declarations directly in the HLA UNIT module. These EXTERNAL prototype declarations tell HLA that the associated functions will be public so that Delphi can access their names during the link process. Here's a typical example:
unit LinkWithDelphi; procedure prototype; external; procedure prototype; begin prototype; << Code to implement prototype's functionality >> end prototype; end LinkWithDelphi;After creating the module above, you'd compile it using HLA's "-s" (compile to assembly only) command line option. This will produce an ASM file. Were this just about any other language, you'd then assemble the ASM file with MASM. Unfortunately, Delphi doesn't like OBJ files that MASM produces. For all but the most trivial of assembly modules, Delphi will reject the MASM's output. Borland Delphi expects external assembly modules to be written with Borland's assembler, TASM32.EXE (the 32-bit Turbo Assembler). Fortunately, as of HLA v1.26, HLA provides an option to produce TASM output that is compatible with TASM v5.3 and later. Unfortunately, Borland doesn't really sell TASM anymore; the only way to get a copy of TASM v5.3 is to obtain a copy of Borlands C++ Builder Professional system which includes TASM32 v5.3. If you don't own Borland C++ and really have no interest in using C++ Builder, Borland has produced an evaluation disk for C++ Builder that includes TASM 5.3. Note that earlier versions of TASM32 (e.g., v5.0) do not support MMX and various Pentium-only instructions, you really need TASM v5.3 if you want ot use the MASM output.
Here are all the commands to compile and assemble the module given earlier:
hla -c -tasm -omf LinkWithDelphi.hlaOf course, if you don't like typing this long command to compile and assemble your HLA code, you can always create a make file or a batch file that will let you do both operations with a single command. See the chapter on Managing Large Programs for more details (see "Make Files" on page 578).
After creating the module above, you'd compile it using HLA's "-c" (compile to object only) command line option. This will produce an object (".o") file.
Once you've created the HLA code and compiled it to an object file, the next step is to tell Delphi that it needs to call the HLA/assembly code. There are two steps needed to achieve this: You've got to inform Delphi that a procedure (or function) is written in assembly language (rather than Pascal) and you've got to tell Delphi to link in the object file you've created when compiling the Delphi code.
The second step above, telling Delphi to include the HLA object module, is the easiest task to achieve. All you've got to do is insert a compiler directive of the form "{$L objectFileName.obj }" in the Delphi program before declaring and calling your object module. A good place to put this is after the implementation reserved word in the module that calls your assembly procedure. The code examples a little later in this section will demonstrate this.
The next step is to tell Delphi that you're supplying an external procedure or function. This is done using the Delphi EXTERNAL directive on a procedure or function prototype. For example, a typical external declaration for the prototype procedure appearing earlier is
procedure prototype; external; // This may look like HLA code, but it's // really Delphi code!As you can see here, Delphi's syntax for declaring external procedures is nearly identical to HLA's (in fact, in this particular example the syntax is identical). This is not an accident, much of HLA's syntax was borrowed directly from Pascal.
The next step is to call the assembly procedure from the Delphi code. This is easily accomplished using standard Pascal procedure calling syntax. The following two listings provide a complete, working, example of an HLA procedure that a Delphi program can call. This program doesn't accomplish very much other than to demonstrate how to link in an assembly procedure. The Delphi program contains a form with a single button on it. Pushing the button calls the HLA procedure, whose body is empty and therefore returns immediately to the Delphi code without any visible indication that it was ever called. Nevertheless, this code does provide all the syntactical elements necessary to create and call an assembly language routine from a Delphi program.
unit LinkWithDelphi; procedure CalledFromDelphi; external; procedure CalledFromDelphi; begin CalledFromDelphi; end CalledFromDelphi; end LinkWithDelphi; Program 12.5 CalledFromDelphi.HLA Module Containing the Assembly Codeunit DelphiEx1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TDelphiEx1Form = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var DelphiEx1Form: TDelphiEx1Form; implementation {$R *.DFM} {$L CalledFromDelphi.obj } procedure CalledFromDelphi; external; procedure TDelphiEx1Form.Button1Click(Sender: TObject); begin CalledFromDelphi(); end; end. Program 12.6 DelphiEx1- Delphi Source Code that Calls an Assembly ProcedureThe full Delphi and HLA source code for the programs appearing in Program 12.5 and Program 12.6 accompanies the HLA software distribution in the appropriate subdirectory for this chapter in the Example code module. If you've got a copy of Delphi 5 or later, you might want to load this module and try compiling it. To compile the HLA code for this example, you would use the following commands from the command prompt:
hla -tasm -c -omf CalledFromDelphi.hlaAfter producing the CalledFromDelphi object module with the two commands above, you'd enter the Delphi Integrated Development Environment and tell it to compile the DelphiEx1 code (i.e., you'd load the DelphiEx1Project file into Delphi and the compile the code). This process automatically links in the HLA code and when you run the program you can call the assembly code by simply pressing the single button on the Delphi form.
12.3.2 Register Preservation
Delphi code expects all procedures to preserve the EBX, ESI, EDI, and EBP registers. Routines written in assembly language may freely modify the contents of EAX, ECX, and EDX without preserving their values. The HLA code will have to modify the ESP register to remove the activation record (and, possibly, some parameters). Of course, HLA procedures (unless you specify the @NOFRAME option) automatically preserve and set up EBP for you, so you don't have to worry about preserving this register's value; of course, you will not usually manipulate EBP's value since it points at your procedure's parameters and local variables.
Although you can modify EAX, ECX, and EDX to your heart's content and not have to worry about preserving their values, don't get the idea that these registers are available for your procedure's exclusive use. In particular, Delphi may pass parameters into a procedure within these registers and you may need to return function results in some of these registers. Details on the further use of these registers appears in later sections of this chapter.
Whenever Delphi calls a procedure, that procedure can assume that the direction flag is clear. On return, all procedures must ensure that the direction flag is still clear. So if you manipulate the direction flag in your assembly code (or call a routine that might set the direction flag), be sure to clear the direction flag before returning to the Delphi code.
If you use any MMX instructions within your assembly code, be sure to execute the EMMS instruction before returning. Delphi code assumes that it can manipulate the floating point stack without running into problems.
Although the Delphi documentation doesn't explicitly state this, experiments with Delphi code seem to suggest that you don't have to preserve the FPU (or MMX) registers across a procedure call other than to ensure that you're in FPU mode (versus MMX mode) upon return to Delphi.
12.3.3 Function Results
Delphi generally expects functions to return their results in a register. For ordinal return results, a function should return a byte value in AL, a word value in AX, or a double word value in EAX. Functions return pointer values in EAX. Functions return real values in ST0 on the FPU stack. The code example in this section demonstrates each of these parameter return locations.
For other return types (e.g., arrays, sets, records, etc.), Delphi generally passes an extra VAR parameter containing the address of the location where the function should store the return result. We will not consider such return results in this text, see the Delphi documentation for more details.
The following Delphi/HLA program demonstrates how to return different types of scalar (ordinal and real) parameters to a Delphi program from an assembly language function. The HLA functions return boolean (one byte) results, word results, double word results, a pointer (PChar) result, and a floating point result when you press an appropriate button on the form. See the DelphiEx2 example code in the HLA/Art of Assembly examples code for the full project. Note that the following code doesn't really do anything useful other than demonstrate how to return Function results in EAX and ST0.
unit DelphiEx2; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TDelphiEx2Form = class(TForm) BoolBtn: TButton; BooleanLabel: TLabel; WordBtn: TButton; WordLabel: TLabel; DWordBtn: TButton; DWordLabel: TLabel; PtrBtn: TButton; PCharLabel: TLabel; FltBtn: TButton; RealLabel: TLabel; procedure BoolBtnClick(Sender: TObject); procedure WordBtnClick(Sender: TObject); procedure DWordBtnClick(Sender: TObject); procedure PtrBtnClick(Sender: TObject); procedure FltBtnClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var DelphiEx2Form: TDelphiEx2Form; implementation {$R *.DFM} // Here's the directive that tells Delphi to link in our // HLA code. {$L ReturnBoolean.obj } {$L ReturnWord.obj } {$L ReturnDWord.obj } {$L ReturnPtr.obj } {$L ReturnReal.obj } // Here are the external function declarations: function ReturnBoolean:boolean; external; function ReturnWord:smallint; external; function ReturnDWord:integer; external; function ReturnPtr:pchar; external; function ReturnReal:real; external; // Demonstration of calling an assembly language // procedure that returns a byte (boolean) result. procedure TDelphiEx2Form.BoolBtnClick(Sender: TObject); var b:boolean; begin // Call the assembly code and return its result: b := ReturnBoolean; // Display "true" or "false" depending on the return result. if( b ) then booleanLabel.caption := `Boolean result = true ` else BooleanLabel.caption := `Boolean result = false'; end; // Demonstrate calling an assembly language function that // returns a word result. procedure TDelphiEx2Form.WordBtnClick(Sender: TObject); var si:smallint; // Return result here. strVal:string; // Used to display return result. begin si := ReturnWord(); // Get result from assembly code. str( si, strVal ); // Convert result to a string. WordLabel.caption := `Word Result = ` + strVal; end; // Demonstration of a call to an assembly language routine // that returns a 32-bit result in EAX: procedure TDelphiEx2Form.DWordBtnClick(Sender: TObject); var i:integer; // Return result goes here. strVal:string; // Used to display return result. begin i := ReturnDWord(); // Get result from assembly code. str( i, strVal ); // Convert that value to a string. DWordLabel.caption := `Double Word Result = ` + strVal; end; // Demonstration of a routine that returns a pointer // as the function result. This demo is kind of lame // because we can't initialize anything inside the // assembly module, but it does demonstrate the mechanism // even if this example isn't very practical. procedure TDelphiEx2Form.PtrBtnClick(Sender: TObject); var p:pchar; // Put returned pointer here. begin // Get the pointer (to a zero byte) from the assembly code. p := ReturnPtr(); // Display the empty string that ReturnPtr returns. PCharLabel.caption := `PChar Result = "` + p + `"'; end; // Quick demonstration of a function that returns a // floating point value as a function result. procedure TDelphiEx2Form.FltBtnClick(Sender: TObject); var r:real; strVal:string; begin // Call the assembly code that returns a real result. r := ReturnReal(); // Always returns 1.0 // Convert and display the result. str( r:13:10, strVal ); RealLabel.caption := `Real Result = ` + strVal; end; end. Program 12.7 DelphiEx2: Pascal Code for Assembly Return Results Example// ReturnBooleanUnit- // // Provides the ReturnBoolean function for the DelphiEx2 program. unit ReturnBooleanUnit; // Tell HLA that ReturnBoolean is a public symbol: procedure ReturnBoolean; external; // Demonstration of a function that returns a byte value in AL. // This function simply returns a boolean result that alterates // between true and false on each call. procedure ReturnBoolean; @nodisplay; @noalignstack; @noframe; static b:boolean:=false; begin ReturnBoolean; xor( 1, b ); // Invert boolean status and( 1, b ); // Force to zero (false) or one (true). mov( b, al ); // Function return result comes back in AL. ret(); end ReturnBoolean; end ReturnBooleanUnit; Program 12.8 ReturnBoolean: Demonstrates Returning a Byte Value in AL // ReturnWordUnit- // // Provides the ReturnWord function for the DelphiEx2 program. unit ReturnWordUnit; procedure ReturnWord; external; procedure ReturnWord; @nodisplay; @noalignstack; @noframe; static w:int16 := 1234; begin ReturnWord; // Increment the static value by one on each // call and return the new result as the function // return value. inc( w ); mov( w, ax ); ret(); end ReturnWord; end ReturnWordUnit; Program 12.9 ReturnWord: Demonstrates Returning a Word Value in AX // ReturnDWordUnit- // // Provides the ReturnDWord function for the DelphiEx2 program. unit ReturnDWordUnit; procedure ReturnDWord; external; // Same code as ReturnWord except this one returns a 32-bit value // in EAX rather than a 16-bit value in AX. procedure ReturnDWord; @nodisplay; @noalignstack; @noframe; static d:int32 := -7; begin ReturnDWord; inc( d ); mov( d, eax ); ret(); end ReturnDWord; end ReturnDWordUnit; Program 12.10 ReturnDWord: Demonstrates Returning a DWord Value in EAX // ReturnPtrUnit- // // Provides the ReturnPtr function for the DelphiEx2 program. unit ReturnPtrUnit; procedure ReturnPtr; external; // This function, which is lame, returns a pointer to a zero // byte in memory (i.e., an empty pchar string). Although // not particularly useful, this code does demonstrate how // to return a pointer in EAX. procedure ReturnPtr; @nodisplay; @noalignstack; @noframe; static stringData: byte; @nostorage; byte "Pchar object", 0; begin ReturnPtr; lea( eax, stringData ); ret(); end ReturnPtr; end ReturnPtrUnit; Program 12.11 ReturnPtr: Demonstrates Returning a 32-bit Address in EAX// ReturnRealUnit- // // Provides the ReturnReal function for the DelphiEx2 program. unit ReturnRealUnit; procedure ReturnReal; external; procedure ReturnReal; @nodisplay; @noalignstack; @noframe; static realData: real80 := 1.234567890; begin ReturnReal; fld( realData ); ret(); end ReturnReal; end ReturnRealUnit; Program 12.12 ReturnReal: Demonstrates Returning a Real Value in ST0The second thing to note is the #code, #static, etc., directives at the beginning of each file to change the segment name declarations. You'll learn the reason for these segment renaming directives a little later in this chapter.
12.3.4 Calling Conventions
Delphi supports five different calling mechanisms for procedures and functions: register, pascal, cdecl, stdcall, and safecall. The register and pascal calling methods are very similar except that the pascal parameter passing scheme always passes all parameters on the stack while the register calling mechanism passes the first three parameters in CPU registers. We'll return to these two mechanisms shortly since they are the primary mechanisms we'll use. The cdecl calling convention uses the C/C++ programming language calling convention. We'll study this scheme more in the section on interfacing C/C++ with HLA. There is no need to use this scheme when calling HLA procedures from Delphi. If you must use this scheme, then see the section on the C/C++ languages for details. The stdcall convention is used to call Windows API functions. Again, there really is no need to use this calling convention, so we will ignore it here. See the Delphi documentation for more details. Safecall is another specialized calling convention that we will not use. See, we've already reduced the complexity from five mechanisms to two! Seriously, though, when calling assembly language routines from Delphi code that you're writing, you only need to use the pascal and register conventions.
The calling convention options specify how Delphi passes parameters between procedures and functions as well as who is responsible for cleaning up the parameters when a function or procedure returns to its caller. The pascal calling convention passes all parameters on the stack and makes it the procedure or function's responsibility to remove those parameters from the stack. The pascal calling convention mandates that the caller push parameters in the order the compiler encounters them in the parameter list (i.e., left to right). This is exactly the calling convention that HLA uses (assuming you don't use the "IN register" parameter option). Here's an example of a Delphi external procedure declaration that uses the pascal calling convention:
procedure UsesPascal( parm1:integer; parm2:integer; parm3:integer );The following program provides a quick example of a Delphi program that calls an HLA procedure (function) using the pascal calling convention.
unit DelphiEx3; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) callUsesPascalBtn: TButton; UsesPascalLabel: TLabel; procedure callUsesPascalBtnClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} {$L usespascal.obj} function UsesPascal ( parm1:integer; parm2:integer; parm3:integer ):integer; pascal; external; procedure TForm1.callUsesPascalBtnClick(Sender: TObject); var i: integer; strVal: string; begin i := UsesPascal( 5, 6, 7 ); str( i, strVal ); UsesPascalLabel.caption := `Uses Pascal = ` + strVal; end; end. Program 12.13 DelphiEx3 - Sample Program that Demonstrates the pascal Calling Convention
// UsesPascalUnit- // // Provides the UsesPascal function for the DelphiEx3 program. unit UsesPascalUnit; // Tell HLA that UsesPascal is a public symbol: procedure UsesPascal( parm1:int32; parm2:int32; parm3:int32 ); external; // Demonstration of a function that uses the PASCAL calling convention. // This function simply computes parm1+parm2-parm3 and returns the // result in EAX. Note that this function does not have the // "NOFRAME" option because it needs to build the activation record // (stack frame) in order to access the parameters. Furthermore, this // code must clean up the parameters upon return (another chore handled // automatically by HLA if the "NOFRAME" option is not present). procedure UsesPascal( parm1:int32; parm2:int32; parm3:int32 ); @nodisplay; @noalignstack; begin UsesPascal; mov( parm1, eax ); add( parm2, eax ); sub( parm3, eax ); end UsesPascal; end UsesPascalUnit; Program 12.14 UsesPascal - HLA Function the Previous Delphi Code Will CallTo compile the HLA code, you would use the following two commands in a command window:
hla -st UsesPascal.hla tasm32 -mx -m9 UsesPascal.asmOnce you produce the .o file with the above two commands, you can get into Delphi and compile the Pascal code.
The register calling convention also processes parameters from left to right and requires the procedure/function to clean up the parameters upon return; the difference is that procedures and functions that use the register calling convention will pass their first three (ordinal) parameters in the EAX, EDX, and ECX registers (in that order) rather than on the stack. You can use HLA's "IN register" syntax to specify that you want the first three parameters passed in this registers, e.g.,
procedure UsesRegisters ( parm1:int32 in EAX; parm2:int32 in EDX; parm3:int32 in ECX );If your procedure had four or more parameters, you would not specify registers as their locations. Instead, you'd access those parameters on the stack. Since most procedures have three or fewer parameters, the register calling convention will typically pass all of a procedure's parameters in a register.
Although you can use the register keyword just like pascal to force the use of the register calling convention, the register calling convention is the default mechanism in Delphi. Therefore, a Delphi declaration like the following will automatically use the register calling convention:
procedure UsesRegisters ( parm1:integer; parm2:integer; parm3:integer ); external;The following program is a modification of the previous program in this section that uses the register calling convention rather than the pascal calling convention.
unit DelphiEx4; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) callUsesRegisterBtn: TButton; UsesRegisterLabel: TLabel; procedure callUsesRegisterBtnClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} {$L usesregister.obj} function UsesRegister ( parm1:integer; parm2:integer; parm3:integer; parm4:integer ):integer; external; procedure TForm1.callUsesRegisterBtnClick(Sender: TObject); var i: integer; strVal: string; begin i := UsesRegister( 5, 6, 7, 3 ); str( i, strVal ); UsesRegisterLabel.caption := `Uses Register = ` + strVal; end; end. Program 12.15 DelphiEx4 - Using the register Calling Convention // UsesRegisterUnit- // // Provides the UsesRegister function for the DelphiEx4 program. unit UsesRegisterUnit; // Tell HLA that UsesRegister is a public symbol: procedure UsesRegister ( parm1:int32 in eax; parm2:int32 in edx; parm3:int32 in ecx; parm4:int32 ); external; // Demonstration of a function that uses the REGISTER calling convention. // This function simply computes (parm1+parm2-parm3)*parm4 and returns the // result in EAX. Note that this function does not have the // "NOFRAME" option because it needs to build the activation record // (stack frame) in order to access the fourth parameter. Furthermore, this // code must clean up the fourth parameter upon return (another chore handled // automatically by HLA if the "NOFRAME" option is not present). procedure UsesRegister ( parm1:int32 in eax; parm2:int32 in edx; parm3:int32 in ecx; parm4:int32 ); @nodisplay; @noalignstack; begin UsesRegister; mov( parm1, eax ); add( parm2, eax ); sub( parm3, eax ); intmul( parm4, eax ); end UsesRegister; end UsesRegisterUnit; Program 12.16 HLA Code to support the DelphiEx4 Program
To compile the HLA code, you would use the following two commands in a command window:
hla -st UsesRegister.hla tasm32 -mx -m9 UsesRegister.hlaOnce you produce the OBJ file with the above command, you can get into Delphi and compile the Pascal code.
12.3.5 Pass by Value, Reference, CONST, and OUT in Delphi
A Delphi program can pass parameters to a procedure or function using one of four different mechanisms: pass by value, pass by reference, CONST parameters, and OUT parameters. The examples up to this point in this chapter have all used Delphi's (and HLA's) default pass by value mechanism. In this section we'll look at the other parameter passing mechanisms.
HLA and Delphi also share a (mostly) common syntax for pass by reference parameters. The following two lines provide an external declaration in Delphi and the corresponding external (public) declaration in HLA for a pass by reference parameter using the pascal calling convention:
procedure HasRefParm( var refparm: integer ); pascal; external; // Delphi procedure HasRefParm( var refparm: int32 ); external; // HLALike HLA, Delphi will pass the 32-bit address of whatever actual parameter you specify when calling the HasRefParm procedure. Don't forget, inside the HLA code, that you must dereference this pointer to access the actual parameter data. See the chapter on Intermediate Procedures for more details (see "Pass by Reference" on page 817).
The CONST and OUT parameter passing mechanisms are virtually identical to pass by reference. Like pass by reference these two schemes pass a 32-bit address of their actual parameter. The difference is that the called procedure is not supposed to write to CONST objects since they're, presumably, constant. Conversely, the called procedure is supposed to write to an OUT parameter (and not assume that it contains any initial value of consequence) since the whole purpose of an OUT parameter is to return data from a procedure or function. Other than the fact that the Delphi compiler will check procedures and functions (written in Delphi) for compliance with these rules, there is no difference between CONST, OUT, and reference parameters. Delphi passes all such parameters by reference to the procedure or function. Note that in HLA you would declare all CONST and OUT parameters as pass by reference parameters. HLA does not enforce the readonly attribute of the CONST object nor does it check for an attempt to access an uninitialized OUT parameter; those checks are the responsibility of the assembly language programmer.
As you learned in the previous section, by default Delphi uses the register calling convention. If you pass one of the first three parameters by reference to a procedure or function, Delphi will pass the address of that parameter in the EAX, EDX, or ECX register. This is very convenient as you can immediately apply the register indirect addressing mode without first loading the parameter into a 32-bit register.
Like HLA, Delphi lets you pass untyped parameters by reference (or by CONST or OUT). The syntax to achieve this in Delphi is the following:
procedure UntypedRefParm( var parm1; const parm2; out parm3 ); external;Note that you do not supply a type specification for these parameters. Delphi will compute the 32-bit address of these objects and pass them on to the UntypedRefParm procedure without any further type checking. In HLA, you can use the VAR keyword as the data type to specify that you want an untyped reference parameter. Here's the corresponding prototype for the UntypedRefParm procedure in HLA:
procedure UntypedRefParm( var parm1:var; var parm2:var; var parm3:var ); external;As noted above, you use the VAR keyword (pass by reference) when passing CONST and OUT parameters. Inside the HLA procedure it's your responsibility to use these pointers in a manner that is reasonable given the expectations of the Delphi code.
12.3.6 Scalar Data Type Correspondence Between Delphi and HLA
When passing parameters between Delphi and HLA procedures and functions, it's very important that the calling code and the called code agree on the basic data types for the parameters. In this section we will draw a correspondence between the Delphi scalar data types and the HLA (v1.x) data types2.
Assembly language supports any possible data format, so HLA's data type capabilities will always be a superset of Delphi's. Therefore, there may be some objects you can create in HLA that have no counterpart in Delphi, but the reverse is not true. Since the assembly functions and procedures you write are generally manipulating data that Delphi provides, you don't have to worry too much about not being able to process some data passed to an HLA procedure by Delphi3.
Delphi provides a wide range of different integer data types. The following table lists the Delphi types and the HLA equivalents:
Table 1: Delphi and HLA Integer Types Delphi HLA Equivalent Range Minimum Maximum integer int321 -2147483648 2147483647 cardinal uns322 0 4294967295 shortint int8 -128 127 smallint int16 -32768 32767 longint int32 -2147483648 2147483647 int64 qword -263 (263-1) byte uns8 0 255 word uns16 0 65535 longword uns32 0 4294967295 subrange types Depends on range minimum range value maximum range value
1Int32 is the implementation of integer in Delphi. Though this may change in later releases.2Uns32 is the implementation of cardinal in Delphi. Though this may change in later releases.In addition to the integer values, Delphi supports several non-integer ordinal types. The following table provides their HLA equivalents:
Like the integer types, Delphi supports a wide range of real numeric formats. The following table presents these types and their HLA equivalents.
Table 3: Real Types in Delphi and HLA Delphi HLA Range Minimum Maximum real real64 5.0 E-324 1.7 E+308 single real32 1.5 E-45 3.4 E+38 double real64 5.0 E-324 1.7 E+308 extended real80 3.6 E-4951 1.1 E+4932 comp real80 -263+1 263-1 currency real80 -922337203685477.5808 922337203685477.5807 real481 byte[6] 2.9 E-39 1.7 E+38
1real48 is an obsolete type that depends upon a software floating point library. You should never use this type in assembly code. If you do, you are responsible for writing the necessary floating point subroutines to manipulate the data.The last scalar type of interest is the pointer type. Both HLA and Delphi use a 32-bit address to represent pointers, so these data types are completely equivalent in both languages.
12.3.7 Passing String Data Between Delphi and HLA Code
Delphi supports a couple of different string formats. The native string format is actually very similar to HLA's string format. A string object is a pointer that points at a zero terminated sequence of characters. In the four bytes preceding the first character of the string, Delphi stores the current dynamic length of the string (just like HLA). In the four bytes before the length, Delphi stores a reference count (unlike HLA, which stores a maximum length value in this location). Delphi uses the reference count to keep track of how many different pointers contain the address of this particular string object. Delphi will automatically free the storage associated with a string object when the reference count drops to zero (this is known as garbage collection).
The Delphi string format is just close enough to HLA's to tempt you to use some HLA string functions in the HLA Standard Library. This will fail for two reasons: (1) many of the HLA Standard Library string functions check the maximum length field, so they will not work properly when they access Delphi's reference count field; (2) HLA Standard Library string functions have a habit of raising string overflow (and other) exceptions if they detect a problem (such as exceeding the maximum string length value). Remember, the HLA exception handling facility is not directly compatible with Delphi's, so you should never call any HLA code that might raise an exception.
Of course, you can always grab the source code to some HLA Standard Library string function and strip out the code that raises exceptions and checks the maximum length field (this is usually the same code that raises exceptions). However, you could still run into problems if you attempt to manipulate some Delphi string. In general, it's okay to read the data from a string parameter that Delphi passes to your assembly code, but you should never change the value of such a string. To understand the problem, consider the following HLA code sequence:
static s:string := "Hello World"; sref:string; scopy:string; . . . str.a_cpy( s, scopy ); // scopy has its own copy of "Hello World" mov( s, eax ); // After this sequence, s and sref point at mov( eax, sref ); // the same character string in memory.After the code sequence above, any change you would make to the scopy string would affect only scopy because it has its own copy of the "Hello World" string. On the other hand, if you make any changes to the characters that s points at, you'll also be changing the string that sref points at because sref contains the same pointer value as s; in other words, s and sref are aliases of the same data. Although this aliasing process can lead to the creation of some killer defects in your code, there is a big advantage to using copy by reference rather than copy by value: copy by reference is much quicker since it only involves copying a single four-byte pointer. If you rarely change a string variable after you assign one string to that variable, copy by reference can be very efficient.
Of course, what happens if you use copy by reference to copy s to sref and then you want to modify the string that sref points at without changing the string that s points at? One way to do this is to make a copy of the string at the time you want to change sref and then modify the copy. This is known as copy on write semantics. In the average program, copy on write tends to produce faster running programs because the typical program tends to assign one string to another without modification more often that it assigns a string value and then modifies it later. Of course, the real problem is "how do you know whether multiple string variables are pointing at the same string in memory?" After all, if only one string variable is pointing at the string data, you don't have to make a copy of the data, you can manipulate the string data directly. The reference counter field that Delphi attaches to the string data solves this problem. Each time a Delphi program assigns one string variable to another, the Delphi code simply copies a pointer and then increments the reference counter. Similarly, if you assign a string address to some Delphi string variable and that variable was previously pointing at some other string data, Delphi decrements the reference counter field of that previous string value. When the reference count hits zero, Delphi automatically deallocates storage for the string (this is the garbage collection operation).
Note that Delphi strings don't need a maximum length field because Delphi dynamically allocates (standard) strings whenever you create a new string. Hence, string overflow doesn't occur and there is no need to check for string overflow (and, therefore, no need for the maximum length field). For literal string constants (which the compiler allocates statically, not dynamically on the heap), Delphi uses a reference count field of -1 so that the compiler will not attempt to deallocate the static object.
It wouldn't be that hard to take the HLA Standard Library strings module and modify it to use Delphi's dynamically allocated string format. There is, however, one problem with this approach: Borland has not published the internal string format for Delphi strings (the information appearing above is the result of sleuthing through memory with a debugger). They have probably withheld this information because they want the ability to change the internal representation of their string data type without breaking existing Delphi programs. So if you poke around in memory and modify Delphi string data (or allocate or deallocate these strings on your own), don't be surprised if your program malfunctions when a later version of Delphi appears (indeed, this information may already be obsolete).
Like HLA strings, a Delphi string is a pointer that happens to contain the address of the first character of a zero terminated string in memory. As long as you don't modify this pointer, you don't modify any of the characters in that string, and you don't attempt to access any bytes before the first character of the string or after the zero terminating byte, you can safely access the string data in your HLA programs. Just remember that you cannot use any Standard Library routines that check the maximum string length or raise any exceptions. If you need the length of a Delphi string that you pass as a parameter to an HLA procedure, it would be wise to use the Delphi Length function to compute the length and pass this value as an additional parameter to your procedure. This will keep your code working should Borland ever decide to change their internal string representation.
Delphi also supports a ShortString data type. This data type provides backwards compatibility with older versions of Borland's Turbo Pascal (Borland Object Pascal) product. ShortString objects are traditional length-prefixed strings (see "Character Strings" on page 419). A short string variable is a sequence of one to 256 bytes where the first byte contains the current dynamic string length (a value in the range 0..255) and the following n bytes hold the actual characters in the string (n being the value found in the first byte of the string data). If you need to manipulate the value of a string variable within an assembly language module, you should pass that parameter as a ShortString variable (assuming, of course, that you don't need to handle strings longer than 256 characters). For efficiency reasons, you should always pass ShortString variables by reference (or CONST or OUT) rather than by value. If you pass a short string by value, Delphi must copy all the characters allocated for that string (even if the current length is shorter) into the procedure's activation record. This can be very slow. If you pass a ShortString by reference, then Delphi will only need to pass a pointer to the string's data; this is very efficient.
Note that ShortString objects do not have a zero terminating byte following the string data. Therefore, your assembly code should use the length prefix byte to determine the end of the string, it should not search for a zero byte in the string.
If you need the maximum length of a ShortString object, you can use the Delphi high function to obtain this information and pass it to your HLA code as another parameter. Note that the high function is an compiler intrinsic much like HLA's @size function. Delphi simply replaces this "function" with the equivalent constant at compile-time; this isn't a true function you can call. This maximum size information is not available at run-time (unless you've used the Delphi high function) and you cannot compute this information within your HLA code.
12.3.8 Passing Record Data Between HLA and Delphi
Records in HLA are (mostly) compatible with Delphi records. Syntactically their declarations are very similar and if you've specified the correct Delphi compiler options you can easily translate a Delphi record to an HLA record. In this section we'll explore how to do this and learn about the incompatibilities that exist between HLA records and Delphi records.
For the most part, translating Delphi records to HLA is a no brainer. The two record declarations are so similar syntactically that conversion is trivial. The only time you really run into a problem in the conversion process is when you encounter case variant records in Delphi; fortunately, these don't occur very often and when they do, HLA's anonymous unions within a record come to the rescue.
Consider the following Pascal record type declaration:
type recType = record day: byte; month:byte; year:integer; dayOfWeek:byte; end;The translation to an HLA record is, for the most part, very straight-forward. Just translate the field types accordingly and use the HLA record syntax (see "Records" on page 483) and you're in business. The translation is the following:
type recType: record day: byte; month: byte; year:int32; dayOfWeek:byte; endrecord;There is one minor problem with this example: data alignment. By default Delphi aligns each field of a record on the size of that object and pads the entire record so its size is an even multiple of the largest (scalar) object in the record. This means that the Delphi declaration above is really equivalent to the following HLA declaration:
type recType: record day: byte; month: byte; padding:byte[2]; // Align year on a four-byte boundary. year:int32; dayOfWeek:byte; morePadding: byte[3]; // Make record an even multiple of four bytes. endrecord;Of course, a better solution is to use HLA's ALIGN directive to automatically align the fields in the record:
type recType: record day: byte; month: byte; align( 4 ); // Align year on a four-byte boundary. year:int32; dayOfWeek:byte; align(4); // Make record an even multiple of four bytes. endrecord;Alignment of the fields is good insofar as access to the fields is faster if they are aligned appropriately. However, aligning records in this fashion does consume extra space (five bytes in the examples above) and that can be expensive if you have a large array of records whose fields need padding for alignment.
The alignment parameters for an HLA record should be the following:
Another possibility is to tell Delphi not to align the fields in the record. There are two ways to do this: use the packed reserved word or use the {$A-} compiler directive.
The packed keyword tells Delphi not to add padding to a specific record. For example, you could declare the original Delphi record as follows:
type recType = packed record day: byte; month:byte; year:integer; dayOfWeek:byte; end;With the packed reserved word present, Delphi does not add any padding to the fields in the record. The corresponding HLA code would be the original record declaration above, e.g.,
type recType: record day: byte; month: byte; year:int32; dayOfWeek:byte; endrecord;The nice thing about the packed keyword is that it lets you explicitly state whether you want data alignment/padding in a record. On the other hand, if you've got a lot of records and you don't want field alignment on any of them, you'll probably want to use the "{$A-}" (turn data alignment off) option rather than add the packed reserved word to each record definition. Note that you can turn data alignment back on with the "{$A+"} directive if you want a sequence of records to be packed and the rest of them to be aligned.
While it's far easier (and syntactically safer) to used packed records when passing record data between assembly language and Delphi, you will have to determine on a case-by-case basis whether you're willing to give up the performance gain in exchange for using less memory (and a simpler interface). It is certainly the case that packed records are easier to maintain in HLA than aligned records (since you don't have to carefully place ALIGN directives throughout the record in the HLA code). Furthermore, on new x86 processors most mis-aligned data accesses aren't particularly expensive (the cache takes care of this). However, if performance really matters you will have to measure the performance of your program and determine the cost of using packed records.
Case variant records in Delphi let you add mutually exclusive fields to a record with an optional tag field. Here are two examples:
type r1= record stdField: integer; case choice:boolean of true:( i:integer ); false:( r:real ); end; r2= record s2:real; case boolean of // Notice no tag object here. true:( s:string ); false:( c:char ); end;HLA does not support the case variant syntax, but it does support anonymous unions in a record that let you achieve the same semantics. The two examples above, converted to HLA (assuming "{A-}") are
type r1: record stdField: int32; choice: boolean; // Notice that the tag field is just another field union i:int32; r:real64; endunion; endrecord; r2: record s2:real64; union s: string; c: char; endunion; endrecord;Again, you should insert appropriate ALIGN directives if you're not creating a packed record. Note that you shouldn't place any ALIGN directives inside the anonymous union section; instead, place a single ALIGN directive before the UNION reserved word that specifies the size of the largest (scalar) object in the union as given by the table "Alignment of Record Fields" on page 1179.
In general, if the size of a record exceeds about 16-32 bytes, you should pass the record by reference rather than by value.
12.3.9 Passing Set Data Between Delphi and HLA
Sets in Delphi can have between 1 and 256 elements. Delphi implements sets using an array of bits, exactly as HLA implements character sets (see "Character Sets" on page 441). Delphi reserves one to 32 bytes for each set; the size of the set (in bytes) is (Number_of_elements + 7) div 8. Like HLA's character sets, Delphi uses a set bit to indicate that a particular object is a member of the set and a zero bit indicates absence from the set. You can use the bit test (and set/complement/reset) instructions and all the other bit manipulation operations to manipulate character sets. Furthermore, the MMX instructions might provide a little added performance boost to your set operations (see "The MMX Instruction Set" on page 1113). For more details on the possibilities, consult the Delphi documentation and the chapters on character sets and the MMX instructions in this text.
Generally, sets are sufficiently short (maximum of 32 bytes) that passing the by value isn't totally horrible. However, you will get slightly better performance if you pass larger sets by reference. Note that HLA often passes character sets by value (16 bytes per set) to various Standard Library routines, so don't be totally afraid of passing sets by value.
12.3.10 Passing Array Data Between HLA and Delphi
Passing array data between some procedures written in Delphi and HLA is little different than passing array data between two HLA procedures. Generally, if the arrays are large, you'll want to pass the arrays by reference rather than value. Other than that, you should declare an appropriate array type in HLA to match the type you're passing in from Delphi and have at it. The following code fragments provide a simple example:
type PascalArray = array[0..127, 0..3] of integer; procedure PassedArrray( var ary: PascalArray ); external;type PascalArray: int32[ 128, 4]; procedure PassedArray( var ary: PascalArray ); external;As the above examples demonstrate, Delphi's array declarations specify the starting and ending indicies while HLA's array bounds specify the number of elements for each dimension. Other than this difference, however, you can see that the two declarations are very similar.
Delphi uses row-major ordering for arrays. So if you're accessing elements of a Delphi multi-dimensional array in HLA code, be sure to use the row-major order computation (see "Row Major Ordering" on page 469).
12.3.11 Delphi Limitations When Linking with (Non-TASM) Assembly Code
Delphi places a couple of restrictions on OBJ files that it links with the Pascal code. Some of these restrictions appears to be defects in the implementation of the linker, but only Borland can say for sure if these are defects or they are design deficiencies. The bottom line is that Delphi seems to work okay with the OBJ files that TASM produces, but fails miserably with OBJ files that other assemblers (including MASM) produce. While there are workarounds for those who insist on using the other assemblers, the only reasonable solution is to use the TASM assembler when assembling HLA output.
Note that TASM v5.0 does not support Pentium+ instructions. Further, the latest (and probably last) version of TASM (v5.3) does not support many of the newer SSE instructions. Therefore, you should avoid using these instructions in your HLA programs when linking with Delphi code.
12.3.12 Referencing Delphi Objects from HLA Code
Symbols you declare in the INTERFACE section of a Delphi program are public. Therefore, you can access these objects from HLA code if you declare those objects as external in the HLA program. The following sample program demonstrates this fact by declaring a structured constant (y) and a function (callme) that the HLA code uses when you press the button on a form. The HLA code calls the callme function (which returns the value 10) and then the HLA code stores the function return result into the y structured constant (which is really just a static variable).
1Note that the HLA Standard Library source code is available; feel free to modify the routines you want to use and remove any exception handling statements contained therein.
2Scalar data types are the ordinal, pointer, and real types. It does not include strings or other composite data types.
3Delphi string objects are an exception. For reasons that have nothing to do with data representation, you should not manipulate string parameters passed in from Delphi to an HLA routine. This section will explain the problems more fully a little later.
|