diff --git a/build/Cargo.toml b/build/Cargo.toml index a650395..4de8be8 100644 --- a/build/Cargo.toml +++ b/build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bp3d-os-build" -version = "1.0.0" +version = "1.1.0" authors = ["Yuri Edward "] edition = "2024" description = "Operating System tools designed for BlockProject3D" diff --git a/build/src/lib.rs b/build/src/lib.rs index fb557a8..6331d99 100644 --- a/build/src/lib.rs +++ b/build/src/lib.rs @@ -100,12 +100,13 @@ impl ModuleMain { (\"{mod_const_name}\", unsafe {{ &{mod_const_name} as *const *const i8 as *const std::ffi::c_void }})"); let out_path = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()).join("bp3d_os_module.rs"); - Self { + let this = Self { rust_code, out_path, crate_name, virtual_lib, - } + }; + this.add_init().add_uninit() } pub fn add_export(mut self, func_name: impl AsRef) -> Self { @@ -114,6 +115,40 @@ impl ModuleMain { self } + fn add_init(mut self) -> Self { + let motherfuckingrust = "extern \"C\""; + let crate_name = &self.crate_name; + let rust_code = format!( + r" + #[unsafe(no_mangle)] + #[inline(never)] + pub {motherfuckingrust} fn bp3d_os_module_{crate_name}_init(loader: &'static std::sync::Mutex) {{ + bp3d_os::module::loader::ModuleLoader::install_from_existing(loader); + }} +" + ); + self.rust_code += &rust_code; + let motherfuckingrust = format!("bp3d_os_module_{crate_name}_init"); + self.add_export(motherfuckingrust) + } + + fn add_uninit(mut self) -> Self { + let motherfuckingrust = "extern \"C\""; + let crate_name = &self.crate_name; + let rust_code = format!( + r" + #[unsafe(no_mangle)] + #[inline(never)] + pub {motherfuckingrust} fn bp3d_os_module_{crate_name}_uninit() {{ + bp3d_os::module::loader::ModuleLoader::uninstall(); + }} +" + ); + self.rust_code += &rust_code; + let motherfuckingrust = format!("bp3d_os_module_{crate_name}_uninit"); + self.add_export(motherfuckingrust) + } + pub fn add_open(mut self) -> Self { let motherfuckingrust = "extern \"C\""; let crate_name = &self.crate_name; diff --git a/core/Cargo.toml b/core/Cargo.toml index e2a4986..f8b0444 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bp3d-os" -version = "1.0.2" +version = "2.0.0" authors = ["Yuri Edward "] edition = "2021" description = "Operating System tools designed for BlockProject3D" diff --git a/core/src/module/error.rs b/core/src/module/error.rs index 25c33a0..e27d44a 100644 --- a/core/src/module/error.rs +++ b/core/src/module/error.rs @@ -100,6 +100,9 @@ simple_error! { /// Missing VERSION key for a module. MissingModuleVersion => "missing VERSION metadata key", + /// Missing bp3d_os_module__init symbol for a Rust based module. + MissingModuleInitForRust => "missing module init function for a Rust module", + /// The given string was not UTF8. InvalidUtf8(Utf8Error) => "invalid utf8: {}", diff --git a/core/src/module/library/symbol.rs b/core/src/module/library/symbol.rs index 66b93ed..3701e4a 100644 --- a/core/src/module/library/symbol.rs +++ b/core/src/module/library/symbol.rs @@ -85,6 +85,16 @@ impl<'a, T> Symbol<'a, T> { } } +impl<'a, R> Symbol<'a, extern "Rust" fn() -> R> { + /// Calls this symbol if this symbol is a function. + /// + /// returns: R + pub fn call(&self) -> R { + let f: extern "Rust" fn() -> R = unsafe { std::mem::transmute(self.ptr) }; + f() + } +} + impl<'a, T, R> Symbol<'a, extern "Rust" fn(T) -> R> { /// Calls this symbol if this symbol is a function. /// diff --git a/core/src/module/library/unix.rs b/core/src/module/library/unix.rs index bdf4398..5fb068e 100644 --- a/core/src/module/library/unix.rs +++ b/core/src/module/library/unix.rs @@ -29,6 +29,7 @@ use crate::module; use crate::module::error::Error; use crate::module::library::symbol::Symbol; +use bp3d_debug::debug; use libc::{dlclose, dlopen, dlsym, RTLD_LAZY}; use std::ffi::{c_void, CString}; use std::fmt::Debug; @@ -46,6 +47,8 @@ pub const EXT: &str = "so"; #[repr(transparent)] pub struct Library(*mut c_void); +unsafe impl Send for Library {} + impl Library { /// Attempts to open a handle to the current running program. pub fn open_self() -> module::Result { @@ -95,6 +98,7 @@ impl super::Library for Library { impl Drop for Library { fn drop(&mut self) { + debug!("dlclose"); unsafe { dlclose(self.0) }; } } diff --git a/core/src/module/library/virtual.rs b/core/src/module/library/virtual.rs index b4e935d..e176ce8 100644 --- a/core/src/module/library/virtual.rs +++ b/core/src/module/library/virtual.rs @@ -38,6 +38,8 @@ pub struct VirtualLibrary { symbols: &'static [(&'static str, *const c_void)], } +unsafe impl Send for VirtualLibrary {} + unsafe impl Sync for VirtualLibrary {} impl VirtualLibrary { diff --git a/core/src/module/library/windows.rs b/core/src/module/library/windows.rs index 5a3bc95..feb41b2 100644 --- a/core/src/module/library/windows.rs +++ b/core/src/module/library/windows.rs @@ -42,6 +42,8 @@ pub const EXT: &str = "dll"; #[derive(Debug)] pub struct Library(HMODULE); +unsafe impl Send for Library {} + impl Library { /// Attempts to open a handle to the current running program. pub fn open_self() -> module::Result { diff --git a/core/src/module/loader/core.rs b/core/src/module/loader/core.rs new file mode 100644 index 0000000..455d30e --- /dev/null +++ b/core/src/module/loader/core.rs @@ -0,0 +1,370 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::module::error::Error; +use crate::module::library::types::{OsLibrary, VirtualLibrary}; +use crate::module::library::OS_EXT; +use crate::module::loader::util::{load_by_symbol, load_lib, module_close, Dependency, DepsMap}; +use crate::module::loader::Lock; +use crate::module::Module; +use bp3d_debug::{debug, error}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::atomic::Ordering::SeqCst; +use std::sync::atomic::{AtomicBool, AtomicPtr}; +use std::sync::{Mutex, MutexGuard}; + +struct Data { + loader: AtomicPtr>, + is_root: AtomicBool, +} + +impl Data { + fn install(&self, loader: ModuleLoader) -> bool { + let ptr = self.loader.load(SeqCst); + if ptr.is_null() { + self.loader + .store(Box::leak(Box::new(Mutex::new(loader))), SeqCst); + self.is_root.store(true, SeqCst); + true + } else { + false + } + } + + fn install_existing(&self, loader: &'static Mutex) -> bool { + let ptr = self.loader.load(SeqCst); + if ptr.is_null() { + self.is_root.store(false, SeqCst); + self.loader + .store(loader as *const Mutex as *mut _, SeqCst); + true + } else { + false + } + } + + fn is_root(&self) -> bool { + self.is_root.load(SeqCst) + } + + fn reset(&self) { + self.loader.store(std::ptr::null_mut(), SeqCst); + self.is_root.store(false, SeqCst); + } + + fn is_set(&self) -> bool { + !self.loader.load(SeqCst).is_null() + } + + // This is only safe if this is set. + unsafe fn get(&self) -> &'static Mutex { + let ptr = self.loader.load(SeqCst); + unsafe { &*ptr } + } +} + +static MODULE_LOADER: Data = Data { + loader: AtomicPtr::new(std::ptr::null_mut()), + is_root: AtomicBool::new(false), +}; + +/// Represents a module loader which can support loading multiple related modules. +pub struct ModuleLoader { + paths: Vec, + pub(super) modules: HashMap>, + pub(super) builtin_modules: HashMap>, + deps: DepsMap, + builtins: &'static [&'static VirtualLibrary], + module_name_to_id: HashMap, + last_module_id: usize, +} + +impl ModuleLoader { + /// Create a new instance of a [ModuleLoader] and installs it as this application's + /// [ModuleLoader]. + pub fn install(builtins: &'static [&'static VirtualLibrary]) { + debug!("Installing new ModuleLoader..."); + let mut this = ModuleLoader { + paths: Default::default(), + modules: Default::default(), + deps: DepsMap::new(), + builtin_modules: Default::default(), + builtins, + module_name_to_id: Default::default(), + last_module_id: 0, + }; + this._add_public_dependency(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"), ["*"]); + this._add_public_dependency("bp3d-debug", "1.0.0", ["*"]); + if !MODULE_LOADER.install(this) { + panic!("attempt to initialize module loader twice"); + } + } + + /// Uninstall the application's [ModuleLoader]. This function will panic if this module did not + /// install a [ModuleLoader] but is rather sharing the instance of a different application. + pub fn uninstall() { + debug!("Uninstalling ModuleLoader..."); + if !MODULE_LOADER.is_set() { + panic!("attempt to uninstall a non-existent ModuleLoader"); + } + if !MODULE_LOADER.is_root() { + MODULE_LOADER.reset() + } else { + debug!("Unloading modules..."); + let mut loader = Self::_lock(); + let map = loader.module_name_to_id.clone(); + for (name, _) in map { + debug!("Unloading module {}...", name); + if let Err(e) = loader._unload(&name) { + error!("Failed to unload module {}: {}", name, e); + } + } + drop(loader); + debug!("Deleting ModuleLoader..."); + unsafe { + drop(Box::from_raw( + MODULE_LOADER.get() as *const Mutex as *mut Mutex, + )); + } + MODULE_LOADER.reset(); + } + } + + pub(crate) fn _instance() -> &'static Mutex { + unsafe { MODULE_LOADER.get() } + } + + /// Installs a default [ModuleLoader] for this application. + pub fn install_default() { + Self::install(&[]); + } + + /// Install the [ModuleLoader] of this module to an existing instance. + pub fn install_from_existing(loader: &'static Mutex) { + if MODULE_LOADER.install_existing(loader) { + debug!("Installed ModuleLoader from existing instance"); + } + assert_eq!(loader as *const Mutex, unsafe { + MODULE_LOADER.get() as *const Mutex + }); + } + + fn _lock<'a>() -> MutexGuard<'a, ModuleLoader> { + if !MODULE_LOADER.is_set() { + Self::install_default(); + } + unsafe { MODULE_LOADER.get().lock().unwrap() } + } + + fn _next_module_id(&mut self) -> usize { + let id = self.last_module_id; + self.last_module_id += 1; + id + } + + pub(super) fn _get_builtin(&self, name: &str) -> Option { + let name = name.replace("-", "_"); + if let Some(id) = self.module_name_to_id.get(&name) { + self.builtin_modules.get(id).map(|v| v.id) + } else { + None + } + } + pub(super) fn _get_module(&self, name: &str) -> Option { + let name = name.replace("-", "_"); + if let Some(id) = self.module_name_to_id.get(&name) { + self.modules.get(id).map(|v| v.id) + } else { + None + } + } + + pub(super) unsafe fn _load_builtin(&mut self, name: &str) -> crate::module::Result { + debug!("Loading builtin module: {}", name); + let name = name.replace("-", "_"); + if let Some(id) = self.module_name_to_id.get(&name) { + match self.builtin_modules.get_mut(id) { + Some(v) => { + v.ref_count += 1; + Ok(*id) + } + None => Err(Error::NotFound(name)), + } + } else { + for builtin in self.builtins { + if builtin.name() == name { + let mut module = unsafe { load_by_symbol(**builtin, &name, &mut self.deps) } + .map_err(|e| match e { + Error::NotFound(_) => Error::MissingMetadata, + e => e, + })?; + let id = self._next_module_id(); + module.id = id; + self.module_name_to_id.insert(name, id); + self.builtin_modules.entry(id).or_insert(module); + return Ok(id); + } + } + Err(Error::NotFound(name)) + } + } + + pub(super) unsafe fn _load_self(&mut self, name: &str) -> crate::module::Result { + debug!("Loading static module: {}", name); + let name = name.replace("-", "_"); + if let Some(id) = self.module_name_to_id.get(&name) { + match self.modules.get_mut(id) { + Some(v) => { + v.ref_count += 1; + Ok(*id) + } + None => Err(Error::NotFound(name)), + } + } else { + let this = OsLibrary::open_self()?; + let mut module = unsafe { load_by_symbol(this, &name, &mut self.deps) }?; + let id = self._next_module_id(); + module.id = id; + self.module_name_to_id.insert(name, id); + self.modules.entry(id).or_insert(module); + Ok(id) + } + } + + pub(super) unsafe fn _load(&mut self, name: &str) -> crate::module::Result { + debug!("Loading dynamic module: {}", name); + let name = name.replace("-", "_"); + if let Some(id) = self.module_name_to_id.get(&name) { + match self.modules.get_mut(id) { + Some(v) => { + v.ref_count += 1; + Ok(*id) + } + None => Err(Error::NotFound(name)), + } + } else { + let name2 = format!("{}.{}", name, OS_EXT); + let name3 = format!("lib{}.{}", name, OS_EXT); + for path in self.paths.iter() { + let search = path.join(&name2); + let search2 = path.join(&name3); + let mut module = None; + if search.exists() { + module = Some(load_lib(&mut self.deps, &name, &search)?); + } else if search2.exists() { + module = Some(load_lib(&mut self.deps, &name, &search2)?); + } + if let Some(mut module) = module { + let id = self._next_module_id(); + module.id = id; + self.module_name_to_id.insert(name, id); + self.modules.insert(id, module); + return Ok(id); + } + } + Err(Error::NotFound(name)) + } + } + + pub(super) fn _unload(&mut self, name: &str) -> crate::module::Result<()> { + debug!("Unloading module: {}", name); + let name = name.replace("-", "_"); + let id = self + .module_name_to_id + .get(&name) + .copied() + .ok_or_else(|| Error::NotFound(name.clone()))?; + if self.modules.contains_key(&id) { + let module = self.modules.get_mut(&id).unwrap(); + module.ref_count -= 1; + if module.ref_count == 0 { + self.module_name_to_id.remove(&name); + let module = unsafe { self.modules.remove(&id).unwrap_unchecked() }; + unsafe { module_close(&name, false, &module) }?; + drop(module); + } + } else { + let module = self + .builtin_modules + .get_mut(&id) + .ok_or_else(|| Error::NotFound(name.clone()))?; + module.ref_count -= 1; + if module.ref_count == 0 { + self.module_name_to_id.remove(&name); + let module = unsafe { self.builtin_modules.remove(&id).unwrap_unchecked() }; + unsafe { module_close(&name, true, &module) }?; + drop(module); + } + } + Ok(()) + } + + pub(super) fn _add_search_path(&mut self, path: impl AsRef) { + self.paths.push(path.as_ref().into()); + } + + pub(super) fn _add_public_dependency<'a>( + &mut self, + name: &str, + version: &str, + features: impl IntoIterator, + ) { + let mut negative_features = Vec::new(); + let features = features + .into_iter() + .filter_map(|s| { + if s.starts_with("-") { + negative_features.push(s.into()); + return None; + } + if s != "*" { + Some(String::from(name) + s) + } else { + Some("*".into()) + } + }) + .collect(); + self.deps.add_dep( + name.replace("-", "_"), + Dependency { + version: version.into(), + features, + negative_features, + }, + ) + } + + /// Lock the [ModuleLoader] installed for the application and returns a lock which is used to + /// operate the [ModuleLoader]. + pub fn lock<'a>() -> Lock<'a> { + Lock { + lock: Self::_lock(), + } + } +} diff --git a/core/src/module/loader/interface.rs b/core/src/module/loader/interface.rs new file mode 100644 index 0000000..20fcd34 --- /dev/null +++ b/core/src/module/loader/interface.rs @@ -0,0 +1,210 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::module::library::types::{OsLibrary, VirtualLibrary}; +use crate::module::library::Library; +use crate::module::loader::ModuleLoader; +use crate::module::Module; +use crate::module::Result; +use std::path::Path; +use std::sync::MutexGuard; + +/// Represents a handle to a [Module] stored in the application's [ModuleLoader]. +pub trait ModuleHandle { + /// The type of [Library] to return. + type Library: Library; + + /// Returns a reference to the stored [Module] type. + fn get(&self) -> &Module; +} + +struct VirtualLibraryHandle<'a> { + loader: &'a ModuleLoader, + id: usize, +} + +impl<'a> VirtualLibraryHandle<'a> { + fn new(loader: &'a ModuleLoader, id: usize) -> VirtualLibraryHandle<'a> { + VirtualLibraryHandle { loader, id } + } +} + +impl<'a> ModuleHandle for VirtualLibraryHandle<'a> { + type Library = VirtualLibrary; + + fn get(&self) -> &Module { + self.loader.builtin_modules.get(&self.id).unwrap() + } +} + +struct OsLibraryHandle<'a> { + loader: &'a ModuleLoader, + id: usize, +} + +impl<'a> OsLibraryHandle<'a> { + fn new(loader: &'a ModuleLoader, id: usize) -> OsLibraryHandle<'a> { + OsLibraryHandle { loader, id } + } +} + +impl<'a> ModuleHandle for OsLibraryHandle<'a> { + type Library = OsLibrary; + + fn get(&self) -> &Module { + self.loader.modules.get(&self.id).unwrap() + } +} + +/// A structure that represents a lock to the application's [ModuleLoader]. +pub struct Lock<'a> { + pub(super) lock: MutexGuard<'a, ModuleLoader>, +} + +impl<'a> Lock<'a> { + /// Attempts to load the given builtin module from its name. + /// + /// # Arguments + /// + /// * `name`: the name of the builtin module to load. + /// + /// returns: Result<&Module, Error> + /// + /// # Safety + /// + /// This function assumes the module to be loaded, if it exists has the correct format otherwise + /// this function is UB. + pub unsafe fn load_builtin(&mut self, name: &str) -> Result { + self.lock + ._load_builtin(name) + .map(|id| VirtualLibraryHandle::new(&self.lock, id)) + } + + /// Attempts to load a module from the specified name which is dynamically linked in the current + /// running software. + /// + /// # Arguments + /// + /// * `name`: the name of the module to be loaded. + /// + /// returns: Result<&Module, Error> + /// + /// # Safety + /// + /// This function assumes the module to be loaded, if it exists has the correct format otherwise + /// this function is UB. + pub unsafe fn load_self(&mut self, name: &str) -> Result { + self.lock + ._load_self(name) + .map(|id| OsLibraryHandle::new(&self.lock, id)) + } + + /// Attempts to load a module from the specified name. + /// + /// This function already does check for the version of rustc and dependencies for Rust based + /// modules to ensure maximum ABI compatibility. + /// + /// This function assumes the code to be loaded is trusted and delegates this operation to the + /// underlying OS. + /// + /// # Arguments + /// + /// * `name`: the name of the module to be loaded. + /// + /// returns: () + /// + /// # Safety + /// + /// It is assumed that the module is intended to be used with this instance of [ModuleLoader]; + /// if not, this function is UB. Additionally, if some dependency used in public facing APIs + /// for the module are not added with [add_public_dependency](Self::add_public_dependency), + /// this is also UB. + pub unsafe fn load(&mut self, name: &str) -> Result { + self.lock + ._load(name) + .map(|id| OsLibraryHandle::new(&self.lock, id)) + } + + /// Attempts to unload the given module. + /// + /// # Arguments + /// + /// * `name`: the name of the module to unload. + /// + /// returns: () + pub fn unload(&mut self, name: &str) -> Result<()> { + self.lock._unload(name) + } + + /// Adds the given path to the path search list. + /// + /// # Arguments + /// + /// * `path`: the path to include. + /// + /// returns: () + pub fn add_search_path(&mut self, path: impl AsRef) { + self.lock._add_search_path(path); + } + + /// Adds a public facing API dependency to the list of dependency for version checks. + /// + /// This is used to check if there are any ABI incompatibilities between dependency versions + /// when loading a Rust based module. + /// + /// # Arguments + /// + /// * `name`: the name of the dependency. + /// * `version`: the version of the dependency. + /// + /// returns: () + pub fn add_public_dependency<'b>( + &mut self, + name: &str, + version: &str, + features: impl IntoIterator, + ) { + self.lock._add_public_dependency(name, version, features); + } + + /// Returns the builtin module identified by the name `name`, returns [None] if the module is + /// not loaded. + pub fn get_builtin(&self, name: &str) -> Option { + self.lock + ._get_builtin(name) + .map(|id| VirtualLibraryHandle::new(&self.lock, id)) + } + + /// Returns the module identified by the name `name`, returns [None] if the module is + /// not loaded. + pub fn get_module(&self, name: &str) -> Option { + self.lock + ._get_module(name) + .map(|id| OsLibraryHandle::new(&self.lock, id)) + } +} diff --git a/core/src/module/loader/mod.rs b/core/src/module/loader/mod.rs new file mode 100644 index 0000000..c34a3d5 --- /dev/null +++ b/core/src/module/loader/mod.rs @@ -0,0 +1,36 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! This module contains the implementation of the module loader. + +mod core; +mod interface; +mod util; + +pub use core::ModuleLoader; +pub use interface::*; diff --git a/core/src/module/loader.rs b/core/src/module/loader/util.rs similarity index 75% rename from core/src/module/loader.rs rename to core/src/module/loader/util.rs index 7681bc4..60bb621 100644 --- a/core/src/module/loader.rs +++ b/core/src/module/loader/util.rs @@ -27,82 +27,26 @@ // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use crate::module::error::{Error, IncompatibleDependency, IncompatibleRustc}; -use crate::module::library::types::{OsLibrary, VirtualLibrary}; -use crate::module::library::{Library, OS_EXT}; -use crate::module::metadata::{Metadata as ModuleMetadata, Value}; -use crate::module::Module; -use crate::module::RUSTC_VERSION; +use crate::module::library::types::{OsLibrary, Symbol}; +use crate::module::library::Library; +use crate::module::loader::ModuleLoader; +use crate::module::metadata::Value; +use crate::module::{Module, RUSTC_VERSION}; use bp3d_debug::{debug, info, trace}; use std::collections::{HashMap, HashSet}; use std::ffi::{c_char, CStr}; use std::fs::File; use std::io::Read; -use std::path::{Path, PathBuf}; +use std::path::Path; +use std::sync::Mutex; -type DebugInit = extern "Rust" fn(engine: &'static dyn bp3d_debug::engine::Engine); - -/// Represents a module loader which can support loading multiple related modules. -pub struct ModuleLoader { - paths: Vec, - modules: HashMap>, - builtin_modules: HashMap>, - deps: DepsMap, - builtins: &'static [&'static VirtualLibrary], -} - -const MOD_HEADER: &[u8] = b"BP3D_OS_MODULE|"; - -fn parse_metadata(bytes: &[u8]) -> super::Result { - // Remove terminator NULL. - let bytes = &bytes[..bytes.len() - 1]; - let mut map = HashMap::new(); - let data = std::str::from_utf8(bytes).map_err(Error::InvalidUtf8)?; - let mut vars = data.split("|"); - vars.next(); - for var in vars { - let pos = var.find('=').ok_or(Error::InvalidMetadata)?; - let key = &var[..pos]; - let value = &var[pos + 1..]; - map.insert(key.into(), Value::new(value.into())); - } - Ok(map) -} - -fn load_metadata(path: &Path) -> super::Result { - let mut file = File::open(path).map_err(Error::Io)?; - let mut buffer: [u8; 8192] = [0; 8192]; - let mut v = Vec::new(); - while file.read(&mut buffer).map_err(Error::Io)? > 0 { - let mut slice = &buffer[..]; - while let Some(pos) = slice.iter().position(|v| *v == b'B') { - let inner = &slice[pos..]; - let end = inner - .iter() - .position(|v| *v == 0) - .unwrap_or(inner.len() - 1); - v.extend_from_slice(&inner[..end + 1]); - if v[v.len() - 1] == 0 { - if v.starts_with(MOD_HEADER) { - // We found the module metadata. - return parse_metadata(&v); - } - v.clear(); - slice = &inner[end + 1..]; - } else { - break; - } - } - } - Err(Error::MissingMetadata) -} - -struct Dependency { +pub struct Dependency { pub version: String, pub features: Vec, pub negative_features: Vec, } -struct DepsMap { +pub struct DepsMap { pub deps_by_module: HashMap>, pub module_by_dep: HashMap>, pub module_version: HashMap, @@ -131,7 +75,7 @@ impl DepsMap { version: &Value, deps: &Value, features: &Value, - ) -> super::Result<()> { + ) -> crate::module::Result<()> { let mut deps3 = Vec::new(); let mut features1: Vec = Vec::new(); if let Some(deps) = deps.parse_key_value_pairs() { @@ -186,11 +130,63 @@ impl DepsMap { } } +type DebugInit = extern "Rust" fn(engine: &'static dyn bp3d_debug::engine::Engine); + +type ModuleInit = extern "Rust" fn(engine: &'static Mutex); + +type ModuleUninit = extern "Rust" fn(); + +const MOD_HEADER: &[u8] = b"BP3D_OS_MODULE|"; + +fn parse_metadata(bytes: &[u8]) -> crate::module::Result { + // Remove terminator NULL. + let bytes = &bytes[..bytes.len() - 1]; + let mut map = HashMap::new(); + let data = std::str::from_utf8(bytes).map_err(Error::InvalidUtf8)?; + let mut vars = data.split("|"); + vars.next(); + for var in vars { + let pos = var.find('=').ok_or(Error::InvalidMetadata)?; + let key = &var[..pos]; + let value = &var[pos + 1..]; + map.insert(key.into(), Value::new(value.into())); + } + Ok(map) +} + +fn load_metadata(path: &Path) -> crate::module::Result { + let mut file = File::open(path).map_err(Error::Io)?; + let mut buffer: [u8; 8192] = [0; 8192]; + let mut v = Vec::new(); + while file.read(&mut buffer).map_err(Error::Io)? > 0 { + let mut slice = &buffer[..]; + while let Some(pos) = slice.iter().position(|v| *v == b'B') { + let inner = &slice[pos..]; + let end = inner + .iter() + .position(|v| *v == 0) + .unwrap_or(inner.len() - 1); + v.extend_from_slice(&inner[..end + 1]); + if v[v.len() - 1] == 0 { + if v.starts_with(MOD_HEADER) { + // We found the module metadata. + return parse_metadata(&v); + } + v.clear(); + slice = &inner[end + 1..]; + } else { + break; + } + } + } + Err(Error::MissingMetadata) +} + fn check_deps( deps: &Value, features: &Value, deps2: &HashMap, -) -> super::Result<()> { +) -> crate::module::Result<()> { if let Some(deps) = deps.parse_key_value_pairs() { for res in deps { let (name, version) = res?; @@ -234,7 +230,10 @@ fn check_deps( Ok(()) } -fn check_metadata(metadata: &ModuleMetadata, deps3: &mut DepsMap) -> super::Result<()> { +fn check_metadata( + metadata: &crate::module::metadata::Metadata, + deps3: &mut DepsMap, +) -> crate::module::Result<()> { if metadata.get("TYPE").ok_or(Error::InvalidMetadata)?.as_str() == "RUST" { // This symbol is optional and will not exist on C/C++ modules, only on Rust based modules. // The main reason the rustc version is checked on Rust modules is for interop with user @@ -302,11 +301,11 @@ fn check_metadata(metadata: &ModuleMetadata, deps3: &mut DepsMap) -> super::Resu Ok(()) } -unsafe fn load_lib( +pub unsafe fn load_lib( deps3: &mut DepsMap, name: &str, path: &Path, -) -> super::Result> { +) -> crate::module::Result> { let metadata = load_metadata(path)?; check_metadata(&metadata, deps3)?; let module = Module::new(OsLibrary::load(path)?, metadata); @@ -314,10 +313,10 @@ unsafe fn load_lib( Ok(module) } -unsafe fn module_open(name: &str, module: &Module) -> super::Result<()> { +unsafe fn module_open(name: &str, module: &Module) -> crate::module::Result<()> { let name = module.get_metadata_key("NAME").unwrap_or(name); let version = module.get_metadata_key("VERSION").unwrap_or("UNKNOWN"); - info!("Opening module {}-{}", name, version); + info!("Opening module {}-{}...", name, version); if module .get_metadata_key("TYPE") .ok_or(Error::InvalidMetadata)? @@ -328,6 +327,12 @@ unsafe fn module_open(name: &str, module: &Module) -> super::Resu debug!("Initializing bp3d-debug for module: {}", name); debug_init.call(bp3d_debug::engine::get()) } + let init_name = format!("bp3d_os_module_{}_init", name); + let sym: Symbol = module + .lib() + .load_symbol(init_name)? + .ok_or(Error::MissingModuleInitForRust)?; + sym.call(ModuleLoader::_instance()); } let main_name = format!("bp3d_os_module_{}_open", name); if let Some(main) = module.lib().load_symbol::(main_name)? { @@ -337,11 +342,41 @@ unsafe fn module_open(name: &str, module: &Module) -> super::Resu Ok(()) } -unsafe fn load_by_symbol( +pub unsafe fn module_close( + name: &str, + builtin: bool, + module: &Module, +) -> crate::module::Result<()> { + let name = module.get_metadata_key("NAME").unwrap_or(name); + let version = module.get_metadata_key("VERSION").unwrap_or("UNKNOWN"); + info!("Closing module {}-{}...", name, version); + if !builtin + && module + .get_metadata_key("TYPE") + .ok_or(Error::InvalidMetadata)? + == "RUST" + { + let init_name = format!("bp3d_os_module_{}_uninit", name); + let sym: Symbol = module + .lib() + .load_symbol(init_name)? + .ok_or(Error::MissingModuleInitForRust)?; + debug!("module_uninit"); + sym.call(); + } + let main_name = format!("bp3d_os_module_{}_close", &name); + if let Some(main) = unsafe { module.lib().load_symbol::(main_name)? } { + debug!("module_close"); + main.call(); + } + Ok(()) +} + +pub unsafe fn load_by_symbol( lib: L, name: &str, deps: &mut DepsMap, -) -> super::Result> { +) -> crate::module::Result> { let mod_const_name = format!("BP3D_OS_MODULE_{}", name.to_uppercase()); if let Some(sym) = lib.load_symbol::<*const c_char>(mod_const_name)? { let bytes = CStr::from_ptr((*sym.as_ptr()).offset(1)).to_bytes_with_nul(); @@ -354,221 +389,9 @@ unsafe fn load_by_symbol( Err(Error::NotFound(name.into())) } -impl Default for ModuleLoader { - fn default() -> Self { - let mut this = ModuleLoader { - paths: Default::default(), - modules: Default::default(), - deps: DepsMap::new(), - builtin_modules: Default::default(), - builtins: &[], - }; - this.add_public_dependency(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"), ["*"]); - this.add_public_dependency("bp3d-debug", "1.0.0", ["*"]); - this - } -} - -impl ModuleLoader { - /// Create a new instance of a [ModuleLoader]. - // Apparently clippy prefers code duplication, well I said no... - #[allow(clippy::field_reassign_with_default)] - pub fn new(builtins: &'static [&'static VirtualLibrary]) -> ModuleLoader { - let mut def = Self::default(); - def.builtins = builtins; - def - } - - /// Attempts to load the given builtin module from its name. - /// - /// # Arguments - /// - /// * `name`: the name of the builtin module to load. - /// - /// returns: Result<&Module, Error> - /// - /// # Safety - /// - /// This function assumes the module to be loaded, if it exists has the correct format otherwise - /// this function is UB. - pub unsafe fn load_builtin(&mut self, name: &str) -> super::Result<&Module> { - debug!("Loading builtin module: {}", name); - let name = name.replace("-", "_"); - if self.builtin_modules.contains_key(&name) { - Ok(unsafe { self.builtin_modules.get(&name).unwrap_unchecked() }) - } else { - for builtin in self.builtins { - if builtin.name() == name { - let module = unsafe { load_by_symbol(**builtin, &name, &mut self.deps) } - .map_err(|e| match e { - Error::NotFound(_) => Error::MissingMetadata, - e => e, - })?; - return Ok(self.builtin_modules.entry(name).or_insert(module)); - } - } - Err(Error::NotFound(name)) - } - } - - /// Attempts to load a module from the specified name which is dynamically linked in the current - /// running software. - /// - /// # Arguments - /// - /// * `name`: the name of the module to be loaded. - /// - /// returns: Result<&Module, Error> - /// - /// # Safety - /// - /// This function assumes the module to be loaded, if it exists has the correct format otherwise - /// this function is UB. - pub unsafe fn load_self(&mut self, name: &str) -> super::Result<&Module> { - debug!("Loading static module: {}", name); - let name = name.replace("-", "_"); - if self.modules.contains_key(&name) { - unsafe { Ok(self.modules.get(&name).unwrap_unchecked()) } - } else { - let this = OsLibrary::open_self()?; - let module = unsafe { load_by_symbol(this, &name, &mut self.deps) }?; - Ok(self.modules.entry(name).or_insert(module)) - } - } - - /// Attempts to load a module from the specified name. - /// - /// This function already does check for the version of rustc and dependencies for Rust based - /// modules to ensure maximum ABI compatibility. - /// - /// This function assumes the code to be loaded is trusted and delegates this operation to the - /// underlying OS. - /// - /// # Arguments - /// - /// * `name`: the name of the module to be loaded. - /// - /// returns: () - /// - /// # Safety - /// - /// It is assumed that the module is intended to be used with this instance of [ModuleLoader]; - /// if not, this function is UB. Additionally, if some dependency used in public facing APIs - /// for the module are not added with [add_public_dependency](Self::add_public_dependency), - /// this is also UB. - pub unsafe fn load(&mut self, name: &str) -> super::Result<&Module> { - debug!("Loading dynamic module: {}", name); - let name = name.replace("-", "_"); - if self.modules.contains_key(&name) { - Ok(self.modules.get(&name).unwrap_unchecked()) - } else { - let name2 = format!("{}.{}", name, OS_EXT); - let name3 = format!("lib{}.{}", name, OS_EXT); - for path in self.paths.iter() { - let search = path.join(&name2); - let search2 = path.join(&name3); - let mut module = None; - if search.exists() { - module = Some(load_lib(&mut self.deps, &name, &search)?); - } else if search2.exists() { - module = Some(load_lib(&mut self.deps, &name, &search2)?); - } - if let Some(module) = module { - self.modules.insert(name.clone(), module); - return Ok(&self.modules[&name]); - } - } - Err(Error::NotFound(name)) - } - } - - /// Attempts to unload the given module. - /// - /// # Arguments - /// - /// * `name`: the name of the module to unload. - /// - /// returns: () - pub fn unload(&mut self, name: &str) -> super::Result<()> { - debug!("Closing module: {}", name); - let name = name.replace("-", "_"); - if self.modules.contains_key(&name) { - let module = unsafe { self.modules.remove(&name).unwrap_unchecked() }; - let main_name = format!("bp3d_os_module_{}_close", &name); - if let Some(main) = unsafe { module.lib().load_symbol::(main_name)? } { - main.call(); - } - drop(module); - } else { - let module = self - .builtin_modules - .remove(&name) - .ok_or_else(|| Error::NotFound(name.clone()))?; - let main_name = format!("bp3d_os_module_{}_close", &name); - if let Some(main) = unsafe { module.lib().load_symbol::(main_name)? } { - main.call(); - } - } - Ok(()) - } - - /// Adds the given path to the path search list. - /// - /// # Arguments - /// - /// * `path`: the path to include. - /// - /// returns: () - pub fn add_search_path(&mut self, path: impl AsRef) { - self.paths.push(path.as_ref().into()); - } - - /// Adds a public facing API dependency to the list of dependency for version checks. - /// - /// This is used to check if there are any ABI incompatibilities between dependency versions - /// when loading a Rust based module. - /// - /// # Arguments - /// - /// * `name`: the name of the dependency. - /// * `version`: the version of the dependency. - /// - /// returns: () - pub fn add_public_dependency<'a>( - &mut self, - name: &str, - version: &str, - features: impl IntoIterator, - ) { - let mut negative_features = Vec::new(); - let features = features - .into_iter() - .filter_map(|s| { - if s.starts_with("-") { - negative_features.push(s.into()); - return None; - } - if s != "*" { - Some(String::from(name) + s) - } else { - Some("*".into()) - } - }) - .collect(); - self.deps.add_dep( - name.replace("-", "_"), - Dependency { - version: version.into(), - features, - negative_features, - }, - ) - } -} - #[cfg(test)] mod tests { - use crate::module::loader::{check_metadata, Dependency, DepsMap}; + use super::{check_metadata, Dependency, DepsMap}; use crate::module::metadata::{Metadata, Value}; use crate::module::RUSTC_VERSION; diff --git a/core/src/module/mod.rs b/core/src/module/mod.rs index 3b018b8..180c006 100644 --- a/core/src/module/mod.rs +++ b/core/src/module/mod.rs @@ -37,7 +37,7 @@ pub type Result = std::result::Result; pub mod error; -mod loader; +pub mod loader; #[allow(clippy::module_inception)] mod module; @@ -46,8 +46,6 @@ pub mod library; pub mod metadata; -pub use loader::ModuleLoader; - pub use module::Module; /// Link the set of modules statically. diff --git a/core/src/module/module.rs b/core/src/module/module.rs index c15be2c..b854e53 100644 --- a/core/src/module/module.rs +++ b/core/src/module/module.rs @@ -35,6 +35,8 @@ use std::fmt::{Debug, Display, Formatter}; pub struct Module { lib: L, metadata: Metadata, + pub(super) id: usize, + pub(super) ref_count: usize, } impl Display for Module { @@ -53,7 +55,12 @@ impl Module { /// /// returns: Module pub fn new(lib: L, metadata: Metadata) -> Self { - Module { lib, metadata } + Module { + lib, + metadata, + id: 0, + ref_count: 1, + } } /// Gets a metadata key by its name. diff --git a/core/src/shell/core.rs b/core/src/shell/core.rs index c5eb9f8..337dcd2 100644 --- a/core/src/shell/core.rs +++ b/core/src/shell/core.rs @@ -32,6 +32,8 @@ use crate::shell_println; use std::sync::mpsc; use std::thread::JoinHandle; +//FIXME: The shift key is broken under windows. + /// Represents an event emitted from the input abstraction. pub enum Event { /// A command string was submitted to the application. diff --git a/module_test/test_mod/Cargo.toml b/module_test/test_mod/Cargo.toml index 1b383c8..ed29b1c 100644 --- a/module_test/test_mod/Cargo.toml +++ b/module_test/test_mod/Cargo.toml @@ -11,5 +11,5 @@ crate-type = ["rlib", "cdylib"] bp3d-os-build = { version = "1.0.0", path = "../../build" } [dependencies] -bp3d-os = { version = "1.0.2", path = "../../core", features = ["module"] } +bp3d-os = { version = "2.0.0", path = "../../core", features = ["module"] } bp3d-debug = "1.0.0" diff --git a/module_test/test_mod/tests/test_static_modules.rs b/module_test/test_mod/tests/test_static_modules.rs index 5c377fb..4c608a3 100644 --- a/module_test/test_mod/tests/test_static_modules.rs +++ b/module_test/test_mod/tests/test_static_modules.rs @@ -26,18 +26,23 @@ // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use bp3d_os::module::ModuleLoader; +use bp3d_os::module::loader::ModuleLoader; bp3d_os::link_modules!(test_mod); #[test] fn test_static_modules() { - let mut loader = ModuleLoader::new(BUILTIN_MODULES); + ModuleLoader::install(BUILTIN_MODULES); + let mut loader = ModuleLoader::lock(); println!("Running simple module load/unload test"); unsafe { loader.load_builtin("test-mod").unwrap() }; loader.unload("test-mod").unwrap(); println!("Running module load twice test followed by unload"); unsafe { loader.load_builtin("test-mod").unwrap() }; - unsafe { loader.load_builtin("test-mod").unwrap() }; - loader.unload("test-mod").unwrap(); + unsafe { loader.load_builtin("test-mod").unwrap() }; // ref-count becomes 2 + loader.unload("test-mod").unwrap(); // ref-count becomes 1. + loader.unload("test-mod").unwrap(); //This call should actually unload as ref-count will be back to 0. + loader.unload("test-mod").unwrap_err(); + drop(loader); + ModuleLoader::uninstall(); } diff --git a/module_test/testbin/Cargo.toml b/module_test/testbin/Cargo.toml index 0752f1f..f81e2be 100644 --- a/module_test/testbin/Cargo.toml +++ b/module_test/testbin/Cargo.toml @@ -5,4 +5,4 @@ edition = "2024" publish = false [dependencies] -bp3d-os = { version = "1.0.0-rc.4.2.1", path = "../../core", features = ["module"] } +bp3d-os = { version = "2.0.0", path = "../../core", features = ["module"] } diff --git a/module_test/testbin/src/main.rs b/module_test/testbin/src/main.rs index dc02c87..fd6b12b 100644 --- a/module_test/testbin/src/main.rs +++ b/module_test/testbin/src/main.rs @@ -26,16 +26,26 @@ // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use bp3d_os::module::ModuleLoader; +use bp3d_os::module::loader::ModuleLoader; -fn main() { - let mut loader = ModuleLoader::default(); +fn run_module_test() { + ModuleLoader::install_default(); + let mut loader = ModuleLoader::lock(); loader.add_search_path("./target/debug/"); println!("Running simple module load/unload test"); unsafe { loader.load("test-mod").unwrap() }; loader.unload("test-mod").unwrap(); println!("Running module load twice test followed by unload"); unsafe { loader.load("test-mod").unwrap() }; - unsafe { loader.load("test-mod").unwrap() }; - loader.unload("test-mod").unwrap(); + unsafe { loader.load("test-mod").unwrap() }; // ref-count becomes 2. + loader.unload("test-mod").unwrap(); // ref-count becomes 1. + loader.unload("test-mod").unwrap(); //This call should actually unload as ref-count will be back to 0. + loader.unload("test-mod").unwrap_err(); + drop(loader); + ModuleLoader::uninstall(); +} + +fn main() { + run_module_test(); + run_module_test(); } diff --git a/shelltestbin/Cargo.toml b/shelltestbin/Cargo.toml index fea9688..3ad165e 100644 --- a/shelltestbin/Cargo.toml +++ b/shelltestbin/Cargo.toml @@ -5,4 +5,4 @@ edition = "2024" publish = false [dependencies] -bp3d-os = { version = "1.0.0-rc.4.5.0", path = "../core", features = ["shell"] } +bp3d-os = { version = "2.0.0", path = "../core", features = ["shell"] }