Go 語言 CGO 基礎(chǔ)

2023-03-22 14:57 更新

原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-02-basic.html


2.2 CGO 基礎(chǔ)

要使用 CGO 特性,需要安裝 C/C++ 構(gòu)建工具鏈,在 macOS 和 Linux 下是要安裝 GCC,在 windows 下是需要安裝 MinGW 工具。同時(shí)需要保證環(huán)境變量 CGO_ENABLED 被設(shè)置為 1,這表示 CGO 是被啟用的狀態(tài)。在本地構(gòu)建時(shí) CGO_ENABLED 默認(rèn)是啟用的,當(dāng)交叉構(gòu)建時(shí) CGO 默認(rèn)是禁止的。比如要交叉構(gòu)建 ARM 環(huán)境運(yùn)行的 Go 程序,需要手工設(shè)置好 C/C++ 交叉構(gòu)建的工具鏈,同時(shí)開啟 CGO_ENABLED 環(huán)境變量。然后通過 import "C" 語句啟用 CGO 特性。

2.2.1 import "C" 語句

如果在 Go 代碼中出現(xiàn)了 import "C" 語句則表示使用了 CGO 特性,緊跟在這行語句前面的注釋是一種特殊語法,里面包含的是正常的 C 語言代碼。當(dāng)確保 CGO 啟用的情況下,還可以在當(dāng)前目錄中包含 C/C++ 對(duì)應(yīng)的源文件。

舉個(gè)最簡單的例子:

package main

/*
#include <stdio.h>

void printint(int v) {
    printf("printint: %d\n", v);
}
*/
import "C"

func main() {
    v := 42
    C.printint(C.int(v))
}

這個(gè)例子展示了 cgo 的基本使用方法。開頭的注釋中寫了要調(diào)用的 C 函數(shù)和相關(guān)的頭文件,頭文件被 include 之后里面的所有的 C 語言元素都會(huì)被加入到”C” 這個(gè)虛擬的包中。需要注意的是,import "C" 導(dǎo)入語句需要單獨(dú)一行,不能與其他包一同 import。向 C 函數(shù)傳遞參數(shù)也很簡單,就直接轉(zhuǎn)化成對(duì)應(yīng) C 語言類型傳遞就可以。如上例中 C.int(v) 用于將一個(gè) Go 中的 int 類型值強(qiáng)制類型轉(zhuǎn)換轉(zhuǎn)化為 C 語言中的 int 類型值,然后調(diào)用 C 語言定義的 printint 函數(shù)進(jìn)行打印。

需要注意的是,Go 是強(qiáng)類型語言,所以 cgo 中傳遞的參數(shù)類型必須與聲明的類型完全一致,而且傳遞前必須用”C” 中的轉(zhuǎn)化函數(shù)轉(zhuǎn)換成對(duì)應(yīng)的 C 類型,不能直接傳入 Go 中類型的變量。同時(shí)通過虛擬的 C 包導(dǎo)入的 C 語言符號(hào)并不需要是大寫字母開頭,它們不受 Go 語言的導(dǎo)出規(guī)則約束。

cgo 將當(dāng)前包引用的 C 語言符號(hào)都放到了虛擬的 C 包中,同時(shí)當(dāng)前包依賴的其它 Go 語言包內(nèi)部可能也通過 cgo 引入了相似的虛擬 C 包,但是不同的 Go 語言包引入的虛擬的 C 包之間的類型是不能通用的。這個(gè)約束對(duì)于要自己構(gòu)造一些 cgo 輔助函數(shù)時(shí)有可能會(huì)造成一點(diǎn)的影響。

比如我們希望在 Go 中定義一個(gè) C 語言字符指針對(duì)應(yīng)的 CChar 類型,然后增加一個(gè) GoString 方法返回 Go 語言字符串:

package cgo_helper

//#include <stdio.h>
import "C"

type CChar C.char

func (p *CChar) GoString() string {
    return C.GoString((*C.char)(p))
}

