##// END OF EJS Templates
rust-pyo3: error propagation for UnsafePyLeaked wrapping a result...
Georges Racinet -
r53426:bd65cb04 default
parent child Browse files
Show More
@@ -1,214 +1,252
1 1 //! This module takes care of all conversions involving `rusthg` (hg-cpython)
2 2 //! objects in the PyO3 call context.
3 3 //!
4 4 //! For source code clarity, we only import (`use`) [`cpython`] traits and not
5 5 //! any of its data objects. We are instead using full qualifiers, such as
6 6 //! `cpython::PyObject`, and believe that the added heaviness is an acceptatble
7 7 //! price to pay to avoid confusion.
8 8 //!
9 9 //! Also it, is customary in [`cpython`] to label the GIL lifetime as `'p`,
10 10 //! whereas it is `'py` in PyO3 context. We keep both these conventions in
11 11 //! the arguments side of function signatures when they are not simply elided.
12 12 use pyo3::exceptions::PyTypeError;
13 13 use pyo3::prelude::*;
14 14 use pyo3::{pyclass::boolean_struct::False, PyClass};
15 15
16 16 use cpython::ObjectProtocol;
17 17 use cpython::PythonObject;
18 18 use lazy_static::lazy_static;
19 19
20 20 use hg::revlog::index::Index as CoreIndex;
21 21 use rusthg::revlog::{InnerRevlog, PySharedIndex};
22 22
23 23 /// Marker trait for PyO3 objects with a lifetime representing the acquired GIL
24 24 ///
25 25 /// # Safety
26 26 ///
27 27 /// This trait must not be implemented for objects with lifetimes that
28 28 /// do not imply in PyO3 that the GIL is acquired during the whole lifetime.
29 29 pub unsafe trait WithGIL<'py> {}
30 30
31 31 // Safety: the lifetime on these PyO3 objects all represent the acquired GIL
32 32 unsafe impl<'py> WithGIL<'py> for Python<'py> {}
33 33 unsafe impl<'py, T> WithGIL<'py> for Bound<'py, T> {}
34 34 unsafe impl<'py, T: PyClass> WithGIL<'py> for PyRef<'py, T> {}
35 35 unsafe impl<'py, T: PyClass<Frozen = False>> WithGIL<'py>
36 36 for PyRefMut<'py, T>
37 37 {
38 38 }
39 39
40 40 /// Force cpython's GIL handle with the appropriate lifetime
41 41 ///
42 42 /// In `pyo3`, the fact that we have the GIL is expressed by the lifetime of
43 43 /// the incoming [`Bound`] smart pointer. We therefore simply instantiate
44 44 /// the `cpython` handle and coerce its lifetime by the function signature.
45 45 ///
46 46 /// Reacquiring the GIL is also a possible alternative, as the CPython
47 47 /// documentation explicitely states that "recursive calls are allowed"
48 48 /// (we interpret that as saying that acquiring the GIL within a thread that
49 49 /// already has it works) *as long as it is properly released*
50 50 /// reference:
51 51 /// <https://docs.python.org/3.8/c-api/init.html#c.PyGILState_Ensure>
52 52 pub(crate) fn cpython_handle<'py, T: WithGIL<'py>>(
53 53 _with_gil: &T,
54 54 ) -> cpython::Python<'py> {
55 55 // safety: this is safe because the returned object has the same lifetime
56 56 // as the incoming object.
57 57 unsafe { cpython::Python::assume_gil_acquired() }
58 58 }
59 59
60 60 /// Force PyO3 GIL handle from cpython's.
61 61 ///
62 62 /// Very similar to [`cpython_handle`]
63 63 pub fn pyo3_handle(_py: cpython::Python<'_>) -> Python<'_> {
64 64 // safety: this is safe because the returned object has the same lifetime
65 65 // as the incoming object.
66 66 unsafe { Python::assume_gil_acquired() }
67 67 }
68 68
69 69 /// Convert a PyO3 [`PyObject`] into a [`cpython::PyObject`]
70 70 ///
71 71 /// During this process, the reference count is increased, then decreased.
72 72 /// This means that the GIL (symbolized by the lifetime on the `obj`
73 73 /// argument) is needed.
74 74 ///
75 75 /// We could make something perhaps more handy by simply stealing the
76 76 /// pointer, forgetting the incoming and then implement `From` with "newtype".
77 77 /// It would be worth the effort for a generic cpython-to-pyo3 crate, perhaps
78 78 /// not for the current endeavour.
79 79 pub(crate) fn to_cpython_py_object<'py>(
80 80 obj: &Bound<'py, PyAny>,
81 81 ) -> (cpython::Python<'py>, cpython::PyObject) {
82 82 let py = cpython_handle(obj);
83 83 // public alias of the private cpython::fii::PyObject (!)
84 84 let raw = obj.as_ptr() as *mut python3_sys::PyObject;
85 85 // both pyo3 and rust-cpython will decrement the refcount on drop.
86 86 // If we use from_owned_ptr, that's a segfault.
87 87 (py, unsafe { cpython::PyObject::from_borrowed_ptr(py, raw) })
88 88 }
89 89
90 90 /// Convert a [`cpython::PyObject`] into a PyO3 [`PyObject`]
91 91 ///
92 92 /// During this process, the reference count is increased, then decreased.
93 93 /// This means that the GIL (symbolized by the PyO3 [`Python`] handle is
94 94 /// needed.
95 95 ///
96 96 /// We could make something perhaps more handy by simply stealing the
97 97 /// pointer, forgetting the incoming and then implement `From` with "newtype".
98 98 /// It would be worth the effort for a generic cpython-to-pyo3 crate, perhaps
99 99 /// not for the current endeavour.
100 100 pub(crate) fn from_cpython_py_object(
101 101 py: Python<'_>,
102 102 obj: cpython::PyObject,
103 103 ) -> PyObject {
104 104 let raw = obj.as_ptr() as *mut pyo3::ffi::PyObject;
105 105 unsafe { Py::from_borrowed_ptr(py, raw) }
106 106 }
107 107
108 108 /// Convert [`cpython::PyErr`] into [`pyo3::PyErr`]
109 109 ///
110 110 /// The exception class remains the same as the original exception,
111 111 /// hence if it is also defined in another dylib based on `cpython` crate,
112 112 /// it will need to be converted to be downcasted in this crate.
113 113 pub(crate) fn from_cpython_pyerr(
114 114 py: cpython::Python<'_>,
115 115 mut e: cpython::PyErr,
116 116 ) -> PyErr {
117 117 let pyo3_py = pyo3_handle(py);
118 118 let cpython_exc_obj = e.instance(py);
119 119 let pyo3_exc_obj = from_cpython_py_object(pyo3_py, cpython_exc_obj);
120 120 PyErr::from_value(pyo3_exc_obj.into_bound(pyo3_py))
121 121 }
122 122
123 123 /// Retrieve the PyType for objects from the `mercurial.rustext` crate.
124 124 fn retrieve_cpython_py_type(
125 125 submodule_name: &str,
126 126 type_name: &str,
127 127 ) -> cpython::PyResult<cpython::PyType> {
128 128 let guard = cpython::Python::acquire_gil();
129 129 let py = guard.python();
130 130 let module = py.import(&format!("mercurial.rustext.{submodule_name}"))?;
131 131 module.get(py, type_name)?.extract::<cpython::PyType>(py)
132 132 }
133 133
134 134 lazy_static! {
135 135 static ref INNER_REVLOG_PY_TYPE: cpython::PyType = {
136 136 retrieve_cpython_py_type("revlog", "InnerRevlog")
137 137 .expect("Could not import InnerRevlog in Python")
138 138 };
139 139 }
140 140
141 141 /// Downcast [`InnerRevlog`], with the appropriate Python type checking.
142 142 ///
143 143 /// The PyType object representing the `InnerRevlog` Python class is not the
144 144 /// the same in this dylib as it is in the `mercurial.rustext` module.
145 145 /// This is because the code created with the [`cpython::py_class!`]
146 146 /// macro is itself duplicated in both dylibs. In the case of this crate, this
147 147 /// happens by linking to the [`rusthg`] crate and provides the `InnerRevlog`
148 148 /// that is visible from this crate. The `InnerRevlog::get_type` associated
149 149 /// function turns out to return a `static mut` (look for `TYPE_OBJECT` in
150 150 /// `py_class_impl3.rs`), which obviously is different in both dylibs.
151 151 ///
152 152 /// The consequence of that is that downcasting an `InnerRevlog` originally
153 153 /// from the `mecurial.rustext` module to our `InnerRevlog` cannot be done with
154 154 /// the usual `extract::<InnerRevlog>(py)`, as it would perform the type
155 155 /// checking with the `PyType` that is embedded in `mercurial.pyo3_rustext`.
156 156 /// We must check the `PyType` that is within `mercurial.rustext` instead.
157 157 /// This is what this function does.
158 158 fn extract_inner_revlog(
159 159 py: cpython::Python,
160 160 inner_revlog: cpython::PyObject,
161 161 ) -> PyResult<InnerRevlog> {
162 162 if !(*INNER_REVLOG_PY_TYPE).is_instance(py, &inner_revlog) {
163 163 return Err(PyTypeError::new_err("Not an InnerRevlog instance"));
164 164 }
165 165 // Safety: this is safe because we checked the PyType already, with the
166 166 // value embedded in `mercurial.rustext`.
167 167 Ok(unsafe { InnerRevlog::unchecked_downcast_from(inner_revlog) })
168 168 }
169 169
170 170 /// This is similar to [`rusthg.py_rust_index_to_graph`], with difference in
171 171 /// how we retrieve the [`InnerRevlog`].
172 172 pub fn py_rust_index_to_graph(
173 173 py: cpython::Python,
174 174 index_proxy: cpython::PyObject,
175 175 ) -> PyResult<cpython::UnsafePyLeaked<PySharedIndex>> {
176 176 let inner_revlog = extract_inner_revlog(
177 177 py,
178 178 index_proxy
179 179 .getattr(py, "inner")
180 180 .map_err(|e| from_cpython_pyerr(py, e))?,
181 181 )?;
182 182
183 183 let leaked = inner_revlog.pub_inner(py).leak_immutable();
184 184 // Safety: we don't leak the "faked" reference out of the `UnsafePyLeaked`
185 185 Ok(unsafe { leaked.map(py, |idx| PySharedIndex { inner: &idx.index }) })
186 186 }
187 187
188 188 pub(crate) fn proxy_index_py_leak<'py>(
189 189 index_proxy: &Bound<'py, PyAny>,
190 190 ) -> PyResult<(cpython::Python<'py>, cpython::UnsafePyLeaked<PySharedIndex>)> {
191 191 let (py, idx_proxy) = to_cpython_py_object(index_proxy);
192 192 let py_leaked = py_rust_index_to_graph(py, idx_proxy)?;
193 193 Ok((py, py_leaked))
194 194 }
195 195
196 196 /// Full extraction of the proxy index object as received in PyO3 to a
197 197 /// [`CoreIndex`] reference.
198 198 ///
199 199 /// # Safety
200 200 ///
201 201 /// The invariants to maintain are those of the underlying
202 202 /// [`UnsafePyLeaked::try_borrow`]: the caller must not leak the inner
203 203 /// reference.
204 204 pub(crate) unsafe fn proxy_index_extract<'py>(
205 205 index_proxy: &Bound<'py, PyAny>,
206 206 ) -> PyResult<&'py CoreIndex> {
207 207 let (py, py_leaked) = proxy_index_py_leak(index_proxy)?;
208 208 let py_shared = &*unsafe {
209 209 py_leaked
210 210 .try_borrow(py)
211 211 .map_err(|e| from_cpython_pyerr(py, e))?
212 212 };
213 213 Ok(py_shared.inner)
214 214 }
215
216 /// Error propagation for an [`UnsafePyLeaked`] wrapping a [`Result`]
217 ///
218 /// TODO (will consider when implementing UnsafePyLeaked in PyO3):
219 /// It would be nice for UnsafePyLeaked to provide this directly as a variant
220 /// of the `map` method with a signature such as:
221 ///
222 /// ```
223 /// unsafe fn map_or_err(&self,
224 /// py: Python,
225 /// f: impl FnOnce(T) -> Result(U, E),
226 /// convert_err: impl FnOnce(E) -> PyErr)
227 /// ```
228 ///
229 /// This would spare users of the `cpython` crate the additional `unsafe` deref
230 /// to inspect the error and return it outside `UnsafePyLeaked`, and the
231 /// subsequent unwrapping that this function performs.
232 #[allow(dead_code)]
233 pub(crate) fn py_leaked_or_map_err<T, E: std::fmt::Debug + Copy>(
234 py: cpython::Python,
235 leaked: cpython::UnsafePyLeaked<Result<T, E>>,
236 convert_err: impl FnOnce(E) -> PyErr,
237 ) -> PyResult<cpython::UnsafePyLeaked<T>> {
238 // Safety: we don't leak the "faked" reference out of `UnsafePyLeaked`
239 if let Err(e) = *unsafe {
240 leaked
241 .try_borrow(py)
242 .map_err(|e| from_cpython_pyerr(py, e))?
243 } {
244 return Err(convert_err(e));
245 }
246 // Safety: we don't leak the "faked" reference out of `UnsafePyLeaked`
247 Ok(unsafe {
248 leaked.map(py, |res| {
249 res.expect("Error case should have already be treated")
250 })
251 })
252 }
General Comments 0
You need to be logged in to leave comments. Login now