我该如何在运行时指定 [DllImport] 的路径?

176

实际上,我有一个可以工作的C++ DLL,我想将其导入到我的C#项目中以调用其函数。

当我指定DLL的完整路径时,它确实可以工作,像这样:

string str = "C:\\Users\\userName\\AppData\\Local\\myLibFolder\\myDLL.dll";
[DllImport(str, CallingConvention = CallingConvention.Cdecl)]
public static extern int DLLFunction(int Number1, int Number2);

问题在于这将是一个可安装的项目,因此用户的文件夹将根据运行它的计算机/会话而不同(例如:pierre、paul、jack、mum、dad等)。

所以我希望我的代码能更加通用,像这样:

/* 
goes right to the temp folder of the user 
    "C:\\Users\\userName\\AppData\\Local\\temp"
then go to parent folder
    "C:\\Users\\userName\\AppData\\Local"
and finally go to the DLL's folder
    "C:\\Users\\userName\\AppData\\Local\\temp\\myLibFolder"
*/

string str = Path.GetTempPath() + "..\\myLibFolder\\myDLL.dll"; 
[DllImport(str, CallingConvention = CallingConvention.Cdecl)]
public static extern int DLLFunction(int Number1, int Number2);

"DllImport"需要一个"const string"参数来指定DLL的目录,这是非常重要的。

所以我的问题是:在这种情况下应该怎么做?


21
只需将DLL部署到与EXE相同的文件夹中,这样您就无需执行任何操作,只需指定DLL名称而无需指定路径。其他方案也可能可行,但都会带来麻烦。 - Hans Passant
2
问题在于它将成为一个MS Office Excel插件,因此我不认为将dll放在exe目录中是最佳解决方案... - Jsncrdnl
10
你的解决方案是错误的。不要将文件放在Windows或系统文件夹中。它们之所以被命名为这些名称,是因为它们是用于Windows系统文件的。你没有创造这样的文件,因为你不在Windows团队工作。请记住在幼儿园学到的关于未经允许使用不属于你的东西的道理,把你的文件放在其他地方而不是那里。 - Cody Gray
3
将其放在程序文件中并非固定不变的。例如,64位计算机有Program Files (x86)文件夹。 - Louis Kottmann
@Jsncrdnl,我刚刚加入了这个Directory.SetCurrentDirectory(DLLsFolder),DLL是按需加载的,所以在实例化使用引用DLL的对象之前,请调用“SetCurrentDirectory”。在使用dllimport加载后,当进程结束(或任何AppDomain完成)时,dll将被卸载。 - antonio
显示剩余2条评论
11个回答

210
与其他答案中的建议相反,使用 DllImport 属性仍然是正确的方法。
我真的不明白为什么你不能像世界上其他人一样指定一个相对路径到你的 DLL。是的,你的应用程序安装的路径在不同人的计算机上不同,但这基本上是部署时普遍的规则。 DllImport 机制就是考虑到这一点设计的。
事实上,它甚至不是 DllImport 处理它。它是本地 Win32 DLL 加载规则来控制的,无论你是否使用方便的托管包装器(P/Invoke marshaller 只是调用 LoadLibrary)。这些规则在here中详细列出,但重要的摘录如下:
在系统搜索DLL之前,它会检查以下内容: - 如果已经在内存中加载了具有相同模块名称的DLL,则系统将使用加载的DLL,无论它位于哪个目录中。系统不会搜索该DLL。 - 如果DLL在应用程序运行的Windows版本的已知DLL列表中,则系统使用其已知DLL的副本(以及其依赖的DLL,如果有)。系统不会搜索该DLL。
如果启用了SafeDllSearchMode(默认情况下),搜索顺序如下: 1. 应用程序加载的目录。 2. 系统目录。使用GetSystemDirectory函数获取此目录的路径。 3. 16位系统目录。没有获取此目录路径的函数,但是会搜索该目录。 4. Windows目录。使用GetWindowsDirectory函数获取此目录的路径。 5. 当前目录。 6. 列在PATH环境变量中的目录。请注意,这不包括由App Paths注册表键指定的每个应用程序路径。计算DLL搜索路径时不使用App Paths键。
因此,除非您将DLL命名为与系统DLL相同的名称(在任何情况下都不应该这样做),否则默认搜索顺序将开始查找从应用程序加载的目录中。如果您在安装期间将DLL放置在那里,则会找到它。如果只使用相对路径,所有复杂的问题都会消失。
只需编写:
[DllImport("MyAppDll.dll")] // relative path; just give the DLL's name
static extern bool MyGreatFunction(int myFirstParam, int mySecondParam);

