原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-01-hello-cgo.html
本節(jié)我們將通過(guò)一系列由淺入深的小例子來(lái)快速掌握 CGO 的基本用法。
真實(shí)的 CGO 程序一般都比較復(fù)雜。不過(guò)我們可以由淺入深,一個(gè)最簡(jiǎn)的 CGO 程序該是什么樣的呢?要構(gòu)造一個(gè)最簡(jiǎn) CGO 程序,首先要忽視一些復(fù)雜的 CGO 特性,同時(shí)要展示 CGO 程序和純 Go 程序的差別來(lái)。下面是我們構(gòu)建的最簡(jiǎn) CGO 程序:
// hello.go
package main
import "C"
func main() {
println("hello cgo")
}
代碼通過(guò) import "C"
語(yǔ)句啟用 CGO 特性,主函數(shù)只是通過(guò) Go 內(nèi)置的 println 函數(shù)輸出字符串,其中并沒(méi)有任何和 CGO 相關(guān)的代碼。雖然沒(méi)有調(diào)用 CGO 的相關(guān)函數(shù),但是 go build
命令會(huì)在編譯和鏈接階段啟動(dòng) gcc 編譯器,這已經(jīng)是一個(gè)完整的 CGO 程序了。
第一章那個(gè) CGO 程序還不夠簡(jiǎn)單,我們現(xiàn)在來(lái)看看更簡(jiǎn)單的版本:
// hello.go
package main
//#include <stdio.h>
import "C"
func main() {
C.puts(C.CString("Hello, World\n"))
}
我們不僅僅通過(guò) import "C"
語(yǔ)句啟用 CGO 特性,同時(shí)包含 C 語(yǔ)言的 <stdio.h>
頭文件。然后通過(guò) CGO 包的 C.CString
函數(shù)將 Go 語(yǔ)言字符串轉(zhuǎn)為 C 語(yǔ)言字符串,最后調(diào)用 CGO 包的 C.puts
函數(shù)向標(biāo)準(zhǔn)輸出窗口打印轉(zhuǎn)換后的 C 字符串。
相比 “Hello, World 的革命” 一節(jié)中的 CGO 程序最大的不同是:我們沒(méi)有在程序退出前釋放 C.CString
創(chuàng)建的 C 語(yǔ)言字符串;還有我們改用 puts
函數(shù)直接向標(biāo)準(zhǔn)輸出打印,之前是采用 fputs
向標(biāo)準(zhǔn)輸出打印。
沒(méi)有釋放使用 C.CString
創(chuàng)建的 C 語(yǔ)言字符串會(huì)導(dǎo)致內(nèi)存泄漏。但是對(duì)于這個(gè)小程序來(lái)說(shuō),這樣是沒(méi)有問(wèn)題的,因?yàn)槌绦蛲顺龊蟛僮飨到y(tǒng)會(huì)自動(dòng)回收程序的所有資源。
前面我們使用了標(biāo)準(zhǔn)庫(kù)中已有的函數(shù)?,F(xiàn)在我們先自定義一個(gè)叫 SayHello
的 C 函數(shù)來(lái)實(shí)現(xiàn)打印,然后從 Go 語(yǔ)言環(huán)境中調(diào)用這個(gè) SayHello
函數(shù):
// hello.go
package main
/*
#include <stdio.h>
static void SayHello(const char* s) {
puts(s);
}
*/
import "C"
func main() {
C.SayHello(C.CString("Hello, World\n"))
}
除了 SayHello
函數(shù)是我們自己實(shí)現(xiàn)的之外,其它的部分和前面的例子基本相似。
我們也可以將 SayHello
函數(shù)放到當(dāng)前目錄下的一個(gè) C 語(yǔ)言源文件中(后綴名必須是 .c
)。因?yàn)槭蔷帉懺讵?dú)立的 C 文件中,為了允許外部引用,所以需要去掉函數(shù)的 static
修飾符。
// hello.c
#include <stdio.h>
void SayHello(const char* s) {
puts(s);
}
然后在 CGO 部分先聲明 SayHello
函數(shù),其它部分不變:
// hello.go
package main
//void SayHello(const char* s);
import "C"
func main() {
C.SayHello(C.CString("Hello, World\n"))
}
注意,如果之前運(yùn)行的命令是 go run hello.go
或 go build hello.go
的話,此處須使用 go run "your/package"
或 go build "your/package"
才可以。若本就在包路徑下的話,也可以直接運(yùn)行 go run .
或 go build
。
既然 SayHello
函數(shù)已經(jīng)放到獨(dú)立的 C 文件中了,我們自然可以將對(duì)應(yīng)的 C 文件編譯打包為靜態(tài)庫(kù)或動(dòng)態(tài)庫(kù)文件供使用。如果是以靜態(tài)庫(kù)或動(dòng)態(tài)庫(kù)方式引用 SayHello
函數(shù)的話,需要將對(duì)應(yīng)的 C 源文件移出當(dāng)前目錄(CGO 構(gòu)建程序會(huì)自動(dòng)構(gòu)建當(dāng)前目錄下的 C 源文件,從而導(dǎo)致 C 函數(shù)名沖突)。關(guān)于靜態(tài)庫(kù)等細(xì)節(jié)將在稍后章節(jié)講解。
在編程過(guò)程中,抽象和模塊化是將復(fù)雜問(wèn)題簡(jiǎn)化的通用手段。當(dāng)代碼語(yǔ)句變多時(shí),我們可以將相似的代碼封裝到一個(gè)個(gè)函數(shù)中;當(dāng)程序中的函數(shù)變多時(shí),我們將函數(shù)拆分到不同的文件或模塊中。而模塊化編程的核心是面向程序接口編程(這里的接口并不是 Go 語(yǔ)言的 interface,而是 API 的概念)。
在前面的例子中,我們可以抽象一個(gè)名為 hello 的模塊,模塊的全部接口函數(shù)都在 hello.h 頭文件定義:
// hello.h
void SayHello(const char* s);
其中只有一個(gè) SayHello 函數(shù)的聲明。但是作為 hello 模塊的用戶來(lái)說(shuō),就可以放心地使用 SayHello 函數(shù),而無(wú)需關(guān)心函數(shù)的具體實(shí)現(xiàn)。而作為 SayHello 函數(shù)的實(shí)現(xiàn)者來(lái)說(shuō),函數(shù)的實(shí)現(xiàn)只要滿足頭文件中函數(shù)的聲明的規(guī)范即可。下面是 SayHello 函數(shù)的 C 語(yǔ)言實(shí)現(xiàn),對(duì)應(yīng) hello.c 文件:
// hello.c
#include "hello.h"
#include <stdio.h>
void SayHello(const char* s) {
puts(s);
}
在 hello.c 文件的開(kāi)頭,實(shí)現(xiàn)者通過(guò) #include "hello.h"
語(yǔ)句包含 SayHello 函數(shù)的聲明,這樣可以保證函數(shù)的實(shí)現(xiàn)滿足模塊對(duì)外公開(kāi)的接口。
接口文件 hello.h 是 hello 模塊的實(shí)現(xiàn)者和使用者共同的約定,但是該約定并沒(méi)有要求必須使用 C 語(yǔ)言來(lái)實(shí)現(xiàn) SayHello 函數(shù)。我們也可以用 C++ 語(yǔ)言來(lái)重新實(shí)現(xiàn)這個(gè) C 語(yǔ)言函數(shù):
// hello.cpp
#include <iostream>
extern "C" {
#include "hello.h"
}
void SayHello(const char* s) {
std::cout << s;
}
在 C++ 版本的 SayHello 函數(shù)實(shí)現(xiàn)中,我們通過(guò) C++ 特有的 std::cout
輸出流輸出字符串。不過(guò)為了保證 C++ 語(yǔ)言實(shí)現(xiàn)的 SayHello 函數(shù)滿足 C 語(yǔ)言頭文件 hello.h 定義的函數(shù)規(guī)范,我們需要通過(guò) extern "C"
語(yǔ)句指示該函數(shù)的鏈接符號(hào)遵循 C 語(yǔ)言的規(guī)則。
在采用面向 C 語(yǔ)言 API 接口編程之后,我們徹底解放了模塊實(shí)現(xiàn)者的語(yǔ)言枷鎖:實(shí)現(xiàn)者可以用任何編程語(yǔ)言實(shí)現(xiàn)模塊,只要最終滿足公開(kāi)的 API 約定即可。我們可以用 C 語(yǔ)言實(shí)現(xiàn) SayHello 函數(shù),也可以使用更復(fù)雜的 C++ 語(yǔ)言來(lái)實(shí)現(xiàn) SayHello 函數(shù),當(dāng)然我們也可以用匯編語(yǔ)言甚至 Go 語(yǔ)言來(lái)重新實(shí)現(xiàn) SayHello 函數(shù)。
其實(shí) CGO 不僅僅用于 Go 語(yǔ)言中調(diào)用 C 語(yǔ)言函數(shù),還可以用于導(dǎo)出 Go 語(yǔ)言函數(shù)給 C 語(yǔ)言函數(shù)調(diào)用。在前面的例子中,我們已經(jīng)抽象一個(gè)名為 hello 的模塊,模塊的全部接口函數(shù)都在 hello.h 頭文件定義:
// hello.h
void SayHello(/*const*/ char* s);
現(xiàn)在我們創(chuàng)建一個(gè) hello.go 文件,用 Go 語(yǔ)言重新實(shí)現(xiàn) C 語(yǔ)言接口的 SayHello 函數(shù):
// hello.go
package main
import "C"
import "fmt"
//export SayHello
func SayHello(s *C.char) {
fmt.Print(C.GoString(s))
}
我們通過(guò) CGO 的 //export SayHello
指令將 Go 語(yǔ)言實(shí)現(xiàn)的函數(shù) SayHello
導(dǎo)出為 C 語(yǔ)言函數(shù)。為了適配 CGO 導(dǎo)出的 C 語(yǔ)言函數(shù),我們禁止了在函數(shù)的聲明語(yǔ)句中的 const 修飾符。需要注意的是,這里其實(shí)有兩個(gè)版本的 SayHello
函數(shù):一個(gè) Go 語(yǔ)言環(huán)境的;另一個(gè)是 C 語(yǔ)言環(huán)境的。cgo 生成的 C 語(yǔ)言版本 SayHello
函數(shù)最終會(huì)通過(guò)橋接代碼調(diào)用 Go 語(yǔ)言版本的 SayHello 函數(shù)。
通過(guò)面向 C 語(yǔ)言接口的編程技術(shù),我們不僅僅解放了函數(shù)的實(shí)現(xiàn)者,同時(shí)也簡(jiǎn)化的函數(shù)的使用者?,F(xiàn)在我們可以將 SayHello 當(dāng)作一個(gè)標(biāo)準(zhǔn)庫(kù)的函數(shù)使用(和 puts 函數(shù)的使用方式類似):
package main
//#include <hello.h>
import "C"
func main() {
C.SayHello(C.CString("Hello, World\n"))
}
一切似乎都回到了開(kāi)始的 CGO 代碼,但是代碼內(nèi)涵更豐富了。
在開(kāi)始的例子中,我們的全部 CGO 代碼都在一個(gè) Go 文件中。然后,通過(guò)面向 C 接口編程的技術(shù)將 SayHello 分別拆分到不同的 C 文件,而 main 依然是 Go 文件。再然后,是用 Go 函數(shù)重新實(shí)現(xiàn)了 C 語(yǔ)言接口的 SayHello 函數(shù)。但是對(duì)于目前的例子來(lái)說(shuō)只有一個(gè)函數(shù),要拆分到三個(gè)不同的文件確實(shí)有些繁瑣了。
正所謂合久必分、分久必合,我們現(xiàn)在嘗試將例子中的幾個(gè)文件重新合并到一個(gè) Go 文件。下面是合并后的成果:
package main
//void SayHello(char* s);
import "C"
import (
"fmt"
)
func main() {
C.SayHello(C.CString("Hello, World\n"))
}
//export SayHello
func SayHello(s *C.char) {
fmt.Print(C.GoString(s))
}
現(xiàn)在版本的 CGO 代碼中 C 語(yǔ)言代碼的比例已經(jīng)很少了,但是我們依然可以進(jìn)一步以 Go 語(yǔ)言的思維來(lái)提煉我們的 CGO 代碼。通過(guò)分析可以發(fā)現(xiàn) SayHello
函數(shù)的參數(shù)如果可以直接使用 Go 字符串是最直接的。在 Go1.10 中 CGO 新增加了一個(gè) _GoString_
預(yù)定義的 C 語(yǔ)言類型,用來(lái)表示 Go 語(yǔ)言字符串。下面是改進(jìn)后的代碼:
// +build go1.10
package main
//void SayHello(_GoString_ s);
import "C"
import (
"fmt"
)
func main() {
C.SayHello("Hello, World\n")
}
//export SayHello
func SayHello(s string) {
fmt.Print(s)
}
雖然看起來(lái)全部是 Go 語(yǔ)言代碼,但是執(zhí)行的時(shí)候是先從 Go 語(yǔ)言的 main
函數(shù),到 CGO 自動(dòng)生成的 C 語(yǔ)言版本 SayHello
橋接函數(shù),最后又回到了 Go 語(yǔ)言環(huán)境的 SayHello
函數(shù)。這個(gè)代碼包含了 CGO 編程的精華,讀者需要深入理解。
思考題: main 函數(shù)和 SayHello 函數(shù)是否在同一個(gè) Goroutine 里執(zhí)行?
![]() | ![]() |
更多建議: