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.
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.
; ------------------ 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
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.
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.
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.
Open the project property page (the Property Pages dialog box).
Select the C/C++ folder.
Select the Advanced page.
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 .
;---------------- 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
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.
;------------------ 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
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.
;----------------- 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
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).
;------------- 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
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.
// 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; }
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.
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.
;------------ 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
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 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.
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.
.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
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.
void CCdecl_FastCall_MFCDlg::OnBnClickedButton1() { // TODO: Add your control notification handler code here UpdateData(TRUE); ires = AddSubFc(i1, i2, i3, i4); UpdateData(FALSE); }
After you successfully build the project and start the application, its window will appear as shown in Fig. 5.4.
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.
;--------------- 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
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.
// 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; }
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.
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.