.Net – Kĩ thuật Reflection Emit trong C#

 

Reflection Emit như tên gọi của nó, cung cấp cho bạn một phương pháp để tạo nên các kiểu dữ liệu động trong quá trình thực thi. Tương tự như CodeDom nhưng thay vì làm việc với các mã lệnh bậc cao như C# hoặc VB.Net, bạn cần phải có kiến thức ở mức thấp về MSIL/CIL để làm việc với lớp ILGenerator nhằm sinh ra mã lệnh cho các phương thức và property,…

Cấu trúc cơ bản để xây dựng nên một assembly trong kĩ thuật Reflection Emit được minh họa như hình dưới đây. Có thể coi Type (class/structure/enum/delegate) là đơn vị cấu trúc nhỏ nhất. Bên trong Type còn có các thành phần khác field, property, method, event làm nhiệm vụ tạo nên các đặc tính và hành vi của Type đó. Đây là những khái niệm căn bản của hướng đối tượng trong .Net nên có lẽ bạn cũng đã nắm khá rõ.


Reflection Emit – Structure

Như vậy, bạn có thể hình dung được phần nào những bước để tạo nên một nên một assembly hoàn chỉnh. Cụ thể ta sẽ đi từng bước để tạo ra những thành phần dựa vào các lớp Builder tương ứng mà .Net cung cấp:

–               Tạo Assembly bên trong một Application Domain.

–               Tạo Module bên trong Assembly

–               Tạo Type bên trong Module

–               Tạo các property, method, event,… bên trong Type

–               Tạo các lệnh xử lý cho property, method,.. bằng ILGenerator.

Đó là tất cả những bước cần làm và không có gì khó khăn để hiểu nếu như bạn từng làm việc với CodeDom hoặc hiểu rõ việc lập trình OOP trong .Net.

Bây giờ tôi muốn làm lại ví dụ trong bài viết CodeDom, cụ thể tôi sẽ dùng Reflection Emit để tạo ra một lớp sau:

Rectangle.cs:

Và lưu lại thành một assembly là RectangleTest.dll. Bạn có thể thấy mục đích của tôi là tạo một thư viện DLL chứ không phải một tập tin thực thi EXE như trong bài ví dụ về CodeDom. Đây là một phần trong cách giới thiệu của tôi để bạn nắm rõ hơn về cách tạo và sử dụng cả hai loại kiểu assembly này. Trong phần cuối tôi sẽ bổ sung phương thức Main() như sau để hoàn chỉnh ví dụ của mình:

Ví dụ trên có đôi chút khác biệt so với ví dụ mà tôi sử dụng trong bài CodeDom. Khi đọc đến phần về tạo method và property với Reflection Emit bạn có thể thấy được lý do của sự thay đổi và sắp xếp của tôi nhằm giới thiệu kĩ thuật này rõ ràng hơn.

Tạo cấu trúc cho assembly: RectangleTest

Các bước sau được trình bày tuần tự và được đặt trong phương thức CreateDynamicAssembly() với kiểu trả về là một đối tượng kiểu Type, lưu giữ thông tin về lớp mà ta sẽ tạo ra.

Các lớp đại diện cho mỗi cấu trúc như AssemblyBuilder, ModuleBuilder, TypeBuilder chứa các phương thức có dạng DefineXXX() và GetXXX() cho phép định nghĩa và lấy các thành viên của chúng. Bạn sẽ thấy chúng khá đơn giản cho đến khi chúng ta bắt đầu làm việc với ILGenerator.

1. Tạo Assembly

Một assembly khi được tạo ra phải nằm trong một Application Domain. Application Domain có thể được hiểu đơn giản là một vùng cách ly để các chương trình hoạt động độc lập với nhau. Thông thường assembly sẽ được tạo trong chính Application Domain của ứng dụng sinh ra nó. Việc này cần thiết để ta có thể truy xuất được assembly dễ dàng. Hai cách để lấy Application Domain hiện tại là:

AppDomain.CurrentDomain

Thread.GetDomain()

Khi đã có AppDomain ta sẽ gọi phương thức DefineDynamicAssembly(). Kiểu của hai tham số bắt buộc mà các overload của phương thức này cần là AssemblyName và AssemblyBuilderAccess.

