BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Dynamiczne generowanie kodu MSIL z wykorzystaniem Reflection Emit

21-12-2004 22:19 | spidey888
Niniejszy artykuł poświęcony jest zagadnieniu dynamicznego generowania i wykonywania kodu z poziomu aplikacji .NET. Przyjrzymy się niektórym możliwościom, które oferują nam moduły System.Reflection oraz System.Reflection.Emit. Nauczymy się, jak tworzyć dynamiczne assemblies, oraz poznamy sposoby wykorzystania stworzonego w ten sposób kodu. Całość na przykładzie prostego programu w C#.

1. Wprowadzenie

Niniejszy artykuł poświęcony jest zagadnieniu dynamicznego generowania i wykonywania kodu z poziomu aplikacji .NET. Przyjrzymy się niektórym możliwościom, które oferują nam moduły System.Reflection oraz System.Reflection.Emit. Nauczymy się, jak tworzyć dynamiczne assemblies, oraz poznamy sposoby wykorzystania stworzonego w ten sposób kodu. Całość na przykładzie prostego programu w C#. Do przyswojenia sobie tych wiadomości nie będzie nam – wbrew pozorom – potrzebne doświadczenie w korzystaniu z mechanizmu refleksji. Wystarczy podstawowe pojęcie o przeznaczeniu tego mechanizmu. Przydatna za to może być umiejętność rozumienia niskopoziomowego kodu, gdyż takim będziemy się w tym artykule zajmować. Dobrym wprowadzeniem będzie dostępny na CodeGuru artykuł „Elementarz MSIL” Maurycego Prodeusa.

2. Czego musimy być świadom zanim zaczniemy?

 

2.1 ABC Mechanizmu refleksji typów

Mechanizm refleksji typów najczęściej jest definiowany jako funkcjonalność w .NET pozwalająca na uzyskiwanie szczegółowych informacji o typach danych w trakcie wykonania programu. Z takiego opisu można wywnioskować, że mechanizm ten jest zupełnie pasywny. W rzeczywistości jego możliwości są o wiele większe. Chcąc być bardziej ścisłym w jego definicji, należałoby powiedzieć, że jest to zbiór klas i metod, które w całości tworzą interfejs pozwalający na manipulowanie typami danych.

Przestrzeń nazw System.Reflection pozwala na manipulowanie istniejącym już kodem, czyli m.in. na:

- tworzenie instancji danego typu

- przypisywanie stworzonych instancji do obiektów

- pobieranie informacji o typie na podstawie obiektu

- dostęp do wszystkich metod, właściwości i pól danego obiektu

Przestrzeń System.Reflection.Emit służy z kolei do dynamicznego generowania kodu.

2.2 Struktura Assemblies w (dużym) skrócie

Z punku widzenia tematu podjętego w artykule, musimy wiedzieć, że Assemblies zawierają w sobie moduły. Moduły zawierają typy danych, które z kolei zawierają w sobie składowe (w szczególnym przypadku zawierają typy zagnieżdżone, ale to już inna bajka).

3. Dynamiczne tworzenie assemblies.

Przejdźmy do przykładu. Załóżmy, że chcemy wygenerować dynamicznie taką oto klasę:

 

 

class Book

{

      private int pagesCount = 0;

      public string Title = "";

      public string Autor = "";

      public int Pages

      {

            set

            {

                  if (value < 1)

                        throw new ArgumentException("Liczba stron nie moze byc mniejsza od 1");

                  pagesCount = value;

            }

            get

            {

                  return pagesCount;

            }

      }

      public Book(int _pagesCount)

      {    

            try

            {

                  Pages = _pagesCount;

            }

            catch (ArgumentException ex)

            {

                  throw new ArgumentException("Nie mozna stworzyc obiektu. Liczba stron nie moze być mniejsza od 1");

            }

      }

      public override string ToString()

      {

            return Title + ", " + Autor + ", stron: " + pagesCount.ToString();

      }

}

 

            Tworzenie generatora kodu rozpoczniemy od utworzenia instancji klasy AssemblyBuilder:

 

1

string name = "Book";

2

string filename = "Book.dll";

3

AssemblyName bookAssemblyName = new AssemblyName();

4

bookAssemblyName.Version = new Version(1, 0, 0, 0);

5

bookAssemblyName.Name = name;

6

AppDomain appDomain = Thread.GetDomain();

7

AssemblyBuilder bookAssemblyBuilder =

