在C# DllImport中使用32位或64位dll

77

这是情况,我在我的Dot.Net应用程序中使用一个基于C的dll。有两个dll,一个是名为MyDll32.dll的32位版本,另一个是名为MyDll64.dll的64位版本。

有一个静态变量持有DLL文件名:字符串DLL_FILE_NAME。

它被以下方式使用:

[DllImport(DLL_FILE_NAME, CallingConvention=CallingConvention.Cdecl, EntryPoint=Func1")]
private static extern int is_Func1(int var1, int var2);

到目前为止很简单。

正如你所想象的那样,该软件是使用"Any CPU"编译的。

我还有以下代码来确定系统应该使用64位文件还是32位文件。

#if WIN64
        public const string DLL_FILE_NAME = "MyDll64.dll";
#else
        public const string DLL_FILE_NAME = "MyDll32.dll";        
#endif

到现在你应该看出问题所在了。DLL_FILE_NAME是在编译时定义的,而不是在执行时定义的,因此根据执行上下文加载正确的dll文件。

如何解决这个问题?我不想要两个执行文件(一个用于32位,另一个用于64位)。我怎样才能在DllImport语句中使用前设置DLL_FILE_NAME?


1
64位和32位dll之间有什么区别?32位在64位上做不到的事情吗?如果是这样,我就只使用32位。 - Bali C
在64位操作系统上,执行代码的纯64位或WOW64(32位模拟)决策是在程序执行时做出的。如果程序以32位模式执行,则应使用基于C编译的32位和64位dll。 - Gilad
2
如果你真的想这样做,你需要完全绕过 DllImport 属性,并手动使用 LoadLibraryGetProcAddessFreeLibrary 函数加载 DLL。该技术在 这里 中有所讨论。虽然这是相当繁琐的工作,而且很容易出错。让 P/Invoke 机制为你完成这项工作要容易得多。正如其他人所指出的那样,如果你可以始终回退到 32 位 DLL 作为最低公共分母,那么可能不值得这样做。 - Cody Gray
10个回答

75

我发现最简单的方法是导入两个具有不同名称的方法,并调用正确的一个。 DLL 在调用之前不会被加载,所以没问题:

[DllImport("MyDll32.dll", EntryPoint = "Func1", CallingConvention = CallingConvention.Cdecl)]
private static extern int Func1_32(int var1, int var2);

[DllImport("MyDll64.dll", EntryPoint = "Func1", CallingConvention = CallingConvention.Cdecl)]
private static extern int Func1_64(int var1, int var2);

public static int Func1(int var1, int var2) {
    return IntPtr.Size == 8 /* 64bit */ ? Func1_64(var1, var2) : Func1_32(var1, var2);
}

当然,如果你有很多导入,手动维护会变得非常麻烦。


40
为了最大程度地提高可读性,建议使用Environment.Is64BitProcess属性来代替检查IntPtr类型的大小。当然,这个属性是在.NET 4.0中引入的,所以如果你的目标版本较老,则不会可用。 - Cody Gray
这正是我想做的。虽然编码有一些额外开销,但它能够工作并完成任务。 - Gilad
我对你的成功感到印象深刻。通常我发现在决策和调用之间需要再加一层间接层。 - Joshua

74

这是另一种选择,需要两个 DLL 的名称相同并放置在不同的文件夹中。例如:

  • win32/MyDll.dll
  • win64/MyDll.dll

诀窍是在 CLR 加载 DLL 之前使用 LoadLibrary 手动加载 DLL。然后它将看到已经加载了 MyDll.dll 并使用它。

这可以很容易地在父类的静态构造函数中完成。

static class MyDll
{
    static MyDll()
    {            
        var myPath = new Uri(typeof(MyDll).Assembly.CodeBase).LocalPath;
        var myFolder = Path.GetDirectoryName(myPath);

        var is64 = IntPtr.Size == 8;
        var subfolder = is64 ? "\\win64\\" : "\\win32\\";

        LoadLibrary(myFolder + subfolder + "MyDll.dll");
    }

    [DllImport("kernel32.dll")]
    private static extern IntPtr LoadLibrary(string dllToLoad);

    [DllImport("MyDll.dll")]
    public static extern int MyFunction(int var1, int var2);
}

编辑 2017/02/01: 使用 Assembly.CodeBase,以便在启用阴影复制的情况下也能正常工作。


4
这绝对是最优雅的答案,最适合这个问题。我成功地将其应用于FreeImage项目的.NET包装器!真是个很棒的想法。我使用了 Environment.Is64BitProcess 并像Kisdeds的回答建议的那样解决了程序集的路径问题。非常感谢! - JanW
您还可以将32位和64位的DLL文件作为资源嵌入到.NET DLL中,并在需要时提取正确的DLL文件/调用LoadLibrary,以便将所有内容打包。 - Matt
1
如果你的本地dll需要其他库,可能会遇到问题。 - cahit beyaz
准备好接收用户不断发送的报告,关于那些愚蠢的反恶意软件程序将你的应用标记为恶意软件,仅仅因为使用了LoadLibrary函数。我已经经历过这种情况了。 - Guavaman

20
在这种情况下,我应该这样做(创建2个文件夹,x64和x86,将相应的dll以相同的名称放入这两个文件夹中):
using System;
using System.Runtime.InteropServices;
using System.Reflection;
using System.IO;

class Program {
    static void Main(string[] args) {
        var path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
        path = Path.Combine(path, IntPtr.Size == 8 ? "x64" : "x86");
        bool ok = SetDllDirectory(path);
        if (!ok) throw new System.ComponentModel.Win32Exception();
    }
    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool SetDllDirectory(string path);
}

8

有一个静态变量保存着DLL文件名

它不是一个静态变量,它是一个在编译时的常量。您无法在运行时更改编译时常量。

应该如何正确处理此问题?

老实说,我建议只针对x86并完全忘记64位版本,让您的应用程序在WOW64上运行,除非您的应用程序有必要作为x64运行。

如果需要x64,则可以:

  • 将DLL更改为相同的名称,例如MyDll.dll,并在安装/部署时放置正确的DLL。(如果操作系统是x64,则部署64位版本的DLL,否则是x86版本)。

  • 完全拥有两个不同的构建,一个用于x86,另一个用于x64。


1
同意,这绝对是解决问题最简单的方法。绝大多数应用程序,特别是业务线应用程序,从成为64位本机应用程序中获得不了任何好处。即使可能会有一些边际性能提升,64位的增加开销往往会抵消这些提升。你提出的另一个建议是使用相同的DLL名称,并根据系统架构部署正确的版本,这正是微软在系统DLL中所做的。这也是一个不错的选择。 - Cody Gray

2
另一种方法可能是:
public static class Sample
{
    public Sample()
    {

        string StartupDirEndingWithSlash = System.IO.Path.GetDirectoryName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName) + "\\";
        string ResolvedDomainTimeFileName = StartupDirEndingWithSlash + "ABCLib_Resolved.dll";
        if (!File.Exists(ResolvedDomainTimeFileName))
        {
            if (Environment.Is64BitProcess)
            {
                if (File.Exists(StartupDirEndingWithSlash + "ABCLib_64.dll"))
                    File.Copy(StartupDirEndingWithSlash + "ABCLib_64.dll", ResolvedDomainTimeFileName);
            }
            else
            {
                if (File.Exists(StartupDirEndingWithSlash + "ABCLib_32.dll"))
                    File.Copy(StartupDirEndingWithSlash + "ABCLib_32.dll", ResolvedDomainTimeFileName);
            }
        }
    }

    [DllImport("ABCLib__Resolved.dll")]
    private static extern bool SomeFunctionName(ref int FT);
}

它应该复制dll,使应用程序花费更多时间。 - lindexi

2
您所描述的是“并排装配”(同一装配的两个版本,一个为32位,另一个为64位)...我认为下面这些内容会对您有所帮助:

在此处您可以找到一个完全符合您情况的演练(.NET DLL 封装 C++/CLI DLL 引用本地 DLL)。

建议:

只需构建为x86即可...或者有2个版本的构建(一个x86和一个x64)...因为上述技术相当复杂...


2
我不想要两个应用程序。那太90年代了。 - Gilad
@Gilad 接着点击提供的链接,它们展示了一些也许更符合您要求的选项(注意:这会变得非常复杂)... - Yahia
2
90年代还没有64位,所以那不可能是90年代 :p - banging
2
@banging 不完全正确... ALPHA 和 MIPS 架构在 90 年代都有 64 位的产品... Itanium 64 位于 2001 年推出(在 90 年代下半年开发)。 - Yahia
1
这与Visual Studio无关,也不是“严重出错”。你只是做错了。 - Cody Gray
显示剩余2条评论

1
根据Julien Lebosquain的优秀回答,在类似情况下我做了以下处理:
private static class Api32
{
    private const string DllPath = "MyDll32.dll";

    [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl)]
    private static extern int Func1(int var1, int var2);

    [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl)]
    private static extern int Func2();

    ...
}

