اذهب إلى المحتوى

التعامل مع المكتبات في لغة سي C


Naser Dakhel

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

القفزات اللا محلية

تقدم القفزات اللا محلية non-local jumps طريقةً مشابهة لطريقة goto بالانتقال من دالة إلى أخرى. نلجأ إلى استخدام الماكرو setjmp والدالة longjmp لأن الأمر غير ممكن الحدوث باستخدام goto والعناوين labels إذ أن للعناوين نطاق scope داخل الدالة فقط، وتُعرف هذه الطريقة باسم goto اللا محلية أو القفزة اللا محلية.

يصرح ملف الترويسة <setjmp.h> شيئًا يدعى jmp_buf، وهو اسم مستخدم في الماكرو والدالة لتخزين المعلومات الضرورية لإجراء القفزة، وتُكتب التصاريح على النحو التالي:

#include <setjmp.h>

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

يُستخدم الماكرو setjmp لتهيئة قيمة jmp_buf ويُعيد القيمة صفر عند استدعائه الأولي، إلا أن الأمر غير الاعتيادي هنا، هو أنه يُعيد مجددًا قيمة غير صفرية لاحقًا عند استدعاء الدالة longjmp، وتكون القيمة غير الصفرية هذه مساويةً للقيمة المُمرّرة للدالة longjmp. لعل الأمر سيتضح لك بوضوح بعد المثال التالي:

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

void func(void);
jmp_buf place;

main(){
        int retval;

        /*
    ‫يُعيد الاستدعاء الأول القيمة 0، ويعيد استدعاء آخر للدالة longjmp قيمة غير صفرية
         */
        if(setjmp(place) != 0){
                printf("Returned using longjmp\n");
                exit(EXIT_SUCCESS);
        }

        /*
    لن يُعيد الاستدعاء التالي أي قيمة لأنه يقفز مجددًا إلى الأعلى
         */
        func();
        printf("What! func returned!\n");
}

void
func(void){
      /*
    العودة إلى‫ main، ويبدو أن الاستدعاء الثاني لدالة setjmp يعيد القيمة 4
       */
      longjmp(place, 4);
      printf("What! longjmp returned!\n");
}

[مثال 1]

يمثل وسيط الدالة longjmp المسمى val القيمة المُعادة من الاستدعاء الثاني اللاحق لتعليمة الإعادة return ضمن الدالة setjmp، ويجب أن تكون هذه القيمة قيمة غير صفرية عادةً، وستغيّر القيمة إلى 1 إذا حاولت إعادة القيمة صفر باستخدام longjmp، وبذلك يمكننا معرفة فيما إذا كان استدعاء الدالة setjmp مباشرةً أو عن طريق استدعاء الدالة longjmp.

تأثير الدالة setjmp غير محدد إذا لم يكن هناك أي استدعاء لها قبل استدعاء longjmp، وسيتسبب ذلك غالبًا بتوقف البرنامج. لا يُتوقّع من الدالة longjmp أن تُعيد قيمة بعد استدعائها مباشرةً. يكون لجميع الكائنات الممكن الوصول إليها من تعليمة الإعادة return داخل الدالة setjmp القيم السابقة المخزنة عند استدعاء longjmp عدا الكائنات ذات صنف التخزين التلقائي automatic storage class التي لا تحتوي على نوع "volatile"، وتكون قيمها غير محددة إذا تغيرت هذه الكائنات بين استدعاء setjmp واستدعاء longjmp.

تُنفّذ الدالة longjmpعلى نحوٍ صحيح بخصوص المقاطعات interrupts والإشارات وأي دوال أخرى مرتبطة، ونحصل على سلوك غير معرف إذا حصل استدعاء longjmp باستخدام دالة نتج استدعائها عن إشارة وصلت بينما تُعالج إشارة أخرى.

يُعدّ القفز إلى دالة غير فعالة باستخدام longjmp خطئًا فادحًا (ويُقصد بدالة غير فعالة أنها أعادت قيمة للتو، أو أن استدعاء longjmp آخر تحوّل إلى setjmp ضمن مجموعة من الاستدعاءات المترابطة nested calls).

يصرّ المعيار على أن setjmp يجب أن تستخدم فقط مثل تعبير للتحكم في تعليمات if و switch و do و while و for (إضافةً إلى كونها التعليمة الوحيدة الموجودة في تعليمة تعبير)، وامتدادًا لهذه القاعدة، يمكن لاستدعاء setjmp (طالما يشكّل تعبير التحكم بأكمله كما ذكرنا سابقًا) أن يخضع للعامل !، أو أن يُقارن مباشرةً مع تعبير ثابت ذي قيمة عدد صحيح باستخدام إحدى العوامل العلاقيّة أو عوامل المساواة، ولا يجب استخدام أي تعابير معقدة أكثر من ذلك. إليك الأمثلة التالية:

setjmp(place);                    /* تعليمة تعبير */
if(setjmp(place)) ...             /* تعبير تحكم كامل */
if(!setjmp(place)) ...            /* تعبير تحكم كامل */
if(setjmp(place) < 4) ...         /* تعبير تحكم كامل */
if(setjmp(place)<;4 && 1!=2) ...  /* ممنوع */

التعامل مع الإشارة

تقدّم لنا دالتان إمكانية التعامل مع الأحداث غير المتزامنة؛ وتُعرف الإشارة signal بأنها شرط قد يحدث خلال تنفيذ البرنامج ويمكن تجاهله أو التعامل معه بصورةٍ خاصة أو استخدامه لإنهاء البرنامج كما هي الحالة الاعتيادية. تُرسل إحدى الدوال الإشارة بينما تُستخدم الأخرى لتحديد كيفية معالجة الإشارة، وقد تولّد الكثير من الإشارات من العتاد الصلب أو نظام التشغيل إضافةً إلى دوال إرسال الإشارات raise.

الإشارات المُعرّفة في ملف الترويسة <signal.h>، هي:

  • الإشارة SIGABRT: إنهاء غير اعتيادي للبرنامج، مثل الإنهاء الحاصل باستخدام الدالة abort (إبطال).
  • الإشارة SIGFPE: عملية حسابية خاطئة، مثل التقسيم على الصفر أو الطفحان overflow (استثناء الفاصلة والأرقام العشرية Floating point exception).
  • الإشارة SIGILL: العثور على "كائن برنامج غير صالح"، وهذا يعني غالبًا أن هناك تعليمات غير صالحة في البرنامج. (تعليمة غير صالحة Illegal instruction).
  • الإشارة SIGINT: إشارة تفاعلية للفت الانتباه، وتولد هذه الإشارة على الأنظمة التفاعلية عادةً بكتابة مفتاح الهروب break-in في الطرفية terminal (مقاطعة Interrupt).
  • الإشارة SIGSEGV: محاولة غير صالحة للوصول إلى مساحة تخزين، وتُسبب غالبًا بمحاولة تخزين قيمة في كائن مُشار إليه بمؤشر خاطئ. (انتهاك جزء segment violation).
  • الإشارة SIGTERM: طلب إنهاء للبرنامج. (إنهاء Terminate).

قد تمتلك بعض التنفيذات implementations بعض الإشارات الإضافية الزائدة عن الإشارات السابقة المعرفة في المعيار، وستبدأ أسماء الإشارات بالأحرف SIG وستمتلك قيمًا مميزة مختلفة عن القيم السابقة.

تسمح لك الدالة signal بتحديد الفعل الذي تريد اتخاذه عند تلقي إشارة، وتُصطحب بحالة إشارة من الإشارات المذكورة سابقًا ومؤشر يشير إلى دالة تُنفّذ للتعامل مع الإشارة، وذلك بتغيير المؤشر وإعادة القيمة الأصلية، إذًا، نعرف الدالة كما يلي:

#include <signal.h>
void (*signal (int sig, void (*func)(int)))(int);

يدل ما سبق على أن الدالة signal تُعيد مؤشرًا يشير إلى دالة أخرى وتأخذ الدالة الثانية وسيطًا واحدًا من نوع عدد صحيح وتُعيد void؛ بينما يكون الوسيط الثاني للدالة signal مؤشرًا يشير إلى دالة تعيد void بصورةٍ مشابهة، وتأخذ int وسيطًا لها.

يمكن استخدام قيمتين مميزتين لوسيط لدالة func (دالة التعامل مع الإشارة)، ألا وهما SIG_DFL وهي معالج الإشارة الافتراضي الأولي و SIG_IGN الذي يُستخدم لتجاهل الإشارة، ويضبط التنفيذ حالة جميع الإشارات إلى واحدة من هذه القيمتين في بداية البرنامج.

تُعاد قيمة func السابقة للإشارة إذ استدعيت signal بنجاح، وإلا فتُعاد SIG_ERR ويُضبط errno إلى قيمة.

