UnityでDllNotFoundExceptionをデバッグする

## 背景
Unityで、特に[Native Plugin](https://docs.unity3d.com/Manual/NativePlugins.html)を使った or に依存した開発中に、`DllNotFoundException` が発生することがあります。
`Exception` の名前からすれば、ライブラリが見つからない or 存在しない場合に発生しそうですが（[ドキュメント](https://docs.microsoft.com/en-us/dotnet/api/system.dllnotfoundexception?view=net-6.0)にもそう書いてある）、実際には依存ライブラリが見つからない場合や、一部のシンボルが解決できない場合にも発生します。
エラーメッセージから原因の切り分けができればよいのですが、大抵の場合は以下のようにこれ以上の情報がありません。

> 2021/12/09 13:14:12.173 13325 13432 Error Unity DllNotFoundException: mediapipe_jni
2021/12/09 13:14:12.173 13325 13432 Error Unity   at (wrapper managed-to-native) Mediapipe.UnsafeNativeMethods.glog_FLAGS_logtostderr(bool)
...

さて、ライブラリのパスが解決できない場合は[公式ドキュメント](https://docs.unity3d.com/Manual/PluginInspector.html)を読めば良いとして、その他の原因を特定するためには、ライブラリの中身を調べる必要があります。
通常、`ldd` や `objdump` などを使えば良いことが多いのですが、

- Android上だと使えない or 面倒
- 再現環境が手元にないと厳しい
   - 例えば、GitHubのissue等でこの手の質問が来て、解決の必要がある場合。相手にどうにか原因の一端を特定してもらう必要がある

という問題があり、効率的に原因を特定する方法が欲しくなったのでメモを残しておきます。
ただし、今のところ、Windowsで有効なエラーメッセージを得る方法は見つけていません。

## コード
```cs
using System;
using System.Runtime.InteropServices;

using UnityEngine;

/// <summary>
///   <see cref="DllNotFoundException" />の原因のデバッグ用のコード。
///   ライブラリをロードし、そのエラーコードやエラーメッセージを読む。
/// </summary>
/// <remarks>
///   Linux, macOS, Windows, Android, iOSのみを考慮。
///   ただし、iOSは静的リンクしている想定なので、何もしない。
/// </remarks>
public class NativeLibraryTester
{
  /// <summary>
  ///   ライブラリをロードする。
  /// </summary>
  /// <remarks>
  ///   Androidの場合は、通常 <see cref="LoadLibrary" /> を使うほうが良い。
  /// </remarks>
  /// <param name="path">
  ///   ライブラリのパス
  ///     UnityEditorの場合: プロジェクトのルートからの相対パスでOK
  ///     Standaloneの場合: `{Application.dataPath}/Plugins/*.{so,dylib,dll}`
  ///     Androidの場合: 絶対パス
  /// </param>
  public static void Load(string path)
  {
#if UNITY_IOS && !UNITY_EDITOR
    // iOS
    // 静的リンクを想定してスルー
#elif UNITY_ANDROID && !UNITY_EDITOR
    // Android
    // NOTE: Linux, macOSと同じくdlopenしても良いけど
    using (var system = new AndroidJavaClass("java.lang.System"))
    {
      system.CallStatic("load", path);
    }
#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
    // Windows
    // ただし、まともなエラーメッセージは出ない
    var handle = LoadLibraryW(path);

    if (handle != IntPtr.Zero)
    {
      // Success
      if (!FreeLibrary(handle))
      {
        Debug.LogError($"Failed to unload {path}: {Marshal.GetLastWin32Error()}");
      }
    }
    else
    {
      // Error
      // cf. https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
      var errorCode = Marshal.GetLastWin32Error();
      Debug.LogError($"Failed to load {path}: {errorCode}");

      if (errorCode == 126)
      {
        // ERROR_MOD_NOT_FOUND
        Debug.LogError(@"Check missing dependencies using [Dependencies](https://github.com/lucasg/Dependencies).
If all the required libraries exist, open the plugin inspector for dependent libraries and check `Load on startup`.
");
      }
    }
#else
    // Linux, macOS
    var handle = dlopen(path, 2);

    if (handle != IntPtr.Zero)
    {
      var result = dlclose(handle);

      if (result != 0)
      {
        Debug.LogError($"Failed to unload {path}");
      }
    }
    else
    {
      Debug.LogError($"Failed to load {path}: {Marshal.GetLastWin32Error()}");
      var error = Marshal.PtrToStringAnsi(dlerror());
      // TODO: release memory

      if (error != null)
      {
        Debug.LogError(error);
      }
    }
#endif
  }

  /// <summary>
  ///   ライブラリをロードする。
  /// </summary>
  /// <param name="name">
  ///   ライブラリ名 (e.g. libfoo.so -> foo)
  /// </param>
  public static void LoadLibrary(string name)
  {
#if UNITY_ANDROID && !UNITY_EDITOR
    using (var system = new AndroidJavaClass("java.lang.System"))
    {
      system.CallStatic("loadLibrary", name);
    }
#else
    throw new NotSupportedException("Androidのみ対応");
#endif
  }

  [DllImport("dl", SetLastError = true, ExactSpelling = true)]
  private static extern IntPtr dlopen(string name, int flags);

  [DllImport("dl", ExactSpelling = true)]
  private static extern IntPtr dlerror();

  [DllImport("dl", ExactSpelling = true)]
  private static extern int dlclose(IntPtr handle);

  [DllImport("kernel32", SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
  private static extern IntPtr LoadLibraryW(string path);

  [DllImport("kernel32", SetLastError = true)]
  [return: MarshalAs(UnmanagedType.Bool)]
  private static extern bool FreeLibrary(IntPtr handle);
}
```

適当なスクリプトから、以下のようにライブラリをロードすると、`DllNotFoundException`よりはまともなエラーメッセージが表示されます。

```cs
NativeLibraryTester.Load("/path/to/library");
```

## ちなみに
一般的な方法なのか分かりませんが、 [Plugin Inspector](https://docs.unity3d.com/Manual/PluginInspector.html)の `Load on startup` にチェックを入れて `Apply` すると、実行時のメッセージよりもなぜか詳しいメッセージが出ることがある（最初から出して）ので、UnityEditorで開発中は、こちらも有力な方法な気がします。