Go 語言 靜態(tài)庫和動(dòng)態(tài)庫

2023-03-22 15:01 更新

原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-09-static-shared-lib.html


2.9 靜態(tài)庫和動(dòng)態(tài)庫

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

2.9.1 使用 C 靜態(tài)庫

如果 CGO 中引入的 C/C++ 資源有代碼而且代碼規(guī)模也比較小,直接使用源碼是最理想的方式,但很多時(shí)候我們并沒有源代碼,或者從 C/C++ 源代碼開始構(gòu)建的過程異常復(fù)雜,這種時(shí)候使用 C 靜態(tài)庫也是一個(gè)不錯(cuò)的選擇。靜態(tài)庫因?yàn)槭庆o態(tài)鏈接,最終的目標(biāo)程序并不會(huì)產(chǎn)生額外的運(yùn)行時(shí)依賴,也不會(huì)出現(xiàn)動(dòng)態(tài)庫特有的跨運(yùn)行時(shí)資源管理的錯(cuò)誤。不過靜態(tài)庫對(duì)鏈接階段會(huì)有一定要求:靜態(tài)庫一般包含了全部的代碼,里面會(huì)有大量的符號(hào),如果不同靜態(tài)庫之間出現(xiàn)了符號(hào)沖突則會(huì)導(dǎo)致鏈接的失敗。

我們先用純 C 語言構(gòu)造一個(gè)簡單的靜態(tài)庫。我們要構(gòu)造的靜態(tài)庫名叫 number,庫中只有一個(gè) number_add_mod 函數(shù),用于表示數(shù)論中的模加法運(yùn)算。number 庫的文件都在 number 目錄下。

number/number.h 頭文件只有一個(gè)純 C 語言風(fēng)格的函數(shù)聲明:

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

number/number.c 對(duì)應(yīng)函數(shù)的實(shí)現(xiàn):

#include "number.h"

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

因?yàn)?CGO 使用的是 GCC 命令來編譯和鏈接 C 和 Go 橋接的代碼。因此靜態(tài)庫也必須是 GCC 兼容的格式。

通過以下命令可以生成一個(gè)叫 libnumber.a 的靜態(tài)庫:

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

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

創(chuàng)建 main.go 文件如下:

package main

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

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

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

因?yàn)槲覀冇?number 庫的全部代碼,所以我們可以用 go generate 工具來生成靜態(tài)庫,或者是通過 Makefile 來構(gòu)建靜態(tài)庫。因此發(fā)布 CGO 源碼包時(shí),我們并不需要提前構(gòu)建 C 靜態(tài)庫。

因?yàn)槎嗔艘粋€(gè)靜態(tài)庫的構(gòu)建步驟,這種使用了自定義靜態(tài)庫并已經(jīng)包含了靜態(tài)庫全部代碼的 Go 包無法直接用 go get 安裝。不過我們依然可以通過 go get 下載,然后用 go generate 觸發(fā)靜態(tài)庫構(gòu)建,最后才是 go install 來完成安裝。

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

創(chuàng)建 z_link_number_c.c 文件如下:

#include "./number/number.c"

然后在執(zhí)行 go get 或 go build 之類命令的時(shí)候,CGO 就是自動(dòng)構(gòu)建 number 庫對(duì)應(yīng)的代碼。這種技術(shù)是在不改變靜態(tài)庫源代碼組織結(jié)構(gòu)的前提下,將靜態(tài)庫轉(zhuǎn)化為了源代碼方式引用。這種 CGO 包是最完美的。

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

在 Linux 環(huán)境,有一個(gè) pkg-config 命令可以查詢要使用某個(gè)靜態(tài)庫或動(dòng)態(tài)庫時(shí)的編譯和鏈接參數(shù)。我們可以在 #cgo 命令中直接使用 pkg-config 命令來生成編譯和鏈接參數(shù)。而且還可以通過 PKG_CONFIG 環(huán)境變量定制 pkg-config 命令。因?yàn)椴煌牟僮飨到y(tǒng)對(duì) pkg-config 命令的支持不盡相同,通過該方式很難兼容不同的操作系統(tǒng)下的構(gòu)建參數(shù)。不過對(duì)于 Linux 等特定的系統(tǒng),pkg-config 命令確實(shí)可以簡化構(gòu)建參數(shù)的管理。關(guān)于 pkg-config 的使用細(xì)節(jié)在此我們不深入展開,大家可以自行參考相關(guān)文檔。

