diff --git a/vm/src/lib.rs b/vm/src/lib.rs index 15bde79d7..d0ba7965e 100644 --- a/vm/src/lib.rs +++ b/vm/src/lib.rs @@ -77,9 +77,6 @@ pub mod types; pub mod utils; pub mod version; mod vm; -mod vm_new; -mod vm_object; -mod vm_ops; mod pyobject { pub use super::_pyobject::*; diff --git a/vm/src/vm/interpreter.rs b/vm/src/vm/interpreter.rs new file mode 100644 index 000000000..6713b792e --- /dev/null +++ b/vm/src/vm/interpreter.rs @@ -0,0 +1,93 @@ +use super::{setting::PySettings, thread, InitParameter, VirtualMachine}; + +/// The general interface for the VM +/// +/// # Examples +/// Runs a simple embedded hello world program. +/// ``` +/// use rustpython_vm::Interpreter; +/// use rustpython_vm::compile::Mode; +/// Interpreter::default().enter(|vm| { +/// let scope = vm.new_scope_with_builtins(); +/// let code_obj = vm.compile(r#"print("Hello World!")"#, +/// Mode::Exec, +/// "".to_owned(), +/// ).map_err(|err| vm.new_syntax_error(&err)).unwrap(); +/// vm.run_code_obj(code_obj, scope).unwrap(); +/// }); +/// ``` +pub struct Interpreter { + vm: VirtualMachine, +} + +impl Interpreter { + pub fn new(settings: PySettings, init: InitParameter) -> Self { + Self::new_with_init(settings, |_| init) + } + + pub fn new_with_init(settings: PySettings, init: F) -> Self + where + F: FnOnce(&mut VirtualMachine) -> InitParameter, + { + let mut vm = VirtualMachine::new(settings); + let init = init(&mut vm); + vm.initialize(init); + Self { vm } + } + + pub fn enter(&self, f: F) -> R + where + F: FnOnce(&VirtualMachine) -> R, + { + thread::enter_vm(&self.vm, || f(&self.vm)) + } + + // TODO: interpreter shutdown + // pub fn run(self, f: F) + // where + // F: FnOnce(&VirtualMachine), + // { + // self.enter(f); + // self.shutdown(); + // } + + // pub fn shutdown(self) {} +} + +impl Default for Interpreter { + fn default() -> Self { + Self::new(PySettings::default(), InitParameter::External) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + builtins::{int, PyStr}, + PyObjectRef, + }; + use num_bigint::ToBigInt; + + #[test] + fn test_add_py_integers() { + Interpreter::default().enter(|vm| { + let a: PyObjectRef = vm.ctx.new_int(33_i32).into(); + let b: PyObjectRef = vm.ctx.new_int(12_i32).into(); + let res = vm._add(&a, &b).unwrap(); + let value = int::get_value(&res); + assert_eq!(*value, 45_i32.to_bigint().unwrap()); + }) + } + + #[test] + fn test_multiply_str() { + Interpreter::default().enter(|vm| { + let a = vm.new_pyobj(crate::common::ascii!("Hello ")); + let b = vm.new_pyobj(4_i32); + let res = vm._mul(&a, &b).unwrap(); + let value = res.payload::().unwrap(); + assert_eq!(value.as_ref(), "Hello Hello Hello Hello ") + }) + } +} diff --git a/vm/src/vm.rs b/vm/src/vm/mod.rs similarity index 79% rename from vm/src/vm.rs rename to vm/src/vm/mod.rs index a61b4ce8c..bf1764b62 100644 --- a/vm/src/vm.rs +++ b/vm/src/vm/mod.rs @@ -2,7 +2,6 @@ //! //! See also: //! -//! #[cfg(feature = "rustpython-compiler")] use crate::compile::{self, CompileError, CompileOpts}; @@ -33,6 +32,17 @@ use std::{ collections::{HashMap, HashSet}, }; +mod interpreter; +mod setting; +pub mod thread; +mod vm_new; +mod vm_object; +mod vm_ops; + +pub use interpreter::Interpreter; +pub use setting::PySettings; +pub use thread::PyThread; + // Objects are live when they are on stack, or referenced by a name (for now) /// Top level container of a python virtual machine. In theory you could @@ -65,49 +75,6 @@ struct ExceptionStack { prev: Option>, } -pub(crate) mod thread { - use super::{AsObject, PyObject, VirtualMachine}; - use itertools::Itertools; - use std::{cell::RefCell, ptr::NonNull, thread_local}; - - thread_local! { - pub(super) static VM_STACK: RefCell>> = Vec::with_capacity(1).into(); - } - - pub fn enter_vm(vm: &VirtualMachine, f: impl FnOnce() -> R) -> R { - VM_STACK.with(|vms| { - vms.borrow_mut().push(vm.into()); - let ret = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); - vms.borrow_mut().pop(); - ret.unwrap_or_else(|e| std::panic::resume_unwind(e)) - }) - } - - pub fn with_vm(obj: &PyObject, f: F) -> Option - where - F: Fn(&VirtualMachine) -> R, - { - let vm_owns_obj = |intp: NonNull| { - // SAFETY: all references in VM_STACK should be valid - let vm = unsafe { intp.as_ref() }; - obj.fast_isinstance(&vm.ctx.types.object_type) - }; - VM_STACK.with(|vms| { - let intp = match vms.borrow().iter().copied().exactly_one() { - Ok(x) => { - debug_assert!(vm_owns_obj(x)); - x - } - Err(mut others) => others.find(|x| vm_owns_obj(*x))?, - }; - // SAFETY: all references in VM_STACK should be valid, and should not be changed or moved - // at least until this function returns and the stack unwinds to an enter_vm() call - let vm = unsafe { intp.as_ref() }; - Some(f(vm)) - }) - } -} - pub struct PyGlobalState { pub settings: PySettings, pub module_inits: stdlib::StdlibMap, @@ -125,98 +92,6 @@ pub enum InitParameter { External, } -/// Struct containing all kind of settings for the python vm. -#[non_exhaustive] -pub struct PySettings { - /// -d command line switch - pub debug: bool, - - /// -i - pub inspect: bool, - - /// -i, with no script - pub interactive: bool, - - /// -O optimization switch counter - pub optimize: u8, - - /// -s - pub no_user_site: bool, - - /// -S - pub no_site: bool, - - /// -E - pub ignore_environment: bool, - - /// verbosity level (-v switch) - pub verbose: u8, - - /// -q - pub quiet: bool, - - /// -B - pub dont_write_bytecode: bool, - - /// -b - pub bytes_warning: u64, - - /// -Xfoo[=bar] - pub xopts: Vec<(String, Option)>, - - /// -I - pub isolated: bool, - - /// -Xdev - pub dev_mode: bool, - - /// -Wfoo - pub warnopts: Vec, - - /// Environment PYTHONPATH and RUSTPYTHONPATH: - pub path_list: Vec, - - /// sys.argv - pub argv: Vec, - - /// PYTHONHASHSEED=x - pub hash_seed: Option, - - /// -u, PYTHONUNBUFFERED=x - // TODO: use this; can TextIOWrapper even work with a non-buffered? - pub stdio_unbuffered: bool, -} - -/// Sensible default settings. -impl Default for PySettings { - fn default() -> Self { - PySettings { - debug: false, - inspect: false, - interactive: false, - optimize: 0, - no_user_site: false, - no_site: false, - ignore_environment: false, - verbose: 0, - quiet: false, - dont_write_bytecode: false, - bytes_warning: 0, - xopts: vec![], - isolated: false, - dev_mode: false, - warnopts: vec![], - path_list: vec![ - #[cfg(all(feature = "pylib", not(feature = "freeze-stdlib")))] - rustpython_pylib::LIB_PATH.to_owned(), - ], - argv: vec![], - hash_seed: None, - stdio_unbuffered: false, - } - } -} - impl VirtualMachine { /// Create a new `VirtualMachine` structure. fn new(settings: PySettings) -> VirtualMachine { @@ -969,132 +844,3 @@ impl<'vm> Drop for ReprGuard<'vm> { self.vm.repr_guards.borrow_mut().remove(&self.id); } } - -/// The general interface for the VM -/// -/// # Examples -/// Runs a simple embedded hello world program. -/// ``` -/// use rustpython_vm::Interpreter; -/// use rustpython_vm::compile::Mode; -/// Interpreter::default().enter(|vm| { -/// let scope = vm.new_scope_with_builtins(); -/// let code_obj = vm.compile(r#"print("Hello World!")"#, -/// Mode::Exec, -/// "".to_owned(), -/// ).map_err(|err| vm.new_syntax_error(&err)).unwrap(); -/// vm.run_code_obj(code_obj, scope).unwrap(); -/// }); -/// ``` -pub struct Interpreter { - vm: VirtualMachine, -} - -impl Interpreter { - pub fn new(settings: PySettings, init: InitParameter) -> Self { - Self::new_with_init(settings, |_| init) - } - - pub fn new_with_init(settings: PySettings, init: F) -> Self - where - F: FnOnce(&mut VirtualMachine) -> InitParameter, - { - let mut vm = VirtualMachine::new(settings); - let init = init(&mut vm); - vm.initialize(init); - Self { vm } - } - - pub fn enter(&self, f: F) -> R - where - F: FnOnce(&VirtualMachine) -> R, - { - thread::enter_vm(&self.vm, || f(&self.vm)) - } - - // TODO: interpreter shutdown - // pub fn run(self, f: F) - // where - // F: FnOnce(&VirtualMachine), - // { - // self.enter(f); - // self.shutdown(); - // } - - // pub fn shutdown(self) {} -} - -impl Default for Interpreter { - fn default() -> Self { - Self::new(PySettings::default(), InitParameter::External) - } -} - -#[must_use = "PyThread does nothing unless you move it to another thread and call .run()"] -#[cfg(feature = "threading")] -pub struct PyThread { - thread_vm: VirtualMachine, -} - -#[cfg(feature = "threading")] -impl PyThread { - /// Create a `FnOnce()` that can easily be passed to a function like [`std::thread::Builder::spawn`] - /// - /// # Note - /// - /// If you return a `PyObjectRef` (or a type that contains one) from `F`, and don't `join()` - /// on the thread this `FnOnce` runs in, there is a possibility that that thread will panic - /// as `PyObjectRef`'s `Drop` implementation tries to run the `__del__` destructor of a - /// Python object but finds that it's not in the context of any vm. - pub fn make_spawn_func(self, f: F) -> impl FnOnce() -> R - where - F: FnOnce(&VirtualMachine) -> R, - { - move || self.run(f) - } - - /// Run a function in this thread context - /// - /// # Note - /// - /// If you return a `PyObjectRef` (or a type that contains one) from `F`, and don't return the object - /// to the parent thread and then `join()` on the `JoinHandle` (or similar), there is a possibility that - /// the current thread will panic as `PyObjectRef`'s `Drop` implementation tries to run the `__del__` - /// destructor of a python object but finds that it's not in the context of any vm. - pub fn run(self, f: F) -> R - where - F: FnOnce(&VirtualMachine) -> R, - { - let vm = &self.thread_vm; - thread::enter_vm(vm, || f(vm)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::builtins::{int, PyStr}; - use num_bigint::ToBigInt; - - #[test] - fn test_add_py_integers() { - Interpreter::default().enter(|vm| { - let a: PyObjectRef = vm.ctx.new_int(33_i32).into(); - let b: PyObjectRef = vm.ctx.new_int(12_i32).into(); - let res = vm._add(&a, &b).unwrap(); - let value = int::get_value(&res); - assert_eq!(*value, 45_i32.to_bigint().unwrap()); - }) - } - - #[test] - fn test_multiply_str() { - Interpreter::default().enter(|vm| { - let a = vm.new_pyobj(crate::common::ascii!("Hello ")); - let b = vm.new_pyobj(4_i32); - let res = vm._mul(&a, &b).unwrap(); - let value = res.payload::().unwrap(); - assert_eq!(value.as_ref(), "Hello Hello Hello Hello ") - }) - } -} diff --git a/vm/src/vm/setting.rs b/vm/src/vm/setting.rs new file mode 100644 index 000000000..f7638035b --- /dev/null +++ b/vm/src/vm/setting.rs @@ -0,0 +1,91 @@ +/// Struct containing all kind of settings for the python vm. +#[non_exhaustive] +pub struct PySettings { + /// -d command line switch + pub debug: bool, + + /// -i + pub inspect: bool, + + /// -i, with no script + pub interactive: bool, + + /// -O optimization switch counter + pub optimize: u8, + + /// -s + pub no_user_site: bool, + + /// -S + pub no_site: bool, + + /// -E + pub ignore_environment: bool, + + /// verbosity level (-v switch) + pub verbose: u8, + + /// -q + pub quiet: bool, + + /// -B + pub dont_write_bytecode: bool, + + /// -b + pub bytes_warning: u64, + + /// -Xfoo[=bar] + pub xopts: Vec<(String, Option)>, + + /// -I + pub isolated: bool, + + /// -Xdev + pub dev_mode: bool, + + /// -Wfoo + pub warnopts: Vec, + + /// Environment PYTHONPATH and RUSTPYTHONPATH: + pub path_list: Vec, + + /// sys.argv + pub argv: Vec, + + /// PYTHONHASHSEED=x + pub hash_seed: Option, + + /// -u, PYTHONUNBUFFERED=x + // TODO: use this; can TextIOWrapper even work with a non-buffered? + pub stdio_unbuffered: bool, +} + +/// Sensible default settings. +impl Default for PySettings { + fn default() -> Self { + PySettings { + debug: false, + inspect: false, + interactive: false, + optimize: 0, + no_user_site: false, + no_site: false, + ignore_environment: false, + verbose: 0, + quiet: false, + dont_write_bytecode: false, + bytes_warning: 0, + xopts: vec![], + isolated: false, + dev_mode: false, + warnopts: vec![], + path_list: vec![ + #[cfg(all(feature = "pylib", not(feature = "freeze-stdlib")))] + rustpython_pylib::LIB_PATH.to_owned(), + ], + argv: vec![], + hash_seed: None, + stdio_unbuffered: false, + } + } +} diff --git a/vm/src/vm/thread.rs b/vm/src/vm/thread.rs new file mode 100644 index 000000000..8d5083277 --- /dev/null +++ b/vm/src/vm/thread.rs @@ -0,0 +1,80 @@ +use crate::{AsObject, PyObject, VirtualMachine}; +use itertools::Itertools; +use std::{cell::RefCell, ptr::NonNull, thread_local}; + +thread_local! { + pub(super) static VM_STACK: RefCell>> = Vec::with_capacity(1).into(); +} + +pub fn enter_vm(vm: &VirtualMachine, f: impl FnOnce() -> R) -> R { + VM_STACK.with(|vms| { + vms.borrow_mut().push(vm.into()); + let ret = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); + vms.borrow_mut().pop(); + ret.unwrap_or_else(|e| std::panic::resume_unwind(e)) + }) +} + +pub fn with_vm(obj: &PyObject, f: F) -> Option +where + F: Fn(&VirtualMachine) -> R, +{ + let vm_owns_obj = |intp: NonNull| { + // SAFETY: all references in VM_STACK should be valid + let vm = unsafe { intp.as_ref() }; + obj.fast_isinstance(&vm.ctx.types.object_type) + }; + VM_STACK.with(|vms| { + let intp = match vms.borrow().iter().copied().exactly_one() { + Ok(x) => { + debug_assert!(vm_owns_obj(x)); + x + } + Err(mut others) => others.find(|x| vm_owns_obj(*x))?, + }; + // SAFETY: all references in VM_STACK should be valid, and should not be changed or moved + // at least until this function returns and the stack unwinds to an enter_vm() call + let vm = unsafe { intp.as_ref() }; + Some(f(vm)) + }) +} + +#[must_use = "PyThread does nothing unless you move it to another thread and call .run()"] +#[cfg(feature = "threading")] +pub struct PyThread { + pub(super) thread_vm: VirtualMachine, +} + +#[cfg(feature = "threading")] +impl PyThread { + /// Create a `FnOnce()` that can easily be passed to a function like [`std::thread::Builder::spawn`] + /// + /// # Note + /// + /// If you return a `PyObjectRef` (or a type that contains one) from `F`, and don't `join()` + /// on the thread this `FnOnce` runs in, there is a possibility that that thread will panic + /// as `PyObjectRef`'s `Drop` implementation tries to run the `__del__` destructor of a + /// Python object but finds that it's not in the context of any vm. + pub fn make_spawn_func(self, f: F) -> impl FnOnce() -> R + where + F: FnOnce(&VirtualMachine) -> R, + { + move || self.run(f) + } + + /// Run a function in this thread context + /// + /// # Note + /// + /// If you return a `PyObjectRef` (or a type that contains one) from `F`, and don't return the object + /// to the parent thread and then `join()` on the `JoinHandle` (or similar), there is a possibility that + /// the current thread will panic as `PyObjectRef`'s `Drop` implementation tries to run the `__del__` + /// destructor of a python object but finds that it's not in the context of any vm. + pub fn run(self, f: F) -> R + where + F: FnOnce(&VirtualMachine) -> R, + { + let vm = &self.thread_vm; + enter_vm(vm, || f(vm)) + } +} diff --git a/vm/src/vm_new.rs b/vm/src/vm/vm_new.rs similarity index 100% rename from vm/src/vm_new.rs rename to vm/src/vm/vm_new.rs diff --git a/vm/src/vm_object.rs b/vm/src/vm/vm_object.rs similarity index 100% rename from vm/src/vm_object.rs rename to vm/src/vm/vm_object.rs diff --git a/vm/src/vm_ops.rs b/vm/src/vm/vm_ops.rs similarity index 100% rename from vm/src/vm_ops.rs rename to vm/src/vm/vm_ops.rs