2.3. Chuyển đổi kiểu dữ liệu
Ban đầu, CGO được tạo nên để thuận tiện cho việc sử dụng các hàm trong C (các hàm hiện thực khai báo Golang trong C) để sử dụng lại các tài nguyên của C. Ngày nay, CGO đã lớn mạnh thành điểm kết nối giao tiếp hai chiều giữa C & Go. Để tận dụng chức năng của CGO, việc hiểu các nguyên tắc chuyển đổi kiểu giữa hai loại ngôn từ là điều trọng yếu.
2.3.1. Các kiểu dữ liệu số học
Khi ta sử dụng các ký hiệu của C trong Golang, thường nó sẽ truy cập thông qua package “C” ảo, ví dụ như kiểu int
tương ứng với C.int
. Một số kiểu trong C bao gồm nhiều keyword, nhưng khi truy cập chúng thông qua package “C” ảo, phần tên chẳng thể có ký tự khoảng trắng, chẳng hạn unsigned int
chẳng thể truy cập bằng C.unsigned int
. Thành ra, CGO phân phối nguyên tắc chuyển đổi tương ứng cho các kiểu trong C:
Mặc dầu kích cỡ của những kiểu không những rõ kích cỡ (trong C) như int
, short
, …, kích cỡ của chúng đều được xác nhận trong CGO: kiểu int
& uint
của C đều có kích cỡ 4 byte, kiểu size_t
có thể được xem là kiểu số nguyên không dấu uint
của ngôn từ Go .
Mặc dầu kiểu int
& uint
của C đều có kích cỡ cố định, nhưng với Go thì int
& uint
có thể là 4 byte hoặc 8 byte (tuỳ platform). Nếu cần sử dụng đúng kiểu int
của C trong Go, bạn có thể sử dụng kiểu GoInt
được xác nhận trong file header _cgo_export.h
được tạo nên bởi dụng cụ CGO. Trong file header này, mỗi kiểu giá trị căn bản của Go sẽ xác nhận kiểu tương ứng trong C (kiểu có tiền tố “Go”). Chẳng hạn sau trong hệ thống 64-bit, file header _cgo_export.h
khái niệm các kiểu giá trị:
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;
Trừ GoInt
& GoUint
, chúng tôi không động viên bạn sử dụng trực tiếp GoInt32
, GoInt64
& các kiểu khác.
Một cách tốt hơn là sử dụng các kiểu có trong khai báo file header (chuẩn C99):
Như đã đề cập trước đây, nếu kiểu trong C bao gồm nhiều từ, nó chẳng thể được sử dụng trực tiếp thông qua package “C” ảo (chẳng hạn: unsigned short
chẳng thể được truy cập trực tiếp C.unsigned short
). Không những thế, sau khoảng thời gian khái niệm lại kiểu trong bằng cách dùng typedef
, tất cả chúng ta có thể truy cập tới kiểu gốc. So với các kiểu trong C cầu kỳ hơn thì nên sử dụng typedef
để đặt lại tên cho nó, thuận lợi cho việc truy cập từ CGO.
2.3.2. Go Strings & Slices
Trong file header _cgo_export.h
được tạo nên bởi CGO, kiểu trong C tương ứng cũng được tạo cho Go string, slice, dictionary, interface & channel:
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;
Không những thế, cần cảnh báo rằng chỉ các string & slice là có giá trị sử dụng trong CGO, vì CGO tạo nên các phiên bản ngôn từ C cho một số hàm trong Go, chính vì như vậy cả hai đều có thể gọi các hàm C trong Go, điều này được thực hiện ngay lặp tức & CGO không phân phối các hàm bổ trợ liên quan cho các kiểu khác, song song mô hình bộ nhớ lưu trữ giành cho ngôn từ Go ngăn tất cả chúng ta duy trì các kiểu con trỏ tới các vùng bộ nhớ lưu trữ Go làm chủ, chính vì như vậy mà môi trường ngôn từ C của các kiểu đó không có giá trị sử dụng.
Trong hàm C đã export, tất cả chúng ta có thể trực tiếp sử dụng các string & slice trong Go. Giả sử có hai hàm export sau:
func
helloString(s string
) {}
func
helloSlice(s []byte
) {}
File header _cgo_export.h
được CGO tạo nên sẽ chứa khai báo hàm sau:
extern
void
helloString
(GoString p0)
;
extern
void
helloSlice
(GoSlice p0)
;
Nhưng cảnh báo rằng nếu bạn sử dụng kiểu GoString
thì sẽ lệ thuộc vào file header _cgo_export.h
& file này có bài viết hay thay đổi do CGO chào đời.
Phiên bản Go1.10 thêm một chuỗi kiểu _GoString_
, có thể làm giảm code có nguy cơ lệ thuộc file header _cgo_export.h
. Tất cả chúng ta có thể bố trí khai báo ngôn từ C của hàm helloString
thành:
extern
void
helloString
(_GoString_ p0)
;
Bởi vì _GoString_
là kiểu khái niệm trước, ta chẳng thể truy cập rực tiếp các thông tin như length hay pointer của string qua kiểu này. Go1.10 thêm vào 2 hàm sau để bổ sung:
size_t
_GoStringLen(_GoString_ s);
const
char
*_GoStringPtr(_GoString_ s);
2.3.3. Struct, Union, Enum
Các kiểu struct, Union & Enum của ngôn từ C chẳng thể được thêm vào struct dưới dạng tính chất ẩn danh.
Struct
Trong Go, tất cả chúng ta có thể truy cập các kiểu struct như struct xxx
tương ứng là C.struct_xxx
trong ngôn từ C. Tổ chức bộ nhớ lưu trữ của struct tuân theo các nguyên tắc alignment. Trong môi trường ngôn từ Go 32 bit, struct của C tuân theo nguyên tắc alignment 32 bit & môi trường ngôn từ Go 64 bit tuân theo nguyên tắc alignment 64 bit. So với các struct có nguyên tắc alignment đặc biệt được chỉ định, chúng chẳng thể được truy cập trong CGO.
Cách dùng struct dễ dàng như sau:
import
"C"
import
"fmt"
func
main() {
var
a C.struct_A
fmt.Println(a.i)
fmt.Println(a.f)
}
Nếu tên thành phần của struct tự dưng là một keyword trong Go, bạn có thể truy cập nó bằng cách thêm một dấu gạch dưới ở đầu tên member:
import
"C"
import
"fmt"
func
main() {
var
a C.struct_A
fmt.Println(a._type)
}
Nhưng nếu có 2 thành phần: một thành phần được đặt tên theo keyword của Go & phần kia là trùng khi thêm vào dấu gạch dưới, thì các thành phần được đặt tên theo keyword ngôn từ Go sẽ chẳng thể truy cập:
import
"C"
import
"fmt"
func
main() {
var
a C.struct_A
fmt.Println(a._type)
}
Các thành phần tương ứng với trường bit (tính chất được khái niệm với giá trị độ lớn kèm theo) trong kết cấu ngôn từ C chẳng thể được truy cập bằng ngôn từ Go. Nếu bạn cần thao tác với các thành phần này, bạn cần khái niệm hàm bổ trợ trong C.
import
"C"
import
"fmt"
func
main() {
var
a C.struct_A
fmt.Println(a.size)
fmt.Println(a.arr)
}
Trong ngôn từ C, tất cả chúng ta chẳng thể truy cập trực tiếp vào kiểu struct được khái niệm bởi Go.
Union
So với các kiểu union, tất cả chúng ta có thể truy cập các kiểu union xxx
tương ứng là C.union_xxx
trong ngôn từ C. Không những thế, các kiểu union trong C không được bổ trợ trong Go & chúng được chuyển đổi thành các mảng byte có kích cỡ tương ứng.
import
"C"
import
"fmt"
func
main() {
var
b1 C.union_B1;
fmt.Printf("%Tn"
, b1)
var
b2 C.union_B2;
fmt.Printf("%Tn"
, b2)
}
Nếu bạn cần thao tác biến kiểu lồng nhau trong C (union):
- Cách thứ đặc biệt là khái niệm hàm bổ trợ trong C,
- Cách thứ hai là phân giải thủ công các thành phần đó thông qua “encoding/binary” của ngôn từ Go (không phải vấn đề big endian),
- Cách thứ ba là sử dụng package
unsafe
để chuyển sang kiểu tương ứng (đây là cách hiệu quả nhất để thực hiện).
Sau đây cho thấy cách truy cập các member kiểu union thông qua package unsafe
:
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)))
}
Mặc dầu truy cập bằng package unsafe
là cách dễ nhất & tốt nhất về năng suất, nó có thể làm cầu kỳ hoá vấn đề với các tình huống mà trong đó các kiểu union lồng nhau được giải quyết. So với các kiểu này ta nên giải quyết chúng bằng cách khái niệm các hàm bổ trợ trong C.
Enum
So với các kiểu liệt kê (enum), tất cả chúng ta có thể truy cập các kiểu enum xxx
tương ứng là C.enum_xxx
trong C.
import
"C"
import
"fmt"
func
main() {
var
c C.enum_C = C.TWO
fmt.Println(c)
fmt.Println(C.ONE)
fmt.Println(C.TWO)
}
Trong ngôn từ C, kiểu int
bên dưới kiểu liệt kê bổ trợ giá trị âm. Tất cả chúng ta có thể truy cập trực tiếp các giá trị liệt kê được xác nhận bằng C.ONE
, C.TWO
, ….
2.3.4. Array, String & Slice
Chuỗi (string) trong C là một mảng kiểu char & độ dài của nó phải được xác nhận theo địa điểm của ký tự NULL (đại diện chấm dứt mảng). Không có kiểu slice trong ngôn từ C.
Array
Trong C, biến mảng thực ra tương ứng với một con trỏ trỏ tới một phần bộ nhớ lưu trữ có độ dài rõ ràng và cụ thể của một kiểu rõ ràng và cụ thể, con trỏ này chẳng thể được sửa đổi, khi truyền biến mảng vào một hàm, thực ra là truyền địa chỉ phần tử trước hết của mảng.
Trong Go, mảng là một kiểu giá trị & độ dài của mảng là một phần của kiểu mảng. Chuỗi trong Go tương ứng với một vùng nhớ “chỉ đọc” có độ dài khẳng định. Slice trong Go là phiên bản dễ dàng hơn của mảng động (dynamic array).
Chuyển đổi giữa Go & C với các kiểu array, string & slice có thể được dễ dàng hóa thành chuyển đổi giữa Go slice & C pointer trỏ tới vùng nhớ có độ dài khẳng định.
Package C ảo của CGO phân phối tập các hàm sau để chuyển đổi hai chiều array & string giữa Go & C:
func
C.CString(string
) *C.char
func
C.CBytes([]byte
) unsafe.Pointer
func
C.GoString(*C.char) string
func
C.GoStringN(*C.char, C.int
) string
func
C.GoBytes(unsafe.Pointer, C.int
) []byte
Khi string & slice của Go được chuyển đổi thành phiên bản trong C, hàm malloc
của C cấp phát một vùng nhớ mới & cuối cùng có thể được giải phóng bằng free
. Trái lại khi một string hoặc array trong C được chuyển đổi thành kiểu tương ứng trong Go, vùng nhớ của dữ liệu được chuyển đổi được làm chủ bởi ngôn từ Go.
Với các hàm chuyển đổi này, vùng nhớ trước chuyển đổi & sau chuyển đổi vẫn ở trong vùng nhớ cục vùng tương ứng của chúng. Ưu thế của việc chuyển đổi đó là làm chủ interface & vùng nhớ rất dễ. Điểm yếu là cần cấp phát vùng nhớ mới & các hoạt động copy của nó sẽ dẫn nhiều đến ngân sách phụ.
String & Slice
Các khái niệm cho string & slice trong package reflect
:
type
StringHeader struct
{
Data uintptr
Len int
}
type
SliceHeader struct
{
Data uintptr
Len int
Cap int
}
Còn nếu như không mong muốn cấp phát vùng nhớ riêng, bạn có thể truy cập trực tiếp vào không gian bộ nhớ lưu trữ của C bằng Go:
import
"C"
import
(
"reflect"
"unsafe"
"fmt"
)
func
main() {
var
arr0 []byte
var
arr0Hdr = (*reflect.SliceHeader)(unsafe.Pointer(&arr0))
arr0Hdr.Data = uintptr
(unsafe.Pointer(&C.arr[]))
arr0Hdr.Len = 10
arr0Hdr.Cap = 10
arr1 := (*[31
]byte
)(unsafe.Pointer(&C.arr[]))[:10
:10
]
var
s0 string
var
s0Hdr = (*reflect.StringHeader)(unsafe.Pointer(&s0))
s0Hdr.Data = uintptr
(unsafe.Pointer(C.s))
s0Hdr.Len = int
(C.strlen(C.s))
sLen := int
(C.strlen(C.s))
s1 := string
((*[31
]byte
)(unsafe.Pointer(C.s))[:sLen:sLen])
fmt.Println("arr1: "
, arr1)
fmt.Println("s1: "
, s1)
}
Vì chuỗi trong Go là chuỗi chỉ đọc, người dùng cần bảo đảm rằng bài viết của chuỗi C bên dưới sẽ không thay đổi trong công cuộc sử dụng chuỗi đó trong Go & bộ nhớ lưu trữ sẽ không được giải phóng trước.
Trong CGO, phiên bản ngôn từ C của struct tương ứng với struct string & slice trên:
typedef struct
{ const
char *p; GoInt n; } GoString;
typedef struct
{ void *data; GoInt len
; GoInt cap
; } GoSlice;
Trong C có thể dùng GoString
& GoSlice
để truy cập string & slice trong Go. Nếu là một kiểu mảng trong Go, bạn có thể chuyển đổi mảng thành một slice & sau đó chuyển đổi nó. Còn nếu như không gian bộ nhớ lưu trữ bên dưới tương ứng với một string hoặc slice được làm chủ bởi runtime của Go thì đối tượng bộ nhớ lưu trữ Go có thể được lưu trong một thời gian dài trong ngôn từ C.
Cụ thể về mô hình bộ nhớ lưu trữ CGO sẽ được luận bàn kĩ hơn trong các chương sau.
2.3.5. Chuyển đổi giữa các con trỏ
Trong ngôn từ C, các kiểu con trỏ khác nhau có thể được chuyển đổi tường minh hoặc ngầm định. Việc chuyển đổi giữa các con trỏ cũng là vấn đề mấu chốt trước hết cần được khắc phục trong code CGO.
Trong ngôn từ Go, nếu một kiểu con trỏ được xây dựng dựa theo một kiểu con trỏ khác, nói cách khác, hai con trỏ bên dưới là các con trỏ có cùng kết cấu, thì tất cả chúng ta có thể chuyển đổi giữa các con trỏ bằng cú pháp cast trực tiếp. Không những thế, CGO thường phải ứng phó với việc chuyển đổi giữa hai kiểu con trỏ hoàn toàn khác nhau. Về phép tắc, thao tác này bị nghiêm cấm trong code Go thuần.
Một trong những mục đích của CGO là phá vỡ sự cấm đoán nói trên & khôi phục các thao tác chuyển đổi con trỏ tự do mà ngôn từ C nên có. Đoạn code sau trình bày cách chuyển đổi một con trỏ kiểu X thành một con trỏ kiểu Y:
var
p *X
var
q *Y
q = (*Y)(unsafe.Pointer(p))
p = (*X)(unsafe.Pointer(q))
Để chuyển đổi con trỏ kiểu X thành con trỏ kiểu Y, tất cả chúng ta cần hiện thực hàm unsafe.Pointer
chuyển đổi giữa các kiểu con trỏ khác nhau như một kiểu điểm kết nối trung gian. Kiểu con trỏ unsafe.Pointer
tương đương với ngôn từ C với con trỏ void*
.
Sau đây là sơ đồ công cuộc chuyển đổi giữa các con trỏ:
Bất kỳ kiểu con trỏ nào cũng có thể được chuyển sang kiểu con trỏ unsafe.Pointer
để bỏ đi thông tin kiểu ban đầu, sau đó gán lại một kiểu con trỏ mới để đạt được mục đích chuyển đổi.
2.3.6. Chuyển đổi giá trị & con trỏ
Trong ngôn từ C, ta thường gặp trường hợp con trỏ được trình diễn bởi giá trị thông thường, làm sao để hiện thực việc chuyển đổi giá trị & con trỏ cũng là một vấn đề mà CGO cần phải đương đầu.
Để kiểm tra chặt chẽ việc sử dụng con trỏ, ngôn từ Go không cho phép chuyển đổi các kiểu số trực tiếp thành các kiểu con trỏ. Không những thế, Go đã đặc biệt khái niệm một kiểu uintptr
cho các kiểu con trỏ unsafe.Pointer
. Tất cả chúng ta có thể sử dụng uintptr
làm trung gian để hiện thực các kiểu số thành các kiểu unsafe.Pointer
.
Biểu đồ sau đây trình bày cách hiện thực chuyển đổi lẫn nhau của kiểu int32
sang kiểu con trỏ char*
là chuỗi trong ngôn từ C:
Việc chuyển đổi được chia thành nhiều công đoạn: trước hết là kiểu int32
sang uintptr
, sau này là uintptr
thành kiểu con trỏ unsafe.Pointr
& cuối cùng là kiểu con trỏ unsafe.Pointr
thành kiểu *C.char
.
2.3.7. Chuyển đổi giữa kiểu slice
Mảng cũng là một loại con trỏ trong ngôn từ C, chính vì như vậy việc chuyển đổi giữa hai kiểu mảng khác nhau về căn bản cũng giống như chuyển đổi giữa các con trỏ. Không những thế trong ngôn từ Go, slice thực ra là một con trỏ tới một mảng (fat pointer), chính vì như vậy tất cả chúng ta chẳng thể chuyển đổi trực tiếp giữa các kiểu slice khác nhau.
Không những thế, package reflection
của ngôn từ Go đã phân phối sẵn kết cấu căn bản của kiểu slice nhờ đó chuyển đổi slice có thể được hiện thực:
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[]) / unsafe.Sizeof(p[])
pHdr.Cap = qHdr.Cap * unsafe.Sizeof(q[]) / unsafe.Sizeof(p[])
Cần cảnh báo rằng nếu X hoặc Y là kiểu null, đoạn code trên có thể gây ra lỗi chia cho 0 & code thực tiễn cần được giải quyết khi phù hợp.
Sau đây cho thấy luồng rõ ràng và cụ thể của thao tác chuyển đổi giữa các slice:
So với các chức năng hay được dùng trong CGO, Author package github.com/chai2010/cgo, đã phân phối các tính năng chuyển đổi căn bản. Để biết thêm cụ thể hãy tìm hiểu code hiện thực.