Skip to content

CGO

Go'nun GC'ye ihtiyacı olduğu için, performans gereksinimleri daha yüksek olan bazı senaryolarda Go uygun olmayabilir. C, geleneksel bir sistem programlama dili olarak mükemmel bir performansa sahiptir ve cgo ikisini birbirine bağlayabilir, birbirlerini çağırabilir, Go'nun C'yi çağırmasına izin verebilir, performansa duyarlı görevleri C'ye bırakabilir ve Go üst düzey mantığı işlemekten sorumlu olabilir. Cgo, C'nin Go'yu çağırmasını da destekler, ancak bu senaryo daha az yaygındır ve önerilmez.

::: ipucu

Makaledeki kod örnekleri Windows 10 ortamında gösterilmiştir ve komut satırı için gitbash kullanılmıştır. Windows kullanıcılarının mingw'yi önceden yüklemeleri önerilir.

:::

Cgo hakkında, resmi bir tanıtım bulunmaktadır: C? Go? Cgo! - The Go Programming Language. Daha detaylı bir tanıtım istiyorsanız, standart kütüphane cmd/cgo/doc.go içinde daha ayrıntılı bilgi bulabilir veya doğrudan belgelere bakabilirsiniz cgo command - cmd/cgo - Go Packages. Her ikisinin içeriği tamamen aynıdır.

Kod Çağrısı

Aşağıdaki örneğe bakalım

go
package main

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

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

Cgo özelliğini kullanmak için, import "C" import ifadesi ile açılabilir. Unutulmaması gereken nokta, C'nin büyük harf olması gerektiği ve import adının değiştirilemeyeceğidir. Ayrıca, ortam değişkeni CGO_ENABLED'in 1 olarak ayarlanıp ayarlanmadığından emin olunmalıdır. Varsayılan olarak bu ortam değişkeni etkindir.

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

Bunun dışında, yerel makinede C/C++ derleme araç zincirine, yani gcc'ye sahip olduğunuzdan emin olunmalıdır. Windows platformunda bu mingw'dir. Bu şekilde programın derlenmesi sağlanabilir. Derlemek için aşağıdaki komutu çalıştırın. Cgo etkinleştirildikten sonra derleme süresi saf Go'ya göre daha uzun olacaktır.

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

Dikkat edilmesi gereken bir diğer nokta, cgo etkinleştirildikten sonra, çapraz derleme desteğinin olmayacağıdır.

Go'ya Gömülü C Kodu

Cgo, C kodunu doğrudan Go kaynak dosyalarına yazmayı ve doğrudan çağırmayı destekler. Aşağıdaki örneğe bakalım. Örnekte printSum adında bir fonksiyon yazılmış ve Go'daki main fonksiyonunda çağrılmıştır.

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))
}

Çıktı

c:1+2=3

Bu basit senaryolar için uygundur. Eğer C kodu çok fazlaysa ve Go kodu ile karıştırılırsa, okunabilirlik büyük ölçüde azalır, bu durumda bu yaklaşım uygun değildir.

Hata İşleme

Go dilinde hata işleme dönüş değeri olarak döndürülür, ancak C dili çoklu dönüş değerlerine izin vermez. Bu nedenle C'deki errno kullanılabilir. Bu, fonksiyon çağrısı sırasında bir hata oluştuğunu belirtir. Cgo buna uyum sağlamıştır ve C fonksiyonlarını çağırırken Go'daki gibi dönüş değerleri kullanarak hataları işleyebilirsiniz. errno kullanmak için, önce errno.h dosyasını import etmelisiniz. Aşağıdaki örneğe bakalım

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)
}

Çıktı

syscall.Errno
The device does not recognize the command.

Hata türünün syscall.Errno olduğunu görebilirsiniz. errno.h içinde başka birçok hata kodu tanımlanmıştır, kendiniz öğrenebilirsiniz.

Go'da C Dosyası İçe Aktarma

C dosyalarını içe aktararak, yukarıdaki sorunu çözebilirsiniz. Önce bir başlık dosyası sum.h oluşturun, içeriği şu şekildedir

c
int sum(int a, int b);

Ardından sum.c oluşturun ve fonksiyonu yazın

c
#include "sum.h"

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

Sonra main.go içinde başlık dosyasını import edin

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)
}

Şimdi derlemek için, mevcut klasörü belirtmelisiniz, aksi takdirde c dosyası bulunamaz

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

