use {
crate::{
config::{OxidizedPythonInterpreterConfig, ResolvedOxidizedPythonInterpreterConfig},
conversion::osstring_to_bytes,
error::NewInterpreterError,
osutils::resolve_terminfo_dirs,
pyalloc::PythonMemoryAllocator,
},
once_cell::sync::Lazy,
oxidized_importer::{
install_path_hook, remove_external_importers, replace_meta_path_importers, ImporterState,
OxidizedFinder, PyInit_oxidized_importer, PythonResourcesState, OXIDIZED_IMPORTER_NAME,
OXIDIZED_IMPORTER_NAME_STR,
},
pyo3::{
exceptions::PyRuntimeError, ffi as pyffi, prelude::*, types::PyDict, AsPyPointer,
PyTypeInfo,
},
python_packaging::interpreter::{MultiprocessingStartMethod, TerminfoResolution},
std::{
collections::BTreeSet,
env, fs,
io::Write,
os::raw::c_char,
path::{Path, PathBuf},
},
};
static GLOBAL_INTERPRETER_GUARD: Lazy<std::sync::Mutex<()>> =
Lazy::new(|| std::sync::Mutex::new(()));
pub struct MainPythonInterpreter<'interpreter, 'resources: 'interpreter> {
config: ResolvedOxidizedPythonInterpreterConfig<'resources>,
interpreter_guard: Option<std::sync::MutexGuard<'interpreter, ()>>,
pub(crate) allocator: Option<PythonMemoryAllocator>,
write_modules_path: Option<PathBuf>,
}
impl<'interpreter, 'resources> MainPythonInterpreter<'interpreter, 'resources> {
pub fn new(
config: OxidizedPythonInterpreterConfig<'resources>,
) -> Result<MainPythonInterpreter<'interpreter, 'resources>, NewInterpreterError> {
let config: ResolvedOxidizedPythonInterpreterConfig<'resources> = config.try_into()?;
match config.terminfo_resolution {
TerminfoResolution::Dynamic => {
if let Some(v) = resolve_terminfo_dirs() {
env::set_var("TERMINFO_DIRS", v);
}
}
TerminfoResolution::Static(ref v) => {
env::set_var("TERMINFO_DIRS", v);
}
TerminfoResolution::None => {}
}
let mut res = MainPythonInterpreter {
config,
interpreter_guard: None,
allocator: None,
write_modules_path: None,
};
res.init()?;
Ok(res)
}
fn init(&mut self) -> Result<(), NewInterpreterError> {
assert!(self.interpreter_guard.is_none());
self.interpreter_guard = Some(GLOBAL_INTERPRETER_GUARD.lock().map_err(|_| {
NewInterpreterError::Simple("unable to acquire global interpreter guard")
})?);
if let Some(tcl_library) = &self.config.tcl_library {
std::env::set_var("TCL_LIBRARY", tcl_library);
}
set_pyimport_inittab(&self.config);
let pre_config = pyffi::PyPreConfig::try_from(&self.config)?;
unsafe {
let status = pyffi::Py_PreInitialize(&pre_config);
if pyffi::PyStatus_Exception(status) != 0 {
return Err(NewInterpreterError::new_from_pystatus(
&status,
"Python pre-initialization",
));
}
};
self.allocator = PythonMemoryAllocator::from_backend(self.config.allocator_backend);
if let Some(allocator) = &self.allocator {
if self.config.allocator_raw {
allocator.set_allocator(pyffi::PyMemAllocatorDomain::PYMEM_DOMAIN_RAW);
}
if self.config.allocator_mem {
allocator.set_allocator(pyffi::PyMemAllocatorDomain::PYMEM_DOMAIN_MEM);
}
if self.config.allocator_obj {
allocator.set_allocator(pyffi::PyMemAllocatorDomain::PYMEM_DOMAIN_OBJ);
}
if self.config.allocator_pymalloc_arena {
if self.config.allocator_mem || self.config.allocator_obj {
return Err(NewInterpreterError::Simple("A custom pymalloc arena allocator cannot be used with custom `mem` or `obj` domain allocators"));
}
allocator.set_arena_allocator();
}
}
if self.config.allocator_debug {
unsafe {
pyffi::PyMem_SetupDebugHooks();
}
}
let mut py_config: pyffi::PyConfig = (&self.config).try_into()?;
py_config._init_main = 0;
let status = unsafe { pyffi::Py_InitializeFromConfig(&py_config) };
if unsafe { pyffi::PyStatus_Exception(status) } != 0 {
return Err(NewInterpreterError::new_from_pystatus(
&status,
"initializing Python core",
));
}
debug_assert_eq!(unsafe { pyffi::PyGILState_Check() }, 1);
let oxidized_finder_loaded =
unsafe { Python::with_gil_unchecked(|py| self.inject_oxidized_importer(py))? };
debug_assert_eq!(unsafe { pyffi::PyGILState_Check() }, 1);
let status = unsafe { pyffi::_Py_InitializeMain() };
if unsafe { pyffi::PyStatus_Exception(status) } != 0 {
return Err(NewInterpreterError::new_from_pystatus(
&status,
"initializing Python main",
));
}
debug_assert_eq!(unsafe { pyffi::PyGILState_Check() }, 1);
unsafe {
pyffi::PyEval_SaveThread();
}
self.write_modules_path =
self.with_gil(|py| self.init_post_main(py, oxidized_finder_loaded))?;
debug_assert_eq!(unsafe { pyffi::PyGILState_Check() }, 0);
Ok(())
}
fn inject_oxidized_importer(&self, py: Python) -> Result<bool, NewInterpreterError> {
if !self.config.oxidized_importer {
return Ok(false);
}
let resources_state = Box::new(PythonResourcesState::try_from(&self.config)?);
let oxidized_importer = py.import(OXIDIZED_IMPORTER_NAME_STR).map_err(|err| {
NewInterpreterError::new_from_pyerr(py, err, "import of oxidized importer module")
})?;
let cb = |importer_state: &mut ImporterState| match self.config.multiprocessing_start_method
{
MultiprocessingStartMethod::None => {}
MultiprocessingStartMethod::Fork
| MultiprocessingStartMethod::ForkServer
| MultiprocessingStartMethod::Spawn => {
importer_state.set_multiprocessing_set_start_method(Some(
self.config.multiprocessing_start_method.to_string(),
));
}
MultiprocessingStartMethod::Auto => {
let method = if cfg!(target_family = "windows") {
"spawn"
} else {
"fork"
};
importer_state.set_multiprocessing_set_start_method(Some(method.to_string()));
}
};
replace_meta_path_importers(py, oxidized_importer, resources_state, Some(cb)).map_err(
|err| {
NewInterpreterError::new_from_pyerr(py, err, "initialization of oxidized importer")
},
)?;
Ok(true)
}
fn init_post_main(
&self,
py: Python,
oxidized_finder_loaded: bool,
) -> Result<Option<PathBuf>, NewInterpreterError> {
let sys_module = py
.import("sys")
.map_err(|e| NewInterpreterError::new_from_pyerr(py, e, "obtaining sys module"))?;
if !self.config.filesystem_importer {
remove_external_importers(sys_module).map_err(|err| {
NewInterpreterError::new_from_pyerr(py, err, "removing external importers")
})?;
}
let oxidized_finder = if oxidized_finder_loaded {
sys_module
.getattr("meta_path")
.map_err(|err| {
NewInterpreterError::new_from_pyerr(py, err, "obtaining sys.meta_path")
})?
.iter()
.map_err(|err| {
NewInterpreterError::new_from_pyerr(
py,
err,
"obtaining iterator for sys.meta_path",
)
})?
.find(|finder| {
if let Ok(finder) = finder {
OxidizedFinder::is_type_of(finder)
} else {
false
}
})
} else {
None
};
if let Some(Ok(finder)) = oxidized_finder {
install_path_hook(finder, sys_module).map_err(|err| {
NewInterpreterError::new_from_pyerr(
py,
err,
"installing OxidizedFinder in sys.path_hooks",
)
})?;
}
if self.config.argvb {
let args_objs = self
.config
.resolve_sys_argvb()
.iter()
.map(|x| osstring_to_bytes(py, x.clone()))
.collect::<Vec<_>>();
let args = args_objs.to_object(py);
let argvb = b"argvb\0";
let res =
unsafe { pyffi::PySys_SetObject(argvb.as_ptr() as *const c_char, args.as_ptr()) };
match res {
0 => (),
_ => return Err(NewInterpreterError::Simple("unable to set sys.argvb")),
}
}
let oxidized = b"oxidized\0";
let py_true = true.into_py(py);
let res =
unsafe { pyffi::PySys_SetObject(oxidized.as_ptr() as *const c_char, py_true.as_ptr()) };
match res {
0 => (),
_ => return Err(NewInterpreterError::Simple("unable to set sys.oxidized")),
}
if self.config.sys_frozen {
let frozen = b"frozen\0";
match unsafe {
pyffi::PySys_SetObject(frozen.as_ptr() as *const c_char, py_true.as_ptr())
} {
0 => (),
_ => return Err(NewInterpreterError::Simple("unable to set sys.frozen")),
}
}
if self.config.sys_meipass {
let meipass = b"_MEIPASS\0";
let value = self.config.origin().display().to_string().to_object(py);
match unsafe {
pyffi::PySys_SetObject(meipass.as_ptr() as *const c_char, value.as_ptr())
} {
0 => (),
_ => return Err(NewInterpreterError::Simple("unable to set sys._MEIPASS")),
}
}
let write_modules_path = if let Some(key) = &self.config.write_modules_directory_env {
if let Ok(path) = std::env::var(key) {
let path = PathBuf::from(path);
std::fs::create_dir_all(&path).map_err(|e| {
NewInterpreterError::Dynamic(format!(
"error creating directory for loaded modules files: {}",
e
))
})?;
let uuid_mod = py.import("uuid").map_err(|e| {
NewInterpreterError::new_from_pyerr(py, e, "importing uuid module")
})?;
let uuid4 = uuid_mod.getattr("uuid4").map_err(|e| {
NewInterpreterError::new_from_pyerr(py, e, "obtaining uuid.uuid4")
})?;
let uuid = uuid4.call0().map_err(|e| {
NewInterpreterError::new_from_pyerr(py, e, "calling uuid.uuid4()")
})?;
let uuid_str = uuid
.str()
.map_err(|e| {
NewInterpreterError::new_from_pyerr(py, e, "converting uuid to str")
})?
.to_string();
Some(path.join(format!("modules-{}", uuid_str)))
} else {
None
}
} else {
None
};
Ok(write_modules_path)
}
#[inline]
pub fn with_gil<F, R>(&self, f: F) -> R
where
F: for<'py> FnOnce(Python<'py>) -> R,
{
Python::with_gil(f)
}
pub fn py_runmain(self) -> i32 {
unsafe {
pyffi::PyGILState_Ensure();
pyffi::Py_RunMain()
}
}
pub fn run_multiprocessing(&self) -> PyResult<i32> {
let argv = self.config.resolve_sys_argv().to_vec();
if argv.len() < 2 {
panic!("run_multiprocessing() called prematurely; sys.argv does not indicate multiprocessing mode");
}
self.with_gil(|py| {
let kwargs = PyDict::new(py);
for arg in argv.iter().skip(2) {
let arg = arg.to_string_lossy();
let mut parts = arg.splitn(2, '=');
let key = parts
.next()
.ok_or_else(|| PyRuntimeError::new_err("invalid multiprocessing argument"))?;
let value = parts
.next()
.ok_or_else(|| PyRuntimeError::new_err("invalid multiprocessing argument"))?;
let value = if value == "None" {
py.None()
} else {
let v = value.parse::<isize>().map_err(|e| {
PyRuntimeError::new_err(format!(
"unable to convert multiprocessing argument to integer: {}",
e
))
})?;
v.into_py(py)
};
kwargs.set_item(key, value)?;
}
let spawn_module = py.import("multiprocessing.spawn")?;
spawn_module.getattr("spawn_main")?.call1((kwargs,))?;
Ok(0)
})
}
pub fn is_multiprocessing(&self) -> bool {
let argv = self.config.resolve_sys_argv();
argv.len() >= 2 && argv[1] == "--multiprocessing-fork"
}
pub fn run(self) -> i32 {
if self.config.multiprocessing_auto_dispatch && self.is_multiprocessing() {
match self.run_multiprocessing() {
Ok(code) => code,
Err(e) => {
self.with_gil(|py| {
e.print(py);
});
1
}
}
} else {
self.py_runmain()
}
}
}
static mut ORIGINAL_BUILTIN_EXTENSIONS: Option<Vec<pyffi::_inittab>> = None;
static mut REPLACED_BUILTIN_EXTENSIONS: Option<Vec<pyffi::_inittab>> = None;
fn set_pyimport_inittab(config: &OxidizedPythonInterpreterConfig) {
unsafe {
if ORIGINAL_BUILTIN_EXTENSIONS.is_none() {
let mut entries: Vec<pyffi::_inittab> = Vec::new();
for i in 0.. {
let record = pyffi::PyImport_Inittab.offset(i);
if (*record).name.is_null() {
break;
}
entries.push(*record);
}
ORIGINAL_BUILTIN_EXTENSIONS = Some(entries);
}
}
let mut extensions = unsafe { ORIGINAL_BUILTIN_EXTENSIONS.as_ref().unwrap().clone() };
if config.oxidized_importer {
let ptr = PyInit_oxidized_importer as *const ();
extensions.push(pyffi::_inittab {
name: OXIDIZED_IMPORTER_NAME.as_ptr() as *mut _,
initfunc: Some(unsafe {
std::mem::transmute::<*const (), extern "C" fn() -> *mut pyffi::PyObject>(ptr)
}),
});
}
if let Some(extra_extension_modules) = &config.extra_extension_modules {
for extension in extra_extension_modules {
let ptr = extension.init_func as *const ();
extensions.push(pyffi::_inittab {
name: extension.name.as_ptr() as *mut _,
initfunc: Some(unsafe {
std::mem::transmute::<*const (), extern "C" fn() -> *mut pyffi::PyObject>(ptr)
}),
});
}
}
extensions.push(pyffi::_inittab {
name: std::ptr::null_mut(),
initfunc: None,
});
unsafe {
REPLACED_BUILTIN_EXTENSIONS = Some(extensions);
pyffi::PyImport_Inittab = REPLACED_BUILTIN_EXTENSIONS.as_mut().unwrap().as_mut_ptr();
}
}
fn write_modules_to_path(py: Python, path: &Path) -> Result<(), &'static str> {
let sys = py
.import("sys")
.map_err(|_| "could not obtain sys module")?;
let modules = sys
.getattr("modules")
.map_err(|_| "could not obtain sys.modules")?;
let modules = modules
.cast_as::<PyDict>()
.map_err(|_| "sys.modules is not a dict")?;
let mut names = BTreeSet::new();
for (key, _value) in modules.iter() {
names.insert(
key.extract::<String>()
.map_err(|_| "module name is not a str")?,
);
}
let mut f = fs::File::create(path).map_err(|_| "could not open file for writing")?;
for name in names {
f.write_fmt(format_args!("{}\n", name))
.map_err(|_| "could not write")?;
}
Ok(())
}
impl<'interpreter, 'resources> Drop for MainPythonInterpreter<'interpreter, 'resources> {
fn drop(&mut self) {
if unsafe { pyffi::Py_IsInitialized() } == 0 {
return;
}
if let Some(path) = self.write_modules_path.as_ref() {
match self.with_gil(|py| write_modules_to_path(py, path)) {
Ok(_) => {}
Err(msg) => {
eprintln!("error writing modules file: {}", msg);
}
}
}
unsafe {
pyffi::PyGILState_Ensure();
pyffi::Py_FinalizeEx();
}
}
}