C++ 中 extern“C”含義

常在cpp的代碼之中看到這樣的代碼:

以下是引用片段:
  #ifdef __cplusplus
  extern "C" {
  #endif
  //一段代碼
  #ifdef __cplusplus
  }
  #endif

  這樣的代碼到底是什麼意思呢?首先,__cplusplus是cpp中的自定義宏,那麼定義了這個宏的話表示這是一段cpp的代碼,也就是說,上面的代碼的含義是:如果這是一段cpp的代碼,那麼加入extern "C"{和}處理其中的代碼。

  要明白為何使用extern "C",還得從cpp中對函數的重載處理開始說起。在c++中,為了支援重載機制,在編譯生成的彙編碼中,要對函數的名字進行一些處理,加入比如函數的返回類型等等.而在C中,只是簡單的函數名字而已,不會加入其他的信息.也就是說:C++和C對產生的函數名字的處理是不一樣的.

  比如下面的一段簡單的函數,我們看看加入和不加入extern "C"產生的彙編代碼都有哪些變化:

以下是引用片段:
  int f(void)
  {
  return 1;
  }

  在加入extern "C"的時候產生的彙編代碼是:

以下是引用片段:
  .file "test.cxx"
  .text
  .align 2
  .globl _f
  .def _f; .scl 2; .type 32; .endef
  _f:
  pushl %ebp
  movl %esp, %ebp
  movl $1, %eax
  popl %ebp
  ret

  但是不加入了extern "C"之後

以下是引用片段:
  .file "test.cxx"
  .text
  .align 2
  .globl __Z1fv
  .def __Z1fv; .scl 2; .type 32; .endef
  __Z1fv:
  pushl %ebp
  movl %esp, %ebp
  movl $1, %eax
  popl %ebp
  ret

  兩段彙編代碼同樣都是使用gcc -S命令產生的,所有的地方都是一樣的,唯獨是產生的函數名,一個是_f,一個是__Z1fv。

  明白了加入與不加入extern "C"之後對函數名稱產生的影響,我們繼續我們的討論:為什麼需要使用extern "C"呢?C++之父在設計C++之時,考慮到當時已經存在了大量的C代碼,為了支援原來的C代碼和已經寫好C庫,需要在C++中盡可能的支援C,而 extern "C"就是其中的一個策略。

  試想這樣的情況:一個庫文件已經用C寫好了而且運行得很良好,這個時候我們需要使用這個庫文件,但是我們需要使用C++來寫這個新的代碼。如果這個代碼使用的是C++的方式鏈結這個C庫文件的話,那麼就會出現鏈結錯誤.我們來看一段代碼:首先,我們使用C的處理方式來寫一個函數,也就是說假設這個函數當時是用C寫成的:

以下是引用片段:
  //f1.c
  extern "C"
  {
  void f1()
  {
  return;
  }
  }

  編譯命令是:gcc -c f1.c -o f1.o 產生了一個叫f1.o的庫文件。再寫一段代碼調用這個f1函數:

以下是引用片段:
  // test.cxx
  //這個extern表示f1函數在別的地方定義,這樣可以通過
  //編譯,但是鏈結的時候還是需要
  //鏈結上原來的庫文件.
  extern void f1();
  int main()
  {
  f1();
  return 0;
  }

  通過gcc -c test.cxx -o test.o 產生一個叫test.o的文件。然後,我們使用gcc test.o f1.o來鏈結兩個文件,可是出錯了,錯誤的提示是:

以下是引用片段:
  test.o(.text + 0x1f):test.cxx: undefine reference to 'f1()'

  也就是說,在編譯test.cxx的時候編譯器是使用C++的方式來處理f1()函數的,但是實際上鏈結的庫文件卻是用C的方式來處理函數的,所以就會出現鏈結過不去的錯誤:因為鏈結器找不到函數。

  因此,為了在C++代碼中調用用C寫成的庫文件,就需要用extern "C"來告訴編譯器:這是一個用C寫成的庫文件,請用C的方式來鏈結它們。

  比如,現在我們有了一個C庫文件,它的頭文件是f.h,產生的lib文件是f.lib,那麼我們如果要在C++中使用這個庫文件,我們需要這樣寫:

