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}