Skip to content

CGO

نظرًا لأن Go تحتاج إلى GC، بالنسبة لبعض السيناريوهات التي تتطلب أداءً أعلى، قد لا تكون Go مناسبة للمعالجة. C كلغة برمجة نظام تقليدية لها أداء ممتاز، ويمكن لـ cgo ربط الاثنين معًا للاستدعاء المتبادل، مما يسمح لـ Go باستدعاء C، وإسناد المهام الحساسة للأداء إلى C لإكمالها، بينما تتولى Go معالجة المنطق عالي المستوى. يدعم cgo أيضًا استدعاء Go من C، لكن هذا السيناريو نادر نسبيًا وغير موصى به.

TIP

بيئة عرض الكود في هذه المقالة هي win10، وسطر الأوامر يستخدم gitbash، يُنصح مستخدمو Windows بتثبيت mingw مسبقًا.

حول cgo، توجد مقدمة بسيطة رسمية: C? Go? Cgo! - The Go Programming Language، إذا كنت تريد مقدمة أكثر تفصيلاً، يمكنك الحصول على معلومات أكثر تفصيلاً في المكتبة القياسية cmd/cgo/doc.go، أو يمكنك أيضًا عرض الوثائق مباشرة cgo command - cmd/cgo - Go Packages، المحتوى متماثل تمامًا.

استدعاء الكود

شاهد المثال التالي

go
package main

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

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

للاستفادة من ميزة cgo، يمكنك تفعيلها من خلال بيان الاستيراد import "C"، ويجب ملاحظة أن C يجب أن تكون حرفًا كبيرًا، ولا يمكن إعادة تسمية حزمة الاستيراد، كما يجب التأكد من تعيين متغير البيئة CGO_ENABLED إلى 1، وبشكل افتراضي يكون هذا المتغير مفعّلاً.

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

بالإضافة إلى ذلك، يجب التأكد من وجود سلسلة أدوات البناء C/C++ محليًا، وهي gcc، وعلى منصة Windows تكون mingw، لضمان ترجمة البرنامج بشكل طبيعي. نفذ الأمر التالي للترجمة، بعد تفعيل cgo سيكون وقت الترجمة أطول من Go النقي.

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

نقطة أخرى يجب ملاحظتها هي أنه بعد تفعيل cgo، لن يكون الترجمة المتقاطعة مدعومة.

تضمين كود C في Go

يدعم cgo كتابة كود C مباشرة في ملف مصدر Go ثم استدعاؤه مباشرة، شاهد المثال التالي، حيث تمت كتابة دالة باسم printSum، ثم استدعاؤها في دالة 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))
}

الإخراج

c:1+2=3

هذا مناسب للسيناريوهات البسيطة، إذا كان كود C كثيرًا جدًا ومختلطًا مع كود Go، فإن ذلك يقلل من قابلية القراءة بشكل كبير، فلا يُنصح بذلك.

معالجة الأخطاء

في لغة Go، تُعاد معالجة الأخطاء كقيمة إرجاع، لكن لغة C لا تسمح بقيم إرجاع متعددة، ولهذا يمكن استخدام errno في C، للإشارة إلى حدوث خطأ أثناء استدعاء الدالة. قام cgo بعمل توافق مع هذا، عند استدعاء دوال C يمكن معالجة الأخطاء بقيم الإرجاع مثل Go. لاستخدام errno، أولاً قم بتضمين errno.h، شاهد المثال التالي

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

الإخراج

syscall.Errno
The device does not recognize the command.

يمكن ملاحظة أن نوع الخطأ هو syscall.Errno، كما تم تعريف رموز أخطاء أخرى كثيرة في errno.h، يمكنك التعرف عليها بنفسك.

استيراد ملفات C في Go

من خلال استيراد ملفات C، يمكن حل المشكلة المذكورة أعلاه بشكل جيد، أولاً قم بإنشاء ملف رأس sum.h، بالمحتوى التالي

c
int sum(int a, int b);

ثم أنشئ sum.c، واكتب الدالة المحددة

c
#include "sum.h"

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

ثم استورد ملف الرأس في 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)
}

للترجمة الآن، يجب تحديد المجلد الحالي، وإلا لن يتم العثور على ملف C، كما يلي

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

في الكود، res هو متغير في Go، و C.sum هي دالة في لغة C، وقيمة الإرجاع الخاصة بها هي int في C وليس int في Go، والسبب في نجاح الاستدعاء هو أن cgo قام بتحويل النوع.

