Windows to UNIX porting, Part 1: Porting C/C++ sources

Demystifying the process of porting a C/C++-based project from Windows to UNIX

Software programs are often made to run on systems that are completely different from the system in which the program is coded or developed. This process of adapting software across systems is known as porting. You might need to port software for any one of several reasons. Perhaps your end users want to use the software in a new environment, such as a different version of UNIX®, or perhaps your developers are integrating their own code into the software to optimize it for your organization's platform.

Share:

Rahul Kumar Kardam (rahul@syncad.com), Senior Software Developer, Synapti Computer Aided Design Pvt Ltd

Rahul Kardam is a senior software developer specializing in complex C++-based electronic design automation tools such as simulators for hardware designs. He has programming experience in both Windows and UNIX platforms. Rahul enjoys tinkering with open source software, which he uses as a framework for designing robust, scalable code for the design automation tools he works on.



18 September 2007

Also available in Chinese Russian

Porting from Windows to a UNIX environment

Most Microsoft® Windows®-based projects are built using Microsoft Visual Studio®, which has a sophisticated integrated development environment (IDE) that automates almost the entire build process for the developer. In addition, Windows developers use Windows platform-specific application program interfaces (APIs), headers, and language extensions. Most UNIX®-like systems, such as SunOS, OpenBSD, and IRIX, don't support an IDE or any Windows-specific headers or extensions, thereby making porting a time-consuming activity. To make matters more complicated, legacy Windows-based code was meant to be run on 16-bit or 32-bit x86 architecture. UNIX-based environments are often 64-bit, and most UNIX vendors don't support the x86 instruction set. This article, the first in a two-part series, demystifies the process of porting a typical Visual C++ project in a Windows operating system to a g++ environment in SunOS while addressing the aforementioned issues in some detail.

C/C++ project types in Visual Studio

You can use a Visual C++ project to create one of three variants (single or multi-threaded) of a project:

  • Dynamic-link library (DLL or .dll)
  • Static library (LIB or .lib)
  • Executable (.exe)

For more complex variants, use a Visual Studio .NET solution—this solution makes it possible to create and manage multiple projects. The next couple of sections in this document focus on porting dynamic and static library project variants from Windows to UNIX.

Porting a DLL to a UNIX environment

The UNIX equivalent of a .dll file in Windows is a shared object (.so) file. However, the process of creating a .so file is rather different from that of creating a .dll file. Consider the example in Listing 1, where you try to create a small .dll file that has a single function, printHello, which is called from the main routine in the main.cpp file.

Listing 1. File hello.h containing the declaration for the printHello routine
#ifdef BUILDING_DLL
  #define PRINT_API __declspec(dllexport)
#else
  #define PRINT_API __declspec(dllimport)
#endif

extern "C" PRINT_API void printHello();

Listing 2 provides the source code for hello.cpp.

Listing 2. File hello.cpp
#include <iostream>
#include "hello.h"

void printHello  
  {
  std::cout << "hello Windows/UNIX users\n";
  }

extern "C" PRINT_API void printHello();

If you use the Microsoft 32-bit C/C++ standard compiler for 80x86 platforms (cl), the following command creates the hello.dll file:

cl /LD  hello.cpp /DBUILDING_DLL

/LD instructs cl to create a .dll file. (It can be instructed to create other formats such as .exe or .obj.) /DBUILDING_DLL defines the PRINT_API macro for this particular building process so that the printHello symbol is exported from this DLL.

Listing 3 contains the main.cpp main source file, which uses the printHello routine. The assumption here is that hello.h, hello.cpp, and main.cpp are all in the same folder.

Listing 3. Main sources using the printHello routine
#include "hello.h"

int main ( )
  {
  printHello();
  return 0;
  }

To compile and link the main code, use the following command line:

cl main.cpp hello.lib

A quick inspection of the sources and generated output reveals two important facts. First, the Windows-specific syntax, __declspec(dllexport), is needed to export any functions, variables, or classes from a DLL. Likewise, the Windows-specific syntax, __declspec(dllimport), is needed to import any functions, variables, or classes from a DLL. And second, the compilation generates two files: printHello.dll and printHello.lib. PrintHello.lib is used to link the main sources, and the UNIX headers for shared objects don't need the declspec syntax. The output of a successful compilation is a single .so file that gets linked with the main sources.

