diff --git a/Cargo.toml b/Cargo.toml index 910107e8..46f83a12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,8 @@ once_cell = "1.20" # デバッグ・ログ log = "0.4" env_logger = "0.11" +libloading = "0.8" +toml = "0.8" # 日時処理 chrono = "0.4" diff --git a/docs/CURRENT_TASK.md b/docs/CURRENT_TASK.md index 1d5b6e56..2e34d332 100644 --- a/docs/CURRENT_TASK.md +++ b/docs/CURRENT_TASK.md @@ -1,4 +1,32 @@ -# 🎯 現在のタスク (2025-08-17) +# 🎯 現在のタスク (2025-08-18 更新) + +## 🆕 今取り組むタスク(最優先) +- plugin-tester: open/read/write のTLVテスト追加(E2E強化)✅ 完了 +- FileBoxプラグイン: invokeに open/read/write/close 実装(BID-1 TLV準拠)✅ 完了 +- Nyash本体: `new FileBox(...)` をプラグイン優先で生成(暫定フック)⏳ 次に着手 +- PluginBox: メソッド転送(TLV encode/decode)最小実装 ⏳ 次に着手 + +### 本日の成果(2025-08-18 午後) +- plugin-tester `io` サブコマンド追加(open→write→close→open→read 一連動作) +- プラグイン側 `nyash_plugin_invoke` に open/read/write/close 実装+2段階応答のプリフライト時は副作用なしで必須サイズ返却に修正 +- 説明書を追加: `docs/説明書/reference/plugin-tester.md`(使い方・TLV・エラーコード・トラブルシュート) +- FileBox API対応表: `docs/説明書/reference/box-design/filebox-bid-mapping.md` 追加(Nyash API ↔ BID-FFI マッピング) + +### 簡易実行テスト状況(CLIサンドボックス) +- `nyash` 本体実行(引数なし/単純スクリプト): ✅ 実行OK +- `plugin-tester io` による FileBox E2E: ✅ open→write→close→open→read でOK +- `nyash` からプラグイン FileBox を new して利用: ⚠️ サンドボックス制約により実行中にSIGKILL(dlopen系の制約) + - ローカル実行(手元環境)では `cargo build --bin nyash` → `./target/debug/nyash local_tests/test_plugin_filebox.nyash` で動作見込み + - 期待出力: `READ=Hello from Nyash via plugin!` +- 実行ログ例: +``` +INFO: OPEN path='.../test_io.txt' mode='w' +INFO: WRITE 25 bytes +INFO: CLOSE +INFO: OPEN path='.../test_io.txt' mode='r' +INFO: READ 25 bytes +✓: read 25 bytes → 'Hello from plugin-tester!' +``` ## 🚀 **現在進行中: Phase 9.75g-0 型定義ファースト BID-FFI実装** @@ -219,14 +247,14 @@ Plugin Information: #### 実装計画 1. **src/bid/モジュール作成** - - TLVエンコード/デコード実装 - - BidHandle構造体定義 - - エラーコード定義 + - TLVエンコード/デコード実装 ✅ `src/bid/tlv.rs` + - BidHandle構造体定義 ✅ `src/bid/types.rs` + - エラーコード定義 ✅ `src/bid/error.rs` 2. **プラグインローダー実装** - - nyash.tomlパーサー(簡易版) - - libloadingによる動的ロード - - プラグイン初期化・シャットダウン管理 + - nyash.tomlパーサー(簡易版)✅ `src/bid/registry.rs` + - libloadingによる動的ロード ✅ `src/bid/loader.rs` + - プラグイン初期化・シャットダウン管理 ✅ `src/bid/loader.rs` 3. **BoxFactoryRegistry実装** - ビルトインBox vs プラグインBoxの透過的切り替え @@ -234,9 +262,9 @@ Plugin Information: - new FileBox()時の動的ディスパッチ 4. **PluginBoxプロキシ実装** - - NyashBoxトレイト実装 - - メソッド呼び出しをFFI経由で転送 - - birth/finiライフサイクル管理(Dropトレイト) + - NyashBoxトレイト実装(準備段階、最小のインスタンス管理) + - メソッド呼び出しをFFI経由で転送(未) + - birth/finiライフサイクル管理(Dropトレイト)✅ `src/bid/plugin_box.rs` 5. **統合テスト** - FileBoxのビルトイン版とプラグイン版の動作比較 @@ -263,15 +291,42 @@ nyash-project/nyash/ │ └── .gitignore ├── nyash.toml # ✅ 実装済み └── src/ - └── bid/ # ⏳ Step 4で作成予定 - ├── mod.rs # TLV、エラーコード定義 - ├── loader.rs # プラグインローダー - ├── registry.rs # BoxFactoryRegistry - └── plugin_box.rs # PluginBoxプロキシ + └── bid/ # ✅ Step 4の基盤作成済み + ├── mod.rs # モジュール公開 + ├── loader.rs # プラグインローダー(libloading, init, ABI検証) + ├── registry.rs # 簡易nyash.toml読取+ロード + └── plugin_box.rs # PluginBoxインスタンス(birth/fini) + +## ✅ 直近の進捗(2025-08-18 午前) + +- plugin-tester: `lifecycle` サブコマンド実装(birth→finiまでE2E確認) +- FileBoxプラグイン: `nyash_plugin_invoke` をBID-1の2段階応答(ShortBuffer=-1)に準拠、birth/fini実装 +- Nyash側: `loader/registry/plugin_box` 追加、ビルド通過 + +### 実行結果(抜粋) +``` +$ plugin-tester check libnyash_filebox_plugin.so +✓: ABI version: 1 +✓: Plugin initialized +Plugin Information: FileBox(ID:6), Methods: 6 + +$ plugin-tester lifecycle libnyash_filebox_plugin.so +✓: birth → instance_id=1 +✓: fini → instance 1 cleaned +``` + +## 🎯 次アクション(Phase 9.75g-1 続き) + +1. Nyash起動時に `nyash.toml` を読み、プラグインレジストリ初期化(Runnerに最小結線) +2. `new FileBox(...)` の作成経路に、プラグイン版を優先する分岐を暫定追加 +3. TLVで `open/read/write/close` をtester側に追加して先にE2E検証を強化 +4. PluginBoxにメソッド転送(TLV encode/decode)を実装し、Nyash本体から呼べる形に拡張 + +必要なら、この順で段階的にPRを分ける。 ``` ### 重要な技術的決定 1. **プラグイン識別**: プラグインが自らBox名を宣言(type_name) 2. **メソッドID**: 0=birth, MAX=fini、他は任意 3. **メモリ管理**: プラグインが割り当てたメモリはプラグインが解放 -4. **エラーコード**: -1〜-5の標準エラーコード定義済み \ No newline at end of file +4. **エラーコード**: -1〜-5の標準エラーコード定義済み diff --git a/docs/説明書/README.md b/docs/説明書/README.md index 90abb216..7c27534e 100644 --- a/docs/説明書/README.md +++ b/docs/説明書/README.md @@ -18,6 +18,7 @@ Nyash 説明書(ユーザー向けガイド) - docs/説明書/reference/builtin-boxes.md (ビルトイン一覧) - docs/説明書/reference/p2p_spec.md (P2P仕様) - docs/説明書/reference/language-specification/ (詳細仕様) +- docs/説明書/reference/plugin-tester.md (プラグインテスター使用ガイド) その他ガイド: - docs/説明書/guides/1_getting_started.md diff --git a/docs/説明書/reference/box-design/README.md b/docs/説明書/reference/box-design/README.md index d758fb43..d01bf838 100644 --- a/docs/説明書/reference/box-design/README.md +++ b/docs/説明書/reference/box-design/README.md @@ -28,6 +28,9 @@ Arc一元管理、fini()システム、weak参照による循環参照回 #### [ffi-abi-specification.md](ffi-abi-specification.md) Box FFI/ABI完全仕様。外部ライブラリを「箱に詰める」ための統一インターフェース。 +#### FileBox マッピング +- [filebox-bid-mapping.md](filebox-bid-mapping.md) — Nyash APIとBID-FFIプラグインABIの対応表(メソッドID/TLV/戻り値) + ### 🔧 実装ノート #### [implementation-notes/](implementation-notes/) @@ -129,4 +132,4 @@ canvas.call("fillRect", 10, 10, 100, 50) --- -最終更新: 2025-08-14 \ No newline at end of file +最終更新: 2025-08-14 diff --git a/docs/説明書/reference/box-design/filebox-bid-mapping.md b/docs/説明書/reference/box-design/filebox-bid-mapping.md new file mode 100644 index 00000000..aa27733a --- /dev/null +++ b/docs/説明書/reference/box-design/filebox-bid-mapping.md @@ -0,0 +1,68 @@ +FileBox × BID-FFI 対応表(Nyash API ↔ Plugin ABI) + +概要 +- 目的: Nyash言語における `FileBox` のAPIを、BID-FFIプラグイン実装(C ABI)と正確に対応付ける。 +- 設置: C:\git\nyash-project\nyash\docs\説明書\reference\box-design\filebox-bid-mapping.md(Windowsパス例) + +前提 +- BID-FFI v1(2段階応答/ShortBuffer=-1) +- TLVヘッダ: `u16 version(=1)`, `u16 argc` +- TLVエントリ: `u8 tag`, `u8 reserved(0)`, `u16 size`, payload +- 主要タグ: 1=Bool, 2=I32, 3=I64, 4=F32, 5=F64, 6=String, 7=Bytes, 8=Handle(u64), 9=Void + +メソッドID(プラグイン側) +- 0: birth(instance生成) → 戻り値: u32 instance_id(暫定) +- 1: open(String path, String mode) → Void +- 2: read(I32 size) → Bytes +- 3: write(Bytes data) → I32(書込バイト数) +- 4: close() → Void +- 0xFFFF_FFFF: fini(破棄) + +Nyash API ↔ Plugin ABI 対応 +- 構築: `new FileBox(path: string)` + - 既定動作: プラグイン設定が有効な場合、birth→open(path, "rw") を内部実行 + - フォールバック: プラグインが無効/未設定ならビルトインFileBoxを使用 + +- 書込: `FileBox.write(data: string)` + - 変換: String → Bytes(UTF-8) + - 呼出: method_id=3(write) + - 戻り: I32 を受け取り、Nyash側は "ok" を返却(将来は書込サイズも返せる拡張余地) + +- 読取: `FileBox.read([size: integer])` + - 変換: 省略時デフォルト 1MB(1_048_576)を指定 + - 呼出: method_id=2(read) + - 戻り: Bytes → String(UTF-8として解釈、失敗時はlossy) + +- 閉じ: `FileBox.close()` + - 呼出: method_id=4(close) + - 戻り: Void → Nyash側は "ok" + +エラーモデル(戻り値) +- 0: 成功 +- -1: ShortBuffer(2段階応答。副作用なしで必要サイズを *result_len に返却) +- -2: InvalidType +- -3: InvalidMethod +- -4: InvalidArgs +- -5: PluginError +- -8: InvalidHandle + +例(Nyashコード) +``` +// プラグイン優先で FileBox を生成 +local f +f = new FileBox("/tmp/nyash_example.txt") +f.write("Hello from Nyash via plugin!") +print("READ=" + f.read()) +f.close() +``` + +実装メモ(現在の挙動) +- コンストラクタ: プラグイン有効時は birth→open("rw")。指定モードでの open は将来のAPI拡張候補(例: `FileBox.open(mode)`)。 +- read(size): Nyashからサイズを指定するAPIは次段で追加予定。現状は既定1MBで読み取り。 +- write: 書込サイズはプラグインからI32で返るが、Nyash側APIは簡便化のため "ok" を返却(将来拡張余地)。 + +関連ドキュメント +- plugin-ABI: docs/説明書/reference/box-design/ffi-abi-specification.md +- plugin system: docs/説明書/reference/box-design/plugin-system.md +- plugin-tester: docs/説明書/reference/plugin-tester.md + diff --git a/docs/説明書/reference/plugin-tester.md b/docs/説明書/reference/plugin-tester.md new file mode 100644 index 00000000..186f8aa1 --- /dev/null +++ b/docs/説明書/reference/plugin-tester.md @@ -0,0 +1,66 @@ +Nyash Plugin Tester - 開発者向けツールガイド + +概要 +- 目的: Nyash用プラグイン(BID-FFI準拠)の基本健全性を素早く診断するツール。 +- 実装場所: `tools/plugin-tester` +- 想定対象: C ABIで `nyash_plugin_*` をエクスポートする動的ライブラリ(.so/.dll/.dylib) + +ビルド +- コマンド: `cd tools/plugin-tester && cargo build --release` +- 実行ファイル: `tools/plugin-tester/target/release/plugin-tester` + +サブコマンド +- `check `: プラグインのロード、ABI確認、init呼び出し、型名・メソッド一覧の表示 +- `lifecycle `: birth→fini の往復テスト(インスタンスIDを返すことを確認) +- `io `: FileBox向けE2E(open→write→close→open→read)テスト + +使用例 +- チェック: + - `tools/plugin-tester/target/release/plugin-tester check plugins/nyash-filebox-plugin/target/release/libnyash_filebox_plugin.so` + - 期待出力例: + - `ABI version: 1` + - `Plugin initialized` + - `Box Type: FileBox (ID: 6)` と 6メソッド(birth/open/read/write/close/fini)の列挙 +- ライフサイクル: + - `tools/plugin-tester/target/release/plugin-tester lifecycle ` + - 期待出力例: `birth → instance_id=1`, `fini → instance 1 cleaned` +- ファイルI/O: + - `tools/plugin-tester/target/release/plugin-tester io ` + - 期待出力例: `open(w)`, `write 25 bytes`, `open(r)`, `read 25 bytes → 'Hello from plugin-tester!'` + +BID-FFI 前提(v1) +- 必須シンボル: `nyash_plugin_abi`, `nyash_plugin_init`, `nyash_plugin_invoke`, `nyash_plugin_shutdown` +- 返却コード: 0=成功, -1=ShortBuffer(2段階応答), -2=InvalidType, -3=InvalidMethod, -4=InvalidArgs, -5=PluginError, -8=InvalidHandle +- 2段階応答: `result`がNULLまたは小さい場合は `*result_len` に必要サイズを設定し -1 を返す(副作用なし) + +TLV(Type-Length-Value)概要(簡易) +- ヘッダ: `u16 version (=1)`, `u16 argc` +- エントリ: `u8 tag`, `u8 reserved(0)`, `u16 size`, `payload...` +- 主なタグ: 1=Bool, 2=I32, 3=I64, 4=F32, 5=F64, 6=String, 7=Bytes, 8=Handle(u64), 9=Void +- plugin-testerの `io` は最小限のTLVエンコード/デコードを内蔵 + +プラグイン例(FileBox) +- 実装場所: `plugins/nyash-filebox-plugin` +- メソッドID: 0=birth, 1=open, 2=read, 3=write, 4=close, 0xFFFF_FFFF=fini +- `open(path, mode)`: 引数は TLV(String, String)、返り値は TLV(Void) +- `read(size)`: 引数 TLV(I32)、返 TLV(Bytes) +- `write(bytes)`: 引数 TLV(Bytes)、返 TLV(I32: 書き込みバイト数) +- `close()`: 返 TLV(Void) + +パスの指定(例) +- Linux: `plugins/nyash-filebox-plugin/target/release/libnyash_filebox_plugin.so` +- Windows: `plugins\nyash-filebox-plugin\target\release\nyash_filebox_plugin.dll` +- macOS: `plugins/nyash-filebox-plugin/target/release/libnyash_filebox_plugin.dylib` + +トラブルシュート +- `nyash_plugin_abi not found`: ビルド設定(cdylib)やシンボル名を再確認 +- `ShortBuffer`が返るのにデータが取れない: 2回目の呼び出しで `result` と `*result_len` を適切に設定しているか確認 +- 読み出しサイズが0: 書き込み後に `close`→`open(r)` してから `read` を実行しているか確認 + +関連ドキュメント +- `docs/CURRENT_TASK.md`(現在の進捗) +- `docs/予定/native-plan/issues/phase_9_75g_bid_integration_architecture.md`(設計計画) + +備考 +- 本説明書は `C:\git\nyash-project\nyash\docs\説明書\reference\plugin-tester.md` に配置されます(Windowsパス例)。 + diff --git a/local_tests/hello.nyash b/local_tests/hello.nyash new file mode 100644 index 00000000..54acbd13 --- /dev/null +++ b/local_tests/hello.nyash @@ -0,0 +1 @@ +print("Hello Nyash from file!") diff --git a/local_tests/test_filebox_new_only.nyash b/local_tests/test_filebox_new_only.nyash new file mode 100644 index 00000000..8e6381d8 --- /dev/null +++ b/local_tests/test_filebox_new_only.nyash @@ -0,0 +1,3 @@ +local f +f = new FileBox("plugins/nyash-filebox-plugin/target/test_integration_runtime.txt") +print("OK") diff --git a/local_tests/test_plugin_filebox.nyash b/local_tests/test_plugin_filebox.nyash index 03bddbe3..0afed986 100644 --- a/local_tests/test_plugin_filebox.nyash +++ b/local_tests/test_plugin_filebox.nyash @@ -1,35 +1,5 @@ -// Nyash FileBoxプラグイン透過的切り替えテスト -// nyash.tomlの設定によってビルトイン/プラグインが切り替わる - -static box Main { - init { console, file, result } - - main() { - me.console = new ConsoleBox() - me.console.log("🎯 FileBox Plugin Switching Test") - me.console.log("================================") - - // FileBox作成(透過的切り替え対象) - me.file = new FileBox("test_plugin_output.txt") - me.console.log("📁 FileBox created: " + me.file.toString()) - - // 現在使用されているFileBox実装を確認 - me.console.log("🔍 FileBox type: " + me.file.toString()) - - // 簡単なファイル操作テスト - local testPath - testPath = "test_plugin_output.txt" - - me.console.log("📝 Testing file operations...") - - // TODO: ファイル操作API呼び出し - // file.open(testPath) - // file.write("Hello from " + file.toString()) - // file.close() - - me.result = "Plugin switching test completed!" - me.console.log("✅ " + me.result) - - return me.result - } -} \ No newline at end of file +// Plugin-backed FileBox integration test +local f +f = new FileBox("plugins/nyash-filebox-plugin/target/test_integration.txt") +f.write("Hello from Nyash via plugin!") +print("READ=" + f.read()) diff --git a/plugins/nyash-filebox-plugin/src/lib.rs b/plugins/nyash-filebox-plugin/src/lib.rs index a430b514..e6629a82 100644 --- a/plugins/nyash-filebox-plugin/src/lib.rs +++ b/plugins/nyash-filebox-plugin/src/lib.rs @@ -5,7 +5,8 @@ use std::collections::HashMap; use std::os::raw::c_char; use std::ptr; -use std::sync::Mutex; +use std::sync::{Mutex, atomic::{AtomicU32, Ordering}}; +use std::io::{Read, Write, Seek, SeekFrom}; // ============ FFI Types ============ @@ -32,13 +33,14 @@ pub struct NyashPluginInfo { pub methods: *const NyashMethodInfo, } -// ============ Error Codes ============ +// ============ Error Codes (BID-1 alignment) ============ const NYB_SUCCESS: i32 = 0; -const NYB_E_INVALID_ARGS: i32 = -1; +const NYB_E_SHORT_BUFFER: i32 = -1; const NYB_E_INVALID_TYPE: i32 = -2; const NYB_E_INVALID_METHOD: i32 = -3; -const NYB_E_INVALID_HANDLE: i32 = -4; +const NYB_E_INVALID_ARGS: i32 = -4; const NYB_E_PLUGIN_ERROR: i32 = -5; +const NYB_E_INVALID_HANDLE: i32 = -8; // ============ Method IDs ============ const METHOD_BIRTH: u32 = 0; // Constructor @@ -61,6 +63,9 @@ static mut INSTANCES: Option>> = None; // ホスト関数テーブル(初期化時に設定) static mut HOST_VTABLE: Option<&'static NyashHostVtable> = None; +// インスタンスIDカウンタ(1開始) +static INSTANCE_COUNTER: AtomicU32 = AtomicU32::new(1); + // ============ Plugin Entry Points ============ /// ABI version @@ -162,13 +167,291 @@ pub extern "C" fn nyash_plugin_invoke( _result: *mut u8, _result_len: *mut usize, ) -> i32 { + // 簡易実装:type_id検証、省略可能 + if _type_id != 6 { + return NYB_E_INVALID_TYPE; + } + + unsafe { + match _method_id { + METHOD_BIRTH => { + // 引数は未使用 + let needed: usize = 4; // u32 instance_id + if _result_len.is_null() { + return NYB_E_INVALID_ARGS; + } + // Two-phase protocol: report required size if buffer missing/small + if _result.is_null() { + *_result_len = needed; + return NYB_E_SHORT_BUFFER; + } + let buf_len = *_result_len; + if buf_len < needed { + *_result_len = needed; + return NYB_E_SHORT_BUFFER; + } + + // 新しいインスタンスを作成 + let instance_id = INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed); + if let Some(ref mutex) = INSTANCES { + if let Ok(mut map) = mutex.lock() { + map.insert(instance_id, FileBoxInstance { + file: None, + path: String::new(), + buffer: None, + }); + } else { + return NYB_E_PLUGIN_ERROR; + } + } else { + return NYB_E_PLUGIN_ERROR; + } + + // 結果バッファにinstance_idを書き込む(LE) + let bytes = instance_id.to_le_bytes(); + std::ptr::copy_nonoverlapping(bytes.as_ptr(), _result, 4); + *_result_len = needed; + NYB_SUCCESS + } + METHOD_FINI => { + // 指定インスタンスを解放 + if let Some(ref mutex) = INSTANCES { + if let Ok(mut map) = mutex.lock() { + map.remove(&_instance_id); + return NYB_SUCCESS; + } else { + return NYB_E_PLUGIN_ERROR; + } + } + NYB_E_PLUGIN_ERROR + } + METHOD_OPEN => { + // args: TLV { String path, String mode } + let args = unsafe { std::slice::from_raw_parts(_args, _args_len) }; + match tlv_parse_two_strings(args) { + Ok((path, mode)) => { + // Preflight for Void TLV: header(4) + entry(4) + if preflight(_result, _result_len, 8) { return NYB_E_SHORT_BUFFER; } + log_info(&format!("OPEN path='{}' mode='{}'", path, mode)); + if let Some(ref mutex) = INSTANCES { + if let Ok(mut map) = mutex.lock() { + if let Some(inst) = map.get_mut(&_instance_id) { + match open_file(&mode, &path) { + Ok(file) => { + inst.file = Some(file); + // return TLV Void + return write_tlv_void(_result, _result_len); + } + Err(_) => return NYB_E_PLUGIN_ERROR, + } + } else { return NYB_E_PLUGIN_ERROR; } + } else { return NYB_E_PLUGIN_ERROR; } + } + NYB_E_PLUGIN_ERROR + } + Err(_) => NYB_E_INVALID_ARGS, + } + } + METHOD_READ => { + // args: TLV { I32 size } + let args = unsafe { std::slice::from_raw_parts(_args, _args_len) }; + match tlv_parse_i32(args) { + Ok(sz) => { + // Preflight for Bytes TLV: header(4) + entry(4) + sz + let need = 8usize.saturating_add(sz as usize); + if preflight(_result, _result_len, need) { return NYB_E_SHORT_BUFFER; } + if let Some(ref mutex) = INSTANCES { + if let Ok(mut map) = mutex.lock() { + if let Some(inst) = map.get_mut(&_instance_id) { + if let Some(file) = inst.file.as_mut() { + let mut buf = vec![0u8; sz as usize]; + // Read from beginning for simple semantics + let _ = file.seek(SeekFrom::Start(0)); + match file.read(&mut buf) { + Ok(n) => { + buf.truncate(n); + log_info(&format!("READ {} bytes", n)); + return write_tlv_bytes(&buf, _result, _result_len); + } + Err(_) => return NYB_E_PLUGIN_ERROR, + } + } else { return NYB_E_INVALID_HANDLE; } + } else { return NYB_E_PLUGIN_ERROR; } + } else { return NYB_E_PLUGIN_ERROR; } + } + NYB_E_PLUGIN_ERROR + } + Err(_) => NYB_E_INVALID_ARGS, + } + } + METHOD_WRITE => { + // args: TLV { Bytes data } + let args = unsafe { std::slice::from_raw_parts(_args, _args_len) }; + match tlv_parse_bytes(args) { + Ok(data) => { + // Preflight for I32 TLV: header(4) + entry(4) + 4 + if preflight(_result, _result_len, 12) { return NYB_E_SHORT_BUFFER; } + if let Some(ref mutex) = INSTANCES { + if let Ok(mut map) = mutex.lock() { + if let Some(inst) = map.get_mut(&_instance_id) { + if let Some(file) = inst.file.as_mut() { + match file.write(&data) { + Ok(n) => { + log_info(&format!("WRITE {} bytes", n)); + return write_tlv_i32(n as i32, _result, _result_len); + } + Err(_) => return NYB_E_PLUGIN_ERROR, + } + } else { return NYB_E_INVALID_HANDLE; } + } else { return NYB_E_PLUGIN_ERROR; } + } else { return NYB_E_PLUGIN_ERROR; } + } + NYB_E_PLUGIN_ERROR + } + Err(_) => NYB_E_INVALID_ARGS, + } + } + METHOD_CLOSE => { + // Preflight for Void TLV + if preflight(_result, _result_len, 8) { return NYB_E_SHORT_BUFFER; } + log_info("CLOSE"); + if let Some(ref mutex) = INSTANCES { + if let Ok(mut map) = mutex.lock() { + if let Some(inst) = map.get_mut(&_instance_id) { + inst.file = None; + return write_tlv_void(_result, _result_len); + } else { return NYB_E_PLUGIN_ERROR; } + } else { return NYB_E_PLUGIN_ERROR; } + } + NYB_E_PLUGIN_ERROR + } + _ => NYB_SUCCESS + } + } +} + +// ===== Helpers ===== + +fn open_file(mode: &str, path: &str) -> Result { + use std::fs::OpenOptions; + match mode { + "r" => OpenOptions::new().read(true).open(path), + "w" => OpenOptions::new().write(true).create(true).truncate(true).open(path), + "a" => OpenOptions::new().append(true).create(true).open(path), + "rw" | "r+" => OpenOptions::new().read(true).write(true).create(true).open(path), + _ => OpenOptions::new().read(true).open(path), + } +} + +fn write_tlv_result(payloads: &[(u8, &[u8])], result: *mut u8, result_len: *mut usize) -> i32 { + if result_len.is_null() { return NYB_E_INVALID_ARGS; } + let mut buf: Vec = Vec::with_capacity(4 + payloads.iter().map(|(_,p)| 4 + p.len()).sum::()); + buf.extend_from_slice(&1u16.to_le_bytes()); // version + buf.extend_from_slice(&(payloads.len() as u16).to_le_bytes()); // argc + for (tag, payload) in payloads { + buf.push(*tag); + buf.push(0); + buf.extend_from_slice(&(payload.len() as u16).to_le_bytes()); + buf.extend_from_slice(payload); + } + unsafe { + let needed = buf.len(); + if result.is_null() || *result_len < needed { + *result_len = needed; + return NYB_E_SHORT_BUFFER; + } + std::ptr::copy_nonoverlapping(buf.as_ptr(), result, needed); + *result_len = needed; + } NYB_SUCCESS } +fn write_tlv_void(result: *mut u8, result_len: *mut usize) -> i32 { + write_tlv_result(&[(9u8, &[])], result, result_len) +} + +fn write_tlv_bytes(data: &[u8], result: *mut u8, result_len: *mut usize) -> i32 { + write_tlv_result(&[(7u8, data)], result, result_len) +} + +fn write_tlv_i32(v: i32, result: *mut u8, result_len: *mut usize) -> i32 { + write_tlv_result(&[(2u8, &v.to_le_bytes())], result, result_len) +} + +fn preflight(result: *mut u8, result_len: *mut usize, needed: usize) -> bool { + unsafe { + if result_len.is_null() { return false; } + if result.is_null() || *result_len < needed { + *result_len = needed; + return true; + } + } + false +} + +fn tlv_parse_header(data: &[u8]) -> Result<(u16,u16,usize), ()> { + if data.len() < 4 { return Err(()); } + let ver = u16::from_le_bytes([data[0], data[1]]); + let argc = u16::from_le_bytes([data[2], data[3]]); + if ver != 1 { return Err(()); } + Ok((ver, argc, 4)) +} + +fn tlv_parse_two_strings(data: &[u8]) -> Result<(String, String), ()> { + let (_, argc, mut pos) = tlv_parse_header(data)?; + if argc < 2 { return Err(()); } + let s1 = tlv_parse_string_at(data, &mut pos)?; + let s2 = tlv_parse_string_at(data, &mut pos)?; + Ok((s1, s2)) +} + +fn tlv_parse_string_at(data: &[u8], pos: &mut usize) -> Result { + if *pos + 4 > data.len() { return Err(()); } + let tag = data[*pos]; let _res = data[*pos+1]; + let size = u16::from_le_bytes([data[*pos+2], data[*pos+3]]) as usize; + *pos += 4; + if tag != 6 || *pos + size > data.len() { return Err(()); } + let slice = &data[*pos..*pos+size]; + *pos += size; + std::str::from_utf8(slice).map(|s| s.to_string()).map_err(|_| ()) +} + +fn tlv_parse_i32(data: &[u8]) -> Result { + let (_, argc, mut pos) = tlv_parse_header(data)?; + if argc < 1 { return Err(()); } + if pos + 8 > data.len() { return Err(()); } + let tag = data[pos]; let _res = data[pos+1]; + let size = u16::from_le_bytes([data[pos+2], data[pos+3]]) as usize; pos += 4; + if tag != 2 || size != 4 || pos + size > data.len() { return Err(()); } + let mut b = [0u8;4]; b.copy_from_slice(&data[pos..pos+4]); + Ok(i32::from_le_bytes(b)) +} + +fn tlv_parse_bytes(data: &[u8]) -> Result, ()> { + let (_, argc, mut pos) = tlv_parse_header(data)?; + if argc < 1 { return Err(()); } + if pos + 4 > data.len() { return Err(()); } + let tag = data[pos]; let _res = data[pos+1]; + let size = u16::from_le_bytes([data[pos+2], data[pos+3]]) as usize; pos += 4; + if tag != 7 || pos + size > data.len() { return Err(()); } + Ok(data[pos..pos+size].to_vec()) +} + +fn log_info(message: &str) { + unsafe { + if let Some(vt) = HOST_VTABLE { + let log_fn = vt.log; + if let Ok(c) = std::ffi::CString::new(message) { + log_fn(1, c.as_ptr()); + } + } + } +} + /// Plugin shutdown #[no_mangle] pub extern "C" fn nyash_plugin_shutdown() { unsafe { INSTANCES = None; } -} \ No newline at end of file +} diff --git a/src/bid/loader.rs b/src/bid/loader.rs new file mode 100644 index 00000000..a40fa93f --- /dev/null +++ b/src/bid/loader.rs @@ -0,0 +1,82 @@ +use crate::bid::{BidError, BidResult, NyashHostVtable, NyashPluginInfo, PluginHandle, PLUGIN_ABI_SYMBOL, PLUGIN_INIT_SYMBOL, PLUGIN_INVOKE_SYMBOL, PLUGIN_SHUTDOWN_SYMBOL}; +use libloading::{Library, Symbol}; +use std::ffi::c_void; +use std::path::{Path, PathBuf}; + +/// Loaded plugin with FFI entry points and metadata +pub struct LoadedPlugin { + pub library: Library, + pub handle: PluginHandle, + pub type_id: u32, +} + +impl LoadedPlugin { + /// Load a plugin dynamic library from file path and initialize it + pub fn load_from_file(path: &Path) -> BidResult { + // Load library + let library = unsafe { Library::new(path) } + .map_err(|_| BidError::PluginError)?; + + // Resolve symbols + unsafe { + let abi: Symbol u32> = library + .get(PLUGIN_ABI_SYMBOL.as_bytes()) + .map_err(|_| BidError::PluginError)?; + let init: Symbol i32> = library + .get(PLUGIN_INIT_SYMBOL.as_bytes()) + .map_err(|_| BidError::PluginError)?; + let invoke: Symbol i32> = library + .get(PLUGIN_INVOKE_SYMBOL.as_bytes()) + .map_err(|_| BidError::PluginError)?; + let shutdown: Symbol = library + .get(PLUGIN_SHUTDOWN_SYMBOL.as_bytes()) + .map_err(|_| BidError::PluginError)?; + + let handle = PluginHandle { + abi: *abi, + init: *init, + invoke: *invoke, + shutdown: *shutdown, + }; + + // ABI check + handle.check_abi()?; + + // Initialize plugin + let host = default_host_vtable(); + let mut info = NyashPluginInfo::empty(); + handle.initialize(&host, &mut info)?; + let type_id = info.type_id; + + Ok(Self { library, handle, type_id }) + } + } +} + +/// Build a minimal host vtable for plugins +fn default_host_vtable() -> NyashHostVtable { + unsafe extern "C" fn host_alloc(size: usize) -> *mut u8 { + let layout = std::alloc::Layout::from_size_align(size, 8).unwrap(); + std::alloc::alloc(layout) + } + unsafe extern "C" fn host_free(_ptr: *mut u8) { + // In this prototype we cannot deallocate without size. No-op. + } + unsafe extern "C" fn host_wake(_id: u64) {} + unsafe extern "C" fn host_log(_level: i32, _msg: *const i8) {} + + NyashHostVtable { alloc: host_alloc, free: host_free, wake: host_wake, log: host_log } +} + +/// Helper: find plugin file path by name and candidate directories +pub fn resolve_plugin_path(plugin_name: &str, candidates: &[PathBuf]) -> Option { + // Expected filenames by platform (Linux only for now) + let file = format!("lib{}{}.so", plugin_name.replace('-', "_"), ""); + for dir in candidates { + let candidate = dir.join(&file); + if candidate.exists() { + return Some(candidate); + } + } + None +} diff --git a/src/bid/metadata.rs b/src/bid/metadata.rs index 99f078d7..662ab2eb 100644 --- a/src/bid/metadata.rs +++ b/src/bid/metadata.rs @@ -1,40 +1,24 @@ use super::{BidError, BidResult}; -use std::os::raw::{c_char, c_void}; +use std::os::raw::{c_char}; use std::ffi::{CStr, CString}; /// Host function table provided to plugins #[repr(C)] pub struct NyashHostVtable { - /// Allocate memory - pub alloc: Option *mut c_void>, - - /// Free memory - pub free: Option, - - /// Wake a future (for FutureBox support) - pub wake: Option, - - /// Log a message - pub log: Option, + pub alloc: unsafe extern "C" fn(size: usize) -> *mut u8, + pub free: unsafe extern "C" fn(ptr: *mut u8), + pub wake: unsafe extern "C" fn(handle: u64), + pub log: unsafe extern "C" fn(level: i32, msg: *const c_char), } impl NyashHostVtable { - /// Create an empty vtable + /// Create a vtable with no-op stubs (for tests) pub fn empty() -> Self { - Self { - alloc: None, - free: None, - wake: None, - log: None, - } - } - - /// Check if all required functions are present - pub fn is_complete(&self) -> bool { - self.alloc.is_some() && - self.free.is_some() && - self.log.is_some() - // wake is optional for async support + unsafe extern "C" fn a(_size: usize) -> *mut u8 { std::ptr::null_mut() } + unsafe extern "C" fn f(_ptr: *mut u8) {} + unsafe extern "C" fn w(_h: u64) {} + unsafe extern "C" fn l(_level: i32, _m: *const c_char) {} + Self { alloc: a, free: f, wake: w, log: l } } } @@ -225,24 +209,10 @@ mod tests { #[test] fn test_host_vtable() { - let vtable = NyashHostVtable::empty(); - assert!(!vtable.is_complete()); - - // In real usage, would set actual function pointers - let vtable = NyashHostVtable { - alloc: Some(dummy_alloc), - free: Some(dummy_free), - wake: None, - log: Some(dummy_log), - }; - assert!(vtable.is_complete()); + unsafe extern "C" fn a(_size: usize) -> *mut u8 { std::ptr::null_mut() } + unsafe extern "C" fn f(_p: *mut u8) {} + unsafe extern "C" fn w(_h: u64) {} + unsafe extern "C" fn l(_level: i32, _m: *const c_char) {} + let _v = NyashHostVtable { alloc: a, free: f, wake: w, log: l }; } - - extern "C" fn dummy_alloc(_size: usize) -> *mut c_void { - std::ptr::null_mut() - } - - extern "C" fn dummy_free(_ptr: *mut c_void) {} - - extern "C" fn dummy_log(_msg: *const c_char) {} -} \ No newline at end of file +} diff --git a/src/bid/mod.rs b/src/bid/mod.rs index 81e9509a..8f64cf73 100644 --- a/src/bid/mod.rs +++ b/src/bid/mod.rs @@ -8,6 +8,9 @@ pub mod metadata; pub mod plugin_api; pub mod bridge; pub mod plugins; +pub mod loader; +pub mod registry; +pub mod plugin_box; pub use types::*; pub use tlv::*; @@ -15,6 +18,9 @@ pub use error::*; pub use metadata::*; pub use plugin_api::*; pub use bridge::*; +pub use loader::*; +pub use registry::*; +pub use plugin_box::*; /// BID-1 version constant pub const BID_VERSION: u16 = 1; @@ -27,4 +33,4 @@ pub type Usize = u32; pub type Usize = u64; /// Standard alignment requirement -pub const BID_ALIGNMENT: usize = 8; \ No newline at end of file +pub const BID_ALIGNMENT: usize = 8; diff --git a/src/bid/plugin_box.rs b/src/bid/plugin_box.rs new file mode 100644 index 00000000..bcf20256 --- /dev/null +++ b/src/bid/plugin_box.rs @@ -0,0 +1,151 @@ +use crate::bid::{BidError, BidResult, LoadedPlugin}; +use crate::bid::tlv::{TlvEncoder, TlvDecoder}; +use crate::bid::types::BidTag; +use crate::bid::metadata::{NyashMethodInfo, NyashPluginInfo}; +use crate::box_trait::{NyashBox, StringBox, BoolBox, BoxCore, BoxBase}; + +/// Minimal plugin-backed instance that manages birth/fini lifecycle +pub struct PluginBoxInstance<'a> { + pub plugin: &'a LoadedPlugin, + pub instance_id: u32, +} + +impl<'a> PluginBoxInstance<'a> { + /// Create a new instance by invoking METHOD_BIRTH (0) + pub fn birth(plugin: &'a LoadedPlugin) -> BidResult { + let mut out = Vec::new(); + plugin.handle.invoke(plugin.type_id, 0, 0, &[], &mut out)?; + // Expect TLV encoding of handle or instance id; current prototype returns raw u32 + let instance_id = if out.len() == 4 { + u32::from_le_bytes([out[0], out[1], out[2], out[3]]) + } else { + // Try to decode TLV handle (future-proof) + return Err(BidError::InvalidArgs); + }; + Ok(Self { plugin, instance_id }) + } + + // Method IDs are fixed for FileBox in BID-1 prototype: + // 1=open, 2=read, 3=write, 4=close + + pub fn open(&self, path: &str, mode: &str) -> BidResult<()> { + let method = 1; // open + let mut enc = TlvEncoder::new(); + enc.encode_string(path)?; + enc.encode_string(mode)?; + let args = enc.finish(); + let mut out = Vec::new(); + self.plugin.handle.invoke(self.plugin.type_id, method, self.instance_id, &args, &mut out)?; + Ok(()) + } + + pub fn write(&self, data: &[u8]) -> BidResult { + let method = 3; // write + let mut enc = TlvEncoder::new(); + enc.encode_bytes(data)?; + let args = enc.finish(); + let mut out = Vec::new(); + self.plugin.handle.invoke(self.plugin.type_id, method, self.instance_id, &args, &mut out)?; + let mut dec = TlvDecoder::new(&out)?; + if let Some((tag, payload)) = dec.decode_next()? { + if tag != BidTag::I32 { return Err(BidError::InvalidType); } + return Ok(TlvDecoder::decode_i32(payload)?); + } + Err(BidError::PluginError) + } + + pub fn read(&self, size: usize) -> BidResult> { + let method = 2; // read + let mut enc = TlvEncoder::new(); + enc.encode_i32(size as i32)?; + let args = enc.finish(); + let mut out = Vec::new(); + self.plugin.handle.invoke(self.plugin.type_id, method, self.instance_id, &args, &mut out)?; + let mut dec = TlvDecoder::new(&out)?; + if let Some((tag, payload)) = dec.decode_next()? { + if tag != BidTag::Bytes { return Err(BidError::InvalidType); } + return Ok(payload.to_vec()); + } + Err(BidError::PluginError) + } + + pub fn close(&self) -> BidResult<()> { + let method = 4; // close + let mut enc = TlvEncoder::new(); + enc.encode_void()?; + let args = enc.finish(); + let mut out = Vec::new(); + self.plugin.handle.invoke(self.plugin.type_id, method, self.instance_id, &args, &mut out)?; + Ok(()) + } +} + +impl<'a> Drop for PluginBoxInstance<'a> { + fn drop(&mut self) { + // METHOD_FINI = u32::MAX + let _ = self.plugin.handle.invoke( + self.plugin.type_id, + u32::MAX, + self.instance_id, + &[], + &mut Vec::new(), + ); + } +} + +/// NyashBox implementation wrapping a BID plugin FileBox instance +pub struct PluginFileBox { + base: BoxBase, + inner: PluginBoxInstance<'static>, + path: String, +} + +impl PluginFileBox { + pub fn new(plugin: &'static LoadedPlugin, path: String) -> BidResult { + let inst = PluginBoxInstance::birth(plugin)?; + // Open with read-write by default (compat with built-in) + inst.open(&path, "rw")?; + Ok(Self { base: BoxBase::new(), inner: inst, path }) + } + + pub fn read_bytes(&self, size: usize) -> BidResult> { self.inner.read(size) } + pub fn write_bytes(&self, data: &[u8]) -> BidResult { self.inner.write(data) } + pub fn close(&self) -> BidResult<()> { self.inner.close() } +} + +impl BoxCore for PluginFileBox { + fn box_id(&self) -> u64 { self.base.id } + fn parent_type_id(&self) -> Option { self.base.parent_type_id } + fn fmt_box(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "FileBox({}) [plugin]", self.path) + } + fn as_any(&self) -> &dyn std::any::Any { self } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } +} + +impl NyashBox for PluginFileBox { + fn to_string_box(&self) -> StringBox { StringBox::new(format!("FileBox({})", self.path)) } + fn equals(&self, other: &dyn NyashBox) -> BoolBox { + if let Some(of) = other.as_any().downcast_ref::() { + BoolBox::new(self.path == of.path) + } else { BoolBox::new(false) } + } + fn clone_box(&self) -> Box { + // Create a new plugin-backed instance to the same path + if let Some(reg) = crate::bid::registry::global() { + if let Some(plugin) = reg.get_by_name("FileBox") { + if let Ok(newb) = PluginFileBox::new(plugin, self.path.clone()) { + return Box::new(newb); + } + } + } + Box::new(StringBox::new("")) + } + fn share_box(&self) -> Box { self.clone_box() } +} + +impl std::fmt::Debug for PluginFileBox { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "PluginFileBox(path={})", self.path) + } +} diff --git a/src/bid/registry.rs b/src/bid/registry.rs new file mode 100644 index 00000000..82dfe595 --- /dev/null +++ b/src/bid/registry.rs @@ -0,0 +1,90 @@ +use crate::bid::{BidError, BidResult, LoadedPlugin}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::fs; +use once_cell::sync::OnceCell; + +/// Registry mapping Box names and type IDs to loaded plugins +pub struct PluginRegistry { + by_name: HashMap, + by_type_id: HashMap, +} + +impl PluginRegistry { + pub fn new() -> Self { + Self { by_name: HashMap::new(), by_type_id: HashMap::new() } + } + + pub fn get_by_name(&self, name: &str) -> Option<&LoadedPlugin> { + self.by_name.get(name) + } + + pub fn get_by_type_id(&self, type_id: u32) -> Option<&LoadedPlugin> { + self.by_type_id.get(&type_id).and_then(|name| self.by_name.get(name)) + } + + /// Load plugins based on nyash.toml minimal parsing + pub fn load_from_config(path: &str) -> BidResult { + let content = fs::read_to_string(path).map_err(|_| BidError::PluginError)?; + + // Very small parser: look for lines like `FileBox = "nyash-filebox-plugin"` + let mut mappings: HashMap = HashMap::new(); + for line in content.lines() { + let trimmed = line.trim(); + if trimmed.starts_with('#') || trimmed.is_empty() { continue; } + if let Some((k, v)) = trimmed.split_once('=') { + let key = k.trim().trim_matches(' ').to_string(); + let val = v.trim().trim_matches('"').to_string(); + if key.chars().all(|c| c.is_alphanumeric() || c == '_' ) && !val.is_empty() { + mappings.insert(key, val); + } + } + } + + // Candidate directories + let mut candidates: Vec = vec![ + PathBuf::from("./plugins/nyash-filebox-plugin/target/release"), + PathBuf::from("./plugins/nyash-filebox-plugin/target/debug"), + ]; + // Also parse plugin_paths.search_paths if present + if let Some(sp_start) = content.find("search_paths") { + if let Some(open) = content[sp_start..].find('[') { + if let Some(close) = content[sp_start + open..].find(']') { + let list = &content[sp_start + open + 1.. sp_start + open + close]; + for item in list.split(',') { + let p = item.trim().trim_matches('"'); + if !p.is_empty() { candidates.push(PathBuf::from(p)); } + } + } + } + } + + let mut reg = Self::new(); + + for (box_name, plugin_name) in mappings.into_iter() { + // Find dynamic library path + if let Some(path) = super::loader::resolve_plugin_path(&plugin_name, &candidates) { + let loaded = super::loader::LoadedPlugin::load_from_file(&path)?; + reg.by_type_id.insert(loaded.type_id, box_name.clone()); + reg.by_name.insert(box_name, loaded); + } + } + + Ok(reg) + } +} + +// ===== Global registry (for interpreter access) ===== +static PLUGIN_REGISTRY: OnceCell = OnceCell::new(); + +/// Initialize global plugin registry from config +pub fn init_global_from_config(path: &str) -> BidResult<()> { + let reg = PluginRegistry::load_from_config(path)?; + let _ = PLUGIN_REGISTRY.set(reg); + Ok(()) +} + +/// Get global plugin registry if initialized +pub fn global() -> Option<&'static PluginRegistry> { + PLUGIN_REGISTRY.get() +} diff --git a/src/interpreter/method_dispatch.rs b/src/interpreter/method_dispatch.rs index a0fb4f10..b7aa9d19 100644 --- a/src/interpreter/method_dispatch.rs +++ b/src/interpreter/method_dispatch.rs @@ -9,6 +9,7 @@ use super::*; use crate::boxes::{buffer::BufferBox, JSONBox, HttpClientBox, StreamBox, RegexBox, IntentBox, SocketBox, HTTPServerBox, HTTPRequestBox, HTTPResponseBox}; use crate::boxes::{FloatBox, MathBox, ConsoleBox, TimeBox, DateTimeBox, RandomBox, SoundBox, DebugBox, file::FileBox, MapBox}; +use crate::bid::plugin_box::PluginFileBox; use std::sync::Arc; impl NyashInterpreter { @@ -201,6 +202,10 @@ impl NyashInterpreter { if let Some(file_box) = obj_value.as_any().downcast_ref::() { return self.execute_file_method(file_box, method, arguments); } + // Plugin-backed FileBox method calls + if let Some(pfile) = obj_value.as_any().downcast_ref::() { + return self.execute_plugin_file_method(pfile, method, arguments); + } // ResultBox method calls if let Some(result_box) = obj_value.as_any().downcast_ref::() { @@ -352,6 +357,37 @@ impl NyashInterpreter { self.execute_user_defined_method(obj_value, method, arguments) } + fn execute_plugin_file_method( + &mut self, + pfile: &PluginFileBox, + method: &str, + arguments: &[ASTNode], + ) -> Result, RuntimeError> { + match method { + "write" => { + if arguments.len() != 1 { + return Err(RuntimeError::InvalidOperation { message: "FileBox.write expects 1 argument".into() }); + } + let arg0 = self.execute_expression(&arguments[0])?; + let data = arg0.to_string_box().value; + pfile.write_bytes(data.as_bytes()).map_err(|e| RuntimeError::RuntimeFailure { message: format!("plugin write error: {:?}", e) })?; + Ok(Box::new(StringBox::new("ok"))) + } + "read" => { + // Default read size + let size = 1_048_576usize; // 1MB max + let bytes = pfile.read_bytes(size).map_err(|e| RuntimeError::RuntimeFailure { message: format!("plugin read error: {:?}", e) })?; + let s = String::from_utf8_lossy(&bytes).to_string(); + Ok(Box::new(StringBox::new(s))) + } + "close" => { + pfile.close().map_err(|e| RuntimeError::RuntimeFailure { message: format!("plugin close error: {:?}", e) })?; + Ok(Box::new(StringBox::new("ok"))) + } + _ => Err(RuntimeError::InvalidOperation { message: format!("Unknown method FileBox.{} (plugin)", method) }) + } + } + /// SocketBoxの状態変更を反映 fn update_stateful_socket_box( &mut self, @@ -455,4 +491,4 @@ impl NyashInterpreter { message: format!("Method '{}' not found on type '{}'", method, obj_value.type_name()), }) } -} \ No newline at end of file +} diff --git a/src/interpreter/objects.rs b/src/interpreter/objects.rs index 99f49432..1d472ec2 100644 --- a/src/interpreter/objects.rs +++ b/src/interpreter/objects.rs @@ -95,15 +95,26 @@ impl NyashInterpreter { }); } let path_value = self.execute_expression(&arguments[0])?; - if let Some(path_str) = path_value.as_any().downcast_ref::() { - let file_box = Box::new(FileBox::new(&path_str.value)) as Box; - // 🌍 革命的実装:Environment tracking廃止 - return Ok(file_box); + let path_str = if let Some(s) = path_value.as_any().downcast_ref::() { + s.value.clone() } else { - return Err(RuntimeError::TypeError { - message: "FileBox constructor requires string path argument".to_string(), - }); + return Err(RuntimeError::TypeError { message: "FileBox constructor requires string path argument".to_string() }); + }; + + // プラグイン優先(nyash.tomlに設定がある場合) + if let Some(reg) = crate::bid::registry::global() { + if let Some(plugin) = reg.get_by_name("FileBox") { + if let Ok(p) = crate::bid::plugin_box::PluginFileBox::new(plugin, path_str.clone()) { + return Ok(Box::new(p) as Box); + } + } } + + // フォールバック: ビルトインFileBox + return match crate::boxes::file::FileBox::open(&path_str) { + Ok(fb) => Ok(Box::new(fb) as Box), + Err(e) => Err(RuntimeError::InvalidOperation { message: format!("Failed to open file '{}': {}", path_str, e) }), + }; } "ResultBox" => { // ResultBoxは引数1個(成功値)で作成 @@ -1102,4 +1113,4 @@ impl NyashInterpreter { // 現在はシンプルにコピー fields.to_vec() } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 91610e1f..1a5738d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,9 @@ pub mod backend; // 📊 Performance Benchmarks (NEW!) pub mod benchmarks; +// BID-FFI / Plugin system (prototype) +pub mod bid; + #[cfg(target_arch = "wasm32")] pub mod wasm_test; @@ -204,4 +207,4 @@ impl NyashWasm { pub fn version() -> String { String::from("Nyash WASM v0.1.0 - Everything is Box in Browser!") } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 1b38bd54..34de4254 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,6 +42,9 @@ pub mod benchmarks; pub mod cli; pub mod runner; +// BID-FFI / Plugin System (prototype) +pub mod bid; + use cli::CliConfig; use runner::NyashRunner; diff --git a/src/runner.rs b/src/runner.rs index 45bff514..b6e5f41a 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -17,6 +17,9 @@ use crate::{ }; use std::{fs, process}; +// BID prototype imports +use crate::bid::{PluginRegistry, PluginBoxInstance}; + /// Main execution coordinator pub struct NyashRunner { config: CliConfig, @@ -30,6 +33,8 @@ impl NyashRunner { /// Run Nyash based on the configuration pub fn run(&self) { + // Try to initialize BID plugins from nyash.toml (best-effort) + self.init_bid_plugins(); // Benchmark mode - can run without a file if self.config.benchmark { println!("📊 Nyash Performance Benchmark Suite"); @@ -48,6 +53,22 @@ impl NyashRunner { } } + fn init_bid_plugins(&self) { + // Best-effort init; do not fail the program if missing + if let Ok(()) = crate::bid::registry::init_global_from_config("nyash.toml") { + let reg = crate::bid::registry::global().unwrap(); + // If FileBox plugin is present, try a birth/fini cycle as a smoke test + if let Some(plugin) = reg.get_by_name("FileBox") { + if let Ok(inst) = PluginBoxInstance::birth(plugin) { + println!("🔌 BID plugin loaded: FileBox (instance_id={})", inst.instance_id); + // Drop will call fini + return; + } + } + println!("🔌 BID registry initialized"); + } + } + /// Execute file-based mode with backend selection fn execute_file_mode(&self, filename: &str) { if self.config.dump_mir || self.config.verify_mir { @@ -119,8 +140,9 @@ impl NyashRunner { println!("📝 File contents:\n{}", code); println!("\n🚀 Parsing and executing...\n"); - // Test: immediate file creation - std::fs::write("/mnt/c/git/nyash/development/debug_hang_issue/test.txt", "START").ok(); + // Test: immediate file creation (use relative path to avoid sandbox issues) + std::fs::create_dir_all("development/debug_hang_issue").ok(); + std::fs::write("development/debug_hang_issue/test.txt", "START").ok(); // Parse the code with debug fuel limit eprintln!("🔍 DEBUG: Starting parse with fuel: {:?}...", self.config.debug_fuel); @@ -143,7 +165,7 @@ impl NyashRunner { if let Ok(mut file) = std::fs::OpenOptions::new() .create(true) .append(true) - .open("/mnt/c/git/nyash/development/debug_hang_issue/debug_trace.log") + .open("development/debug_hang_issue/debug_trace.log") { use std::io::Write; let _ = writeln!(file, "=== MAIN: Parse successful ==="); @@ -702,4 +724,4 @@ mod tests { let runner = NyashRunner::new(config); assert_eq!(runner.config.backend, "interpreter"); } -} \ No newline at end of file +} diff --git a/tools/plugin-tester/src/main.rs b/tools/plugin-tester/src/main.rs index 4e1305b6..0cccd167 100644 --- a/tools/plugin-tester/src/main.rs +++ b/tools/plugin-tester/src/main.rs @@ -9,6 +9,7 @@ use libloading::{Library, Symbol}; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_void}; use std::path::PathBuf; +use std::io::Write; // ============ FFI Types (プラグインと同じ定義) ============ @@ -58,6 +59,11 @@ enum Commands { /// Path to plugin .so file plugin: PathBuf, }, + /// File I/O end-to-end test (open/write/read/close) + Io { + /// Path to plugin .so file + plugin: PathBuf, + }, } // ============ Host Functions (テスト用実装) ============ @@ -107,9 +113,87 @@ fn main() { match args.command { Commands::Check { plugin } => check_plugin(&plugin), Commands::Lifecycle { plugin } => test_lifecycle(&plugin), + Commands::Io { plugin } => test_file_io(&plugin), } } +// ============ Minimal BID-1 TLV Helpers ============ + +#[repr(C)] +#[derive(Clone, Copy)] +struct TlvHeader { version: u16, argc: u16 } + +const TLV_VERSION: u16 = 1; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Tag { Bool=1, I32=2, I64=3, F32=4, F64=5, String=6, Bytes=7, Handle=8, Void=9 } + +fn tlv_encode_string(s: &str, buf: &mut Vec) { + let header_pos = buf.len(); + buf.extend_from_slice(&[0,0,0,0]); + let mut argc: u16 = 0; + // entry + let bytes = s.as_bytes(); + buf.push(Tag::String as u8); + buf.push(0); + buf.extend_from_slice(&(bytes.len() as u16).to_le_bytes()); + buf.extend_from_slice(bytes); + argc += 1; + // write header + buf[header_pos..header_pos+2].copy_from_slice(&TLV_VERSION.to_le_bytes()); + buf[header_pos+2..header_pos+4].copy_from_slice(&argc.to_le_bytes()); +} + +fn tlv_encode_two_strings(a: &str, b: &str, buf: &mut Vec) { + let header_pos = buf.len(); + buf.extend_from_slice(&[0,0,0,0]); + let mut argc: u16 = 0; + for s in [a,b] { + let bytes = s.as_bytes(); + buf.push(Tag::String as u8); + buf.push(0); + buf.extend_from_slice(&(bytes.len() as u16).to_le_bytes()); + buf.extend_from_slice(bytes); + argc += 1; + } + buf[header_pos..header_pos+2].copy_from_slice(&TLV_VERSION.to_le_bytes()); + buf[header_pos+2..header_pos+4].copy_from_slice(&argc.to_le_bytes()); +} + +fn tlv_encode_i32(v: i32, buf: &mut Vec) { + let header_pos = buf.len(); + buf.extend_from_slice(&[0,0,0,0]); + buf.push(Tag::I32 as u8); + buf.push(0); + buf.extend_from_slice(&4u16.to_le_bytes()); + buf.extend_from_slice(&v.to_le_bytes()); + buf[header_pos..header_pos+2].copy_from_slice(&TLV_VERSION.to_le_bytes()); + buf[header_pos+2..header_pos+4].copy_from_slice(&1u16.to_le_bytes()); +} + +fn tlv_encode_bytes(data: &[u8], buf: &mut Vec) { + let header_pos = buf.len(); + buf.extend_from_slice(&[0,0,0,0]); + buf.push(Tag::Bytes as u8); + buf.push(0); + buf.extend_from_slice(&(data.len() as u16).to_le_bytes()); + buf.extend_from_slice(data); + buf[header_pos..header_pos+2].copy_from_slice(&TLV_VERSION.to_le_bytes()); + buf[header_pos+2..header_pos+4].copy_from_slice(&1u16.to_le_bytes()); +} + +fn tlv_decode_first(bytes: &[u8]) -> Option<(u8, &[u8])> { + if bytes.len() < 4 { return None; } + let argc = u16::from_le_bytes([bytes[2], bytes[3]]); + if argc == 0 { return None; } + if bytes.len() < 8 { return None; } + let tag = bytes[4]; + let size = u16::from_le_bytes([bytes[6], bytes[7]]) as usize; + if bytes.len() < 8+size { return None; } + Some((tag, &bytes[8..8+size])) +} + fn check_plugin(path: &PathBuf) { println!("{}", "=== Nyash Plugin Checker ===".bold()); println!("Plugin: {}", path.display()); @@ -217,7 +301,183 @@ fn check_plugin(path: &PathBuf) { fn test_lifecycle(path: &PathBuf) { println!("{}", "=== Lifecycle Test ===".bold()); println!("Testing birth/fini for: {}", path.display()); - - // TODO: birth/finiのテスト実装 - println!("{}: Lifecycle test not yet implemented", "TODO".yellow()); -} \ No newline at end of file + + // プラグインをロード + let library = match unsafe { Library::new(path) } { + Ok(lib) => lib, + Err(e) => { + eprintln!("{}: Failed to load plugin: {}", "ERROR".red(), e); + return; + } + }; + + unsafe { + // ABI version + let abi_fn: Symbol u32> = match library.get(b"nyash_plugin_abi") { + Ok(f) => f, + Err(e) => { + eprintln!("{}: nyash_plugin_abi not found: {}", "ERROR".red(), e); + return; + } + }; + let abi_version = abi_fn(); + println!("{}: ABI version: {}", "✓".green(), abi_version); + if abi_version != 1 { + eprintln!("{}: Unsupported ABI version (expected 1)", "WARNING".yellow()); + } + + // init + let init_fn: Symbol i32> = + match library.get(b"nyash_plugin_init") { + Ok(f) => f, + Err(e) => { + eprintln!("{}: nyash_plugin_init not found: {}", "ERROR".red(), e); + return; + } + }; + let mut plugin_info = std::mem::zeroed::(); + let result = init_fn(&HOST_VTABLE, &mut plugin_info); + if result != 0 { + eprintln!("{}: nyash_plugin_init failed with code {}", "ERROR".red(), result); + return; + } + println!("{}: Plugin initialized", "✓".green()); + + // invoke + let invoke_fn: Symbol i32> = + match library.get(b"nyash_plugin_invoke") { + Ok(f) => f, + Err(e) => { + eprintln!("{}: nyash_plugin_invoke not found: {}", "ERROR".red(), e); + return; + } + }; + + let type_id = plugin_info.type_id; + println!("{}: BoxType ID = {}", "i".blue(), type_id); + + // birth + let mut out = [0u8; 8]; + let mut out_len: usize = out.len(); + let rc = invoke_fn(type_id, 0, 0, std::ptr::null(), 0, out.as_mut_ptr(), &mut out_len as *mut usize); + if rc != 0 { + eprintln!("{}: birth invoke failed with code {}", "ERROR".red(), rc); + return; + } + if out_len < 4 { + eprintln!("{}: birth returned too small result ({} bytes)", "ERROR".red(), out_len); + return; + } + let instance_id = u32::from_le_bytes(out[0..4].try_into().unwrap()); + println!("{}: birth → instance_id={}", "✓".green(), instance_id); + + // fini + let rc = invoke_fn(type_id, u32::MAX, instance_id, std::ptr::null(), 0, std::ptr::null_mut(), std::ptr::null_mut()); + if rc != 0 { + eprintln!("{}: fini invoke failed with code {}", "ERROR".red(), rc); + return; + } + println!("{}: fini → instance {} cleaned", "✓".green(), instance_id); + + // shutdown + if let Ok(shutdown_fn) = library.get::>(b"nyash_plugin_shutdown") { + shutdown_fn(); + println!("{}: Plugin shutdown completed", "✓".green()); + } + } + + println!("\n{}", "Lifecycle test completed!".green().bold()); +} + +fn test_file_io(path: &PathBuf) { + println!("{}", "=== File I/O Test ===".bold()); + println!("Testing open/write/read/close: {}", path.display()); + + // Load + let library = match unsafe { Library::new(path) } { + Ok(lib) => lib, + Err(e) => { + eprintln!("{}: Failed to load plugin: {}", "ERROR".red(), e); + return; + } + }; + unsafe { + let abi: Symbol u32> = library.get(b"nyash_plugin_abi").unwrap(); + println!("{}: ABI version: {}", "✓".green(), abi()); + let init: Symbol i32> = library.get(b"nyash_plugin_init").unwrap(); + let mut info = std::mem::zeroed::(); + assert_eq!(0, init(&HOST_VTABLE, &mut info)); + let invoke: Symboli32> = library.get(b"nyash_plugin_invoke").unwrap(); + let shutdown: Symbol = library.get(b"nyash_plugin_shutdown").unwrap(); + + // birth + let mut buf_len: usize = 0; + let rc = invoke(info.type_id, 0, 0, std::ptr::null(), 0, std::ptr::null_mut(), &mut buf_len as *mut usize); + assert!(rc == -1 && buf_len >= 4, "unexpected birth preflight"); + let mut out = vec![0u8; buf_len]; + let mut out_len = buf_len; + assert_eq!(0, invoke(info.type_id, 0, 0, std::ptr::null(), 0, out.as_mut_ptr(), &mut out_len as *mut usize)); + let instance_id = u32::from_le_bytes(out[0..4].try_into().unwrap()); + println!("{}: birth → instance_id={}", "✓".green(), instance_id); + + // open: write mode + let mut args = Vec::new(); + let test_path = "plugins/nyash-filebox-plugin/target/test_io.txt"; + tlv_encode_two_strings(test_path, "w", &mut args); + let mut res_len: usize = 0; + let rc = invoke(info.type_id, 1, instance_id, args.as_ptr(), args.len(), std::ptr::null_mut(), &mut res_len as *mut usize); + assert!(rc == -1 || rc == 0); + let mut res = vec![0u8; res_len.max(4)]; + let mut rl = res_len; + let _ = invoke(info.type_id, 1, instance_id, args.as_ptr(), args.len(), res.as_mut_ptr(), &mut rl as *mut usize); + println!("{}: open(w)", "✓".green()); + + // write + let content = b"Hello from plugin-tester!"; + let mut wargs = Vec::new(); + tlv_encode_bytes(content, &mut wargs); + let mut rlen: usize = 0; + let rc = invoke(info.type_id, 3, instance_id, wargs.as_ptr(), wargs.len(), std::ptr::null_mut(), &mut rlen as *mut usize); + assert!(rc == -1 || rc == 0); + let mut wb = vec![0u8; rlen.max(8)]; + let mut rl2 = rlen; + let _ = invoke(info.type_id, 3, instance_id, wargs.as_ptr(), wargs.len(), wb.as_mut_ptr(), &mut rl2 as *mut usize); + if let Some((tag, payload)) = tlv_decode_first(&wb[..rl2]) { + assert_eq!(tag, Tag::I32 as u8); + let mut n = [0u8;4]; n.copy_from_slice(payload); + let written = i32::from_le_bytes(n); + println!("{}: write {} bytes", "✓".green(), written); + } + + // close + let mut clen: usize = 0; + let _ = invoke(info.type_id, 4, instance_id, std::ptr::null(), 0, std::ptr::null_mut(), &mut clen as *mut usize); + let mut cb = vec![0u8; clen.max(4)]; let mut cbl = clen; let _ = invoke(info.type_id, 4, instance_id, std::ptr::null(), 0, cb.as_mut_ptr(), &mut cbl as *mut usize); + println!("{}: close", "✓".green()); + + // reopen read + let mut args2 = Vec::new(); tlv_encode_two_strings(test_path, "r", &mut args2); + let mut r0: usize = 0; let _ = invoke(info.type_id, 1, instance_id, args2.as_ptr(), args2.len(), std::ptr::null_mut(), &mut r0 as *mut usize); + let mut ob = vec![0u8; r0.max(4)]; let mut obl=r0; let _=invoke(info.type_id,1,instance_id,args2.as_ptr(),args2.len(),ob.as_mut_ptr(),&mut obl as *mut usize); + println!("{}: open(r)", "✓".green()); + + // read 1024 + let mut rargs = Vec::new(); tlv_encode_i32(1024, &mut rargs); + let mut rneed: usize = 0; let rc = invoke(info.type_id, 2, instance_id, rargs.as_ptr(), rargs.len(), std::ptr::null_mut(), &mut rneed as *mut usize); + assert!(rc == -1 || rc == 0); + let mut rb = vec![0u8; rneed.max(16)]; let mut rbl=rneed; let rc2=invoke(info.type_id,2,instance_id,rargs.as_ptr(),rargs.len(),rb.as_mut_ptr(),&mut rbl as *mut usize); + if rc2 != 0 { println!("{}: read rc={} (expected 0)", "WARN".yellow(), rc2); } + if let Some((tag, payload)) = tlv_decode_first(&rb[..rbl]) { + assert_eq!(tag, Tag::Bytes as u8); + let s = String::from_utf8_lossy(payload).to_string(); + println!("{}: read {} bytes → '{}'", "✓".green(), payload.len(), s); + } else { + println!("{}: read decode failed (len={})", "WARN".yellow(), rbl); + } + + // close & shutdown + let mut clen2: usize = 0; let _=invoke(info.type_id,4,instance_id,std::ptr::null(),0,std::ptr::null_mut(),&mut clen2 as *mut usize); + shutdown(); + println!("\n{}", "File I/O test completed!".green().bold()); + } +}