2.9.2 使用 C 動(dòng)態(tài)庫

動(dòng)態(tài)庫出現(xiàn)的初衷是對(duì)于相同的庫,多個(gè)進(jìn)程可以共享同一個(gè),以節(jié)省內(nèi)存和磁盤資源。但是在磁盤和內(nèi)存已經(jīng)白菜價(jià)的今天,這兩個(gè)作用已經(jīng)顯得微不足道了,那么除此之外動(dòng)態(tài)庫還有哪些存在的價(jià)值呢?從庫開發(fā)角度來說,動(dòng)態(tài)庫可以隔離不同動(dòng)態(tài)庫之間的關(guān)系,減少鏈接時(shí)出現(xiàn)符號(hào)沖突的風(fēng)險(xiǎn)。而且對(duì)于 windows 等平臺(tái),動(dòng)態(tài)庫是跨越 VC 和 GCC 不同編譯器平臺(tái)的唯一的可行方式。

對(duì)于 CGO 來說,使用動(dòng)態(tài)庫和靜態(tài)庫是一樣的,因?yàn)閯?dòng)態(tài)庫也必須要有一個(gè)小的靜態(tài)導(dǎo)出庫用于鏈接動(dòng)態(tài)庫(Linux 下可以直接鏈接 so 文件,但是在 Windows 下必須為 dll 創(chuàng)建一個(gè) .a 文件用于鏈接)。我們還是以前面的 number 庫為例來說明如何以動(dòng)態(tài)庫方式使用。

對(duì)于在 macOS 和 Linux 系統(tǒng)下的 gcc 環(huán)境,我們可以用以下命令創(chuàng)建 number 庫的的動(dòng)態(tài)庫:

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

因?yàn)閯?dòng)態(tài)庫和靜態(tài)庫的基礎(chǔ)名稱都是 libnumber,只是后綴名不同而已。因此 Go 語言部分的代碼和靜態(tài)庫版本完全一樣:

package main

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

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

編譯時(shí) GCC 會(huì)自動(dòng)找到 libnumber.a 或 libnumber.so 進(jìn)行鏈接。

對(duì)于 windows 平臺(tái),我們還可以用 VC 工具來生成動(dòng)態(tài)庫(windows 下有一些復(fù)雜的 C++ 庫只能用 VC 構(gòu)建)。我們需要先為 number.dll 創(chuàng)建一個(gè) def 文件,用于控制要導(dǎo)出到動(dòng)態(tài)庫的符號(hào)。

number.def 文件的內(nèi)容如下:

LIBRARY number.dll

EXPORTS
number_add_mod

其中第一行的 LIBRARY 指明動(dòng)態(tài)庫的文件名,然后的 EXPORTS 語句之后是要導(dǎo)出的符號(hào)名列表。

現(xiàn)在我們可以用以下命令來創(chuàng)建動(dòng)態(tài)庫(需要進(jìn)入 VC 對(duì)應(yīng)的 x64 命令行環(huán)境)。

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

這時(shí)候會(huì)為 dll 同時(shí)生成一個(gè) number.lib 的導(dǎo)出庫。但是在 CGO 中我們無法使用 lib 格式的鏈接庫。

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

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

生成了 libnumber.a 文件之后,就可以通過 -lnumber 鏈接參數(shù)進(jìn)行鏈接了。

需要注意的是,在運(yùn)行時(shí)需要將動(dòng)態(tài)庫放到系統(tǒng)能夠找到的位置。對(duì)于 windows 來說,可以將動(dòng)態(tài)庫和可執(zhí)行程序放到同一個(gè)目錄,或者將動(dòng)態(tài)庫所在的目錄絕對(duì)路徑添加到 PATH 環(huán)境變量中。對(duì)于 macOS 來說,需要設(shè)置 DYLD_LIBRARY_PATH 環(huán)境變量。而對(duì)于 Linux 系統(tǒng)來說,需要設(shè)置 LD_LIBRARY_PATH 環(huán)境變量。

2.9.3 導(dǎo)出 C 靜態(tài)庫

