[للنقاش] حدود التعامل مع ذاكرة لغة تملك Garbage Collection من لغة أخرى، Go وC بالتحديد.

السلام عليكم ورحمة الله وبركاته.

في الإصدارات السابقة Go 1.5 وأسفل، لم يكن هنالك مشكلة من تمرير مؤشرات ذاكرة Go إلى C، الآن ومع Go 1.6 أصبحت تخرج أخطاء Runtime إن مررتَ مؤشر Go إلى C، وقرأتُ عدّة مراجع لا تنصح بذلك لا تنصح بالتمرير من Go إلى C، لدي بعض الإستفسارات إنطلاقًا من هذا:

  1. إلى أي مدى هو آمن الإحتفاظ بمتغيير Go بداخل C لعملية طويلة في Theard مختلف عن Go بحيث تصبح القيمة غير مستخدمة في Go لكنها مستخدمة في C هل ستحذف؟

  2. إذا حجزتُ قيمة بداخل C ومررتها إلى Go كقيمة خاصة بـGo هل ستحدث مشاكل في Go؟ وهل ستعتبر قيمة خاصة بـCGo لا تحرر؟ وهل أُحررها فيما بعد باستخدام free؟

هذه الشيفرة كمثال على الأثنين التمرير من C إلى Go ومن الأخيرة إلى C:

package main

import (
    "fmt"
    "unsafe"
)

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

const int initArrayLen = 4;

void*
goArray() {
    char* array = (char*)malloc(initArrayLen * sizeof(char*));

    array[0] = 47;
    array[1] = 78;
    array[2] = 124;
    array[3] = 240;
    return array;
} 

void
iterGoSlice(void* ptr) {
    ptr = *(void**)ptr;
    int len = *((char*)ptr + sizeof(void*)), i;

    printf("Go Slice Length: %d\n", len);
    for (i=0; i<len; i++) 
        printf("GoSlice[%d] = %d\n", i, (*(char**)ptr)[i]);
}
*/
import "C"


func main() {
    arrayFromC := *(*[4]byte)(C.goArray())
    goSlice := []byte{32, 12, 43, 54}

    fmt.Println("CArray length", len(arrayFromC))

    for i, val := range arrayFromC {
        fmt.Printf("CArray[%d] = %d\n", i, val);
    }

    // Go 1.6 تمنع تمرير مؤشراتها إلى C مباشرًا
    gptr := unsafe.Pointer(&goSlice)
    cptr := C.malloc(C.size_t(unsafe.Sizeof(gptr)))
    *(*unsafe.Pointer)(cptr) = gptr;
    defer C.free(cptr)
    C.iterGoSlice(cptr)
}
على الجانب كنتُ أضيف مثل تلك الإستفسارات في مجتمعات أجنبية متخصصة، لكن البعض يتضايق من ضعف النشاط البرمجي هنا.
يرجى الدخول لحسابك أو تسجيل حساب لتستطيع إضافة تعليق
حساب جديد دخول

التعليقات

a3f أضف ردا

لا أفقه في Go لكن ما هذا الهراء الذي كتبته في كود السي؟

goArray()

لم تضع قيم integer داخل مصفوفة من المؤشرات؟ لم لا تختار النوع الصحيح؟

(بالمناسبة لا حاجة ل cast ال malloc في السي)

ثم في iterGoSlice(void* ptr) ماذا تظن أنك تفعله في السطر التاني؟

ولما لا تستخدم مؤشر جديد بنوع صحيح طالما لا تستطيع تغيير ال void pointer في ال parameter list؟

ولما لا تعرف int i داخل ال for كما يفعل الاشخاص العاديون؟

يوسف سيد أضف ردا

لم تضع قيم integer داخل مصفوفة من المؤشرات؟ لم لا تختار النوع الصحيح؟

هل تقصد *int؟ لأنه في Go سيكون *C.int وليس unsafe.Pointer ، ولأن Go متشددة فيما يخص الأنواع لن يكون سهلًا تحويلها إلى []byte

ماذا تظن أنك تفعله في السطر التاني؟

ولما لا تستخدم مؤشر جديد بنوع صحيح طالما لا تستطيع تغيير ال void pointer في ال parameter list؟

نفس الأول النوع Slice في Go هو مصفوفة ثلاثية لثلاثة مؤشرات الأول مؤشر إلى مؤشر القيّم الحقيقية، والثاني مؤشر الطول والثالث الـcapacity، وإذا جعلته كذا *int سأحصل على Runtime Error من Go لأن بالكاد جعلته غير معرف unsafe.Pointer لكي يمرر من CGo هي أصبحت متصرمة جدًا في CGo.

