feat: Add parallel HTTP server E2E tests and enhance plugin system

- Add e2e_plugin_net_additional.rs with parallel server tests
- Fix test to properly handle request objects (no double accept)
- Add comprehensive net-plugin documentation
- Implement debug tracing for method calls
- Enhance plugin lifecycle documentation
- Improve error handling in plugin loader
- Add leak tracking infrastructure (for future use)
- Update language spec with latest plugin features

This enhances test coverage for concurrent HTTP servers and improves
the overall plugin system documentation and debugging capabilities.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Moe Charm
2025-08-22 05:01:11 +09:00
parent 98e9893bf5
commit f2761004d3
20 changed files with 1288 additions and 105 deletions

View File

@ -3,9 +3,11 @@
Nyashプログラミング言語開発に必要な情報をまとめたクイックリファレンス。
## 🧭 Start Here (最初に見る)
- **🎯 主軸タスク**: [docs/予定/native-plan/copilot_issues.txt](docs/予定/native-plan/copilot_issues.txt) **← 最重要!**
- 現在のタスク: [docs/CURRENT_TASK.md](docs/CURRENT_TASK.md)
- コア概念(速習): [docs/nyash_core_concepts.md](docs/nyash_core_concepts.md)
- **🎯 主軸タスク**: [docs/development/roadmap/native-plan/copilot_issues.txt](docs/development/roadmap/native-plan/copilot_issues.txt) **← 最重要!**
- 現在のタスク: [docs/development/current/CURRENT_TASK.md](docs/development/current/CURRENT_TASK.md)
- ドキュメント入口: [docs/README.md](docs/README.md)
- コア概念(速習): [docs/reference/architecture/nyash_core_concepts.md](docs/reference/architecture/nyash_core_concepts.md)
- NetプラグインHTTP/TCP: [docs/reference/plugin-system/net-plugin.md](docs/reference/plugin-system/net-plugin.md)
## 🤖 **Claude×Copilot協調開発の主軸**
### 📋 **copilot_issues.txt - 開発の軸となるファイル**
@ -22,7 +24,7 @@ Nyashプログラミング言語開発に必要な情報をまとめたクイッ
## 🚀 クイックスタート
### 🎯 実行方式選択 (重要!)
- **実行バックエンド完全ガイド**: [docs/execution-backends.md](docs/execution-backends.md)
- **実行バックエンド完全ガイド**: [docs/reference/architecture/execution-backends.md](docs/reference/architecture/execution-backends.md)
- インタープリター(開発・デバッグ)/ VM高速実行/ WASMWeb配布
-**ベンチマーク機能**: `--benchmark` で3バックエンド性能比較13.5倍実行高速化実証済み!)
@ -76,33 +78,34 @@ python3 -m http.server 8010
## 📚 ドキュメント構造
### 🎯 **最重要ドキュメント(開発者向け)**
- **[copilot_issues.txt](docs/予定/native-plan/copilot_issues.txt)** - **Phase順開発計画の軸**
- **[CURRENT_TASK.md](docs/CURRENT_TASK.md)** - 現在進行状況詳細
- **[native-plan/README.md](docs/予定/native-plan/README.md)** - ネイティブビルド計画
- **[copilot_issues.txt](docs/development/roadmap/native-plan/copilot_issues.txt)** - **Phase順開発計画の軸**
- **[CURRENT_TASK.md](docs/development/current/CURRENT_TASK.md)** - 現在進行状況詳細
- **[native-plan/README.md](docs/development/roadmap/native-plan/README.md)** - ネイティブビルド計画
### 📖 利用者向けドキュメント
- 説明書: docs/説明書/README.md
- ネイティブビルド: docs/説明書/native-build/README.md
- WASM: docs/説明書/wasm/
- リファレンス: docs/説明書/reference/
- 予定: docs/予定/README.md
- CURRENT_TASK, ネイティブ計画, フェーズ課題
- archive: docs/archive/
- 入口: [docs/README.md](docs/README.md)
- Getting Started: [docs/guides/getting-started.md](docs/guides/getting-started.md)
- Language Guide: [docs/guides/language-guide.md](docs/guides/language-guide.md)
- Reference: [docs/reference/](docs/reference/)
- 開発計画/進捗: [docs/development/](docs/development/)
- 現在タスク: [docs/development/current/CURRENT_TASK.md](docs/development/current/CURRENT_TASK.md)
- ネイティブ計画: [docs/development/roadmap/native-plan/](docs/development/roadmap/native-plan/)
- フェーズ課題: [docs/development/roadmap/](docs/development/roadmap/)
- アーカイブ: [docs/archive/](docs/archive/)
### 🎯 よく使う情報
- Getting Started: docs/説明書/GETTING_STARTED.md
- Language Guide: docs/説明書/LANGUAGE_GUIDE.md
- Playground Guide: docs/説明書/guides/playground_guide.md
- Getting Started: [docs/guides/getting-started.md](docs/guides/getting-started.md)
- Language Guide: [docs/guides/language-guide.md](docs/guides/language-guide.md)
- Playground Guide: [docs/guides/tutorials/playground_guide.md](docs/guides/tutorials/playground_guide.md)
### 📊 最新開発状況
- 現在のタスク: docs/CURRENT_TASK.md
- 予定インデックス: docs/予定/README.md
- 現在のタスク: [docs/development/current/CURRENT_TASK.md](docs/development/current/CURRENT_TASK.md)
- ロードマップ: [docs/development/roadmap/](docs/development/roadmap/)
### 📖 詳細リファレンス
- リファレンス: docs/説明書/reference/
- 言語: docs/説明書/reference/language-reference.md
- デリゲーション構文: docs/説明書/reference/override-delegation-syntax.md
- ビルトイン: docs/説明書/reference/builtin-boxes.md
- fini/弱参照: docs/説明書/reference/finalization-system.md
- リファレンス: [docs/reference/](docs/reference/)
- 言語仕様: [docs/reference/language/LANGUAGE_REFERENCE_2025.md](docs/reference/language/LANGUAGE_REFERENCE_2025.md)
- 可視性/デリゲーション: [docs/reference/language/field-visibility-and-delegation.md](docs/reference/language/field-visibility-and-delegation.md)
- Box/プラグイン: [docs/reference/boxes-system/](docs/reference/boxes-system/), [docs/reference/plugin-system/](docs/reference/plugin-system/)
### 🎮 実用例・アプリ
- **[実用例](docs/説明書/guides/)** - サンプルコード・パターン集
- **[実用例](docs/guides/)** - サンプルコード・パターン集
- **実装済みアプリ**: サイコロRPG・統計計算・LISPインタープリター
## ⚡ 重要な設計原則
@ -285,12 +288,12 @@ app.setSize(800, 600)
#### 🔤 言語仕様
- **[構文早見表](docs/quick-reference/syntax-cheatsheet.md)** - 基本構文・よくある間違い
- **[完全リファレンス](docs/説明書/reference/)** - 言語仕様詳細
- **[予約語一覧](docs/説明書/reference/keywords.md)** - from, override, pack等
- **[完全リファレンス](docs/reference/)** - 言語仕様詳細
- 予約語や構文: [docs/reference/language/LANGUAGE_REFERENCE_2025.md](docs/reference/language/LANGUAGE_REFERENCE_2025.md)
#### 📦 主要BOXのAPI
- **[ビルトイン型](docs/説明書/reference/built-in-boxes.md)** - 全Box型API仕様
- **P2PBox & IntentBox** - docs/説明書/reference/built-in-boxes.md内
- **[Box/プラグイン関連](docs/reference/boxes-system/)** - APIと設計
- **P2PBox & IntentBox** - `docs/reference/boxes-system/` を参照
- **StringBox, IntegerBox, ConsoleBox** - 基本Box API
- **EguiBox, DebugBox, MathBox** - 特殊Box API
@ -300,7 +303,7 @@ app.setSize(800, 600)
Read src/boxes/p2p_box.rs # 直接ソース参照
# ✅ 良い例:ドキュメント優先
Read docs/説明書/reference/built-in-boxes.md # まずドキュメント
Read docs/reference/ # まずドキュメントAPI/言語仕様の入口)
# → 古い/不足 → ドキュメント更新
# → それでも不明 → ソース確認
```

View File

@ -28,6 +28,9 @@
- native-plan/ - ネイティブビルド計画
- **proposals/** - RFC、新機能提案
### 🔌 Net PluginHTTP/TCP
- 使い方と仕様: `reference/plugin-system/net-plugin.md`
### 🗄️ [archive/](archive/) - アーカイブ
- **consultations/** - AI相談記録gemini/chatgpt/codex
- **decisions/** - 過去の設計決定

View File

@ -97,6 +97,28 @@ box ClassName {
}
```
#### フィールドの可視性public/private
- `init { ... }` で宣言したフィールドは既定で public外部から参照・代入可能
- 追加で private フィールドを宣言する場合は `private { ... }` ブロックを使用
- 外部から private フィールドへ直接アクセスすることはできません(ゲッター/セッター経由)
```nyash
box User {
// 既定: init は public フィールド宣言
init { name }
// private フィールド宣言
private { age, passwordHash }
birth(n, a) { me.name = n; me.age = a }
setAge(a) { me.age = a }
getAge() { return me.age }
}
```
補足: 旧仕様では `init` の可視性が明確ではありませんでしたが、現在は「public」に統一されています。
#### **デリゲーションBox**
```nyash
box Child from Parent interface Comparable {
@ -522,4 +544,4 @@ loop(condition) { } # 統一ループ構文
**🎉 Nyash 2025は、AI協働設計による最先端言語システムとして、シンプルさと強力さを完全に両立しました。**
*最終更新: 2025年8月10日 - Arc<Mutex> Revolution + AI大相談会成功記念*
*最終更新: 2025年8月10日 - Arc<Mutex> Revolution + AI大相談会成功記念*

View File

@ -21,6 +21,25 @@
- プラグインの動作確認とデバッグに使用
- `tools/plugin-tester`ツールの使用方法
- **[plugin_lifecycle.md](./plugin_lifecycle.md)** - ライフサイクル/RAII/シングルトン/ログ
- 共有ハンドル、scope終了時の扱い、`shutdown_plugins_v2()` の動作
- NetPluginHTTP/TCPの並列E2E時の注意点
- **[net-plugin.md](./net-plugin.md)** - NetプラグインHTTP/TCP PoC
- GET/POST、ヘッダ、Content-Length、環境変数によるログ
### ⚙️ 戻り値のResult化B案サポート
- `nyash.toml` のメソッド定義に `returns_result = true` を付けると、
- 成功: `Ok(value)``ResultBox` に包んで返す
- 失敗BID負エラー: `Err(ErrorBox(message))` を返す(例外にはしない)
```toml
[libraries."libnyash_example.so".ExampleBox.methods]
dangerousOp = { method_id = 10, returns_result = true }
```
未指定の場合は従来通り(成功=生値、失敗=例外として伝播)。
- **[filebox-bid-mapping.md](./filebox-bid-mapping.md)** - 参考資料
- FileBox APIとプラグイン実装の対応表
- API設計の参考として有用
@ -84,4 +103,4 @@ cargo build --release
---
**Status**: Phase 2 Documentation Reorganization - Completed
**Last Updated**: 2025-08-20
**Last Updated**: 2025-08-20

View File

@ -0,0 +1,72 @@
# Net Plugin (HTTP over TCP PoC)
最終更新: 2025-08-22
## 概要
- `nyash-net-plugin` は Socket/HTTP をプラグインとして提供します。
- HTTP は最小限の HTTP/1.1 実装GET/POST、ヘッダ、Content-Lengthを実ソケットで処理します。
## 提供Box
- `SocketServerBox`, `SocketClientBox`, `SocketConnBox`
- `HttpServerBox`, `HttpRequestBox`, `HttpResponseBox`, `HttpClientBox`
`nyash.toml` 定義例(抜粋)はリポジトリ直下の `nyash.toml` を参照。
## 動作仕様HTTP
- Server
- `start(port)`: TCP待受を開始
- `accept()`: 接続受理リクエスト簡易パースpath/body`HttpRequestBox` を返す
- `HttpRequestBox.respond(resp)`: `resp` の status/header/body を HTTP/1.1 として送出(`Connection: close`
- Client
- `get(url)`, `post(url, body)`: host:port に接続し、HTTP/1.1 リクエストを送出
- `HttpResponseBox` は遅延受信。`readBody()/getStatus()/getHeader()` 呼び出し時に受信・パース
制限:
- Chunked/Keep-Alive/HTTP/2は非対応PoC。Content-Length のみ対応。
## エラーモデルPoC
- BID-FFI の負値(例: `-5` PluginErrorは Nyash 側で例外(`RuntimeFailure`)として扱われます。
- タイムアウト系Socket の `acceptTimeout/recvTimeout`)はエラーではなく、以下の値を返します:
- `acceptTimeout(ms)`: タイムアウト時は `void`
- `recvTimeout(ms)`: タイムアウト時は 空 `bytes`長さ0の文字列
将来の整合:
- `ResultBox` での返却に対応する設計(`Ok(value)`/`Err(ErrorBox)`)を検討中。
- `nyash.toml` のメソッド宣言に戻り値型Resultを記載し、ランタイムで自動ラップする案。
## 並列E2E運用の注意
- 各テストで異なるポートを使用(例: 8080/8081/8090/8091...
- サーバの `stop()` と再起動時はポート開放の遅延に注意(短い待機を挟むと安定)
- ログを有効化して競合や順序の問題を診断可能
## ログ出力
環境変数で簡易ログを有効化できます。
```bash
export NYASH_NET_LOG=1
export NYASH_NET_LOG_FILE=net_plugin.log
```
stderr とファイルの両方に出力されます。
## 例
```nyash
local srv, cli, r, req, resp
srv = new HttpServerBox()
srv.start(8080)
cli = new HttpClientBox()
r = cli.get("http://localhost:8080/hello")
req = srv.accept()
resp = new HttpResponseBox()
resp.setStatus(200)
resp.setHeader("X-Test", "V")
resp.write("OK")
req.respond(resp)
// client side
print(r.getStatus())
print(r.getHeader("X-Test"))
print(r.readBody())
```

View File

@ -0,0 +1,53 @@
# Plugin Lifecycle and Box RAII
最終更新: 2025-08-22
## 概要
NyashのBoxには「ユーザー定義Box」「ビルトインBox」「プラグインBox」があります。いずれもRAII取得した資源は所有者の寿命で解放に従いますが、プラグインBoxは共有やシングルトン運用があるため、追加ルールがあります。
## 共通ライフサイクル(ユーザー/ビルトイン/プラグイン)
- インスタンスの寿命が尽きると、強参照フィールドpublic/privateに対し順に `fini()` が呼ばれ解放weak はスキップ)
- `local` 変数のスコープを抜けると、そのスコープで生成されたインスタンスは解放対象
- 明示的に `fini()` が呼ばれた場合も同様に後処理を実施
補足:
- これらは Nyash のスコープトラッカにより実施されます
- 解放順は生成の逆順LIFOで、カスケード `fini` を保証します
## プラグインBoxの特則シングルトン
- シングルトン(`nyash.toml`
- プラグインのBox型は `singleton = true` を宣言可能
- ローダが起動時に `birth()` し、以後は同一ハンドルを共有して返却
- シャットダウン時(`shutdown_plugins_v2()` など)に一括 `fini()` されます
補足:
- Nyashは参照カウントを採用しません。解放は「スコープ終了」または「明示的`fini`」のみで決まります(自己責任モデル)。
- プラグインBoxも同じルールです。スコープ終了時に`fini`され、以後の利用はエラーUse after fini
- 長寿命が必要なケースは「シングルトン」で運用してください個別のBoxに特例は設けない
### 例: `nyash.toml` 抜粋
```toml
[libraries."libnyash_counter_plugin.so".CounterBox]
type_id = 7
singleton = true
```
## Net PluginHTTP/TCP運用メモ
- ログ
- `NYASH_NET_LOG=1` で有効化、`NYASH_NET_LOG_FILE=net_plugin.log` 出力先
- 並列実行とポート
- E2Eや並列CIではポート競合を避けるため、テスト毎にポートを明示例: 8080, 8081, ...
- サーバ終了タイミング(`stop()`/スコープ終了)とクライアント接続の順序に注意
## ベストプラクティス
- ユーザー/ビルトインBox
- フィールドの weak 指定(循環参照の解消)を活用
- 必要に応じて明示 `fini()` を呼び、高価な資源(ファイル/ソケット等)を早期解放
- プラグインBox
- シングルトン化が望ましい長寿命資源(サーバ、デバイス)に `singleton = true`
- 複数スコープで共有される可能性がある値は、スコープ終了時に自動 `fini` されないことを前提に設計
- 終了前に `shutdown_plugins_v2()` を呼ぶと単一箇所で確実に `fini` を実行可能
## 実装参照
- スコープ追跡: `src/scope_tracker.rs`(スコープ終了時の `fini` 呼出し、プラグインBox自動 `fini` 回避)
- プラグインローダ: `src/runtime/plugin_loader_v2.rs`(シングルトン生成・保持・シャットダウン、`PluginHandleInner::drop``fini`

View File

@ -0,0 +1,62 @@
# AI Agent Challenge 1日実装戦略
## 概要
DEV.toのAI Agents Challengen8n + Bright DataにNyashで参加する戦略。
締切: 2025年8月31日
## 重要な発見
- 他の参加者は普通にJavaScriptで実装しているWASMは不要だった
- n8nはーコードツール、Webhookで簡単に連携可能
- NetBoxプラグインが既にHTTP機能を提供
## 1日実装プラン
### タイムライン
- 8:00-10:00: 基本設計・n8n理解
- 10:00-13:00: Nyash実装HTTPBox活用
- 13:00-16:00: n8n連携・Bright Data統合
- 16:00-19:00: デモアプリ完成
- 19:00-21:00: 記事作成・動画録画
- 21:00-23:00: 投稿・最終調整
### 実装アーキテクチャ
```nyash
// n8nブリッジ
box N8nBridge {
init { httpClient, workflows }
triggerWorkflow(webhookUrl, data) {
return me.httpClient.post(webhookUrl, data)
}
}
// 価格監視AIエージェント例
box PriceMonitorAgent {
init { products, notifier }
monitorPrices() {
// Bright Data経由でスクレイピング
// n8n経由で通知
}
}
```
### 差別化ポイント
1. **Nyashという独自言語**での実装(創造性満点)
2. Everything is Box哲学によるエレガントな設計
3. 既存のNetBoxプラグインを活用した高速開発
### 必要な準備
- n8n無料アカウント作成
- Bright Data $250クレジット取得
- NetBoxプラグインのテスト修正完了
### リスクと対策
- 時間制約 → シンプルな実装に集中
- 技術学習 → Webhook連携のみに限定
- デモ作成 → 録画で対応(ライブ不要)
## 結論
技術的には1日で実装可能。Nyashの知名度向上と$1,000の賞金獲得のチャンス。
最終更新: 2025-08-21

48
http_test_log.txt Normal file
View File

@ -0,0 +1,48 @@
HTTP E2Eテスト実行ログ (2025-08-21)
================================
【テスト実行コマンド】
NYASH_NET_LOG=1 cargo test --features plugins --test e2e_plugin_net -- --nocapture
【テスト結果サマリー】
- 全5テスト失敗
- エラー内容: Cannot call method 'respond' on non-instance type (VoidBox)
【詳細なエラー解析】
1. 根本原因: HttpRequestBox作成失敗
- HttpServerBox.start() → 成功instance_id作成確認
- server.accept() → VoidBoxを返す本来はHttpRequestBoxを返すべき
- VoidBox.respond() → エラーVoidBoxにはrespond()メソッドがない)
2. 主要なログ出力:
- 🎉 birth() success: HttpServerBox instance_id=5
- 🎉 birth() success: HttpClientBox instance_id=6
- ❌ Interpreter error: Type error: Cannot call method 'respond' on non-instance type
3. プラグインシステムの挙動:
- プラグインロード: 成功
- Box型の認識: 成功type_id=20 for HttpServerBox等
- メソッド呼び出し: 部分的成功start/stopは動作、acceptが問題
【失敗したテスト一覧】
1. e2e_http_stub_end_to_end
2. e2e_http_post_and_headers
3. e2e_http_multiple_requests_order
4. e2e_http_server_restart
5. e2e_http_server_shutdown_and_restart
【Socket E2Eテストとの比較】
- Socket E2Eテスト: 全て成功2/2 passed
- HTTP E2Eテスト: 全て失敗0/5 passed
- 結論: プラグインシステム自体は正常、HTTP実装固有の問題
【推測される問題】
1. HTTP accept()メソッドの実装が未完成PoC段階
2. 実際のHTTPリクエスト受信処理が未実装
3. HttpRequestBoxインスタンスの生成ロジックが欠落
【ChatGPT5さんへの報告事項】
- VoidBoxエラーはaccept()メソッドの戻り値が原因
- HTTPサーバーの実装は基本的なソケット処理が必要
- Socket実装は安定しているので参考にできる可能性あり

459
http_test_raw_log.txt Normal file
View File

@ -0,0 +1,459 @@
warning: unused macro definition: `debug_trace`
--> src/interpreter/core.rs:35:14
|
35 | macro_rules! debug_trace {
| ^^^^^^^^^^^
|
= note: `#[warn(unused_macros)]` on by default
warning: unexpected `cfg` condition value: `llvm`
--> src/backend/mod.rs:13:7
|
13 | #[cfg(feature = "llvm")]
| ^^^^^^^^^^^^^^^^
|
= note: expected values for `feature` are: `all-examples`, `cli`, `default`, `dynamic-file`, `gui`, `gui-examples`, `plugins`, and `wasm-backend`
= help: consider adding `llvm` as a feature in `Cargo.toml`
= note: see <https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html> for more information about checking conditional configuration
= note: `#[warn(unexpected_cfgs)]` on by default
warning: unexpected `cfg` condition value: `llvm`
--> src/backend/mod.rs:23:7
|
23 | #[cfg(feature = "llvm")]
| ^^^^^^^^^^^^^^^^
|
= note: expected values for `feature` are: `all-examples`, `cli`, `default`, `dynamic-file`, `gui`, `gui-examples`, `plugins`, and `wasm-backend`
= help: consider adding `llvm` as a feature in `Cargo.toml`
= note: see <https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html> for more information about checking conditional configuration
warning: unreachable statement
--> src/interpreter/objects.rs:109:9
|
27 | / match nyash_args {
28 | | Ok(args) => {
29 | | // Handle generics: if user-defined and type arguments provided, specialize declaration
30 | | let mut target_class = class.to_string();
... |
106 | | }
| |_________- any code following this `match` expression is unreachable, as all arms diverge
...
109 | return Err(RuntimeError::UndefinedClass { name: class.to_string() });
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unreachable statement
|
= note: `#[warn(unreachable_code)]` on by default
warning: unreachable pattern
--> src/interpreter/objects.rs:231:13
|
120 | / "StringBox" | "IntegerBox" | "BoolBox" | "ArrayBox" | "ResultBox" |
121 | | "ErrorBox" | "NullBox" | "FloatBox" | "MapBox" => {
| |__________________________________________________________- matches all the relevant values
...
231 | "NullBox" => {
| ^^^^^^^^^ no value can reach this
|
= note: `#[warn(unreachable_patterns)]` on by default
warning: unreachable pattern
--> src/interpreter/objects.rs:384:13
|
120 | / "StringBox" | "IntegerBox" | "BoolBox" | "ArrayBox" | "ResultBox" |
121 | | "ErrorBox" | "NullBox" | "FloatBox" | "MapBox" => {
| |__________________________________________________________- matches all the relevant values
...
384 | "FloatBox" => {
| ^^^^^^^^^^ no value can reach this
warning: unreachable pattern
--> src/interpreter/objects.rs:484:13
|
120 | / "StringBox" | "IntegerBox" | "BoolBox" | "ArrayBox" | "ResultBox" |
121 | | "ErrorBox" | "NullBox" | "FloatBox" | "MapBox" => {
| |__________________________________________________________- matches all the relevant values
...
484 | "MapBox" => {
| ^^^^^^^^ no value can reach this
warning: `nyash-rust` (lib) generated 7 warnings
warning: unexpected `cfg` condition value: `llvm`
--> src/runner.rs:28:7
|
28 | #[cfg(feature = "llvm")]
| ^^^^^^^^^^^^^^^^
|
= note: expected values for `feature` are: `all-examples`, `cli`, `default`, `dynamic-file`, `gui`, `gui-examples`, `plugins`, and `wasm-backend`
= help: consider adding `llvm` as a feature in `Cargo.toml`
= note: see <https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html> for more information about checking conditional configuration
warning: unexpected `cfg` condition value: `llvm`
--> src/runner.rs:568:15
|
568 | #[cfg(feature = "llvm")]
| ^^^^^^^^^^^^^^^^
|
= note: expected values for `feature` are: `all-examples`, `cli`, `default`, `dynamic-file`, `gui`, `gui-examples`, `plugins`, and `wasm-backend`
= help: consider adding `llvm` as a feature in `Cargo.toml`
= note: see <https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html> for more information about checking conditional configuration
warning: unexpected `cfg` condition value: `llvm`
--> src/runner.rs:591:19
|
591 | #[cfg(not(feature = "llvm"))]
| ^^^^^^^^^^^^^^^^
|
= note: expected values for `feature` are: `all-examples`, `cli`, `default`, `dynamic-file`, `gui`, `gui-examples`, `plugins`, and `wasm-backend`
= help: consider adding `llvm` as a feature in `Cargo.toml`
= note: see <https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html> for more information about checking conditional configuration
warning: `nyash-rust` (bin "nyash") generated 3 warnings (1 duplicate)
running 5 tests
🔍 create_box called for: HttpServerBox
🔍 Config loaded successfully
🔍 create_box called for: HttpServerBox
🔍 Config loaded successfully
🔍 create_box called for: HttpServerBox
🔍 Config loaded successfully
🔍 create_box called for: HttpServerBox
🔍 Config loaded successfully
🔍 create_box called for: HttpServerBox
🔍 Config loaded successfully
🔍 Found library: libnyash_net_plugin.so for box type: HttpServerBox
🔍 Plugin loaded successfully
🔍 Reading nyash.toml for type configuration...
🔍 Found library: libnyash_net_plugin.so for box type: HttpServerBox
🔍 Plugin loaded successfully
🔍 Reading nyash.toml for type configuration...
🔍 Found library: libnyash_net_plugin.so for box type: HttpServerBox
🔍 Plugin loaded successfully
🔍 Reading nyash.toml for type configuration...
🔍 Found library: libnyash_net_plugin.so for box type: HttpServerBox
🔍 Plugin loaded successfully
🔍 Reading nyash.toml for type configuration...
🔍 Found library: libnyash_net_plugin.so for box type: HttpServerBox
🔍 Plugin loaded successfully
🔍 Reading nyash.toml for type configuration...
🔍 nyash.toml read successfully
🔍 nyash.toml read successfully
🔍 nyash.toml read successfully
🔍 nyash.toml read successfully
🔍 nyash.toml read successfully
🔍 nyash.toml parsed successfully
🔍 Found box config for HttpServerBox with type_id: 20
🔍 Preparing to call birth() with type_id: 20
🔍 Output buffer allocated, about to call plugin invoke_fn...
🔍 Calling invoke_fn(type_id=20, method_id=0, instance_id=0, tlv_args=[1, 0, 0, 0], output_buf, output_size=1024)
🔍 invoke_fn returned with result: 0
🎉 birth() success: HttpServerBox instance_id=1
🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=1)
🔍 stdlib not initialized for method call
🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=1)
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: start
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpServerBox.start
[PluginLoaderV2] Invoke HttpServerBox.start: resolving and encoding args (argc=1)
[PluginLoaderV2] arg[0]: Integer(8099) -> Integer(tag=5)
🔍 stdlib not initialized for method call
🔍 nyash.toml parsed successfully
🔍 Found box config for HttpServerBox with type_id: 20
🔍 Preparing to call birth() with type_id: 20
🔍 Output buffer allocated, about to call plugin invoke_fn...
🔍 Calling invoke_fn(type_id=20, method_id=0, instance_id=0, tlv_args=[1, 0, 0, 0], output_buf, output_size=1024)
🔍 invoke_fn returned with result: 0
🎉 birth() success: HttpServerBox instance_id=2
🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=2)
🔍 stdlib not initialized for method call
🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=2)
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: start
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpServerBox.start
[PluginLoaderV2] Invoke HttpServerBox.start: resolving and encoding args (argc=1)
[PluginLoaderV2] arg[0]: Integer(8095) -> Integer(tag=5)
🔍 stdlib not initialized for method call
🔍 nyash.toml parsed successfully
🔍 Found box config for HttpServerBox with type_id: 20
🔍 Preparing to call birth() with type_id: 20
🔍 Output buffer allocated, about to call plugin invoke_fn...
🔍 Calling invoke_fn(type_id=20, method_id=0, instance_id=0, tlv_args=[1, 0, 0, 0], output_buf, output_size=1024)
🔍 invoke_fn returned with result: 0
🎉 birth() success: HttpServerBox instance_id=3
🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=3)
🔍 stdlib not initialized for method call
🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=3)
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: start
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpServerBox.start
[PluginLoaderV2] Invoke HttpServerBox.start: resolving and encoding args (argc=1)
[PluginLoaderV2] arg[0]: Integer(8096) -> Integer(tag=5)
🔍 stdlib not initialized for method call
🔍 nyash.toml parsed successfully
🔍 Found box config for HttpServerBox with type_id: 20
🔍 Preparing to call birth() with type_id: 20
🔍 Output buffer allocated, about to call plugin invoke_fn...
🔍 Calling invoke_fn(type_id=20, method_id=0, instance_id=0, tlv_args=[1, 0, 0, 0], output_buf, output_size=1024)
🔍 invoke_fn returned with result: 0
🎉 birth() success: HttpServerBox instance_id=4
🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=4)
🔍 stdlib not initialized for method call
🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=4)
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: start
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpServerBox.start
[PluginLoaderV2] Invoke HttpServerBox.start: resolving and encoding args (argc=1)
[PluginLoaderV2] arg[0]: Integer(8097) -> Integer(tag=5)
🔍 stdlib not initialized for method call
🔍 nyash.toml parsed successfully
🔍 Found box config for HttpServerBox with type_id: 20
🔍 Preparing to call birth() with type_id: 20
🔍 Output buffer allocated, about to call plugin invoke_fn...
🔍 Calling invoke_fn(type_id=20, method_id=0, instance_id=0, tlv_args=[1, 0, 0, 0], output_buf, output_size=1024)
🔍 invoke_fn returned with result: 0
🎉 birth() success: HttpServerBox instance_id=5
🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=5)
🔍 stdlib not initialized for method call
🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=5)
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: start
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpServerBox.start
[PluginLoaderV2] Invoke HttpServerBox.start: resolving and encoding args (argc=1)
[PluginLoaderV2] arg[0]: Integer(8098) -> Integer(tag=5)
🔍 stdlib not initialized for method call
🔍 create_box called for: HttpClientBox
🔍 Config loaded successfully
🔍 create_box called for: HttpClientBox
🔍 Config loaded successfully
🔍 create_box called for: HttpClientBox
🔍 Config loaded successfully
🔍 Found library: libnyash_net_plugin.so for box type: HttpClientBox
🔍 Plugin loaded successfully
🔍 Reading nyash.toml for type configuration...
🔍 Found library: libnyash_net_plugin.so for box type: HttpClientBox
🔍 Plugin loaded successfully
🔍 Reading nyash.toml for type configuration...
🔍 Found library: libnyash_net_plugin.so for box type: HttpClientBox
🔍 Plugin loaded successfully
🔍 Reading nyash.toml for type configuration...
🔍 nyash.toml read successfully
🔍 nyash.toml read successfully
🔍 nyash.toml read successfully
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: accept
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpServerBox.accept
[PluginLoaderV2] Invoke HttpServerBox.accept: resolving and encoding args (argc=0)
🔍 stdlib not initialized for method call
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: accept
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpServerBox.accept
[PluginLoaderV2] Invoke HttpServerBox.accept: resolving and encoding args (argc=0)
🔍 stdlib not initialized for method call
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: accept
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpServerBox.accept
[PluginLoaderV2] Invoke HttpServerBox.accept: resolving and encoding args (argc=0)
🔍 stdlib not initialized for method call
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: accept
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpServerBox.accept
[PluginLoaderV2] Invoke HttpServerBox.accept: resolving and encoding args (argc=0)
🔍 stdlib not initialized for method call
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: accept
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpServerBox.accept
[PluginLoaderV2] Invoke HttpServerBox.accept: resolving and encoding args (argc=0)
🔍 stdlib not initialized for method call
🔍 nyash.toml parsed successfully
🔍 Found box config for HttpClientBox with type_id: 23
🔍 Preparing to call birth() with type_id: 23
🔍 Output buffer allocated, about to call plugin invoke_fn...
🔍 Calling invoke_fn(type_id=23, method_id=0, instance_id=0, tlv_args=[1, 0, 0, 0], output_buf, output_size=1024)
🔍 invoke_fn returned with result: 0
🎉 birth() success: HttpClientBox instance_id=6
🔍 DEBUG: PluginBoxV2::share_box called for HttpClientBox (id=6)
🔍 stdlib not initialized for method call
🔍 DEBUG: PluginBoxV2::share_box called for HttpClientBox (id=6)
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: get
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpClientBox.get
[PluginLoaderV2] Invoke HttpClientBox.get: resolving and encoding args (argc=1)
[PluginLoaderV2] arg[0]: String(len=27) -> String(tag=6)
🔍 stdlib not initialized for method call
🔍 nyash.toml parsed successfully
🔍 Found box config for HttpClientBox with type_id: 23
🔍 Preparing to call birth() with type_id: 23
🔍 Output buffer allocated, about to call plugin invoke_fn...
🔍 Calling invoke_fn(type_id=23, method_id=0, instance_id=0, tlv_args=[1, 0, 0, 0], output_buf, output_size=1024)
🔍 invoke_fn returned with result: 0
🎉 birth() success: HttpClientBox instance_id=7
🔍 DEBUG: PluginBoxV2::share_box called for HttpClientBox (id=7)
🔍 stdlib not initialized for method call
🔍 DEBUG: PluginBoxV2::share_box called for HttpClientBox (id=7)
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: post
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpClientBox.post
[PluginLoaderV2] Invoke HttpClientBox.post: resolving and encoding args (argc=2)
[PluginLoaderV2] arg[0]: String(len=35) -> String(tag=6)
[PluginLoaderV2] arg[1]: String(len=15) -> String(tag=6)
🔍 stdlib not initialized for method call
🔍 nyash.toml parsed successfully
🔍 Found box config for HttpClientBox with type_id: 23
🔍 Preparing to call birth() with type_id: 23
🔍 Output buffer allocated, about to call plugin invoke_fn...
🔍 Calling invoke_fn(type_id=23, method_id=0, instance_id=0, tlv_args=[1, 0, 0, 0], output_buf, output_size=1024)
🔍 invoke_fn returned with result: 0
🎉 birth() success: HttpClientBox instance_id=8
🔍 DEBUG: PluginBoxV2::share_box called for HttpClientBox (id=8)
🔍 stdlib not initialized for method call
🔍 DEBUG: PluginBoxV2::share_box called for HttpClientBox (id=8)
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: get
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpClientBox.get
[PluginLoaderV2] Invoke HttpClientBox.get: resolving and encoding args (argc=1)
[PluginLoaderV2] arg[0]: String(len=21) -> String(tag=6)
🔍 stdlib not initialized for method call
🔍 Plugin invoke completed with result: 0
🔍 plugin_invoke_v2 returned result: 0
🔍 TLV output from plugin (len=4): [1, 0, 0, 0]
🔍 Plugin invoke completed with result: 0
🔍 plugin_invoke_v2 returned result: 0
🔍 TLV output from plugin (len=4): [1, 0, 0, 0]
🔍 Plugin invoke completed with result: 0
🔍 plugin_invoke_v2 returned result: 0
🔍 TLV output from plugin (len=4): [1, 0, 0, 0]
🔍 Plugin invoke completed with result: 0
🔍 plugin_invoke_v2 returned result: 0
🔍 TLV output from plugin (len=4): [1, 0, 0, 0]
🔍 Plugin invoke completed with result: 0
🔍 plugin_invoke_v2 returned result: 0
🔍 TLV output from plugin (len=4): [1, 0, 0, 0]
🔍 Plugin invoke completed with result: 0
🔍 plugin_invoke_v2 returned result: 0
🔍 TLV output from plugin (len=4): [1, 0, 0, 0]
🔍 Plugin invoke completed with result: 0
🔍 plugin_invoke_v2 returned result: 0
🔍 TLV output from plugin (len=4): [1, 0, 0, 0]
🔍 Plugin invoke completed with result: 0
🔍 plugin_invoke_v2 returned result: 0
🔍 TLV output from plugin (len=4): [1, 0, 0, 0]
🔍 create_box called for: VoidBox
VoidBox is not a plugin box type
🔍 DEBUG: execute_method_call - object type: VoidBox, method: respond
🔍 DEBUG: Checking StringBox downcast for type: VoidBox
🔍 DEBUG: StringBox downcast failed
🔍 DEBUG: Reached non-instance type error for type: VoidBox, method: respond
❌ Interpreter error: Type error: Cannot call method 'respond' on non-instance type
🔍 create_box called for: VoidBox
VoidBox is not a plugin box type
🔍 create_box called for: VoidBox
VoidBox is not a plugin box type
🔍 DEBUG: execute_method_call - object type: VoidBox, method: readBody
🔍 DEBUG: Checking StringBox downcast for type: VoidBox
🔍 DEBUG: StringBox downcast failed
🔍 DEBUG: Reached non-instance type error for type: VoidBox, method: readBody
❌ Interpreter error: Type error: Cannot call method 'readBody' on non-instance type
🔍 create_box called for: HttpResponseBox
🔍 Config loaded successfully
🔍 create_box called for: HttpResponseBox
🔍 Config loaded successfully
thread 'e2e_http_post_and_headers' panicked at tests/e2e_plugin_net.rs:159:43:
exec failed: TypeError { message: "Cannot call method 'readBody' on non-instance type" }
🔍 Found library: libnyash_net_plugin.so for box type: HttpResponseBox
🔍 Plugin loaded successfully
🔍 Reading nyash.toml for type configuration...
🔍 Found library: libnyash_net_plugin.so for box type: HttpResponseBox
🔍 Plugin loaded successfully
🔍 Reading nyash.toml for type configuration...
🔍 nyash.toml read successfully
🔍 nyash.toml read successfully
🔍 nyash.toml parsed successfully
🔍 Found box config for HttpResponseBox with type_id: 22
🔍 Preparing to call birth() with type_id: 22
🔍 Output buffer allocated, about to call plugin invoke_fn...
🔍 Calling invoke_fn(type_id=22, method_id=0, instance_id=0, tlv_args=[1, 0, 0, 0], output_buf, output_size=1024)
🔍 invoke_fn returned with result: 0
🎉 birth() success: HttpResponseBox instance_id=9
🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=9)
🔍 stdlib not initialized for method call
🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=9)
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: write
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpResponseBox.write
🔍 nyash.toml parsed successfully
🔍 Found box config for HttpResponseBox with type_id: 22
🔍 Preparing to call birth() with type_id: 22
🔍 Output buffer allocated, about to call plugin invoke_fn...
🔍 Calling invoke_fn(type_id=22, method_id=0, instance_id=0, tlv_args=[1, 0, 0, 0], output_buf, output_size=1024)
🔍 invoke_fn returned with result: 0
🎉 birth() success: HttpResponseBox instance_id=10
🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=10)
🔍 stdlib not initialized for method call
🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=10)
🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: write
🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2
🔍 DEBUG: StringBox downcast failed
🔍 execute_plugin_box_v2_method called: HttpResponseBox.write
🔍 DEBUG: execute_method_call - object type: VoidBox, method: path
🔍 DEBUG: Checking StringBox downcast for type: VoidBox
🔍 DEBUG: StringBox downcast failed
🔍 DEBUG: Reached non-instance type error for type: VoidBox, method: path
❌ Interpreter error: Type error: Cannot call method 'path' on non-instance type
thread 'e2e_http_multiple_requests_order' panicked at tests/e2e_plugin_net.rs:186:43:
exec failed: TypeError { message: "Cannot call method 'path' on non-instance type" }
[PluginLoaderV2] Invoke HttpResponseBox.write: resolving and encoding args (argc=1)
[PluginLoaderV2] arg[0]: String(len=1) -> String(tag=6)
🔍 stdlib not initialized for method call
[PluginLoaderV2] Invoke HttpResponseBox.write: resolving and encoding args (argc=1)
[PluginLoaderV2] arg[0]: String(len=1) -> String(tag=6)
🔍 stdlib not initialized for method call
🔍 DEBUG: execute_method_call - object type: VoidBox, method: respond
🔍 DEBUG: Checking StringBox downcast for type: VoidBox
🔍 DEBUG: StringBox downcast failed
🔍 DEBUG: Reached non-instance type error for type: VoidBox, method: respond
🔍 DEBUG: execute_method_call - object type: VoidBox, method: respond
🔍 DEBUG: Checking StringBox downcast for type: VoidBox
🔍 DEBUG: StringBox downcast failed
🔍 DEBUG: Reached non-instance type error for type: VoidBox, method: respond
❌ Interpreter error: Type error: Cannot call method 'respond' on non-instance type
❌ Interpreter error: Type error: Cannot call method 'respond' on non-instance type
thread 'e2e_http_server_shutdown_and_restart' panicked at tests/e2e_plugin_net.rs:102:22:
exec1: TypeError { message: "Cannot call method 'respond' on non-instance type" }
thread 'e2e_http_server_restart' panicked at tests/e2e_plugin_net.rs:80:43:
exec failed: TypeError { message: "Cannot call method 'respond' on non-instance type" }
error: test failed, to rerun pass `--test e2e_plugin_net`
running 5 tests
test e2e_http_stub_end_to_end ... FAILED
test e2e_http_post_and_headers ... FAILED
test e2e_http_multiple_requests_order ... FAILED
test e2e_http_server_shutdown_and_restart ... FAILED
test e2e_http_server_restart ... FAILED
failures:
failures:
e2e_http_multiple_requests_order
e2e_http_post_and_headers
e2e_http_server_restart
e2e_http_server_shutdown_and_restart
e2e_http_stub_end_to_end
test result: FAILED. 0 passed; 5 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.81s

