Skip to main content

sim_lib_server/
coroutine.rs

1use std::sync::{
2    Arc, Mutex,
3    atomic::{AtomicU8, AtomicU64, Ordering},
4};
5
6use sim_citizen_derive::non_citizen;
7use sim_kernel::{Args, ClassRef, Cx, Error, Object, Result, Symbol, Value};
8
9use crate::ServerAddress;
10
11static NEXT_COROUTINE_ID: AtomicU64 = AtomicU64::new(1);
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14/// Lifecycle state of a [`Coroutine`].
15pub enum CoroutineStatus {
16    /// The coroutine handler is currently executing.
17    Running,
18    /// The coroutine has yielded and is waiting to be resumed.
19    Suspended,
20    /// The coroutine has finished, errored, or been cancelled.
21    Done,
22}
23
24impl CoroutineStatus {
25    fn as_u8(self) -> u8 {
26        match self {
27            Self::Running => 0,
28            Self::Suspended => 1,
29            Self::Done => 2,
30        }
31    }
32
33    fn from_u8(value: u8) -> Self {
34        match value {
35            0 => Self::Running,
36            1 => Self::Suspended,
37            2 => Self::Done,
38            _ => Self::Done,
39        }
40    }
41
42    /// Returns the status as its `running`/`suspended`/`done` symbol.
43    pub fn as_symbol(self) -> Symbol {
44        Symbol::new(match self {
45            Self::Running => "running",
46            Self::Suspended => "suspended",
47            Self::Done => "done",
48        })
49    }
50}
51
52#[derive(Default)]
53struct CoroutineState {
54    mailbox: Option<Value>,
55    yielded: Option<Value>,
56    cancelled: bool,
57}
58
59struct CoroutineInner {
60    id: u64,
61    address: ServerAddress,
62    handler: Value,
63    status: AtomicU8,
64    state: Mutex<CoroutineState>,
65}
66
67#[derive(Clone)]
68#[non_citizen(
69    reason = "live coroutine handle; inspect through server/Frame descriptor and coroutine ops",
70    kind = "handle",
71    descriptor = "server/Frame"
72)]
73/// Live, resumable coroutine bound to a server address and handler value.
74///
75/// Cloning shares the same underlying state across handles.
76pub struct Coroutine {
77    inner: Arc<CoroutineInner>,
78}
79
80impl Coroutine {
81    /// Creates a suspended coroutine for `address` driven by `handler`.
82    pub fn new(address: ServerAddress, handler: Value) -> Self {
83        Self {
84            inner: Arc::new(CoroutineInner {
85                id: NEXT_COROUTINE_ID.fetch_add(1, Ordering::Relaxed),
86                address,
87                handler,
88                status: AtomicU8::new(CoroutineStatus::Suspended.as_u8()),
89                state: Mutex::new(CoroutineState::default()),
90            }),
91        }
92    }
93
94    /// Returns the unique, process-local coroutine id.
95    pub fn id(&self) -> u64 {
96        self.inner.id
97    }
98
99    /// Returns the server address this coroutine is bound to.
100    pub fn address(&self) -> &ServerAddress {
101        &self.inner.address
102    }
103
104    /// Returns the current lifecycle status.
105    pub fn status(&self) -> CoroutineStatus {
106        CoroutineStatus::from_u8(self.inner.status.load(Ordering::Relaxed))
107    }
108
109    /// Resumes the coroutine, delivering `input` and running until it yields or
110    /// finishes.
111    ///
112    /// Returns the yielded value if the handler yielded, otherwise the
113    /// handler's final result. Errors if the coroutine is already done or has
114    /// been cancelled.
115    pub fn resume(&self, cx: &mut Cx, input: Value) -> Result<Value> {
116        if self.status() == CoroutineStatus::Done {
117            return Err(Error::Eval("coroutine is done".to_owned()));
118        }
119
120        {
121            let mut state = self
122                .inner
123                .state
124                .lock()
125                .map_err(|_| Error::PoisonedLock("coroutine"))?;
126            if state.cancelled {
127                self.inner
128                    .status
129                    .store(CoroutineStatus::Done.as_u8(), Ordering::Relaxed);
130                return Err(Error::Eval("coroutine is cancelled".to_owned()));
131            }
132            state.mailbox = Some(input.clone());
133            state.yielded = None;
134        }
135
136        self.inner
137            .status
138            .store(CoroutineStatus::Running.as_u8(), Ordering::Relaxed);
139
140        let handle = cx.factory().opaque(Arc::new(self.clone()))?;
141        let result = cx.call_value(self.inner.handler.clone(), Args::new(vec![handle, input]));
142
143        let yielded = {
144            let mut state = self
145                .inner
146                .state
147                .lock()
148                .map_err(|_| Error::PoisonedLock("coroutine"))?;
149            state.mailbox = None;
150            state.yielded.take()
151        };
152
153        match result {
154            Ok(value) => {
155                if let Some(yielded) = yielded {
156                    self.inner
157                        .status
158                        .store(CoroutineStatus::Suspended.as_u8(), Ordering::Relaxed);
159                    Ok(yielded)
160                } else {
161                    self.inner
162                        .status
163                        .store(CoroutineStatus::Done.as_u8(), Ordering::Relaxed);
164                    Ok(value)
165                }
166            }
167            Err(err) => {
168                self.inner
169                    .status
170                    .store(CoroutineStatus::Done.as_u8(), Ordering::Relaxed);
171                Err(err)
172            }
173        }
174    }
175
176    /// Records `value` as the coroutine's yield and returns it.
177    ///
178    /// Only valid from a running coroutine; errors otherwise.
179    pub fn yield_value(&self, value: Value) -> Result<Value> {
180        let mut state = self
181            .inner
182            .state
183            .lock()
184            .map_err(|_| Error::PoisonedLock("coroutine"))?;
185        if self.status() != CoroutineStatus::Running {
186            return Err(Error::Eval(
187                "server/yield may only be used from a running coroutine".to_owned(),
188            ));
189        }
190        state.yielded = Some(value.clone());
191        Ok(value)
192    }
193
194    /// Cancels the coroutine, clearing its state and marking it done.
195    pub fn cancel(&self) -> Result<()> {
196        let mut state = self
197            .inner
198            .state
199            .lock()
200            .map_err(|_| Error::PoisonedLock("coroutine"))?;
201        state.cancelled = true;
202        state.mailbox = None;
203        state.yielded = None;
204        self.inner
205            .status
206            .store(CoroutineStatus::Done.as_u8(), Ordering::Relaxed);
207        Ok(())
208    }
209}
210
211impl Object for Coroutine {
212    fn display(&self, _cx: &mut Cx) -> Result<String> {
213        Ok("#<server-coroutine>".to_owned())
214    }
215
216    fn as_any(&self) -> &dyn std::any::Any {
217        self
218    }
219}
220
221impl sim_kernel::ObjectCompat for Coroutine {
222    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
223        cx.factory().class_stub(
224            sim_kernel::ClassId(0),
225            Symbol::qualified("server", "Coroutine"),
226        )
227    }
228    fn as_expr(&self, cx: &mut Cx) -> Result<sim_kernel::Expr> {
229        self.as_table(cx)?.object().as_expr(cx)
230    }
231    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
232        let state = self
233            .inner
234            .state
235            .lock()
236            .map_err(|_| Error::PoisonedLock("coroutine"))?;
237        let mailbox = match &state.mailbox {
238            Some(mailbox) => mailbox.clone(),
239            None => cx.factory().nil()?,
240        };
241        let cancelled = state.cancelled;
242        drop(state);
243        let address = self.inner.address.as_value(cx)?;
244        cx.factory().table(vec![
245            (
246                Symbol::new("kind"),
247                cx.factory().symbol(Symbol::new("coroutine"))?,
248            ),
249            (
250                Symbol::new("id"),
251                cx.factory().string(self.inner.id.to_string())?,
252            ),
253            (
254                Symbol::new("status"),
255                cx.factory().symbol(self.status().as_symbol())?,
256            ),
257            (Symbol::new("address"), address),
258            (Symbol::new("mailbox"), mailbox),
259            (Symbol::new("cancelled"), cx.factory().bool(cancelled)?),
260        ])
261    }
262}