AssemblyName: như một phần tên gọi của nó, mô tả đầy đủ về định danh cũng như các thông tin về phiên bản,…

AssemblyBuilderAccess: một kiểu enum chỉ ra cách mà assembly này có thể được truy xuất. Chúng gồm có: ReflectionOnly, Run, RunAndSave, Save.

Trong ví dụ của mình, tôi sẽ tạo assembly với các thông tin như sau:

2. Tạo Module

Một module thường được hiểu với nghĩa chung chung nhưng trong .Net nó được xác định là một tập tin vật lý, và tên của module cũng chính là tên của tập tin đó.

Note: Khi làm việc với lớp System.Reflection.Module bạn có thể thấy các thuộc tính như FullyQualifiedName, Name và ScopeName. Trong đó FullyQualifiedName và Name trả về tên của tập tin còn ScopeName trả về tên đại diện của Module đó.

Để tạo thêm một module trong một assembly, bạn sử dụng phương thức DefineDynamicModule() của assembly đó với tham số đầu là ScopeName của module và tham số thứ hai là đường dẫn mà bạn sẽ lưu Module này.

3. Tạo Type

Định nghĩa lớp public Retangle trong module:

Enum TypeAttributes chứa các giá trị mô tả đặc tính của Type mà bạn muốn tạo ra. Các giá trị của enum TypeAttributes khá nhiều nên tôi sẽ không giới thiệu lên đây. Nếu muốn kết hợp nhiều giá trị TypeAttributes với nhau bạn hãy dùng toán tử bitwise | (OR).

Để xác định lớp cha mà Type của bạn thừa kế, hãy dùng overload DefineType(string name, TypeAttribute attr, Type parent). Trong trường hợp cần hiện thực nhiều interface, TypeBuilder cung cấp cho bạn phương thức AddInterfaceImplementation(Type interfaceType).

Như vậy ta đã xong phần khung cho assembly của mình. Không có gì phức tạp và bạn có thể hiểu tất cả những gì mà tôi đã trình bày. Chúng ta sẽ đi vào phần chính của bài và như tôi đã nói trước, bạn sẽ thấy là mình nên chuẩn bị những kiến thức về MSIL/CIL.

Hoàn thành Type: Rectangle

1. Tạo Field

Tạo hai field _length và _width kiểu Int32 với thuộc tính truy xuất là private:

2. Tạo Method

Tạo phương thức CalculateArea() của lớp Rectangle với thuộc tính là public và virtual:

1
MethodBuilder mCalArea = typeBuilder.DefineMethod("CalculateArea", MethodAttributes.Public | MethodAttributes.Virtual, CallingConventions.HasThis, typeof(Int32), null);

Overload của phương thức trên có dạng sau:

TypeBuilder.DefineMethod(string name,
MethodAttributes attributes,
CallingConventions callingConvention,
Type returnType,
Type[] parameterTypes)

Tham số thứ ba là CallingConventions được đặt là HasThis để xác định rằng đây là một phương thức instance hoặc virtual (không phải static).

Tiếp theo ta phải tạo một đối tượng ILGenerator để sinh ra phần mã lệnh cho phương thức này bằng cách gọi phương thức GetILGenerator():

Phương thức chính của đối tượng ILGenerator mà ta sử dụng là Emit(). Mỗi lần phương thức này được sử dụng, tương ứng với một dòng mã MSIL được sinh ra. Tham số của phương này là một đối tượng kiểu OpCode đại diện cho một lệnh MSIL. Như bạn thấy lớp OpCodes chứa đầy đủ các đối tượng kiểu OpCode cho phép bạn truy xuất và tham khảo công dụng của từng lệnh MSIL nhanh chóng và dễ dàng.

Bạn có thể thấy hai dòng lệnh với OpCode là Ldfld dùng để nạp giá trị của field vào stack với tham số thứ hai là đối tượng kiểu FieldInfo (FieldBuilder kế thừa từ FieldInfo) chỉ ra field sẽ được sử dụng. Trong phần sau bạn có thể thấy ta còn sử dụng cả OpCode Stfld để gán giá trị cho một field.

