2.3 Chuyển đổi kiểu · GitBook

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.

Xem Thêm  Kinh nghiệm chọn ebook reader - e reader là gì

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, ….

Xem Thêm  Các thẻ cơ bản, phần tử và thuộc tính trong HTML

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.

Xem Thêm  giúp máy tính chạy ổn định và mướt mà hơn.
Tính năng recovery win 10 là một tính năng vô cùng tuyệt vời

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.

Link

Viết một bình luận