استدعاء Go من C

استدعاء Go من C يعني استدعاء Go من C ضمن cgo، وليس استدعاء Go من برنامج C أصلي، وسلسلة الاستدعاء هي go-cgo-c->cgo->go. استدعاء C من Go هو للاستفادة من بيئة C وأدائه، ولا توجد تقريبًا حالة تحتاج فيها برنامج C أصلي لاستدعاء Go، وإذا وُجدت فيُنصح باستخدام الاتصال الشبكي كبديل.

يدعم cgo تصدير دوال Go لاستدعائها من C، لتصدير دالة Go، يجب إضافة تعليق //export func_name فوق توقيع الدالة، ويجب أن تكون معاملاتها وقيمة الإرجاع من الأنواع التي يدعمها cgo، المثال التالي

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

أعد كتابة ملف sum.c بالمحتوى التالي

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

كما عدّل ملف الرأس sum.h

void do_sum();

ثم صدّر الدالة في 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
}

الدالة sum المستخدمة الآن في C هي في الواقع من Go، نتيجة الإخراج كما يلي

20

النقطة الأساسية هي استيراد _cgo_export.h في ملف sum.c، الذي يحتوي على جميع الأنواع المصدرة من Go، إذا لم يتم استيراده فلن يمكن استخدام الدوال المصدرة من Go. نقطة أخرى يجب ملاحظتها هي أنه لا يمكن استيراد _cgo_export.h في ملف Go، لأن الشرط الأساسي لتوليد هذا الملف الرأسي هو أن تمر جميع ملفات Go المصدرية في الترجمة. لذلك الكتابة التالية خاطئة

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
}

سيشير المترجم إلى عدم وجود الملف الرأسي

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

إذا كانت دالة Go لها قيم إرجاع متعددة، فستُرجع بنية عند استدعائها من C.

بالمناسبة، يمكننا تمرير مؤشر Go عبر معامل دالة C إلى C، وأثناء استدعاء دالة C سيحاول cgo ضمان أمان الذاكرة قدر الإمكان، لكن قيمة الإرجاع لدالة Go المصدرة لا يمكن أن تحتوي على مؤشرات، لأنه في هذه الحالة لا يستطيع cgo الحكم على ما إذا كانت مُشار إليها، ولا تثبيت الذاكرة بشكل جيد، إذا تمت الإشارة إلى الذاكرة المُعادة، ثم تم تحرير هذه الذاكرة في Go عن طريق GC أو حدث إزاحة، سيؤدي ذلك إلى تجاوز المؤشر، كما هو موضح أدناه.

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

الكتابة أعلاه غير مسموح بها في الترجمة بشكل افتراضي، إذا كنت تريد إيقاف هذا الفحص، يمكنك الضبط كما يلي.

GODEBUG=cgocheck=0

له مستويان من الفحص، يمكن ضبطهما على 1، 2، كلما زاد المستوى زادت تكلفة التشغيل التي يسببها الفحص، يمكنك الانتقال إلى cgo command - passing_pointer لمعرفة التفاصيل.

تحويل الأنواع

قام cgo بعمل تعيين بين أنواع C و Go لتسهيل الاستدعاء بينهما في وقت التشغيل. بالنسبة لأنواع C، بعد استيراد import "C" في Go، في معظم الحالات يمكن الوصول إليها مباشرة عبر

C.typename

على سبيل المثال

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

لكن نوع لغة C يمكن أن يتكون من عدة كلمات مفتاحية، مثل

unsigned char

في هذه الحالة لا يمكن الوصول إليه مباشرة، لكن يمكن استخدام الكلمة المفتاحية typedef في C لإعطاء النوع اسمًا مستعارًا، ووظيفتها تماثل الاسم المستعار للنوع في Go. كما يلي

c
typedef unsigned char byte;

بهذه الطريقة، يمكن الوصول إلى النوع unsigned char عبر C.byte. المثال التالي

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

الإخراج

a
b
c

في معظم الحالات، قام cgo بالفعل بتعيين أسماء مستعارة للأنواع الشائعة (مثل الأنواع الأساسية)، ويمكنك أيضًا تعريفها بنفسك حسب الطريقة المذكورة أعلاه، ولن يحدث تعارض.

char

