設定和使用 gccgo

本文件介紹瞭如何使用 gccgo,這是一個 Go 語言的編譯器。gccgo 編譯器是 GCC(一款廣泛使用的 GNU 編譯器)的一個新前端。儘管前端本身遵循 BSD 風格的許可,但 gccgo 通常作為 GCC 的一部分使用,因此受 GNU 通用公共許可證 的約束(該許可證涵蓋了作為 GCC 一部分的 gccgo 本身;它不涵蓋由 gccgo 生成的程式碼)。

請注意,gccgo 不是 gc 編譯器;有關該編譯器的資訊,請參閱 安裝 Go 說明。

釋出

安裝 gccgo 的最簡單方法是安裝一個已構建為支援 Go 的 GCC 二進位制發行版。GCC 二進位制發行版可從 各種網站 獲取,並且通常包含在 GNU/Linux 發行版中。我們預計大多數構建這些二進位制發行版的人都會包含 Go 支援。

GCC 4.7.1 版本及之後的所有 4.7 版本都包含一個完整的 Go 1 編譯器和庫。

由於時間原因,GCC 4.8.0 和 4.8.1 版本接近但並不完全等同於 Go 1.1。GCC 4.8.2 版本包含一個完整的 Go 1.1.2 實現。

GCC 4.9 版本包含一個完整的 Go 1.2 實現。

GCC 5 版本包含 Go 1.4 使用者庫的完整實現。Go 1.4 執行時並未完全合併,但這應該不會對 Go 程式產生可見影響。

GCC 6 版本包含 Go 1.6.1 使用者庫的完整實現。Go 1.6 執行時並未完全合併,但這應該不會對 Go 程式產生可見影響。

GCC 7 版本包含 Go 1.8.1 使用者庫的完整實現。與早期版本一樣,Go 1.8 執行時並未完全合併,但這應該不會對 Go 程式產生可見影響。

GCC 8 版本包含 Go 1.10.1 版本的完整實現。Go 1.10 執行時現在已完全合併到 GCC 開發原始碼中,並且完全支援併發垃圾回收。

GCC 9 版本包含 Go 1.12.2 版本的完整實現。

GCC 10 版本包含 Go 1.14.6 版本的完整實現。

GCC 11 版本包含 Go 1.16.3 版本的完整實現。

GCC 12 和 13 版本包含 Go 1.18 標準庫的完整實現。但是,GCC 尚不支援泛型。

原始碼

如果您無法使用發行版,或者傾向於自己構建 gccgo,可以透過 Git 訪問 gccgo 原始碼。GCC 網站提供了 獲取 GCC 原始碼的說明。gccgo 原始碼已包含在內。為了方便起見,Go 支援的穩定版本可以在主 GCC 程式碼倉庫的 devel/gccgo 分支中獲取:git://gcc.gnu.org/git/gcc.git。該分支會定期使用穩定的 Go 編譯器原始碼進行更新。

請注意,儘管 gcc.gnu.org 是獲取 Go 前端原始碼最方便的方式,但它並非主原始碼庫。如果您想為 Go 前端編譯器貢獻更改,請參閱 為 gccgo 做貢獻

構建

構建 gccgo 就像構建 GCC 一樣,只需一兩個額外的選項。請參閱 GCC 網站上的說明。執行 configure 時,新增選項 --enable-languages=c,c++,go(以及您可能想構建的其他語言)。如果您針對的是 32 位 x86,那麼您需要將 gccgo 構建為預設支援鎖定比較和交換指令;為此,請同時使用 configure 選項 --with-arch=i586(或更新的架構,取決於您希望程式在哪種平臺上執行)。如果您針對的是 64 位 x86,但有時想使用 -m32 選項,則使用 configure 選項 --with-arch-32=i586

Gold

在 x86 GNU/Linux 系統上,gccgo 編譯器能夠為 goroutine 使用一個小的離散堆疊。這使得程式能夠執行更多的 goroutine,因為每個 goroutine 可以使用相對較小的堆疊。這需要使用 gold 連結器版本 2.22 或更高版本。您可以安裝 GNU binutils 2.22 或更高版本,或者自己構建 gold。

要自己構建 gold,請構建 GNU binutils,並在執行 configure 指令碼時使用 --enable-gold=default。在構建之前,您必須安裝 flex 和 bison 包。典型的序列如下所示(您可以將 /opt/gold 替換為您有寫入許可權的任何目錄)

