General Principles of Developing Interfaces to High-level Languages

In this section, we will discuss general issues of developing interfaces when calling assembly procedures from C++ .NET programs. To illustrate the material, we will use numerous original examples that cannot be found elsewhere. When necessary, detailed comments are included.

The assembly modules were developed with Microsoft MASM 6.14 compiler. When developing the modules, we used a simplified syntax of assembly language. This means we used the .data and .code directives everywhere in the source code. We will not describe all features of the MASM compiler here, since only a few of them are used, and all explanations are given in the text.

Compiled assembly modules have the OBJ extension, and the command line for the MASM compiler appears as follows :

 ml /c /Fo <file_name.obj> <file_name.asm> 

The obtained OBJ file should be linked to the C++ main program. When developing the assembly module interface to the main program, you should take into account the following points:

  • The rules of name treatment for variables and functions in object files. The high level language compiler may or may not change the original names in the object module, and you should be aware of whether it does this and how.

  • The memory model used by the assembly module ( tiny , small , compact , medium , huge , large , or flat ).

  • The parameters of the call to the assembly function. The parameters of a call is a broad notion that includes the following important aspects:

    • Whether registers should be saved in the function; if yes, which ones

    • Order of passing the parameters to the function

    • Method of passing the parameters to the function (whether registers, the stack, or the shared memory is used)

    • Whether the parameters are passed to the function by value or by reference

    • If the parameters are passed via the stack, where the stack pointer is restored: in the calling or the called program of function

    • Method of returning the result to the calling program (via the stack, registers, or shared memory)

Now, we will look at the principles of developing interfaces more closely, beginning with the identifier name agreement. The C++ compiler does not change the case of letters , so the identifiers remain case-sensitive. However, the C++ compiler adds an underline character as a prefix to every external name.

You should also take into account the memory models used by external functions. For 32-bit applications, only one memory model, flat , is used. Regarding the parameters, things are quite simple, although their descriptions in some books are complicated and confusing. For those readers who want to understand these directives and conventions, we will highlight the main points of calling external functions from a C++ program:

  • In 32-bit applications, parameters are passed to a called function in either of two manners: by value or by reference. When a parameter is passed by value, the function directly gets the 32-bit operand; when the parameter is passed by reference, the function gets the address (also 32-bit) of this operand.

  • Parameters are passed via the stack, registers, or shared memory. Passing parameters via the shared memory in 32-bit applications is very difficult to implement and is normally used in system programming and developing device drivers. It is an isolated topic that will be addressed in greater detail in Chapter 7 . All variants of passing parameters to a function via the stack or registers are shown in Table 5.1.

Table 5.1: Passing parameters

Directive

Order of parameters

Stack reset by

Passing parameters via registers

_fastcall

From left to right

Procedure

ECX , EDX , the stack; from right to left

_cdecl

From right to left

Caller

No

_stdcall

From right to left

Procedure

No

Please note the following details related to Table 5.1. The order of passing parameters for each directive tells the compiler how parameters are passed to a called function. For the _cdecl and _stdcall directives, the parameters are passed via the stack. When the _fastcall directive is used, the parameters are passed via the registers (the first two parameters) and the stack (the others).

Before you return to the main program, you should restore the stack pointer. This is true for all the directives. There are no strict recommendations concerning the use of particular methods of calling external functions.. If you are using Windows API, _stdcall is the standard way of calling them. It is best to use the _cdecl directive for calls to procedures and functions from C++ programs.

The quickest way of passing parameters in Visual C++ .NET is _fastcall . The stack is not used for passing the first two parameters, so you can obtain a gain in performance.

All functions return the result either in the EAX register or on the top of the mathematical coprocessor stack ST (0) .

We will illustrate this with a simple assembly function that adds together two numbers . A C++ program passes to the function two integer parameters and takes their sum as the result. We will name the function AddInts , its first parameter X1 , and its second parameter X2 . In this case, a call to the function will appear as AddInts (X1, X2) .