View File

@ -88,9 +88,10 @@ static SOCK_CONN_ID: AtomicU32 = AtomicU32::new(1);
static SOCK_CLIENT_ID: AtomicU32 = AtomicU32::new(1);
struct ServerState {
running: bool,
running: Arc<AtomicBool>,
port: i32,
pending: VecDeque<u32>, // queue of request ids
pending: Arc<Mutex<VecDeque<u32>>>, // queue of request ids
handle: Mutex<Option<std::thread::JoinHandle<()>>>,
start_seq: u32,
}
@ -98,12 +99,17 @@ struct RequestState {
path: String,
body: Vec<u8>,
response_id: Option<u32>,
// For HTTP-over-TCP server: map to an active accepted socket to respond on
server_conn_id: Option<u32>,
}
struct ResponseState {
status: i32,
headers: HashMap<String, String>,
body: Vec<u8>,
// For HTTP-over-TCP client: associated socket connection id to read from
client_conn_id: Option<u32>,
parsed: bool,
}
struct ClientState;
@ -155,14 +161,54 @@ unsafe fn server_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res:
match m {
M_BIRTH => {
let id = SERVER_ID.fetch_add(1, Ordering::Relaxed);
SERVER_INSTANCES.lock().unwrap().insert(id, ServerState { running: false, port: 0, pending: VecDeque::new(), start_seq: 0 });
SERVER_INSTANCES.lock().unwrap().insert(id, ServerState {
running: Arc::new(AtomicBool::new(false)),
port: 0,
pending: Arc::new(Mutex::new(VecDeque::new())),
handle: Mutex::new(None),
start_seq: 0,
});
write_u32(id, res, res_len)
}
M_SERVER_START => {
// args: TLV string/int (port)
let port = tlv_parse_i32(slice(args, args_len)).unwrap_or(0);
if let Some(s) = SERVER_INSTANCES.lock().unwrap().get_mut(&id) {
s.running = true; s.port = port; s.start_seq = SERVER_START_SEQ.fetch_add(1, Ordering::Relaxed);
s.port = port; s.start_seq = SERVER_START_SEQ.fetch_add(1, Ordering::Relaxed);
let running = s.running.clone();
let pending = s.pending.clone();
running.store(true, Ordering::SeqCst);
// Spawn HTTP listener thread (real TCP)
let handle = std::thread::spawn(move || {
let addr = format!("127.0.0.1:{}", port);
if let Ok(listener) = TcpListener::bind(addr) {
let _ = listener.set_nonblocking(true);
loop {
if !running.load(Ordering::SeqCst) { break; }
match listener.accept() {
Ok((mut stream, _)) => {
// Parse minimal HTTP request (GET/POST)
let _ = stream.set_read_timeout(Some(Duration::from_millis(2000)));
if let Some((path, body)) = read_http_request(&mut stream) {
// Store stream for later respond()
let conn_id = SOCK_CONN_ID.fetch_add(1, Ordering::Relaxed);
SOCK_CONNS.lock().unwrap().insert(conn_id, SockConnState { stream: Mutex::new(stream) });
let req_id = REQUEST_ID.fetch_add(1, Ordering::Relaxed);
REQUESTS.lock().unwrap().insert(req_id, RequestState { path, body, response_id: None, server_conn_id: Some(conn_id) });
pending.lock().unwrap().push_back(req_id);
} else {
// Malformed; drop connection
}
}
Err(_) => {
std::thread::sleep(Duration::from_millis(10));
}
}
}
}
});
*s.handle.lock().unwrap() = Some(handle);
}
// mark active server
*ACTIVE_SERVER_ID.lock().unwrap() = Some(id);
@ -170,7 +216,8 @@ unsafe fn server_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res:
}
M_SERVER_STOP => {
if let Some(s) = SERVER_INSTANCES.lock().unwrap().get_mut(&id) {
s.running = false;
s.running.store(false, Ordering::SeqCst);
if let Some(h) = s.handle.lock().unwrap().take() { let _ = h.join(); }
}
// clear active if this server was active
let mut active = ACTIVE_SERVER_ID.lock().unwrap();
@ -182,7 +229,7 @@ unsafe fn server_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res:
for _ in 0..1000 {
if let Some(req_id) = {
let mut map = SERVER_INSTANCES.lock().unwrap();
if let Some(s) = map.get_mut(&id) { s.pending.pop_front() } else { None }
if let Some(s) = map.get_mut(&id) { s.pending.lock().unwrap().pop_front() } else { None }
} {
return write_tlv_handle(T_REQUEST, req_id, res, res_len);
}
@ -198,7 +245,7 @@ unsafe fn request_invoke(m: u32, id: u32, _args: *const u8, _args_len: usize, re
match m {
M_BIRTH => {
let id = REQUEST_ID.fetch_add(1, Ordering::Relaxed);
REQUESTS.lock().unwrap().insert(id, RequestState { path: String::new(), body: vec![], response_id: None });
REQUESTS.lock().unwrap().insert(id, RequestState { path: String::new(), body: vec![], response_id: None, server_conn_id: None });
write_u32(id, res, res_len)
}
M_REQ_PATH => {
@ -218,17 +265,45 @@ unsafe fn request_invoke(m: u32, id: u32, _args: *const u8, _args_len: usize, re
// Acquire request
let mut rq_map = REQUESTS.lock().unwrap();
if let Some(rq) = rq_map.get_mut(&id) {
// Determine target response id: prefer existing client response id if present
// If request is backed by a real socket, write HTTP over that socket
if let Some(conn_id) = rq.server_conn_id {
drop(rq_map);
// Read response content from provided response handle
let (status, headers, body) = {
let resp_map = RESPONSES.lock().unwrap();
if let Some(src) = resp_map.get(&provided_resp_id) {
(src.status, src.headers.clone(), src.body.clone())
} else { return E_INV_HANDLE }
};
// Build minimal HTTP/1.1 response
let reason = match status { 200 => "OK", 201 => "Created", 204 => "No Content", 400 => "Bad Request", 404 => "Not Found", 500 => "Internal Server Error", _ => "OK" };
let mut buf = Vec::new();
buf.extend_from_slice(format!("HTTP/1.1 {} {}\r\n", status, reason).as_bytes());
let mut has_len = false;
for (k,v) in &headers {
if k.eq_ignore_ascii_case("Content-Length") { has_len = true; }
buf.extend_from_slice(format!("{}: {}\r\n", k, v).as_bytes());
}
if !has_len { buf.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes()); }
buf.extend_from_slice(b"Connection: close\r\n");
buf.extend_from_slice(b"\r\n");
buf.extend_from_slice(&body);
// Write and close
if let Some(conn) = SOCK_CONNS.lock().unwrap().remove(&conn_id) {
if let Ok(mut s) = conn.stream.lock() { let _ = s.write_all(&buf); let _ = s.flush(); }
}
return write_tlv_void(res, res_len);
}
// Stub fallback: copy into paired client-side response
let target_id = if let Some(existing) = rq.response_id { existing } else { provided_resp_id };
rq.response_id = Some(target_id);
drop(rq_map); // release before locking responses
// Copy response content from provided_resp_id to target_id
let mut resp_map = RESPONSES.lock().unwrap();
let (src_status, src_headers, src_body) = if let Some(src) = resp_map.get(&provided_resp_id) {
(src.status, src.headers.clone(), src.body.clone())
} else { return E_INV_HANDLE };
let dst = resp_map.entry(target_id).or_insert(ResponseState { status: 200, headers: HashMap::new(), body: vec![] });
let dst = resp_map.entry(target_id).or_insert(ResponseState { status: 200, headers: HashMap::new(), body: vec![], client_conn_id: None, parsed: true });
dst.status = src_status;
dst.headers = src_headers;
dst.body = src_body;
@ -244,7 +319,7 @@ unsafe fn response_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res
match m {
M_BIRTH => {
let id = RESPONSE_ID.fetch_add(1, Ordering::Relaxed);
RESPONSES.lock().unwrap().insert(id, ResponseState { status: 200, headers: HashMap::new(), body: vec![] });
RESPONSES.lock().unwrap().insert(id, ResponseState { status: 200, headers: HashMap::new(), body: vec![], client_conn_id: None, parsed: false });
write_u32(id, res, res_len)
}
M_RESP_SET_STATUS => {
@ -266,13 +341,35 @@ unsafe fn response_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res
write_tlv_void(res, res_len)
}
M_RESP_READ_BODY => {
// If bound to a client connection, lazily read and parse
let mut need_parse = None;
{
if let Some(rp) = RESPONSES.lock().unwrap().get(&id) {
if rp.client_conn_id.is_some() && !rp.parsed { need_parse = rp.client_conn_id; }
} else { return E_INV_HANDLE; }
}
if let Some(conn_id) = need_parse { parse_client_response_into(id, conn_id); }
if let Some(rp) = RESPONSES.lock().unwrap().get(&id) { write_tlv_bytes(&rp.body, res, res_len) } else { E_INV_HANDLE }
}
M_RESP_GET_STATUS => {
let mut need_parse = None;
{
if let Some(rp) = RESPONSES.lock().unwrap().get(&id) {
if rp.client_conn_id.is_some() && !rp.parsed { need_parse = rp.client_conn_id; }
} else { return E_INV_HANDLE; }
}
if let Some(conn_id) = need_parse { parse_client_response_into(id, conn_id); }
if let Some(rp) = RESPONSES.lock().unwrap().get(&id) { write_tlv_i32(rp.status, res, res_len) } else { E_INV_HANDLE }
}
M_RESP_GET_HEADER => {
if let Ok(name) = tlv_parse_string(slice(args, args_len)) {
let mut need_parse = None;
{
if let Some(rp) = RESPONSES.lock().unwrap().get(&id) {
if rp.client_conn_id.is_some() && !rp.parsed { need_parse = rp.client_conn_id; }
} else { return E_INV_HANDLE; }
}
if let Some(conn_id) = need_parse { parse_client_response_into(id, conn_id); }
if let Some(rp) = RESPONSES.lock().unwrap().get(&id) {
let v = rp.headers.get(&name).cloned().unwrap_or_default();
return write_tlv_string(&v, res, res_len);
@ -294,33 +391,23 @@ unsafe fn client_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res:
M_CLIENT_GET => {
// args: TLV String(url)
let url = tlv_parse_string(slice(args, args_len)).unwrap_or_default();
let path = parse_path(&url);
let port_hint = parse_port(&url);
// Create Request
let req_id = REQUEST_ID.fetch_add(1, Ordering::Relaxed);
REQUESTS.lock().unwrap().insert(req_id, RequestState { path, body: vec![], response_id: None });
// Enqueue to server: prefer port match, else newest running
{
let mut servers = SERVER_INSTANCES.lock().unwrap();
if let Some(ph) = port_hint {
if let Some((_, s)) = servers.iter_mut().find(|(_, s)| s.running && s.port == ph) {
s.pending.push_back(req_id);
} else {
if let Some((_, s)) = servers.iter_mut().filter(|(_, s)| s.running).max_by_key(|(_, s)| s.start_seq) {
s.pending.push_back(req_id);
}
}
} else if let Some((_, s)) = servers.iter_mut().filter(|(_, s)| s.running).max_by_key(|(_, s)| s.start_seq) {
s.pending.push_back(req_id);
let port = parse_port(&url).unwrap_or(80);
let host = parse_host(&url).unwrap_or_else(|| "127.0.0.1".to_string());
let (_h, _p, req_bytes) = build_http_request("GET", &url, None);
let addr = format!("{}:{}", host, port);
match TcpStream::connect(addr) {
Ok(mut stream) => {
let _ = stream.write_all(&req_bytes);
let _ = stream.flush();
let conn_id = SOCK_CONN_ID.fetch_add(1, Ordering::Relaxed);
SOCK_CONNS.lock().unwrap().insert(conn_id, SockConnState { stream: Mutex::new(stream) });
// Response handle
let resp_id = RESPONSE_ID.fetch_add(1, Ordering::Relaxed);
RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 0, headers: HashMap::new(), body: vec![], client_conn_id: Some(conn_id), parsed: false });
write_tlv_handle(T_RESPONSE, resp_id, res, res_len)
}
Err(_) => E_ERR,
}
// Create Response handle for client side to read later
let resp_id = RESPONSE_ID.fetch_add(1, Ordering::Relaxed);
RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 200, headers: HashMap::new(), body: vec![] });
// Link
if let Some(rq) = REQUESTS.lock().unwrap().get_mut(&req_id) { rq.response_id = Some(resp_id); }
// Return Handle(Response)
write_tlv_handle(T_RESPONSE, resp_id, res, res_len)
}
M_CLIENT_POST => {
// args: TLV String(url), Bytes body
@ -334,32 +421,22 @@ unsafe fn client_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res:
let (t2, s2, p2) = tlv_parse_entry_hdr(data, pos).map_err(|_| ()).or(Err(())).unwrap_or((0,0,0));
if t2 != 6 && t2 != 7 { return E_INV_ARGS; }
let body = data[p2..p2+s2].to_vec();
let path = parse_path(&url);
let port_hint = parse_port(&url);
// Create Request
let req_id = REQUEST_ID.fetch_add(1, Ordering::Relaxed);
REQUESTS.lock().unwrap().insert(req_id, RequestState { path, body, response_id: None });
// Enqueue to server: prefer port match, else newest running
{
let mut servers = SERVER_INSTANCES.lock().unwrap();
if let Some(ph) = port_hint {
if let Some((_, s)) = servers.iter_mut().find(|(_, s)| s.running && s.port == ph) {
s.pending.push_back(req_id);
} else {
if let Some((_, s)) = servers.iter_mut().filter(|(_, s)| s.running).max_by_key(|(_, s)| s.start_seq) {
s.pending.push_back(req_id);
}
}
} else if let Some((_, s)) = servers.iter_mut().filter(|(_, s)| s.running).max_by_key(|(_, s)| s.start_seq) {
s.pending.push_back(req_id);
let port = parse_port(&url).unwrap_or(80);
let host = parse_host(&url).unwrap_or_else(|| "127.0.0.1".to_string());
let (_h, _p, req_bytes) = build_http_request("POST", &url, Some(&body));
let addr = format!("{}:{}", host, port);
match TcpStream::connect(addr) {
Ok(mut stream) => {
let _ = stream.write_all(&req_bytes);
let _ = stream.flush();
let conn_id = SOCK_CONN_ID.fetch_add(1, Ordering::Relaxed);
SOCK_CONNS.lock().unwrap().insert(conn_id, SockConnState { stream: Mutex::new(stream) });
let resp_id = RESPONSE_ID.fetch_add(1, Ordering::Relaxed);
RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 0, headers: HashMap::new(), body: vec![], client_conn_id: Some(conn_id), parsed: false });
write_tlv_handle(T_RESPONSE, resp_id, res, res_len)
}
Err(_) => E_ERR,
}
// Create paired client Response
let resp_id = RESPONSE_ID.fetch_add(1, Ordering::Relaxed);
RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 200, headers: HashMap::new(), body: vec![] });
if let Some(rq) = REQUESTS.lock().unwrap().get_mut(&req_id) { rq.response_id = Some(resp_id); }
write_tlv_handle(T_RESPONSE, resp_id, res, res_len)
}
_ => E_INV_METHOD,
}
@ -483,6 +560,127 @@ fn tlv_parse_entry_hdr(data: &[u8], pos: usize) -> Result<(u8,usize,usize), ()>
Ok((tag,size,p))
}
// ===== HTTP helpers =====
fn parse_host(url: &str) -> Option<String> {
// http://host[:port]/...
if let Some(rest) = url.split("//").nth(1) {
let host_port = rest.split('/').next().unwrap_or("");
let host = host_port.split(':').next().unwrap_or("");
if !host.is_empty() { return Some(host.to_string()); }
}
None
}
fn build_http_request(method: &str, url: &str, body: Option<&[u8]>) -> (String, String, Vec<u8>) {
let host = parse_host(url).unwrap_or_else(|| "127.0.0.1".to_string());
let path = parse_path(url);
let mut buf = Vec::new();
buf.extend_from_slice(format!("{} {} HTTP/1.1\r\n", method, &path).as_bytes());
buf.extend_from_slice(format!("Host: {}\r\n", host).as_bytes());
buf.extend_from_slice(b"User-Agent: nyash-net-plugin/0.1\r\n");
match body {
Some(b) => {
buf.extend_from_slice(format!("Content-Length: {}\r\n", b.len()).as_bytes());
buf.extend_from_slice(b"Content-Type: application/octet-stream\r\n");
buf.extend_from_slice(b"Connection: close\r\n\r\n");
buf.extend_from_slice(b);
}
None => {
buf.extend_from_slice(b"Connection: close\r\n\r\n");
}
}
(host, path, buf)
}
fn read_http_request(stream: &mut TcpStream) -> Option<(String, Vec<u8>)> {
let mut buf = Vec::with_capacity(1024);
let mut tmp = [0u8; 1024];
// Read until we see CRLFCRLF
let header_end;
loop {
match stream.read(&mut tmp) {
Ok(0) => break, // EOF
Ok(n) => {
buf.extend_from_slice(&tmp[..n]);
if let Some(pos) = find_header_end(&buf) { header_end = pos; break; }
if buf.len() > 64 * 1024 { return None; }
}
Err(_) => return None,
}
}
// Parse request line and headers
let header = &buf[..header_end];
let after = &buf[header_end+4..];
let header_str = String::from_utf8_lossy(header);
let mut lines = header_str.split("\r\n");
let request_line = lines.next().unwrap_or("");
let mut parts = request_line.split_whitespace();
let method = parts.next().unwrap_or("");
let path = parts.next().unwrap_or("/").to_string();
let mut content_length: usize = 0;
for line in lines { if let Some((k,v)) = line.split_once(':') { if k.eq_ignore_ascii_case("Content-Length") { content_length = v.trim().parse().unwrap_or(0); } } }
let mut body = after.to_vec();
while body.len() < content_length {
match stream.read(&mut tmp) { Ok(0) => break, Ok(n) => body.extend_from_slice(&tmp[..n]), Err(_) => break }
}
if method == "GET" || method == "POST" { Some((path, body)) } else { None }
}
fn find_header_end(buf: &[u8]) -> Option<usize> {
if buf.len() < 4 { return None; }
for i in 0..=buf.len()-4 { if &buf[i..i+4] == b"\r\n\r\n" { return Some(i); } }
None
}
fn parse_client_response_into(resp_id: u32, conn_id: u32) {
// Read full response from socket and fill ResponseState
let mut status: i32 = 200;
let mut headers: HashMap<String, String> = HashMap::new();
let mut body: Vec<u8> = Vec::new();
if let Some(conn) = SOCK_CONNS.lock().unwrap().remove(&conn_id) {
if let Ok(mut s) = conn.stream.lock() {
let _ = s.set_read_timeout(Some(Duration::from_millis(4000)));
let mut buf = Vec::with_capacity(2048);
let mut tmp = [0u8; 2048];
let header_end;
loop {
match s.read(&mut tmp) {
Ok(0) => break,
Ok(n) => {
buf.extend_from_slice(&tmp[..n]);
if let Some(pos) = find_header_end(&buf) { header_end = pos; break; }
if buf.len() > 256 * 1024 { break; }
}
Err(_) => break,
}
}
if let Some(pos) = find_header_end(&buf) {
let header = &buf[..pos];
let after = &buf[pos+4..];
// Parse status line and headers
let header_str = String::from_utf8_lossy(header);
let mut lines = header_str.split("\r\n");
if let Some(status_line) = lines.next() {
let mut sp = status_line.split_whitespace();
let _ver = sp.next();
if let Some(code_str) = sp.next() { status = code_str.parse::<i32>().unwrap_or(200); }
}
for line in lines {
if let Some((k,v)) = line.split_once(':') { headers.insert(k.trim().to_string(), v.trim().to_string()); }
}
body.extend_from_slice(after);
let need = headers.get("Content-Length").and_then(|v| v.parse::<usize>().ok()).unwrap_or(0);
while body.len() < need {
match s.read(&mut tmp) { Ok(0) => break, Ok(n) => body.extend_from_slice(&tmp[..n]), Err(_) => break }
}
}
}
}
if let Some(rp) = RESPONSES.lock().unwrap().get_mut(&resp_id) {
rp.status = status; rp.headers = headers; rp.body = body; rp.parsed = true; rp.client_conn_id = None;
}
}
// ===== Simple logger (enabled when NYASH_NET_LOG=1) =====
static LOG_ON: Lazy<bool> = Lazy::new(|| std::env::var("NYASH_NET_LOG").unwrap_or_default() == "1");
static LOG_PATH: Lazy<String> = Lazy::new(|| std::env::var("NYASH_NET_LOG_FILE").unwrap_or_else(|_| "net_plugin.log".to_string()));

View File

@ -61,6 +61,9 @@ pub struct MethodDefinition {
/// Optional argument declarations (v2.1+)
#[serde(default)]
pub args: Option<Vec<ArgDecl>>,
/// Optional: wrap return and errors into ResultBox when true
#[serde(default)]
pub returns_result: bool,
}
/// Method argument declaration (v2.1+)

38
src/debug/leak_tracker.rs Normal file
View File

@ -0,0 +1,38 @@
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::Mutex;
static ENABLED: Lazy<bool> = Lazy::new(|| std::env::var("NYASH_LEAK_LOG").unwrap_or_default() == "1");
static LEAKS: Lazy<Mutex<HashMap<(String, u32), &'static str>>> = Lazy::new(|| Mutex::new(HashMap::new()));
pub fn init() {
let _ = &*REPORTER; // ensure reporter is constructed
}
pub fn register_plugin(box_type: &str, instance_id: u32) {
if !*ENABLED { return; }
let mut m = LEAKS.lock().unwrap();
m.insert((box_type.to_string(), instance_id), "plugin");
}
pub fn finalize_plugin(box_type: &str, instance_id: u32) {
if !*ENABLED { return; }
let mut m = LEAKS.lock().unwrap();
m.remove(&(box_type.to_string(), instance_id));
}
struct Reporter;
impl Drop for Reporter {
fn drop(&mut self) {
if !*ENABLED { return; }
let m = LEAKS.lock().unwrap();
if m.is_empty() { return; }
eprintln!("[leak] Detected {} non-finalized plugin boxes:", m.len());
for ((ty, id), _) in m.iter() {
eprintln!(" - {}(id={}) not finalized (missing fini or scope)", ty, id);
}
}
}
static REPORTER: Lazy<Reporter> = Lazy::new(|| Reporter);

View File

@ -228,6 +228,9 @@ pub struct NyashInterpreter {
/// 共有ランタイムBoxレジストリ等
#[allow(dead_code)]
pub(super) runtime: NyashRuntime,
/// 現在の文脈で式結果が破棄されるかmust_use警告用
pub(super) discard_context: bool,
}
impl NyashInterpreter {
@ -259,6 +262,7 @@ impl NyashInterpreter {
invalidated_ids: Arc::new(Mutex::new(HashSet::new())),
stdlib: None, // 遅延初期化
runtime,
discard_context: false,
}
}
@ -292,6 +296,7 @@ impl NyashInterpreter {
invalidated_ids: Arc::new(Mutex::new(HashSet::new())),
stdlib: None,
runtime,
discard_context: false,
}
}
@ -321,6 +326,7 @@ impl NyashInterpreter {
invalidated_ids: Arc::new(Mutex::new(HashSet::new())),
stdlib: None, // 遅延初期化
runtime,
discard_context: false,
}
}
@ -351,6 +357,7 @@ impl NyashInterpreter {
invalidated_ids: Arc::new(Mutex::new(HashSet::new())),
stdlib: None,
runtime,
discard_context: false,
}
}
@ -379,8 +386,12 @@ impl NyashInterpreter {
ASTNode::Program { statements, .. } => {
let mut result: Box<dyn NyashBox> = Box::new(VoidBox::new());
for statement in statements.iter() {
let last = statements.len().saturating_sub(1);
for (i, statement) in statements.iter().enumerate() {
let prev = self.discard_context;
self.discard_context = i != last; // 最終文以外は値が破棄される
result = self.execute_statement(statement)?;
self.discard_context = prev;
// 制御フローチェック
match &self.control_flow {

View File

@ -996,6 +996,10 @@ impl NyashInterpreter {
method: &str,
arguments: &[ASTNode],
) -> Result<Box<dyn NyashBox>, RuntimeError> {
// Guard: use-after-fini is a runtime error (明示ライフサイクル)
if plugin_box.is_finalized() {
return Err(RuntimeError::RuntimeFailure { message: format!("Use after fini: {}", plugin_box.box_type) });
}
eprintln!("🔍 execute_plugin_box_v2_method called: {}.{}", plugin_box.box_type, method);
let mut arg_values: Vec<Box<dyn NyashBox>> = Vec::new();
for arg in arguments {

View File

@ -20,6 +20,26 @@ macro_rules! debug_trace {
}
impl NyashInterpreter {
fn warn_if_must_use(&self, value: &Box<dyn NyashBox>) {
if std::env::var("NYASH_LINT_MUSTUSE").unwrap_or_default() != "1" { return; }
if !self.discard_context { return; }
// 重資源のヒューリスティクス: プラグインBox、またはHTTP/Socket/File系の型名
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
{
if value.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>().is_some() {
eprintln!("[lint:must_use] Discarded resource value (plugin box). Consider assigning it or calling fini().");
return;
}
}
let ty = value.type_name();
let heavy = matches!(ty,
"FileBox" | "SocketBox" | "SocketServerBox" | "SocketClientBox" | "SocketConnBox" |
"HTTPServerBox" | "HTTPRequestBox" | "HTTPResponseBox" | "HttpClientBox"
);
if heavy {
eprintln!("[lint:must_use] Discarded {} value. Consider assigning it or calling fini().", ty);
}
}
/// 文を実行 - Core statement execution engine
pub(super) fn execute_statement(&mut self, statement: &ASTNode) -> Result<Box<dyn NyashBox>, RuntimeError> {
match statement {
@ -180,8 +200,12 @@ impl NyashInterpreter {
Ok(Box::new(VoidBox::new()))
}
// 式文
_ => self.execute_expression(statement),
// 式文結果は多くの場合破棄されるため、must_use警告を出力
_ => {
let v = self.execute_expression(statement)?;
self.warn_if_must_use(&v);
Ok(v)
},
}
}

View File

@ -0,0 +1,34 @@
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::Mutex;
static ENABLED: Lazy<bool> = Lazy::new(|| std::env::var("NYASH_LEAK_LOG").unwrap_or_default() == "1");
static LEAKS: Lazy<Mutex<HashMap<(String, u32), &'static str>>> = Lazy::new(|| Mutex::new(HashMap::new()));
pub fn init() { let _ = &*REPORTER; }
pub fn register_plugin(box_type: &str, instance_id: u32) {
if !*ENABLED { return; }
LEAKS.lock().unwrap().insert((box_type.to_string(), instance_id), "plugin");
}
pub fn finalize_plugin(box_type: &str, instance_id: u32) {
if !*ENABLED { return; }
LEAKS.lock().unwrap().remove(&(box_type.to_string(), instance_id));
}
struct Reporter;
impl Drop for Reporter {
fn drop(&mut self) {
if !*ENABLED { return; }
let m = LEAKS.lock().unwrap();
if m.is_empty() { return; }
eprintln!("[leak] Detected {} non-finalized plugin boxes:", m.len());
for ((ty, id), _) in m.iter() {
eprintln!(" - {}(id={}) not finalized (missing fini or scope)", ty, id);
}
}
}
static REPORTER: Lazy<Reporter> = Lazy::new(|| Reporter);

View File

@ -5,6 +5,7 @@
pub mod plugin_config;
pub mod box_registry;
pub mod plugin_loader_v2;
pub mod leak_tracker;
pub mod unified_registry;
pub mod nyash_runtime;
// pub mod plugin_box; // legacy - 古いPluginBox

View File

@ -14,6 +14,7 @@ mod enabled {
// use std::ffi::c_void; // unused
use std::any::Any;
use once_cell::sync::Lazy;
use crate::runtime::leak_tracker;
/// Loaded plugin information
pub struct LoadedPluginV2 {
@ -71,6 +72,7 @@ mod enabled {
pub fn finalize_now(&self) {
if let Some(fini_id) = self.fini_method_id {
if !self.finalized.swap(true, std::sync::atomic::Ordering::SeqCst) {
leak_tracker::finalize_plugin("PluginBox", self.instance_id);
let tlv_args: [u8; 4] = [1, 0, 0, 0];
let mut out: [u8; 4] = [0; 4];
let mut out_len: usize = out.len();
@ -196,6 +198,7 @@ mod enabled {
impl PluginBoxV2 {
pub fn instance_id(&self) -> u32 { self.inner.instance_id }
pub fn finalize_now(&self) { self.inner.finalize_now() }
pub fn is_finalized(&self) -> bool { self.inner.finalized.load(std::sync::atomic::Ordering::SeqCst) }
}
/// Plugin loader v2
@ -376,6 +379,7 @@ impl PluginBoxV2 {
let toml_value: toml::Value = toml::from_str(&toml_content).map_err(|_| 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;
let returns_result = box_conf.methods.get(method_name).map(|m| m.returns_result).unwrap_or(false);
eprintln!("[PluginLoaderV2] Invoke {}.{}: resolving and encoding args (argc={})", box_type, method_name, args.len());
// TLV args: encode using BID-1 style (u16 ver, u16 argc, then entries)
let tlv_args = {
@ -497,15 +501,29 @@ impl PluginBoxV2 {
if rc != 0 {
let be = BidError::from_raw(rc);
eprintln!("[PluginLoaderV2] invoke rc={} ({}) for {}.{}", rc, be.message(), box_type, method_name);
if returns_result {
let err = crate::exception_box::ErrorBox::new(&format!("{} (code: {})", be.message(), rc));
return Ok(Some(Box::new(crate::boxes::result::NyashResultBox::new_err(Box::new(err)))));
}
return Err(be);
}
// Decode: BID-1 style header + first entry
let result = if out_len == 0 { None } else {
let result = if out_len == 0 {
if returns_result {
Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(Box::new(crate::box_trait::VoidBox::new()))) as Box<dyn NyashBox>)
} else { None }
} else {
let data = &out[..out_len];
if data.len() < 4 { return Ok(None); }
let _ver = u16::from_le_bytes([data[0], data[1]]);
let argc = u16::from_le_bytes([data[2], data[3]]);
if argc == 0 { return Ok(None); }
if argc == 0 {
if returns_result {
return Ok(Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(Box::new(crate::box_trait::VoidBox::new())))));
} else {
return Ok(None);
}
}
if data.len() < 8 { return Ok(None); }
let tag = data[4];
let _rsv = data[5];
@ -536,7 +554,12 @@ impl PluginBoxV2 {
finalized: std::sync::atomic::AtomicBool::new(false),
}),
};
return Ok(Some(Box::new(pbox) as Box<dyn NyashBox>));
let val: Box<dyn NyashBox> = Box::new(pbox);
if returns_result {
return Ok(Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(val))));
} else {
return Ok(Some(val));
}
}
}
}
@ -544,13 +567,17 @@ impl PluginBoxV2 {
}
2 if size == 4 => { // I32
let mut b = [0u8;4]; b.copy_from_slice(payload);
Some(Box::new(IntegerBox::new(i32::from_le_bytes(b) as i64)) as Box<dyn NyashBox>)
let val: Box<dyn NyashBox> = Box::new(IntegerBox::new(i32::from_le_bytes(b) as i64));
if returns_result { Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(val)) as Box<dyn NyashBox>) } else { Some(val) }
}
6 | 7 => { // String/Bytes
let s = String::from_utf8_lossy(payload).to_string();
Some(Box::new(StringBox::new(s)) as Box<dyn NyashBox>)
let val: Box<dyn NyashBox> = Box::new(StringBox::new(s));
if returns_result { Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(val)) as Box<dyn NyashBox>) } else { Some(val) }
}
9 => None, // Void
9 => {
if returns_result { Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(Box::new(crate::box_trait::VoidBox::new()))) as Box<dyn NyashBox>) } else { None }
},
_ => None,
}
};
@ -723,7 +750,7 @@ impl PluginBoxV2 {
};
eprintln!("🎉 birth() success: {} instance_id={}", box_type, instance_id);
// Create v2 plugin box wrapper with actual instance_id
let plugin_box = PluginBoxV2 {
box_type: box_type.to_string(),
@ -735,6 +762,8 @@ impl PluginBoxV2 {
finalized: std::sync::atomic::AtomicBool::new(false),
}),
};
leak_tracker::init();
leak_tracker::register_plugin(&plugin_box.box_type, instance_id);
Ok(Box::new(plugin_box))
}

View File

@ -39,9 +39,10 @@ impl ScopeTracker {
let _ = instance.fini();
continue;
}
// PluginBoxV2: do not auto-finalize (shared handle may be referenced elsewhere)
// PluginBoxV2: 明示ライフサイクルに合わせ、スコープ終了時にfini自己責任運用
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
if arc_box.as_any().downcast_ref::<PluginBoxV2>().is_some() {
if let Some(p) = arc_box.as_any().downcast_ref::<PluginBoxV2>() {
p.finalize_now();
continue;
}
// Builtin and others: no-op for now

View File

@ -0,0 +1,99 @@
#![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_http_two_servers_parallel() {
std::env::set_var("NYASH_NET_LOG", "1");
std::env::set_var("NYASH_NET_LOG_FILE", "net_plugin.log");
if !try_init_plugins() { return; }
let code = r#"
local s1, s2, c, r1, r2, req1, req2, p1, p2, x, y
s1 = new HttpServerBox()
s2 = new HttpServerBox()
s1.start(8101)
s2.start(8102)
c = new HttpClientBox()
r1 = c.get("http://localhost:8101/a")
r2 = c.get("http://localhost:8102/b")
// accept once per pending request and keep handles
req1 = s1.accept()
req2 = s2.accept()
p1 = req1.path()
p2 = req2.path()
x = new HttpResponseBox()
x.write("X")
y = new HttpResponseBox()
y.write("Y")
// respond using kept request handles
req1.respond(x)
req2.respond(y)
// read results
x = r1.readBody()
y = r2.readBody()
x + ":" + y
"#;
let ast = NyashParser::parse_from_string(code).expect("parse");
let mut i = nyash_rust::interpreter::NyashInterpreter::new();
let result = i.execute(ast).expect("exec");
assert!(result.to_string_box().value.contains(":"));
}
#[test]
fn e2e_http_long_body_and_headers() {
std::env::set_var("NYASH_NET_LOG", "1");
std::env::set_var("NYASH_NET_LOG_FILE", "net_plugin.log");
if !try_init_plugins() { return; }
let code = r#"
local s, c, r, q, resp, body, hv
s = new HttpServerBox()
s.start(8103)
c = new HttpClientBox()
r = c.post("http://localhost:8103/long", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
q = s.accept()
body = q.readBody()
resp = new HttpResponseBox()
resp.setStatus(202)
resp.setHeader("X-Alpha", "A")
resp.setHeader("X-Beta", "B")
resp.write("OK-LONG")
q.respond(resp)
body = r.readBody()
hv = r.getHeader("X-Alpha")
hv + ":" + body
"#;
let ast = NyashParser::parse_from_string(code).expect("parse");
let mut i = nyash_rust::interpreter::NyashInterpreter::new();
let result = i.execute(ast).expect("exec");
let s = result.to_string_box().value;
assert!(s.starts_with("A:"));
assert!(s.contains("OK-LONG"));
}