Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ローダ自己展開とモジュール解決

クローディア(目閉じ)

目を覚ました pasta.dll が最初のトークを話すまでには、きちんと決まった段取りがございますの。設定読込、同梱スクリプトの自己展開、require の解決――その舞台裏を順にお見せいたしますわ。

アンソニー(素)

荷ほどきと申しますと、同梱のスクリプトを起動のたびに広げ直すのでございますか。

クローディア(高笑い)

毎回ではございませんわ! 展開済みの版が同梱のものと同じなら、ほどきもせずに素通りいたしますのよ!


この章では、起動シーケンスの実装、フレームワークスクリプトの自己展開、ファイル検出とモジュール名の生成、モジュール解決、設定読込を扱う。

目的と責務

ローダ(PastaLoader)は、ゴーストの設置ディレクトリ(以下、基準ディレクトリ)を受け取り、Lua VM が最初のリクエストに応答できる状態の PastaLuaRuntime を返すまでの段階を順に実行する。

この章が責務を持つのは次の事項である。

  • 起動シーケンスの段階構成と、各段階の失敗が致命か継続か
  • 設定読込(pasta.toml の読込、[loader] とそれ以外のセクションの分離、既定値の補完)
  • フレームワークスクリプトの埋め込み(ビルド時の zip 化)と、起動時の自己展開・版比較
  • .pasta・.lua のファイル検出と、モジュール名の生成
  • モジュール検索パス(package.path)の組立と、require のファイル解決を担う searcher の置換
  • FFI を開いた VM での ffi.load の置き換え(Windows のときだけ)

利用者から見た起動シーケンス・モジュール検索パスの優先順位・失敗時の示され方は 起動シーケンスとモジュール解決 が正であり、この章はそれを実現する仕組みだけを書く。pasta.toml の各キーの意味と既定値は pasta.toml リファレンス が正である。

ほかの章へ委ねる事項は次のとおりである。

構成要素

要素所在役割
PastaLoadercrates/pasta_lua/src/loader/mod.rs起動シーケンスの本体。load(既定の RuntimeConfig)と load_with_config(呼び出し側が渡す RuntimeConfig)を持つ。ディレクトリの準備も行う
PastaConfig・LoaderConfigcrates/pasta_lua/src/loader/config/mod.rspasta.toml の読込と解析。[loader] を型付きの LoaderConfig に、それ以外を custom_fields(TOML の表)に分ける。SHIORI 用の既定値を補完する
装いの読み込みcrates/pasta_lua/src/loader/config/actor_font.rs[actor."名前"] の font を pasta_core の ActorFont で検査し、検査済みのタグ列(文字列)に置き換える
型付きの設定セクションcrates/pasta_lua/src/loader/config/sections.rsLoggingConfig・PersistenceConfig・TalkConfig・GhostConfig・DebugFileConfig と各既定値の関数
ビルドスクリプトcrates/pasta_lua/build.rscrates/pasta_lua/pasta_scripts/ を zip に固めて OUT_DIR に書き出し、その MD5 を環境変数 PASTA_SCRIPTS_MD5 としてコンパイルに渡す
決定論的 zip パッカーcrates/pasta_lua/build_zip.rsソースツリーから毎回バイト単位で同じ zip を作る純粋な関数。ビルドスクリプトとビルド決定論テスト(crates/pasta_lua/tests/build_determinism_test.rs)が共有する
自己展開crates/pasta_lua/src/loader/extract.rssync_pasta_scripts。埋め込んだ zip と MD5 を定数として持ち、版を比較して必要なら展開する
ファイル検出crates/pasta_lua/src/loader/discovery.rsdiscover_files。glob パターンでファイルを集め、基準ディレクトリの外・シンボリックリンク・profile/ 配下を除く
検出の統合と増分処理crates/pasta_lua/src/loader/process.rsdiscover_all_files が .pasta と .lua を検出し、禁止ファイル名と同名衝突を処理する。増分処理(process_incremental)は トランスパイル結果キャッシュ で扱う
CacheManagercrates/pasta_lua/src/loader/cache.rsモジュール名とキャッシュ先の導出(source_to_module_name・source_to_cache_path)。版管理と scene_dic.lua の生成はトランスパイル章で扱う
LoaderContextcrates/pasta_lua/src/loader/context.rsローダからランタイムへ渡す材料(絶対パスにした基準ディレクトリ・検索パス・custom_fields・[loader] transpiled_output_dir)。package.path の文字列を組み立てる
LoaderErrorcrates/pasta_lua/src/loader/error.rs起動シーケンスのエラー。設定・入出力・検出・キャッシュ・自己展開・トランスパイルの部分失敗・ランタイムの構築失敗を区別する
setup_package_pathcrates/pasta_lua/src/runtime/module_registry.rspackage.path を設定し、searcher を置換し、読み込んだファイルの表(@pasta_chunks)を作る。@pasta_config の登録(register_config_module)も同じファイルにある
searchercrates/pasta_lua/src/runtime/searcher.rsinstall_module_searcher。package.loaders の 2 番目(Lua ファイルの searcher)を Rust の実装に置き換える。register_chunk_table が作る @pasta_chunks に、読み込めたファイルを書き足す
RuntimeConfig・lua_requirecrates/pasta_lua/src/runtime/runtime_config.rsVM に入れる標準ライブラリ、解決済みのデバッグ設定、キックの注入口を持つ設定。[loader] ffi が真なら with_ffi が std_ffi を足す。lua_require は Rust から Lua の require を呼ぶ
ffi.load の置き換えcrates/pasta_lua/src/runtime/ffi_load/設置(install)・Lua のラッパ・prepare・名前の規則(resolve.rs)・Windows の呼び出し(win32.rs)。詳細は ffi.load の置き換え
既定の maincrates/pasta_lua/pasta_scripts/main.lua空の表を返すだけの利用者初期化スクリプト。自己展開され、scripts/main.lua があればそちらが優先される
pasta.configcrates/pasta_lua/pasta_scripts/pasta/config.lua@pasta_config を pcall(require, …) で取り込み、get(section, key, default) で値か既定値を返す Lua 側のラッパ。@pasta_config を取得できない環境(テストなど)では空の表を使う

処理とデータの流れ

起動の段階構成

PastaLoader::load_with_config は次の順に進む。段階の番号はコードのログ(Phase N・Stage 1.5)に合わせている。

load_with_config(base_dir, runtime_config)
 0.   基準ディレクトリの存在確認                         … 無ければ DirectoryNotFound(致命)
 1.   PastaConfig::load                                   … pasta.toml の読込と既定値の補完(致命)
 1.5  インスタンスロガーの作成と登録・ログフィルタの更新   … 失敗は既定のログファイルへ切り替えて続行(継続)
      別名表の info ログ(Scene alias table)
 2.   profile/ 配下のディレクトリの作成・キャッシュの版と別名表の確認 … 致命
 2.5  sync_pasta_scripts(フレームワークスクリプトの自己展開) … 失敗は ERROR ログで続行(継続)
 3.   discover_all_files(.pasta と .lua の検出)           … glob の誤り・禁止ファイル名は致命
 4.   process_incremental(トランスパイルと .lua のコピー)  … 1 件でも失敗すれば致命
      孤立キャッシュの検出(警告のみ。削除しない)
 5.   generate_scene_dic(scene_dic.lua の生成)            … 致命
 5.5  (デバッグ有効時だけ)ソースマップの構築               … ファイル単位の失敗は飛ばす
 6.   LoaderContext::from_config → PastaLuaRuntime::from_loader_with_scene_dic … 失敗は Runtime(致命)
  • 0 の直後に、基準ディレクトリで LoadDirGuard を張り、load_with_config を抜けるまで保つ。組み込みで複数のゴーストを読んでも、ローダのログは自分のロガーへ届く(ロギングとエンコーディング)。
  • 1.5 で [logging] の file_path からロガーを作れないとき(profile/ ディレクトリの下にないなど)は、既定のログファイル profile/pasta/logs/pasta.log のロガーに切り替えて登録し、不正と判断した file_path を warn に残して続ける。level・filter は反映される(ロギングとエンコーディング)。
  • 1 の PastaConfig::load が決めた別名表(設定読込)は、1.5 でロガーを登録した直後に info ログ Scene alias table として出す。source は表の出どころ(BuiltinDefault は既定の表、PastaToml は [scene.alias] に書いた行を既定の表に重ねた表)、table は表の中身(OnTalk <- 会話 の形。空の表は (empty))である。ロガーの登録より後に出すため、ゴーストのログファイルに残る。
  • 2 で作るディレクトリは profile/pasta/save・profile/pasta/save/lua・profile/pasta/cache と [loader] transpiled_output_dir である。続けて CacheManager::prepare_cache_dir がキャッシュの版と別名表の指紋を確認し、どちらかが違えばキャッシュを全破棄する(トランスパイル結果キャッシュ)。
  • 2.5 を 3 より前に置くのは、検出とトランスパイルより前、package.path を組む 6 より前に、フレームワークスクリプトをディスク上で最新にしておくためである。
  • 4 のトランスパイルでは、.pasta をパースした後に pasta_file.path をゴーストのフォルダーから見たパス(CacheManager::get_relative_path。Windows では dic\天気.pasta)に置き換えてからトランスパイラに渡す。このパスは生成コードの行の対応(init_scene の 4 つ目の引数。init_scene の引数)を埋めるのにだけ使う。パースのエラー文は置き換える前の絶対パスのままである。
  • 4 の統計(トランスパイル・省略・失敗・コピーの件数)は、[loader] debug_mode が true のときだけ info ログに出る。孤立キャッシュは CacheManager::find_orphaned_caches が debug_mode に関わらず件数と各パスを警告し、debug_mode が true のときはローダが各パスを重ねて警告する。キャッシュへの保存は debug_mode に関わらず行う。
  • 4 と 5.5 は、別名表を持たせた同じ 1 つのトランスパイラ(transpiler_for)を使う。キャッシュの .lua の宣言名と、ソースマップ・デバッグの突合キーの宣言名は、常に同じ別名表から決まる。6 では同じ表を RuntimeConfig.scene_aliases に入れて @pasta_search へ渡すため、宣言と検索にも同じ表が効く(シーン・単語レジストリとシーン検索)。
  • 5.5 の要否は、6 で VM が使うのと同じ判定(pasta.toml の [debug] と環境変数を DebugConfig::from_env で解決した結果)で決める。無効なら何も作らない。
  • 6 の中で VM を作り、モジュールを登録し、main・pasta.shiori.entry・pasta.scene_dic を require する。その順序と失敗の扱いは VM の構築とモジュール登録 が書く。

どの段階の LoaderError も load_with_config の戻り値としてそのまま返り、SHIORI 層がロード失敗として扱う。利用者から見た失敗の示され方は 起動シーケンス を参照する。

設定読込

PastaConfig::load は基準ディレクトリ直下の pasta.toml を読む。ファイルが無ければ ConfigNotFound、読めなければ Io、TOML として解析できなければ Config を返し、いずれも起動を止める。

解析(PastaConfig::parse)は次の順に行う。

  1. 全体を TOML の表として読む。
  2. loader キーを表から取り除き、LoaderConfig へ変換する。各キーは serde の既定値の関数で補われ、型の合わない値は解析エラーになる。loader キーが無ければ LoaderConfig::default() を使う。
  3. [scene.alias] を同じ内容から読み直し、有効な別名表(scene_aliases)とその出どころ(scene_alias_source)を決める。[scene.alias] が無ければ既定の表(SceneAliasTable::builtin_default の 7 件)を使い、あれば SceneAliasTable::with_written で書いたキーだけ既定の別名を書いた配列に置き換える(空の配列なら消すだけ。書かなかったキーの既定は残す。見出しだけなら既定の表と等しく、出どころも既定になる)。型の合わない値・空の名前・別名の重複・置き換え先を別名にも書いた表・残った既定の別名とぶつかる表は解析エラーになり、起動を止める(利用者向けの説明は pasta.toml の [scene])。
  4. 残りの表をすべて custom_fields とする。[scene] も書いたとおりに残り、既定の別名表は書き足さない。[loader] 以外のセクションはこの段階では型付きの値にしない。
  5. [actor."名前"] の font を、actor_font::apply が pasta_core の ActorFont で検査し、custom_fields の中で検査済みのタグ列の文字列に置き換える(font = { color = "90,110,140", bold = true } は \f[color,90,110,140]\f[bold,1]、空の表は空文字列)。属性の書いた順は、同じ内容を toml::de::DeTable で読み直して値の位置から取る。知らない属性・型の合わない値・空の文字列・] か \ を含む値・表でない font は解析エラーになり、起動を止める。エラーの文言にはアクター名・属性名と、取れるときは行番号が入る。同じ規則で pasta_check validate も config-actor-font を報告する(利用者向けの説明は 装い(font)、出し入れは トーク出力とアピアランス)。
  6. apply_shiori_defaults で SHIORI 用の既定値を補完する。

apply_shiori_defaults は custom_fields の [ghost] に、書かれていないキー(talk_interval_min・talk_interval_max・hour_margin・spot_newlines・choice_newlines・after_choice_newlines)だけを GhostConfig::default() の値で書き足す。[ghost] が無ければ表を作って 6 キーとも入れる。書かれている値は上書きしないため、2 回適用しても結果は変わらない。あわせて [actor] が表として存在しなければ警告を 1 行出す(起動は止めない)。補完をここ 1 か所でだけ行うので、Rust 側の消費者と Lua 側(@pasta_config)は同じ補完後の値を見る。

[ghost] 以外のセクションは、使う側が必要になった時点でアクセサ(logging・persistence・talk・debug)を呼び、custom_fields の該当セクションを型付きの構造体へ変換する。セクションが無いとき、または変換に失敗したときは None を返し、使う側が既定値を使う。型の合わない値を含むセクションが丸ごと既定値になるのはこのためである(利用者向けの説明は 値の型が合わないとき)。

アクセサ使う場所
loggingローダの段階 1.5
debugローダの段階 5.5 と VM の構築(from_loader_with_scene_dic。待ち受けの起動と、@pasta_debug の enabled に渡す値)
persistence@pasta_persistence の登録と、ランタイムの破棄時の保存
talk@pasta_sakura_script の登録

custom_fields は LoaderContext に複製されてランタイムへ渡り、register_config_module が Lua の表へ変換して @pasta_config として package.loaded に置く。変換では TOML の整数・浮動小数は Lua の数値に、日時は文字列になり、[actor] の各サブテーブルにはキー名を name として書き込む。作る表は普通の Lua の表である。Lua 側のスクリプトは @pasta_config を直接 require するか、pasta.config の get を通して読む。[loader] は custom_fields に含まれないため Lua からは見えない。利用者から見た @pasta_config は @pasta_config が正である。

フレームワークスクリプトの埋め込み

フレームワークスクリプト(crates/pasta_lua/pasta_scripts/ のツリー全体)は、ビルド時に zip へ固めて DLL に埋め込む。

  1. crates/pasta_lua/build.rs が build_zip::collect でツリーをたどり、ディレクトリとファイルのすべてに cargo:rerun-if-changed を出す。入れ子のファイルの変更も再ビルドの契機になる。
  2. build_zip::build_deterministic_zip が zip を作る。エントリ名はルートからの相対パスを / 区切りにしたもので、名前順に格納する。更新時刻は固定値、Unix 権限は 0o644、圧縮は Deflate の固定レベル(6)である。シンボリックリンクは格納しない。同じ内容のツリーからは常に同じバイト列ができる。
  3. ビルドスクリプトは zip を OUT_DIR/pasta_scripts.zip に書き出し、zip のバイト列の MD5 を PASTA_SCRIPTS_MD5 として公開する。
  4. crates/pasta_lua/src/loader/extract.rs が include_bytes! で zip を、env! で MD5 を、それぞれコンパイル時の定数(EMBEDDED_ZIP・EXPECTED_MD5)として取り込む。

MD5 はツリーのどのファイルの内容が変わっても変わる。クレートの版とは連動しない(トランスパイル結果キャッシュの版がクレートの版で決まるのと対照的である)。

自己展開と版比較

sync_pasta_scripts は自己展開先 profile/pasta/pasta_scripts/ を埋め込みの zip に合わせる。

sync_pasta_scripts(base_dir)
  marker = profile/pasta/pasta_scripts/.md5 を読む
  marker を trim した文字列 == EXPECTED_MD5 ?
    はい   → 何も書かずに Skipped(DEBUG ログ)
    いいえ → deploy(マーカーが無い・読めない場合も含む)

版比較はマーカーの文字列と定数の比較だけで行い、展開済みのファイルを読み直してハッシュを取ることはしない。

deploy は準アトミックに展開する。一時ディレクトリと退避ディレクトリは、同じボリュームに置くため自己展開先の親(profile/pasta/)の下に作る。名前はプロセス ID・時刻(ナノ秒)・プロセス内のカウンタを連結した接尾辞で区別する。

1. profile/pasta/.pasta_scripts.new.<接尾辞>/ に全エントリを展開する(自己展開先にはまだ触れない)
2. 自己展開先があれば profile/pasta/.pasta_scripts.old.<接尾辞>/ へ rename して退避する
3. 一時ディレクトリを自己展開先へ rename する(失敗したら退避を戻す)
4. 退避ディレクトリを削除する(失敗しても無視する)
5. .md5 マーカーに EXPECTED_MD5 を書き込む
  • 1〜3 のどこで失敗しても、自己展開先を直前の状態に戻し、一時ディレクトリを削除して LoaderError::SelfDeploy を返す。
  • マーカーは最後に書く。3 と 5 の間で中断した場合はマーカーが古いまま残り、次回の起動で再び展開される。
  • 展開では zip のエントリ名を enclosed_name で検査し、展開先の外を指す名前を飛ばす。ファイルは解凍した生のバイト列として書き出す。
  • 自己展開先はディレクトリごと置き換わる。自己展開先に置かれた、zip に含まれないファイルは、次に展開が起きたときに消える。

段階 2.5 は SelfDeploy を含むすべてのエラーを ERROR ログに記録して捨て、ディスク上にある既存のスクリプトのまま起動を続ける。

ファイル検出

discover_all_files は [loader] pasta_patterns から .pasta と .lua を検出する。パターンの書き方と既定値は pasta_patterns が正である。

  1. discover_files(base_dir, pasta_patterns) で .pasta を集める。0 件なら警告を出して続ける。
  2. .pasta で終わる各パターンの末尾を .lua に替えたパターンを作る。.pasta で終わらないパターンは警告を出して飛ばす。
  3. 作ったパターンで discover_files を呼び、.lua を集める。ここでのエラーは警告にとどめ、.lua は 0 件として扱う。0 件でも警告は出さない。
  4. .pasta と .lua のどちらかにファイル名が init.lua または init.pasta のものがあれば、LoaderError::InvalidFileName で起動を止める。
  5. .pasta のモジュール名(後述の source_to_module_name)の集合を作り、同じモジュール名の .lua を警告付きで除く。

discover_files は各パターンについて次を行う。

  • パターンが ..・ルート・ドライブ接頭辞を含むなら、警告を出してそのパターンを使わない。
  • 基準ディレクトリとパターンを連結した文字列を glob クレートに渡す。基準ディレクトリの部分は glob::Pattern::escape でエスケープするため、設置先のパスに [・]・*・? が含まれてもそのまま一致する。パターンの誤りと走査中のエラーは LoaderError として返る。
  • 一致したパスのうち、基準ディレクトリの外にあるもの、基準ディレクトリからの途中にシンボリックリンク(Windows のジャンクションを含む)を含むもの、メタデータを読めないもの、profile/ 配下にあるものを除く。

返す一覧はパターンの順・glob の列挙順に並び、重複は取り除かない。

モジュール名の生成

検出したファイルは、基準ディレクトリからの相対パスで pasta.scene.* のモジュール名とキャッシュ先が決まる。CacheManager が 2 つを導出する。

導出手順
source_to_module_name相対パスの先頭の dic を除く(パス要素の境界で一致する場合だけ)→ 拡張子を除く → .・- を _ に置き換える → /・\ を . に置き換える → 先頭に pasta.scene. を付ける
source_to_cache_pathsource_to_module_name のモジュール名の . を / に置き換え、末尾に .lua を付けて <キャッシュ>/ の下に置く
dic/baseware/system.pasta
  モジュール名:  pasta.scene.baseware.system
  キャッシュ先:  <キャッシュ>/pasta/scene/baseware/system.lua
dic/会話.pasta
  モジュール名:  pasta.scene.会話
  キャッシュ先:  <キャッシュ>/pasta/scene/会話.lua
dic/v1.2.pasta
  モジュール名:  pasta.scene.v1_2
  キャッシュ先:  <キャッシュ>/pasta/scene/v1_2.lua
  • .lua も .pasta と同じ規則で名前とキャッシュ先が決まる。拡張子が違うだけの同名ファイルは同じモジュール名になるため、検出の手順 5 で .lua を除く。
  • 非 ASCII の文字はそのまま残す。モジュール名を解決する searcher も UTF-8 のまま扱う(次節)。
  • dic を除くのは、dic がパス要素として完結する場合だけである。dictionary.pasta や dicx/foo.pasta はそのまま残る。 モジュール名の一覧は増分処理で集められ、scene_dic.lua が並べ替えて require する(トランスパイル結果キャッシュ)。require("pasta.scene.baseware.system") は、次節の searcher が検索パスの profile/pasta/cache/lua から pasta/scene/baseware/system.lua を見つけることで解決する。searcher はモジュール名の . をすべてパス区切りに戻すため、キャッシュ先はモジュール名から同じ規則で導出し、ファイル名やディレクトリ名に含まれる . はモジュール名の時点で _ に置き換えておく。

モジュール検索パス

LoaderContext::from_config は基準ディレクトリを std::path::absolute で絶対パスにし、[loader] lua_search_paths と custom_fields を写す。canonicalize を使わないのは、8.3 短縮名やシンボリックリンクを解決すると、package.path から決まる実行時のチャンク名と、ローダが元の基準ディレクトリから作るキャッシュのパス・ソースマップのキーが食い違うためである(デバッガのブレークポイントの照合がこの一致に依存する)。Windows では std::path::absolute は .・.. を字句的に解決し、\\?\ の拡張長接頭辞も付けない。

LoaderContext::generate_package_path は、検索パスの各要素を基準ディレクトリに連結し、区切りを / に正規化して、要素ごとに 2 つのテンプレートを宣言順に並べる。

lua_search_paths = ["scripts", "profile/pasta/pasta_scripts"]
package.path = <基準>/scripts/?.lua;<基準>/scripts/?/init.lua;<基準>/profile/pasta/pasta_scripts/?.lua;<基準>/profile/pasta/pasta_scripts/?/init.lua

setup_package_path はこの文字列を変換せずに package.path へ上書きで設定し、続けて install_module_searcher、Windows では ffi_load::install(ffi.load の置き換え)、register_chunk_table(次節の @pasta_chunks)を呼ぶ。package.cpath は変えない。呼び出しは VM を作って @pasta_search などを登録した直後、@pasta_config 以降の登録と最初の require より前に 1 回だけ行われる(VM の構築とモジュール登録 の手順 3)。

require の解決(searcher)

LuaJIT の package.loaders は preload・Lua ファイル・C ライブラリ・C ライブラリのルートの 4 つの searcher を持つ。install_module_searcher はこのうち 2 番目(Lua ファイル)だけを Rust の実装に置き換える。package.loaders が 4 要素でなければ置き換えずに Err を返し、VM の構築を失敗させる(mlua・LuaJIT の更新で並びが変わったときに、別の searcher を黙って上書きしないための検査である)。package.searchers は LuaJIT では同じ表なので、両方の名前から置き換えが見える。

置き換えた searcher は、LuaJIT 標準の searchpath・loader_Lua と次の点で同じに保つ。

  • 候補の作り方: モジュール名の . を OS の区切り(Windows では \)に置き換え、package.path を ; で分けた各テンプレート(空の要素は飛ばす)の ? をすべてその名前に置き換える。テンプレートの順がそのまま探索順である。
  • 最初に開けた候補を採用する。開けなかった理由(不在・権限・不正なパス)は区別しない。
  • チャンク名は @ に候補のパスを付けたもの。
  • 未検出なら \n\tno file '<候補>' を候補の順に連結した文字列を返し、require がほかの searcher の結果と合わせて module '…' not found: のエラーにする。
  • 開けたが読めない、または構文エラーなら、error loading module '…' from file '…': に原因を続けたメッセージで失敗する。構文エラーの本文には mlua が付ける syntax error: の接頭辞を付けない。

標準との違いは 2 点である。

  • ファイルを開くのに C の fopen ではなく Rust の std::fs::File を使う。Windows では Rust の標準ライブラリがワイド文字の API を使い、長い絶対パスには拡張長の接頭辞を内部で付けるため、パスの長さや ANSI コードページに無い文字に左右されない。searcher 自身はパス長を判定せず、接頭辞の付与・短縮名の変換・canonicalize もしない。
  • package.path とモジュール名を UTF-8 として解釈する(不正なバイト列は置換文字に変換され、その候補は開けずに no file の行に載る)。

本体は 2 層になっている。Rust 関数 find_module は (ローダ, メッセージ, ロード失敗か) の組を返すだけにし、エラーの送出は Lua のラッパ(チャンク名 =pasta_searcher)が error(message, 0) で行う。Rust の Err をそのまま返すと pcall(require, …) が受け取るエラーが文字列ではなくユーザーデータになり、位置情報の付かない標準の文言とも一致しなくなるためである。

find_module は、読み込めた(コンパイルできた)ファイルを package.loaded["@pasta_chunks"] の表に書き足す。表は setup_package_path が register_chunk_table で空のまま置き、Lua からは require("@pasta_chunks") で読める。チャンク名そのものは変えない。

項目内容
キー@ を除いたチャンク名を、LuaJIT がエラー文の位置に出す形にしたもの(lj_debug_shortname と同じく、60 バイト以上なら ... に末尾 56 バイトを続ける。バイトで切るため先頭で UTF-8 の文字が割れることがある)
値(作者の .lua)ゴーストのフォルダーから見たパスを \ 区切りにした文字列(scripts\天気.lua)
値(変換した Lua のキャッシュ)true([loader] transpiled_output_dir の下のファイル。LoaderContext から受け取る)
値(別のファイルが同じキーになった)false(見分けられないため、無いのと同じに扱う)
控えないもの標準ランタイム(profile/pasta/pasta_scripts/ の下)とゴーストのフォルダーの外のファイル。ただしキーの重複の見分けには数える

同じファイルを 2 回読み込んでも値は変わらない。この表を読むのは pasta.shiori.stop だけで、止まるエラーの表記で場所を示すのに使う(止まるエラーの控えと応答)。

Rust 側で登録するモジュール(@pasta_config・@pasta_search など)は package.loaded に直接置くため、require は searcher を通らずにそれを返す。利用者から見た検索の優先順位・候補パスの表記・長パスと非 ANSI パスの保証は モジュール検索パス が正である。

ffi.load の置き換え

LuaJIT の ffi.load は、受け取った名前を LoadLibraryExA(name, NULL, 0) に渡して DLL を読む。ANSI の API なので、ゴーストのフォルダが深い(260 文字以上)・ANSI コードページに無い文字を含むと読めず、名前だけの指定はゴーストのフォルダではなくホスト(SSP)の側を探し、隣に置いた依存 DLL も見つからない。pasta は LuaJIT を改造せず、ffi の表の load だけを「先に LoadLibraryExW で読んでから、元の ffi.load に ANSI のファイル名を渡す」関数に置き換える。Windows は、読み込み済みのモジュールと同じファイル名を LoadLibraryExA に渡すと、探さずにそのモジュールを返す。そのため元の ffi.load は先に読んだ DLL を受け取り、CLibrary の作成・ffi.cdef の解決・後始末は LuaJIT のまま動く。.lua の require に対して searcher を置き換えたのと同じ「ホストの側で包む」方針を DLL へ広げたものである。利用者から見た探すフォルダ・基点・既知の制限は 起動シーケンスとモジュール解決 が正である。

設置

ffi_load::install は setup_package_path の中で、.lua の searcher を置き換えた直後に呼ばれる(Windows のときだけ。#[cfg(windows)])。

  • package.loaded.ffi が無ければ何もしない。FFI は [loader] ffi = true のときだけ開く(RuntimeConfig::with_ffi が std_ffi を足す)ので、既定の VM には何も入らない。
  • ffi.load が関数でなければ、mlua::Error で VM の構築を失敗させる(LuaJIT の構成が想定と違うことを黙って見逃さない)。
  • 参照の集まり DllPins を mlua の app data に置く(参照の保持)。
  • Lua のラッパ(チャンク名 =pasta_ffi_load)に元の ffi.load と Rust の prepare を渡して作り、ffi の表の load へ raw_set する。ほかの FFI の機能と package.* には触らない。

setup_package_path はローダを通る構築(from_loader*)だけが呼ぶ。ローダを通らずに PastaLuaRuntime::with_config へ std_ffi を渡して作った VM の ffi.load は LuaJIT のままである。

呼び出しの流れ

ffi.load(name, ...)                                  … ラッパ(Lua)
  name が数 → tostring(name)
  name が文字列 → prepare(name)                      … Rust。Lua のエラーは投げない
    UTF-8 でない → (nil, nil)
    1. resolve(name, 基点, 探索のフォルダ)
         Ghost { path, file_name } / System { file_name }
         Missing → 理由 = FormatMessageW(ERROR_MOD_NOT_FOUND)
    2. ansi_file_name(file_name)                     … 往復で一致しなければ理由 = pasta の文言
    3. 先読み
         Ghost  → LoadLibraryExW(load_path_form(path), LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)
         System → LoadLibraryExW(file_name, NULL, 0)
         失敗   → 理由 = FormatMessageW(GetLastError())
    4. Ghost だけ: GetModuleHandleW(file_name) が 3 のモジュールか
         違えば FreeLibrary して理由 = pasta の文言
    5. DllPins.pin(module)                           … 持っていれば今回の参照を FreeLibrary
    6. (ANSI のファイル名, nil)
  理由あり   → error("cannot load module '" .. name .. "': " .. 理由, 2)
  pass あり  → return orig(pass, select(2, ...))     … 末尾呼び出し
  それ以外   → return orig(...)                      … 末尾呼び出し
  • 形は searcher と同じく「Rust は値を返すだけ、送出は Lua のラッパ」である。Rust の Err を返すと pcall が受け取る値が文字列でなくなる。
  • ラッパは引数を ... のまま扱う。function(name, ...) の形だと、引数なしの pcall(ffi.load) に nil を足して文言が got nil になり、LuaJIT の got no value と変わる。
  • 数の名前は、LuaJIT(lj_lib_checkstr)と同じく tostring で文字列にしてから文字列の名前と同じに扱う(どちらも lj_strfmt_number なので同じ文字列になる)。数でも Windows の理由が出るので、理由を UTF-8 にするためである。数でも文字列でもない引数(nil・表など)と UTF-8 でない文字列は、元の ffi.load へ同じ引数で渡り、bad argument #1 to 'load' などの文言は LuaJIT のままである。
  • 元の ffi.load は末尾呼び出しで呼ぶ。ラッパの枠が消えるので、元の関数が投げる文言の位置は作者のコードの位置になる。pasta の失敗は error(msg, 2) でラッパを呼んだ作者のコードの位置を付ける。どちらも LuaJIT の lj_err_callermsg と同じ <短いチャンク名>:<行>: の形で、pcall(ffi.load, …) のように C の関数から直接呼ばれたときは位置が付かない。
  • 元の ffi.load に渡すのはパスではなくファイル名(lib/foo なら foo.dll)である。名前だけの LoadLibraryExA は読み込み済みのモジュールを返すので、4 の確かめで「そのファイル名で返るのが今回の DLL であること」を保証しておく。名前だけで OS に探させた DLL(System)には 4 を行わない。

名前の規則(resolve.rs)は次のとおりである。ファイルシステムには is_file と std::path::absolute だけで触り、8.3 短縮名とシンボリックリンクは解かない(canonicalize を使わない)。

名前解き方
拡張子最後の部分(区切りの後ろ)に . が無ければ .dll を足す
名前だけ(/・\・: を含まない)探索のフォルダを順に見て、最初にファイルがあったもの(Ghost)。どこにも無ければ OS の既定の探索に任せる(System)
/・\・: を含む絶対パス(ドライブ文字と区切り・\\ で始まる UNC)はそのまま、それ以外は基点の後ろに \ と名前を文字列としてつなぐ。ファイルがあれば Ghost、無ければ Missing(OS の探索には回さない)
末尾が区切りMissing(lib/ を lib.dll と読まない)
  • 探索のフォルダは LoaderContext::absolute_search_paths、つまり package.path を作るのと同じ lua_search_paths の各要素を基準ディレクトリに連結したもので、宣言順に探す。
  • 基点は基準ディレクトリ(LoaderContext.base_dir)である。Path::join は \foo・C:foo で基点を捨てるため使わない。\foo・/foo・C:foo は基点の下を指し、ドライブの根やドライブのカレントへは出ない。区切りの無い C:foo もパスの扱いにするのは、名前だけの扱いにすると OS がドライブのカレントで探してしまうためである。

LuaJIT と違う点

項目LuaJIT の ffi.loadpasta の置き換え
読み込みLoadLibraryExA(name, NULL, 0)(ANSI)先に LoadLibraryExW(UTF-16)で読む。ゴーストのファイルは LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS で、DLL の隣と既定のフォルダから依存 DLL を探す(カレントと PATH は探さない)。OS に探させる名前はフラグ 0 で、探索は LuaJIT と同じ
名前だけのときに探す場所OS の既定の探索(ホストの EXE の隣など)先に探索のフォルダ(package.path と同じフォルダ)。無ければ OS の既定の探索
相対パスの基点Windows の規則のまま(ホストのプロセスのカレントディレクトリなど)ゴーストの基準ディレクトリ
長いパス・ANSI に無い文字ローダーは長いパスの設定と関係なく 260 文字超えも読むが、パスが ANSI を通るので ANSI に無い文字を含むフォルダは読めないUTF-16 で 260 文字以上のパスにだけ \\?\(UNC は \\?\UNC\)を付けて読む。この形は読む呼び出しにだけ使い、文言とログには出さない
理由の文字コードFormatMessageA の ANSI のままFormatMessageW を UTF-8 にする。言語の指定(0)と末尾の \r\n は同じ。数の名前も tostring してから同じ道を通るので UTF-8 になる
理由のバッファ128 バイト512 個の UTF-16。バッファに入らない長い文言では、LuaJIT は理由が空になり、pasta は文言が出る
pasta だけの失敗—ファイル名が ANSI で表せない(the file name '<ファイル名>' cannot be represented in the ANSI code page)、同名の別 DLL が読み込み済み(a different DLL named '<ファイル名>' is already loaded in this process)。どちらも英語の固定の文言
モジュールの参照CLibrary が持つCLibrary の参照に加えて、DllPins が 1 モジュールにつき 1 つを持つ

失敗の文言の形(<位置>: cannot load module '<名前>': <理由>)は LuaJIT と同じである。区切りのある名前のファイルが無いときの理由は、LuaJIT がそのパスを読んで失敗したときと同じ ERROR_MOD_NOT_FOUND の文言にしている。

参照の保持

DllPins は読み込んだモジュール(HMODULE)の並びで、同じモジュールは 1 つだけ持つ(2 回目の先読みで増えた参照はその場で FreeLibrary する)。mlua の app data に置くので、lua_close の後に落ちる。__gc・ffi.gc の後始末がすべて済んでから Drop が全部を FreeLibrary するため、後始末の途中で DLL のコードが消えることはない。prepare が参照を保った後に元の ffi.load が失敗した場合、その参照は VM を閉じるまで残る。

境界の受け渡し

境界渡す側 → 受け取る側渡すもの所有
ビルド → 実行時crates/pasta_lua/build.rs → extract.rsOUT_DIR/pasta_scripts.zip と PASTA_SCRIPTS_MD5正本はソースツリー。DLL はコンパイル時の定数として 1 版だけを持つ
DLL → ゴーストのディレクトリ自己展開 → ディスクprofile/pasta/pasta_scripts/ のファイルと .md5 マーカー自己展開先とその隣の一時・退避ディレクトリだけを操作する。scripts/ やほかのファイルには触れない
SHIORI 層 → ローダpasta_shiori → PastaLoader::load_with_config基準ディレクトリと RuntimeConfig(キックの注入口を含む)基準ディレクトリの決定とロード失敗の応答は SHIORI 層が持つ(SHIORI 層)
ローダ → ランタイムPastaLoader → PastaLuaRuntime::from_loader_with_scene_dic統合した TranspileContext・LoaderContext・RuntimeConfig・PastaConfig・ロガー・scene_dic.lua のパス・ソースマップPastaConfig はランタイムが保持し、破棄時の保存などで使う
Rust → Lua(設定)register_config_module → @pasta_configcustom_fields を変換した表表は VM が所有する。pasta.toml へは書き戻さない
Rust → Lua(解決)setup_package_path → package.path・package.loaders[2]検索パスの文字列と searcher、@pasta_chunks の表以後の require のファイル解決は Rust の searcher が担い、モジュールの読み込みと実行は Lua が担う。@pasta_chunks は VM が所有し、searcher だけが書き足す
Rust → Lua(FFI)ffi_load::install → ffi.load元の ffi.load と prepare を閉じ込めたラッパ、app data の DllPinsDLL を探して先に読むのは Rust、CLibrary の作成と後始末は LuaJIT。DllPins は VM が所有し、lua_close の後に解放する

不変条件と制約

  • 段階の順序は固定である。自己展開(2.5)は検出(3)とランタイムの構築(6)より前に終わり、package.path の設定と searcher の置換は最初の require より前に終わる。
  • 起動を止めずに警告やエラーのログだけを残す失敗は、ロガーの作成(1.5)、自己展開(2.5)、.lua の検出エラー、.pasta のキャッシュへの保存、ソースマップの構築(5.5)、シーン identity 索引の突合である。段階表で致命とした失敗は LoaderError として起動を止める。
  • .pasta・.lua の読込・パース・トランスパイル・コピーの失敗は、全ファイルを処理し終えてから PartialTranspileError にまとめる。
  • 自己展開の版比較はマーカーの文字列だけで行う。自己展開先のファイルを直接書き換えても、埋め込みの MD5 が変わるまで元に戻らない。フレームワークスクリプトを差し替える経路は、検索パスで先に来る scripts/ に同名のファイルを置くことである。
  • 自己展開先は profile/pasta/pasta_scripts に固定されており、lua_search_paths からは独立している。検索パスからこのディレクトリを外すと、展開は行われてもフレームワークスクリプトは解決されない。
  • キャッシュの出力先(transpiled_output_dir)と検索パスも独立している。pasta.scene.* と pasta.scene_dic は検索パスを通して解決されるため、出力先を変えるときは検索パスにもそれを含める必要がある。また、検索パスで先に来るディレクトリに同じ相対パスのファイルがあれば、そちらが解決される。
  • ファイル検出は profile/ 配下を常に除く。キャッシュや自己展開先が検出の対象になることはない。
  • 設定読込を失敗させるのは、[loader] の型の誤りと、不正な [scene]/[scene.alias] と、不正な [actor."名前"] の font だけである。[scene]/[scene.alias] の不正とは、型の合わない値(scene・alias が表でない、別名が文字列の配列でないなど)・空の名前・別名の重複・置き換え先を別名にも書いた表(連鎖)・既定の別名とのぶつかり・既定の別名を置き換え先に書いた表である。font の不正は 設定読込 の 5 のとおりである。ほかのセクションの型の誤りは、使う時点でそのセクションが既定値になるだけで、エラーにも警告にもならない。
  • ランタイムの構築は std のワイド文字 API でモジュールを開くため、require の解決は設置パスの長さと文字種に依存しない。ゴースト作者のコードが直接呼ぶ io.open などは対象外である(既知の制限)。
  • ffi.load の置き換えは ffi の表の load の 1 つだけを書き換え、LuaJIT のソースには手を入れない。元の ffi.load に渡すファイル名は ANSI で表せるものに限られ、表せなければ読む前に失敗させる。

ソースの所在

  • crates/pasta_lua/src/loader/
  • crates/pasta_lua/build.rs
  • crates/pasta_lua/build_zip.rs
  • crates/pasta_lua/src/runtime/searcher.rs
  • crates/pasta_lua/src/runtime/module_registry.rs
  • crates/pasta_lua/src/runtime/runtime_config.rs
  • crates/pasta_lua/src/runtime/ffi_load/
  • crates/pasta_lua/pasta_scripts/main.lua
  • crates/pasta_lua/pasta_scripts/pasta/config.lua

ffi.load の置き換えのファイルと役割は次のとおりである。

ファイル役割
crates/pasta_lua/src/runtime/ffi_load/mod.rs設置(install)・Lua のラッパ(WRAPPER_CHUNK)・prepare の組み立て・pasta の理由の文言
crates/pasta_lua/src/runtime/ffi_load/resolve.rs名前を読むパスへ解く規則(resolve・拡張子の規則・ANSI の往復の確かめ・\\?\ の形)。unsafe を持たない
crates/pasta_lua/src/runtime/ffi_load/win32.rsunsafe の Windows の呼び出し(LoadLibraryExW・GetModuleHandleW・FreeLibrary・FormatMessageW)と DllPins
crates/pasta_lua/src/runtime/module_registry.rssetup_package_path から ffi_load::install を呼ぶ
crates/pasta_lua/src/runtime/runtime_config.rswith_ffi が [loader] ffi を std_ffi に変える
crates/pasta_lua/src/loader/config/mod.rsLoaderConfig の ffi(既定 false)

埋め込みの zip に固める対象は crates/pasta_lua/pasta_scripts/ のツリー全体である。テストは crates/pasta_lua/src/loader/extract_tests.rs・crates/pasta_lua/src/loader/discovery_tests.rs・crates/pasta_lua/src/loader/config_tests.rs・crates/pasta_lua/tests/build_determinism_test.rs・crates/pasta_lua/tests/loader/ にある。ffi.load の置き換えのテストは、各ファイルの単体テストと crates/pasta_lua/tests/ffi/(テスト用の DLL をその場で作り、元の ffi.load と並べて比べるテストを含む)にある。

経緯


クローディア(にっこり)

目覚めの儀式も、こうして並べれば整然としたものでしょう?

アンソニー(素)

ひとつだけ、段の番号に 1.5 や 2.5 が混じっているのが気になっておりました。

クローディア(したり顔)

あれは、コードのログの番号に合わせてありますの。ログと突き合わせても、迷わずに済みますでしょう?