2011-01-10 9 views
6

Kontekst: Przekształciłem starszy autonomiczny silnik w komponent wtyczki dla narzędzia kompozycji. Z technicznego punktu widzenia oznacza to, że skompilowałem bazę kodu silnika do biblioteki DLL C, którą wywołuję z opakowania .NET za pomocą P/Invoke; wrapper implementuje interfejs zdefiniowany przez narzędzie do komponowania. Działa to całkiem dobrze, ale teraz otrzymuję żądanie załadowania wielu instancji silnika dla różnych projektów. Ponieważ silnik przechowuje dane projektu w zbiorze zmiennych globalnych, a ponieważ biblioteka DLL z podstawą kodu silnika jest ładowana tylko raz, ładowanie wielu projektów oznacza, że ​​dane projektu są nadpisywane.Obsługa wielu wystąpień biblioteki DLL wtyczki z danymi globalnymi

widzę szereg rozwiązań, ale wszystkie one mają pewne wady:

  1. Można utworzyć wiele bibliotek DLL z tego samego kodu, które są postrzegane jako różne biblioteki DLL w systemie Windows, więc ich kod nie jest udostępniony. Prawdopodobnie działa to, jeśli masz wiele kopii DLL silnika o różnych nazwach. Jednak silnik jest wywoływany z opakowania przy użyciu atrybutów DllImport i myślę, że nazwa silnika DLL musi być znana podczas kompilowania opakowania. Oczywiście, jeśli muszę skompilować różne wersje opakowania dla każdego projektu, jest to dość kłopotliwe.

  2. Silnik może działać jako oddzielny proces. Oznacza to, że owijka uruchomiłaby oddzielny proces dla silnika podczas ładowania projektu i użyłaby jakiejś formy IPC do komunikacji z tym procesem. Chociaż jest to stosunkowo czyste rozwiązanie, to wymaga trochę wysiłku, aby uzyskać pracę, nie wiem, która technologia IPC byłaby najlepsza do stworzenia tego rodzaju konstrukcji. Może również występować znaczny narzut komunikacji: silnik musi często wymieniać tablice liczb zmiennoprzecinkowych.

  3. Silnik można dostosować do obsługi wielu projektów. Oznacza to, że zmienne globalne powinny zostać umieszczone w strukturze projektu, a każde odniesienie do globali powinno zostać przekonwertowane na odpowiadające odniesienie, które odnosi się do konkretnego projektu. Istnieje około 20-30 zmiennych globalnych, ale jak można sobie wyobrazić, te zmienne globalne są przywoływane z całej bazy kodu, więc ta konwersja musiałaby zostać wykonana w sposób automatyczny. Powiązanym problemem jest to, że powinieneś być w stanie odnieść się do "aktualnej" struktury projektu we wszystkich miejscach, ale przekazanie tego jako dodatkowego argumentu w każdym podpisie funkcji jest również uciążliwe. Czy istnieje technika (w języku C) do rozważenia bieżącego stosu wywołań i znalezienia najbliższej instancji zawierającej odpowiednią wartość danych?

Czy społeczność stackoverflow może udzielić porady na temat tych (lub innych) rozwiązań?

+0

Czy nie można zmodyfikować biblioteki DLL silnika, aby miała fabryczną metodę zwracania stanu "globalnego" na żądanie? E.g, podsumować wszystko, co jest obecnie globalne, do obiektu, który można utworzyć, umożliwiając tworzenie wielu silników z pojedynczej biblioteki DLL? –

+5

Jeśli ktoś kiedykolwiek potrzebował powodu, dla którego zmienne globalne są złym pomysłem, skieruj go na to pytanie. – Timbo

+0

@ Moo-Juice: Myślę, że to odpowiadałoby rozwiązanie nr 3? –

Odpowiedz

4