Koddaki res Go'daki bir değişkendir ve C.sum C dilindeki bir fonksiyondur. Dönüş değeri C dilindeki int'tir, Go'daki int değil. Başarılı bir şekilde çağrılabilmesinin nedeni, cgo'nun tür dönüşümü yapmasıdır.

C'nin Go'yu Çağırması

C'nin Go'yu çağırması, cgo içinde C'nin Go'yu çağırması anlamına gelir, yerel C programının Go'yu çağırması değil. Çağrı zinciri go-cgo-c->cgo->go şeklindedir. Go'nun C'yi çağırması, C'nin ekosistemini ve performansını kullanmak içindir. Neredeyse hiç yerel C programının Go'yu çağırma ihtiyacı yoktur. Varsa bile, ağ iletişimi kullanılarak değiştirilmesi önerilir.

Cgo, C'nin çağırması için Go fonksiyonlarını dışa aktarabilir. Go fonksiyonlarını dışa aktarmak için, fonksiyon imzasının üzerine //export func_name yorumu eklenmelidir. Parametreler ve dönüş değerleri cgo tarafından desteklenen türler olmalıdır. Örnek aşağıdaki gibidir

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

sum.c dosyasını aşağıdaki gibi değiştirin

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);
}

Aynı zamanda başlık dosyası sum.h'yi değiştirin

void do_sum();

Ardından Go'da fonksiyonu dışa aktarın

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
}

Şimdi C'de kullanılan sum fonksiyonu aslında Go tarafından sağlanmaktadır. Çıktı sonucu şu şekildedir

20

Anahtar nokta, sum.c dosyasında import edilen _cgo_export.h dosyasıdır. Bu dosya, Go tarafından dışa aktarılan tüm türler hakkında bilgi içerir. İçe aktarılmazsa, Go tarafından dışa aktarılan fonksiyonlar kullanılamaz. Diğer bir dikkat edilmesi gereken nokta, _cgo_export.h dosyasının Go dosyasında import edilememesidir. Çünkü bu başlık dosyasının oluşturulmasının ön koşulu, tüm Go kaynak dosyalarının derlenebilmesidir. Bu nedenle aşağıdaki yazım yanlıştır

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
}

Derleyici başlık dosyasının bulunmadığını belirtecektir

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

Eğer Go fonksiyonunun birden fazla dönüş değeri varsa, C çağrısı bir yapı döndürecektir.

Bu arada, Go işaretçisini C fonksiyon parametresi aracılığıyla C'ye aktarabiliriz. C fonksiyon çağrısı sırasında cgo bellek güvenliğini mümkün olduğunca sağlar. Ancak dışa aktarılan Go fonksiyonunun dönüş değeri işaretçi içeremez. Çünkü bu durumda cgo bunun referans alınıp alınmadığını belirleyemez ve belleği sabitleyemez. Döndürülen bellek referans alınırsa ve ardından Go'da bu bellek GC tarafından temizlenirse veya kayarsa, işaretçi sınır dışı olacaktır. Aşağıda gösterildiği gibi.

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

Yukarıdaki yazım varsayılan olarak derlenemez. Bu kontrolü kapatmak istiyorsanız, aşağıdaki gibi ayarlayabilirsiniz.

GODEBUG=cgocheck=0

İki kontrol seviyesi vardır, 1 veya 2 olarak ayarlanabilir. Seviye ne kadar yüksekse, çalışma zamanı maliyeti o kadar büyük olur. Detaylar için cgo command - passing_pointer adresine gidebilirsiniz.

Tür Dönüşümü

Cgo, C ile Go arasında tür eşlemesi yapar, çalışma zamanı çağrıları için kolaylık sağlar. C'deki türler için, Go'da import "C" import ettikten sonra, çoğu durumda

C.typename

bu şekilde doğrudan erişilebilir. Örneğin

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

Ancak C dili türleri birden fazla anahtar kelimeden oluşabilir. Örneğin

unsigned char

Bu durumda doğrudan erişilemez. Ancak C'deki typedef anahtar kelimesi kullanılarak türe bir takma ad verilebilir. Bu, Go'daki tür takma adına eşdeğerdir. Aşağıdaki gibi

c
typedef unsigned char byte;

Bu şekilde, C.byte aracılığıyla unsigned char türüne erişebilirsiniz. Örnek aşağıdaki gibidir

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'))
}

Çıktı

a
b
c