appDomain.DefineDynamicAssembly(bookAssemblyName, AssemblyBuilderAccess.RunAndSave, ".");

 

            W pierwszej kolejności tworzymy obiekt klasy AssemblyName(3). Obiekt ten służy jednoznacznej identyfikacji tworzonego przez nas kodu. Następnie pobieramy kontekst wykonania bieżącego wątku(6) naszej aplikacji i w jego obrębie tworzymy dynamiczne assembly(7). Drugi argument metody DefineDynamicAssembly to enumerator definiujący przeznaczenie tworzonego przez nas kodu. W naszym przykładzie kod będzie zarówno uruchamiany z poziomu aplikacji, jak i wyeksportowany do pliku DLL, co sygnalizujemy wartością AssemblyBuilderAccess.RunAndSave. Możliwe są również wartości AssemblyBuilderAccess.Run (tylko dynamiczne wywołanie stworzonego kodu) oraz AssemblyBuilderAccess.Save (tylko eksport assembly do pliku). Trzeci argument tej metody określa nam docelowy katalog, w którym assembly zostanie zapisany.

            Kolejnym krokiem będzie zdefiniowanie modułu w obrębie utworzonego już assembly oraz typu w obrębie utworzonego modułu.

 

8

ModuleBuilder bookModuleBuilder = bookAssemblyBuilder.DefineDynamicModule(name, filename);

9

TypeBuilder bookTypeBuilder = bookModuleBuilder.DefineType(name + "." + name, TypeAttributes.Class | TypeAttributes.Public);

 

Powyższy kod nie wymaga dłuższego komentarza. Istotny jest ostatni parametr metody DefineType klasy ModuleBuilder(9). Parametr ten określa nam cechy tworzonego przez nas typu, np. określić że typ jest interfejsem albo klasą abstrakcyjną. W naszym przykładzie tworzymy publiczną klasę, dlatego parametr ten przyjmuje wartość TypeAttributes.Class | TypeAttributes.Public.

Teraz nadszedł czas na zdefiniowanie składowych stworzonego przez nas typu.

 

10

FieldBuilder m_pagesCount = bookTypeBuilder.DefineField("pagesCount", typeof(int), FieldAttributes.Private);

11

FieldBuilder m_Author = bookTypeBuilder.DefineField("Author", typeof(string), FieldAttributes.Public);

12

FieldBuilder m_Title = bookTypeBuilder.DefineField("Title", typeof(string), FieldAttributes.Public);

13

PropertyBuilder prop_Pages = bookTypeBuilder.DefineProperty("Pages",

      PropertyAttributes.HasDefault,

      typeof(int),

      new Type[]{typeof(int)});

14

MethodBuilder set_Pages = bookTypeBuilder.DefineMethod("set_Pages",

      MethodAttributes.Public , null, new Type[]{typeof(int)});

15

MethodBuilder get_Pages = bookTypeBuilder.DefineMethod("get_Pages",

      MethodAttributes.Public , typeof(int), new Type[0]);

16

MethodBuilder toString = typeBuilder.DefineMethod("ToString", MethodAttributes.HideBySig | MethodAttributes.Virtual | MethodAttributes.Public, CallingConventions.Standard, typeof(string), new Type[0]);

 

Powyższy kod spowoduje utworzenie kolejno:

- prywatnej składowej typu int o nazwie pagesCount (10)

- publicznej składowej typu string o nazwie Author (11)

- publicznej składowej typu string o nazwie Title (12)

- właściwości klasy o typie int i nazwie Pages (13)

- metody set_Pages, którą wykorzystamy do obsługi nadawania wartości właściwości Pages (14)

- metody get_Pages, którą wykorzystamy do obsługi pobierania wartości właściwości Pages (15)

- publicznej, bezparametrowej metody ToString, zwracającej obiekt typu string (16).

 

Przechodzimy teraz do najbardziej „soczystego” etapu, czyli implementacji metod. Uwaga! Tylko dla osób o mocnych nerwach!

Najpierw na warsztat – ze względu na jej największą prostotę - weźmiemy implementację metody get_Pages.

Całość wygląda następująco:

 

17

ILGenerator ilgen = get_Pages.GetILGenerator();

18

ilgen.Emit(OpCodes.Ldarg_0);

19

ilgen.Emit(OpCodes.Ldfld, m_pagesCount);

20

ilgen.Emit(OpCodes.Ret);

 

