Skip to content

CGO

Karena Go memerlukan GC (Garbage Collection), untuk beberapa skenario yang membutuhkan performa lebih tinggi, Go mungkin kurang cocok. C sebagai bahasa pemrograman sistem tradisional memiliki performa yang sangat baik, dan cgo dapat menghubungkan keduanya, memungkinkan saling panggilan. Go dapat memanggil C untuk menangani tugas yang sensitif terhadap performa, sementara Go menangani logika tingkat atas. Cgo juga mendukung C memanggil Go, meskipun skenario ini jarang terjadi dan tidak terlalu disarankan.

TIP

Kode dalam artikel ini didemonstrasikan di lingkungan Windows 10, menggunakan gitbash untuk command line. Pengguna Windows disarankan untuk menginstal mingw terlebih dahulu.

Untuk cgo, terdapat pengenalan singkat dari官方: C? Go? Cgo! - The Go Programming Language. Jika ingin penjelasan lebih detail, dapat dilihat di paket standar cmd/cgo/doc.go, atau langsung melihat dokumentasi cgo command - cmd/cgo - Go Packages. Kedua konten tersebut sama.

Pemanggilan Kode

Lihat contoh berikut

go
package main

//#include <stdio.h>
import "C"

func main() {
  C.puts(C.CString("hello, cgo!"))
}

Untuk menggunakan fitur cgo, cukup dengan statement import import "C" untuk mengaktifkannya. Perlu diperhatikan bahwa C harus huruf besar, dan nama import tidak dapat diubah. Selain itu, perlu memastikan variabel environment CGO_ENABLED diset ke 1. Secara default, variabel environment ini sudah aktif.

bash
$ go env | grep  CGO
$ go env -w CGO_ENABLED=1

Selain itu, perlu memastikan memiliki toolchain build C/C++ di lokal, yaitu gcc. Di platform Windows adalah mingw, agar program dapat dikompilasi dengan normal. Jalankan command berikut untuk kompilasi. Dengan mengaktifkan cgo, waktu kompilasi akan lebih lama dibandingkan Go murni.

bash
$ go build -o ./ main.go
$ ./main.exe
hello, cgo!

Hal lain yang perlu diperhatikan adalah setelah mengaktifkan cgo, tidak akan mendukung cross-compile.

Menyisipkan Kode C di Go

Cgo mendukung penulisan kode C langsung di source file Go, lalu langsung dipanggil. Lihat contoh berikut, contoh ini membuat fungsi bernama printSum, lalu dipanggil di fungsi main Go.

go
package main

/*
#include <stdio.h>
void printSum(int a, int b) {
  printf("c:%d+%d=%d",a,b,a+b);
}
*/
import "C"

func main() {
  C.printSum(C.int(1), C.int(2))
}

Output

c:1+2=3

Ini cocok untuk skenario sederhana. Jika kode C sangat banyak dan tercampur dengan kode Go akan sangat mengurangi keterbacaan, maka tidak cocok dilakukan seperti ini.

Penanganan Error

Dalam bahasa Go, penanganan error dikembalikan sebagai nilai return, tetapi bahasa C tidak mengizinkan multi-return. Untuk ini dapat menggunakan errno di C, yang menunjukkan terjadi error selama pemanggilan fungsi. Cgo telah melakukan kompatibilitas untuk ini, saat memanggil fungsi C dapat menangani error seperti di Go. Untuk menggunakan errno, pertama import errno.h, lihat contoh berikut

go
package main

/*
#include <stdio.h>
#include <stdint.h>
#include <errno.h>

int32_t sum_positive(int32_t a, int32_t b) {
  if (a <= 0 || b <= 0) {
    errno = EINVAL;
    return 0;
  }
  return a + b;
}

*/
import "C"
import (
  "fmt"
  "reflect"
)

func main() {
  sum, err := C.sum_positive(C.int32_t(0), C.int32_t(1))
  if err != nil {
    fmt.Println(reflect.TypeOf(err))
    fmt.Println(err)
    return
  }
  fmt.Println(sum)
}

