hyperlight_common/resource.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4//! Shared operations around resources
5
6// "Needless" lifetimes are useful for clarity
7#![allow(clippy::needless_lifetimes)]
8
9use alloc::sync::Arc;
10
11#[cfg(feature = "std")]
12extern crate std;
13use core::marker::{PhantomData, Send};
14use core::ops::Deref;
15#[cfg(feature = "std")]
16use std::sync::{RwLock, RwLockReadGuard};
17
18#[cfg(not(feature = "std"))]
19use spin::{RwLock, RwLockReadGuard};
20
21/// The semantics of component model resources are, pleasingly,
22/// roughly compatible with those of Rust references, so we would like
23/// to use the more-or-less directly in interfaces generated by
24/// hyperlight_component_macro. Less pleasingly, it's not terribly
25/// easy to show the semantic agreement statically.
26///
27/// In particular, if the host calls into the guest and gives it a
28/// borrow of a resource, reentrant host function calls that use that
29/// borrow need to be able to resolve the original reference and use
30/// it in an appropriately scoped manner, but it is not simple to do
31/// this, because the core Hyperlight machinery doesn't offer an easy
32/// way to augment the host's context for the span of time of a guest
33/// function call. This may be worth revisiting at some time, but in
34/// the meantime, it's easier to just do it dynamically.
35///
36/// # Safety
37/// Informally: this only creates SharedRead references, so having a
38/// bunch of them going at once is fine. Safe Rust in the host can't
39/// use any earlier borrows (potentially invalidating these) until
40/// borrow passed into [`ResourceEntry::lend`] has expired. Because
41/// that borrow outlives the [`LentResourceGuard`], it will not expire
42/// until that destructor is called. That destructor ensures that (a)
43/// there are no outstanding [`BorrowedResourceGuard`]s alive (since
44/// they would be holding the read side of the [`RwLock`] if they
45/// were), and that (b) the shared flag has been set to false, so
46/// [`ResourceEntry::borrow`] will never create another borrow
47pub enum ResourceEntry<T> {
48 Empty,
49 Owned(T),
50 Borrowed(Arc<RwLock<bool>>, *const T),
51}
52unsafe impl<T: Send> Send for ResourceEntry<T> {}
53
54pub struct LentResourceGuard<'a> {
55 flag: Arc<RwLock<bool>>,
56 already_revoked: bool,
57 _phantom: core::marker::PhantomData<&'a mut ()>,
58}
59impl<'a> LentResourceGuard<'a> {
60 pub fn revoke_nonblocking(&mut self) -> bool {
61 #[cfg(feature = "std")]
62 let Ok(mut flag) = self.flag.try_write() else {
63 return false;
64 };
65 #[cfg(not(feature = "std"))]
66 let Some(mut flag) = self.flag.try_write() else {
67 return false;
68 };
69 *flag = false;
70 self.already_revoked = true;
71 true
72 }
73}
74impl<'a> Drop for LentResourceGuard<'a> {
75 fn drop(&mut self) {
76 if !self.already_revoked {
77 #[allow(unused_mut)] // it isn't actually unused
78 let mut guard = self.flag.write();
79 #[cfg(feature = "std")]
80 // If a mutex that is just protecting us from our own
81 // mistakes is poisoned, something is so seriously
82 // wrong that dying is a sensible response.
83 #[allow(clippy::unwrap_used)]
84 {
85 *guard.unwrap() = false;
86 }
87 #[cfg(not(feature = "std"))]
88 {
89 *guard = false;
90 }
91 }
92 }
93}
94pub struct BorrowedResourceGuard<'a, T> {
95 _flag: Option<RwLockReadGuard<'a, bool>>,
96 reference: &'a T,
97}
98impl<'a, T> Deref for BorrowedResourceGuard<'a, T> {
99 type Target = T;
100 fn deref(&self) -> &T {
101 self.reference
102 }
103}
104impl<T> ResourceEntry<T> {
105 pub fn give(x: T) -> ResourceEntry<T> {
106 ResourceEntry::Owned(x)
107 }
108 pub fn lend<'a>(x: &'a T) -> (LentResourceGuard<'a>, ResourceEntry<T>) {
109 let flag = Arc::new(RwLock::new(true));
110 (
111 LentResourceGuard {
112 flag: flag.clone(),
113 already_revoked: false,
114 _phantom: PhantomData {},
115 },
116 ResourceEntry::Borrowed(flag, x as *const T),
117 )
118 }
119 pub fn borrow<'a>(&'a self) -> Option<BorrowedResourceGuard<'a, T>> {
120 match self {
121 ResourceEntry::Empty => None,
122 ResourceEntry::Owned(t) => Some(BorrowedResourceGuard {
123 _flag: None,
124 reference: t,
125 }),
126 ResourceEntry::Borrowed(flag, t) => {
127 let guard = flag.read();
128 // If a mutex that is just protecting us from our own
129 // mistakes is poisoned, something is so seriously
130 // wrong that dying is a sensible response.
131 #[allow(clippy::unwrap_used)]
132 let flag = {
133 #[cfg(feature = "std")]
134 {
135 guard.unwrap()
136 }
137 #[cfg(not(feature = "std"))]
138 {
139 guard
140 }
141 };
142 if *flag {
143 Some(BorrowedResourceGuard {
144 _flag: Some(flag),
145 reference: unsafe { &**t },
146 })
147 } else {
148 None
149 }
150 }
151 }
152 }
153 pub fn take(&mut self) -> Option<T> {
154 match core::mem::replace(self, ResourceEntry::Empty) {
155 ResourceEntry::Owned(t) => Some(t),
156 _ => None,
157 }
158 }
159}