feat: WASMビルド完全対応+finiシステム修正 🎉

## WASMビルド対応
- TimerBox、AudioBox等の問題のあるBoxをWASM環境では条件付きコンパイルで除外
- WebBox (WebDisplayBox, WebConsoleBox, WebCanvasBox) にas_anyメソッド追加
- プラグイン関連コードに#[cfg]ガードを追加
- web-sysフィーチャーを追加(Performance、MouseEvent等)
- projects/nyash-wasmのビルドが完全に通るように!

## finiシステム修正
- フィールド差し替え時の自動fini削除(Nyashの明示的哲学に従う)
- スコープ離脱時のみfini実行(meは除外)
- ドキュメント更新で正しいfiniルールを明記

## その他
- CLAUDE.mdにWASMビルド方法を追記(wasm-pack build --target web)
- 開発サーバー起動方法を記載(python3 -m http.server 8010)
- cargo testで全テスト成功を確認

これでNyashがブラウザで動作可能に!🐱

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Moe Charm
2025-08-20 07:33:18 +09:00
parent 83d3914e46
commit 670615d1de
18 changed files with 209 additions and 78 deletions

View File

@ -1,3 +1,6 @@
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
mod plugin_impl {
use crate::bid::{BidError, BidResult, LoadedPlugin};
use crate::bid::tlv::{TlvEncoder, TlvDecoder};
use crate::bid::types::BidTag;
@ -129,4 +132,9 @@ impl fmt::Display for GenericPluginBox {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.fmt_box(f)
}
}
}
} // mod plugin_impl
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
pub use plugin_impl::*;

View File

@ -1,4 +1,5 @@
use crate::bid::{BidError, BidResult, NyashHostVtable, NyashPluginInfo, PluginHandle, PLUGIN_ABI_SYMBOL, PLUGIN_INIT_SYMBOL, PLUGIN_INVOKE_SYMBOL, PLUGIN_SHUTDOWN_SYMBOL};
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
use libloading::{Library, Symbol};
use std::ffi::c_void;
use std::path::{Path, PathBuf};

View File

@ -8,6 +8,7 @@ pub mod metadata;
pub mod plugin_api;
pub mod bridge;
pub mod plugins;
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
pub mod loader;
// pub mod registry; // legacy - v2 plugin system uses BoxFactoryRegistry instead
// pub mod plugin_box; // legacy - FileBox専用実装
@ -19,6 +20,7 @@ pub use error::*;
pub use metadata::*;
pub use plugin_api::*;
pub use bridge::*;
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
pub use loader::*;
// pub use registry::*; // legacy - v2 plugin system uses BoxFactoryRegistry instead
// pub use plugin_box::*; // legacy

View File