Wszystkie elementy typu, których przeznaczeniem jest wykonywanie jakiegoś kodu, posiadają interfejs, dzięki któremu możemy „wstrzykiwać” kod w postaci instrukcji języka MSIL. Interfejsem tym jest klasa ILGenerator, a funkcją „wstrzykującą” kod jest metoda Emit. Parametrem metody Emit jest kod instrukcji, którą chcemy wyemitować, oraz (opcjonalnie) dodatkowy parameter. Kody wszystkich instrukcji zawiera klasa OpCodes. Klasa ILGenerator potrafi ponadto umieszać w kodzie dodatkowe informacje, takie jak etykiety oznaczające pewne miejsca w kodzie (wykorzystywane przez instrukcje sterujące wykonaniem kodu) czy też oznaczenia początku i końca bloków obsługi wyjątków. O tym wszystkim jeszcze wspomnimy, powróćmy tymczasem do implementacji funkcji get_Pages. Implementacja ta składa się z trzech tylko instrukcji. Kolejno ładujemy na stos wskaźnik this(18), wartość składowej pagesCount(19) (która jest definiowana przez obiekt m_pagesCount) oraz sygnalizujemy powrót z funkcji(20). Wrzucone na stos dane będą dla maszyny wirtualnej .NET informacją, jaką wartość ma zwrócić i na rzecz którego obiektu.

Podczas opisu następnych metod nie będziemy już tak szczegółowo analizować emitowanego kodu, gdyż nie to jest celem tego artykułu. Jest rzeczą oczywistą, że podczas emisji instrukcji pojawi się pytanie „Skąd mam wiedzieć jakie instrukcje emitować i jakie konstrukcje z nich tworzyć?”. Nieoceniony w tym momencie będzie wspomniany już artykuł „elementarz MSIL” oraz lektura MSDN (dla posiadających dużą ilość wolnego czasu). W ostatnim rozdziale tego artykułu dostępna jest również garść użytecznych porad, do których przeczytania gorąco zachęcam .

Przejdźmy do implementacji metody set_pages. Jest ona nieco ciekawsza od get_Pages, a to dlatego że napotkamy tu etykiety.

Oto ona:

 

21

Ilgen = set_Pages.GetILGenerator();

22

Label afterThrow = ilgen.DefineLabel();

23

ilgen.Emit(OpCodes.Ldarg_1);

24

ilgen.Emit(OpCodes.Ldc_I4_1);

25

ilgen.Emit(OpCodes.Bge_S, afterThrow);

26

ilgen.Emit(OpCodes.Ldstr, "Liczba stron nie moze byc mniejsza od 1");

27

ilgen.Emit(OpCodes.Newobj, typeof(System.ArgumentException).GetConstructor(new type[]{typeof(string)}));

28

ilgen.Emit(OpCodes.Throw);

29

ilgen.MarkLabel(afterThrow);

30

ilgen.Emit(OpCodes.Ldarg_0);

31

ilgen.Emit(OpCodes.Ldarg_1);

32

ilgen.Emit(OpCodes.Stfld, m_pagesCount);

33

ilgen.Emit(OpCodes.Ret);

 

Po pobraniu generatora instrukcji (21) definujemy etykietę w obrębie kodu (22). Miejsce w kodzie, do którego odności się etykieta sygnalizujemy metodą MarkLabel(29). Do etykiety możemy się odwołać w kodzie, zanim jeszcze oznaczymy miejsce, którego dotyczy. Ze zdefiniowanej tutaj etykiety korzysta instrukcja skoku warunkowego bge.s (25).

Kolejną implementowaną przez nas funkcją będzie ToString(). Przy okazji tej metody pokażemy jak definiować lokalne zmienne w obrębie implementacji kodu.

 

35

ilgen = toString.GetILGenerator();

36

ilgen.DeclareLocal(typeof(string));

37

ilgen.DeclareLocal(typeof(string[]));

38

ilgen.Emit(OpCodes.Ldc_I4_5);

39

ilgen.Emit(OpCodes.Newarr, typeof(System.String));

40

ilgen.Emit(OpCodes.Stloc_1);

41

ilgen.Emit(OpCodes.Ldloc_1);

42

ilgen.Emit(OpCodes.Ldc_I4_0);

43

ilgen.Emit(OpCodes.Ldarg_0);

44

ilgen.Emit(OpCodes.Ldfld, m_Title);

45

