Go 調用 C-C-- 函數全攻略

一、Go 語言調用 C/C++ 函數

  1. 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 代碼中的函數, 兩個語言實現了無縫銜接。

  1. 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 語言編譯而成的動態庫。

  1. 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++ 函數。

  1. 數據類型映射及類型不安全

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 函數

  1. 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);
  1. 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 語言編寫的函數。

  1. Go 函數性能考量

與 Go 相比, C/C++ 函數調用性能更高, 而 Go 函數調用會有一定的性能損耗。

所以在混合編程時, 對性能敏感的功能應優先使用 C/C++ 實現, 非核心流程可以使用 Go 實現。

三、Go 與 C/C++ 的數據交換

  1. 數組及切片傳遞爲 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 指針, 兩種語言實現了無縫對接。

  1. 字符串處理差異及轉換

Go 中的字符串爲 UTF-8 編碼, 而 C/C++ 使用空終止符結束。字符串處理有一定區別。

簡單示例

cs := C.CString("Hello")
defer C.free(unsafe.Pointer(cs))
cstr := C.GoString(cs)

通過 C.CString 和 C.GoString 可以安全地在兩種字符串表示之間轉換。

  1. 結構體互相映射機制

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++ 的內存管理

  1. Go 自動內存回收機制

Go 語言採用高效的垃圾回收機制來自動管理內存。不需要開發者手動分配釋放內存, 避免了常見的內存錯誤, 例如泄漏、重用已釋放內存等。

Go GC 主要採用標記清除算法, 由垃圾回收器在後臺定期自動執行, 不會暫停應用程序線程。可以實現很低的 GC 延遲。

  1. C/C++ 手動內存管理

C/C++ 要求開發者手動動態分配和釋放堆內存。容易出現內存泄漏和重用懸空指針等嚴重 Bug。

智能指針等技術可部分緩解問題。

手動內存管理的好處是可以細粒度控制內存使用, 優化性能。且不存在 GC 停頓的問題。

  1. 內存安全和性能的權衡

Go 自動 GC 實現了內存安全防護, 降低了開發難度, 但是會有一定的性能損耗。

而 C/C++ 手動內存管理可以利用複雜手段優化內存和性能, 但開發難度大且易出錯。

綜合來說, Go 更適合互聯網服務端開發, 而 C/C++ 更擅長一些對性能要求極高的場景, 如遊戲、圖像處理等。

五、Go 與 C/C++ 進程間通信

  1. 基於文件 / 管道的 IPC

Go 和 C/C++ 可以通過文件、命名管道 (FIFO)、匿名管道實現進程間通信。

文件可實現數據持久化, 管道支持全雙工通信。都存在一定的時間和空間開銷。

  1. 基於 Socket 的網絡通信

Socket 套接字可實現高效的網絡數據傳輸, 支持任意倍擴展。是跨語言混合編程的理想 IPC 方式。

Go 語言實現 socket 編程很高效, 與 C/C++ 可完美配合。

  1. 基於 RPC 的跨語言調用

Go 語言和 C/C++ 都有成熟的 RPC 框架, 可以實現高性能的進程 / 網絡間通信, 構建分佈式系統。

例如 Go 語言的 gRPC 框架, 已經成爲 CNCF 基金會的畢業項目, 在雲原生領域得到廣泛應用。

六、Go 與 C/C++ 混合編程案例分析

  1. 異步 IO 網絡服務

服務端可以用 Go 實現, 利用 Go 協程實現高併發。再通過 C 優化底層 IO 性能。這樣既保證了易用性, 又兼顧了性能。

  1. 圖形圖像處理

圖像處理算法利用 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