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}