نتحدث في هذا المقال عن مجموعة من الخصائص غير المرتبطة مع بعضها بعضًا مباشرةً ولكنها تصب في موضوع المكتبات والتعامل معها في لغة سي، وهي القفزات اللا محلية والتعامل مع الإشارات والدوال ذات العدد المتغير من الوسطاء، ونستعرض الدوال والأنواع والماكرو الموجودة بداخل ملفات الترويسة الموافقة لكل منها.
القفزات اللا محلية
تقدم القفزات اللا محلية 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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.