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)
|
||||
|
||||
## 🤝 プロアクティブ開発方針
|
||||
|
||||
### 🎯 エラー対応時の姿勢
|
||||
エラーを見つけた際は、単に報告するだけでなく:
|
||||
|
||||
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)` を呼び出す。
|
||||
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> }` を生成して返す。
|
||||
- `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)との関係
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -564,7 +564,7 @@ impl VM {
|
||||
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 = 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));
|
||||
|
||||
@ -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::<crate::runtime::plugin_loader_v2::PluginBoxV2>().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<Box<dyn NyashBox>> = 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::<crate::runtime::plugin_loader_v2::PluginBoxV2>().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<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();
|
||||
|
||||
ArrayBox {
|
||||
|
||||
@ -135,7 +135,13 @@ impl MapBox {
|
||||
pub fn get(&self, key: Box<dyn NyashBox>) -> Box<dyn NyashBox> {
|
||||
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::<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))),
|
||||
}
|
||||
}
|
||||
@ -310,4 +316,4 @@ impl Debug for MapBox {
|
||||
.field("keys", &data.keys().collect::<Vec<_>>())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,6 +47,10 @@ pub struct BoxTypeConfig {
|
||||
|
||||
/// Method definitions
|
||||
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)
|
||||
|
||||
@ -575,7 +575,14 @@ impl NyashInterpreter {
|
||||
/// local変数スタックを保存・復元(関数呼び出し時)
|
||||
pub(super) fn save_local_vars(&self) -> HashMap<String, Box<dyn NyashBox>> {
|
||||
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()
|
||||
}
|
||||
|
||||
@ -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::<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等はリソース管理不要)
|
||||
}
|
||||
@ -612,7 +614,14 @@ impl NyashInterpreter {
|
||||
/// outbox変数スタックを保存・復元(static関数呼び出し時)
|
||||
pub(super) fn save_outbox_vars(&self) -> HashMap<String, Box<dyn NyashBox>> {
|
||||
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()
|
||||
}
|
||||
|
||||
@ -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::<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 = 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::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
|
||||
let mut arg_values: Vec<Box<dyn NyashBox>> = 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::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
|
||||
let mut arg_values: Vec<Box<dyn NyashBox>> = 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::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
|
||||
let mut arg_values: Vec<Box<dyn NyashBox>> = 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) }),
|
||||
|
||||
@ -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<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 {
|
||||
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> {
|
||||
@ -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<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()
|
||||
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<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
|
||||
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<NyashConfigV2>,
|
||||
/// Path to the loaded nyash.toml (absolute), used for consistent re-reads
|
||||
config_path: Option<String>,
|
||||
|
||||
/// Singleton instances: (lib_name, box_type) -> shared handle
|
||||
singletons: RwLock<HashMap<(String,String), std::sync::Arc<PluginHandleInner>>>,
|
||||
}
|
||||
|
||||
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::<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(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<dyn NyashBox>));
|
||||
}
|
||||
@ -521,7 +609,7 @@ impl PluginBoxV2 {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Create a Box instance
|
||||
pub fn create_box(&self, box_type: &str, _args: &[Box<dyn NyashBox>]) -> BidResult<Box<dyn NyashBox>> {
|
||||
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::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);
|
||||
|
||||
// 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::Value>(&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<RwLock<PluginLoaderV2>> { 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")))]
|
||||
|
||||
@ -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::<PluginBoxV2>() {
|
||||
plugin.call_fini();
|
||||
if arc_box.as_any().downcast_ref::<PluginBoxV2>().is_some() {
|
||||
continue;
|
||||
}
|
||||
// Builtin and others: no-op for now
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
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