CGO類型轉換


類型轉換

最初CGO是為了達到方便從Go語言函數調用C語言函數(用C語言實現Go語言聲明的函數)以復用C語言資源這一目的而出現的(因為C語言還會涉及回調函數,自然也會涉及到從C語言函數調用Go語言函數(用Go語言實現C語言聲明的函數))。現在,它已經演變為C語言和Go語言雙向通訊的橋梁。要想利用好CGO特性,自然需要了解此二語言類型之間的轉換規則,這是本節要討論的問題。

數值類型

在Go語言中訪問C語言的符號時,一般是通過虛擬的“C”包訪問,比如C.int對應C語言的int類型。有些C語言的類型是由多個關鍵字組成,但通過虛擬的“C”包訪問C語言類型時名稱部分不能有空格字符,比如unsigned int不能直接通過C.unsigned int訪問。因此CGO為C語言的基礎數值類型都提供了相應轉換規則,比如C.uint對應C語言的unsigned int

需要注意的是,雖然在C語言中int、short等類型沒有明確定義內存大小,但是在CGO中它們的內存大小是確定的。在CGO中,C語言的int和long類型都是對應4個字節的內存大小size_t類型可以當作Go語言uint無符號整數類型對待。

CGO中,雖然C語言的int固定為4字節的大小,但是Go語言自己的int和uint卻在32位和64位系統下分別對應4個字節和8個字節大小。如果需要在C語言中訪問Go語言的int類型,可以通過GoInt類型訪問,GoInt類型在CGO工具生成的_cgo_export.h頭文件中定義。其實在_cgo_export.h頭文件中,每個基本的Go數值類型都定義了對應的C語言類型,它們一般都是以單詞Go為前綴。下面是64位環境下,_cgo_export.h頭文件生成的Go數值類型的定義,其中GoInt和GoUint類型分別對應GoInt64和GoUint64:

typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef float GoFloat32;
typedef double GoFloat64;

除了GoIntGoUint之外,我們並不推薦直接訪問GoInt32、GoInt64等類型。更好的做法是通過C語言的C99標准引入的<stdint.h>頭文件。為了提高C語言的可移植性,在<stdint.h>文件中,不但每個數值類型都提供了明確內存大小,而且和Go語言的類型命名更加一致。

Go語言類型<stdint.h>頭文件類型對比如表

前文說過,如果C語言的類型是由多個關鍵字組成,則無法通過虛擬的“C”包直接訪問(比如C語言的unsigned short不能直接通過C.unsigned short訪問)。但是,在<stdint.h>中通過使用C語言的typedef關鍵字將unsigned short重新定義為uint16_t這樣一個單詞的類型后,我們就可以通過C.uint16_t訪問原來的unsigned short類型了。對於比較復雜的C語言類型,推薦使用typedef關鍵字提供一個規則的類型命名,這樣更利於在CGO中訪問。

Go 字符串和切片

在CGO生成的_cgo_export.h頭文件中還會為Go語言的字符串、切片、字典、接口和管道等特有的數據類型生成對應的C語言類型:

typedef struct { const char *p; GoInt n; } GoString;
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

不過需要注意的是,其中只有字符串和切片在CGO中有一定的使用價值,因為CGO為他們的某些GO語言版本的操作函數生成了C語言版本,因此二者可以在Go調用C語言函數時馬上使用;而CGO並未針對其他的類型提供相關的輔助函數,且Go語言特有的內存模型導致我們無法保持這些由Go語言管理的內存指針,所以它們C語言環境並無使用的價值。

在導出的C語言函數中我們可以直接使用Go字符串和切片。假設有以下兩個導出函數:

//export helloString
func helloString(s string) {}

//export helloSlice
func helloSlice(s []byte) {}

CGO生成的_cgo_export.h頭文件會包含以下的函數聲明:

extern void helloString(GoString p0);
extern void helloSlice(GoSlice p0);

不過需要注意的是,如果使用了GoString類型則會對_cgo_export.h頭文件產生依賴,而這個頭文件是動態輸出的。

Go1.10針對Go字符串增加了一個_GoString_預定義類型,可以降低在cgo代碼中可能對_cgo_export.h頭文件產生的循環依賴的風險。我們可以調整helloString函數的C語言聲明為:

extern void helloString(_GoString_ p0);

因為_GoString_是預定義類型,我們無法通過此類型直接訪問字符串的長度和指針等信息。Go1.10同時也增加了以下兩個函數用於獲取字符串結構中的長度和指針信息:

size_t _GoStringLen(_GoString_ s);
const char *_GoStringPtr(_GoString_ s);

更嚴謹的做法是為C語言函數接口定義嚴格的頭文件,然后基於穩定的頭文件實現代碼。

結構體、聯合、枚舉類型

C語言的結構體、聯合、枚舉類型不能作為匿名成員被嵌入到Go語言的結構體中。在Go語言中,我們可以通過C.struct_xxx來訪問C語言中定義的struct xxx結構體類型。結構體的內存布局按照C語言的通用對齊規則,在32位Go語言環境C語言結構體也按照32位對齊規則,在64位Go語言環境按照64位的對齊規則。對於指定了特殊對齊規則的結構體,無法在CGO中訪問。

結構體的簡單用法如下:

/*
struct A {
    int i;
    float f;
};
*/
import "C"
import "fmt"

func main() {
    var a C.struct_A
    fmt.Println(a.i)
    fmt.Println(a.f)
}

如果結構體的成員名字中碰巧是Go語言的關鍵字,可以通過在成員名開頭添加下划線來訪問:

/*
struct A {
    int type; // type 是 Go 語言的關鍵字
};
*/
import "C"
import "fmt"

func main() {
    var a C.struct_A
    fmt.Println(a._type) // _type 對應 type
}

但是如果有2個成員:一個是以Go語言關鍵字命名,另一個剛好是以下划線和Go語言關鍵字命名,那么以Go語言關鍵字命名的成員將無法訪問(被屏蔽):

/*
struct A {
    int   type;  // type 是 Go 語言的關鍵字
    float _type; // 將屏蔽CGO對 type 成員的訪問
};
*/
import "C"
import "fmt"

func main() {
    var a C.struct_A
    fmt.Println(a._type) // _type 對應 _type
}

C語言結構體中位字段對應的成員無法在Go語言中訪問,如果需要操作位字段成員,需要通過在C語言中定義輔助函數來完成。對應零長數組的成員,無法在Go語言中直接訪問數組的元素,但其中零長的數組成員所在位置的偏移量依然可以通過unsafe.Offsetof(a.arr)來訪問。

/*
struct A {
    int   size: 10; // 位字段無法訪問
    float arr[];    // 零長的數組也無法訪問
};
*/
import "C"
import "fmt"

func main() {
    var a C.struct_A
    fmt.Println(a.size) // 錯誤: 位字段無法訪問
    fmt.Println(a.arr)  // 錯誤: 零長的數組也無法訪問
}

在C語言中,我們無法直接訪問Go語言定義的結構體類型。

對於聯合類型,我們可以通過C.union_xxx來訪問C語言中定義的union xxx類型。但是Go語言中並不支持C語言聯合類型,它們會被轉為對應大小的字節數組

/*
#include <stdint.h>

union B1 {
    int i;
    float f;
};

union B2 {
    int8_t i8;
    int64_t i64;
};
*/
import "C"
import "fmt"

func main() {
    var b1 C.union_B1;
    fmt.Printf("%T\n", b1) // [4]uint8

    var b2 C.union_B2;
    fmt.Printf("%T\n", b2) // [8]uint8
}

如果需要操作C語言的聯合類型變量,一般有三種方法:第一種是在C語言中定義輔助函數;第二種是通過Go語言的"encoding/binary"手工解碼成員(需要注意大端小端問題);第三種是使用unsafe包強制轉型為對應類型(這是性能最好的方式)。下面展示通過unsafe包訪問聯合類型成員的方式:

/*
#include <stdint.h>

union B {
    int i;
    float f;
};
*/
import "C"
import "fmt"

func main() {
    var b C.union_B;
    fmt.Println("b.i:", *(*C.int)(unsafe.Pointer(&b)))
    fmt.Println("b.f:", *(*C.float)(unsafe.Pointer(&b)))
}

雖然unsafe包訪問最簡單、性能也最好,但是對於有嵌套聯合類型的情況處理會導致問題復雜化。對於復雜的聯合類型,推薦通過在C語言中定義輔助函數的方式處理。

