起動シーケンスとモジュール解決
ごきげんよう。ゴーストが「何も言わずに黙り込む」――これほど不気味で、これほど心細いことはございませんわね。 けれど安心なさいまし。pasta は黙って死んだりはいたしませんの。どこを探し、どの順に読み込み、どこで力尽きたのか、必ず足跡を残しますわ。 その足跡の読み方を、わたくしが手ほどきいたします。さあ、恐れずに参りましょう。
このページでは、pasta.dll がゴーストを起動するまでに何をどの順で読み込むのか、どこまでが仕様上の保証でどこからが制限なのか、そして起動に失敗したときにどう切り分けるのかを示す。
以下では、ベースウェアが SHIORI に渡すゴーストの設置ディレクトリ(ghost/master/。pasta.dll が置かれている場所)を基準ディレクトリと呼ぶ。本ページのすべての相対パスはこのディレクトリからの相対である。
1. モジュール検索パス
Lua の require が解決するモジュールは、次の 5 つのディレクトリをこの優先順位で探索する。先に見つかったものが勝つ。同名のモジュールを上位のディレクトリに置けば、下位のものを上書きできる。
| 順 | ディレクトリ | 役割 |
|---|---|---|
| 1 | profile/pasta/save/lua | 保存領域に置かれた Lua |
| 2 | scripts | ゴースト作者が書く利用者スクリプト |
| 3 | profile/pasta/pasta_scripts | pasta.dll が起動時に自己展開する内蔵スクリプト |
| 4 | profile/pasta/cache/lua | .pasta をトランスパイルしたシーンモジュールのキャッシュ |
| 5 | scriptlibs | スクリプトライブラリ |
探索するディレクトリは pasta.toml の [loader] lua_search_paths で変更できる。上表は既定値である。
各ディレクトリでは、次の 2 つのファイル名パターンをこの順で試す。
| 順 | パターン | require "pasta.shiori.entry" の例(scripts の場合) |
|---|---|---|
| 1 | ?.lua | scripts/pasta/shiori/entry.lua |
| 2 | ?/init.lua | scripts/pasta/shiori/entry/init.lua |
? にはモジュール名が入る。モジュール名の . はディレクトリの区切りへ置き換えられる。
なお Windows では、? に代入される部分の区切りは \ になる一方、検索パス側の区切りは / に正規化されている。このため実際のエラーメッセージに出る候補パスは …/scripts/pasta\shiori\entry.lua のように / と \ が混在した表記になる。表記の違いであって別の場所を探しているわけではないので、no file '…' の行を読むときに食い違いと誤解しないこと。
設置パスの長さ・文字種に依存しない
モジュールの解決は、ゴーストの設置場所がどれほど深くても、パスにどのような文字が含まれていても同じ結果になる。
- 絶対パスが 260 文字を超える場所に設置しても
requireは成功する。ランタイムは独自のパス長上限を持たない。 - システムの ANSI コードページで表現できない文字(利用者名やフォルダ名に含まれる各国語の文字など)がパスに含まれても
requireは成功する。 - モジュールが見つからなかった場合のエラーメッセージには、探索した候補パスが
no file '…'の行として探索順に並ぶ。非 ASCII の文字も欠落・置換されずにそのまま表記される。どこを探したかがそのまま読み取れる。
ただし、これはランタイムが解決するモジュール(require)についての保証である。ゴースト作者のコードが直接呼ぶ Lua 標準のファイル入出力は対象外である(第 3 節を参照)。
実測(SSP 2.8.98)でも確認している。解決先が 260 文字を超えるモジュールを含むゴースト、ANSI コードページに存在しない文字(ハングル・絵文字)を含む設置パスのゴースト、およびその両方を同時に満たすゴースト(設置パスが全段非 ANSI で、require の解決先が 263 文字)が、いずれも起動して応答した。なおホストが短縮名を渡してくる場合があり、その条件は第 3 節に記す。
挙動変更: package.path を UTF-8 として解釈する
検索パスの設定値(package.path)は UTF-8 の文字列として解釈される。これは従来からの挙動変更である。
ゴースト作者のコードが package.path へ独自のエントリを追記している場合、その文字列が UTF-8 でなければ(たとえば ANSI のバイト列を連結している場合)、そのエントリは解決に使えず、未検出時の no file '…' 行に載るだけになる。package.path へ追記する場合は UTF-8 の文字列を使うこと。
2. 起動シーケンス
SHIORI の load(あるいは loadu)を受け取ったあと、ランタイムは次の順にモジュールを読み込む。上から順に実行され、致命に分類されたモジュールのロードに失敗した時点で起動は中止される。
| 順 | 読み込むもの | 失敗時の扱い | 備考 |
|---|---|---|---|
| 1 | main(利用者初期化スクリプト) | 致命 | 既定の main.lua が内蔵スクリプトとして起動時に自己展開されるため、不在は正常な状態ではない |
| 2 | pasta.shiori.entry(SHIORI 応答関数) | 致命 | SHIORI.load / SHIORI.request / SHIORI.unload を定義する唯一の場所 |
| 3 | pasta.scene_dic(シーン辞書) | 致命 | トランスパイル済みシーンモジュールの読み込みと確定を行う |
| 4 | シーン identity 索引の突合 | 継続 | モジュールのロードではない。デバッグ有効時のみ実行される best-effort 処理で、失敗しても起動は続く |
致命に分類されたロードが失敗した場合、ランタイムは次の 3 経路で原因を示す。
- ホストへ返す
loadの戻り値が失敗になる。 - 以降の応答を要するリクエスト(GET)に対し、500 応答と
X-ERROR-REASONヘッダで原因を返す。黙って既定の空応答を返すことはない。 - ログファイルに、失敗したモジュール名と根本原因を記録する。
このうち 経路 3 だけがホストの実装に依存しない。経路 2 は「ホストが load の失敗後もリクエストを送ってくる」ことが前提であり、送ってこないホストでは発動しない。実測では SSP 2.8.98 は load が失敗を返した時点でゴーストの起動を打ち切り、リクエストを一切送らなかった。したがって SSP 環境で原因を読む場所は、実質ログファイルだけになる(第 4 節)。
挙動変更: main のロード失敗は致命になった
main のロード失敗は、従来は警告を残して起動を続行していたが、致命として起動を中止するよう変更された。
壊れた scripts/main.lua を抱えたまま動いていたゴーストは、この変更後はロードに失敗する。ただし黙って壊れるのではなく、上記 3 経路で原因が示される。辞書登録やイベントハンドラが欠けたまま無言で動き続けるよりも、原因を出して止まるほうが復旧が早いという判断による。
3. 既知の制限
loadu を呼ばないホストでは非 ANSI の設置パスを扱えない
SHIORI の DLL には、設置パスを UTF-8 で受け取る初期化入口 loadu がある(SSP 2.6.92 以降が対応する)。pasta.dll はこれを実装しており、loadu で初期化されたあとに従来の load が呼ばれた場合は無視して初期化済みの状態を保つ。
loadu を呼ばないホストでは、設置パスはシステムの ANSI コードページでランタイムへ渡される。このため ANSI コードページで表現できない文字を含む設置パスは、その時点で欠落しており、ランタイム側では回復できない。ホスト側の制約であり、pasta.dll をどれだけ堅牢にしても解消できない。該当する場合は、ホストを loadu 対応版へ更新するか、設置パスを ANSI で表現できる文字に限る必要がある。
設置パスが 260 文字を超えると、ホストが短縮名を渡すことがある
ランタイム側に長さの上限は無い(第 1 節)。しかしその手前で、ホストがゴーストを登録する時点で設置パスが 8.3 短縮名に置き換わることがある。Windows のフォルダ選択は MAX_PATH(260 文字・終端を含む)のバッファを前提とする経路があり、そこに収まらないパスは短縮名で返される。ホストはそれをそのまま保存し、以後 loadu にもその短縮名を渡す。
この場合、長いパスはランタイムに届かないため、長パス対応は発動しない(そして発動する必要も無い)。実測では、285 文字のフォルダへ設置したゴーストは 104 文字の短縮名としてランタイムへ渡された。191 文字のフォルダでは短縮されず、そのまま渡された。
短縮名はボリューム上に 8.3 名が存在して初めて成立する点に注意する。次の条件では短縮名が作られず、本来の長いパスがそのままランタイムへ渡る。
- 8.3 名の生成を無効化したボリューム(システムボリューム以外は既定で無効なことがある)
- exFAT / ReFS など 8.3 名を持たないファイルシステム
- ネットワーク共有(UNC)
つまり、同じゴーストを同じ深さに置いても、ボリュームの設定次第でランタイムへ渡るパスが変わる。ランタイムはどちらでも動作するが、ホストやゴースト作者のコードが 260 文字を前提にしている場合は、後者の条件で初めて問題が表面化しうる。
Lua 標準のファイル入出力は長パス・非 ANSI パスに対応しない
ゴースト作者のコードが直接呼ぶ次の関数は、OS の narrow API を使うため、絶対パスが 260 文字を超えるファイル、および ANSI コードページ外の文字を含むパスのファイルを扱えない。
io.openloadfiledofilepackage.searchpath
これらは本ランタイムの改善対象外であり、制限として残る。実測でも、絶対パスが 260 文字を超える場所へ設置したゴーストでは、ゴースト側の SHIORI.unload 内で呼ばれた io.open が失敗することが確認されている。
データの永続化にこれらを使ってはならない。永続化は @pasta_persistence モジュールを用いること(公開モジュール API を参照)。同モジュールはランタイム側でファイルを扱うため、設置パスの長さ・文字種の影響を受けない。
4. ゴーストが起動しない・喋らないとき
ゴーストがまったく反応しない場合、起動シーケンスのどこかで失敗している可能性が高い。次の順で切り分ける。
なお、ゴーストは起動しているがデバッガが接続できない場合は、接続できないとき(トラブルシューティング) を参照する。
手順 1: ログファイルの fatal=true の行を読む
まずログファイルを読む。 起動失敗を示す 3 経路のうち、ホストの実装に依存しないのはログだけである(第 2 節)。ホストによっては load の失敗後にリクエストを送らず、500 応答そのものが発生しない。実測では SSP 2.8.98 がこれに該当し、ゴーストは起動されず、ホスト側にはファイルとして何も残らなかった。
ログファイルは既定で次の場所にある。
profile/pasta/logs/pasta.log
pasta.toml の [logging] file_path で変更できるが、指定できるのは基準ディレクトリからの相対パスで、かつ profile から始まる場所に限られる。.. を含むパスも受け付けない。この条件を満たさない値(例: logs/pasta.log)を設定するとログの初期化自体が拒否される。
起動モジュールのロードに失敗すると、module(モジュール名)と fatal(致命なら true)のフィールドを持つ error レベルの行が記録される。まず fatal=true を含む行を探す。その行には根本原因の全文が、複数行のメッセージとスタックトレースを含めて欠落なく記録されている。X-ERROR-REASON で足りないときは、ここを読む。
起動モジュールのロード失敗は必ず fatal=true で記録される。一方、継続扱いの処理(シーン identity 索引の突合)の失敗は、module / fatal のフィールドを持たない warn 行(scene identity index join failed を含む)として記録される。したがって fatal フィールドの有無で両者を区別できる。
あわせて、ロードの締めくくりに次の 1 行が記録される。
INFO pasta::windows: SHIORI load entry completed entry="loadu" loaded=false
entry はホストが呼んだ初期化入口(loadu または load)、loaded はロードの成否である。ログにこの行がまったく無い場合は、ランタイムがロード処理に入る前の段階(DLL の読み込み、設置パスのデコード)で止まっている。entry の値は、非 ANSI の設置パスを扱えているかの判断にも使える(第 3 節)。
手順 2: ホストの SHIORI 通信ログで 500 応答を読む
この手順はホストが load の失敗後もリクエストを送ってくる場合にのみ成立する。 送ってこないホスト(実測: SSP 2.8.98)では応答自体が発生しないので、手順 1 と手順 3 だけで切り分ける。
ホストの SHIORI 通信ログを開き、pasta.dll が返している応答を確認する。起動に失敗している場合、応答は次の形になる。
SHIORI/3.0 500 Internal Server Error
Charset: UTF-8
X-ERROR-REASON: Load error: ... failed to load startup module 'pasta.shiori.entry' ...
X-ERROR-REASON の値は必ず単一行であり、改行を含まない。値には、失敗した起動モジュールの名前と根本原因が先頭から読める順で入る。スタックトレースはヘッダには含まれない(全文はログファイルに残る)。
手順 3: 典型的な原因を読み分ける
| メッセージに現れるもの | 典型的な原因 | 対処 |
|---|---|---|
failed to load startup module 'main' + 構文エラー | scripts/main.lua の構文エラー | 示された行番号を修正する。エラーにはファイル名と行番号が付く |
failed to load startup module 'pasta.shiori.entry' + 実行時エラー | scripts/pasta/shiori/entry.lua で内蔵の entry.lua を上書きしており、その内容に誤りがある | 上書きの必要が無ければ、そのファイルを削除して内蔵版に戻す |
module 'X' not found: + no file '…' の並び | モジュール未検出 | no file の行が実際に探したパスの全リストである。意図した置き場所が並んでいるか、ファイル名・モジュール名の綴りが一致しているかを照合する |
error loading module 'X' from file '…' | ファイルは見つかったが、構文エラーまたは読み込みに失敗した | 示されたファイルを確認する |
no file '…' の並びは、第 1 節の優先順位とファイル名パターンの組み合わせがそのまま出力されたものである。置いたつもりのファイルがリストのどのパスとも一致しないなら、置き場所かモジュール名のどちらかが間違っている。
ね、黙り込んだように見えても、ゴーストはちゃんと悲鳴を上げておりますのよ。あとはそれを聞き取ってあげるだけですわ。 フンッ、別にあなたのゴーストを心配していたわけではありませんけれど……原因が分かってしまえば、直すのは造作もないこと。さあ、胸を張って起動させてやりましょう!