ولما لا تعرف int i داخل ال for كما يفعل الاشخاص العاديون؟

آسف اعتدتُ على الGlobal Scope من JS ES5- :) على العموم أليس هذا stdc99؟ تعريف Cflag في Go مملٌ إما عن طريق flag قبل Import cgo أو أحد متغيرات البيئة ما لا أذكره.

لستُ متمرسًا في الـC بالكاد أستخدمها بما يفيد شيفرتي في Go على العموم شكرًا لك.

a3f أضف ردا
void* goArray() {
    char* array = malloc(initArrayLen * sizeof(int));

    array[0] = 47;
    array[1] = 78;
    array[2] = 124;
    array[3] = 240;
    return array;
} 

struct Go_slice {
    void *ptr;
    int *len;
    int *cap;
};
_Static_assert(sizeof (struct Go_slice) == 3*sizeof (void*));

void iterGoSlice(void *slice_) {
    struct Go_slice *slice = slice_;
    int *elems = slice->ptr;

    printf("Go Slice Length: %d\n", slice->len);
    for (int i=0; i<slice->len; i++) 
        printf("GoSlice[%d] = %d\n", i, elems[i]);
}
a3f أضف ردا

بالتأكيد يوجد هناك headers تعرف structs ك Go_slice التي عرفتها للتو. في هذه الحالة استخدمها.

أنت عملت مصفوفة من المؤشرات. كان بالأحرى تعمل مصفوفة عادية من القيم ثم ترجع مؤشر عليها.

لكي تحصل على الطول من المؤشر كان يكفيك أن تكتب

int *iptr = *(char**)ptr;
int len = cptr[1];

C99 عمرها 17 عاما الآن. ما دامت متاحة استخدمها.

بالتأكيد يوجد هناك headers تعرف structs ك Go_slice التي عرفتها للتو. في هذه الحالة استخدمها.

نعم _cgo_export.h على ما أتذكر

لكي تحصل على الطول من المؤشر كان يكفيك أن تكتب

لن أحتاج إلى هذا لن أحصل مثلًا على الCap سأحتاج إلى الطول والقييم، لذا سطر واحد يكفي.

C99 عمرها 17 عاما الآن. ما دامت متاحة استخدمها.

المشكلة في تعريف الـCflag إن كان في عمل حقيقي أستخدمها بالطبع.

إذن استخدم ال header.

النسخة التي كتبتها من غير struct سطر واحد كذلك مع الفارق أنها صحيحة (على إعتبار أن النوع int صحيح).

ما فعلته هو أنك غيرت argument (لا تفعل ذلك. يقلل من وضوح الكود) بأنك عملت dereference مرة ثم حولته ل pointer على char (رغم أن الطول بكل تأكيد حجمه ليس بايت وحيد) ثم جمعت sizeof void pointer (رغم أن المؤشر char pointer ولكن من باب التنويع أظن) ثم عملت dereference لذلك لكي تحصل على أول بايت.

من باب الصدفة الحجم أصغر من بايت ونظامك little-endian لذلك ظننت أن كودك صحيح. كودك خطأ وقبيح.

يوسف سيد أضف ردا

نعم أعرف :)، لحسن الحظ هو ليس برنامجًا حقيقيًا كان لتوضيح مثالًا يبدو أنني سأتوقف عن كتابة شيفرات C على العام :)

يوسف سيد أضف ردا

أليست الشيفرة التي كتبتَها تعمل بحدود البايت؟ على العموم لا مشكلة ما دام length <= 256 في المثال،

لا، لا لم أعد لهذا فكرتُ في البحث عن أداة بناء حديثة عابرة للمنصات، بدلًا من عمل حفلة جديدة هنا أردتُ أن أستشيرك مع العلم أنّ عملي الحقيقي هو تطوير الويب لذا أنا معتاد على أدوات مثل Gulp ببعض الأسطر القليل سأصنع دالة تراقب ملفات المشروع وتعيد البناء بعد أي تعديل وعابرة للمنصات، لم تعجبني CMake البدائية وMake لا يوجد تطبيق على ويندوز بشكلٍ جيّد بالخصوص mingw32-make ربما Msys2 Make جيّدة لكنها أيضًا أداة بدائية جدًا، وماذا عن كتابة ملفات Python للبناء مثلما أفعل مع Go هل هذا حلٌ جيّد، وماذا عن استخدام Nodejs وأحد أدواتها كـGulp هل هذا معتاد في عالم C/C++؟

