feat(phase-9.75g-0): Implement BID-FFI Day 5 - FileBox plugin library and transparent switching (90% complete)

Day 5 achievements:
- Created independent FileBox plugin crate with C FFI exports
- Integrated plugin loading into Nyash interpreter startup
- Implemented transparent builtin/plugin Box switching via nyash.toml
- Successfully loaded plugin library (385KB .so) at runtime
- Confirmed PluginBox proxy creation for FileBox instances

Architecture changes:
- Added plugins/ directory with .gitignore for build artifacts
- Modified runner.rs to load plugins from nyash.toml on startup
- Updated objects.rs to use BoxFactoryRegistry for FileBox creation
- Fixed bid module visibility between lib.rs and main.rs

Remaining work (10%):
- Complete PluginBox proxy method implementations (toString, etc.)
- Test actual file operations through plugin interface
- Finalize error handling and edge cases

Build status: All tests passing, plugin loading confirmed
This commit is contained in:
Moe Charm
2025-08-18 00:33:01 +09:00
parent a0e3c0dc75
commit 75868a5a96
11 changed files with 688 additions and 46 deletions

View File

@ -0,0 +1,43 @@
//! BID-FFI型定義
//!
//! C FFI境界で使用される型定義
use std::os::raw::{c_char, c_void};
/// BID-1エラーコード
pub const BID_SUCCESS: i32 = 0;
pub const BID_INVALID_ARGUMENT: i32 = -1;
pub const BID_ENCODING_ERROR: i32 = -2;
pub const BID_OUT_OF_MEMORY: i32 = -3;
pub const BID_UNKNOWN_METHOD: i32 = -4;
pub const BID_INVALID_HANDLE: i32 = -5;
pub const BID_IO_ERROR: i32 = -6;
/// Box型IDBID-1仕様
pub const BOX_TYPE_STRING: u32 = 1;
pub const BOX_TYPE_INTEGER: u32 = 2;
pub const BOX_TYPE_BOOL: u32 = 3;
pub const BOX_TYPE_NULL: u32 = 4;
pub const BOX_TYPE_ARRAY: u32 = 5;
pub const BOX_TYPE_MAP: u32 = 6;
pub const BOX_TYPE_FUTURE: u32 = 7;
pub const BOX_TYPE_FILEBOX: u32 = 8;
/// プラグイン情報構造体
#[repr(C)]
pub struct NyashPluginInfo {
pub name: *const c_char,
pub version: *const c_char,
pub abi_version: *const c_char,
pub provides_count: u32,
pub provides: *const *const c_char,
}
/// ホスト機能テーブル
#[repr(C)]
pub struct HostVtable {
pub alloc: extern "C" fn(size: usize) -> *mut c_void,
pub free: extern "C" fn(ptr: *mut c_void),
pub wake: extern "C" fn(future_handle: u64),
pub log: extern "C" fn(level: u32, message: *const c_char),
}

View File

