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