Make unshell-lib, and module system with components.

This commit is contained in:
Michael Mikovsky
2025-11-08 14:56:03 -07:00
parent 8f33945633
commit 3dba32264c
27 changed files with 461 additions and 311 deletions
+22
View File
@@ -0,0 +1,22 @@
use bincode::{Decode, Encode};
#[derive(Debug, Encode, Decode)]
pub enum Announcement {
TestAnnouncement(String),
}
const BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard();
impl Announcement {
pub fn encode(&self) -> Vec<u8> {
bincode::encode_to_vec(self, BINCODE_CONFIG).unwrap()
}
pub fn decode(bytes: &[u8]) -> Option<Self> {
if let Ok((decoded, _)) = bincode::decode_from_slice(&bytes[..], BINCODE_CONFIG) {
Some(decoded)
} else {
None
}
}
}
+79
View File
@@ -0,0 +1,79 @@
use std::{
io::Read,
net::TcpStream,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
thread::{self, JoinHandle},
};
// use unshell_modules::{Manager, ModuleRuntime};
use crate::{Announcement, ModuleRuntime, module::Manager};
pub struct RuntimeTest {
thread_handle: JoinHandle<()>,
join_signal: Arc<AtomicBool>,
}
impl RuntimeTest {
pub fn new(_manager: Arc<Mutex<Manager>>) -> RuntimeTest {
let join_signal = Arc::new(AtomicBool::new(false));
let join_clone = join_signal.clone();
Self {
thread_handle: thread::spawn(move || {
info!("Connecting to server...");
let mut stream = TcpStream::connect("localhost:1234").unwrap();
info!("Connectied");
// let reader = BufReader::new(stream.try_clone().unwrap());
// let mut writer = BufWriter::new(stream.try_clone().unwrap());
// let (a, b) = crossbeam_channel::unbounded();
// a.
// if join_receiver.len() == 0 {
// join_receiver.recv().unwrap();
// }
while !join_clone.load(Ordering::Relaxed) {
let mut size_buf = [0u8; 4];
stream.read_exact(&mut size_buf).unwrap();
let size = u32::from_be_bytes(size_buf);
let mut buf = vec![0u8; size as usize];
stream.read_exact(&mut buf).unwrap();
let a = Announcement::decode(&buf).unwrap();
match a {
Announcement::TestAnnouncement(s) => {
info!("Received test announcement: {}", s)
}
}
}
}),
join_signal,
}
}
}
impl ModuleRuntime for RuntimeTest {
// fn init(&mut self) {}
fn is_running(&self) -> bool {
// println!("Checking if running");
!self.thread_handle.is_finished()
}
fn kill(self: Box<Self>) {
if !self.thread_handle.is_finished() {
self.join_signal.store(true, Ordering::Relaxed);
let _ = self.thread_handle.join();
}
// drop(self);
}
}
+74
View File
@@ -0,0 +1,74 @@
mod client_runtime;
// use crate::module::Interface;
use std::sync::{Arc, Mutex};
pub const MODULE_NAME: &'static str = "client";
// pub use unshell_modules::setup_logger;
// use unshell_modules::{Manager, ModuleRuntime, module_interface};
use crate::{
Component,
ModuleRuntime,
client::client_runtime::RuntimeTest,
module::{Interface, Manager},
module_interface,
// module_interface,
};
pub extern "C" fn test1() {
warn!("Test1 called xxxxxxxxxxx");
}
pub extern "C" fn test2() {
warn!("Test2 called");
}
pub extern "C" fn test3() {
warn!("Test3 called");
}
module_interface! {
ClientInterface {
fn test1();
fn test2();
fn test3();
}
}
// #[unsafe(no_mangle)]
// pub fn interface() -> Interface {
// Interface::from_raw(test1, test2, test3)
// }
// #[unsafe(no_mangle)]
// pub fn init(manager: Arc<Mutex<Manager>>) -> Box<dyn ModuleRuntime> {
// info!("Initializing client module");
// }
#[derive(Clone)]
pub struct ClientComponent;
impl ClientComponent {
pub fn new() -> Self {
ClientComponent
}
}
impl Component for ClientComponent {
fn name(&self) -> &'static str {
MODULE_NAME
}
fn start_runtime(&self, manager: Arc<Mutex<Manager>>) -> Option<Box<dyn ModuleRuntime>> {
Some(Box::new(RuntimeTest::new(manager)))
}
fn clone_box(&self) -> Box<dyn Component> {
Box::new(self.clone())
}
fn get_interface(&self) -> Box<dyn Interface> {
Box::new(ClientInterface::from_raw(test1, test2, test3))
}
}
+45
View File
@@ -0,0 +1,45 @@
#![no_main]
#[macro_use]
extern crate log;
pub mod client;
pub mod module;
pub mod server;
mod announcement;
use std::sync::{Arc, Mutex};
pub use announcement::Announcement;
use crate::module::{Interface, Manager};
///Generic error type for module-related operations.
#[derive(Debug)]
pub enum ModuleError {
LibLoadingError(libloading::Error),
LogError(log::SetLoggerError),
LinkError(String),
Error(String),
}
/// Trait for defining modules that have a runtime.
pub trait ModuleRuntime: Send {
/// Returns true if the module is running.
/// After returning false, the module will be dropped.
fn is_running(&self) -> bool;
/// Consumes the module, implementation should kill whatever is running.
fn kill(self: Box<Self>);
}
pub trait Component {
fn name(&self) -> &'static str;
fn start_runtime(&self, manager: Arc<Mutex<Manager>>) -> Option<Box<dyn ModuleRuntime>>;
fn get_interface(&self) -> Box<dyn Interface>;
fn clone_box(&self) -> Box<dyn Component>;
}
impl Clone for Box<dyn Component> {
fn clone(&self) -> Box<dyn Component> {
self.clone_box()
}
}
+16
View File
@@ -0,0 +1,16 @@
#![allow(improper_ctypes_definitions)]
use log::{LevelFilter, Log, SetLoggerError};
#[allow(dead_code)]
pub type SetupLogger =
extern "C" fn(logger: &'static dyn Log, level: LevelFilter) -> Result<(), SetLoggerError>;
#[unsafe(no_mangle)]
pub extern "C" fn setup_logger(
logger: &'static dyn log::Log,
level: log::LevelFilter,
) -> Result<(), log::SetLoggerError> {
log::set_max_level(level);
log::set_logger(logger)
}
+88
View File
@@ -0,0 +1,88 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
thread,
time::Duration,
};
use crate::{Component, ModuleRuntime, module::Module};
pub struct Manager {
modules: Vec<Module>,
components: HashMap<&'static str, Box<dyn Component>>,
}
// static mut MANAGER_RUNTIME: Option<Arc<Mutex<Manager>>> = None;
impl Manager {
/// Create Manager, and run initilization for each Module
#[allow(static_mut_refs)]
pub fn run<'a>(modules: Vec<Module>) {
let this: Self = Self::load_modules(modules);
let components = this.components.clone();
let this = Arc::new(Mutex::new(this));
let mut runtimes: Vec<Box<dyn ModuleRuntime>> = Vec::new();
for (name, component) in components {
let module_runtime = component.start_runtime(this.clone());
if let Some(module_runtime) = module_runtime {
info!("Initialized {}", name);
runtimes.push(module_runtime);
}
}
Self::join(&mut runtimes);
}
pub fn load_modules<'a>(modules: Vec<Module>) -> Self {
let module_count = modules.len();
let mut this = Self {
modules,
components: HashMap::new(),
};
// let mut runtimes = Vec::new();
for i in 0..module_count {
info!("Importing module {}", i);
// let this_lock = .unwrap();
let component_func = if let Ok(component_func) = this.modules[i]
.get_symbol::<fn() -> HashMap<&'static str, Box<dyn Component>>>(b"get_components")
{
component_func
} else {
warn!("get_components function not found");
continue;
};
let components = component_func();
info!("[{i}] Loaded {} components", components.len());
this.components.extend(components);
}
this
}
/// Iterateratively loop through all runtimes, until all are finished executing
pub fn join(runtimes: &mut Vec<Box<dyn ModuleRuntime>>) {
// let mut len = runtimes.len().clone();
while runtimes.len() > 0 {
runtimes.retain(|runtime| runtime.is_running());
thread::sleep(Duration::from_micros(100));
}
}
// pub extern "C" fn test1234(&self, float: f32) {
// info!("Manager Test Sucsessfull! {}", float.powf(2.));
// }
// #[allow(static_mut_refs, improper_ctypes_definitions)]
// pub extern "C" fn get_manager() -> Arc<Mutex<Manager>> {
// unsafe { MANAGER_RUNTIME.clone().unwrap() }
// }
}
+68
View File
@@ -0,0 +1,68 @@
mod logger;
mod manager;
mod module;
// use std::any::Any;
pub use logger::setup_logger;
pub use manager::Manager;
pub use module::Module;
pub trait Interface {
fn as_any(self: Box<Self>) -> Box<dyn std::any::Any>;
}
/// "Module Interface" helper macro that creates a struct with function pointers
/// Useful for defining and requiring modules' functions accross FFI boundry.
#[macro_export]
macro_rules! module_interface {
($(#[$struct_meta:meta])* $interface_name:ident { $($(#[$fn_meta:meta])* fn $fn_name:ident $(<$($gen:ident),+ $(,)?>)?($($arg:ident : $ty:ty),* $(,)?) $(-> $ret:ty)? $(where $($where_clause:tt)*)?);* $(;)? }) => {
#[repr(C)]
#[allow(non_camel_case_types)]
#[derive(Clone, Copy)]
#[allow(improper_ctypes_definitions)]
$(#[$struct_meta])*
pub struct $interface_name {
$(
// This line will FAIL TO COMPILE if you use generics in the macro input.
// You MUST use concrete types like *mut c_void for "generic" data.
$fn_name: extern "C" fn($($ty),*) $(-> $ret)?,
)*
}
impl $interface_name {
$(
#[inline(always)]
$(#[$fn_meta])* // Propagate function attributes
// This is the fix for the `impl` block.
// It adds the captured generics and where-clause to the wrapper function.
pub fn $fn_name $(<$($gen),+>)? (&self, $($arg: $ty),*) $(-> $ret)?
$(where $($where_clause)*)?
{
(self.$fn_name)($($arg),*)
}
)*
/// Create from raw function pointers
///
/// # Safety
///
/// The caller must ensure all function pointers are valid and have
/// the correct signatures
pub fn from_raw(
$($fn_name: extern "C" fn($($ty),*) $(-> $ret)?),*
) -> Self {
Self {
$($fn_name),*
}
}
}
impl crate::module::Interface for $interface_name {
fn as_any(self: Box<Self>) -> Box<dyn std::any::Any> {
self
}
}
};
}
+46
View File
@@ -0,0 +1,46 @@
use libloading::{Library, Symbol};
use crate::{ModuleError, module::logger::SetupLogger};
pub struct Module {
// name: String,
lib: Library,
}
impl Module {
pub fn new(path: &str) -> Result<Self, ModuleError> {
let lib = unsafe { Library::new(&path) }.map_err(|e| ModuleError::LibLoadingError(e))?;
let this = Self {
// name: path.to_owned(),
lib,
};
if let Ok(setup_logger) = this.get_symbol::<SetupLogger>(b"setup_logger") {
setup_logger(log::logger(), log::max_level()).map_err(|e| ModuleError::LogError(e))?;
} else {
warn!("setup_logger not found");
}
Ok(this)
}
pub fn get_symbol<T>(&self, symbol: &[u8]) -> Result<Symbol<'_, T>, ModuleError> {
let symbol = unsafe { self.lib.get::<T>(symbol) }
.map_err(|e| ModuleError::LinkError(format!("Failed to load symbol: {}", e)))?;
Ok(symbol)
}
// pub fn get_interface<T>(&self) -> Result<T, ModuleError> {
// if let Ok(interface_function) = self.get_symbol::<fn() -> T>(b"interface") {
// Ok(interface_function())
// } else {
// Err(ModuleError::LinkError(format!(
// "Interface function not found!"
// )))
// }
// }
}
// extern "C" fn test1234() {
// info!("Test1234!");
// }
+3
View File
@@ -0,0 +1,3 @@
mod server_runtime;
pub use server_runtime::ListenerRuntime;
+77
View File
@@ -0,0 +1,77 @@
use std::{
io::Write,
net::{TcpListener, TcpStream},
sync::{Arc, Mutex},
thread::{self, JoinHandle},
};
use crate::ModuleRuntime;
use crate::Announcement;
pub struct ListenerRuntime {
thread_handle: JoinHandle<()>,
// listener: TcpListener,
streams: Arc<Mutex<Vec<TcpStream>>>,
// reader: BufReader<TcpListener>,
// writer: BufWriter<TcpListener>,
}
impl ListenerRuntime {
pub fn new() -> ListenerRuntime {
info!("Starting listener runtime on 127.0.0.1:1234");
let listener = TcpListener::bind("127.0.0.1:1234").unwrap();
let streams = Arc::new(Mutex::new(Vec::new()));
let streams_clone = streams.clone();
let thread_handle = thread::spawn(move || {
let streams = streams_clone.clone();
for stream in listener.incoming() {
let stream = stream.unwrap();
println!("New connection from {}", stream.peer_addr().unwrap());
streams.lock().unwrap().push(stream);
}
});
Self {
thread_handle,
streams,
}
}
pub fn send(&mut self, announcement: &Announcement) -> Result<(), std::io::Error> {
let bytes = announcement.encode();
let mut streams = self.streams.lock().unwrap();
for stream in streams.iter_mut() {
stream.write_all(&u32::to_be_bytes(bytes.len() as u32))?;
stream.write_all(&bytes)?;
stream.flush()?;
}
println!("Announcement {:?} sent", announcement);
Ok(())
// self.stream
// .write_all(&u32::to_be_bytes(bytes.len() as u32))?;
// self.stream.write_all(&bytes)?;
// self.stream.flush()?;
}
}
impl ModuleRuntime for ListenerRuntime {
// fn init(&mut self) {}
fn is_running(&self) -> bool {
true
}
fn kill(self: Box<Self>) {
if !self.thread_handle.is_finished() {
let _ = self.thread_handle.join();
}
// drop(self);
}
}