go語言與c語言的相互呼叫

p了個c發表於2019-04-09

最近由於工作原因,需要實現go語言與c語言的相互呼叫。由於go語言與c語言有著千絲萬縷的曖昧關係,兩者之間的呼叫可以通過語言層面實現。下文是對此的總結。

go語言呼叫c語言

以下為一個簡短的例子:

package main

// #include <stdio.h>
// #include <stdlib.h>
/*
void print(char *str) {
    printf("%s
", str);
}
*/
import "C"

import "unsafe"

func main() {
    s := "Hello Cgo"
    cs := C.CString(s)
    C.print(cs)
    C.free(unsafe.pointer(cs))
}

與“正常”的go程式碼相比,上述程式碼有幾處“特殊”的地方:

  1. 在開頭的註釋中出現了c語言標頭檔案的include字樣
  2. 在註釋中定義了c語言函式print
  3. import了一個名為C的“包”
  4. 在main函式中呼叫了上述定義的c語言函式print

首先,go原始碼檔案中的c語言程式碼是需要用註釋包裹的,就像上面的include標頭檔案以及print函式定義;其次,import “C”這個語句是必須的,而且其與上面的c程式碼之間不能用空行分隔,必須緊密相連。這裡的”C“不是包名,而是一種類似名字空間的概念,或可以理解為偽包,c語言所有語法元素均在該偽包下面;最後,訪問c語法元素時都要在其前面加上偽包字首,比如C.uint和上面程式碼中的C.print、C.free等。

更詳細的在go中呼叫c語言的用法可以參考Go與C語言的互操作,本文不再一一細述。

在上面的例子中,c語言是內嵌在go程式碼中的,如果程式碼量更大更復雜的話,這顯然是很不”專業“的。那麼,是否可以將c語言程式碼從go程式碼中分離出去,單獨定義呢?答案是肯定的,可以通過共享庫的方式實現。

cgo提供了#cgo指示符可以指定go原始碼在編譯後與哪些共享庫進行連結。例子如下:

// hello.go
package main

// #cgo LDFLAGS: -L ./ -lhello
// #include <stdio.h>
// #include <stdlib.h>
// #include "hello.h"
import "C"

func main() {
    C.hello()
}

// hello.c
#include "hello.h"

void hello()
{
    printf("hello, go
");
}

// hello.h
extern void hello();

其中在hello.go中,#cgo指示符後面新增LDFLAGS: -L ./ -lhello,作用是在go程式碼編譯時,指定在當前目錄查詢so庫並進行連結。

因此,只需要把hello.c編譯成動態庫,再編譯go程式碼,即可在執行go程式碼的時候呼叫共享庫中的c語言函式。指令如下:

  1. gcc -fPIC -o libhello.so hello.c
  2. go build -o hello
  3. ./hello

c語言呼叫go語言

與在go中呼叫c原始碼相比,在c中使用go函式的場合較少。因為一般來說,採用高階語言作為粘合劑呼叫低階語言能充分發揮各自的特點,而用低階語言呼叫高階語言反而有可能降低低階語言的效能優勢,在go中,可以使用”export + 函式名“來匯出go函式為c程式碼所用,看一個簡單的例子:

// hello.go
package main

import "C"

import "fmt"

// export Go2C
func Go2C() {
    fmt.Println("hello, C")
}

可通過go build的編譯選項,將go程式碼編譯成共享庫以供c程式碼呼叫。注意,編譯so庫時必須存在main及main函式(即使main函式為空)。編譯指令如下:go build -v -x -buildmode=c-shared -o libhello.so hello.go

編譯成功後,只需在c程式碼中引入新生成的標頭檔案及編譯時連結動態庫即可實現go函式的呼叫。程式碼如下:

// hello.c
#include <stdio.h>
#include "libhello.h"

int main()
{
    Go2C();
    return 0;
}

通過gcc -o hello -L. -lhello,即可編譯成可執行程式。注意,執行前必須確定共享庫執行時查詢路徑中存在需要連結的共享庫,可通過將so庫路徑放到/usr/lib或者修改環境變數LD_LIBRARY_PATH。

小結

go語言可以通過內嵌c程式碼的形式呼叫c語言,也可以通過呼叫共享庫函式的方式實現;至於c語言呼叫go函式,則可以通過go build將go程式碼編譯成共享庫提供給c程式碼使用。注意,本文中的共享庫均為動態共享庫,至於靜態共享庫則未曾實驗,有興趣的可以實現一下。

相關文章