以下是引用片段:
  extern "C"
  {
  #include "f.h"
  }

  回到上面的問題,如果要改正鏈結錯誤,我們需要這樣子改寫test.cxx:

以下是引用片段:
  extern "C"
  {
  extern void f1();
  }
  int main()
  {
  f1();
  return 0;
  }

  重新編譯並且鏈結就可以過去了.

  總結

  C和C++對函數的處理方式是不同的.extern "C"是使C++能夠調用C寫作的庫文件的一個手段,如果要對編譯器提示使用C的方式來處理函數的話,那麼就要使用extern "C"來說明。
動態鏈接庫*.so的編譯與使用- -


動態庫*.so在linux下用c和c++編程時經常會碰到,最近在網站找了幾篇文章介紹動態庫的編譯和鏈接,總算搞懂了這個之前一直不太瞭解得東東,這裡做個筆記,也為其它正為動態庫鏈接庫而苦惱的兄弟們提供一點幫助。
1、動態庫的編譯

下面通過一個例子來介紹如何生成一個動態庫。這裡有一個頭文件:so_test.h,三個.c文件:test_a.c、test_b.c、test_c.c,我們將這幾個文件編譯成一個動態庫:libtest.so。

so_test.h:

#include
#include

void test_a();
void test_b();
void test_c();


test_a.c:

#include "so_test.h"
void test_a()
{
printf("this is in test_a...\n");
}



test_b.c:

#include "so_test.h"
void test_b()
{
printf("this is in test_b...\n");
}


test_c.c:

#include "so_test.h"
void test_c()
{
printf("this is in test_c...\n");
}


將這幾個文件編譯成一個動態庫:libtest.so

$ gcc test_a.c test_b.c test_c.c -fPIC -shared -o libtest.so


2、動態庫的鏈接

在1、中,我們已經成功生成了一個自己的動態鏈接庫libtest.so,下面我們通過一個程序來調用這個庫裡的函數。程序的源文件為:test.c。

test.c:

#include "so_test.h"
int main()
{
test_a();
test_b();
test_c();
return 0;
}

將test.c與動態庫libtest.so鏈接生成執行文件test:

$ gcc test.c -L. -ltest -o test

測試是否動態連接,如果列出libtest.so,那麼應該是連接正常了

$ ldd test

執行test,可以看到它是如何調用動態庫中的函數的。

3、編譯參數解析
最主要的是GCC命令行的一個選項:

-shared 該選項指定生成動態連接庫(讓連接器生成T類型的導出符號表,有時候也生成弱連接W類型的導出符號),不用該標誌外部程序無法連接。相當於一個可執行文件

-fPIC:表示編譯為位置獨立的代碼,不用此選項的話編譯後的代碼是位置相關的所以動態載入時是通過代碼拷貝的方式來滿足不同進程的需要,而不能達到真正代碼段共享的目的。

-L.:表示要連接的庫在當前目錄中

-ltest:編譯器查找動態連接庫時有隱含的命名規則,即在給出的名字前面加上lib,後面加上.so來確定庫的名稱

LD_LIBRARY_PATH:這個環境變量指示動態連接器可以裝載動態庫的路徑。

當然如果有root權限的話,可以修改/etc/ld.so.conf文件,然後調用 /sbin/ldconfig來達到同樣的目的,不過如果沒有root權限,那麼只能採用輸出LD_LIBRARY_PATH的方法了。

4、注意

調用動態庫的時候有幾個問題會經常碰到,有時,明明已經將庫的頭文件所在目錄 通過 「-I」 include進來了,庫所在文件通過「-L」參數引導,並指定了「-l」的庫名,但通過ldd命令察看時,就是死活找不到你指定鏈接的so文件,這時你要作的就是通過修改 LD_LIBRARY_PATH或者/etc/ld.so.conf文件來指定動態庫的目錄。通常這樣做就可以解決庫無法鏈接的問題了。
Library可分成三種,static、shared與dynamically loaded。

1. Static libraries

