مقدمه

تاکنون در این فصل، به بررسی زبان میانی مشترک (CIL) به عنوان یک محصول نهایی از فرآیند کامپایل پرداخته‌ایم. ما دیدیم که چگونه کد C# به CIL ترجمه می‌شود و چگونه می‌توانیم با استفاده از ابزارهایی مانند ildasm، این کد سطح پایین را مشاهده و تحلیل کنیم. اما این رابطه یک‌طرفه نیست. فریم‌ورک .NET یک API بسیار قدرتمند و سطح پایین را در اختیار ما قرار می‌دهد که به ما اجازه می‌دهد تا در زمان اجرا، کد CIL تولید کرده و آن را به یک اسمبلی کامل و کاربردی تبدیل کنیم. به این اسمبلی‌هایی که در حافظه و به صورت پویا ساخته می‌شوند، اسمبلی‌های دینامیک (Dynamic Assemblies) گفته می‌شود.

این قابلیت که از طریق فضای نام System.Reflection.Emit فراهم می‌شود، یکی از پیشرفته‌ترین و پیچیده‌ترین ویژگی‌های .NET است. این API به ما اجازه می‌دهد تا دقیقاً همان کاری را انجام دهیم که کامپایلر C# انجام می‌دهد: تعریف ماژول‌ها، کلاس‌ها، متدها، و سپس "نوشتن" دستورات CIL برای پیاده‌سازی منطق آن‌ها. در این درس، با مفاهیم و کلاس‌های اصلی این فضای نام آشنا شده و یک اسمبلی دینامیک ساده را از صفر خواهیم ساخت.

فضای نام System.Reflection.Emit

این فضای نام، مجموعه‌ای از کلاس‌ها را فراهم می‌کند که به ما اجازه می‌دهند تا به صورت برنامه‌نویسی شده، یک اسمبلی را در حافظه بسازیم. این فرآیند، "ساطع کردن" (emitting) کد نامیده می‌شود، زیرا ما بایت‌کدهای CIL را یکی‌یکی در یک جریان حافظه "می‌ریزیم". کار با این API نیازمند درک دقیق ساختار اسمبلی‌ها و زبان CIL است.

فرآیند کلی ساخت یک اسمبلی دینامیک شامل چند مرحله‌ی سلسله مراتبی است:

  1. تعریف یک اسمبلی دینامیک با استفاده از AssemblyBuilder.
  2. تعریف یک ماژول دینامیک در داخل آن اسمبلی با استفاده از ModuleBuilder.
  3. تعریف یک یا چند نوع داده (کلاس) در داخل آن ماژول با استفاده از TypeBuilder.
  4. تعریف اعضای آن نوع (متدها، فیلدها و ...) با استفاده از کلاس‌هایی مانند MethodBuilder.
  5. تولید کد CIL برای بدنه‌ی متدها با استفاده از کلاس ILGenerator.
  6. نهایی کردن نوع و اسمبلی.

ساخت یک اسمبلی دینامیک: مثال "Hello, World"

بهترین راه برای درک این فرآیند، انجام یک مثال کامل است. ما می‌خواهیم به صورت دینامیک، یک اسمبلی به نام MyDynamicAssembly.dll بسازیم که حاوی یک کلاس HelloWorld با یک متد SayHello باشد. این متد باید رشته‌ی "Hello from the dynamic world!" را در کنسول چاپ کند.

قدم اول: تعریف اسمبلی و ماژول

ابتدا باید یک نام برای اسمبلی خود تعریف کرده و سپس با استفاده از AssemblyBuilder و ModuleBuilder، ساختار اولیه را ایجاد کنیم.

Copy Icon Program.cs
using System.Reflection;
using System.Reflection.Emit;

// 1. Define the name of our dynamic assembly.
AssemblyName assemblyName = new("MyDynamicAssembly");

// 2. Create an AssemblyBuilder. We specify that we want to save it to disk.
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Save);

// 3. Create a ModuleBuilder within the assembly.
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name, $"{assemblyName.Name}.dll");

در این کد، ما یک AssemblyBuilder ایجاد کرده‌ایم. پارامتر AssemblyBuilderAccess.Save به .NET می‌گوید که ما قصد داریم این اسمبلی را در نهایت به صورت یک فایل فیزیکی ذخیره کنیم. سپس، یک ماژول دینامیک در داخل این اسمبلی تعریف کرده‌ایم.

قدم دوم: تعریف نوع (کلاس)

حالا با استفاده از ModuleBuilder، یک کلاس عمومی به نام HelloWorld را تعریف می‌کنیم.

Copy Icon Program.cs
// 4. Define a public class named "HelloWorld".
TypeBuilder typeBuilder = moduleBuilder.DefineType("HelloWorld", TypeAttributes.Public);

TypeAttributes.Public معادل کلمه‌ی کلیدی public در C# است.

قدم سوم: تعریف متد

درون کلاس HelloWorld، یک متد عمومی و استاتیک به نام SayHello تعریف می‌کنیم که هیچ پارامتری ندارد و void برمی‌گرداند.

Copy Icon Program.cs
// 5. Define a public static method named "SayHello" with no parameters.
MethodBuilder methodBuilder = typeBuilder.DefineMethod("SayHello", MethodAttributes.Public | MethodAttributes.Static);

قدم چهارم: تولید کد CIL

این مهم‌ترین و پیچیده‌ترین مرحله است. ما باید با استفاده از یک ILGenerator، دستورات CIL لازم را برای بدنه‌ی متد `SayHello` تولید کنیم. منطق ما این است: ۱) رشته‌ی "Hello from the dynamic world!" را روی پشته قرار بده، ۲) متد Console.WriteLine(string) را فراخوانی کن، ۳) از متد خارج شو.

Copy Icon Program.cs
// 6. Get an ILGenerator to emit the CIL opcodes.
ILGenerator ilGenerator = methodBuilder.GetILGenerator();

// 7. Emit the CIL instructions.
// Load the string onto the evaluation stack.
ilGenerator.Emit(OpCodes.Ldstr, "Hello from the dynamic world!");

// Get the MethodInfo for Console.WriteLine(string).
MethodInfo writeLineMethod = typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) });

// Call the WriteLine method.
ilGenerator.Emit(OpCodes.Call, writeLineMethod);

// Return from the method.
ilGenerator.Emit(OpCodes.Ret);

در اینجا، ما با استفاده از متد Emit و کلاس استاتیک OpCodes، دستورات CIL را یکی‌یکی "ساطع" می‌کنیم. برای فراخوانی Console.WriteLine، ابتدا باید با استفاده از انعکاس، شیء MethodInfo مربوط به آن را به دست آوریم.

قدم پنجم: نهایی کردن و ذخیره‌ی اسمبلی

در نهایت، باید نوع و اسمبلی را نهایی کرده و آن را بر روی دیسک ذخیره کنیم.

Copy Icon Program.cs
// 8. Finalize the type.
typeBuilder.CreateType();

// 9. Save the dynamic assembly to disk.
assemblyBuilder.Save($"{assemblyName.Name}.dll");

Console.WriteLine("Dynamic assembly created successfully!");

پس از اجرای این برنامه، یک فایل به نام MyDynamicAssembly.dll در پوشه‌ی خروجی ایجاد می‌شود. شما می‌توانید این .dll را در یک پروژه‌ی دیگر ارجاع داده و متد HelloWorld.SayHello() را فراخوانی کنید و خواهید دید که به درستی کار می‌کند!