char في C يقابل نوع int8 في Go، و unsigned char يقابل uint8 في Go أي نوع 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)
}

الإخراج

type: main._Ctype_char, val: 99

إذا تم تغيير معامل set إلى C.char(math.MaxInt8 + 1)، فستفشل الترجمة وسيظهر الخطأ التالي

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

السلاسل النصية

يوفر cgo بعض الدوال الزائفة لتمرير السلاسل النصية وشرائح البايت بين C و Go، هذه الدوال غير موجودة فعليًا، ولا يمكنك العثور على تعريفها، تمامًا مثل import "C"، الحزمة C غير موجودة أيضًا، هي فقط لتسهيل استخدام المطورين، وبعد الترجمة سيتم تحويلها إلى عمليات أخرى.

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 هي في الأساس بنية، تحتوي على مرجع للمصفوفة الأساسية، عند تمريرها إلى دالة C، يجب استخدام C.CString() لإنشاء "سلسلة نصية" في C باستخدام malloc، وتخصيص مساحة ذاكرة لها، ثم إرجاع مؤشر C، لأنه لا يوجد نوع سلسلة نصية في C، عادةً ما يُستخدم char* لتمثيل السلسلة النصية، أي مؤشر لمصفوفة أحرف، بعد الانتهاء من الاستخدام تذكر استخدام free لتحرير الذاكرة.

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

يمكن أيضًا أن يكون نوع مصفوفة char، كلاهما متشابه، كلاهما مؤشر للعنصر الأول.

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

يمكن أيضًا تمرير شريحة بايت، نظرًا لأن C.CBytes() ستعيد unsafe.Pointer، قبل تمريرها إلى دالة C يجب تحويلها إلى نوع *C.char.

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

الإخراج في الأمثلة أعلاه متشابه

this is a go string

طرق تمرير السلاسل النصية المذكورة أعلاه تتضمن نسخ ذاكرة واحدة، بعد التمرير يوجد نسخة في ذاكرة C وذاكرة Go،这样做会更安全。话虽如此,我们依然可以直接传递指针给 c 函数,也可以在 c 中直接修改 go 中的字符串,看下面的例子

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

الإخراج

this is a go string

المثال يحصل مباشرة على مؤشر المصفوفة الأساسية للسلسلة النصية عبر unsafe.SliceData، ويحوله إلى مؤشر C ثم يمرره إلى دالة C، ذاكرة هذه السلسلة يديرها Go، لذا لا حاجة لـ free، ميزة ذلك هي أن عملية التمرير لا تحتاج إلى نسخ، لكن هناك مخاطرة معينة. المثال التالي يوضح تعديل السلسلة في Go من داخل 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))
}

الإخراج

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

الأعداد الصحيحة

علاقة التعيين بين الأعداد الصحيحة في Go و C كما هو موضح في الجدول التالي، ويمكن أيضًا رؤية بعض المعلومات المتعلقة بتعيين أنواع الأعداد الصحيحة في المكتبة القياسية 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

مثال الكود التالي

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 أنواع الأعداد الصحيحة في <stdint.h>، حيث يكون حجم ذاكرة الأنواع أكثر وضوحًا وتحديدًا، كما أن أسلوب تسميتها مشابه جدًا لـ 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

عند استخدام cgo، يُنصح باستخدام أنواع الأعداد الصحيحة من <stdint.h>.

الأعداد العشرية

تعيين أنواع الأعداد العشرية بين Go و C كما يلي

goccgo
float32floatC.float
float64doubleC.double

مثال الكود التالي

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

الشرائح

حالة الشرائح تشبه إلى حد كبير السلاسل النصية المذكورة أعلاه، لكن الفرق هو أن cgo لا يوفر دوال زائفة لنسخ الشرائح، وإذا أردت أن تصل C إلى شريحة في Go فلا يمكن إلا تمرير مؤشر الشريحة. شاهد المثال التالي

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

الإخراج

0 1 2 3 4 5 6 7 8 9

هنا تم تمرير مؤشر المصفوفة الأساسية للشريحة إلى دالة C، وبما أن ذاكرة هذه المصفوفة يديرها Go، لا يُنصح بأن تحتفظ C بمرجع المؤشر لفترة طويلة. والعكس، مثال استخدام مصفوفة C كمصفوفة أساسية لشريحة Go كما يلي

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

الإخراج

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