Output

syscall.Errno
The device does not recognize the command.

Dapat dilihat tipe errornya adalah syscall.Errno. errno.h juga mendefinisikan banyak kode error lainnya, dapat dipelajari sendiri.

Mengimport File C di Go

Dengan mengimport file C, dapat menyelesaikan masalah di atas dengan baik. Pertama buat file header sum.h, kontennya sebagai berikut

c
int sum(int a, int b);

Kemudian buat sum.c, tulis fungsi spesifik

c
#include "sum.h"

int sum(int a, int b) {
    return a + b;
}

Lalu import file header di main.go

go
package main

//#include "sum.h"
import "C"
import "fmt"

func main() {
  res := C.sum(C.int(1), C.int(2))
  fmt.Printf("cgo sum: %d\n", res)
}

Sekarang untuk kompilasi, harus menentukan folder saat ini, jika tidak akan找不到 file C

$ go build -o sum.exe . && ./sum.exe
cgo sum: 3

Dalam kode, res adalah variabel di Go, C.sum adalah fungsi di bahasa C. Nilai returnnya adalah int di C bukan int di Go. Alasan dapat berhasil dipanggil adalah karena cgo melakukan konversi tipe dari sana.

C Memanggil Go

C memanggil Go yang dimaksud adalah C memanggil Go di cgo, bukan program C native memanggil Go. Rantai panggilannya adalah go-cgo-c->cgo->go. Go memanggil C untuk memanfaatkan ekosistem dan performa C, hampir tidak ada kebutuhan program C native memanggil Go. Jika ada, juga disarankan menggunakan komunikasi jaringan sebagai gantinya.

Cgo mendukung ekspor fungsi Go untuk dipanggil C. Jika ingin mengekspor fungsi Go, perlu menambahkan komentar //export func_name di atas signature fungsi, dan parameter serta nilai returnnya harus tipe yang didukung cgo. Contoh sebagai berikut

go
//export sum
func sum(a, b C.int32_t) C.int32_t {
  return a + b
}

Ubah file sum.c sebelumnya menjadi konten berikut

c
#include <stdint.h>
#include <stdio.h>
#include "sum.h"
#include "_cgo_export.h"

extern int32_t sum(int32_t a, int32_t b);

void do_sum() {
  int32_t a = 10;
  int32_t b = 10;
  int32_t c = sum(a, b);

  printf("%d", c);
}

Sekaligus ubah file header sum.h

void do_sum();

Lalu ekspor fungsi di Go

go
package main

/*
#include <stdio.h>
#include <stdint.h>
#include "sum.h"
*/
import "C"

func main() {
  C.do_sum()
}

//export sum
func sum(a, b C.int32_t) C.int32_t {
  return a + b
}

Sekarang fungsi sum yang digunakan di C sebenarnya disediakan oleh Go. Output hasil sebagai berikut

20

Kunci utamanya adalah _cgo_export.h yang diimport di file sum.c, file ini berisi semua tipe yang diekspor Go. Jika tidak mengimport maka tidak dapat menggunakan fungsi yang diekspor Go. Hal lain yang perlu diperhatikan adalah _cgo_export.h tidak dapat diimport di file Go, karena prasyarat file header ini dihasilkan adalah semua source file Go harus dapat dikompilasi. Oleh karena itu penulisan seperti ini salah

go
package main

/*
#include <stdint.h>
#include <stdio.h>
#include "_cgo_export.h"

void do_sum() {
  int32_t a = 10;
  int32_t b = 10;
  int32_t c = sum(a, b);

  printf("%d", c);
}
*/
import "C"

func main() {
  C.do_sum()
}

//export sum
func sum(a, b C.int32_t) C.int32_t {
  return a + b
}

Compiler akan memberitahu file header tidak ada

fatal error: _cgo_export.h: No such file or directory
 #include "_cgo_export.h"
          ^~~~~~~~~~~~~~~
