luna_core/vm/userdata_trait.rs
1//! `LuaUserdata` trait sugar (v1.2 Track B).
2//!
3//! Layered on top of v1.1 B8 (`UserdataPayload::Host`, `Vm::create_userdata`,
4//! `Vm::userdata_borrow`). The B8 base lets embedders stash a `T: 'static`
5//! Rust value inside a `Value::Userdata`; this module is what makes that
6//! userdata *callable from Lua* — methods, metamethods, and a cached
7//! per-Vm metatable.
8//!
9//! ```
10//! use luna_core::vm::{LuaUserdata, MetaMethod, UserdataMethods, Vm};
11//! use luna_core::version::LuaVersion;
12//!
13//! struct Counter { value: i64 }
14//!
15//! impl LuaUserdata for Counter {
16//! fn type_name() -> &'static str { "Counter" }
17//! fn add_methods<M: UserdataMethods<Self>>(m: &mut M) {
18//! m.add_method("get", |_vm, this, ()| Ok::<_, _>(this.value));
19//! m.add_method_mut("incr", |_vm, this, (by,): (i64,)| {
20//! this.value += by;
21//! Ok::<_, _>(())
22//! });
23//! m.add_meta_method(MetaMethod::ToString, |_vm, this, ()| {
24//! Ok::<_, _>(format!("Counter({})", this.value))
25//! });
26//! }
27//! }
28//!
29//! let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().build();
30//! vm.set_userdata("c", Counter { value: 100 }).unwrap();
31//! vm.eval("c:incr(50)").unwrap();
32//! let r = vm.eval("return c:get()").unwrap();
33//! assert!(matches!(r[0], luna_core::runtime::Value::Int(150)));
34//! ```
35//!
36//! The trait + builder live in `luna-core` (alongside `typed_native.rs`)
37//! because nothing here depends on JIT-bearing types: dispatch routes
38//! through the existing metatable plumbing (`exec.rs::metatable_of` /
39//! `get_mm` / `check_finalizer_userdata`), and trampolines reuse the
40//! `pack` / `reconstruct` machinery from `typed_native.rs`.
41
42use std::any::TypeId;
43use std::marker::PhantomData;
44
45use crate::runtime::heap::{Gc, GcHeader};
46use crate::runtime::table::Table;
47use crate::runtime::value::{NativeFn, Value};
48use crate::vm::error::LuaError;
49use crate::vm::exec::Vm;
50use crate::vm::typed_native::{FromLuaArgs, IntoLuaReturn};
51
52// ─────────────────────────────────────────────────────────────────────
53// UserdataMarker — public facade over the GC marker passed to
54// `LuaUserdata::trace`. Phase TB (v1.3).
55// ─────────────────────────────────────────────────────────────────────
56
57/// Public facade over the GC mark accumulator passed to
58/// [`LuaUserdata::trace`].
59///
60/// Wraps the crate-internal `Marker` (private GC primitive in
61/// `runtime::heap`) so embedders never see the gray-stack / weak-table
62/// internals. Holds a mutable
63/// borrow of the underlying marker for the duration of a single trace
64/// call. Constructed only by the collector via the crate-internal
65/// `__new_internal` constructor; embedders cannot synthesize one outside
66/// a trace call.
67///
68/// ## Trace-method contract
69///
70/// Inside [`LuaUserdata::trace`] the embedder may **only**:
71/// - call [`UserdataMarker::mark`] / [`UserdataMarker::mark_value`] on
72/// `Gc<...>` handles / `Value`s reachable from `&self`
73/// - read fields of `&self`
74///
75/// The embedder must **not** allocate new GC objects, reenter the `Vm`,
76/// take locks, or perform I/O. The trace call runs synchronously inside
77/// the collector's mark phase and must return in bounded wall time.
78pub struct UserdataMarker<'a> {
79 inner: &'a mut crate::runtime::heap::Marker,
80}
81
82impl<'a> UserdataMarker<'a> {
83 /// Crate-internal constructor. Not part of the public API — only
84 /// the collector (`Userdata::trace`) builds one.
85 #[doc(hidden)]
86 pub(crate) fn __new_internal(inner: &'a mut crate::runtime::heap::Marker) -> Self {
87 UserdataMarker { inner }
88 }
89
90 /// Mark a Gc-managed object as reachable. Returns `true` on the
91 /// first visit (white → gray transition). Idempotent on later
92 /// visits within the same cycle.
93 pub fn mark<T>(&mut self, g: Gc<T>) -> bool {
94 self.inner.header(g.as_ptr() as *mut GcHeader)
95 }
96
97 /// Convenience: mark every Gc-managed object referenced by a
98 /// [`Value`]. No-op for primitive variants (`Int`, `Float`,
99 /// `Bool`, `Nil`, `LightUserdata`).
100 pub fn mark_value(&mut self, v: Value) -> bool {
101 self.inner.value(v)
102 }
103}
104
105// ─────────────────────────────────────────────────────────────────────
106// MetaMethod — public-facing metamethod tag
107// ─────────────────────────────────────────────────────────────────────
108
109/// Public metamethod kinds for [`UserdataMethods::add_meta_method`].
110///
111/// Maps 1:1 onto the dispatcher's internal `Mm` enum. Listed
112/// explicitly so the public surface doesn't leak `Mm`'s discriminant
113/// layout — `Mm` stays `pub(crate)` in `exec.rs`.
114///
115/// Not all `Mm` variants are exposed: `Mm::Metatable` (the `__metatable`
116/// guard) and `Mm::Name` are set indirectly via [`LuaUserdata::type_name`]
117/// and `getmetatable`; surfacing them as `add_meta_method` targets
118/// would be confusing.
119#[non_exhaustive]
120#[derive(Copy, Clone, Debug, PartialEq, Eq)]
121pub enum MetaMethod {
122 /// `__add` — binary `+`.
123 Add,
124 /// `__sub` — binary `-`.
125 Sub,
126 /// `__mul` — binary `*`.
127 Mul,
128 /// `__div` — binary `/`.
129 Div,
130 /// `__mod` — binary `%`.
131 Mod,
132 /// `__pow` — binary `^`.
133 Pow,
134 /// `__idiv` — binary `//`.
135 IDiv,
136 /// `__band` — binary `&`.
137 BAnd,
138 /// `__bor` — binary `|`.
139 BOr,
140 /// `__bxor` — binary `~` (bitwise xor).
141 BXor,
142 /// `__shl` — `<<`.
143 Shl,
144 /// `__shr` — `>>`.
145 Shr,
146 /// `__bnot` — unary `~`.
147 BNot,
148 /// `__unm` — unary `-`.
149 Unm,
150 /// `__concat` — binary `..`.
151 Concat,
152 /// `__len` — unary `#`.
153 Len,
154 /// `__eq` — `==`.
155 Eq,
156 /// `__lt` — `<`.
157 Lt,
158 /// `__le` — `<=`.
159 Le,
160 /// `__index` — non-existent key lookup. Setting this directly
161 /// overrides the per-method dispatch table installed by
162 /// [`UserdataMethods::add_method`] etc., so only use it when you
163 /// want full control of the lookup; the trait's default `__index`
164 /// is a table of `add_method` entries.
165 Index,
166 /// `__newindex` — non-existent key assignment.
167 NewIndex,
168 /// `__call` — `obj(args)`.
169 Call,
170 /// `__tostring` — `tostring(obj)`.
171 ToString,
172 /// `__pairs` — `pairs(obj)` (5.2+).
173 Pairs,
174 /// `__close` — to-be-closed handler (5.4+).
175 Close,
176 /// `__gc` — finalizer. **The metatable's `__gc` fires before
177 /// Rust's `Drop` on the host payload.**
178 Gc,
179}
180
181impl MetaMethod {
182 /// Lua-side string spelling of this metamethod (`"__add"`, `"__gc"`, …).
183 pub const fn name(self) -> &'static str {
184 match self {
185 MetaMethod::Add => "__add",
186 MetaMethod::Sub => "__sub",
187 MetaMethod::Mul => "__mul",
188 MetaMethod::Div => "__div",
189 MetaMethod::Mod => "__mod",
190 MetaMethod::Pow => "__pow",
191 MetaMethod::IDiv => "__idiv",
192 MetaMethod::BAnd => "__band",
193 MetaMethod::BOr => "__bor",
194 MetaMethod::BXor => "__bxor",
195 MetaMethod::Shl => "__shl",
196 MetaMethod::Shr => "__shr",
197 MetaMethod::BNot => "__bnot",
198 MetaMethod::Unm => "__unm",
199 MetaMethod::Concat => "__concat",
200 MetaMethod::Len => "__len",
201 MetaMethod::Eq => "__eq",
202 MetaMethod::Lt => "__lt",
203 MetaMethod::Le => "__le",
204 MetaMethod::Index => "__index",
205 MetaMethod::NewIndex => "__newindex",
206 MetaMethod::Call => "__call",
207 MetaMethod::ToString => "__tostring",
208 MetaMethod::Pairs => "__pairs",
209 MetaMethod::Close => "__close",
210 MetaMethod::Gc => "__gc",
211 }
212 }
213}
214
215// ─────────────────────────────────────────────────────────────────────
216// LuaUserdata + UserdataMethods traits
217// ─────────────────────────────────────────────────────────────────────
218
219/// Embedder-side trait: implement on any `T: 'static` to expose
220/// method-rich Lua userdata via `vm.set_userdata::<T>(...)`.
221///
222/// The trait's only required method is [`add_methods`], which defaults
223/// to registering nothing — yielding a userdata that still type-checks
224/// as `"userdata"` but only carries identity + `__name`. An empty impl
225/// (`impl LuaUserdata for MyType {}`) is the source-compatible bridge
226/// for B8 callers upgrading from v1.1.
227///
228/// [`add_methods`]: LuaUserdata::add_methods
229///
230/// ## v1.1 → v1.2 migration
231///
232/// v1.1 [`Vm::create_userdata`] / [`Vm::set_userdata`] accepted any
233/// `T: Any + 'static`; v1.2 narrows the bound to `T: LuaUserdata`. Any
234/// existing type carries over with a one-line empty impl:
235///
236/// ```
237/// # use luna_core::vm::LuaUserdata;
238/// struct MyType { /* … */ }
239/// impl LuaUserdata for MyType {}
240/// ```
241///
242/// ## Contract on the host payload
243///
244/// `T` may hold `Gc<...>` fields **provided it overrides [`trace`]** to
245/// mark every such handle. The default [`trace`] is a no-op, suitable
246/// for pure host types (no Gc-managed inner state). Forgetting to
247/// override [`trace`] when `T` carries a `Gc<Table>` / `Gc<LuaStr>` /
248/// `Gc<NativeClosure>` / `Gc<Coro>` / `Gc<Userdata>` field whose
249/// lifetime is not otherwise rooted risks dangling references after
250/// collection.
251///
252/// v1.2 forbade Gc-bearing payloads entirely; v1.3 Phase TB lifts the
253/// limitation by giving the trait a default [`trace`] method and
254/// storing a monomorphic adapter in [`crate::runtime::userdata::UserdataPayload::Host`].
255///
256/// [`trace`]: LuaUserdata::trace
257pub trait LuaUserdata: 'static + Sized {
258 /// Lua-visible type name. Used as the `__name` field of the
259 /// generated metatable; surfaces in tostring fallback messages and
260 /// in PUC-style `"attempt to index a Counter value"` errors.
261 /// Defaults to [`std::any::type_name`].
262 fn type_name() -> &'static str {
263 std::any::type_name::<Self>()
264 }
265
266 /// Register methods + metamethods on `m`. Called exactly once per
267 /// `T` per `Vm`, at the first
268 /// [`Vm::create_userdata::<T>`](Vm::create_userdata) /
269 /// [`set_userdata::<T>`](Vm::set_userdata) — the resulting
270 /// metatable is cached on the Vm keyed by `TypeId::of::<T>()`.
271 fn add_methods<M: UserdataMethods<Self>>(_m: &mut M) {}
272
273 /// Mark every Gc-managed handle reachable from `self`. The default
274 /// is a no-op — override only when `T` directly holds
275 /// `Gc<Table>` / `Gc<LuaStr>` / `Gc<NativeClosure>` / `Gc<Coro>` /
276 /// `Gc<Userdata>` fields whose lifetime is not otherwise rooted
277 /// (i.e. not pinned via [`Vm::pin_host`] and not reachable from a
278 /// Lua-side table).
279 ///
280 /// Called by the collector during the mark phase; the call runs
281 /// synchronously, single-threaded, and must return in bounded wall
282 /// time. The embedder must not allocate new GC objects, reenter the
283 /// `Vm`, take locks, or perform I/O from inside `trace` — see the
284 /// [`UserdataMarker`] type docs for the full contract.
285 ///
286 /// ## Override example
287 ///
288 /// ```ignore
289 /// use luna_core::runtime::{Gc, Table};
290 /// use luna_core::vm::{LuaUserdata, UserdataMarker};
291 ///
292 /// struct Cache { entries: Gc<Table> }
293 /// impl LuaUserdata for Cache {
294 /// fn trace(&self, m: &mut UserdataMarker) {
295 /// m.mark(self.entries);
296 /// }
297 /// }
298 /// ```
299 ///
300 /// Overriding `trace` does not require touching any other trait
301 /// method; existing B8 / v1.2 types remain source-compatible with
302 /// an unchanged empty `impl LuaUserdata for T {}` (the default
303 /// no-op runs and no Gc tracing is performed).
304 fn trace(&self, _m: &mut UserdataMarker) {}
305}
306
307/// Builder passed to [`LuaUserdata::add_methods`]. The concrete impl
308/// is [`MetatableBuilder<T>`] (in this module) — `UserdataMethods` is
309/// a trait only to keep the `M:` bound usable from generic code.
310pub trait UserdataMethods<T> {
311 /// Register a regular method bound to `__index[name]` on the
312 /// generated metatable; method lookup `u:name(args)` resolves
313 /// through Lua's normal `__index` table dispatch.
314 fn add_method<F, A, R>(&mut self, name: &str, f: F)
315 where
316 F: Fn(&mut Vm, &T, A) -> Result<R, LuaError> + Copy + 'static,
317 A: FromLuaArgs + 'static,
318 R: IntoLuaReturn + 'static;
319
320 /// Mutable variant of [`add_method`](Self::add_method). The
321 /// `&mut T` borrow is exclusive within the call window; an
322 /// embedder must not concurrently `userdata_borrow_mut` the same
323 /// payload through another path during the method body.
324 fn add_method_mut<F, A, R>(&mut self, name: &str, f: F)
325 where
326 F: Fn(&mut Vm, &mut T, A) -> Result<R, LuaError> + Copy + 'static,
327 A: FromLuaArgs + 'static,
328 R: IntoLuaReturn + 'static;
329
330 /// Register a static-style function (no implicit receiver). Bound
331 /// directly on the metatable, not under `__index`, so it is
332 /// reachable as `Vec3.new(...)` after `vm.set_global("Vec3", mt)`.
333 fn add_function<F, A, R>(&mut self, name: &str, f: F)
334 where
335 F: Fn(&mut Vm, A) -> Result<R, LuaError> + Copy + 'static,
336 A: FromLuaArgs + 'static,
337 R: IntoLuaReturn + 'static;
338
339 /// Register a metamethod (`__add` / `__tostring` / …). Stored
340 /// directly on the metatable; the dispatcher's existing
341 /// `get_mm` path resolves it.
342 fn add_meta_method<F, A, R>(&mut self, meta: MetaMethod, f: F)
343 where
344 F: Fn(&mut Vm, &T, A) -> Result<R, LuaError> + Copy + 'static,
345 A: FromLuaArgs + 'static,
346 R: IntoLuaReturn + 'static;
347
348 /// Mutable variant of [`add_meta_method`](Self::add_meta_method).
349 fn add_meta_method_mut<F, A, R>(&mut self, meta: MetaMethod, f: F)
350 where
351 F: Fn(&mut Vm, &mut T, A) -> Result<R, LuaError> + Copy + 'static,
352 A: FromLuaArgs + 'static,
353 R: IntoLuaReturn + 'static;
354
355 /// Field-getter sugar: equivalent to [`add_method`](Self::add_method)
356 /// with no args and a single-value return.
357 ///
358 /// **v1.3 (UD1+UD2)**: true field-style `obj.name` (no parens) is
359 /// supported alongside the legacy call-syntax `obj:name()` shape.
360 /// When any `add_field_method_get` is registered, `MetatableBuilder`
361 /// emits a native trampoline for `__index` that dispatches in the
362 /// order *methods → field getters → nil*. Methods win on name
363 /// collision (matches mlua and keeps v1.2 callers source-compatible).
364 fn add_field_method_get<F, R>(&mut self, name: &str, f: F)
365 where
366 F: Fn(&mut Vm, &T) -> Result<R, LuaError> + Copy + 'static,
367 R: IntoLuaReturn + 'static;
368
369 /// Field-setter sugar: registers a setter for `obj.name = value`
370 /// (v1.3 UD1). When any `add_field_method_set` is registered,
371 /// `MetatableBuilder` installs a `__newindex` trampoline that
372 /// dispatches `(self, value)` to the registered setter. Unknown
373 /// fields raise a runtime error rather than silently dropping the
374 /// write (matches `code/no-unsolicited-fallback`).
375 fn add_field_method_set<F, A>(&mut self, name: &str, f: F)
376 where
377 F: Fn(&mut Vm, &mut T, A) -> Result<(), LuaError> + Copy + 'static,
378 A: FromLuaArgs + 'static;
379}
380
381// ─────────────────────────────────────────────────────────────────────
382// MetatableBuilder<T> — the concrete UserdataMethods<T> impl
383// ─────────────────────────────────────────────────────────────────────
384
385/// Concrete builder that emits a [`Gc<Table>`] metatable for `T`.
386/// Created internally by [`Vm::register_userdata`]; embedders never
387/// name this type.
388pub struct MetatableBuilder<'vm, T> {
389 vm: &'vm mut Vm,
390 /// `__index` sub-table entries (regular methods).
391 methods: Vec<(Gc<crate::runtime::LuaStr>, Value)>,
392 /// Field getters for true field-style `obj.name` (v1.3 UD2).
393 fields_get: Vec<(Gc<crate::runtime::LuaStr>, Value)>,
394 /// Field setters for `obj.name = value` (v1.3 UD1).
395 fields_set: Vec<(Gc<crate::runtime::LuaStr>, Value)>,
396 /// Direct metatable entries (metamethods + static functions).
397 meta_entries: Vec<(Gc<crate::runtime::LuaStr>, Value)>,
398 _phantom: PhantomData<fn() -> T>,
399}
400
401impl<'vm, T: LuaUserdata> MetatableBuilder<'vm, T> {
402 fn new(vm: &'vm mut Vm) -> Self {
403 Self {
404 vm,
405 methods: Vec::new(),
406 fields_get: Vec::new(),
407 fields_set: Vec::new(),
408 meta_entries: Vec::new(),
409 _phantom: PhantomData,
410 }
411 }
412
413 fn intern(&mut self, s: &str) -> Gc<crate::runtime::LuaStr> {
414 self.vm.heap.intern(s.as_bytes())
415 }
416
417 fn make_native(&mut self, f: NativeFn, upvals: Box<[Value]>) -> Value {
418 self.vm.native_with(f, upvals)
419 }
420
421 /// Build the metatable from the accumulated entries. Called by
422 /// [`Vm::register_userdata`] after [`LuaUserdata::add_methods`] returns.
423 ///
424 /// Three-way fork on `__index`:
425 /// 1. **No methods, no field getters** → no `__index` slot.
426 /// 2. **Methods only, no field getters** → v1.2 fast path:
427 /// `__index` is a plain `Value::Table` of methods.
428 /// 3. **Any field getters registered** → `__index` is a native
429 /// trampoline ([`index_trampoline`]) with upvals
430 /// `(methods_table_or_nil, fields_get_table)` dispatching
431 /// *methods → field-getters → nil*.
432 ///
433 /// `__newindex` is installed only when any field setter is
434 /// registered (Phase UD1).
435 fn finalize(self) -> Result<Gc<Table>, LuaError> {
436 let MetatableBuilder {
437 vm,
438 methods,
439 fields_get,
440 fields_set,
441 meta_entries,
442 ..
443 } = self;
444
445 let mt = vm.heap.new_table();
446 // __name — drives PUC-style error messages.
447 let name_key = vm.heap.intern(b"__name");
448 let type_name_str = vm.heap.intern(T::type_name().as_bytes());
449 let name_val = Value::Str(type_name_str);
450 // SAFETY: mt is a fresh Gc<Table>; the heap is single-threaded.
451 unsafe { mt.as_mut() }.set(&mut vm.heap, Value::Str(name_key), name_val)?;
452
453 // Helper: build a Gc<Table> from a (key, value) bucket (or None
454 // for the empty case so the caller can skip the allocation).
455 let mk_bucket = |vm: &mut Vm,
456 entries: Vec<(Gc<crate::runtime::LuaStr>, Value)>|
457 -> Result<Option<Gc<Table>>, LuaError> {
458 if entries.is_empty() {
459 return Ok(None);
460 }
461 let t = vm.heap.new_table();
462 for (k, v) in entries {
463 // SAFETY: t is freshly allocated.
464 unsafe { t.as_mut() }.set(&mut vm.heap, Value::Str(k), v)?;
465 }
466 Ok(Some(t))
467 };
468
469 // __index — fork on whether any field getters are registered.
470 if fields_get.is_empty() {
471 // Methods-only fast path (v1.2 shape preserved).
472 if let Some(idx) = mk_bucket(vm, methods)? {
473 let key = vm.heap.intern(b"__index");
474 // SAFETY: mt is freshly allocated.
475 unsafe { mt.as_mut() }.set(&mut vm.heap, Value::Str(key), Value::Table(idx))?;
476 }
477 } else {
478 // Trampoline path — methods table + fields_get table as upvals.
479 let methods_val = match mk_bucket(vm, methods)? {
480 Some(t) => Value::Table(t),
481 None => Value::Nil,
482 };
483 let fields_val =
484 Value::Table(mk_bucket(vm, fields_get)?.expect("fields_get non-empty checked"));
485 let upvals: Box<[Value]> = Box::new([methods_val, fields_val]);
486 let trampoline = vm.native_with(index_trampoline, upvals);
487 let key = vm.heap.intern(b"__index");
488 // SAFETY: mt is freshly allocated.
489 unsafe { mt.as_mut() }.set(&mut vm.heap, Value::Str(key), trampoline)?;
490 }
491
492 // __newindex — installed only when any field setter is registered.
493 if !fields_set.is_empty() {
494 let setters_tbl = mk_bucket(vm, fields_set)?.expect("fields_set non-empty checked");
495 let upvals: Box<[Value]> =
496 Box::new([Value::Table(setters_tbl), Value::Str(type_name_str)]);
497 let trampoline = vm.native_with(newindex_trampoline, upvals);
498 let key = vm.heap.intern(b"__newindex");
499 // SAFETY: mt is freshly allocated.
500 unsafe { mt.as_mut() }.set(&mut vm.heap, Value::Str(key), trampoline)?;
501 }
502
503 // Direct metatable entries (metamethods + static fns).
504 for (k, v) in meta_entries {
505 unsafe { mt.as_mut() }.set(&mut vm.heap, Value::Str(k), v)?;
506 }
507 vm.heap
508 .barrier_back(mt.as_ptr() as *mut crate::runtime::heap::GcHeader);
509
510 Ok(mt)
511 }
512}
513
514impl<'vm, T: LuaUserdata> UserdataMethods<T> for MetatableBuilder<'vm, T> {
515 fn add_method<F, A, R>(&mut self, name: &str, f: F)
516 where
517 F: Fn(&mut Vm, &T, A) -> Result<R, LuaError> + Copy + 'static,
518 A: FromLuaArgs + 'static,
519 R: IntoLuaReturn + 'static,
520 {
521 let (raw_fn, upvals) = pack_method::<T, F, A, R>(f);
522 let v = self.make_native(raw_fn, upvals);
523 let k = self.intern(name);
524 self.methods.push((k, v));
525 }
526
527 fn add_method_mut<F, A, R>(&mut self, name: &str, f: F)
528 where
529 F: Fn(&mut Vm, &mut T, A) -> Result<R, LuaError> + Copy + 'static,
530 A: FromLuaArgs + 'static,
531 R: IntoLuaReturn + 'static,
532 {
533 let (raw_fn, upvals) = pack_method_mut::<T, F, A, R>(f);
534 let v = self.make_native(raw_fn, upvals);
535 let k = self.intern(name);
536 self.methods.push((k, v));
537 }
538
539 fn add_function<F, A, R>(&mut self, name: &str, f: F)
540 where
541 F: Fn(&mut Vm, A) -> Result<R, LuaError> + Copy + 'static,
542 A: FromLuaArgs + 'static,
543 R: IntoLuaReturn + 'static,
544 {
545 let (raw_fn, upvals) = pack_function::<F, A, R>(f);
546 let v = self.make_native(raw_fn, upvals);
547 let k = self.intern(name);
548 self.meta_entries.push((k, v));
549 }
550
551 fn add_meta_method<F, A, R>(&mut self, meta: MetaMethod, f: F)
552 where
553 F: Fn(&mut Vm, &T, A) -> Result<R, LuaError> + Copy + 'static,
554 A: FromLuaArgs + 'static,
555 R: IntoLuaReturn + 'static,
556 {
557 let (raw_fn, upvals) = pack_method::<T, F, A, R>(f);
558 let v = self.make_native(raw_fn, upvals);
559 let k = self.intern(meta.name());
560 self.meta_entries.push((k, v));
561 }
562
563 fn add_meta_method_mut<F, A, R>(&mut self, meta: MetaMethod, f: F)
564 where
565 F: Fn(&mut Vm, &mut T, A) -> Result<R, LuaError> + Copy + 'static,
566 A: FromLuaArgs + 'static,
567 R: IntoLuaReturn + 'static,
568 {
569 let (raw_fn, upvals) = pack_method_mut::<T, F, A, R>(f);
570 let v = self.make_native(raw_fn, upvals);
571 let k = self.intern(meta.name());
572 self.meta_entries.push((k, v));
573 }
574
575 fn add_field_method_get<F, R>(&mut self, name: &str, f: F)
576 where
577 F: Fn(&mut Vm, &T) -> Result<R, LuaError> + Copy + 'static,
578 R: IntoLuaReturn + 'static,
579 {
580 // Adapt to add_method's (this, args) shape with A = ().
581 let adapter = move |vm: &mut Vm, this: &T, _args: ()| f(vm, this);
582 // v1.3 UD2: getter lives ONLY in the fields_get bucket. The
583 // `__index` trampoline calls it with `(self,)` so `obj.name`
584 // returns the field value directly.
585 //
586 // **Breaking change from v1.2**: the v1.2 call-syntax shape
587 // (`obj:name()`) no longer works for getters defined this way
588 // — the trampoline calls the getter and returns its value, so
589 // `obj.name` is `Value::Int(...)` not the closure, and
590 // `obj:name()` evaluates to `Int(...)(obj)` which errors.
591 // Embedders who need both shapes should register an explicit
592 // `add_method("name", ...)` (returns the closure unchanged
593 // through the table-`__index` fallback) alongside the
594 // `add_field_method_get` if a same-named field-getter is also
595 // wanted. The audit's "dual registration" idea was
596 // load-bearing-broken — see CHANGELOG [1.3.0] for migration.
597 let (raw_fn, upvals) = pack_method::<T, _, (), R>(adapter);
598 let v = self.make_native(raw_fn, upvals);
599 let k = self.intern(name);
600 self.fields_get.push((k, v));
601 }
602
603 fn add_field_method_set<F, A>(&mut self, name: &str, f: F)
604 where
605 F: Fn(&mut Vm, &mut T, A) -> Result<(), LuaError> + Copy + 'static,
606 A: FromLuaArgs + 'static,
607 {
608 // Same trampoline shape as add_method_mut — `()` is a valid
609 // `IntoLuaReturn`. Native is bucketed into `fields_set`, which
610 // `newindex_trampoline` forwards to.
611 let (raw_fn, upvals) = pack_method_mut::<T, F, A, ()>(f);
612 let v = self.make_native(raw_fn, upvals);
613 let k = self.intern(name);
614 self.fields_set.push((k, v));
615 }
616}
617
618// ─────────────────────────────────────────────────────────────────────
619// Trampolines + pack helpers
620// ─────────────────────────────────────────────────────────────────────
621
622/// Trampoline for `add_method` (`&T` self).
623fn method_trampoline<T, F, A, R>(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError>
624where
625 T: LuaUserdata,
626 F: Fn(&mut Vm, &T, A) -> Result<R, LuaError> + Copy + 'static,
627 A: FromLuaArgs + 'static,
628 R: IntoLuaReturn + 'static,
629{
630 let f: F = reconstruct_zst_or_fnptr(vm, fs);
631 let self_val = vm.nat_arg(fs, nargs, 0);
632 let ud_gc = match self_val {
633 Value::Userdata(g) => g,
634 _ => {
635 return Err(vm.rt_err(&format!(
636 "method called on non-userdata value (expected {})",
637 T::type_name()
638 )));
639 }
640 };
641 // Take a raw pointer up front so the borrow isn't tied to vm.
642 let ud_ptr = ud_gc.as_ptr();
643 // SAFETY: single-threaded GC heap; the Userdata at `ud_ptr` is
644 // pinned by being on the Lua stack at slot `fs`.
645 let type_matches = unsafe { (*ud_ptr).downcast::<T>().is_some() };
646 if !type_matches {
647 return Err(vm.rt_err(&format!(
648 "method called on wrong userdata type (expected {})",
649 T::type_name()
650 )));
651 }
652 let args = A::from_lua_args_skip_self(vm, fs, nargs)?;
653 // SAFETY: type_matches is true; the &T borrow is independent of `vm`.
654 let this: &T = unsafe { (*ud_ptr).downcast::<T>().unwrap_unchecked() };
655 f(vm, this, args).into_lua_return(vm, fs)
656}
657
658/// Trampoline for `add_method_mut` (`&mut T` self).
659fn method_mut_trampoline<T, F, A, R>(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError>
660where
661 T: LuaUserdata,
662 F: Fn(&mut Vm, &mut T, A) -> Result<R, LuaError> + Copy + 'static,
663 A: FromLuaArgs + 'static,
664 R: IntoLuaReturn + 'static,
665{
666 let f: F = reconstruct_zst_or_fnptr(vm, fs);
667 let self_val = vm.nat_arg(fs, nargs, 0);
668 let ud_gc = match self_val {
669 Value::Userdata(g) => g,
670 _ => {
671 return Err(vm.rt_err(&format!(
672 "method called on non-userdata value (expected {})",
673 T::type_name()
674 )));
675 }
676 };
677 let ud_ptr = ud_gc.as_ptr();
678 // SAFETY: see method_trampoline.
679 let type_matches = unsafe { (*ud_ptr).downcast::<T>().is_some() };
680 if !type_matches {
681 return Err(vm.rt_err(&format!(
682 "method called on wrong userdata type (expected {})",
683 T::type_name()
684 )));
685 }
686 let args = A::from_lua_args_skip_self(vm, fs, nargs)?;
687 // SAFETY: see method_trampoline. The &mut T is exclusive within
688 // this trampoline; embedders must not concurrently borrow the
689 // same userdata payload through another API during the call.
690 let this: &mut T = unsafe { (*ud_ptr).downcast_mut::<T>().unwrap_unchecked() };
691 f(vm, this, args).into_lua_return(vm, fs)
692}
693
694/// Trampoline for `add_function` (no self).
695fn function_trampoline<F, A, R>(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError>
696where
697 F: Fn(&mut Vm, A) -> Result<R, LuaError> + Copy + 'static,
698 A: FromLuaArgs + 'static,
699 R: IntoLuaReturn + 'static,
700{
701 let f: F = reconstruct_zst_or_fnptr(vm, fs);
702 let args = A::from_lua_args(vm, fs, nargs)?;
703 f(vm, args).into_lua_return(vm, fs)
704}
705
706/// `__index` trampoline (v1.3 UD2). Installed by
707/// [`MetatableBuilder::finalize`] whenever any field getter is
708/// registered. Upvals:
709///
710/// - `upvals[0]` — `Value::Table` (methods bucket) or `Value::Nil`
711/// (field-only embedder).
712/// - `upvals[1]` — `Value::Table` (field-getter dispatch table).
713///
714/// Args (PUC `__index` calling convention): `(self_userdata, key)`.
715///
716/// Dispatch order: methods → field getters → nil. Methods win on
717/// collision; v1.2 callers using `add_method("foo")` keep the existing
718/// shape even if a same-named getter is registered later.
719fn index_trampoline(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
720 let methods_upval = vm.nat_upval(fs, 0);
721 let fields_upval = vm.nat_upval(fs, 1);
722 let self_val = vm.nat_arg(fs, nargs, 0);
723 let key = vm.nat_arg(fs, nargs, 1);
724
725 // 1. methods first (preserves v1.2 precedence).
726 if let Value::Table(m) = methods_upval {
727 let v = m.get(key);
728 if !v.is_nil() {
729 return Ok(vm.nat_return(fs, &[v]));
730 }
731 }
732 // 2. field getters — call getter(self,) and surface its result.
733 if let Value::Table(g) = fields_upval {
734 let getter = g.get(key);
735 if !getter.is_nil() {
736 let mut results = vm.call_value(getter, &[self_val])?;
737 let r = if results.is_empty() {
738 Value::Nil
739 } else {
740 results.swap_remove(0)
741 };
742 return Ok(vm.nat_return(fs, &[r]));
743 }
744 }
745 // 3. nothing matched — return nil (matches PUC `__index` semantics).
746 Ok(vm.nat_return(fs, &[Value::Nil]))
747}
748
749/// `__newindex` trampoline (v1.3 UD1). Installed by
750/// [`MetatableBuilder::finalize`] whenever any field setter is
751/// registered. Upvals:
752///
753/// - `upvals[0]` — `Value::Table` (field-setter dispatch table).
754/// - `upvals[1]` — `Value::Str` (host type name, for error messages).
755///
756/// Args (PUC `__newindex` calling convention): `(self_userdata, key,
757/// value)`. Unknown fields raise a runtime error rather than silently
758/// dropping the write (matches `code/no-unsolicited-fallback`).
759fn newindex_trampoline(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, LuaError> {
760 let setters_upval = vm.nat_upval(fs, 0);
761 let type_name_upval = vm.nat_upval(fs, 1);
762 let self_val = vm.nat_arg(fs, nargs, 0);
763 let key = vm.nat_arg(fs, nargs, 1);
764 let value = vm.nat_arg(fs, nargs, 2);
765
766 if let Value::Table(s) = setters_upval {
767 let setter = s.get(key);
768 if !setter.is_nil() {
769 // setter(self, value) → Result<(), LuaError>; discard return.
770 vm.call_value(setter, &[self_val, value])?;
771 return Ok(vm.nat_return(fs, &[]));
772 }
773 }
774 // Unknown field — pretty-print key + host type name.
775 let key_str = match key {
776 Value::Str(s) => std::str::from_utf8(s.as_bytes())
777 .unwrap_or("<non-utf8>")
778 .to_string(),
779 other => format!("{:?}", other),
780 };
781 let type_str = match type_name_upval {
782 Value::Str(s) => std::str::from_utf8(s.as_bytes())
783 .unwrap_or("<non-utf8>")
784 .to_string(),
785 _ => "userdata".to_string(),
786 };
787 Err(vm.rt_err(&format!(
788 "attempt to write unknown field '{}' on {} (no setter registered)",
789 key_str, type_str
790 )))
791}
792
793fn pack_method<T, F, A, R>(f: F) -> (NativeFn, Box<[Value]>)
794where
795 T: LuaUserdata,
796 F: Fn(&mut Vm, &T, A) -> Result<R, LuaError> + Copy + 'static,
797 A: FromLuaArgs + 'static,
798 R: IntoLuaReturn + 'static,
799{
800 (method_trampoline::<T, F, A, R>, pack_zst_or_fnptr::<F>(f))
801}
802
803fn pack_method_mut<T, F, A, R>(f: F) -> (NativeFn, Box<[Value]>)
804where
805 T: LuaUserdata,
806 F: Fn(&mut Vm, &mut T, A) -> Result<R, LuaError> + Copy + 'static,
807 A: FromLuaArgs + 'static,
808 R: IntoLuaReturn + 'static,
809{
810 (
811 method_mut_trampoline::<T, F, A, R>,
812 pack_zst_or_fnptr::<F>(f),
813 )
814}
815
816fn pack_function<F, A, R>(f: F) -> (NativeFn, Box<[Value]>)
817where
818 F: Fn(&mut Vm, A) -> Result<R, LuaError> + Copy + 'static,
819 A: FromLuaArgs + 'static,
820 R: IntoLuaReturn + 'static,
821{
822 (function_trampoline::<F, A, R>, pack_zst_or_fnptr::<F>(f))
823}
824
825/// Mirror of [`crate::vm::typed_native`]'s private `pack` — kept
826/// internal to this module to avoid widening that module's API.
827#[inline]
828fn pack_zst_or_fnptr<F: Copy + 'static>(f: F) -> Box<[Value]> {
829 if std::mem::size_of::<F>() == 0 {
830 Box::new([])
831 } else {
832 assert!(
833 std::mem::size_of::<F>() == std::mem::size_of::<*const ()>(),
834 "LuaUserdata method closure must be ZST (non-capturing) or fn-pointer-sized; \
835 capturing closures unsupported in v1.2"
836 );
837 // SAFETY: F is fn-pointer-sized; transmute_copy stashes its
838 // bytes as a raw *const () for storage. Recovered in
839 // `reconstruct_zst_or_fnptr` below.
840 let raw_ptr: *const () = unsafe { std::mem::transmute_copy(&f) };
841 Box::new([Value::LightUserdata(raw_ptr)])
842 }
843}
844
845#[inline]
846fn reconstruct_zst_or_fnptr<F: Copy + 'static>(vm: &Vm, fs: u32) -> F {
847 if std::mem::size_of::<F>() == 0 {
848 // SAFETY: F is ZST.
849 #[allow(clippy::uninit_assumed_init)]
850 unsafe {
851 std::mem::MaybeUninit::<F>::uninit().assume_init()
852 }
853 } else {
854 let upval = vm.nat_upval(fs, 0);
855 match upval {
856 Value::LightUserdata(ptr) => {
857 debug_assert_eq!(
858 std::mem::size_of::<F>(),
859 std::mem::size_of::<*const ()>(),
860 "non-ZST F must be fn-pointer-sized"
861 );
862 // SAFETY: stored via `pack_zst_or_fnptr` with the same F.
863 unsafe { std::mem::transmute_copy::<*const (), F>(&ptr) }
864 }
865 _ => unreachable!("LuaUserdata method upval shape corrupted"),
866 }
867 }
868}
869
870// ─────────────────────────────────────────────────────────────────────
871// Vm::register_userdata
872// ─────────────────────────────────────────────────────────────────────
873
874impl Vm {
875 /// Build (or fetch from cache) the metatable for `T`. Called
876 /// lazily by [`Vm::create_userdata`] / [`Vm::set_userdata`];
877 /// embedders rarely need to invoke it directly. Returns the same
878 /// [`Gc<Table>`] on every call within a given `Vm` (keyed by
879 /// `TypeId::of::<T>()`).
880 ///
881 /// The metatable is pinned as a host root so it survives GC even
882 /// when no userdata of type `T` is currently reachable.
883 pub fn register_userdata<T: LuaUserdata>(&mut self) -> Result<Gc<Table>, LuaError> {
884 let tid = TypeId::of::<T>();
885 if let Some(&mt) = self.userdata_metatables.get(&tid) {
886 return Ok(mt);
887 }
888 let mut builder = MetatableBuilder::<T>::new(self);
889 T::add_methods(&mut builder);
890 let mt = builder.finalize()?;
891 self.userdata_metatables.insert(tid, mt);
892 // Pin as a host root so the cached metatable survives GC even
893 // when no userdata of type T is reachable.
894 self.pin_host(Value::Table(mt));
895 Ok(mt)
896 }
897}