Skip to main content

graphrecords_python/graphrecord/
borrowed.rs

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