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

أنظمة التصريف المستخدمة لبناء البرامج المكتوبة بلغة Cpp وأهم أخطاء عملية البناء


محمد بغات

لغة C++‎، على غرار C، لها تاريخ طويل ومتنوّع بخصوص سير العمل التصريفي وعمليات البناء. واليوم، لدى لغة C++‎ العديد من أنظمة البناء التي تُستخدَم لتصريف البرامج لعدة منصات أحيانًا داخل نظام بناء واحد. وسننظر في هذا الدرس في بعض أنظمة البناء ونحلّلها.

إنشاء بيئة بناء باستخدام CMake

ينشئ CMake بيئات البناء للمُصرِّفات أو بيئات التطوير المتكاملة (IDE) انطلاقًا من تعريف مشروع واحد، وسوف توضّح الأمثلة التالية كيفية إضافة ملف CMake إلى برنامج "Hello World" متعدّد المنصات.

تأخذ ملفات CMake دائمًا الاسم "CMakeLists.txt"، ويجب أن تكون موجودة في مجلّد الجذر لكل مشروع، وربما في المجلدات الفرعية أيضا.

هذا مثال على ملف CMakeLists.txt:

cmake_minimum_required(VERSION 2.4)
project(HelloWorld)
add_executable(HelloWorld main.cpp)

يمكنك رؤية البرنامج يعمل مباشرة من هنا.

هذا الملفُّ يخبر أداةَ CMake باسم المشروع، وإصدار الملف المتوقع، وبعض التعليمات الضرورية لتوليد ملف تنفيذي (executable) يُسمّى "HelloWorld"، والذي يتطلب ملف ‎main.cpp‎.

في المثال التالي: أنشئ بيئة بناء للمُصرِّف أو بيئة تطوير متكاملة من سطر الأوامر:

> cmake .

ابن التطبيق بما يلي:

> cmake --build .

تولّد هذه الشيفرة بيئة البناء الافتراضية للنظام وفق نظام التشغيل والأدوات المُثبّتة، حافظ على الشيفرة المصدرية نظيفة من آثار البناء (build artifacts) باستخدام نمط تصميم "خارج المصدر" (out-of-source):

> mkdir build
> cd build
> cmake ..
> cmake --build .

يمكن لـ CMake أيضًا تجريد (abstract) الأوامر الأساسية للصدفة (shell) الخاصة بالمنصة من المثال السابق:

> cmake -E make_directory build
> cmake -E chdir build cmake ..
> cmake --build build

يتضمّن CMake مولدّات لعدد من أدوات البناء الشائعة وبيئات التطوير المتكاملة، ولإنشاء ملفات makefiles لأجل nmake الخاصة بـ Visual Studio:

> cmake -G "NMake Makefiles" ..
> nmake

التصريف باستخدام GNU

GNU Make هو برنامج مخصّص لأتمتة تنفيذ أوامر الصدفة، وينتمي لعائلة برامج Make، ويتمتع Make بشعبية كبيرة لدى مستخدمي أنظمة التشغيل الشبيهة بيونكس وتلك الموافقة لمعايير POSIX، بما في ذلك الأنظمة المشتقة من نواة لينُكس ونظام Mac OSX ونظام BSD، ويتميّز GNU Make بأنّه مرتبط بمشروع جنو الذي يرتبط بدوره بأنظمة التشغيل المبنية على جنو\لينكس الشهير.

هناك إصدارات أخرى من GNU Make متوافقة مع أنظمة تشغيل أخرى، مثل ويندوز و Mac OS X، وهذا البرنامج مستقر للغاية وله أثر تاريخي يبقيه مشهورًا، لهذا يُدرَّس GNU Make إلى جانب لغتي C و C++‎.

القواعد الأساسية

يجب أن تنشئ ملف Makefile في مجلّد المشروع لأجل التصريف بواسطة make. هذا مثال بسيط على ملف Makefile: سنقسم الملف أثناء سرد المثال للشرح …. Makefile

# إعداد بعض المتغيرات لاستخدامها مع الأمر
# g++ أولا، سنحدد المصرف
CXX=g++