func PrintCString(cs *C.char) {
    C.puts(cs)
}

現(xiàn)在我們可能會(huì)想在其它的 Go 語言包中也使用這個(gè)輔助函數(shù):

package main

//static const char* cs = "hello";
import "C"
import "./cgo_helper"

func main() {
    cgo_helper.PrintCString(C.cs)
}

這段代碼是不能正常工作的,因?yàn)楫?dāng)前 main 包引入的 C.cs 變量的類型是當(dāng)前 main 包的 cgo 構(gòu)造的虛擬的 C 包下的 *char 類型(具體點(diǎn)是 *C.char,更具體點(diǎn)是 *main.C.char),它和 cgo_helper 包引入的 *C.char 類型(具體點(diǎn)是 *cgo_helper.C.char)是不同的。在 Go 語言中方法是依附于類型存在的,不同 Go 包中引入的虛擬的 C 包的類型卻是不同的(main.C 不等 cgo_helper.C),這導(dǎo)致從它們延伸出來的 Go 類型也是不同的類型(*main.C.char 不等 *cgo_helper.C.char),這最終導(dǎo)致了前面代碼不能正常工作。

有 Go 語言使用經(jīng)驗(yàn)的用戶可能會(huì)建議參數(shù)轉(zhuǎn)型后再傳入。但是這個(gè)方法似乎也是不可行的,因?yàn)?nbsp;cgo_helper.PrintCString 的參數(shù)是它自身包引入的 *C.char 類型,在外部是無法直接獲取這個(gè)類型的。換言之,一個(gè)包如果在公開的接口中直接使用了 *C.char 等類似的虛擬 C 包的類型,其它的 Go 包是無法直接使用這些類型的,除非這個(gè) Go 包同時(shí)也提供了 *C.char 類型的構(gòu)造函數(shù)。因?yàn)檫@些諸多因素,如果想在 go test 環(huán)境直接測試這些 cgo 導(dǎo)出的類型也會(huì)有相同的限制。

2.2.2 #cgo 語句

在 import "C" 語句前的注釋中可以通過 #cgo 語句設(shè)置編譯階段和鏈接階段的相關(guān)參數(shù)。編譯階段的參數(shù)主要用于定義相關(guān)宏和指定頭文件檢索路徑。鏈接階段的參數(shù)主要是指定庫文件檢索路徑和要鏈接的庫文件。

// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include <png.h>
import "C"

上面的代碼中,CFLAGS 部分,-D 部分定義了宏 PNG_DEBUG,值為 1;-I 定義了頭文件包含的檢索目錄。LDFLAGS 部分,-L 指定了鏈接時(shí)庫文件檢索目錄,-l 指定了鏈接時(shí)需要鏈接 png 庫。

因?yàn)?C/C++ 遺留的問題,C 頭文件檢索目錄可以是相對(duì)目錄,但是庫文件檢索目錄則需要絕對(duì)路徑。在庫文件的檢索目錄中可以通過 ${SRCDIR} 變量表示當(dāng)前包目錄的絕對(duì)路徑:

// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo

上面的代碼在鏈接時(shí)將被展開為:

// #cgo LDFLAGS: -L/go/src/foo/libs -lfoo

#cgo 語句主要影響 CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS 和 LDFLAGS 幾個(gè)編譯器環(huán)境變量。LDFLAGS 用于設(shè)置鏈接時(shí)的參數(shù),除此之外的幾個(gè)變量用于改變編譯階段的構(gòu)建參數(shù) (CFLAGS 用于針對(duì) C 語言代碼設(shè)置編譯參數(shù))。