Çoğu durumda, cgo yaygın türler (temel türler vb.) için zaten takma adlar tanımlamıştır. Yukarıdaki yönteme göre kendiniz de tanımlayabilirsiniz, çakışma olmaz.

char

C'deki char, Go'daki int8 türüne karşılık gelir. unsigned char, Go'daki uint8 yani byte türüne karşılık gelir.

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)
}

Çıktı

type: main._Ctype_char, val: 99

Eğer set parametresi C.char(math.MaxInt8 + 1) olarak değiştirilirse, derleme başarısız olur ve aşağıdaki hata mesajı verilir

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

String

Cgo, C ve Go arasında string ve byte slice aktarmak için bazı sahte fonksiyonlar sağlar. Bu fonksiyonlar aslında mevcut değildir, tanımlarını da bulamazsınız. Tıpkı import "C" gibi, C paketi de mevcut değildir. Sadece geliştiricilerin kullanımını kolaylaştırmak içindir. Derlemeden sonra başka işlemlere dönüştürülürler.

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

Go'daki string temelde bir yapıdır, altta yatan diziye bir referans tutar. C fonksiyonuna aktarılırken, C.CString() kullanılarak C'de malloc ile bir "string" oluşturulmalı ve bellek alanı ayrılmalıdır. Ardından bir C işaretçisi döndürülür. Çünkü C'de string türü yoktur, genellikle string için char* kullanılır, yani bir karakter dizisi işaretçisidir. Kullanıldıktan sonra belleği serbest bırakmak için free kullanmayı unutmayın.

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))
}

Veya char dizi türü de olabilir. Her ikisi de aslında aynıdır, her ikisi de baş elemana işaret eden işaretçidir.

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

Byte slice da aktarılabilir. C.CBytes() bir unsafe.Pointer döndürdüğünden, C fonksiyonuna aktarmadan önce *C.char türüne dönüştürülmelidir.

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))
}

Yukarıdaki örneklerin çıktısı aynıdır

this is a go string

Yukarıdaki bu string aktarma yöntemleri bir bellek kopyalaması içerir. Aktarımdan sonra aslında C belleği ve Go belleğinde ayrı ayrı bir kopya saklanır. Bu daha güvenli olmasını sağlar. Bununla birlikte, doğrudan işaretçiyi C fonksiyonuna aktarabilir ve Go'daki string'i C'de doğrudan değiştirebilirsiniz. Aşağıdaki örneğe bakalım

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))
}

Çıktı

this is a go string

Örnek, unsafe.SliceData kullanarak doğrudan string'in altta yatan dizisinin işaretçisini alır ve bunu C işaretçisine dönüştürerek C fonksiyonuna aktarır. Bu string'in belleği Go tarafından yönetilir, bu nedenle artık free gerekmez. Bunun avantajı, aktarım sırasında kopyalama gerekmemesidir, ancak belirli bir riski vardır. Aşağıdaki örnek, C'de Go'daki string'in değiştirilmesini gösterir

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))
}

Çıktı

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

Tamsayı

Go ile C arasındaki tamsayı eşleme ilişkisi aşağıdaki tabloda gösterilmiştir. Tamsayı tür eşlemesi hakkında standart kütüphane cmd/cgo/gcc.go içinde bazı bilgiler bulabilirsiniz.

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

Örnek kod aşağıdaki gibidir

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 aynı zamanda <stdint.h> tamsayı türleri için destek sağlar. Buradaki türlerin bellek boyutu daha açık ve nettir ve adlandırma stili Go ile çok benzerdir.

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

Cgo kullanırken, <stdint.h> içindeki tamsayı türlerini kullanmanız önerilir.

Kayan Nokta

Go ile C'nin kayan nokta tür eşlemesi aşağıdaki gibidir

goccgo
float32floatC.float
float64doubleC.double

Kod örneği aşağıdaki gibidir

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

Slice durumu aslında yukarıda anlatılan string ile aynıdır. Ancak fark, cgo'nun slice için kopyalama amacıyla sahte fonksiyonlar sağlamamasıdır. C'nin Go'daki slice'e erişmesini istiyorsanız, slice işaretçisini aktarmanız gerekir. Aşağıdaki örneğe bakalım

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)))
}

Çıktı

0 1 2 3 4 5 6 7 8 9

Burada slice'in altta yatan dizisinin işaretçisi C fonksiyonuna aktarılır. Bu dizinin belleği Go tarafından yönetildiğinden, C'nin uzun süre işaretçi referansı tutması önerilmez. Tersine, C'nin dizisini Go slice'inin altta yatan dizisi olarak kullanma örneği aşağıdaki gibidir

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)
  }
}

