加载多个版本的DLLs

6
我有一个C#应用程序,与一些硬件(USB设备)进行接口如下: C#应用程序->中间DLL->硬件DLL->硬件。中间DLL和硬件DLL随USB设备提供,因此我无法控制这些内容。 中间DLL是我需要在VS项目中包含的唯一内容,因为这是我所调用的。硬件DLL位于同一目录中,因此必须自动找到它。
现在发布了一个带有不同硬件DLL的新版本硬件设备。旧的DLL与新的硬件不兼容,新的DLL也与旧的硬件不兼容。
我该如何使我的应用程序能够使用两个硬件部件?我猜我需要根据需要加载和卸载每个DLL?

2
你首先需要确定你要针对哪种硬件进行开发。你解决了这个问题吗? - Grant Thomas
中间 DLL 是本地 DLL 吗? - Dennis
@HansPassant:是的,那将是简单的选择!然而,这并不理想,这也是我学习一点的好机会! :) - Mark
@JayWalker:和上面一样,那肯定更容易 - 尽管它不会通知我错误的 DLL - 只是无法检测到任何硬件,所以对用户来说可能不是那么透明。我可能不得不退而求其次采用这种解决方案,但我喜欢挑战 :) - Mark
@Justin:谢谢,我会联系他们了解更多细节。无论如何,有解决方案吗? - Mark
显示剩余6条评论
3个回答

4
这是我为类似问题所做的事情。我有一段代码需要处理,但我必须在运行时加载dll。因此,在我的项目中引用它,但我不将它放在与其余程序集相同的目录中。相反,在消费代码中,我有一些代码看起来像这样:
// constructor called from a static constructor elsewhere
MyDllLoader(string hardwareFolder) {
    _hardwareFolder = hardwareFolder;
    AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
    SeeIfAlreadyLoaded();
}


private void SeeIfAlreadyLoaded() {
    // if the assembly is still in the current app domain then the AssemblyResolve event will
    // never fire.
    // Since we need to know where the assembly is, we have to look for it
    // here.
    Assembly[] assems = AppDomain.CurrentDomain.GetAssemblies();
    foreach (Assembly am in assems)
    {
        // if it matches, just mark the local _loaded as true and get as much
        // other information as you need
    }
}

System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) {
    string name = args.Name;
    if (name.StartsWith("Intermediate.dll,"))
    {
        string candidatePath = Path.Combine(_hardwareFolder, "Intermediate.dll");
        try {
            Assembly assem = Assembly.LoadFrom(candidatePath);
            if (assem != null) {
                _location = candidateFolder;
                _fullPath = candidatePath;
                _loaded = true;
                return assem;
            }
        }
        catch (Exception err) {
            sb.Append(err.Message);
        }
    }
    return null;
}

还有另一种解决方案-它很复杂,但我已经完成了工作。您声明一个抽象类,比如MyHardwareAbstraction,其中包含您想要的方法的签名,并针对该接口编写代码。然后,您编写一些代码,给定程序集路径,加载它并动态定义一个与MyHardwareAbstraction匹配并将其映射到所需对象实例的新类。我几年前写了一篇关于如何做到这一点的博客
这样做的好处是,在您的代码中使用抽象类型并针对它进行操作,然后适配器编译器会在运行时编译一个新类,使用其他类型作为目标类型来完成该抽象类型。这也相当高效。

谢谢,这提供了最多的线索,帮助我找到最终解决方案。 - Mark

0

编辑:

如果中间DLL是一个.NET程序集,您可以使用这里提到的方法,在调用任何使用中间DLL的方法之前指定查找中间DLL的位置,而无需更改现有代码。

然后,您不应该直接在C#项目中引用DLL,因为.NET程序集会在调用Main方法之前被发现和加载。相反,您必须使用AppDomain或其他方法动态加载中间DLL,然后通过反射或使用dynamic对象来使用库。

显然,这会使编程变得非常繁琐。但是,有一种替代方法。您可以编写一个启动程序,加载您的原始应用程序(您可以将.exe文件作为库加载),并反射地调用您的原始程序的Main方法。为了确保加载正确的中间DLL,您可以使用此处提到的方法,而您的启动程序正在加载您的原始应用程序。

以下讨论仍适用于硬件DLL。


如果满足以下条件,则以下内容有效:

  1. 您在应用程序运行期间仅需要一个版本的dll,并且
  2. 两个中间DLL版本具有完全相同的API。

根据MSDN,DLL搜索路径包括在PATH环境变量下指定的目录。(http://msdn.microsoft.com/en-us/library/7d83bc18%28v=vs.80%29.aspx) 因此,您可以将两个中间DLL版本放置在应用程序目录下的不同子目录中,但是每个目录下的名称必须完全相同,例如:

bin\
   hardware-intermediate-v1\
       intermediate.dll
   hardware-intermediate-v2\
       intermediate.dll

然后,在启动时,当您的应用程序确定要使用哪个版本后,您可以将上述目录之一添加到您的PATH环境变量中,

using System;
using System.Reflection;
using System.IO;
...
Environment.SetEnvironmentVariable(
    "PATH", 
    Environment.GetEnvironmentVariable("PATH") + ";" + 
        Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + 
        "\\hardware-intermediate-v1"
);

然后,对P-Invoke方法(DLLImport)的调用将导致加载相应版本的DLL。要立即加载所有DLL,请参考DllImport,如何检查DLL是否已加载?

但是,如果您希望在不重启应用程序的情况下同时使用两个版本的DLL,或者如果两个DLL之间在方法名称和/或参数计数/类型的级别存在任何API差异,则必须创建两组单独的P-Invoke方法,每个绑定到其对应的中间DLL版本。


这似乎在我的项目中无法工作。如果像链接中建议的那样调用Marshal.PrelinkAll(Type),也没有帮助。还没有返回值,以知道它是否真正做了什么。 - Mark
关于更新,那听起来像是我已经做过的,但没有运气 :( - Mark

0
如果您希望程序中存在两个dll,您将需要使用AppDomains,如此处所述。
否则,您可以在用户明确选择所需版本后使用LoadLibrary。

我可以看出这可以用于加载中间DLL,但如果它调用硬件DLL,有没有指定它应该使用给定的AppDomain的方法? - Mark
我不确定。如果这两个中间层DLL在不同的文件夹中,而相应的硬件在同一个文件夹中,我认为(希望?)中间层将会从各自的文件夹中加载DLL。 - C4stor
1
中间的 DLL 是 .Net 程序集吗? - Interarticle
@Interarticle:看起来是Intermediate。对于硬件DLL,我不确定。 - Mark
@C4stor:我是按照这样设置的,但似乎它们没有正确加载。 - Mark

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接