位元詩人 [Golang] 程式設計教學:在 Golang 用使用 C 或 C++ 程式碼

GolangC 語言C++
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

雖然 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 來重用程式碼。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。