ilgen.Emit(OpCodes.Stelem_Ref);

46

ilgen.Emit(OpCodes.Ldloc_1);

47

ilgen.Emit(OpCodes.Ldc_I4_1);

48

ilgen.Emit(OpCodes.Ldstr, ", ");

49

ilgen.Emit(OpCodes.Stelem_Ref);

50

ilgen.Emit(OpCodes.Ldloc_1);

51

ilgen.Emit(OpCodes.Ldc_I4_2);

52

ilgen.Emit(OpCodes.Ldarg_0);

53

ilgen.Emit(OpCodes.Ldfld, m_Author);

54

ilgen.Emit(OpCodes.Stelem_Ref);

55

ilgen.Emit(OpCodes.Ldloc_1);

56

ilgen.Emit(OpCodes.Ldc_I4_3);

57

ilgen.Emit(OpCodes.Ldstr, ", stron: ");

58

ilgen.Emit(OpCodes.Stelem_Ref);

59

ilgen.Emit(OpCodes.Ldloc_1);

60

ilgen.Emit(OpCodes.Ldc_I4_4);

61

ilgen.Emit(OpCodes.Ldarg_0);

62

ilgen.Emit(OpCodes.Ldflda, m_pagesCount);

63

ilgen.Emit(OpCodes.Call, typeof(int).GetMethod("ToString", new Type[0]));

64

ilgen.Emit(OpCodes.Stelem_Ref);

65

ilgen.Emit(OpCodes.Ldloc_1);

66

ilgen.Emit(OpCodes.Call, typeof(string).GetMethod("Concat", new Type[]{typeof(string[])}));

67

ilgen.Emit(OpCodes.Ret);

 

Na początku powyższego kody deklarujemy lokalne zmienne (36, 37), do których odwołujemy się za pomocą instrukcji ldloc (wstawianie na stos zmiennej – 41, 46, 50, 55, 59, 65) i stloc (pobieranie danej ze stosu do zmiennej – 40).

Zakończyliśmy tworzenie metod, jedyne co nam jeszcze pozostało, to stworzenie kodu konstruktora. Jak wiemy, chcemy by wygenerowany przez nas kod odpowiadał następującemu:

 

Public Book(int _pagesCount)

{    

      try

      {

            Pages = _pagesCount;

      }

      catch (ArgumentException ex)

      {

            throw new ArgumentException("Nie mozna stworzyc obiektu. Liczba stron nie moze być mniejsza od 1");

      }

}

 

Będziemy mieli zatem okazję dowiedzieć się, jak za pomocą klasy ILGenerator generować bloki obsługi wyjątków.

 

68

ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof(int) });

69

ilgen = constructorBuilder.GetILGenerator();

70

Label afterTry = ilgen.DefineLabel();

71

ilgen.DeclareLocal(typeof(System.ArgumentException));

72

ilgen.Emit(OpCodes.Ldarg_0);

73

ilgen.Emit(OpCodes.Ldc_I4_0);

74

ilgen.Emit(OpCodes.Stfld, m_pagesCount);

75

ilgen.Emit(OpCodes.Ldarg_0);

76

ilgen.Emit(OpCodes.Ldstr, "");

77

ilgen.Emit(OpCodes.Stfld, m_Title);

78

ilgen.Emit(OpCodes.Ldarg_0);

79

ilgen.Emit(OpCodes.Ldstr, "");

80

ilgen.Emit(OpCodes.Stfld, m_Author);

81

ilgen.Emit(OpCodes.Ldarg_0);

82

ilgen.Emit(OpCodes.Call, typeof(System.Object).GetConstructor(new Type[0]));

83

ilgen.BeginExceptionBlock();

84

ilgen.Emit(OpCodes.Ldarg_0);

85

ilgen.Emit(OpCodes.Ldarg_1);

86

ilgen.Emit(OpCodes.Call, set_Pages);

87

ilgen.Emit(OpCodes.Leave_S, afterTry);

88

ilgen.BeginCatchBlock(typeof(System.ArgumentException));

89

ilgen.Emit(OpCodes.Stloc_0);

90

ilgen.Emit(OpCodes.Ldstr, "Nie mozna stworzyc obiektu. Liczba stron nie moze byc mniejsza od 1");

91

ilgen.Emit(OpCodes.Newobj, typeof(System.ArgumentException).GetConstructor(new Type[]{typeof(string)}));

92

ilgen.Emit(OpCodes.Throw);