من خلال الدالة unsafe.Slice يمكن تحويل مؤشر مصفوفة إلى شريحة، حسب الحدس، مصفوفة في C هي مجرد مؤشر للعنصر الأول، حسب المنطق يجب استخدامها هكذا

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

من خلال الإخراج يمكن ملاحظة أنه إذا فعلت ذلك، باستثناء العنصر الأول، باقي الذاكرة كلها خارج الحدود.

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 مجرد مؤشر للرأس، بعد تغليفها بـ cgo أصبحت مصفوفة Go، لها عنوانها الخاص، لذا يجب أخذ عنوان العنصر الأول للمصفوفة.

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

البنى

من خلال بادئة C.struct_ مع اسم البنية، يمكن الوصول إلى بنية C، ولا يمكن تضمين بنية C كبنية مجهولة في بنية Go. المثال التالي مثال بسيط لبنية 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)
}

الإخراج

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

إذا كان بعض أعضاء بنية C يحتوي على bit-field، فسيتجاهل cgo这类结构体成员,比如将person修改为下面这种

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

عند التنفيذ مرة أخرى سيظهر خطأ

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

قواعد محاذاة ذاكرة حقول البنى في C و Go ليست متطابقة، إذا كان cgo مفعّلاً، في معظم الحالات ستكون C هي السائدة.

الاتحادات

باستخدام C.union_ مع الاسم يمكن الوصول إلى الاتحادات في C، وبما أن Go لا يدعم الاتحادات، فستكون على شكل مصفوفة بايت في Go. المثال التالي مثال بسيط

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

الإخراج

[4]uint8 [0 0 0 0]

من خلال unsafe.Pointer يمكن الوصول والتعديل

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

الإخراج

0
1024
[0 4 0 0]

التعدادات

من خلال بادئة C.enum_ مع اسم نوع التعداد يمكن الوصول إلى أنواع التعداد في C. المثال التالي مثال بسيط

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

الإخراج

0 alive
1 dead

المؤشرات

الحديث عن المؤشرات لا يمكن فصله عن الذاكرة، أكبر مشكلة في الاستدعاء المتبادل عبر cgo هي أن نموذجي الذاكرة للغتين مختلفان، ذاكرة لغة C يديرها المطور يدويًا بالكامل، باستخدام malloc() لتخصيص الذاكرة و free() لتحريرها، إذا لم يتم التحرير يدويًا، فلن يتم تحريرها أبدًا، لذا إدارة ذاكرة C مستقرة جدًا. أما Go فمختلف، فهي تحتوي على GC، ومساحة مكدس Goroutine يتم تعديلها ديناميكيًا، عندما لا تكفي مساحة المكدس يتم توسيعها، وبهذا قد يتغير عنوان الذاكرة، كما في الصورة أعلاه (الرسم غير دقيق)، وقد يصبح المؤشر مؤشرًا معلقًا شائعًا في C. حتى لو كان cgo يمكنه تجنب حركة الذاكرة في معظم الحالات (بواسطة runtime.Pinner لتثبيت الذاكرة)، فإن المسؤولين عن Go لا ينصحون بأن تشير C إلى ذاكرة Go لفترة طويلة. لكن العكس، إذا كان مؤشر في Go يشير إلى ذاكرة في C، فهو آمن نسبيًا، إلا إذا تم استدعاء C.free() يدويًا، وإلا هذه الذاكرة لن يتم تحريرها تلقائيًا.

إذا أردت تمرير مؤشرات بين C و Go، تحتاج أولًا إلى تحويلها إلى unsafe.Pointer، ثم تحويلها إلى نوع المؤشر المقابل، تمامًا مثل void* في C. شاهد مثالين، الأول مثال على مؤشر C يشير إلى متغير Go، وتم تعديل المتغير أيضًا.

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

الإخراج

1
3
3

الثاني مثال على مؤشر Go يشير إلى متغير C وتعديله.

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

الإخراج

10
11

بالمناسبة، cgo لا يدعم مؤشرات الدوال في C.

مكتبات الربط

لغة C لا تملك إدارة تبعيات مثل Go، لاستخدام مكتبات كتبها آخرون مباشرةً بالإضافة إلى الحصول على الشيفرة المصدرية، هناك طريقة أخرى وهي مكتبات الربط الثابتة والديناميكية، ويدعم cgo هذه أيضًا، وبفضل ذلك يمكننا استيراد مكتبات كتبها آخرون في برامج Go دون الحاجة للشيفرة المصدرية.