git clone git://sourceware.org/git/binutils-gdb.git
mkdir binutils-objdir
cd binutils-objdir
../binutils-gdb/configure --enable-gold=default --prefix=/opt/gold
make
make install

無論您如何安裝 gold,在配置 gccgo 時,請使用選項 --with-ld=GOLD_BINARY

先決條件

如 GCC 網站 上所述,構建 GCC 需要一系列先決條件。在執行 GCC configure 指令碼之前安裝所有先決條件非常重要。可以使用 GCC 原始碼中的指令碼 contrib/download_prerequisites 來方便地下載先決條件庫。

構建命令

在安裝完所有先決條件後,典型的構建和安裝序列如下所示(僅當您使用上面描述的 gold 連結器時才使用 --with-ld 選項)

git clone --branch devel/gccgo git://gcc.gnu.org/git/gcc.git gccgo
mkdir objdir
cd objdir
../gccgo/configure --prefix=/opt/gccgo --enable-languages=c,c++,go --with-ld=/opt/gold/bin/ld
make
make install

使用 gccgo

gccgo 編譯器與其他 gcc 前端的工作方式相同。從 GCC 5 開始,gccgo 安裝還包括一個 go 命令版本,可以用於按照 https://golang.org.tw/cmd/go 中的說明構建 Go 程式。

不使用 go 命令編譯檔案

gccgo -c file.go

這將生成 file.o。連結檔案以建立可執行檔案

gccgo -o file file.o

要執行生成的檔案,您需要告訴程式在哪裡可以找到已編譯的 Go 包。有幾種方法可以做到這一點

選項

gccgo 編譯器支援所有與語言無關的 GCC 選項,特別是 -O-g 選項。

-fgo-pkgpath=PKGPATH 選項可用於為正在編譯的包設定唯一的字首。go 命令會自動使用此選項,但如果您直接呼叫 gccgo,您可能希望使用它。此選項旨在與包含許多包的大型程式一起使用,以便允許多個包使用相同的識別符號作為包名。PKGPATH 可以是任何字串;字串的一個好選擇是匯入包時使用的路徑。

-I-L 選項(編譯器同義詞)可用於設定查詢匯入的搜尋路徑。如果您使用 go 命令進行構建,則不需要這些選項。

匯入

當您編譯一個匯出某個內容的包時,匯出資訊將直接儲存在目標檔案中。如果您直接使用 gccgo 構建,而不是使用 go 命令,那麼當您匯入一個包時,必須告訴 gccgo 如何找到該檔案。

當您使用 gccgo 匯入包 FILE 時,它會按以下檔案順序查詢匯入資料,並使用找到的第一個檔案。

FILE.gox 在使用時,通常只包含匯出資料。可以透過以下方式從 FILE.o 生成:

objcopy -j .go_export FILE.o FILE.gox

gccgo 編譯器會在當前目錄中查詢匯入檔案。在更復雜的場景中,您可以將 -I-L 選項傳遞給 gccgo。這兩個選項都接受要搜尋的目錄。-L 選項也會傳遞給連結器。

gccgo 編譯器目前(2015-06-15)不會在目標檔案中記錄匯入包的檔名。您必須安排將匯入的資料鏈接到程式中。同樣,在使用 go 命令構建時也不是必需的。

gccgo -c mypackage.go              # Exports mypackage
gccgo -c main.go                   # Imports mypackage
gccgo -o main main.o mypackage.o   # Explicitly links with mypackage.o

除錯

如果您在編譯時使用 -g 選項,您可以在可執行檔案上執行 gdb。偵錯程式對 Go 的瞭解有限。您可以設定斷點、單步執行等。您可以列印變數,但它們會像 C/C++ 型別一樣列印。對於數值型別,這無關緊要。Go 字串和介面會顯示為兩個元素的結構。Go 對映和通道始終表示為指向執行時結構的 C 指標。

C 互操作性

在使用 gccgo 時,與 C 或使用 extern "C" 編譯的 C++ 程式碼的互操作性有限。

型別

基本型別直接對映:Go 中的 int32 是 C 中的 int32_tint64int64_t,依此類推。Go 型別 int 是與指標大小相同的整數,因此對應於 C 型別 intptr_t。Go byte 等同於 C unsigned char。Go 中的指標是 C 中的指標。Go struct 與具有相同欄位和型別的 C struct 相同。