93

ilgen.EndExceptionBlock();

94

ilgen.MarkLabel(afterTry);

95

ilgen.Emit(OpCodes.Ret);

 

Powyższy kod można streścić następująco: po zadeklarowaniu zmiennej lokalnej (71) inicjalizujemy składowe obiektu domyślnymi wartościami (od 72 do 80), następnie wywołujemy konstruktor klasy nadrzędnej (82), po czym następuje interesujący nas blok obsługi wyjątku (83 do 93). Bloki te tworzy się za pomocą następujących metod:

- BeginExceptionBlock (83) - odpowiadającej rozpoczęciu bloku try w kodzie,

- BeginCatchBlock (88) - odpowiadającej zakończeniu bloku try i rozpoczęciu bloku catch, bądź zakończeniu jednego bloku catch i rozpoczęciu następnego,

- BeginFinallyBlock - odpowiadającej za zakończenie bloków catch oraz rozpoczęcie bloku finally,

- EndExceptionBlock (93) - odpowiadającej za zakończenie bloku obsługi wyjątku.

 

Zakończyliśmy implementację generowanych metod, pozostaje nam jeszcze wykonać kilka operacji wykańczających. Oto one:

 

96

prop_Pages.SetSetMethod(set_Pages);

97

prop_Pages.SetGetMethod(get_Pages);

98

Type typ = bookTypeBuilder.CreateType();

99

bookAssemblyBuilder.Save(filename);

           

Musieliśmy stworzonej przez nas właściwości Pages przypisać metody set_Pages(96) i get_Pages(97). Następnie dokonujemy dosyć istotnej operacji stworzenia typu za pomocą funkcji CreateType(98). Metoda ta przetwarza wszystie informacje, które wrzuciliśmy do obiektu klasy TypeBuilder i zwraca nam je w gotowej do wykorzystania formie typu. Ostatnią – opcjonalną – czynnością jest zapisanie wygenerowanego assembly do pliku.

            Niniejszym zakończyliśmy cały proces przygotowywania dynamicznego assembly. Pozostaje nam zająć się o wiele przyjemniejszą czynnością, jaką niewątpliwie jest jego wykorzystywanie.

4. Uruchamianie dynamicznie stworzonego kodu.

            W poprzednim rozdziale wygenerowaliśmy dynamicznie klasę Book i zapisaliśmy wynik do pliku „Book.dll”. Plik ten możemy z powodzeniem dodać do dowolnego projektu .NET i korzystać z funkcjonalności klasy Book. Jak to zrobić? W Visual Studio .NET wybieramy Menu a Project a Add Reference... Następnie klikamy przysiskiem Browse i wyszukujemy wygenerowany plik Book.dll. Zatwierdzamy dodanie pliku. Od tej pory możemy korzystać z klasy Book, znajdującej się w namespace Book.

            Jednak nie po to chyba tworzyliśmy dynamicznie kod, żeby teraz go „niedynamicznie” wykonywać. Do dynamicznego uruchamiania kodu posłużymy się wspomnianym wcześniej mechanizmem refleksji. Spójrzmy na poniższy kod:

 

100

System.Console.WriteLine("Creating instance of Book class.");

101

object bookInstance = Activator.CreateInstance(typ, new object[]{1});

102

System.Console.WriteLine("Created. \nSetting author to \"Terry Pratchett\"");

103

typ.InvokeMember("Author", BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetField, null, bookInstance, new Object[] {"Terry Pratchett"});

104

System.Console.WriteLine("Set. \nSetting title to \"The Colour of Magic\"");

105

typ.InvokeMember("Title", BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetField,      null, bookInstance, new Object[] {"The Colour of Magic"});

106

System.Console.WriteLine("Set. \nCalling ToString() function.");

107

System.Console.WriteLine("Returned string: " + (string)(typ.InvokeMember("ToString", BindingFlags.InvokeMethod, null, bookInstance, null)));

108

System.Console.WriteLine("Setting the Pages property to 245.");

109

typ.InvokeMember("Pages", BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetProperty, null, bookInstance, new Object[] {245});

110

System.Console.WriteLine("Set. \nCalling ToString() function.");

111

System.Console.WriteLine("Returned string: " + (string)(typ.InvokeMember("ToString", BindingFlags.InvokeMethod, null, bookInstance, null)));

 

