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

假設(並非完全假設,這裏有 demo[1])你正在編寫一個程序包,用於連接 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 <file>.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[2] ,你有兩個選擇,第一種是使用建議的 CL 修復 [3],這個目前仍然適用於 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[4] 譯者:befovy[5] 校對:polaris1119[6]

本文由 GCTT[7] 原創編譯,Go 中文網 [8] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。

參考資料

[1]

並非完全假設,這裏有 demo: https://github.com/siebenmann/go-kstat/

[2]

issue 5253: https://github.com/golang/go/issues/5253

[3]

建議的 CL 修復: https://codereview.appspot.com/122900043

[4]

ChrisSiebenmann: https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann

[5]

befovy: https://github.com/befovy

[6]

polaris1119: https://github.com/polaris1119

[7]

GCTT: https://github.com/studygolang/GCTT

[8]

Go 中文網: https://studygolang.com/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bqUPN6wwua4x2_FOwyzkYg