Wyszukiwanie plug-inów z szacunkiem dla pamięci

Opublikowane 27 sierpnia 08 09:43 | chaniewski 

Bardzo często, gdy tworzymy nietrywialną aplikację, pojawia się konieczność rozszerzania jej za pomocą plug-inów, czy też mówiąc po naszemu po prostu wtyczek. W tej notce nie będę skupiał się na tym jak tworzyć mechanizmy rozszerzalności czy też jak zaprojektować architekturę aplikacji, by udostępniała taką funkcjonalność. Zamiast tego, założę że te fragmenty mamy już gotowe i chcemy po prostu wyszukać na dysku pliki (assemblies) zawierające wtyczki - a także wyciągnąć z tych wtyczek pewne informacje.

Temat w zasadzie nie jest skomplikowany - Assembly.Load(), do tego trochę refleksji, i już. Co się jednak dzieje, jeżeli na dysku mamy więcej assemblies? Co, jeżeli jedynie niektóre wtyczki używamy - bo aplikacja pozwala w innym miejscu je wyłączyć, albo steruje ich użyciem innego rodzaju logika?

Zwykłe Assembly.Load() jest nieodwołalne. Kod załadowany do bieżącej domeny aplikacji pozostanie tam do czasu, gdy zamkniemy aplikację. Jeżeli przeglądaliśmy w poszukiwaniu wtyczek wiele bibliotek, to może się okazać, że w pamięci trzymamy znacznie więcej śmiecia niż jest to nam konieczne.

W poprzednim akapicie kluczowym fragmentem było "załadowany do bieżącej domeny aplikacji". Rozwiązaniem naszego problemu jest utworzenie domeny tymczasowej, wywołanie w niej kodu wykrywającego pluginy, a następnie usunięcie całej tej domeny z pamięci. Ale niestety, życie nie jest tak proste jak mogłoby się wydawać...

Pierwsze podejście

   1:  AppDomain temporary = AppDomain.CreateDomain("TEMPORARY");
   2:  Assembly a = temporary.Load("testowane_assembly.dll");
   3:   
   4:  // wyszukanie pluginów wewnątrz załadowanego assembly
   5:  // zapewne korzystające z a.GetExportedTypes()
   6:   
   7:  AppDomain.Unload(temporary);

Niestety, powyższy kod, choć na pierwszy rzut oka mógłby wydawać się poprawny, nie zadziała jak trzeba. Owszem, załaduje assembly z dysku. Owszem, wyszuka w nim wtyczki. Owszem, usunie tymczasową domenę aplikacji... ale wywołanie AppDomain.CurrentDomain.GetAssemblies() pokaże, że w bieżącej, głównej domenie naszej aplikacji, ciągle tkwi załadowane assembly które przeszukiwaliśmy... i którego się w tym miejscu w żaden sposób nie spodziewaliśmy.

Dlaczego tak się dzieje? Co sprowadziło nasze assembly na złą drogę?

Dzieje się tak dlatego, że klasa Assembly nie dziedziczy z MarshalByRefObject. W praktyce oznacza to, że efekt wywołania metody temporary.Load(...), czyli właśnie obiekt klasy Assembly, nie może być używany "zdalnie" (marshalowany? jak to po polsku się mówi?) pomiędzy dwiema domenami aplikacji. W rezultacie, nasza biblioteka zawierająca (lub nie) wtyczkę jest ładowana również do podstawowej domeny aplikacji. A to pech.

Podejście drugie, właściwe

Cóż, jeżeli jedynym problemem powodującym wyciekanie assembly do naszej podstawowej domeny aplikacji jest fakt, że klasa Assembly nie dziedziczy z MarshalByRefObject... to zróbmy sobie przejściówkę, która się może takim przodkiem poszczycić. I niech ta przejściówka po cichu zrobi co ma zrobić, tak by podstawowa domena aplikacji o tym nie wiedziała, a potem zwróci jedynie rezultat.

Spróbujmy zatem w ten sposób.

