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}