koudenpaのブログ

趣味のブログです。株式会社はてなでWebアプリケーションエンジニアをやっています。職業柄IT関連の記事が多いと思います。

OpenCvSharp を Azure Functions で使用する際のDLLロード

あるいは、何かしらのNugetパッケージが他の依存DLLをロードや、単に関数内でローカルファイルを読み込みたい場合の注意点をメモする。

OpenCvSharp を Azure Functions で使用する

OpenCvSharp .NET 向けのOpenCVラッパーで、基本的にNugetパッケージを解決するだけでそのプロジェクトでOpenCVを呼び出して画像処理を行うことができる素晴らしいライブラリである。

これが、Azure FunctionsのC#ライブラリ形式での実行環境ではネイティブDLLのロードに失敗して動作しなかった。

// こんな例外が発生した
[Error] Exception while executing function: ImageUpload. 
OpenCvSharp: The type initializer for 'OpenCvSharp.NativeMethods' threw an exception. 
OpenCvSharp: Unable to load DLL 'OpenCvSharpExtern': The specified module could not be found.
(Exception from HRESULT: 0x8007007E).

他のWebアプリケーションやデスクトップアプリケーションとは実行環境の構成が異なるからであるようだった。

これを解決するには、ライブラリがDLLをロードする際に使用しているヘルパーに、実際にDLLが存在しているフォルダのパスを指示してやればよかった。

// OpenCvSharp を使用する前の依存DLLが存在するパスを追加のDLLロードパスとして指示する
WindowsLibraryLoader.Instance.AdditionalPaths.Add(@"D:\home\site\wwwroot\dll\x64");
// 実行環境のbit数に合わせる
//WindowsLibraryLoader.Instance.AdditionalPaths.Add(@"D:\home\site\wwwroot\dll\x86");

// 後は OpenCvSharp でOpenCVを使用できる
var lbpCascade = new CascadeClassifier(Path.Combine(functionAppDirectory, "lbpcascade_animeface.xml"));
using (var faceMat = Mat.FromStream(normalizedImage.CopyToMemoryStream(), ImreadModes.AnyColor))
{
    var faces = lbpCascade.DetectMultiScale(faceMat);
    // なにがしかの処理
}

Azure Functions のアプリケーションフォルダのパス

先のスニペットfunctionAppDirectory という、未定義の変数がある。

DLLに限らず、何かしらの依存ファイルをアプリケーションとして持っており、それをFunction Appで読み込みたい場合は ExecutionContext からアプリケーションの配置パスを取得する。 ExecutionContext#FunctionAppDirectory を参照してやればよい。

先のスニペットでハードコーディングしている D:\home\site\wwwroot\ 自体はFunction Appの既定の配置パスでしかないので、しっかりと実装する場合はこれもコンテキストから実行時のパスを解決しなくてはならない。

ExecutionContext 自体は関数の引数に指定することで実行時にバインドされる。

最小構成だとこのような形になるだろうか。

[FunctionName("SampleFunction")]
public static async Task<HttpResponseMessage> ImageUpload(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "path/to/function")]HttpRequestMessage req,
    TraceWriter log,
    Microsoft.Azure.WebJobs.ExecutionContext context)
{
    var functionAppDirectory = context.FunctionAppDirectory;
    WindowsLibraryLoader.Instance.AdditionalPaths.Add(
        Path.Combine(functionAppDirectory , @"dll\x64");
}

くれぐれも相対パスでカレントフォルダを当てにすることはないように。

所感

はまりポイント

ローカルマシンでFuncton Appを実行していた際には、単にNugetパッケージを解決しただけで動作していたので、Azure上に上げて動作させてエラーした際には「???」状態だった。

同じようにカレントフォルダを当てにしてファイルを読み込んでいたら、Azure上では全然別のフォルダがカレントだった。 ( D:\Windows\System32という無機質なフォルダだった(その時は32bit))

これに限らず、エミューレーションしている環境は本物とは異なる場面があるので気を付けたい。

夜中にフォルダパスに振り回されている様子:

助かったこと

OpenCvSharp が簡単にDLLの読み込みパスを追加できるように構成されていたこと。 スゴい。

得たこと

どんな場合も カレントディレクト は当てにならないので、その実行環境に即した形で目的のパスを解決しなくてはならない。 という当たり前の知見を改めて得た。

得られなかったこと

ExecutionContext に関する一次情報。

docs.microsoft.com

この辺りにサラッと出てきてはいるのだけれど、今一つスッキリするドキュメントは見当たらなかった。

何故だ!? なんにしても動いたので良しとする。