P/Invoke到Mono上动态加载的库



我正在编写一个使用一些非托管代码的跨平台.NET库。在我的类的静态构造函数中,检测到平台,并从嵌入式资源中提取相应的非托管库,并将其保存到临时目录中,类似于另一个stackoverflow答案中给出的代码。

为了在库不在PATH中时可以找到它,我在将它保存到临时文件后显式地加载它。在windows上,这与kernel32.dll中的LoadLibrary配合使用很好。我正试图在Linux上对dlopen进行同样的操作,但在稍后加载P/Invoke方法时,我得到了DllNotFoundException

我已经验证了库"libindexfile.so"是否已成功保存到临时目录,并且对dlopen的调用是否成功。我深入研究了mono源代码,试图弄清楚发生了什么,我认为这可能归结为后续对dlopen的调用是否只会重用以前加载的库。(当然,假设我对单一来源的天真突袭得出了正确的结论)。

以下是我要做的事情的形状:

// actual function that we're going to p/invoke to
[DllImport("indexfile")]
private static extern IntPtr openIndex(string pathname);
const int RTLD_NOW = 2; // for dlopen's flags
const int RTLD_GLOBAL = 8;
// its okay to have imports for the wrong platforms here
// because nothing will complain until I try to use the
// function
[DllImport("libdl.so")]
static extern IntPtr dlopen(string filename, int flags);
[DllImport("kernel32.dll")]
static extern IntPtr LoadLibrary(string filename);

static IndexFile()
{
string libName = "";
if (IsLinux)
libName += "libindexfile.so";
else
libName += "indexfile.dll";
// [snip] -- save embedded resource to temp dir
IntPtr handle = IntPtr.Zero;
if (IsLinux)
handle = dlopen(libPath, RTLD_NOW|RTLD_GLOBAL);
else
handle = LoadLibrary(libPath);
if (handle == IntPtr.Zero)
throw new InvalidOperationException("Couldn't load the unmanaged library");
}

public IndexFile(String path)
{
// P/Invoke to the unmanaged function
// currently on Linux this throws a DllNotFoundException
// works on Windows
IntPtr ptr = openIndex(path);
}

更新:

在windows上对LoadLibrary的后续调用似乎会查看是否已经加载了相同名称的dll,然后使用该路径。例如,在以下代码中,对LoadLibrary的两个调用都将返回一个有效句柄:

int _tmain(int argc, _TCHAR* argv[])
{
LPCTSTR libpath = L"D:\some\path\to\library.dll";
HMODULE handle1 = LoadLibrary(libpath);
printf("Handle: %xn", handle1);
HMODULE handle2 = LoadLibrary(L"library.dll");
printf("Handle: %xn", handle2);
return 0;
}

如果在Linux上对dlopen进行同样的尝试,第二个调用将失败,因为它不会假设具有相同名称的库将位于同一路径。有什么办法绕过这个吗?

经过多次搜索和挠头,我发现了一个解决方案。通过使用动态P/Invoke来告诉运行时在哪里可以找到代码,可以对P/Invoke进程进行完全控制。


编辑:

Windows解决方案

你需要这些进口:

[DllImport("kernel32.dll")]
protected static extern IntPtr LoadLibrary(string filename);
[DllImport("kernel32.dll")]
protected static extern IntPtr GetProcAddress(IntPtr hModule, string procname);

应通过调用LoadLibrary:加载非托管库

IntPtr moduleHandle = LoadLibrary("path/to/library.dll");

通过调用GetProcAddress:获取指向dll中函数的指针

IntPtr ptr = GetProcAddress(moduleHandle, methodName);

将此ptr强制转换为TDelegate:类型的委托

TDelegate func = Marshal.GetDelegateForFunctionPointer(
ptr, typeof(TDelegate)) as TDelegate;

Linux解决方案

使用这些导入:

[DllImport("libdl.so")]
protected static extern IntPtr dlopen(string filename, int flags);
[DllImport("libdl.so")]
protected static extern IntPtr dlsym(IntPtr handle, string symbol);
const int RTLD_NOW = 2; // for dlopen's flags 

加载库:

IntPtr moduleHandle = dlopen(modulePath, RTLD_NOW);

获取函数指针:

IntPtr ptr = dlsym(moduleHandle, methodName);

像以前一样将其投射给一名代表:

TDelegate func = Marshal.GetDelegateForFunctionPointer(
ptr, typeof(TDelegate)) as TDelegate;

有关我编写的助手库,请参阅我的GitHub。

试着从终端像这样运行它:

export MONO_LOG_LEVEL=debug
export MONO_LOG_MASK=dll
mono --debug yourapp.exe

现在,每个库查找都将打印到终端,这样您就可以找出问题所在。

我需要加载一个提取到临时位置的本地库,我几乎找到了一个解决方案。我检查了Mono的源代码,找到了一种方法:

[DllImport("__Internal", CharSet = CharSet.Ansi)]
private static extern void mono_dllmap_insert(IntPtr assembly, string dll, string func, string tdll, string tfunc);
// and then somewhere:
mono_dllmap_insert(IntPtr.Zero, "somelib", null, "/path/to/libsomelib.so", null);

这类工作。问题是,在调用mono_dllmap_insert()之前,不能让Mono愚蠢的JIT编译器捕捉到任何引用该库的DllImported方法的味道。

因为如果真的发生了,奇怪的事情就会发生:

Mono: DllImport searching in: '/tmp/yc1ja5g7.emu/libsomelib.so' ('/tmp/yc1ja5g7.emu/libsomelib.so').
Mono: Searching for 'someGreatFunc'.
Mono: Probing 'someGreatFunc'.
Mono: Found as 'someGreatFunc'.
Error. ex=System.DllNotFoundException: somelib

所以现在我调用了我的原生someGreatFunc(),Mono能够找到库并加载它(我检查了),它能够找到符号(我检查过),但因为在过去执行JIT时,它无法加载该库,所以它决定无论如何都抛出DllNotFoundException。我猜生成的代码包含一个硬编码的throw语句或其他什么:-O

当您从同一个库中调用另一个本机函数时,该函数在调用mono_dllmap_insert()之前恰好没有被JITted,它将起作用。

因此,您可以使用@gordonmleigh添加的手动解决方案,也可以在Mono卸载这些导入之前告诉它库在哪里。反思可能会有所帮助。

不确定为什么你认为这与mono有关,因为你遇到的问题与mono的动态加载设施无关。

如果您更新的样本有效,这只是意味着windows上的LoadLibrary()与Linux上的dlopen()具有不同的语义:因此,您要么必须接受这种差异,要么实现自己的抽象来处理目录问题(我的直觉是,它不是保留的目录,但windows只是查看是否已经加载了具有相同名称的库,然后重用它)。

最新更新