[P/Invoke] 使用 `SetDllImportResolver`[^1] 改寫 `DllImport` 的庫解析規則

ijklmnop發表於2024-09-30

[P/Invoke] 使用 SetDllImportResolver[1] 改寫 DllImport 的庫解析規則

目錄
  • [P/Invoke] 使用 SetDllImportResolver[1] 改寫 DllImport 的庫解析規則
    • 問題匯入
    • 嘗試解決
    • 總結

問題匯入

我們都知道,DllImport 在載入本機庫時,是在程式資料夾裡,或者環境變數指定的路徑裡,按照特定的規則來尋找庫的。那麼,如果我們想要稍微改變一下載入規則,要怎麼做呢?

這個問題我是在 F# 互動環境中引用 PaddleOCRSharp 遇到的。當時,我像在專案中引用一樣,直接在互動環境中引用 PaddleOCRSharp,配好模型引數,然後構建 PaddleOCREngine,結果報錯了:

- let engine = PaddleOCREngine(config);;
System.DllNotFoundException: Unable to load DLL 'PaddleOCR' or one of its dependencies:  找不到指定的模組。 (0x8007007E)
   at PaddleOCRSharp.PaddleOCREngine.Initialize(String det_infer, String cls_infer, String rec_infer, String keys, OCRParameter parameter)
   at ...
已因出錯而停止

嘗試解決

看起來是缺 DLL 檔案。由於互動環境是在 dotnet 安裝目錄裡面的,我只能將所需的 DLL 複製出來放到另外的資料夾,然後試試庫自帶的更改載入路徑方法:

- let dllPath = "..."
- PaddleOCREngine.PaddleOCRdllPath <- dllPath
- let engine = PaddleOCREngine(config);;
System.DllNotFoundException: Unable to load DLL 'PaddleOCR' or one of its dependencies:  找不到指定的模組。 (0x8007007E)
   at PaddleOCRSharp.PaddleOCREngine.Initialize(String det_infer, String cls_infer, String rec_infer, String keys, OCRParameter parameter)
   at ...
已因出錯而停止

還是報錯。此時我還沒有意識到報錯的真正原因,只是懷疑還是找不到 DLL 。於是我又換了個方法——使用 SetDllImportResolver 來試試。

SetDllImportResolver 的說明文件主要如下:

Sets a callback for resolving native library imports from an assembly.

static member SetDllImportResolver : 
     System.Reflection.Assembly * 
     System.Runtime.InteropServices.DllImportResolver 
     -> unit

This per-assembly resolver is the first attempt to resolve native library loads initiated by this assembly.

可以看到,SetDllImportResolver 是用於改寫某個程式集內解析本機庫匯入規則的。

DllImportResolver 是一個接受三個引數而返回一個本機庫控制代碼的委託:

type DllImportResolver = delegate of
     libraryName: string  *
     assembly   : Assembly  *
     searchPath : Nullable<DllImportSearchPath> 
               -> nativeint

說明文件有示例,那就簡單了,很快就可以寫出下面的程式碼:

- NativeLibrary.SetDllImportResolver(
-     // PaddleOCREngine 所在的程式集
-     typeof<PaddleOCREngine>.Assembly, 
-     fun libraryName assembly searchPath ->
-         if libraryName = "PaddleOCR" then 
-             let path = Path.Combine(dllPath, libraryName + ".dll")
-             // 載入本機庫,返回控制代碼
-             NativeLibrary.Load(path, assembly, searchPath)
-         // 如果不是 PaddleOCR,則使用預設的載入規則
-         else 0n)
- let engine = PaddleOCREngine(config);;
System.DllNotFoundException: Unable to load DLL 'D:\l\Desktop\sp\dll\ocr\PaddleOCR.dll' or one of its dependencies: 找不到指定的模組。 (0x8007007E)
   at System.Runtime.InteropServices.NativeLibrary.LoadLibraryByName(String libraryName, Assembly assembly, Nullable`1 searchPath, Boolean throwOnError)
   at ...
已因出錯而停止

依然報錯,但是輸出改變了。於是我終於發現了問題所在:依賴庫沒有被複制過來。將依賴庫複製過來後,程式終於執行起來了。

總結

SetDllImportResolver 可以用來改寫某個程式集內解析本機庫匯入的規則。

對於 PaddleOCREngine 來說,修改 DLL 載入路徑方法最好是修改 PaddleOCREngine.PaddleOCRdllPath,因為它是透過修改環境變數實現的,可以讓本機庫也找得到依賴。如果是用 SetDllImportResolver 讓程式在託管部分執行起來的話,在非託管那邊也會報錯:

System.Exception: Initialize err:                
                                                 
--------------------------------------           
C++ Traceback (most recent call last):           
--------------------------------------           
Not support stack backtrace yet.                 
                                                 
----------------------                           
Error Message Summary:                           
----------------------                           
PreconditionNotMetError: The third-party dynamic library (mklml.dll) that Paddle depends on is not configured correctly. (error code is 126)       
  Suggestions:                                   
  1. Check if the third-party dynamic library (e.g. CUDA, CUDNN) is installed correctly and its version is matched with paddlepaddle you installed.
  2. Configure third-party dynamic library environment variables as follows:                      
  - Linux: set LD_LIBRARY_PATH by `export LD_LIBRARY_PATH=...`                                    
  - Windows: set PATH by `set PATH=XXX; (at D:\MyWorks\Paddle\PaddleBuild\Paddle-v2.6\paddle\phi\backends\dynload\dynamic_loader.cc:312)           
                                                 
   at PaddleOCRSharp.PaddleOCREngine..ctor(OCRModelConfig config, OCRParameter parameter)
   at PaddleOCRSharp.PaddleOCREngine..ctor(OCRModelConfig config)                                 
   at FSI_0014.staticInitialization@() in d:\l\Desktop\sp\240929 ocr.fsx:line 27                  
   at <StartupCode$FSI_0014>.$FSI_0014.main@()   
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)                               
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)   
已因出錯而停止                               

  1. SetDllImportResolver 僅在 .NET Core 3.1 和 .NET 5+ 中可用。 ↩︎

相關文章