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
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.
$ go env | grep CGO
$ go env -w CGO_ENABLED=1Bunun 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.
$ 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.
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=3Bu 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
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
int sum(int a, int b);Ardından sum.c oluşturun ve fonksiyonu yazın
#include "sum.h"
int sum(int a, int b) {
return a + b;
}Sonra main.go içinde başlık dosyasını import edin
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: 3Koddaki 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
//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
#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
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
20Anahtar 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
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.
//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.typenamebu ş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 charBu 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
typedef unsigned char byte;Bu şekilde, C.byte aracılığıyla unsigned char türüne erişebilirsiniz. Örnek aşağıdaki gibidir
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.
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: 99Eğ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_charString
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 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) []byteGo'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.
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.
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.
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 stringYukarı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
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
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 stringTamsayı
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.
| go | c | cgo |
|---|---|---|
| int8 | singed char | C.schar |
| uint8 | unsigned char | C.uchar |
| int16 | short | C.short |
| uint16 | unsigned short | C.ushort |
| int32 | int | C.int |
| uint32 | unsigned int | C.uint |
| int32 | long | C.long |
| uint32 | unsigned long | C.ulong |
| int64 | long long int | C.longlong |
| uint64 | unsigned long long int | C.ulonglong |
Örnek kod aşağıdaki gibidir
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.
| go | c | cgo |
|---|---|---|
| int8 | int8_t | C.int8_t |
| uint8 | uint8_t | C.uint8_t |
| int16 | int16_t | C.int16_t |
| uint16 | uint16_t | C.uint16_t |
| int32 | int32_t | C.int32_t |
| uint32 | uint32_t | C.uint32_t |
| int64 | int64_t | C.int64_t |
| uint64 | uint64_t | C.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
| go | c | cgo |
|---|---|---|
| float32 | float | C.float |
| float64 | double | C.double |
Kod örneği aşağıdaki gibidir
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
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 9Burada 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
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 7unsafe.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
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
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
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
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
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
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.
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.
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
11Bu 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
#include <stdint.h>
int32_t sum(int32_t a, int32_t b) {
return a + b;
}Başlık dosyası lib/sum.h yazın
#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
$ cd lib
$ gcc -c sum.c -o sum.oArdından dinamik bağlantı kütüphanesi oluşturun
$ gcc -shared -o libsum.dll sum.oOluş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
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: -Ibaşlık dosyalarını arama göreli yoludur-Lkütüphane arama yoludur,${SRCDIR}mevcut yolun mutlak yolunu temsil eder, çünkü parametresi mutlak yol olmalıdır-lkütüphane dosyasının adıdır, sumsum.dllanlamı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
$ ls
go.mod go.sum lib/ libsum.dll* main.exe* main.goSon olarak Go programını derleyin ve çalıştırın
$ go build main.go && ./main.exe
3Bu 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
$ gcc -o sum.o -c sum.cArdından hedef dosyayı statik bağlantı kütüphanesi olarak paketleyin (lib öneki ile başlamalıdır, aksi takdirde bulunamaz)
$ ar rcs libsum.a sum.oGo dosyası içeriği
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
$ go build && ./main.exe
3Bu 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
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/opSonuç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
- 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.
- Derleme hızı yavaşlar, yerleşik çapraz derleme de kullanılamaz
- Bellek güvenliği sorunları
- 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.
