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


141

实际上,我有一个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”需要DLL目录的“ const string”参数。

所以我的问题是::在这种情况下可以做什么?


15
只需将DLL与EXE部署在同一文件夹中,这样您就无需执行任何操作,只需指定不带路径的DLL名称即可。其他方案是可能的,但都麻烦。
汉斯·帕桑

2
事实是,它将成为MS Office Excel
插件

8
您的解决方案是错误的。不要将文件放在Windows或系统文件夹中。他们之所以选择这些名称,是有原因的:因为它们用于Windows系统文件。您之所以没有创建其中之一,是因为您不在Windows团队中为Microsoft工作。记住在幼稚园学到的关于在未经允许的情况下使用不属于您的东西的知识,然后将文件放在任何地方。
科迪·格雷

您的解决方案仍然是错误的。行为良好的应用程序实际上并不做管理工作,不需要管理访问权限。另一个问题是,你不知道你的应用程序实际上安装的文件夹中。我可能会将其移动到其他地方,或者在安装过程中更改安装路径(我这样做是出于娱乐目的,只是为了破坏行为不佳的应用程序)。硬编码路径是不良行为的缩影,并且完全没有必要。如果您使用的是应用程序的文件夹,则这是DLL 默认搜索顺序中的第一个路径。全自动。
科迪·格雷

3
将其放在程序文件中不是恒定的。例如,64位计算机具有程序文件(x86)。
Louis Kottmann 2012年

Answers:


184

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

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

实际上,它甚至DllImport无法解决问题。无论您是否使用方便的托管包装器(P / Invoke marshaller只是调用LoadLibrary),它都是控制事情的本机Win32 DLL加载规则。这些规则在这里详细列出,但是重要的规则在这里摘录:

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

  • 如果内存中已加载了具有相同模块名称的DLL,则系统将使用已加载的DLL,无论它位于哪个目录中。系统都不会搜索该DLL。
  • 如果该DLL在运行该应用程序的Windows版本的已知DLL列表中,则系统使用其已知DLL的副本(以及已知DLL的从属DLL,如果有的话)。系统不搜索DLL。

如果SafeDllSearchMode启用(默认),则搜索顺序如下:

  1. 从中加载应用程序的目录。
  2. 系统目录。使用该GetSystemDirectory函数获取此目录的路径。
  3. 16位系统目录。没有获取该目录路径的函数,但会对其进行搜索。
  4. Windows目录。使用该GetWindowsDirectory函数获取此目录的路径。
  5. 当前目录。
  6. PATH环境变量中列出的目录。请注意,这不包括“应用程序路径”注册表项指定的每个应用程序路径。计算DLL搜索路径时不使用“应用程序路径”键。

因此,除非您将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 /调用此功能。声明看起来像这样:

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool SetDllDirectory(string lpPathName);

16
对此的另一个小改进可能是从DLL名称中删除了扩展名。Windows将自动添加.dll,其他系统将在Mono下添加相应的扩展名(例如,.so在Linux上)。如果要考虑可移植性,这可能会有所帮助。
jheddings 2012年

6
为+1 SetDllDirectory。您也可以更改Environment.CurrentDirectory,所有相对路径将从该路径进行评估!
GameScripting

2
甚至在发布此消息之前,OP便声明他正在制作一个插件,因此将DLL放入Microsoft的程序文件中并不是一件容易的事情。另外,更改进程DllDirectory或CWD可能不是一个好主意,它们可能导致进程失败。现在AddDllDirectory,另一方面...
Mooing Duck 2015年

3
依赖于工作目录是一个潜在的严重安全漏洞@GameScripting,尤其是对于以超级用户权限运行的某些东西不明智。值得编写代码并进行设计工作以使其正确。
科迪·格雷

2
请注意,这DllImport不只是一个包装LoadLibrary。它还考虑该extern方法在中定义的程序集的目录。的DllImport搜索路径可以使用另外的限制DefaultDllImportSearchPath
米奇

38

比Ran建议使用更好GetProcAddress,只需LoadLibrary在对DllImport函数的任何调用之前进行调用(只有文件名而不带路径),它们将自动使用已加载的模块。

我已经使用此方法在运行时选择是加载32位还是64位本机DLL,而无需修改大量P / Invoke-d函数。将加载代码粘贴在具有导入函数的类型的静态构造函数中,一切正常。


