支持具有全局数据的插件DLL的多个实例

时间:2011-01-10 10:03:45

标签: c windows dll pinvoke ipc

上下文:我将旧版独立引擎转换为合成工具的插件组件。从技术上讲,这意味着我将引擎代码库编译为C DLL,我使用P / Invoke从.NET包装器调用它;包装器实现由组合工具定义的接口。这非常有效,但现在我收到了为不同项目加载多个引擎实例的请求。由于引擎将项目数据保存在一组全局变量中,并且由于具有引擎代码库的DLL仅加载一次,因此加载多个项目意味着项目数据被覆盖。

我可以看到许多解决方案,但它们都有一些缺点:

  1. 您可以创建多个具有相同代码的DLL,这些代码被Windows视为不同的DLL,因此不会共享其代码。如果你有不同名称的引擎DLL的多个副本,这可能已经有效。但是,使用DllImport属性从包装器调用引擎,我认为在编译包装器时需要知道引擎DLL的名称。显然,如果我必须为每个项目编译不同版本的包装器,这非常麻烦。

  2. 引擎可以作为单独的进程运行。这意味着包装器在加载项目时会为引擎启动一个单独的进程,并且它将使用某种形式的IPC与此进程进行通信。虽然这是一个相对干净的解决方案,但它需要一些努力才能开始工作,我现在不知道哪种IPC技术最适合建立这种结构。通信可能还有很大的开销:引擎需要经常交换浮点数组。

  3. 引擎可以适应多个项目。这意味着全局变量应该放入项目结构中,并且对全局变量的每个引用都应该转换为相对于特定项目的相应引用。大约有20-30个全局变量,但是可以想象,这些全局变量是从整个代码库引用的,因此需要以某种自动方式完成此转换。一个相关的问题是你应该能够引用"当前"所有地方的项目结构,但在每个函数签名中作为额外参数传递它也很麻烦。是否存在一种技术(在C中)来考虑当前的调用堆栈并在那里找到最近的相关数据值的封闭实例?

  4. stackoverflow社区可以就这些(或其他)解决方案提供一些建议吗?

7 个答案:

答案 0 :(得分:6)

将整个darn事物放入C ++类中,然后对变量的引用将自动找到实例变量。

您可以创建一个指向活动实例的全局指针。这应该是线程本地的(参见__declspec(thread))。

添加委托给活动实例上相应成员函数的extern "C"包装函数。提供创建新实例的功能,拆除现有实例,并设置活动实例。

OpenGL使用这个范例产生了很大的效果(参见wglMakeCurrent),找到它的状态数据,而不必将状态指针传递给每个函数。

答案 1 :(得分:4)

在我看来,解决方案3是要走的路。

您必须触及对DLL的每次调用的缺点也应该适用于其他解决方案......而不会缺乏多DLL方法的可伸缩性和丑陋以及IPC的不必要开销。

答案 2 :(得分:4)

虽然我收到了很多建议寻求解决方案3的答案,虽然我同意它是一个更好的解决方案概念,但我认为没有办法实现这个解决方案几乎< 在我的约束下可靠

相反,我实际实现的是解决方案#1的变体。虽然DLLImport中的DLL名称需要是编译时常量,但this question解释了如何动态地执行它。

如果之前我的代码看起来像这样:

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层中的修改本地化,这意味着我不需要在非常大的代码库中分散修改。

答案 3 :(得分:1)

解决方案3是要走的路。

想象一下,当前面向对象的编程语言与第三种解决方案类似,但只是隐式地将指针传递给包含“this”数据的结构。

传递“某种上下文的东西”并不麻烦,但事情就是这样! ; - )

答案 4 :(得分:1)

使用方法#3。由于代码在C中,处理遍布各处的全局变量的一种简单方法是为每个与全局变量同名的全局定义一个宏,但宏扩展为类似于getSession() - &gt; theGlobal其中getSession()将pinter返回到“特定于会话”的结构,该结构包含全局变量的所有数据。 getSession()会以某种方式从数据结构的全局映射中捕获正确的数据结构,可能使用线程本地存储,或者基于进程ID等。

答案 5 :(得分:0)

实际上,解决方案3比听起来更容易。所有其他解决方案都是一种补丁,会随着时间的推移而破裂。

  • 创建一个.net类,它将封装对遗留代码的所有访问权限。使它成为IDisposable。
  • 将所有全局变量更改为驻留在名为“Context”
  • 的类中
  • 让所有C ++接口获取上下文对象并将其作为第一个参数传递。这可能是最长的阶段,您可以使用其他人建议的“线程本地存储”方法来避免它,但我会投票反对该解决方案:如果您的库有任何工作线程,它运行,“线程本地 - 存储“解决方案将破裂。只需在需要的地方添加上下文对象。
  • 使用上下文对象访问所有全局数据。
  • 从.net ctor创建上下文对象(通过p /调用新的create_context函数)并通过.net Dispose()方法删除。

享受。

答案 6 :(得分:0)

对建议的解决方案#2(以及#1和#3上的一点)的一些想法。

  1. 某种IPC层可能会引入滞后。这取决于实际发动机有多糟糕。如果引擎是一个渲染引擎,并且它被称为,例如,每秒60次,则开销可能太大。但是,如果没有,命名管道可能足够快,并且易于使用WCF创建。
  2. 您是否完全确定您需要多次完全使用相同的引擎,或者您是否有可能导致需求变化的危险,这可能会导致强制您同时加载多个版本的情况?如果是这样,选项#2可能比选项#3更好,因为它可以更容易。
  3. 如果IPC层没有减慢太多的速度,这个架构可能允许你将引擎分配到其他PC上。这可能使您可以使用比以前计划更多的硬件。您甚至可以考虑在Azure云中托管引擎。