Skip to main content

graphrecords_python/graphrecord/
borrowed.rs

1use super::{PyGraphRecord, PyGraphRecordInner};
2use graphrecords_core::{
3    PluginGraphRecord,
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 `PluginGraphRecord` pointer, protected by an [`RwLock`].
14///
15/// The inner [`NonNull`] is only set to `Some` inside [`PyGraphRecord::scope`] and is
16/// cleared when the scope ends. A "dead" handle (`None`) returns a runtime error
17/// on every access attempt.
18///
19/// # Construction invariant
20///
21/// Only [`PyGraphRecord::scope`] (defined in this module) can create a *live*
22/// `BorrowedGraphRecord`. Outside code can only obtain a *dead* handle via
23/// [`BorrowedGraphRecord::dead`], because the tuple field is private to this module.
24pub(super) struct BorrowedGraphRecord(RwLock<Option<NonNull<PluginGraphRecord>>>);
25
26// SAFETY: The `NonNull<PluginGraphRecord>` is protected by an `RwLock`, ensuring synchronized
27// access. The pointer is only valid during the `scope()` call, and the scope's Drop guard
28// acquires a write lock to clear it, which cannot proceed while any read/write guard is
29// held, preventing use-after-free.
30unsafe impl Send for BorrowedGraphRecord {}
31unsafe impl Sync for BorrowedGraphRecord {}
32
33impl Debug for BorrowedGraphRecord {
34    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
35        f.debug_struct("BorrowedGraphRecord")
36            .field("alive", &self.0.read().is_some())
37            .finish()
38    }
39}
40
41impl BorrowedGraphRecord {
42    /// Creates a dead handle with no pointer. Any access will return an error.
43    pub(super) const fn dead() -> Self {
44        Self(RwLock::new(None))
45    }
46
47    pub(super) fn read(&self) -> RwLockReadGuard<'_, Option<NonNull<PluginGraphRecord>>> {
48        self.0.read()
49    }
50
51    pub(super) fn write(&self) -> RwLockWriteGuard<'_, Option<NonNull<PluginGraphRecord>>> {
52        self.0.write()
53    }
54}
55
56impl PyGraphRecord {
57    /// Safely pass a `&mut PluginGraphRecord` to Python as a `PyGraphRecord` for the
58    /// duration of the callback. The pointer is invalidated when `function` returns.
59    ///
60    /// Based on the discussion in:
61    /// - <https://github.com/PyO3/pyo3/issues/1180>
62    ///
63    /// Guard pattern adapted from:
64    /// - <https://github.com/PyO3/pyo3/issues/1180#issuecomment-692898577>
65    pub fn scope<R>(
66        py: Python<'_>,
67        graphrecord: &mut PluginGraphRecord,
68        function: impl FnOnce(Python<'_>, &Py<Self>) -> GraphRecordResult<R>,
69    ) -> GraphRecordResult<R> {
70        struct PanicOnDrop(bool);
71        impl Drop for PanicOnDrop {
72            fn drop(&mut self) {
73                assert!(!self.0, "failed to clear PyGraphRecord borrow");
74            }
75        }
76
77        struct Guard<'py>(Python<'py>, Py<PyGraphRecord>, NonNull<PluginGraphRecord>);
78        impl Drop for Guard<'_> {
79            #[allow(clippy::significant_drop_tightening)]
80            fn drop(&mut self) {
81                let panic_on_drop = PanicOnDrop(true);
82                let py_graphrecord = self.1.bind(self.0).get();
83                match &py_graphrecord.inner {
84                    PyGraphRecordInner::Borrowed(borrowed) => {
85                        let mut guard = borrowed.write();
86                        assert_eq!(
87                            guard.take(),
88                            Some(self.2),
89                            "PyGraphRecord was tampered with"
90                        );
91                    }
92                    PyGraphRecordInner::Owned(_) => {
93                        panic!("PyGraphRecord was replaced with an owned variant");
94                    }
95                }
96                std::mem::forget(panic_on_drop);
97            }
98        }
99
100        let pointer = NonNull::from(graphrecord);
101        let guard = Guard(
102            py,
103            Py::new(
104                py,
105                Self {
106                    inner: PyGraphRecordInner::Borrowed(BorrowedGraphRecord(RwLock::new(Some(
107                        pointer,
108                    )))),
109                },
110            )
111            .map_err(|error| {
112                GraphRecordError::ConversionError(format!(
113                    "Failed to create PyGraphRecord: {error}"
114                ))
115            })?,
116            pointer,
117        );
118        function(py, &guard.1)
119    }
120}