use std::{
env,
error::Error,
ffi::CString,
fmt,
mem::MaybeUninit,
os::unix::ffi::OsStrExt,
path::PathBuf,
ptr::{null, null_mut},
sync::atomic::{AtomicBool, Ordering},
};
pub use pyo3;
use pyo3::{ffi, prelude::*};
static STDLIB_BLOB: &[u8] = include_bytes!(env!("OMP_STDLIB_BLOB"));
static OMP_MODULES_BLOB: &[u8] = include_bytes!(env!("OMP_PY_MODULES_BLOB"));
pub const THIRD_PARTY_LICENSES: &str = include_str!("../THIRD-PARTY-NOTICES.txt");
static INITIALIZED: AtomicBool = AtomicBool::new(false);
#[derive(Debug)]
#[non_exhaustive]
pub enum InitError {
AlreadyInitialized,
InvalidPath(PathBuf),
}
impl fmt::Display for InitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AlreadyInitialized => f.write_str("python engine already initialized"),
Self::InvalidPath(p) => write!(f, "search path contains NUL byte: {}", p.display()),
}
}
}
impl Error for InitError {}
#[derive(Debug, Default)]
#[must_use = "call .init() to boot the interpreter"]
pub struct Builder {
site_packages: Option<PathBuf>,
}
impl Builder {
pub fn site_packages(mut self, dir: impl Into<PathBuf>) -> Self {
self.site_packages = Some(dir.into());
self
}
pub fn init(self) -> Result<Engine, InitError> {
if INITIALIZED.swap(true, Ordering::SeqCst) {
return Err(InitError::AlreadyInitialized);
}
let site = self.site_packages.unwrap_or_else(default_site_packages);
let site_c = CString::new(site.as_os_str().as_bytes())
.map_err(|_| InitError::InvalidPath(site.clone()))?;
install_frozen_modules();
init_python(&site_c);
Ok(Engine { _priv: () })
}
}
#[derive(Debug)]
pub struct Engine {
_priv: (),
}
impl Engine {
pub fn builder() -> Builder {
Builder::default()
}
pub fn attach<F, R>(&self, f: F) -> R
where
F: for<'py> FnOnce(Python<'py>) -> R,
{
Python::attach(f)
}
}
pub fn default_site_packages() -> PathBuf {
env::var_os("OMP_PY_SITE").map_or_else(
|| {
env::home_dir()
.map_or_else(env::temp_dir, |home| home.join(".local/share/omp-py"))
.join("site-packages")
},
PathBuf::from,
)
}
fn install_frozen_modules() {
let mut table = Vec::new();
for blob in [STDLIB_BLOB, OMP_MODULES_BLOB] {
let count = u32::from_le_bytes(blob[..4].try_into().unwrap()) as usize;
table.reserve(count + 1);
let mut rest = &blob[4..];
for _ in 0..count {
let name_len = u16::from_le_bytes(rest[..2].try_into().unwrap()) as usize;
let is_pkg = rest[2];
let code_len = u32::from_le_bytes(rest[3..7].try_into().unwrap()) as usize;
let (name, code) = (&rest[7..7 + name_len], &rest[7 + name_len..7 + name_len + code_len]);
assert_eq!(name[name_len - 1], 0, "blob names must be NUL-terminated");
table.push(ffi::_frozen {
name: name.as_ptr().cast(),
code: code.as_ptr(),
size: i32::try_from(code_len).unwrap(),
is_package: i32::from(is_pkg),
});
rest = &rest[7 + name_len + code_len..];
}
}
table.push(ffi::_frozen {
name: null(),
code: null(),
size: 0,
is_package: 0,
});
unsafe {
ffi::PyImport_FrozenModules = Vec::leak(table).as_ptr();
}
}
fn check(status: ffi::PyStatus) {
unsafe {
if ffi::PyStatus_Exception(status) != 0 {
ffi::Py_ExitStatusException(status);
}
}
}
fn init_python(site_packages: &CString) {
unsafe {
let mut config = MaybeUninit::<ffi::PyConfig>::uninit();
ffi::PyConfig_InitIsolatedConfig(config.as_mut_ptr());
let config = config.as_mut_ptr();
(*config).site_import = 0;
(*config).write_bytecode = 0;
(*config).buffered_stdio = 0;
(*config).use_frozen_modules = 1;
check(ffi::PyConfig_SetBytesString(
config,
&raw mut (*config).program_name,
c"omp-py".as_ptr(),
));
check(ffi::PyConfig_SetBytesString(
config,
&raw mut (*config).stdio_encoding,
c"utf-8".as_ptr(),
));
(*config).module_search_paths_set = 1;
let wide = ffi::Py_DecodeLocale(site_packages.as_ptr(), null_mut());
assert!(!wide.is_null(), "failed to decode search path");
let status = ffi::PyWideStringList_Append(&raw mut (*config).module_search_paths, wide);
ffi::PyMem_RawFree(wide.cast());
check(status);
let status = ffi::Py_InitializeFromConfig(config);
ffi::PyConfig_Clear(config);
check(status);
}
}