feat(phase-9.75g-0): Implement BID-FFI Day 2 - Metadata API and plugin lifecycle

BID-1 Implementation Progress (Day 2 Complete\! 🎉):

Metadata API Implementation:
- Add NyashHostVtable for host function integration
  - Memory allocation/free functions
  - FutureBox wake support
  - Logging capability
- Implement plugin metadata structures
  - NyashPluginInfo: type_id, type_name, methods
  - NyashMethodInfo: method_id, method_name, signature_hash
  - PluginMetadata: Rust-side holder with lifecycle state

Plugin API Definitions:
- Define C FFI function signatures
  - nyash_plugin_abi(): Get ABI version
  - nyash_plugin_init(): Initialize with host vtable
  - nyash_plugin_invoke(): Unified method invocation
  - nyash_plugin_shutdown(): Cleanup resources
- Implement PluginHandle for loaded plugin management
- Add two-call pattern support for dynamic result sizes

Test Coverage: 7/7 
- bid::types::tests (2 tests)
- bid::tlv::tests (2 tests)
- bid::metadata::tests (2 tests)
- bid::plugin_api::tests (1 test)

Everything is Box philosophy progressing with practical FFI design\!

Next: Day 3 - Existing Box integration (StringBox/IntegerBox/FutureBox bridge)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Moe Charm
2025-08-17 19:01:16 +09:00
parent f5ee08f375
commit ec99a97957
3 changed files with 506 additions and 0 deletions

248
src/bid/metadata.rs Normal file
View File