Çıktı

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

unsafe.Slice fonksiyonu kullanılarak dizi işaretçisi slice'e dönüştürülebilir. Sezgisel olarak, C'deki dizi baş elemana işaret eden bir işaretçidir, mantıken şu şekilde kullanılmalıdır

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

Çıktıdan görülebileceği gibi, böyle yapılırsa ilk eleman hariç diğer tüm bellekler sınır dışı olur.

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]

C'deki dizi sadece bir baş işaretçi olsa da, cgo tarafından sarıldıktan sonra Go dizisi haline gelir ve kendi adresine sahip olur. Bu nedenle dizi baş elemanına adres alınmalıdır.

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

Yapı

C.struct_ önekine yapı adını ekleyerek C yapısına erişebilirsiniz. C yapıları Go yapılarına anonim yapı olarak gömülemez. Aşağıda basit bir C yapısı örneği bulunmaktadır

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)
}

Çıktı

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

Eğer C yapısının bazı üyeleri bit-field içeriyorsa, cgo bu tür yapı üyelerini yoksayar. Örneğin person aşağıdaki gibi değiştirilirse

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

Tekrar çalıştırıldığında hata verilir

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

C ve Go'nun yapı alanı bellek hizalama kuralları aynı değildir. Cgo etkinleştirilirse, çoğu durumda C öncülük eder.

Birlik (Union)

C.union_ ile ad ekleyerek C'deki birliğe erişebilirsiniz. Go birliği desteklemediğinden, Go'da byte dizisi olarak bulunurlar. Aşağıda basit bir örnek bulunmaktadır

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)
}

Çıktı

[4]uint8 [0 0 0 0]

unsafe.Pointer kullanılarak erişilebilir ve değiştirilebilir

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)
}

Çıktı

0
1024
[0 4 0 0]

Enum

C.enum_ önekine enum tür adı ekleyerek C'deki enum türüne erişebilirsiniz. Aşağıda basit bir örnek bulunmaktadır

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))
}

Çıktı

0 alive
1 dead

İşaretçi

İşaretçiden bahsettiğimizde bellekten bahsetmemek olmaz. Cgo'nun karşılıklı çağrısındaki en büyük sorun, iki dilin bellek modellerinin aynı olmamasıdır. C dilinde bellek tamamen geliştirici tarafından manuel olarak yönetilir, malloc() ile bellek ayrılır ve free() ile bellek serbest bırakılır. Manuel olarak serbest bırakılmazsa, asla kendi kendine serbest bırakılmaz. Bu nedenle C'nin bellek yönetimi çok stabildir. Go ise farklıdır, GC'ye sahiptir ve Goroutine'in yığın alanı dinamik olarak ayarlanabilir. Yığın alanı yetersiz olduğunda büyür, bu durumda bellek adresi değişebilir. Yukarıdaki resimde olduğu gibi (resim çok kesin değildir), işaretçi C'de yaygın olan asılı işaretçi haline gelebilir. Cgo çoğu durumda bellek hareketini önleyebilse de (runtime.Pinner ile belleği sabitleyerek), Go resmi C'de Go belleğinin uzun süre referans alınmasını önermez. Ancak tersine, Go'daki işaretçi C'deki belleği referans alıyorsa, bu daha güvenlidir. Manuel olarak C.free() çağrılmadıkça, bu bellek otomatik olarak serbest bırakılmaz.

C ve Go arasında işaretçi aktarılması gerekiyorsa, önce unsafe.Pointer'a dönüştürülmeli, ardından ilgili işaretçi türüne dönüştürülmelidir. Tıpkı C'deki void* gibi. İki örneğe bakalım. İlki, C işaretçisinin Go değişkenini referans aldığı ve değişkeni değiştirdiği bir örnektir.

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)
}

Çıktı

1
3
3

İkincisi, Go işaretçisinin C değişkenini referans aldığı ve değiştirdiği bir örnektir.

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)
}

Çıktı

10
11

Bu arada, cgo C'deki fonksiyon işaretçilerini desteklemez.

Bağlantı Kütüphaneleri

