المقالات العلمية

مقالات الشبكة العربية لمطوري الألعاب

تـَـقـَصـِّي و كشف الأخطاء في Visual Studio (الجزء الثاني)

هذه المقالة محمية ضمن الحقوق الفكرية لشركة In|Framez Technology Corp.، ومرخصة للعرض فقط ضمن الشبكة العربية لمطوري الألعاب مع الموافقة الصريحة من المؤلف وشركة In|Framez Technology Corp.. لا يسمح بإعادة نشر هذه المقالة أو تعديلها دون الرجوع للمؤلف. يمنع النسخ والاقتباس دون ذكر المصدر والموافقة من المؤلف.


حمل الملفات المرفقة مع هذه المقالة


سنتابع اليوم ما بدأناه معاً، و نقوم بتغطية ميزتين أخريين من مزايا الـ Debugger الموجود في Visual Studio. و اسمح لي أن أكرر قولي ثانيةً: إن المزايا التي نتحدث عنها تتوفر في أي بيئة تطوير محترمة، و لا تقتصر فقط على Visual Studio.


فلندخل بالموضوع مباشرة...

 

النقاط التي ستتم تغطيتها اليوم

  • كيف نستفيد من نافذة المـُـخرَجات (Output Window)؟
  • ماذا يحدث عندما يكون المشروع في وضع Debug؟
  • Output Window، ها قد عدنا...


ملاحظة: إن المشروع المرفق مع هذا المقال، هو نفسه المرفق مع المقال السابق، مع بعض التعديلات على الملف main.cpp.

 

كيف نستفيد من نافذة المـُـخرَجات (Output Window)؟

كما شاهدنا في المقال الأول، تظهر نافذة المُخرجات (Output Window) في الزاوية السفلى اليمنى (في الحالة الاعتيادية). و تبدأ في العمل عندما تنفذ برنامجك في وضع Debug. إذا كنت قد طبقت المثال المرفق مع المقال السابق، فلا بد أنك قد لاحظت المعلومات التي تظهر في هذه النافذة أثناء سير البرنامج.

إن هذه النافذة (تدعى Pane بمصطلحات Visual Studio) تـُـظهـِر معلومات عن أخطاء البناء (Build Errors) أثناء ترجمة و بناء البرنامج . و عند تنفيذ البرنامج، تتحول وظيفتها إلى وسيلة تخاطب تتيح للبرنامج أن يخبر صاحبه بأي شيء يفعله.


ملاحظة : إذا كانت نافذة المخرجات لا تظهر أمامك، يمكنك إظهارها بالطريقة الآتية:

قائمة View : القائمة الفرعية Other Windows : الأمر Output.

أو باستخدام الاختصار (Ctrl+Alt+O)...




يمكننا استغلال هذه النافذة كطريقة إخراج معلومات، تعمل بنفس الأسلوب الشائع في كشف الأخطاء: printf() أو MessageBox() . لكي أكون واضحاً في كلامي، أنظر المثال الآتي:

   1: void main(void)
   2: {
   3:     int iNum = 10;
   4:     for (int i=0;i<3;i++)
   5:     {
   6:         iNum += 2;       
   7:         // Dump out the contents of iNum
   8:         printf("iNum now contains : %d \n",iNum);
   9:     }
  10: }

عند تنفيذ هذا البرنامج في نافذة Console، فإنه سيظهر على الشاشة الرسائل الآتية:

iNum now contains : 12
iNum now contains : 14
iNum now contains : 16

و في حالة تطبيق Win32 نظامي، فإنك تكتب:

   1: int WINAPI WinMain (HINSTANCE, HINSTANCE, LPSTR, int)
   2: {
   3:     int iNum = 10;
   4:  
   5:     for (int i=0;i<3;i++)
   6:     {
   7:         iNum += 2;
   8:         
   9:         // Dump out the contents of iNum
  10:         char szText[256];
  11:         sprintf(szText,"iNum now contains : %d \n",iNum);
  12:         MessageBox(NULL,szText,"INFO",MB_OK);
  13:     }
  14:  
  15:     return 0;
  16: }

