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}