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}