掌握 cgo 的字符串函數

cgo[1] 的大量文檔都提到過,它提供了四個用於轉換 Go 和 C 類型的字符串的函數,都是通過複製數據來實現。在 CGo 的文檔中有簡潔的解釋,但我認爲解釋得太簡潔了,因爲文檔只涉及了定義中的某些特定字符串,而忽略了兩個很重要的注意事項。我曾經踩過這裏的坑,現在我要詳細解釋一下。

四個函數分別是:

func C.CString(string) *C.char
func C.GoString(*C.char) string
func C.GoStringN(*C.char, C.int) string
func C.GoBytes(unsafe.Pointer, C.int) []byte

C.CString() 等價於 C 的 strdup(),像文檔中提到的那樣,把 Go 的字符串複製爲可以傳遞給 C 函數的 C 的 char *。很討厭的一件事是,由於 Go 和 CGo 類型的定義方式,調用 C.free 時需要做一個轉換:

cs := C.CString("a string")
C.free(unsafe.Pointer(cs))

請留意,Go 字符串中可能嵌入了 \0 字符,而 C 字符串不會。如果你的 Go 字符串中有 \0 字符,當你調用 C.CString() 時,C 代碼會從 \0 字符處截斷你的字符串。這往往不會被注意到,但有時文本並不保證不含 null 字符 [2]。

C.GoString() 也等價於 strdup(),但與 C.CString() 相反,是把 C 字符串轉換爲 Go 字符串。你可以用它定義結構體的字段,或者是聲明爲 C 的 char *(在 Go 中叫 *C.cahr) 的其他變量,抑或其他的一些變量(我們後面會看到)。

C.GoStringN() 等價於 C 的 memmove(),與 C 中普通的字符串函數不同。**它把整個 N 長度的 C buffer 複製爲一個 Go 字符串,不單獨處理 null 字符。**再詳細點,它也通過複製來實現。如果你有一個定義爲 char feild[64] 的結構體的字段,然後調用了 C.GoStringN(&field, 64),那麼你得到的 Go 字符串一定是 64 個字符,字符串的末尾有可能是一串 \0 字符。

(我認爲這是 cgo 文檔中的一個 bug。它宣稱 GoStringN 的入參是一個 C 的字符串,但實際上很明顯不是,因爲 C 的字符串不能以 null 字符結束,而 GoStringN 不會在 null 字符處結束處理。)

C.GoBytes()C.GoStringN() 的另一個版本,不返回 string 而是返回 []byte。它沒有宣稱以 C 字符串作爲入參,它僅僅是對整個 buffer 做了內存拷貝。

如果你要拷貝的東西不是以 null 字符結尾的 C 字符串,而是固定長度的 memory buffer,那麼 C.GoString() 正好能滿足需求;它避開了 C 中傳統的問題處理不是 C 字符串的 ’string‘[3]。然而,如果你要處理定義爲 char field[N] 的結構體字段這種限定長度的 C 字符串時,這些函數_都不能_滿足需求。

傳統語義的結構體中固定長度的字符串變量,定義爲 char field[N] 的字段,以及 “包含一個字符串” 等描述,都表示當且僅當字符串有足夠空間時以 null 字符結尾,換句話說,字符串最多有 N-1 個字符。如果字符串正好有 N 個字符,那麼它不會以 null 字符結尾。這是 C 代碼中諸多 bug 的根源 [4],也不是一個好的 API,但我們卻擺脫不了這個 API。每次我們遇到這樣的字段,文檔不會明確告訴你字段的內容並不一定是 null 字符結尾的,你需要自己假設你有這種 API。

C.GoString()C.GoStringN() 都不能正確處理這些字段。使用 GoStringN() 相對來說出錯更少;它僅僅返回一個末尾有一串 \0 字符長度爲 N 的 Go 字符串(如果你僅僅是把這些字段打印出來,那麼你可能不會留意到;我經常幹這種事)。使用有誘惑力的 GoString() 更是引狼入室,因爲它內部會對入參做 strlen();如果字符末尾沒有 null 字符,strlen() 會訪問越界的內存地址。如果你走運,你得到的 Go 字符串末尾會有大量的垃圾。如果你不走運,你的 Go 程序出現段錯誤,因爲 strlen() 訪問了未映射的內存地址。

(總的來說,如果字符串末尾出現了大量垃圾,通常意味着在某處有不含結束符的 C 字符串。)

你需要的是與 C 的 strndup() 等價的 Go 函數,以此來確保複製不超過 N 個字符且在 null 字符處終止。下面是我寫的版本,不保證無錯誤:

func strndup(cs *C.char, len int) string {
   s := C.GoStringN(cs, C.int(len))
   i := strings.IndexByte(s, 0)
   if i == -1 {
      return s
   }
   return C.GoString(cs)
}

由於有 Go 的字符串怎樣佔用內存 [5] 的問題,這段代碼做了些額外的工作來最小化額外的內存佔用。你可能想用另一種方法,返回一個 GoStringN() 字符串的切片。你也可以寫複雜的代碼,根據 i 和 len 的不同來決定選用哪種方法。

更新:Ian Lance Taylor 給我展示了份更好的代碼 [6]:

func strndup(cs *C.char, len int) string {
   return C.GoStringN(cs, C.int(C.strnlen(cs, C.size_t(len))))
}

是的,這裏有大量的轉換。這篇文章就是你看到的 Go 和 Gco 類型的結合。


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

作者:ChrisSiebenmann[7] 譯者:lxbwolf[8] 校對:polaris1119[9]

本文由 GCTT[10] 原創編譯,Go 中文網 [11] 榮譽推出

參考資料

[1]

cgo: https://github.com/golang/go/wiki/cgo

[2]

有時文本並不保證不含 null 字符: https://utcc.utoronto.ca/~cks/space/blog/programming/BeSureItsACString

[3]

處理不是 C 字符串的 ’string‘: https://utcc.utoronto.ca/~cks/space/blog/programming/BeSureItsACString

[4]

N]` 的字段,以及 “包含一個字符串” 等描述,都表示當且僅當字符串有足夠空間時以 null 字符結尾,換句話說,字符串最多有 N-1 個字符。如果字符串正好有 N 個字符,那麼它不會以 null 字符結尾。這是 [C 代碼中諸多 bug 的根源: https://utcc.utoronto.ca/~cks/space/blog/programming/UnixAPIMistake

[5]

Go 的字符串怎樣佔用內存: https://utcc.utoronto.ca/~cks/space/blog/programming/GoStringsMemoryHolding

[6]

Ian Lance Taylor 給我展示了份更好的代碼: https://github.com/golang/go/issues/12428#issuecomment-136581154

[7]

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

[8]

lxbwolf: https://github.com/lxbwolf

[9]

polaris1119: https://github.com/polaris1119

[10]

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

[11]

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

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