From 455b4666219ea9bfb1aa67aa23b89e7076a1c119 Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Tue, 13 Jan 2026 22:15:56 -0500 Subject: [PATCH] Add basic C FFI for chat plugins and corresponding plugin loader --- Cargo.lock | 11 ++++ rc_chat_room/src/main.rs | 3 +- rc_chat_room/src/plugin_wrapper.rs | 40 ++++++++++++++ rc_chat_room/src/state/chat/chat.rs | 8 +-- rc_plugins/Cargo.toml | 1 + rc_plugins/src/chat/c_binding.rs | 86 +++++++++++++++++++++++++++++ rc_plugins/src/chat/mod.rs | 3 + 7 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 rc_plugins/src/chat/c_binding.rs diff --git a/Cargo.lock b/Cargo.lock index e979a55..103ec4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2049,6 +2049,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.15" @@ -2608,6 +2618,7 @@ dependencies = [ name = "oj_rc_plugins" version = "1.1.0" dependencies = [ + "libloading", "log", ] diff --git a/rc_chat_room/src/main.rs b/rc_chat_room/src/main.rs index 5d08fcf..5c01cb6 100644 --- a/rc_chat_room/src/main.rs +++ b/rc_chat_room/src/main.rs @@ -35,7 +35,8 @@ async fn main() -> std::io::Result<()> { let cubes = oj_rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config data"); let users = std::sync::Arc::new(oj_rc_core::persist::user::UserImpl::load(&args.data, &cubes).await.expect("Bad user data")); - let chat_system = state::chat::ChatImpl::new(>::chat_system_config(&cubes)).expect("Bad chat config data"); + let chat_plugin_path = std::path::PathBuf::from(&args.data).join("plugins/chat"); + let chat_system = state::chat::ChatImpl::new(>::chat_system_config(&cubes), chat_plugin_path).expect("Bad chat config data"); let server = std::sync::Arc::new(polariton_server::Server::new(operations::handler(chat_system.clone(), &cubes), polariton_server::events::EventsHandler::new())); diff --git a/rc_chat_room/src/plugin_wrapper.rs b/rc_chat_room/src/plugin_wrapper.rs index b99e552..d6dccaf 100644 --- a/rc_chat_room/src/plugin_wrapper.rs +++ b/rc_chat_room/src/plugin_wrapper.rs @@ -51,3 +51,43 @@ impl oj_rc_plugins::chat::ChatProvider for ProviderWrapper { tokio::task::spawn(Self::do_send_message(self.provider.clone(), message.to_owned(), channel.to_owned(), username.to_owned())); } } + +pub fn load_chat_plugins(from_dir: impl AsRef) -> Vec> { + let path_ref = from_dir.as_ref(); + if path_ref.exists() { + log::warn!("Chat plugins are experimental and insecure"); + } else { + log::info!("Not loading chat plugins; {} does not exist", path_ref.display()); + return Vec::default(); + } + match path_ref.read_dir() { + Ok(dir) => { + let mut plugins = Vec::new(); + for entry in dir { + match entry { + Ok(entry) => { + if entry.path().is_file() { + match oj_rc_plugins::chat::ChatCPlugin::new(entry.path()) { + Ok(plugin) => { + plugins.push(Box::new(plugin) as Box); + }, + Err(e) => { + log::warn!("Failed to load chat plugin {}: {}", entry.path().display(), e); + } + } + + } + }, + Err(e) => { + log::warn!("Failed to read entry in {}: {}", path_ref.display(), e); + } + } + } + plugins + }, + Err(e) => { + log::error!("Failed to load chat plugins from {}: {}", path_ref.display(), e); + Vec::default() + } + } +} diff --git a/rc_chat_room/src/state/chat/chat.rs b/rc_chat_room/src/state/chat/chat.rs index 374e902..e2b0243 100644 --- a/rc_chat_room/src/state/chat/chat.rs +++ b/rc_chat_room/src/state/chat/chat.rs @@ -6,9 +6,9 @@ pub struct ChatProvider { } impl ChatProvider { - pub fn new(conf: oj_rc_core::persist::config::ChatSystemConfig) -> std::io::Result { + pub fn new(conf: oj_rc_core::persist::config::ChatSystemConfig, dir: impl AsRef) -> std::io::Result { Ok(Self { - chat_system: std::sync::Arc::new(tokio::sync::RwLock::new(crate::state::chat::ChatSystem::new(conf)?)), + chat_system: std::sync::Arc::new(tokio::sync::RwLock::new(crate::state::chat::ChatSystem::new(conf, dir)?)), }) } @@ -198,12 +198,12 @@ impl ChatSystem { handle.send_private_message(response); } - pub fn new(config: oj_rc_core::persist::config::ChatSystemConfig) -> std::io::Result { + pub fn new(config: oj_rc_core::persist::config::ChatSystemConfig, dir: impl AsRef) -> std::io::Result { Ok(Self { chats: HashMap::new(), online_users: HashMap::new(), config: super::ChatSystemConfig::from_persist(config)?, - plugin: crate::PluginWrapper::new(vec![]), // TODO construct plugins + plugin: crate::PluginWrapper::new(crate::plugin_wrapper::load_chat_plugins(dir)), }) } diff --git a/rc_plugins/Cargo.toml b/rc_plugins/Cargo.toml index 80ddf36..1941f51 100644 --- a/rc_plugins/Cargo.toml +++ b/rc_plugins/Cargo.toml @@ -9,3 +9,4 @@ readme.workspace = true [dependencies] log.workspace = true +libloading = "0.9" diff --git a/rc_plugins/src/chat/c_binding.rs b/rc_plugins/src/chat/c_binding.rs new file mode 100644 index 0000000..3dd8f92 --- /dev/null +++ b/rc_plugins/src/chat/c_binding.rs @@ -0,0 +1,86 @@ +//! The foreign function interface implementation for writing ChatPlugins in different shared objects/libraries. +//! This is sort of cursed, I'm sorry in advance. +use std::ffi::{CString, c_char, CStr}; + +const SET_SEND_MESSAGE_CALLBACK_SYMBOL_NAME: &[u8] = b"oj_rc_set_chat_send_message_callback"; +const SET_SEND_MESSAGE_CALLBACK_SYMBOL_NAME_STR: &str = "oj_rc_set_chat_send_message_callback"; + +const ON_MESSAGE_SYMBOL_NAME: &[u8] = b"oj_rc_on_chat_message"; +const ON_MESSAGE_SYMBOL_NAME_STR: &str = "oj_rc_on_chat_message"; + +pub struct ChatCPlugin { + dll: libloading::Library, + pretty_name: String, + provider: std::sync::Mutex>>>, +} + +impl ChatCPlugin { + pub fn new(file: impl AsRef) -> Result { + let dll = unsafe { libloading::Library::new(file.as_ref()) }?; + Ok(Self { + dll, + pretty_name: file.as_ref().to_string_lossy().to_string(), + provider: std::sync::Mutex::new(None), + }) + } +} + +struct ContextC { + provider: std::sync::Weak>, +} + +impl super::ChatPlugin for ChatCPlugin { + fn set_provider(&self, provider: std::sync::Arc>) { + let func: libloading::Symbol = match unsafe { self.dll.get(SET_SEND_MESSAGE_CALLBACK_SYMBOL_NAME) } { + Ok(x) => x, + Err(e) => { + log::error!("Failed to find symbol {} in library {}: {}", SET_SEND_MESSAGE_CALLBACK_SYMBOL_NAME_STR, self.pretty_name, e); + return; + } + }; + *self.provider.lock().unwrap() = Some(provider.clone()); + let weak_provider = std::sync::Arc::downgrade(&provider); + let ctx = Box::new(ContextC { + provider: weak_provider, + }); + extern "C" fn callback(ctx: *const ContextC, msg: *const c_char, chann: *const c_char, usern: *const c_char) { + let ctx = if let Some(ctx) = unsafe { ctx.as_ref() } { ctx } else { return }; + let msg = unsafe { CStr::from_ptr(msg) }; + let chann = unsafe { CStr::from_ptr(chann) }; + let usern = unsafe { CStr::from_ptr(usern) }; + if let Some(provider) = ctx.provider.upgrade() { + match (msg.to_str(), chann.to_str(), usern.to_str()) { + (Ok(message), Ok(channel), Ok(username)) => { + provider.send_message(message, channel, username); + }, + _ => { + log::warn!("{} called with invalid string parameter", SET_SEND_MESSAGE_CALLBACK_SYMBOL_NAME_STR); + } + } + } else { + log::warn!("Chat provider callback invoked after it has been dropped"); + } + } + unsafe { + func(&*ctx, callback) + } + } + + fn on_message(&self, message: &str, channel: &str, username: &str) { + let func: libloading::Symbol = match unsafe { self.dll.get(ON_MESSAGE_SYMBOL_NAME) } { + Ok(x) => x, + Err(e) => { + log::error!("Failed to find symbol {} in library {}: {}", ON_MESSAGE_SYMBOL_NAME_STR, self.pretty_name, e); + return; + } + }; + let message = CString::new(message).unwrap_or_default(); + let channel = CString::new(channel).unwrap_or_default(); + let username = CString::new(username).unwrap_or_default(); + unsafe { + func(message.as_c_str().as_ptr(), channel.as_c_str().as_ptr(), username.as_c_str().as_ptr()); + } + } +} + +impl crate::Plugin for ChatCPlugin {} diff --git a/rc_plugins/src/chat/mod.rs b/rc_plugins/src/chat/mod.rs index 6da659a..6aaa3a4 100644 --- a/rc_plugins/src/chat/mod.rs +++ b/rc_plugins/src/chat/mod.rs @@ -1,2 +1,5 @@ mod plugin; pub use plugin::{ChatProvider, ChatPlugin}; + +mod c_binding; +pub use c_binding::ChatCPlugin;