The source code of the function when _stdcall is used for passing parameters is shown in Listing 5.1.

Listing 5.1: A function that adds together two numbers. The _stdcall convention is used
image from book
 ; ------------------ addints. asm------------- .686  .model flat    public _AddInts@8  .data  .code  _AddInts@8 proc    push    EBP    mov     EBP, ESP    mov     EAX, DWORD PTR [EBP+8]    add     EAX, DWORD PTR [EBP+i2]    pop     EBP    ret     8  _AddInts@8 endp  end 
image from book
 

For a correct call to the function, an underline character should be put at the beginning of its name and a @n suffix should be added at its end. Here, n is the number of bytes required for passing parameters. In this example, n is equal to eight. Such a form of a function s name is required by the C++ .NET compiler.

The function receives its parameters in the stack and returns the result in the EAX register.

Since the AddInts function should be available to external program modules, it should be declared with the public directive. The first two lines of the body of the procedure are:

 push   EBP  mov    EBP, ESP 

They are necessary for accessing the parameters in the stack with the EBP register. The parameters are in the stack at the addresses [EBP+8] and [EBP+i2] . Which of them is the first, and which is the second? To answer this question, you should specify the order of the parameters in the calling program.

The _stdcall directive (see Table 5.1) indicates that the X1 and X2 parameters of the AddInts procedure are passed via the stack from right to left. That is, X2 is pushed on the stack first, and then X1 . Since the stack grows from larger addresses to lesser, X2 will have a larger address than X1 .

After the AddInts procedure is called and the EBP register is saved, the X1 and X2 parameters will be located on the stack as shown in Fig. 5.1.

image from book
Fig. 5.1: The location of the parameters in the stack

To find the sum of two integers, the following commands are used:

 mov   EAX, DWORD PTR [EBP+8]  add   EAX, DWORD PTR [EBP+12] 

After these commands are executed, the sum is in the EAX register. When returning control to the calling program, the AddInts function restores the stack in accordance with the _stdcall directive. Before the ret command is executed, there are two double words, i.e., eight bytes, on the stack. To clear the stack, you should specify the 8 parameter in the ret 8 command. You can use the following sequence of commands:

 add   ESP, 8  ret 

Save the source code of the function in the AddInts . asm file and compile it:

 ml /c AddInts.asm 

The /c option tells the compiler that the source module should be only compiled. If there are no errors, you will obtain the AddInts . obj object module file that will be used in the main program.

A C++ .NET program that calls the AddInts function should declare it in the variables and functions declaration section:

 extern "C" int_stdcall AddInts(int i1, int i2); 

It is important to concentrate on the directives and specifiers in the declaration of the AddInts function, because understanding them is necessary in order to correctly develop the interfaces to any external functions.

The extern directive indicates that the function is external to the module that uses it.

The "C" specifier prohibits the C++ compiler to decorate (modify) the name of the external identifier. The decorated name of a function used in C++ holds the following information:

  • Function name

  • Class whose member the function is

  • Namespace (the scope) of the function

  • Types of the function s parameters

  • Calling convention

  • Return type

Decorated names are meaningful only to the C++ .NET compiler and linker. Examples of original and decorated names are given in Table 5.2.

Table 5.2: Examples of original and decorated names

Original name

Decorated name

  int a (char)   {   int i = 3;   return i;   };   void _stdcallb: :c (float) {};  
 ?a@@YAHD@Z   ?c@b@@AAGXM@Z   

In most cases, you do not need to know the decorated name of a function. It can be necessary, for example, when you call C++ functions from assembly programs.

The function declaration uses the _stdcall calling convention. In general, a calling convention in the C++ .NET development environment can be set on the project properties page. The compiler can work with the following options:

  • /Gd is set by default; it defines the _cdecl convention for all functions except member functions and functions explicitly declared as _stdcall or _fastcall .

  • /Gr defines the _fastcall convention for all functions except member functions and functions explicitly declared as _cdecl or _stdcall . All _fastcall functions should have prototypes .

  • /Gz defines the _stdcall convention for all functions except those with a variable number of arguments and functions explicitly declared as _cdecl or __fastcall . All _stdcall functions should have prototypes.