Static 程式庫用於靜態連結,簡單講是把一堆object檔用ar(archiver)
包裝集合起來,檔名以 `.a' 結尾。優點是執行效能通常會比後兩者快,
而且因為是靜態連結,所以不易發生執行時找不到library或版本錯置而
無法執行的問題。缺點則是檔案較大,維護度較低;例如library如果發
現bug需要更新,那麼就必須重新連結執行檔。

1.1 編譯

編譯方式很簡單,先例用 `-c' 編出 object 檔,再用 ar 包起來即可。

____ hello.c ____
#include
void hello(){ printf("Hello "); }

____ world.c ____
#include
void world(){ printf("world."); }

____ mylib.h ____
void hello();
void world();

$ gcc -c hello.c world.c /* 編出 hello.o 與 world.o */
$ ar rcs libmylib.a hello.o world.o /* 包成 limylib.a */

這樣就可以建出一個檔名為 libmylib.a 的檔。輸出的檔名其實沒有硬性規定,
但如果想要配合 gcc 的 '-l' 參數來連結,一定要以 `lib' 開頭,中間是你要
的library名稱,然後緊接著 `.a' 結尾。

1.2 使用

____ main.c ____
#include "mylib.h"
int main() {
hello();
world();
}

使用上就像與一般的 object 檔連結沒有差別。

$ gcc main.c libmylib.a

也可以配合 gcc 的 `-l' 參數使用

$ gcc main.c -L. -lmylib

`-Ldir' 參數用來指定要搜尋程式庫的目錄,`.' 表示搜尋現在所在的目錄。
通常預設會搜 /usr/lib 或 /lib 等目錄。
`-llibrary' 參數用來指定要連結的程式庫 ,'mylib' 表示要與mylib進行連結
,他會搜尋library名稱前加`lib'後接`.a'的檔案來連結。

$ ./a.out
Hello world.


2. Shared libraries

Shared library 會在程式執行起始時才被自動載入。因為程式庫與執行檔
是分離的,所以維護彈性較好。有兩點要注意,shared library是在程式起始
時就要被載入,而不是執行中用到才載入,而且在連結階段需要有該程式庫
才能進行連結。

首先有一些名詞要弄懂,soname、real name與linker name。

soname 用來表示是一個特定 library 的名稱,像是 libmylib.so.1 。
前面以 `lib' 開頭,接著是該 library 的名稱,然後是 `.so' ,接著
是版號,用來表名他的介面;如果介面改變時,就會增加版號來維護相容度。

real name 是實際放有library程式的檔案名稱,後面會再加上 minor 版號與
release 版號,像是 libmylib.so.1.0.0 。

一般來說,版號的改變規則是(印象中在 APress-Difinitive Guide to GCC中有
提到,但目前手邊沒這本書),最尾碼的release版號用於程式內容的修正,
介面完全沒有改變。中間的minor用於有新增加介面,但相舊介面沒改變,所以
與舊版本相容。最前面的version版號用於原介面有移除或改變,與舊版不相容
時。

linker name是用於連結時的名稱,是不含版號的 soname ,如: libmylib.so。
通常 linker name與 real name是用 ln 指到對應的 real name ,用來提供
彈性與維護性。

2.1 編譯
shared library的製作過程較複雜。

$ gcc -c -fPIC hello.c world.c

編譯時要加上 -fPIC 用來產生 position-independent code。也可以用 -fpic
參數。 (不太清楚差異,只知道 -fPIC 較通用於不同平台,但產生的code較大
,而且編譯速度較慢)。

$ gcc -shared -Wl,-soname,libmylib.so.1 -o libmylib.so.1.0.0 \
hello.o world.o

-shared 表示要編譯成 shared library
-Wl 用於參遞參數給linker,因此-soname與libmylib.so.1會被傳給linker處理。
-soname用來指名 soname 為 limylib.so.1
library會被輸出成libmylib.so.1.0.0 (也就是real name)

若不指定 soname 的話,在編譯結連後的執行檔會以連時的library檔名為
soname,並載入他。否則是載入soname指定的library檔案。

可以利用 objdump 來看 library 的 soname。

$ objdump -p libmylib.so | grep SONAME
SONAME libmylib.so.1

若不指名-soname參數的話,則library不會有這個欄位資料。

在編譯後再用 ln 來建立 soname 與 linker name 兩個檔案。
$ ln -s libmylib.so.1.0.0 libmylib.so
$ ln -s libmylib.so.1.0.0 libmylib.so.1