@ -0,0 +1,248 @@
use super::{BidError, BidResult};
use std::os::raw::{c_char, c_void};
use std::ffi::{CStr, CString};
/// Host function table provided to plugins
#[repr(C)]
pub struct NyashHostVtable {
/// Allocate memory
pub alloc: Option<extern "C" fn(size: usize) -> *mut c_void>,
/// Free memory
pub free: Option<extern "C" fn(ptr: *mut c_void)>,
/// Wake a future (for FutureBox support)
pub wake: Option<extern "C" fn(future_id: u32)>,
/// Log a message
pub log: Option<extern "C" fn(msg: *const c_char)>,
}
impl NyashHostVtable {
/// Create an empty vtable
pub fn empty() -> Self {
Self {
alloc: None,
free: None,
wake: None,
log: None,
}
}
/// Check if all required functions are present
pub fn is_complete(&self) -> bool {
self.alloc.is_some() &&
self.free.is_some() &&
self.log.is_some()
// wake is optional for async support
}
}
/// Method information
#[repr(C)]
pub struct NyashMethodInfo {
/// Method ID (unique within the Box type)
pub method_id: u32,
/// Method name (null-terminated C string)
pub method_name: *const c_char,
/// Type signature hash for validation
pub signature_hash: u32,
}
impl NyashMethodInfo {
/// Create method info with safe string handling
pub fn new(method_id: u32, method_name: &str, signature_hash: u32) -> BidResult<(Self, CString)> {
let c_name = CString::new(method_name)
.map_err(|_| BidError::InvalidUtf8)?;
let info = Self {
method_id,
method_name: c_name.as_ptr(),
signature_hash,
};
Ok((info, c_name))
}
/// Get method name as string (unsafe: requires valid pointer)
pub unsafe fn name(&self) -> BidResult<&str> {
if self.method_name.is_null() {
return Err(BidError::InvalidArgs);
}
CStr::from_ptr(self.method_name)
.to_str()
.map_err(|_| BidError::InvalidUtf8)
}
}
/// Plugin information
#[repr(C)]
pub struct NyashPluginInfo {
/// Box type ID (e.g., 6 for FileBox)
pub type_id: u32,
/// Type name (null-terminated C string)
pub type_name: *const c_char,
/// Number of methods
pub method_count: u32,
/// Method information array
pub methods: *const NyashMethodInfo,
}
impl NyashPluginInfo {
/// Create an empty plugin info
pub fn empty() -> Self {
Self {
type_id: 0,
type_name: std::ptr::null(),
method_count: 0,
methods: std::ptr::null(),
}
}
/// Get type name as string (unsafe: requires valid pointer)
pub unsafe fn name(&self) -> BidResult<&str> {
if self.type_name.is_null() {
return Err(BidError::InvalidArgs);
}
CStr::from_ptr(self.type_name)
.to_str()
.map_err(|_| BidError::InvalidUtf8)
}
/// Get methods as slice (unsafe: requires valid pointer and count)
pub unsafe fn methods_slice(&self) -> BidResult<&[NyashMethodInfo]> {
if self.methods.is_null() || self.method_count == 0 {
return Ok(&[]);
}
Ok(std::slice::from_raw_parts(
self.methods,
self.method_count as usize,
))
}
}
/// Plugin lifecycle state
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluginState {
/// Plugin loaded but not initialized
Loaded,
/// Plugin initialized and ready
Ready,
/// Plugin shutting down
ShuttingDown,
/// Plugin unloaded
Unloaded,
}
/// Plugin metadata holder for Rust side
pub struct PluginMetadata {
pub info: NyashPluginInfo,
pub state: PluginState,
// Keep CStrings alive for C interop
type_name_holder: Option<CString>,
method_holders: Vec<(NyashMethodInfo, CString)>,
}
impl PluginMetadata {
/// Create metadata from plugin info
pub fn new(
type_id: u32,
type_name: &str,
methods: Vec<(u32, &str, u32)>, // (id, name, hash)
) -> BidResult<Self> {
// Create type name
let type_name_holder = CString::new(type_name)
.map_err(|_| BidError::InvalidUtf8)?;
// Create method infos
let mut method_holders = Vec::new();
for (id, name, hash) in methods {
let (info, holder) = NyashMethodInfo::new(id, name, hash)?;
method_holders.push((info, holder));
}
// Build plugin info
let info = NyashPluginInfo {
type_id,
type_name: type_name_holder.as_ptr(),
method_count: method_holders.len() as u32,
methods: if method_holders.is_empty() {
std::ptr::null()
} else {
method_holders.as_ptr() as *const NyashMethodInfo
},
};
Ok(Self {
info,
state: PluginState::Loaded,
type_name_holder: Some(type_name_holder),
method_holders,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_metadata_creation() {
let methods = vec![
(1, "open", 0x12345678),
(2, "read", 0x87654321),
(3, "write", 0x11223344),
(4, "close", 0xABCDEF00),
];
let metadata = PluginMetadata::new(6, "FileBox", methods).unwrap();
assert_eq!(metadata.info.type_id, 6);
assert_eq!(metadata.info.method_count, 4);
assert_eq!(metadata.state, PluginState::Loaded);
unsafe {
assert_eq!(metadata.info.name().unwrap(), "FileBox");
let methods = metadata.info.methods_slice().unwrap();
assert_eq!(methods.len(), 4);
assert_eq!(methods[0].method_id, 1);
assert_eq!(methods[0].name().unwrap(), "open");
}
}
#[test]
fn test_host_vtable() {
let vtable = NyashHostVtable::empty();
assert!(!vtable.is_complete());
// In real usage, would set actual function pointers
let vtable = NyashHostVtable {
alloc: Some(dummy_alloc),
free: Some(dummy_free),
wake: None,
log: Some(dummy_log),
};
assert!(vtable.is_complete());
}
extern "C" fn dummy_alloc(_size: usize) -> *mut c_void {
std::ptr::null_mut()
}
extern "C" fn dummy_free(_ptr: *mut c_void) {}
extern "C" fn dummy_log(_msg: *const c_char) {}
}

View File

@ -4,10 +4,14 @@
pub mod types;
pub mod tlv;
pub mod error;
pub mod metadata;
pub mod plugin_api;
pub use types::*;
pub use tlv::*;
pub use error::*;
pub use metadata::*;
pub use plugin_api::*;
/// BID-1 version constant
pub const BID_VERSION: u16 = 1;

254
src/bid/plugin_api.rs Normal file
View File

@ -0,0 +1,254 @@
use super::{BidError, BidResult, NyashHostVtable, NyashPluginInfo};
use std::os::raw::c_char;
/// Plugin API function signatures for C FFI
///
/// These are the function signatures that plugins must implement.
/// They are defined as Rust types for type safety when loading plugins.
/// Get plugin ABI version
/// Returns: BID version number (1 for BID-1)
pub type PluginAbiFn = unsafe extern "C" fn() -> u32;
/// Initialize plugin
/// Parameters:
/// - host: Host function table for plugin to use
/// - info: Plugin information to be filled by plugin
/// Returns: 0 on success, negative error code on failure
pub type PluginInitFn = unsafe extern "C" fn(
host: *const NyashHostVtable,
info: *mut NyashPluginInfo,
) -> i32;
/// Invoke a plugin method
/// Parameters:
/// - type_id: Box type ID
/// - method_id: Method ID
/// - instance_id: Instance ID (for instance methods)
/// - args: BID-1 TLV encoded arguments
/// - args_len: Length of arguments
/// - result: Buffer for BID-1 TLV encoded result
/// - result_len: Input: buffer size, Output: actual result size
/// Returns: 0 on success, negative error code on failure
pub type PluginInvokeFn = unsafe extern "C" fn(
type_id: u32,
method_id: u32,
instance_id: u32,
args: *const u8,
args_len: usize,
result: *mut u8,
result_len: *mut usize,
) -> i32;
/// Shutdown plugin and cleanup resources
pub type PluginShutdownFn = unsafe extern "C" fn();
/// Plugin API entry points
pub const PLUGIN_ABI_SYMBOL: &str = "nyash_plugin_abi";
pub const PLUGIN_INIT_SYMBOL: &str = "nyash_plugin_init";
pub const PLUGIN_INVOKE_SYMBOL: &str = "nyash_plugin_invoke";
pub const PLUGIN_SHUTDOWN_SYMBOL: &str = "nyash_plugin_shutdown";
/// Plugin handle containing loaded functions
pub struct PluginHandle {
pub abi: PluginAbiFn,
pub init: PluginInitFn,
pub invoke: PluginInvokeFn,
pub shutdown: PluginShutdownFn,
}
impl PluginHandle {
/// Validate ABI version
pub fn check_abi(&self) -> BidResult<()> {
let version = unsafe { (self.abi)() };
if version != super::BID_VERSION as u32 {
return Err(BidError::VersionMismatch);
}
Ok(())
}
/// Initialize plugin with host vtable
pub fn initialize(
&self,
host: &NyashHostVtable,
info: &mut NyashPluginInfo,
) -> BidResult<()> {
let result = unsafe {
(self.init)(
host as *const NyashHostVtable,
info as *mut NyashPluginInfo,
)
};
if result != 0 {
Err(BidError::from_raw(result))
} else {
Ok(())
}
}
/// Invoke a plugin method
pub fn invoke(
&self,
type_id: u32,
method_id: u32,
instance_id: u32,
args: &[u8],
result_buffer: &mut Vec<u8>,
) -> BidResult<()> {
// First call: get required size
let mut required_size = 0;
let result = unsafe {
(self.invoke)(
type_id,
method_id,
instance_id,
args.as_ptr(),
args.len(),
std::ptr::null_mut(),
&mut required_size,
)
};
// Check for error (except buffer too small)
if result != 0 && result != -1 {
return Err(BidError::from_raw(result));
}
// Allocate buffer if needed
if required_size > 0 {
result_buffer.resize(required_size, 0);
// Second call: get actual data
let mut actual_size = required_size;
let result = unsafe {
(self.invoke)(
type_id,
method_id,
instance_id,
args.as_ptr(),
args.len(),
result_buffer.as_mut_ptr(),
&mut actual_size,
)
};
if result != 0 {
return Err(BidError::from_raw(result));
}
// Trim to actual size
result_buffer.truncate(actual_size);
}
Ok(())
}
/// Shutdown plugin
pub fn shutdown(&self) {
unsafe {
(self.shutdown)();
}
}
}
/// Helper for creating host vtable with Rust closures
pub struct HostVtableBuilder {
vtable: NyashHostVtable,
}
impl HostVtableBuilder {
pub fn new() -> Self {
Self {
vtable: NyashHostVtable::empty(),
}
}
pub fn with_alloc<F>(mut self, f: F) -> Self
where
F: Fn(usize) -> *mut std::os::raw::c_void + 'static,
{
// Note: In real implementation, would need to store the closure
// and create a proper extern "C" function. This is simplified.
self
}
pub fn with_free<F>(mut self, f: F) -> Self
where
F: Fn(*mut std::os::raw::c_void) + 'static,
{
self
}
pub fn with_log<F>(mut self, f: F) -> Self
where
F: Fn(&str) + 'static,
{
self
}
pub fn build(self) -> NyashHostVtable {
self.vtable
}
}
#[cfg(test)]
mod tests {
use super::*;
// Mock plugin functions for testing
unsafe extern "C" fn mock_abi() -> u32 {
1 // BID-1
}
unsafe extern "C" fn mock_init(
_host: *const NyashHostVtable,
info: *mut NyashPluginInfo,
) -> i32 {
if !info.is_null() {
(*info).type_id = 99;
(*info).method_count = 0;
}
0
}
unsafe extern "C" fn mock_invoke(
_type_id: u32,
_method_id: u32,
_instance_id: u32,
_args: *const u8,
_args_len: usize,
_result: *mut u8,
result_len: *mut usize,
) -> i32 {
if !result_len.is_null() {
*result_len = 0;
}
0
}
unsafe extern "C" fn mock_shutdown() {}
#[test]
fn test_plugin_handle() {
let handle = PluginHandle {
abi: mock_abi,
init: mock_init,
invoke: mock_invoke,
shutdown: mock_shutdown,
};
// Check ABI
assert!(handle.check_abi().is_ok());
// Initialize
let host = NyashHostVtable::empty();
let mut info = NyashPluginInfo::empty();
assert!(handle.initialize(&host, &mut info).is_ok());
assert_eq!(info.type_id, 99);
// Invoke
let mut result = Vec::new();
assert!(handle.invoke(99, 1, 0, &[], &mut result).is_ok());
}
}