Major Interpreter Refactoring: - Split core.rs (373 lines removed) into focused modules - Split expressions/calls.rs (460 lines removed) into cleaner structure - Added new modules: calls.rs, errors.rs, eval.rs, methods_dispatch.rs, state.rs - Improved separation of concerns across interpreter components P2PBox Enhancements: - Added on_once() for one-time event handlers - Added off() for handler deregistration - Implemented handler flags with AtomicBool for thread-safe management - Added loopback testing cache (last_from, last_intent_name) - Improved Arc-based state sharing for transport and handlers Plugin Loader Unification (In Progress): - Created plugin_loader_unified.rs skeleton - Created plugin_ffi_common.rs for shared FFI utilities - Migration plan documented (2400 lines → 1100 lines target) MIR & VM Improvements: - Enhanced modularized MIR builder structure - Added BoxCall dispatch improvements - Better separation in builder modules Documentation Updates: - Added Phase 9.79a unified box dispatch plan - Created plugin loader migration plan - Updated CURRENT_TASK.md with latest progress All tests passing (180 tests) - ready for next phase of refactoring 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
882 lines
39 KiB
Rust
882 lines
39 KiB
Rust
//! Nyash v2 Plugin Loader
|
||
//!
|
||
//! cfg/features で2パスを提供:
|
||
//! - enabled: plugins feature 有効 かつ 非wasm32 ターゲット
|
||
//! - stub : それ以外(WASMやplugins無効)
|
||
|
||
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
|
||
mod enabled {
|
||
use crate::bid::{BidResult, BidError};
|
||
use crate::box_trait::{NyashBox, BoxCore, StringBox, IntegerBox};
|
||
use crate::config::nyash_toml_v2::{NyashConfigV2, LibraryDefinition};
|
||
use std::collections::HashMap;
|
||
use std::sync::{Arc, RwLock};
|
||
// use std::ffi::c_void; // unused
|
||
use std::any::Any;
|
||
use once_cell::sync::Lazy;
|
||
use crate::runtime::leak_tracker;
|
||
fn dbg_on() -> bool { std::env::var("NYASH_DEBUG_PLUGIN").unwrap_or_default() == "1" }
|
||
|
||
/// Loaded plugin information
|
||
pub struct LoadedPluginV2 {
|
||
/// Library handle
|
||
_lib: Arc<libloading::Library>,
|
||
|
||
/// Box types provided by this plugin
|
||
#[allow(dead_code)]
|
||
box_types: Vec<String>,
|
||
|
||
/// Optional init function
|
||
#[allow(dead_code)]
|
||
init_fn: Option<unsafe extern "C" fn() -> i32>,
|
||
|
||
/// Required invoke function
|
||
invoke_fn: unsafe extern "C" fn(u32, u32, u32, *const u8, usize, *mut u8, *mut usize) -> i32,
|
||
}
|
||
|
||
/// v2 Plugin Box wrapper - temporary implementation
|
||
#[derive(Debug)]
|
||
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,
|
||
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) {
|
||
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();
|
||
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.inner.instance_id as u64
|
||
}
|
||
|
||
fn parent_type_id(&self) -> Option<std::any::TypeId> {
|
||
None
|
||
}
|
||
|
||
fn fmt_box(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||
write!(f, "{}({})", self.box_type, self.inner.instance_id)
|
||
}
|
||
|
||
fn as_any(&self) -> &dyn Any {
|
||
self
|
||
}
|
||
|
||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||
self
|
||
}
|
||
}
|
||
|
||
impl NyashBox for PluginBoxV2 {
|
||
fn is_identity(&self) -> bool { true }
|
||
fn type_name(&self) -> &'static str {
|
||
// Return the actual box type name for proper method dispatch
|
||
match self.box_type.as_str() {
|
||
"FileBox" => "FileBox",
|
||
_ => "PluginBoxV2",
|
||
}
|
||
}
|
||
|
||
fn clone_box(&self) -> Box<dyn NyashBox> {
|
||
if dbg_on() { eprintln!("🔍 DEBUG: PluginBoxV2::clone_box called for {} (id={})", self.box_type, self.inner.instance_id); }
|
||
// Clone means creating a new instance by calling birth() on the plugin
|
||
let mut output_buffer = vec![0u8; 1024];
|
||
let mut output_len = output_buffer.len();
|
||
let tlv_args = [1u8, 0, 0, 0]; // version=1, argc=0
|
||
|
||
let result = unsafe {
|
||
(self.inner.invoke_fn)(
|
||
self.inner.type_id,
|
||
0, // method_id=0 (birth)
|
||
0, // instance_id=0 (static call)
|
||
tlv_args.as_ptr(),
|
||
tlv_args.len(),
|
||
output_buffer.as_mut_ptr(),
|
||
&mut output_len,
|
||
)
|
||
};
|
||
|
||
if result == 0 && output_len >= 4 {
|
||
let new_instance_id = u32::from_le_bytes([
|
||
output_buffer[0], output_buffer[1], output_buffer[2], output_buffer[3]
|
||
]);
|
||
if dbg_on() { eprintln!("🎉 clone_box success: created new {} instance_id={}", self.box_type, new_instance_id); }
|
||
Box::new(PluginBoxV2 {
|
||
box_type: self.box_type.clone(),
|
||
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 {
|
||
if dbg_on() { eprintln!("❌ clone_box failed: birth() returned error code {}", result); }
|
||
Box::new(StringBox::new(format!("Clone failed for {}", self.box_type)))
|
||
}
|
||
}
|
||
|
||
fn to_string_box(&self) -> crate::box_trait::StringBox {
|
||
StringBox::new(format!("{}({})", self.box_type, self.inner.instance_id))
|
||
}
|
||
|
||
fn equals(&self, _other: &dyn NyashBox) -> crate::box_trait::BoolBox {
|
||
crate::box_trait::BoolBox::new(false)
|
||
}
|
||
|
||
fn share_box(&self) -> Box<dyn NyashBox> {
|
||
if dbg_on() { 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(),
|
||
inner: self.inner.clone(),
|
||
})
|
||
}
|
||
}
|
||
|
||
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
|
||
pub struct PluginLoaderV2 {
|
||
/// Loaded plugins (library name -> plugin info)
|
||
plugins: RwLock<HashMap<String, Arc<LoadedPluginV2>>>,
|
||
|
||
/// Configuration
|
||
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 {
|
||
fn find_box_by_type_id<'a>(&'a self, config: &'a NyashConfigV2, toml_value: &'a toml::Value, type_id: u32) -> Option<(&'a str, &'a str)> {
|
||
for (lib_name, lib_def) in &config.libraries {
|
||
for box_name in &lib_def.boxes {
|
||
if let Some(box_conf) = config.get_box_config(lib_name, box_name, toml_value) {
|
||
if box_conf.type_id == type_id {
|
||
return Some((lib_name.as_str(), box_name.as_str()));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
None
|
||
}
|
||
/// Create new loader
|
||
pub fn new() -> Self {
|
||
Self {
|
||
plugins: RwLock::new(HashMap::new()),
|
||
config: None,
|
||
config_path: None,
|
||
singletons: RwLock::new(HashMap::new()),
|
||
}
|
||
}
|
||
|
||
/// Load configuration from nyash.toml
|
||
pub fn load_config(&mut self, config_path: &str) -> BidResult<()> {
|
||
// Canonicalize path for later re-reads
|
||
let canonical = std::fs::canonicalize(config_path)
|
||
.map(|p| p.to_string_lossy().to_string())
|
||
.unwrap_or_else(|_| config_path.to_string());
|
||
self.config_path = Some(canonical.clone());
|
||
|
||
self.config = Some(NyashConfigV2::from_file(&canonical)
|
||
.map_err(|e| {
|
||
eprintln!("Failed to load config: {}", e);
|
||
BidError::PluginError
|
||
})?);
|
||
Ok(())
|
||
}
|
||
|
||
/// Load all plugins from config
|
||
pub fn load_all_plugins(&self) -> BidResult<()> {
|
||
let config = self.config.as_ref()
|
||
.ok_or(BidError::PluginError)?;
|
||
|
||
for (lib_name, lib_def) in &config.libraries {
|
||
if let Err(e) = self.load_plugin(lib_name, lib_def) {
|
||
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 = crate::runtime::plugin_ffi_common::encode_empty_args();
|
||
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(
|
||
&self,
|
||
iface_name: &str,
|
||
method_name: &str,
|
||
args: &[Box<dyn NyashBox>],
|
||
) -> BidResult<Option<Box<dyn NyashBox>>> {
|
||
match (iface_name, method_name) {
|
||
("env.console", "log") => {
|
||
for a in args {
|
||
println!("{}", a.to_string_box().value);
|
||
}
|
||
Ok(None)
|
||
}
|
||
("env.canvas", _) => {
|
||
eprintln!("[env.canvas] {} invoked (stub)", method_name);
|
||
Ok(None)
|
||
}
|
||
_ => {
|
||
// Future: route to plugin-defined extern interfaces via config
|
||
Err(BidError::InvalidMethod)
|
||
}
|
||
}
|
||
}
|
||
|
||
fn resolve_method_id_from_file(&self, box_type: &str, method_name: &str) -> BidResult<u32> {
|
||
let config = self.config.as_ref().ok_or(BidError::PluginError)?;
|
||
let (lib_name, _lib_def) = config.find_library_for_box(box_type)
|
||
.ok_or(BidError::InvalidType)?;
|
||
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 box_conf = config.get_box_config(lib_name, box_type, &toml_value).ok_or(BidError::InvalidType)?;
|
||
let method = box_conf.methods.get(method_name).ok_or_else(|| {
|
||
eprintln!("[PluginLoaderV2] Method '{}' not found for box '{}' in {}", method_name, box_type, cfg_path);
|
||
eprintln!("[PluginLoaderV2] Available methods: {:?}", box_conf.methods.keys().collect::<Vec<_>>());
|
||
BidError::InvalidMethod
|
||
})?;
|
||
Ok(method.method_id)
|
||
}
|
||
|
||
/// Invoke an instance method on a plugin box by name (minimal TLV encoding)
|
||
pub fn invoke_instance_method(
|
||
&self,
|
||
box_type: &str,
|
||
method_name: &str,
|
||
instance_id: u32,
|
||
args: &[Box<dyn NyashBox>],
|
||
) -> BidResult<Option<Box<dyn NyashBox>>> {
|
||
// v2.1: 引数ありのメソッドを許可(BoxRef/基本型/文字列化フォールバック)
|
||
let method_id = self.resolve_method_id_from_file(box_type, method_name)?;
|
||
// Find plugin and type_id
|
||
let config = self.config.as_ref().ok_or(BidError::PluginError)?;
|
||
let (lib_name, _lib_def) = config.find_library_for_box(box_type).ok_or(BidError::InvalidType)?;
|
||
let plugins = self.plugins.read().unwrap();
|
||
let plugin = plugins.get(lib_name).ok_or(BidError::PluginError)?;
|
||
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 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 = {
|
||
let mut buf = crate::runtime::plugin_ffi_common::encode_tlv_header(args.len() as u16);
|
||
// Validate against nyash.toml method args schema if present
|
||
let expected_args = box_conf.methods.get(method_name).and_then(|m| m.args.clone());
|
||
if let Some(exp) = expected_args.as_ref() {
|
||
if exp.len() != args.len() {
|
||
eprintln!(
|
||
"[PluginLoaderV2] InvalidArgs: {}.{} expects {} args, got {} (schema={:?})",
|
||
box_type,
|
||
method_name,
|
||
exp.len(),
|
||
args.len(),
|
||
exp.iter().map(|a| a.kind_str()).collect::<Vec<_>>()
|
||
);
|
||
return Err(BidError::InvalidArgs);
|
||
}
|
||
}
|
||
|
||
for (idx, a) in args.iter().enumerate() {
|
||
// If schema exists, validate per expected kind
|
||
if let Some(exp) = expected_args.as_ref() {
|
||
let decl = &exp[idx];
|
||
match decl {
|
||
crate::config::nyash_toml_v2::ArgDecl::Typed { kind, category } => {
|
||
match kind.as_str() {
|
||
"box" => {
|
||
// Only plugin box supported for now
|
||
if category.as_deref() != Some("plugin") {
|
||
return Err(BidError::InvalidArgs);
|
||
}
|
||
if a.as_any().downcast_ref::<PluginBoxV2>().is_none() {
|
||
return Err(BidError::InvalidArgs);
|
||
}
|
||
}
|
||
"string" => {
|
||
if a.as_any().downcast_ref::<StringBox>().is_none() {
|
||
return Err(BidError::InvalidArgs);
|
||
}
|
||
}
|
||
"int" | "i32" => {
|
||
if a.as_any().downcast_ref::<IntegerBox>().is_none() {
|
||
return Err(BidError::InvalidArgs);
|
||
}
|
||
}
|
||
_ => {
|
||
eprintln!("[PluginLoaderV2] InvalidArgs: unsupported kind '{}' for {}.{} arg[{}]",
|
||
kind, box_type, method_name, idx);
|
||
return Err(BidError::InvalidArgs);
|
||
}
|
||
}
|
||
}
|
||
crate::config::nyash_toml_v2::ArgDecl::Name(_) => {
|
||
// Back-compat: allow common primitives (string or int)
|
||
let is_string = a.as_any().downcast_ref::<StringBox>().is_some();
|
||
let is_int = a.as_any().downcast_ref::<IntegerBox>().is_some();
|
||
if !(is_string || is_int) {
|
||
eprintln!("[PluginLoaderV2] InvalidArgs: expected string/int for {}.{} arg[{}]",
|
||
box_type, method_name, idx);
|
||
return Err(BidError::InvalidArgs);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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.inner.instance_id);
|
||
crate::runtime::plugin_ffi_common::encode::plugin_handle(&mut buf, p.inner.type_id, p.inner.instance_id);
|
||
continue;
|
||
}
|
||
// Integer: prefer i32
|
||
if let Some(i) = a.as_any().downcast_ref::<IntegerBox>() {
|
||
eprintln!("[PluginLoaderV2] arg[{}]: Integer({}) -> I32(tag=2)", idx, i.value);
|
||
let v = i.value as i32;
|
||
crate::runtime::plugin_ffi_common::encode::i32(&mut buf, v);
|
||
continue;
|
||
}
|
||
// String: tag=6
|
||
if let Some(s) = a.as_any().downcast_ref::<StringBox>() {
|
||
eprintln!("[PluginLoaderV2] arg[{}]: String(len={}) -> String(tag=6)", idx, s.value.len());
|
||
crate::runtime::plugin_ffi_common::encode::string(&mut buf, &s.value);
|
||
continue;
|
||
}
|
||
// No schema or unsupported type: only allow fallback when schema is None
|
||
if expected_args.is_none() {
|
||
eprintln!("[PluginLoaderV2] arg[{}]: fallback stringify", idx);
|
||
let sv = a.to_string_box().value;
|
||
crate::runtime::plugin_ffi_common::encode::string(&mut buf, &sv);
|
||
} else {
|
||
return Err(BidError::InvalidArgs);
|
||
}
|
||
}
|
||
buf
|
||
};
|
||
eprintln!("[VM→Plugin] call {}.{} recv_id={} returns_result={}", box_type, method_name, instance_id, returns_result);
|
||
if dbg_on() {
|
||
// Dump compact TLV header and first few bytes for diagnostics
|
||
let hdr_ver = u16::from_le_bytes([tlv_args[0], tlv_args[1]]);
|
||
let hdr_argc = u16::from_le_bytes([tlv_args[2], tlv_args[3]]);
|
||
let preview_len = std::cmp::min(tlv_args.len(), 64);
|
||
let preview: Vec<String> = tlv_args[..preview_len].iter().map(|b| format!("{:02X}", b)).collect();
|
||
eprintln!("[VM→Plugin] TLV ver={} argc={} bytes={} preview={}...",
|
||
hdr_ver, hdr_argc, tlv_args.len(), preview.join(" "));
|
||
}
|
||
let mut out = vec![0u8; 1024];
|
||
let mut out_len: usize = out.len();
|
||
let rc = unsafe {
|
||
(plugin.invoke_fn)(
|
||
type_id,
|
||
method_id,
|
||
instance_id,
|
||
tlv_args.as_ptr(),
|
||
tlv_args.len(),
|
||
out.as_mut_ptr(),
|
||
&mut out_len,
|
||
)
|
||
};
|
||
if rc != 0 {
|
||
let be = BidError::from_raw(rc);
|
||
if dbg_on() { 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 {
|
||
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 {
|
||
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 let Some((tag, size, payload)) = crate::runtime::plugin_ffi_common::decode::tlv_first(data) {
|
||
match tag {
|
||
8 if size == 8 => { // Handle -> PluginBoxV2
|
||
let mut t = [0u8;4]; t.copy_from_slice(&payload[0..4]);
|
||
let mut i = [0u8;4]; i.copy_from_slice(&payload[4..8]);
|
||
let r_type = u32::from_le_bytes(t);
|
||
let r_inst = u32::from_le_bytes(i);
|
||
if dbg_on() { eprintln!("[Plugin→VM] return handle type_id={} inst={} (returns_result={})", r_type, r_inst, returns_result); }
|
||
// Map type_id -> (lib_name, box_name)
|
||
if let Some((ret_lib, ret_box)) = self.find_box_by_type_id(config, &toml_value, r_type) {
|
||
// Get plugin for ret_lib
|
||
let plugins = self.plugins.read().unwrap();
|
||
if let Some(ret_plugin) = plugins.get(ret_lib) {
|
||
// Need fini_method_id from config
|
||
if let Some(ret_conf) = config.get_box_config(ret_lib, ret_box, &toml_value) {
|
||
let fini_id = ret_conf.methods.get("fini").map(|m| m.method_id);
|
||
let pbox = PluginBoxV2 {
|
||
box_type: ret_box.to_string(),
|
||
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),
|
||
}),
|
||
};
|
||
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));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
None
|
||
}
|
||
2 if size == 4 => { // I32
|
||
let n = crate::runtime::plugin_ffi_common::decode::i32(payload).unwrap();
|
||
let val: Box<dyn NyashBox> = Box::new(IntegerBox::new(n as i64));
|
||
if dbg_on() { eprintln!("[Plugin→VM] return i32 value={} (returns_result={})", n, returns_result); }
|
||
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 = crate::runtime::plugin_ffi_common::decode::string(payload);
|
||
if dbg_on() { eprintln!("[Plugin→VM] return str/bytes len={} (returns_result={})", size, returns_result); }
|
||
if returns_result {
|
||
// Heuristic: for Result-returning methods, string payload represents an error message
|
||
let err = crate::exception_box::ErrorBox::new(&s);
|
||
Some(Box::new(crate::boxes::result::NyashResultBox::new_err(Box::new(err))) as Box<dyn NyashBox>)
|
||
} else {
|
||
Some(Box::new(StringBox::new(s)) as Box<dyn NyashBox>)
|
||
}
|
||
}
|
||
9 => {
|
||
if dbg_on() { eprintln!("[Plugin→VM] return void (returns_result={})", returns_result); }
|
||
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,
|
||
}} else { None }
|
||
};
|
||
Ok(result)
|
||
}
|
||
|
||
/// Load single plugin
|
||
pub fn load_plugin(&self, lib_name: &str, lib_def: &LibraryDefinition) -> BidResult<()> {
|
||
// Check if already loaded
|
||
{
|
||
let plugins = self.plugins.read().unwrap();
|
||
if plugins.contains_key(lib_name) {
|
||
return Ok(());
|
||
}
|
||
}
|
||
|
||
// Load library
|
||
let lib = unsafe {
|
||
libloading::Library::new(&lib_def.path)
|
||
.map_err(|e| {
|
||
eprintln!("Failed to load library: {}", e);
|
||
BidError::PluginError
|
||
})?
|
||
};
|
||
|
||
// Get required invoke function and dereference it
|
||
let invoke_fn = unsafe {
|
||
let symbol: libloading::Symbol<unsafe extern "C" fn(u32, u32, u32, *const u8, usize, *mut u8, *mut usize) -> i32> =
|
||
lib.get(b"nyash_plugin_invoke")
|
||
.map_err(|e| {
|
||
eprintln!("Missing nyash_plugin_invoke: {}", e);
|
||
BidError::InvalidMethod
|
||
})?;
|
||
*symbol // Dereference to get the actual function pointer
|
||
};
|
||
|
||
// Get optional init function and dereference it
|
||
let init_fn = unsafe {
|
||
lib.get::<unsafe extern "C" fn() -> i32>(b"nyash_plugin_init").ok()
|
||
.map(|f| *f)
|
||
};
|
||
|
||
// Call init if available
|
||
if let Some(init) = init_fn {
|
||
let result = unsafe { init() };
|
||
eprintln!("[PluginLoaderV2] nyash_plugin_init rc={} for {}", result, lib_name);
|
||
if result != 0 {
|
||
eprintln!("Plugin init failed with code: {}", result);
|
||
return Err(BidError::PluginError);
|
||
}
|
||
} else {
|
||
eprintln!("[PluginLoaderV2] nyash_plugin_init not found for {} (optional)", lib_name);
|
||
}
|
||
|
||
// Store plugin with Arc-wrapped library
|
||
let lib_arc = Arc::new(lib);
|
||
let plugin = Arc::new(LoadedPluginV2 {
|
||
_lib: lib_arc,
|
||
box_types: lib_def.boxes.clone(),
|
||
init_fn,
|
||
invoke_fn,
|
||
});
|
||
|
||
let mut plugins = self.plugins.write().unwrap();
|
||
plugins.insert(lib_name.to_string(), plugin);
|
||
|
||
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);
|
||
|
||
let config = self.config.as_ref()
|
||
.ok_or(BidError::PluginError)?;
|
||
|
||
eprintln!("🔍 Config loaded successfully");
|
||
|
||
// Find library that provides this box type
|
||
let (lib_name, _lib_def) = config.find_library_for_box(box_type)
|
||
.ok_or_else(|| {
|
||
eprintln!("No plugin provides box type: {}", box_type);
|
||
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
|
||
let plugins = self.plugins.read().unwrap();
|
||
let plugin = plugins.get(lib_name)
|
||
.ok_or_else(|| {
|
||
eprintln!("Plugin not loaded: {}", lib_name);
|
||
BidError::PluginError
|
||
})?;
|
||
|
||
eprintln!("🔍 Plugin loaded successfully");
|
||
|
||
// Get type_id from config - read actual nyash.toml content
|
||
eprintln!("🔍 Reading nyash.toml for type configuration...");
|
||
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) {
|
||
eprintln!("🔍 nyash.toml parsed successfully");
|
||
if let Some(box_config) = config.get_box_config(lib_name, box_type, &toml_value) {
|
||
eprintln!("🔍 Found box config for {} with type_id: {}", box_type, box_config.type_id);
|
||
let fini_id = box_config.methods.get("fini").map(|m| m.method_id);
|
||
(box_config.type_id, fini_id)
|
||
} else {
|
||
eprintln!("No type configuration for {} in {}", box_type, lib_name);
|
||
return Err(BidError::InvalidType);
|
||
}
|
||
} else {
|
||
eprintln!("Failed to parse nyash.toml");
|
||
return Err(BidError::PluginError);
|
||
}
|
||
} else {
|
||
eprintln!("Failed to read nyash.toml");
|
||
return Err(BidError::PluginError);
|
||
};
|
||
|
||
// Call birth constructor (method_id = 0) via TLV encoding
|
||
eprintln!("🔍 Preparing to call birth() with type_id: {}", type_id);
|
||
let mut output_buffer = vec![0u8; 1024]; // 1KB buffer for output
|
||
let mut output_len = output_buffer.len();
|
||
|
||
// Create TLV-encoded empty arguments (version=1, argc=0)
|
||
let tlv_args = crate::runtime::plugin_ffi_common::encode_empty_args();
|
||
eprintln!("🔍 Output buffer allocated, about to call plugin invoke_fn...");
|
||
|
||
let birth_result = unsafe {
|
||
eprintln!("🔍 Calling invoke_fn(type_id={}, method_id=0, instance_id=0, tlv_args={:?}, output_buf, output_size={})", type_id, tlv_args, output_buffer.len());
|
||
(plugin.invoke_fn)(
|
||
type_id, // Box type ID
|
||
0, // method_id for birth
|
||
0, // instance_id = 0 for birth (static call)
|
||
tlv_args.as_ptr(), // TLV-encoded input data
|
||
tlv_args.len(), // input size
|
||
output_buffer.as_mut_ptr(), // output buffer
|
||
&mut output_len, // output buffer size (mutable)
|
||
)
|
||
};
|
||
|
||
eprintln!("🔍 invoke_fn returned with result: {}", birth_result);
|
||
|
||
if birth_result != 0 {
|
||
eprintln!("birth() failed with code: {}", birth_result);
|
||
return Err(BidError::PluginError);
|
||
}
|
||
|
||
// Parse instance_id from output (first 4 bytes as u32)
|
||
let instance_id = if output_len >= 4 {
|
||
u32::from_le_bytes([output_buffer[0], output_buffer[1], output_buffer[2], output_buffer[3]])
|
||
} else {
|
||
eprintln!("birth() returned insufficient data (got {} bytes, need 4)", output_len);
|
||
return Err(BidError::PluginError);
|
||
};
|
||
|
||
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(),
|
||
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),
|
||
}),
|
||
};
|
||
leak_tracker::init();
|
||
leak_tracker::register_plugin(&plugin_box.box_type, instance_id);
|
||
|
||
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
|
||
static GLOBAL_LOADER_V2: Lazy<Arc<RwLock<PluginLoaderV2>>> =
|
||
Lazy::new(|| Arc::new(RwLock::new(PluginLoaderV2::new())));
|
||
|
||
/// Get global v2 loader
|
||
pub fn get_global_loader_v2() -> Arc<RwLock<PluginLoaderV2>> {
|
||
GLOBAL_LOADER_V2.clone()
|
||
}
|
||
|
||
/// Initialize global loader with config
|
||
pub fn init_global_loader_v2(config_path: &str) -> BidResult<()> {
|
||
let loader = get_global_loader_v2();
|
||
let mut loader = loader.write().unwrap();
|
||
loader.load_config(config_path)?;
|
||
drop(loader); // Release write lock
|
||
|
||
// Load all plugins
|
||
let loader = get_global_loader_v2();
|
||
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"))]
|
||
mod stub {
|
||
use crate::bid::{BidResult, BidError};
|
||
use crate::box_trait::NyashBox;
|
||
use once_cell::sync::Lazy;
|
||
use std::sync::{Arc, RwLock};
|
||
|
||
// Stub implementation of PluginBoxV2 for WASM/non-plugin builds
|
||
#[derive(Debug, Clone)]
|
||
pub struct PluginBoxV2 {
|
||
pub box_type: String,
|
||
pub inner: std::sync::Arc<PluginHandleInner>,
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub struct PluginHandleInner {
|
||
pub type_id: u32,
|
||
pub instance_id: u32,
|
||
pub fini_method_id: Option<u32>,
|
||
}
|
||
|
||
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(()) }
|
||
pub fn create_box(&self, _t: &str, _a: &[Box<dyn NyashBox>]) -> BidResult<Box<dyn NyashBox>> {
|
||
Err(BidError::PluginError)
|
||
}
|
||
|
||
pub fn extern_call(
|
||
&self,
|
||
_iface_name: &str,
|
||
_method_name: &str,
|
||
_args: &[Box<dyn NyashBox>],
|
||
) -> BidResult<Option<Box<dyn NyashBox>>> {
|
||
Err(BidError::PluginError)
|
||
}
|
||
|
||
pub fn invoke_instance_method(
|
||
&self,
|
||
_box_type: &str,
|
||
_method_name: &str,
|
||
_instance_id: u32,
|
||
_args: &[Box<dyn NyashBox>],
|
||
) -> BidResult<Option<Box<dyn NyashBox>>> {
|
||
Err(BidError::PluginError)
|
||
}
|
||
}
|
||
|
||
static GLOBAL_LOADER_V2: Lazy<Arc<RwLock<PluginLoaderV2>>> =
|
||
Lazy::new(|| Arc::new(RwLock::new(PluginLoaderV2::new())));
|
||
|
||
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")))]
|
||
pub use enabled::*;
|
||
#[cfg(any(not(feature = "plugins"), target_arch = "wasm32"))]
|
||
pub use stub::*;
|