feat: Implement plugin singleton pattern with shutdown support

- Add singleton support for plugin boxes (e.g., CounterBox)
- Implement shutdown_plugins_v2() for controlled plugin lifecycle
- Plugin instances now shared across multiple new() calls
- Shutdown properly releases and allows re-initialization
- All singleton E2E tests passing 

ChatGPT5による高度なプラグインライフサイクル管理実装
- シングルトンパターンでプラグインインスタンス共有
- 明示的なshutdownでリソース解放と再初期化対応
- Nyashの統一ライフサイクルポリシー維持

Note: ast.rs test failures are due to rapid development pace -
tests need updating for new BoxDeclaration fields (private_fields, public_fields)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Moe Charm
2025-08-21 21:35:17 +09:00
parent 8c6d5b5adc
commit da716addc8
16 changed files with 465 additions and 107 deletions

View File

@ -450,6 +450,29 @@ docs/
**📋 詳細**: [DOCUMENTATION_REORGANIZATION_STRATEGY.md](DOCUMENTATION_REORGANIZATION_STRATEGY.md) **📋 詳細**: [DOCUMENTATION_REORGANIZATION_STRATEGY.md](DOCUMENTATION_REORGANIZATION_STRATEGY.md)
## 🤝 プロアクティブ開発方針
### 🎯 エラー対応時の姿勢
エラーを見つけた際は、単に報告するだけでなく:
1. **🔍 原因分析** - エラーの根本原因を探る
2. **📊 影響範囲** - 他のコードへの影響を調査
3. **💡 改善提案** - 関連する問題も含めて解決策を提示
4. **🧹 機会改善** - デッドコード削除など、ついでにできる改善も実施
### ⚖️ バランスの取り方
- **積極的に分析・提案**するが、最終判断はユーザーに委ねる
- 「ChatGPTさんに任せてる」と言われても、分析結果は共有する
- 複数のAIが協調する場合でも、各自の視点で価値を提供する
### 📝 例
```
❌ 受動的: 「エラーをファイルに出力しました」
✅ 能動的: 「エラーをファイルに出力しました。主な原因は型の不一致7箇所で、
instance_id()のメソッド呼び出し修正で5つ解決できそうです。
また、関連してclone_boxの実装にも同様の問題を発見しました。」
```
## 🚨 コンテキスト圧縮時の重要ルール ## 🚨 コンテキスト圧縮時の重要ルール
### ⚠️ **コンテキスト圧縮を検出した場合の必須手順** ### ⚠️ **コンテキスト圧縮を検出した場合の必須手順**

24
chatgpt5_build_errors.txt Normal file
View File