To set these options in the Visual C++ .NET compiler, proceed as follows.

  1. Open the project property page (the Property Pages dialog box).

  2. Select the C/C++ folder.

  3. Select the Advanced page.

  4. Change the Calling Convention property.

Now, we will return to our program. Before compiling a C++ program, you should add to the project the object module file containing your function. The simplest way is to copy this to the project work directory.

A fragment of a C++ program that uses the AddInts external function could look like this:

 ... int Int1 = 74;  int Int2=   56;  int ires;   ...  ires = AddInts(Int1, Int2);   ... 

One more note: The Visual C++ linker works with object module files in the COFF (Common Object File Format) format. When compiling a source module with MASM, you can obtain object file either in the COFF format or in the OMF (Object Module Format) format. This is why, when linking a project, the C++ .NET linker can display the following warning:

 Warning: Converting object format from OMF to COFF. 

Generally speaking, this does not matter, because the C++ compiler converts the OMF format to COFF in any case. You can specify the /coff option in the MASM compiler to obtain a COFF file:

 ml /c /coff AddInts.asm 

Now, we will discuss how this interface will work when the _cdecl calling convention is used (Listing 5.2). This method of passing parameters differs from _stdcall in that the calling program must restore the stack on its own. Parameters are passed from right to left, like in _stdcall .

Listing 5.2: An assembly function with the _cdecl calling convention
image from book
 ;---------------- addints. asm------------ .686 .model flat    public _AddInts  .data  .code  _AddInts proc    push    EBP    mov     EBP, ESP    mov     EAX, DWORD PTR [EBP+8]    add     EAX, DWORD PTR [EBP+i2]    pop     EBP    ret  _AddInts endp  end 
image from book
 

As you can see from the source code, you should add an underscore character at the beginning of the function to work with the _cdecl directive. The ret command that exits the function is used here without parameters.

With regard to the source code of the Visual C++ program, the changes are minimal and should be done in the declaration section, in which you should replace the _stdcall directive with _cdecl :

 extern "C" int _cdecl AddInts(int i1, int i2); 

Finally, we will consider a widely used register method of passing parameters to a called function. It is specified with the _fastcall directive. The arguments are passed via the ECX and EDX registers from left to right. If there are three or more arguments, the third and the others are passed via the stack. We will consider a function (named AddSubFc ) that computes I1 ˆ’ I2 ˆ’ I3 + I4 , where I1 , I2 , I3 , and I4 are integers. The source code of this function is shown in Listing 5.3.

Listing 5.3: A function with the _fastcall calling convention
image from book
 ;------------------ addsubf c. asm ---------- .686  .model flat    public @AddSubFc@16  .code  @AddSubFc@16 proc    push    EBP    mov     EBP, ESP    sub     ECX, EDX    sub     ECX, DWORD PTR [EBP+8]    add     ECX, DWORD PTR [EBP+i2]    mov     EAX, ECX    pop     EBP    ret     8  @AddSubFc@16 endp  end 
image from book
 

When the number of parameters is not greater than two, the _fastcall method makes it possible to significantly speed up the application as a whole because stack initialization and restoration are not required, unlike the other methods of passing parameters. However, you should not abuse this method, because intensive use of processor registers in functions will hamper optimization done by the compiler. As you know, many high-level language compilers use the processor registers for program optimization.

Note the @16 suffix in the function s name. It specifies the total number of bytes taken by the parameters (two double words in the ECX and EDX registers and two double words in the stack).

In the main Visual C++ program, the _fastcall should be specified to call this procedure:

 extern "C" int _fastcall AddInts(int i1, int i2, int I3, int i4); 

