From da716addc83ca8a93ad52f834797f8fb77a7dafc Mon Sep 17 00:00:00 2001 From: Moe Charm Date: Thu, 21 Aug 2025 21:35:17 +0900 Subject: [PATCH] feat: Implement plugin singleton pattern with shutdown support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CLAUDE.md | 23 ++ chatgpt5_build_errors.txt | 24 ++ chatgpt5_build_errors_updated.txt | 21 ++ .../boxes-system/plugin_lifecycle.md | 38 ++- nyash.toml | 1 + src/backend/vm.rs | 2 +- src/boxes/array/mod.rs | 24 +- src/boxes/map_box.rs | 10 +- src/config/nyash_toml_v2.rs | 4 + src/interpreter/core.rs | 32 +-- src/interpreter/expressions/calls.rs | 38 +-- src/runtime/plugin_loader_v2.rs | 234 ++++++++++++++---- src/scope_tracker.rs | 5 +- tests/e2e_plugin_counterbox.rs | 26 ++ tests/e2e_plugin_singleton.rs | 39 +++ tests/e2e_plugin_singleton_shutdown.rs | 51 ++++ 16 files changed, 465 insertions(+), 107 deletions(-) create mode 100644 chatgpt5_build_errors.txt create mode 100644 chatgpt5_build_errors_updated.txt create mode 100644 tests/e2e_plugin_singleton.rs create mode 100644 tests/e2e_plugin_singleton_shutdown.rs diff --git a/CLAUDE.md b/CLAUDE.md index 5025db71..6119aefa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -450,6 +450,29 @@ docs/ **📋 詳細**: [DOCUMENTATION_REORGANIZATION_STRATEGY.md](DOCUMENTATION_REORGANIZATION_STRATEGY.md) +## 🤝 プロアクティブ開発方針 + +### 🎯 エラー対応時の姿勢 +エラーを見つけた際は、単に報告するだけでなく: + +1. **🔍 原因分析** - エラーの根本原因を探る +2. **📊 影響範囲** - 他のコードへの影響を調査 +3. **💡 改善提案** - 関連する問題も含めて解決策を提示 +4. **🧹 機会改善** - デッドコード削除など、ついでにできる改善も実施 + +### ⚖️ バランスの取り方 +- **積極的に分析・提案**するが、最終判断はユーザーに委ねる +- 「ChatGPTさんに任せてる」と言われても、分析結果は共有する +- 複数のAIが協調する場合でも、各自の視点で価値を提供する + +### 📝 例 +``` +❌ 受動的: 「エラーをファイルに出力しました」 +✅ 能動的: 「エラーをファイルに出力しました。主な原因は型の不一致(7箇所)で、 + instance_id()のメソッド呼び出し修正で5つ解決できそうです。 + また、関連してclone_boxの実装にも同様の問題を発見しました。」 +``` + ## 🚨 コンテキスト圧縮時の重要ルール ### ⚠️ **コンテキスト圧縮を検出した場合の必須手順** diff --git a/chatgpt5_build_errors.txt b/chatgpt5_build_errors.txt new file mode 100644 index 00000000..d12b9ae7 --- /dev/null +++ b/chatgpt5_build_errors.txt @@ -0,0 +1,24 @@ +ChatGPT5kɨ: + +1. [E0599] no method named `call_fini` found for reference `&enabled::PluginBoxV2` + 4@: src/scope_tracker.rs:45:28 + : plugin.call_fini() - LX(WjD + +2. [E0308] mismatched types (2@) + 4@: src/interpreter/core.rs:579:45, 618:45 + : &**v - expected `&Box`, 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 + : plugin.instance_id - ɒգhWf( + cH: plugin.instance_id() k + +4. [E0609] no field `invoke_fn` on type `&enabled::PluginBoxV2` + 4@: src/runtime/plugin_loader_v2.rs:139:19 + : self.invoke_fn - X(WjDգ + cH: self.inner.invoke_fn + +: 7 nѤ \ No newline at end of file diff --git a/chatgpt5_build_errors_updated.txt b/chatgpt5_build_errors_updated.txt new file mode 100644 index 00000000..2e72d90b --- /dev/null +++ b/chatgpt5_build_errors_updated.txt @@ -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`, found `&dyn NyashBox` + +2. [E0308] mismatched types - src/interpreter/core.rs:618:45 + エラー: &**v - expected `&Box`, found `&dyn NyashBox` + +推奨修正: +- &**v を v に変更(Arcへの参照として扱う) +- または型注釈を &dyn NyashBox に変更 + +影響範囲: +- interpreter/core.rsのみ(他のモジュールのエラーは解決済み) \ No newline at end of file diff --git a/docs/reference/boxes-system/plugin_lifecycle.md b/docs/reference/boxes-system/plugin_lifecycle.md index 889c7242..bdcf5b6e 100644 --- a/docs/reference/boxes-system/plugin_lifecycle.md +++ b/docs/reference/boxes-system/plugin_lifecycle.md @@ -1,6 +1,6 @@ -# プラグインBoxのライフサイクルと nyash.toml methods 定義 +# プラグインBoxのライフサイクル(v2)と nyash.toml 定義 -本書は、プラグインBox(PluginBoxV2)の生成(birth)と終了(fini)の流れ、ならびに nyash.toml v2 における `methods` 定義の役割をまとめたものです。 +本書は、プラグインBox(PluginBoxV2)の生成(birth)と終了(fini)の流れ、`singleton` オプション、ならびに nyash.toml v2 における `methods` 定義の役割をまとめたものです。 --- @@ -15,26 +15,26 @@ 1. `unified registry` が `PluginLoaderV2::create_box(box_type, args)` を呼び出す。 2. `PluginLoaderV2` は `nyash.toml` から `type_id` と `methods` を読み込む。 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` は `{ type_id, instance_id, invoke_fn, fini_method_id, finalized }` を保持し、参照カウント(Arc)で共有される。 補足: - `fini_method_id` は `nyash.toml` の `methods` から `fini` の `method_id` を取り出して保持します。未定義の場合は `None`。 --- -## 3. 終了(fini)の流れ(現状) +## 3. 終了(fini)の流れ(現在) - フィールド差し替え時(代入で旧値を置き換えるとき): - 旧値が `InstanceBox` の場合: インタプリタが `fini()` を呼び、finalized としてマーキングします。 - 旧値が `PluginBoxV2` の場合: `fini_method_id` が設定されていれば `invoke_fn(type_id, fini_method_id, instance_id, ...)` を呼びます。 -- 破棄(Drop)時: - - RustのDropでFFIを呼ぶのは安全性の観点でリスクがあるため、現状は「明示タイミング(フィールド差し替えなど)」での fini 呼び出しを優先しています。 - -注意: -- ローカル変数のスコープ終了時に自動で fini を呼ぶ実装は、現時点では入っていません(将来検討)。 +- プラグインBox(PluginBoxV2): + - すべての参照(Arc)がDropされ「最後の参照が解放」された時、`Drop`で一度だけ `fini` を呼ぶ(RAII、二重呼び出し防止)。 + - 明示finiが必要な場合は `PluginBoxV2::finalize_now()` を使える(内部的に一度だけfini実行)。 + - 代入/フィールド代入/Map.get/Array.get/slice/退避などは「PluginBoxV2は共有(share)、それ以外は複製(clone)」で統一。 --- -## 4. nyash.toml v2 の定義例 +## 4. nyash.toml v2 の定義例(methods + singleton) ```toml [libraries] @@ -58,6 +58,24 @@ fini = { method_id = 4294967295 } # 任意の終端ID - `methods` に `fini` を定義すれば、差し替え時などに fini が呼ばれます。 - `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. WASM(wasm-bindgen)との関係 diff --git a/nyash.toml b/nyash.toml index ffcf2a25..f578f080 100644 --- a/nyash.toml +++ b/nyash.toml @@ -38,6 +38,7 @@ cloneSelf = { method_id = 8 } [libraries."libnyash_counter_plugin.so".CounterBox] type_id = 7 +singleton = true [libraries."libnyash_counter_plugin.so".CounterBox.methods] birth = { method_id = 0 } diff --git a/src/backend/vm.rs b/src/backend/vm.rs index bca1184d..fa08b56a 100644 --- a/src/backend/vm.rs +++ b/src/backend/vm.rs @@ -564,7 +564,7 @@ impl VM { if let Some(plugin) = box_nyash.as_any().downcast_ref::() { let loader = crate::runtime::get_global_loader_v2(); 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)) => { if let Some(dst_id) = dst { self.set_value(*dst_id, VMValue::from_nyash_box(result_box)); diff --git a/src/boxes/array/mod.rs b/src/boxes/array/mod.rs index 4065f09f..a4432262 100644 --- a/src/boxes/array/mod.rs +++ b/src/boxes/array/mod.rs @@ -59,7 +59,13 @@ impl ArrayBox { let idx = idx_box.value as usize; let items = self.items.read().unwrap(); 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::().is_some() { + return item.share_box(); + } + item.clone_box() + } None => Box::new(crate::boxes::null_box::NullBox::new()), } } else { @@ -228,7 +234,13 @@ impl ArrayBox { // Create slice let slice_items: Vec> = items[start_idx..end_idx] .iter() - .map(|item| item.clone_box()) + .map(|item| { + #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] + if item.as_any().downcast_ref::().is_some() { + return item.share_box(); + } + item.clone_box() + }) .collect(); Box::new(ArrayBox::new_with_elements(slice_items)) @@ -241,7 +253,13 @@ impl Clone for ArrayBox { // ディープコピー(独立インスタンス) let items_guard = self.items.read().unwrap(); let cloned_items: Vec> = items_guard.iter() - .map(|item| item.clone_box()) // 要素もディープコピー + .map(|item| { + #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] + if item.as_any().downcast_ref::().is_some() { + return item.share_box(); + } + item.clone_box() + }) // 要素もディープコピー(ハンドルは共有) .collect(); ArrayBox { diff --git a/src/boxes/map_box.rs b/src/boxes/map_box.rs index 27a10b62..9f41840a 100644 --- a/src/boxes/map_box.rs +++ b/src/boxes/map_box.rs @@ -135,7 +135,13 @@ impl MapBox { pub fn get(&self, key: Box) -> Box { let key_str = key.to_string_box().value; 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::().is_some() { + return value.share_box(); + } + value.clone_box() + } None => Box::new(StringBox::new(&format!("Key not found: {}", key_str))), } } @@ -310,4 +316,4 @@ impl Debug for MapBox { .field("keys", &data.keys().collect::>()) .finish() } -} \ No newline at end of file +} diff --git a/src/config/nyash_toml_v2.rs b/src/config/nyash_toml_v2.rs index 8033ebc3..5c3dd310 100644 --- a/src/config/nyash_toml_v2.rs +++ b/src/config/nyash_toml_v2.rs @@ -47,6 +47,10 @@ pub struct BoxTypeConfig { /// Method definitions pub methods: HashMap, + + /// Singleton service flag (keep one shared instance alive in loader) + #[serde(default)] + pub singleton: bool, } /// Method definition (simplified - no argument info needed) diff --git a/src/interpreter/core.rs b/src/interpreter/core.rs index f37768a8..64a97265 100644 --- a/src/interpreter/core.rs +++ b/src/interpreter/core.rs @@ -575,7 +575,14 @@ impl NyashInterpreter { /// local変数スタックを保存・復元(関数呼び出し時) pub(super) fn save_local_vars(&self) -> HashMap> { 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::().is_some() { + return (k.clone(), b.share_box()); + } + (k.clone(), b.clone_box()) + }) .collect() } @@ -593,12 +600,7 @@ impl NyashInterpreter { let _ = instance.fini(); eprintln!("🔄 Scope exit: Called fini() on local variable '{}' (InstanceBox)", name); } - // プラグインBoxの場合 - #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] - if let Some(plugin) = (**value).as_any().downcast_ref::() { - plugin.call_fini(); - eprintln!("🔄 Scope exit: Called fini() on local variable '{}' (PluginBox)", name); - } + // プラグインBoxは共有ハンドルの可能性が高いため自動finiしない(明示呼び出しのみ) // ビルトインBoxは元々finiメソッドを持たないので呼ばない // (StringBox、IntegerBox等はリソース管理不要) } @@ -612,7 +614,14 @@ impl NyashInterpreter { /// outbox変数スタックを保存・復元(static関数呼び出し時) pub(super) fn save_outbox_vars(&self) -> HashMap> { 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::().is_some() { + return (k.clone(), b.share_box()); + } + (k.clone(), b.clone_box()) + }) .collect() } @@ -624,12 +633,7 @@ impl NyashInterpreter { let _ = instance.fini(); eprintln!("🔄 Scope exit: Called fini() on outbox variable '{}' (InstanceBox)", name); } - // プラグインBoxの場合 - #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] - if let Some(plugin) = (**value).as_any().downcast_ref::() { - plugin.call_fini(); - eprintln!("🔄 Scope exit: Called fini() on outbox variable '{}' (PluginBox)", name); - } + // プラグインBoxは共有ハンドルの可能性が高いため自動finiしない // ビルトインBoxは元々finiメソッドを持たないので呼ばない(要修正) } diff --git a/src/interpreter/expressions/calls.rs b/src/interpreter/expressions/calls.rs index f089f016..69523a33 100644 --- a/src/interpreter/expressions/calls.rs +++ b/src/interpreter/expressions/calls.rs @@ -692,7 +692,7 @@ impl NyashInterpreter { } let loader = crate::runtime::get_global_loader_v2(); 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(None) => return Ok(Box::new(VoidBox::new())), Err(_) => {} @@ -782,7 +782,7 @@ impl NyashInterpreter { if let Some(plugin) = plugin_ref.as_any().downcast_ref::() { let mut arg_values: Vec> = Vec::new(); 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(None) => return Ok(Box::new(crate::box_trait::VoidBox::new())), Err(e) => { @@ -818,23 +818,23 @@ impl NyashInterpreter { { // 親がユーザー定義に見つからない場合は、プラグインとして試行 // 現在のインスタンスから __plugin_content を参照 - if let Some(plugin_shared) = current_instance.get_field_legacy("__plugin_content") { - // 引数を評価(ロックは既に解放済みの設計) - let plugin_ref = &*plugin_shared; - if let Some(plugin) = plugin_ref.as_any().downcast_ref::() { - let mut arg_values: Vec> = Vec::new(); - for arg in arguments { - arg_values.push(self.execute_expression(arg)?); + if let Some(plugin_shared) = current_instance.get_field_legacy("__plugin_content") { + // 引数を評価(ロックは既に解放済みの設計) + let plugin_ref = &*plugin_shared; + if let Some(plugin) = plugin_ref.as_any().downcast_ref::() { + let mut arg_values: Vec> = Vec::new(); + for arg in arguments { + arg_values.push(self.execute_expression(arg)?); + } + let loader = crate::runtime::get_global_loader_v2(); + let loader = loader.read().unwrap(); + match loader.invoke_instance_method(&plugin.box_type, method, plugin.instance_id(), &arg_values) { + Ok(Some(result_box)) => return Ok(result_box), + Ok(None) => return Ok(Box::new(VoidBox::new())), + Err(_) => {} + } + } } - let loader = crate::runtime::get_global_loader_v2(); - let loader = loader.read().unwrap(); - match loader.invoke_instance_method(&plugin.box_type, method, plugin.instance_id, &arg_values) { - Ok(Some(result_box)) => return Ok(result_box), - Ok(None) => return Ok(Box::new(VoidBox::new())), - Err(_) => {} - } - } - } } // 3. 親クラスのBox宣言を取得(ユーザー定義Boxの場合) @@ -1003,7 +1003,7 @@ impl NyashInterpreter { } 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() })?; - 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(None) => Ok(Box::new(VoidBox::new())), Err(e) => Err(RuntimeError::RuntimeFailure { message: format!("Plugin method {} failed: {:?}", method, e) }), diff --git a/src/runtime/plugin_loader_v2.rs b/src/runtime/plugin_loader_v2.rs index 404ee17a..9b5f5930 100644 --- a/src/runtime/plugin_loader_v2.rs +++ b/src/runtime/plugin_loader_v2.rs @@ -34,18 +34,71 @@ mod enabled { /// v2 Plugin Box wrapper - temporary implementation #[derive(Debug)] - pub struct PluginBoxV2 { - pub box_type: String, + pub struct PluginHandleInner { pub type_id: u32, pub invoke_fn: unsafe extern "C" fn(u32, u32, u32, *const u8, usize, *mut u8, *mut usize) -> i32, pub instance_id: u32, - /// Optional fini method_id from nyash.toml (None if not provided) pub fini_method_id: Option, + 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, } impl BoxCore for PluginBoxV2 { fn box_id(&self) -> u64 { - self.instance_id as u64 + self.inner.instance_id as u64 } fn parent_type_id(&self) -> Option { @@ -53,7 +106,7 @@ mod enabled { } 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 { @@ -75,7 +128,7 @@ mod enabled { } fn clone_box(&self) -> Box { - 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() 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 result = unsafe { - (self.invoke_fn)( - self.type_id, + (self.inner.invoke_fn)( + self.inner.type_id, 0, // method_id=0 (birth) 0, // instance_id=0 (static call) tlv_args.as_ptr(), @@ -103,13 +156,16 @@ mod enabled { 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_type: self.box_type.clone(), - type_id: self.type_id, - invoke_fn: self.invoke_fn, - instance_id: new_instance_id, - fini_method_id: self.fini_method_id, + inner: std::sync::Arc::new(PluginHandleInner { + type_id: self.inner.type_id, + invoke_fn: self.inner.invoke_fn, + instance_id: new_instance_id, + fini_method_id: self.inner.fini_method_id, + finalized: std::sync::atomic::AtomicBool::new(false), + }), }) } else { eprintln!("❌ clone_box failed: birth() returned error code {}", result); @@ -119,7 +175,7 @@ mod enabled { } 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 { @@ -127,43 +183,19 @@ mod enabled { } fn share_box(&self) -> Box { - 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 Box::new(PluginBoxV2 { box_type: self.box_type.clone(), - type_id: self.type_id, - invoke_fn: self.invoke_fn, - instance_id: self.instance_id, // Same instance_id - this is sharing! - fini_method_id: self.fini_method_id, + inner: self.inner.clone(), }) } } impl PluginBoxV2 { - /// Call fini() on this plugin instance if configured - pub fn call_fini(&self) { - 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); - } - } - } + pub fn instance_id(&self) -> u32 { self.inner.instance_id } + pub fn finalize_now(&self) { self.inner.finalize_now() } } /// Plugin loader v2 @@ -175,6 +207,9 @@ impl PluginBoxV2 { pub config: Option, /// Path to the loaded nyash.toml (absolute), used for consistent re-reads config_path: Option, + + /// Singleton instances: (lib_name, box_type) -> shared handle + singletons: RwLock>>, } impl PluginLoaderV2 { @@ -196,6 +231,7 @@ impl PluginBoxV2 { plugins: RwLock::new(HashMap::new()), config: None, config_path: None, + singletons: RwLock::new(HashMap::new()), } } @@ -225,10 +261,59 @@ impl PluginBoxV2 { 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(()) } + /// 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 /// Returns Some(Box) for a value result, or None for void-like calls pub fn extern_call( @@ -344,12 +429,12 @@ impl PluginBoxV2 { // Plugin Handle (BoxRef): tag=8, size=8 if let Some(p) = a.as_any().downcast_ref::() { - 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(0u8); // reserved buf.extend_from_slice(&(8u16).to_le_bytes()); - buf.extend_from_slice(&p.type_id.to_le_bytes()); - buf.extend_from_slice(&p.instance_id.to_le_bytes()); + buf.extend_from_slice(&p.inner.type_id.to_le_bytes()); + buf.extend_from_slice(&p.inner.instance_id.to_le_bytes()); continue; } // Integer: prefer i32 @@ -436,10 +521,13 @@ impl PluginBoxV2 { let fini_id = ret_conf.methods.get("fini").map(|m| m.method_id); let pbox = PluginBoxV2 { box_type: ret_box.to_string(), - type_id: r_type, - invoke_fn: ret_plugin.invoke_fn, - instance_id: r_inst, - fini_method_id: fini_id, + inner: std::sync::Arc::new(PluginHandleInner { + type_id: r_type, + invoke_fn: ret_plugin.invoke_fn, + instance_id: r_inst, + fini_method_id: fini_id, + finalized: std::sync::atomic::AtomicBool::new(false), + }), }; return Ok(Some(Box::new(pbox) as Box)); } @@ -521,7 +609,7 @@ impl PluginBoxV2 { Ok(()) } - + /// Create a Box instance pub fn create_box(&self, box_type: &str, _args: &[Box]) -> BidResult> { eprintln!("🔍 create_box called for: {}", box_type); @@ -538,6 +626,23 @@ impl PluginBoxV2 { 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_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); // Get loaded plugin @@ -552,7 +657,6 @@ impl PluginBoxV2 { // Get type_id from config - read actual nyash.toml content 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) { eprintln!("🔍 nyash.toml read successfully"); if let Ok(toml_value) = toml::from_str::(&toml_content) { @@ -616,14 +720,25 @@ impl PluginBoxV2 { // Create v2 plugin box wrapper with actual instance_id let plugin_box = PluginBoxV2 { box_type: box_type.to_string(), - type_id, - invoke_fn: plugin.invoke_fn, - instance_id, - fini_method_id, + inner: std::sync::Arc::new(PluginHandleInner { + type_id, + invoke_fn: plugin.invoke_fn, + instance_id, + fini_method_id, + finalized: std::sync::atomic::AtomicBool::new(false), + }), }; 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 @@ -647,6 +762,14 @@ impl PluginBoxV2 { let loader = loader.read().unwrap(); 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"))] @@ -696,6 +819,7 @@ mod stub { pub fn get_global_loader_v2() -> Arc> { GLOBAL_LOADER_V2.clone() } 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")))] diff --git a/src/scope_tracker.rs b/src/scope_tracker.rs index 05a5c60b..7c30196b 100644 --- a/src/scope_tracker.rs +++ b/src/scope_tracker.rs @@ -39,10 +39,9 @@ impl ScopeTracker { let _ = instance.fini(); continue; } - // PluginBox: call plugin fini + // PluginBoxV2: do not auto-finalize (shared handle may be referenced elsewhere) #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] - if let Some(plugin) = arc_box.as_any().downcast_ref::() { - plugin.call_fini(); + if arc_box.as_any().downcast_ref::().is_some() { continue; } // Builtin and others: no-op for now diff --git a/tests/e2e_plugin_counterbox.rs b/tests/e2e_plugin_counterbox.rs index fad48bac..621c2496 100644 --- a/tests/e2e_plugin_counterbox.rs +++ b/tests/e2e_plugin_counterbox.rs @@ -72,3 +72,29 @@ v 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), + } +} diff --git a/tests/e2e_plugin_singleton.rs b/tests/e2e_plugin_singleton.rs new file mode 100644 index 00000000..da7af1d0 --- /dev/null +++ b/tests/e2e_plugin_singleton.rs @@ -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"); +} + diff --git a/tests/e2e_plugin_singleton_shutdown.rs b/tests/e2e_plugin_singleton_shutdown.rs new file mode 100644 index 00000000..8f2129df --- /dev/null +++ b/tests/e2e_plugin_singleton_shutdown.rs @@ -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"); +} +