Skip to main content

luna_core/runtime/
userdata.rs

1//! Userdata objects. In luna the only userdata are io file handles (there is no
2//! C API exposing arbitrary host objects), so a userdata wraps a file/stream
3//! handle plus an optional metatable — the shared `FILE*` metatable attached by
4//! the io library. Full io handle methods (read/write/seek/…) land with the io
5//! file model; this is the GC-level object + identity.
6
7use crate::runtime::heap::{Gc, GcHeader, Marker};
8use crate::runtime::table::Table;
9use crate::runtime::value::Value;
10
11/// Type of the per-host trace adapter stored in [`UserdataPayload::Host`].
12/// Captured at `create_userdata::<T>` time as a monomorphic
13/// `trace_fn_for::<T>` whose body calls `T::trace` on the downcast
14/// payload.
15///
16/// `fn` items are inherently `Send + Sync` and `Copy`, so this type
17/// imposes no auto-trait constraints on `Userdata`; `feature = "send"`
18/// layers atop without changing the signature.
19pub(crate) type HostTraceFn =
20    fn(&(dyn std::any::Any + 'static), &mut crate::vm::UserdataMarker<'_>);
21
22/// A Lua userdata object — a GC-managed handle wrapping a host-side payload
23/// (an io file handle, a `newproxy` identity token, or an embedder-supplied
24/// Rust value) plus an optional metatable.
25#[repr(C)]
26pub struct Userdata {
27    pub(crate) hdr: GcHeader,
28    /// per-object metatable (the io library installs the shared `FILE*` one)
29    metatable: Option<Gc<Table>>,
30    /// host-side payload
31    pub(crate) payload: UserdataPayload,
32    /// Bytes read from the OS but not yet consumed (PUC's stdio input
33    /// buffer): `read_buf[read_pos..]` is what the next read returns. It also
34    /// serves as the pushback for `ungetc`, which 5.1/5.2's `fscanf`-based
35    /// number reader needs for more than one byte.
36    pub(crate) read_buf: Vec<u8>,
37    /// Consumed prefix of `read_buf`.
38    pub(crate) read_pos: usize,
39    /// 5.2/5.3 `lua_getuservalue`/`lua_setuservalue` slot (nil until set)
40    pub(crate) user_value: Value,
41    /// User-space write buffer for `FileHandle::File` (PUC's stdio FILE*).
42    /// A `:write` only appends here; the buffer is drained to the OS by
43    /// `:flush` / `:seek` / `:close` (and before a `:read` on the same handle).
44    /// Without this, writes to `/dev/full` would fail at the `write` call
45    /// instead of at `flush`, breaking files.lua :475's expectation.
46    pub(crate) write_buf: Vec<u8>,
47    /// Whether `:write` should buffer (true for files opened in a write-
48    /// capable mode and for `stdout`/`stderr`). A read-only file's write
49    /// still goes through `write_to` so the OS surfaces the EBADF — files.lua
50    /// :302 asserts `io.input():write(...)` returns `(nil, msg, errno)`.
51    pub(crate) writable: bool,
52    /// PUC `setvbuf` mode: 0 = `"full"` (default), 1 = `"line"`, 2 = `"no"`.
53    /// `"line"` flushes after every newline written; `"no"` flushes after
54    /// every write; `"full"` only flushes on close/seek/explicit flush.
55    /// files.lua 5.1 :245 baselines on the `"line"` mode behaviour.
56    pub(crate) buf_mode: u8,
57    /// Child process for an `io.popen` handle. The pipe end (stdout for `"r"`,
58    /// stdin for `"w"`) is re-wrapped as a `std::fs::File` and lives in the
59    /// `FileHandle::File` slot so all read/write/seek/flush paths stay
60    /// untouched; this field keeps the `Child` alive so `:close` can wait on
61    /// it and return PUC's `(success, "exit"|"signal", code)` triple. Cleared
62    /// on close. Unaffected by `__gc` paths that just drop the pipe — the
63    /// process will be reaped by the kernel.
64    pub(crate) popen_child: Option<std::process::Child>,
65}
66
67/// A userdata's host-side payload. Beyond io file handles luna exposes:
68///
69/// - `Empty` — PUC 5.1 `newproxy()` carries only identity + an optional
70///   metatable hook for `__index` / `__newindex` / `__gc`.
71/// - `Host` — embedder-supplied Rust value. The host owns the value;
72///   luna treats it as opaque Any. Host types must be `'static`; GC
73///   references inside the payload are reached through the captured
74///   trace adapter.
75pub enum UserdataPayload {
76    /// an io stream/file handle
77    File(FileHandle),
78    /// a PUC 5.1 `newproxy` userdata — no host payload, only identity
79    Empty,
80    /// Embedder-supplied Rust value. `type_id` keys the downcast;
81    /// `data` is the boxed payload.
82    Host {
83        /// `TypeId` of the host value, used as the downcast key.
84        type_id: std::any::TypeId,
85        /// Boxed host payload (the embedder owns the underlying data
86        /// semantically; luna treats it as opaque `Any`).
87        data: Box<dyn std::any::Any + 'static>,
88        /// Trace adapter for the concrete `T` keyed by `type_id`.
89        /// Captured by [`crate::vm::Vm::create_userdata`] as a monomorphic
90        /// `fn(&dyn Any, &mut UserdataMarker)` whose body downcasts the
91        /// payload to `&T` and calls [`crate::vm::LuaUserdata::trace`].
92        ///
93        /// `None` means "no trace adapter wired" — only possible for
94        /// payloads constructed via the [`crate::runtime::heap::Heap::new_userdata`]
95        /// path that bypasses the trait sugar (none exist in luna today,
96        /// but the `Option` preserves source-compat for any external
97        /// crate that built a `UserdataPayload::Host` literal without a
98        /// trace adapter).
99        trace_fn: Option<HostTraceFn>,
100    },
101}
102
103/// The OS resource behind a file userdata. Standard streams cannot be closed;
104/// an opened file carries its handle and becomes `Closed` after `:close()`.
105pub enum FileHandle {
106    /// Standard input (cannot be closed).
107    Stdin,
108    /// Standard output (cannot be closed).
109    Stdout,
110    /// Standard error (cannot be closed).
111    Stderr,
112    /// An opened OS file.
113    File(
114        /// Underlying handle.
115        std::fs::File,
116    ),
117    /// A closed file (post-`:close()`); further operations error.
118    Closed,
119}
120
121impl FileHandle {
122    /// PUC io.type: an open file vs. a closed one vs. (caller handles non-file).
123    pub fn is_closed(&self) -> bool {
124        matches!(self, FileHandle::Closed)
125    }
126
127    /// Standard streams cannot be closed (io.close(io.stdin) fails in PUC).
128    pub fn is_std(&self) -> bool {
129        matches!(
130            self,
131            FileHandle::Stdin | FileHandle::Stdout | FileHandle::Stderr
132        )
133    }
134}
135
136impl Userdata {
137    pub(crate) fn new(hdr: GcHeader, payload: UserdataPayload, writable: bool) -> Userdata {
138        Userdata {
139            hdr,
140            metatable: None,
141            payload,
142            read_buf: Vec::new(),
143            read_pos: 0,
144            user_value: Value::Nil,
145            write_buf: Vec::new(),
146            writable,
147            buf_mode: 0,
148            popen_child: None,
149        }
150    }
151
152    /// This userdata's metatable, if one is attached.
153    pub fn metatable(&self) -> Option<Gc<Table>> {
154        self.metatable
155    }
156
157    /// Install (or clear) this userdata's metatable.
158    pub fn set_metatable(&mut self, mt: Option<Gc<Table>>) {
159        self.metatable = mt;
160    }
161
162    pub(crate) fn trace(&self, m: &mut Marker) {
163        if let Some(mt) = self.metatable {
164            m.header(mt.as_ptr() as *mut GcHeader);
165        }
166        m.value(self.user_value);
167        // recurse into the host payload via the
168        // captured monomorphic trace adapter. The adapter's body
169        // downcasts to the concrete `T` (paired with `type_id` at
170        // `create_userdata::<T>` time) and calls `T::trace`.
171        if let UserdataPayload::Host {
172            data,
173            trace_fn: Some(trace_fn),
174            ..
175        } = &self.payload
176        {
177            let mut um = crate::vm::UserdataMarker::__new_internal(m);
178            trace_fn(data.as_ref(), &mut um);
179        }
180    }
181
182    /// The file handle behind this userdata (all io userdata are files; the
183    /// `Empty` proxy variant is only constructed by `newproxy` and surfaces
184    /// via [`Self::is_proxy`], so callers reaching `.file()` must already
185    /// know they hold a file handle — luna's io builtins all guard with
186    /// `is_proxy()` or `Userdata` matching before unpacking).
187    pub fn file(&self) -> &FileHandle {
188        match &self.payload {
189            UserdataPayload::File(fh) => fh,
190            UserdataPayload::Empty => panic!("file() on a newproxy userdata"),
191            UserdataPayload::Host { .. } => panic!("file() on a host userdata"),
192        }
193    }
194
195    /// Mutable variant of [`Self::file`].
196    pub fn file_mut(&mut self) -> &mut FileHandle {
197        match &mut self.payload {
198            UserdataPayload::File(fh) => fh,
199            UserdataPayload::Empty => panic!("file_mut() on a newproxy userdata"),
200            UserdataPayload::Host { .. } => panic!("file_mut() on a host userdata"),
201        }
202    }
203
204    /// True for `newproxy` userdata — they have no host payload, only a
205    /// metatable and identity. io builtins reject these with the PUC
206    /// "bad argument" error rather than panicking on `file()`.
207    pub fn is_proxy(&self) -> bool {
208        matches!(self.payload, UserdataPayload::Empty)
209    }
210
211    /// True for host userdata (embedder-supplied `T: 'static`).
212    pub fn is_host(&self) -> bool {
213        matches!(self.payload, UserdataPayload::Host { .. })
214    }
215
216    /// Borrow the host payload as `&T` if this userdata holds a `T`.
217    /// Returns `None` when the userdata isn't a host payload, or holds
218    /// a different `T`. Embedders typically reach this through
219    /// [`crate::vm::Vm::userdata_borrow`] or via a `Value::Userdata`
220    /// match arm.
221    pub fn downcast<T: std::any::Any + 'static>(&self) -> Option<&T> {
222        match &self.payload {
223            UserdataPayload::Host { type_id, data, .. } => {
224                if *type_id == std::any::TypeId::of::<T>() {
225                    data.downcast_ref::<T>()
226                } else {
227                    None
228                }
229            }
230            _ => None,
231        }
232    }
233
234    /// Mutable borrow variant of [`Self::downcast`].
235    pub fn downcast_mut<T: std::any::Any + 'static>(&mut self) -> Option<&mut T> {
236        match &mut self.payload {
237            UserdataPayload::Host { type_id, data, .. } => {
238                if *type_id == std::any::TypeId::of::<T>() {
239                    data.downcast_mut::<T>()
240                } else {
241                    None
242                }
243            }
244            _ => None,
245        }
246    }
247}