In practice, you often deal with more than one assembly modules, so now we will discuss a more complex variant of the interface between a C++ .NET program and assembly procedures. Suppose the main program must compute the sum of two integers and decrease it by 20. Use two separately compiled assembly functions. Let the first one (name it AddTwo ) add together two integers, Int1 and Int2 , taken as parameters. The second function (named Sub20 ) will subtract 20 from the result obtained by AddTwo . Let the final result be returned to the main program by the AddTwo function.

Save the source code of the AddTwo function in the AddTwo . asm file, and the source code of the Sub20 function in the Sub20 . asm file.

Also, let the AddTwo function process the parameters in accordance with the _stdcall directive, and the Sub20 function process its parameters in accordance with the _cdecl directive. The source code of the AddTwo function is shown in Listing 5.4.

Listing 5.4: The AddTwo function (a modified variant)
image from book
 ;----------------- AddTwo. asm --------------- .686    .model flat    public _AddTwo@8    extern _Sub20:proc  .code   _AddTwo@8 proc     push EBP     mov  EBP, ESP     mov  EAX, DWORD PTR [EBP+8]     add  EAX, DWORD PTR [EBP+i2]     push EAX     call _Sub20     add  ESP, 4     pop  EBP     ret  8    _AddTwo@8 endp    end 
image from book
 

Now, we will discuss the difference between the modified variant of the AddTwo function and the original. First, there is the line:

 extern _Sub20:proc 

in the directives section. This line makes the MASM compiler treat the Sub20 function as external to the module that contains AddTwo . Also, when the application is built, the Visual C++ .NET compiler assumes that the parameters are passed to the Sub20 function in accordance with the _cdecl directive (because the function s identifier begins with an underscore character).

Second, there are the commands:

 push    EAX  call    _Sub20  add     ESP, 4 

At the moment of execution of these lines, the EAX register contains the sum of two integers. This value is a parameter of the Sub20 function. It is pushed on the stack with the push EAX command. After the Sub20 function completes, the result is stored in the EAX register. Since the main program should restore the stack when a _cdecl call is made, the presence of the add ESP , 4 command is explicable.

Consider the source code of the Sub20 function (Listing 5.5).

Listing 5.5: The Sub20 assembly function
image from book
 ;------------- Sub20.asm ------------- .686  .model flat    public _Sub20  .code   _Sub20 proc     push EBP     mov  EBP, ESP     mov  EAX, DWORD PTR [EBP+8]     sub  EAX, 20     pop  EBP     ret    _Sub20 endp    end 
image from book
 

The Sub20 function takes the sum of two integers as a parameter. It returns the result in the EAX register and does not restore the stack before returning, because this must be done by a caller, the AddTwo function in this case. The directive

 public _Sub20 

makes the Sub20 function available to other modules.

After you compile the AddTwo . asm and Sub20 . asm files, you should add the obtained object modules AddTwo . obj and Sub20 . obj to your Visual C++ .NET project. Use the Visual Studio .NET 2003 Application Wizard to develop a Windows console application that will use the AddTwo and Sub20 functions. The source code of the main project file is shown in Listing 5.6.

Listing 5.6: A project file that uses the AddTwo and Sub20 functions
image from book
 // This is the main project file for VC++ application project  // generated using an Application Wizard.  #include "stdafx.h"  extern "C" int _stdcall AddTwo(int i1, int i2);  extern "C" int _cdecl Sub20(int i3);  #using <mscorlib.dll>  using namespace System;  int _tmain ()  {  // TODO: Please replace the sample code below with your own.         String  *SInt1, *SInt2, *SIres;         int Int1, Int2, ires;         Console::Write("Enter Int1: ");         SInt1 = Console::ReadLine();         Console::Write("Enter Int2: ");         SInt2 = Console::ReadLine ();         Int1 = Convert::ToInt32(SInt1);         Int2 = Convert::ToInt32(SInt2);         ires = AddTwo(Int1, Int2);         SIres = Convert::ToString(ires);         Console::Write("Result:[Int1 + Int2  20] = ");         Console::WriteLine(ires);         Console::WriteLine("Press any key to exit   ");         Console::ReadLine ();         return 0;  } 