C dilinin Go gibi bağımlılık yönetimi yoktur. Başkaları tarafından yazılmış kütüphaneleri doğrudan kullanmak için kaynak kodunu elde etmenin yanı sıra, statik bağlantı kütüphaneleri ve dinamik bağlantı kütüphaneleri de vardır. Cgo bunları da destekler. Bu sayede Go programlarında başkaları tarafından yazılmış kütüphaneleri kaynak koduna ihtiyaç duymadan import edebiliriz.

Dinamik Bağlantı Kütüphanesi

Dinamik bağlantı kütüphaneleri tek başına çalıştırılamaz. Çalışma zamanında çalıştırılabilir dosya ile birlikte belleğe yüklenir. Aşağıda basit bir dinamik bağlantı kütüphanesi oluşturma ve cgo ile çağırma gösterilmektedir. Önce bir lib/sum.c dosyası hazırlayın, içeriği şu şekildedir

c
#include <stdint.h>

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

Başlık dosyası lib/sum.h yazın

c
#include <stdint.h>

int sum(int32_t a, int32_t b);

Ardından gcc kullanarak dinamik bağlantı kütüphanesi oluşturun. Önce hedef dosyayı derleyin

bash
$ cd lib

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

Ardından dinamik bağlantı kütüphanesi oluşturun

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

Oluşturduktan sonra, Go kodunda sum.h başlık dosyasını import edin ve cgo'ya kütüphane dosyasını nerede bulacağını söylemek için makro kullanın

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 başlık dosyalarını arama göreli yoludur
  • -L kütüphane arama yoludur, ${SRCDIR} mevcut yolun mutlak yolunu temsil eder, çünkü parametresi mutlak yol olmalıdır
  • -l kütüphane dosyasının adıdır, sum sum.dll anlamına gelir.

CFFLAGS ve LDFLAGS ikisi de gcc derleme seçenekleridir. Güvenlik nedeniyle, cgo bazı parametreleri devre dışı bırakır. Detaylar için cgo command adresine gidin.

Dinamik kütüphaneyi exe ile aynı dizine koyun

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

Son olarak Go programını derleyin ve çalıştırın

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

Bu noktada dinamik bağlantı kütüphanesi çağrısı başarılı oldu.

Statik Bağlantı Kütüphanesi

Dinamik bağlantı kütüphanesinden farklı olarak, cgo kullanılarak statik bağlantı kütüphanesi import edildiğinde, Go hedef dosyası ile birlikte çalıştırılabilir bir dosyaya bağlanır. Yine sum.c örneğini kullanalım. Önce kaynak dosyayı hedef dosyaya derleyin

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

Ardından hedef dosyayı statik bağlantı kütüphanesi olarak paketleyin (lib öneki ile başlamalıdır, aksi takdirde bulunamaz)

bash
$ ar rcs libsum.a sum.o

Go dosyası içeriği

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)
}

Derleme

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

Bu noktada statik bağlantı kütüphanesi çağrısı başarılı oldu.

Son

Cgo kullanmanın amacı performans olsa da, C ile Go arasında geçiş yapmak önemli bir performans kaybına neden olur. Bazı çok basit görevler için cgo'nun verimliliği saf Go kadar iyi değildir. Bir örneğe bakalım

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)
}

Bu çok basit bir testtir. C ve Go ile iki sayının toplamını hesaplayan bir fonksiyon yazılır ve her biri 1 milyon kez çalıştırılır, ortalama süre hesaplanır. Test sonucu şu şekildedir

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

Sonuçtan görülebileceği gibi, cgo'nun ortalama süresi saf Go'nun yirmi küsur katıdır. Eğer sadece iki sayı toplamak değil de daha zaman alıcı bir görev yapılıyorsa, cgo'nun avantajı daha büyük olur. Bunun dışında, cgo kullanmanın aşağıdaki dezavantajları vardır

  1. Go ile birlikte gelen birçok araç zinciri kullanılamaz hale gelir. Örneğin gotest, pprof. Yukarıdaki test örneği gotest kullanamaz, sadece manuel olarak yazılabilir.
  2. Derleme hızı yavaşlar, yerleşik çapraz derleme de kullanılamaz
  3. Bellek güvenliği sorunları
  4. Bağımlılık sorunları. Eğer başkaları kütüphanenizi kullanırsa, cgo'yu açmak zorunda kalırlar.

İyice düşünmeden önce, projeye cgo eklemeyin. Bazı çok karmaşık görevler için cgo kullanmak gerçekten fayda sağlayabilir. Ancak sadece basit görevler için, Go kullanmak daha iyidir.

Golang by www.golangdev.cn edit