مقدمه
تاکنون در این فصل، به بررسی زبان میانی مشترک (CIL) به عنوان یک محصول نهایی از فرآیند
کامپایل پرداختهایم. ما دیدیم که چگونه کد C# به CIL ترجمه میشود و چگونه
میتوانیم با استفاده از ابزارهایی مانند ildasm، این کد سطح پایین را
مشاهده و تحلیل کنیم. اما این رابطه یکطرفه نیست. فریمورک .NET یک API
بسیار قدرتمند و سطح پایین را در اختیار ما قرار میدهد که به ما اجازه میدهد تا در زمان
اجرا، کد CIL تولید کرده و آن را به یک اسمبلی کامل و کاربردی تبدیل کنیم.
به این اسمبلیهایی که در حافظه و به صورت پویا ساخته میشوند، اسمبلیهای دینامیک (Dynamic
Assemblies) گفته میشود.
این قابلیت که از طریق فضای نام System.Reflection.Emit فراهم میشود،
یکی از پیشرفتهترین و پیچیدهترین ویژگیهای .NET است. این API به ما اجازه
میدهد تا دقیقاً همان کاری را انجام دهیم که کامپایلر C# انجام میدهد: تعریف ماژولها،
کلاسها، متدها، و سپس "نوشتن" دستورات CIL برای پیادهسازی منطق آنها. در این درس، با
مفاهیم و کلاسهای اصلی این فضای نام آشنا شده و یک اسمبلی دینامیک ساده را از صفر خواهیم ساخت.
فضای نام System.Reflection.Emit
این فضای نام، مجموعهای از کلاسها را فراهم میکند که به ما اجازه میدهند تا به صورت
برنامهنویسی شده، یک اسمبلی را در حافظه بسازیم. این فرآیند، "ساطع کردن" (emitting) کد نامیده
میشود، زیرا ما بایتکدهای CIL را یکییکی در یک جریان حافظه "میریزیم". کار با این
API نیازمند درک دقیق ساختار اسمبلیها و زبان CIL است.
فرآیند کلی ساخت یک اسمبلی دینامیک شامل چند مرحلهی سلسله مراتبی است:
- تعریف یک اسمبلی دینامیک با استفاده از AssemblyBuilder.
- تعریف یک ماژول دینامیک در داخل آن اسمبلی با استفاده از ModuleBuilder.
- تعریف یک یا چند نوع داده (کلاس) در داخل آن ماژول با استفاده از TypeBuilder.
- تعریف اعضای آن نوع (متدها، فیلدها و ...) با استفاده از کلاسهایی مانند MethodBuilder.
- تولید کد CIL برای بدنهی متدها با استفاده از کلاس ILGenerator.
- نهایی کردن نوع و اسمبلی.
ساخت یک اسمبلی دینامیک: مثال "Hello, World"
بهترین راه برای درک این فرآیند، انجام یک مثال کامل است. ما میخواهیم به صورت دینامیک، یک اسمبلی
به نام MyDynamicAssembly.dll بسازیم که حاوی یک کلاس HelloWorld با
یک متد SayHello باشد. این متد باید رشتهی "Hello from the dynamic world!" را در کنسول چاپ کند.
قدم اول: تعریف اسمبلی و ماژول
ابتدا باید یک نام برای اسمبلی خود تعریف کرده و سپس با استفاده از AssemblyBuilder و
ModuleBuilder، ساختار اولیه را ایجاد کنیم.
Program.cs
using System.Reflection;
using System.Reflection.Emit;
AssemblyName assemblyName = new("MyDynamicAssembly");
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Save);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name, $"{assemblyName.Name}.dll");
در این کد، ما یک AssemblyBuilder ایجاد کردهایم. پارامتر AssemblyBuilderAccess.Save به
.NET میگوید که ما قصد داریم این اسمبلی را در نهایت به صورت یک فایل فیزیکی ذخیره
کنیم. سپس، یک ماژول دینامیک در داخل این اسمبلی تعریف کردهایم.
قدم دوم: تعریف نوع (کلاس)
حالا با استفاده از ModuleBuilder، یک کلاس عمومی به نام HelloWorld را تعریف میکنیم.
Program.cs
TypeBuilder typeBuilder = moduleBuilder.DefineType("HelloWorld", TypeAttributes.Public);
TypeAttributes.Public معادل کلمهی کلیدی public در C# است.
قدم سوم: تعریف متد
درون کلاس HelloWorld، یک متد عمومی و استاتیک به نام SayHello تعریف میکنیم که هیچ پارامتری
ندارد و void برمیگرداند.
Program.cs
MethodBuilder methodBuilder = typeBuilder.DefineMethod("SayHello", MethodAttributes.Public | MethodAttributes.Static);
قدم چهارم: تولید کد CIL
این مهمترین و پیچیدهترین مرحله است. ما باید با استفاده از یک ILGenerator، دستورات
CIL لازم را برای بدنهی متد `SayHello` تولید کنیم. منطق ما این است: ۱) رشتهی "Hello
from the dynamic world!" را روی پشته قرار بده، ۲) متد Console.WriteLine(string) را فراخوانی
کن، ۳) از متد خارج شو.
Program.cs
ILGenerator ilGenerator = methodBuilder.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldstr, "Hello from the dynamic world!");
MethodInfo writeLineMethod = typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) });
ilGenerator.Emit(OpCodes.Call, writeLineMethod);
ilGenerator.Emit(OpCodes.Ret);
در اینجا، ما با استفاده از متد Emit و کلاس استاتیک OpCodes، دستورات CIL را
یکییکی "ساطع" میکنیم. برای فراخوانی Console.WriteLine، ابتدا باید با استفاده از انعکاس، شیء
MethodInfo مربوط به آن را به دست آوریم.
قدم پنجم: نهایی کردن و ذخیرهی اسمبلی
در نهایت، باید نوع و اسمبلی را نهایی کرده و آن را بر روی دیسک ذخیره کنیم.
Program.cs
typeBuilder.CreateType();
assemblyBuilder.Save($"{assemblyName.Name}.dll");
Console.WriteLine("Dynamic assembly created successfully!");
پس از اجرای این برنامه، یک فایل به نام MyDynamicAssembly.dll در
پوشهی خروجی ایجاد میشود. شما میتوانید این .dll را در یک پروژهی دیگر ارجاع داده و
متد HelloWorld.SayHello() را فراخوانی کنید و خواهید دید که به درستی کار میکند!