前言
雖然 Golang 是跨平台的編譯語言,寫起來相當方便,但我們不會把所有的程式碼都用 Golang 寫,主要的原因是效能和共用性。
Golang 是編譯語言,但和其他編譯語言比起來,Golang 的效能沒有很好。此外,Golang 內建垃圾回收機制,我們無法移除 Golang 的垃圾回收器,在程式中能對垃圾回收器所做的操作也不多。對於系統效能斤斤計較的系統語言來說,垃圾回收反而是一項缺點。
Golang 寫的函式庫,基本上只有 Golang 能用。由於 Golang 輸出 C API 的功能做得不是很好,如果想寫多語言共用的 C API,Golang 就不是一個很好的選擇。這時候,我們會回頭用 C,或是用 C++、Rust 等更好的替代方案。
此外,現存的 C 或 C++ 函式庫已經使用多年且運行良好,不會為了要使用 Golang 就重寫。反之,應該要讓 Golang 直接使用現有的 C 或 C++ 程式碼。Golang 官方團隊也有注意到這個議題,因而在 Golang 中引入 cgo 的機制。
實例:以 C 實作的 Point 型態
在本節中,我們使用平面座標的點為實例,來看 cgo 如何使用。雖然平面座標點很簡單,但簡單的實作正可突顯語法的使用方式,不需思考複雜的資料結構或演算法。
我們先來看 point.h 的宣告:
#ifndef POINT_H
#define POINT_H
typedef struct point_t point_t;
point_t * point_new(double x, double y);
void point_delete(void *self);
double point_x(point_t *self);
double point_y(point_t *self);
double point_distance(point_t *p, point_t *q);
#endif /* POINT_H */
基本上就是典型的物件 C 的寫法,對物件 C 有興趣的讀者可以看這篇文章。
接著來看 point.c 的實作:
#include <assert.h>
#include <math.h>
#include <stdlib.h>
#include "point.h"
struct point_t {
double x;
double y;
};
point_t * point_new(double x, double y)
{
point_t *pt = (point_t *) malloc(sizeof(point_t));
if (!pt)
return pt;
pt->x = x;
pt->y = y;
return pt;
}
void point_delete(void *self)
{
assert(self);
free(self);
}
double point_x(point_t *self)
{
assert(self);
return self->x;
}
double point_y(point_t *self)
{
assert(self);
return self->y;
}
double point_distance(point_t *p, point_t *q)
{
assert(p);
assert(q);
double dx = p->x - q->x;
double dy = p->y - q->y;
return sqrt(dx * dx + dy * dy);
}
這裡也沒有什麼複雜的程式碼,請讀者自行閱讀。
關鍵的地方在於如何用 cgo 橋接 C 程式碼。我們來看以下的 Golang 模組:
package point /* 1 */
// #include "point.h" /* 2 */
import "C" /* 3 */
import "unsafe" /* 4 */
type Point struct { /* 5 */
point *C.point_t /* 6 */
} /* 7 */
func NewPoint(x float64, y float64) *Point { /* 8 */
pt := new(Point) /* 9 */
pt.point = C.point_new(C.double(x), C.double(y)) /* 10 */
return pt /* 11 */
} /* 12 */
func (pt *Point) Delete() { /* 13 */
C.point_delete(unsafe.Pointer(pt.point)) /* 14 */
} /* 15 */
func (pt *Point) X() float64 { /* 16 */
return float64(C.point_x(pt.point)) /* 17 */
} /* 18 */
func (pt *Point) Y() float64 { /* 19 */
return float64(C.point_y(pt.point)) /* 20 */
} /* 21 */
func Distance(p *Point, q *Point) float64 { /* 22 */
return float64(C.point_distance(p.point, q.point)) /* 23 */
} /* 24 */
第 2 行至第 3 行的部分是 cgo 程式碼。cgo 為了要相容於 Golang,把程式碼寫在註解裡。對於正規的 Golang 程式碼來說,cgo 的部分只是註解。cgo 引入 C 模組後,會以 C
做為前綴來呼叫 C 型態和函式。
第 4 行引入 unsafe
模組。我們會用到 unsafe.Pointer 將指標型態轉型,之後就可以把該指標視為 void *
指標。
為了操作方便,我們用額外的 Golang 結構體 Point
將 C 結構體 point
包起來。將 C 結構體包起來之後,我們呼叫 C 函式的髒活就可以封裝在函式中,外部程式使用起來和一般的 Golang 程式無異。Golang 結構體宣告的部分位於第 5 行至第 7 行。
大部分的 C 程式碼都可以無縫接軌到 Golang 函式上,但在 Golang 函式中一定要額外製做解構函式,像是本模組的第 13 行至第 15 行。因為 Golang 沒有設計解構函式的機制,所以我們只得自行製作,並在外部程式明確地呼叫該函式。
使用本模組的外部程式如下:
package main /* 1 */
import ( /* 2 */
"go-c-mix/point" /* 3 */
"log" /* 4 */
) /* 5 */
func main() { /* 6 */
p := point.NewPoint(0.0, 0.0) /* 7 */
q := point.NewPoint(3.0, 4.0) /* 8 */
dist := point.Distance(p, q) /* 9 */
if 5 != dist { /* 10 */
log.Fatal("Wrong distance") /* 11 */
} /* 12 */
p.Delete() /* 13 */
q.Delete() /* 14 */
} /* 15 */
除了在第 13 行及第 14 行需要手動釋放記憶體外,這段程式和一般用純 Golang 實作的程式無異。
我們將本節的完整程式碼放在這裡,有興趣的讀者可以看一下。
實例:以 C++ 實作的 Point 型態
cgo 無法直接使用 C++ API,只能使用 C API,這點和大部分的高階語言是類似的。因應的方式是額外寫 C API 把 C++ API 包起來。用 C API 包 C++ API 的方式不是 cgo 限定的方式,在其他的高階語言也可以用,算是蠻實用的技能。
我們承接上一節的主題,假定平面座標點是以 C++ 實作,現在要給 Golang 使用,所以中間要額外寫一層 C API。
以下是 point.hpp 的宣告:
#ifndef POINT_HPP
#define POINT_HPP
class Point {
public:
Point(double x, double y);
double x();
double y();
static double distance(Point *p, Point *q);
private:
double _x;
double _y;
};
#endif /* POINT_HPP */
由於 C++ 有原生的類別,我們就不需要再以結構體模擬類別了。
接著來看 point.cpp 的實作:
#include <cmath>
#include "point.hpp"
Point::Point(double x, double y)
{
this->_x = x;
this->_y = y;
}
double Point::x()
{
return this->_x;
}
double Point::y()
{
return this->_y;
}
double Point::distance(Point *p, Point *q)
{
double dx = p->x() - q->x();
double dy = p->y() - q->y();
return sqrt(dx * dx + dy * dy);
}
除了把語言從 C 換成 C++ 外,並沒有什麼困難的地方,請讀者自行閱讀。
point.h 的宣告和上一節相同。注意在製作 C API 時,不能引入 C++ 特有的語法,只能用純 C 語法去宣告和實作。
接著來看 cpoint.cpp 部分的實作:
#include <cassert>
#include <cstdlib>
#include "point.h"
#include "point.hpp"
struct point_t {
Point *obj;
};
point_t * point_new(double x, double y)
{
point_t *pt = (point_t *) malloc(sizeof(point_t));
if (!pt)
return NULL;
pt->obj = new Point(x, y);
return pt;
}
void point_delete(void *self)
{
assert(self);
Point *point = ((point_t *) self)->obj;
delete point;
free(self);
}
double point_x(point_t *self)
{
assert(self);
return self->obj->x();
}
double point_y(point_t *self)
{
assert(self);
return self->obj->y();
}
double point_distance(point_t *p, point_t *q)
{
assert(p);
assert(q);
return Point::distance(p->obj, q->obj);
}
cpoint.cpp 不負責實作,只是用來橋接 C++ 程式碼,所以撰寫方式和前一節的 point.c 差異很大。
Golang 程式碼的部分和前一節雷同,這裡就不重覆展示。對於 cgo 來說,平面座標點的公開界面是相同的,我們在本節中所做的操作是把內部實作從純 C 抽換成 C++。我們把完整的程式碼放在這裡,有興趣的讀者可以自行追蹤一下。
用 cgo 寫 Golang binding
除了用 cgo 來引入自己寫的 C 或 C++ 程式碼外,cgo 更大的意義是用來做 Golang binding。例如,以下程式碼節錄自 gotk3 套件 (GTK3 的 Golang binding) 中的 glib.go:
// #cgo pkg-config: gio-2.0 glib-2.0 gobject-2.0
// #include <gio/gio.h>
// #include <glib.h>
// #include <glib-object.h>
// #include "glib.go.h"
import "C"
由此可知,gotk3 的 glib 子套件是引用系統上的 GLib、GObject、GIO 等函式庫內的 C 程式碼,而非從頭撰寫新的 Golang 程式碼。
結語
在本文中,我們展示了在 Golang 中使用 C 或 C++ 程式碼的方式。透過 cgo,我們可以直接使用 C 或 C++ 生態圈的龐大資產,而不用移植程式。
在這個高階語言爆炸的年代,不可能換個語言就重寫程式。學會 C 的知識後,就可以用來寫其他高階語言的 binding。在本文中,我們介紹 cgo,因為我們想在 Golang 中使用 C 或 C++ 程式碼。但我們可以將這些知識應用在其他高階語言上,就可以藉由 C API 來重用程式碼。