feat(phase-9.75g-0): Implement BID-FFI Day 6 - Dynamic method discovery system
- Enhanced plugin metadata API with find_method() and get_methods() - Implemented generic plugin method calling system (execute_plugin_method_generic) - Fixed TLV encoding: use Bytes tag for string data in write() - Fixed read() method: provide default size argument when called without args - Replaced hardcoded execute_plugin_file_method with dynamic system - Full end-to-end test successful: FileBox plugin write/read working 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -8,6 +8,7 @@ pub struct LoadedPlugin {
|
||||
pub library: Library,
|
||||
pub handle: PluginHandle,
|
||||
pub type_id: u32,
|
||||
pub plugin_info: NyashPluginInfo, // プラグイン情報を保存
|
||||
}
|
||||
|
||||
impl LoadedPlugin {
|
||||
@ -48,9 +49,46 @@ impl LoadedPlugin {
|
||||
handle.initialize(&host, &mut info)?;
|
||||
let type_id = info.type_id;
|
||||
|
||||
Ok(Self { library, handle, type_id })
|
||||
Ok(Self { library, handle, type_id, plugin_info: info })
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the plugin's Box type name
|
||||
pub fn get_type_name(&self) -> BidResult<&str> {
|
||||
unsafe { self.plugin_info.name() }
|
||||
}
|
||||
|
||||
/// Get all available methods for this plugin
|
||||
pub fn get_methods(&self) -> BidResult<Vec<(u32, String, u32)>> {
|
||||
let mut methods = Vec::new();
|
||||
|
||||
unsafe {
|
||||
let methods_slice = self.plugin_info.methods_slice()?;
|
||||
for method_info in methods_slice {
|
||||
let method_name = method_info.name()?.to_string();
|
||||
methods.push((
|
||||
method_info.method_id,
|
||||
method_name,
|
||||
method_info.signature_hash,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(methods)
|
||||
}
|
||||
|
||||
/// Find a method by name and return its info
|
||||
pub fn find_method(&self, method_name: &str) -> BidResult<Option<(u32, u32)>> {
|
||||
unsafe {
|
||||
let methods_slice = self.plugin_info.methods_slice()?;
|
||||
for method_info in methods_slice {
|
||||
if method_info.name()? == method_name {
|
||||
return Ok(Some((method_info.method_id, method_info.signature_hash)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a minimal host vtable for plugins
|
||||
|
||||
@ -35,6 +35,11 @@ pub struct NyashMethodInfo {
|
||||
pub signature_hash: u32,
|
||||
}
|
||||
|
||||
// SAFETY: The C pointers in NyashMethodInfo are read-only after initialization
|
||||
// and point to valid memory managed by the plugin system
|
||||
unsafe impl Send for NyashMethodInfo {}
|
||||
unsafe impl Sync for NyashMethodInfo {}
|
||||
|
||||
impl NyashMethodInfo {
|
||||
/// Create method info with safe string handling
|
||||
pub fn new(method_id: u32, method_name: &str, signature_hash: u32) -> BidResult<(Self, CString)> {
|
||||
@ -78,6 +83,11 @@ pub struct NyashPluginInfo {
|
||||
pub methods: *const NyashMethodInfo,
|
||||
}
|
||||
|
||||
// SAFETY: The C pointers in NyashPluginInfo are read-only after initialization
|
||||
// and point to valid memory managed by the plugin system
|
||||
unsafe impl Send for NyashPluginInfo {}
|
||||
unsafe impl Sync for NyashPluginInfo {}
|
||||
|
||||
impl NyashPluginInfo {
|
||||
/// Create an empty plugin info
|
||||
pub fn empty() -> Self {
|
||||
|
||||
@ -111,6 +111,48 @@ impl PluginFileBox {
|
||||
pub fn read_bytes(&self, size: usize) -> BidResult<Vec<u8>> { self.inner.read(size) }
|
||||
pub fn write_bytes(&self, data: &[u8]) -> BidResult<i32> { self.inner.write(data) }
|
||||
pub fn close(&self) -> BidResult<()> { self.inner.close() }
|
||||
|
||||
/// 汎用メソッド呼び出し(動的ディスパッチ)
|
||||
pub fn call_method(&self, method_name: &str, args: &[u8]) -> BidResult<Vec<u8>> {
|
||||
eprintln!("🔍 call_method: method_name='{}', args_len={}", method_name, args.len());
|
||||
|
||||
// プラグインからメソッドIDを動的取得
|
||||
match self.inner.plugin.find_method(method_name) {
|
||||
Ok(Some((method_id, signature))) => {
|
||||
eprintln!("🔍 Found method '{}': ID={}, signature=0x{:08X}", method_name, method_id, signature);
|
||||
let mut out = Vec::new();
|
||||
match self.inner.plugin.handle.invoke(
|
||||
self.inner.plugin.type_id,
|
||||
method_id,
|
||||
self.inner.instance_id,
|
||||
args,
|
||||
&mut out
|
||||
) {
|
||||
Ok(()) => {
|
||||
eprintln!("🔍 Plugin invoke succeeded, output_len={}", out.len());
|
||||
Ok(out)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("🔍 Plugin invoke failed: {:?}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
eprintln!("🔍 Method '{}' not found in plugin", method_name);
|
||||
Err(BidError::InvalidArgs) // メソッドが見つからない
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("🔍 Error looking up method '{}': {:?}", method_name, e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// プラグインのメソッド一覧を取得
|
||||
pub fn get_available_methods(&self) -> BidResult<Vec<(u32, String, u32)>> {
|
||||
self.inner.plugin.get_methods()
|
||||
}
|
||||
}
|
||||
|
||||
impl BoxCore for PluginFileBox {
|
||||
|
||||
@ -107,72 +107,156 @@ impl NyashInterpreter {
|
||||
}
|
||||
}
|
||||
|
||||
/// PluginFileBoxのメソッド呼び出しを実行 (BID-FFI system)
|
||||
/// Handles plugin-backed file I/O operations via FFI interface
|
||||
pub(in crate::interpreter) fn execute_plugin_file_method(&mut self, plugin_file_box: &PluginFileBox, method: &str, arguments: &[ASTNode])
|
||||
/// 汎用プラグインメソッド呼び出し実行 (BID-FFI system)
|
||||
/// Handles generic plugin method calls via dynamic method discovery
|
||||
pub(in crate::interpreter) fn execute_plugin_method_generic(&mut self, plugin_box: &PluginFileBox, method: &str, arguments: &[ASTNode])
|
||||
-> Result<Box<dyn NyashBox>, RuntimeError> {
|
||||
match method {
|
||||
"read" => {
|
||||
if !arguments.is_empty() {
|
||||
return Err(RuntimeError::InvalidOperation {
|
||||
message: format!("read() expects 0 arguments, got {}", arguments.len()),
|
||||
});
|
||||
}
|
||||
// Read the entire file content
|
||||
match plugin_file_box.read_bytes(8192) { // Read up to 8KB
|
||||
Ok(bytes) => {
|
||||
let content = String::from_utf8_lossy(&bytes).to_string();
|
||||
Ok(Box::new(StringBox::new(content)))
|
||||
}
|
||||
Err(e) => Err(RuntimeError::InvalidOperation {
|
||||
message: format!("Plugin read failed: {:?}", e),
|
||||
})
|
||||
|
||||
eprintln!("🔍 execute_plugin_method_generic: method='{}', args_count={}", method, arguments.len());
|
||||
|
||||
// まず利用可能なメソッドを確認
|
||||
match plugin_box.get_available_methods() {
|
||||
Ok(methods) => {
|
||||
eprintln!("🔍 Available plugin methods:");
|
||||
for (id, name, sig) in &methods {
|
||||
eprintln!("🔍 - {} [ID: {}, Sig: 0x{:08X}]", name, id, sig);
|
||||
}
|
||||
}
|
||||
"write" => {
|
||||
if arguments.len() != 1 {
|
||||
return Err(RuntimeError::InvalidOperation {
|
||||
message: format!("write() expects 1 argument, got {}", arguments.len()),
|
||||
});
|
||||
}
|
||||
let content = self.execute_expression(&arguments[0])?;
|
||||
let text = content.to_string_box().value;
|
||||
match plugin_file_box.write_bytes(text.as_bytes()) {
|
||||
Ok(bytes_written) => Ok(Box::new(StringBox::new("OK".to_string()))),
|
||||
Err(e) => Err(RuntimeError::InvalidOperation {
|
||||
message: format!("Plugin write failed: {:?}", e),
|
||||
})
|
||||
}
|
||||
Err(e) => eprintln!("⚠️ Failed to get plugin methods: {:?}", e),
|
||||
}
|
||||
|
||||
// 引数をTLVエンコード(メソッド名も渡す)
|
||||
let encoded_args = self.encode_arguments_to_tlv(arguments, method)?;
|
||||
eprintln!("🔍 Encoded args length: {} bytes", encoded_args.len());
|
||||
|
||||
// プラグインのメソッドを動的呼び出し
|
||||
match plugin_box.call_method(method, &encoded_args) {
|
||||
Ok(response_bytes) => {
|
||||
eprintln!("🔍 Plugin method '{}' succeeded, response length: {} bytes", method, response_bytes.len());
|
||||
// レスポンスをデコードしてNyashBoxに変換
|
||||
self.decode_tlv_to_nyash_box(&response_bytes, method)
|
||||
}
|
||||
"exists" => {
|
||||
if !arguments.is_empty() {
|
||||
return Err(RuntimeError::InvalidOperation {
|
||||
message: format!("exists() expects 0 arguments, got {}", arguments.len()),
|
||||
});
|
||||
}
|
||||
// Plugin FileBox doesn't have exists() method in current implementation
|
||||
// Return true if we can read (approximate)
|
||||
match plugin_file_box.read_bytes(1) {
|
||||
Ok(_) => Ok(Box::new(crate::box_trait::BoolBox::new(true))),
|
||||
Err(_) => Ok(Box::new(crate::box_trait::BoolBox::new(false))),
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("🔍 Plugin method '{}' failed with error: {:?}", method, e);
|
||||
Err(RuntimeError::InvalidOperation {
|
||||
message: format!("Plugin method '{}' failed: {:?}", method, e),
|
||||
})
|
||||
}
|
||||
"close" => {
|
||||
if !arguments.is_empty() {
|
||||
return Err(RuntimeError::InvalidOperation {
|
||||
message: format!("close() expects 0 arguments, got {}", arguments.len()),
|
||||
});
|
||||
}
|
||||
match plugin_file_box.close() {
|
||||
Ok(()) => Ok(Box::new(StringBox::new("OK".to_string()))),
|
||||
Err(e) => Err(RuntimeError::InvalidOperation {
|
||||
message: format!("Plugin close failed: {:?}", e),
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => Err(RuntimeError::InvalidOperation {
|
||||
message: format!("Unknown method '{}' for PluginFileBox", method),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 引数をTLVエンコード(メソッドに応じて特殊処理)
|
||||
fn encode_arguments_to_tlv(&mut self, arguments: &[ASTNode], method_name: &str) -> Result<Vec<u8>, RuntimeError> {
|
||||
use crate::bid::tlv::TlvEncoder;
|
||||
|
||||
let mut encoder = TlvEncoder::new();
|
||||
|
||||
// 特殊ケース: readメソッドは引数がなくても、サイズ引数が必要
|
||||
if method_name == "read" && arguments.is_empty() {
|
||||
// デフォルトで8192バイト読み取り
|
||||
encoder.encode_i32(8192)
|
||||
.map_err(|e| RuntimeError::InvalidOperation {
|
||||
message: format!("TLV i32 encoding failed: {:?}", e),
|
||||
})?;
|
||||
} else {
|
||||
// 通常の引数エンコード
|
||||
for arg in arguments {
|
||||
let value = self.execute_expression(arg)?;
|
||||
|
||||
// 型に応じてエンコード
|
||||
if let Some(str_box) = value.as_any().downcast_ref::<StringBox>() {
|
||||
// 🔍 writeメソッドなど、文字列データはBytesとしてエンコード
|
||||
// プラグインは通常、文字列データをBytesタグ(7)で期待する
|
||||
encoder.encode_bytes(str_box.value.as_bytes())
|
||||
.map_err(|e| RuntimeError::InvalidOperation {
|
||||
message: format!("TLV bytes encoding failed: {:?}", e),
|
||||
})?;
|
||||
} else if let Some(int_box) = value.as_any().downcast_ref::<crate::box_trait::IntegerBox>() {
|
||||
encoder.encode_i32(int_box.value as i32)
|
||||
.map_err(|e| RuntimeError::InvalidOperation {
|
||||
message: format!("TLV integer encoding failed: {:?}", e),
|
||||
})?;
|
||||
} else if let Some(bool_box) = value.as_any().downcast_ref::<crate::box_trait::BoolBox>() {
|
||||
encoder.encode_bool(bool_box.value)
|
||||
.map_err(|e| RuntimeError::InvalidOperation {
|
||||
message: format!("TLV bool encoding failed: {:?}", e),
|
||||
})?;
|
||||
} else {
|
||||
// デフォルト: バイトデータとして扱う
|
||||
let str_val = value.to_string_box().value;
|
||||
encoder.encode_bytes(str_val.as_bytes())
|
||||
.map_err(|e| RuntimeError::InvalidOperation {
|
||||
message: format!("TLV default bytes encoding failed: {:?}", e),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(encoder.finish())
|
||||
}
|
||||
|
||||
/// TLVレスポンスをNyashBoxに変換
|
||||
fn decode_tlv_to_nyash_box(&self, response_bytes: &[u8], method_name: &str) -> Result<Box<dyn NyashBox>, RuntimeError> {
|
||||
use crate::bid::tlv::TlvDecoder;
|
||||
use crate::bid::types::BidTag;
|
||||
|
||||
if response_bytes.is_empty() {
|
||||
return Ok(Box::new(StringBox::new("".to_string())));
|
||||
}
|
||||
|
||||
let mut decoder = TlvDecoder::new(response_bytes)
|
||||
.map_err(|e| RuntimeError::InvalidOperation {
|
||||
message: format!("TLV decoder creation failed: {:?}", e),
|
||||
})?;
|
||||
|
||||
if let Some((tag, payload)) = decoder.decode_next()
|
||||
.map_err(|e| RuntimeError::InvalidOperation {
|
||||
message: format!("TLV decoding failed: {:?}", e),
|
||||
})? {
|
||||
|
||||
match tag {
|
||||
BidTag::String => {
|
||||
let text = String::from_utf8_lossy(payload).to_string();
|
||||
Ok(Box::new(StringBox::new(text)))
|
||||
}
|
||||
BidTag::Bytes => {
|
||||
// ファイル読み取り等のバイトデータは文字列として返す
|
||||
let text = String::from_utf8_lossy(payload).to_string();
|
||||
Ok(Box::new(StringBox::new(text)))
|
||||
}
|
||||
BidTag::I32 => {
|
||||
let value = TlvDecoder::decode_i32(payload)
|
||||
.map_err(|e| RuntimeError::InvalidOperation {
|
||||
message: format!("TLV i32 decoding failed: {:?}", e),
|
||||
})?;
|
||||
Ok(Box::new(crate::box_trait::IntegerBox::new(value as i64)))
|
||||
}
|
||||
BidTag::Bool => {
|
||||
let value = TlvDecoder::decode_bool(payload)
|
||||
.map_err(|e| RuntimeError::InvalidOperation {
|
||||
message: format!("TLV bool decoding failed: {:?}", e),
|
||||
})?;
|
||||
Ok(Box::new(crate::box_trait::BoolBox::new(value)))
|
||||
}
|
||||
BidTag::Void => {
|
||||
Ok(Box::new(StringBox::new("OK".to_string())))
|
||||
}
|
||||
_ => {
|
||||
Ok(Box::new(StringBox::new(format!("Unknown TLV tag: {:?}", tag))))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(Box::new(StringBox::new("".to_string())))
|
||||
}
|
||||
}
|
||||
|
||||
/// PluginFileBoxのメソッド呼び出しを実行 (BID-FFI system) - LEGACY HARDCODED VERSION
|
||||
/// Handles plugin-backed file I/O operations via FFI interface
|
||||
/// 🚨 DEPRECATED: This method has hardcoded method names and violates BID-FFI principles
|
||||
/// Use execute_plugin_method_generic instead for true dynamic method calling
|
||||
pub(in crate::interpreter) fn execute_plugin_file_method(&mut self, plugin_file_box: &PluginFileBox, method: &str, arguments: &[ASTNode])
|
||||
-> Result<Box<dyn NyashBox>, RuntimeError> {
|
||||
// 🎯 新しい汎用システムにリダイレクト
|
||||
self.execute_plugin_method_generic(plugin_file_box, method, arguments)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user