Download CD Content
This chapter looks at issues of building and running C++ .NET programs, including assembly modules. There are many ways to link C++ and assembly programs; here, I will try to emphasize the key aspects of this process.
A C++ application can include one or more assembly modules. Modules are either files with the OBJ extension or standard libraries (with the LIB extension). These modules can contain calls to functions in other modules.
Moreover, assembly functions can in turn call C++ .NET library functions. In fact, programs normally use separately compiled object files or libraries, so building a finished application requires some effort from a developer. What are the advantages of using object files generated with separate compilers? Ready-made object files usually require a minimum of memory and other system resources, which is very useful when creating fast applications.
I will describe how to link assembly modules with a C++ .NET program, assuming the use of a stand-alone assembly compiler when creating such modules. Compilers you can use include either ML.EXE of the MASM 6.14 software package, or the ML compiler of the C++ .NET development environment, located in the \bin subdirectory of the C++ .NET working directory. Files with the ASM extension can be compiled either from a command line or at one of the stages of building an application’s executable file. There are no significant differences between these compiling techniques, but you should set the assembly compiler to use the chosen technique in the development environment for the sake of debugging and tracing convenience. In either case, you’ll obtain an object file that can be used.
First, a few words about the formats of the obtained object modules. The linker (LINK) operates with OBJ files of either COFF or OMF format. The Microsoft Visual C++ .NET compiler generates a COFF object file.
The linker converts OMF to COFF automatically. There are certain restrictions that sometimes hamper OMF-to- COFF conversion. More precisely, there are a few differences between the structures of files of these types. To avoid running into problems, you should set COFF-format object files as input files for the LINK linker. To obtain a COFF file, use the following command line:
ML /c /coff <an ASM file>
To use a file with useful functions in a C++ .NET application, use the following procedure:
1. Compile the source ASM file to obtain an object module in an OBJ file (the COFF format should be preferred).
2. Add the obtained OBJ files to the project and generate an application with the linker.
Step 2 can consist of several stages and can be implemented in various ways, because object files can be added to an application using various methods. Here are a few variants of integration:
● You can add object files to your project by specifying the appropriate functions in the declarations section.
● You can combine object files into static libraries of object modules. To do this, use the LIB.EXE utility of the
Visual Studio C++ .NET package. The resulting LIB file can be added to your project.
● You can use dynamic link libraries (DLLs) that contain object files.
Of course, you can use a combination of these methods.
Now we will turn our attention to the ML.EXE compiler’s options used for generating object files. For example, to obtain an object module from the myproc.asm file, execute the following command:
ML /c /coff myproc.asm
file:///D|/2/0031.html (1 von 30) [25.01.2008 00:11:23]
It is assumed that the source file is located in the same directory as ML.EXE.
The /c option tells the compiler to create only an object file. By default, if the compiler does not detect any errors, it will create the myproc.obj file in the COFF format. The obtained object module should be added to the C++ .NET main program and used for calling the functions it contains.
The object module file can contain several functions that can share data and call other functions of this module.
With relatively small numbers of functions, variables, and their interconnections, the variant with one or more object modules is the most acceptable.
Frequently, the most convenient method of arranging and using functions of external modules involves creating a library (standard or import). In this case, the C++ .NET compiler offers many more options for application
optimization. It is also very convenient to store the code of several object modules as one library. Library module files have the LIB extension. To create a library from an object file such as myproc.obj, use the LIB.EXE utility of the C++ .NET package:
LIB /OUTrmyproc.lib myproc.obj
If there are no errors, the myproc.lib file will be created. You should add it to the application project.
The variant of using assembly functions from dynamic link libraries is very popular. This variant is very flexible, and it allows you to create multiple copies of a DLL easily and to use various methods of linking DLLs to your
application.
We will discuss linking an application and assembly modules in more detail beginning with object modules. An assembly module is compiled with the MASM 6.14 macro assembler or the assembler of the Visual C++ .NET environment. Every assembly program in our examples begins with the following directives:
.686
.model flat, C
The .686 directive allows the assembler to compile all commands of Pentium Pro or higher.
The .model flat, c directive defines the memory model used by the application, and the call convention (in this case, _cdec1).
For example, suppose you want to find the difference of two floating-point numbers and display the result. The difference will be computed with an assembly function (we will name it subf2). Put the source code of this function in the sub2.asm file.
Assume the subf2 function is called using the _cdecl convention. The source code of the function is simple (Listing 7.1).
Listing 7.1: The source code of the subf2 function
;--- subf2.asm --- .686
.model flat, C .code
subf2 proc push EBP mov EBP, ESP ;
finit
fld DWORD PTR [EBP+8] ; load f1 to ST(0) fsub DWORD PTR [EBP+12] ; subtract f2 from f1 fwait
pop EBP
file:///D|/2/0031.html (2 von 30) [25.01.2008 00:11:23]
ret
subf2 endp end
Now, we will focus on the source code briefly. The first two directives are described earlier. The function prolog is implemented with the following assembly commands:
push EBP mov EBP, ESP
They initialize the EBP register to the stack address to gain access to the variables. Computation is done with the mathematical coprocessor’s commands, and the result is returned at the top of the coprocessor stack ST (0):
fld DWORD PTR [EBP+8]
fsub DWORD PTR [EBP+12]
The last two commands reset the stack and exit the function. Note that the ret command has no parameters according to the _cdecl convention.
Now you should obtain an object module of the main C++ .NET program and add it to the project. The object module can be generated with either of two methods: with the stand-alone compiler of the MASM 6.14 package or with the assembler of the Visual C++ .NET environment.
Here is a description of each of these two variants.
As noted earlier, when you use the MASM 6.14 stand-alone compiler, you can obtain an object module with the following command line:
ML.EXE /c /coff sub2.asm
Develop a C++ .NET console application that calls the subf2 function from the sub2.obj file. The source code of this application is shown in Listing 7.2.
Listing 7.2: A C++ application that uses a separate object module
// USING_STAND-ALONE_ASM_COMPILER.cpp : Defines the entry point for the // console application
#include "stdafx.h"
extern "C" float subf2(float f1, float f2 );
int _tmain(int argc, _TCHAR* argv[]) {
float f1, f2;
printf("CUSTOM BUILD WITH ASM-FILE IN PROJECT DEMO\n\n");
printf("Enter float f1: ");
scanf("%f", &f1);
printf("Enter float f2: ");
scanf("%f", &f2);
float fsub = subf2(f1, f2);
printf("f1 − f2 = %.2f\n", fsub);
getchar();
return 0;
}
Here is a brief analysis of the listing. Two floating-point numbers, f1 and f2, are entered in the window of the console application. Their difference is found with the subf2 function. The function is external for the executable
file:///D|/2/0031.html (3 von 30) [25.01.2008 00:11:23]
module, so it is declared with the extern directive:
extern "C" float subf2(float f1, float f2)
By default, the _cdecl call convention is used. The subf2 function takes two floating-point variables f1 and f2 and returns their difference. The “C” specifier prohibits decorating the function name.
The statement
float fsub = subf2(f1, f2)
computes the difference between the variables f1 and f2 and writes it to the fsub variable.
The object file that contains the subf2 function should be added to the project. To do this, select the Add Existing Item…option in the Project menu and then select the name of the module in the dialog box that will open (Fig. 7.1).
Fig. 7.1: Adding an object module to the project
After the object module is added to the project, the sub2.obj file name will appear in the file list in the
Solution Explorer. Note that adding a separate module to a project does not necessarily mean that all functions of this module become visible to the application related to this project. It is important to specify the functions of the added modules with the extern directive.
As usual, adding a module to a project is done with the Project menu item. Select the Add Existing Item…option.
In the dialog box that will open, select the object module file.
After the object module file is added to the project, the file name will appear in the project file list (Fig. 7.2).
file:///D|/2/0031.html (4 von 30) [25.01.2008 00:11:23]
Fig. 7.2: Project window after the object file is added
Note that the sub2.obj object file is added to the project as a resource file.
Save the project and rebuild it with the Rebuild option of the Build menu item.
After you start the application and enter two floating-point numbers, the window of the program will look similar to that shown in Fig. 7.3.
Fig. 7.3: Window of an application that computes the difference between two numbers with a separately compiled object module
Linking an assembly function and a calling program can be done with the MASM compiler of the Visual C++ .NET development environment. Note that the C++ .NET macro assembler has features similar to those of the MASM 6.14 stand-alone compiler and supports the same directives and commands.
You will not need any other tools besides those available in C++ .NET 2003. One advantage of this method is that it is very easy to edit and debug an assembly module with the Visual C++ .NET 2003 environment interface.
At first, you might think the process is too complicated because the examples discussed here are non-trivial.
Therefore, I will explain each step in detail.
As an example, we will develop a console application. Take the source code in Listing 7.2 as a sample for the source code of this application. Then add the file with the source code of the assembly function to your application.
For this purpose, you must complete a few additional actions.
file:///D|/2/0031.html (5 von 30) [25.01.2008 00:11:23]
First, add a new text file to your project. This file will contain the source code of the assembly function. Create this file with the Add New Item…option of the Project menu item as shown in Fig. 7.4.
Fig. 7.4: Adding a new file to a project
Select the type of the file added to the project. There is no template for an ASM file in the Visual C++ .NET development environment. Therefore, use one of the text templates. This should be a text file. Of the available templates, select Text File (.txt) and specify the file name in the text box. Let it be sub2.asm.
Note that the text file does not have to have the ASM extension. Nothing prevents us from saving it as, say, sub2.txt. The C++ .NET inline assembler will process any text file you pass it.
Now we will go back to the sub2.asm file (Fig. 7.5).
file:///D|/2/0031.html (6 von 30) [25.01.2008 00:11:23]
Fig. 7.5: Selecting the file type and extension
Put the assembly code of the subf2 function into the empty file sub2.asm. Save the project. Next you should tell the compiler how it must process the ASM file. To do this, select the sub2.asm file in Solution Explorer and go to the Properties tab (Fig. 7.6).
Fig. 7.6: Setting options for processing the sub2.asm file
In the Property page that will open, specify parameters for processing the sub2.asm file. The command line for the MASM inline compiler can be as follows:
ML /c /coff sub2.asm
If you saved the file as a TXT file, the only thing you should change in this command line is the file name:
ML /c /coff sub2.txt
Setting the MASM compiler options of the C++ .NET environment is shown in Fig. 7.7.
file:///D|/2/0031.html (7 von 30) [25.01.2008 00:11:23]
Fig. 7.7: Setting parameters for compiling the sub2.asm file
The Command Line parameter should be the following:
ML.EXE /c /coff sub2.asm
The Outputs parameter should contain the object module name, sub2.obj in our case. Save the project again and compile it. After you start the application, its window will look like shown in Fig. 7.8.
Fig. 7.8: Window of an application that demonstrates the use of an ASM file in the C++ .NET environment
We can draw a few conclusions concerning the use of object modules in a C++ .NET application. Although the examples above are quite simple, you can judge the advantages of a particular method of compiling and adding a module to a project from these examples.
Using the Visual C++ .NET 2003 environment for compiling ASM files and add-ing them to your project is very convenient. This is particularly true for the application debugging stage where you have to rebuild the application or recompile its individual modules repeatedly. Using the inline compiler saves you a lot of time in this case.
Note that object files generated with the macro assembler are external for the C++ .NET compiler regardless of the method of generating them. This means that all functions in these modules should be declared with the extern directive.
ASM files can contain several functions, and some of the object module’s functions can be called by other functions of the same module and use their results. Change the source code of the assembly module by adding the code of a function that adds 100 to an integer parameter it takes (name the function add100). The modified source code is shown in Listing 7.3.
Listing 7.3: A modified version of the subf2.asm assembly file
; --- subf 2.asm---
file:///D|/2/0031.html (8 von 30) [25.01.2008 00:11:23]
.686
.model flat, C .code
subf2 proc ; cdecl push EBP
mov EBP, ESP ;
finit
fld DWORD PTR [EBP+8] ; load f1 to ST(0) fsub DWORD PTR [EBP+12] ; subtract f2 from f1 fwait
pop EBP ret
subf2 endp add100 proc push EBP mov EBP, ESP
mov EAX, DWORD PTR [EBP+8]
add EAX, 100 pop EBP ret
add100 endp end
Save the source code in the subf2.asm file. Develop a console application that uses the functions subf2 and add100 and add the subf2.asm file to your project. The source code of the console application is shown in Listing 7.4.
Listing 7.4: A console application that uses the functions subf2 and add100 from the added assembly module
#include <stdio.h>
extern "C" float subf2(float f1, float f2);
extern "C" int add100(int i1);
void main(void) {
float f1, f2;
printf("CUSTOM BUILD WITH ASM-FILE BUILT-IN\n\n");
printf("Enter float f1: ");
scanf("%f", &f1);
printf("Enter float f2: ");
scanf("%f", &f2);
float fsub = subf2(f1, f2);
printf("f1 - f2 = %.2f\n", fsub);
printf("Rounded +100 = %d\n", addl00((int)(fsub/10)));
getchar();
}
The window of this application is shown in Fig. 7.9.
file:///D|/2/0031.html (9 von 30) [25.01.2008 00:11:23]
Fig. 7.9: Window of an application that demonstrates the use of two functions from the added assembly module
So far, we assumed that the calls to the functions from the assembly module are done according to the _cdecl convention. To use another convention, such as _stdcall, you need to make some changes to the file with the source code of these functions. Suppose you want the add100 function to be called in accordance with the _stdcall convention. In this case, the source code of the subf2.asm assembly module should appear as shown in Listing 7.5 (the changes are in bold).
Listing 7.5: The source code of the functions subf2 and add100 with the conventions _cdecl and _stdcall
;--- subf2.asm (variant 3)--- .686
.model flat .code
_subf2 proc ; cdecl push EBP
mov EBP, ESP ;
finit
fld DWORD PTR [EBP+8] ; Load f1 to ST(0) fsub DWORD PTR [EBP+12] ; Subtract f2 from f1 fwait
pop EBP ret
_subf2 endp _add100@4 proc push EBP mov EBP, ESP
mov EAX, DWORD PTR [EBP+8]
add EAX, 100 pop EBP ret 4 _add100@4 endp end
Note that if the default convention _cdecl is used for all functions of an assembly module, it will suffice to use the
"C" qualifier in the model directive, and you will not need to use special notation for the function names with an underscore character at the beginning. If another convention for function calls or a mixed variant as in Listing 7.5 is used, this should be explicity specified with the function name notation.
The main program should contain the following lines corresponding to the subf2 and add100 functions:
file:///D|/2/0031.html (10 von 30) [25.01.2008 00:11:23]
extern "C" float subf2(float f1, float f2) extern "C" int _stdcall add100(int i1)
The use of assembly modules is not confined to calls to functions in a C++ .NET main program. For data exchange, you can use common variables. This term is borrowed from earlier versions of Microsoft C++ compilers. It is not very precise, but it is appropriate for describing the essence of the method.
We will illustrate this with an example, taking the previous console application and the assembly module from Listing 7.5 as a model. Change the source code in the assembly file so that a common variable can be used for the result of computing. Remember that the difference of two floating-point numbers was returned by the subf2
function in the coprocessor’s stack ST(0) and used in other pieces of the program code.
Now, the result of the function will be put into a double-word variable fres and then used in the main program. The modified source code of the assembly module is shown in Listing 7.6 (the changes are in bold).
Listing 7.6: The use of the fres common variable in an assembly module
;--- subf 2std.asm ---— .686
.model flat public _fres .data
_fres DD 0 .code
_subf2 proc ; cdecl push EBP
mov EBP, ESP ;
finit
fld DWORD PTR [EBP+8] ; load f1 to ST(0) fsub DWORD PTR [EBP+12] ; subtract f2 from f1 lea ESI, _fres
fst DWORD PTR [ESI]
fwait pop EBP ret
_subf2 endp _add100@4 proc push EBP mov EBP, ESP
mov EAX, DWORD PTR [EBP+8]
add EAX, 100 pop EBP ret 4
_add100@4 endp end
Now, we will explain this source code. A new section, .data, has appeared. It declares the fres variable as public. This means that the variable is accessible from other modules. Since there is no explicit call convention, you can assume the fres variable is processed in accordance with the _cdecl convention. This is why the name of the variable begins with an underscore character.
The commands lea ESI, _fres fst DWORD PTR [ESI]
file:///D|/2/0031.html (11 von 30) [25.01.2008 00:11:23]