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

14
我正在编写一个跨平台的.NET库,其中包含一些非托管代码。在我的类的静态构造函数中,检测平台并从嵌入资源中提取适当的非托管库并保存到临时目录,类似于另一个stackoverflow答案中给出的代码。
为了在未在PATH中找到库的情况下仍然可以找到它,我将其保存到临时文件后明确加载它。在Windows上,使用kernel32.dll中的LoadLibrary很好用。我试图在Linux上使用dlopen做同样的事情,但是当它需要加载P/Invoke方法时,我会遇到DllNotFoundException异常。
我已经验证了库“libindexfile.so”已成功保存到临时目录,并且调用dlopen也成功了。我深入研究了Mono源码,尝试弄清楚发生了什么,我认为这可能归结于后续调用dlopen是否只是重复使用先前加载的库。(当然假设我的对Mono源代码的浏览能够得出正确的结论)。
这就是我正在尝试做的事情的大致形状:
// 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: %x\n", handle1);

    HMODULE handle2 = LoadLibrary(L"library.dll");
    printf("Handle: %x\n", handle2);

    return 0;
}

在Linux上使用dlopen尝试相同的操作会失败,因为它不会假设同名库位于相同路径下。有没有什么方法可以解决这个问题?

4个回答

23

经过大量搜索和思考,我找到了一个解决方案。可以使用动态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上找到。

我想看到这个解决方案,但是链接的文档似乎相关但不完整。链接的文档有两个其他链接可能会有用,但是这些链接现在已经失效了。直接描述解决方案而不仅仅是提供一个链接会更好。 - Joe

1

我需要加载一个本地库,该库已提取到临时位置,我几乎找到了解决方案。我查看了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
当你从同一库中调用另一个尚未 JIT 的本机函数时,你调用 mono_dllmap_insert()之前,它将正常工作。
因此,你可以使用@gordonmleigh添加的手动解决方案,或者在 JIT 任何这些导入之前告诉 Mono 库的位置。反射可能会有帮助。

1

尝试从终端以这种方式运行它:

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

现在每个库查找都将被打印到终端,因此您将能够找出出了什么问题。


我已经做过这个了——它并没有告诉我任何我不知道的东西。它告诉我找不到库文件... - Gordon Leigh
1
尝试将LD_LIBRARY_PATH设置为您保存库的目录(在启动程序之前),因此您可以在终端中执行类似于“LD_LIBRARY_PATH=/my/tmp/dir:$LD_LIBRARY_PATH mono myapp.exe”的操作。 - Rolf Bjarne Kvinge

0

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

如果你的更新示例有效,那么这意味着在Windows上LoadLibrary()具有不同的语义,而在Linux上则是dlopen():因此,你必须接受差异或实现自己的抽象来处理目录问题(我的直觉是它不是保留目录,而是Windows只是查看是否已经加载了相同名称的库,并重用它)。


1
这与mono有关,因为我正在尝试在mono上实现某些东西。可能有一种特定于mono的简单解决方案。是的,我同意你的直觉,如果我没有说明清楚,很抱歉。不幸的是,我无法看到如何编写一个抽象,因为mono在执行P / Invoke时直接使用dlopen - Gordon Leigh
这与Mono无关,因为如果您使用C、C++、Python或任何其他编程语言,您将遇到完全相同的问题,因为您遇到的问题是dlopen()不是LoadLibrary()的精确匹配。您需要的抽象是类似于:handle = lookup_loaded_handle(basename(library_path)); 如果(handle != null)则返回handle; 否则返回dlopen(library_path); - lupus
1
иҝҷдёҺMonoжңүе…іпјҢеӣ дёәжҲ‘并дёҚжҺҢжҺ§йӮЈдёӘжҠҪиұЎеұӮгҖӮжҲ‘ж— жі•йҮҚеҶҷMonoжЎҶжһ¶жқҘж”№еҸҳе®ғеӨ„зҗҶDllImportжҲ–и°ғз”Ёdlopenзҡ„ж–№ејҸгҖӮдәӢе®һиҜҒжҳҺпјҢжҲ‘еҸҜд»Ҙжү§иЎҢжңӘз®ЎзҗҶд»Јз ҒиҖҢдёҚж¶үеҸҠDllImportпјҲиҜ·еҸӮи§ҒжҲ‘зҡ„зӯ”жЎҲпјүгҖӮиҝҷдёӘи§ЈеҶіж–№жЎҲжҳҜC#д»Јз Ғпјӣиҝҷд»Қ然жҳҜдёҖдёӘ.NET / Monoзҡ„й—®йўҳгҖӮ - Gordon Leigh

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