CGO靜態庫和動態庫


CGO靜態庫和動態庫

CGO在使用C/C++資源的時候一般有三種形式:直接使用源碼鏈接靜態庫鏈接動態庫。直接使用源碼就是在import "C"之前的注釋部分包含C代碼,或者在當前包中包含C/C++源文件。鏈接靜態庫和動態庫的方式比較類似,都是通過在LDFLAGS選項指定要鏈接的庫方式鏈接。本節我們主要關注在CGO中如何使用靜態庫和動態庫相關的問題。

使用C靜態庫

如果CGO中引入的C/C++資源有代碼而且代碼規模也比較小,直接使用源碼是最理想的方式,但很多時候我們並沒有源代碼,或者從C/C++源代碼開始構建的過程異常復雜,這種時候使用C靜態庫也是一個不錯的選擇。靜態庫因為是靜態鏈接,最終的目標程序並不會產生額外的運行時依賴,也不會出現動態庫特有的跨運行時資源管理的錯誤。不過靜態庫對鏈接階段會有一定要求:靜態庫一般包含了全部的代碼,里面會有大量的符號,如果不同靜態庫之間出現了符號沖突則會導致鏈接的失敗

我們先用純C語言構造一個簡單的靜態庫。我們要構造的靜態庫名叫number,庫中只有一個number_add_mod函數,用於表示數論中的模加法運算。number庫的文件都在number目錄下。

number/number.h頭文件只有一個純C語言風格的函數聲明:

int number_add_mod(int a, int b, int mod);

number/number.c對應函數的實現:

#include "number.h"

int number_add_mod(int a, int b, int mod) {
    return (a+b)%mod;
}

因為CGO使用的是GCC命令來編譯和鏈接C和Go橋接的代碼。因此靜態庫也必須是GCC兼容的格式。

通過以下命令可以生成一個叫libnumber.a的靜態庫:

$ cd ./number
$ gcc -c -o number.o number.c
$ ar rcs libnumber.a number.o

生成libnumber.a靜態庫之后,我們就可以在CGO中使用該資源了。

創建main.go文件如下:

package main

//#cgo CFLAGS: -I./number
//#cgo LDFLAGS: -L${SRCDIR} -lnumber
//
//#include "number.h"
import "C"
import "fmt"

func main() {
    fmt.Println(C.number_add_mod(10, 5, 12))
}

其中有兩個#cgo命令,分別是編譯和鏈接參數。CFLAGS通過-I./number將number庫對應頭文件所在的目錄加入頭文件檢索路徑LDFLAGS通過-L${SRCDIR}/number將編譯后number靜態庫所在目錄加為鏈接庫檢索路徑,-lnumber表示鏈接libnumber.a靜態庫。需要注意的是,在鏈接部分的檢索路徑不能使用相對路徑(C/C++代碼的鏈接程序所限制),我們必須通過cgo特有的${SRCDIR}變量將源文件對應的當前目錄路徑展開為絕對路徑(因此在windows平台中絕對路徑不能有空白符號)。

因為我們有number庫的全部代碼,所以我們可以用go generate工具來生成靜態庫,或者是通過Makefile來構建靜態庫。因此發布CGO源碼包時,我們並不需要提前構建C靜態庫。

因為多了一個靜態庫的構建步驟,這種使用了自定義靜態庫並已經包含了靜態庫全部代碼的Go包無法直接用go get安裝。不過我們依然可以通過go get下載,然后用go generate觸發靜態庫構建,最后才是go install來完成安裝。

為了支持go get命令直接下載並安裝,我們C語言的#include語法可以將number庫的源文件鏈接到當前的包。

#include "./number/number.c"

然后在執行go getgo build之類命令的時候,CGO就是自動構建number庫對應的代碼。這種技術是在不改變靜態庫源代碼組織結構的前提下,將靜態庫轉化為了源代碼方式引用。這種CGO包是最完美的。

