Go 調用 C-C-- 函數全攻略
一、Go 語言調用 C/C++ 函數
- cgo 基礎及工作原理
Go 語言通過 cgo 和 C 語言的 ABI(Application Binary Interface) 進行交互。
cgo 會生成相應的 C 代碼, 與 Go 代碼一起編譯成可執行文件或動態庫。
cgo 的工作流程主要分爲 3 步:
(1) 預處理: 將 Go 源碼中的 C 代碼塊提取出來, 生成 .c 和 .h 文件
(2) 編譯: 調用 C 編譯器, 分別編譯 C 文件和 Go 文件
(3) 鏈接: 將 C 文件和 Go 文件鏈接生成最終的可執行程序
通過這種方式, Go 代碼就可以直接調用 C 代碼中的函數, 兩個語言實現了無縫銜接。
- Go 調用 C 動態庫示例
下面是一個 Go 程序調用 C 語言編寫的動態庫 libadd.so 的示例
package main
//#include <stdio.h>
//int add(int a, int b);
import "C"
import "fmt"
func main() {
res := C.add(10, 20)
fmt.Println(res)
}
libadd.c
int add(int a, int b) {
return a + b;
}
編譯運行
$ gcc -fPIC -shared -o libadd.so libadd.c
$ go build demo.go -ldflags="-L." -gccgoflags="-Wl,-rpath,."
$ ./demo
30
通過 cgo 和 ABI,Go 程序可以無障礙調用 C 語言編譯而成的動態庫。
- Go 調用 C++ 方法示例
Go 調用 C++ 函數與調用 C 函數基本類似, 也需要通過 cgo 進行 ABI 層的適配。
主要不同在於 C++ 編譯器的支持和名稱修飾 (name mangling) 問題。
示例代碼
package main
//#include "add.h"
import "C"
import "fmt"
func main() {
res := C.add(10, 20)
fmt.Println(res)
}
add.h
#ifdef __cplusplus
extern "C" {
#endif
int add(int a, int b);
#ifdef __cplusplus
}
#endif
add.cpp
int add(int a, int b) {
return a + b;
}
編譯運行
$ g++ -fPIC -shared -o libadd.so add.cpp
$ go build demo.go -ldflags="-L." -gccgoflags="-Wl,-rpath,."
$ ./demo
30
通過外部 "C" 包裝, 解決了 C++ 名稱修飾的問題, 使得 Go 可以正確調用 C++ 函數。
- 數據類型映射及類型不安全
Go 和 C/C++ 中的基礎數據類型可以通過 cgo 自動映射, 如:
Go int、uint 對應 C/C++ int、unsigned int
Go byte 對應 C/C++ char
Go string 對應 C/C++ char*
但是也存在一些類型無法精確映射的情況, 例如 Go 的 string 和 slice 類型, 會映射爲 C 語言的指針, 這種轉換是類型不安全的, 需要開發者自行處理。
此外, 複雜數據結構的自定義類型也需要開發者自行轉換處理。
二、C/C++ 調用 Go 函數
- cgo 生成頭文件供 C/C++ 使用
cgo 可以爲 Go 包生成對應的 C 語言頭文件, 供 C/C++ 代碼調用。
示例
package main
//export Add
func Add(a, b int) int {
return a + b
}
func main() {}
生成頭文件
$ go tool cgo example.go
example.h
extern int Add(int a, int b);
- C/C++ 主調 Go 函數示例
main.c
#include "example.h"
#include <stdio.h>
int main() {
int res = Add(10, 20);
printf("%d\n", res);
return 0;
}
編譯鏈接
$ gcc -o app main.c example.o
輸出
30
通過 cgo 生成的頭文件, C/C++ 代碼可以無縫調用 Go 語言編寫的函數。
- Go 函數性能考量
與 Go 相比, C/C++ 函數調用性能更高, 而 Go 函數調用會有一定的性能損耗。
所以在混合編程時, 對性能敏感的功能應優先使用 C/C++ 實現, 非核心流程可以使用 Go 實現。
三、Go 與 C/C++ 的數據交換
- 數組及切片傳遞爲 C 指針
Go 語言中的數組和切片可以直接轉換爲 C 語言中的指針進行傳遞。
示例代碼
package main
//#include <stdio.h>
//void print_array(int* arr, int len);
import "C"
import "unsafe"
func main() {
arr := []int{1, 2, 3, 4, 5}
C.print_array
((*C.int)(unsafe.Pointer(&arr[0])), C.int(len(arr)))
}
print_array 函數定義
void print_array(int* arr, int len) {
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
}
輸出
1 2 3 4 5
Go Slice 直接轉換爲 C 指針, 兩種語言實現了無縫對接。
- 字符串處理差異及轉換
Go 中的字符串爲 UTF-8 編碼, 而 C/C++ 使用空終止符結束。字符串處理有一定區別。
簡單示例
cs := C.CString("Hello")
defer C.free(unsafe.Pointer(cs))
cstr := C.GoString(cs)
通過 C.CString 和 C.GoString 可以安全地在兩種字符串表示之間轉換。
- 結構體互相映射機制
Go 語言中的結構體可以與 C 結構體互相映射,前提是兩個結構體的定義是兼容的。
示例代碼
package main
/*
struct MyData {
int a;
char b;
float c;
};
*/
import "C"
import "unsafe"
import "fmt"
type MyData struct {
a int
b byte
c float32
}
func main() {
v := MyData{1, 'a', 2.5}
cv := (*C.struct_MyData)(unsafe.Pointer(&v))
fmt.Println(cv.a) // 1
fmt.Println(cv.b) // 'a'
fmt.Println(cv.c) // 2.5
}
Go 與 C 結構體直接轉換爲指針使用, 成員按內存分佈順序訪問。這實現了兩種語言自定義類型字段的無縫映射。
四、Go 與 C/C++ 的內存管理
- Go 自動內存回收機制
Go 語言採用高效的垃圾回收機制來自動管理內存。不需要開發者手動分配釋放內存, 避免了常見的內存錯誤, 例如泄漏、重用已釋放內存等。
Go GC 主要採用標記清除算法, 由垃圾回收器在後臺定期自動執行, 不會暫停應用程序線程。可以實現很低的 GC 延遲。
- C/C++ 手動內存管理
C/C++ 要求開發者手動動態分配和釋放堆內存。容易出現內存泄漏和重用懸空指針等嚴重 Bug。
智能指針等技術可部分緩解問題。
手動內存管理的好處是可以細粒度控制內存使用, 優化性能。且不存在 GC 停頓的問題。
- 內存安全和性能的權衡
Go 自動 GC 實現了內存安全防護, 降低了開發難度, 但是會有一定的性能損耗。
而 C/C++ 手動內存管理可以利用複雜手段優化內存和性能, 但開發難度大且易出錯。
綜合來說, Go 更適合互聯網服務端開發, 而 C/C++ 更擅長一些對性能要求極高的場景, 如遊戲、圖像處理等。
五、Go 與 C/C++ 進程間通信
- 基於文件 / 管道的 IPC
Go 和 C/C++ 可以通過文件、命名管道 (FIFO)、匿名管道實現進程間通信。
文件可實現數據持久化, 管道支持全雙工通信。都存在一定的時間和空間開銷。
- 基於 Socket 的網絡通信
Socket 套接字可實現高效的網絡數據傳輸, 支持任意倍擴展。是跨語言混合編程的理想 IPC 方式。
Go 語言實現 socket 編程很高效, 與 C/C++ 可完美配合。
- 基於 RPC 的跨語言調用
Go 語言和 C/C++ 都有成熟的 RPC 框架, 可以實現高性能的進程 / 網絡間通信, 構建分佈式系統。
例如 Go 語言的 gRPC 框架, 已經成爲 CNCF 基金會的畢業項目, 在雲原生領域得到廣泛應用。
六、Go 與 C/C++ 混合編程案例分析
- 異步 IO 網絡服務
服務端可以用 Go 實現, 利用 Go 協程實現高併發。再通過 C 優化底層 IO 性能。這樣既保證了易用性, 又兼顧了性能。
- 圖形圖像處理
圖像處理算法利用 C/C++ 編寫, 並行計算由 Go 實現, 兩種語言各司其職, 共同完成任務。
七、優缺點和注意事項分析
Go 與 C/C++ 混合編程, 綜合了兩者的優勢, 但也需要注意一些問題。
優點包括:
提高性能: 計算密集型任務用 C/C++, 業務邏輯用 Go
複用代碼: 大量成熟 C/C++ 代碼可複用
降低難度: Go 易用性提高整體開發效率
缺點和注意事項:
類型轉換: 類型映射不完全, 需要處理
調試複雜: 跨語言代碼調用鏈導致 debug 難度增大
內存安全: C/C++ 代碼中可能出現各類內存問題
併發處理: C/C++ 部分需要注意線程安全
總體而言, Go 與 C/C++ 互補優勢多於競爭關係, 謹慎適當混合可以取得最好效果。
八、總結
通過 cgo 和 ABI,Go 語言可以非常便利地與 C/C++ 進行交互調用, 實現兩種語言的優勢互補。
數據類型基本可以無縫映射, 通過指針和 RPC 可高效通信。
Go 自動內存管理提高了安全性, C/C++ 手動優化可取得更高性能。
在網絡服務、計算任務等場景, 混合編程可以充分發揮各自語言的長處。
Go 與 C/C++ 的交互座標, 是當前跨語言編程的重要內容。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/b1DXVo3v5_t1UluXt7TAKw