Go Wiki: cgo
引言
首先,https://pkg.go.dev/cmd/cgo 是主要的 cgo 文件。
在 https://golang.com.tw/blog/cgo 也有一個很好的介紹文章。
基礎知識
如果一個 Go 原始檔匯入了 "C",那麼它就在使用 cgo。該 Go 檔案將可以訪問緊隨 import "C" 語句之前的註釋中的任何內容,並且將與所有其他 Go 檔案中的 cgo 註釋以及構建過程中包含的所有 C 檔案連結。
請注意,cgo 註釋和 import 語句之間不能有空行。
要訪問來自 C 語言端的符號,請使用包名 C。也就是說,如果你想從 Go 程式碼呼叫 C 函式 printf(),你可以寫 C.printf()。由於像 printf 這樣的可變引數方法尚未支援(參見 issue 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 程式碼使用。注意:如果你使用了 export,則不能在 preamble 中定義任何 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.charfunc C.GoString(cString *C.char) stringfunc 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 或更高版本,請記住設定 cap),例如(參閱 https://golang.com.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 陣列在 C 端被釋放,任何使用該切片的 Go 程式碼的行為都將是不可預測的。
常見陷阱
結構體對齊問題
由於 Go 不支援 packed struct(例如,最大對齊為 1 位元組的 struct),因此不能在 Go 中使用 packed C struct。即使你的程式編譯透過,它也不會按預期工作。要使用它,你必須將 struct 讀取/寫入為位元組陣列/切片。
另一個問題是,某些型別在 C 中的對齊要求比在 Go 中的對齊要求低,如果該型別在 C 中對齊而在 Go 規則中不對齊,那麼該 struct 根本無法在 Go 中表示。例如(參見 issue 7560):
struct T {
uint32_t pad;
complex float x;
};
Go 的 complex64 有 8 位元組的對齊,而 C 只有 4 位元組(因為 C 在內部將 complex float 視為 struct { float real; float imag; },而不是基本型別)。這個 T struct 根本沒有 Go 的表示。在這種情況下,如果你控制 struct 的佈局,最好將 complex float 移動到也對齊到 8 位元組的位置,如果你不願意移動它,使用這種形式可以強制它對齊到 8 位元組(並浪費 4 位元組):
struct T {
uint32_t pad;
__attribute__((align(8))) complex float x;
};
但是,如果你不控制 struct 的佈局,你將不得不為該 struct 定義訪問器 C 函式,因為 cgo 無法將該 struct 翻譯成等效的 Go struct。
//export 和 preamble 中的定義
如果 Go 原始檔使用任何 //export 指令,那麼註釋中的 C 程式碼只能包含宣告(extern int f();),而不能包含定義(int f() { return 1; } 或 int n;)。注意:你可以使用 static inline 技巧來繞過這個限制,用於在 preamble 中定義的微小函式(參見上面的完整示例)。
Windows
要在 Windows 上使用 cgo,你還需要先安裝一個 gcc 編譯器(例如,mingw-w64),並在 PATH 環境變數中包含 gcc.exe(等),然後才能成功編譯 cgo。
環境變數
Go 的 os.Getenv() 看不到 C.setenv() 設定的變數。
測試
_test.go 檔案不能使用 cgo。
此內容是 Go Wiki 的一部分。