用 cgo 生成用於 cgo 的 C 相容的結構體

polarisxu發表於2020-08-09

假設(並非完全假設,這裡有 demo)你正在編寫一個程式包,用於連線 Go 和其它一些提供大量 C 結構體記憶體的程式。這些結構可能是系統呼叫的結果,也可能是一個庫給你提供的純粹資訊性內容。無論哪種情況,你都希望將這些結構傳遞給你的程式包的使用者,以便他們可以使用這些結構執行操作。在你的包中,你可以直接使用 cgo 提供的 C. 型別。但這有點惱人(這些整型它們沒有對應的原生 Go 型別,使得與常規 Go 程式碼互動需要亂七八糟的強制轉換),並且對於其它匯入你的包的程式碼沒有幫助。因此,你需要以某種方式使用原生的 Go 結構體。

一種方式是手動為這些 C 結構體的定義你自己的 Go 版本。這有兩個缺點。這太枯燥了(還很容易出錯),並且不能保證你能獲得與 C 完全相同的記憶體佈局(後者通常但並非總是很重要)。幸運的是有一種更好的方法,那就是使用 cgo 的 -godefs 功能或多或少地為你自動生成結構體宣告。生成結果並不總是完美的,但可能會為你帶來最大的收益。

使用 -godefs 的起點是特殊的 cgo Go 原始檔,該檔案需要將某些 Go 型別宣告為某些 C 型別。例如:

// +build ignore
package kstat
// #include <kstat.h>
import "C"

type IO C.kstat_io_t
type Sysinfo C.sysinfo_t

const Sizeof_IO = C.sizeof_kstat_io_t
const Sizeof_SI = C.sizeof_sysinfo_t

這些常量對於喜歡較真的人很有用,可以用來在後面對比檢查 Go 型別的 unsafe.Sizeof() 和 C 型別的大小是否一致。

執行 go tool cgo -godefs .go ,它將列印一系列帶有匯出欄位和所有內容的標準 Go 型別到標準輸出。然後,你可以將其儲存到檔案中並使用。如果你認為 C 型別可能會更改,則應將生成的檔案保留下來,這樣就避免重新生成檔案遇到的很多麻煩。如果 C 型別基本上是固定的,則可以使用 godoc 對生成的輸出進行註釋。 cgo 會考慮型別匹配問題,它會把原始的 C 結構中存在的 padding 也插入到輸出中。

我不知道如果原始的 C 結構體不可能在 Go 中重建出來,cgo 會怎麼辦。 比如 Go 需要 padding,但是 C 不需要。希望它會指出錯誤。這是你以後可能要檢查這些 sizeof 的原因之一。

-godefs 最大的限制是與 cgo 通常具有的限制相同:它沒有對 C 聯合型別(union)的真正支援,因為 Go 確實沒有這個。如果你的 C 結構體中有聯合,你得自己弄清楚如何處理它們;我相信 cgo 把這些轉換為大小合適的 uint8 陣列,但這對於實際訪問內容不是很有用。

這裡有兩個問題。假設你有一個嵌入了另一個結構體型別的結構體:

struct cpu_stat {
   struct cpu_sysinfo cpu_sysinfo;
   struct cpu_syswait cpu_syswait;
   struct vminfo cpu_vminfo;
}

在這裡,你必須給 cgo 一些幫助,方式是在主結構體型別之前建立嵌入結構型別的 Go 版本:

type Sysinfo C.struct_cpu_sysinfo
type Syswait C.struct_cpu_syswait
type Vminfo  C.struct_cpu_vminfo

type CpuStat C.struct_cpu_stat

然後 cgo 才能生成正確的內嵌的 Go 結構的 CpuStat 結構。如果不這樣做,你將獲得一個 CpuStat 結構型別,該結構型別具有不完整的型別資訊,其中的 Sysinfo 等欄位將引用名為 _Ctype_… 的未在任何地方定義的型別。

順便說一句,我在這確實是指 Sysinfo ,而不是 Cpu_sysinfo 。cgo 足夠聰明,可以從結構欄位名稱中刪除這種常見的字首。我不知道它的演算法是怎樣的,但至少是有用的。

第二個問題是嵌入了匿名結構:

struct mntinfo_kstat {
   ....
   struct {
      uint32_t srtt;
      uint32_t deviate;
   } m_timers[4];
   ....
}

不幸的是,cgo 根本無法處理這種問題。具體可以去看 issue 5253 ,你有兩個選擇,第一種是使用建議的 CL 修復,這個目前仍然適用於 src/cmd/cgo/gcc.go 並且能夠工作(對我來說)。如果你不想構建自己的 Go 工具鏈(或者如果 CL 不再適用或無法工作),另一種解決方案是建立一個新的 C 標頭檔案,該檔案具有整個結構體的變體,通過建立具名結構體去除結構體的匿名化。

struct m_timer {
   uint32_t srtt;
   uint32_t deviate;
}

struct mntinfo_kstat_cgo {
   ....
   struct m_timer m_timers [4];
   ....
}

然後,在你的 Go 檔案中,

...
// #include "myhacked.h"
...

type MTimer C.struct_m_timer
type Mntinfo C.struct_mntinfo_kstat_cgo

除非你搞錯了,否則兩個 C 結構體應具有完全相同的大小和佈局,並且彼此完全相容。現在你可以在你的版本上使用 -godefs 了,記住按照前面問題 1 的處理,需要為 m_timer 建立明確的 Go 型別。 如果你飄了(你認為你不在需要重新生成這些內容了),你可以在生成的 Go 檔案中逆轉這個過程,重新將 MTimer 型別匿名化到結構體中(因為 Go 對匿名結構體有很好的支援)。因為你沒有更改實際內容,只是改了型別宣告,所以結果應該與原始的佈局相同。

PS:-godefs 的輸入檔案被設定為不被正常 go build 過程構建,因為它只用於 godefs 生成。如果這個檔案也被包含在 go build 構建的原始碼中,你會得到關於 Go 型別多處定義的構建錯誤。必然的結果是,你不需要將此檔案和任何相關 .h 檔案與軟體包的常規 .go 檔案放在同一目錄。你可以把他們放在子目錄,或者放在完全獨立的位置。

(我認為該 package 行在 godefs .go 檔案中唯一要做的就是設定 cgo 將在輸出中列印的軟體包名稱。)


via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoCGoCompatibleStructs

作者:ChrisSiebenmann 譯者:befovy 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

相關文章