對於枚舉類型,我們可以通過C.enum_xxx來訪問C語言中定義的enum xxx結構體類型。

/*
enum C {
    ONE,
    TWO,
};
*/
import "C"
import "fmt"

func main() {
    var c C.enum_C = C.TWO
    fmt.Println(c)
    fmt.Println(C.ONE)
    fmt.Println(C.TWO)
}

在C語言中,枚舉類型底層對應int類型,支持負數類型的值。我們可以通過C.ONE、C.TWO等直接訪問定義的枚舉值。

數組、字符串和切片

在C語言中,數組名其實對應於一個指針,指向特定類型特定長度的一段內存,但是這個指針不能被修改;當把數組名傳遞給一個函數時,實際上傳遞的是數組第一個元素的地址。為了討論方便,我們將一段特定長度的內存統稱為數組。C語言的字符串是一個char類型的數組,字符串的長度需要根據表示結尾的NULL字符的位置確定。C語言中沒有切片類型。

在Go語言中,數組是一種值類型,而且數組的長度是數組類型的一個部分。Go語言字符串對應一段長度確定的只讀byte類型的內存。Go語言的切片則是一個簡化版的動態數組。

Go語言和C語言的數組、字符串和切片之間的相互轉換可以簡化為Go語言的切片和C語言中指向一定長度內存的指針之間的轉換。

CGO的C虛擬包提供了以下一組函數,用於Go語言和C語言之間數組和字符串的雙向轉換:

// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char

// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer

// C string to Go string
func C.GoString(*C.char) string

// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string

// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

其中C.CString針對輸入的Go字符串,克隆一個C語言格式的字符串;返回的字符串由C語言的malloc函數分配,不使用時需要通過C語言的free函數釋放。C.CBytes函數的功能和C.CString類似,用於從輸入的Go語言字節切片克隆一個C語言版本的字節數組,同樣返回的數組需要在合適的時候釋放。C.GoString用於將從NULL結尾的C語言字符串克隆一個Go語言字符串。C.GoStringN是另一個字符數組克隆函數。C.GoBytes用於從C語言數組,克隆一個Go語言字節切片。

該組輔助函數都是以克隆的方式運行。當Go語言字符串和切片向C語言轉換時,克隆的內存由C語言的malloc函數分配,最終可以通過free函數釋放當C語言字符串或數組向Go語言轉換時,克隆的內存由Go語言分配管理。通過該組轉換函數,轉換前和轉換后的內存依然在各自的語言環境中,它們並沒有跨越Go語言和C語言。克隆方式實現轉換的優點是接口和內存管理都很簡單,缺點是克隆需要分配新的內存和復制操作都會導致額外的開銷

在reflect包中有字符串和切片的定義:

