Skip to main content

mujoco_rs/wrappers/mj_editing/
traits.rs

1//! Trait definitions for model editing.
2use std::panic::{AssertUnwindSafe, catch_unwind};
3use std::os::raw::c_void;
4use std::process::abort;
5use std::ffi::CString;
6use std::any::Any;
7
8use crate::error::MjEditError;
9use crate::mujoco_c::*;
10
11/// Prefix of every user value key that the wrapper stores, which separates the keys of this
12/// crate from the keys that another language writes on the same element.
13const USER_VALUE_KEY_PREFIX: &str = "mujoco-rs:";
14
15use super::{MjSpec, MjsBody, MjsFrame, MjsSite};
16use super::default::MjsDefault;
17use super::utility::*;
18
19pub(crate) mod sealed {
20    /// Prevents external implementations of [`SpecItem`](super::SpecItem) and
21    /// [`AttachTo`](super::AttachTo).
22    pub trait Sealed {}
23}
24
25/// Every type that [`MjSpec`](super::MjSpec) supports. Sealed.
26pub trait SpecItem: Sized + sealed::Sealed {
27    /// Returns the `mjsElement` that MuJoCo keeps behind the item.
28    ///
29    /// The pointer is const. A caller that must satisfy MJS's wrong use of mutable pointers, such
30    /// as [`mjs_getName`], casts it at the call site.
31    fn element_pointer(&self) -> *const mjsElement;
32
33    /// Same as [`SpecItem::element_pointer`], but with a mutable borrow and a mutable pointer.
34    fn element_mut_pointer(&mut self) -> *mut mjsElement {
35        self.element_pointer() as *mut _
36    }
37
38    /// Returns the item's name.
39    ///
40    /// # Panics
41    /// Panics if the stored MuJoCo string is not valid UTF-8.
42    fn name(&self) -> &str {
43        // SAFETY: the string belongs to the element and lives as long as it does. mjs_getName
44        // takes a mutable pointer but writes nothing.
45        unsafe { read_mjs_string(mjs_getName(self.element_pointer() as *mut _)) }
46    }
47
48    /// Set a new name.
49    /// # Errors
50    /// Returns [`MjEditError::AlreadyExists`] when an element with the same name already exists.
51    /// # Panics
52    /// When the `name` contains '\0' characters mid string, a panic occurs.
53    fn set_name(&mut self, name: &str) -> Result<(), MjEditError> {
54        let cstr = CString::new(name).unwrap();  // panics on interior NUL bytes; &str guarantees UTF-8
55        let result = unsafe { mjs_setName(self.element_mut_pointer(), cstr.as_ptr()) };
56        if result != 0 {
57            return Err(MjEditError::AlreadyExists);
58        }
59        Ok(())
60    }
61
62    /// Builder style set a new name.
63    /// # Panics
64    /// Panics when an element with the same name already exists, or when `name` contains '\0'.
65    fn with_name(&mut self, name: &str) -> &mut Self {
66        self.set_name(name).expect("mjs_setName failed: duplicate name or null byte");
67        self
68    }
69
70    /// Returns the used default, or `None` when the element carries no default class name.
71    ///
72    /// Only a body, joint, geom, site, camera, light, actuator, pair, equality, tendon, mesh or
73    /// material can carry one. Every other element, a frame included, returns `None`.
74    fn default(&self) -> Option<&MjsDefault> {
75        let ptr = unsafe { mjs_getDefault(self.element_pointer()) };
76        // SAFETY: a non-null return points to the mjsDefault owned by a live mjCDef of the spec.
77        unsafe { crate::wrappers::mj_editing::MjsDefault::from_ffi_ptr(ptr) }
78    }
79
80    /// Returns the numeric id for this element, or `None` when it has none yet (before
81    /// compilation, for example).
82    fn id(&self) -> Option<usize> {
83        let id = unsafe { mjs_getId(self.element_pointer()) };
84        usize::try_from(id).ok()
85    }
86
87    /// Assign the item to a default class.
88    /// # Errors
89    /// Returns [`MjEditError::NotFound`] when the default with the `class_name` doesn't exist.
90    /// # Panics
91    /// When the `class_name` contains '\0' characters, a panic occurs.
92    fn set_default(&mut self, class_name: &str) -> Result<(), MjEditError> {
93        /* Workaround to pass the borrow checker (we use the existing borrow) */
94        let cname = CString::new(class_name).unwrap();  // panics on interior NUL bytes only.
95        let element = self.element_pointer();
96        let spec = unsafe { mjs_getSpec(element) };
97        let default = unsafe { mjs_findDefault(spec, cname.as_ptr()) };
98        if default.is_null() {
99            return Err(MjEditError::NotFound);
100        }
101
102        unsafe { mjs_setDefault(self.element_mut_pointer(), default); }
103        Ok(())
104    }
105
106    /// Builder style make the item inherit from a default class.
107    /// # Errors
108    /// Same as [`SpecItem::set_default`].
109    /// # Panics
110    /// When the `class_name` contains '\0' characters, a panic occurs.
111    fn with_default(&mut self, class_name: &str) -> Result<&mut Self, MjEditError> {
112        self.set_default(class_name)?;
113        Ok(self)
114    }
115
116}
117
118/// A [`SpecItem`] that becomes a concrete object inside
119/// [`crate::wrappers::mj_model::MjModel`] once [`super::MjSpec`] compiles. That is every
120/// [`SpecItem`] except [`MjsDefault`] and [`MjsWrap`](super::MjsWrap). Only such an object carries
121/// [`SpecObject::delete`].
122pub trait SpecObject: SpecItem {
123    /// The `mjtObj` discriminant passed to `mjs_firstElement` / `mjs_firstChild`.
124    const OBJ_TYPE: mjtObj;
125
126    /// Casts a raw `*mut mjsElement` to `*mut Self`.
127    ///
128    /// # Safety
129    /// `ptr` must point to a valid element of type `Self`.
130    unsafe fn from_element_as_ptr_mut(ptr: *mut mjsElement) -> *mut Self;
131
132    /// Delete the element from the specification that holds it.
133    ///
134    /// Deleting a body deletes its subtree, and frees every keyframe, and every actuator, sensor,
135    /// tendon, equality, pair and exclude that refers to the subtree.
136    ///
137    /// # Errors
138    /// - [`MjEditError::UnsupportedOperation`] if the element is a frame or the world body.
139    /// - [`MjEditError::DeleteFailed`] if MuJoCo refuses the deletion, which it does while another
140    ///   specification holds this one.
141    ///
142    /// # Safety
143    /// - Delete each element at most once. MuJoCo keeps the element allocated until the
144    ///   specification drops, so a second deletion frees it twice.
145    /// - Do not delete an element that the deletion of a body already took out of the
146    ///   specification. An iterator collected before that deletion still hands out its handle.
147    /// - Do not use the handle of an element that the deletion of a body freed.
148    ///
149    /// # Examples
150    /// ```
151    /// # use mujoco_rs::prelude::*;
152    /// let mut spec = MjSpec::new();
153    /// spec.world_body_mut().add_body().with_name("ball");
154    ///
155    /// // SAFETY: the body is deleted once, and no handle of the spec outlives the call.
156    /// unsafe { spec.body_mut("ball").unwrap().delete() }.unwrap();
157    /// ```
158    ///
159    /// A default class is no `SpecObject`, so it carries no `delete`.
160    /// ```compile_fail
161    /// # use mujoco_rs::prelude::*;
162    /// let mut spec = MjSpec::new();
163    /// unsafe { spec.add_default("cls", None).delete() }.unwrap();
164    /// ```
165    unsafe fn delete(&mut self) -> Result<(), MjEditError> {
166        // SAFETY: the handle stands at a live element, which the caller keeps out of a second
167        // deletion.
168        unsafe { delete_element(self.element_mut_pointer()) }
169    }
170}
171
172/// Represents the types of spec items that carry some user-set values (key-value map).
173/// These values are only available while the [`MjSpec`], to which spec items belong,
174/// is alive, and don't get carried over to the compiled [`MjModel`](crate::wrappers::mj_model::MjModel).
175///
176/// The wrapper prefixes every key that it stores, so a key that another language wrote on the
177/// same element stays out of reach.
178///
179/// Note that the user storage is spec-local, even after copying.
180/// Only spec attachments by reference share values.
181/// 
182/// # Lifetime
183/// A stored value is dropped when its key is removed, when another value replaces it, or when
184/// MuJoCo deletes the element that holds it. An element survives at most until the belonging
185/// [`MjSpec`] is freed.
186pub trait UserValued: SpecItem {
187    /// Obtains a polymorphic reference to the stored data under `key` contained within this spec item.
188    /// If no data is stored under `key`, [`None`] is returned.
189    /// Wraps [`mjs_getUserValue`].
190    /// 
191    /// # Panics
192    /// When `key` contains null-bytes.
193    /// 
194    /// # Note
195    /// Plugin-stored values, under plugin-named keys, will always return [`None`], unless the plugin
196    /// adds the 'mujoco-rs:' prefix, used internally in MuJoCo-rs as a prefix for keys, in front of
197    /// its keys. Doing so, the entire implementation becomes undefined behavior, as non-boxed data
198    /// of types that is non-[`Any`] will be cast in our implementation to `&dyn Any`.
199    /// 
200    /// This function remains a non-`unsafe` function as we consider custom plugins, specifically designed
201    /// to crash this crate, outside our safety scope.
202    ///
203    /// # Examples
204    /// ```
205    /// # use mujoco_rs::prelude::*;
206    /// # let mut spec = MjSpec::new();
207    /// # let geom = spec.world_body_mut().add_geom();
208    /// geom.set_user_value("serial", Box::new(String::from("user-value-1")));
209    /// let value = geom.user_value("serial").unwrap();
210    /// assert_eq!(value.downcast_ref::<String>().unwrap(), "user-value-1");
211    ///
212    /// // A downcast to any other type yields `None`.
213    /// assert!(value.downcast_ref::<u32>().is_none());
214    /// assert!(geom.user_value("absent").is_none());
215    /// ```
216    fn user_value(&self, key: &str) -> Option<&dyn Any> {
217        let c_key = user_value_key(key);
218        // SAFETY: the handle stands at a live element, and the key outlives the call. The C
219        // parameter is mutable although the function only reads the element.
220        let maybe_data = unsafe {
221            mjs_getUserValue(self.element_pointer() as *mut _, c_key.as_ptr())
222        };
223
224        if maybe_data.is_null() {
225            return None;
226        }
227
228        // SAFETY: the key prefix keeps foreign writers out, so only set_user_value stores here,
229        // and it stores a pointer to a boxed trait object.
230        Some(unsafe { &**(maybe_data as *const Box<dyn Any>) })
231    }
232
233    /// Obtains a polymorphic mutable reference to the stored data under `key` contained within
234    /// this spec item. If no data is stored under `key`, [`None`] is returned.
235    /// Wraps [`mjs_getUserValue`].
236    ///
237    /// # Panics
238    /// When `key` contains null-bytes.
239    ///
240    /// # Note
241    /// Plugin-stored values, under plugin-named keys, will always return [`None`], unless under
242    /// conditions described in [`UserValued::user_value`].
243    ///
244    /// # Examples
245    /// ```
246    /// # use mujoco_rs::prelude::*;
247    /// # let mut spec = MjSpec::new();
248    /// # let geom = spec.world_body_mut().add_geom();
249    /// # geom.set_user_value("trace", Box::new(vec![1u32, 2]));
250    /// geom.user_value_mut("trace").unwrap().downcast_mut::<Vec<u32>>().unwrap().push(3);
251    /// assert_eq!(geom.user_value("trace").unwrap().downcast_ref::<Vec<u32>>().unwrap(), &[1, 2, 3]);
252    /// ```
253    fn user_value_mut(&mut self, key: &str) -> Option<&mut dyn Any> {
254        let c_key = user_value_key(key);
255        // SAFETY: the handle is a live element and the key is valid throughout the call.
256        let maybe_data = unsafe {
257            mjs_getUserValue(self.element_mut_pointer(), c_key.as_ptr())
258        };
259
260        if maybe_data.is_null() {
261            return None;
262        }
263
264        // SAFETY: same as in user_value. mjs_getUserValue does return `*const c_void`,
265        // however, the actual owned data is a mutable Box from the start, thus making
266        // the cast from const to mut perfectly sound.
267        Some(unsafe { &mut **maybe_data.cast_mut().cast::<Box<dyn Any>>() })
268    }
269
270    /// Sets `value` under `key` into this spec item. The value that `key` held before is dropped.
271    /// Wraps [`mjs_setUserValueWithCleanup`].
272    /// 
273    /// # Panics
274    /// When `key` contains null-bytes.
275    ///
276    /// # Examples
277    /// ```
278    /// # use mujoco_rs::prelude::*;
279    /// # let mut spec = MjSpec::new();
280    /// # let geom = spec.world_body_mut().add_geom();
281    /// geom.set_user_value("serial", Box::new(String::from("user-value-1")));
282    /// # assert_eq!(geom.user_value("serial").unwrap().downcast_ref::<String>().unwrap(), "user-value-1");
283    ///
284    /// // The same key takes another type, and drops the value it held.
285    /// geom.set_user_value("serial", Box::new(42u32));
286    /// assert_eq!(geom.user_value("serial").unwrap().downcast_ref::<u32>(), Some(&42));
287    /// ```
288    fn set_user_value(&mut self, key: &str, value: Box<dyn Any>) {
289        let c_key = user_value_key(key);
290        // The outer box keeps the stored pointer thin, because a trait object is a fat pointer.
291        let data = Box::into_raw(Box::new(value)).cast();
292        // SAFETY: the handle stands at a live element, and MuJoCo hands `data` back to
293        // clean_box_any exactly once.
294        unsafe {
295            mjs_setUserValueWithCleanup(
296                self.element_mut_pointer(),
297                c_key.as_ptr(), data,
298                Some(clean_box_any)
299            );
300        }
301    }
302
303    /// Drops the value that `key` holds. Does nothing when `key` holds no value.
304    /// Wraps [`mjs_deleteUserValue`].
305    ///
306    /// # Panics
307    /// When `key` contains null-bytes.
308    ///
309    /// # Examples
310    /// ```
311    /// # use mujoco_rs::prelude::*;
312    /// # let mut spec = MjSpec::new();
313    /// # let geom = spec.world_body_mut().add_geom();
314    /// # geom.set_user_value("serial", Box::new(String::from("user-value-1")));
315    /// geom.remove_user_value("serial");
316    /// assert!(geom.user_value("serial").is_none());
317    /// ```
318    fn remove_user_value(&mut self, key: &str) {
319        let c_key = user_value_key(key);
320        // SAFETY: the handle stands at a live element, and the key outlives the call.
321        unsafe { mjs_deleteUserValue(self.element_mut_pointer(), c_key.as_ptr()) };
322    }
323}
324
325/// Returns the key under which MuJoCo stores the user value of `key`.
326/// 
327/// This is needed to avoid accidental clashes with plugin-set keys.
328/// The only way a plugin can now clash, is for the plugin itself
329/// to prepend the same [`USER_VALUE_KEY_PREFIX`] to the key.
330/// 
331///
332/// # Panics
333/// Panics when `key` contains null-bytes.
334fn user_value_key(key: &str) -> CString {
335    // Allocate and then push.
336    // This avoids unnecessary reallocations, as, due to the CString implementation,
337    // only one allocation is made (String::with_capacity).
338    let mut prefixed = String::with_capacity(USER_VALUE_KEY_PREFIX.len() + key.len() + 1);
339    prefixed.push_str(USER_VALUE_KEY_PREFIX);
340    prefixed.push_str(key);
341    CString::new(prefixed).unwrap()
342}
343
344/// Drops the box that [`UserValued::set_user_value`] leaked. MuJoCo calls it when the key takes
345/// another value, when the key is removed, and when the element dies.
346///
347/// # Safety
348/// `data` must be a pointer that [`UserValued::set_user_value`] stored, passed back once.
349unsafe extern "C" fn clean_box_any(data: *const c_void) {
350    // SAFETY: the caller passes back the box that set_user_value leaked, and passes it once.
351    let value = unsafe { Box::from_raw(data as *mut Box<dyn Any>) };
352    if catch_unwind(AssertUnwindSafe(move || drop(value))).is_err() {
353        abort();
354    }
355}
356
357
358/// A child that [`mjs_attach`] accepts for a parent of type `P`.
359///
360/// # Supported attachments
361/// | Child | Parent `P` |
362/// |---|---|
363/// | [`MjsBody`] | [`MjsFrame`], [`MjsSite`] |
364/// | [`MjsFrame`] | [`MjsFrame`], [`MjsSite`] |
365/// | [`MjSpec`] | [`MjsBody`], [`MjsFrame`], [`MjsSite`] |
366///
367/// ## Attaching a frame to a body
368/// MuJoCo does not copy an [`MjsFrame`] in full when it attaches directly onto an [`MjsBody`].
369/// The attachment pair (parent `MjsBody`, child `MjsFrame`) is therefore not permitted,
370/// thus [`AttachTo`] for that pair is not implemented.
371/// Attach the frame to an [`MjsFrame`] of that body instead.
372/// 
373/// The following will fail to compile:
374/// ```compile_fail
375/// # use mujoco_rs::prelude::*;
376/// let mut child = MjSpec::new();
377/// let mut parent = MjSpec::new();
378/// let frame = child.world_body_mut().add_frame();
379/// parent.world_body_mut()
380///     .attach_by_deep_copy(frame, "c_", "").unwrap();
381/// ```
382/// 
383/// After adding a frame in between, it compiles fine:
384/// ```
385/// # use mujoco_rs::prelude::*;
386/// let mut child = MjSpec::new();
387/// let mut parent = MjSpec::new();
388/// let frame = child.world_body_mut().add_frame();
389/// parent.world_body_mut()
390///     .add_frame()
391///     .attach_by_deep_copy(frame, "c_", "").unwrap();
392/// ```
393pub trait AttachTo<P>: sealed::Sealed {
394    /// Returns the `mjsElement` that MuJoCo attaches to the parent. The pointer is mutable,
395    /// because [`mjs_attach`] renames and reparents the child that it receives.
396    fn child_element_mut_pointer(&mut self) -> *mut mjsElement;
397}
398
399// A specification is no `SpecItem`, so it carries its own seal for this trait.
400impl sealed::Sealed for MjSpec {}
401
402impl AttachTo<MjsFrame> for MjsBody {
403    fn child_element_mut_pointer(&mut self) -> *mut mjsElement {
404        self.element_mut_pointer()
405    }
406}
407
408impl AttachTo<MjsSite> for MjsBody {
409    fn child_element_mut_pointer(&mut self) -> *mut mjsElement {
410        self.element_mut_pointer()
411    }
412}
413
414impl AttachTo<MjsFrame> for MjsFrame {
415    fn child_element_mut_pointer(&mut self) -> *mut mjsElement {
416        self.element_mut_pointer()
417    }
418}
419
420impl AttachTo<MjsSite> for MjsFrame {
421    fn child_element_mut_pointer(&mut self) -> *mut mjsElement {
422        self.element_mut_pointer()
423    }
424}
425
426impl AttachTo<MjsBody> for MjSpec {
427    fn child_element_mut_pointer(&mut self) -> *mut mjsElement {
428        self.ffi().element
429    }
430}
431
432impl AttachTo<MjsFrame> for MjSpec {
433    fn child_element_mut_pointer(&mut self) -> *mut mjsElement {
434        self.ffi().element
435    }
436}
437
438impl AttachTo<MjsSite> for MjSpec {
439    fn child_element_mut_pointer(&mut self) -> *mut mjsElement {
440        self.ffi().element
441    }
442}
443
444/// A parent that [`mjs_attach`] accepts.
445/// The trait is sealed (cannot be implemented by the user).
446///
447/// The [`AttachTo`] trait (also sealed) is used for providing
448/// supported attachment combinations.
449pub trait Attach: SpecItem {
450    /// Attaches a **deep-copy** of the `child` to `Self`. Wraps [`mjs_attach`].
451    /// For faster attachments, call [`Attach::attach_by_reference`], which is
452    /// MuJoCo's default behavior. However, the latter requires `unsafe` due to
453    /// possible UBs it allows.
454    ///
455    /// # Note
456    /// MuJoCo mutates the `child` even with deep-copying enabled.
457    /// When the child is a [`MjSpec`], it will create a new [`MjsFrame`] in its world body
458    /// on every attachment, under which all the sub-elements of `child` are reparented.
459    ///
460    /// # Errors
461    /// Returns [`MjEditError::AttachFailed`] when MuJoCo rejects the attachment.
462    ///
463    /// # Panics
464    /// Panics when `prefix` or `suffix` contain NULL bytes.
465    ///
466    /// # Examples
467    /// ```
468    /// # use mujoco_rs::prelude::*;
469    /// let mut child = MjSpec::new();
470    /// child.world_body_mut().add_body().with_name("ball");
471    ///
472    /// let mut parent = MjSpec::new();
473    /// parent.world_body_mut().attach_by_deep_copy(&mut child, "robot_", "").unwrap();
474    /// assert!(parent.body("robot_ball").is_some());
475    /// ```
476    fn attach_by_deep_copy<C>(&mut self, child: &mut C, prefix: &str, suffix: &str)
477        -> Result<(), MjEditError>
478        where C: AttachTo<Self>
479    {
480        // SAFETY: all pointers are valid always.
481        unsafe {
482            attach_element(
483                self.element_mut_pointer(), child.child_element_mut_pointer(), prefix, suffix, true
484            )
485        }
486    }
487
488    /// Attaches the `child` to `Self` by reference. Wraps [`mjs_attach`].
489    /// Attachment-by-reference is the default behavior in MuJoCo (C library).
490    ///
491    /// # Safety
492    /// This method is safe as long as the following conditions are met:
493    /// - no element of the [`MjSpec`] in which the `child` lives is used anymore,
494    ///   including the elements outside the attached subtree;
495    /// - no further element of that [`MjSpec`] is attached anywhere;
496    /// - no existing references to the child (or other tree elements of child's [`MjSpec`])
497    ///   can be used further;
498    /// - that child [`MjSpec`] is not compiled, because a compilation can free an element to
499    ///   which the parent keeps a pointer.
500    ///
501    /// # Note
502    /// An attachment that returns an error still marks the `child` specification as attached, thus
503    /// the conditions above hold also for a failed attachment.
504    ///
505    /// # Errors
506    /// Returns [`MjEditError::AttachFailed`] when MuJoCo rejects the attachment.
507    ///
508    /// # Panics
509    /// Panics when `prefix` or `suffix` contain NULL bytes.
510    ///
511    /// # Examples
512    /// ```
513    /// # use mujoco_rs::prelude::*;
514    /// let mut child = MjSpec::new();
515    /// child.world_body_mut().add_body().with_name("ball");
516    ///
517    /// let mut parent = MjSpec::new();
518    /// // SAFETY: no element handle of the child is used after the attachment.
519    /// unsafe { parent.world_body_mut().attach_by_reference(&mut child, "robot_", "") }.unwrap();
520    /// assert!(parent.body("robot_ball").is_some());
521    /// ```
522    unsafe fn attach_by_reference<C>(&mut self, child: &mut C, prefix: &str, suffix: &str)
523        -> Result<(), MjEditError>
524        where C: AttachTo<Self>
525    {
526        // SAFETY: the parent element is live, AttachTo permits the pair, and the caller keeps
527        // every handle of the child unused.
528        unsafe {
529            attach_element(
530                self.element_mut_pointer(), child.child_element_mut_pointer(), prefix, suffix, false
531            )
532        }
533    }
534}
535
536impl Attach for MjsBody {}
537impl Attach for MjsFrame {}
538impl Attach for MjsSite {}
539
540/// Attaches the `child` element to the `parent` element. `deep_copy` selects whether the parent
541/// deep-copies the elements of the child or "copies" by-reference.
542/// Wraps [`mjs_attach`].
543///
544/// # Errors
545/// Returns [`MjEditError::AttachFailed`] when MuJoCo rejects the attachment.
546///
547/// # Panics
548/// Panics when `prefix` or `suffix` contain NULL bytes.
549///
550/// # Safety
551/// Both pointers must stand at a live element of a specification.
552/// With `deep_copy` false, the parent shares the elements of the child.
553unsafe fn attach_element(
554    parent: *mut mjsElement, child: *mut mjsElement,
555    prefix: &str, suffix: &str, deep_copy: bool
556) -> Result<(), MjEditError>
557{
558    let c_prefix = CString::new(prefix).unwrap();  // panics on interior NUL bytes only.
559    let c_suffix = CString::new(suffix).unwrap();
560
561    // SAFETY: the caller guarantees a live parent element, which belongs to a live specification.
562    let spec = unsafe { mjs_getSpec(parent) };
563    // MuJoCo keeps the flag on the parent, so every attachment sets the value that it needs.
564    unsafe { mjs_setDeepCopy(spec, deep_copy.into()) };
565
566    // The const on the C child parameter is misleading, because mjs_attach renames and reparents
567    // the child regardless, so the pointer that reaches here is mutable.
568    // SAFETY: both elements stand at a live specification and the two strings outlive the call.
569    let element = unsafe { mjs_attach(parent, child, c_prefix.as_ptr(), c_suffix.as_ptr()) };
570    if element.is_null() {
571        // SAFETY: spec stands at the live specification of the parent.
572        return Err(MjEditError::AttachFailed(unsafe { read_spec_error(spec) }));
573    }
574    Ok(())
575}
576
577#[cfg(test)]
578mod tests {
579    use std::rc::Rc;
580    use std::cell::Cell;
581
582    use super::*;
583
584    /// Counts its own drops.
585    struct DropCounter(Rc<Cell<u32>>);
586
587    impl Drop for DropCounter {
588        fn drop(&mut self) {
589            self.0.set(self.0.get() + 1);
590        }
591    }
592
593    #[test]
594    fn test_user_value() {
595        const VALID_USER_VALUE_KEY: &str = "valid_user_value_key";
596        
597        enum SetOfUserValueTypes {
598            Integer(u32),
599        }
600
601        let mut spec = MjSpec::new();
602        let geom = spec.world_body_mut().add_geom();
603
604        geom.set_user_value(VALID_USER_VALUE_KEY, Box::new(SetOfUserValueTypes::Integer(14)));
605        let value = geom.user_value(VALID_USER_VALUE_KEY).unwrap()
606            .downcast_ref::<SetOfUserValueTypes>().unwrap();
607
608        assert!(matches!(value, SetOfUserValueTypes::Integer(14)));
609        assert!(geom.user_value(VALID_USER_VALUE_KEY).unwrap().downcast_ref::<u32>().is_none());
610
611        *geom.user_value_mut(VALID_USER_VALUE_KEY).unwrap()
612            .downcast_mut::<SetOfUserValueTypes>().unwrap() = SetOfUserValueTypes::Integer(15);
613        assert!(matches!(
614            geom.user_value(VALID_USER_VALUE_KEY).unwrap()
615                .downcast_ref::<SetOfUserValueTypes>().unwrap(),
616            SetOfUserValueTypes::Integer(15)
617        ));
618        assert!(geom.user_value_mut("absent").is_none());
619
620        geom.remove_user_value(VALID_USER_VALUE_KEY);
621        assert!(geom.user_value(VALID_USER_VALUE_KEY).is_none());
622    }
623
624    #[test]
625    fn test_user_value_cleanup() {
626        let drops = Rc::new(Cell::new(0));
627        {
628            let mut spec = MjSpec::new();
629            let geom = spec.world_body_mut().add_geom();
630
631            geom.set_user_value("key", Box::new(DropCounter(Rc::clone(&drops))));
632            assert_eq!(drops.get(), 0);
633
634            geom.set_user_value("key", Box::new(DropCounter(Rc::clone(&drops))));
635            assert_eq!(drops.get(), 1, "overwriting a key must drop the value it held");
636
637            geom.remove_user_value("key");
638            assert_eq!(drops.get(), 2, "removing a key must drop the value it held");
639
640            geom.set_user_value("key", Box::new(DropCounter(Rc::clone(&drops))));
641        }
642        assert_eq!(drops.get(), 3, "dropping the spec must drop the value it holds");
643    }
644
645    #[test]
646    fn test_user_value_dropped_on_element_delete() {
647        let drops = Rc::new(Cell::new(0));
648        {
649            let mut spec = MjSpec::new();
650            let geom = spec.world_body_mut().add_geom();
651            geom.set_user_value("key", Box::new(DropCounter(Rc::clone(&drops))));
652
653            // SAFETY: the borrow of the geom ends with the call, so no handle reaches the element
654            // after its deletion.
655            unsafe { geom.delete() }.unwrap();
656            assert_eq!(drops.get(), 0, "memory is supposed to be freed after the spec is dropped");
657        }
658        assert_eq!(drops.get(), 1, "the spec must drop the value a deleted element held");
659    }
660}