BID-FFI integration:\n- Add plugin-tester io subcommand and TLV helpers; E2E open/write/read/close\n- Implement FileBox plugin invoke (birth/open/read/write/close) with BID-1 two-pass\n- Add BID loader/registry/plugin_box modules; prefer plugin-backed FileBox in interpreter\n- Introduce PluginFileBox with minimal read/write/close dispatch\n- Update runner debug paths; add local simple tests\n- Docs: plugin-tester guide and FileBox Nyash↔BID mapping; CURRENT_TASK updated

This commit is contained in:
Moe Charm
2025-08-18 11:07:03 +09:00
parent ea40437d4d
commit 0441608db3
21 changed files with 1209 additions and 123 deletions

View File

@ -94,6 +94,8 @@ once_cell = "1.20"
# デバッグ・ログ
log = "0.4"
env_logger = "0.11"
libloading = "0.8"
toml = "0.8"
# 日時処理
chrono = "0.4"

View File

@ -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 して利用: ⚠️ サンドボックス制約により実行中にSIGKILLdlopen系の制約
- ローカル実行(手元環境)では `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,11 +291,38 @@ 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を分ける。
```
### 重要な技術的決定

View File

@ -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

View File

@ -28,6 +28,9 @@ Arc<Mutex>一元管理、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/)

View File

@ -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.mdWindowsパス例
前提
- BID-FFI v12段階応答/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 → BytesUTF-8
- 呼出: method_id=3write
- 戻り: I32 を受け取り、Nyash側は "ok" を返却(将来は書込サイズも返せる拡張余地)
- 読取: `FileBox.read([size: integer])`
- 変換: 省略時デフォルト 1MB1_048_576を指定
- 呼出: method_id=2read
- 戻り: Bytes → StringUTF-8として解釈、失敗時はlossy
- 閉じ: `FileBox.close()`
- 呼出: method_id=4close
- 戻り: Void → Nyash側は "ok"
エラーモデル(戻り値)
- 0: 成功
- -1: ShortBuffer2段階応答。副作用なしで必要サイズを *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

View File

@ -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 <plugin>`: プラグインのロード、ABI確認、init呼び出し、型名・メソッド一覧の表示
- `lifecycle <plugin>`: birth→fini の往復テストインスタンスIDを返すことを確認
- `io <plugin>`: FileBox向けE2Eopen→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 <path-to-plugin>`
- 期待出力例: `birth → instance_id=1`, `fini → instance 1 cleaned`
- ファイルI/O:
- `tools/plugin-tester/target/release/plugin-tester io <path-to-plugin>`
- 期待出力例: `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=ShortBuffer2段階応答, -2=InvalidType, -3=InvalidMethod, -4=InvalidArgs, -5=PluginError, -8=InvalidHandle
- 2段階応答: `result`がNULLまたは小さい場合は `*result_len` に必要サイズを設定し -1 を返す(副作用なし)
TLVType-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パス例

1
local_tests/hello.nyash Normal file
View File

@ -0,0 +1 @@
print("Hello Nyash from file!")

View File

@ -0,0 +1,3 @@
local f
f = new FileBox("plugins/nyash-filebox-plugin/target/test_integration_runtime.txt")
print("OK")

View File

@ -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
}
}
// 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())

View File

@ -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<Mutex<HashMap<u32, FileBoxInstance>>> = None;
// ホスト関数テーブル(初期化時に設定)
static mut HOST_VTABLE: Option<&'static NyashHostVtable> = None;
// インスタンスIDカウンタ1開始
static INSTANCE_COUNTER: AtomicU32 = AtomicU32::new(1);
// ============ Plugin Entry Points ============
/// ABI version
@ -162,9 +167,287 @@ 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<std::fs::File, std::io::Error> {
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<u8> = Vec::with_capacity(4 + payloads.iter().map(|(_,p)| 4 + p.len()).sum::<usize>());
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<String, ()> {
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<i32, ()> {
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<Vec<u8>, ()> {
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() {

82
src/bid/loader.rs Normal file
View File

@ -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<Self> {
// Load library
let library = unsafe { Library::new(path) }
.map_err(|_| BidError::PluginError)?;
// Resolve symbols
unsafe {
let abi: Symbol<unsafe extern "C" fn() -> u32> = library
.get(PLUGIN_ABI_SYMBOL.as_bytes())
.map_err(|_| BidError::PluginError)?;
let init: Symbol<unsafe extern "C" fn(*const NyashHostVtable, *mut NyashPluginInfo) -> i32> = library
.get(PLUGIN_INIT_SYMBOL.as_bytes())
.map_err(|_| BidError::PluginError)?;
let invoke: Symbol<unsafe extern "C" fn(u32, u32, u32, *const u8, usize, *mut u8, *mut usize) -> i32> = library
.get(PLUGIN_INVOKE_SYMBOL.as_bytes())
.map_err(|_| BidError::PluginError)?;
let shutdown: Symbol<unsafe extern "C" fn()> = 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<PathBuf> {
// 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
}

View File

@ -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<extern "C" fn(size: usize) -> *mut c_void>,
/// Free memory
pub free: Option<extern "C" fn(ptr: *mut c_void)>,
/// Wake a future (for FutureBox support)
pub wake: Option<extern "C" fn(future_id: u32)>,
/// Log a message
pub log: Option<extern "C" fn(msg: *const c_char)>,
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) {}
}

View File

@ -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;

151
src/bid/plugin_box.rs Normal file
View File

@ -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<Self> {
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<i32> {
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<Vec<u8>> {
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<Self> {
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<Vec<u8>> { self.inner.read(size) }
pub fn write_bytes(&self, data: &[u8]) -> BidResult<i32> { 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<std::any::TypeId> { 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::<PluginFileBox>() {
BoolBox::new(self.path == of.path)
} else { BoolBox::new(false) }
}
fn clone_box(&self) -> Box<dyn NyashBox> {
// 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("<plugin clone failed>"))
}
fn share_box(&self) -> Box<dyn NyashBox> { 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)
}
}

90
src/bid/registry.rs Normal file
View File

@ -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<String, LoadedPlugin>,
by_type_id: HashMap<u32, String>,
}
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<Self> {
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<String, String> = 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<PathBuf> = 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<PluginRegistry> = 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()
}

View File

@ -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::<crate::boxes::file::FileBox>() {
return self.execute_file_method(file_box, method, arguments);
}
// Plugin-backed FileBox method calls
if let Some(pfile) = obj_value.as_any().downcast_ref::<PluginFileBox>() {
return self.execute_plugin_file_method(pfile, method, arguments);
}
// ResultBox method calls
if let Some(result_box) = obj_value.as_any().downcast_ref::<ResultBox>() {
@ -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<Box<dyn NyashBox>, 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,

View File

@ -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::<StringBox>() {
let file_box = Box::new(FileBox::new(&path_str.value)) as Box<dyn NyashBox>;
// 🌍 革命的実装Environment tracking廃止
return Ok(file_box);
let path_str = if let Some(s) = path_value.as_any().downcast_ref::<StringBox>() {
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<dyn NyashBox>);
}
}
}
// フォールバック: ビルトインFileBox
return match crate::boxes::file::FileBox::open(&path_str) {
Ok(fb) => Ok(Box::new(fb) as Box<dyn NyashBox>),
Err(e) => Err(RuntimeError::InvalidOperation { message: format!("Failed to open file '{}': {}", path_str, e) }),
};
}
"ResultBox" => {
// ResultBoxは引数1個成功値で作成

View File

@ -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;

View File

@ -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;

View File

@ -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 ===");

View File

@ -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<u8>) {
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<u8>) {
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<u8>) {
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<u8>) {
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());
@ -218,6 +302,182 @@ 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());
// プラグインをロード
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<unsafe extern "C" fn() -> 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<unsafe extern "C" fn(*const NyashHostVtable, *mut NyashPluginInfo) -> 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::<NyashPluginInfo>();
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<unsafe extern "C" fn(u32, u32, u32, *const u8, usize, *mut u8, *mut usize) -> 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::<Symbol<unsafe extern "C" fn()>>(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<unsafe extern "C" fn() -> u32> = library.get(b"nyash_plugin_abi").unwrap();
println!("{}: ABI version: {}", "".green(), abi());
let init: Symbol<unsafe extern "C" fn(*const NyashHostVtable, *mut NyashPluginInfo) -> i32> = library.get(b"nyash_plugin_init").unwrap();
let mut info = std::mem::zeroed::<NyashPluginInfo>();
assert_eq!(0, init(&HOST_VTABLE, &mut info));
let invoke: Symbol<unsafe extern "C" fn(u32,u32,u32,*const u8,usize,*mut u8,*mut usize)->i32> = library.get(b"nyash_plugin_invoke").unwrap();
let shutdown: Symbol<unsafe extern "C" fn()> = 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());
}
}