如果由于某种原因无法工作,您需要强制应用程序在不同的目录中查找DLL,则可以使用SetDllDirectory函数修改默认搜索路径。请注意,根据文档: 调用SetDllDirectory后,标准DLL搜索路径为: 1. 应用程序加载的目录。 2. 由lpPathName参数指定的目录。 3. 系统目录。使用GetSystemDirectory函数获取此目录的路径。 4. 16位系统目录。没有获取此目录路径的功能,但会搜索。 5. Windows目录。使用GetWindowsDirectory函数获取此目录的路径。 6. 在PATH环境变量中列出的目录。
只要在第一次调用从DLL导入的函数之前调用此函数,您就可以修改用于查找DLL的默认搜索路径。当然,好处是您可以传递计算时生成的动态值给此函数。这在DllImport属性中是不可能的,因此您仍将在那里使用相对路径(仅DLL名称),并依靠新的搜索顺序来查找它。
您需要P/Invoke此函数。声明如下:
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool SetDllDirectory(string lpPathName);

23
对此的另一个小改进是从 DLL 名称中删除扩展名。Windows 将自动添加“.dll”,其他系统将在 Mono 下添加相应的扩展名(例如 Linux 上的“.so”)。如果需要可移植性,这可能会有所帮助。 - jheddings
9
+1 对于 SetDllDirectory。您也可以更改 Environment.CurrentDirectory,所有相对路径将根据该路径进行评估! (注:这句话的意思是通过更改当前目录,可以使得程序中使用的相对路径都以这个新的目录为基准来计算) - GameScripting
2
即使在此发布之前,OP已经澄清他正在制作一个插件,因此将DLL放入Microsoft的程序文件中有点行不通。此外,更改进程DllDirectory或CWD可能不是一个好主意,它们可能会导致进程失败。现在另一方面,AddDllDirectory... - Mooing Duck
3
过度依赖当前工作目录可能存在严重的安全漏洞,特别是对于以超级用户权限运行的程序而言更加不可取。值得花费时间编写代码和进行设计以确保正确性。 - Cody Gray
3
请注意,DllImport 不仅是 LoadLibrary 的包装器。它还考虑了包含外部方法的程序集目录。可以使用 DefaultDllImportSearchPath 限制 DllImport 搜索路径。 - Mitch
显示剩余18条评论

41

比Ran提出的使用GetProcAddress方法更好的方法是,在调用任何DllImport函数之前,仅使用文件名(不带路径)调用LoadLibrary函数,它们将自动使用已加载的模块。

我使用了这种方法来在运行时选择加载32位或64位本机DLL,而无需修改一堆P / Invoke-d函数。将加载代码放入具有导入函数的类型的静态构造函数中,它将完美地工作。


1
我不确定这是否保证可行。或者它只是在当前版本的框架上偶然发生的。 - CodesInChaos
3
@Code:对我来说似乎是有保证的:动态链接库搜索顺序。具体来说,“影响搜索的因素”,第一点。 - Cody Gray
1
不错。我的解决方案有一个小优点,即使函数名不必是静态的并在编译时已知。如果您有两个具有相同签名但名称不同的函数,则可以使用我的“FunctionLoader”代码调用它们。 - Ran
这听起来就是我想要的。我本来希望使用像mylibrary32.dll和mylibrary64.dll这样的文件名,但我猜我可以接受它们有相同的名称但在不同的文件夹中。 - yoyo

