rust/hg-pyo3/src/convert_cpython.rs
changeset 52411 c2480ac4c5e2
child 52414 233707101dae
equal deleted inserted replaced
52410:15011324a80b 52411:c2480ac4c5e2
       
     1 //! This module takes care of all conversions involving `rusthg` (hg-cpython)
       
     2 //! objects in the PyO3 call context.
       
     3 //!
       
     4 //! For source code clarity, we only import (`use`) [`cpython`] traits and not
       
     5 //! any of its data objects. We are instead using full qualifiers, such as
       
     6 //! `cpython::PyObject`, and believe that the added heaviness is an acceptatble
       
     7 //! price to pay to avoid confusion.
       
     8 //!
       
     9 //! Also it, is customary in [`cpython`] to label the GIL lifetime as `'p`,
       
    10 //! whereas it is `'py` in PyO3 context. We keep both these conventions in
       
    11 //! the arguments side of function signatures when they are not simply elided.
       
    12 use pyo3::exceptions::PyTypeError;
       
    13 use pyo3::prelude::*;
       
    14 
       
    15 use cpython::ObjectProtocol;
       
    16 use cpython::PythonObject;
       
    17 use lazy_static::lazy_static;
       
    18 
       
    19 use rusthg::revlog::{InnerRevlog, PySharedIndex};
       
    20 
       
    21 /// Force cpython's GIL handle with the appropriate lifetime
       
    22 ///
       
    23 /// In `pyo3`, the fact that we have the GIL is expressed by the lifetime of
       
    24 /// the incoming [`Bound`] smart pointer. We therefore simply instantiate
       
    25 /// the `cpython` handle and coerce its lifetime by the function signature.
       
    26 ///
       
    27 /// Reacquiring the GIL is also a possible alternative, as the CPython
       
    28 /// documentation explicitely states that "recursive calls are allowed"
       
    29 /// (we interpret that as saying that acquiring the GIL within a thread that
       
    30 /// already has it works) *as long as it is properly released*
       
    31 /// reference:
       
    32 /// <https://docs.python.org/3.8/c-api/init.html#c.PyGILState_Ensure>
       
    33 pub(crate) fn cpython_handle<'py, T>(
       
    34     _bound: &Bound<'py, T>,
       
    35 ) -> cpython::Python<'py> {
       
    36     // safety: this is safe because the returned object has the 'py lifetime
       
    37     unsafe { cpython::Python::assume_gil_acquired() }
       
    38 }
       
    39 
       
    40 /// Force PyO3 GIL handle from cpython's.
       
    41 ///
       
    42 /// Very similar to [`cpython_handle`]
       
    43 pub fn pyo3_handle(_py: cpython::Python<'_>) -> Python<'_> {
       
    44     // safety: this is safe because the returned object has the same lifetime
       
    45     // as the incoming object.
       
    46     unsafe { Python::assume_gil_acquired() }
       
    47 }
       
    48 
       
    49 /// Convert a PyO3 [`PyObject`] into a [`cpython::PyObject`]
       
    50 ///
       
    51 /// During this process, the reference count is increased, then decreased.
       
    52 /// This means that the GIL (symbolized by the lifetime on the `obj`
       
    53 /// argument) is needed.
       
    54 ///
       
    55 /// We could make something perhaps more handy by simply stealing the
       
    56 /// pointer, forgetting the incoming and then implement `From` with "newtype".
       
    57 /// It would be worth the effort for a generic cpython-to-pyo3 crate, perhaps
       
    58 /// not for the current endeavour.
       
    59 pub(crate) fn to_cpython_py_object<'py>(
       
    60     obj: &Bound<'py, PyAny>,
       
    61 ) -> (cpython::Python<'py>, cpython::PyObject) {
       
    62     let py = cpython_handle(obj);
       
    63     // public alias of the private cpython::fii::PyObject (!)
       
    64     let raw = obj.as_ptr() as *mut python3_sys::PyObject;
       
    65     // both pyo3 and rust-cpython will decrement the refcount on drop.
       
    66     // If we use from_owned_ptr, that's a segfault.
       
    67     (py, unsafe { cpython::PyObject::from_borrowed_ptr(py, raw) })
       
    68 }
       
    69 
       
    70 /// Convert a [`cpython::PyObject`] into a PyO3 [`PyObject`]
       
    71 ///
       
    72 /// During this process, the reference count is increased, then decreased.
       
    73 /// This means that the GIL (symbolized by the PyO3 [`Python`] handle is
       
    74 /// needed.
       
    75 ///
       
    76 /// We could make something perhaps more handy by simply stealing the
       
    77 /// pointer, forgetting the incoming and then implement `From` with "newtype".
       
    78 /// It would be worth the effort for a generic cpython-to-pyo3 crate, perhaps
       
    79 /// not for the current endeavour.
       
    80 pub(crate) fn from_cpython_py_object(
       
    81     py: Python<'_>,
       
    82     obj: cpython::PyObject,
       
    83 ) -> PyObject {
       
    84     let raw = obj.as_ptr() as *mut pyo3::ffi::PyObject;
       
    85     unsafe { Py::from_borrowed_ptr(py, raw) }
       
    86 }
       
    87 
       
    88 /// Convert [`cpython::PyErr`] into [`pyo3::PyErr`]
       
    89 ///
       
    90 /// The exception class remains the same as the original exception,
       
    91 /// hence if it is also defined in another dylib based on `cpython` crate,
       
    92 /// it will need to be converted to be downcasted in this crate.
       
    93 pub(crate) fn from_cpython_pyerr(
       
    94     py: cpython::Python<'_>,
       
    95     mut e: cpython::PyErr,
       
    96 ) -> PyErr {
       
    97     let pyo3_py = pyo3_handle(py);
       
    98     let cpython_exc_obj = e.instance(py);
       
    99     let pyo3_exc_obj = from_cpython_py_object(pyo3_py, cpython_exc_obj);
       
   100     PyErr::from_value(pyo3_exc_obj.into_bound(pyo3_py))
       
   101 }
       
   102 
       
   103 /// Retrieve the PyType for objects from the `mercurial.rustext` crate.
       
   104 fn retrieve_cpython_py_type(
       
   105     submodule_name: &str,
       
   106     type_name: &str,
       
   107 ) -> cpython::PyResult<cpython::PyType> {
       
   108     let guard = cpython::Python::acquire_gil();
       
   109     let py = guard.python();
       
   110     let module = py.import(&format!("mercurial.rustext.{submodule_name}"))?;
       
   111     module.get(py, type_name)?.extract::<cpython::PyType>(py)
       
   112 }
       
   113 
       
   114 lazy_static! {
       
   115     static ref INNER_REVLOG_PY_TYPE: cpython::PyType = {
       
   116         retrieve_cpython_py_type("revlog", "InnerRevlog")
       
   117             .expect("Could not import InnerRevlog in Python")
       
   118     };
       
   119 }
       
   120 
       
   121 /// Downcast [`InnerRevlog`], with the appropriate Python type checking.
       
   122 ///
       
   123 /// The PyType object representing the `InnerRevlog` Python class is not the
       
   124 /// the same in this dylib as it is in the `mercurial.rustext` module.
       
   125 /// This is because the code created with the [`cpython::py_class!`]
       
   126 /// macro is itself duplicated in both dylibs. In the case of this crate, this
       
   127 /// happens by linking to the [`rusthg`] crate and provides the `InnerRevlog`
       
   128 /// that is visible from this crate. The `InnerRevlog::get_type` associated
       
   129 /// function turns out to return a `static mut` (look for `TYPE_OBJECT` in
       
   130 /// `py_class_impl3.rs`), which obviously is different in both dylibs.
       
   131 ///
       
   132 /// The consequence of that is that downcasting an `InnerRevlog` originally
       
   133 /// from the `mecurial.rustext` module to our `InnerRevlog` cannot be done with
       
   134 /// the usual `extract::<InnerRevlog>(py)`, as it would perform the type
       
   135 /// checking with the `PyType` that is embedded in `mercurial.pyo3_rustext`.
       
   136 /// We must check the `PyType` that is within `mercurial.rustext` instead.
       
   137 /// This is what this function does.
       
   138 fn extract_inner_revlog(
       
   139     py: cpython::Python,
       
   140     inner_revlog: cpython::PyObject,
       
   141 ) -> PyResult<InnerRevlog> {
       
   142     if !(*INNER_REVLOG_PY_TYPE).is_instance(py, &inner_revlog) {
       
   143         return Err(PyTypeError::new_err("Not an InnerRevlog instance"));
       
   144     }
       
   145     // Safety: this is safe because we checked the PyType already, with the
       
   146     // value embedded in `mercurial.rustext`.
       
   147     Ok(unsafe { InnerRevlog::unchecked_downcast_from(inner_revlog) })
       
   148 }
       
   149 
       
   150 /// This is similar to [`rusthg.py_rust_index_to_graph`], with difference in
       
   151 /// how we retrieve the [`InnerRevlog`].
       
   152 pub fn py_rust_index_to_graph(
       
   153     py: cpython::Python,
       
   154     index_proxy: cpython::PyObject,
       
   155 ) -> PyResult<cpython::UnsafePyLeaked<PySharedIndex>> {
       
   156     let inner_revlog = extract_inner_revlog(
       
   157         py,
       
   158         index_proxy
       
   159             .getattr(py, "inner")
       
   160             .map_err(|e| from_cpython_pyerr(py, e))?,
       
   161     )?;
       
   162 
       
   163     let leaked = inner_revlog.pub_inner(py).leak_immutable();
       
   164     // Safety: we don't leak the "faked" reference out of the `UnsafePyLeaked`
       
   165     Ok(unsafe { leaked.map(py, |idx| PySharedIndex { inner: &idx.index }) })
       
   166 }
       
   167 
       
   168 pub(crate) fn proxy_index_extract<'py>(
       
   169     index_proxy: &Bound<'py, PyAny>,
       
   170 ) -> PyResult<(cpython::Python<'py>, cpython::UnsafePyLeaked<PySharedIndex>)> {
       
   171     let (py, idx_proxy) = to_cpython_py_object(index_proxy);
       
   172     Ok((py, py_rust_index_to_graph(py, idx_proxy)?))
       
   173 }