فصل ۱۸: Attributeها و تولید Dynamic IL با Reflection.Emit و DynamicMethod
فصل ۱۸: Attributeها و تولید Dynamic IL با Reflection.Emit و DynamicMethod
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
ادامهٔ بازتاب اسمبلیها (Reflecting Assemblies)
مثال بعد همهٔ typeهای اسمبلی mylib.dll در مسیر e:\demo را فهرست میکند:
Assembly a = Assembly.LoadFile (@"e:\demo\mylib.dll");
foreach (Type t in a.GetTypes())
Console.WriteLine (t);
یا:
Assembly a = typeof (Foo).GetTypeInfo().Assembly;
foreach (Type t in a.ExportedTypes)
Console.WriteLine (t);
GetTypes و ExportedTypes فقط typeهای سطح بالا را برمیگردانند و typeهای تودرتو را شامل نمیشوند.
ماژولها (Modules)
فراخوانی GetTypes روی یک اسمبلی چندماژوله، همهٔ typeها را در همهٔ ماژولها برمیگرداند. بنابراین معمولاً میتوانید وجود ماژولها را نادیده بگیرید و اسمبلی را بهعنوان ظرف typeها در نظر بگیرید. با این حال یک حالت وجود دارد که ماژولها در آن اهمیت پیدا میکنند: کار با metadata tokenها.
یک metadata token عدد صحیحی است که در محدودهٔ یک ماژول، بهطور یکتا به یک type، member، string یا resource اشاره میکند. IL از metadata tokenها استفاده میکند؛ بنابراین اگر IL را parse میکنید، باید بتوانید آنها را resolve کنید. متدهای این کار روی نوع Module تعریف شدهاند و ResolveType، ResolveMember، ResolveString و ResolveSignature نام دارند. در بخش پایانی این فصل، هنگام نوشتن disassembler دوباره به این موضوع برمیگردیم.
میتوانید فهرست همهٔ ماژولهای یک اسمبلی را با GetModules بگیرید. همچنین از طریق propertyِ ManifestModule مستقیماً به ماژول اصلی اسمبلی دسترسی دارید.
کار با Attributeها (Working with Attributes)
CLR اجازه میدهد از طریق attributeها، فرادادهٔ بیشتری به typeها، memberها و assemblyها متصل شود. بعضی قابلیتهای مهم CLR—مانند شناسایی اسمبلی یا marshaling نوعها برای interoperability بومی—از همین سازوکار هدایت میشوند؛ بنابراین attributeها بخش جداییناپذیر یک برنامهاند.
ویژگی مهم attributeها این است که میتوانید attribute خودتان را بنویسید و سپس درست مانند هر attribute دیگری از آن برای «تزئین» یک عنصر کد با اطلاعات اضافی استفاده کنید. این اطلاعات اضافی در اسمبلی زیربنایی کامپایل میشود و در زمان اجرا با Reflection قابل بازیابی است؛ در نتیجه میتوان سرویسهایی ساخت که بهصورت declarative کار میکنند، مانند unit testing خودکار.
مبانی Attribute
سه نوع attribute وجود دارد:
- attributeهای bit-mapped
- custom attributeها
- pseudocustom attributeها
از میان این سه، فقط custom attributeها توسعهپذیرند.
attributeهای bit-mapped—این اصطلاحی است که ما استفاده میکنیم—به bitهای اختصاصی در فرادادهٔ یک type نگاشت میشوند. بیشتر کلیدواژههای modifier در C#، مانند public، abstract و sealed، به attributeهای bit-mapped کامپایل میشوند. این attributeها بسیار کارآمدند، چون فضای بسیار کمی در metadata مصرف میکنند—معمولاً فقط یک bit—و CLR میتواند آنها را با اندirection بسیار کم یا بدون آن پیدا کند. API مربوط به Reflection آنها را از طریق propertyهای اختصاصی روی Type و دیگر زیرکلاسهای MemberInfo مانند IsPublic، IsAbstract و IsSealed در اختیار میگذارد. propertyِ Attributes یک flags enum برمیگرداند که بیشتر این موارد را یکجا توصیف میکند:
static void Main()
{
TypeAttributes ta = typeof (Console).Attributes;
MethodAttributes ma = MethodInfo.GetCurrentMethod().Attributes;
Console.WriteLine (ta + "\r\n" + ma);
}
نتیجه:
AutoLayout, AnsiClass, Class, Public, Abstract, Sealed, BeforeFieldInit
PrivateScope, Private, Static, HideBySig
در مقابل، custom attributeها به یک blob کامپایل میشوند که از جدول اصلی فرادادهٔ type آویزان است. همهٔ custom attributeها با یک زیرکلاس از System.Attribute نمایش داده میشوند و برخلاف bit-mapped attributeها توسعهپذیرند. blob داخل metadata کلاس attribute را مشخص میکند و مقدار هر آرگومان positional یا named را که هنگام اعمال attribute تعیین شده نیز ذخیره میکند. custom attributeهایی که خودتان تعریف میکنید از نظر معماری کاملاً مشابه attributeهای تعریفشده در کتابخانههای .NET هستند.
فصل ۴ توضیح میدهد custom attribute را چگونه در C# به یک type یا member متصل کنید. اینجا attribute از پیش تعریفشدهٔ Obsolete را به کلاس Foo متصل میکنیم:
[Obsolete] public class Foo {...}
این کار به کامپایلر دستور میدهد یک نمونه از ObsoleteAttribute را داخل metadata مربوط به Foo قرار دهد؛ سپس در زمان اجرا میتوان با فراخوانی GetCustomAttributes روی یک Type یا MemberInfo آن را Reflection کرد.
pseudocustom attributeها از نظر ظاهر و شیوهٔ استفاده مانند custom attributeهای معمولیاند. آنها با یک زیرکلاس از System.Attribute نمایش داده میشوند و به شکل استاندارد متصل میشوند:
[System.Runtime.InteropServices.StructLayout(LayoutKind.Sequential)]
class SystemTime { ... }
تفاوت در این است که کامپایلر یا CLR در داخل، pseudocustom attributeها را با تبدیل به bit-mapped attribute بهینه میکند. نمونهها شامل StructLayout، In و Out هستند—فصل ۲۴. Reflection این attributeها را از طریق propertyهای اختصاصی مانند IsLayoutSequential آشکار میکند و در بسیاری از موارد هنگام فراخوانی GetCustomAttributes بهصورت اشیای System.Attribute نیز بازگردانده میشوند. در نتیجه تقریباً میتوانید تفاوت بین pseudo- و non-pseudocustom attributeها را نادیده بگیرید؛ یک استثنای مهم زمانی است که با Reflection.Emit در زمان اجرا type تولید میکنید—بخش «Emitting Assemblies and Types» در صفحهٔ ۸۴۱.
Attribute مربوط به AttributeUsage
AttributeUsage attributeای است که روی کلاسهای attribute اعمال میشود و به کامپایلر میگوید attribute مقصد باید چگونه استفاده شود:
public sealed class AttributeUsageAttribute : Attribute
{
public AttributeUsageAttribute (AttributeTargets validOn);
public bool AllowMultiple { get; set; }
public bool Inherited { get; set; }
public AttributeTargets ValidOn { get; }
}
AllowMultiple تعیین میکند attribute در حال تعریف بتواند بیش از یک بار روی همان target اعمال شود یا نه. Inherited مشخص میکند attributeی که روی base class اعمال شده است به derived classها نیز اعمال شود یا نه—و برای متدها، آیا attribute متصل به یک virtual method به متدهای overrideکننده هم برسد. ValidOn مجموعهٔ targetها—class، interface، property، method، parameter و غیره—را تعیین میکند که attribute میتواند به آنها متصل شود. این property هر ترکیبی از مقادیر enumِ AttributeTargets را میپذیرد:
All Delegate GenericParameter Parameter
Assembly Enum Interface Property
Class Event Method ReturnValue
Constructor Field Module Struct
برای نمونه، نویسندگان .NET، AttributeUsage را به شکل زیر روی attributeِ Serializable اعمال کردهاند:
[AttributeUsage (AttributeTargets.Delegate |
AttributeTargets.Enum |
AttributeTargets.Struct |
AttributeTargets.Class, Inherited = false)
]
public sealed class SerializableAttribute : Attribute { }
در واقع این تقریباً تعریف کامل attributeِ Serializable است. نوشتن یک کلاس attribute که هیچ property یا سازندهٔ ویژهای ندارد همینقدر ساده است.
تعریف Attribute خودتان
برای نوشتن attribute خودتان این مراحل را انجام دهید:
- یک کلاس را از
System.Attribute یا یکی از فرزندان آن مشتق کنید. طبق convention، نام کلاس باید با واژهٔ Attribute تمام شود، هرچند اجباری نیست. - attributeِ
AttributeUsage را که در بخش قبل توضیح داده شد اعمال کنید. اگر attribute به هیچ property یا آرگومان constructor نیاز ندارد، کار تمام است. - یک یا چند constructorِ public بنویسید. پارامترهای constructor، پارامترهای positionalِ attribute را تعریف میکنند و هنگام استفاده از attribute اجباری خواهند شد.
- برای هر named parameter که میخواهید پشتیبانی کنید یک field یا propertyِ public تعریف کنید. named parameterها هنگام استفاده از attribute اختیاریاند.
کلاس زیر attributeای برای کمک به یک سامانهٔ unit testing خودکار تعریف میکند. این attribute مشخص میکند یک متد باید تست شود، چند بار تست تکرار شود و در صورت شکست چه پیامی نمایش داده شود:
[AttributeUsage (AttributeTargets.Method)]
public sealed class TestAttribute : Attribute
{
public int Repetitions;
public string FailureMessage;
public TestAttribute () : this (1) { }
public TestAttribute (int repetitions) { Repetitions = repetitions; }
}
کلاس Foo زیر متدهایی دارد که به شکلهای مختلف با attributeِ Test تزئین شدهاند:
class Foo
{
[Test]
public void Method1() { ... }
[Test(20)]
public void Method2() { ... }
[Test(20, FailureMessage="Debugging Time!")]
public void Method3() { ... }
}
بازیابی Attributeها در زمان اجرا
برای بازیابی attributeها در زمان اجرا دو راه استاندارد وجود دارد:
- فراخوانی
GetCustomAttributes روی هر شیء Type یا MemberInfo. - فراخوانی
Attribute.GetCustomAttribute یا Attribute.GetCustomAttributes.
دو متد دستهٔ دوم overload شدهاند تا هر شیء Reflection متناظر با یک target معتبر attribute—یعنی Type، Assembly، Module، MemberInfo یا ParameterInfo—را بپذیرند.
با کد زیر میتوانیم همهٔ متدهای کلاس Foo بالا را که TestAttribute دارند enumerate کنیم:
foreach (MethodInfo mi in typeof (Foo).GetMethods())
{
TestAttribute att = (TestAttribute) Attribute.GetCustomAttribute
(mi, typeof (TestAttribute));
if (att != null)
Console.WriteLine ("Method {0} will be tested; reps={1}; msg={2}",
mi.Name, att.Repetitions, att.FailureMessage);
}
یا:
foreach (MethodInfo mi in typeof (Foo).GetTypeInfo().DeclaredMethods)
...
خروجی:
Method Method1 will be tested; reps=1; msg=
Method Method2 will be tested; reps=20; msg=
Method Method3 will be tested; reps=20; msg=Debugging Time!
برای کاملکردن این نمونه و نشاندادن اینکه چگونه میتوان از آن برای نوشتن یک سامانهٔ unit testing استفاده کرد، مثال را گسترش میدهیم تا واقعاً متدهایی را که با attributeِ Test تزئین شدهاند فراخوانی کند:
foreach (MethodInfo mi in typeof (Foo).GetMethods())
{
TestAttribute att = (TestAttribute) Attribute.GetCustomAttribute
(mi, typeof (TestAttribute));
if (att != null)
for (int i = 0; i < att.Repetitions; i++)
try
{
mi.Invoke (new Foo(), null); // Call method with no arguments
}
catch (Exception ex) // Wrap exception in att.FailureMessage
{
throw new Exception ("Error: " + att.FailureMessage, ex);
}
}
با بازگشت به Reflection روی attributeها، مثال زیر attributeهای موجود روی یک type مشخص را فهرست میکند:
object[] atts = Attribute.GetCustomAttributes (typeof (Test));
foreach (object att in atts) Console.WriteLine (att);
[Serializable, Obsolete]
class Test
{
}
و خروجی:
System.ObsoleteAttribute
System.SerializableAttribute
تولید پویای کد (Dynamic Code Generation)
فضای نام System.Reflection.Emit شامل کلاسهایی برای ایجاد metadata و IL در زمان اجرا است. تولید پویاى کد برای بعضی کارهای برنامهنویسی مفید است. یک مثال API مربوط به regular expressionهاست که typeهای بهینه و مخصوص regular expressionهای مشخص تولید میکند. مثال دیگر Entity Framework Core است که با Reflection.Emit کلاسهای proxy میسازد تا lazy loading را ممکن کند.
تولید IL با DynamicMethod
کلاس DynamicMethod ابزاری سبکوزن در فضای نام System.Reflection.Emit برای تولید متد در لحظه است. برخلاف TypeBuilder لازم نیست ابتدا یک dynamic assembly، module و type برای دربرگرفتن متد ایجاد کنید. بنابراین هم برای کارهای ساده مناسب است و هم مقدمهٔ خوبی برای Reflection.Emit محسوب میشود.
در ادامه نمونهای ساده از DynamicMethod برای ساخت متدی میبینید که Hello world را در Console مینویسد:
public class Test
{
static void Main()
{
var dynMeth = new DynamicMethod ("Foo", null, null, typeof (Test));
ILGenerator gen = dynMeth.GetILGenerator();
gen.EmitWriteLine ("Hello world");
gen.Emit (OpCodes.Ret);
dynMeth.Invoke (null, null); // Hello world
}
}
OpCodes برای هر opcode در IL یک fieldِ static read-only دارد. بیشتر قابلیتها از طریق opcodeهای مختلف عرضه میشوند، هرچند ILGenerator برای تولید labelها و local variableها و نیز exception handling متدهای تخصصی دارد.
یک متد همیشه با Opcodes.Ret—به معنای «return»—یا نوعی دستور branch/throw پایان مییابد. متد EmitWriteLine در ILGenerator میانبری برای emit کردن چند opcode سطح پایین است. اگر فراخوانی EmitWriteLine را با کد زیر جایگزین کنیم، همان نتیجه را میگیریم:
MethodInfo writeLineStr = typeof (Console).GetMethod ("WriteLine",
new Type[] { typeof (string) });
gen.Emit (OpCodes.Ldstr, "Hello world"); // Load a string
gen.Emit (OpCodes.Call, writeLineStr); // Call a method
توجه کنید که در سازندهٔ DynamicMethod مقدار typeof(Test) را پاس دادیم. این کار به dynamic method اجازه میدهد به متدهای nonpublic آن type دسترسی داشته باشد و در نتیجه بتوانیم چنین کاری انجام دهیم:
public class Test
{
static void Main()
{
var dynMeth = new DynamicMethod ("Foo", null, null, typeof (Test));
ILGenerator gen = dynMeth.GetILGenerator();
MethodInfo privateMethod = typeof(Test).GetMethod ("HelloWorld",
BindingFlags.Static | BindingFlags.NonPublic);
gen.Emit (OpCodes.Call, privateMethod); // Call HelloWorld
gen.Emit (OpCodes.Ret);
dynMeth.Invoke (null, null); // Hello world
}
static void HelloWorld() // private method, yet we can call it
{
Console.WriteLine ("Hello world");
}
}
درک IL نیازمند صرف زمان قابلتوجهی است. بهجای یادگیری همهٔ opcodeها، بسیار سادهتر است یک برنامهٔ C# را کامپایل کنید و بعد IL آن را بررسی، کپی و اصلاح کنید. LINQPad میتواند IL هر متد یا code snippetی را که وارد میکنید نمایش دهد و ابزارهای مشاهدهٔ اسمبلی مانند ILSpy نیز برای بررسی اسمبلیهای موجود مفیدند.
پشتهٔ ارزیابی (The Evaluation Stack)
یکی از مفاهیم محوری IL، پشتهٔ ارزیابی (Evaluation Stack) است. برای فراخوانی متدی با آرگومان، ابتدا آرگومانها را روی evaluation stack قرار میدهید—load میکنید—و بعد متد را فراخوانی میکنید. متد سپس آرگومانهای موردنیاز را از evaluation stack برمیدارد. پیشتر هنگام فراخوانی Console.WriteLine این موضوع را نشان دادیم. مثال مشابهی با یک عدد صحیح:
var dynMeth = new DynamicMethod ("Foo", null, null, typeof(void));
ILGenerator gen = dynMeth.GetILGenerator();
MethodInfo writeLineInt = typeof (Console).GetMethod ("WriteLine",
new Type[] { typeof (int) });
// The Ldc* op-codes load numeric literals of various types and sizes.
gen.Emit (OpCodes.Ldc_I4, 123); // Push a 4-byte integer onto stack
gen.Emit (OpCodes.Call, writeLineInt);
gen.Emit (OpCodes.Ret);
dynMeth.Invoke (null, null); // 123
برای جمعکردن دو عدد، ابتدا هر عدد را روی evaluation stack بارگذاری میکنید و سپس Add را فراخوانی میکنید. opcodeِ Add دو مقدار را از evaluation stack برمیدارد و نتیجه را دوباره روی پشته قرار میدهد. کد زیر ۲ و ۲ را جمع میکند و سپس نتیجه را با متد writeLine که قبلاً بهدست آوردیم مینویسد:
gen.Emit (OpCodes.Ldc_I4, 2); // Push a 4-byte integer, value=2
gen.Emit (OpCodes.Ldc_I4, 2); // Push a 4-byte integer, value=2
gen.Emit (OpCodes.Add); // Add the result together
gen.Emit (OpCodes.Call, writeLineInt);
برای محاسبهٔ 10 / 2 + 1 میتوانید به یکی از این دو شکل عمل کنید:
gen.Emit (OpCodes.Ldc_I4, 10);
gen.Emit (OpCodes.Ldc_I4, 2);
gen.Emit (OpCodes.Div);
gen.Emit (OpCodes.Ldc_I4, 1);
gen.Emit (OpCodes.Add);
gen.Emit (OpCodes.Call, writeLineInt);
یا:
gen.Emit (OpCodes.Ldc_I4, 1);
gen.Emit (OpCodes.Ldc_I4, 10);
gen.Emit (OpCodes.Ldc_I4, 2);
gen.Emit (OpCodes.Div);
gen.Emit (OpCodes.Add);
gen.Emit (OpCodes.Call, writeLineInt);
پاسدادن آرگومان به Dynamic Method
opcodeهای Ldarg و Ldarg_XXX آرگومانی را که به متد پاس شده است روی stack بارگذاری میکنند. برای برگرداندن یک مقدار، در پایان دقیقاً یک مقدار روی stack باقی بگذارید. برای اینکه این کار ممکن باشد، هنگام فراخوانی سازندهٔ DynamicMethod باید نوع بازگشتی و نوع آرگومانها را مشخص کنید. مثال زیر dynamic methodی میسازد که مجموع دو integer را برمیگرداند:
DynamicMethod dynMeth = new DynamicMethod ("Foo",
typeof (int), // Return type = int
new[] { typeof (int), typeof (int) }, // Parameter types = int, int
typeof (void));
ILGenerator gen = dynMeth.GetILGenerator();
gen.Emit (OpCodes.Ldarg_0); // Push first arg onto eval stack
gen.Emit (OpCodes.Ldarg_1); // Push second arg onto eval stack
gen.Emit (OpCodes.Add); // Add them together (result on stack)
gen.Emit (OpCodes.Ret); // Return with stack having 1 value
int result = (int) dynMeth.Invoke (null, new object[] { 3, 4 } ); // 7
بهجای فراخوانی Invoke، گاهی کار با dynamic method به شکل یک typed delegate راحتتر است. متد CreateDelegate دقیقاً همین کار را انجام میدهد. در مثال ما delegate لازم دو پارامتر integer و یک نوع بازگشتی integer دارد؛ بنابراین میتوانیم از Func<int,int,int> استفاده کنیم. خط آخر مثال قبل چنین میشود:
var func = (Func<int,int,int>) dynMeth.CreateDelegate
(typeof (Func<int,int,int>));
int result = func (3, 4); // 7
در بخش «Emitting Type Members» در صفحهٔ ۸۴۴ نشان میدهیم چگونه آرگومان را by reference پاس دهید.
تولید متغیرهای محلی (Generating Local Variables)
با فراخوانی DeclareLocal روی ILGenerator میتوانید یک local variable تعریف کنید. این متد یک شیء LocalBuilder برمیگرداند که میتوانید آن را در کنار opcodeهایی مانند Ldloc—بارگذاری local variable—یا Stloc—ذخیرهٔ local variable—استفاده کنید. Ldloc چیزی را روی evaluation stack push میکند و Stloc چیزی را از آن pop میکند. برای نمونه، کد C# زیر را در نظر بگیرید:
int x = 6;
int y = 7;
x *= y;
Console.WriteLine (x);
کد زیر همان را بهصورت پویا تولید میکند:
var dynMeth = new DynamicMethod ("Test", null, null, typeof (void));
ILGenerator gen = dynMeth.GetILGenerator();
LocalBuilder localX = gen.DeclareLocal (typeof (int)); // Declare x
LocalBuilder localY = gen.DeclareLocal (typeof (int)); // Declare y
gen.Emit (OpCodes.Ldc_I4, 6); // Push literal 6 onto eval stack
gen.Emit (OpCodes.Stloc, localX); // Store in localX
gen.Emit (OpCodes.Ldc_I4, 7); // Push literal 7 onto eval stack
gen.Emit (OpCodes.Stloc, localY); // Store in localY
gen.Emit (OpCodes.Ldloc, localX); // Push localX onto eval stack
gen.Emit (OpCodes.Ldloc, localY); // Push localY onto eval stack
gen.Emit (OpCodes.Mul); // Multiply values together
gen.Emit (OpCodes.Stloc, localX); // Store the result to localX
gen.EmitWriteLine (localX); // Write the value of localX
gen.Emit (OpCodes.Ret);
dynMeth.Invoke (null, null); // 42
Branching
در IL حلقههای while، do و for وجود ندارند؛ همهچیز با labelها و معادل دستورهای goto و goto شرطی انجام میشود. اینها opcodeهای branching هستند؛ مانند Br—branch بدون شرط—Brtrue—branch اگر مقدار روی evaluation stack درست باشد—و Blt—branch اگر مقدار اول از مقدار دوم کمتر باشد.
برای تعیین مقصد branch، ابتدا DefineLabel را فراخوانی کنید که یک شیء Label برمیگرداند و سپس در محلی که میخواهید label لنگر شود MarkLabel را فراخوانی کنید. برای نمونه، کد C# زیر را در نظر بگیرید:
int x = 5;
while (x <= 10) Console.WriteLine (x++);
میتوانیم آن را اینگونه emit کنیم:
ILGenerator gen = ...
Label startLoop = gen.DefineLabel(); // Declare labels
Label endLoop = gen.DefineLabel();
LocalBuilder x = gen.DeclareLocal (typeof (int)); // int x
gen.Emit (OpCodes.Ldc_I4, 5); //
gen.Emit (OpCodes.Stloc, x); // x = 5
gen.MarkLabel (startLoop);
gen.Emit (OpCodes.Ldc_I4, 10); // Load 10 onto eval stack
gen.Emit (OpCodes.Ldloc, x); // Load x onto eval stack
gen.Emit (OpCodes.Blt, endLoop); // if (x > 10) goto endLoop
gen.EmitWriteLine (x); // Console.WriteLine (x)
gen.Emit (OpCodes.Ldloc, x); // Load x onto eval stack
gen.Emit (OpCodes.Ldc_I4, 1); // Load 1 onto the stack
gen.Emit (OpCodes.Add); // Add them together
gen.Emit (OpCodes.Stloc, x); // Save result back to x
gen.Emit (OpCodes.Br, startLoop); // return to start of loop
gen.MarkLabel (endLoop);
gen.Emit (OpCodes.Ret);
نمونهسازی اشیا و فراخوانی متدهای Instance
معادل new در IL، opcodeِ Newobj است. این opcode یک constructor میگیرد و شیء ساختهشده را روی evaluation stack قرار میدهد. برای مثال، کد زیر یک StringBuilder میسازد:
var dynMeth = new DynamicMethod ("Test", null, null, typeof (void));
ILGenerator gen = dynMeth.GetILGenerator();
ConstructorInfo ci = typeof (StringBuilder).GetConstructor (new Type[0]);
gen.Emit (OpCodes.Newobj, ci);
بعد از بارگذاری شیء روی evaluation stack، میتوانید با opcodeِ Call یا Callvirt متدهای instance آن را فراخوانی کنید. با ادامهٔ مثال، propertyِ MaxCapacity از StringBuilder را با فراخوانی accessorِ get آن query میکنیم و نتیجه را مینویسیم:
gen.Emit (OpCodes.Callvirt, typeof (StringBuilder)
.GetProperty ("MaxCapacity").GetGetMethod());
gen.Emit (OpCodes.Call, typeof (Console).GetMethod ("WriteLine",
new[] { typeof (int) } ));
gen.Emit (OpCodes.Ret);
dynMeth.Invoke (null, null); // 2147483647
برای تقلید semantics فراخوانی C#:
- برای فراخوانی متدهای static و متدهای instanceِ value type از
Call استفاده کنید. - برای فراخوانی متدهای instanceِ reference type—چه virtual اعلام شده باشند چه نه—از
Callvirt استفاده کنید.
در مثال ما Callvirt را روی نمونهٔ StringBuilder بهکار بردیم، با اینکه MaxCapacity virtual نیست. این خطا ایجاد نمیکند و صرفاً یک فراخوانی nonvirtual انجام میدهد. استفادهٔ همیشگی از Callvirt برای متدهای instanceِ reference type خطر حالت عکس را از بین میبرد: فراخوانی یک virtual method با Call. این خطر واقعی است، زیرا نویسندهٔ متد مقصد ممکن است بعداً declaration آن را تغییر دهد. Callvirt همچنین بررسی میکند receiver مقدار null نباشد.
در مثال بعد StringBuilder را با دو آرگومان میسازیم، رشتهٔ ", world!" را به آن append میکنیم و سپس ToString را فراخوانی میکنیم:
// We will call: new StringBuilder ("Hello", 1000)
ConstructorInfo ci = typeof (StringBuilder).GetConstructor (
new[] { typeof (string), typeof (int) } );
gen.Emit (OpCodes.Ldstr, "Hello"); // Load a string onto the eval stack
gen.Emit (OpCodes.Ldc_I4, 1000); // Load an int onto the eval stack
gen.Emit (OpCodes.Newobj, ci); // Construct the StringBuilder
Type[] strT = { typeof (string) };
gen.Emit (OpCodes.Ldstr, ", world!");
gen.Emit (OpCodes.Call, typeof (StringBuilder).GetMethod ("Append", strT));
gen.Emit (OpCodes.Callvirt, typeof (object).GetMethod ("ToString"));
gen.Emit (OpCodes.Call, typeof (Console).GetMethod ("WriteLine", strT));
gen.Emit (OpCodes.Ret);
dynMeth.Invoke (null, null); // Hello, world!
برای سرگرمی، GetMethod را روی typeof(object) فراخوانی کردیم و سپس با Callvirt یک فراخوانی virtual روی ToString انجام دادیم. میتوانستیم همان نتیجه را با فراخوانی ToString مستقیماً روی نوع StringBuilder بگیریم:
gen.Emit (OpCodes.Callvirt, typeof (StringBuilder).GetMethod ("ToString",
new Type[0] ));
آرایهٔ خالی type هنگام فراخوانی GetMethod لازم است، زیرا StringBuilder متد ToString را با signature دیگری نیز overload میکند.
مدیریت Exception
ILGenerator متدهای اختصاصی برای exception handling دارد. بنابراین ترجمهٔ IL برای کد C# زیر:
try { throw new NotSupportedException(); }
catch (NotSupportedException ex) { Console.WriteLine (ex.Message); }
finally { Console.WriteLine ("Finally"); }
چنین است:
MethodInfo getMessageProp = typeof (NotSupportedException)
.GetProperty ("Message").GetGetMethod();
MethodInfo writeLineString = typeof (Console).GetMethod ("WriteLine",
new[] { typeof (object) } );
gen.BeginExceptionBlock();
ConstructorInfo ci = typeof (NotSupportedException).GetConstructor (
new Type[0] );
gen.Emit (OpCodes.Newobj, ci);
gen.Emit (OpCodes.Throw);
gen.BeginCatchBlock (typeof (NotSupportedException));
gen.Emit (OpCodes.Callvirt, getMessageProp);
gen.Emit (OpCodes.Call, writeLineString);
gen.BeginFinallyBlock();
gen.EmitWriteLine ("Finally");
gen.EndExceptionBlock();
همانند C#، میتوانید چندین catch block داشته باشید. برای پرتاب مجدد همان exception، opcodeِ Rethrow را emit کنید.