Поддержка нескольких экземпляров плагина DLL с глобальными данными - PullRequest
6 голосов
/ 10 января 2011

Контекст: я преобразовал устаревший автономный движок в компонент плагина для инструмента композиции. Технически это означает, что я скомпилировал кодовую базу движка в C DLL, которую я вызываю из оболочки .NET, используя P / Invoke; Оболочка реализует интерфейс, определенный инструментом композиции. Это работает довольно хорошо, но теперь я получаю запрос на загрузку нескольких экземпляров движка для разных проектов. Поскольку движок хранит данные проекта в виде набора глобальных переменных, а поскольку DLL с базой кода движка загружается только один раз, загрузка нескольких проектов означает, что данные проекта перезаписываются.

Я вижу несколько решений, но все они имеют некоторые недостатки:

  1. Вы можете создать несколько библиотек DLL с одним и тем же кодом, которые Windows воспринимают как разные библиотеки DLL, поэтому их код не является общим. Вероятно, это уже работает, если у вас есть несколько копий DLL движка с разными именами. Однако механизм вызывается из оболочки с использованием атрибутов DllImport, и я думаю, что имя библиотеки DLL движка необходимо знать при компиляции оболочки. Очевидно, что если мне придется компилировать разные версии обертки для каждого проекта, это довольно громоздко.

  2. Двигатель может работать как отдельный процесс. Это означает, что оболочка будет запускать отдельный процесс для движка при загрузке проекта, и она будет использовать некоторую форму IPC для связи с этим процессом. Хотя это относительно чистое решение, оно требует определенных усилий для работы, но сейчас я не знаю, какая технология IPC лучше всего подходит для создания такого рода конструкции. Также может быть значительная нагрузка на связь: движку необходимо часто обмениваться массивами чисел с плавающей запятой.

  3. Движок может быть адаптирован для поддержки нескольких проектов. Это означает, что глобальные переменные должны быть помещены в структуру проекта, а каждая ссылка на глобальные переменные должна быть преобразована в соответствующую ссылку, относящуюся к конкретному проекту. Существует около 20-30 глобальных переменных, но, как вы можете себе представить, на эти глобальные переменные ссылаются со всей базы кода, поэтому это преобразование должно быть выполнено каким-то автоматическим способом. Связанная проблема заключается в том, что вы должны иметь возможность ссылаться на «текущую» структуру проекта во всех местах, но передавать ее как дополнительный аргумент в каждой сигнатуре каждой функции также громоздко. Существует ли методика (в C) для рассмотрения текущего стека вызовов и поиска там ближайшего включающего экземпляра соответствующего значения данных?

Может ли сообщество stackoverflow дать несколько советов по поводу этих (или других) решений?

Ответы [ 7 ]

6 голосов
/ 10 января 2011

Поместите всю чертову вещь в класс C ++, тогда ссылки на переменные автоматически найдут переменную экземпляра.

Вы можете сделать глобальный указатель на активный экземпляр. Вероятно, это должно быть локально для потока (см. __declspec(thread)).

Добавление extern "C" функций-оболочек, которые делегируются соответствующей функции-члену в активном экземпляре. Предоставьте функции для создания нового экземпляра, завершения существующего экземпляра и установки активного экземпляра.

OpenGL использует эту парадигму с большим эффектом (см. wglMakeCurrent), находя данные своего состояния, фактически не передавая указатель состояния на каждую функцию.

4 голосов
/ 07 февраля 2011

Хотя я получил много ответов, в которых предлагалось перейти к решению 3, и хотя я согласен, что это лучшее решение концептуально , я думаю, что не было никакого способа реализовать это решение практически и надежно при моих ограничениях.

Вместо этого я фактически реализовал вариант решения № 1.Хотя имя DLL в DLLImport должно быть константой времени компиляции, этот вопрос объясняет, как сделать это динамически.

Если мой код раньше выглядел так:

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
    }
}

Теперь это выглядит так:

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
    }

}

Фу.

Это может показаться чрезвычайно сложным, если вы видите две версии рядом друг с другом, но после того, как вы установилиинфраструктура, это очень систематическое изменение.И что более важно, он локализует изменения в моем DotNetAccess слое, что означает, что мне не нужно делать модификации, разбросанные по очень большой кодовой базе, которая не принадлежит мне.

4 голосов
/ 10 января 2011

На мой взгляд, решение 3 - это путь.

Недостаток, связанный с каждым обращением к DLL, должен относиться и к другим решениям ... без недостатка масштабируемости иуродство подхода нескольких DLL и ненужные издержки IPC.

1 голос
/ 22 января 2011

Используйте подход № 3.Поскольку код написан на C, простой способ справиться с глобальными переменными, которые распространены повсюду, - это определить макрос для каждого глобала, имя которого совпадает с именем глобальной переменной, но макрос расширяется до чего-то вроде getSession () -> theGlobalгде getSession () возвращает pinter в «специфичную для сессии» структуру, которая содержит все данные для ваших глобальных переменных.getSession () каким-то образом извлекает правильную структуру данных из глобальной карты структур данных, возможно, с использованием локального хранилища потоков или на основе идентификатора процесса и т. д.

1 голос
/ 21 января 2011

Решение 3 - это путь.

Представьте себе, что современные языки объектно-ориентированного программирования работают подобно вашему 3-му решению, но только неявно передают указатель на структуру, которая содержит данные "this".

Обойти «какую-то контекстную вещь» не громоздко, а просто как все работает! ; -)

0 голосов
/ 23 января 2011

Некоторые мысли о предлагаемом решении № 2 (и немного о № 1 и № 3).

  1. Какой-то уровень IPC может привести к отставанию. Это зависит от фактического двигателя, насколько это плохо. Если движок представляет собой движок рендеринга и он вызывается, скажем, 60 раз в секунду, накладные расходы могут быть слишком большими. Но если нет, то именованный канал может быть достаточно быстрым и его легко создать с помощью WCF.
  2. Вы полностью уверены, что вам потребуется ТОЛЬКО ТОЛЬКО один и тот же движок несколько раз, или вы рискуете изменить требования, которые могут привести к сценарию, который заставит вас загружать несколько версий одновременно? Если это так, вариант № 2 может быть лучше, чем вариант № 3, поскольку это позволит сделать это проще.
  3. Если уровень IPC не слишком сильно замедляет работу, эта архитектура может позволить вам распределить механизмы по другим ПК. Это может позволить вам использовать больше оборудования, чем вы планировали ранее. Можно даже подумать о размещении движка в облаке Azure.
0 голосов
/ 22 января 2011

На самом деле решение 3 проще, чем кажется. Все остальные решения являются своего рода патчем и со временем сломаются.

  • Создайте класс .net, который будет инкапсулировать весь доступ к устаревшему коду. Сделай его IDisposable.
  • Измените все глобальные переменные, чтобы они находились в классе с именем «Context»
  • Пусть все интерфейсы C ++ получают объект контекста и передают его в качестве первого аргумента. Вероятно, это самый длинный этап, и вы можете избежать его, используя метод "thread-local-storage", предложенный кем-то другим, но я бы проголосовал против этого решения: если в вашей библиотеке есть какие-либо рабочие потоки, которые она запускает, "thread-local "Хранилище" сломается. Просто добавьте объект контекста там, где он нужен.
  • Использование объекта контекста для доступа ко всем глобальным данным.
  • Создать объект контекста из .net ctor (путем p / вызова новой функции create_context) и удалить его методом .net Dispose ().

Наслаждайтесь.

...