Skip to main content

graphrecords_python/graphrecord/
borrowed.rs

1use super::{PyGraphRecord, PyGraphRecordInner};
2use graphrecords_core::{
3    GraphRecord,
4    errors::{GraphRecordError, GraphRecordResult},
5};
6use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
7use pyo3::{Py, Python};
8use std::{
9    fmt::{Debug, Formatter, Result},
10    ptr::NonNull,
11};
12
13/// Wrapper around a borrowed `GraphRecord` pointer, protected by an [`RwLock`].
14///
15/// The inner [`NonNull`] is only set to `Some` inside [`PyGraphRecord::scope`] /
16/// [`PyGraphRecord::scope_mut`] and is cleared when the scope ends. A "dead" handle
17/// (`None`) returns a runtime error on every access attempt.
18///
19/// The `mutable` flag tracks whether the pointer originated from `&mut GraphRecord`
20/// (via [`PyGraphRecord::scope_mut`]) or `&GraphRecord` (via [`PyGraphRecord::scope`]).
21/// When `mutable` is `false`, [`PyGraphRecord::inner_mut`] refuses to hand out an
22/// `InnerRefMut`, preventing `NonNull::as_mut()` from ever being called on a pointer
23/// that came from a shared reference.
24///
25/// # Construction invariant
26///
27/// Only [`PyGraphRecord::scope`] and [`PyGraphRecord::scope_mut`] (defined in this
28/// module) can create a *live* `BorrowedGraphRecord`. Outside code can only obtain a
29/// *dead* handle via [`BorrowedGraphRecord::dead`], because the fields are private to
30/// this module.
31pub(super) struct BorrowedGraphRecord {
32    ptr: RwLock<Option<NonNull<GraphRecord>>>,
33    mutable: bool,
34}
35
36// SAFETY: The `NonNull<GraphRecord>` is protected by an `RwLock`, ensuring synchronized
37// access. The pointer is only valid during the `scope()`/`scope_mut()` call, and the
38// scope's Drop guard acquires a write lock to clear it, which cannot proceed while any
39// read/write guard is held, preventing use-after-free. The `mutable` field is set at
40// construction and never modified, so concurrent reads are safe.
41unsafe impl Send for BorrowedGraphRecord {}
42unsafe impl Sync for BorrowedGraphRecord {}
43
44impl Debug for BorrowedGraphRecord {
45    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
46        f.debug_struct("BorrowedGraphRecord")
47            .field("alive", &self.ptr.read().is_some())
48            .field("mutable", &self.mutable)
49            .finish()
50    }
51}
52
53impl BorrowedGraphRecord {
54    /// Creates a dead handle with no pointer. Any access will return an error.
55    pub(super) const fn dead() -> Self {
56        Self {
57            ptr: RwLock::new(None),
58            mutable: false,
59        }
60    }
61
62    pub(super) const fn is_mutable(&self) -> bool {
63        self.mutable
64    }
65
66    pub(super) fn read(&self) -> RwLockReadGuard<'_, Option<NonNull<GraphRecord>>> {
67        self.ptr.read()
68    }
69
70    pub(super) fn write(&self) -> RwLockWriteGuard<'_, Option<NonNull<GraphRecord>>> {
71        self.ptr.write()
72    }
73}
74
75/// Generates a scoped borrow method on [`PyGraphRecord`].
76///
77/// Each invocation produces a standalone function with its own `Guard` and `PanicOnDrop`
78/// types. The macro's `$ref_type` parameter determines the input reference kind
79/// (`&GraphRecord` or `&mut GraphRecord`), and `$mutable` controls whether the resulting
80/// `BorrowedGraphRecord` permits mutation — so the safety properties are fixed at compile
81/// time per expansion, with no shared runtime helper that could be misused.
82///
83/// Based on the discussion in:
84/// - <https://github.com/PyO3/pyo3/issues/1180>
85///
86/// Guard pattern adapted from:
87/// - <https://github.com/PyO3/pyo3/issues/1180#issuecomment-692898577>
88macro_rules! impl_scope {
89    ($(#[$meta:meta])* $name:ident, $ref_type:ty, $mutable:expr) => {
90        $(#[$meta])*
91        pub fn $name<R>(
92            py: Python<'_>,
93            graphrecord: $ref_type,
94            function: impl FnOnce(Python<'_>, &Py<Self>) -> GraphRecordResult<R>,
95        ) -> GraphRecordResult<R> {
96            struct PanicOnDrop(bool);
97            impl Drop for PanicOnDrop {
98                fn drop(&mut self) {
99                    assert!(!self.0, "failed to clear PyGraphRecord borrow");
100                }
101            }
102
103            struct Guard<'py>(Python<'py>, Py<PyGraphRecord>, NonNull<GraphRecord>);
104            impl Drop for Guard<'_> {
105                #[allow(clippy::significant_drop_tightening)]
106                fn drop(&mut self) {
107                    let panic_on_drop = PanicOnDrop(true);
108                    let py_graphrecord = self.1.bind(self.0).get();
109                    match &py_graphrecord.inner {
110                        PyGraphRecordInner::Borrowed(borrowed) => {
111                            let mut guard = borrowed.write();
112                            assert_eq!(
113                                guard.take(),
114                                Some(self.2),
115                                "PyGraphRecord was tampered with"
116                            );
117                        }
118                        PyGraphRecordInner::Owned(_)
119                        | PyGraphRecordInner::Connected(_) => {
120                            panic!("PyGraphRecord was replaced with a non-borrowed variant");
121                        }
122                    }
123                    std::mem::forget(panic_on_drop);
124                }
125            }
126
127            let pointer = NonNull::from(graphrecord);
128            let guard = Guard(
129                py,
130                Py::new(
131                    py,
132                    Self {
133                        inner: PyGraphRecordInner::Borrowed(BorrowedGraphRecord {
134                            ptr: RwLock::new(Some(pointer)),
135                            mutable: $mutable,
136                        }),
137                    },
138                )
139                .map_err(|error| {
140                    GraphRecordError::ConversionError(format!(
141                        "Failed to create PyGraphRecord: {error}"
142                    ))
143                })?,
144                pointer,
145            );
146            function(py, &guard.1)
147        }
148    };
149}
150
151impl PyGraphRecord {
152    impl_scope!(
153        /// Safely pass a `&GraphRecord` to Python as a read-only `PyGraphRecord` for the
154        /// duration of the callback. The pointer is invalidated when `function` returns.
155        ///
156        /// The resulting `PyGraphRecord` will reject any mutation attempts with a runtime
157        /// error. See [`Self::scope_mut`] for the read-write variant.
158        scope, &GraphRecord, false
159    );
160
161    impl_scope!(
162        /// Safely pass a `&mut GraphRecord` to Python as a `PyGraphRecord` for the
163        /// duration of the callback. The pointer is invalidated when `function` returns.
164        ///
165        /// The resulting `PyGraphRecord` allows both reads and mutations. See [`Self::scope`]
166        /// for the read-only variant.
167        scope_mut, &mut GraphRecord, true
168    );
169}