# وغيرها g++ ثم نصرّف مع التنبيهات الموصى بها في
CXXFLAGS=-Wall -Wextra -pedantic

# هذا هو ملف الخرج
EXE=app

SRCS=main.cpp

يُستدَعى هذا الهدف عند استدعاء make في سطر الأوامر، وتقول (EXE)$ الموجودة على اليمين أن الهدف all يعتمد على الهدف (EXE)$. لاحظ أنه بما أن هذا هو الهدف الأول، فسيكون الهدف الافتراضي إن استُدعيَت make بدون هدف، نتابع المثال …

all: $(EXE)

# هذا يكافئ
# app: $(SRCS)

(SRCS)$ يمكن فصلها، مما يعني أن هذا الهدف سيعتمد على كل الملفات. لاحظ أن هذا الهدف له متن تابع (method body)، وهو الجزء المزاح بمسافة جدول واحدة (tab) وليس أربع مسافات فارغة. وسينفذ make الأمر التالي عند بناء هذا الهدف:

g++ -Wall -Wextra -pedantic -o app main.cpp

والذي يعني تصريف main.cpp مع التحذيرات، والإخراج إلى الملف app/.، نتابع …

$(EXE): $(SRCS)
@$(CXX) $(CXXFLAGS) -o $@ $(SRCS)

هذا الهدف ينبغي له أن يعكس الهدف all، وإن استدعيْتَ make مع وسيط مثل make clean فسيُستَدعى الهدف gets الموافق له، …

clean:
@rm -f $(EXE)

ملاحظة: تأكد من أن الإزاحات البادئة قد تمت بزر الجدول (tab)، ولا تستخدم 4 مسافات بيضاء من زر Space. وإلا فسيُطلق الخطأ ‎Makefile:10: *** missing separator. Stop.‎.

لتنفيذ هذه الشيفرة من سطر الأوامر، عليك كتابة ما يلي:

$ cd ~/Path/to/project
$ make
$ ls
app main.cpp Makefile
$ ./app
Hello World!
$ make clean
$ ls
main.cpp Makefile

عمليات البناء التزايدية التزايدي Incremental builds

تظهر فائدة make عندما تكثر الملفات، فماذا لو عدّلت الملف a.cpp دون الملف b.cpp؟ لن يكون من الحكمة إذًا أن تعيد تصريف b.cpp، لأنّ ذلك سيُهدر الوقت. في المثال أدناه، لنفرض أن لدينا الهيكل التالي لمجلد:

.
+-- src
|   +-- a.cpp
|   +-- a.hpp
|   +-- b.cpp
|   +-- b.hpp
+-- Makefile

فسيكون هذا ملف Makefile جيد:

CXX=g++
CXXFLAGS=-Wall -Wextra -pedantic
EXE=app
SRCS_GLOB=src/*.cpp
SRCS=$(wildcard $(SRCS_GLOB))
OBJS=$(SRCS:.cpp=.o)
all: $(EXE)
$(EXE): $(OBJS)
@$(CXX) -o $@ $(OBJS)
depend: .depend
.depend: $(SRCS)
@-rm -f ./.depend
@$(CXX) $(CXXFLAGS) -MM $^>>./.depend
clean:
-rm -f $(EXE)
-rm $(OBJS)
-rm *~
-rm .depend
include .depend

مرّة أخرى، لاحظ إزاحات الجدول البادئة. سيضمن ملفّ Makefile الجديد ألّا تصرّف إلّا الملفات التي عُدِّلت، وهذا سيقلّل وقت التصريف.

التوثيق

للحصول على المزيد من المعلومات عن make، راجع التوثيق الرسمي -بالإنجليزية- من مؤسسة البرمجيات الحرة وإجابة dmckee التفصيلية على موقع stackoverflow.

البناء بواسطة SCons

يمكنك بناء شيفرة "Hello World" متعدّدة المنصات باستخدام أداة Scons، وهي أداة بناء برامج تستخدم لغة بايثون.

أولاً، عليك إنشاء ملف يُسمّى ‎SConstruct‎ -سيبحث SCons عن هذا الملف افتراضيًا-، وينبغي أن يكون الملف -للآن- في مجلّد مع الملف ‎hello.cpp‎. اكتب السطر التالي في الملف الجديد:

Program('hello.cpp')

الآن، من سطر الأوامر، نفّذ الأمر ‎scons‎، ينبغي أن ترى شيئًا من هذا القبيل:

$ scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
g++ -o hello.o -c hello.cpp
g++ -o hello hello.o
scons: done building targets.

رغم أن التفاصيل ستختلف وفقًا لنظام التشغيل والمُصرِّف المُستخدم. سيساعدك الصنفان ‎Environment‎ و ‎Glob‎ على إعداد ما يجب بناؤه. على سبيل المثال، سيبني الملف ‎SConstruct‎ الملف التنفيذي hello باستخدام جميع ملفات cpp في مجلد src، كما أن CPPPATH الخاص به سيكون ‎/usr/include/boost‎، كما يحدّد معيار C++‎ 11، انظر:

env=Environment(CPPPATH='/usr/include/boost/',
CPPDEFINES=[],
LIBS=[],
SCONS_CXX_STANDARD="c++11"
)
env.Program('hello', Glob('src/*.cpp'))

Autotools

هي مجموعة من البرامج التي تُستخدَم لإنشاء نظام بناء جنو (GNU Build System) لأجل حزمة برامج معيّنة، وهي حزمة من الأدوات تعمل معًا لإنتاج العديد من موارد البناء، مثل ملفات Makefile -لتُستخدَم مع برنامج GNU Make، لهذا تُعدُّ هذه الحزمة المولّد المعياري لأنظمة البناء. فيما يلي بعض برامج Autotools الأساسية:

  • Autoconf.
  • Automake (لا تخلط بينها وبين ‎make‎).

والهدف من حزمة Autotools بشكل عام، هو توليد برنامج نصّي متوافق مع يونيكس (Unix-compatible script)، إضافة إلى ملفّ Makefile للسماح للأمر التالي بإنجاز عملية البناء -وكذلك تثبيت- معظم الحُزم:

./configure && make && make install

وترتبط حزمة Autotools على هذا النحو أيضًا ببعض مدراء الحزم (package managers)، خاصة تلك المرتبطة بأنظمة التشغيل التي تتوافق مع POSIX.

Ninja

يوصف نظام بناء Ninja في موقعه بأنه "نظام بناء صغير يركّز على السرعة"، وصُمِّم هذا النظام لكي تُبنى ملفاته بواسطة مولدات ملفات بناء النظام (build system file generators)، ويعتمد أسلوبًا منخفض المستوى (low-level) لبناء الأنظمة، وذلك على عكس مدراء أنظمة البناء عالية المستوى مثل CMake أو Meson.

كُتِب نظام Ninja أساسًا بلغة C++‎ و Python، وقد أُنشِئ كبديل لنظام البناء SCons في مشروع Chromium.

NMAKE (أداة صيانة برامج Microsoft)

NMAKE هي أداة سطر أوامر طوّرتها ميكروسوفت لاستخدامها مع Visual Studio و / أو أدوات سطر أوامر Visual C++‎. ويُنظر إليها على أنها نظام بناء يندرج ضمن مجموعة عائلة Make لأنظمة البناء، ولكنّه يتميّز ببعض الميزات الخاصّة التي تميّزه عن برامج Make الشبيهة بيونكس الأخرى، مثل دعم صياغة مسارات ملفات Windows (والتي تختلف عن مسارات الملفات في يونيكس).

أخطاء المصرف الشائعة (GCC)

مرجعّ غير معرَّف إلى "***"

تحدث أخطاء الرابط (linker) عندما يعجز عن العثور عن رمز مُستخدم، ويحدث هذا غالبًا عند عدم ربط إحدى المكتبات المستخدمة.

  • qmake:
LIBS += nameOfLib
  • cmake:
TARGET_LINK_LIBRARIES(target nameOfLib)
  • استدعاء g++‎:
g++ -o main main.cpp -Llibrary/dir -lnameOfLib

قد ينسى المرء أيضًا تصريف وربط جميع ملفّات ‎.cpp‎ المستخدمة (تحدّد functionModule.cpp الدالّة المطلوبة):

g++ -o binName main.o functionsModule.o

error: '***' was not declared in this scope

يحدث هذا الخطأ في حال استخدام كائن غير معروف.

أخطاء متعلقة بالمتغيّرات

لن تُصرّف الشيفرة التالية، لأن المتغير i غير موجود في نطاق الدالة main:

#include <iostream>
int main(int argc, char *argv[])
{
    {
        int i = 2;
    }
    std::cout << i << std::endl; 
    return 0;
}

الحل:

#include <iostream>
int main(int argc, char *argv[])
{
    {
        int i = 2;
        std::cout << i << std::endl;
    }
    return 0;
}

أخطاء متعلقة بالدوال

يحدث هذا الخطأ غالبًا في حال عدم تضمين الترويسة المطلوبة (على سبيل المثال استخدام ‎std::cout‎ دون ‎#include <iostream>‎). ففي المثال التالي، لن تُصرّف الشيفرة التالية:

#include <iostream>
int main(int argc, char *argv[])
{
    doCompile();
    return 0;
}
void doCompile()
{
    std::cout << "No!" << std::endl;
}

الحلّ:

#include <iostream>
void doCompile(); // تصريح لاحق للدالة
int main(int argc, char *argv[])
{
    doCompile();
    return 0;
}
void doCompile()
{
    std::cout << "No!" << std::endl;
}

أو:

#include <iostream>
void doCompile() // تعريف الدالة قبل استخدامها
{
    std::cout << "No!" << std::endl;
}
int main(int argc, char *argv[])
{
    doCompile();
    return 0;
}

ملاحظة: يفسر المُصرِّف الشيفرة من الأعلى إلى الأسفل (للتبسيط فقط)، لذا يجب التصريح عن كل شيء (أو تعريفه) قبل استخدامه.

fatal error: ***: No such file or directory

يحدث هذا الخطأ عندما يعجز المُصرِّف عن العثور عن ملفّ معيّن (ملف مصدري يستخدم ‎#include "someFile.hpp"‎).

  • qmake:
INCLUDEPATH += dir / Of / File
  • cmake:
include_directories(dir/Of/File)
  • استدعاء g++‎:
g++ -o main main.cpp -Idir/Of/File

عدم التوافقية مع لغة C

سنتحدّث في هذا القسم عن شيفرات C غير المتوافقة مع لغة C++‎ والتي ستعطل مصرّفاتها‎.

الكلمات المفتاحية المحجوزة

هناك كلمات مفتاحية لها غرض خاص في C++‎، فالشيفرة التالية مثلًا جائزة في C، لكن غير جائزة في C++‎.

int class = 5

وإصلاح هذه الأخطاء سهل، فكل ما عليك هو إعادة تسمية المتغيّر.

المؤشّرات ذات النوعية الضعيفة Weakly Typed Pointers

يمكن تحويل المؤشّرات في C إلى النوع ‎void*‎، أما في C++‎ فستحتاج إلى تحويل صريح explicit cast. انظر المثال التالي حيث تكون الشيفرة غير جائزة في C++‎، ولكن جائزة في C:

void* ptr;
int* intptr = ptr;

إضافة تحويل صريح (explicit cast) ستحل المشكلة لكن قد يسبّب مشاكل أخرى.

goto أو switch

لا يجوز في ++C أن تتخطي التهيئة باستخدام ‎goto‎ أو ‎switch‎. انظر المثال التالي حيث تكون الشيفرة صحيحة في C، وغير صحيحة في C++‎:

goto foo;
int skipped = 1;
foo;

قد يتطلب إصلاح هذه الأخطاء إعادة تصميم البرنامج.

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -بتصرّف- للفصول

  • Chapter 130: Build Systems
  • Chapter 139: Common compile/linker errors (GCC)‎
  • Chapter 136: C incompatibilities

من كتاب C++ Notes for Professionals


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...