image from book
 

As you can see from this source code, the declaration section should contain the following lines:

 extern "C" int _stdcall AddTwo(int i1, int i2);  extern "C" int _cdecl Sub20(int i3); 

The rest of the code is simple and does not require further explanation. The window of the application is shown in Fig. 5.2.

image from book
Fig. 5.2: Window of an application that demonstrates the use of two stand-alone assembly procedures from two object files

Linking and debugging this application can be made simpler by using one file with the source codes of the functions rather than two. Save the source codes of the AddTwo and Sub20 assembly functions in the AddSub . asm file. Change the source code of the Sub20 function a little by replacing the sub EAX , 20 command with sub EAX , 100 . Name the modified function Sub100 . The source code of the functions in the AddSub . asm file is shown in Listing 5.7.

Listing 5.7: The AddTwo and Sub100 functions
image from book
 ;------------ AddSub.asm ------- .model flat    public _AddTwo@8     ;_stdcall convention    public _Sub100       ;_cdecl convention  .code  _AddTwo@8 proc     push EBP     mov  EBP, ESP     mov  EAX, DWORD PTR [EBP+8]     add  EAX, DWORD PTR [EBP+i2]     push EAX     call _Sub100     add  ESP, 4     pop  EBP     ret  8   _AddTwo@8 endp  _Sub100 proc    push EBP    mov  EBP, ESP    mov  EAX, DWORD PTR [EBP+8]    sub  EAX, 100    pop  EBP    ret  _Sub100 endp  end 
image from book
 

As you can see from the listing, the line

 extern _Sub20:proc 

disappeared from the declaration section because both procedures are in the same module now. If the MASM compile does not detect any errors, you will obtain the image from book  AddSub.obj object module file. In the C++ project source file, change the line:

 extern "C" int _cdecl Sub20(int i3); 

to

 extern "C" int _cdecl Sub100(int i3); 

Also, delete the files AddTwo . obj and Sub20 . obj from the project and add the AddSub . obj file to the project. The window of the application is shown in Fig. 5.3.

image from book
Fig. 5.3: Window of an application that demonstrates the use of two stand-alone assembly procedures from one object file

From these examples, you can see that every time you call functions written according to the _stdcall or _cdecl convention, you have to save and restore the stack. With intensive computation when such functions are called many times, these operations may decrease the performance of your application. If you use the _fastcall convention when developing your functions, this will allow you to increase the performance of your application. You should use this method with care because the C++ .NET compiler uses the registers intensively.

The next example computes the formula Int1 ˆ’ Int2 ˆ’ Int3+Int4 ˆ’ 100 , where Int1 , Int2 , Int3 , and Int4 are integers, and displays the result. Develop a dialog-based C++ .NET application. Put five edit controls, a button, and five static text controls on the main form. Associate the edit controls with the integer variables i1 to i4 that correspond to the numbers from Int1 to Int4 and with the ires variable that will hold the result. The result is displayed after clicking the button.

The computation is done with two assembly functions, AddSubFc and Sub100 , which use the _fastcall and _cdecl calling conventions, respectively. Their source code is shown in Listing 5.8.

Listing 5.8: Functions that use the _fastcall and _cdecl calling conventions
image from book
 .686  .model flat    public @AddSubFc@16, _Sub100  .code  @AddSubFc@16 proc    push    EBP    mov     EBP, ESP    sub     ECX, EDX                ; ECX > Int1Int2    sub     ECX, DWORD PTR [EBP+8]  ; ECX > ECXInt3    add     ECX, DWORD PTR [EBP+12] ; ECX > ECX+Int4    mov     EAX, ECX    push    EAX    call    _Sub100    add     ESP, 4    pop     EBP    ret     8  @AddSubFc@16 endp  _Sub100 proc    push    EBP    mov     EBP, ESP    mov     EAX, DWORD PTR [EBP+8]    sub     EAX, 100    pop     EBP    ret  _Sub100 endp  end 
image from book
 