2.2 使用

與使用 static library 同。

$ gcc main.c libmylib.so

以上直接指定與 libmylib.so 連結。

或用

$ gcc main.c -L. -lmylib

linker會搜尋 libmylib.so 來進行連結。

如果目錄下同時有static與shared library的話,會以shared為主。
使用 -static 參數可以避免使用shared連結。

$ gcc main.c -static -L. -lmylib

此時可以用 ldd 看編譯出的執行檔與shared程式庫的相依性
$ldd a.out
linux-gate.so.1 => (0xffffe000)
libmylib.so.1 => not found
libc.so.6 => /lib/libc.so.6 (0xb7dd6000)
/lib/ld-linux.so.2 (0xb7f07000)
輸出結果顯示出該執行檔需要 libmylib.so.1 這個shared library。
會顯示 not found 因為沒指定該library所在的目錄,所找不到該library。

因為編譯時有指定-soname參數為 libmylib.so.1 的關係,所以該執行檔會
載入libmylib.so.1。否則以libmylib.so連結,執行檔則會變成要求載入
libmylib.so

$ ./a.out
./a.out: error while loading shared libraries: libmylib.so.1:
cannot open shared object file: No such file or directory

因為找不到 libmylib.so.1 所以無法執行程式。
有幾個方式可以處理。

a. 把 libmylib.so.1 安裝到系統的library目錄,如/usr/lib下
b. 設定 /etc/ld.so.conf ,加入一個新的library搜尋目錄,並執行ldconfig
更新快取
c. 設定 LD_LIBRARY_PATH 環境變數來搜尋library
這個例子是加入目前的目錄來搜尋要載作的library
$ LD_LIBRARY_PATH=. ./a.out
Hello world.

3. Dynamically loaded libraries

Dynamicaaly loaded libraries 才是像 windows 所用的 DLL ,在使用到
時才載入,編譯連結時不需要相關的library。動態載入庫常被用於像plug-ins
的應用。

3.1 使用方式
動態載入是透過一套 dl function來處理。
#include
void *dlopen(const char *filename, int flag);
開啟載入 filename 指定的 library。
void *dlsym(void *handle, const char *symbol);
取得 symbol 指定的symbol name在library被載入的記憶體位址。
int dlclose(void *handle);
關閉dlopen開啟的handle。
char *dlerror(void);
傳回最近所發生的錯誤訊息。

____ dltest.c ____
#include
#include
#include
int main() {
void *handle;
void (*f)();
char *error;

/* 開啟之前所撰寫的 libmylib.so 程式庫 */
handle = dlopen("./libmylib.so", RTLD_LAZY);
if( !handle ) {
fputs( dlerror(), stderr);
exit(1);
}

/* 取得 hello function 的 address */
f = dlsym(handle, "hello");
if(( error=dlerror())!=NULL) {
fputs(error, stderr);
exit(1);
}
/* 呼叫該 function */
f();
dlclose(handle);
}

編譯時要加上 -ldl 參數來與 dl library 連結
$ gcc dltest.c -ldl
結果會印出 Hello 字串
$ ./a.out
Hello

關於dl的詳細內容請參閱 man dlopen

--
參考資料:

Creating a shared and static library with the gnu compiler [gcc]
http://www.adp-gmbh.ch/cpp/gcc/create_lib.html

Program Library HOWTO
http://tldp.org/HOWTO/Program-Library-HOWTO/index.html

APress - Definitive Guide to GCC

------------------------------------------------------------------------------------------------------------------

Makefile



GCC=gcc
CFLAGS=-Wall -ggdb -fPIC
#CFLAGS=

all: libfunc

libfunc:func.o func1.o
$(GCC) -shared -Wl,-soname,libfunc.so.1 -o libfunc.so.1.1 $<
ln -sf libfunc.so.1.1 libfunc.so.1
ln -sf libfunc.so.1 libfunc.so

%.o:%.c
$(GCC) -c $(CFLAGS) -o $@ $<

clean:
rm -fr *.o
rm -fr *.so*

-------------------------------------------------------------------------------------------------------------------

Blogger Templates by Blog Forum