عند حصول حدث إشارة غير مُتجاهل، يُنفّذ أول signal(sig, SIG_DFL)‎ يطابق الحالة وذلك إذا كانت الدالة func المترافقة تمثّل مؤشرًا يشير إلى دالة، وتتسبب تلك العملية بإعادة تشغيل معالج الإشارة إلى الإجراء الافتراضي ألا وهو إنهاء البرنامج، وإذا كانت الإشارة هي SIGILL فسيكون إعادة التشغيل معرفًا حسب التنفيذ، إذ قد تختار بعض التنفيذات حجب أي حالات أخرى من الإشارة عوضًا عن إعادة التشغيل.

بعد ذلك، يُجرى استدعاء لدالة معالجة الإشارة، وسيعاود البرنامج عمله من نقطة حصول الحدث في معظم الحالات وذلك إذا أعادت الدالة قيمة بنجاح، إلا أننا نحصل على سلوك غير معرف إذا كانت قيمة sig مساويةً إلى SIGFPE (استثناء الفاصلة العائمة) أو أي استثناء حسابي معرف بحسب التنفيذ، والحل الأكثر استخدامًا لمعالج SIGFPE هو استدعاء إحدى الدوال: abort، أو exit، أو longjmp.

يستعرض الجزء التالي استخدام الإشارة لتحقيق خروج أنيق من البرنامج عند تلقي مقاطعة أو إشارة "الانتباه التفاعلي interactive attention".

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>


FILE *temp_file;
void leave(int sig);

main() {
        (void) signal(SIGINT,leave);
        temp_file = fopen("tmp","w");
        for(;;) {
                /*
                 افعل بعض الأشياء هنا
                 */
                printf("Ready...\n");
                (void)getchar();
        }
        /* لا يمكننا الوصول إلى هذه النقطة */
        exit(EXIT_SUCCESS);
}

/*
أغلق الملف‫ tmp عند الحصول على SIGINT، لكن انتبه
لأن استدعاء دوال المكتبات من معالج الإشارة غير مضمون العمل في جميع التنفيذات 
وهذا ليس ببرنامج متجاوب مع جميع التنفيذات بالضرورة
 */

void
leave(int sig) {
        fprintf(temp_file,"\nInterrupted..\n");
        fclose(temp_file);
        exit(sig);
}

[مثال 2]

من الممكن للبرنامج أن يرسل إشارات إلى نفسه باستخدام دالة raise وهذا معرّف على النحو التالي:

include <signal.h>
int raise (int sig);

تُرسل الإشارة sig في هذه الحالة إلى البرنامج.

تُعيد التعليمة raise القيمة صفر في حال النجاح، وقيمة غير صفرية عدا ذلك، تُنفّذ دالة abort على النحو التالي:

#include <signal.h>

void
abort(void) {
  raise(SIGABRT);
}

إذا حصلنا على إشارة لأي سبب كان -باستثناء استدعاء abort أو raise- فمن الممكن للدالة أن تستدعي فقط الإشارة أو أن تُسند قيمةً إلى كائن ساكن static متطاير volatile (مؤهل باستخدام volatile) من النوع sig_atomic_t، وهذا النوع مصرّحٌ في ملف الترويسة <signal.h>، وهو النوع الوحيد من الكائنات الممكن تعديله بأمان مثل كيان ذري atomic entity حتى مع وجود المقاطعات اللا متزامنة، وهذا قيدٌ مرهق مفروض من المعيار، الذي على سبيل المثال، يُبطل الدالة leave في مثالنا أعلاه، وعلى الرغم من أن الدالة ستعمل بصورةٍ صحيحة في بعض البيئات إلى أنها لا تتبع القوانين الصارمة الخاصة بالمعيار.

أعداد متغيرة من الوسطاء

غالبًا ما يكون تنفيذ دالة تأخذ عددًا غير معروفًا أو غير ثابت من الوسطاء محبّذًا عند كتابة الدالة، نذكر دالة printf على سبيل المثال التي سنتكلم عنها لاحقًا. يوضح المثال التالي تصريح دالة مشابهة.

int f(int, ... );

int f(int, ... ) {
        .
        .
        .
}

int g() {
        f(1,2,3);
}

[مثال 3]

علينا تضمين الدوال المصرح عنها ضمن ملف الترويسة <stdarg.h> لكي نستطيع الوصول إلى الوسطاء الموجودة بداخل الدالة المُستدعاة، ونحصل نتيجةً لذلك على نوع جديد يدعى va_list وثلاثة دوال تتعامل مع كائنات من هذا النوع وتدعى va_start و va_arg و va_end.

علينا استدعاء va_start قبل محاولة الوصول إلى لائحة الوسطاء المتغيرة، وهي معرفة على النحو التالي:

#include <stdarg.h>
void va_start(va_list ap, parmN);

