mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
Add canonical cross-interpreter serialization tests and fixture bridges (#9408)
* Add canonical cross-interpreter serialization tests and fixture bridges Import the complete CPython test_crossinterp module and provide its capsule bridges using existing serialization and validation logic. Keep fixture modes private instead of expanding SharedValue or interpreter lifecycle APIs. Assisted-by: OpenAI Codex: model version unavailable * Simplify canonical cross-interpreter fixture dispatch Match CPython 3.15.0 mode dispatch with one private constructor instead of six forwarding entry points. Keep generic marshal data distinct from code transport, preserve capsule ownership checks and pure-script validation, and leave the canonical tests unchanged. Assisted-by: OpenAI Codex: model version unavailable --------- Co-authored-by: youknowdot <336763669+youknowdot@users.noreply.github.com>
This commit is contained in:
1 parent
77046cf527
commit
058bebf0f0
4 files changed
+1713
-1
No files matched your search
Vendored
+1550
File diff suppressed because it is too large.
Load diff
@@ -27,11 +27,12 @@ pub(crate) fn note_func_modification() {
|
|||||||
#[pymodule]
|
#[pymodule]
|
||||||
mod _testinternalcapi {
|
mod _testinternalcapi {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::vm::crossinterp::test_support::CrossinterpData;
|
||||||
use crate::{
|
use crate::{
|
||||||
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
|
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
|
||||||
atomic_func,
|
atomic_func,
|
||||||
builtins::{
|
builtins::{
|
||||||
PyBytesRef, PyCode, PyDict, PyDictRef, PyStrRef, PyType, PyTypeRef,
|
PyBytesRef, PyCapsule, PyCode, PyDict, PyDictRef, PyStrRef, PyType, PyTypeRef,
|
||||||
descriptor::PyWrapper,
|
descriptor::PyWrapper,
|
||||||
},
|
},
|
||||||
common::{hash::PyHash, lock::LazyLock},
|
common::{hash::PyHash, lock::LazyLock},
|
||||||
@@ -154,6 +155,66 @@ mod _testinternalcapi {
|
|||||||
crate::vm::crossinterp::code_returns_only_none(&code)
|
crate::vm::crossinterp::code_returns_only_none(&code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static CROSSINTERP_DATA_TAG: u8 = 0;
|
||||||
|
|
||||||
|
fn crossinterp_data_tag() -> *mut core::ffi::c_void {
|
||||||
|
core::ptr::from_ref(&CROSSINTERP_DATA_TAG).cast_mut().cast()
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn release_crossinterp_data(obj: *mut PyObject) {
|
||||||
|
// SAFETY: this is installed only as a PyCapsule destructor; the object
|
||||||
|
// remains alive for the duration of the call.
|
||||||
|
let Some(capsule) = (unsafe { &*obj }).downcast_ref::<PyCapsule>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if capsule.context() != crossinterp_data_tag() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let data = capsule.pointer().cast::<CrossinterpData>();
|
||||||
|
capsule.set_pointer(core::ptr::null_mut());
|
||||||
|
if !data.is_null() {
|
||||||
|
// SAFETY: get_crossinterp_data transferred this Box to the capsule.
|
||||||
|
// Clearing the pointer above prevents a second release.
|
||||||
|
drop(unsafe { Box::from_raw(data) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromArgs)]
|
||||||
|
struct GetCrossinterpDataArgs {
|
||||||
|
obj: PyObjectRef,
|
||||||
|
#[pyarg(any, optional)]
|
||||||
|
mode: OptionalArg<PyObjectRef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn get_crossinterp_data(args: GetCrossinterpDataArgs, vm: &VirtualMachine) -> PyResult {
|
||||||
|
let mode = args.mode.into_option();
|
||||||
|
let data = CrossinterpData::from_object(&args.obj, mode.as_deref(), vm)?;
|
||||||
|
let data = Box::into_raw(Box::new(data)).cast();
|
||||||
|
let capsule = vm
|
||||||
|
.ctx
|
||||||
|
.new_capsule(data, None, Some(release_crossinterp_data));
|
||||||
|
capsule.set_context(crossinterp_data_tag());
|
||||||
|
Ok(capsule.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn restore_crossinterp_data(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult {
|
||||||
|
let invalid =
|
||||||
|
|| vm.new_value_error("PyCapsule_GetPointer called with invalid PyCapsule object");
|
||||||
|
let capsule = obj.downcast_ref::<PyCapsule>().ok_or_else(invalid)?;
|
||||||
|
if capsule.name().is_some()
|
||||||
|
|| capsule.context() != crossinterp_data_tag()
|
||||||
|
|| capsule.pointer().is_null()
|
||||||
|
{
|
||||||
|
return Err(invalid());
|
||||||
|
}
|
||||||
|
// SAFETY: only get_crossinterp_data creates capsules with this context.
|
||||||
|
// `obj` owns the capsule during the clone, so the payload cannot be freed.
|
||||||
|
let data = unsafe { &*capsule.pointer().cast::<CrossinterpData>() }.clone();
|
||||||
|
data.into_object(vm)
|
||||||
|
}
|
||||||
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
fn get_co_localskinds(code: PyRef<PyCode>, vm: &VirtualMachine) -> PyResult<PyDictRef> {
|
fn get_co_localskinds(code: PyRef<PyCode>, vm: &VirtualMachine) -> PyResult<PyDictRef> {
|
||||||
let kinds = vm.ctx.new_dict();
|
let kinds = vm.ctx.new_dict();
|
||||||
|
|||||||
@@ -864,6 +864,14 @@ pub(crate) fn code_returns_only_none(code: &Py<PyCode>) -> bool {
|
|||||||
|
|
||||||
/// Extract a script code object from source text / function / code object.
|
/// Extract a script code object from source text / function / code object.
|
||||||
pub fn script_code(obj: &PyObject, vm: &VirtualMachine) -> PyResult<PyRef<PyCode>> {
|
pub fn script_code(obj: &PyObject, vm: &VirtualMachine) -> PyResult<PyRef<PyCode>> {
|
||||||
|
script_code_with_purity(obj, false, vm)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_code_with_purity(
|
||||||
|
obj: &PyObject,
|
||||||
|
pure: bool,
|
||||||
|
vm: &VirtualMachine,
|
||||||
|
) -> PyResult<PyRef<PyCode>> {
|
||||||
let code = if let Ok(code) = obj.to_owned().downcast::<PyCode>() {
|
let code = if let Ok(code) = obj.to_owned().downcast::<PyCode>() {
|
||||||
code
|
code
|
||||||
} else if let Some(func) = obj.downcast_ref::<PyFunction>() {
|
} else if let Some(func) = obj.downcast_ref::<PyFunction>() {
|
||||||
@@ -882,6 +890,22 @@ pub fn script_code(obj: &PyObject, vm: &VirtualMachine) -> PyResult<PyRef<PyCode
|
|||||||
"can't compile a script to bytecode when the `codegen` feature of rustpython is disabled",
|
"can't compile a script to bytecode when the `codegen` feature of rustpython is disabled",
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
if pure {
|
||||||
|
if let Some(func) = obj.downcast_ref::<PyFunction>() {
|
||||||
|
verify_stateless_function(func, vm)?;
|
||||||
|
} else {
|
||||||
|
let builtins =
|
||||||
|
crate::frame::current_builtins().unwrap_or_else(|| vm.builtins.dict().into());
|
||||||
|
let builtins = builtins.downcast_ref::<PyDict>().ok_or_else(|| {
|
||||||
|
vm.new_type_error(format!(
|
||||||
|
"unsupported builtins {}",
|
||||||
|
render_repr(&builtins, vm)
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let globals = vm.ctx.new_dict();
|
||||||
|
verify_stateless(&code, Some((&globals, builtins)), vm)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
verify_script(&code, vm)?;
|
verify_script(&code, vm)?;
|
||||||
Ok(code)
|
Ok(code)
|
||||||
}
|
}
|
||||||
@@ -961,3 +985,6 @@ where
|
|||||||
pub fn is_running(id: i64) -> bool {
|
pub fn is_running(id: i64) -> bool {
|
||||||
crate::vm::runtime::lookup_interpreter(id).is_some_and(|s| s.is_running_main())
|
crate::vm::runtime::lookup_interpreter(id).is_some_and(|s| s.is_running_main())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bridges for canonical _testinternalcapi cross-interpreter tests.
|
||||||
|
pub(crate) mod test_support;
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
//! Serialization modes exposed only by the canonical cross-interpreter test hooks.
|
||||||
|
//! Keep fixture-specific modes out of the production SharedValue representation.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::builtins::PyUtf8Str;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) enum CrossinterpData {
|
||||||
|
Shared(SharedValue),
|
||||||
|
Marshalled(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CrossinterpData {
|
||||||
|
pub(crate) fn from_object(
|
||||||
|
obj: &PyObject,
|
||||||
|
mode: Option<&PyObject>,
|
||||||
|
vm: &VirtualMachine,
|
||||||
|
) -> PyResult<Self> {
|
||||||
|
let mode_obj = mode.filter(|mode| !vm.is_none(mode));
|
||||||
|
let mode = match mode_obj {
|
||||||
|
None => "xidata",
|
||||||
|
Some(mode) => {
|
||||||
|
if !mode.downcastable::<PyStr>() {
|
||||||
|
return Err(
|
||||||
|
vm.new_type_error(format!("expected mode str, got {}", mode.repr(vm)?))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// CPython compares a NUL-terminated UTF-8 string.
|
||||||
|
mode.try_downcast_ref::<PyUtf8Str>(vm)?
|
||||||
|
.as_str()
|
||||||
|
.split('\0')
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let data = match mode {
|
||||||
|
"" | "xidata" => SharedValue::from_object(obj, Fallback::XidataOnly, vm),
|
||||||
|
"fallback" => SharedValue::from_object(obj, Fallback::Full, vm),
|
||||||
|
"pickle" => pickle_dumps(obj, vm)
|
||||||
|
.map(SharedValue::Pickled)
|
||||||
|
.map_err(|cause| {
|
||||||
|
not_shareable_error_from(vm, "object could not be pickled", cause)
|
||||||
|
}),
|
||||||
|
"marshal" => return marshal_dumps(obj, vm).map(Self::Marshalled),
|
||||||
|
"code" => {
|
||||||
|
let code = obj.downcast_ref::<PyCode>().ok_or_else(|| {
|
||||||
|
not_shareable_error(vm, format!("expected code, got {}", render_repr(obj, vm)))
|
||||||
|
})?;
|
||||||
|
SharedValue::from_code(code, vm)
|
||||||
|
}
|
||||||
|
"func" => SharedValue::from_function(obj, vm),
|
||||||
|
"script" | "script-pure" => {
|
||||||
|
let code =
|
||||||
|
script_code_with_purity(obj, mode == "script-pure", vm).map_err(|cause| {
|
||||||
|
not_shareable_error_from(vm, "object not a valid script", cause)
|
||||||
|
})?;
|
||||||
|
SharedValue::from_code(&code, vm)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(
|
||||||
|
vm.new_value_error(format!("unsupported mode {}", mode_obj.unwrap().repr(vm)?))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
Ok(Self::Shared(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn into_object(self, vm: &VirtualMachine) -> PyResult {
|
||||||
|
match self {
|
||||||
|
Self::Shared(data) => data.into_object(vm),
|
||||||
|
Self::Marshalled(data) => marshal_loads(&data, vm),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user