Go string 型別目前定義為一個兩元素的結構(此定義可能會更改

struct __go_string {
  const unsigned char *__data;
  intptr_t __length;
};

您無法在 C 和 Go 之間傳遞陣列。但是,Go 中的陣列指標等同於 C 中指向元素型別等效項的指標。例如,Go *[10]int 等同於 C int*,前提是 C 指標指向 10 個元素。

Go 中的 slice 是一個結構。當前定義是(此定義可能會更改

struct __go_slice {
  void *__values;
  intptr_t __count;
  intptr_t __capacity;
};

Go 函式的型別是指向結構的指標(此型別可能會更改)。結構中的第一個欄位指向函式程式碼,該程式碼等同於指向引數型別等效的 C 函式的指標,並帶有一個額外的尾隨引數。尾隨引數是閉包,傳遞的引數是指向 Go 函式結構的指標。當 Go 函式返回多個值時,C 函式返回一個結構。例如,以下函式大致等效

func GoFunction(int) (int, float64)
struct { int i; float64 f; } CFunction(int, void*)

Go interfacechannelmap 型別沒有對應的 C 型別(interface 是一個兩元素的結構,channelmap 是指向 C 中結構的指標,但這些結構故意不予記錄)。C enum 型別對應於某種整數型別,但具體是哪種通常難以預測;請使用強制型別轉換。C union 型別沒有對應的 Go 型別。包含位域的 C struct 型別沒有對應的 Go 型別。C++ class 型別沒有對應的 Go 型別。

C 和 Go 之間的記憶體分配方式完全不同,因為 Go 使用垃圾回收。該領域的具體指導方針尚未確定,但很可能允許將從 C 分配的記憶體指標傳遞給 Go。最終釋放指標的責任將保留在 C 端,當然,如果 C 端在 Go 端仍有副本時釋放了指標,程式將失敗。當將指標從 Go 傳遞給 C 時,Go 函式必須在某個 Go 變數中保留該指標的可見副本。否則,Go 垃圾回收器可能會在 C 函式仍在使用的同時刪除該指標。

函式名

Go 程式碼可以直接呼叫 C 函式,使用 gccgo 中實現的一個 Go 擴充套件:函式宣告可以前置 //extern NAME。例如,C 函式 open 在 Go 中宣告如下

//extern open
func c_open(name *byte, mode int, perm int) int

C 函式自然期望一個以 NUL 結尾的字串,在 Go 中這等同於一個指向 byte 陣列(不是 slice!)的指標,並且有一個終止的零位元組。所以從 Go 進行的示例呼叫如下(匯入 syscall 包後)

var name = [4]byte{'f', 'o', 'o', 0};
i := c_open(&name[0], syscall.O_RDONLY, 0);

(這僅作為示例,要在 Go 中開啟檔案,請使用 Go 的 os.Open 函式。)

請注意,如果 C 函式可能阻塞,例如呼叫 read 時,呼叫 C 函式可能會阻塞 Go 程式。除非您清楚自己在做什麼,否則 C 和 Go 之間的所有呼叫都應透過 cgo 或 SWIG 來實現,就像對 gc 編譯器一樣。

從 C 訪問的 Go 函式的名稱可能會發生變化。目前,沒有接收者的 Go 函式的名稱是 prefix.package.Functionname。字首由編譯包時使用的 -fgo-prefix 選項設定;如果未使用的選項,則預設為 go。要從 C 呼叫該函式,您必須使用 GCC 副檔名設定名稱。

extern int go_function(int) __asm__ ("myprefix.mypackage.Function");

從 C 原始檔自動生成 Go 宣告

GCC 的 Go 版本支援從 C 程式碼自動生成 Go 宣告。該功能相當笨拙,大多數使用者應該改用帶有 -gccgo 選項的 cgo 程式。

像往常一樣編譯您的 C 程式碼,並新增選項 -fdump-go-spec=FILENAME。這將作為編譯的副作用建立檔案 FILENAME。該檔案將包含 C 程式碼中宣告的型別、變數和函式的 Go 宣告。無法在 Go 中表示的 C 型別將被記錄為 Go 程式碼中的註釋。生成的檔案將沒有 package 宣告,但除此之外可以由 gccgo 直接編譯。

此過程充滿了未說明的特殊情況和限制,我們不保證它將來不會發生變化。它比常規過程更有可能成為實際 Go 程式碼的起點。