compilation terminated.

Jika fungsi Go memiliki banyak nilai return, maka C akan mengembalikan struct saat dipanggil.

Sekadar informasi, kita dapat meneruskan pointer Go ke C melalui parameter fungsi C. Selama pemanggilan fungsi C, cgo akan berusaha menjamin keamanan memori. Namun nilai return fungsi Go yang diekspor tidak boleh berupa pointer, karena dalam kasus ini cgo tidak dapat menentukan apakah pointer tersebut direferensikan, dan juga sulit untuk mem-fix memori. Jika memori yang dikembalikan direferensikan, lalu di Go memori tersebut di-GC atau terjadi offset, akan menyebabkan pointer out of bounds, seperti berikut.

go
//export newCharPtr
func newCharPtr() *C.char {
  return new(C.char)
}

Penulisan di atas secara default tidak diizinkan untuk dikompilasi. Jika ingin menonaktifkan pemeriksaan ini, dapat diset seperti berikut.

GODEBUG=cgocheck=0

Ada dua level pemeriksaan, dapat diset 1, 2. Semakin tinggi level, semakin besar overhead runtime yang disebabkan. Dapat前往 cgo command - passing_pointer untuk了解 detail.

Konversi Tipe

Cgo melakukan pemetaan tipe antara C dan Go, memudahkan pemanggilan saat runtime. Untuk tipe di C, setelah mengimport import "C" di Go, sebagian besar dapat langsung diakses melalui

C.typename

Cara ini, misalnya

C.int(1)
C.char('a')

Namun tipe C dapat terdiri dari banyak keyword, misalnya

unsigned char

Kasus seperti ini tidak dapat diakses langsung, tetapi dapat menggunakan keyword typedef di C untuk memberikan alias pada tipe, fungsinya setara dengan alias tipe di Go. Sebagai berikut

c
typedef unsigned char byte;

Dengan demikian, dapat mengakses tipe unsigned char melalui C.byte. Contoh sebagai berikut

go
package main

/*
#include <stdio.h>

typedef unsigned char byte;

void printByte(byte b) {
  printf("%c\n",b);
}
*/
import "C"

func main() {
  C.printByte(C.byte('a'))
  C.printByte(C.byte('b'))
  C.printByte(C.byte('c'))
}

Output

a
b
c

Sebagian besar kasus, cgo sudah memberikan alias untuk tipe yang umum digunakan (seperti tipe dasar), juga dapat mendefinisikan sendiri sesuai metode di atas, tidak akan konflik.

char

char di C sesuai dengan tipe int8 di Go, unsigned char sesuai dengan uint8 di Go yaitu tipe byte.

go
package main

/*
#include <stdio.h>
#include<complex.h>

char ch;

char get() {
  return ch;
}

void set(char c) {
  ch = c;
}
*/
import "C"
import (
  "fmt"
  "reflect"
)

func main() {
  C.set(C.char('c'))
  res := C.get()
  fmt.Printf("type: %s, val: %v", reflect.TypeOf(res), res)
}

Output

type: main._Ctype_char, val: 99

Jika parameter set diganti dengan C.char(math.MaxInt8 + 1), maka kompilasi akan gagal, dan menampilkan error berikut

cannot convert math.MaxInt8 + 1 (untyped int constant 128) to type _Ctype_char

String

Cgo menyediakan beberapa pseudo-fungsi untuk meneruskan string dan slice byte antara C dan Go. Fungsi-fungsi ini sebenarnya tidak ada, tidak dapat menemukan definisinya, sama seperti import "C", paket C juga tidak ada, hanya untuk memudahkan developer. Setelah kompilasi, mereka akan dikonversi menjadi operasi lain.

go
// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char

// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer

// C string to Go string
func C.GoString(*C.char) string

// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string

// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