To create a shared library in UNIX platforms using g++, compile all source files as relocatable shared objects by passing the -fPIC flag to g++. PIC stands for position independent code. A shared library is potentially mapped to a new memory address every time it gets loaded. Therefore, it makes sense to generate the addresses of all variables and functions inside the library in a way that can be easily computed relative to the start address that the library is loaded to. This code is generated by the -fPIC option and makes the code relocatable. The -o option is used to specify the name of an output file, and the -shared option builds a shared library in which unresolved references are allowed. To create the hello.so file, you must modify the header, as shown in Listing 4 below.

Listing 4. Modified header for hello.h with UNIX-specific changes
#if defined (__GNUC__) && defined(__unix__)
  #define PRINT_API __attribute__ ((__visibility__("default")))
#elif defined (WIN32)
  #ifdef BUILDING_DLL
    #define PRINT_API __declspec(dllexport)
  #else
    #define PRINT_API __declspec(dllimport)
#endif

extern "C" PRINT_API void printHello();

And here's the g++ command for linking the shared library hello.so:

g++ -fPIC -shared hello.cpp -o hello.so

To create the main executable, compile the sources:

g++ -o main main.cpp hello.so

Symbol hiding in g++

There are two typical ways to export symbols from a Windows-based DLL. The first method is to use __declspec(dllexport) only on select elements (for example, classes, global variables, or global functions) that are exported from the DLL. The second method is to use a module-definition (.def) file. .def files have their own syntax and contain the symbols that need to be exported from the DLL.

The default behavior of the g++ linker is to export all the symbols from a .so file. This might not be desirable, and it makes linking multiple DLLs a time-consuming task. To selectively export symbols from a shared library, use the g++ attribute mechanism. For example, consider that the user sources have two methods, 'void print1();' and ' int print2(char*);', and the user needs to export print2 only. Listing 5 encloses a means of achieving this in both Windows and UNIX.

Listing 5. Hiding symbols in g++
#ifdef _MSC_VER // Visual Studio specific macro
  #ifdef BUILDING_DLL
    #define DLLEXPORT __declspec(dllexport)
  #else
    #define DLLEXPORT __declspec(dllimport)
  #endif
  #define DLLLOCAL 
#else 
  #define DLLEXPORT __attribute__ ((visibility("default")))
  #define DLLLOCAL   __attribute__ ((visibility("hidden")))
#endif 

extern "C" DLLLOCAL void print1();         // print1 hidden 
extern "C" DLLEXPORT int print2(char*); // print2 exported

Using __attribute__ ((visibility("hidden"))) prevents symbol exporting from a DLL. The latest versions of g++ (4.0.0 and higher) also provide the -fvisibility switch, which you can use to selectively export symbols from a shared library. Using g++ with -fvisibility=hidden in a command line suspends exporting of all symbols from a shared library, except for those that have been declared with __attribute__ ((visibility("default"))). This is a neat way of telling g++ that every declaration that is not explicitly marked with a visibility attribute has a hidden visibility. Using dlsym to extract a hidden symbol returns NULL.

Overview of the attribute mechanism in g++

Much like the Visual Studio environment, which provides a lot of additional syntax on top of C/C++, g++ supports many non-standard extensions to the language. One of these, the attribute mechanism in g++, is handy for porting purposes. The previous example discussed symbol hiding. Yet another use of attributes is to set function types, such as cdecl, stdcall, and fastcall, to Visual C++. Part 2 of this series discusses the attribute mechanism in greater detail.

Explicit DLL or shared object loading in a UNIX environment