Zróbmy sobie malutki dodatkowy projekcik, niech nazywa się on Proxy, zawierający klasę:

   1:  public class Gateway : MarshalByRefObject
   2:  {
   3:      public RezultatSzukania LoadAndMatch(string fileName)
   4:      {
   5:          try
   6:          {
   7:              // załadowanie assembly
   8:              Assembly pluginAssembly = Assembly.LoadFile(fileName);
   9:   
  10:              // wyszukanie wtyczek...
  11:              foreach (Type type in pluginAssembly.GetExportedTypes())
  12:              {
  13:                  // ...
  14:              }
  15:          }
  16:          catch
  17:          {
  18:              // ... tu paskudnie połykam wyjątki :p
  19:          }
  20:   
  21:          return rezultatSzukania;
  22:      }
  23:  }

Pozostaje teraz tylko skorzystać z gotowej przelotki:

   1:  AppDomain temporary = AppDomain.CreateDomain("TEMPORARY");
   2:  try
   3:  {
   4:      Gateway proxy =
   5:                      temporary.CreateInstanceAndUnwrap("Proxy", "Proxy.Gateway") as
   6:                      Gateway;
   7:      RezultatSzukania rezultat = proxy.LoadAndMatch("testowane_assembly.dll");
   8:  }
   9:  finally
  10:  {
  11:      AppDomain.Unload(temporary);
  12:  }

Co tu się dzieje? No cóż, to dosyć proste. Po utworzeniu tymczasowej domeny aplikacji, używamy na tej domenie metody CreateInstanceAndUnwrap(), która ładuje assembly Proxy.dll do tymczasowej domeny, tworzy obiekt klasy Gateway - również w tej domenie, a następnie tworzy proxy do tego obiektu w bieżącej domenie. Zupełnie jak w remotingu - aby to sobie unaocznić, najlepiej na chwilę sobie wyobrazić, że tymczasowa domena powstała na innym komputerze, w innej galaktyce... więc używamy lokalnego obiektu proxy reprezentującego kod działający zdalnie. Gdy wywołamy metodę LoadAndMatch na lokalnym obiekcie proxy, tak naprawdę jej argumenty będą przesłane (przemarshalowane, blah) do metody na obiekcie w tymczasowej domenie - a rezultat zostanie przesłany z powrotem do proxy i do głównej domeny aplikacji.

Kilka uwag:

  • RezultatSzukania musi być serializowalny (jest to konieczne, aby można było przekazać go między domenami aplikacji).
  • Definicję tej klasy należy umieścić ponadto w takim miejscu, aby była widoczna zarówno dla obiektu Gateway w assembly Proxy, jak i dla naszej głównej aplikacji.
  • Assembly Proxy przecieka do naszej głównej domeny aplikacji. Jeżeli chce się tego uniknąć, to trzeba wyciągnąć z obiektu Gateway interfejs (np. IGateway), umieścić go w miejscu widocznym zarówno dla assembly Proxy jak i głównej aplikacji, usunąć referencję do assembly Proxy z aplikacji - a przy wywołaniu CreateInstanceAndUnwrap() rezultat rzutować na IGateway zamiast na Gateway.
Filed under:

Powiadamianie o komentarzach

Jeżeli chciałbyś otrzymywać email gdy ta wypowiedź zostanie zaktualizowana, to zarejestruj się tutaj

Subskrybuj komentarze za pomocą RSS

Komentarze:

# chaniewski said on sierpnia 28, 2008 00:40:

heh, wygląda na to że powtórzyłem artykuł dario-g...

http://zine.net.pl/blogs/dario-g/archive/2007/05/09/r-czne-adowanie-assembly.aspx

Przepraszam Dario, nie widziałem go wcześniej i nie miałem pojęcia...

# dario-g said on sierpnia 28, 2008 08:58:

spoko :) Dałeś więcej przykładowego kodu. Może Michał weźmie, zmiksuje i wrzuci do zina ;)

# Bysza said on sierpnia 29, 2008 10:11:

.Net 3.5 (3.0?) wspiera bodajże tworzenie addinów (wtyczek) natywnie, bodajże w namespaceach System.AddIn i pokrewnych.

Czy nie lepiej byłoby zastosować gotową funkcjonalność, o ile się oczywiście ma odpowiednią wersję frameworka?

Macie jakieś za i przeciw?

Chętnie poslucham, bo jak dotąd pisałem chyba tylko trywialne aplikacje ;D

Co o tym myślisz?

(wymagane) 
(opcjonalne)
(wymagane) 

  
Wprowadź kod: (wymagane)