String di Go pada dasarnya adalah struct, di dalamnya memegang referensi array底层. Saat diteruskan ke fungsi C, perlu menggunakan C.CString() untuk membuat "string" di C menggunakan malloc, mengalokasikan ruang memori, lalu mengembalikan pointer C. Karena C tidak memiliki tipe string, biasanya menggunakan char* untuk merepresentasikan string, yaitu pointer ke array char. Ingat untuk menggunakan free untuk melepaskan memori setelah selesai.

go
package main

/*
#include <stdio.h>
#include <stdlib.h>

void printfGoString(char* s) {
  puts(s);
}
*/
import "C"
import "unsafe"

func main() {
  cstring := C.CString("this is a go string")
  C.printfGoString(cstring)
  C.free(unsafe.Pointer(cstring))
}

Bisa juga tipe array char, keduanya sebenarnya sama, sama-sama pointer ke elemen head.

c
void printfGoString(char s[]) {
  puts(s);
}

Bisa juga meneruskan slice byte. Karena C.CBytes() mengembalikan unsafe.Pointer, perlu dikonversi ke tipe *C.char sebelum diteruskan ke fungsi C.

go
package main

/*
#include <stdio.h>
#include <stdlib.h>

void printfGoString(char* s) {
  puts(s);
}
*/
import "C"
import "unsafe"

func main() {
  cbytes := C.CBytes([]byte("this is a go string"))
  C.printfGoString((*C.char)(cbytes))
  C.free(unsafe.Pointer(cbytes))
}

Output contoh di atas sama

this is a go string

Beberapa metode penerusan string di atas melibatkan sekali copy memori. Setelah penerusan, sebenarnya masing-masing menyimpan salinan di memori C dan Go, ini lebih aman. Meskipun demikian, kita masih dapat langsung meneruskan pointer ke fungsi C, dan juga dapat memodifikasi string di Go langsung di C. Lihat contoh berikut

go
package main

/*
#include <stdio.h>
#include <stdlib.h>

void printfGoString(char* s) {
  puts(s);
}
*/
import "C"
import "unsafe"

func main() {
  ptr := unsafe.Pointer(unsafe.SliceData([]byte("this is a go string")))
  C.printfGoString((*C.char)(ptr))
}

Output

this is a go string

Contoh ini langsung mendapatkan pointer array底层 string melalui unsafe.SliceData, dan mengkonversinya menjadi pointer C lalu diteruskan ke fungsi C. Memori string ini dikelola oleh Go, tentu tidak perlu free lagi. Keuntungan melakukan ini adalah tidak perlu copy saat penerusan, tetapi ada risiko tertentu. Contoh berikut mendemonstrasikan memodifikasi string di Go di C

go
package main

/*
#include <stdio.h>
#include <stdlib.h>

void printfGoString(char* s, int len) {
  puts(s);
  s[8] = 'c';
  puts(s);
}
*/
import "C"
import (
  "fmt"
  "unsafe"
)

func main() {
  var buf []byte
  buf = []byte("this is a go string")
  ptr := unsafe.Pointer(unsafe.SliceData(buf))
  C.printfGoString((*C.char)(ptr), C.int(len(buf)))
  fmt.Println(string(buf))
}

Output

this is a go string
this is c go string
this is c go string

Integer

Hubungan pemetaan integer antara Go dan C seperti tabel berikut. Tentang pemetaan tipe integer juga dapat melihat beberapa informasi di paket standar cmd/cgo/gcc.go.

goccgo
int8singed charC.schar
uint8unsigned charC.uchar
int16shortC.short
uint16unsigned shortC.ushort
int32intC.int
uint32unsigned intC.uint
int32longC.long
uint32unsigned longC.ulong
int64long long intC.longlong
uint64unsigned long long intC.ulonglong

Kode contoh sebagai berikut

go
package main

