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:
23
CLAUDE.md
23
CLAUDE.md
@ -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
24
chatgpt5_build_errors.txt
Normal 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
|
||||||
|
|
||||||
|
: 7n<><6E>Ѥ<EFBFBD><D1A4><EFBFBD><EFBFBD>
|
||||||
21
chatgpt5_build_errors_updated.txt
Normal file
21
chatgpt5_build_errors_updated.txt
Normal 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のみ(他のモジュールのエラーは解決済み)
|
||||||
@ -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)` を呼び出す。
|
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)時:
|
- プラグインBox(PluginBoxV2):
|
||||||
- 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. WASM(wasm-bindgen)との関係
|
## 5. WASM(wasm-bindgen)との関係
|
||||||
|
|||||||
@ -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 }
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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メソッドを持たないので呼ばない(要修正)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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) => {
|
||||||
@ -818,23 +818,23 @@ impl NyashInterpreter {
|
|||||||
{
|
{
|
||||||
// 親がユーザー定義に見つからない場合は、プラグインとして試行
|
// 親がユーザー定義に見つからない場合は、プラグインとして試行
|
||||||
// 現在のインスタンスから __plugin_content を参照
|
// 現在のインスタンスから __plugin_content を参照
|
||||||
if let Some(plugin_shared) = current_instance.get_field_legacy("__plugin_content") {
|
if let Some(plugin_shared) = current_instance.get_field_legacy("__plugin_content") {
|
||||||
// 引数を評価(ロックは既に解放済みの設計)
|
// 引数を評価(ロックは既に解放済みの設計)
|
||||||
let plugin_ref = &*plugin_shared;
|
let plugin_ref = &*plugin_shared;
|
||||||
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 {
|
for arg in arguments {
|
||||||
arg_values.push(self.execute_expression(arg)?);
|
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の場合)
|
// 3. 親クラスのBox宣言を取得(ユーザー定義Boxの場合)
|
||||||
@ -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) }),
|
||||||
|
|||||||
@ -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,
|
||||||
instance_id: new_instance_id,
|
invoke_fn: self.inner.invoke_fn,
|
||||||
fini_method_id: self.fini_method_id,
|
instance_id: new_instance_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(),
|
||||||
type_id: r_type,
|
inner: std::sync::Arc::new(PluginHandleInner {
|
||||||
invoke_fn: ret_plugin.invoke_fn,
|
type_id: r_type,
|
||||||
instance_id: r_inst,
|
invoke_fn: ret_plugin.invoke_fn,
|
||||||
fini_method_id: fini_id,
|
instance_id: r_inst,
|
||||||
|
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(),
|
||||||
type_id,
|
inner: std::sync::Arc::new(PluginHandleInner {
|
||||||
invoke_fn: plugin.invoke_fn,
|
type_id,
|
||||||
instance_id,
|
invoke_fn: plugin.invoke_fn,
|
||||||
fini_method_id,
|
instance_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")))]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
39
tests/e2e_plugin_singleton.rs
Normal file
39
tests/e2e_plugin_singleton.rs
Normal 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");
|
||||||
|
}
|
||||||
|
|
||||||
51
tests/e2e_plugin_singleton_shutdown.rs
Normal file
51
tests/e2e_plugin_singleton_shutdown.rs
Normal 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");
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user