@ -66,38 +66,14 @@ use web_sys::{
#[derive(Debug, Clone)]
pub struct AudioBox {
base: BoxBase,
#[cfg(target_arch = "wasm32")]
context: Option<AudioContext>,
#[cfg(target_arch = "wasm32")]
gain_node: Option<GainNode>,
#[cfg(target_arch = "wasm32")]
analyser_node: Option<AnalyserNode>,
volume: f64,
is_playing: bool,
}
impl AudioBox {
pub fn new() -> Self {
#[cfg(target_arch = "wasm32")]
let context = AudioContext::new().ok();
#[cfg(target_arch = "wasm32")]
let (gain_node, analyser_node) = if let Some(ctx) = &context {
let gain = ctx.create_gain().ok();
let analyser = ctx.create_analyser().ok();
(gain, analyser)
} else {
(None, None)
};
Self {
base: BoxBase::new(),
#[cfg(target_arch = "wasm32")]
context,
#[cfg(target_arch = "wasm32")]
gain_node,
#[cfg(target_arch = "wasm32")]
analyser_node,
volume: 1.0,
is_playing: false,
}

View File

@ -60,10 +60,16 @@ pub mod math_box;
pub mod time_box;
pub mod debug_box;
pub mod random_box;
// These boxes use web APIs that require special handling in WASM
#[cfg(not(target_arch = "wasm32"))]
pub mod timer_box;
#[cfg(not(target_arch = "wasm32"))]
pub mod canvas_event_box;
#[cfg(not(target_arch = "wasm32"))]
pub mod canvas_loop_box;
#[cfg(not(target_arch = "wasm32"))]
pub mod audio_box;
#[cfg(not(target_arch = "wasm32"))]
pub mod qr_box;
pub mod sound_box;
pub mod map_box;
@ -85,10 +91,15 @@ pub use math_box::{MathBox, FloatBox};
pub use time_box::{TimeBox, DateTimeBox};
pub use debug_box::DebugBox;
pub use random_box::RandomBox;
#[cfg(not(target_arch = "wasm32"))]
pub use timer_box::TimerBox;
#[cfg(not(target_arch = "wasm32"))]
pub use canvas_event_box::CanvasEventBox;
#[cfg(not(target_arch = "wasm32"))]
pub use canvas_loop_box::CanvasLoopBox;
#[cfg(not(target_arch = "wasm32"))]
pub use audio_box::AudioBox;
#[cfg(not(target_arch = "wasm32"))]
pub use qr_box::QRBox;
pub use sound_box::SoundBox;
pub use map_box::MapBox;

View File

@ -52,25 +52,18 @@ use std::any::Any;
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
use web_sys::{window, Performance};
use web_sys::window;
/// タイマー管理Box
#[derive(Debug, Clone)]
pub struct TimerBox {
base: BoxBase,
#[cfg(target_arch = "wasm32")]
performance: Option<Performance>,
}
impl TimerBox {
pub fn new() -> Self {
#[cfg(target_arch = "wasm32")]
let performance = window().and_then(|w| w.performance().ok());
Self {
base: BoxBase::new(),
#[cfg(target_arch = "wasm32")]
performance,
}
}
@ -78,8 +71,12 @@ impl TimerBox {
pub fn now(&self) -> f64 {
#[cfg(target_arch = "wasm32")]
{
if let Some(perf) = &self.performance {
perf.now()
if let Some(window) = window() {
if let Ok(perf) = window.performance() {
perf.now()
} else {
js_sys::Date::now()
}
} else {
js_sys::Date::now()
}

View File

@ -275,6 +275,9 @@ impl BoxCore for WebCanvasBox {
write!(f, "WebCanvasBox({}, {}x{})", self.canvas_id, self.width, self.height)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self

View File

@ -152,6 +152,9 @@ impl BoxCore for WebConsoleBox {
write!(f, "WebConsoleBox({})", self.target_element_id)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self

View File

@ -145,6 +145,9 @@ impl BoxCore for WebDisplayBox {
write!(f, "WebDisplayBox({})", self.target_element_id)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self

View File

@ -503,6 +503,30 @@ impl NyashInterpreter {
}
pub(super) fn restore_local_vars(&mut self, saved: HashMap<String, Box<dyn NyashBox>>) {
// 🎯 スコープ離脱時現在のローカル変数に対してfiniを呼ぶ
// ただし「me」は特別扱いインスタンス自身なのでfiniしない
for (name, value) in &self.local_vars {
// 「me」はインスタンス自身なのでスコープ離脱時にfiniしない
if name == "me" {
continue;
}
// ユーザー定義BoxInstanceBoxの場合
if let Some(instance) = (**value).as_any().downcast_ref::<InstanceBox>() {
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メソッドを持たないので呼ばない
// StringBox、IntegerBox等はリソース管理不要
}
// その後、保存されていた変数で復元
self.local_vars = saved.into_iter()
.map(|(k, v)| (k, Arc::from(v))) // Convert Box to Arc
.collect();
@ -516,6 +540,23 @@ impl NyashInterpreter {
}
pub(super) fn restore_outbox_vars(&mut self, saved: HashMap<String, Box<dyn NyashBox>>) {
// 🎯 スコープ離脱時現在のoutbox変数に対してもfiniを呼ぶ
for (name, value) in &self.outbox_vars {
// ユーザー定義BoxInstanceBoxの場合
if let Some(instance) = (**value).as_any().downcast_ref::<InstanceBox>() {
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メソッドを持たないので呼ばない要修正
}
// その後、保存されていた変数で復元
self.outbox_vars = saved.into_iter()
.map(|(k, v)| (k, Arc::from(v))) // Convert Box to Arc
.collect();

View File

@ -13,6 +13,7 @@ use crate::instance_v2::InstanceBox;
use crate::channel_box::ChannelBox;
use crate::interpreter::core::{NyashInterpreter, RuntimeError};
use crate::interpreter::finalization;
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
use crate::runtime::plugin_loader_v2::PluginBoxV2;
use std::sync::Arc;
@ -489,6 +490,7 @@ impl NyashInterpreter {
// RangeBox method calls (将来的に追加予定)
// PluginBoxV2 method calls
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
if let Some(plugin_box) = obj_value.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
return self.execute_plugin_box_v2_method(plugin_box, method, arguments);
}
@ -902,6 +904,7 @@ impl NyashInterpreter {
}
/// Execute method call on PluginBoxV2
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
fn execute_plugin_box_v2_method(
&mut self,
plugin_box: &PluginBoxV2,

View File

@ -305,17 +305,8 @@ impl NyashInterpreter {
}
}
// 既存のフィールド値があれば fini() を呼ぶ
if let Some(old_field_value) = instance.get_field(field) {
if let Some(old_instance) = (*old_field_value).as_any().downcast_ref::<InstanceBox>() {
let _ = old_instance.fini();
finalization::mark_as_finalized(old_instance.box_id());
}
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
if let Some(old_plugin) = (*old_field_value).as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
old_plugin.call_fini();
}
}
// 🚨 フィールド差し替え時の自動finiは削除Nyashの明示的哲学
// プログラマーが必要なら明示的にfini()を呼ぶべき
instance.set_field(field, Arc::from(val.clone_box()))
.map_err(|e| RuntimeError::InvalidOperation { message: e })?;
@ -342,17 +333,8 @@ impl NyashInterpreter {
});
}
// 既存のthis.field値があれば fini() を呼ぶ
if let Some(old_field_value) = instance.get_field(field) {
if let Some(old_instance) = (*old_field_value).as_any().downcast_ref::<InstanceBox>() {
let _ = old_instance.fini();
finalization::mark_as_finalized(old_instance.box_id());
}
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
if let Some(old_plugin) = (*old_field_value).as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
old_plugin.call_fini();
}
}
// 🚨 フィールド差し替え時の自動finiは削除Nyashの明示的哲学
// プログラマーが必要なら明示的にfini()を呼ぶべき
instance.set_field(field, Arc::from(val.clone_box()))
.map_err(|e| RuntimeError::InvalidOperation { message: e })?;
@ -379,17 +361,8 @@ impl NyashInterpreter {
});
}
// 既存のme.field値があれば fini() を呼ぶ
if let Some(old_field_value) = instance.get_field(field) {
if let Some(old_instance) = (*old_field_value).as_any().downcast_ref::<InstanceBox>() {
let _ = old_instance.fini();
finalization::mark_as_finalized(old_instance.box_id());
}
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
if let Some(old_plugin) = (*old_field_value).as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
old_plugin.call_fini();
}
}
// 🚨 フィールド差し替え時の自動finiは削除Nyashの明示的哲学
// プログラマーが必要なら明示的にfini()を呼ぶべき
instance.set_field(field, Arc::from(val.clone_box()))
.map_err(|e| RuntimeError::InvalidOperation { message: e })?;

View File

@ -15,6 +15,8 @@
use super::*;
#[cfg(target_arch = "wasm32")]
use crate::boxes::web::{WebDisplayBox, WebConsoleBox, WebCanvasBox};
#[cfg(target_arch = "wasm32")]
use crate::boxes::FloatBox;
#[cfg(target_arch = "wasm32")]
impl NyashInterpreter {

View File

@ -399,8 +399,14 @@ mod stub {
use once_cell::sync::Lazy;
use std::sync::{Arc, RwLock};
pub struct PluginLoaderV2;
impl PluginLoaderV2 { pub fn new() -> Self { Self } }
pub struct PluginLoaderV2 {
pub config: Option<()>, // Dummy config for compatibility
}
impl PluginLoaderV2 {
pub fn new() -> Self {
Self { config: None }
}
}
impl PluginLoaderV2 {
pub fn load_config(&mut self, _p: &str) -> BidResult<()> { Ok(()) }
pub fn load_all_plugins(&self) -> BidResult<()> { Ok(()) }