【问题标题】:P/Invoke to dynamically loaded library on MonoP/Invoke 在 Mono 上动态加载库
【发布时间】:2012-11-07 20:35:52
【问题描述】:

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

为了使库不在 PATH 中时可以找到,我在将其保存到临时文件后显式加载它。在 Windows 上,这适用于 kernel32.dll 中的LoadLibrary。我正在尝试在 Linux 上对 dlopen 执行相同的操作,但稍后在加载 P/Invoke 方法时会得到 DllNotFoundException

我已验证库“libindexfile.so”已成功保存到临时目录,并且对dlopen 的调用成功。我深入研究了mono source 以试图弄清楚发生了什么,我认为这可能归结为对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: %x\n", handle1);

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

    return 0;
}

如果在 Linux 上尝试使用 dlopen 进行相同操作,第二次调用将失败,因为它不会假定同名库位于同一路径。有没有办法解决这个问题?

【问题讨论】:

    标签: c# mono pinvoke unmanaged


    【解决方案1】:

    经过大量搜索和摸索,我找到了解决方案。通过使用dynamic 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;
    

    有关我编写的帮助程序库,请参阅my GitHub

    【讨论】:

    • 我想看看这个解决方案,但链接到的文档似乎相关,但不完整。链接的文档还有另外两个可能有用的链接,但这些链接现在已失效。直接描述解决方案而不仅仅是一个链接会很好。
    【解决方案2】:

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

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

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

    【讨论】:

    • 我已经这样做了——它没有告诉我任何我不知道的事情。它告诉我找不到库...
    • 尝试将 LD_LIBRARY_PATH 设置为您保存库的目录(在启动程序之前,您可以执行以下操作:“LD_LIBRARY_PATH=/my/tmp/dir:$LD_LIBRARY_PATH mono myapp .exe”从终端)。
    【解决方案3】:

    我需要加载一个提取到临时位置的本机库,我几乎找到了解决方案。我检查了 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() 之前未经过 JIT 处理的本机函数时,它将起作用。

    因此,您可以使用 @gordonmleigh 添加的手动解决方案,或者您必须在 Mono 对这些导入进行 JIT 之前告诉 Mono 库在哪里。反思可能会有所帮助。

    【讨论】:

      【解决方案4】:

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

      如果您更新的示例有效,这仅意味着 Windows 上的 LoadLibrary() 与 Linux 上的 dlopen() 具有不同的语义:因此,您要么必须忍受差异,要么实现自己的抽象来处理目录问题 (我的预感是它不是保留的目录,而是 windows 只是查看是否已经加载了同名的库并重用它)。

      【讨论】:

      • 它与单声道有关,因为我想要实现的是单声道。可能有一个简单的解决方案,专门与单声道相关。是的,我同意你的预感,如果不清楚,抱歉。不幸的是,我看不到如何编写抽象,因为 mono 在执行 P/Invoke 时直接使用dlopen
      • 这与单声道无关,因为如果您使用 C、C++、python 或任何其他编程语言,您将遇到完全相同的问题,因为您遇到的问题是 dlopen() 不是与 LoadLibrary() 完全匹配。您需要的抽象类似于:handle = lookup_loaded_handle (basename (library_path)); if (handle != null) 返回句柄; ekse return dlopen (library_path);
      • 这与单声道有关,因为我不负责那个抽象。我无法重写单声道框架来更改它处理 DllImport 的方式或它调用dlopen 的方式。事实证明,我可以在不涉及 DllImport 的情况下执行非托管代码(请参阅我的答案)。这个解决方案是 C# 代码;这仍然是一个 .NET/mono 问题。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-11-12
      • 2011-01-25
      • 1970-01-01
      • 2023-03-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多