اگر دقت کرده باشید از دو قسمت قبل تاکنون در حال پاسخ به چالش هایی بودیم که میخواستند بهگونه ای به ما بفهمانند که makefile های ما باید دارای انعطاف پذیری باشد.یعنی مثلا اگر یک سورس فایل جدید به برنامه ما اضافه شد، نیازی نباشد makefile را تغییر دهیم تا کامپایل برنامه به درستی انجام شود یا حداقل نیاز به تغییرات اساسی نباشد. یکی از ابزارهایی که بهشدت در این مسیر به کمک ما میآید، توابع هستند. از این رو در این بخش شما را با توابعی در GNU make آشنا میکنیم. پس با سیسوگ همراه باشید…
توابع در GNU make
بر اساس مقالات گنو توابع عبارتند از:
توابع وظیفه پردازش متن برای بدست آوردن فایل هایی را دارند که توسط قسمت های مختلف یک Rule استفاده میشود. زمانی که یک تابع را فراخوانی میکنید، در حال استفاده از آن هستید و هنگامی که کار تابع تمام شد، مقدار برگشتی تابع جایگزین فراخوانی تابع میشود، همانطور که مقدار یک متغیر جایگزین ارجاع آن میشد. |
توابع در گنو به دو گروه تقسیم میشوند:
- توابع از پیش تعریف شده: این توابع، توابعی هستند که برای انجام یک سری کارهای خاص توسط پروژه گنو تعریف شده اند و ما می توانیم با استفاده از آنها تا حد بسیار زیادی نیاز های خود را در جهت نوشتن یک makefile منعطف رفع کنیم.
- توابع Custom: این توابع در واقع توابعی هستند که ما آنها را مینویسیم و در makefile آنها را فراخوانی میکنیم. در این قسمت ما با این توابع کاری نداریم، چرا که برای نوشتن یک سیستم عامل بلادرنگ برای میکروکنترلرها همان توابع از پیش تعریف شده تا حد زیادی نیاز ما را رفع میکنند.
استفاده از توابع در makefile
برای استفاده از توابع در makefile میبایست نام و آرگومان های ورودی تابع را مانند زیر در ارجاع تابع قرار دهیم.
خب فرمت کلی توابع و آرگومان های ورودی آنها به صورت زیر است:
1 | function arg1,arg2,... |
و اما برای استفاده از یک تابع فرمت بالا را به صورت زیر استفاده میکنیم که به این کار فراخوانی تابع گفته میشود.
1 | $(function arg1,arg2,...) |
توابع ممکن است خروجی داشته باشند، ممکن هم هست که خروجی نداشته باشند. در صورتی که یک تابع خروجی داشته باشد، میتوانیم خروجی تابع را برابر با مقدار یک متغیر یا آرگومان ورودی یک تابع دیگر قرار دهیم و همانطور که در قسمت قبل نتیجه گرفتیم بهتر است که این متغیر از نوع Simply باشد، اما اگر تابع دارای خروجی نمیباشد، نیازی به قرار دادن یک متغیر برای خروجی آن نیست، چرا که خروجی null برمیگرداند.
حالا ما با بیان چالش هایی قصد داریم تعدادی از توابع از پیش تعریف شده را معرفی کنیم.برای بیان چالش اول به یک makefile نیاز داریم:
1 2 3 4 5 6 7 8 9 10 | vpath %.c src vpath %.h ./header output : main.o sum.o mine.o gcc $^ -o $@ obj/%.o : %.c header.h gcc -c $< -I./header -o $@ clean : rm -f *.o output .PHONY : clean |
چالش اول
قرار است یک سورس فایل دیگر به نام mul.c به برنامه اضافه کنیم که وظیفه انجام عملیات ضرب را دارد. در این صورت آبجکت فایل mul.o باید به عنوان پیش نیاز Rule اول قرار بگیرد.خب در شرایط عادی ما باید هر بار با اضافه کردن یک سورس فایل، فایل make رو هم تغییر بدهیم.چطور میتوانیم از این کار جلوگیری کنیم تا هنگام اضافه شدن یک سورس فایل جدید به صورت اتوماتیک آن فایل شناسایی شده و آبجکت فایل آن به صورت پیش نیاز Rule اول قرار گیرد؟ |
برای پاسخ به این چالش ما از دو تابع استفاده می کنیم:
مرحله اول: در این مرحله باید ابتدا کلیه فایل هایی که پسوند c. دارند را پیدا کنیم و در یک لیست قرار دهیم.به این منظور از تابع wildcard استفاده میکنیم.
تابع wildcard: این تابع با دریافت الگوی فایل هایی که میخواهیم در یک لیست قرار دهیم، لیست فایل های درخواستی را که با فاصله از هم جدا شده اند، در خروجی برمیگرداند.
1 | $(wildcard pattern) |
برای مثال: در مثال زیر منظور از src دایرکتوری هست که سورس فایل ها در آن قرار دارد و sources هم متغیری است که لیست سورس فایل ها در آن قرار میگیرد.
1 | sources = $(wildcard src/*.c) |
نتیجه:
1 | sources => src/main.c src/sum.c src/mine.c |
مرحله دوم: در این مرحله باید این لیست را که شامل سورس فایل هاست به لیستی تبدیل کنیم که شامل آبجکت فایل هایی با همان نام ها باشد. بنابراین از تابعی به نام patsubst استفاده میکنیم. این تابع در واقع شبیه به تابع subst است، با این تفاوت که یک الگوی خاص در داخل لیست را به یک الگوی دیگر تبدیل میکند.
تابع subst: این تابع یک مقدار در داخل یک text را از from به to تبدیل کرده و در نتیجه رشته خروجی تابع را به جای فراخوانی تابع جایگزین میکند.
1 | $(subst from,to,text) |
برای مثال:
1 | modString = $(subst l,L,Hello World) |
نتیجه: در این مثال تمامی “l”ها در رشته به “L” تبدیل میشود و در خروجی رشته HeLLo WorLd برگردانده میشود.
تابع patsubst: این تابع کلمات جدا شده با فضای خالی که با الگوی pattern مطابقت دارد، در داخل text پیدا میکند و سپس آنها را با الگوی replacement جایگزین میکند و لیستی از کلمات جایگزین شده که با فاصله از هم جدا شدهاند را جایگزین فراخوانی تابع میکند. برای نشان دادن الگو در این تابع از “%” استفاده میکنیم و هر چیزی که بعد از “%” بیاید، به عنوان الگو واقع میشود.
1 | $(patsubst pattern,replacement,text) |
برای مثال:
1 | objects = $(patsubst src/%.c,obj/%.o,$(sources)) |
نتیجه: در این مثال همه سورس فایل ها در متغیر sources (که در مرحله اول تعریف کردیم)، به آبجکت فایل هایی با همان نام ها تبدیل شده و در متغیر objects ذخیره میشود.
1 | objects => obj/main.o obj/sum.o obj/mine.o |
نکته: به جای استفاده از تابع patsubst میتوان متغیر را به گونه ای ارجاع داد که همان کار را انجام دهد:
1 | objects = $(sources:src/%.c=obj/%.o) |
در پایان این چالش هم کافی است که از محتوای این متغیر به عنوان پیش نیاز Rule اول استفاده کنیم:
1 2 | output : $(objects) gcc $^ -o $@ |
جمع بندی چالش:
1 2 3 4 5 6 7 8 9 10 | vpath %.c src vpath %.h ./header output : $(objects) gcc $^ -o $@ object/%.o : %.c header.h gcc -c $< -I./header -o $@ clean : rm -f $(objects) output .PHONY : clean |
چالش دوم
فرض کنید که ما به جای یک دایرکتوری که در آن سورس فایل ها قرار دارد، چند دایرکتوری داشته باشیم که در هر کدام چند سورس فایل وجود داشته باشد، در چنین شرایطی باید توابعی که در چالش اول به آنها اشاره شد را برای هر دایرکتوری به صورت جداگانه استفاده کنیم و نهایتا لیست های به دست آمده را به یکدیگر بچسبانیم. چطور میتوانیم بدون تکرار دستورات این کار را انجام دهیم؟ |
برای انجام این کار دو راه وجود دارد:
راه اول: میتوانیم لیست های بدست آمده از توابع را با هم در یک متغیر جمع کنیم.برای این کار به صورت زیر عمل میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | sources := $(wildcard src1/*.c) sources += $(wildcard src2/*.c) objects := $(patsubst src1/%.c,obj/%.o,$(sources)) objects += $(patsubst src2/%.c,obj/%.o,$(sources)) vpath %.c src vpath %.h ./header output : $(objects) gcc $^ -o $@ obj/%.o : %.c header.h gcc -c $< -I./header -o $@ clean : rm -f $(objects) output .PHONY : clean |
مشکل این راه این است که makefile از انعطاف پذیری زیادی برخوردار نیست و میزان تغییراتی که ما باید بعد از هر بار اضافه شدن یک دایرکتوری از سورس فایل های جدید در این فایل بدهیم، زیاد است و همین احتمال خطا را بالا میبرد.به همین دلیل ما راه دوم را در پیش میگیریم…
راه دوم: در این راه ما از تابعی به نام foreach استفاده میکنیم. این تابع شبیه به حلقه for در زبان c میباشد.
تابع foreach: در واقع یک حلقه برای تکرار یک تابع ایجاد میکند که هر بار یک عضو از لیست ورودی به این تابع به عنوان آرگومان ورودی تابع تکرارشونده قرار میگیرد و خروجی های متفاوتی از تابع تکرارشونده ایجاد میشود. تابع foreach این خروجی ها را به یکدیگر میچسباند و یک لیست که اعضای آن با یک فاصله از هم جداشدهاند را جایگزین فراخوانی تابع میکند.این تابع سه آرگومان ورودی دارد:
var همان متغیر شمارنده حلقه است که هر بار یک مقدار جدید دریافت میکند، list رشتهای است که مقادیر آن هر بار برای استفاده توسط تابع تکرارشونده در متغیر var قرار میگیرد و در نهایت text که میتواند شامل یک تابع یا هرچیز تکرارشونده دیگر باشد اما ما اینجا از تابع استفاده میکنیم:
1 | $(foreach var,list,text) |
مثال:
1 2 3 | srcDir := src src2 sources := $(foreach dir,$(srcDir),$(wildcard $(dir)/*.c)) objects = $(foreach dir,$(srcDir),$(patsubst $(dir)/%.c,object/%.o,$(sources)) |
نتیجه: اگر این مثال اجرا شود، ابتدا تمامی سورس فایل ها از هر دو دایرکتوری در متغیر sources لیست میشوند. در مرحله بعد اما، یک باگ وجود دارد که در تصویر زیر میتوانید مشاهده کنید:
همانطور که در تصویر بالا میبینید قسمت اول به درستی انجام شده است اما قسمت دوم آن که مرحله تبدیل سورسفایلها به آبجکتفایلها است، با مشکل مواجه است و به درستی انجام نشده است. چرا؟
زیرا ابتدا دایرکتوری src به عنوان آرگومان ورودی تابع انتخاب میشود و در نتیجهِ این ورودی، تابع patsubst خروجی زیر را برمیگرداند:
1 | object/mine.o object/sum.o src2/main.c |
سپس دایرکتوری src2 به عنوان ورودی تابع patsubst انتخاب میشود و خروجی زیر را برمیگرداند:
1 | src/mine.c src/sum.c object/main.o |
و در نهایت این دو خروجی به هم میچسبند.حالا که مشکل رو درک کردیم، می خواهیم با استفاده از تابع filter pattern این مشکل را حل کنیم.
تابع filter pattern: این تابع همه کلمات جدا شده با فضای خالی در آرگومان text را که با هر یک از کلمات pattern مطابقت دارند، برمیگرداند، و کلماتی را که مطابقت ندارند را حذف میکند. الگوها با استفاده از “%” نوشته میشوند، درست مانند الگوهای استفاده شده در تابع patsubst:
1 | $(filter pattern patternWord,text) |
مثال:
1 | sources1 = $(filter pattern src/%.c, $(sources)) |
نتیجه: در پایان این مثال فقط سورسفایلهایی که مربوط به دایرکتوری src است از لیست همه سورس فایل ها جدا میشود.
جمع بندی چالش:
با توجه به تفاسیری که گفته شد، به طور خلاصه makefile حاصل به صورت زیر میشود:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | srcDir := src src2 sources := $(foreach dir,$(srcDir),$(wildcard $(dir)/*.c)) objects := $(foreach dir,$(srcDir),\ $(patsubst $(dir)/%.c,object/%.o,$(filter pattern $(dir)/%.c,$(sources)))) vpath %.c $(srcDir) vpath %.h ./header output : $(objects) gcc $^ -o $@ object/%.o : %.c header.h gcc -c $< -I ./header -o $@ clean : rm -f $(objects) output .PHONY : clean |
توابعی برای اشکالیابی makefile
توابعی که در ادامه گفته میشود به روند عیبیابی makefile کمک زیادی میکند.
تابع info: این تابع به منظور چاپ آرگومان ورودی در Standard Output مورداستفاده قرار میگیرد.یعنی رشته ورودی را جایگزین فراخوانی تابع در Standard output میکند.لازم به ذکر است که این تابع خروجی برنمیگرداند.
1 | $(info text) |
مثال:
1 | $(info Hello World) |
تابع flavor: این تابع نوع متغیری که به عنوان آرگومان ورودی به آن داده میشود را برمیگرداند.
1 | $(flavor var) |
مثال:
1 2 | var = foo $(info $(flavor var)) |
نتیجه: عبارت بالا Recursivly را در Standard output چاپ می کند.
جمع بندی
به طور کلی در این سه قسمت تلاش بر این بود که تا حدودی شما را با ابزار make و متعلقات آن آشنا کنیم تا بتوانید در مراحل آتی از این مجموعه آموزشی گلیم خود را از آب بیرون بکشید. اگر می خواهید نتیجه این سه قسمت آموزش را ببینید می توانید makefile بخش اول را با makefile که در پایان چالش دوم از این بخش نوشتیم، مقایسه کنید. همچنین جهت دریافت اطلاعات بیشتر در مورد make می توانید به این لینک مراجعه کنید. در قسمت بعد یکی از مهم ترین ابزارها برای یک برنامه نویس یعنی دیباگر را معرفی و با آن شروع به کار میکنیم.
با درود.
مشتاقانه منظر قسمتهای بعدی هستم و یک پیشنهاد دارم .
لطفا فعلا قسمت مربوط به make file رو فاکتور بگیرید چون برای دانستن RTOS ضروری نیست و توی بعضی IDE ها به طور اتوماتیک تولید میشه یا اصلا نیازی بهش ندارن !!
شما فعلا مطالب مربوط به rtos رو با استفاده از یک makefile جنرال یا یک ide رایج شروع کنید به نظرم مخاطب بهتری هم خواهید داشت.
خلاصه مطلب رو ادامه بدید.
سپاس.
خیلی بهتر و شیواتر از متن اصلی توضیح دادید مطالب رو
خیلی مقوله جذابی هست لطفاً ادامشم بزارید لطفا
سلام دوست عزیز به زودی قسمت جدید منتشر میشه
با درود.
من قبلا خودم make user manual رو خوانده بودم ولی نمیدونستم که خودمون هم میتونیم تابع تعریف کنیم .و یکسری تابع دیگه که شما نام بردی و در متن اصلی کاربردشون رو درست متوجه نشده بودم.
من مشتاقانه منتظرم تا به قسمت RTOS برسیم.
سپاس بابت مطلب خوبتون .
و البته متاسف شدم که در دو قسمت قبل تعداد کامنتها اینقدر کم هست!!!.