كودك كان يفعل ذلك. كودي يتوقع أن حجم ال buffer نوعه int.

Make تفي بالغرض جيدا للمشاريع الصغيرة.

أنا استخدم mingw32-make على ويندوز. ما هي المشكلة التي تقابلك؟

و CMake ليست بدائية. ربما من الأفضل أن تستخدم الشيء قبل أن تحكم عليه.

يوسف سيد أضف ردا

كودك كان يفعل ذلك. كودي يتوقع أن حجم ال buffer نوعه int.

للأسف يجب أن يكون النوع GoInit المعرف من الـ_cgo_export.h هو لا يساوي int دائمًا آسف لم أذكر هذا،

أنا استخدم mingw32-make على ويندوز. ما هي المشكلة التي تقابلك؟

إن استخدمتها بدون bash فسأحتاج إلى استخدام batch للقيام ببعض العمليات كحذف الملفات، لذا لا تعمل جيّدًا بدون CygWin/Msys2 وأيضًا لا تعمل مع CMD جيدًا أحتاج إلى القيام بشيء مثل التالي لتنفيذة تعليمة batch:

cmd \C **

و CMake ليست بدائية. ربما من الأفضل أن تستخدم الشيء قبل أن تحكم عليه.

استخدمتَها أكثر من مرة في مشاريع مختلفة للغات مختلفة، هل يمكنك أن تلقي نظرة على Gulp؟ هل سأجد شيء مثل gulp-watch، أيضًا يزعجني فيها أنها تحتاج إلى ترجمة وليس تنفيذ مباشرة، وكثرة الملفات الناتجة لها، وكبر نص البناء طبعًا أقارن بـGulp كوني مطور ويب.

يمكنك أن تعيد تعريف RM على حسب النظام. عرفها لكي تكون del /Q على Windows_NT.

لا أرى الفائدة تحديدا في إستخدم gulp-watch. يمكنك إعادة البناء بأمر make عندما تريد ذلك أو تشغيل make تلقائيا كلما خزنت ملف.

يوسف سيد أضف ردا

شكرًا لك، فقط كان قصدي أداة متطورة مثل أدوات تطوير الويب وليس gulp-watch بالتحديد، "يمكنك أن تعيد تعريف RM على حسب النظام. عرفها لكي تكون del /Q على Windows_NT." نعم هذا جربته من أحد الاقتراحات على الويب، لكن المشكلة الأخرى هي تنفيذ البناء بداخل CMD تحدث مشكلة غريبة عندي ولا يمكن تنفيذ اسكربتات Batch ، الـCMD ليس مهمًا ولا أحبه فقط إن نتحدث عن ويندوز بدون حلوى إضافية، على العموم شكرًا لك استرحتُ مع Msys2، سؤال أخر آسف ماذا إن استخدمتُ شيفرة Python 2 و3 هل هذا وارد في عالم بناء C/C++ أم أنه ممارسة سيئة؟

رائع جدًا إذن لا مشكلة من Python وهو أعجبني جدًا، شكرًا لك أتعبتك معي أخي :)

يوسف سيد أضف ردا

شكرًا لك أخي، يوجد خطأ فقط في التجميع من مجمع GCC:

error: expected ',' before ')' token
  _Static_assert(sizeof (struct Go_slice) == 3*sizeof (void*));
                                                        ^

نسيت. عدلها ل

 _Static_assert(sizeof (struct Go_slice) == 3*sizeof (void*), "struct not packed");

تمام أخي لكنك نسيتَ أنني أتهرب من الـGo Runtime لذا أمرر متغير أحجزه بـC Malloc وهو مؤشر إلى المؤشر الخاص بالـSlice لكي لا أحصل على ذاك الخطأ الخاص بـGo شاهد ذاك الجزء الذي اقتبسته من التوثيق يمكن ذلك لكني سأمرر متغير إلى البيئة، لذا فعلتُ شيء كمثل:

struct Go_slice *slice = *(void**)slice_;

ويمكن عمل لكن سأغير شيفرة Go:

void iterGoSlice(void **slice_) {

هل هنالك كتابٌ تنصحني به لأفضل الممارسات في C كي أكتب شيفرة جيّدة تساعد شيفرتي في Go، قرأتُ القليل عن أفضل الممارسات في Gnu C لكني مللتُ وتوقفتُ،

a3f أضف ردا

لم أنسى ذلك لذلك حافظت على ال arguments وال return values كما هي رغم أن متأكد أنه يمكن كتابتها بصورة أفضل لكن كما قلت لا أعرف Go. أنا غيرت فقط الكود القبيح داخل الدوال.

أنصحك بقراءة كتاب يشرح سي من البداية.

كونك لا تعرف الفرق بين مؤشر على array و array من مؤشرات ومحاولتك السيئة لقراءة len توحي لي بأن مشكلتك مع أساسيات اللغة وليس مع أفضل الممارسات فيها.

يوسف سيد أضف ردا

-- تعديل شاهد التعليق اللاحق، CGo لا تبحث بقيمة المؤشر-

لم أنسى ذلك لذلك حافظت على ال parameters وال return values كما هي رغم أن متأكد أنه يمكن كتابتها بصورة أفضل لكن كما قلت لا أعرف Go. أنا غيرت فقط الكود القبيح داخل الدوال.

بحسب توثيق Go يمكن لكن ستسخدم Go 1.5 أو أقل، ويمكن ظبط GODEBUG، لا تنسى أن هنالك وسيط بين Go وC وأي مؤشر لGo سيمرر سيكتشف سأحصل على:

panic: runtime error: cgo argument has Go pointer to Go pointer

فهمتُ لماذا هم يفعلون هذا،

،

كونك لا تعرف الفرق بين مؤشر على array و array من مؤشرات ومحاولتك السيئة لقراءة len توحي لي بأن مشكلتك مع أساسيات اللغة وليس مع أفضل الممارسات فيها.

قرأتُ مذ عام The C Programing Language لمرة وأحدة، تعاملي مع اللغة فقط عندما أحتاج إلى فعل شيء خارج Go، أكتبُ فقط ما أحتاجه، وحقًا كنت أعرف الحصول عليه بـGoSlice struct الخاص بـCGo فقط كتبتُ مثالًا سريعًا، ولا أكتب شيفرات C إلى كل قرن وقرن؛ لن أستطيع تعلم اللغة بالممارسة الطويلة،

أسحب كلامي أخي يمكن القيام بvoid* اكتشفتُ أن CGo تمنع المتغيرات الخاصة بـGo وليس بقيمة المؤشر نفسه آسف :) والله لا يوجد ذكر لهذا أبدًا في التوثيق:

cptr := C.malloc(C.size_t(unsafe.Sizeof(&goSlice)))
*(*[]byte)(cptr) = goSlice;
defer C.free(cptr)
C.iterGoSlice(cptr)

شكرًا لك @a3f بعدما انبتهتُ إلى هاته النقطة في التوثيق وبالخصوص السطر الآخير، يبدو أنّ ما قمتُ به خطأ تمامًا :) رغم أني مررتُ على الصفحة أكثر من مرة:

Go is a garbage collected language, and the garbage collector needs to know the location of every pointer to Go memory. Because of this, there are restrictions on passing pointers between Go and C.

In this section the term Go pointer means a pointer to memory allocated by Go (such as by using the & operator or calling the predefined new function) and the term C pointer means a pointer to memory allocated by C (such as by a call to C.malloc). Whether a pointer is a Go pointer or a C pointer is a dynamic property determined by how the memory was allocated; it has nothing to do with the type of the pointer.

Go code may pass a Go pointer to C provided the Go memory to which it points does not contain any Go pointers. The C code must preserve this property: it must not store any Go pointers in Go memory, even temporarily. When passing a pointer to a field in a struct, the Go memory in question is the memory occupied by the field, not the entire struct. When passing a pointer to an element in an array or slice, the Go memory in question is the entire array or the entire backing array of the slice.

C code may not keep a copy of a Go pointer after the call returns.

A Go function called by C code may not return a Go pointer. A Go function called by C code may take C pointers as arguments, and it may store non-pointer or C pointer data through those pointers, but it may not store a Go pointer in memory pointed to by a C pointer. A Go function called by C code may take a Go pointer as an argument, but it must preserve the property that the Go memory to which it points does not contain any Go pointers.

Go code may not store a Go pointer in C memory. C code may store Go pointers in C memory, subject to the rule above: it must stop storing the Go pointer when the C function returns.

These rules are checked dynamically at runtime. The checking is controlled by the cgocheck setting of the GODEBUG environment variable. The default setting is GODEBUG=cgocheck=1, which implements reasonably cheap dynamic checks. These checks may be disabled entirely using GODEBUG=cgocheck=0. Complete checking of pointer handling, at some cost in run time, is available via GODEBUG=cgocheck=2.

It is possible to defeat this enforcement by using the unsafe package, and of course there is nothing stopping the C code from doing anything it likes. However, programs that break these rules are likely to fail in unexpected and unpredictable ways.