從Windows 移植到 UNIX 環境
大多數基於 Microsoft Windows 的項目都是使用 Microsoft Visual Studio 構建的,這是一種複雜的整合式開發環境 (IDE),它可以為開發人員實現幾乎整個構建過程的自動化。此外,Windows 開發人員使用了 Windows 平台特定的應用程式程式介面 (API)、標頭檔和語言擴充。大多數類 UNIX 系統,如 SunOS、OpenBSD 和 IRIX,都不支援 IDE 或者任何 Windows 特定的標頭檔或擴充,因此進行移植是一項非常耗費時間的活動。更麻煩的是,遺留的基於
Windows 的代碼需要運行於 16 位或者 32 位的 x86 體繫結構中。基於 UNIX 的環境通常是 64 位元的,並且大多數 UNIX 供應商都不支援 x86 指令集。本系列文章共由兩個部分組成,本文是其中的第一部分,介紹將 Windows 作業系統中一個典型的 Visual C++ 項目移植到 SunOS 中的
g++
環境的過程,同時詳細說明了如何解決前面提到的一些問題。
Visual Studio 中的 C/C++ 項目類型
您可以使用 Visual C++ 項目建立三種項目變體(單線程或者多線程)中的一種:
- 動態連結程式庫(DLL 或者 .dll)
- 靜態庫(LIB 或者 .lib)
- 可執行檔(.exe)
對於更複雜的變體,可以使用 Visual Studio .NET 解決方案,這種解決方案允許建立和管理多重專案。本文在下面的幾個部分中將重點關注如何將動態和靜態庫項目變體從 Windows 移植到 UNIX。
將 DLL 移植到 UNIX 環境
對於 Windows 中的 .DLL 檔案,UNIX 的等價物是共用對象 (.so) 檔案。然而,建立一個 .so 檔案的過程與建立一個 .DLL 檔案的過程完全不同。請考慮清單 1 中的樣本,在這個樣本中,您嘗試建立一個小的 .DLL 檔案,其中僅包含一個函數
printHello
,並且在 main.cpp 檔案的 main 常式中調用了這個函數。
清單 1. 包含 printHello 常式聲明的檔案 hello.h
#ifdef BUILDING_DLL #define PRINT_API __declspec(dllexport)#else #define PRINT_API __declspec(dllimport)#endifextern "C" PRINT_API void printHello(); |
清單 2 提供了 hello.cpp 的原始碼。
清單 2. 檔案 hello.cpp
#include <iostream>#include "hello.h"void printHello { std::cout << "hello Windows/UNIX users\n"; }extern "C" PRINT_API void printHello(); |
如果您使用了用於 80x86 平台的 Microsoft 32 位 C/C++
標準編譯器 (cl
),那麼可以使用下面的命令來建立 hello.DLL 檔案:
cl /LD hello.cpp /DBUILDING_DLL |
/LD
指示 cl
建立一個 .DLL 檔案。(還可以指示它建立其他格式的檔案,如 .exe 或者 .obj。)/DBUILDING_DLL
為這個特定的構建流程定義了
PRINT_API
宏,以便從這個 DLL 匯出 printHello 符號。
清單 3 包含了 main.cpp main 源檔案,其中使用了 printHello 常式。這裡所做的假設是,hello.h、hello.cpp 和 main.cpp 都位於相同的檔案夾中。
清單 3. 使用 printHello 常式的 main 的原始碼
#include "hello.h"int main ( ) { printHello(); return 0; } |
要編譯並串連 main 代碼,可以使用下面的命令列:
快速地查看源檔案和產生的輸出,其中說明了兩個重要的問題。第一點,要從一個 DLL 中匯出任何函數、變數、或者類,都需要使用 Windows 特定的文法
__declspec(dllexport)
。同樣地,要向一個 DLL 匯入任何函數、變數、或者類,都需要使用 Windows 特定的文法
__declspec(dllimport)
。第二點,這個編譯過程產生了兩個檔案:printHello.dll 和 printHello.lib。PrintHello.lib 用於串連 main 源檔案,而 UNIX 中共用對象的標頭檔不需要
declspec
文法。成功的編譯過程將輸出一個 .so 檔案,它已經與 main 源檔案進行了串連。
要在 UNIX 平台中使用 g++
建立一個共用庫,需要通過向 g++
傳遞 -fPIC
標誌,將所有的源檔案編譯為可重定位的共用對象。PIC 表示位置無關代碼 (position independent code)。在每次載入一個共用庫時,可以將其潛在地映射為一個新的記憶體位址。因此,需要通過某種很容易進行計算的方式在庫中產生所有變數和函數的地址(相對於載入該庫的起始地址)。這個代碼由
-fPIC
選項產生,並使得代碼成為可重定位的。-o
選項用於指定輸出檔案的名稱,而 -shared
選項用於構建一個共用庫,其中允許出現未解析的引用。要建立 hello.so 檔案,您必須修改標頭檔,如下面的清單 4 所示。
清單 4. 包含 UNIX 特定更改的、經過修改的 hello.h 標頭檔
#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)#endifextern "C" PRINT_API void printHello(); |
下面的 g++
命令用於串連共用庫 hello.so:
g++ -fPIC -shared hello.cpp -o hello.so |
要建立 main 可執行檔,請編譯原始碼:
g++ -o main main.cpp hello.so |
g++ 中的符號隱藏
有兩種典型的方式可以從一個基於 Windows 的 DLL 中匯出符號。第一種方法是僅對從 DLL 中匯出的選擇元素(例如,類、全域變數或者全域函數)使用
__declspec(dllexport)
。第二種方法是使用一個模組-定義 (.def) 檔案。.def 檔案具有自己的文法,並且包含需要從 DLL 中匯出的符號。
g++
連接器的預設行為是從一個 .so 檔案中匯出所有的符號。這可能並不是所需要的,並且將使得串連多個 DLL 變成一項非常耗時的任務。為了從一個共用庫中有選擇地匯出符號,可以使用
g++
屬性機制。例如,可以考慮使用者原始碼中包含兩個方法,'void print1();'
和 ' int print2(char*);'
,並且使用者只需要匯出 print2。清單 5 包含一種實現這個目的的方法,可用於 Windows 和 UNIX。
清單 5. 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 |
使用 __attribute__ ((visibility("hidden")))
可以防止從 DLL 中匯出符號。最新版本的
g++
(4.0.0 以及更高的版本)還提供了 -fvisibility
開關,您可以使用它從一個共用庫中有選擇地匯出相關符號。在命令列中使用
g++
加上 -fvisibility=hidden
延遲從共用庫中匯出所有的符號,除了那些使用
__attribute__ ((visibility("default")))
聲明的符號。這是一種非常簡潔的方式,用於通知 g++
沒有顯式地標註可見屬性的每項聲明,其可見度都是隱藏的。使用
dlsym
提取一個隱藏的符號將會返回 NULL
。
g++ 中的屬性機制概述
與 Visual Studio 環境非常相似(Visual Studio 環境在 C/C++
的基礎上提供了許多附加的文法),g++
也支援該語言的許多非標準擴充。在
g++
中,屬性機制的用途之一就是便於進行移植。前面的樣本討論了符號隱藏。屬性的另一個用途是為 Visual C++ 設定函數類型,如
cdecl
、stdcall
和 fastcall
。本系列文章的第 2 部分將詳細地介紹屬性機制。
在 UNIX 環境中明確式載入 DLL 或者共用對象
在 Windows 系統中,可以由 Windows 程式顯式地載入一個 .DLL 檔案,這是很常見的情況。例如,可以考慮一個複雜的、提供了列印功能的、基於 Windows 的編輯器。在使用者第一次提出相應請求的時候,這種編輯器將動態地載入印表機驅動程式 DLL。基於 Windows 的開發人員可以使用 Visual Studio 提供的 API,如
LoadLibrary
顯式地載入一個 DLL,GetProcAddress
用於查詢 DLL 中的符號,而
FreeLibrary
則用於卸載一個明確式載入的 DLL。對於這些函數,UNIX 的等價物分別是 dlopen
、dlsym
和
dlclose
常式。而且在 Windows 中,有一個特殊的 DllMain
方法,在第一次將 DLL 載入到記憶體時將調用這個方法。類 UNIX 系統提供了一個對應的方法,稱為
_init
。
可以考慮前面樣本的一個變體。清單 6 中是 loadlib.h 標頭檔,在調用 main 方法的源檔案中使用了這個檔案。
清單 6. 標頭檔 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#endifextern "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 |
main 方法現在顯式地載入 printHello.DLL 檔案,並調用相同的 print
方法,如所示清單 7 中所示。
清單 7. 主檔案 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; } |
Windows 和 UNIX 環境中的 DLL 搜尋路徑
在 Windows 作業系統中,按照下面的順序搜尋 DLL:
- 可執行檔所處的目錄(例如,notepad.exe 位於 Windows 目錄中)
- 當前工作目錄(即,從哪個目錄啟動了 notepad.exe。)
- Windows 系統目錄(通常為 C:\Windows\System32)
- Windows 目錄(通常為 C:\Windows)
- 作為 PATH 環境變數中的一部分所列舉的目錄
在類 UNIX 系統中,如 Solaris,LD_LIBRARY_PATH 環境變數可以指定共用庫搜尋順序。指向一個新的共用庫的路徑需要追加到 LD_LIBRARY_PATH 變數末尾。HP-UX 的搜尋順序包括作為 LD_LIBRARY_PATH 的一部分所列舉的目錄,然後是 SHLIB_PATH 中列舉的目錄。對於 IBM AIX 作業系統,由 LIBPATH 變數確定共用庫搜尋順序。
將靜態庫從 Windows 移植到 UNIX
與動態庫不同,在編譯應用程式時對靜態庫的目標代碼進行串連,並且因此成為該應用程式的一部分。在 UNIX 系統中,靜態庫遵循一種命名規範,使用 lib 作為首碼,而使用 .a 作為庫名的尾碼。例如在 UNIX 系統中,Windows 的 user.lib 檔案通常被命名為 libuser.a。作業系統提供的命令
ar
和 ranlib
可用於建立靜態庫。清單 8 說明了如何從 user_sqrt1.cpp 和 user_log1.cpp 源檔案建立一個靜態庫 libuser.a。
清單 8. 在 UNIX 環境中建立靜態庫
g++ -o user_sqrt1.o -c user_sqrt1.cpp g++ -o user_log1.o -c user_log1.cppar rc libuser.a user_sqrt1.o user_log1.o ranlib libuser.a |
ar
工具建立了靜態庫 libuser.a,並將 user_sqrt1.o 和 user_log1.o 目標檔案的副本放置於其中。如果存在一個現有的庫檔案,那麼將目標檔案添加到其中。如果所使用的目標檔案比庫中的檔案更新一些,那麼則替換舊的目標檔案。r
標誌表示使用相同目標檔案的更新版本替換庫中舊的目標檔案。如果這個庫並不存在,那麼
c
選項將建立這個庫。
在建立了一個新的封存檔案,或者修改了一個現有的封存檔案之後,需要建立封存檔案內容的索引,並將其作為該封存檔案的一部分進行儲存。這個索引列出了封存檔案的成員(可重定位目標檔案)所定義的每個符號。該索引可以提高與靜態庫進行串連的速度,並允許調用庫中的常式,而不考慮它們在庫中的實際位置。請注意,GNU
ranlib
是 ar
工具的擴充,並且使用 s
參數調用 ar
,[ar -s]
與調用
ranlib
具有相同的效果。
先行編譯標頭檔
在 Visual C++ 中,基於 C/C++
的應用程式通常會使用先行編譯標頭檔。先行編譯標頭檔是某些編譯器(如 Visual Studio 中的
cl
)的一項效能特性,它可以協助提高編譯的速度。複雜的應用程式通常會使用標頭檔(.h 或者 .hpp)檔案,它們是需要作為一部分進行包括的一個或多個源檔案的代碼部分。在一個項目的範圍內,很少對標頭檔進行修改。因此,為了提高編譯的速度,可以將這些檔案轉換為一種編譯器更容易理解的中間形式,以便提高後續編譯工作的速度。在 Visual Studio 環境中,這種中間形式稱為先行編譯標頭檔或者 PCH。
考慮本文前面清單
1 和
2 中包括 hello.cpp 的樣本。其中包含了 iostream
和 EXPORT_API
宏的定義,在該項目的範圍內,這些可以被看作是該檔案中不變的代碼部分。因此,它們適合放在一個標頭檔中進行包含。清單 9 顯示了可能會發生相關更改的代碼。
清單 9. 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 |
清單 10 顯示了 DLL 的原始碼,其中包括相關的更改。
清單 10. 新的 hello.cpp 檔案的內容
#include "precomp.h"#pragma hdrstopextern "C" EXPORT_API void printHello() { std::cout << "hello Windows/UNIX users" << std::endl; } |
正如其名稱所表示的,先行編譯標頭檔在 頭中止 (header stop) 點之前,以一種經過編譯的形式包含目標代碼。源檔案中的這個點通常由一個詞素進行標記,而預先處理程式不會使用該詞素作為一個語言符號,這表示它並不是一項預先處理程式指令。或者,還可以將這個頭中止點指定為
#pragma hdrstop
,如果在源文本中,它出現在一個有效非預先處理程式語言關鍵字之前。
在 Solaris 中進行編譯時間,當碰到 #include
時,將搜尋先行編譯標頭檔。在搜尋包含檔案的過程中,編譯器首先在每個目錄中尋找先行編譯標頭檔,然後再在這些目錄中搜尋包含檔案。需要搜尋的名稱可以在帶
.gch
的 #include
中進行指定。如果無法使用這個先行編譯標頭檔,那麼將忽略它。
下面的命令列可用於在 Windows 中實現先行編譯標頭檔功能:
cl /Yc precomp.h hello.cpp /DWIN32 /LD |
/Yc
通知 cl
編譯器從 precomp.h 產生先行編譯標頭檔。可以使用下面的命令在 Solaris 中實現相同的功能:
g++ precomp.hg++ -fPIC -G hello.cpp -o hello.so |
第一個命令建立了先行編譯標頭檔 precomp.h.gch
。剩下的產生共用對象的過程與本文前面所描述的相同。
注意: g++
版本 3.4 及更高的版本提供了對先行編譯標頭檔的支援。
結束語
在兩個完全不同的系統之間(如 Windows 和 UNIX)進行移植,絕不是一項簡單的任務,並且它需要大量的調整工作和耐心。本文說明了將最基本的項目類型從 Visual Studio 環境移植到基於
g++
/Solaris 環境的基本要素。第二篇文章作為本系列文章的總結,將介紹 Visual Studio 環境及其 g++
等價物中各種可用的編譯器選項、g++
屬性機制、從 32 位(通常是指 Windows)環境移植到 64 位元(UNIX)環境時的一些問題,以及多線程等等。
共用本文……
|
|
請 Digg 這個故事 |
|
|
發布到 del.icio.u |
|
|
Slashdot 一下! |
|
參考資料
學習
- 您可以參閱本文在 developerWorks 全球網站上的
英文原文 。
- Microsoft Developer Network:這個網站提供了有關動態連結程式庫函數的文檔說明。
- Making C++ Loadable Modules Work:Frank Pilhofer 詳細地介紹了有關這個主題的內容。
- Building And Using Static And Shared 'C' Libraries:在 Little Unix Programmers Group (LUPG)'s Little 網站中查看這個教程。
- KAI
C++
使用者指南:參考這個指南,以獲得有關先行編譯標頭檔的詳細的描述。
- GCC 聯機文檔:這個網站為 GCC/G++ 的最新發行版提供了參考手冊。
- AIX and UNIX 專區:developerWorks 的“AIX and UNIX 專區”提供了大量與 AIX 系統管理的所有方面相關的資訊,您可以利用它們來擴充自己的 UNIX 技能。
- AIX and UNIX 新手入門:訪問“AIX and UNIX 新手入門”頁面可瞭解更多關於 AIX 和 UNIX 的內容。
- AIX Wiki:AIX 相關技術資訊的協作環境。
引用:http://www.ibm.com/developerworks/cn/aix/library/au-porting/index.html