Go Wiki:cgo
簡介
首先,https://pkg.go.dev/cmd/cgo 是主要的 cgo 文件。
在 https://go.dev.org.tw/blog/cgo 也有不錯的入門文章
基礎
如果 Go 原始檔匯入 "C"
,它正在使用 cgo。Go 檔案將可以存取出現在 import "C"
行之前註解中的任何內容,並會連結到其他 Go 檔案中的所有其他 cgo 註解,以及建置程序中包含的所有 C 檔案。
請注意,cgo 註解和匯入陳述式之間不能有空白行。
若要存取來自 C 端的符號,請使用套件名稱 C
。也就是說,如果你想從 Go 程式碼呼叫 C 函式 printf()
,你可以寫 C.printf()
。由於目前不支援可變引數方法(例如 printf)(問題 975),我們將把它包在 C 方法「myprint」中
package cgoexample
/*
##include <stdio.h>
##include <stdlib.h>
void myprint(char* s) {
printf("%s\n", s);
}
*/
import "C"
import "unsafe"
func Example() {
cs := C.CString("Hello from stdio\n")
C.myprint(cs)
C.free(unsafe.Pointer(cs))
}
從 C 呼叫 Go 函式
可以使用 cgo 從 Go 程式碼呼叫的 C 程式碼呼叫頂層 Go 函式和函式變數。
全域函式
Go 透過使用特殊的 //export
註解讓 C 程式碼可以使用其函式。請注意:如果你使用匯出,則無法在前言中定義任何 C 函式。
例如,有兩個檔案,foo.c 和 foo.go:foo.go 包含
package gocallback
import "fmt"
/*
##include <stdio.h>
extern void ACFunction();
*/
import "C"
//export AGoFunction
func AGoFunction() {
fmt.Println("AGoFunction()")
}
func Example() {
C.ACFunction()
}
foo.c 包含
##include "_cgo_export.h"
void ACFunction() {
printf("ACFunction()\n");
AGoFunction();
}
函式變數
以下程式碼顯示從 C 程式碼呼叫 Go 回呼的範例。由於 指標傳遞規則,Go 程式碼無法直接將函式值傳遞給 C。相反地,必須使用間接參照。此範例使用帶有互斥鎖的註冊表,但還有許多其他方式可以從可傳遞給 C 的值對應到 Go 函式。
package gocallback
import (
"fmt"
"sync"
)
/*
extern void go_callback_int(int foo, int p1);
// normally you will have to define function or variables
// in another separate C file to avoid the multiple definition
// errors, however, using "static inline" is a nice workaround
// for simple functions like this one.
static inline void CallMyFunction(int foo) {
go_callback_int(foo, 5);
}
*/
import "C"
//export go_callback_int
func go_callback_int(foo C.int, p1 C.int) {
fn := lookup(int(foo))
fn(p1)
}
func MyCallback(x C.int) {
fmt.Println("callback with", x)
}
func Example() {
i := register(MyCallback)
C.CallMyFunction(C.int(i))
unregister(i)
}
var mu sync.Mutex
var index int
var fns = make(map[int]func(C.int))
func register(fn func(C.int)) int {
mu.Lock()
defer mu.Unlock()
index++
for fns[index] != nil {
index++
}
fns[index] = fn
return index
}
func lookup(i int) func(C.int) {
mu.Lock()
defer mu.Unlock()
return fns[i]
}
func unregister(i int) {
mu.Lock()
defer mu.Unlock()
delete(fns, i)
}
從 Go 1.17 開始,runtime/cgo
套件提供 runtime/cgo.Handle 機制,並簡化上述範例為
package main
import (
"fmt"
"runtime/cgo"
)
/*
##include <stdint.h>
extern void go_callback_int(uintptr_t h, int p1);
static inline void CallMyFunction(uintptr_t h) {
go_callback_int(h, 5);
}
*/
import "C"
//export go_callback_int
func go_callback_int(h C.uintptr_t, p1 C.int) {
fn := cgo.Handle(h).Value().(func(C.int))
fn(p1)
}
func MyCallback(x C.int) {
fmt.Println("callback with", x)
}
func main() {
h := cgo.NewHandle(MyCallback)
C.CallMyFunction(C.uintptr_t(h))
h.Delete()
}
函式指標回呼
C 程式碼可以使用明確名稱呼叫匯出的 Go 函式。但是,如果 C 程式需要函式指標,則必須撰寫閘道函式。這是因為我們無法取得 Go 函式的位址並將其提供給 C 程式碼,因為 cgo 工具會在 C 中產生一個存根程式,應該呼叫它。以下範例顯示如何與需要給定型別函式指標的 C 程式碼整合。
將這些原始檔放在 $GOPATH/src/ccallbacks/ 下。使用下列指令編譯並執行
$ gcc -c clibrary.c
$ ar cru libclibrary.a clibrary.o
$ go build
$ ./ccallbacks
Go.main(): calling C function with callback to us
C.some_c_func(): calling callback with arg = 2
C.callOnMeGo_cgo(): called with arg = 2
Go.callOnMeGo(): called with arg = 2
C.some_c_func(): callback responded with 3
goprog.go
package main
/*
##cgo CFLAGS: -I .
##cgo LDFLAGS: -L . -lclibrary
##include "clibrary.h"
int callOnMeGo_cgo(int in); // Forward declaration.
*/
import "C"
import (
"fmt"
"unsafe"
)
//export callOnMeGo
func callOnMeGo(in int) int {
fmt.Printf("Go.callOnMeGo(): called with arg = %d\n", in)
return in + 1
}
func main() {
fmt.Printf("Go.main(): calling C function with callback to us\n")
C.some_c_func((C.callback_fcn)(unsafe.Pointer(C.callOnMeGo_cgo)))
}
cfuncs.go
package main
/*
##include <stdio.h>
// The gateway function
int callOnMeGo_cgo(int in)
{
printf("C.callOnMeGo_cgo(): called with arg = %d\n", in);
int callOnMeGo(int);
return callOnMeGo(in);
}
*/
import "C"
clibrary.h
##ifndef CLIBRARY_H
##define CLIBRARY_H
typedef int (*callback_fcn)(int);
void some_c_func(callback_fcn);
##endif
clibrary.c
##include <stdio.h>
##include "clibrary.h"
void some_c_func(callback_fcn callback)
{
int arg = 2;
printf("C.some_c_func(): calling callback with arg = %d\n", arg);
int response = callback(2);
printf("C.some_c_func(): callback responded with %d\n", response);
}
Go 字串與 C 字串
Go 字串與 C 字串不同。Go 字串是長度與字串中第一個字元的指標的組合。C 字串只是一個指標指向第一個字元,並以第一個空字元 '\0'
結束。
Go 提供了以下三個函式來轉換這兩種字串
func C.CString(goString string) *C.char
func C.GoString(cString *C.char) string
func C.GoStringN(cString *C.char, length C.int) string
要記住的一件重要事情是 C.CString()
會配置一個適當長度的新字串,並回傳它。這表示 C 字串不會被垃圾回收,而由 **你** 負責釋放它。以下是一個標準做法。
// #include <stdlib.h>
import "C"
import "unsafe"
...
var cmsg *C.char = C.CString("hi")
defer C.free(unsafe.Pointer(cmsg))
// do something with the C string
當然,你不需要使用 defer
來呼叫 C.free()
。你可以在任何時候釋放 C 字串,但確保你這麼做是你的責任。
將 C 陣列轉換為 Go 切片
C 陣列通常是空值終止或長度保存在其他地方。
Go 提供以下函式來從 C 陣列建立新的 Go 位元組切片
func C.GoBytes(cArray unsafe.Pointer, length C.int) []byte
若要建立由 C 陣列支援的 Go 切片(不複製原始資料),需要在執行階段取得這個長度,並使用類型轉換為一個非常大的陣列的指標,然後將其切片成你想要的長度(如果你使用 Go 1.2 或更新版本,也記得設定上限),例如(請參閱 https://go.dev.org.tw/play/p/XuC0xqtAIC 以取得可執行範例)
import "C"
import "unsafe"
...
var theCArray *C.YourType = C.getTheArray()
length := C.getTheArrayLength()
slice := (*[1 << 28]C.YourType)(unsafe.Pointer(theCArray))[:length:length]
在 Go 1.17 或更新版本中,程式可以使用 unsafe.Slice
,它會產生由 C 陣列支援的 Go 切片
import "C"
import "unsafe"
...
var theCArray *C.YourType = C.getTheArray()
length := C.getTheArrayLength()
slice := unsafe.Slice(theCArray, length) // Go 1.17
請務必記住,Go 垃圾收集器不會與底層 C 陣列互動,而且如果從 C 端釋放陣列,使用切片的任何 Go 程式碼的行為都是不確定的。
常見陷阱
結構體對齊問題
由於 Go 不支援封裝結構體(例如,最大對齊為 1 位元的結構體),因此您無法在 Go 中使用封裝 C 結構體。即使您的程式通過編譯,它也不會執行您想要的操作。若要使用它,您必須將結構體讀取/寫入為位元組陣列/切片。
另一個問題是,某些類型的對齊需求低於其在 Go 中的對應類型,而且如果該類型碰巧在 C 中對齊但在 Go 規則中沒有對齊,則該結構體根本無法在 Go 中表示。範例如下 (問題 7560)
struct T {
uint32_t pad;
complex float x;
};
Go 的 complex64 對齊為 8 位元組,而 C 只有 4 位元組(因為 C 在內部將複數浮點數視為 struct { float real; float imag; }
,而不是基本類型),因此這個 T 結構體根本沒有 Go 表示。對於這種情況,如果您控制結構體的配置,請移動複數浮點數,使其也對齊到 8 位元組會更好,如果您不願意移動它,請使用此表單將其強制對齊到 8 位元組(並浪費 4 位元組)
struct T {
uint32_t pad;
__attribute__((align(8))) complex float x;
};
但是,如果您不控制結構體配置,您將必須為該結構體定義存取器 C 函式,因為 cgo 無法將該結構體轉換為等效的 Go 結構體。
//export
和前言中的定義
如果 Go 原始檔使用任何 //export
指令,則註解中的 C 程式碼只能包含宣告(extern int f();
),不能包含定義(int f() { return 1; }
或 int n;
)。注意:您可以使用 static inline
技巧來解決此限制,以解決前言中定義的微小函式(請參閱上方以取得完整範例)。
Windows
要在 Windows 上使用 cgo,您還需要先安裝 gcc 編譯器(例如 mingw-w64),並在編譯 cgo 之前將 gcc.exe(等)放入您的 PATH 環境變數中。
環境變數
Go os.Getenv() 看不到 C.setenv() 設定的變數
測試
_test.go 檔案無法使用 cgo。
此內容為 Go Wiki 的一部分。