//! This module takes care of all conversions involving `rusthg` (hg-cpython) //! objects in the PyO3 call context. //! //! For source code clarity, we only import (`use`) [`cpython`] traits and not //! any of its data objects. We are instead using full qualifiers, such as //! `cpython::PyObject`, and believe that the added heaviness is an acceptatble //! price to pay to avoid confusion. //! //! Also it, is customary in [`cpython`] to label the GIL lifetime as `'p`, //! whereas it is `'py` in PyO3 context. We keep both these conventions in //! the arguments side of function signatures when they are not simply elided. use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use cpython::ObjectProtocol; use cpython::PythonObject; use lazy_static::lazy_static; use rusthg::revlog::{InnerRevlog, PySharedIndex}; /// Force cpython's GIL handle with the appropriate lifetime /// /// In `pyo3`, the fact that we have the GIL is expressed by the lifetime of /// the incoming [`Bound`] smart pointer. We therefore simply instantiate /// the `cpython` handle and coerce its lifetime by the function signature. /// /// Reacquiring the GIL is also a possible alternative, as the CPython /// documentation explicitely states that "recursive calls are allowed" /// (we interpret that as saying that acquiring the GIL within a thread that /// already has it works) *as long as it is properly released* /// reference: /// pub(crate) fn cpython_handle<'py, T>( _bound: &Bound<'py, T>, ) -> cpython::Python<'py> { // safety: this is safe because the returned object has the 'py lifetime unsafe { cpython::Python::assume_gil_acquired() } } /// Force PyO3 GIL handle from cpython's. /// /// Very similar to [`cpython_handle`] pub fn pyo3_handle(_py: cpython::Python<'_>) -> Python<'_> { // safety: this is safe because the returned object has the same lifetime // as the incoming object. unsafe { Python::assume_gil_acquired() } } /// Convert a PyO3 [`PyObject`] into a [`cpython::PyObject`] /// /// During this process, the reference count is increased, then decreased. /// This means that the GIL (symbolized by the lifetime on the `obj` /// argument) is needed. /// /// We could make something perhaps more handy by simply stealing the /// pointer, forgetting the incoming and then implement `From` with "newtype". /// It would be worth the effort for a generic cpython-to-pyo3 crate, perhaps /// not for the current endeavour. pub(crate) fn to_cpython_py_object<'py>( obj: &Bound<'py, PyAny>, ) -> (cpython::Python<'py>, cpython::PyObject) { let py = cpython_handle(obj); // public alias of the private cpython::fii::PyObject (!) let raw = obj.as_ptr() as *mut python3_sys::PyObject; // both pyo3 and rust-cpython will decrement the refcount on drop. // If we use from_owned_ptr, that's a segfault. (py, unsafe { cpython::PyObject::from_borrowed_ptr(py, raw) }) } /// Convert a [`cpython::PyObject`] into a PyO3 [`PyObject`] /// /// During this process, the reference count is increased, then decreased. /// This means that the GIL (symbolized by the PyO3 [`Python`] handle is /// needed. /// /// We could make something perhaps more handy by simply stealing the /// pointer, forgetting the incoming and then implement `From` with "newtype". /// It would be worth the effort for a generic cpython-to-pyo3 crate, perhaps /// not for the current endeavour. pub(crate) fn from_cpython_py_object( py: Python<'_>, obj: cpython::PyObject, ) -> PyObject { let raw = obj.as_ptr() as *mut pyo3::ffi::PyObject; unsafe { Py::from_borrowed_ptr(py, raw) } } /// Convert [`cpython::PyErr`] into [`pyo3::PyErr`] /// /// The exception class remains the same as the original exception, /// hence if it is also defined in another dylib based on `cpython` crate, /// it will need to be converted to be downcasted in this crate. pub(crate) fn from_cpython_pyerr( py: cpython::Python<'_>, mut e: cpython::PyErr, ) -> PyErr { let pyo3_py = pyo3_handle(py); let cpython_exc_obj = e.instance(py); let pyo3_exc_obj = from_cpython_py_object(pyo3_py, cpython_exc_obj); PyErr::from_value(pyo3_exc_obj.into_bound(pyo3_py)) } /// Retrieve the PyType for objects from the `mercurial.rustext` crate. fn retrieve_cpython_py_type( submodule_name: &str, type_name: &str, ) -> cpython::PyResult { let guard = cpython::Python::acquire_gil(); let py = guard.python(); let module = py.import(&format!("mercurial.rustext.{submodule_name}"))?; module.get(py, type_name)?.extract::(py) } lazy_static! { static ref INNER_REVLOG_PY_TYPE: cpython::PyType = { retrieve_cpython_py_type("revlog", "InnerRevlog") .expect("Could not import InnerRevlog in Python") }; } /// Downcast [`InnerRevlog`], with the appropriate Python type checking. /// /// The PyType object representing the `InnerRevlog` Python class is not the /// the same in this dylib as it is in the `mercurial.rustext` module. /// This is because the code created with the [`cpython::py_class!`] /// macro is itself duplicated in both dylibs. In the case of this crate, this /// happens by linking to the [`rusthg`] crate and provides the `InnerRevlog` /// that is visible from this crate. The `InnerRevlog::get_type` associated /// function turns out to return a `static mut` (look for `TYPE_OBJECT` in /// `py_class_impl3.rs`), which obviously is different in both dylibs. /// /// The consequence of that is that downcasting an `InnerRevlog` originally /// from the `mecurial.rustext` module to our `InnerRevlog` cannot be done with /// the usual `extract::(py)`, as it would perform the type /// checking with the `PyType` that is embedded in `mercurial.pyo3_rustext`. /// We must check the `PyType` that is within `mercurial.rustext` instead. /// This is what this function does. fn extract_inner_revlog( py: cpython::Python, inner_revlog: cpython::PyObject, ) -> PyResult { if !(*INNER_REVLOG_PY_TYPE).is_instance(py, &inner_revlog) { return Err(PyTypeError::new_err("Not an InnerRevlog instance")); } // Safety: this is safe because we checked the PyType already, with the // value embedded in `mercurial.rustext`. Ok(unsafe { InnerRevlog::unchecked_downcast_from(inner_revlog) }) } /// This is similar to [`rusthg.py_rust_index_to_graph`], with difference in /// how we retrieve the [`InnerRevlog`]. pub fn py_rust_index_to_graph( py: cpython::Python, index_proxy: cpython::PyObject, ) -> PyResult> { let inner_revlog = extract_inner_revlog( py, index_proxy .getattr(py, "inner") .map_err(|e| from_cpython_pyerr(py, e))?, )?; let leaked = inner_revlog.pub_inner(py).leak_immutable(); // Safety: we don't leak the "faked" reference out of the `UnsafePyLeaked` Ok(unsafe { leaked.map(py, |idx| PySharedIndex { inner: &idx.index }) }) } pub(crate) fn proxy_index_extract<'py>( index_proxy: &Bound<'py, PyAny>, ) -> PyResult<(cpython::Python<'py>, cpython::UnsafePyLeaked)> { let (py, idx_proxy) = to_cpython_py_object(index_proxy); Ok((py, py_rust_index_to_graph(py, idx_proxy)?)) }