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}