Skip to main content

nsi_ffi_wrap/
context.rs

1//! An ɴsɪ context.
2
3// Needed for the example dode to build.
4extern crate self as nsi;
5use crate::{argument::*, *};
6#[allow(unused_imports)]
7use std::{
8    ffi::{CStr, CString, c_char},
9    marker::PhantomData,
10    ops::Drop,
11    os::raw::{c_int, c_void},
12};
13use triomphe::Arc;
14use ustr::ustr;
15
16/// The actual context and a marker to hold on to callbacks
17/// (closures)/references passed via [`set_attribute()`] or the like.
18///
19/// We wrap this in an [`Arc`] in [`Context`] to make sure drop() is only
20/// called when the last clone ceases existing.
21#[derive(Clone, Debug, Hash, PartialEq, Eq)]
22struct InnerContext<'a> {
23    context: NSIContext,
24    // _marker needs to be invariant in 'a.
25    // See "Making a struct outlive a parameter given to a method of
26    // that struct": https://stackoverflow.com/questions/62374326/.
27    _marker: PhantomData<*mut &'a ()>,
28}
29
30// Guaranteed by the C API.
31unsafe impl<'a> Send for InnerContext<'a> {}
32unsafe impl<'a> Sync for InnerContext<'a> {}
33
34impl<'a> Drop for InnerContext<'a> {
35    #[inline]
36    fn drop(&mut self) {
37        NSI_API.NSIEnd(self.context);
38    }
39}
40
41/// # An ɴꜱɪ Context.
42///
43/// A `Context` is used to describe a scene to the renderer and request images
44/// to be rendered from it.
45///
46/// ## Safety
47/// A `Context` may be used in multiple threads at once.
48///
49/// ## Lifetime
50/// A `Context` can be used without worrying about its lifetime until you want
51/// to store it somewhere, e.g. in a struct.
52///
53/// The reason `Context` has an explicit lifetime is so that it can take
54/// [`Reference`]s and [`Callback`]s (closures). These must be valid until the
55/// context is dropped and this guarantee requires explicit lifetimes. When you
56/// use a context directly this is not an issue but when you want to reference
57/// it somewhere the same rules as with all references apply.
58///
59/// ## Further Reading
60/// See the [ɴꜱɪ documentation on context
61/// handling](https://nsi.readthedocs.io/en/latest/c-api.html#context-handling).
62#[derive(Clone, Debug, Hash, PartialEq, Eq)]
63pub struct Context<'a>(Arc<InnerContext<'a>>);
64
65unsafe impl<'a> Send for Context<'a> {}
66unsafe impl<'a> Sync for Context<'a> {}
67
68impl<'a> From<Context<'a>> for NSIContext {
69    #[inline]
70    fn from(context: Context<'a>) -> Self {
71        context.0.context
72    }
73}
74
75impl<'a> From<NSIContext> for Context<'a> {
76    #[inline]
77    fn from(context: NSIContext) -> Self {
78        Self(Arc::new(InnerContext {
79            context,
80            _marker: PhantomData,
81        }))
82    }
83}
84
85impl<'a> Context<'a> {
86    /// Creates an ɴsɪ context.
87    ///
88    /// Contexts may be used in multiple threads at once.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// # use nsi_ffi_wrap as nsi;
94    /// // Create rendering context that dumps to stdout.
95    /// let ctx =
96    ///     nsi::Context::new(Some(&[nsi::string!("streamfilename", "stdout")]))
97    ///         .expect("Could not create ɴsɪ context.");
98    /// ```
99    /// # Error
100    /// If this method fails for some reason, it returns [`None`].
101    #[inline]
102    pub fn new(args: Option<&ArgSlice<'_, 'a>>) -> Option<Self> {
103        let (_, _, mut args_out) = get_c_param_vec(args);
104        let errorhandler_payload: *const c_void;
105
106        let fn_pointer: nsi_sys::NSIErrorHandler = Some(
107            error_handler
108                as extern "C" fn(*mut c_void, c_int, c_int, *const c_char),
109        );
110
111        if let Some(args) = args
112            && let Some(arg) =
113                args.iter().find(|arg| ustr("errorhandler") == arg.name)
114        {
115            errorhandler_payload = arg.data.as_c_ptr();
116            // SAFETY: The NSI API copies the pointer value before returning,
117            // so taking the address of this stack slot is sufficient for
118            // the lifetime of the FFI call.
119            args_out.push(nsi_sys::NSIParam {
120                name: ustr("errorhandler").as_char_ptr(),
121                data: &fn_pointer as *const _ as _,
122                type_: NSIType::Pointer as _,
123                arraylength: 0,
124                count: 1,
125                flags: 0,
126            });
127            args_out.push(nsi_sys::NSIParam {
128                name: ustr("errorhandlerdata").as_char_ptr(),
129                data: &errorhandler_payload as *const _ as _,
130                type_: NSIType::Pointer as _,
131                arraylength: 1,
132                count: 1,
133                flags: 0,
134            });
135        }
136
137        let context = NSI_API.NSIBegin(args_out.len() as _, args_out.as_ptr());
138
139        if 0 == context {
140            None
141        } else {
142            Some(Self(Arc::new(InnerContext {
143                context,
144                _marker: PhantomData,
145            })))
146        }
147    }
148
149    /// Creates a new node.
150    ///
151    /// # Arguments
152    ///
153    /// * `handle` -- A node handle. This string will uniquely identify the node
154    ///   in the scene.
155    ///
156    ///   If the supplied handle matches an existing node, the function does
157    ///   nothing if all other parameters match the call which created that
158    ///   node. Otherwise, it emits an error. Note that handles need only be
159    ///   unique within a given [`Context`]. It is ok to reuse the same
160    ///   handle inside different [`Context`]s.
161    ///
162    /// * `node_type` -- The type of node to create. The crate has `&str`
163    ///   constants for all [`node`]s that are in the official NSI
164    ///   specification. As this parameter is just a string you can instance
165    ///   other node types that a particular implementation may provide and
166    ///   which are not part of the official specification.
167    ///
168    /// * `args` -- A [`slice`](std::slice) of optional [`Arg`] arguments.
169    ///   *There are no optional arguments defined as of now*.
170    ///
171    /// ```
172    /// # use nsi_ffi_wrap as nsi;
173    /// // Create a context to send the scene to.
174    /// let ctx = nsi::Context::new(None).unwrap();
175    ///
176    /// // Create an infinte plane.
177    /// ctx.create("ground", nsi::PLANE, None);
178    /// ```
179    #[inline]
180    pub fn create(
181        &self,
182        handle: &str,
183        node_type: &str,
184        args: Option<&ArgSlice<'_, 'a>>,
185    ) {
186        let handle = HandleString::from(handle);
187        let node_type = ustr(node_type);
188        let (args_len, args_ptr, _args_out) = get_c_param_vec(args);
189
190        NSI_API.NSICreate(
191            self.0.context,
192            handle.as_char_ptr(),
193            node_type.as_char_ptr(),
194            args_len,
195            args_ptr,
196        );
197    }
198
199    /// This function deletes a node from the scene. All connections to and from
200    /// the node are also deleted.
201    ///
202    /// Note that it is not possible to delete the `.root` or the `.global`
203    /// node.
204    ///
205    /// # Arguments
206    ///
207    /// * `handle` -- A handle to a node previously created with
208    ///   [`create()`](Context::create()).
209    ///
210    /// * `args` -- A [`slice`](std::slice) of optional [`Arg`] arguments.
211    ///
212    /// # Optional Arguments
213    ///
214    /// * `"recursive"` ([`I32`]) -- Specifies whether deletion is
215    ///   recursive. By default, only the specified node is deleted. If a value
216    ///   of `1` is given, then nodes which connect to the specified node are
217    ///   recursively removed. Unless they meet one of the following conditions:
218    ///   * They also have connections which do not eventually lead to the
219    ///     specified node.
220    ///   * Their connection to the node to be deleted was created with a
221    ///     `strength` greater than `0`.
222    ///
223    ///   This allows, for example, deletion of an entire shader network in a
224    ///   single call.
225    #[inline]
226    pub fn delete(&self, handle: &str, args: Option<&ArgSlice<'_, 'a>>) {
227        let handle = HandleString::from(handle);
228        let (args_len, args_ptr, _args_out) = get_c_param_vec(args);
229
230        NSI_API.NSIDelete(
231            self.0.context,
232            handle.as_char_ptr(),
233            args_len,
234            args_ptr,
235        );
236    }
237
238    /// This functions sets attributes on a previously node.
239    /// All optional arguments of the function become attributes of
240    /// the node.
241    ///
242    /// On a [`shader`](`node::SHADER`), this function is used to set the
243    /// implicitly defined shader arguments.
244    ///
245    /// Setting an attribute using this function replaces any value
246    /// previously set by [`set_attribute()`](Context::set_attribute()) or
247    /// [`set_attribute_at_time()`](Context::set_attribute_at_time()).
248    ///
249    /// To reset an attribute to its default value, use
250    /// [`delete_attribute()`](Context::delete_attribute()).
251    ///
252    /// # Arguments
253    ///
254    /// * `handle` -- A handle to a node previously created with
255    ///   [`create()`](Context::create()).
256    ///
257    /// * `args` -- A [`slice`](std::slice) of optional [`Arg`] arguments.
258    #[inline]
259    pub fn set_attribute(&self, handle: &str, args: &ArgSlice<'_, 'a>) {
260        let handle = HandleString::from(handle);
261        let (args_len, args_ptr, _args_out) = get_c_param_vec(Some(args));
262
263        NSI_API.NSISetAttribute(
264            self.0.context,
265            handle.as_char_ptr(),
266            args_len,
267            args_ptr,
268        );
269    }
270
271    /// This function sets time-varying attributes (i.e. motion blurred).
272    ///
273    /// The `time` argument specifies at which time the attribute is being
274    /// defined.
275    ///
276    /// It is not required to set time-varying attributes in any
277    /// particular order. In most uses, attributes that are motion blurred must
278    /// have the same specification throughout the time range.
279    ///
280    /// A notable  exception is the `P` attribute on [`particles`
281    /// node](`node::PARTICLES`) which can be of different size for each
282    /// time step because of appearing or disappearing particles. Setting an
283    /// attribute using this function replaces any value previously set by
284    /// [`set_attribute()`](Context::set_attribute()).
285    ///
286    /// # Arguments
287    ///
288    /// * `handle` -- A handle to a node previously created with
289    ///   [`create()`](Context::create()).
290    ///
291    /// * `time` -- The time at which to set the value.
292    ///
293    /// * `args` -- A [`slice`](std::slice) of optional [`Arg`] arguments.
294    #[inline]
295    pub fn set_attribute_at_time(
296        &self,
297        handle: &str,
298        time: f64,
299        args: &ArgSlice<'_, 'a>,
300    ) {
301        let handle = HandleString::from(handle);
302        let (args_len, args_ptr, _args_out) = get_c_param_vec(Some(args));
303
304        NSI_API.NSISetAttributeAtTime(
305            self.0.context,
306            handle.as_char_ptr(),
307            time,
308            args_len,
309            args_ptr,
310        );
311    }
312
313    /// This function deletes any attribute with a name which matches
314    /// the `name` argument on the specified object. There is no way to
315    /// delete an attribute only for a specific time value.
316    ///
317    /// Deleting an attribute resets it to its default value.
318    ///
319    /// For example, after deleting the `transformationmatrix` attribute
320    /// on a [`transform` node](`node::TRANSFORM`), the transform will be an
321    /// identity. Deleting a previously set attribute on a [`shader`
322    /// node](`node::SHADER`) will default to whatever is declared inside
323    /// the shader.
324    ///
325    /// # Arguments
326    ///
327    /// * `handle` -- A handle to a node previously created with
328    ///   [`create()`](Context::create()).
329    ///
330    /// * `name` -- The name of the attribute to be deleted/reset.
331    #[inline]
332    pub fn delete_attribute(&self, handle: &str, name: &str) {
333        let handle = HandleString::from(handle);
334        let name = ustr(name);
335
336        NSI_API.NSIDeleteAttribute(
337            self.0.context,
338            handle.as_char_ptr(),
339            name.as_char_ptr(),
340        );
341    }
342
343    /// Create a connection between two elements.
344    ///
345    /// It is not an error to create a connection which already exists
346    /// or to remove a connection which does not exist but the nodes
347    /// on which the connection is performed must exist.
348    ///
349    /// # Arguments
350    ///
351    /// * `from` -- The handle of the node from which the connection is made.
352    ///
353    /// * `from_attr` -- The name of the attribute from which the connection is
354    ///   made.
355    ///
356    ///   If this is `None` or `Some("")` the `from` node itself will be
357    ///   connected.
358    ///
359    /// * `to` -- The handle of the node to which the connection is made.
360    ///
361    /// * `to_attr` -- The name of the attribute to which the connection is
362    ///   made. If this is an empty string then the connection is made to the
363    ///   node instead of to a specific attribute of the node.
364    ///
365    /// # Optional Arguments
366    ///
367    /// * `"value"` -- This can be used to change the value of a node's
368    ///   attribute in some contexts. Refer to guidelines on inter-object
369    ///   visibility for more information about the utility of this parameter.
370    ///
371    /// * `"priority"` ([`I32`]) -- When connecting attribute nodes,
372    ///   indicates in which order the nodes should be considered when
373    ///   evaluating the value of an attribute.
374    ///
375    /// * `"strength"` ([`I32`]) -- A connection with a `strength` greater
376    ///   than `0` will *block* the progression of a recursive
377    ///   [`delete()`](Context::delete()).
378    #[inline]
379    pub fn connect(
380        &self,
381        from: &str,
382        from_attr: Option<&str>,
383        to: &str,
384        to_attr: &str,
385        args: Option<&ArgSlice<'_, 'a>>,
386    ) {
387        let from = HandleString::from(from);
388        let from_attr = ustr(from_attr.unwrap_or(""));
389        let to = HandleString::from(to);
390        let to_attr = ustr(to_attr);
391        let (args_len, args_ptr, _args_out) = get_c_param_vec(args);
392
393        NSI_API.NSIConnect(
394            self.0.context,
395            from.as_char_ptr(),
396            from_attr.as_char_ptr(),
397            to.as_char_ptr(),
398            to_attr.as_char_ptr(),
399            args_len,
400            args_ptr,
401        );
402    }
403
404    /// This function removes a connection between two elements.
405    ///
406    /// The handle for either node may be the special value
407    /// [`.all`](crate::node::ALL). This will remove all connections which
408    /// match the other three arguments.
409    ///
410    /// # Examples
411    ///
412    /// ```
413    /// # use nsi_ffi_wrap as nsi;
414    /// # // Create a rendering context.
415    /// # let ctx = nsi::Context::new(None).unwrap();
416    /// // Disconnect everything from the scene's root.
417    /// ctx.disconnect(nsi::ALL, None, ".root", "");
418    /// ```
419    #[inline]
420    pub fn disconnect(
421        &self,
422        from: &str,
423        from_attr: Option<&str>,
424        to: &str,
425        to_attr: &str,
426    ) {
427        let from = HandleString::from(from);
428        let from_attr = ustr(from_attr.unwrap_or(""));
429        let to = HandleString::from(to);
430        let to_attr = ustr(to_attr);
431
432        NSI_API.NSIDisconnect(
433            self.0.context,
434            from.as_char_ptr(),
435            from_attr.as_char_ptr(),
436            to.as_char_ptr(),
437            to_attr.as_char_ptr(),
438        );
439    }
440
441    /// This function includes a block of interface calls from an external
442    /// source into the current scene. It blends together the concepts of a
443    /// file include, commonly known as an *archive*, with that of
444    /// procedural include which is traditionally a compiled executable. Both
445    /// are the same idea expressed in a different language.
446    ///
447    /// Note that for delayed procedural evaluation you should use a
448    /// `Procedural` node.
449    ///
450    /// The ɴꜱɪ adds a third option which sits in-between -- [Lua
451    /// scripts](https://nsi.readthedocs.io/en/latest/lua-api.html). They are more powerful than a
452    /// simple included file yet they are also easier to generate as they do not
453    /// require compilation.
454    ///
455    /// For example, it is realistic to export a whole new script for every
456    /// frame of an animation. It could also be done for every character in
457    /// a frame. This gives great flexibility in how components of a scene
458    /// are put together.
459    ///
460    /// The ability to load ɴꜱɪ commands from memory is also provided.
461    ///
462    /// # Optional Arguments
463    ///
464    /// * `"type"` ([`String`]) -- The type of file which will generate the
465    ///   interface calls. This can be one of:
466    ///   * `"apistream"` -- Read in an ɴꜱɪ stream. This requires either
467    ///     `"filename"` or `"buffer"`/`"size"` arguments to be specified too.
468    ///
469    ///   * `"lua"` -- Execute a Lua script, either from file or inline. See also
470    ///     [how to evaluate a Lua script](https://nsi.readthedocs.io/en/latest/lua-api.html#luaapi-evaluation).
471    ///
472    ///   * `"dynamiclibrary"` -- Execute native compiled code in a loadable library. See
473    ///     [dynamic library procedurals](https://nsi.readthedocs.io/en/latest/procedurals.html#section-procedurals)
474    ///     for an implementation example in C.
475    ///
476    /// * `"filename"` ([`String`]) -- The name of the file which contains the
477    ///   interface calls to include.
478    ///
479    /// * `"script"` ([`String`]) -- A valid Lua script to execute when `"type"`
480    ///   is set to `"lua"`.
481    ///
482    /// * `"buffer"` ([`String`]) -- A memory block that contain ɴꜱɪ commands to
483    ///   execute.
484    ///
485    /// * `"backgroundload"` ([`I32`]) -- If this is nonzero, the object may
486    ///   be loaded in a separate thread, at some later time. This requires that
487    ///   further interface calls not directly reference objects defined in the
488    ///   included file. The only guarantee is that the file will be loaded
489    ///   before rendering begins.
490    #[inline]
491    pub fn evaluate(&self, args: &ArgSlice<'_, 'a>) {
492        let (args_len, args_ptr, _args_out) = get_c_param_vec(Some(args));
493
494        NSI_API.NSIEvaluate(self.0.context, args_len, args_ptr);
495    }
496
497    /// This function is the only control function of the API.
498    ///
499    /// It is responsible of starting, suspending and stopping the render. It
500    /// also allows for synchronizing the render with interactive calls that
501    /// might have been issued.
502    ///
503    /// Note that this call will block if [`Action::Wait`] is selected.
504    ///
505    /// # Arguments
506    ///
507    /// * `action` -- Specifies the render [`Action`] to be performed on the
508    ///   scene.
509    ///
510    /// # Optional Arguments
511    ///
512    /// * `"progressive"` ([`I32`]) -- If set to `1`, render the image in a
513    ///   progressive fashion.
514    ///
515    /// * `"interactive"` ([`I32`]) -- If set to `1`, the renderer will
516    ///   accept commands to edit scene’s state while rendering. The difference
517    ///   with a normal render is that the render task will not exit even if
518    ///   rendering is finished. Interactive renders are by definition
519    ///   progressive.
520    ///
521    /// * `"callback"` ([`FnStatus`]) -- A closure that will be called be when
522    ///   the status of the render changes.
523    ///
524    ///   # Example
525    ///
526    ///   ```
527    ///   # use nsi_ffi_wrap as nsi;
528    ///   # let ctx = nsi::Context::new(None).unwrap();
529    ///   let status_callback = nsi::StatusCallback::new(
530    ///       |_ctx: &nsi::Context, status: nsi::RenderStatus| {
531    ///           println!("Status: {:?}", status);
532    ///         },
533    ///      );
534    ///
535    ///   /// The renderer will abort because we didn't define an output driver.
536    ///   /// So our status_callback() above will receive RenderStatus::Aborted.
537    ///   ctx.render_control(
538    ///       nsi::Action::Start,
539    ///       Some(&[
540    ///           nsi::i32!("interactive", true as _),
541    ///           nsi::callback!("callback", status_callback),
542    ///       ]),
543    ///   );
544    ///
545    ///   // Block until the renderer is really done.
546    ///   ctx.render_control(nsi::Action::Wait, None);
547    ///   ```
548    #[inline]
549    pub fn render_control(
550        &self,
551        action: Action,
552        args: Option<&ArgSlice<'_, 'a>>,
553    ) {
554        let (_, _, mut args_out) = get_c_param_vec(args);
555        let stopped_callback_payload: *const c_void;
556
557        let fn_pointer: nsi_sys::NSIRenderStopped =
558            Some(render_status as extern "C" fn(*mut c_void, c_int, c_int));
559
560        args_out.push(nsi_sys::NSIParam {
561            name: ustr("action").as_char_ptr(),
562            data: &ustr(action.as_str()).as_char_ptr() as *const _ as _,
563            type_: NSIType::String as _,
564            arraylength: 0,
565            count: 1,
566            flags: 0,
567        });
568
569        if let Some(args) = args
570            && let Some(arg) =
571                args.iter().find(|arg| ustr("callback") == arg.name)
572        {
573            stopped_callback_payload = arg.data.as_c_ptr();
574            // SAFETY: The NSI API copies the pointer value before returning,
575            // so taking the address of this stack slot is sufficient for
576            // the lifetime of the FFI call.
577            args_out.push(nsi_sys::NSIParam {
578                name: ustr("stoppedcallback").as_char_ptr(),
579                data: &fn_pointer as *const _ as _,
580                type_: NSIType::Pointer as _,
581                arraylength: 0,
582                count: 1,
583                flags: 0,
584            });
585            args_out.push(nsi_sys::NSIParam {
586                name: ustr("stoppedcallbackdata").as_char_ptr(),
587                data: &stopped_callback_payload as *const _ as _,
588                type_: NSIType::Pointer as _,
589                arraylength: 1,
590                count: 1,
591                flags: 0,
592            });
593        }
594
595        NSI_API.NSIRenderControl(
596            self.0.context,
597            args_out.len() as _,
598            args_out.as_ptr(),
599        );
600    }
601}
602
603// Action is re-exported from nsi-trait via lib.rs
604
605/// The status of a *interactive* render session.
606#[repr(i32)]
607#[derive(Debug, Copy, Clone, PartialEq, Eq, num_enum::FromPrimitive)]
608pub enum RenderStatus {
609    #[num_enum(default)]
610    Completed = nsi_sys::NSIStoppingStatus::RenderCompleted as _,
611    Aborted = nsi_sys::NSIStoppingStatus::RenderAborted as _,
612    Synchronized = nsi_sys::NSIStoppingStatus::RenderSynchronized as _,
613    Restarted = nsi_sys::NSIStoppingStatus::RenderRestarted as _,
614}
615
616/// A closure which is called to inform about the status of an ongoing render.
617///
618/// It is passed to ɴsɪ via [`render_control()`](Context::render_control())’s
619/// `"callback"` argument.
620///
621/// # Examples
622///
623/// ```
624/// # use nsi_ffi_wrap as nsi;
625/// # let ctx = nsi::Context::new(None).unwrap();
626/// let status_callback = nsi::context::StatusCallback::new(
627///     |_: &nsi::context::Context, status: nsi::context::RenderStatus| {
628///         println!("Status: {:?}", status);
629///     },
630/// );
631///
632/// ctx.render_control(
633///     nsi::Action::Start,
634///     Some(&[nsi::callback!("callback", status_callback)]),
635/// );
636/// ```
637pub trait FnStatus<'a>: Fn(
638    // The [`Context`] for which this closure was called.
639    &Context,
640    // Status of interactive render session.
641    RenderStatus,
642)
643+ 'a {}
644
645#[doc(hidden)]
646impl<
647    'a,
648    T: Fn(&Context, RenderStatus)
649        + 'a
650        + for<'r, 's> Fn(&'r context::Context<'s>, RenderStatus),
651> FnStatus<'a> for T
652{
653}
654
655// FIXME once trait aliases are in stable.
656/*
657trait FnStatus<'a> = FnMut(
658    // Status of interactive render session.
659    status: RenderStatus
660    )
661    + 'a
662*/
663
664/// Wrapper to pass a [`FnStatus`] closure to a [`Context`].
665pub struct StatusCallback<'a>(Box<Box<dyn FnStatus<'a>>>);
666
667unsafe impl Send for StatusCallback<'static> {}
668unsafe impl Sync for StatusCallback<'static> {}
669
670impl<'a> StatusCallback<'a> {
671    pub fn new<F>(fn_status: F) -> Self
672    where
673        F: FnStatus<'a>,
674    {
675        StatusCallback(Box::new(Box::new(fn_status)))
676    }
677}
678
679impl CallbackPtr for StatusCallback<'_> {
680    #[doc(hidden)]
681    fn to_ptr(self) -> *const core::ffi::c_void {
682        Box::into_raw(self.0) as *const _ as _
683    }
684}
685
686// Trampoline function for the FnStatus callback.
687#[unsafe(no_mangle)]
688pub(crate) extern "C" fn render_status(
689    payload: *mut c_void,
690    context: nsi_sys::NSIContext,
691    status: c_int,
692) {
693    // Catch any panics to prevent unwinding into C code.
694    let _ = std::panic::catch_unwind(|| {
695        if !payload.is_null() {
696            // SAFETY: The payload originates from `StatusCallback::to_ptr`, which
697            // leaks a `Box<dyn FnStatus>`. We only borrow it here and never take
698            // ownership, so the pointer stays valid for the lifetime of the
699            // context.
700            let fn_status = unsafe { &*(payload as *mut Box<dyn FnStatus>) };
701            let ctx = Context(Arc::new(InnerContext {
702                context,
703                _marker: PhantomData,
704            }));
705
706            fn_status(&ctx, status.into());
707
708            // We must not call drop() on this context.
709            // This is safe as Context doesn't allocate and this one is on
710            // the stack anyway.
711            std::mem::forget(ctx);
712        }
713    });
714    // If we panic, we can't do much -- just return silently.
715}
716
717/// A closure which is called to inform about the errors during scene defintion
718/// or a render.
719///
720/// It is passed to ɴsɪ via [`new()`](Context::new())’s `"errorhandler"`
721/// argument.
722///
723/// # Examples
724///
725/// ```
726/// # use nsi_ffi_wrap as nsi;
727/// use log::{Level, debug, error, info, trace, warn};
728///
729/// let error_handler = nsi::ErrorCallback::new(
730///     |level: Level, message_id: i32, message: &str| match level {
731///         Level::Error => error!("[{}] {}", message_id, message),
732///         Level::Warn => warn!("[{}] {}", message_id, message),
733///         Level::Info => info!("[{}] {}", message_id, message),
734///         Level::Debug => debug!("[{}] {}", message_id, message),
735///         Level::Trace => trace!("[{}] {}", message_id, message),
736///     },
737/// );
738///
739/// let ctx = nsi::Context::new(Some(&[nsi::callback!(
740///     "errorhander",
741///     error_handler
742/// )]))
743/// .unwrap();
744///
745/// // Do something with ctx ...
746/// ```
747pub trait FnError<'a>: Fn(
748    // The error level.
749    log::Level,
750    // The message id.
751    i32,
752    // The message.
753    &str,
754)
755+ 'a {}
756
757#[doc(hidden)]
758impl<
759    'a,
760    T: Fn(log::Level, i32, &str) + 'a + for<'r> Fn(log::Level, i32, &'r str),
761> FnError<'a> for T
762{
763}
764
765/// Wrapper to pass a [`FnError`] closure to a [`Context`].
766pub struct ErrorCallback<'a>(Box<Box<dyn FnError<'a>>>);
767
768unsafe impl Send for ErrorCallback<'static> {}
769unsafe impl Sync for ErrorCallback<'static> {}
770
771impl<'a> ErrorCallback<'a> {
772    pub fn new<F>(fn_error: F) -> Self
773    where
774        F: FnError<'a>,
775    {
776        ErrorCallback(Box::new(Box::new(fn_error)))
777    }
778}
779
780impl CallbackPtr for ErrorCallback<'_> {
781    #[doc(hidden)]
782    fn to_ptr(self) -> *const core::ffi::c_void {
783        Box::into_raw(self.0) as *const _ as _
784    }
785}
786
787// Trampoline function for the FnError callback.
788#[unsafe(no_mangle)]
789pub(crate) extern "C" fn error_handler(
790    payload: *mut c_void,
791    level: c_int,
792    code: c_int,
793    message: *const c_char,
794) {
795    // Catch any panics to prevent unwinding into C code.
796    let _ = std::panic::catch_unwind(|| {
797        if !payload.is_null() {
798            // SAFETY: The payload originates from `ErrorCallback::to_ptr`, which
799            // leaks a `Box<dyn FnError>`. We only borrow it here and never take
800            // ownership, so the pointer stays valid for the lifetime of the
801            // context.
802            let fn_error = unsafe { &*(payload as *mut Box<dyn FnError>) };
803
804            // SAFETY: message pointer comes from NSI C API and should be valid.
805            // We validate it is not null and handle UTF-8 errors gracefully.
806            let message = if message.is_null() {
807                "<null message>"
808            } else {
809                let c_str = unsafe { CStr::from_ptr(message as _) };
810                match c_str.to_str() {
811                    Ok(s) => s,
812                    Err(_) => {
813                        // If the string is not valid UTF-8, use lossy conversion
814                        // This prevents crashes while still conveying the message
815                        &c_str.to_string_lossy()
816                    }
817                }
818            };
819
820            let level = match NSIErrorLevel::from(level) {
821                NSIErrorLevel::Message => log::Level::Trace,
822                NSIErrorLevel::Info => log::Level::Info,
823                NSIErrorLevel::Warning => log::Level::Warn,
824                NSIErrorLevel::Error => log::Level::Error,
825            };
826
827            fn_error(level, code as _, message);
828        }
829    });
830    // If we panic, we cannot do much - just return silently.
831}