如何在运行时指定[DllImport]路径

本文关键字:DllImport 路径 运行时 | 更新日期: 2023-09-27 18:20:02

事实上,我得到了一个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);

问题是,它将是一个可安装的项目,因此用户的文件夹将不相同(例如:皮埃尔、保罗、杰克、妈妈、爸爸…),这取决于它运行的计算机/会话。

所以我希望我的代码更通用一点,比如:

/* 
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"需要DLL目录的"const string"参数。

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

如何在运行时指定[DllImport]路径

与其他一些答案的建议相反,使用DllImport属性仍然是正确的方法。

老实说,我不明白为什么你不能像世界上其他人一样,为你的DLL指定一个相对路径。是的,在不同的人的计算机上安装应用程序的路径不同,但在部署时,这基本上是一条通用规则。DllImport机制的设计就是考虑到了这一点。

事实上,它甚至不是DllImport来处理它。它是本机Win32 DLL加载规则来控制事情,无论你是否使用方便的托管包装器(p/Invoke-mashaller只调用LoadLibrary)。这些规则在这里列举得非常详细,但重要的规则在这里摘录:

在系统搜索DLL之前,它会检查以下内容:

  • 如果内存中已经加载了具有相同模块名称的DLL,则无论加载的DLL在哪个目录中,系统都会使用该DLL。系统不会搜索该DLL
  • 如果DLL在运行应用程序的Windows版本的已知DLL列表中,则系统将使用已知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);

甚至比Ran建议的使用GetProcAddress更好,只需在调用DllImport函数之前先调用LoadLibrary(只有文件名而没有路径),它们就会自动使用加载的模块。

我使用这个方法在运行时选择是加载32位还是64位的本机DLL,而不必修改一堆p/Invoke-d函数。将加载代码粘贴到具有导入函数的类型的静态构造函数中,一切都会正常工作。

如果您需要一个不在路径或应用程序位置上的.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,所以这段代码并不完整。在实际应用程序中,应该小心释放加载的模块,以避免内存泄漏。

在所有其他好的答案中,在.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);
        

另请参阅:

  • https://learn.microsoft.com/en-us/dotnet/standard/native-interop/cross-platform

如果您知道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#代码中的DLLFunction函数(存在于C++库中)之前,请添加以下代码行:

string assemblyProbeDirectory = Path.GetTempPath() + "..''myLibFolder''myDLL.dll"; 
Directory.SetCurrentDirectory(assemblyProbeDirectory);

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

在配置文件中设置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);

由于.NET Core 3.0-也可与.NET 5&一起使用。NET 6-您可以使用NativeLibrary.Load(string)在运行时动态加载非托管DLL,您可以通过P/Invoke使用它。

有关更多详细信息,请参阅SO上的此答案:https://stackoverflow.com/a/69958827/211672

//[?] 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
  }
}

DllImport只要dll位于系统路径上的某个位置,就可以在没有指定完整路径的情况下正常工作。您可以将用户的文件夹临时添加到路径中。

如果全部失败,只需将DLL放入windows'system32文件夹即可。编译器会找到它。指定要从中加载的DLL:DllImport("user32.dll"...,设置EntryPoint = "my_unmanaged_function"将所需的非托管函数导入C#应用程序:

 using System;
using System.Runtime.InteropServices;
class Example
{
   // Use DllImport to import the Win32 MessageBox function.
   [DllImport ("user32.dll", CharSet = CharSet.Auto)]
   public static extern int MessageBox 
      (IntPtr hWnd, String text, String caption, uint type);
   static void Main()
   {
      // Call the MessageBox function using platform invoke.
      MessageBox (new IntPtr(0), "Hello, World!", "Hello Dialog", 0);    
   }
}

Source和更多DllImport示例:http://msdn.microsoft.com/en-us/library/aa288468(v=vs.71).aspx