C可以調用Go,並且Go可以調用C, 如果更進一步呢, C-->Go-->C
或者 Go-->C-->Go
的調用如何??
本文通過兩個簡單的例子協助你瞭解這兩種複雜的調用關係。本文不涉及兩者之間的複雜的資料轉換,官方文章C? Go? Cgo!、wiki/cgo和cmd/cgo有一些介紹。
Go-->C-->Go
Go程式調用C實現的函數,然後C實現的函數又調用Go實現的函數。
1、首先,我們建立一個hello.go
的檔案:
hello.go
123456789 |
package mainimport "C"import "fmt"//export HelloFromGofunc HelloFromGo() {fmt.Printf("Hello from Go!\n")} |
它定義了一個HelloFromGo
函數,注意這個函數是一個純的Go函數,我們定義它的輸出符號為HelloFromGo
。
2、接著我們建立一個hello.c
的檔案:
hello.c
123456789 |
#include <stdio.h>#include "_cgo_export.h"int helloFromC() { printf("Hi from C\n"); //call Go function HelloFromGo(); return 0;} |
這個c檔案定義了一個C函數helloFromC
,內部它會調用我們剛才定義的HelloFromGo
函數。
這樣,我們實現了C
調用Go
: C-->Go
,下面我們再實現Go調用C。
3、最後建立一個main.go
檔案:
main.go
1234567891011 |
package main/*extern int helloFromC();*/import "C"func main() {//call c functionC.helloFromC()} |
它調用第二步實現的C函數helloFromC
。
運行測試一下:
123 |
$ go run .Hi from CHello from Go! |
可以看到,期望的函數調用正常的運行。第一行是C函數的輸出,第二行是Go函數的輸出。
C-->Go-->C
第二個例子示範了C程式調用Go實現的函數,然後Go實現的函數又調用C實現的函數。
1、首先建立一個hello.c
檔案:
hello.c
123456 |
#include <stdio.h>int helloFromC() { printf("Hi from C\n"); return 0;} |
它定義了一個純C實現的函數。
2、接著建立一個hello.go
檔案:
12345678910111213141516171819 |
// go build -o hello.so -buildmode=c-shared .package main/*extern int helloFromC();*/import "C"import "fmt"//export HelloFromGofunc HelloFromGo() {fmt.Printf("Hello from Go!\n")C.helloFromC()}func main() {} |
它實現了一個Go函數HelloFromGo
,內部實現調用了C實現的函數helloFromC
,這樣我們就實現了Go-->C
。
注意包名設定為package main
,並且增加一個空的main
函數。
運行go build -o hello.so -buildmode=c-shared .
產生一個C可以調用的庫,這調命令執行完後會產生hello.so
檔案和hello.h
檔案。
3、最後建立一個檔案夾,隨便起個名字,比如main
將剛才產生的hello.so
檔案和hello.h
檔案複製到main
檔案夾,並在main
檔案夾中建立一個檔案main.c
:
main.c
12345678910 |
#include <stdio.h>#include "hello.h"int main() { printf("use hello lib from C:\n"); HelloFromGo(); return 0;} |
運行gcc -o main main.c hello.so
產生可執行檔main
, 運行main
:
1234 |
$ ./mainuse hello lib from C:Hello from Go!Hi from C |
第一行輸出來自main.c
,第二行來自Go函數,第三行來自hello.c
中的C函數,這樣我們就實現了C-->Go--C
的複雜調用。
C-->Go-->C
的狀態變數
我們來分析第二步中的一個特殊的情境, 為了下面我們好區分,我們給程式標記一下, 記為C1-->Go-->C2
, C2的程式修改一下,加入一個狀態變數a
,並且函數helloFromC
中會列印a
的地址和值,也會將a
加一。
hello.c
12345678 |
#include <stdio.h>int a = 1;int helloFromC() { printf("Hi from C: %p, %d\n", &a, a++); return 0;} |
然後修改main.c
程式,讓它既通過Go嗲用C1.helloFromC
,又直接調用C1.helloFromC
,看看多次調用的時候a
的指標是否一致,並且a
的值是否有變化。
main.c
1234567891011121314151617 |
#include <stdio.h>#include "hello.h"int main() { printf("use hello lib from C:\n"); // 1. 直接調用C函數 helloFromC(); // 2. 調用Go函數 HelloFromGo(); // 3. 直接調用C函數 helloFromC(); return 0;} |
激動人心的時候到了。我們不同的編譯方式會產生不同的結果。
1、gcc -o main main.c hello.so
和第二步相同的編譯方式,編譯出main
並執行, 因為hello.so
中包含C1.helloFromC
實現,所以可以正常執行。
123456 |
./mainuse hello lib from C:Hi from C: 0x10092a370, 1Hello from Go!Hi from C: 0x10092a370, 2Hi from C: 0x10092a370, 3 |
可以看到a
的指標是同一個值,無論通過Go函數改變還是通過C函數改變都是更改的同一個變數。
nm可以查看產生的main
的符號:
1234567 |
nm main U _HelloFromGo0000000100000000 T __mh_execute_header U _helloFromC0000000100000f10 T _main U _printf U dyld_stub_binder |
U
代表這個符號是未定義的符號,通過動態庫連結進來。
2、 gcc -o main main.c hello.so ../hello.c
我們編譯的時候直接連結hello.c
的實現,然後運行main
:
123456 |
./mainuse hello lib from C:Hi from C: 0x104888020, 1Hello from Go!Hi from C: 0x1049f7370, 1Hi from C: 0x104888020, 2 |
可以看到a
是不同的兩個變數。
nm可以查看產生的main
的符號:
12345678 |
nm main U _HelloFromGo0000000100000000 T __mh_execute_header0000000100001020 D _a0000000100000f10 T _helloFromC0000000100000ec0 T _main U _printf U dyld_stub_binder |
可以看到_a
是初始化的環境變數,_helloFromC
的類型是T
而不是U
,代表它是一個全域的Text符號,這和上一步是不一樣的。
參考文檔
- https://medium.com/using-go-in-mobile-apps/using-go-in-mobile-apps-part-1-calling-go-functions-from-c-be1ecf7dfbc6
- https://github.com/vladimirvivien/go-cshared-examples
- http://golang.org/cmd/cgo
- https://gist.github.com/zchee/b9c99695463d8902cd33
- https://medium.com/@liamkelly17/working-with-packed-c-structs-in-cgo-224a0a3b708b
- https://groups.google.com/forum/#!topic/golang-nuts/EhndTzcPJxQ
- https://docs.google.com/document/d/1nr-TQHw_er6GOQRsF6T43GGhFDelrAP0NqSS_00RgZQ/edit#
- https://www.mkssoftware.com/docs/man1/nm.1.asp