W pierwszej kolejności tworzymy tu instancję wygenerowanego wcześniej typu korzystając z klasy Activator(101). W kolejnych liniach kodu, wykonujemy różnorakie operacje na utworzonym obiekcie korzystając z mechanizmu refleksji dla utworzonego przez nas typu. Operacji tych dokonujemy wykorzystując metody InvokeMember. I tak najpierw ustawiamy wartość publicznej składowej Author(103), następnie ustawiamy wartość publicznej składowej Title(105). Po ustawieniu tych składowych wyświetlamy na konsoli napis zwrócony przez metodę ToString() (107). Potem, korzystając z właściwości Pages, ustawiamy liczbę stron naszej książki(109) i znów wyświetlamy stan obiektu korzystając z metody ToString() (111). Nie są to oczywiście wszystkie możliwości metody InvokeMember. Zainteresowanym szczegółowymi możliwościami tej motody (a są one duże) polecam lekturę MSDN’a.

 

Wykonanie tego kodu zaskutkuje następującymi komunikatami na standardowym wyjściu:

 

Creating instance of Book class.

Created.

Setting author to "Terry Pratchett"

Set.

Setting title to "The Colour of Magic"

Set.

Calling ToString() function.

Returned string: The Colour of Magic, Terry Pratchett, stron: 1

Setting the Pages property to 245.

Set.

Calling ToString() function.

Returned string: The Colour of Magic, Terry Pratchett, stron: 245

 

5. Zastosowania

            Przejdźmy do zastosowań. Po głębszym przemyśleniu sprawy, przeprowadzeniu kilku eksperymentów i spojrzeniu na sprawę z szerszej perspektywy można dojść do wniosku, że praktyczne zastosowania przedstawionej w tym artykule technologii są niestety bardzo ograniczone. Postaram się w miarę skrótowo objaśnić dlaczego.

Pierwszym i zupełnie naturalnie nasuwającym się obszarem zastosowań jest optymalizacja końcowa kodu. Wyobraźmy sobie sytuację (przykład typowo akademicki), że zadajemy aplikacji wielomian n-tego stopnia z jednym parametrem, a następnie 100’000’000 razy obliczamy wartość tego wielomianu dla różnych parametrów. Aż chciałoby się wygenerować gotowy, „sztywny” wzór funkcji dla tego wielomianu, która wyeliminowałaby odwołania do zmiennych zawierających współczynniki wielomianu, przez co byłaby szybsza i bardziej wydajna. Niestety, wywołując taką funkcję zmuszeni jesteśmy do korzystania z mechanizmu refleksji, który do czasu wykonania daje trochę „od siebie”. Wyniki przeprowadzonych testów przedstawia tabelka poniżej:

 

Funkcja nieoptymalizowana                             00 min. 04sek 750 ms

Funkcja zoptymalizowana                                00 min. 04sek 375 ms

Funkcja zoptymalizowana wywoływana           17 min. 20sek 250 ms

za pomocą mechanizmu refleksji

 

Test przeprowadzono dla wielomianu 4 stopnia i 100mln. wywołań. Jak widać spowolnienie wynikłe z korzystania z mechanizmu refleksji jest o kilka rzędów wielkości większe niż zysk w szybkości kodu. Przeskoczenie narzutu mechanizmu refleksji byłoby możliwe tylko w sytuacji gdy optymalizowana funkcja jest bardziej złożona obliczeniowo i nie jest wywoływana bardzo często lub gdy wygenerowane assembly jest zapisane do pliku i  statycznie dołączone do inego projektu – wtedy mamy możliwość wywołać optymalizowaną funkcję bez pośrednictwa mechanizmu refleksji. Oba przypadki są rzadkie a ponadto w pierwszym pojawia się pytanie o sensowność tworzenie kosztownego, podatnego na błędy, trudnego w debugowaniu i konserwacji kodu.

            Jedynym naprawdę ciekawym i przydatnym przykładem zastosowania Reflection Emit, jaki udało mi się znaleźć, jest pomysł uniwersalnego cache’a dla metod zawartych w assemblies. Idea polega na doklejeniu do wybranych metod w danym assemblies kodu, który zapamiętuje wartości zwracane przez metodę dla danego zestawu parametrów, oraz sprawdza, czy zestaw parametrów podany przy wywołaniu metody nie pojawił się w zadanym ostatnim przedziale czasu. W przypadku, gdy taki zestaw parametrów był i nie zdeaktualizował się, doklejony dynamicznie kod nie wywołuje opakowywanej metody tylko zwraca zachowaną w cache wartość. Cała treść tego świetnego artykułu dostępna jest pod http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndotnet/html/rflemitcache.asp.

            Żadna technologia – nawet najbardziej zaawansowana - nie jest warta czasu poświęconego na jej opanowanie, jeśli nie ma dostatecznie dużego obszaru zastosowań. W przypadku możliwości oferowanych przez System.Reflection.Emit pytanie „Czy warto?” pozostaje otwarte.