Mimo, że otrzymał wiele odpowiedzi, które sugerowane iść do sporządzania roztworu 3, chociaż zgadzam się, że jest to lepsze rozwiązanie koncepcyjnie, myślę, że nie było sposobu, aby uświadomić sobie, że rozwiązanie praktycznie i niezawodnie pod moimi ograniczeniami.

Zamiast tego, co faktycznie zaimplementowałem, było odmianą rozwiązania nr 1. Chociaż nazwa biblioteki DLL w DLLImport musi być stała czasu kompilacji, this question wyjaśnia, jak to zrobić dynamicznie.

Jeśli mój kod przed wyglądał następująco:

using System.Runtime.InteropServices; 

class DotNetAccess { 
    [DllImport("mylib.dll", EntryPoint="GetVersion")] 
    private static extern int _getVersion(); 

    public int GetVersion() 
    { 
     return _getVersion(); 
     //May include error handling 
    } 
} 

ona teraz wygląda tak:

using System.IO; 
using System.ComponentModel; 
using System.Runtime.InteropServices; 
using Assembly = System.Reflection.Assembly; 

class DotNetAccess: IDisposable { 
    [DllImport("kernel32.dll", EntryPoint="LoadLibrary", SetLastError=true)] 
    private static extern IntPtr _loadLibrary(string name); 
    [DllImport("kernel32.dll", EntryPoint = "FreeLibrary", SetLastError = true)] 
    private static extern bool _freeLibrary(IntPtr hModule); 
    [DllImport("kernel32.dll", EntryPoint="GetProcAddress", CharSet=CharSet.Ansi, ExactSpelling=true, SetLastError=true)] 
    private static extern IntPtr _getProcAddress(IntPtr hModule, string name); 

    private static IntPtr LoadLibrary(string name) 
    { 
     IntPtr dllHandle = _loadLibrary(name); 
     if (dllHandle == IntPtr.Zero) 
      throw new Win32Exception(); 
     return dllHandle; 
    } 

    private static void FreeLibrary(IntPtr hModule) 
    { 
     if (!_freeLibrary(hModule)) 
      throw new Win32Exception(); 
    } 

    private static D GetProcEntryDelegate<D>(IntPtr hModule, string name) 
     where D: class 
    { 
     IntPtr addr = _getProcAddress(hModule, name); 
     if (addr == IntPtr.Zero) 
      throw new Win32Exception(); 
     return Marshal.GetDelegateForFunctionPointer(addr, typeof(D)) as D; 
    } 

    private string dllPath; 
    private IntPtr dllHandle; 

    public DotNetAccess() 
    { 
     string dllDir = Path.GetDirectoryName(Assembly.GetCallingAssembly().Location); 
     string origDllPath = Path.Combine(dllDir, "mylib.dll"); 
     if (!File.Exists(origDllPath)) 
      throw new Exception("MyLib DLL not found"); 

     string myDllPath = Path.Combine(dllDir, String.Format("mylib-{0}.dll", GetHashCode())); 
     File.Copy(origDllPath, myDllPath); 
     dllPath = myDllPath; 

     dllHandle = LoadLibrary(dllPath); 
     _getVersion = GetProcEntryDelegate<_getVersionDelegate>(dllHandle, "GetVersion"); 
    } 

    public void Dispose() 
    { 
     if (dllHandle != IntPtr.Zero) 
     { 
      FreeLibrary(dllHandle); 
      dllHandle = IntPtr.Zero; 
     } 
     if (dllPath != null) 
     { 
      File.Delete(dllPath); 
      dllPath = null; 
     } 
    } 

    private delegate int _getVersionDelegate(); 
    private readonly _getVersionDelegate _getVersion; 

    public int GetVersion() 
    { 
     return _getVersion(); 
     //May include error handling 
    } 

} 

Uff.