/*
#include <stdio.h>

void printGoInt8(signed char n) {
  printf("%d\n",n);
}
void printGoUInt8(unsigned char n) {
  printf("%d\n",n);
}
void printGoInt16(signed short n) {
  printf("%d\n",n);
}
void printGoUInt16(unsigned short n) {
  printf("%d\n",n);
}
void printGoInt32(signed int n) {
  printf("%d\n",n);
}
void printGoUInt32(unsigned int n) {
  printf("%d\n",n);
}
void printGoInt64(signed long long int n) {
  printf("%ld\n",n);
}
void printGoUInt64(unsigned long long int n) {
  printf("%ld\n",n);
}
*/
import "C"

func main() {
  C.printGoInt8(C.schar(1))
  C.printGoInt8(C.schar(1))
  C.printGoInt16(C.short(1))
  C.printGoUInt16(C.ushort(1))
  C.printGoInt32(C.int(1))
  C.printGoUInt32(C.uint(1))
  C.printGoInt64(C.longlong(1))
  C.printGoUInt64(C.ulonglong(1))
}

Cgo juga mendukung tipe integer <stdint.h>, tipe di sini lebih jelas ukuran memorinya, dan gaya penamaannya juga sangat mirip dengan Go.

goccgo
int8int8_tC.int8_t
uint8uint8_tC.uint8_t
int16int16_tC.int16_t
uint16uint16_tC.uint16_t
int32int32_tC.int32_t
uint32uint32_tC.uint32_t
int64int64_tC.int64_t
uint64uint64_tC.uint64_t

Saat menggunakan cgo, disarankan menggunakan tipe integer di <stdint.h>.

Floating Point

Pemetaan tipe floating point antara Go dan C sebagai berikut

goccgo
float32floatC.float
float64doubleC.double

Contoh kode sebagai berikut

go
package main

/*
#include <stdio.h>

void printGoFloat32(float n) {
  printf("%f\n",n);
}
void printGoFloat64(double n) {
  printf("%lf\n",n);
}
*/
import "C"

func main() {
  C.printGoFloat32(C.float(1.11))
  C.printGoFloat64(C.double(3.14))
}

Slice

Kasus slice sebenarnya mirip dengan string yang dibahas di atas, tetapi perbedaannya adalah cgo tidak menyediakan pseudo-fungsi untuk copy slice. Jika ingin C mengakses slice di Go, hanya bisa meneruskan pointer slice. Lihat contoh berikut

go
package main

/*
#include <stdio.h>
#include <stdint.h>

void printInt32Arr(int32_t* s, int32_t len) {
  for (int32_t i = 0; i < len; i++) {
    printf("%d ", s[i]);
  }
}
*/
import "C"
import (
  "unsafe"
)

func main() {
  var arr []int32
  for i := 0; i < 10; i++ {
    arr = append(arr, int32(i))
  }
  ptr := unsafe.Pointer(unsafe.SliceData(arr))
  C.printInt32Arr((*C.int32_t)(ptr), C.int(len(arr)))
}

Output

0 1 2 3 4 5 6 7 8 9

Di sini pointer array底层 slice diteruskan ke fungsi C. Karena memori array ini dikelola oleh Go, tidak disarankan C memegang referensi pointer dalam jangka panjang. Sebaliknya, contoh menggunakan array C sebagai array底层 slice Go sebagai berikut

go
package main

/*
#include <stdio.h>
#include <stdint.h>

int32_t s[] = {1, 2, 3, 4, 5, 6, 7};

*/
import "C"
import (
  "fmt"
  "unsafe"
)

func main() {
  l := unsafe.Sizeof(C.s) / unsafe.Sizeof(C.s[0])
  fmt.Println(l)
  goslice := unsafe.Slice(&C.s[0], l)
  for i, e := range goslice {
    fmt.Println(i, e)
  }
}

Output

7
0 1
1 2
2 3
3 4
4 5
5 6
6 7

Melalui fungsi unsafe.Slice dapat mengkonversi pointer array menjadi slice. Sesuai intuisi, array di C hanyalah pointer ke elemen head, seharusnya digunakan seperti ini

go
goslice := unsafe.Slice(&C.s, l)

