تو قسمت قبلی ابزار AL.exe معرفی شد و روش تولید یه اسمبلی چند فایله با استفاده از این ابزار نشون داده شد. Assembly Linker هم مثل کامپایلر سی شارپ امکان تولید فایلهای اجرایی مختلف رو داره. اینکار هم با استفاده از سوییچهای مختلف زیر انجام میشه:
- CUI (یا Console User Interface) یا همون برنامه کنسول: t[arget]:exe/
- GUI (یا Graphical User Interface) یا همون برنامه با رابط کاربری گرافیکی: t[arget]:winexe/
- Windows Store App یا همون برنامه های فروشگاه ویندوز: t[arget]:appcontainerexe/
البته استفاده از ابزار AL.exe برای تولید اسمبلی های اجرایی کمی غیرمعموله، چون با اینکار یه اسمبلی تقریبا خالی تولید میشه که تنها حاوی کمی کد IL فقط برای فراخوانی متد اصلی برنامه تو یه ماژول دیگه است.
برای مشخص کردن نقطه ورودی برنامه (entry point) میشه از سوییچ main/ استفاده کرد. با استفاده از این سوییچ محل دقیق متد ورودی برنامه به این ابزار اعلام میشه.
برای روشنتر شدن موضوع، مثالی آورده میشه. تو این مثال کلاسی بصورت زیر داریم:
using System; namespace ConsoleApp { public class Program { public static void Main(string[] args) { if (args.Length > 0) { Console.WriteLine(string.Format("args[0] is: {0}", args[0])); } } } }
سپس با استفاده از دستور زیر، کامپایل این کد به یه ماژول مدیریت شده انجام میشه:
با اجرای این دستور یه ماژول مدیریت شده تو ریشه درایو :d تولید میشه. برای تولید یه اسمبلی اجرایی از این ماژول مدیریت شده (p.netmodule) با استفاده از AL.exe باید از دستور زیر استفاده کرد. به روش معرفی یه نقطه ورودی با استفاده از سوییچ main/ تو این دستور دقت کنین:
همونطور که مشاهده میشه، آدرس دقیق متد ورودی برنامه (که در اینجا متد Main از کلاس Program به همراه فضای نامش هست) با استفاده از سوییچ main/ معرفی شده. با اجرای این دستور یه فایل اجرایی PE به نام p.exe تو ریشه درایو :d تولید میشه. تصویر زیر نتیجه اجرای این دستورات رو تو خط فرمان ویژوال استودیو نشون میده:
با اینکار درواقع یه اسمبلی دو فایله تولید میشه که فایل اصلی این اسمبلی (d:\p.exe) حاوی اطلاعات جداول مانیفست اسمبلیه. علاوه بر این جداول، یه متد گلوبال کوچیک به نام EntryPoint__ هم به فایل اصلی اسمبلی اضافه میشه. این متد بخاطر استفاده از سوییچ main/ توسط ابزار AL.exe تولید شده. بهتره برای روشنتر شده مطلب، فایل p.exe با استفاده از ابزار ildasm.exe مورد بررسی قرار داده بشه:
مشخصه که متد جدید EntryPoint__ به فایل اصلی اسمبلی اضافه شده. برای مشاهده محتوای این متد کافی روی اون دایل کلیک کنید تا پنجره زیر نمایش داده بشه:
برای بررسی راحتتر محتوای این پنجره بصورت متنی آورده شده:
.method privatescope static void __EntryPoint$PST06000001(string[] A_0) cil managed { .entrypoint // Code size 9 (0x9) .maxstack 8 IL_0000: ldarg.0 IL_0001: tail. IL_0003: call void [.module p.netmodule]ConsoleApp.Program::Main(string[]) IL_0008: ret } // end of method 'Global Functions'::__EntryPoint
همونطور که مشخصه این متد صرفا وظیفه فراخونی متد Main کلاس Program (که تو ماژول مدیریت شده p.netmodule موجوده) رو بر عهده داره.
با توجه به مطالب اشاره شده تا اینجا به نظر میرسه که استفاده از ابزار AL.exe برای تولید فایلهای اجرایی درصورت تک فایله بودن اسمبلی، کاری بیهوده باشه. اما در ادامه برخی تفاوتها و ویژگیهای منحصربفرد این ابزار روشن میشه.
نقطه ورودی برنامه یا Entry Point
تا اینجا مفهوم entry point تو اسمبلی های دات نتی بصورت ناقص و کلی ارائه شده. اما این نقطه ورودی واقعا به چه معنیه و چه ویژگیهایی باید داشته باشه؟
نقطه ورود برنامه محلی از کد برنامس که پس از بارگذاری و اتمام عملیات آماده سازی، توسط CLR برای اجرای اپلیکیشن فراخونی میشه. در عمل این نقطه ورودی باید یه «متد» باشه. این متد خاص باید یه سری ویژگی داشته باشه:
1- باید حتما از نوع استاتیک باشه. با اینکار CLR نیازی به تولید یک نمونه از کلاس مربوطه نداره (مطالب بیشتر: ! و !).
2- باید نوع داده خروجی اون int یا void باشه.
3- درصورت نیاز به آرگومانهای خط فرمان باید از یک پارامتر ورودی از نوع []string استفاده کرد.
برای جلوگیری از پیچیده شدن بیش از حد مطلب، خوندن مطالب تکمیلی درباره وجود این ویژگی های خاص به خوانندگان واگذار میشه.
اما نکته ای که باید به اون توجه کرد اینه که رفتار کامپایلر #C با ابزار AL.exe در تولید این نقطه ورودی کمی فرق میکنه. برای بررسی بیشتر، ابتدا یه اسمبلی اجرایی ساده با استفاده از کامپایلر سی شارپ تولید میکنیم. کلاس زیر رو درنظر بگیرین:
public class Program { static void Main(string[] args) { System.Console.WriteLine("hi"); } }
سپس با استفاده از دستور زیر، این کد به یه اسمبلی اجرایی کنسولی کامپایل میشه:
حالا این فایل با استفاده از ابزار ildasm.exe بررسی میشه. با مشاهده متادیتای این اسمبلی میشه نحوه معرفی نقطه ورودی تو این اسمبلی در این حالت رو دریافت. این معرفی با استفاده از یه خاصیت مخصوص به نام [ENTRYPOINT] تو جدول TypeDef (معرفی نوع ها) انجام میشه. این مورد رو میشه تو این مثال بصورت زیر دید:
... TypeDef #1 (02000002) ------------------------------------------------------- TypDefName: Program (02000002) Flags : [Public] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit] (00100001) Extends : 01000001 [TypeRef] System.Object Method #1 (06000001) [ENTRYPOINT] <-- این خاصیت معرف نقطه ورود برنامه است ------------------------------------------------------- MethodName: Main (06000001) Flags : [Private] [Static] [HideBySig] [ReuseSlot] (00000091) RVA : 0x00002050 ImplFlags : [IL] [Managed] (00000000) CallCnvntn: [DEFAULT] ReturnType: Void 1 Arguments Argument #1: SZArray String 1 Parameters (1) ParamToken : (08000001) Name : args flags: [none] (00000000) ...
با توجه به این بخش از متادیتای اسمبلی، میشه فهمید که متد Main کلاس Program به عنوان نقطه ورودی برنامه درنظر گرفته شده. اما نکته ای که باید بهش دقت کرد اینه که درصورت تولید اسمبلی اجرایی توسط کامپایلر سی شارپ، متد ورودی برنامه باید یکسری ویژگی های دیگه هم داشته باشه:
1-این متد باید به نام Main تعریف شده باشه.
2- تو کلاسی که متد Main تعریف شده باید تنها یه متد استاتیک با این اسم (فارغ از امضای متد) وجود داشته باشه. یعنی اگه کلاس حاوی متد Main بصورت زیر تعریف شده باشه، کامپایلر قادر به کامپایل برنامه نیست:
public class Program { public static void Main(string[] args) { System.Console.WriteLine("hi"); } static string Main(int v1, int v2) { System.Console.WriteLine("v"); return "s"; } }
نتیجه کامپایل این کد تو تصویر زیر نشون داده شده:
که نشون میده کامپایلر سی شارپ به نام Main برای متدهای استاتیک بسیار حساسه! بنابراین باید از پیاده سازی متدهایی که چنین ویژگیهایی دارن در سایر قسمتهای برنامه جلوگیری کرد. مثلا اگه تو این کد، کلمه static از متد دوم حذف بشه، کامپایل بدرستی انجام میشه.
در ضمن اسم کلاسی که این متد Main توش تعریف میشه و همچنین دسترسی ها و سایر modifierهای اون مهم نیستند. یعنی اگه کد برنامه بصورت زیر هم باشه کامپایلر بدرستی عملیات کامپایل رو انجام میده و برنامه بدرستی کار میکنه:
internal static class Yusefnejad { static void Main(string[] args) { System.Console.WriteLine("hi"); } }
استفاده از سوییچ main/ در تولید یه اسمبلی اجرایی با استفاده از کامپایلر #C درصورت وجود تنها یک متد با این مشخصات در کل برنامه اختیاریه. درواقع کامپایلر سی شارپ درصورتیکه این سوییچ فراهم نشده باشه، کل برنامه رو برای پیدا کردن متدی با این مشخصات جستجو میکنه و درصورت یافتن تنها یک مورد، کار کامپایل برنامه رو با استفاده از اون انجام میده. اما درغیراینصورت باید با استفاده از سوییچ main/ نقطه ورودی برنامه صراحتا اعلام بشه. مثل کد زیر:
public class Program { static void Main(string[] args) { System.Console.WriteLine("hi"); } static void Main() { System.Console.WriteLine("hi"); } }
درصورتیکه کامپایل این کد با همون دستور قبلی انجام بشه، کامپایلر سی شارپ خطاهای زیر رو صادر میکنه:
Program.cs(7,15): error CS0017: Program 'd:\p1.exe' has more than one entry point defined: 'Program.Main()'. Compile with /main to specify the type that contains the entry point.
یا کلاسهایی بصورت زیر:
public class Program1 { static void Main(string[] args) { System.Console.WriteLine("hi1"); } } public class Program2 { static void Main() { System.Console.WriteLine("hi2"); } }
در اینصورت خطاهای زیر نمایش داده میشه:
Program1.cs(10,15): error CS0017: Program 'd:\p1.exe' has more than one entry point defined: 'Program2.Main()'. Compile with /main to specify the type that contains the entry point.
در این مواقع برای رفع این مشکل همونجوری که تو متن خطاهای بالا هم اشاره شده باید از سوییچ main/ استفاده کرد. اما این سوییچ تو کامپایلر سی شارپ رفتاری متفاوت از همین سوییچ تو ابزار AL.exe داره که در ابتدای این مطلب معرفی شد. سوییچ main/ تو کامپایلر سی شارپ برخلاف ابزار AL.exe که معرف متد ورودی برنامه هست، بدلیل وجود قید نام Main برای متد ورودی، تنها معرف کلاسیه که این متد با امضای صحیح توش تعریف شده.
البته با این توضیحات متوجه میشیم که نمونه اول مثالهای بالا قابل کامپایل شدن حتی با استفاده از سوییچ main/ هم نیست، چون هیچ راهی برای متمایز کردن دو متد Main موجود تو یه کلاس در این حالت وجود نداره. بنابراین همونطور که قبلا هم اشاره شد، نباید تو کلاس معرف متد Main، متد استاتیک دیگه ای با همین نام وجود داشته باشه.
بنابراین برای رفع خطای نشون داده شده تو مورد دوم، باید از دستوری شبیه دستور زیر استفاده کرد:
برای نمونه نتیجه اجرای دو حالت مختلف برای سوییچ main/ تو تصویر زیر نشون داده شده:
ویژگی منحصربه فرد Assembly Linker
با توجه به مطالب ارائه شده تا اینجا نکته ای که به نظر میرسه به عنوان ویژگی منحصر به فرد ابزار AL.exe میشه بهش اشاره کرد، امکان تعیین هر متدی به عنوان نقطه ورودی برنامس. برخلاف کامپایلر سی شارپ که نیاز داره اسم این متد حتما Main باشه، با ابزار AL.exe میشه نامهای مختلف دیگه ای برای اینکار انتخاب کرد، البته به شرطی که امضای درستی داشته باشه. مثل کد زیر:
namespace MyApp { public class Starter { public static void Run(string[] args) { if (args.Length > 0) { System.Console.WriteLine(string.Format("args[0] is: {0}", args[0])); } } } }
تو تصویر زیر دستورات موردنیاز برای تولید اسمبلی مربوطه و نتیجه اجرای اون نشون داده شده:
اما نکته کار کجاست؟
از نظر CLR نقطه ورودی برنامه اون متدیه که تو جداول متادیتا، خاصیت [ENTRYPOINT] بهش داده شده باشه. کاری که کامپایلر سی شارپ با متد خاص Main انجام میده. اما در اینجا چه اتفاقی برای اسمبلی تولید شده توسط ابزار AL.exe میفته؟
برای روشن شدن موضوع متادیتای اسمبلی تولیدی با این ابزار رو توسط ildasm.exe مورد بررسی قرار میدیم. قسمت ابتدایی این متادیتا بصورت زیره:
=========================================================== ScopeName : p.exe MVID : {39EB3607-07A1-46B1-BB88-4EBF62841964} =========================================================== Global functions ------------------------------------------------------- Method #1 (06000001) [ENTRYPOINT] <-- مشخص کننده نقطه ورودی برنامه ------------------------------------------------------- MethodName: __EntryPoint (06000001) Flags : [PrivateScope] [Static] [ReuseSlot] (00000010) RVA : 0x00002050 ImplFlags : [IL] [Managed] (00000000) CallCnvntn: [DEFAULT] ReturnType: Void 1 Arguments Argument #1: SZArray String ...
مشاهده میشه که خاصیت اشاره شده، تو این حالت برای متد EntryPoint__ تعیین شده. بنابراین این متد به عنوان نقطه ورودی برنامه ثبت شده و توسط CLR اجرا میشه. پس ابزار AL.exe عملا با تغییر نقطه ورودی برنامه، امکان اجرای متد ورودی ما رو در آغاز برنامه فراهم میکنه.
نقطه ورودی برنامه در ویژوال استودیو
تنظیم نقطه ورودی تو ویژوال استودیو در صورت وجود دو یا چند متد Main، از طریق پنجره تنظیمات پروژه انجام میشه. اینکار با مقداردهی گزینه Startup object در تب Application از این پنجره انجام میشه. این بخش تو تصویر زیر نمایش داده شده:
تو این قسمت تمامی کلاسهایی که حاوی متد Main مناسب هستن، لیست میشن. مثلا تو این تصویر نشون میده که سه کلاس مختلف امکان تنظیم برای نقطه ورودی رو دارن.
.: . :.