##// END OF EJS Templates
rust-pyo3: simplified API to get core Index references...
Georges Racinet -
r53311:23370710 default
parent child Browse files
Show More
@@ -1,173 +1,186
1 //! This module takes care of all conversions involving `rusthg` (hg-cpython)
1 //! This module takes care of all conversions involving `rusthg` (hg-cpython)
2 //! objects in the PyO3 call context.
2 //! objects in the PyO3 call context.
3 //!
3 //!
4 //! For source code clarity, we only import (`use`) [`cpython`] traits and not
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
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
6 //! `cpython::PyObject`, and believe that the added heaviness is an acceptatble
7 //! price to pay to avoid confusion.
7 //! price to pay to avoid confusion.
8 //!
8 //!
9 //! Also it, is customary in [`cpython`] to label the GIL lifetime as `'p`,
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
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.
11 //! the arguments side of function signatures when they are not simply elided.
12 use pyo3::exceptions::PyTypeError;
12 use pyo3::exceptions::PyTypeError;
13 use pyo3::prelude::*;
13 use pyo3::prelude::*;
14
14
15 use cpython::ObjectProtocol;
15 use cpython::ObjectProtocol;
16 use cpython::PythonObject;
16 use cpython::PythonObject;
17 use lazy_static::lazy_static;
17 use lazy_static::lazy_static;
18
18
19 use hg::revlog::index::Index as CoreIndex;
19 use rusthg::revlog::{InnerRevlog, PySharedIndex};
20 use rusthg::revlog::{InnerRevlog, PySharedIndex};
20
21
21 /// Force cpython's GIL handle with the appropriate lifetime
22 /// Force cpython's GIL handle with the appropriate lifetime
22 ///
23 ///
23 /// In `pyo3`, the fact that we have the GIL is expressed by the lifetime of
24 /// 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 incoming [`Bound`] smart pointer. We therefore simply instantiate
25 /// the `cpython` handle and coerce its lifetime by the function signature.
26 /// the `cpython` handle and coerce its lifetime by the function signature.
26 ///
27 ///
27 /// Reacquiring the GIL is also a possible alternative, as the CPython
28 /// Reacquiring the GIL is also a possible alternative, as the CPython
28 /// documentation explicitely states that "recursive calls are allowed"
29 /// documentation explicitely states that "recursive calls are allowed"
29 /// (we interpret that as saying that acquiring the GIL within a thread that
30 /// (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 /// already has it works) *as long as it is properly released*
31 /// reference:
32 /// reference:
32 /// <https://docs.python.org/3.8/c-api/init.html#c.PyGILState_Ensure>
33 /// <https://docs.python.org/3.8/c-api/init.html#c.PyGILState_Ensure>
33 pub(crate) fn cpython_handle<'py, T>(
34 pub(crate) fn cpython_handle<'py, T>(
34 _bound: &Bound<'py, T>,
35 _bound: &Bound<'py, T>,
35 ) -> cpython::Python<'py> {
36 ) -> cpython::Python<'py> {
36 // safety: this is safe because the returned object has the 'py lifetime
37 // safety: this is safe because the returned object has the 'py lifetime
37 unsafe { cpython::Python::assume_gil_acquired() }
38 unsafe { cpython::Python::assume_gil_acquired() }
38 }
39 }
39
40
40 /// Force PyO3 GIL handle from cpython's.
41 /// Force PyO3 GIL handle from cpython's.
41 ///
42 ///
42 /// Very similar to [`cpython_handle`]
43 /// Very similar to [`cpython_handle`]
43 pub fn pyo3_handle(_py: cpython::Python<'_>) -> Python<'_> {
44 pub fn pyo3_handle(_py: cpython::Python<'_>) -> Python<'_> {
44 // safety: this is safe because the returned object has the same lifetime
45 // safety: this is safe because the returned object has the same lifetime
45 // as the incoming object.
46 // as the incoming object.
46 unsafe { Python::assume_gil_acquired() }
47 unsafe { Python::assume_gil_acquired() }
47 }
48 }
48
49
49 /// Convert a PyO3 [`PyObject`] into a [`cpython::PyObject`]
50 /// Convert a PyO3 [`PyObject`] into a [`cpython::PyObject`]
50 ///
51 ///
51 /// During this process, the reference count is increased, then decreased.
52 /// During this process, the reference count is increased, then decreased.
52 /// This means that the GIL (symbolized by the lifetime on the `obj`
53 /// This means that the GIL (symbolized by the lifetime on the `obj`
53 /// argument) is needed.
54 /// argument) is needed.
54 ///
55 ///
55 /// We could make something perhaps more handy by simply stealing the
56 /// We could make something perhaps more handy by simply stealing the
56 /// pointer, forgetting the incoming and then implement `From` with "newtype".
57 /// 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 /// It would be worth the effort for a generic cpython-to-pyo3 crate, perhaps
58 /// not for the current endeavour.
59 /// not for the current endeavour.
59 pub(crate) fn to_cpython_py_object<'py>(
60 pub(crate) fn to_cpython_py_object<'py>(
60 obj: &Bound<'py, PyAny>,
61 obj: &Bound<'py, PyAny>,
61 ) -> (cpython::Python<'py>, cpython::PyObject) {
62 ) -> (cpython::Python<'py>, cpython::PyObject) {
62 let py = cpython_handle(obj);
63 let py = cpython_handle(obj);
63 // public alias of the private cpython::fii::PyObject (!)
64 // public alias of the private cpython::fii::PyObject (!)
64 let raw = obj.as_ptr() as *mut python3_sys::PyObject;
65 let raw = obj.as_ptr() as *mut python3_sys::PyObject;
65 // both pyo3 and rust-cpython will decrement the refcount on drop.
66 // both pyo3 and rust-cpython will decrement the refcount on drop.
66 // If we use from_owned_ptr, that's a segfault.
67 // If we use from_owned_ptr, that's a segfault.
67 (py, unsafe { cpython::PyObject::from_borrowed_ptr(py, raw) })
68 (py, unsafe { cpython::PyObject::from_borrowed_ptr(py, raw) })
68 }
69 }
69
70
70 /// Convert a [`cpython::PyObject`] into a PyO3 [`PyObject`]
71 /// Convert a [`cpython::PyObject`] into a PyO3 [`PyObject`]
71 ///
72 ///
72 /// During this process, the reference count is increased, then decreased.
73 /// During this process, the reference count is increased, then decreased.
73 /// This means that the GIL (symbolized by the PyO3 [`Python`] handle is
74 /// This means that the GIL (symbolized by the PyO3 [`Python`] handle is
74 /// needed.
75 /// needed.
75 ///
76 ///
76 /// We could make something perhaps more handy by simply stealing the
77 /// We could make something perhaps more handy by simply stealing the
77 /// pointer, forgetting the incoming and then implement `From` with "newtype".
78 /// 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 /// It would be worth the effort for a generic cpython-to-pyo3 crate, perhaps
79 /// not for the current endeavour.
80 /// not for the current endeavour.
80 pub(crate) fn from_cpython_py_object(
81 pub(crate) fn from_cpython_py_object(
81 py: Python<'_>,
82 py: Python<'_>,
82 obj: cpython::PyObject,
83 obj: cpython::PyObject,
83 ) -> PyObject {
84 ) -> PyObject {
84 let raw = obj.as_ptr() as *mut pyo3::ffi::PyObject;
85 let raw = obj.as_ptr() as *mut pyo3::ffi::PyObject;
85 unsafe { Py::from_borrowed_ptr(py, raw) }
86 unsafe { Py::from_borrowed_ptr(py, raw) }
86 }
87 }
87
88
88 /// Convert [`cpython::PyErr`] into [`pyo3::PyErr`]
89 /// Convert [`cpython::PyErr`] into [`pyo3::PyErr`]
89 ///
90 ///
90 /// The exception class remains the same as the original exception,
91 /// 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 /// 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 /// it will need to be converted to be downcasted in this crate.
93 pub(crate) fn from_cpython_pyerr(
94 pub(crate) fn from_cpython_pyerr(
94 py: cpython::Python<'_>,
95 py: cpython::Python<'_>,
95 mut e: cpython::PyErr,
96 mut e: cpython::PyErr,
96 ) -> PyErr {
97 ) -> PyErr {
97 let pyo3_py = pyo3_handle(py);
98 let pyo3_py = pyo3_handle(py);
98 let cpython_exc_obj = e.instance(py);
99 let cpython_exc_obj = e.instance(py);
99 let pyo3_exc_obj = from_cpython_py_object(pyo3_py, cpython_exc_obj);
100 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 PyErr::from_value(pyo3_exc_obj.into_bound(pyo3_py))
101 }
102 }
102
103
103 /// Retrieve the PyType for objects from the `mercurial.rustext` crate.
104 /// Retrieve the PyType for objects from the `mercurial.rustext` crate.
104 fn retrieve_cpython_py_type(
105 fn retrieve_cpython_py_type(
105 submodule_name: &str,
106 submodule_name: &str,
106 type_name: &str,
107 type_name: &str,
107 ) -> cpython::PyResult<cpython::PyType> {
108 ) -> cpython::PyResult<cpython::PyType> {
108 let guard = cpython::Python::acquire_gil();
109 let guard = cpython::Python::acquire_gil();
109 let py = guard.python();
110 let py = guard.python();
110 let module = py.import(&format!("mercurial.rustext.{submodule_name}"))?;
111 let module = py.import(&format!("mercurial.rustext.{submodule_name}"))?;
111 module.get(py, type_name)?.extract::<cpython::PyType>(py)
112 module.get(py, type_name)?.extract::<cpython::PyType>(py)
112 }
113 }
113
114
114 lazy_static! {
115 lazy_static! {
115 static ref INNER_REVLOG_PY_TYPE: cpython::PyType = {
116 static ref INNER_REVLOG_PY_TYPE: cpython::PyType = {
116 retrieve_cpython_py_type("revlog", "InnerRevlog")
117 retrieve_cpython_py_type("revlog", "InnerRevlog")
117 .expect("Could not import InnerRevlog in Python")
118 .expect("Could not import InnerRevlog in Python")
118 };
119 };
119 }
120 }
120
121
121 /// Downcast [`InnerRevlog`], with the appropriate Python type checking.
122 /// Downcast [`InnerRevlog`], with the appropriate Python type checking.
122 ///
123 ///
123 /// The PyType object representing the `InnerRevlog` Python class is not the
124 /// 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 /// 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 /// 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 /// 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 /// happens by linking to the [`rusthg`] crate and provides the `InnerRevlog`
128 /// that is visible from this crate. The `InnerRevlog::get_type` associated
129 /// 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 /// 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 /// `py_class_impl3.rs`), which obviously is different in both dylibs.
131 ///
132 ///
132 /// The consequence of that is that downcasting an `InnerRevlog` originally
133 /// The consequence of that is that downcasting an `InnerRevlog` originally
133 /// from the `mecurial.rustext` module to our `InnerRevlog` cannot be done with
134 /// 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 /// the usual `extract::<InnerRevlog>(py)`, as it would perform the type
135 /// checking with the `PyType` that is embedded in `mercurial.pyo3_rustext`.
136 /// checking with the `PyType` that is embedded in `mercurial.pyo3_rustext`.
136 /// We must check the `PyType` that is within `mercurial.rustext` instead.
137 /// We must check the `PyType` that is within `mercurial.rustext` instead.
137 /// This is what this function does.
138 /// This is what this function does.
138 fn extract_inner_revlog(
139 fn extract_inner_revlog(
139 py: cpython::Python,
140 py: cpython::Python,
140 inner_revlog: cpython::PyObject,
141 inner_revlog: cpython::PyObject,
141 ) -> PyResult<InnerRevlog> {
142 ) -> PyResult<InnerRevlog> {
142 if !(*INNER_REVLOG_PY_TYPE).is_instance(py, &inner_revlog) {
143 if !(*INNER_REVLOG_PY_TYPE).is_instance(py, &inner_revlog) {
143 return Err(PyTypeError::new_err("Not an InnerRevlog instance"));
144 return Err(PyTypeError::new_err("Not an InnerRevlog instance"));
144 }
145 }
145 // Safety: this is safe because we checked the PyType already, with the
146 // Safety: this is safe because we checked the PyType already, with the
146 // value embedded in `mercurial.rustext`.
147 // value embedded in `mercurial.rustext`.
147 Ok(unsafe { InnerRevlog::unchecked_downcast_from(inner_revlog) })
148 Ok(unsafe { InnerRevlog::unchecked_downcast_from(inner_revlog) })
148 }
149 }
149
150
150 /// This is similar to [`rusthg.py_rust_index_to_graph`], with difference in
151 /// This is similar to [`rusthg.py_rust_index_to_graph`], with difference in
151 /// how we retrieve the [`InnerRevlog`].
152 /// how we retrieve the [`InnerRevlog`].
152 pub fn py_rust_index_to_graph(
153 pub fn py_rust_index_to_graph(
153 py: cpython::Python,
154 py: cpython::Python,
154 index_proxy: cpython::PyObject,
155 index_proxy: cpython::PyObject,
155 ) -> PyResult<cpython::UnsafePyLeaked<PySharedIndex>> {
156 ) -> PyResult<cpython::UnsafePyLeaked<PySharedIndex>> {
156 let inner_revlog = extract_inner_revlog(
157 let inner_revlog = extract_inner_revlog(
157 py,
158 py,
158 index_proxy
159 index_proxy
159 .getattr(py, "inner")
160 .getattr(py, "inner")
160 .map_err(|e| from_cpython_pyerr(py, e))?,
161 .map_err(|e| from_cpython_pyerr(py, e))?,
161 )?;
162 )?;
162
163
163 let leaked = inner_revlog.pub_inner(py).leak_immutable();
164 let leaked = inner_revlog.pub_inner(py).leak_immutable();
164 // Safety: we don't leak the "faked" reference out of the `UnsafePyLeaked`
165 // Safety: we don't leak the "faked" reference out of the `UnsafePyLeaked`
165 Ok(unsafe { leaked.map(py, |idx| PySharedIndex { inner: &idx.index }) })
166 Ok(unsafe { leaked.map(py, |idx| PySharedIndex { inner: &idx.index }) })
166 }
167 }
167
168
168 pub(crate) fn proxy_index_extract<'py>(
169 /// Full extraction of the proxy index object as received in PyO3 to a
170 /// [`CoreIndex`] reference.
171 ///
172 /// The safety invariants to maintain are those of the underlying
173 /// [`UnsafePyLeaked::try_borrow`]: the caller must not leak the inner
174 /// reference.
175 pub(crate) unsafe fn proxy_index_extract<'py>(
169 index_proxy: &Bound<'py, PyAny>,
176 index_proxy: &Bound<'py, PyAny>,
170 ) -> PyResult<(cpython::Python<'py>, cpython::UnsafePyLeaked<PySharedIndex>)> {
177 ) -> PyResult<&'py CoreIndex> {
171 let (py, idx_proxy) = to_cpython_py_object(index_proxy);
178 let (py, idx_proxy) = to_cpython_py_object(index_proxy);
172 Ok((py, py_rust_index_to_graph(py, idx_proxy)?))
179 let py_leaked = py_rust_index_to_graph(py, idx_proxy)?;
180 let py_shared = &*unsafe {
181 py_leaked
182 .try_borrow(py)
183 .map_err(|e| from_cpython_pyerr(py, e))?
184 };
185 Ok(py_shared.inner)
173 }
186 }
@@ -1,73 +1,67
1 // dagops.rs
1 // dagops.rs
2 //
2 //
3 // Copyright 2024 Georges Racinet <georges.racinet@cloudcrane.io>
3 // Copyright 2024 Georges Racinet <georges.racinet@cloudcrane.io>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 //! Bindings for the `hg::dagops` module provided by the
8 //! Bindings for the `hg::dagops` module provided by the
9 //! `hg-core` package.
9 //! `hg-core` package.
10 //!
10 //!
11 //! From Python, this will be seen as `mercurial.pyo3-rustext.dagop`
11 //! From Python, this will be seen as `mercurial.pyo3-rustext.dagop`
12 use pyo3::prelude::*;
12 use pyo3::prelude::*;
13
13
14 use std::collections::HashSet;
14 use std::collections::HashSet;
15
15
16 use hg::{dagops, Revision};
16 use hg::{dagops, Revision};
17
17
18 use crate::convert_cpython::{from_cpython_pyerr, proxy_index_extract};
18 use crate::convert_cpython::proxy_index_extract;
19 use crate::exceptions::GraphError;
19 use crate::exceptions::GraphError;
20 use crate::revision::{rev_pyiter_collect, PyRevision};
20 use crate::revision::{rev_pyiter_collect, PyRevision};
21 use crate::util::new_submodule;
21 use crate::util::new_submodule;
22
22
23 /// Using the the `index_proxy`, return heads out of any Python iterable of
23 /// Using the the `index_proxy`, return heads out of any Python iterable of
24 /// Revisions
24 /// Revisions
25 ///
25 ///
26 /// This is the Rust counterpart for `mercurial.dagop.headrevs`
26 /// This is the Rust counterpart for `mercurial.dagop.headrevs`
27 #[pyfunction]
27 #[pyfunction]
28 pub fn headrevs(
28 pub fn headrevs(
29 index_proxy: &Bound<'_, PyAny>,
29 index_proxy: &Bound<'_, PyAny>,
30 revs: &Bound<'_, PyAny>,
30 revs: &Bound<'_, PyAny>,
31 ) -> PyResult<HashSet<PyRevision>> {
31 ) -> PyResult<HashSet<PyRevision>> {
32 let (py, py_leaked) = proxy_index_extract(index_proxy)?;
33 // Safety: we don't leak the "faked" reference out of `UnsafePyLeaked`
32 // Safety: we don't leak the "faked" reference out of `UnsafePyLeaked`
34 let index = &*unsafe {
33 let index = unsafe { proxy_index_extract(index_proxy)? };
35 py_leaked
36 .try_borrow(py)
37 .map_err(|e| from_cpython_pyerr(py, e))?
38 };
39
40 let mut as_set: HashSet<Revision> = rev_pyiter_collect(revs, index)?;
34 let mut as_set: HashSet<Revision> = rev_pyiter_collect(revs, index)?;
41 dagops::retain_heads(index, &mut as_set).map_err(GraphError::from_hg)?;
35 dagops::retain_heads(index, &mut as_set).map_err(GraphError::from_hg)?;
42 Ok(as_set.into_iter().map(Into::into).collect())
36 Ok(as_set.into_iter().map(Into::into).collect())
43 }
37 }
44
38
45 /// Computes the rank, i.e. the number of ancestors including itself,
39 /// Computes the rank, i.e. the number of ancestors including itself,
46 /// of a node represented by its parents.
40 /// of a node represented by its parents.
47 ///
41 ///
48 /// Currently, the pure Rust index supports only the REVLOGV1 format, hence
42 /// Currently, the pure Rust index supports only the REVLOGV1 format, hence
49 /// the only possible return value is that the rank is unknown.
43 /// the only possible return value is that the rank is unknown.
50 ///
44 ///
51 /// References:
45 /// References:
52 /// - C implementation, function `index_fast_rank()`.
46 /// - C implementation, function `index_fast_rank()`.
53 /// - `impl vcsgraph::graph::RankedGraph for Index` in `crate::cindex`.
47 /// - `impl vcsgraph::graph::RankedGraph for Index` in `crate::cindex`.
54 #[pyfunction]
48 #[pyfunction]
55 pub fn rank(
49 pub fn rank(
56 _index: &Bound<'_, PyAny>,
50 _index: &Bound<'_, PyAny>,
57 _p1r: PyRevision,
51 _p1r: PyRevision,
58 _p2r: PyRevision,
52 _p2r: PyRevision,
59 ) -> PyResult<()> {
53 ) -> PyResult<()> {
60 Err(GraphError::from_vcsgraph(
54 Err(GraphError::from_vcsgraph(
61 vcsgraph::graph::GraphReadError::InconsistentGraphData,
55 vcsgraph::graph::GraphReadError::InconsistentGraphData,
62 ))
56 ))
63 }
57 }
64
58
65 pub fn init_module<'py>(
59 pub fn init_module<'py>(
66 py: Python<'py>,
60 py: Python<'py>,
67 package: &str,
61 package: &str,
68 ) -> PyResult<Bound<'py, PyModule>> {
62 ) -> PyResult<Bound<'py, PyModule>> {
69 let m = new_submodule(py, package, "dagop")?;
63 let m = new_submodule(py, package, "dagop")?;
70 m.add_function(wrap_pyfunction!(headrevs, &m)?)?;
64 m.add_function(wrap_pyfunction!(headrevs, &m)?)?;
71 m.add_function(wrap_pyfunction!(rank, &m)?)?;
65 m.add_function(wrap_pyfunction!(rank, &m)?)?;
72 Ok(m)
66 Ok(m)
73 }
67 }
General Comments 0
You need to be logged in to leave comments. Login now