pub mod config;
pub(crate) mod context;
pub mod ffi;
pub(crate) mod isolation;
pub mod loader;
pub use config::PythonConfig;
pub use loader::PythonLoaderData;
use std::collections::HashMap;
use std::ffi::CString;
use std::path::Path;
use std::sync::Mutex;
use std::sync::PoisonError;
use pyo3::Bound;
use pyo3::PyAny;
use pyo3::Python;
use pyo3::types::PyAnyMethods;
use pyo3::types::PyDict;
use pyo3::types::PyDictMethods;
use pyo3::types::PyModule;
use polyplug::error::LoaderError;
use polyplug::loader::BundleLoader;
use polyplug::loader::BundleSource;
use polyplug::loader::ManifestData;
use polyplug::runtime::Runtime;
use polyplug_abi::BundleInitContext;
use polyplug_abi::HostApi;
use polyplug_abi::StringView;
use polyplug_abi::SupportedLanguage;
use polyplug_utils::BundleId;
use crate::context::ensure_python_initialized;
use crate::loader::ContractRegistration;
pub struct PythonLoader {
config: PythonConfig,
module_prefixes: Mutex<HashMap<BundleId, Vec<String>>>,
}
impl PythonLoader {
pub fn new(config: PythonConfig) -> PythonLoader {
PythonLoader {
config,
module_prefixes: Mutex::new(HashMap::new()),
}
}
fn track_module_prefix(&self, bundle_id: u64, prefix: String) {
let mut map: std::sync::MutexGuard<'_, HashMap<BundleId, Vec<String>>> = self
.module_prefixes
.lock()
.unwrap_or_else(PoisonError::into_inner);
map.entry(BundleId::from_u64(bundle_id))
.or_default()
.push(prefix);
}
}
impl Default for PythonLoader {
fn default() -> PythonLoader {
PythonLoader::new(PythonConfig::default())
}
}
fn synthetic_module_name(bundle_name: &str) -> String {
let mut name: String = String::with_capacity(bundle_name.len() + 1);
for ch in bundle_name.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' {
name.push(ch);
} else {
name.push('_');
}
}
if name.is_empty() {
name.push_str("polyplug_inline_bundle");
} else if name
.chars()
.next()
.is_some_and(|c: char| c.is_ascii_digit())
{
name.insert(0, '_');
}
name
}
impl PythonLoader {
fn collect_and_register(
py: Python<'_>,
init_ret: &Bound<'_, PyAny>,
host_interface: *const HostApi,
bundle_name: &str,
) -> Result<(), LoaderError> {
let registrations: Vec<ContractRegistration> =
loader::collect_registrations(py, init_ret, bundle_name)?;
loader::register_contracts(registrations, host_interface, bundle_name)?;
Ok(())
}
fn load_from_source_text(
&self,
manifest: &ManifestData,
code: &str,
runtime: &Runtime,
) -> Result<(), LoaderError> {
ensure_python_initialized(&self.config)?;
let bundle_name: String = synthetic_module_name(&manifest.name);
let bundle_id: u64 = manifest.id;
let code_c: CString =
CString::new(code).map_err(|e: std::ffi::NulError| LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!("Python source contained an interior nul byte: {}", e),
})?;
let file_name_c: CString =
CString::new(format!("{}.py", bundle_name)).map_err(|e: std::ffi::NulError| {
LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!("synthetic file name contained an interior nul byte: {}", e),
}
})?;
let module_name_c: CString =
CString::new(bundle_name.as_str()).map_err(|e: std::ffi::NulError| {
LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!(
"synthetic module name contained an interior nul byte: {}",
e
),
}
})?;
let host_interface: *const HostApi = runtime.as_context_ptr();
runtime.push_init_bundle_id(bundle_id);
let _load_guard: std::sync::MutexGuard<'_, ()> = crate::context::acquire_load_lock();
let result: Result<String, LoaderError> = Python::attach(|py| {
let modules_before: std::collections::HashSet<String> =
crate::isolation::snapshot_loaded_modules(py, &bundle_name)?;
let module: pyo3::Bound<'_, PyModule> =
PyModule::from_code(py, &code_c, &file_name_c, &module_name_c).map_err(
|e: pyo3::PyErr| LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!("inline module compile/exec failed: {}", e),
},
)?;
let init_fn: pyo3::Bound<'_, pyo3::PyAny> =
module
.getattr("polyplug_init")
.map_err(|_| LoaderError::InitSymbolMissing {
bundle: bundle_name.clone(),
})?;
let bundle_path_static: &'static str = Box::leak(String::new().into_boxed_str());
let ctx: BundleInitContext = BundleInitContext {
bundle_id,
bundle_path: StringView {
ptr: bundle_path_static.as_ptr(),
len: bundle_path_static.len(),
},
};
let host_interface_i64: i64 = host_interface as usize as i64;
let ctx_ptr: i64 = &ctx as *const BundleInitContext as i64;
let init_ret: Bound<'_, PyAny> = init_fn
.call((host_interface_i64, ctx_ptr), None)
.map_err(|e: pyo3::PyErr| LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!("polyplug_init call failed: {}", e),
})?;
Self::collect_and_register(py, &init_ret, host_interface, &bundle_name)?;
let prefix: String = crate::isolation::isolate_bundle_modules(
py,
&bundle_name,
bundle_id,
"",
&modules_before,
)?;
Ok::<String, LoaderError>(prefix)
});
runtime.pop_init_bundle_id();
let prefix: String = result?;
self.track_module_prefix(bundle_id, prefix);
Ok(())
}
}
impl BundleLoader for PythonLoader {
fn loader_name(&self) -> &'static str {
"python"
}
fn loader_language(&self) -> SupportedLanguage {
SupportedLanguage::Python
}
fn supports_hot_reload(&self) -> bool {
false
}
fn load(
&self,
manifest: &ManifestData,
source: &BundleSource,
runtime: &Runtime,
) -> Result<(), LoaderError> {
match source {
BundleSource::Path(_) => {}
BundleSource::Code(code) => {
return self.load_from_source_text(manifest, code, runtime);
}
BundleSource::Bytes(bytes) => {
let code: &str = core::str::from_utf8(bytes).map_err(|_| {
LoaderError::InvalidSourceEncoding {
loader: "python",
source_kind: source.kind(),
bundle: manifest.name.clone(),
}
})?;
return self.load_from_source_text(manifest, code, runtime);
}
}
let bundle_path: std::path::PathBuf = if !manifest.file.is_empty() {
manifest.path.join(&manifest.file)
} else {
return Err(LoaderError::ManifestMissingFile {
bundle: manifest.name.clone(),
});
};
if !bundle_path.exists() {
return Err(LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!(
"failed to import Python module at {}: file does not exist",
bundle_path.to_string_lossy()
),
});
}
ensure_python_initialized(&self.config)?;
let abs_path: std::path::PathBuf = bundle_path.canonicalize().map_err(|_| {
LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!(
"failed to canonicalize Python module path {}: path does not exist or is not accessible",
bundle_path.to_string_lossy()
),
}
})?;
let bundle_name: String = abs_path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
let bundle_dir: &Path = &manifest.path;
let bundle_dir_str: String = bundle_dir.to_string_lossy().into_owned();
let bundle_id: u64 = manifest.id;
let host_interface: *const HostApi = runtime.as_context_ptr();
runtime.push_init_bundle_id(bundle_id);
let _load_guard: std::sync::MutexGuard<'_, ()> = crate::context::acquire_load_lock();
let prefix: String = Python::attach(|py| {
let sys_mod: pyo3::Bound<'_, PyModule> =
PyModule::import(py, "sys").map_err(|e: pyo3::PyErr| LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!("Python sys import failed: {}", e),
})?;
let sys_path: pyo3::Bound<'_, pyo3::PyAny> =
sys_mod
.getattr("path")
.map_err(|e: pyo3::PyErr| LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!("Python sys.path get failed: {}", e),
})?;
sys_path
.call_method1("insert", (0usize, bundle_dir_str.as_str()))
.map_err(|e: pyo3::PyErr| LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!("sys.path insert failed: {}", e),
})?;
let site_pkgs: std::path::PathBuf = bundle_dir.join("site-packages");
if site_pkgs.exists() {
let sp: String = site_pkgs.to_string_lossy().into_owned();
sys_path
.call_method1("insert", (0usize, sp.as_str()))
.map_err(|e: pyo3::PyErr| LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!("site-packages path insert failed: {}", e),
})?;
}
let importlib_util: pyo3::Bound<'_, PyModule> = PyModule::import(py, "importlib.util")
.map_err(|e| LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!("importlib.util import failed: {}", e),
})?;
let spec: pyo3::Bound<'_, pyo3::PyAny> = importlib_util
.getattr("spec_from_file_location")
.map_err(|e| LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!("spec_from_file_location not found: {}", e),
})?
.call1((&bundle_name, abs_path.to_string_lossy().as_ref()))
.map_err(|e| LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!(
"spec_from_file_location call failed for {}: {}",
abs_path.to_string_lossy(),
e
),
})?;
let module_from_spec: pyo3::Bound<'_, pyo3::PyAny> = importlib_util
.getattr("module_from_spec")
.map_err(|e| LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!("module_from_spec not found: {}", e),
})?
.call1((&spec,))
.map_err(|e| LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!(
"module_from_spec call failed for {}: {}",
abs_path.to_string_lossy(),
e
),
})?;
let modules_before: std::collections::HashSet<String> =
crate::isolation::snapshot_loaded_modules(py, &bundle_name)?;
spec.getattr("loader")
.and_then(|loader: pyo3::Bound<'_, pyo3::PyAny>| loader.getattr("exec_module"))
.and_then(|exec_module: pyo3::Bound<'_, pyo3::PyAny>| {
exec_module.call1((&module_from_spec,))
})
.map_err(|e| LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!(
"exec_module failed for {}: {}",
abs_path.to_string_lossy(),
e
),
})?;
let init_fn: pyo3::Bound<'_, pyo3::PyAny> =
module_from_spec.getattr("polyplug_init").map_err(|_| {
LoaderError::InitSymbolMissing {
bundle: bundle_name.clone(),
}
})?;
let bundle_path_static: &'static str =
Box::leak(bundle_dir_str.clone().into_boxed_str());
let ctx: BundleInitContext = BundleInitContext {
bundle_id,
bundle_path: StringView {
ptr: bundle_path_static.as_ptr(),
len: bundle_path_static.len(),
},
};
let host_interface_i64: i64 = host_interface as usize as i64;
let ctx_ptr: i64 = &ctx as *const BundleInitContext as i64;
let init_ret: Bound<'_, PyAny> = init_fn
.call((host_interface_i64, ctx_ptr), None)
.map_err(|e: pyo3::PyErr| LoaderError::InitFailed {
bundle: bundle_name.clone(),
error: format!("polyplug_init call failed: {}", e),
})?;
Self::collect_and_register(py, &init_ret, host_interface, &bundle_name)?;
let prefix: String = crate::isolation::isolate_bundle_modules(
py,
&bundle_name,
bundle_id,
&bundle_dir_str,
&modules_before,
)?;
Ok::<String, LoaderError>(prefix)
})?;
runtime.pop_init_bundle_id();
self.track_module_prefix(bundle_id, prefix);
Ok(())
}
fn reload(&self, _manifest: &ManifestData, _runtime: &Runtime) -> Result<(), LoaderError> {
Err(LoaderError::HotReloadUnsupported {
loader_name: self.loader_name().to_owned(),
})
}
fn unload(&self, bundle_id: BundleId, _runtime: &Runtime) -> Result<(), LoaderError> {
let prefixes: Vec<String> = {
let mut map: std::sync::MutexGuard<'_, HashMap<BundleId, Vec<String>>> = self
.module_prefixes
.lock()
.unwrap_or_else(PoisonError::into_inner);
match map.remove(&bundle_id) {
Some(p) => p,
None => return Ok(()),
}
};
for prefix in prefixes {
purge_prefix_from_sys_modules(&prefix)?;
}
Ok(())
}
}
fn purge_prefix_from_sys_modules(prefix: &str) -> Result<(), LoaderError> {
let dotted: String = format!("{}.", prefix);
Python::attach(|py: Python<'_>| -> Result<(), LoaderError> {
let sys_mod: Bound<'_, PyModule> =
PyModule::import(py, "sys").map_err(|e: pyo3::PyErr| LoaderError::InitFailed {
bundle: "python".to_owned(),
error: format!("Python sys import failed during unload purge: {}", e),
})?;
let modules: Bound<'_, PyAny> =
sys_mod
.getattr("modules")
.map_err(|e: pyo3::PyErr| LoaderError::InitFailed {
bundle: "python".to_owned(),
error: format!("sys.modules access failed during unload purge: {}", e),
})?;
let dict: Bound<'_, PyDict> =
modules
.cast_into::<PyDict>()
.map_err(|_| LoaderError::InitFailed {
bundle: "python".to_owned(),
error: "sys.modules is not a dict during unload purge".to_owned(),
})?;
let mut to_delete: Vec<Bound<'_, PyAny>> = Vec::new();
for (key, _value) in dict.iter() {
let key_str: String = match key.extract::<String>() {
Ok(s) => s,
Err(_) => continue,
};
if key_str == prefix || key_str.starts_with(&dotted) {
to_delete.push(key);
}
}
for key in to_delete {
let _ = dict.del_item(&key);
}
Ok(())
})
}