用 cgo 生成用於 cgo 的 C 相容的結構體
阿新 • • 發佈:2020-08-09
假設([並非完全假設,這裡有 demo](https://github.com/siebenmann/go-kstat/))你正在編寫一個程式包,用於連線 Go 和其它一些提供大量 C 結構體記憶體的程式。這些結構可能是系統呼叫的結果,也可能是一個庫給你提供的純粹資訊性內容。無論哪種情況,你都希望將這些結構傳遞給你的程式包的使用者,以便他們可以使用這些結構執行操作。在你的包中,你可以直接使用 cgo 提供的 C. 型別。但這有點惱人(這些整型它們沒有對應的原生 Go 型別,使得與常規 Go 程式碼互動需要亂七八糟的強制轉換),並且對於其它匯入你的包的程式碼沒有幫助。因此,你需要以某種方式使用原生的 Go 結構體。
一種方式是手動為這些 C 結構體的定義你自己的 Go 版本。這有兩個缺點。這太枯燥了(還很容易出錯),並且不能保證你能獲得與 C 完全相同的記憶體佈局(後者通常但並非總是很重要)。幸運的是有一種更好的方法,那就是使用 cgo 的 `-godefs` 功能或多或少地為你自動生成結構體宣告。生成結果並不總是完美的,但可能會為你帶來最大的收益。
使用 `-godefs` 的起點是特殊的 cgo Go 原始檔,該檔案需要將某些 Go 型別宣告為某些 C 型別。例如:
```go
// +build ignore
package kstat
// #include
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 陣列,但這對於實際訪問內容不是很有用。
這裡有兩個問題。假設你有一個嵌入了另一個結構體型別的結構體:
```c
struct cpu_stat {
struct cpu_sysinfo cpu_sysinfo;
struct cpu_syswait cpu_syswait;
struct vminfo cpu_vminfo;
}
```
在這裡,你必須給 cgo 一些幫助,方式是在主結構體型別之前建立嵌入結構型別的 Go 版本:
```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 足夠聰明,可以從結構欄位名稱中刪除這種常見的字首。我不知道它的演算法是怎樣的,但至少是有用的。
第二個問題是嵌入了匿名結構:
```c
struct mntinfo_kstat {
....
struct {
uint32_t srtt;
uint32_t deviate;
} m_timers[4];
....
}
```
不幸的是,cgo 根本無法處理這種問題。具體可以去看 [issue 5253](https://github.com/golang/go/issues/5253) ,你有兩個選擇,第一種是使用[建議的 CL 修復](https://codereview.appspot.com/122900043),這個目前仍然適用於 src/cmd/cgo/gcc.go 並且能夠工作(對我來說)。如果你不想構建自己的 Go 工具鏈(或者如果 CL 不再適用或無法工作),另一種解決方案是建立一個新的 C 標頭檔案,該檔案具有整個結構體的變體,通過建立具名結構體去除結構體的匿名化。
```c
struct m_timer {
uint32_t srtt;
uint32_t deviate;
}
struct mntinfo_kstat_cgo {
....
struct m_timer m_timers [4];
....
}
```
然後,在你的 Go 檔案中,
```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](https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann) 譯者:[befovy](https://github.com/befovy) 校對:[polaris1119](https://github.com/polaris1119)
本文由 [GCTT](https://github.com/studygolang/GCTT) 原創編譯,[Go語言中文網](https://studygolang.com/articles/27148) 榮