In Windows systems, it is quite common to for a .dll file to be explicitly loaded by a Windows program. For example, consider a sophisticated Windows-based editor that has printing capabilities. Such an editor would dynamically load the DLL for the printer driver the first time a user makes the corresponding request. Windows-based developers use the Visual Studio-provided APIs, such as LoadLibrary, to explicitly load a DLL, GetProcAddress to query for a symbol from the DLL, and FreeLibrary to unload an explicitly loaded DLL. The UNIX equivalents for the same functions are the dlopen, dlsym, and dlclose routines. Further, in Windows, there's a special DllMain method that is invoked the first time the DLL is loaded onto memory. UNIX-like systems have a corresponding method called _init.

Consider a variant of the previous example. Listing 6 is the loadlib.h header file, which is used in the sources that call the main method.

Listing 6. Header file loadlib.h
#ifndef  __LOADLIB_H
#define  __LOADLIB_H

#ifdef UNIX
#include <dlfcn.h>
#endif 

#include <iostream>
using namespace std;

typedef void* (*funcPtr)();

#ifdef UNIX
#  define IMPORT_DIRECTIVE __attribute__((__visibility__("default")))
#  define CALL  
#else
#  define IMPORT_DIRECTIVE __declspec(dllimport) 
#  define CALL __stdcall
#endif

extern "C" {
  IMPORT_DIRECTIVE void* CALL LoadLibraryA(const char* sLibName); 
  IMPORT_DIRECTIVE funcPtr CALL GetProcAddress(
                                    void* hModule, const char* lpProcName);
  IMPORT_DIRECTIVE bool CALL  FreeLibrary(void* hLib);
}

#endif

The main method now explicitly loads the printHello.dll file and invokes the print method for the same, as shown in Listing 7.

Listing 7. Main file Loadlib.cpp
#include "loadlib.h"

int main(int argc, char* argv[])
  {
  #ifndef UNIX
    char* fileName = "hello.dll";
    void* libraryHandle = LoadLibraryA(fileName);
    if (libraryHandle == NULL)
      cout << "dll not found" << endl;
    else  // make a call to "printHello" from the hello.dll 
      (GetProcAddress(libraryHandle, "printHello"))();
    FreeLibrary(libraryHandle);
#else // unix
    void (*voidfnc)(); 
    char* fileName = "hello.so";
    void* libraryHandle = dlopen(fileName, RTLD_LAZY);
    if (libraryHandle == NULL)
      cout << "shared object not found" << endl;
    else  // make a call to "printHello" from the hello.so
      {
      voidfnc = (void (*)())dlsym(libraryHandle, "printHello"); 
      (*voidfnc)();
      }
    dlclose(libraryHandle);
  #endif

  return 0;
  }

DLL search path in Windows and UNIX environments

In Windows operating systems, a DLL is searched in the following order:

  1. Directory where the executable is located (for example, notepad.exe in Windows)
  2. Current working directory (That is, the directory from which notepad.exe is launched.)
  3. Windows system directory (typically C:\Windows\System32)
  4. Windows directory (typically C:\Windows)
  5. Directories listed as part of the PATH environment variable

In UNIX-like systems, such as Solaris, the LD_LIBRARY_PATH environment variable specifies the shared library search order. The path to a new shared library needs to be appended to the LD_LIBRARY_PATH variable. The search order for HP-UX involves directories listed as part of LD_LIBRARY_PATH followed by those in SHLIB_PATH. For IBM AIX® operating systems, it's the LIBPATH variable that determines the shared library search order.

Porting a static library from Windows to UNIX

The object code of static libraries, as opposed to dynamic link libraries, is linked when the application compiles and, as such, becomes a part of the application. Static libraries in UNIX systems follow a naming convention, where lib is prefixed and .a is suffixed to the library name. For example, the Windows user.lib file would typically be named libuser.a in a UNIX system. The operating system-provided commands ar and ranlib are used to create static libraries. Listing 8 illustrates how to create a static library, libuser.a, from the user_sqrt1.cpp and user_log1.cpp source files.

Listing 8. Creating a static library in a UNIX environment
g++ -o user_sqrt1.o -c user_sqrt1.cpp 
g++ -o user_log1.o -c user_log1.cpp
ar rc libuser.a user_sqrt1.o user_log1.o 
ranlib libuser.a