مكتبة الربط الديناميكية

مكتبة الربط الديناميكية لا يمكن تشغيلها بشكل مستقل، يتم تحميلها مع الملف التنفيذي في الذاكرة عند التشغيل، فيما يلي عرض توضيحي لصنع مكتبة ربط ديناميكية بسيطة واستدعائها عبر cgo. أولًا جهّز ملف lib/sum.c، بالمحتوى التالي

c
#include <stdint.h>

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

اكتب ملف الرأس lib/sum.h

c
#include <stdint.h>

int sum(int32_t a, int32_t b);

الآن استخدم gcc لصنع مكتبة الربط الديناميكية، أولًا ترجم لتوليد ملف الهدف

bash
$ cd lib

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

ثم اصنع مكتبة الربط الديناميكية

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

بعد الانتهاء، استورد ملف الرأس sum.h في كود Go، ويجب أيضًا إخبار cgo من خلال الماكرو أين يبحث عن ملفات المكتبة

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 يشير إلى المسار النسبي للبحث عن ملفات الرأس،

  • -L يشير إلى مسار البحث عن المكتبات، ${SRCDIR} يعبر عن المسار المطلق للمسار الحالي، لأن معامله يجب أن يكون مسارًا مطلقًا

  • -l يشير إلى اسم ملف المكتبة، sum هو sum.dll.

CFFLAGS و LDFLAGS كلاهما خيارات ترجمة لـ gcc، لأسباب أمنية، عطّل cgo بعض المعاملات، انتقل إلى cgo command لمعرفة التفاصيل.

ضع المكتبة الديناميكية في نفس مستوى exe

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

أخيرًا ترجم برنامج Go ونفذه

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

بهذا تم استدعاء مكتبة الربط الديناميكي بنجاح.

مكتبة الربط الثابتة

تختلف عن مكتبة الربط الديناميكية، عند استيراد مكتبة ربط ثابتة عبر cgo، سيتم ربطها مع ملف الهدف لـ Go في ملف تنفيذي واحد. لنأخذ sum.c مثالًا، أولًا ترجم الملف المصدر إلى ملف الهدف

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

ثم حوّل ملف الهدف إلى مكتبة ربط ثابتة (يجب أن تبدأ بادئة lib، وإلا لن يتم العثور عليها)

bash
$ ar rcs libsum.a sum.o

محتوى ملف 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)
}

الترجمة

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

بهذا تم استدعاء مكتبة الربط الثابت بنجاح.

أخيرًا

رغم أن الغرض من استخدام cgo هو الأداء، إلا أن التبديل بين C و Go يسبب فقدانًا في الأداء، بالنسبة لبعض المهام البسيطة جدًا، كفاءة cgo ليست أفضل من Go النقي. شاهد المثال التالي

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

هذا اختبار بسيط جدًا، تمت كتابة دالة جمع رقمين بـ C و Go على التوالي، ثم تشغيل كل منهما مليون مرة وحساب متوسط الوقت المستغرق، نتائج الاختبار كما يلي

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

من النتائج يمكن ملاحظة أن متوسط الوقت المستغرق لـ cgo أكبر بعشرين مرة تقريبًا من Go النقي، إذا لم يكن ما يتم تنفيذه مجرد جمع رقمين، بل مهمة تستغرق وقتًا أطول، ستكون ميزة cgo أكبر. بالإضافة إلى ذلك، استخدام cgo له العيوب التالية

  1. لن يمكن استخدام العديد من سلاسل أدوات Go المصاحبة، مثل gotest، pprof، مثال الاختبار أعلاه لا يمكن استخدام gotest، يجب كتابته يدويًا.
  2. تباطؤ سرعة الترجمة، والترجمة المتقاطعة المدمجة لن يمكن استخدامها أيضًا
  3. مشاكل أمن الذاكرة
  4. مشاكل التبعية، إذا استخدم شخص مكتبتك، فهذا يعني أنه يجب تفعيل cgo أيضًا.

قبل التفكير جيدًا، لا تُدخل cgo في مشروعك، بالنسبة لبعض المهام المعقدة جدًا، استخدام cgo يمكن أن يجلب فوائد، ولكن إذا كانت مجرد مهام بسيطة، فاستخدم Go بكل أمانة.

Golang تم تحريره بواسطة www.golangdev.cn