31
如果你需要一个不在路径或应用程序位置上的.dll文件,那么我不认为你可以做到这一点,因为DllImport是一个属性,属性只是设置在类型、成员和其他语言元素上的元数据。
一个可行的替代方案是通过P/Invoke使用本地的LoadLibrary来加载你需要的路径中的.dll文件,然后使用GetProcAddress从该.dll文件中获取所需函数的引用。然后使用这些内容创建一个委托,你可以调用它。
为了使用更加容易,你可以将这个委托设置为类中的字段,这样使用它就像调用成员方法一样。
以下是一个代码片段,它可以工作,并展示了我的意思。
class Program
{
    static void Main(string[] args)
    {
        var a = new MyClass();
        var result = a.ShowMessage();
    }
}

class FunctionLoader
{
    [DllImport("Kernel32.dll")]
    private static extern IntPtr LoadLibrary(string path);

    [DllImport("Kernel32.dll")]
    private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

    public static Delegate LoadFunction<T>(string dllPath, string functionName)
    {
        var hModule = LoadLibrary(dllPath);
        var functionAddress = GetProcAddress(hModule, functionName);
        return Marshal.GetDelegateForFunctionPointer(functionAddress, typeof (T));
    }
}

public class MyClass
{
    static MyClass()
    {
        // Load functions and set them up as delegates
        // This is just an example - you could load the .dll from any path,
        // and you could even determine the file location at runtime.
        MessageBox = (MessageBoxDelegate) 
            FunctionLoader.LoadFunction<MessageBoxDelegate>(
                @"c:\windows\system32\user32.dll", "MessageBoxA");
    }

    private delegate int MessageBoxDelegate(
        IntPtr hwnd, string title, string message, int buttons); 

    /// <summary>
    /// This is the dynamic P/Invoke alternative
    /// </summary>
    static private MessageBoxDelegate MessageBox;

    /// <summary>
    /// Example for a method that uses the "dynamic P/Invoke"
    /// </summary>
    public int ShowMessage()
    {
        // 3 means "yes/no/cancel" buttons, just to show that it works...
        return MessageBox(IntPtr.Zero, "Hello world", "Loaded dynamically", 3);
    }
}

注意:我没有使用FreeLibrary,因此这段代码不完整。在实际应用中,您应该注意释放加载的模块以避免内存泄漏。

如果你有一些代码示例,那对我来说理解起来会更容易!^^(实际上,这有点模糊不清) - Jsncrdnl
1
@Luca Piccioni:如果你指的是Assembly.LoadFrom,那么它只能加载.NET程序集,而不能加载本地库。你的意思是什么? - Ran
1
我本来是这个意思,但我不知道有这个限制。唉。 - Luca
1
当然不是。那只是一个示例,展示了您可以调用本地dll中的函数而无需使用需要静态路径的P/Invoke。 - Ran
哇,这真是一个很棒的解决方案,谢谢!非常灵活!应该是答案。 - user2440074
显示剩余4条评论

17

在所有其他好的答案中,.NET Core 3.0之后,您可以使用NativeLibrary。例如,在Linux中,您没有这样的kernel32.dllNativeLibrary.LoadNative.SetDllImportResolver可能是解决问题的方法:

        static MyLib()
        {
            //Available for .NET Core 3+
            NativeLibrary.SetDllImportResolver(typeof(MyLib).Assembly, ImportResolver);
        }

        private static IntPtr ImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
        {
            IntPtr libHandle = IntPtr.Zero;
            if (libraryName == "MyLib")
            {
                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                {
                    libHandle = NativeLibrary.Load("xxxx.dll");
                }
                else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
                {
                    libHandle = NativeLibrary.Load("xxxx.so");
                }
            }
            return libHandle;
        }

        [DllImport("MyLib", CallingConvention = CallingConvention.Cdecl)]
        public static extern IntPtr foo(string name);
        

另请参阅:


8
如果您知道C++库在运行时所在的目录,则这将很简单。我可以看出这是您问题陈述中的情况。您的程序集名为myDll.dll,将位于当前用户的临时文件夹内的myLibFolder目录中。
string str = Path.GetTempPath() + "..\\myLibFolder\\myDLL.dll"; 

