Skip to main content

generic_lang_api/
host.rs

1//! Safe wrapper around the host vtable for Rust plugin authors.
2
3use crate::abi::{FfiReturn, FfiStatus, FfiStr, GenericValue, HostApi};
4use crate::{PluginError, ValueKind};
5
6/// Generates the typed error constructors on [`Host`]: one per builtin
7/// exception class, with the class name spelled exactly once - a plugin
8/// author cannot typo a builtin class name by going through these.
9macro_rules! host_error_constructors {
10    ($($(#[$doc:meta])* $name:ident => $class_name:literal),* $(,)?) => {
11        $(
12            $(#[$doc])*
13            #[must_use]
14            pub fn $name(&self, message: &str) -> PluginError {
15                self.error($class_name, message)
16            }
17        )*
18    };
19}
20
21/// Safe access to the host VM for the duration of one plugin call.
22///
23/// Methods that run generic bytecode (and may therefore trigger garbage
24/// collection) take `&mut self`: the borrow checker then guarantees the
25/// rooting contract's string rule - any [`&str`](str) obtained from
26/// the host borrows `self` and cannot be held across a re-entering call.
27/// Values held across a re-entering call must be rooted, e.g.
28/// via [`Host::rooted`].
29pub struct Host<'a> {
30    api: &'a HostApi,
31}
32
33/// A decoded view of a [`GenericValue`], obtained via [`Host::decode`].
34#[derive(Debug, Clone, Copy)]
35pub enum ArgValue<'h> {
36    /// The `nil` value.
37    Nil,
38    /// A boolean.
39    Bool(bool),
40    /// An integer that fits in an `i64`.
41    Int(i64),
42    /// An integer that does not fit in an `i64`; convert via
43    /// [`Host::display`] if a textual form suffices.
44    BigInt(GenericValue),
45    /// A float.
46    Float(f64),
47    /// A rational number; inspect via [`Host::display`].
48    Rational(GenericValue),
49    /// A string, borrowed from the host (see the re-fetch rule).
50    Str(&'h str),
51    /// A list ([`Host::list_len`], [`Host::list_get`]).
52    List(GenericValue),
53    /// A tuple ([`Host::tuple_len`], [`Host::tuple_get`]).
54    Tuple(GenericValue),
55    /// A dict ([`Host::dict_get`], [`Host::dict_set`]).
56    Dict(GenericValue),
57    /// A set ([`Host::set_add`], [`Host::set_contains`]).
58    Set(GenericValue),
59    /// A range; inspect via [`Host::display`] or drive its iterator.
60    Range(GenericValue),
61    /// The exhausted-iterator sentinel.
62    StopIteration,
63    /// A plain class instance ([`Host::attr_get`], [`Host::invoke`]).
64    Instance(GenericValue),
65    /// A class ([`Host::call`] instantiates).
66    Class(GenericValue),
67    /// A callable ([`Host::call`]).
68    Function(GenericValue),
69    /// A module.
70    Module(GenericValue),
71    /// An exception instance.
72    Exception(GenericValue),
73    /// A generator; drive via [`Host::invoke`] with `__next__`.
74    Generator(GenericValue),
75    /// An iterator (drive via [`Host::invoke`] with `__next__`).
76    Iterator(GenericValue),
77    /// VM-internal values a plugin should never meaningfully receive.
78    Other(GenericValue),
79}
80
81impl<'a> Host<'a> {
82    /// Wrap a host vtable. Called by the `export_module!` glue.
83    #[doc(hidden)]
84    #[must_use]
85    pub const fn new(api: &'a HostApi) -> Self {
86        Self { api }
87    }
88
89    // --- inspect ---
90
91    /// The [`ValueKind`] of a value.
92    #[must_use]
93    pub fn kind(&self, value: GenericValue) -> ValueKind {
94        ValueKind::from_u32((self.api.value_kind)(self.api.ctx, value))
95    }
96
97    /// Decode a value into a borrowed view.
98    #[must_use]
99    pub fn decode(&self, value: GenericValue) -> ArgValue<'_> {
100        match self.kind(value) {
101            ValueKind::Nil => ArgValue::Nil,
102            ValueKind::Bool => ArgValue::Bool(self.as_bool(value).unwrap_or_default()),
103            ValueKind::Int => ArgValue::Int(self.as_int(value).unwrap_or_default()),
104            ValueKind::BigInt => ArgValue::BigInt(value),
105            ValueKind::Float => ArgValue::Float(self.as_float(value).unwrap_or_default()),
106            ValueKind::Rational => ArgValue::Rational(value),
107            ValueKind::String => ArgValue::Str(self.as_str(value).unwrap_or_default()),
108            ValueKind::List => ArgValue::List(value),
109            ValueKind::Tuple => ArgValue::Tuple(value),
110            ValueKind::Dict => ArgValue::Dict(value),
111            ValueKind::Set => ArgValue::Set(value),
112            ValueKind::Range => ArgValue::Range(value),
113            ValueKind::StopIteration => ArgValue::StopIteration,
114            ValueKind::Instance => ArgValue::Instance(value),
115            ValueKind::Class => ArgValue::Class(value),
116            ValueKind::Function => ArgValue::Function(value),
117            ValueKind::Module => ArgValue::Module(value),
118            ValueKind::Exception => ArgValue::Exception(value),
119            ValueKind::Generator => ArgValue::Generator(value),
120            ValueKind::Iterator => ArgValue::Iterator(value),
121            ValueKind::Other => ArgValue::Other(value),
122        }
123    }
124
125    /// `None` if the value is not a bool.
126    #[must_use]
127    pub fn as_bool(&self, value: GenericValue) -> Option<bool> {
128        let mut out = false;
129        (self.api.bool_get)(self.api.ctx, value, &raw mut out).then_some(out)
130    }
131
132    /// The value as an `i64`; `None` if it is not an integer or does not
133    /// fit in an `i64` (big integers - fall back to `display`).
134    #[must_use]
135    pub fn as_int(&self, value: GenericValue) -> Option<i64> {
136        let mut out = 0i64;
137        (self.api.int_get)(self.api.ctx, value, &raw mut out).then_some(out)
138    }
139
140    /// `None` if the value is not a float.
141    #[must_use]
142    pub fn as_float(&self, value: GenericValue) -> Option<f64> {
143        let mut out = 0f64;
144        (self.api.float_get)(self.api.ctx, value, &raw mut out).then_some(out)
145    }
146
147    /// The contents of a string value; `None` if the value is not a string
148    /// (or the host answered with malformed string data - a null pointer or
149    /// invalid UTF-8, both protocol violations).
150    ///
151    /// The returned string borrows the host and therefore cannot be held
152    /// across a re-entering call (`&mut self` methods); the compiler
153    /// rejects it:
154    ///
155    /// ```compile_fail,E0502
156    /// use generic_lang_api::{GenericValue, Host, PluginError};
157    ///
158    /// fn plugin_fn(host: &mut Host, args: &[GenericValue]) -> Result<GenericValue, PluginError> {
159    ///     let name = host.as_str(args[0]).unwrap();     // borrows `host`
160    ///     host.call(args[1], &[])?;                     // re-enters: needs `&mut host`
161    ///     Ok(host.make_str(name))                       // ERROR: `name` still borrowed
162    /// }
163    /// ```
164    ///
165    /// Copy the string out (`.to_owned()`) before re-entering if it is
166    /// needed afterwards.
167    #[must_use]
168    pub fn as_str(&self, value: GenericValue) -> Option<&str> {
169        let mut out = FfiStr::null();
170        if !(self.api.string_get)(self.api.ctx, value, &raw mut out) {
171            return None;
172        }
173        // A null pointer is not a valid `FfiStr` (see `abi::FfiStr`): a host
174        // that wrote one - or answered `true` without writing at all -
175        // violated the protocol. Report "not a string" rather than fabricate
176        // a value from it.
177        if out.ptr.is_null() {
178            return None;
179        }
180        // SAFETY: on success the host wrote a non-null pointer (checked
181        // above, per the `FfiStr` contract) to `len` initialized bytes of
182        // interned string data, stable until the next re-entering callback,
183        // which `&mut self` methods make unreachable while the returned
184        // borrow lives.
185        let bytes = unsafe { core::slice::from_raw_parts(out.ptr, out.len) };
186        core::str::from_utf8(bytes).ok()
187    }
188
189    /// `None` if the value is not a list.
190    #[must_use]
191    pub fn list_len(&self, value: GenericValue) -> Option<usize> {
192        let mut out = 0usize;
193        (self.api.list_len)(self.api.ctx, value, &raw mut out).then_some(out)
194    }
195
196    /// The list element at `index`.
197    ///
198    /// # Errors
199    ///
200    /// `TypeError` if the value is not a list; `IndexError` if the index
201    /// is out of bounds.
202    pub fn list_get(&self, value: GenericValue, index: usize) -> Result<GenericValue, PluginError> {
203        self.ffi_result((self.api.list_get)(self.api.ctx, value, index))
204    }
205
206    /// `None` if the value is not a tuple.
207    #[must_use]
208    pub fn tuple_len(&self, value: GenericValue) -> Option<usize> {
209        let mut out = 0usize;
210        (self.api.tuple_len)(self.api.ctx, value, &raw mut out).then_some(out)
211    }
212
213    /// The tuple element at `index`.
214    ///
215    /// # Errors
216    ///
217    /// `TypeError` if the value is not a tuple; `IndexError` if the index
218    /// is out of bounds.
219    pub fn tuple_get(
220        &self,
221        value: GenericValue,
222        index: usize,
223    ) -> Result<GenericValue, PluginError> {
224        self.ffi_result((self.api.tuple_get)(self.api.ctx, value, index))
225    }
226
227    /// `None` if the value is not a dict.
228    #[must_use]
229    pub fn dict_len(&self, value: GenericValue) -> Option<usize> {
230        let mut out = 0usize;
231        (self.api.dict_len)(self.api.ctx, value, &raw mut out).then_some(out)
232    }
233
234    /// `None` if the value is not a set.
235    #[must_use]
236    pub fn set_len(&self, value: GenericValue) -> Option<usize> {
237        let mut out = 0usize;
238        (self.api.set_len)(self.api.ctx, value, &raw mut out).then_some(out)
239    }
240
241    /// Look up a builtin global by name - exception classes like
242    /// `"TypeError"`, native classes, builtin functions.
243    ///
244    /// # Errors
245    ///
246    /// `NameError` if absent.
247    pub fn builtin(&self, name: &str) -> Result<GenericValue, PluginError> {
248        self.ffi_result((self.api.builtin_get)(self.api.ctx, Self::ffi_str(name)))
249    }
250
251    /// Whether `value` is an instance of `class` or of a subclass of it -
252    /// the exact semantics of the `isinstance` builtin, value-type proxy
253    /// classes included.
254    ///
255    /// # Errors
256    ///
257    /// `TypeError` if `class` is not a class.
258    pub fn is_instance(
259        &self,
260        value: GenericValue,
261        class: GenericValue,
262    ) -> Result<bool, PluginError> {
263        let result = self.ffi_result((self.api.is_instance)(self.api.ctx, value, class))?;
264        Ok(self.as_bool(result).unwrap_or_default())
265    }
266
267    /// The class of an instance, as a class value (the analogue of
268    /// `type(self)`). Call it to construct another instance of the same class,
269    /// or pass it to [`Host::is_instance`] to type-check another argument
270    /// before reading its opaque state.
271    ///
272    /// # Errors
273    ///
274    /// `TypeError` if `value` is not an instance.
275    pub fn class_of(&self, value: GenericValue) -> Result<GenericValue, PluginError> {
276        self.ffi_result((self.api.class_of)(self.api.ctx, value))
277    }
278
279    // --- attributes (plain field access; never re-enters) ---
280
281    /// A field of an instance.
282    ///
283    /// # Errors
284    ///
285    /// `AttributeError` if the field is absent, `TypeError` if the receiver
286    /// is not an instance.
287    pub fn attr_get(
288        &self,
289        receiver: GenericValue,
290        name: &str,
291    ) -> Result<GenericValue, PluginError> {
292        self.ffi_result((self.api.attr_get)(
293            self.api.ctx,
294            receiver,
295            Self::ffi_str(name),
296        ))
297    }
298
299    /// Set a field on an instance.
300    ///
301    /// # Errors
302    ///
303    /// `TypeError` if the receiver is not an instance.
304    pub fn attr_set(
305        &self,
306        receiver: GenericValue,
307        name: &str,
308        value: GenericValue,
309    ) -> Result<(), PluginError> {
310        self.ffi_result((self.api.attr_set)(
311            self.api.ctx,
312            receiver,
313            Self::ffi_str(name),
314            value,
315        ))
316        .map(|_| ())
317    }
318
319    /// Whether an instance has a field.
320    ///
321    /// # Errors
322    ///
323    /// `TypeError` if the receiver is not an instance.
324    pub fn attr_has(&self, receiver: GenericValue, name: &str) -> Result<bool, PluginError> {
325        let result = self.ffi_result((self.api.attr_has)(
326            self.api.ctx,
327            receiver,
328            Self::ffi_str(name),
329        ))?;
330        Ok(self.as_bool(result).unwrap_or_default())
331    }
332
333    // --- construct ---
334
335    /// A new `nil` value.
336    #[must_use]
337    pub fn make_nil(&self) -> GenericValue {
338        (self.api.nil_new)(self.api.ctx)
339    }
340
341    /// A new boolean value.
342    #[must_use]
343    pub fn make_bool(&self, value: bool) -> GenericValue {
344        (self.api.bool_new)(self.api.ctx, value)
345    }
346
347    /// A new integer value.
348    #[must_use]
349    pub fn make_int(&self, value: i64) -> GenericValue {
350        (self.api.int_new)(self.api.ctx, value)
351    }
352
353    /// A new float value.
354    #[must_use]
355    pub fn make_float(&self, value: f64) -> GenericValue {
356        (self.api.float_new)(self.api.ctx, value)
357    }
358
359    /// Intern a string value.
360    ///
361    /// # Panics
362    ///
363    /// Panics if the host rejects the string, which cannot happen for Rust
364    /// strings (they are always valid UTF-8).
365    #[must_use]
366    pub fn make_str(&self, value: &str) -> GenericValue {
367        let ffi = FfiStr {
368            ptr: value.as_ptr(),
369            len: value.len(),
370        };
371        self.ffi_result((self.api.string_new)(self.api.ctx, ffi))
372            .expect("host rejected a valid UTF-8 string")
373    }
374
375    /// A new, empty list.
376    #[must_use]
377    pub fn make_list(&self) -> GenericValue {
378        (self.api.list_new)(self.api.ctx)
379    }
380
381    /// Append to a list value.
382    ///
383    /// # Errors
384    ///
385    /// `TypeError` if the target is not a list.
386    pub fn list_push(&self, list: GenericValue, item: GenericValue) -> Result<(), PluginError> {
387        self.ffi_result((self.api.list_push)(self.api.ctx, list, item))
388            .map(|_| ())
389    }
390
391    /// Replace the element at an index.
392    ///
393    /// # Errors
394    ///
395    /// `TypeError` if the target is not a list; `IndexError` if the index
396    /// is out of bounds.
397    pub fn list_set(
398        &self,
399        list: GenericValue,
400        index: usize,
401        value: GenericValue,
402    ) -> Result<(), PluginError> {
403        self.ffi_result((self.api.list_set)(self.api.ctx, list, index, value))
404            .map(|_| ())
405    }
406
407    /// A new exception instance of `class` (any class deriving from
408    /// `Exception` - builtin or user-defined), ready to be thrown
409    /// (returned inside [`PluginError::Exception`]) or passed to generic
410    /// code. Sets the message directly, bypassing `__init__`. Prefer the
411    /// typed constructors below for the common builtin-class case.
412    ///
413    /// # Errors
414    ///
415    /// `TypeError` if `class` is not a class deriving from `Exception`.
416    pub fn make_exception(
417        &self,
418        class: GenericValue,
419        message: &str,
420    ) -> Result<GenericValue, PluginError> {
421        self.ffi_result((self.api.exception_new)(
422            self.api.ctx,
423            class,
424            Self::ffi_str(message),
425        ))
426    }
427
428    /// A [`PluginError`] carrying a fresh instance of the builtin
429    /// exception class `class_name`. Unknown names fall back to the base
430    /// `Exception` (unreachable through the typed constructors below). A
431    /// fatal host error during construction stays [`PluginError::Fatal`] -
432    /// it must never be downgraded to something catchable.
433    fn error(&self, class_name: &str, message: &str) -> PluginError {
434        let result = self
435            .builtin(class_name)
436            .and_then(|class| self.make_exception(class, message))
437            .or_else(|error| {
438                // Only fall back for catchable failures (an unknown class
439                // name); a fatal host error must propagate as-is.
440                if matches!(error, PluginError::Fatal) {
441                    return Err(error);
442                }
443                let class = self.builtin("Exception")?;
444                self.make_exception(class, message)
445            });
446        match result {
447            Ok(exception) => PluginError::Exception(exception),
448            Err(PluginError::Fatal) => PluginError::Fatal,
449            // Unreachable with the real host (the base `Exception` always
450            // exists and `builtin_get`/`exception_new` fail catchably at
451            // worst), but a real nil keeps this a valid `Value`: if it ever
452            // escaped, the host would reject the non-exception gracefully
453            // rather than transmute an invalid blob.
454            Err(_) => PluginError::Exception(self.make_nil()),
455        }
456    }
457
458    host_error_constructors!(
459        /// A [`PluginError`] carrying a fresh base `Exception` instance.
460        exception => "Exception",
461        /// A [`PluginError`] carrying a fresh `TypeError` instance.
462        type_error => "TypeError",
463        /// A [`PluginError`] carrying a fresh `ValueError` instance.
464        value_error => "ValueError",
465        /// A [`PluginError`] carrying a fresh `NameError` instance.
466        name_error => "NameError",
467        /// A [`PluginError`] carrying a fresh `ConstReassignmentError` instance.
468        const_reassignment_error => "ConstReassignmentError",
469        /// A [`PluginError`] carrying a fresh `AttributeError` instance.
470        attribute_error => "AttributeError",
471        /// A [`PluginError`] carrying a fresh `ImportError` instance.
472        import_error => "ImportError",
473        /// A [`PluginError`] carrying a fresh `AssertionError` instance.
474        assertion_error => "AssertionError",
475        /// A [`PluginError`] carrying a fresh `IoError` instance.
476        io_error => "IoError",
477        /// A [`PluginError`] carrying a fresh `KeyError` instance.
478        key_error => "KeyError",
479        /// A [`PluginError`] carrying a fresh `IndexError` instance.
480        index_error => "IndexError",
481    );
482
483    // --- display ---
484
485    /// The raw string representation of any value, as a string value.
486    /// Does NOT honor a user class's `__str__` - see [`Host::to_str`].
487    #[must_use]
488    pub fn display(&self, value: GenericValue) -> GenericValue {
489        (self.api.value_display)(self.api.ctx, value)
490    }
491
492    /// [`Host::display`], copied out as an owned Rust `String`.
493    #[must_use]
494    pub fn display_string(&self, value: GenericValue) -> String {
495        let displayed = self.display(value);
496        self.as_str(displayed).unwrap_or_default().to_owned()
497    }
498
499    // --- re-entering (run generic bytecode; GC may occur) ---
500
501    /// Call a callable value with the given arguments.
502    ///
503    /// # Errors
504    ///
505    /// Returns the generic exception raised by the callee, if any.
506    pub fn call(
507        &mut self,
508        callee: GenericValue,
509        args: &[GenericValue],
510    ) -> Result<GenericValue, PluginError> {
511        self.ffi_result((self.api.call_value)(
512            self.api.ctx,
513            callee,
514            args.as_ptr(),
515            args.len(),
516        ))
517    }
518
519    /// Invoke a named method on a receiver.
520    ///
521    /// # Errors
522    ///
523    /// Returns the generic exception raised by the method, if any.
524    pub fn invoke(
525        &mut self,
526        receiver: GenericValue,
527        name: &str,
528        args: &[GenericValue],
529    ) -> Result<GenericValue, PluginError> {
530        let name = FfiStr {
531            ptr: name.as_ptr(),
532            len: name.len(),
533        };
534        self.ffi_result((self.api.invoke_method)(
535            self.api.ctx,
536            receiver,
537            name,
538            args.as_ptr(),
539            args.len(),
540        ))
541    }
542
543    /// String conversion honoring a user class's `__str__`.
544    ///
545    /// # Errors
546    ///
547    /// Returns the generic exception raised by `__str__`, if any.
548    pub fn to_str(&mut self, value: GenericValue) -> Result<GenericValue, PluginError> {
549        self.ffi_result((self.api.value_str)(self.api.ctx, value))
550    }
551
552    /// Look up a key in a dict.
553    ///
554    /// # Errors
555    ///
556    /// `KeyError` if absent, `TypeError` for unusable targets/keys, or any
557    /// exception raised by `__hash__`/`__eq__`.
558    pub fn dict_get(
559        &mut self,
560        dict: GenericValue,
561        key: GenericValue,
562    ) -> Result<GenericValue, PluginError> {
563        self.ffi_result((self.api.dict_get)(self.api.ctx, dict, key))
564    }
565
566    /// Insert or replace a key in a dict.
567    ///
568    /// # Errors
569    ///
570    /// `TypeError` for unusable targets/keys, or any exception raised by
571    /// `__hash__`/`__eq__`.
572    pub fn dict_set(
573        &mut self,
574        dict: GenericValue,
575        key: GenericValue,
576        value: GenericValue,
577    ) -> Result<(), PluginError> {
578        self.ffi_result((self.api.dict_set)(self.api.ctx, dict, key, value))
579            .map(|_| ())
580    }
581
582    /// Whether a dict contains a key.
583    ///
584    /// # Errors
585    ///
586    /// `TypeError` for unusable targets/keys, or any exception raised by
587    /// `__hash__`/`__eq__`.
588    pub fn dict_contains(
589        &mut self,
590        dict: GenericValue,
591        key: GenericValue,
592    ) -> Result<bool, PluginError> {
593        let value = self.ffi_result((self.api.dict_contains)(self.api.ctx, dict, key))?;
594        Ok(self.as_bool(value).unwrap_or_default())
595    }
596
597    /// Add an item to a set.
598    ///
599    /// # Errors
600    ///
601    /// `TypeError` for unusable targets/items, or any exception raised by
602    /// `__hash__`/`__eq__`.
603    pub fn set_add(&mut self, set: GenericValue, item: GenericValue) -> Result<(), PluginError> {
604        self.ffi_result((self.api.set_add)(self.api.ctx, set, item))
605            .map(|_| ())
606    }
607
608    /// Whether a set contains an item.
609    ///
610    /// # Errors
611    ///
612    /// `TypeError` for unusable targets/items, or any exception raised by
613    /// `__hash__`/`__eq__`.
614    pub fn set_contains(
615        &mut self,
616        set: GenericValue,
617        item: GenericValue,
618    ) -> Result<bool, PluginError> {
619        let value = self.ffi_result((self.api.set_contains)(self.api.ctx, set, item))?;
620        Ok(self.as_bool(value).unwrap_or_default())
621    }
622
623    /// Truthiness honoring `__bool__`.
624    ///
625    /// # Errors
626    ///
627    /// Returns the generic exception raised by `__bool__`, if any.
628    pub fn truthy(&mut self, value: GenericValue) -> Result<bool, PluginError> {
629        let result = self.ffi_result((self.api.value_truthy)(self.api.ctx, value))?;
630        Ok(self.as_bool(result).unwrap_or_default())
631    }
632
633    /// Equality honoring `__eq__`.
634    ///
635    /// # Errors
636    ///
637    /// Returns the generic exception raised by `__eq__`, if any.
638    pub fn equals(&mut self, a: GenericValue, b: GenericValue) -> Result<bool, PluginError> {
639        let result = self.ffi_result((self.api.value_equals)(self.api.ctx, a, b))?;
640        Ok(self.as_bool(result).unwrap_or_default())
641    }
642
643    /// Hash honoring `__hash__`.
644    ///
645    /// # Errors
646    ///
647    /// Returns the generic exception raised by `__hash__`, if any.
648    pub fn hash(&mut self, value: GenericValue) -> Result<i64, PluginError> {
649        let result = self.ffi_result((self.api.value_hash)(self.api.ctx, value))?;
650        Ok(self.as_int(result).unwrap_or_default())
651    }
652
653    // --- rooting ---
654
655    /// Keep a value alive across re-entering calls for the rest of this
656    /// plugin call (the host releases all roots automatically on return).
657    /// Prefer the RAII form, [`Host::rooted`].
658    pub fn root(&self, value: GenericValue) {
659        (self.api.root)(self.api.ctx, value);
660    }
661
662    /// Release the `n` most recent roots early. Releasing more roots than
663    /// were pushed corrupts interpreter state; prefer the RAII form,
664    /// [`Host::rooted`].
665    pub fn unroot(&self, n: usize) {
666        (self.api.unroot)(self.api.ctx, n);
667    }
668
669    /// Root a value for the lifetime of the returned guard.
670    ///
671    /// Guards release in LIFO order - drop them in reverse order of
672    /// creation (scopes do this naturally).
673    #[must_use]
674    pub fn rooted(&self, value: GenericValue) -> Rooted<'a> {
675        (self.api.root)(self.api.ctx, value);
676        Rooted {
677            api: self.api,
678            value,
679        }
680    }
681
682    // --- plugin instance state ---
683
684    /// Install the plugin's opaque pointer on a plugin-backed instance.
685    ///
686    /// Typically called from `__init__` with `args[0]` (the receiver) and a
687    /// `Box::into_raw(state)` pointer. The class's `drop` callback is called
688    /// with this pointer when the instance is garbage-collected.
689    ///
690    /// Overwriting an already-installed pointer leaks the previous one: the
691    /// host does not run `drop` on it, since it cannot know whether the plugin
692    /// still holds a copy elsewhere. If a plugin means to replace state, it must
693    /// [`Host::get_opaque`] and free the old pointer itself first.
694    ///
695    /// # Errors
696    ///
697    /// `TypeError` if `receiver` is not a plugin-backed instance.
698    pub fn set_opaque(
699        &self,
700        receiver: GenericValue,
701        ptr: *mut core::ffi::c_void,
702    ) -> Result<(), PluginError> {
703        self.ffi_result((self.api.instance_set_opaque)(self.api.ctx, receiver, ptr))
704            .map(|_| ())
705    }
706
707    /// Recover the pointer installed by [`Host::set_opaque`], or null if none
708    /// was installed (e.g. before `__init__` ran) or `receiver` is not a
709    /// plugin-backed instance. Never raises.
710    #[must_use]
711    pub fn get_opaque(&self, receiver: GenericValue) -> *mut core::ffi::c_void {
712        (self.api.instance_get_opaque)(self.api.ctx, receiver)
713    }
714
715    /// Typed mutable view of the opaque pointer, or `None` if it is null or
716    /// `receiver` is not a plugin-backed instance.
717    ///
718    /// # Safety
719    ///
720    /// The caller must ensure `T` is the correct type for this instance's
721    /// opaque state. The reference is valid while the instance is alive (the
722    /// GC will not collect it while the plugin holds the instance value).
723    // The `&mut T` derives from the opaque `*mut` the plugin installed, not
724    // from `&self`; the shared borrow only scopes the call, so the plugin can
725    // mutate its own per-instance state through a `&Host`.
726    #[allow(clippy::mut_from_ref)]
727    #[must_use]
728    pub unsafe fn opaque_ref<T>(&self, receiver: GenericValue) -> Option<&mut T> {
729        let ptr = self.get_opaque(receiver).cast::<T>();
730        // SAFETY: guaranteed by the caller (see the `# Safety` section).
731        unsafe { ptr.as_mut() }
732    }
733
734    const fn ffi_str(s: &str) -> FfiStr {
735        FfiStr {
736            ptr: s.as_ptr(),
737            len: s.len(),
738        }
739    }
740
741    fn ffi_result(&self, ret: FfiReturn) -> Result<GenericValue, PluginError> {
742        match FfiStatus::from_u32(ret.status) {
743            Some(FfiStatus::Ok) => Ok(ret.value),
744            Some(FfiStatus::Exception) => Err(PluginError::Exception(ret.value)),
745            Some(FfiStatus::Fatal) => Err(PluginError::Fatal),
746            // A status outside the enum is a protocol violation.
747            None => Err(self.protocol_violation(&format!(
748                "host callback returned unknown status {}",
749                ret.status
750            ))),
751        }
752    }
753
754    /// Builds the protocol-violation exception without going through
755    /// [`Self::ffi_result`]: a host broken enough to answer `builtin_get`/
756    /// `exception_new` with unknown statuses too would otherwise recurse
757    /// through error construction forever. Any failure here falls back to
758    /// a nil-carrying exception rather than another decode attempt.
759    fn protocol_violation(&self, message: &str) -> PluginError {
760        let class = (self.api.builtin_get)(self.api.ctx, Self::ffi_str("Exception"));
761        if FfiStatus::from_u32(class.status) == Some(FfiStatus::Ok) {
762            let exception =
763                (self.api.exception_new)(self.api.ctx, class.value, Self::ffi_str(message));
764            if FfiStatus::from_u32(exception.status) == Some(FfiStatus::Ok) {
765                return PluginError::Exception(exception.value);
766            }
767        }
768        PluginError::Exception(self.make_nil())
769    }
770}
771
772/// RAII guard for a rooted value; see [`Host::rooted`].
773///
774/// Holds the vtable (not the [`Host`] borrow), so re-entering `&mut Host`
775/// methods remain callable while guards are alive.
776pub struct Rooted<'a> {
777    api: &'a HostApi,
778    value: GenericValue,
779}
780
781impl Rooted<'_> {
782    /// The rooted value.
783    #[must_use]
784    pub const fn get(&self) -> GenericValue {
785        self.value
786    }
787}
788
789impl Drop for Rooted<'_> {
790    fn drop(&mut self) {
791        (self.api.unroot)(self.api.ctx, 1);
792    }
793}
794
795/// Signature of a Rust plugin function used with `export_module!`.
796pub type RustPluginFn = fn(&mut Host, &[GenericValue]) -> Result<GenericValue, PluginError>;
797
798/// Implementation detail of `export_module!`: runs a Rust plugin function
799/// behind `catch_unwind` and maps the outcome to an [`FfiReturn`].
800///
801/// # Safety
802///
803/// `host` must point to a valid [`HostApi`] and `args` to `nargs`
804/// contiguous values, both valid for the duration of the call - which is
805/// what the interpreter guarantees when calling an exported plugin function.
806#[doc(hidden)]
807pub unsafe fn __invoke_plugin_fn(
808    fun: RustPluginFn,
809    host: *const HostApi,
810    args: *const GenericValue,
811    nargs: usize,
812) -> FfiReturn {
813    // SAFETY: guaranteed by the caller, see above.
814    let api = unsafe { &*host };
815    let args: &[GenericValue] = if nargs == 0 {
816        &[]
817    } else {
818        // SAFETY: guaranteed by the caller, see above.
819        unsafe { core::slice::from_raw_parts(args, nargs) }
820    };
821
822    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
823        let mut host = Host::new(api);
824        fun(&mut host, args)
825    }));
826
827    finish_plugin_invoke(api, result)
828}
829
830/// Signature of a Rust plugin method used with `export_module!`. The receiver
831/// (`self`) is a separate parameter; `args` are the remaining arguments only.
832pub type RustPluginMethodFn =
833    fn(&mut Host, GenericValue, &[GenericValue]) -> Result<GenericValue, PluginError>;
834
835/// Implementation detail of `export_module!`: the method counterpart of
836/// [`__invoke_plugin_fn`], threading the receiver through as a separate value.
837///
838/// # Safety
839///
840/// As [`__invoke_plugin_fn`]: `host` and `args`/`nargs` must be valid for the
841/// call. `receiver` is a bit-copy of the receiver value.
842#[doc(hidden)]
843pub unsafe fn __invoke_plugin_method_fn(
844    fun: RustPluginMethodFn,
845    host: *const HostApi,
846    receiver: GenericValue,
847    args: *const GenericValue,
848    nargs: usize,
849) -> FfiReturn {
850    // SAFETY: guaranteed by the caller, see above.
851    let api = unsafe { &*host };
852    let args: &[GenericValue] = if nargs == 0 {
853        &[]
854    } else {
855        // SAFETY: guaranteed by the caller, see above.
856        unsafe { core::slice::from_raw_parts(args, nargs) }
857    };
858
859    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
860        let mut host = Host::new(api);
861        fun(&mut host, receiver, args)
862    }));
863
864    finish_plugin_invoke(api, result)
865}
866
867/// Signature of a Rust plugin value creator used with `export_module!`:
868/// builds one module constant at import time.
869pub type RustPluginValueFn = fn(&mut Host) -> Result<GenericValue, PluginError>;
870
871/// Implementation detail of `export_module!`: the value-creator counterpart
872/// of [`__invoke_plugin_fn`].
873///
874/// # Safety
875///
876/// `host` must point to a valid [`HostApi`], valid for the duration of the
877/// call - which is what the interpreter guarantees when importing the
878/// plugin module.
879#[doc(hidden)]
880pub unsafe fn __invoke_plugin_value_fn(fun: RustPluginValueFn, host: *const HostApi) -> FfiReturn {
881    // SAFETY: guaranteed by the caller, see above.
882    let api = unsafe { &*host };
883
884    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
885        let mut host = Host::new(api);
886        fun(&mut host)
887    }));
888
889    finish_plugin_invoke(api, result)
890}
891
892/// Map a caught plugin invocation outcome to an [`FfiReturn`], turning a panic
893/// into a catchable base `Exception` so nothing unwinds across the C ABI.
894fn finish_plugin_invoke(
895    api: &HostApi,
896    result: std::thread::Result<Result<GenericValue, PluginError>>,
897) -> FfiReturn {
898    let host = Host::new(api);
899    match result {
900        Ok(Ok(value)) => FfiReturn {
901            status: FfiStatus::Ok as u32,
902            value,
903        },
904        Ok(Err(error)) => error_return(&host, error),
905        Err(panic) => {
906            let message = panic
907                .downcast_ref::<&str>()
908                .map(ToString::to_string)
909                .or_else(|| panic.downcast_ref::<String>().cloned())
910                .unwrap_or_else(|| "plugin function panicked".to_owned());
911            error_return(&host, host.exception(&format!("panic: {message}")))
912        }
913    }
914}
915
916fn error_return(host: &Host, error: PluginError) -> FfiReturn {
917    match error {
918        PluginError::Exception(value) => FfiReturn {
919            status: FfiStatus::Exception as u32,
920            value,
921        },
922        // The value is never read for a fatal status; a real nil keeps it a
923        // valid `Value` rather than a zeroed blob that is not one.
924        PluginError::Fatal => FfiReturn {
925            status: FfiStatus::Fatal as u32,
926            value: host.make_nil(),
927        },
928    }
929}