Dari output dapat dilihat, jika melakukan seperti ini, kecuali elemen pertama, sisa memori semuanya out of bounds.

0 [1 2 3 4 5 6 7]
1 [0 -1 0 0 0 3432824 0]
2 [0 0 -1 -1 0 0 -1]
3 [0 0 0 255 0 0 0]
4 [2 0 0 0 3432544 0 0]
5 [0 3432576 0 3432592 0 3432608 0]
6 [0 0 3432624 0 0 0 1422773729]

Meskipun array di C hanyalah pointer head, setelah dibungkus cgo menjadi array Go, memiliki alamat sendiri. Oleh karena itu harus mengambil alamat elemen head array.

goslice := unsafe.Slice(&C.s[0], l)

Struct

Melalui prefix C.struct_ ditambah nama struct, dapat mengakses struct C. Struct C tidak dapat disisipkan sebagai struct anonim di struct Go. Berikut adalah contoh sederhana struct C

go
package main

/*
#include <stdio.h>
#include <stdint.h>

struct person {
  int32_t age;
  char*  name;
};

*/
import "C"
import (
  "fmt"
  "reflect"
)

func main() {
  var p C.struct_person
  p.age = C.int32_t(18)
  p.name = C.CString("john")
  fmt.Println(reflect.TypeOf(p))
  fmt.Printf("%+v", p)
}

Output

main._Ctype_struct_person
{age:18 name:0x1dd043b6e30}

Jika beberapa member struct C mengandung bit-field, cgo akan mengabaikan member struct seperti ini. Misalnya mengubah person menjadi seperti berikut

c
struct person {
  int32_t age: 1;
  char*  name;
};

Eksekusi lagi akan error

p.age undefined (type _Ctype_struct_person has no field or method age)

Aturan alignment memori field struct C dan Go tidak sama. Jika mengaktifkan cgo, sebagian besar kasus akan dipimpin oleh C.

Union

Menggunakan C.union_ ditambah nama dapat mengakses union di C. Karena Go tidak mendukung union, mereka akan ada dalam bentuk array byte di Go. Berikut adalah contoh sederhana

go
package main

/*
#include <stdio.h>
#include <stdint.h>

union data {
  int32_t age;
  char  ch;
};

*/
import "C"
import (
  "fmt"
  "reflect"
)

func main() {
  var u C.union_data
  fmt.Println(reflect.TypeOf(u), u)
}

Output

[4]uint8 [0 0 0 0]

Melalui unsafe.Pointer dapat mengakses dan memodifikasi

go
func main() {
  var u C.union_data
  ptr := (*C.int32_t)(unsafe.Pointer(&u))
  fmt.Println(*ptr)
  *ptr = C.int32_t(1024)
  fmt.Println(*ptr)
  fmt.Println(u)
}

Output

0
1024
[0 4 0 0]

Enum

Melalui prefix C.enum_ ditambah nama tipe enum dapat mengakses tipe enum di C. Berikut adalah contoh sederhana

go
package main

/*
#include <stdio.h>
#include <stdint.h>

enum player_state {
  alive,
  dead,
};

*/
import "C"
import "fmt"

type State C.enum_player_state

func (s State) String() string {
  switch s {
  case C.alive:
    return "alive"
  case C.dead:
    return "dead"
  default:
    return "unknown"
  }
}

func main() {
  fmt.Println(C.alive, State(C.alive))
  fmt.Println(C.dead, State(C.dead))
}

Output

0 alive
1 dead

Pointer