The ar tool creates a static library, libuser.a, and puts copies of the user_sqrt1.o and user_log1.o object files in it. If there is an existing library file, the object files are added to it. If the object files being used are newer than those inside the library, the older ones are replaced. The r flag replaces older object files in the library with newer versions of the same object files. If it doesn't exist yet, the c option creates the library.

After a new archive is created or an existing one is modified, an index of archive contents needs to be created and stored as part of the archive. The index lists each symbol, defined by a member of an archive, that is a relocatable object file. The index speeds up linking with the static library and allows routines in the library to be called, irrespective of their actual placement inside the library. Note that the GNU ranlib is an extension of the ar tool, and invoking ar with an s argument, [ar -s], has the same effect as invoking ranlib.

Precompiled headers

C/C++-based applications in Visual C++ often use precompiled headers. Precompiled headers are a performance feature that certain compilers, such as cl in Visual Studio, provide to help speed up compilation. Complex applications often make use of header (.h or .hpp) files, which are sections of code that are meant to be included as part of one or more source files. The header files are modified only rarely during the scope of a project. Thus, to speed up compilation, these files can be converted into an intermediate form that is easier for the compiler to understand so that subsequent compilations are faster. This intermediate form is called precompiled header files or PCH in the Visual Studio environment.

Consider the example involving hello.cpp in Listings 1 and 2 earlier in this article. The inclusion of iostream and the definition of the EXPORT_API macro can be considered code-invariant parts of the file throughout the scope of the project. Thus, they are good candidates for inclusion in a header file. Listing 9 shows what the code looks like with the relevant changes.

Listing 9. Contents of precomp.h
#ifndef __PRECOMP_H
#define __PRECOMP_H

#include <iostream>

#  if defined (__GNUC__) && defined(__unix__)
#    define EXPORT_API __attribute__((__visibility__("default")))
#  elif defined WIN32
#    define EXPORT_API __declspec(dllexport) 
#  endif

Listing 10 shows the source code of the DLL with the relevant changes.

Listing 10. Contents of new hello.cpp file
#include "precomp.h"
#pragma hdrstop

extern "C" EXPORT_API void printHello()
  {
  std::cout << "hello Windows/UNIX users" << std::endl;
  }

As the name suggests, a precompiled header file contains object code in a compiled form that is included before the header stop point. This point in the source file is usually marked by a lexeme that is not consumed as a token by the preprocessor, meaning one that is not a preprocessor directive. Alternatively, this header stop point can also be specified as #pragma hdrstop, if it is encountered in the sources before a valid non-preprocessor language keyword in the source text.

In a Solaris build, a precompiled header file is searched for when #include is in the compilation. As it searches for the included file, the compiler looks for a precompiled header in each directory just before it looks for the include file in that directory. The name searched for is the name specified in the #include with .gch appended. If the precompiled header file can't be used, it is ignored.

Here is the command line for achieving precompiled header facility in Windows:

cl /Yc precomp.h hello.cpp /DWIN32 /LD

/Yc tells the cl compiler to generate the precompiled header from precomp.h. The same functionality is achieved in Solaris using the following lines:

g++ precomp.h
g++ -fPIC -G hello.cpp -o hello.so

The first command creates the precompiled header precomp.h.gch. The rest of the procedure for generating the shared object is the same as described earlier in the article.

Note: Support for precompiled headers in g++ is available for versions 3.4 and above.

Conclusion

Porting across two completely divergent systems, such ase Windows and UNIX, is never an easy task and, as such, it requires a lot of tweaking and patience. This article explained the essentials of porting the most basic project types from a Visual Studio environment to a g++/Solaris-based one. The second and concluding article in this series discusses the multitude of compiler options available in the Visual Studio environment and their g++ equivalents, the g++ attribute mechanism, some of the problems porting from a 32-bit (typically Windows) to a 64-bit (UNIX) environment, and multithreading.

Resources

Learn

Get products and technologies

  • IBM trial software: Build your next development project with software for download directly from developerWorks.

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into AIX and Unix on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=AIX and UNIX
ArticleID=255903
ArticleTitle=Windows to UNIX porting, Part 1: Porting C/C++ sources
publish-date=09182007