1
我不确定这是否一定能奏效。或者,如果恰好发生在当前版本的框架上。
CodesInChaos

3
@Code:向我保证:Dynamic-Link库搜索顺序。具体来说,“影响搜索的因素”指出第一点。
科迪·格雷

真好 好吧,我的解决方案还有一个小的附加优势,因为即使函数名也不必是静态的,并且在编译时就知道了。如果您有2个具有相同签名和不同名称的函数,则可以使用我的FunctionLoader代码来调用它们。
2012年

这听起来像我想要的。我曾希望使用诸如mylibrary32.dll和mylibrary64.dll之类的文件名,但我想我可以将它们使用相同的名称,但是放在不同的文件夹中。
yoyo 2015年

27

如果您需要不在路径或应用程序位置上的.dll文件,那么我认为您不能做到这一点,因为它DllImport是一个属性,而属性仅是在类型,成员和其他类型上设置的元数据语言元素。

另一种可以帮助您完成我认为要尝试的工作的替代方法是LoadLibrary通过P / Invoke 使用本机,以便从所需路径加载.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,因此此代码不完整。在实际的应用程序中,应小心释放已加载的模块,以避免内存泄漏。


有LoadLibrary的托管对等项(在Assembly类中)。
卡2012年

如果您有一些代码示例,对我来说更容易理解!^^(实际上,这有点朦胧)
Jsncrdnl 2012年

1
@Luca Piccioni:如果您的意思是Assembly.LoadFrom,则仅加载.NET程序集,而不加载本机库。你什么意思?
2012年

1
我的意思是,但我不知道这个限制。叹。

1
当然不是。那只是一个示例,表明您可以在不使用需要静态路径的P / Invoke的情况下调用本机dll中的函数。
2012年

5

只要您知道在运行时可以找到C ++库的目录,这应该很简单。我可以清楚地看到在您的代码中就是这种情况。您myDll.dll将出现在myLibFolder当前用户临时文件夹内的目录中。

string str = Path.GetTempPath() + "..\\myLibFolder\\myDLL.dll"; 

现在,您可以使用const字符串继续使用DllImport语句,如下所示:

[DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int DLLFunction(int Number1, int Number2);

在运行时,在调用DLLFunction函数(存在于C ++库中)之前,在C#代码中添加以下代码行:

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

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


3
这对我有用。我的执行应用程序的“ bin”目录中有一个文件夹“ Modules”。在那里,我放置了托管dll和托管dll所需的一些非托管dll。使用此解决方案并在我的app.config中设置探测路径,使我能够动态加载所需的程序集。
WBuck

对于使用Azure函数的人员:字符串workingDirectory = Path.GetFullPath(Path.Combine(executionContext.FunctionDirectory,@“ .. \ bin”));
Red Riding Hood '18

4

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

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


我尝试将其放入系统环境变量中,但仍被认为是非常量(我认为是逻辑上的)
Jsncrdnl 2012年

-14

如果全部失败,只需将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);    
   }
}

来源甚至更多DllImport示例:http : //msdn.microsoft.com/zh-cn/library/aa288468(v=vs.71).aspx


好的,我同意您使用win32文件夹的解决方案(最简单的实现方式),但是如何授予Visual Studio调试器(以及编译的应用程序)对该文件夹的访问权限?(除手动运行它为管理员)
Jsncrdnl

如果将其用于除调试辅助工具之外的其他任何事情,那么它将受到我书中的所有审核(安全性或其他方面)的影响。
Christian.K

21
这是一个非常糟糕的解决方案。系统文件夹用于系统 DLL。现在,您需要管理员特权,并且仅仅因为您很懒就依赖于不良做法。
MikeP 2012年

5
对于MikeP +1,对于此答案-1。这是一个糟糕的解决方案,任何这样做的人都应该反复被弄乱,同时被迫阅读The Old New Thing。就像您在幼儿园学到的一样:system文件夹不属于您,因此,未经允许,您不要使用它。
科迪·格雷

好的,我同意您的意见,但是我的问题仍未解决,所以...您会向我推荐哪个位置?(知道我无法使用变量进行设置-因为它正在等待常量字符串-所以我必须使用在每台计算机上都应该相同的位置吗?)(或者有什么方法可以使用变量而不是常量来执行它?)
Jsncrdnl 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.