一文掌握 CGO 處理字符串的問(wèn)題:寫 CGO 必看
cgo[1] 的大量文檔都提到過(guò),它提供了四個(gè)用于轉(zhuǎn)換 Go 和 C 類型的字符串的函數(shù),都是通過(guò)復(fù)制數(shù)據(jù)來(lái)實(shí)現(xiàn)。在 CGo 的文檔中有簡(jiǎn)潔的解釋,但我認(rèn)為解釋得太簡(jiǎn)潔了,因?yàn)槲臋n只涉及了定義中的某些特定字符串,而忽略了兩個(gè)很重要的注意事項(xiàng)。我曾經(jīng)踩過(guò)這里的坑,現(xiàn)在我要詳細(xì)解釋一下。
四個(gè)函數(shù)分別是:
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() 等價(jià)于 C 的 strdup(),像文檔中提到的那樣,把 Go 的字符串復(fù)制為可以傳遞給 C 函數(shù)的 C 的 char *。很討厭的一件事是,由于 Go 和 CGo 類型的定義方式,調(diào)用 C.free 時(shí)需要做一個(gè)轉(zhuǎn)換:
cs?:=?C.CString("a?string")
C.free(unsafe.Pointer(cs))
請(qǐng)留意,Go 字符串中可能嵌入了 \0 字符,而 C 字符串不會(huì)。如果你的 Go 字符串中有 \0 字符,當(dāng)你調(diào)用 C.CString() 時(shí),C 代碼會(huì)從 \0 字符處截?cái)嗄愕淖址_@往往不會(huì)被注意到,但有時(shí)文本并不保證不含 null 字符[2]。
C.GoString() 也等價(jià)于 strdup(),但與 C.CString() 相反,是把 C 字符串轉(zhuǎn)換為 Go 字符串。你可以用它定義結(jié)構(gòu)體的字段,或者是聲明為 C 的 char *(在 Go 中叫 *C.cahr) 的其他變量,抑或其他的一些變量(我們后面會(huì)看到)。
C.GoStringN() 等價(jià)于 C 的 memmove(),與 C 中普通的字符串函數(shù)不同。**它把整個(gè) N 長(zhǎng)度的 C buffer 復(fù)制為一個(gè) Go 字符串,不單獨(dú)處理 null 字符。**再詳細(xì)點(diǎn),它也通過(guò)復(fù)制來(lái)實(shí)現(xiàn)。如果你有一個(gè)定義為 char feild[64] 的結(jié)構(gòu)體的字段,然后調(diào)用了 C.GoStringN(&field, 64),那么你得到的 Go 字符串一定是 64 個(gè)字符,字符串的末尾有可能是一串 \0 字符。
(我認(rèn)為這是 cgo 文檔中的一個(gè) bug。它宣稱 GoStringN 的入?yún)⑹且粋€(gè) C 的字符串,但實(shí)際上很明顯不是,因?yàn)?C 的字符串不能以 null 字符結(jié)束,而 GoStringN 不會(huì)在 null 字符處結(jié)束處理。)
C.GoBytes() 是 C.GoStringN() 的另一個(gè)版本,不返回 string 而是返回 []byte。它沒(méi)有宣稱以 C 字符串作為入?yún)ⅲ鼉H僅是對(duì)整個(gè) buffer 做了內(nèi)存拷貝。
如果你要拷貝的東西不是以 null 字符結(jié)尾的 C 字符串,而是固定長(zhǎng)度的 memory buffer,那么 C.GoString() 正好能滿足需求;它避開(kāi)了 C 中傳統(tǒng)的問(wèn)題處理不是 C 字符串的 ’string‘[3]。然而,如果你要處理定義為 char field[N] 的結(jié)構(gòu)體字段這種限定長(zhǎng)度的 C 字符串時(shí),這些函數(shù)都不能滿足需求。
傳統(tǒng)語(yǔ)義的結(jié)構(gòu)體中固定長(zhǎng)度的字符串變量,定義為 char field[N] 的字段,以及“包含一個(gè)字符串”等描述,都表示當(dāng)且僅當(dāng)字符串有足夠空間時(shí)以 null 字符結(jié)尾,換句話說(shuō),字符串最多有 N-1 個(gè)字符。如果字符串正好有 N 個(gè)字符,那么它不會(huì)以 null 字符結(jié)尾。這是 C 代碼中諸多 bug 的根源[4],也不是一個(gè)好的 API,但我們卻擺脫不了這個(gè) API。每次我們遇到這樣的字段,文檔不會(huì)明確告訴你字段的內(nèi)容并不一定是 null 字符結(jié)尾的,你需要自己假設(shè)你有這種 API。
C.GoString() 或 C.GoStringN() 都不能正確處理這些字段。使用 GoStringN() 相對(duì)來(lái)說(shuō)出錯(cuò)更少;它僅僅返回一個(gè)末尾有一串 \0 字符長(zhǎng)度為 N 的 Go 字符串(如果你僅僅是把這些字段打印出來(lái),那么你可能不會(huì)留意到;我經(jīng)常干這種事)。使用有誘惑力的 GoString() 更是引狼入室,因?yàn)樗鼉?nèi)部會(huì)對(duì)入?yún)⒆?strlen();如果字符末尾沒(méi)有 null 字符,strlen() 會(huì)訪問(wèn)越界的內(nèi)存地址。如果你走運(yùn),你得到的 Go 字符串末尾會(huì)有大量的垃圾。如果你不走運(yùn),你的 Go 程序出現(xiàn)段錯(cuò)誤,因?yàn)?strlen() 訪問(wèn)了未映射的內(nèi)存地址。
(總的來(lái)說(shuō),如果字符串末尾出現(xiàn)了大量垃圾,通常意味著在某處有不含結(jié)束符的 C 字符串。)
你需要的是與 C 的 strndup() 等價(jià)的 Go 函數(shù),以此來(lái)確保復(fù)制不超過(guò) N 個(gè)字符且在 null 字符處終止。下面是我寫的版本,不保證無(wú)錯(cuò)誤:
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 的字符串怎樣占用內(nèi)存[5]的問(wèn)題,這段代碼做了些額外的工作來(lái)最小化額外的內(nèi)存占用。你可能想用另一種方法,返回一個(gè) GoStringN() 字符串的切片。你也可以寫復(fù)雜的代碼,根據(jù) i 和 len 的不同來(lái)決定選用哪種方法。
更新: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))))
}
是的,這里有大量的轉(zhuǎn)換。這篇文章就是你看到的 Go 和 Gco 類型的結(jié)合。
via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoCGoStringFunctions
作者:ChrisSiebenmann[7]譯者:lxbwolf[8]校對(duì):polaris1119[9]
本文由 GCTT[10] 原創(chuàng)編譯,Go 中文網(wǎng)[11] 榮譽(yù)推出
參考資料
cgo: https://github.com/golang/go/wiki/cgo
[2]有時(shí)文本并不保證不含 null 字符: https://utcc.utoronto.ca/~cks/space/blog/programming/BeSureItsACString
[3]處理不是 C 字符串的 ’string‘: https://utcc.utoronto.ca/~cks/space/blog/programming/BeSureItsACString
[4]N]` 的字段,以及“包含一個(gè)字符串”等描述,都表示當(dāng)且僅當(dāng)字符串有足夠空間時(shí)以 null 字符結(jié)尾,換句話說(shuō),字符串最多有 N-1 個(gè)字符。如果字符串正好有 N 個(gè)字符,那么它不會(huì)以 null 字符結(jié)尾。這是 [C 代碼中諸多 bug 的根源: https://utcc.utoronto.ca/~cks/space/blog/programming/UnixAPIMistake
[5]Go 的字符串怎樣占用內(nèi)存: 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 中文網(wǎng): https://studygolang.com/
推薦閱讀