Membahas pointer tidak bisa lepas dari memori. Masalah terbesar dalam saling panggilan cgo adalah model memori kedua bahasa tidak sama. Memori C sepenuhnya dikelola manual oleh developer, mengalokasikan memori dengan malloc(), melepaskan dengan free(). Jika tidak dilepaskan manual, memori tidak akan pernah dilepaskan. Oleh karena itu manajemen memori C sangat stabil. Go berbeda, Go memiliki GC, dan ruang stack Goroutine dapat disesuaikan secara dinamis. Ketika ruang stack tidak cukup akan bertambah, sehingga alamat memori mungkin berubah. Seperti gambar di atas (gambar tidak terlalu ketat), pointer mungkin menjadi dangling pointer yang umum di C. Meskipun cgo dapat menghindari memory move dalam sebagian besar kasus (oleh runtime.Pinner untuk mem-fix memori),官方 Go juga tidak menyarankan C memegang referensi memori Go dalam jangka panjang. Sebaliknya, jika pointer di Go mereferensikan memori di C, ini relatif aman. Kecuali memanggil C.free() manual, memori ini tidak akan dilepaskan otomatis.

Jika ingin meneruskan pointer antara C dan Go, perlu mengkonversinya ke unsafe.Pointer terlebih dahulu, lalu mengkonversi ke tipe pointer yang sesuai. Sama seperti void* di C. Lihat dua contoh, pertama adalah contoh pointer C mereferensikan variabel Go, dan juga memodifikasi variabel.

go
package main

/*
#include <stdio.h>
#include <stdint.h>

void printNum(int32_t* s) {
  printf("%d\n", *s);
  *s = 3;
  printf("%d\n", *s);
}
*/
import "C"
import (
  "fmt"
  "unsafe"
)

func main() {
  var num int32 = 1
  ptr := unsafe.Pointer(&num)
  C.printNum((*C.int32_t)(ptr))
  fmt.Println(num)
}

Output

1
3
3

Kedua adalah contoh pointer Go mereferensikan variabel C, dan memodifikasinya.

go
package main

/*
#include <stdio.h>
#include <stdint.h>

int32_t num = 10;
*/
import "C"
import (
  "fmt"
  "unsafe"
)

func main() {
  fmt.Println(C.num)
  ptr := unsafe.Pointer(&C.num)
  iptr := (*int32)(ptr)
  *iptr++
  fmt.Println(C.num)
}

Output

10
11

Sekadar informasi, cgo tidak mendukung function pointer di C.

Bahasa C tidak memiliki manajemen dependensi seperti Go. Untuk langsung menggunakan library yang ditulis orang lain, selain mendapatkan source code, ada cara lain yaitu static library dan dynamic library. Cgo juga mendukung ini. Berkat ini, kita dapat mengimport library yang ditulis orang lain di program Go, tanpa perlu source code.

Dynamic Library

Dynamic library tidak dapat dijalankan sendiri. Saat runtime akan dimuat bersama file executable ke memori. Berikut mendemonstrasikan membuat dynamic library sederhana, dan memanggilnya menggunakan cgo. Pertama siapkan file lib/sum.c, kontennya sebagai berikut

c
#include <stdint.h>

int32_t sum(int32_t a, int32_t b) {
    return a + b;
}

Tulis file header lib/sum.h

c
#include <stdint.h>

int sum(int32_t a, int32_t b);

Selanjutnya gunakan gcc untuk membuat dynamic library. Pertama kompilasi untuk menghasilkan file object

bash
$ cd lib

$ gcc -c sum.c -o sum.o

Lalu buat dynamic library

bash
$ gcc -shared -o libsum.dll sum.o

Setelah selesai, lalu import file header sum.h di kode Go, dan perlu memberi tahu cgo di mana mencari file library melalui macro

go
package main

/*
#cgo CFLAGS: -I ./lib
#cgo LDFLAGS: -L${SRCDIR}/lib -llibsum
#include "sum.h"

*/
import "C"
import "fmt"

func main() {
  res := C.sum(C.int32_t(1), C.int32_t(2))
  fmt.Println(res)
}
  • CFLAGS: -I adalah path relatif untuk mencari file header
  • -L adalah path pencarian library, ${SRCDIR} mewakili path absolut path saat ini, karena parameternya harus path absolut
  • -l adalah nama file library, sum adalah sum.dll.