如果使用的是第三方的靜態庫,我們需要先下載安裝靜態庫到合適的位置。然后在#cgo命令中通過CFLAGS和LDFLAGS來指定頭文件和庫的位置。對於不同的操作系統甚至同一種操作系統的不同版本來說,這些庫的安裝路徑可能都是不同的,那么如何在代碼中指定這些可能變化的參數呢?

在Linux環境,有一個pkg-config命令可以查詢要使用某個靜態庫或動態庫時的編譯和鏈接參數。我們可以在#cgo命令中直接使用pkg-config命令來生成編譯和鏈接參數。而且還可以通過PKG_CONFIG環境變量定制pkg-config命令。因為不同的操作系統對pkg-config命令的支持不盡相同,通過該方式很難兼容不同的操作系統下的構建參數。不過對於Linux等特定的系統,pkg-config命令確實可以簡化構建參數的管理。關於pkg-config的使用細節在此我們不深入展開,大家可以自行參考相關文檔。

使用C動態庫

動態庫出現的初衷是對於相同的庫,多個進程可以共享同一個,以節省內存和磁盤資源。但是在磁盤和內存已經白菜價的今天,這兩個作用已經顯得微不足道了,那么除此之外動態庫還有哪些存在的價值呢?從庫開發角度來說,動態庫可以隔離不同動態庫之間的關系,減少鏈接時出現符號沖突的風險。而且對於windows等平台,動態庫是跨越VC和GCC不同編譯器平台的唯一的可行方式。

對於CGO來說,使用動態庫和靜態庫是一樣的,因為動態庫也必須要有一個小的靜態導出庫用於鏈接動態庫(Linux下可以直接鏈接so文件,但是在Windows下必須為dll創建一個.a文件用於鏈接)。我們還是以前面的number庫為例來說明如何以動態庫方式使用。

對於在macOS和Linux系統下的gcc環境,我們可以用以下命令創建number庫的的動態庫:

$ cd number
$ gcc -shared -o libnumber.so number.c

因為動態庫和靜態庫的基礎名稱都是libnumber,只是后綴名不同而已。因此Go語言部分的代碼和靜態庫版本完全一樣:

package main

//#cgo CFLAGS: -I./number
//#cgo LDFLAGS: -L${SRCDIR} -lnumber
//
//#include "number.h"
import "C"
import "fmt"

func main() {
    fmt.Println(C.number_add_mod(10, 5, 12))
}

編譯時GCC會自動找到libnumber.a或libnumber.so進行鏈接。

對於windows平台,我們還可以用VC工具來生成動態庫(windows下有一些復雜的C++庫只能用VC構建)。我們需要先為number.dll創建一個def文件,用於控制要導出到動態庫的符號。

number.def文件的內容如下:

LIBRARY number.dll

EXPORTS
number_add_mod

其中第一行的LIBRARY指明動態庫的文件名,然后的EXPORTS語句之后是要導出的符號名列表

現在我們可以用以下命令來創建動態庫(需要進入VC對應的x64命令行環境)。

$ cl /c number.c
$ link /DLL /OUT:number.dll number.obj number.def

這時候會為dll同時生成一個number.lib的導出庫。但是在CGO中我們無法使用lib格式的鏈接庫。

要生成.a格式的導出庫需要通過mingw工具箱中的dlltool命令完成:

$ dlltool -dllname number.dll --def number.def --output-lib libnumber.a

生成了libnumber.a文件之后,就可以通過-lnumber鏈接參數進行鏈接了。

需要注意的是,在運行時需要將動態庫放到系統能夠找到的位置。對於windows來說,可以將動態庫和可執行程序放到同一個目錄,或者將動態庫所在的目錄絕對路徑添加到PATH環境變量中。對於macOS來說,需要設置DYLD_LIBRARY_PATH環境變量。而對於Linux系統來說,需要設置LD_LIBRARY_PATH環境變量。

導出C靜態庫

CGO不僅可以使用C靜態庫,也可以將Go實現的函數導出為C靜態庫。我們現在用Go實現前面的number庫的模加法函數。

package main

import "C"

func main() {}

//export number_add_mod
func number_add_mod(a, b, mod C.int) C.int {
    return (a + b) % mod
}