6. Garść porad 

   

6.1 Uwaga na wyjątki przechwytywane przez mechanizm refleksji!

Wróćmy na chwilę do wygenerowanej wcześniej klasy Book. Jak pamiętamy, jeśli ustawimy wartość property na wartość mniejszą od 1, zostanie rzucony wyjątek ArgumentException. W oparciu o tą wiedzę, sytuację wyjątkową obsłużylibyśmy zapewne w następujący sposób:

 

Try

{

      typ.InvokeMember("Pages",

      BindingFlags.DeclaredOnly |

      BindingFlags.Public | BindingFlags.NonPublic |

BindingFlags.Instance | BindingFlags.SetProperty,

      null, bookInstance, new Object[] {-25});

}

catch (ArgumentException ex)

{

      System.Console.WriteLine("Error: " + ex.Message);

}

 

Jak widzimy, powyższy kod to próba ustawienia wartości właściwości Pages na -25, co spowoduje rzucenie wyjątku ArgumentException. My się jednak tego nie boimy, gdyż łapiemy ten wyjątek. Niestety możemy się mocno zdziwić, gdyż najprawdopodobniej w tym miejscu aplikacja zakończy swe działanie z powodu nieprzechwyconego wyjątku. Dlaczego? Otóż podczas wywoływania wszelkich metod za pośrednictwem mechanizmu refleksji, każdy rzucany wyjątek jest przechwytywany i opakowywany w wyjątek klasy TargetInvocationException. Dostęp do pierwotnie rzuconego wyjątku oferuje nam właściwość InnerException. Prawidłowa obsługa powyżej przedstawionego, ryzykownego fragmentu kodu powinna wyglądać więc następująco:

 

Try

{

      typ.InvokeMember("Pages",

      BindingFlags.DeclaredOnly |

      BindingFlags.Public | BindingFlags.NonPublic |

BindingFlags.Instance | BindingFlags.SetProperty,

      null, bookInstance, new Object[] {-25});

}

catch (TargetInvocationException ex)

{

      System.Console.WriteLine("Error: " + ex.InnerException.Message);

}

 

            Mała dygresja przy okazji tego punktu: wyjątki  w .NET czasem potrafią boleśnie zaskoczyć, czego powyższy przykład jest dowodem. Myślę, że warto w tym miejscu polecić dostępny w portalu CodeGuru, bardzo dobry artykuł „Wyjątki – nie daj się przechytrzyć” autorstwa Michała Głomby.

6.2 Pages == set_Pages + get_Pages

Po zaimportowaniu do projektu pliku DLL z wygenerowanym assembly, nie jest nam dane korzystać z wygenerowanych właściwości w sposób, do jakiego przyzwyczaiło nas normalne, codzienne implementowanie. Próba odwołania się do właściwości w tradycyjny sposób skutkuje błędem kompilacji numer CS1545 o następującym opisie:

Property, indexer, or event 'Pages' is not supported by the language; try directly calling accessor methods Book.Book.get_Pages()' or 'Book.Book.set_Pages(int)'

 

Zamiast konstrukcji typu:

 

Book.Book book = new Book.Book(3);

book.Pages = 100;

int k = book.Pages;

 

skazani jesteśmy więc na konstrukcje:

 

Book.Book book = new Book.Book(3)

book.set_Pages(100);

int k = book.get_Pages();

 

Dziwne, ale prawdziwe…

6.3 Jak zaprzyjaźnić się z klasami ILGenerator i OpCodes

 

Zamiast myślec godzinami jak wygenerować ciało jakieś metody lub konstruktora, podpatrz jak robi to kompilator. Napisz funkcję, którą chcesz wygenerować używając C# lub VB.NET, skompiluj ją a następnie kod wynikowy przekształć do zestawu instrukcji MSIL za pomocą dostępnego w SDK .NET programu Ildasm.exe. Tak wygenereowany kod bardzo łatwo jest przetłumaczyć sobie na zestaw kodów klasy OpCodes. Szybko, przyjemnie i bezboleśnie.