CGO 不僅可以使用 C 靜態(tài)庫,也可以將 Go 實(shí)現(xiàn)的函數(shù)導(dǎo)出為 C 靜態(tài)庫。我們現(xiàn)在用 Go 實(shí)現(xiàn)前面的 number 庫的模加法函數(shù)。

創(chuàng)建 number.go,內(nèi)容如下:

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
}

根據(jù) CGO 文檔的要求,我們需要在 main 包中導(dǎo)出 C 函數(shù)。對(duì)于 C 靜態(tài)庫構(gòu)建方式來說,會(huì)忽略 main 包中的 main 函數(shù),只是簡單導(dǎo)出 C 函數(shù)。采用以下命令構(gòu)建:

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

在生成 number.a 靜態(tài)庫的同時(shí),cgo 還會(huì)生成一個(gè) number.h 文件。

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

#ifdef __cplusplus
extern "C" {
#endif

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

#ifdef __cplusplus
}
#endif

其中 extern "C" 部分的語法是為了同時(shí)適配 C 和 C++ 兩種語言。核心內(nèi)容是聲明了要導(dǎo)出的 number_add_mod 函數(shù)。

然后我們創(chuàng)建一個(gè) _test_main.c 的 C 文件用于測試生成的 C 靜態(tài)庫(用下劃線作為前綴名是讓為了讓 go build 構(gòu)建 C 靜態(tài)庫時(shí)忽略這個(gè)文件):

#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;
}

通過以下命令編譯并運(yùn)行:

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

使用 CGO 創(chuàng)建靜態(tài)庫的過程非常簡單。

2.9.4 導(dǎo)出 C 動(dòng)態(tài)庫

CGO 導(dǎo)出動(dòng)態(tài)庫的過程和靜態(tài)庫類似,只是將構(gòu)建模式改為 c-shared,輸出文件名改為 number.so 而已:

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

_test_main.c 文件內(nèi)容不變,然后用以下命令編譯并運(yùn)行:

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

2.9.5 導(dǎo)出非 main 包的函數(shù)

通過 go help buildmode 命令可以查看 C 靜態(tài)庫和 C 動(dòng)態(tài)庫的構(gòu)建說明:

-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.

文檔說明導(dǎo)出的 C 函數(shù)必須是在 main 包導(dǎo)出,然后才能在生成的頭文件包含聲明的語句。但是很多時(shí)候我們可能更希望將不同類型的導(dǎo)出函數(shù)組織到不同的 Go 包中,然后統(tǒng)一導(dǎo)出為一個(gè)靜態(tài)庫或動(dòng)態(tài)庫。

要實(shí)現(xiàn)從是從非 main 包導(dǎo)出 C 函數(shù),或者是多個(gè)包導(dǎo)出 C 函數(shù)(因?yàn)橹荒苡幸粋€(gè) main 包),我們需要自己提供導(dǎo)出 C 函數(shù)對(duì)應(yīng)的頭文件(因?yàn)?CGO 無法為非 main 包的導(dǎo)出函數(shù)生成頭文件)。

假設(shè)我們先創(chuàng)建一個(gè) number 子包,用于提供模加法函數(shù):

package number

import "C"

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

然后是當(dāng)前的 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))
}

其中我們導(dǎo)入了 number 子包,在 number 子包中有導(dǎo)出的 C 函數(shù) number_add_mod,同時(shí)我們?cè)?main 包也導(dǎo)出了 goPrintln 函數(shù)。

通過以下命令創(chuàng)建 C 靜態(tài)庫:

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

這時(shí)候在生成 main.a 靜態(tài)庫的同時(shí),也會(huì)生成一個(gè) main.h 頭文件。但是 main.h 頭文件中只有 main 包中導(dǎo)出的 goPrintln 函數(shù)的聲明,并沒有 number 子包導(dǎo)出函數(shù)的聲明。其實(shí) number_add_mod 函數(shù)在生成的 C 靜態(tài)庫中是存在的,我們可以直接使用。

創(chuàng)建 _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 自動(dòng)生成的 main.h 頭文件,而是通過手工方式聲明了 goPrintln 和 number_add_mod 兩個(gè)導(dǎo)出函數(shù)。這樣我們就實(shí)現(xiàn)了從多個(gè) Go 包導(dǎo)出 C 函數(shù)了。



以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)