如何在运行时指定[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
属性仍然是正确的方法。
老实说,我不明白为什么你不能像世界上其他人一样,为你的DLL指定一个相对路径。是的,在不同的人的计算机上安装应用程序的路径不同,但在部署时,这基本上是一条通用规则。DllImport
机制的设计就是考虑到了这一点。
事实上,它甚至不是DllImport
来处理它。它是本机Win32 DLL加载规则来控制事情,无论你是否使用方便的托管包装器(p/Invoke-mashaller只调用LoadLibrary
)。这些规则在这里列举得非常详细,但重要的规则在这里摘录:
在系统搜索DLL之前,它会检查以下内容:
- 如果内存中已经加载了具有相同模块名称的DLL,则无论加载的DLL在哪个目录中,系统都会使用该DLL。系统不会搜索该DLL
- 如果DLL在运行应用程序的Windows版本的已知DLL列表中,则系统将使用已知DLL的副本(以及已知DLL的依赖DLL,如果有的话)。系统不搜索DLL
如果启用了
SafeDllSearchMode
(默认值),则搜索顺序如下:
- 从中加载应用程序的目录
- 系统目录。使用
GetSystemDirectory
函数获取该目录的路径- 16位系统目录。没有任何函数可以获取此目录的路径,但会对其进行搜索
- Windows目录。使用
GetWindowsDirectory
函数获取该目录的路径- 当前目录
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搜索路径为:
- 从中加载应用程序的目录
- 由
lpPathName
参数指定的目录- 系统目录。使用
GetSystemDirectory
函数获取该目录的路径- 16位系统目录。没有任何函数可以获取此目录的路径,但会对其进行搜索
- Windows目录。使用
GetWindowsDirectory
函数获取该目录的路径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.dll。NativeLibrary.Load
和Native.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