您可以继续使用DllImport语句,如下所示,使用常量字符串:
[DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int DLLFunction(int Number1, int Number2);

在你的C#代码中调用C++库中的DLLFunction函数之前,请添加以下代码行:
string assemblyProbeDirectory = Path.GetTempPath() + "..\\myLibFolder\\myDLL.dll"; 
Directory.SetCurrentDirectory(assemblyProbeDirectory);

这将指示 .NET CLR 在运行时查找非托管C ++库的目录路径。调用Directory.SetCurrentDirectory可以将应用程序的当前工作目录设置为指定的目录。如果您的myDLL.dll 存在于由 assemblyProbeDirectory 表示的路径上,那么它将被加载。然后您可以通过p / invoke调用所需的函数。


3
这对我很有效。我有一个文件夹“Modules”,位于执行应用程序的“bin”目录中。在那里,我放置了一个托管dll和一些托管dll需要的非托管dll。使用这个解决方案并在我的app.config中设置搜索路径,使我能够动态加载所需的程序集。 - WBuck
对于使用Azure Functions的人来说:string workingDirectory = Path.GetFullPath(Path.Combine(executionContext.FunctionDirectory, @"..\bin")); - Red Riding Hood

1

1
在配置文件中设置dll路径。
<add key="dllPath" value="C:\Users\UserName\YourApp\myLibFolder\myDLL.dll" />

在调用应用程序中的dll之前,请执行以下操作

string dllPath= ConfigurationManager.AppSettings["dllPath"];    
   string appDirectory = Path.GetDirectoryName(dllPath);
   Directory.SetCurrentDirectory(appDirectory);

然后调用dll,您可以像下面这样使用。
 [DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int DLLFunction(int Number1, int Number2);

0
//[?] Method Sample;
[System.Runtime.InteropServices.DllImport("ramdom_Kernel32.dll")] //[!] Error Sample
public dynamic MethodWithDllImport(){
  
}

partial static Main(){
  try{
    //[?] Exception Cannot Be Handled over the Attribute;
    //    handle where it is called;
    MethodWithDllImport();
  } 
  catch{
   //[?] use overloaded\other name methods
  }
}

0
经过一些研究和尝试错误,对于放置在项目根目录下的unmanaged\dlls目录中的dll文件,似乎只能通过在启动时扩展PATH环境变量来实现。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace biometric_sync
{
    internal static class Program
    {
        /// <summary>
        /// Главная точка входа для приложения.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            string workingDirectory = Environment.CurrentDirectory;
            // This will get the current PROJECT directory
            // where "Parent.Parent" => requied for assigned Debug|Release.x86|x64 configurations !
            string projectDirectory = Directory.GetParent(workingDirectory).Parent.Parent.FullName;
            var dllDirectory = projectDirectory + @"\\unmanaged\\dlls";
            Environment.SetEnvironmentVariable(
                "PATH",
                Environment.GetEnvironmentVariable(
                    "PATH", EnvironmentVariableTarget.Process
                ) + ";" + dllDirectory,
                EnvironmentVariableTarget.Process
            );

            Application.Run(new Form1());
        }
    }
}

使用Directory.GetParent(workingDirectory).Parent.FullName(少父级引用)来针对anyCpu目标。
然后下面的代码可以正常工作:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.IO;

namespace sbxpc
{
    class SBXPCDLL
    {
        [DllImport("SBXPCDLL64.dll", CallingConvention = CallingConvention.Winapi)]
        static extern byte _ConnectTcpip(Int32 dwMachineNumber, ref IntPtr lpszIPAddress, Int32 dwPortNumber, Int32 dwPassWord);

        public static bool ConnectTcpip(Int32 dwMachineNumber, string lpszIPAddress, Int32 dwPortNumber, Int32 dwPassWord)
        {
            if (lpszIPAddress == null)
                return false;

            IntPtr string_in = Marshal.StringToBSTR(lpszIPAddress);
            try
            {
                byte ret = _ConnectTcpip(dwMachineNumber, ref string_in, dwPortNumber, dwPassWord);
                return ret > 0;
            }
            catch (Exception)
            {
                return false;
            }
            finally
            {
                Marshal.FreeBSTR(string_in);
            }
        }

    }
}

-1

如果dll文件位于系统路径中的某个位置,DllImport可以正常工作而无需指定完整路径。您可以尝试将用户文件夹暂时添加到路径中。


我尝试将它放置在系统环境变量中,但它仍然被视为非常量(逻辑上,我认为)。 - Jsncrdnl

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