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