يُهيّئ الماكرو va_start الوسيط ap بهدف الاستخدام اللاحق من قبل الدالتين va_arg و va_end، بينما يكون الوسيط الثاني للدالة va_start المسمّى parmN المعرّف identifier الذي يسمّي المعامل الذي يقع أقصى اليمين في لائحة المعاملات المتغيرة (أي المعامل الذي يقع قبل "…,")، ولا يجب التصريح عن المعرف parmN باستخدام صنف التخزين storage class من النوع register أو على أنه دالة أو نوع مصفوفة.

يمكن الوصول إلى الوسطاء على نحوٍ تتابعي بعد التهيئة وذلك باستخدام الماكرو va_arg، وهذا غير مألوف لأن النوع المُعاد يحدّد باستخدام وسيط للماكرو. لاحظ أن ذلك مستحيل التنفيذ في دالة فعلية، ويمكن تنفيذه فقط باستخدام الماكرو، وهو معرّف على النحو التالي:

#include <stdarg.h>
type va_arg(va_list ap, type);

سيتسبب كل استدعاء للماكرو السابق بالحصول على الوسيط التالي من لائحة الوسطاء بقيمة من النوع المُحدّد، ويجب للوسيط va_list أن يُهيّأ باستخدام va_start، ونحصل على سلوك غير معرّف إذا لم يكن الوسيط التالي من النوع المُحدّد. احذر من المشاكل التي قد تنتج من التحويلات الحسابية وتفاداها، إذ أن استخدام النوع char أو عدد صغير short وسيطًا ثانيًا للدالة va_arg خطأٌ واضح؛ لأن هذه الأنواع تُرقّى دائمًا إلى signed int أو unsigned int ويُحوّل float إلى double.

لاحظ أن ترقية الكائنات المصرّحة عنها من الأنواع char و unsigned char و unsigned short وحقول البت عديمة الإشارة unsigned bitfields إلى النوع unsigned int الذي سيعقّد أكثر استخدام الدالة va_arg هو معرّف بحسب التنفيذ، وقد يكون ذلك هو السبب في الحصول على بعض المشاكل الخفية غير المتوقعة.

نحصل على سلوك غير معرف أيضًا إذا استُدعيت الدالة va_arg ولم يكن هناك مزيدًا من الوسطاء.

يجب أن يكون الوسيط type -في تعريفنا السابق لدالة va_arg- ممثلًا لاسم نوع يمكن تحويله إلى مؤشر يشير إلى كائن بإضافة المحرف "*" ببساطة (حتى يعمل الماكرو)، وذلك محقق للأنواع البسيطة مثل char (لأن char *‎ يمثّل نوع مؤشر يشير إلى محرف)، لكن لن تعمل مصفوفة المحارف (لا يتحول النوع char []‎ إلى مؤشر يشير إلى مصفوفة محارف بإضافة "*" إليه). يمكن لحسن الحظ معالجة المصفوفات إذا ما تذكرنا أن اسم المصفوفة الذي يُستخدم وسيطًا فعليًا لاستدعاء الدالة يُحوّل إلى مؤشر، وبذلك فإن النوع الصحيح لوسيط من النوع "مصفوفة من المحارف" هو char *‎.

تُستدعى الدالة va_end بعد معالجة جميع الوسطاء، وهذا سيمنع اللائحة va_list من أن تُستخدم بعد ذلك، ونحصل على سلوك غير معرف إذا لم تُستخدم الدالة va_end.

يمكن إعادة قراءة كامل لائحة الوسطاء باستدعاء الدالة va_start مجددًا بعد استدعاء va_end، وتُصرّح الدالة va_end كما يلي:

#include <stdarg.h>
void va_end(va list ap);

يوضح المثال التالي كيفية استخدام كل من va_start و va_arg و va_end ضمن دالة تُعيد أكبر قيم وسطائها التي تكون من نوع عدد صحيح.

#include <stdlib.h>
#include <stdarg.h>
#include <stdio.h>

int maxof(int, ...) ;
void f(void);

main(){
        f();
        exit(EXIT_SUCCESS);
}

int maxof(int n_args, ...){
        register int i;
        int max, a;
        va_list ap;

        va_start(ap, n_args);
        max = va_arg(ap, int);
        for(i = 2; i <= n_args; i++) {
                if((a = va_arg(ap, int)) > max)
                        max = a;
        }

        va_end(ap);
        return max;
}

void f(void) {
        int i = 5;
        int j[256];
        j[42] = 24;
        printf("%d\n",maxof(3, i, j[42], 0));
}

[مثال 4]

ترجمة -وبتصرف- لقسم من الفصل Libraries من كتاب The C Book.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...