@ -0,0 +1,24 @@
ChatGPT5<EFBFBD><EFBFBD>k<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɨ<EFBFBD><EFBFBD>:
1. [E0599] no method named `call_fini` found for reference `&enabled::PluginBoxV2`
4@: src/scope_tracker.rs:45:28
<20><><EFBFBD>: plugin.call_fini() - <20><><EFBFBD><EFBFBD>LX(WjD
2. [E0308] mismatched types (2<>@)
4@: src/interpreter/core.rs:579:45, 618:45
<20><><EFBFBD>: &**v - expected `&Box<dyn NyashBox>`, found `&dyn NyashBox`
3. [E0615] attempted to take value of method `instance_id` (3<>@)
4@:
- src/interpreter/expressions/calls.rs:695:98
- src/interpreter/expressions/calls.rs:785:98
- src/backend/vm.rs:567:90
<20><><EFBFBD>: plugin.instance_id - <20><><EFBFBD>ɒգ<C992><D5A3><EFBFBD>hWf(
<20>cH: plugin.instance_id() k <09>
4. [E0609] no field `invoke_fn` on type `&enabled::PluginBoxV2`
4@: src/runtime/plugin_loader_v2.rs:139:19
<20><><EFBFBD>: self.invoke_fn - X(WjDգ<44><D5A3><EFBFBD>
<20>cH: self.inner.invoke_fn
: 7 n<><6E>Ѥ<EFBFBD><D1A4><EFBFBD><EFBFBD>

View File

@ -0,0 +1,21 @@
ChatGPT5実装による更新後のビルドエラー:
改善された点(修正済み):
- ✅ call_fini() メソッドエラー解決
- ✅ instance_id フィールド/メソッドエラー解決
- ✅ invoke_fn フィールドエラー解決
残存エラー2個:
1. [E0308] mismatched types - src/interpreter/core.rs:579:45
エラー: &**v - expected `&Box<dyn NyashBox>`, found `&dyn NyashBox`
2. [E0308] mismatched types - src/interpreter/core.rs:618:45
エラー: &**v - expected `&Box<dyn NyashBox>`, found `&dyn NyashBox`
推奨修正:
- &**v を v に変更Arc<dyn NyashBox>への参照として扱う)
- または型注釈を &dyn NyashBox に変更
影響範囲:
- interpreter/core.rsのみ他のモジュールのエラーは解決済み

View File

@ -1,6 +1,6 @@
# プラグインBoxのライフサイクルと nyash.toml methods 定義 # プラグインBoxのライフサイクルv2と nyash.toml 定義
本書は、プラグインBoxPluginBoxV2の生成birthと終了finiの流れ、ならびに nyash.toml v2 における `methods` 定義の役割をまとめたものです。 本書は、プラグインBoxPluginBoxV2の生成birthと終了finiの流れ、`singleton` オプション、ならびに nyash.toml v2 における `methods` 定義の役割をまとめたものです。
--- ---
@ -15,26 +15,26 @@
1. `unified registry``PluginLoaderV2::create_box(box_type, args)` を呼び出す。 1. `unified registry``PluginLoaderV2::create_box(box_type, args)` を呼び出す。
2. `PluginLoaderV2``nyash.toml` から `type_id``methods` を読み込む。 2. `PluginLoaderV2``nyash.toml` から `type_id``methods` を読み込む。
3. `invoke_fn(type_id, method_id=0 /* birth */, instance_id=0, ...)` を呼び、戻り値出力TLVの先頭4バイトから `instance_id` を取得。 3. `invoke_fn(type_id, method_id=0 /* birth */, instance_id=0, ...)` を呼び、戻り値出力TLVの先頭4バイトから `instance_id` を取得。
4. `PluginBoxV2 { type_id, instance_id, invoke_fn, fini_method_id }` を生成して返す。 4. `PluginBoxV2 { box_type, inner: Arc<PluginHandleInner> }` を生成して返す。
- `PluginHandleInner``{ type_id, instance_id, invoke_fn, fini_method_id, finalized }` を保持し、参照カウントArcで共有される。
補足: 補足:
- `fini_method_id``nyash.toml``methods` から `fini``method_id` を取り出して保持します。未定義の場合は `None` - `fini_method_id``nyash.toml``methods` から `fini``method_id` を取り出して保持します。未定義の場合は `None`
--- ---
## 3. 終了finiの流れ ## 3. 終了finiの流れ
- フィールド差し替え時(代入で旧値を置き換えるとき): - フィールド差し替え時(代入で旧値を置き換えるとき):
- 旧値が `InstanceBox` の場合: インタプリタが `fini()` を呼び、finalized としてマーキングします。 - 旧値が `InstanceBox` の場合: インタプリタが `fini()` を呼び、finalized としてマーキングします。
- 旧値が `PluginBoxV2` の場合: `fini_method_id` が設定されていれば `invoke_fn(type_id, fini_method_id, instance_id, ...)` を呼びます。 - 旧値が `PluginBoxV2` の場合: `fini_method_id` が設定されていれば `invoke_fn(type_id, fini_method_id, instance_id, ...)` を呼びます。
- 破棄Drop: - プラグインBoxPluginBoxV2:
- RustのDropでFFIを呼ぶのは安全性の観点でリスクがあるため、現状は「明示タイミングフィールド差し替えなど」での fini 呼び出しを優先しています - すべての参照ArcがDropされ「最後の参照が解放」された時、`Drop`で一度だけ `fini` を呼ぶRAII、二重呼び出し防止
- 明示finiが必要な場合は `PluginBoxV2::finalize_now()` を使える内部的に一度だけfini実行
注意: - 代入/フィールド代入/Map.get/Array.get/slice/退避などは「PluginBoxV2は共有share、それ以外は複製clone」で統一。
- ローカル変数のスコープ終了時に自動で fini を呼ぶ実装は、現時点では入っていません(将来検討)。
--- ---
## 4. nyash.toml v2 の定義例 ## 4. nyash.toml v2 の定義例methods + singleton
```toml ```toml
[libraries] [libraries]
@ -58,6 +58,24 @@ fini = { method_id = 4294967295 } # 任意の終端ID
- `methods``fini` を定義すれば、差し替え時などに fini が呼ばれます。 - `methods``fini` を定義すれば、差し替え時などに fini が呼ばれます。
- `fini` 未定義の場合、プラグインBoxの終了処理は呼ばれませんフォールバック動作 - `fini` 未定義の場合、プラグインBoxの終了処理は呼ばれませんフォールバック動作
### singleton例
```toml
[libraries."libnyash_counter_plugin.so".CounterBox]
type_id = 7
singleton = true
[libraries."libnyash_counter_plugin.so".CounterBox.methods]
birth = { method_id = 0 }
inc = { method_id = 1 }
get = { method_id = 2 }
fini = { method_id = 4294967295 }
```
- `singleton = true` を設定すると、ローダー初期化時に事前birthし、ローダーが共有ハンドルを保持します。
- `create_box()` は保持中の共有ハンドルを返すため、複数回の `new` でも同一インスタンスを共有できます。
- Nyash終了時または明示要求時`shutdown_plugins_v2()` を呼ぶと、ローダーが保持する全シングルトンの `fini` を実行し、クリーンに解放されます。
--- ---
## 5. WASMwasm-bindgenとの関係 ## 5. WASMwasm-bindgenとの関係

View File

@ -38,6 +38,7 @@ cloneSelf = { method_id = 8 }
[libraries."libnyash_counter_plugin.so".CounterBox] [libraries."libnyash_counter_plugin.so".CounterBox]
type_id = 7 type_id = 7
singleton = true
[libraries."libnyash_counter_plugin.so".CounterBox.methods] [libraries."libnyash_counter_plugin.so".CounterBox.methods]
birth = { method_id = 0 } birth = { method_id = 0 }

View File

@ -564,7 +564,7 @@ impl VM {
if let Some(plugin) = box_nyash.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() { if let Some(plugin) = box_nyash.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
let loader = crate::runtime::get_global_loader_v2(); let loader = crate::runtime::get_global_loader_v2();
let loader = loader.read().map_err(|_| VMError::InvalidInstruction("Plugin loader lock poisoned".into()))?; let loader = loader.read().map_err(|_| VMError::InvalidInstruction("Plugin loader lock poisoned".into()))?;
match loader.invoke_instance_method(&plugin.box_type, method, plugin.instance_id, &arg_values) { match loader.invoke_instance_method(&plugin.box_type, method, plugin.instance_id(), &arg_values) {
Ok(Some(result_box)) => { Ok(Some(result_box)) => {
if let Some(dst_id) = dst { if let Some(dst_id) = dst {
self.set_value(*dst_id, VMValue::from_nyash_box(result_box)); self.set_value(*dst_id, VMValue::from_nyash_box(result_box));

View File

@ -59,7 +59,13 @@ impl ArrayBox {
let idx = idx_box.value as usize; let idx = idx_box.value as usize;
let items = self.items.read().unwrap(); let items = self.items.read().unwrap();
match items.get(idx) { match items.get(idx) {
Some(item) => item.clone_box(), Some(item) => {
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
if item.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>().is_some() {
return item.share_box();
}
item.clone_box()
}
None => Box::new(crate::boxes::null_box::NullBox::new()), None => Box::new(crate::boxes::null_box::NullBox::new()),
} }
} else { } else {
@ -228,7 +234,13 @@ impl ArrayBox {
// Create slice // Create slice
let slice_items: Vec<Box<dyn NyashBox>> = items[start_idx..end_idx] let slice_items: Vec<Box<dyn NyashBox>> = items[start_idx..end_idx]
.iter() .iter()
.map(|item| item.clone_box()) .map(|item| {
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
if item.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>().is_some() {
return item.share_box();
}
item.clone_box()
})
.collect(); .collect();
Box::new(ArrayBox::new_with_elements(slice_items)) Box::new(ArrayBox::new_with_elements(slice_items))
@ -241,7 +253,13 @@ impl Clone for ArrayBox {
// ディープコピー(独立インスタンス) // ディープコピー(独立インスタンス)
let items_guard = self.items.read().unwrap(); let items_guard = self.items.read().unwrap();
let cloned_items: Vec<Box<dyn NyashBox>> = items_guard.iter() let cloned_items: Vec<Box<dyn NyashBox>> = items_guard.iter()
.map(|item| item.clone_box()) // 要素もディープコピー .map(|item| {
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
if item.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>().is_some() {
return item.share_box();
}
item.clone_box()
}) // 要素もディープコピー(ハンドルは共有)
.collect(); .collect();
ArrayBox { ArrayBox {

View File

@ -135,7 +135,13 @@ impl MapBox {
pub fn get(&self, key: Box<dyn NyashBox>) -> Box<dyn NyashBox> { pub fn get(&self, key: Box<dyn NyashBox>) -> Box<dyn NyashBox> {
let key_str = key.to_string_box().value; let key_str = key.to_string_box().value;
match self.data.read().unwrap().get(&key_str) { match self.data.read().unwrap().get(&key_str) {
Some(value) => value.clone_box(), Some(value) => {
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
if value.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>().is_some() {
return value.share_box();
}
value.clone_box()
}
None => Box::new(StringBox::new(&format!("Key not found: {}", key_str))), None => Box::new(StringBox::new(&format!("Key not found: {}", key_str))),
} }
} }

View File

@ -47,6 +47,10 @@ pub struct BoxTypeConfig {
/// Method definitions /// Method definitions
pub methods: HashMap<String, MethodDefinition>, pub methods: HashMap<String, MethodDefinition>,
/// Singleton service flag (keep one shared instance alive in loader)
#[serde(default)]
pub singleton: bool,
} }
/// Method definition (simplified - no argument info needed) /// Method definition (simplified - no argument info needed)

View File

@ -575,7 +575,14 @@ impl NyashInterpreter {
/// local変数スタックを保存・復元関数呼び出し時 /// local変数スタックを保存・復元関数呼び出し時
pub(super) fn save_local_vars(&self) -> HashMap<String, Box<dyn NyashBox>> { pub(super) fn save_local_vars(&self) -> HashMap<String, Box<dyn NyashBox>> {
self.local_vars.iter() self.local_vars.iter()
.map(|(k, v)| (k.clone(), (**v).clone_box())) // Deref Arc to get the Box .map(|(k, v)| {
let b: &dyn NyashBox = &**v;
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
if b.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>().is_some() {
return (k.clone(), b.share_box());
}
(k.clone(), b.clone_box())
})
.collect() .collect()
} }
@ -593,12 +600,7 @@ impl NyashInterpreter {
let _ = instance.fini(); let _ = instance.fini();
eprintln!("🔄 Scope exit: Called fini() on local variable '{}' (InstanceBox)", name); eprintln!("🔄 Scope exit: Called fini() on local variable '{}' (InstanceBox)", name);
} }
// プラグインBoxの場合 // プラグインBoxは共有ハンドルの可能性が高いため自動finiしない明示呼び出しのみ
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
if let Some(plugin) = (**value).as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
plugin.call_fini();
eprintln!("🔄 Scope exit: Called fini() on local variable '{}' (PluginBox)", name);
}
// ビルトインBoxは元々finiメソッドを持たないので呼ばない // ビルトインBoxは元々finiメソッドを持たないので呼ばない
// StringBox、IntegerBox等はリソース管理不要 // StringBox、IntegerBox等はリソース管理不要
} }
@ -612,7 +614,14 @@ impl NyashInterpreter {
/// outbox変数スタックを保存・復元static関数呼び出し時 /// outbox変数スタックを保存・復元static関数呼び出し時
pub(super) fn save_outbox_vars(&self) -> HashMap<String, Box<dyn NyashBox>> { pub(super) fn save_outbox_vars(&self) -> HashMap<String, Box<dyn NyashBox>> {
self.outbox_vars.iter() self.outbox_vars.iter()
.map(|(k, v)| (k.clone(), (**v).clone_box())) // Deref Arc to get the Box .map(|(k, v)| {
let b: &dyn NyashBox = &**v;
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
if b.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>().is_some() {
return (k.clone(), b.share_box());
}
(k.clone(), b.clone_box())
})
.collect() .collect()
} }
@ -624,12 +633,7 @@ impl NyashInterpreter {
let _ = instance.fini(); let _ = instance.fini();
eprintln!("🔄 Scope exit: Called fini() on outbox variable '{}' (InstanceBox)", name); eprintln!("🔄 Scope exit: Called fini() on outbox variable '{}' (InstanceBox)", name);
} }
// プラグインBoxの場合 // プラグインBoxは共有ハンドルの可能性が高いため自動finiしない
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
if let Some(plugin) = (**value).as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
plugin.call_fini();
eprintln!("🔄 Scope exit: Called fini() on outbox variable '{}' (PluginBox)", name);
}
// ビルトインBoxは元々finiメソッドを持たないので呼ばない要修正 // ビルトインBoxは元々finiメソッドを持たないので呼ばない要修正
} }

View File

@ -692,7 +692,7 @@ impl NyashInterpreter {
} }
let loader = crate::runtime::get_global_loader_v2(); let loader = crate::runtime::get_global_loader_v2();
let loader = loader.read().unwrap(); let loader = loader.read().unwrap();
match loader.invoke_instance_method(&plugin.box_type, method, plugin.instance_id, &arg_values) { match loader.invoke_instance_method(&plugin.box_type, method, plugin.instance_id(), &arg_values) {
Ok(Some(result_box)) => return Ok(result_box), Ok(Some(result_box)) => return Ok(result_box),
Ok(None) => return Ok(Box::new(VoidBox::new())), Ok(None) => return Ok(Box::new(VoidBox::new())),
Err(_) => {} Err(_) => {}
@ -782,7 +782,7 @@ impl NyashInterpreter {
if let Some(plugin) = plugin_ref.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() { if let Some(plugin) = plugin_ref.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
let mut arg_values: Vec<Box<dyn NyashBox>> = Vec::new(); let mut arg_values: Vec<Box<dyn NyashBox>> = Vec::new();
for arg in arguments { arg_values.push(self.execute_expression(arg)?); } for arg in arguments { arg_values.push(self.execute_expression(arg)?); }
match loader.invoke_instance_method(&plugin.box_type, method, plugin.instance_id, &arg_values) { match loader.invoke_instance_method(&plugin.box_type, method, plugin.instance_id(), &arg_values) {
Ok(Some(result_box)) => return Ok(result_box), Ok(Some(result_box)) => return Ok(result_box),
Ok(None) => return Ok(Box::new(crate::box_trait::VoidBox::new())), Ok(None) => return Ok(Box::new(crate::box_trait::VoidBox::new())),
Err(e) => { Err(e) => {
@ -828,7 +828,7 @@ impl NyashInterpreter {
} }
let loader = crate::runtime::get_global_loader_v2(); let loader = crate::runtime::get_global_loader_v2();
let loader = loader.read().unwrap(); let loader = loader.read().unwrap();
match loader.invoke_instance_method(&plugin.box_type, method, plugin.instance_id, &arg_values) { match loader.invoke_instance_method(&plugin.box_type, method, plugin.instance_id(), &arg_values) {
Ok(Some(result_box)) => return Ok(result_box), Ok(Some(result_box)) => return Ok(result_box),
Ok(None) => return Ok(Box::new(VoidBox::new())), Ok(None) => return Ok(Box::new(VoidBox::new())),
Err(_) => {} Err(_) => {}
@ -1003,7 +1003,7 @@ impl NyashInterpreter {
} }
let loader_guard = crate::runtime::plugin_loader_v2::get_global_loader_v2(); let loader_guard = crate::runtime::plugin_loader_v2::get_global_loader_v2();
let loader = loader_guard.read().map_err(|_| RuntimeError::RuntimeFailure { message: "Plugin loader lock poisoned".into() })?; let loader = loader_guard.read().map_err(|_| RuntimeError::RuntimeFailure { message: "Plugin loader lock poisoned".into() })?;
match loader.invoke_instance_method(&plugin_box.box_type, method, plugin_box.instance_id, &arg_values) { match loader.invoke_instance_method(&plugin_box.box_type, method, plugin_box.instance_id(), &arg_values) {
Ok(Some(result_box)) => Ok(result_box), Ok(Some(result_box)) => Ok(result_box),
Ok(None) => Ok(Box::new(VoidBox::new())), Ok(None) => Ok(Box::new(VoidBox::new())),
Err(e) => Err(RuntimeError::RuntimeFailure { message: format!("Plugin method {} failed: {:?}", method, e) }), Err(e) => Err(RuntimeError::RuntimeFailure { message: format!("Plugin method {} failed: {:?}", method, e) }),

View File

@ -34,18 +34,71 @@ mod enabled {
/// v2 Plugin Box wrapper - temporary implementation /// v2 Plugin Box wrapper - temporary implementation
#[derive(Debug)] #[derive(Debug)]
pub struct PluginBoxV2 { pub struct PluginHandleInner {
pub box_type: String,
pub type_id: u32, pub type_id: u32,
pub invoke_fn: unsafe extern "C" fn(u32, u32, u32, *const u8, usize, *mut u8, *mut usize) -> i32, pub invoke_fn: unsafe extern "C" fn(u32, u32, u32, *const u8, usize, *mut u8, *mut usize) -> i32,
pub instance_id: u32, pub instance_id: u32,
/// Optional fini method_id from nyash.toml (None if not provided)
pub fini_method_id: Option<u32>, pub fini_method_id: Option<u32>,
finalized: std::sync::atomic::AtomicBool,
}
impl Drop for PluginHandleInner {
fn drop(&mut self) {
// Finalize exactly once when the last shared handle is dropped
if let Some(fini_id) = self.fini_method_id {
if !self.finalized.swap(true, std::sync::atomic::Ordering::SeqCst) {
let tlv_args: [u8; 4] = [1, 0, 0, 0];
let mut out: [u8; 4] = [0; 4];
let mut out_len: usize = out.len();
unsafe {
(self.invoke_fn)(
self.type_id,
fini_id,
self.instance_id,
tlv_args.as_ptr(),
tlv_args.len(),
out.as_mut_ptr(),
&mut out_len,
);
}
}
}
}
}
impl PluginHandleInner {
/// Explicitly finalize this handle now (idempotent)
pub fn finalize_now(&self) {
if let Some(fini_id) = self.fini_method_id {
if !self.finalized.swap(true, std::sync::atomic::Ordering::SeqCst) {
let tlv_args: [u8; 4] = [1, 0, 0, 0];
let mut out: [u8; 4] = [0; 4];
let mut out_len: usize = out.len();
unsafe {
(self.invoke_fn)(
self.type_id,
fini_id,
self.instance_id,
tlv_args.as_ptr(),
tlv_args.len(),
out.as_mut_ptr(),
&mut out_len,
);
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct PluginBoxV2 {
pub box_type: String,
pub inner: std::sync::Arc<PluginHandleInner>,
} }
impl BoxCore for PluginBoxV2 { impl BoxCore for PluginBoxV2 {
fn box_id(&self) -> u64 { fn box_id(&self) -> u64 {
self.instance_id as u64 self.inner.instance_id as u64
} }
fn parent_type_id(&self) -> Option<std::any::TypeId> { fn parent_type_id(&self) -> Option<std::any::TypeId> {
@ -53,7 +106,7 @@ mod enabled {
} }
fn fmt_box(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt_box(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}({})", self.box_type, self.instance_id) write!(f, "{}({})", self.box_type, self.inner.instance_id)
} }
fn as_any(&self) -> &dyn Any { fn as_any(&self) -> &dyn Any {
@ -75,7 +128,7 @@ mod enabled {
} }
fn clone_box(&self) -> Box<dyn NyashBox> { fn clone_box(&self) -> Box<dyn NyashBox> {
eprintln!("🔍 DEBUG: PluginBoxV2::clone_box called for {} (id={})", self.box_type, self.instance_id); eprintln!("🔍 DEBUG: PluginBoxV2::clone_box called for {} (id={})", self.box_type, self.inner.instance_id);
// Clone means creating a new instance by calling birth() // Clone means creating a new instance by calling birth()
let mut output_buffer = vec![0u8; 1024]; let mut output_buffer = vec![0u8; 1024];
@ -83,8 +136,8 @@ mod enabled {
let tlv_args = vec![1u8, 0, 0, 0]; // version=1, argc=0 let tlv_args = vec![1u8, 0, 0, 0]; // version=1, argc=0
let result = unsafe { let result = unsafe {
(self.invoke_fn)( (self.inner.invoke_fn)(
self.type_id, self.inner.type_id,
0, // method_id=0 (birth) 0, // method_id=0 (birth)
0, // instance_id=0 (static call) 0, // instance_id=0 (static call)
tlv_args.as_ptr(), tlv_args.as_ptr(),
@ -103,13 +156,16 @@ mod enabled {
eprintln!("🎉 clone_box success: created new {} instance_id={}", self.box_type, new_instance_id); eprintln!("🎉 clone_box success: created new {} instance_id={}", self.box_type, new_instance_id);
// Return new PluginBoxV2 with new instance_id // Return new PluginBoxV2 with new instance_id (separate inner handle)
Box::new(PluginBoxV2 { Box::new(PluginBoxV2 {
box_type: self.box_type.clone(), box_type: self.box_type.clone(),
type_id: self.type_id, inner: std::sync::Arc::new(PluginHandleInner {
invoke_fn: self.invoke_fn, type_id: self.inner.type_id,
invoke_fn: self.inner.invoke_fn,
instance_id: new_instance_id, instance_id: new_instance_id,
fini_method_id: self.fini_method_id, fini_method_id: self.inner.fini_method_id,
finalized: std::sync::atomic::AtomicBool::new(false),
}),
}) })
} else { } else {
eprintln!("❌ clone_box failed: birth() returned error code {}", result); eprintln!("❌ clone_box failed: birth() returned error code {}", result);
@ -119,7 +175,7 @@ mod enabled {
} }
fn to_string_box(&self) -> crate::box_trait::StringBox { fn to_string_box(&self) -> crate::box_trait::StringBox {
StringBox::new(format!("{}({})", self.box_type, self.instance_id)) StringBox::new(format!("{}({})", self.box_type, self.inner.instance_id))
} }
fn equals(&self, _other: &dyn NyashBox) -> crate::box_trait::BoolBox { fn equals(&self, _other: &dyn NyashBox) -> crate::box_trait::BoolBox {
@ -127,43 +183,19 @@ mod enabled {
} }
fn share_box(&self) -> Box<dyn NyashBox> { fn share_box(&self) -> Box<dyn NyashBox> {
eprintln!("🔍 DEBUG: PluginBoxV2::share_box called for {} (id={})", self.box_type, self.instance_id); eprintln!("🔍 DEBUG: PluginBoxV2::share_box called for {} (id={})", self.box_type, self.inner.instance_id);
// Share means returning a new Box with the same instance_id // Share means returning a new Box with the same instance_id
Box::new(PluginBoxV2 { Box::new(PluginBoxV2 {
box_type: self.box_type.clone(), box_type: self.box_type.clone(),
type_id: self.type_id, inner: self.inner.clone(),
invoke_fn: self.invoke_fn,
instance_id: self.instance_id, // Same instance_id - this is sharing!
fini_method_id: self.fini_method_id,
}) })
} }
} }
impl PluginBoxV2 { impl PluginBoxV2 {
/// Call fini() on this plugin instance if configured pub fn instance_id(&self) -> u32 { self.inner.instance_id }
pub fn call_fini(&self) { pub fn finalize_now(&self) { self.inner.finalize_now() }
if let Some(fini_id) = self.fini_method_id {
// Empty TLV args
let tlv_args: [u8; 4] = [1, 0, 0, 0];
let mut out: [u8; 4] = [0; 4];
let mut out_len: usize = out.len();
let rc = unsafe {
(self.invoke_fn)(
self.type_id,
fini_id,
self.instance_id,
tlv_args.as_ptr(),
tlv_args.len(),
out.as_mut_ptr(),
&mut out_len,
)
};
if rc != 0 {
eprintln!("⚠️ PluginBoxV2::fini failed for {} id={} rc={}", self.box_type, self.instance_id, rc);
}
}
}
} }
/// Plugin loader v2 /// Plugin loader v2
@ -175,6 +207,9 @@ impl PluginBoxV2 {
pub config: Option<NyashConfigV2>, pub config: Option<NyashConfigV2>,
/// Path to the loaded nyash.toml (absolute), used for consistent re-reads /// Path to the loaded nyash.toml (absolute), used for consistent re-reads
config_path: Option<String>, config_path: Option<String>,
/// Singleton instances: (lib_name, box_type) -> shared handle
singletons: RwLock<HashMap<(String,String), std::sync::Arc<PluginHandleInner>>>,
} }
impl PluginLoaderV2 { impl PluginLoaderV2 {
@ -196,6 +231,7 @@ impl PluginBoxV2 {
plugins: RwLock::new(HashMap::new()), plugins: RwLock::new(HashMap::new()),
config: None, config: None,
config_path: None, config_path: None,
singletons: RwLock::new(HashMap::new()),
} }
} }
@ -225,10 +261,59 @@ impl PluginBoxV2 {
eprintln!("Warning: Failed to load plugin {}: {:?}", lib_name, e); eprintln!("Warning: Failed to load plugin {}: {:?}", lib_name, e);
} }
} }
// Pre-birth singletons configured in nyash.toml
let cfg_path = self.config_path.as_ref().map(|s| s.as_str()).unwrap_or("nyash.toml");
let toml_content = std::fs::read_to_string(cfg_path).map_err(|_| BidError::PluginError)?;
let toml_value: toml::Value = toml::from_str(&toml_content).map_err(|_| BidError::PluginError)?;
for (lib_name, lib_def) in &config.libraries {
for box_name in &lib_def.boxes {
if let Some(bc) = config.get_box_config(lib_name, box_name, &toml_value) {
if bc.singleton {
let _ = self.ensure_singleton_handle(lib_name, box_name);
}
}
}
}
Ok(()) Ok(())
} }
/// Ensure a singleton handle is created and stored
fn ensure_singleton_handle(&self, lib_name: &str, box_type: &str) -> BidResult<()> {
// Fast path: already present
if self.singletons.read().unwrap().contains_key(&(lib_name.to_string(), box_type.to_string())) {
return Ok(());
}
// Create via birth
let cfg_path = self.config_path.as_ref().map(|s| s.as_str()).unwrap_or("nyash.toml");
let toml_content = std::fs::read_to_string(cfg_path).map_err(|_| BidError::PluginError)?;
let toml_value: toml::Value = toml::from_str(&toml_content).map_err(|_| BidError::PluginError)?;
let config = self.config.as_ref().ok_or(BidError::PluginError)?;
let plugins = self.plugins.read().unwrap();
let plugin = plugins.get(lib_name).ok_or(BidError::PluginError)?;
let box_conf = config.get_box_config(lib_name, box_type, &toml_value).ok_or(BidError::InvalidType)?;
let type_id = box_conf.type_id;
// Call birth
let mut output_buffer = vec![0u8; 1024];
let mut output_len = output_buffer.len();
let tlv_args = vec![1u8, 0, 0, 0];
let birth_result = unsafe {
(plugin.invoke_fn)(type_id, 0, 0, tlv_args.as_ptr(), tlv_args.len(), output_buffer.as_mut_ptr(), &mut output_len)
};
if birth_result != 0 || output_len < 4 { return Err(BidError::PluginError); }
let instance_id = u32::from_le_bytes([output_buffer[0], output_buffer[1], output_buffer[2], output_buffer[3]]);
let fini_id = box_conf.methods.get("fini").map(|m| m.method_id);
let handle = std::sync::Arc::new(PluginHandleInner {
type_id,
invoke_fn: plugin.invoke_fn,
instance_id,
fini_method_id: fini_id,
finalized: std::sync::atomic::AtomicBool::new(false),
});
self.singletons.write().unwrap().insert((lib_name.to_string(), box_type.to_string()), handle);
Ok(())
}
/// Perform an external host call (env.* namespace) or return an error if unsupported /// Perform an external host call (env.* namespace) or return an error if unsupported
/// Returns Some(Box) for a value result, or None for void-like calls /// Returns Some(Box) for a value result, or None for void-like calls
pub fn extern_call( pub fn extern_call(
@ -344,12 +429,12 @@ impl PluginBoxV2 {
// Plugin Handle (BoxRef): tag=8, size=8 // Plugin Handle (BoxRef): tag=8, size=8
if let Some(p) = a.as_any().downcast_ref::<PluginBoxV2>() { if let Some(p) = a.as_any().downcast_ref::<PluginBoxV2>() {
eprintln!("[PluginLoaderV2] arg[{}]: PluginBoxV2({}, id={}) -> Handle(tag=8)", idx, p.box_type, p.instance_id); eprintln!("[PluginLoaderV2] arg[{}]: PluginBoxV2({}, id={}) -> Handle(tag=8)", idx, p.box_type, p.inner.instance_id);
buf.push(8u8); // tag buf.push(8u8); // tag
buf.push(0u8); // reserved buf.push(0u8); // reserved
buf.extend_from_slice(&(8u16).to_le_bytes()); buf.extend_from_slice(&(8u16).to_le_bytes());
buf.extend_from_slice(&p.type_id.to_le_bytes()); buf.extend_from_slice(&p.inner.type_id.to_le_bytes());
buf.extend_from_slice(&p.instance_id.to_le_bytes()); buf.extend_from_slice(&p.inner.instance_id.to_le_bytes());
continue; continue;
} }
// Integer: prefer i32 // Integer: prefer i32
@ -436,10 +521,13 @@ impl PluginBoxV2 {
let fini_id = ret_conf.methods.get("fini").map(|m| m.method_id); let fini_id = ret_conf.methods.get("fini").map(|m| m.method_id);
let pbox = PluginBoxV2 { let pbox = PluginBoxV2 {
box_type: ret_box.to_string(), box_type: ret_box.to_string(),
inner: std::sync::Arc::new(PluginHandleInner {
type_id: r_type, type_id: r_type,
invoke_fn: ret_plugin.invoke_fn, invoke_fn: ret_plugin.invoke_fn,
instance_id: r_inst, instance_id: r_inst,
fini_method_id: fini_id, fini_method_id: fini_id,
finalized: std::sync::atomic::AtomicBool::new(false),
}),
}; };
return Ok(Some(Box::new(pbox) as Box<dyn NyashBox>)); return Ok(Some(Box::new(pbox) as Box<dyn NyashBox>));
} }
@ -538,6 +626,23 @@ impl PluginBoxV2 {
BidError::InvalidType BidError::InvalidType
})?; })?;
// If singleton, return the pre-birthed shared handle
let cfg_path = self.config_path.as_ref().map(|s| s.as_str()).unwrap_or("nyash.toml");
if let Ok(toml_content) = std::fs::read_to_string(cfg_path) {
if let Ok(toml_value) = toml::from_str::<toml::Value>(&toml_content) {
if let Some(bc) = config.get_box_config(lib_name, box_type, &toml_value) {
if bc.singleton {
// ensure created
let _ = self.ensure_singleton_handle(lib_name, box_type);
if let Some(inner) = self.singletons.read().unwrap().get(&(lib_name.to_string(), box_type.to_string())) {
let plugin_box = PluginBoxV2 { box_type: box_type.to_string(), inner: inner.clone() };
return Ok(Box::new(plugin_box));
}
}
}
}
}
eprintln!("🔍 Found library: {} for box type: {}", lib_name, box_type); eprintln!("🔍 Found library: {} for box type: {}", lib_name, box_type);
// Get loaded plugin // Get loaded plugin
@ -552,7 +657,6 @@ impl PluginBoxV2 {
// Get type_id from config - read actual nyash.toml content // Get type_id from config - read actual nyash.toml content
eprintln!("🔍 Reading nyash.toml for type configuration..."); eprintln!("🔍 Reading nyash.toml for type configuration...");
let cfg_path = self.config_path.as_ref().map(|s| s.as_str()).unwrap_or("nyash.toml");
let (type_id, fini_method_id) = if let Ok(toml_content) = std::fs::read_to_string(cfg_path) { let (type_id, fini_method_id) = if let Ok(toml_content) = std::fs::read_to_string(cfg_path) {
eprintln!("🔍 nyash.toml read successfully"); eprintln!("🔍 nyash.toml read successfully");
if let Ok(toml_value) = toml::from_str::<toml::Value>(&toml_content) { if let Ok(toml_value) = toml::from_str::<toml::Value>(&toml_content) {
@ -616,14 +720,25 @@ impl PluginBoxV2 {
// Create v2 plugin box wrapper with actual instance_id // Create v2 plugin box wrapper with actual instance_id
let plugin_box = PluginBoxV2 { let plugin_box = PluginBoxV2 {
box_type: box_type.to_string(), box_type: box_type.to_string(),
inner: std::sync::Arc::new(PluginHandleInner {
type_id, type_id,
invoke_fn: plugin.invoke_fn, invoke_fn: plugin.invoke_fn,
instance_id, instance_id,
fini_method_id, fini_method_id,
finalized: std::sync::atomic::AtomicBool::new(false),
}),
}; };
Ok(Box::new(plugin_box)) Ok(Box::new(plugin_box))
} }
/// Shutdown singletons: finalize and clear all singleton handles
pub fn shutdown_singletons(&self) {
let mut map = self.singletons.write().unwrap();
for (_, handle) in map.drain() {
handle.finalize_now();
}
}
} }
// Global loader instance // Global loader instance
@ -647,6 +762,14 @@ impl PluginBoxV2 {
let loader = loader.read().unwrap(); let loader = loader.read().unwrap();
loader.load_all_plugins() loader.load_all_plugins()
} }
/// Gracefully shutdown plugins (finalize singletons)
pub fn shutdown_plugins_v2() -> BidResult<()> {
let loader = get_global_loader_v2();
let loader = loader.read().unwrap();
loader.shutdown_singletons();
Ok(())
}
} }
#[cfg(any(not(feature = "plugins"), target_arch = "wasm32"))] #[cfg(any(not(feature = "plugins"), target_arch = "wasm32"))]
@ -696,6 +819,7 @@ mod stub {
pub fn get_global_loader_v2() -> Arc<RwLock<PluginLoaderV2>> { GLOBAL_LOADER_V2.clone() } pub fn get_global_loader_v2() -> Arc<RwLock<PluginLoaderV2>> { GLOBAL_LOADER_V2.clone() }
pub fn init_global_loader_v2(_config_path: &str) -> BidResult<()> { Ok(()) } pub fn init_global_loader_v2(_config_path: &str) -> BidResult<()> { Ok(()) }
pub fn shutdown_plugins_v2() -> BidResult<()> { Ok(()) }
} }
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]

View File

@ -39,10 +39,9 @@ impl ScopeTracker {
let _ = instance.fini(); let _ = instance.fini();
continue; continue;
} }
// PluginBox: call plugin fini // PluginBoxV2: do not auto-finalize (shared handle may be referenced elsewhere)
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
if let Some(plugin) = arc_box.as_any().downcast_ref::<PluginBoxV2>() { if arc_box.as_any().downcast_ref::<PluginBoxV2>().is_some() {
plugin.call_fini();
continue; continue;
} }
// Builtin and others: no-op for now // Builtin and others: no-op for now

View File

@ -72,3 +72,29 @@ v
Err(e) => panic!("Counter assignment test failed: {:?}", e), Err(e) => panic!("Counter assignment test failed: {:?}", e),
} }
} }
#[test]
fn e2e_counter_mapbox_shares_handle() {
if !try_init_plugins() { return; }
let code = r#"
local c, m, v
c = new CounterBox()
m = new MapBox()
m.set("k", c)
v = m.get("k")
v.inc()
// c should reflect the increment if handle is shared
v = c.get()
v
"#;
let ast = NyashParser::parse_from_string(code).expect("parse failed");
let mut interpreter = nyash_rust::interpreter::NyashInterpreter::new();
match interpreter.execute(ast) {
Ok(result) => {
assert_eq!(result.to_string_box().value, "1");
}
Err(e) => panic!("Counter MapBox share test failed: {:?}", e),
}
}

View File

@ -0,0 +1,39 @@
#![cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
use nyash_rust::parser::NyashParser;
use nyash_rust::runtime::plugin_loader_v2::{init_global_loader_v2, get_global_loader_v2};
use nyash_rust::runtime::box_registry::get_global_registry;
use nyash_rust::runtime::PluginConfig;
fn try_init_plugins() -> bool {
if !std::path::Path::new("nyash.toml").exists() { return false; }
if let Err(e) = init_global_loader_v2("nyash.toml") { eprintln!("init failed: {:?}", e); return false; }
let loader = get_global_loader_v2();
let loader = loader.read().unwrap();
if let Some(conf) = &loader.config {
let mut map = std::collections::HashMap::new();
for (lib, def) in &conf.libraries { for b in &def.boxes { map.insert(b.clone(), lib.clone()); } }
get_global_registry().apply_plugin_config(&PluginConfig { plugins: map });
true
} else { false }
}
#[test]
fn e2e_counterbox_singleton_shared_across_news() {
if !try_init_plugins() { return; }
// CounterBox is configured as singleton in nyash.toml
let code = r#"
local a, b, v
a = new CounterBox()
b = new CounterBox()
a.inc()
v = b.get()
v
"#;
let ast = NyashParser::parse_from_string(code).expect("parse");
let mut interpreter = nyash_rust::interpreter::NyashInterpreter::new();
let result = interpreter.execute(ast).expect("exec");
assert_eq!(result.to_string_box().value, "1");
}

View File

@ -0,0 +1,51 @@
#![cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
use nyash_rust::parser::NyashParser;
use nyash_rust::runtime::plugin_loader_v2::{init_global_loader_v2, get_global_loader_v2, shutdown_plugins_v2};
use nyash_rust::runtime::box_registry::get_global_registry;
use nyash_rust::runtime::PluginConfig;
fn try_init_plugins() -> bool {
if !std::path::Path::new("nyash.toml").exists() { return false; }
if let Err(e) = init_global_loader_v2("nyash.toml") { eprintln!("init failed: {:?}", e); return false; }
let loader = get_global_loader_v2();
let loader = loader.read().unwrap();
if let Some(conf) = &loader.config {
let mut map = std::collections::HashMap::new();
for (lib, def) in &conf.libraries { for b in &def.boxes { map.insert(b.clone(), lib.clone()); } }
get_global_registry().apply_plugin_config(&PluginConfig { plugins: map });
true
} else { false }
}
#[test]
fn e2e_singleton_shutdown_and_recreate() {
if !try_init_plugins() { return; }
// Use CounterBox singleton and bump to 1
let code1 = r#"
local a
a = new CounterBox()
a.inc()
"#;
let ast1 = NyashParser::parse_from_string(code1).expect("parse1");
let mut interpreter = nyash_rust::interpreter::NyashInterpreter::new();
interpreter.execute(ast1).expect("exec1");
// Shutdown plugins (finalize singleton)
shutdown_plugins_v2().expect("shutdown ok");
// Re-init plugins and ensure singleton is recreated (count resets to 0)
assert!(try_init_plugins());
let code2 = r#"
local b, v
b = new CounterBox()
v = b.get()
v
"#;
let ast2 = NyashParser::parse_from_string(code2).expect("parse2");
let mut interpreter2 = nyash_rust::interpreter::NyashInterpreter::new();
let result = interpreter2.execute(ast2).expect("exec2");
assert_eq!(result.to_string_box().value, "0");
}