7. Kilka przydatnych klas i metod.

 

Poniższe opisy dotyczą tylko wybranych – wykorzystywanych w przykładzie – klas i metod.

7.1 Klasa Type

Opis: służy to pobierania informacji o typie i manipulowaniem danymi obiektu będącego instancją danego typu.

Przydatne funkcje:

public object InvokeMember(string memberName, BindingFlags flags, Binder binder, object instance, object[] invocationArguments)

– służy do przeprowadzania operacji na typie danych lub obiekcie będącym instancją danego typu.

7.2 Klasa AssemblyBuilder

Opis: Służy do konstruowania dynamicznych assemblies.

Przydatne funkcje:

public ModuleBuilder DefineDynamicModule(string name, string fileName); - służy do zdefiniowania nowego modułu w dynamicznym assembly.

7.3 Klasa ModuleBuilder

Opis: Służy do konstruowania modułów składowych dynamicznych assemblies

Przydatne funkcje:

public TypeBuilder DefineType(string name, TypeAttributes attr); - służy do zdefiniowania nowego typu w obrębie danego modułu.

7.4 Klasa TypeBuilder

Opis: Służy do konstrukcji dynamicznie tworzonego typu.

Przydatne funkcje:

public ConstructorBuilder DefineConstructor(MethodAttributes attributes,    CallingConventions callingConvention, Type[] parameterTypes);

– służy do definiowania konstruktora dla danego typu

 

public FieldBuilder DefineField(string fieldName, Type type, FieldAttributes attributes);

- służy do definiowania pola dla danego typu

 

public MethodBuilder DefineMethod(string name, MethodAttributes attributes, Type returnType, Type[] parameterTypes);

- służy do definiowania metody dla danego typu

 

public PropertyBuilder DefineProperty(string name, PropertyAttributes attributes, Type returnType, Type[] parameterTypes);

- służy do definiowania właściwości dla danego typu

7.5 Klasa ConstructorBuilder

Opis: Sluży do definiowania dynamicznie tworzonego konstruktora

7.6 Klasa FieldBuilder

Opis: Służy do definiowania dynamicznie tworzonego pola

7.7 Klasa MethodBuilder

Opis: Służy do definiowania dynamicznie tworzonej metody

7.8 Klasa PropertyBuilder

Opis: Służy do definiowania dynamicznie tworzonej właściwości

7.9 Klasa ILGenerator

Opis: Służy do umieszczania kodu w ciele metody lub konstruktora

Przydatne funkcje:

public virtual void Emit(OpCode opcode [, ...]);

- służy do wrzucania kolejnej instrukcji do generowanego ciągu instrukcji,

7.10 Klasa OPCodes

Opis: Klasa zawierająca kody instrukcji MSIL

7.11 Struktura Label

Opis: Służy do zaznaczania w kodzie etykiet, wykorzystywanych w skokach.

8. Źródła

 

1. MSDN – System.Reflection.Emit

2. MSDN - Reflection Emit Application Scenarios,

3. „Using Reflection Emit to Cache .NET Assemblies”, Simon Guest,

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndotnet/html/rflemitcache.asp.

4. „Elementarz MSIL”, Maurycy Prodeus,

http://www.codeguru.pl/Default.aspx?Page=Articles/Details&pubid=276

5. „Wyjątki – nie daj się przechytrzyć”, Michał Głomba, http://codeguru.pl/Default.aspx?Page=Articles/Details&pubid=292

Załączniki:

Podobne artykuły

Komentarze 2

User 131335
User 131335
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Bardzo ciekawy artykuł o bardzo ciekawej funkcjonalności System.Reflection.Emit, napisany przyjemnym, przyjaznym językiem, a przecież generowanie w locie kodu na poziomie MSIL to potężne narzędzie. Cieszy także niezmiernie dyskusja nad czasami wykonania fragmentów kodu wykorzystujących refleksję. Takie interesujące wprowadzenie do System.Reflection.Emit z całą pewnością skłania do szerszych, samodzielnych poszukiwań. Well done!
jedrekwie
jedrekwie
5 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Artykul kompletny. Autor omawia dokladnie to, co sobie na poczatku zalozyl. Dla mnie - 7pkt-ow.
Mimo wszystko w ogole nie "budzi emocji" - bardzo dobre informaczyczne rzemioslo.
pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.