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:
248
src/bid/metadata.rs
Normal file
248
src/bid/metadata.rs
Normal 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) {}
|
||||
}
|
||||
@ -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
254
src/bid/plugin_api.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user