private static class Api64
{
    private const string DllPath = "MyDll64.dll";

    [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl)]
    private static extern int Func1(int var1, int var2);

    [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl)]
    private static extern int Func2();

    ...
}

public static int Func1(int var1, int var2) {
    return Environment.Is64BitProcess 
           ? Api64.Func1(var1, var2) 
           : Api32.Func1(var1, var2);
}

我认为如果你有同一DLL中的多个入口点,这个选项会更好地扩展,原因如下:

  • Api32和Api64类完全相同,除了定义DLL文件路径的单个常量。这意味着如果有任何更改,我可以将一个类的声明复制并粘贴到另一个类中。
  • 无需指定EntryPoint,减少出现拼写错误的可能性。

我认为可以通过源代码生成器自动连接它,以避免重复类并手动创建样板文件。 - Ryan

0

我使用了vcsjones提到的方法之一:

"将DLL更改为相同的名称,例如MyDll.dll,并在安装/部署时放置正确的DLL。"

这种方法需要维护两个构建平台,详情请参见此链接:https://dev59.com/E2w15IYBdhLWcg3wtd8g#6446638


0

我在V8.Net中使用的技巧如下:

  1. 创建一个新的C# "代理接口"项目,并使用所有定义来在不同架构之间进行切换。在我的情况下,该项目名为V8.Net-ProxyInterface;例如:
 public unsafe static class V8NetProxy
    {
    #if x86
            [DllImport("V8_Net_Proxy_x86")]
    #elif x64
            [DllImport("V8_Net_Proxy_x64")]
    #else
            [DllImport("V8_Net_Proxy")] // (dummy - NOT USED!)
    #endif
            public static extern NativeV8EngineProxy* CreateV8EngineProxy(bool enableDebugging, void* debugMessageDispatcher, int debugPort);

这是你需要参考的项目。请勿参考接下来的两个项目:

  • 创建两个项目来生成库的 x64 和 x86 版本。这非常简单:只需复制和粘贴 .csproj 文件以在相同文件夹中重命名即可。在我的情况下,项目文件被重命名为 V8.Net-ProxyInterface-x64V8.Net-ProxyInterface-x86,然后将这些项目添加到我的解决方案中。在 Visual Studio 中打开每个项目的设置,并确保 Assembly Name 中包含 x64 或 x86 的名称。此时您已经有了 3 个项目:第一个“占位符”项目和两个特定于架构的项目。对于这两个新项目:

    a)在 x64 接口项目设置中打开构建选项卡,在顶部选择 所有平台 作为 平台,然后在 条件编译符号 中输入 x64

    b)在 x86 接口项目设置中打开构建选项卡,在顶部选择 所有平台 作为 平台,然后在 条件编译符号 中输入 x86

  • 打开 构建->配置管理器...,确保选择 x64 作为 x64 项目的平台,选择 x86 作为 x86 项目的平台,并分别在 DebugRelease 配置中进行设置。

  • 确保两个新接口项目(x64 和 x86)输出到主机项目的相同位置(请参见项目设置 构建->输出路径)。

  • 最后的魔法:在我的引擎的静态构造函数中,我快速附加到程序集解析器:

  • static V8Engine()
    {
        AppDomain.CurrentDomain.AssemblyResolve += Resolver;
    }
    

    Resolver方法中,我只是根据当前进程指示的当前平台加载文件(注意:此代码是削减版本且未经测试):
    var currentExecPath = Assembly.GetExecutingAssembly().Location;
    var platform = Environment.Is64BitProcess ? "x64" : "x86";
    var filename = "V8.Net.Proxy.Interface." + platform + ".dll"
    return Assembly.LoadFrom(Path.Combine(currentExecPath , fileName));
    

    最后,在解决方案资源管理器中进入您的主机项目,展开引用,选择第一个在第一步中创建的虚拟项目,在右键单击打开属性,并将复制本地设置为false。这样可以使用一个名称来开发每个P/Invoke函数,同时使用解析器来确定实际加载哪个。

    请注意,程序集加载器仅在需要时才运行。它仅在CLR系统在首次访问引擎类时(在我的情况下)自动触发。这如何转化取决于您的主机项目是如何设计的。


    -1

    我认为这可以帮助动态加载DLL:

       #if X64    
        [DllImport("MyDll64.dll", CallingConvention=CallingConvention.Cdecl, EntryPoint=Func1")]
        #else
        [DllImport("MyDll32.dll", CallingConvention=CallingConvention.Cdecl, EntryPoint=Func1")]
        #endif
        private static extern int is_Func1(int var1, int var2);
    

    请注意,#if X64 ... 在编译时解析,而不是在运行时解析。 - AntonK

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