對(duì)于在 cgo 環(huán)境混合使用 C 和 C++ 的用戶來說,可能有三種不同的編譯選項(xiàng):其中 CFLAGS 對(duì)應(yīng) C 語言特有的編譯選項(xiàng)、CXXFLAGS 對(duì)應(yīng)是 C++ 特有的編譯選項(xiàng)、CPPFLAGS 則對(duì)應(yīng) C 和 C++ 共有的編譯選項(xiàng)。但是在鏈接階段,C 和 C++ 的鏈接選項(xiàng)是通用的,因此這個(gè)時(shí)候已經(jīng)不再有 C 和 C++ 語言的區(qū)別,它們的目標(biāo)文件的類型是相同的。

#cgo 指令還支持條件選擇,當(dāng)滿足某個(gè)操作系統(tǒng)或某個(gè) CPU 架構(gòu)類型時(shí)后面的編譯或鏈接選項(xiàng)生效。比如下面是分別針對(duì) windows 和非 windows 下平臺(tái)的編譯和鏈接選項(xiàng):

// #cgo windows CFLAGS: -DX86=1
// #cgo !windows LDFLAGS: -lm

其中在 windows 平臺(tái)下,編譯前會(huì)預(yù)定義 X86 宏為 1;在非 windows 平臺(tái)下,在鏈接階段會(huì)要求鏈接 math 數(shù)學(xué)庫。這種用法對(duì)于在不同平臺(tái)下只有少數(shù)編譯選項(xiàng)差異的場景比較適用。

如果在不同的系統(tǒng)下 cgo 對(duì)應(yīng)著不同的 c 代碼,我們可以先使用 #cgo 指令定義不同的 C 語言的宏,然后通過宏來區(qū)分不同的代碼:

package main

/*
#cgo windows CFLAGS: -DCGO_OS_WINDOWS=1
#cgo darwin CFLAGS: -DCGO_OS_DARWIN=1
#cgo linux CFLAGS: -DCGO_OS_LINUX=1

#if defined(CGO_OS_WINDOWS)
    const char* os = "windows";
#elif defined(CGO_OS_DARWIN)
    const char* os = "darwin";
#elif defined(CGO_OS_LINUX)
    const char* os = "linux";
#else
#	error(unknown os)
#endif
*/
import "C"

func main() {
    print(C.GoString(C.os))
}

這樣我們就可以用 C 語言中常用的技術(shù)來處理不同平臺(tái)之間的差異代碼。

2.2.3 build tag 條件編譯

build tag 是在 Go 或 cgo 環(huán)境下的 C/C++ 文件開頭的一種特殊的注釋。條件編譯類似于前面通過 #cgo 指令針對(duì)不同平臺(tái)定義的宏,只有在對(duì)應(yīng)平臺(tái)的宏被定義之后才會(huì)構(gòu)建對(duì)應(yīng)的代碼。但是通過 #cgo 指令定義宏有個(gè)限制,它只能是基于 Go 語言支持的 windows、darwin 和 linux 等已經(jīng)支持的操作系統(tǒng)。如果我們希望定義一個(gè) DEBUG 標(biāo)志的宏,#cgo 指令就無能為力了。而 Go 語言提供的 build tag 條件編譯特性則可以簡單做到。

比如下面的源文件只有在設(shè)置 debug 構(gòu)建標(biāo)志時(shí)才會(huì)被構(gòu)建:

// +build debug

package main

var buildMode = "debug"

可以用以下命令構(gòu)建:

go build -tags="debug"
go build -tags="windows debug"

我們可以通過 -tags 命令行參數(shù)同時(shí)指定多個(gè) build 標(biāo)志,它們之間用空格分隔。

當(dāng)有多個(gè) build tag 時(shí),我們將多個(gè)標(biāo)志通過邏輯操作的規(guī)則來組合使用。比如以下的構(gòu)建標(biāo)志表示只有在”linux/386“或”darwin 平臺(tái)下非 cgo 環(huán)境 “才進(jìn)行構(gòu)建。

// +build linux,386 darwin,!cgo

其中 linux,386 中 linux 和 386 用逗號(hào)鏈接表示 AND 的意思;而 linux,386 和 darwin,!cgo 之間通過空白分割來表示 OR 的意思。



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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)