type StringHeader struct {
    Data uintptr
    Len  int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

如果不希望單獨分配內存,可以在Go語言中直接訪問C語言的內存空間:

package main

/*
#include <string.h>
char arr[10];
char *s = "Hello";
*/
import "C"
import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {
	// 通過 reflect.SliceHeader 轉換
	var arr0 []byte
	var arr0Hdr = (*reflect.SliceHeader)(unsafe.Pointer(&arr0))
	arr0Hdr.Data = uintptr(unsafe.Pointer(&C.arr[0]))
	arr0Hdr.Len = 10
	arr0Hdr.Cap = 10
	fmt.Println(arr0)

	var s0 string
	var s0Hdr = (*reflect.StringHeader)(unsafe.Pointer(&s0))
	s0Hdr.Data = uintptr(unsafe.Pointer(C.s))
	s0Hdr.Len = int(C.strlen(C.s))
	fmt.Println(s0)
	// 通過切片語法轉換
	arr1 := (*[31]byte)(unsafe.Pointer(&C.arr[0]))[:10:10]
	fmt.Println(arr1)
	
	sLen := int(C.strlen(C.s))
	s1 := string((*[31]byte)(unsafe.Pointer(C.s))[:sLen:sLen])
	fmt.Println(s1)
}

因為Go語言的字符串是只讀的,用戶需要自己保證Go字符串在使用期間,底層對應的C字符串內容不會發生變化、內存不會被提前釋放掉。

在CGO中,會為字符串和切片生成和上面結構對應的C語言版本的結構體:

typedef struct { const char *p; GoInt n; } GoString;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

在C語言中可以通過GoStringGoSlice來訪問Go語言的字符串和切片。如果是Go語言中數組類型,可以將數組轉為切片后再行轉換。如果字符串或切片對應的底層內存空間由Go語言的運行時管理,那么在C語言中不能長時間保存Go內存對象

指針間的轉換

在C語言中,不同類型的指針是可以顯式或隱式轉換的,如果是隱式只是會在編譯時給出一些警告信息。但是Go語言對於不同類型的轉換非常嚴格,任何C語言中可能出現的警告信息在Go語言中都可能是錯誤!指針是C語言的靈魂,指針間的自由轉換也是cgo代碼中經常要解決的第一個重要的問題。

在Go語言中兩個指針的類型完全一致則不需要轉換可以直接通用。如果一個指針類型是用type命令在另一個指針類型基礎之上構建的,換言之兩個指針底層是相同完全結構的指針,那么我我們可以通過直接強制轉換語法進行指針間的轉換。但是cgo經常要面對的是2個完全不同類型的指針間的轉換,原則上這種操作在純Go語言代碼是嚴格禁止的。

cgo存在的一個目的就是打破Go語言的禁止,恢復C語言應有的指針的自由轉換和指針運算。以下代碼演示了如何將X類型的指針轉化為Y類型的指針:

var p *X
var q *Y

q = (*Y)(unsafe.Pointer(p)) // *X => *Y
p = (*X)(unsafe.Pointer(q)) // *Y => *X

為了實現X類型指針到Y類型指針的轉換,我們需要借助unsafe.Pointer作為中間橋接類型實現不同類型指針之間的轉換。unsafe.Pointer指針類型類似C語言中的void*類型的指針。

任何類型的指針都可以通過強制轉換為unsafe.Pointer指針類型去掉原有的類型信息,然后再重新賦予新的指針類型而達到指針間的轉換的目的。

數值和指針的轉換

不同類型指針間的轉換看似復雜,但是在cgo中已經算是比較簡單的了。在C語言中經常遇到用普通數值表示指針的場景,也就是說如何實現數值和指針的轉換也是cgo需要面對的一個問題。

為了嚴格控制指針的使用,Go語言禁止將數值類型直接轉為指針類型!不過,Go語言針對unsafe.Pointr指針類型特別定義了一個uintptr類型。我們可以uintptr為中介,實現數值類型到unsafe.Pointr指針類型到轉換。再結合前面提到的方法,就可以實現數值和指針的轉換了。

下面流程圖演示了如何實現int32類型到C語言的char*字符串指針類型的相互轉換:

package main

// char * Cc;
import "C"
import "unsafe"

var goi int32

func main() {
	goi = int32(uintptr(unsafe.Pointer(&C.Cc)))
	C.Cc = (* C.char)(unsafe.Pointer(uintptr(goi)))
}

轉換分為幾個階段,在每個階段實現一個小目標:首先是int32到uintptr類型,然后是uintptr到unsafe.Pointr指針類型,最后是unsafe.Pointr指針類型到*C.char類型。

切片間的轉換

在C語言中數組也一種指針,因此兩個不同類型數組之間的轉換和指針間轉換基本類似。但是在Go語言中,數組或數組對應的切片都不再是指針類型,因此我們也就無法直接實現不同類型的切片之間的轉換。

不過Go語言的reflect包提供了切片類型的底層結構,再結合前面討論到不同類型之間的指針轉換技術就可以實現[]X和[]Y類型的切片轉換:

var p []X
var q []Y

pHdr := (*reflect.SliceHeader)(unsafe.Pointer(&p))
qHdr := (*reflect.SliceHeader)(unsafe.Pointer(&q))

pHdr.Data = qHdr.Data
pHdr.Len = qHdr.Len * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0])
pHdr.Cap = qHdr.Cap * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0])

不同切片類型之間轉換的思路是先構造一個空的目標切片,然后用原有的切片底層數據填充目標切片。如果X和Y類型的大小不同,需要重新設置Len和Cap屬性。需要注意的是,如果X或Y是空類型,上述代碼中可能導致除0錯誤,實際代碼需要根據情況酌情處理。

下面演示了切片間的轉換的具體流程:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM