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:
@ -94,6 +94,8 @@ once_cell = "1.20"
|
||||
# デバッグ・ログ
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
libloading = "0.8"
|
||||
toml = "0.8"
|
||||
|
||||
# 日時処理
|
||||
chrono = "0.4"
|
||||
|
||||
@ -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,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を分ける。
|
||||
```
|
||||
|
||||
### 重要な技術的決定
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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/)
|
||||
|
||||
68
docs/説明書/reference/box-design/filebox-bid-mapping.md
Normal file
68
docs/説明書/reference/box-design/filebox-bid-mapping.md
Normal 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.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
|
||||
|
||||
66
docs/説明書/reference/plugin-tester.md
Normal file
66
docs/説明書/reference/plugin-tester.md
Normal 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向け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 <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=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パス例)。
|
||||
|
||||
1
local_tests/hello.nyash
Normal file
1
local_tests/hello.nyash
Normal file
@ -0,0 +1 @@
|
||||
print("Hello Nyash from file!")
|
||||
3
local_tests/test_filebox_new_only.nyash
Normal file
3
local_tests/test_filebox_new_only.nyash
Normal file
@ -0,0 +1,3 @@
|
||||
local f
|
||||
f = new FileBox("plugins/nyash-filebox-plugin/target/test_integration_runtime.txt")
|
||||
print("OK")
|
||||
@ -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())
|
||||
|
||||
@ -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
82
src/bid/loader.rs
Normal 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
|
||||
}
|
||||
@ -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) {}
|
||||
}
|
||||
@ -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
151
src/bid/plugin_box.rs
Normal 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
90
src/bid/registry.rs
Normal 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()
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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個(成功値)で作成
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 ===");
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user