[Golang] 程式設計教學:在 Golang 用使用 C 或 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 來重用程式碼。

    【分享文章】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤網站】
    Facebook Facebook Twitter Plurk
    【支持本站】
    Buy me a coffeeBuy me a coffee