و عند تنفيذ هذا البرنامج، فإنه سيظهر على الشاشة صندوق حوار كالآتي ثلاث مرات متتالية:

 

 
و لكنك لا تريد لأية رسائل أن تعطل سير برنامجك (هذه مشكلة كبيرة في البرامج متعددة المسارات (Multithreaded)، و لا تريد لهذه الرسائل أن تظهر للمستخدم النهائي... فما الحل؟


يقدم الإجراء OutputDebugString() حلاً ناجعاً لهذه المشكلة. حيث يقوم هذا الإجراء بطباعة ما تشاء من نصوص على نافذة المخرجات (Output Window). يتقبل هذا الإجراء البسيط مُعطى واحد، و هو الـ string الذي تريد إظهاره على نافذة المخرجات.
و الآن نستبدل MessageBox() من المثال السابق بهذا الإجراء:

   1: int WINAPI WinMain (HINSTANCE, HINSTANCE, LPSTR, int)
   2: {
   3:     int iNum = 10;
   4:  
   5:     for (int i=0;i<3;i++)
   6:     {
   7:         iNum += 2;
   8:         
   9:         // Dump out the contents of iNum
  10:         char szText[256];
  11:         sprintf(szText,"iNum now contains : %d \n",iNum);
  12:         OutputDebugString(szText);
  13:     }
  14:  
  15:     return 0;
  16: }

نفذ البرنامج في وضع Debug (اضغط F5)، و راقب نافذة المخرجات... عندما ينتهي تنفيذ البرنامج، يجب أن تكون قد حصلت على مُـخرجات شبيهة بالموجودة في الصورة أدناه...

 

 
إن هذه المخرجات تظهر بشكل متزامن (synchronized) مع برنامجك، و بنفس الوقت، لا تعطل سيره. كل هذا جميل، و لكن هل من المعقول أن أكتب 3 أسطر code كلما أردت أن أظهر رسائلي في نافذة المخرجات؟


حسناً، سنقوم بكتابة إجراء صغير يسهل علينا المهمة بشكل كبير... أنظر الـ code التالي:

   1: void OutputDebugf(LPCTSTR lpszFormat, ...)
   2: {
   3:     va_list args;
   4:     va_start(args, lpszFormat);
   5:  
   6:     TCHAR szBuffer[512];
   7:     _vsntprintf(szBuffer, 512, lpszFormat, args);
   8:     OutputDebugString(szBuffer);
   9:     va_end(args);
  10: }

الآن يمكننا إعادة كتابة آخر مثال بالشكل التالي:

   1: int WINAPI WinMain (HINSTANCE, HINSTANCE, LPSTR, int)
   2: {
   3:     int iNum = 10;
   4:  
   5:     for (int i=0;i<3;i++)
   6:     {
   7:         iNum += 2;
   8:         
   9:         // Dump out the contents of iNum
  10:         OutputDebugf("iNum now contains : %d \n",iNum);
  11:     }
  12: }

هكذا، نكون قد وفرنا على أنفسنا الكثير من الجهد، بالإضافة إلى الحفاظ على بساطة الـ code و نظافته.

باعتبار أن المستخدم النهائي لا يملك نافذة مخرجات، فيمكنك ترك هذا الإجراء في الإصدار النهائي للبرنامج، دون أية مشاكل . تستخدم الكثير من الـ APIs هذا الإجراء لإظهار معلومات مفيدة للمبرمجين، نذكر من هذه الـ APIs مجموعة أصناف Microsoft العتيدة MFC . حيث أنها تخبرك بأي تسريب يحدث في الذاكرة (Memory Leak)، مما يساعد على الحفاظ على أفضل أداء لبرنامجك ... مثلاً، قم بحجز مصفوفة المحارف (string) الآتية في أي تطبيق MFC:

   1: char *pText = new char[24];
   2: strcpy(pText,"A leaky string, not freed!");
   3:  
   4: // Don't release
   5: //delete[] pText;

عند أنهاء التطبيق، ستجد أن MFC قد كشفت عن هذا التسريب، و أظهرته لك في نافذة المخرجات كما في الصورة أدناه:

 

 
ليس هذا فقط، بل و أخبرتني باسم الملف و رقم السطر الذي تم حجز هذه الذاكرة فيه (الملف mfctestdlg.cpp، السطر 47)! طبعاً هذه المعلومات لن تظهر للمستخدم النهائي.

حسناً، سنتوقف عن الحديث عن نافذة المخرجات هذه مؤقتاً، و نناقش موضوعاً أكثر فائدة...

 

ماذا يحدث عندما يكون المشروع في وضع Debug؟

ما هو وضع Debug أصلاً؟ سأجيب بشكل عملي...

يحتوي كل مشروع في Visual Studio على نظامي بناء (Build Configurations) بشكل اعتيادي ... يمكنك رؤيتهما عن طريق القائمة Build، فالأمر Configuration Manager...

و هما Debug و Release ... ماذا تعني كل منهما؟


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

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

 
هذه الاختلافات على صعيد الناتج النهائي، و لكن على صعيد المترجم، كيف يفرق بين Debug و Release؟

هناك العديد و العديد من الفروق بين هذين الوضعين، تتواجد كلها في إعدادات المشروع (Project Settings) و التي يمكنك ملاحظتها بنفسك، عن طريق التبديل بين الوضعين ضمن صفحة الإعدادات ذاتها...

 

 
على أن أكثر ما يهمني الآن من هذه الإعدادات، تلك الموجودة تحت القسم C/C++. و بشكل خاص، البند المسمى Preprocessor Definitions.

كل المعرِّفات في هذا البند يتم تضمينها في كل ملف .cpp قبل ترجمته . فمثلاً، إذا كان لديك الملف الآتي:

   1: //// File : mysource.cpp ////
   2: #include <stdio.h>
   3:  
   4: void main(void)
   5: {
   6: }

و كنا قد وضعنا المعرف _CONSOLE في البند Preprocessor Definitions، فإن المترجم سيضيفه في بداية الملف عند بدء الترجمة، ليصبح الملف هكذا:

   1: #define _CONSOLE
   2: //// File : mysource.cpp ////
   3: #include <stdio.h>
   4:  
   5: void main(void)
   6: {
   7: }

حسناً، و الآن فلنأخذ نظرة سريعة على إعدادات المشروع الافتراضية التي يحددها Visual Studio لكلا الوضعين Debug و Release... افتح صفحة إعدادات المشروع Project Settings و اتجه إلى القسم C/C++... لاحظ المعرفات الموجودة في البند Preprocessor Definitions...

 



لا بد أنك قد لاحظت وجود المعرف _DEBUG في الوضع Debug، و لكن ما الأمر بالنسبة للوضع Release؟ نعم! لقد اختفى المعرف _DEBUG و ظهر بدلاً منه المعرف NDEBUG!

(الفرق هو حرف الـ N الذي كان _).
ماذا يعني هذا؟
يعني أنك عندما تبني برنامجك في وضع Debug، فإن كل ملفاتك التي تترجمها تملك المعرف _DEBUG، و بشكل مماثل، فإن كل الملفات المبنية في وضع Release تملك المعرف NDEBUG. رائع! و ماذا بعد؟
سأسترق نظرة إلى الملف afx.h (الملف الرئيسي لـ MFC)، و أقتطع هذا الجزء الصغير من الـ code. أنظر ماذا وجدت:

   1: #ifdef _DEBUG
   2:     #pragma comment(lib, "mfc70d.lib")
   3:     #pragma comment(lib, "mfcs70d.lib")
   4: #else
   5:     #pragma comment(lib, "mfc70.lib")
   6:     #pragma comment(lib, "mfcs70.lib")
   7: #endif

مرة أخرى، ماذا يعني هذا؟

يعني أنك عندما تبني برنامجك في وضع Debug، فإنه سيتم ربطه (Static Link) مع المكتبتين mfc70d.lib و mfcs70d.lib! بينما سيتم ربطه مع الملفين mfc70.lib و mfcs70.lib في حال بناء المشروع في وضع Release. واضح جداً أن المكتبتين mfc70.lib و mfcs70.lib تفتقران إلى معلومات الـ Debug، و هما المكتبتان الواجب استخدامهما عندما تريد توزيع النسخة النهائية من برنامجك . و هما كذلك أصغر حجماً، و أسرع أداءً!

 
بمثل هذه الطريقة، يستطيع المترجم انتقاء الملفات المناسبة لكل نمط من الإعدادات، سواء Debug أو Release أو غيرهما.

تعتمد أغلب الـ APIs على مثل هذه المعرفات لتخصيص إمكانياتها، فمثلاً، وضع المعرف UNICODE يؤدي إلى ترجمة برنامجك لبيئة Unicode (مثل WindowsNT)، و بالتالي تصبح المتغيرات من نوع TCHAR معرفة على أنها wchar_t بدلاً من char النظامي (لمزيد من التفاصيل انظر مقال "ما هو الـ Unicode ؟").

 

Output Window، ها قد عدنا...

جيد، و الآن سأعيد كتابة إجراءنا الصغير OutputDebugf() ثانية، و لكن مع أخذ المعرف _DEBUG بعين الاعتبار ... أنظر الـ code الجديد:

   1: #ifdef _DEBUG
   2:     void _OutputDebugf(LPCTSTR lpszFormat, ...)
   3:     {
   4:         va_list args;
   5:         va_start(args, lpszFormat);
   6:  
   7:         TCHAR szBuffer[512];
   8:         _vsntprintf(szBuffer, 512, lpszFormat, args);
   9:         OutputDebugString(szBuffer);
  10:         va_end(args);
  11:     }
  12:     #define OutputDebugf _OutputDebugf
  13: #else
  14:     inline void _OutputDebugf(LPCTSTR, ...) {};
  15:     // Microsoft Visual C++ supports the __noop intrinsic, use void(0) for other compilers
  16:     #define OutputDebugf __noop
  17: #endif    // _DEBUG

بكل بساطة، إذا كان المعرف _DEBUG محققاً، فإننا نعلن عن الإجراء OutputDebugf() بشكله الكامل . أما إذا كان _DEBUG غير محققاً (المشروع في وضع Release مثلاً)، فإننا نعرف الإجراء على أنه لا شيء! و بالتالي فإن المترجم سيتجاهل الإجراء كلياً عندما يقابله في بقية أجزاء الـ code، مما يؤدي إلى أن الملف التنفيذي النهائي و المبني في وضع Release لن يستدعي هذا الإجراء، و بالتالي لن يتأذى الأداء النهائي بعمليات لا طائل منها (إظهار رسائل لن يقرأها أحد!).


إذا كنت لا تصدقني، يمكنك أن تغير التعليمة #ifdef إلى #ifndef في الـ code أعلاه، ثم أعد بناء البرنامج ثانية و نفذه بوضع Debug (اضغط F5) و راقب نافذة المخرجات لترى صحة كلامي . جميل أليس كذلك؟

 

تسألني ما هي التعليمة __noop ؟ بسيطة، واضح تماماً من اسمها أنها تعليمة "لا تفعل شيئاً!" (No Operation). و هي تعليمة ابتكرتها Microsoft لمثل هذه الحالات تماماً. و هي ليست موثقة ضمن مقاييس ANSI C/C++، لذلك لا يشترط أن تتواجد في بقية المترجمات (يمكنك استخدام void(0) بدلاً من __noop في حالة المترجمات الأخرى).

عندما يواجه المترجم __noop فإنه سيتجاهلها تماماً، و بالتالي لن يكون لها أي تأثير . فإذا عرفنا إجراء ما على أنه __noop، فإننا بذلك نزيل أية استدعاءات له من الـ code.


ملحوظة لمستخدمي MFC: يمكنكم استخدام الإجراء TRACE() و الذي يقوم بنفس دور OutputDebugf() تماماً.

 

في المرة القادمة...

سنكمل حديثنا عن بقية نوافذ Visual Studio المستخدمة في الـ Debugging، محاولين بذلك كشف الغطاء عن هذا القسم الغامض و الضروري من أي عملية تطوير ناجحة. ستجد في المرفقات مع هذا المقال الملف main.cpp الذي يحتوي على آخر مثال ذكرته، متضمناً معه الإجراء القيم OutputDebugf().

و به أتمنى أن أكون قد ساعدتك في تتبع أخطائك و تفاديها، و بالتالي توفير الوقت، و بالتالي توفير المال! (إذاً أنت مدين لي بـ 1000$ الآن!) ... (أمزح فقط!).


إلى اللقاء!

 

لمزيد من المعلومات

المصطلحات المستخدمة

المصطلح معناه
Output Window نافذة المـُـخرَجات . تقوم هذه النافذة بإظهار معلومات عن عمل بيئة التطوير، مثل حالة بناء المشروع . كما تقوم بعض الأدوات الأخرى بإرسال نتائجها إلى هذه النافذة (مثل الـ Debugger) .
Build Errors أخطاء البناء . الأخطاء التي يواجهها المترجم أثناء ترجمة (compile) و ربط (link) ملفات المشروع . بالإضافة إلى الأخطاء التي لا يمكن إكمال بناء المشروع بوجودها، توجد التحذيرات (warnings) و التي يمكن تجاهلها (لا ينصح بذلك) و إكمال بناء المشروع بشكل تام .
Multithreaded متعدد المسارات . التطبيقات متعددة المسارات تستطيع تنفيذ عدة مسارات من الـ code بشكل متوازي و بنفس الوقت . مثلاً، code يقوم برسم شكل معقد، و بنفس الوقت code آخر في نفس البرنامج يقوم بأداء عمليات حسابية معينة .
End User المستخدم النهائي . و هو الزبون الذي سيكون الطرف الأخير في سلسلة مستخدمي برنامجك، و الذي تقوم بتطوير برنامجك من أجل تلبية حاجاته . مثلاً، أنت مستخدم نهائي لـ Visual Studio!
Synchronized متزامن . نقول عن إجراء أنه يعمل بشكل متزامن (synchronous) عندما يضطرك إلى انتظاره حتى ينتهي كي يستطيع برنامجك إكمال سيره . بينما الإجراءات الغير متزامنة (asynchronous) فإنها تعيد التحكم إلى البرنامج الذي استدعاها مباشرة، و تكمل عملها بشكل مستقل ضمن مسار (thread) جديد .
MFC اختصار لـ Microsoft Foundation Classes . و هي مجموعة classes تقوم بتغطية أغلب مهام الـ Win32 API و تقديمها بشكل أسهل استخداماً، لذا فإن استخدامها عادةً يوفر الكثير من الوقت . من أشهر الـ classes الموجودة ضمنها، CDialog، CDC و CWnd .
Memory Leak تسريب في الذاكرة . يحدث هذا التسريب عندما تحجز (بشكل يدوي) جزءاً من الذاكرة و لاتحرره أبداً عندما تفرغ من استعماله . بقولي "شكل يدوي" أقصد الإجراء malloc() أو التعليمة new، أو أي إجراء آخر يستخدم هاتين التعليمتين . إن عدم تحرير الذاكرة بعد الانتهاء من استعمالها يؤدي إلى استنفاذ موارد النظام، و بالتالي التأثير بشكل سيء على أداء برنامجك بشكل خاص، و على أداء النظام و ثباته بشكل عام .
Build Configuration نظام (إعدادات) البناء . يقدم نظام البناء طريقة لتحديد الوحدات المطلوب بناءها، و استبعاد تلك التي لا تريد تضمينها في المشروع، بالإضافة إلى تحديد الخيارات التي تبين كيف سيتم بناء المشروع و على أي نظام سيعمل (Win32 مثلاً) . عندما تقوم بإنشاء مشروع جديد في Visual Studio، سيتم توليد نظامي بناء بشكل أوتوماتيكي، و هما Debug و Release .
Preprocessor Definitions معرفات ما قبل الترجمة . مجموعة من المعرفات التي سيتم تضمينها في بداية ملفات الـ code بنفس الطريقة التي تعمل بها التعليمة #define . وُصِفَت بـ preprocessor لأن تضمينها يتم قبل أن تبدأ عملية الترجمة الفعلية .

أضف تعليقاً

Loading