Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

GNU & Native Compilers


February 04:

While you typically use the same compiler for building all of a C++ program's compilation units, you occasionally need to create some binaries with another compiler. For instance, a required commercial library may only be available as a native-compiled library, but you have to use the gcc compiler for your own sources. This creates two problems:

  • Different name mangling by different C++ compilers leads the linker to fail.
  • During runtime, the initialization of global variables in some modules is missed.

As a workaround to these problems, I present in this article a universal and (almost) platform-independent solution for linking binary files (object files or libraries), which are compiled with different C++ compilers into one executable. For example, assume you have to compile the C++ source files of your project using gcc. However, you must use the functions calcEllipse() and calcCircle(), implemented by a commercial developer as the binary archive [1] "libnative.a" that is compiled with aCC (Hewlett-Packard's native compiler for HP-UX). Listing 1 (native.h) contains the public interface of the archive. (The function calcCircle() is defined as extern "C".)

To test the archive, I built a framework (Listing 2, owncode1.cc) that is compiled using gcc. The file main.cc (Listing 3) contains the program's main function, which only calls mainloop() (implemented in owncode1.cc). In turn, mainloop() calls calcEllipse() and calcCircle() from the native-compiled archive. Figure 1 shows what happens when you try to compile/link this using the prepared makefile, then call gmake main1. But it doesn't work. The linker reports the C++ function calcEllipse(), referenced within owncode1.o, as an undefined symbol, although the archive libnative.a exports the function. However, the C function calcCircle() doesn't cause any trouble.

If you remove calcEllipse()'s call from owncode1.cc, you can link the program, but it will probably crash with a core dump; see Figure 2. Thus, there is a second problem.

How Compilers and Linkers Work

When the compiler processes owncode1.cc (Listing 2), it doesn't find the implementations of calcEllipse() and calcCircle() in this compilation unit. It registers both as unresolved symbols in the symbol table of the generated object file owncode1.o. However, the compilation unit implements the (implicitly) exported C function mainloop() and the compiler generates a corresponding entry. You can check the symbol table using the command nm owncode1.o. Figure 3 presents the important part of its output — U shows that a symbol is unresolved, while T means that the symbol is defined in the file.

The linker has to resolve these symbols — it has to assign an implementation to every unresolved symbol. The linker looks for the implementation of these symbols in all available compilation units and copies them into the executable (static linkage is assumed). It finds the implementation of calcCircle(), but not calcEllipse(), in the archive libnative.a. Comparing the output of nm libnative.a in Figure 4 with that in Figure 3, two things stick out:

  • The compiler appended a suffix to the function name calcEllipse() when creating the symbol name, whereas the function name calcCircle() corresponds to the symbol name.
  • The symbol for calcEllipse() differs in both binaries. It is clear why the linker is unable to make the assignment for calcEllipse().

Name Mangling

The reason calcEllipse() has a long symbol name is because of so-called "name mangling." Among other differences to the C language, C++ allows equal function names — only the parameter lists need to differ in this case. The linker, however, doesn't know the parameter lists, but does need unique identifiers. For that, the compiler also generates unique identifiers for C++ functions by coding the whole function signature into the symbol name (for detailed information, compare [2]). Unfortunately, the assignment is not standardized and is, as a result, compiler dependent. Since calcCircle() was declared as extern "C", the compiler takes a C binding for the function signature although it is implemented in C++. That's why the linker doesn't cause any trouble regarding this function.

Since it is impossible to obtain compatibility between different C++ compilers, you have no choice but to fall back on C. It is (in a restricted sense) possible to map every C++ interface into a semantically equivalent C interface. The mathematical definition of a suitable map would go beyond the scope of this article. In my example, you must be content with an intuitive but workable interface definition. For large interfaces, however, you should use a mathematically evaluated model and leave the creation of the interface to a generator.

The compiler-independent solution to the name-mangling problem lies in avoiding it. I define an interface in C that is semantically equivalent to native.h. In my C++ source code (compiled using the gcc), I only include this interface. The implementation of the interface forms a wrapper layer that connects my gcc-compiled C++ source code to the native-compiled C++ source code of the archive. Since the wrapper layer calls the functions from the library directly, I compile them using the native compiler; see Figure 5. I next define a simple C interface native_wrap.h (Listing 4) for the interface of the native archive as defined in native.h (Listing 1). You must also map all complex data types that are dependent on name mangling. While there is no need to map the C function calcCircle() because of name mangling, I still do it for consistency. This is the only way you can avoid including the native.h header in your own gcc source code.

The implementation of the wrapper layer is trivial, and executed in the native_wrap.cc file (Listing 5). Listing 6 shows the changed part of owncode2.cc in owncode3.cc. Now you can compile and link the program as in Figure 6 — but alas, it still doesn't work.

Although the name-mangling problem could be solved in this way, the linked executable is useless. If you debug the code, you see that the program crashes when it calls calcEllipse(); see Figure 7. Since the source code of commercial libraries isn't typically available, the program crashes for no apparent reason. In this example, however, you can examine the source code; see Listing 7 (native.cc).

The code is admittedly unrealistic, but does illustrate the problem. Pi, a global pointer within this compilation unit, should point to the address of localPi after the initialization by calling the function initPi(). Since the printf output in Figure 6 is missing, you can assume that initPi() was never called. Consequently, the pointer Pi stays undefined. The access to an undefined pointer in calcEllipse() or calcCircle() makes a crash probable.

Global Variable Initialization

Global variables have to be initialized before other functions that use them are entered. For global objects, that means that their constructors have to be called. The compiler and linker are both responsible for this. The detailed procedure depends on the compiler and operating system. In most cases, the compiler generates a special initialization code for every global variable. The linker has to guarantee that this code is executed in good time. But since the linker doesn't know anything about this code, the C++ compiler must tell the linker something about the initialization code. This is possible if you call the linker every time, using the compiler as a front end. The compiler sometimes calls special tools (like collect2, in case of the gcc) before executing the linker. Then it controls the linker by calling them with specific arguments.

If you compile the C++ source files using different compilers (as in my example), you can only use one compiler as front end for the linker. However, some global variables won't be initialized — those variables from the files you compiled using the other compiler. In my example, native.cc (Listing 7) in the archive libnative.a was compiled using aCC, while the linker was called by gcc. Therefore, Pi in native.cc could not be initialized, but the variable filename in owncode3.cc (Listing 6) could. Of course, you could try to use aCC instead of gcc as the front end for the linker, but that won't work either since the variables in libnative.a will be initialized, but not those in owncode3.cc. The same applies for the finalization of global objects. The destructors of global objects should be called before the program is exited. But that problem is also addressed by the solution of the initialization problem.

You have to force the execution of all lost initializers. But to do so, you need some knowledge about the inner workings of the compiler when it generates the initialization code. To stay independent of the internals of diverse native compilers, you should use the native compiler to link the executable. In my example, only the variables in my own compilation units (compiled using gcc) will stay uninitialized. But since the internals of the gcc are well documented, you can force the initialization of the remaining variables without any hacks.

gcc doesn't call the linker directly, but uses the collect2 utility, which collects the initialization code from all gcc compilation units and forces the linker to guarantee the execution of this code. Depending on the operating system, gcc follows up in two ways:

  • On operating systems such as HP-UX and AIX, collect2 integrates the initializer calls into a single function _GLOBAL__DI (finalizer _GLOBAL__DD) and exports this function to the linker. The linker ensures its execution before the main function of the program is entered.
  • On ELF operating systems (such as SunOS), collect2 doesn't produce an initialization function, but puts function pointers to the initialization code in a static array called __CTOR_LIST__ (finalization __DTOR_LIST__) [5]. The initialization consists of an iteration of this array from which the function pointers are called. Some small object files (ctr1.o, crti.o, crtbegin.o, crtend.o, crtn.o) that are part of the system's linker make the mechanism work if they are put to the linker in the correct order. (Calling gcc using the option -v shows this.)

If you don't use collect2 to link the executable, you would have to collect the initialization code from the gcc binaries and ensure its execution on your own. This is almost impossible without hacking an initialization algorithm, a process that is highly compiler- and operating-system dependent. However, there is an elegant solution — you use the native compiler as a front end to link the executable, but you put all gcc modules in a shared library and use the gcc as a front end to link the shared library. The sticking point is the following: When a linker creates a shared library, it collects the initialization code from all object files and archives in the same way an executable is linked. It also generates a single initialization function or a single initialization array for that shared library. In this way, the initialization code of the gcc modules is collected by collect2 (because you use the gcc as a front end to link the shared library) but the initialization code of the native modules is collected during the linkages of the executable by use of the native compiler as a front end. All you have to do, then, is to tell the executable linker the initialization point of the shared library.

Moreover, by linking all gcc modules into one shared library you completely separate the "gcc-world" from the "native-world" (see Figure 8). This implies that you really link all gcc modules to the shared library, including the C++ Standard Library libstdc++.a that comes with gcc (if needed). In this way, the linker can resolve all "gcc-symbols" within the shared library. That's why you should not use a shared version of libstdc++, although it may work on some platforms. Any possible conflicts between the C++ Standard Library that comes with the gcc and the one that comes with the native compiler are avoided. Under some operating systems, you have to force this resolution via a special linker flag (for example, under AIX, option: -r). In addition, you can filter the symbols that are exported by the shared library by using the concerned linker flags. If possible, the shared library should only export the symbols that are really used from outside.

Figure 8 shows the module structure of the example graphically. Owncode3.o is the only gcc module you have to link into the shared library libowncode.sl. Additional other modules, libstdc++.a, for example, are drawn in gray, because they are not yet needed in the simple example, but in the more complex one below. libowncode.sl exports the symbol mainloop, which is used by the native compiled main.o module, and the two symbols for the initialization and finalization code. Then libowncode.sl, native_wrap.o, main.o, and libnative.a will be linked to the executable main4.

Compiling/Linking the Example

HP-UX needs position-independent code (PIC) [3] for building shared libraries [6]. Consequently, you must force the compiler to create PIC for all binaries you have to link into libowncode.sl by use of the flag -fPIC. To restrict the symbols exported by the shared library, you can use the linker flags -E and +e. When linking the executable, you must tell the linker that it has to use the function _GLOBAL__DI to initialize the shared library (and _GLOBAL__DD to finitialize). Use the linker option +init (and +finit) for that. You can use the makefile again and call gmake main4 for doing this (Figure 9), and the program works correctly. After starting the program, you can observe the initialization of the variable Pi in the archive libnative.a by the output initPi from the function initPi() as well as the initialization of the variable filename in owncode3.cc by the output initFilename, as in Figure 9.

Under AIX there is no position-dependent code due to the table of contents (TOC) mechanism [2, Chapter 8.3]. There is no need to use an fPIC-flag. Regarding the initialization of global variables, AIX is comparable to HP-UX. Please use the linker option -binitfini for that. Unique to AIX, the export/import of symbols to/from shared libraries is controlled by separate import/export files. A full explanation of that mechanism would go beyond the scope of this article, although notice that the files main4.exp (Listing 8) and libowncode3.exp control the mechanism in the example. Further, the resolution of all symbols from libgcc.a within the shared library must be forced by use of the linker option -r. That is executed as a single step before the shared library itself is linked. You can build the full example calling gmake main4, as in Figure 10.

SunOS needs PIC; for instance, you should use the fPIC-flag. But the initialization array __CTOR_LIST__ (finalization __DTOR_LIST__) of the shared library is detected automatically without any special linker flag; see Figure 11.

A More Complex Example

To this point, I haven't used any classes, nor explicitly included any symbols from the standard libraries of the C++ compilers. Using the technique presented here should not present any difficulties for more complex examples. For starters, assume that the public interface of the native archive looks like Listing 9.

To include classes into the wrapper interface, all you normally have to do is to extend the language mapping. Again, for serious applications, you should definitely use mathematically evaluated mapping. But for small examples, do this to produce a workable language mapping:

  • Define an abstract pointer type to deal with instances of a class in your own code.
  • Define a wrapper data type for every data type that depends on the name mangling.

  • Define a wrapper function for every constructor. This wrapper function instantiates the class and returns the abstract pointer type.

  • Define a wrapper function for every destructor. This wrapper function expects an argument of the abstract pointer type and it calls the operator delete for this argument.

  • Define a wrapper function for every public method. The first argument of this wrapper function is a pointer of the abstract type. The list of arguments then follows the method.

Listing 10 includes the declaration of the wrapper interface (native_wrap.h), and Listing 11 shows the implementation.

If you use the standard streams cout, cerr, and cin, you also have to link the Standard C++ archives that come with the C++ compilers. For the gcc, it is the libstdc++.a. Figure 8 shows where additional libraries have to be placed. You only have to expand the commands used to link the shared library and the executable. Here, I only present the solution for the operating system HP-UX. The makefile, of course, also contains the solution for the operating systems AIX and SunOS.

Figure 12 shows how to build the example. Be aware of the PIC problem and check whether your libstdc++.a really contains only PIC [7]. Otherwise, your library linker will produce a reallocation error.

Now, use the standard streams cout, cerr, and cin, which are exported by the standard C++ libraries that come with the C++ compilers. Again, you should link the archive libstdc++.a (PIC version) to your shared library libowncode.sl. To force this, use the linker flag -l:libstdc++.a instead of -lstdc++ (Figure 8). Otherwise, the linker could use the shared version of libstdc++.sl if it exists. Only the C++ Standard Library that comes with your native compiler should be linked directly to the executable.

Due to the limited scope of this article, I only present the solution for the operating system HP-UX. The makefile, of course, also contains the solutions for the operating systems AIX and SunOS.

Of course, compiler vendors don't necessarily intend for developers to use different C++ Standard Libraries within a single executable. The approach I present here works also with those, but I can't exclude that in some cases special unexpected side effects will appear at runtime, i.e., I think of the standard streams cout und cerr. Every C++ library has its own, which may be buffered. If some functions write to the buffer of the native library and others to the buffer of the GNU library without flushing it immediately, the output on the screen can finally be mixed up. Other similar problems are conceivable. That is one reason why I would be careful in using this technique to build multithreaded executables.

The Technical Framework

The examples presented here are available at http://www.cuj.com/code/. In the directories sample1 and sample2, you'll find a subdirectory native_library, which contains the "binary libraries from the commercial vendor" for the three operating systems. In truth, I don't deliver the binaries, but the source code. Enter the subdirectories and call gmake yourself.

I tested the examples on HP-UX 11, AIX 5.2, and SunOS 5.8, but it should be easy for you to implement the technique to other versions, or even other operating systems. I used the gcc compiler collection version 3.2.3.

References and Notes

[1] A library can be both an archive or a shared library. Archives are static libraries the included symbols of which are completely resolved at link time. After completing the link procedure, the executable physically contains the needed code of the archive. Symbols from shared libraries are only referenced at link time. At runtime, the shared library is needed to access the implementation of the referenced symbols.

[2] Levin, John R. Linkers and Loaders, Academic Press, 2000.

[3] Shared libraries are loaded at different addresses, so all absolute addresses would have to be relocated at load time. To avoid this necessity, many operating systems work only with shared libraries that contain position-independent code; for example, the code contains only relative addresses. Because of this, I must use the gcc-compiler flag-fPIC for modules that I linked into the shared library. Under AIX, there is no need to use the flag. Because of IBM's table of contents (TOC) schema, the whole code is position independent.

[4] Stallman, Richard M. Using and Porting the GNU Compiler Collection. For gcc 3.3.2, 2002.

[5] Lu, Ilongiju. "ELF: From The Programmer's Perspective," 1995. (ftp://tsx-11.mit.edu/pub/linux/packages/GCC/elf.ps.gz)

[6] Hewlett-Packard. HP-UX Linker and Library User's Guide,

Part Number B 2355-90730, March 2001.

[7] If you build the gcc for your operating system, the regular building procedure won't use PIC for compiling the modules from which libstdc++.a is built. Because of that, you possibly have to rebuild your gcc. Then you have to configure the gcc-building process with the option --with-pic.


Karsten Hoof is a developer for Lufthansa Systems. He can be contacted at khoof@ web.de.



Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.