vpp_plugin/vlibapi/mod.rs
1//! VPP API library
2//!
3//! Traits, types and helpers for working with API messages and client registrations.
4
5use std::{
6 borrow::Cow,
7 fmt,
8 hash::{Hash, Hasher},
9 marker::PhantomData,
10 mem::{self, MaybeUninit},
11 ops::{Deref, DerefMut},
12 ptr::{self, NonNull},
13 slice,
14 str::Utf8Error,
15};
16
17use crate::{
18 bindings::{
19 vl_api_helper_client_index_to_registration, vl_api_helper_send_msg, vl_api_registration_t,
20 vl_msg_api_alloc, vl_msg_api_free,
21 },
22 vlib::BarrierHeldMainRef,
23};
24
25pub mod num_unaligned;
26
27/// An owned VPP message buffer containing a `T`.
28///
29/// The message can be sent to a client using [`Registration::send_message`].
30///
31/// Important invariant:
32///
33/// - `T` must have an alignment of 1 (e.g. by `#[repr(packed)]`)
34pub struct Message<T: ?Sized> {
35 pointer: NonNull<T>,
36}
37
38impl<T> Message<T> {
39 /// Allocate a VPP message and initialise it by copying `value` into the
40 /// newly-allocated buffer.
41 ///
42 /// # Panics
43 ///
44 /// Panics if `align_of::<T>() != 1` because the VPP API message allocator does not provide
45 /// alignment guarantees; generated message structs are expected to be packed.
46 pub fn new(value: T) -> Self {
47 if std::mem::align_of::<T>() != 1 {
48 // It's unclear what alignment guarantees vpp gives. Possibly the memory is aligned
49 // to align_of(msghdr_t), but play it safe and required packed types -
50 // vpp-plugin-api-gen generated types always have this anyway.
51 panic!("Messages must only contain #[repr(packed)] types");
52 }
53
54 // SAFETY: `vl_msg_api_alloc` returns a pointer to at least `size_of::<T>()` bytes (or
55 // null on allocation failure). We have asserted `align_of::<T>() == 1` so the cast to
56 // `*mut T` is valid. It is safe to use `NonNull::new_unchecked` because the VPP
57 // allocation cannot fail and instead aborts on allocation failures.
58 unsafe {
59 let mut me = Self {
60 pointer: NonNull::new_unchecked(
61 vl_msg_api_alloc(std::mem::size_of::<T>() as i32) as *mut T
62 ),
63 };
64 ptr::copy_nonoverlapping(&value, me.pointer.as_mut(), 1);
65 me
66 }
67 }
68
69 /// Allocate an uninitialised VPP message buffer for `T`.
70 ///
71 /// This returns a `Message<MaybeUninit<T>>`. Use [`Self::write`] or [`Self::assume_init`]
72 /// after manually initialising the contents.
73 ///
74 ///
75 /// # Panics
76 ///
77 /// Panics if `align_of::<T>() != 1` for the same reason as `new`.
78 pub fn new_uninit() -> Message<MaybeUninit<T>> {
79 if std::mem::align_of::<T>() != 1 {
80 // It's unclear what alignment guarantees vpp gives. Possibly the memory is aligned
81 // to align_of(msghdr_t), but play it safe and required packed types -
82 // vpp-plugin-api-gen generated types always have this anyway.
83 panic!("Messages must only contain #[repr(packed)] types");
84 }
85
86 // SAFETY: `vl_msg_api_alloc` returns a pointer to at least `size_of::<MaybeUninit<T>>()`
87 // bytes. Casting that pointer to `*mut MaybeUninit<T>` is valid as the buffer is
88 // uninitialised but suitably sized. It is safe to use `NonNull::new_unchecked` because
89 // the VPP allocation cannot fail and instead aborts on allocation failures.
90 unsafe {
91 Message {
92 pointer: NonNull::new_unchecked(vl_msg_api_alloc(
93 std::mem::size_of::<MaybeUninit<T>>() as i32,
94 ) as *mut MaybeUninit<T>),
95 }
96 }
97 }
98}
99
100impl Message<u8> {
101 /// Allocate a VPP message buffer `nbytes` of `u8`s initialised to 0.
102 pub fn new_bytes(nbytes: u32) -> Message<u8> {
103 // SAFETY: `vl_msg_api_alloc` returns a pointer to at least `size_of::<MaybeUninit<T>>()`
104 // bytes. Casting that pointer to `*mut MaybeUninit<T>` is valid as the buffer is
105 // uninitialised but suitably sized. It is safe to use `NonNull::new_unchecked` because
106 // the VPP allocation cannot fail and instead aborts on allocation failures.
107 unsafe {
108 let mut me = Message {
109 pointer: NonNull::new_unchecked(vl_msg_api_alloc(nbytes as i32) as *mut u8),
110 };
111 ptr::write_bytes(me.pointer.as_mut(), 0, nbytes as usize);
112 me
113 }
114 }
115}
116
117impl<T: ?Sized> Message<T> {
118 /// Consume the `Message` and return the raw pointer to the underlying buffer
119 ///
120 /// The returned pointer becomes the caller's responsibility. The `Message` destructor will
121 /// not run for `m` and the underlying buffer will not be freed by Rust; callers must ensure
122 /// the buffer is eventually freed (for example by passing it to VPP or calling
123 /// `vl_msg_api_free`).
124 ///
125 /// Not a method on `Message` to avoid clashing with application methods of the same name on
126 /// the underlying type.
127 pub fn into_raw(m: Self) -> *mut T {
128 let m = mem::ManuallyDrop::new(m);
129 m.pointer.as_ptr()
130 }
131}
132
133impl<T> Message<MaybeUninit<T>> {
134 /// Convert a `Message<MaybeUninit<T>>` into a `Message<T>` without performing any
135 /// initialisation checks
136 ///
137 /// # Safety
138 ///
139 /// The caller must ensure that the underlying buffer is fully initialised for `T`. If the
140 /// memory is not properly initialised, using the resulting `Message<T>` is undefined
141 /// behaviour.
142 pub unsafe fn assume_init(self) -> Message<T> {
143 // SAFETY: The safety requirements are documented in the function's safety comment.
144 unsafe {
145 let pointer = Message::into_raw(self);
146 Message {
147 pointer: NonNull::new_unchecked(pointer as *mut T),
148 }
149 }
150 }
151
152 /// Initialise the previously-uninitialised buffer with `value` and return the initialised
153 /// `Message<T>`
154 pub fn write(mut self, value: T) -> Message<T> {
155 // SAFETY: We have exclusive ownership of the allocated buffer for
156 // `self`. Writing `value` into the `MaybeUninit<T>` buffer
157 // initialises it, after which `assume_init` converts the message to
158 // `Message<T>`.
159 unsafe {
160 (*self).write(value);
161 self.assume_init()
162 }
163 }
164}
165
166impl<T: ?Sized> Deref for Message<T> {
167 type Target = T;
168
169 fn deref(&self) -> &Self::Target {
170 // SAFETY: `self.pointer` was allocated by `vl_msg_api_alloc` and points to a valid,
171 // initialised `T` for the lifetime of `&self`.
172 unsafe { self.pointer.as_ref() }
173 }
174}
175
176impl<T: ?Sized> DerefMut for Message<T> {
177 fn deref_mut(&mut self) -> &mut Self::Target {
178 // SAFETY: `self.pointer` was allocated by `vl_msg_api_alloc` and we hold exclusive access
179 // via `&mut self`, so returning a mutable reference to the inner `T` is valid.
180 unsafe { self.pointer.as_mut() }
181 }
182}
183
184impl<T: Default> Default for Message<T> {
185 fn default() -> Self {
186 Self::new_uninit().write(Default::default())
187 }
188}
189
190impl<T: ?Sized> Drop for Message<T> {
191 fn drop(&mut self) {
192 // SAFETY: We own the underlying buffer and the memory is considered initialised for `T`
193 // at time of drop. It's therefore safe to drop the contained `T` and free the buffer
194 // with the VPP message API free function.
195 unsafe {
196 ptr::drop_in_place(self.pointer.as_ptr());
197 vl_msg_api_free(self.pointer.as_ptr().cast());
198 }
199 }
200}
201
202impl<T> From<T> for Message<T> {
203 fn from(value: T) -> Self {
204 Self::new(value)
205 }
206}
207
208impl<T: ?Sized + PartialOrd> PartialOrd for Message<T> {
209 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
210 (**self).partial_cmp(&**other)
211 }
212}
213
214impl<T: ?Sized + Ord> Ord for Message<T> {
215 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
216 (**self).cmp(&**other)
217 }
218}
219
220impl<T: ?Sized + PartialEq> PartialEq for Message<T> {
221 fn eq(&self, other: &Self) -> bool {
222 **self == **other
223 }
224}
225
226impl<T: ?Sized + Eq> Eq for Message<T> {}
227
228impl<T: ?Sized + Hash> Hash for Message<T> {
229 fn hash<H: Hasher>(&self, state: &mut H) {
230 (**self).hash(state);
231 }
232}
233
234impl<T: fmt::Display + ?Sized> fmt::Display for Message<T> {
235 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236 fmt::Display::fmt(&**self, f)
237 }
238}
239
240impl<T: fmt::Debug + ?Sized> fmt::Debug for Message<T> {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 fmt::Debug::fmt(&**self, f)
243 }
244}
245
246impl<T: ?Sized> fmt::Pointer for Message<T> {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 let ptr: *const T = &**self;
249 fmt::Pointer::fmt(&ptr, f)
250 }
251}
252
253/// Trait used by generated message types that require endian conversions.
254///
255/// Implementations should swap fields between host and network byte order.
256pub trait EndianSwap {
257 /// Swap the endianness of the message in-place.
258 ///
259 /// `to_net == true` indicates conversion from host to network order.
260 ///
261 /// # Safety
262 ///
263 /// The caller must ensure that if `self` contains a variable length array that elements
264 /// indexed from 0 up to the contents of the length field are initialised and contained within
265 /// the memory allocated for the object.
266 unsafe fn endian_swap(&mut self, to_net: bool);
267}
268
269/// Registration state for the VPP side of an API client
270///
271/// A `&mut Registration` corresponds to a C `vl_api_registration *`.
272///
273/// Use [`RegistrationScope::from_client_index`] to obtain a mutable reference.
274#[repr(transparent)]
275pub struct Registration(foreign_types::Opaque);
276
277impl Registration {
278 /// Construct a `&mut Registration` from a raw `vl_api_registration_t` pointer.
279 ///
280 /// # Safety
281 ///
282 /// - `ptr` must be a valid, non-null pointer to a `vl_api_registration_t`.
283 /// - The caller must ensure exclusive mutable access for the returned lifetime `'a` (no other
284 /// references or concurrent uses may alias the same underlying registration for the
285 /// duration of the returned borrow).
286 /// - The pointer must remain valid for the returned lifetime and must not be freed or
287 /// invalidated while the borrow is active.
288 pub unsafe fn from_ptr_mut<'a>(ptr: *mut vl_api_registration_t) -> &'a mut Self {
289 // SAFETY: The safety requirements are documented in the function's safety comment.
290 unsafe { &mut *(ptr as *mut _) }
291 }
292
293 /// Return the raw `vl_api_registration_t` pointer for this `Registration`.
294 pub fn as_ptr(&self) -> *mut vl_api_registration_t {
295 self as *const _ as *mut _
296 }
297
298 /// Send a message to the registration.
299 ///
300 /// This consumes `message` and transfers ownership of the underlying buffer to VPP.
301 pub fn send_message<T>(&mut self, message: Message<T>) {
302 // SAFETY: `self.as_ptr()` returns a raw `vl_api_registration_t` pointer that is valid
303 // for the duration of this call. `Message::into_raw` transfers ownership of the message
304 // buffer and yields a pointer that is safe to pass to the C API; the C API takes
305 // ownership of the buffer. `vl_api_helper_send_msg` is called with valid pointers.
306 unsafe {
307 vl_api_helper_send_msg(self.as_ptr(), Message::into_raw(message).cast());
308 }
309 }
310}
311
312/// Scope helper used to obtain short-lived `&mut Registration` borrows.
313///
314/// This enforces that `Registration` references obtained cannot be retained beyond the
315/// `registration_scope` function call
316pub struct RegistrationScope<'scope>(PhantomData<&'scope ()>);
317
318impl<'scope> RegistrationScope<'scope> {
319 /// Look up a `Registration` by VPP client index.
320 ///
321 /// Returns `Some(&mut Registration)` when the client index corresponds to a current
322 /// registration, or `None` if no registration exists for that index.
323 pub fn from_client_index(
324 &self,
325 _vm: &BarrierHeldMainRef,
326 client_index: u32,
327 ) -> Option<&'scope mut Registration> {
328 // SAFETY: `vl_api_helper_client_index_to_registration` returns either a null pointer or
329 // a valid pointer to a `vl_api_registration_t` that lives as long as the corresponding
330 // client registration in VPP. The lifetime of the returned reference ensures the caller
331 // cannot retain that reference beyond the intended scope.
332 unsafe {
333 let ptr = vl_api_helper_client_index_to_registration(client_index.to_be());
334 if ptr.is_null() {
335 None
336 } else {
337 Some(Registration::from_ptr_mut(ptr))
338 }
339 }
340 }
341}
342
343/// Execute a closure with a temporary `RegistrationScope`.
344///
345/// Used to ensure any `&mut Registration` borrows that are obtained are tied to the lifetime of
346/// the closure and cannot accidentally escape.
347pub fn registration_scope<F, T>(f: F) -> T
348where
349 F: for<'scope> FnOnce(&'scope RegistrationScope<'scope>) -> T,
350{
351 let scope = RegistrationScope(PhantomData);
352 f(&scope)
353}
354
355/// A stream for sending messages to a registration
356pub struct Stream<'scope, T> {
357 registration: &'scope mut Registration,
358 _phantom: PhantomData<T>,
359}
360
361impl<'scope, T> Stream<'scope, T> {
362 /// Creates a new stream from a mutable reference to a registration.
363 pub fn new(registration: &'scope mut Registration) -> Self {
364 Self {
365 registration,
366 _phantom: PhantomData,
367 }
368 }
369
370 /// Sends a network-endian (big-endian) message to the registration
371 ///
372 /// Since the message is in network order, then it is sent without performing an endian swap.
373 pub fn send_message_ne(&mut self, message: Message<T>) {
374 self.registration.send_message(message);
375 }
376
377 /// Consumes the stream and returns the underlying registration reference.
378 pub fn into_inner(self) -> &'scope mut Registration {
379 self.registration
380 }
381}
382
383impl<'scope, T: EndianSwap> Stream<'scope, T> {
384 /// Sends a message to the registration after performing endian swap to network order.
385 ///
386 /// # Safety
387 ///
388 /// The caller must ensure that if `messages` contains a variable length array that elements
389 /// indexed from 0 up to the contents of the length field are initialised and contained within
390 /// the memory allocated for the object.
391 pub unsafe fn send_message(&mut self, mut message: Message<T>) {
392 // SAFETY: The safety requirements are documented in the function's safety comment.
393 unsafe {
394 message.endian_swap(true);
395 self.send_message_ne(message);
396 }
397 }
398}
399
400#[repr(C, packed)]
401#[derive(Copy, Clone, Default)]
402/// A string type used in VPP API messages.
403///
404/// This represents a variable-length string with a length prefix,
405/// commonly used in VPP API message structures.
406///
407/// Note that copying/cloning `ApiString` objects will not copy/clone the contents of the string.
408pub struct ApiString {
409 length: u32,
410 buf: [u8; 0],
411}
412
413impl ApiString {
414 /// Returns the length of the string in bytes.
415 pub const fn len(&self) -> u32 {
416 self.length
417 }
418
419 /// Returns `true` if the string has a length of zero.
420 pub const fn is_empty(&self) -> bool {
421 self.length == 0
422 }
423
424 /// Returns a byte slice of the string's contents.
425 pub fn as_bytes(&self) -> &[u8] {
426 // SAFETY: The buffer memory is valid and initialised for at least self.length bytes.
427 unsafe {
428 slice::from_raw_parts(
429 std::ptr::addr_of!(self.buf) as *const u8,
430 self.length as usize,
431 )
432 }
433 }
434
435 /// Returns a mutable byte slice of the string's contents.
436 fn as_bytes_mut(&mut self) -> &mut [u8] {
437 // SAFETY: The buffer memory is valid and initialised for at least self.length bytes.
438 unsafe {
439 slice::from_raw_parts_mut(
440 std::ptr::addr_of_mut!(self.buf) as *mut u8,
441 self.length as usize,
442 )
443 }
444 }
445
446 /// Converts the string to a `&str` slice.
447 ///
448 /// If the contents of the `ApiString` are valid UTF-8 data, this
449 /// function will return the corresponding `&[str]` slice. Otherwise,
450 /// it will return an error with details of where UTF-8 validation failed.
451 pub fn to_str(&self) -> Result<&str, Utf8Error> {
452 str::from_utf8(self.as_bytes())
453 }
454
455 /// Converts the string to a `Cow<str>`, replacing invalid UTF-8 sequences with �.
456 pub fn to_string_lossy(&self) -> Cow<'_, str> {
457 String::from_utf8_lossy(self.as_bytes())
458 }
459
460 /// Sets the length of the string in bytes.
461 ///
462 /// # Safety
463 ///
464 /// The caller must ensure that the underlying buffer has at least `length` bytes of valid
465 /// memory and is initialised.
466 pub unsafe fn set_len(&mut self, length: u32) {
467 self.length = length;
468 }
469
470 /// Copies the contents of the given string into this `ApiString`.
471 ///
472 /// # Panics
473 ///
474 /// Panics if the length of the `ApiString` is different to the length of the string in bytes.
475 pub fn copy_from_str(&mut self, s: &str) {
476 self.as_bytes_mut().copy_from_slice(s.as_bytes());
477 }
478}
479
480impl std::fmt::Debug for ApiString {
481 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482 std::fmt::Debug::fmt(&self.to_string_lossy(), f)
483 }
484}
485
486impl EndianSwap for ApiString {
487 unsafe fn endian_swap(&mut self, _to_net: bool) {
488 self.length = self.length.to_be();
489 // No endian swap necessary for self.buf
490 }
491}
492
493#[repr(C)]
494#[derive(Copy, Clone, PartialEq, Eq)]
495/// A string type used in VPP API messages.
496///
497/// This represents a fixed-length, nul-terminated string,
498/// commonly used in VPP API message structures.
499pub struct ApiFixedString<const N: usize> {
500 buf: [u8; N],
501}
502
503impl<const N: usize> ApiFixedString<N> {
504 /// Returns the length of the string in bytes.
505 pub const fn len(&self) -> usize {
506 let mut len = 0;
507 while self.buf[len] != 0 {
508 len += 1;
509 }
510 len
511 }
512
513 /// Returns `true` if the string has a length of zero.
514 pub const fn is_empty(&self) -> bool {
515 self.len() == 0
516 }
517
518 /// Returns a byte slice of the string's contents not including the nul-terminator.
519 pub fn as_bytes(&self) -> &[u8] {
520 &self.buf[..self.len()]
521 }
522
523 /// Converts the string to a `&str` slice.
524 ///
525 /// If the contents of the `ApiFixedString` are valid UTF-8 data, this
526 /// function will return the corresponding `&[str]` slice. Otherwise,
527 /// it will return an error with details of where UTF-8 validation failed.
528 pub fn to_str(&self) -> Result<&str, Utf8Error> {
529 str::from_utf8(self.as_bytes())
530 }
531
532 /// Converts the string to a `Cow<str>`, replacing invalid UTF-8 sequences with �.
533 pub fn to_string_lossy(&self) -> Cow<'_, str> {
534 String::from_utf8_lossy(self.as_bytes())
535 }
536
537 /// Copies the contents of the given string into this `ApiFixedString`.
538 ///
539 /// # Panics
540 ///
541 /// Panics if the string exceeds the capacity of the fixed buffer.
542 pub fn copy_from_str(&mut self, s: &str) {
543 let bytes = s.as_bytes();
544 self.buf[0..bytes.len()].copy_from_slice(bytes);
545 // Set any remaining elements to 0 since they are serialised to the wire and so we don't
546 // want any stale data, plus the first 0 acts as a nul-terminator.
547 if bytes.len() + 1 < self.buf.len() {
548 self.buf[bytes.len()..].fill(0);
549 }
550 }
551}
552
553impl<const N: usize> std::fmt::Debug for ApiFixedString<N> {
554 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
555 std::fmt::Debug::fmt(&self.to_string_lossy(), f)
556 }
557}
558
559impl<const N: usize> EndianSwap for ApiFixedString<N> {
560 unsafe fn endian_swap(&mut self, _to_net: bool) {
561 // No endian swap necessary for self.buf
562 }
563}
564
565impl<const N: usize> Default for ApiFixedString<N> {
566 fn default() -> Self {
567 Self { buf: [0; N] }
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574 use std::borrow::Cow;
575
576 #[test]
577 fn test_fixed_string_default() {
578 let s: ApiFixedString<10> = Default::default();
579 assert_eq!(s.len(), 0);
580 assert!(s.is_empty());
581 assert_eq!(s.as_bytes(), &[]);
582 assert_eq!(s.to_string_lossy(), Cow::Borrowed(""));
583 }
584
585 #[test]
586 fn test_fixed_string_copy_from_str() {
587 let mut s: ApiFixedString<10> = Default::default();
588 s.copy_from_str("hello");
589 assert_eq!(s.len(), 5);
590 assert!(!s.is_empty());
591 assert_eq!(s.as_bytes(), b"hello");
592 assert_eq!(s.to_string_lossy(), Cow::Borrowed("hello"));
593 }
594
595 #[test]
596 fn test_fixed_string_copy_from_str_with_padding() {
597 let mut s: ApiFixedString<10> = Default::default();
598 s.copy_from_str("hi");
599 assert_eq!(s.len(), 2);
600 assert_eq!(s.as_bytes(), b"hi");
601 // Check that the rest is zeroed
602 assert_eq!(s.buf[2..], [0; 8]);
603 }
604
605 #[test]
606 fn test_fixed_string_copy_from_str_empty() {
607 let mut s: ApiFixedString<10> = Default::default();
608 s.copy_from_str("");
609 assert_eq!(s.len(), 0);
610 assert!(s.is_empty());
611 }
612
613 #[test]
614 fn test_fixed_string_copy_from_str_max_length() {
615 let mut s: ApiFixedString<5> = Default::default();
616 s.copy_from_str("abcd"); // 4 chars, should fit with nul
617 assert_eq!(s.len(), 4);
618 assert_eq!(s.as_bytes(), b"abcd");
619 }
620
621 #[test]
622 #[should_panic]
623 fn test_fixed_string_copy_from_str_too_long() {
624 let mut s: ApiFixedString<5> = Default::default();
625 s.copy_from_str("abcdef"); // 6 chars, too long
626 }
627
628 #[test]
629 fn test_fixed_string_to_str_valid_utf8() {
630 let mut s: ApiFixedString<10> = Default::default();
631 s.copy_from_str("hello");
632 assert_eq!(s.to_str().unwrap(), "hello");
633 }
634
635 #[test]
636 fn test_fixed_string_to_str_invalid_utf8() {
637 let mut s: ApiFixedString<10> = Default::default();
638 // Manually set invalid UTF-8
639 s.buf[0] = 0xff;
640 s.buf[1] = 0xfe;
641 s.buf[2] = 0;
642 assert!(s.to_str().is_err());
643 }
644
645 #[test]
646 fn test_fixed_string_to_string_lossy_invalid_utf8() {
647 let mut s: ApiFixedString<10> = Default::default();
648 s.copy_from_str("hello");
649 assert_eq!(s.to_string_lossy(), "hello");
650
651 // Invalid UTF-8
652 s.buf[0] = 0xff;
653 s.buf[1] = 0;
654 assert_eq!(s.to_string_lossy(), "�");
655 }
656
657 #[test]
658 fn test_fixed_string_debug() {
659 let mut s: ApiFixedString<10> = Default::default();
660 s.copy_from_str("test");
661 assert_eq!(format!("{:?}", s), "\"test\"");
662 }
663
664 #[test]
665 fn test_fixed_string_partialeq() {
666 // Fill part of the rest of the string to ensure it has no effect when a shorter string is
667 // copied over
668 let mut s1: ApiFixedString<10> = Default::default();
669 s1.copy_from_str("test");
670 assert_eq!(s1.to_string_lossy(), "test");
671
672 s1.copy_from_str("te");
673
674 let mut s2: ApiFixedString<10> = Default::default();
675 s2.copy_from_str("te");
676
677 assert_eq!(s1, s2);
678 }
679}