根據CGO文檔的要求,我們需要在main包中導出C函數。對於C靜態庫構建方式來說,會忽略main包中的main函數,只是簡單導出C函數。采用以下命令構建:

$ go build -buildmode=c-archive -o number.a

在生成number.a靜態庫的同時,cgo還會生成一個number.h文件。

number.h文件的內容如下(為了便於顯示,內容做了精簡):

#ifdef __cplusplus
extern "C" {
#endif

extern int number_add_mod(int p0, int p1, int p2);

#ifdef __cplusplus
}
#endif

其中extern "C"部分的語法是為了同時適配C和C++兩種語言。核心內容是聲明了要導出的number_add_mod函數。

然后我們創建一個_test_main.c的C文件用於測試生成的C靜態庫(用下划線作為前綴名是讓為了讓go build構建C靜態庫時忽略這個文件):

#include "number.h"

#include <stdio.h>

int main() {
    int a = 10;
    int b = 5;
    int c = 12;

    int x = number_add_mod(a, b, c);
    printf("(%d+%d)%%%d = %d\n", a, b, c, x);

    return 0;
}

通過以下命令編譯並運行:

$ gcc -o a.out _test_main.c number.a
$ ./a.out

使用CGO創建靜態庫的過程非常簡單。

導出C動態庫

CGO導出動態庫的過程和靜態庫類似,只是將構建模式改為c-shared,輸出文件名改為number.so而已:

$ go build -buildmode=c-shared -o number.so

_test_main.c文件內容不變,然后用以下命令編譯並運行:

$ gcc -o a.out _test_main.c number.so
$ ./a.out

導出非main包的函數

通過go help buildmode命令可以查看C靜態庫和C動態庫的構建說明:

-buildmode=c-archive
    Build the listed main package, plus all packages it imports,
    into a C archive file. The only callable symbols will be those
    functions exported using a cgo //export comment. Requires
    exactly one main package to be listed.

-buildmode=c-shared
    Build the listed main package, plus all packages it imports,
    into a C shared library. The only callable symbols will
    be those functions exported using a cgo //export comment.
    Requires exactly one main package to be listed.

文檔說明導出的C函數必須是在main包導出,然后才能在生成的頭文件包含聲明的語句。但是很多時候我們可能更希望將不同類型的導出函數組織到不同的Go包中,然后統一導出為一個靜態庫或動態庫。

要實現從是從非main包導出C函數,或者是多個包導出C函數(因為只能有一個main包),我們需要自己提供導出C函數對應的頭文件(因為CGO無法為非main包的導出函數生成頭文件)。

假設我們先創建一個number子包,用於提供模加法函數:

package number

import "C"

//export number_add_mod
func number_add_mod(a, b, mod C.int) C.int {
    return (a + b) % mod
}

然后是當前的main包:

package main

import "C"

import (
    "fmt"

    _ "./number"
)

func main() {
    println("Done")
}

//export goPrintln
func goPrintln(s *C.char) {
    fmt.Println("goPrintln:", C.GoString(s))
}

其中我們導入了number子包,在number子包中有導出的C函數number_add_mod,同時我們在main包也導出了goPrintln函數。

通過以下命令創建C靜態庫:

$ go build -buildmode=c-archive -o main.a

這時候在生成main.a靜態庫的同時,也會生成一個main.h頭文件。但是main.h頭文件中只有main包中導出的goPrintln函數的聲明,並沒有number子包導出函數的聲明。其實number_add_mod函數在生成的C靜態庫中是存在的,我們可以直接使用。

創建_test_main.c測試文件如下:

#include <stdio.h>

void goPrintln(char*);
int number_add_mod(int a, int b, int mod);

int main() {
    int a = 10;
    int b = 5;
    int c = 12;

    int x = number_add_mod(a, b, c);
    printf("(%d+%d)%%%d = %d\n", a, b, c, x);

    goPrintln("done");
    return 0;
}

我們並沒有包含CGO自動生成的main.h頭文件,而是通過手工方式聲明了goPrintln和number_add_mod兩個導出函數。這樣我們就實現了從多個Go包導出C函數了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM