はじめに
Windowsの操作から、スクリーンショット付きの手順書を作る Local Manual Maker(LMM)を個人開発しています。
ローカルAIをアプリに組み込む際、モデルだけでなく、推論を実行するプログラムや、そのプログラムが必要とするDLLも配布する必要があります。
今回は、そのDLLを同梱していたものの、実際にはPC側に入っていた別のDLLが使われていた話です。
アプリは動いていました。問題に気づいたのは、起動できるかだけでなく、どのプロセスが、どこからDLLを読み込んだかを確認したときでした。
この記事は、2026年10月上旬に行った開発中の検証版の記録を整理したものです。記事作成時に現行版を再実行した結果ではありません。
親アプリと、AIを動かす子プロセス
当時の構成では、LMM本体から別プロセスの llama-server.exe を起動していました。
配置を簡略化すると、次のようになります。
配布フォルダー/
├─ ManualMaker.exe
├─ vcruntime140.dll
├─ vcruntime140_1.dll
├─ msvcp140.dll
├─ msvcp140_1.dll
└─ vision-runtime/
└─ llama-server.exe
Visual C++ランタイムのDLLは、LMM本体と同じ場所に置いていました。一方、実際にAI処理を担当する子プロセスは、ひとつ下の vision-runtime にあります。
同じ配布フォルダーに入っているので、ファイルが同梱されているかだけを見れば、問題は見つかりません。
ただし、親アプリの横にあるDLLを、子プロセスも読み込むとは限りません。
動いていた理由を、読み込み先で確かめる
修正前のテスター用ZIPと評価用ZIPを展開し、子プロセスの読み込み先を確認しました。
| DLL | 修正前の子プロセスでの読み込み先 |
|---|---|
| vcruntime140.dll | WindowsのSystem32 |
| vcruntime140_1.dll | WindowsのSystem32 |
| msvcp140.dll | WindowsのSystem32 |
| msvcp140_1.dll | 観測した処理では未ロード |
Text処理後とVision処理中の確認でも、結果は同じでした。
読み込まれた3つは、配布物に入れたDLLではなく、開発PCのSystem32に既に存在するDLLでした。
つまり、起動とAI処理が成功していても、その結果だけでは「同梱したDLLで動いた」とは言えませんでした。
ここで分かったのは、配布先で必ず失敗するということではありません。まず、開発PCにあるランタイムを使って成功していたという事実です。
見るべきだったのは、子プロセスの実行ファイルの場所
確認したコードでは、子プロセスの実行ファイルと作業ディレクトリは、どちらも vision-runtime でした。DLLの検索先を追加する設定や、PATHの変更もありませんでした。
WindowsのDLL検索には複数の条件がありますが、標準の検索順には、実行ファイルのあるフォルダーやシステムフォルダーなどが含まれます。親アプリの配置フォルダーが、子プロセスのために自動的に追加されるわけではありません。
今回の構成では、LMM本体の横だけを見ていても、子の llama-server.exe が必要とするDLLの配置確認にはなっていませんでした。
検索順は、パッケージ化の有無や検索設定によっても変わります。「EXEの横なら常に最優先」と単純化せず、実際の読み込み先で確認しました。
参考:Microsoft Learn:Dynamic-link library search order
子プロセスの横にも配置して、もう一度確認する
修正では、ZIPを作るパッケージスクリプトを変更し、同じ4つのDLLを vision-runtime にも配置しました。
配布フォルダー/
├─ ManualMaker.exe
├─ ランタイムDLL 4つ
└─ vision-runtime/
├─ llama-server.exe
├─ vcruntime140.dll
├─ vcruntime140_1.dll
├─ msvcp140.dll
└─ msvcp140_1.dll
親アプリ側のDLLは、別の依存関係に備えた従来の配置として残しています。
変更したのは梱包処理です。製品コードの変更、永続的なPATHの変更、System32へのコピーは行っていません。修正前後の配布内容の差分も、vision-runtime 配下への4ファイル追加だけでした。
再梱包したZIPを展開して確認すると、先ほどの3つのDLLは、展開先の vision-runtime から読み込まれるようになりました。テスター用・評価用とも、TextとVisionの処理完了も確認できました。
msvcp140_1.dll は、このときも未ロードでした。ただし、観測した処理で使われなかったことと、すべての実行経路で不要なことは別です。今回は削除していません。
System32に対象DLLがないWindows 10でも確認する
開発PCで読み込み先が変わった後、Windows 10の仮想マシンでも確認しました。
当時の環境は次のとおりです。
- Windows 10 Enterprise 22H2、x64
- Build 19045.6456
- メモリ約8GiB、論理CPU 2
- VC++再頒布可能パッケージの登録を、レジストリとアンインストール一覧で確認できない状態
- System32に、今回対象とした4つのDLLがない状態
- VCLibsフレームワークは導入済み
修正後のテスター用ZIPと評価用ZIPでは、子プロセスが同梱先の vision-runtime から3つのDLLを読み込み、TextとVisionの処理が成功した記録が残っています。
ここまで確認できたことで、開発PCのSystem32にある対象DLLを使わずに動くことを、別環境でも確かめられました。
ただし、VCLibsまで一切入っていない環境ではありません。「ランタイムが何もない完全なクリーン環境」とは区別しています。
ZIPとMSIXでは、供給元が違っていた
同じ仮想マシンで確認した検証用MSIXでは、子プロセスはVCLibsフレームワークからDLLを読み込んでいました。
| 配布形態 | 確認した3つのDLLの供給元 |
|---|---|
| 修正後のZIP | 展開先のvision-runtime |
| 検証用MSIX | VCLibsフレームワーク |
同じアプリでも、配布形態によって依存関係の供給元は異なります。ZIPで確認できたことを、そのままMSIXの確認結果として扱わず、別々に記録しました。
また、DLLをアプリと一緒に配布する場合は、配布後の更新も考える必要があります。Microsoftの説明でも、ローカル配置したライブラリは配布側で更新する必要があるとされています。
参考:Microsoft Learn:Deployment in Microsoft C++
今回の検証で残したこと
今回、確認を進めるうえで役立ったのは、次の点でした。
- 親アプリと子プロセスを分けて確認する
- DLLの存在確認だけでなく、実際の読み込み元を記録する
- 開発PCに入っているランタイムの影響を確認する
- 修正前後の配布内容の差分を残す
- ZIPとMSIXを、それぞれの依存関係で確認する
アプリが動くと、つい次の機能や見た目の調整に進みたくなります。
今回は、その前に読み込み先を確認したことで、開発PCの環境に助けられていた部分を見つけられました。
ローカルAIの配布では、モデルの容量や推論速度に目が向きがちです。それと同じくらい、推論を動かすプログラムが、配布先で何を使って動くのかも確認しておく必要があると感じた検証でした。