Może to wydawać się bardzo skomplikowane, jeśli zobaczysz dwie wersje obok siebie, ale po skonfigurowaniu infrastruktury jest to bardzo systematyczne zmiany. Co ważniejsze, lokalizuje modyfikację mojej warstwy DotNetAccess, co oznacza, że ​​nie muszę wykonywać modyfikacji rozproszonych po bardzo dużej podstawie kodu, która nie należy do mnie.

4

Moim zdaniem, rozwiązanie 3 jest drogą do zrobienia.

Wada polegająca na tym, że musisz dotknąć każdego połączenia z biblioteką DLL, powinna również odnosić się do innych rozwiązań ... bez braku skalowalności i nieprzyjemności podejścia opartego na wielu bibliotekach DLL i niepotrzebnego obciążenia IPC.

+0

Pod względem koncepcyjnym jest to najlepsze rozwiązanie, ponieważ od tego czasu silnik powinien działać od samego początku. Ale czy masz jakieś sugestie, jak automatycznie przekonwertować istniejącą podstawę kodu? –

+1

Chciałbym po prostu zmienić pliki nagłówkowe, skompilować całość, a następnie nacisnąć klawisz F4 (następny błąd do momentu ponownego kompilacji), ale domyślam się, że szukasz czegoś bardziej zautomatyzowanego. Znajdź + zamień na regex może działać, ale ja osobiście nie ufałbym, że chyba że zmiana jest super prosta. – Timbo

+0

tak, to musi być bardziej zautomatyzowane. Nie chodzi o to, że mogę jednorazowo wprowadzić zmiany w silniku i zrobić z tym wszystko: w przyszłości wyda nowe wersje w przyszłości, a potem muszę również zaktualizować moją wersję wtyczki. –

6

Umieścić całą rzeczą cry w klasie C++, a odwołania do zmiennych automatycznie znajdą zmienną instancji.

Możesz wykonać globalny wskaźnik do aktywnej instancji. Prawdopodobnie powinien to być lokalny wątek (patrz __declspec(thread)).

Dodaj extern "C" funkcje opakowania, które przekazują do odpowiedniej funkcji elementu na aktywnej instancji. Udostępnia funkcje do tworzenia nowej instancji, usuwania istniejącej instancji i ustawiania aktywnej instancji.

OpenGL używa tego paradygmatu do świetnego efektu (patrz wglMakeCurrent), znajdując jego dane stanu bez konieczności przekazywania wskaźnika stanu do każdej funkcji.

+0

+1. to brzmi jak rozwiązanie. oczywiście rozmiar projektu powinien być wystarczająco duży, w przeciwnym przypadku dodanie prostego obsługi wielu instancji do silnika może być prostsze (i lepsze w dłuższej perspektywie). –

+0

Niektóre problemy z tym rozwiązaniem: jak mogę umieścić rzeczy z kilku plików (jednostek kompilacji) w jednej klasie C++? Ponadto członkowie C++ czasami mają znacznie inną semantykę niż C: funkcje statyczne. –

+0

@Bruno: Będziesz musiał # dołączyć wszystko do jednej jednostki kompilacji. A jeśli masz statycznych członków o nieunikalnych nazwach, będziesz musiał je najpierw naprawić. –

1

Rozwiązanie 3 To jest właściwa droga.

Wyobraź sobie, że obecne języki programowania obiektowego działają podobnie do trzeciego rozwiązania, ale tylko pośrednio przekazują wskaźnik do struktury zawierającej dane "to".

Przekazywanie "czegoś w rodzaju kontekstu" nie jest kłopotliwe, ale tak właśnie działa! ;-)

0