@ -0,0 +1,173 @@
//! FileBox実装
//!
//! ファイル操作を提供するBox実装
use std::fs::File;
use std::io::{Read, Write, Seek, SeekFrom};
use std::collections::HashMap;
use std::path::PathBuf;
/// FileBoxインスタンス
pub struct FileBoxInstance {
path: Option<PathBuf>,
file: Option<File>,
content: String,
}
impl FileBoxInstance {
/// 新しいFileBoxインスタンス作成
pub fn new() -> Self {
Self {
path: None,
file: None,
content: String::new(),
}
}
/// ファイルオープン
pub fn open(&mut self, path: &str) -> Result<(), std::io::Error> {
let path_buf = PathBuf::from(path);
// ファイルを読み書きモードでオープン
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&path_buf)?;
// 既存内容を読み込み
let mut content = String::new();
if let Err(_) = file.read_to_string(&mut content) {
// 読み込めない場合は空文字列
content.clear();
}
// ファイルポインタを先頭に戻す
file.seek(SeekFrom::Start(0))?;
self.path = Some(path_buf);
self.file = Some(file);
self.content = content;
Ok(())
}
/// ファイル読み取り
pub fn read(&mut self) -> Result<String, std::io::Error> {
if let Some(ref mut file) = self.file {
let mut content = String::new();
file.seek(SeekFrom::Start(0))?;
file.read_to_string(&mut content)?;
self.content = content.clone();
Ok(content)
} else {
Ok(self.content.clone())
}
}
/// ファイル書き込み
pub fn write(&mut self, data: &str) -> Result<(), std::io::Error> {
self.content = data.to_string();
if let Some(ref mut file) = self.file {
file.seek(SeekFrom::Start(0))?;
file.set_len(0)?; // ファイル内容をクリア
file.write_all(data.as_bytes())?;
file.flush()?;
}
Ok(())
}
/// ファイルパス取得
pub fn path(&self) -> Option<&str> {
self.path.as_ref().and_then(|p| p.to_str())
}
/// 内容取得
pub fn content(&self) -> &str {
&self.content
}
}
/// FileBoxレジストリ
pub struct FileBoxRegistry {
instances: HashMap<u32, FileBoxInstance>,
}
impl FileBoxRegistry {
pub fn new() -> Self {
Self {
instances: HashMap::new(),
}
}
pub fn register(&mut self, handle: u32, instance: FileBoxInstance) {
self.instances.insert(handle, instance);
}
pub fn get(&self, handle: u32) -> Option<&FileBoxInstance> {
self.instances.get(&handle)
}
pub fn get_mut(&mut self, handle: u32) -> Option<&mut FileBoxInstance> {
self.instances.get_mut(&handle)
}
pub fn remove(&mut self, handle: u32) -> Option<FileBoxInstance> {
self.instances.remove(&handle)
}
pub fn count(&self) -> usize {
self.instances.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_filebox_basic_operations() {
let mut filebox = FileBoxInstance::new();
// テスト用ファイル
let test_file = "test_plugin_file.txt";
// ファイルオープン
assert!(filebox.open(test_file).is_ok());
assert_eq!(filebox.path(), Some(test_file));
// 書き込み
assert!(filebox.write("Hello from plugin!").is_ok());
assert_eq!(filebox.content(), "Hello from plugin!");
// 読み込み
let content = filebox.read().unwrap();
assert_eq!(content, "Hello from plugin!");
// クリーンアップ
let _ = fs::remove_file(test_file);
}
#[test]
fn test_filebox_registry() {
let mut registry = FileBoxRegistry::new();
let filebox1 = FileBoxInstance::new();
let filebox2 = FileBoxInstance::new();
registry.register(1, filebox1);
registry.register(2, filebox2);
assert_eq!(registry.count(), 2);
assert!(registry.get(1).is_some());
assert!(registry.get(2).is_some());
assert!(registry.get(3).is_none());
registry.remove(1);
assert_eq!(registry.count(), 1);
assert!(registry.get(1).is_none());
}
}

View File

@ -0,0 +1,233 @@
//! Nyash FileBox Plugin - BID-FFI Reference Implementation
//!
//! ファイル操作を提供するプラグインの実装例
//! Everything is Box哲学に基づく透過的置き換え
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write, Seek, SeekFrom};
use std::sync::{Arc, Mutex};
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_void};
use once_cell::sync::Lazy;
mod ffi_types;
use ffi_types::*;
mod filebox;
use filebox::*;
/// グローバルファイルハンドルレジストリ
static FILE_REGISTRY: Lazy<Arc<Mutex<FileBoxRegistry>>> =
Lazy::new(|| Arc::new(Mutex::new(FileBoxRegistry::new())));
/// プラグインハンドルカウンター
static mut HANDLE_COUNTER: u32 = 1;
/// ユニークハンドル生成
fn next_handle() -> u32 {
unsafe {
let handle = HANDLE_COUNTER;
HANDLE_COUNTER += 1;
handle
}
}
/// BID-FFIエントリーポイント
///
/// すべてのプラグイン操作はこの関数を通して実行される
#[no_mangle]
pub extern "C" fn nyash_plugin_invoke(
method: *const c_char,
handle: u64,
input_data: *const u8,
input_len: usize,
output_data: *mut *mut u8,
output_len: *mut usize,
) -> i32 {
// 基本的な引数チェック
if method.is_null() || output_data.is_null() || output_len.is_null() {
return -1; // INVALID_ARGUMENT
}
// メソッド名解析
let method_name = unsafe {
match CStr::from_ptr(method).to_str() {
Ok(s) => s,
Err(_) => return -2, // ENCODING_ERROR
}
};
// ハンドル分解 (BID-1仕様)
let type_id = (handle >> 32) as u32;
let instance_id = (handle & 0xFFFFFFFF) as u32;
// メソッド呼び出し
match invoke_method(method_name, type_id, instance_id, input_data, input_len) {
Ok(result) => {
// 結果をC側に返す
if let Ok(c_result) = CString::new(result) {
let result_bytes = c_result.into_bytes_with_nul();
let len = result_bytes.len();
// メモリ確保C側でfreeする必要がある
let ptr = unsafe { libc::malloc(len) as *mut u8 };
if ptr.is_null() {
return -3; // OUT_OF_MEMORY
}
unsafe {
std::ptr::copy_nonoverlapping(result_bytes.as_ptr(), ptr, len);
*output_data = ptr;
*output_len = len;
}
0 // SUCCESS
} else {
-2 // ENCODING_ERROR
}
}
Err(code) => code,
}
}
/// メソッド呼び出し実装
fn invoke_method(
method: &str,
type_id: u32,
instance_id: u32,
input_data: *const u8,
input_len: usize,
) -> Result<String, i32> {
match method {
"new" => handle_new(input_data, input_len),
"open" => handle_open(instance_id, input_data, input_len),
"read" => handle_read(instance_id, input_data, input_len),
"write" => handle_write(instance_id, input_data, input_len),
"close" => handle_close(instance_id),
"toString" => handle_to_string(instance_id),
_ => Err(-4), // UNKNOWN_METHOD
}
}
/// FileBox::new() - 新しいFileBoxインスタンス作成
fn handle_new(input_data: *const u8, input_len: usize) -> Result<String, i32> {
let handle = next_handle();
let filebox = FileBoxInstance::new();
{
let mut registry = FILE_REGISTRY.lock().unwrap();
registry.register(handle, filebox);
}
// BID-1ハンドル返却 (FileBox type_id = 8)
let bid_handle = ((8u64) << 32) | (handle as u64);
Ok(bid_handle.to_string())
}
/// FileBox::open(path) - ファイルオープン
fn handle_open(instance_id: u32, input_data: *const u8, input_len: usize) -> Result<String, i32> {
// TLV解析簡易版
let path = parse_string_from_tlv(input_data, input_len)?;
let mut registry = FILE_REGISTRY.lock().unwrap();
match registry.get_mut(instance_id) {
Some(filebox) => {
match filebox.open(&path) {
Ok(()) => Ok("true".to_string()),
Err(_) => Ok("false".to_string()),
}
}
None => Err(-5), // INVALID_HANDLE
}
}
/// FileBox::read() - ファイル読み取り
fn handle_read(instance_id: u32, _input_data: *const u8, _input_len: usize) -> Result<String, i32> {
let mut registry = FILE_REGISTRY.lock().unwrap();
match registry.get_mut(instance_id) {
Some(filebox) => {
match filebox.read() {
Ok(content) => Ok(content),
Err(_) => Ok("".to_string()),
}
}
None => Err(-5), // INVALID_HANDLE
}
}
/// FileBox::write(data) - ファイル書き込み
fn handle_write(instance_id: u32, input_data: *const u8, input_len: usize) -> Result<String, i32> {
let data = parse_string_from_tlv(input_data, input_len)?;
let mut registry = FILE_REGISTRY.lock().unwrap();
match registry.get_mut(instance_id) {
Some(filebox) => {
match filebox.write(&data) {
Ok(()) => Ok("true".to_string()),
Err(_) => Ok("false".to_string()),
}
}
None => Err(-5), // INVALID_HANDLE
}
}
/// FileBox::close() - ファイルクローズ
fn handle_close(instance_id: u32) -> Result<String, i32> {
let mut registry = FILE_REGISTRY.lock().unwrap();
match registry.remove(instance_id) {
Some(_) => Ok("true".to_string()),
None => Err(-5), // INVALID_HANDLE
}
}
/// FileBox::toString() - 文字列表現
fn handle_to_string(instance_id: u32) -> Result<String, i32> {
let registry = FILE_REGISTRY.lock().unwrap();
match registry.get(instance_id) {
Some(filebox) => Ok(format!("FileBox({})", filebox.path().unwrap_or("none"))),
None => Err(-5), // INVALID_HANDLE
}
}
/// 簡易TLV解析文字列のみ
fn parse_string_from_tlv(data: *const u8, len: usize) -> Result<String, i32> {
if data.is_null() || len == 0 {
return Ok(String::new());
}
// 簡易実装UTF-8文字列として直接解析
let slice = unsafe { std::slice::from_raw_parts(data, len) };
match std::str::from_utf8(slice) {
Ok(s) => Ok(s.to_string()),
Err(_) => Err(-2), // ENCODING_ERROR
}
}
/// プラグイン情報メタデータAPI
#[no_mangle]
pub extern "C" fn nyash_plugin_info() -> *const c_char {
// プラグイン情報のJSON
let info = r#"{
"name": "nyash-filebox-plugin",
"version": "0.1.0",
"provides": ["FileBox"],
"abi_version": "bid-1.0"
}"#;
// リークさせて永続化(プラグインライフタイム中有効)
Box::leak(CString::new(info).unwrap().into_boxed_c_str()).as_ptr()
}
/// プラグイン初期化
#[no_mangle]
pub extern "C" fn nyash_plugin_init() -> i32 {
// 初期化処理(必要に応じて)
0 // SUCCESS
}
/// プラグイン終了処理
#[no_mangle]
pub extern "C" fn nyash_plugin_shutdown() -> i32 {
// クリーンアップ処理
0 // SUCCESS
}