CFFLAGS dan LDFLAGS keduanya adalah opsi kompilasi gcc. Demi keamanan, cgo menonaktifkan beberapa parameter.前往 cgo command untuk了解 detail.

Taruh dynamic library di folder yang sama dengan exe

bash
$ ls
go.mod  go.sum  lib/  libsum.dll*  main.exe*  main.go

Terakhir kompilasi program Go dan jalankan

bash
$ go build main.go && ./main.exe
3

Sampai sini dynamic library berhasil dipanggil.

Static Library

Berbeda dengan dynamic library, saat menggunakan cgo untuk mengimport static library, akan di-link dengan file object Go menjadi file executable. Masih menggunakan sum.c sebagai contoh, pertama kompilasi source file menjadi file object

bash
$ gcc -o sum.o -c sum.c

Lalu paket file object menjadi static library (harus prefix lib, jika tidak akan找不到)

bash
$ ar rcs libsum.a sum.o

Konten file Go

go
package main

/*
#cgo CFLAGS: -I ./lib
#cgo LDFLAGS: -L${SRCDIR}/lib -llibsum
#include "sum.h"

*/
import "C"
import "fmt"

func main() {
  res := C.sum(C.int32_t(1), C.int32_t(2))
  fmt.Println(res)
}

Kompilasi

bash
$ go build && ./main.exe
3

Sampai sini static library berhasil dipanggil.

Terakhir

Meskipun tujuan menggunakan cgo adalah untuk performa, tetapi beralih antara C dan Go juga menyebabkan kehilangan performa tertentu. Untuk tugas yang sangat sederhana, efisiensi cgo tidak sebaik Go murni. Lihat contoh

go
package main

/*
#include <stdint.h>

int32_t cgo_sum(int32_t a, int32_t b) {
  return a + b;
}

*/
import "C"
import (
  "fmt"
  "time"
)

func go_sum(a, b int32) int32 {
  return a + b
}

func testSum(N int, do func()) int64 {
  var sum int64
  for i := 0; i < N; i++ {
    start := time.Now()
    do()
    sum += time.Now().Sub(start).Nanoseconds()
  }
  return sum / int64(N)
}

func main() {
  N := 1000_000
  nsop1 := testSum(N, func() {
    C.cgo_sum(C.int32_t(1), C.int32_t(2))
  })
  fmt.Printf("cgo_sum: %d ns/op\n", nsop1)
  nsop2 := testSum(N, func() {
    go_sum(1, 2)
  })
  fmt.Printf("pure_go_sum: %d ns/op\n", nsop2)
}

Ini adalah tes yang sangat sederhana, masing-masing menulis fungsi penjumlahan dua angka menggunakan C dan Go, lalu masing-masing menjalankan 1 juta kali, menghitung rata-rata waktu. Hasil tes sebagai berikut

cgo_sum: 49 ns/op
pure_go_sum: 2 ns/op

Dari hasil dapat dilihat, rata-rata waktu cgo adalah dua puluh kali lipat dari Go murni. Jika yang dieksekusi bukan penjumlahan dua angka sederhana, tetapi tugas yang memakan waktu, keunggulan cgo akan lebih besar. Selain itu, menggunakan cgo juga memiliki kekurangan berikut

  1. Banyak toolchain Go yang配套 tidak dapat digunakan, seperti gotest, pprof. Contoh tes di atas tidak dapat menggunakan gotest, hanya bisa tulis manual.
  2. Kecepatan kompilasi melambat, cross-compile bawaan juga tidak dapat digunakan
  3. Masalah keamanan memori
  4. Masalah dependensi. Jika orang lain menggunakan library Anda, sama juga harus mengaktifkan cgo.

Sebelum mempertimbangkan dengan matang, jangan mengimport cgo di proyek. Untuk beberapa tugas yang sangat kompleks, menggunakan cgo memang dapat带来 keuntungan. Tetapi jika hanya tugas sederhana, lebih baik tetap menggunakan Go.

Golang by www.golangdev.cn edit