W rzeczywistości rozwiązanie 3 jest łatwiejsze niż się wydaje. Wszystkie inne rozwiązania są rodzajem poprawki i z czasem zerwą.

  • Utwórz klasę .net, która będzie hermetyzować dostęp do dotychczasowego kodu. Spraw, aby był możliwy do zidentyfikowania.
  • Zmień wszystkie zmienne globalne, aby rezydowały w klasie o nazwie "Kontekst"
  • Niech wszystkie interfejsy C++ pobierają obiekt kontekstu i przekazują go jako pierwszy argument. Jest to prawdopodobnie najdłuższy etap i możesz tego uniknąć używając metody "thread-local-storage" sugerowanej przez kogoś innego, ale głosowałbym przeciwko temu rozwiązaniu: jeśli twoja biblioteka ma działające wątki, które uruchamia, "wątek-lokalny" "rozwiązanie" zostanie przerwane. Wystarczy dodać obiekt kontekstu tam, gdzie jest potrzebny.
  • Użyj obiektu kontekstowego, aby uzyskać dostęp do wszystkich danych globalnych.
  • Utworzono obiekt kontekstu z .net ctor (przez p/wywołanie nowej funkcji create_context) i usunięto go przy użyciu metody .net Dispose().

Ciesz się.

+0

Problem jest zdecydowanie krok nr 3: Zmienne globalne są używane w całej bazie kodu; Musiałbym zmienić sygnaturę niemal każdej funkcji w bazie kodu, aby uwzględnić obiekt/strukturę kontekstu jako pierwszy argument. To ogromny wysiłek. –

+0

Jeśli kod nie jest związany z wątkiem, przejdź do rozwiązania "thread-local-storage".Ale to rozwiązanie nie zadziała, jeśli twoja biblioteka tworzy na przykład wewnętrzne wątki robocze. –

+0

Być może źle zrozumiałeś moją sugestię dotyczącą lokalnego przechowywania wątków. Nie sugerowałem, aby wszystkie dane były wątkami lokalnymi, ale pojedynczym wskaźnikiem kontekstu (który zapisuje przekazanie go jako argumentu). Wiele wątków może nadal wskazywać ten sam kontekst, a przekazywanie parametrów może być wykorzystywane np. wywołania zwrotne, które muszą działać na obiekcie innym niż "bieżący". –

1

Użyj podejścia nr 3. Ponieważ kod znajduje się w C, łatwym sposobem radzenia sobie z rozproszonymi wszędzie globami jest zdefiniowanie makra dla każdego globalnego, który ma taką samą nazwę jak zmienna globalna, ale makro rozwija się do czegoś takiego jak getSession() -> theGlobal gdzie getSession() zwraca pinter do struktury "specyficznej dla sesji", która przechowuje wszystkie dane dla twoich globali. getSession() może w jakiś sposób łowić odpowiednią strukturę danych z globalnej mapy struktur danych, być może przy użyciu lokalnego magazynu wątków lub na podstawie identyfikatora procesu itp.

0

Kilka uwag na temat sugerowanego rozwiązania # 2 (i trochę na temat Nr 1 i nr 3).

  1. Pewna warstwa IPC może wprowadzić opóźnienie. To zależy od rzeczywistego silnika, jaki jest zły. Jeśli silnik jest silnikiem renderującym i jest wywoływany, powiedzmy, 60 razy na sekundę, obciążenie może być zbyt duże. Ale jeśli nie, nazwany potok może być wystarczająco szybki i łatwy do utworzenia za pomocą WCF.
  2. Czy jesteś całkowicie pewien, że będziesz potrzebował DOKŁADNIE tego samego silnika wiele razy lub czy istnieje ryzyko zmiany wymagań, które mogą prowadzić do scenariusza, który zmusza do ładowania wielu wersji jednocześnie? Jeśli tak, opcja nr 2 może być lepszym rozwiązaniem niż opcja nr 3, ponieważ pozwoliłoby to na łatwiejsze.
  3. Jeśli warstwa IPC nie spowalnia rzeczy zbyt mocno, architektura ta może pozwolić na dystrybucję silników na innych komputerach. Może to umożliwić użycie większego sprzętu, niż wcześniej zaplanowano. Można nawet myśleć o hostowaniu silnika w chmurze Azure.
Powiązane problemy