Note: Nếu bạn chưa hiểu đoạn mã trên, hãy dành lại một chút thời gian đọc lại các bài hướng dẫn về MSIL tại đây. Nếu chúng vẫn còn quá phức tạp với bạn thì phương pháp đơn giản và an toàn nhất là bạn hãy viết một dự án bằng .Net bằng ngôn ngữ bậc cao như C#, VB.Net sau đó biên dịch và dùng công cụ ildasm.exe để xem mã MSIL của tập tin đó.

3. Tạo Property

Nếu như phân tích một tập tin .Net PE, bạn có thể thấy rằng các property được xây dựng dựa trên hai phương thức set/get tương ứng với tên của property đó. Ví dụ:

Như vậy khi thiết kế một property bằng kĩ thuật Reflection Emit, bạn không thể dựa dẫm vào trình biên dịch tự động sinh ra các phương thức này cho mình. Cụ thể bạn phải thêm các phương thức set/get vào lớp Rectangle như sau:

Khi biên dịch thử đoạn code trên bạn sẽ nhận được hai thông báo lỗi, nguyên nhân là do hai phương thức này trùng tên và tham số với hai phương thức mà trình biên dịch sẽ tự động sinh ra:

Type ‘Y2Examples.Rectangle’ already reserves a member called ‘get_Length’ with the same parameter types

Type ‘Y2Examples.Rectangle’ already reserves a member called ‘set_Length’ with the same parameter types

Bạn không nhất thiết phải đặt tên theo kiểu get_XXX(), set_XXX() như trên và bởi vì chúng ta sẽ làm việc với mã lệnh ở cấp thấp nên bạn cũng không cần quan tâm tới việc trình biên dịch sẽ báo lỗi trên, tức là bạn phải thay thế vai trò của trình biên dịch.

Như bạn thấy tôi sẽ đặt phạm vi truy xuất của hai phương thức get_Length() và set_Length() là public tức là property Length của ta có thể được set và get từ bên ngoài.

Khai báo property Length kiểu Int32 và không có tham số:

Tạo phương thức get_Length() và set_Length():

Cuối cùng ta gắn hai phương thức này vào property vừa tạo:

Đối với property Width bạn cũng làm tương tự chỉ thay đổi tên.

4. Tạo Contructor:

Việc tạo contructor cũng tương tự như tạo method, lớp TypeBuilder cung cấp phương thức DefineConstructor()

Bạn có thể thấy là tôi tạo một đối tượng ConstructorInfo của kiểu Object và thực thi đối tượng này bằng lệnh:

ilGen.Emit(OpCodes.Call, conInfo);

Đây là bước cần thiết để thực hiện constructor của lớp cha từ lớp mà bạn thừa kế. Ở đây vì lớp Rectangle đang tạo thừa kế ngầm định từ lớp Object nên ta có thể bỏ qua không cần thực hiện bước này.

Lưu assembly và kiểm tra kết quả

Sau khi đã hoàn thành Type, bạn cần gọi phương thức CreateType() của TypeBuilder. Nếu phương thức này không được gọi, bạn sẽ nhận được một exception “Type ‘Rectangle’ was not completed” khi thực hiện Save() assembly.

Để kiểm tra xem mọi thứ có chạy đúng những gì mà bạn mong muốn không, ta sẽ sử dụng đối tượng Type trả về từ phương thức CreateDynamicAssembly() và dùng Reflection để tạo instance của lớp Rectangle và thực hiện tính toán trên đó. Tham khảo bài viết về Kĩ thuật Reflection nếu bạn chưa hiểu rõ những dòng lệnh sau:

Output:
Reflection Emit - Test
Bạn cũng có thể Add Reference từ file RectangleTest.dll được tạo ra và sử dụng theo cách thông thường.

Thêm phương thức Main() cho lớp Rectangle

Đồng thời với việc thêm phương thức Main() ta sẽ chuyển assembly sẽ tạo ra thành một tập tin EXE thay vì DLL. Việc tạo phương thức Main() không khác gì tạo một phương thức thông thường:

Đoạn mã trên sử dụng phương thức ILGenerator.DeclareLocal() để khai báo một biến cục bộ chứa kết quả của phương thức CalculateArea(). Cuối cùng điểm quan trọng mà bạn cần phải nhớ là phải đặt Entry Point cho assembly thông qua dòng lệnh sau:

asmBuilder.SetEntryPoint(mMain);

Nhận xét