Skip to main content

player_plugin_loader/
native_library.rs

1use std::mem::ManuallyDrop;
2use std::path::Path;
3use std::sync::Arc;
4
5use libloading::Library;
6use thiserror::Error;
7
8use crate::native_abi::CheckedPluginRoot;
9use crate::{LoadedNativePlugin, NativePluginContractError};
10
11#[derive(Debug, Error)]
12pub enum PluginLoadError {
13    #[error("failed to open plugin library at {path}: {source}")]
14    OpenLibrary {
15        path: String,
16        #[source]
17        source: libloading::Error,
18    },
19    #[error("failed to resolve plugin entry symbol `{symbol}`: {source}")]
20    ResolveEntrySymbol {
21        symbol: &'static str,
22        #[source]
23        source: libloading::Error,
24    },
25    #[error("native plugin root violates the ABI contract: {0}")]
26    NativeContract(#[from] NativePluginContractError),
27}
28
29impl LoadedNativePlugin {
30    /// Loads and validates one unsigned native plugin for explicit
31    /// development or inspection workflows.
32    ///
33    /// The mapped library remains alive for the process lifetime, including
34    /// when symbol resolution or root validation fails after `dlopen`.
35    pub fn load_development(path: impl AsRef<Path>) -> Result<Self, PluginLoadError> {
36        let path = path.as_ref();
37        tracing::warn!(
38            path = %path.display(),
39            "loading an unsigned raw Native plugin library under explicit development policy"
40        );
41        Self::load_unchecked(path)
42    }
43
44    pub(crate) fn load_host_verified(path: impl AsRef<Path>) -> Result<Self, PluginLoadError> {
45        Self::load_unchecked(path.as_ref())
46    }
47
48    fn load_unchecked(path: &Path) -> Result<Self, PluginLoadError> {
49        let path_string = path.display().to_string();
50        // SAFETY: the caller selects the native library. It is immediately
51        // placed in `LibraryHolder`, whose process-lifetime policy keeps all
52        // code and callback pointers mapped.
53        let library =
54            unsafe { Library::new(path) }.map_err(|source| PluginLoadError::OpenLibrary {
55                path: path_string,
56                source,
57            })?;
58        let library = Arc::new(LibraryHolder {
59            library: ManuallyDrop::new(library),
60        });
61
62        // SAFETY: the symbol name and signature come from the raw native ABI
63        // crate. There is deliberately no legacy-symbol fallback.
64        let entry = unsafe {
65            library
66                .library
67                .get::<player_plugin_abi::VesperPluginEntryPoint>(
68                    player_plugin_abi::VESPER_PLUGIN_ENTRY_SYMBOL,
69                )
70        }
71        .map_err(|source| PluginLoadError::ResolveEntrySymbol {
72            symbol: player_plugin_abi::VESPER_PLUGIN_ENTRY_SYMBOL_NAME,
73            source,
74        })?;
75
76        // SAFETY: the resolved plugin entry transfers one root owner to the host.
77        let root = unsafe { entry() };
78        let checked =
79            // SAFETY: the entry contract permits null or a readable native root;
80            // validation owns all subsequent pointer and size checks.
81            unsafe { CheckedPluginRoot::from_raw(root, Some(library)) }?;
82        Ok(Self::from_checked(checked))
83    }
84}
85
86#[derive(Debug)]
87pub(crate) struct LibraryHolder {
88    // Plugins may register thread-local destructors through native
89    // dependencies. Keep the library mapped for the process lifetime.
90    #[allow(dead_code)]
91    pub(crate) library: ManuallyDrop<Library>,
92}