The first parameter of the AddSubFc function (i.e., Int1 ) is passed via the ECX register. The second parameter, Int2 , is in the EDX register. The third parameter, Int3 , is on the stack at the [EBP+8] address, and the fourth is at the [EBP+12] address. Before the call to the Sub100 function, the intermediate result is moved from the ECX register to EAX with the mov EAX , ECX command. The sequence of commands:

 push    EAX  call    _Sub100  add     ESP, 4 

is standard for a procedure that conforms to the _cdecl convention and takes one parameter. Save the source code of the function in the AddSubFc . asm file and compile it. If there are no errors, you will obtain the AddSubFc . obj file. Add it to your C++ .NET project.

Add the following lines to the declaration section in the C++ project source file:

 extern "C" int _fastcall AddSubFc(int i1, int i2, int i3, int i4);  extern "C" int _cdecl Sub100(int i3); 

The OnBnClicked handler in the C++ calling program is shown in Listing 5.9.

Listing 5.9: The onBnclicked handler
image from book
 void CCdecl_FastCall_MFCDlg::OnBnClickedButton1()  {  // TODO: Add your control notification handler code here    UpdateData(TRUE);    ires = AddSubFc(i1, i2, i3, i4);    UpdateData(FALSE);  } 
image from book
 

After you successfully build the project and start the application, its window will appear as shown in Fig. 5.4.

image from book
Fig. 5.4: Window of an application that demonstrates the use of functions with _cdecl and _fastcall convention

The final example in this chapter demonstrates how the result of a mathematical operation can be returned via the stack of the mathematical coprocessor. Suppose a function (named nfp ) takes a floating-point number as a parameter and inverts it. The result is returned on the top of the stack ST (0) . The source code of the nfp function is shown in Listing 5.10.

Listing 5.10: The nfp function that returns the result on the coprocessor s stack
image from book
 ;--------------- nfp.asm ------------ .686 .model flat    public _nfp@4  .code   _nfp@4 proc     push  EBP     mov   EBP, ESP     finit     fld   DWORD PTR [EBP+8]     fchs     fwait     pop   EBP     ret   4   _nfp@4 endp  end 
image from book
 

The parameter, which is a floating-point number, is pushed on the top of the coprocessor stack with the fld DWORD PTR [EBP+8] command. The sign is changed with the fchs command, and the result remains on the top of the coprocessor stack.

A C++ .NET console application uses the result of the nfp function to display it. The source code of this application is shown in Listing 5.11.

Listing 5.11: An application that uses the nfp function
image from book
 // This is the main project file for VC ++ application project  // generated using an Application Wizard.  #include "stdafx.h"  extern "C" float _stdcall nfp (float f1);  #using <mscorlib.dll>  using namespace System;  int _tmain ()  {  // TODO: Please replace the sample code below with your own.      String *s1;      float f1;      Console::Write("Enter float value:");      s1 = Console::ReadLine();      f1 = Convert::ToSingle(s1);      f1 = nfp(f1);      s1 = Convert::ToString(f1);      Console::Write("Changed value:");      Console::WriteLine (s1);      Console::Write("Press any key to exit   ");      Console::ReadLine();      return 0;  } 
image from book
 

The called function is declares as follows:

 extern "C" float _stdcall nfp(float f1); 

Its result is stored in the f1 variable:

 f1 = nfp(f1); 

In this case, the number on the top of the coprocessor stack is copied to the f1 variable without using the EAX register. The window of the application is shown in Fig. 5.5.

image from book
Fig. 5.5: Window of an application that displays an inverted floating-point number

This chapter concentrated on the interfaces of external assembly modules to C++ .NET programs and did not focus on various types of parameters. This will be addressed in the next chapter, in which the method of passing parameters and checking the returned results will be comprehensively discussed for various data types.



Visual C++ Optimization with Assembly Code
Visual C++ Optimization with Assembly Code
ISBN: 193176932X
EAN: 2147483647
Year: 2003
Pages: 50
Authors: Yury Magda

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net