pinocchio_log/logger.rs
1use core::{
2 cmp::min, mem::MaybeUninit, ops::Deref, ptr::copy_nonoverlapping, slice::from_raw_parts,
3};
4
5#[cfg(all(target_os = "solana", not(target_feature = "static-syscalls")))]
6mod syscalls {
7 // Syscalls provided by the SVM runtime (SBPFv0, SBPFv1 and SBPFv2).
8 extern "C" {
9 pub fn sol_log_(message: *const u8, len: u64);
10
11 pub fn sol_memcpy_(dst: *mut u8, src: *const u8, n: u64);
12
13 pub fn sol_memset_(s: *mut u8, c: u8, n: u64);
14
15 pub fn sol_remaining_compute_units() -> u64;
16 }
17}
18
19#[cfg(all(target_os = "solana", target_feature = "static-syscalls"))]
20mod syscalls {
21 // Syscalls provided by the SVM runtime (SBPFv3 and newer).
22 pub(crate) fn sol_log_(message: *const u8, length: u64) {
23 let syscall: extern "C" fn(*const u8, u64) = unsafe { core::mem::transmute(544561597u64) }; // murmur32 hash of "sol_log_"
24 syscall(message, length)
25 }
26
27 pub(crate) fn sol_memcpy_(dest: *mut u8, src: *const u8, n: u64) {
28 let syscall: extern "C" fn(*mut u8, *const u8, u64) =
29 unsafe { core::mem::transmute(1904002211u64) }; // murmur32 hash of "sol_memcpy_"
30 syscall(dest, src, n)
31 }
32
33 pub(crate) fn sol_memset_(s: *mut u8, c: u8, n: u64) {
34 let syscall: extern "C" fn(*mut u8, u8, u64) =
35 unsafe { core::mem::transmute(930151202u64) }; // murmur32 hash of "sol_memset_"
36 syscall(s, c, n)
37 }
38
39 pub(crate) fn sol_remaining_compute_units() -> u64 {
40 let syscall: extern "C" fn() -> u64 = unsafe { core::mem::transmute(3991886574u64) }; // murmur32 hash of "sol_remaining_compute_units"
41 syscall()
42 }
43}
44
45#[cfg(not(target_os = "solana"))]
46extern crate std;
47
48/// Bytes for a truncated `str` log message.
49const TRUNCATED_SLICE: [u8; 3] = [b'.', b'.', b'.'];
50
51/// Byte representing a truncated log.
52const TRUNCATED: u8 = b'@';
53
54/// An uninitialized byte.
55const UNINIT_BYTE: MaybeUninit<u8> = MaybeUninit::uninit();
56
57/// Logger to efficiently format log messages.
58///
59/// The logger is a fixed size buffer that can be used to format log messages
60/// before sending them to the log output. Any type that implements the `Log`
61/// trait can be appended to the logger.
62pub struct Logger<const BUFFER: usize> {
63 // Byte buffer to store the log message.
64 buffer: [MaybeUninit<u8>; BUFFER],
65
66 // Length of the log message.
67 len: usize,
68}
69
70impl<const BUFFER: usize> Default for Logger<BUFFER> {
71 #[inline]
72 fn default() -> Self {
73 Self {
74 buffer: [UNINIT_BYTE; BUFFER],
75 len: 0,
76 }
77 }
78}
79
80impl<const BUFFER: usize> Deref for Logger<BUFFER> {
81 type Target = [u8];
82
83 fn deref(&self) -> &Self::Target {
84 // SAFETY: the slice is created from the buffer up to the length
85 // of the message.
86 unsafe { from_raw_parts(self.buffer.as_ptr() as *const _, self.len) }
87 }
88}
89
90impl<const BUFFER: usize> Logger<BUFFER> {
91 /// Append a value to the logger.
92 #[inline(always)]
93 pub fn append<T: Log>(&mut self, value: T) -> &mut Self {
94 self.append_with_args(value, &[]);
95 self
96 }
97
98 /// Append a value to the logger with formatting arguments.
99 #[inline]
100 pub fn append_with_args<T: Log>(&mut self, value: T, args: &[Argument]) -> &mut Self {
101 if self.is_full() {
102 if BUFFER > 0 {
103 // SAFETY: the buffer is checked to be full.
104 unsafe {
105 let last = self.buffer.get_unchecked_mut(BUFFER - 1);
106 last.write(TRUNCATED);
107 }
108 }
109 } else {
110 self.len += value.write_with_args(&mut self.buffer[self.len..], args);
111
112 if self.len > BUFFER {
113 // Indicates that the buffer is full.
114 self.len = BUFFER;
115 // SAFETY: the buffer length is checked to greater than `BUFFER`.
116 unsafe {
117 let last = self.buffer.get_unchecked_mut(BUFFER - 1);
118 last.write(TRUNCATED);
119 }
120 }
121 }
122
123 self
124 }
125
126 /// Log the message in the buffer.
127 #[inline(always)]
128 pub fn log(&self) {
129 log_message(self);
130 }
131
132 /// Clear the message buffer.
133 #[inline(always)]
134 pub fn clear(&mut self) {
135 self.len = 0;
136 }
137
138 /// Check whether the log buffer is at the maximum length or not.
139 #[inline(always)]
140 pub fn is_full(&self) -> bool {
141 self.len == BUFFER
142 }
143
144 /// Get the remaining space in the log buffer.
145 #[inline(always)]
146 pub fn remaining(&self) -> usize {
147 BUFFER - self.len
148 }
149}
150
151/// Log a message.
152#[inline(always)]
153pub fn log_message(message: &[u8]) {
154 #[cfg(target_os = "solana")]
155 // SAFETY: the message is always a valid pointer to a slice of bytes
156 // and `sol_log_` is a syscall.
157 unsafe {
158 syscalls::sol_log_(message.as_ptr(), message.len() as u64);
159 }
160 #[cfg(not(target_os = "solana"))]
161 {
162 let message = core::str::from_utf8(message).unwrap();
163 std::println!("{}", message);
164 }
165}
166
167/// Remaining CUs.
168#[inline(always)]
169pub fn remaining_compute_units() -> u64 {
170 #[cfg(target_os = "solana")]
171 // SAFETY: `sol_remaining_compute_units` is a syscall that returns the remaining compute units.
172 unsafe {
173 syscalls::sol_remaining_compute_units()
174 }
175 #[cfg(not(target_os = "solana"))]
176 core::hint::black_box(0u64)
177}
178
179/// Formatting arguments.
180///
181/// Arguments can be used to specify additional formatting options for the log message.
182/// Note that types might not support all arguments.
183#[non_exhaustive]
184pub enum Argument {
185 /// Number of decimal places to display for numbers.
186 ///
187 /// This is only applicable for numeric types.
188 Precision(u8),
189
190 /// Truncate the output at the end when the specified maximum number of characters
191 /// is exceeded.
192 ///
193 /// This is only applicable for `str` types.
194 TruncateEnd(usize),
195
196 /// Truncate the output at the start when the specified maximum number of characters
197 /// is exceeded.
198 ///
199 /// This is only applicable for `str` types.
200 TruncateStart(usize),
201}
202
203/// Trait to specify the log behavior for a type.
204///
205/// # Safety
206///
207/// The implementation must ensure that the value returned by any of the methods correctly
208/// reflects the actual number of bytes written to the buffer. Returning a value greater
209/// than the number of bytes written to the buffer will result in undefined behavior, since
210/// it will lead to reading uninitialized memory from the buffer.
211pub unsafe trait Log {
212 #[inline(always)]
213 fn debug(&self, buffer: &mut [MaybeUninit<u8>]) -> usize {
214 self.debug_with_args(buffer, &[])
215 }
216
217 #[inline(always)]
218 fn debug_with_args(&self, buffer: &mut [MaybeUninit<u8>], args: &[Argument]) -> usize {
219 self.write_with_args(buffer, args)
220 }
221
222 #[inline(always)]
223 fn write(&self, buffer: &mut [MaybeUninit<u8>]) -> usize {
224 self.write_with_args(buffer, &[])
225 }
226
227 fn write_with_args(&self, buffer: &mut [MaybeUninit<u8>], parameters: &[Argument]) -> usize;
228}
229
230/// Implement the log trait for unsigned integer types.
231macro_rules! impl_log_for_unsigned_integer {
232 ( $type:tt ) => {
233 unsafe impl Log for $type {
234 #[inline]
235 fn write_with_args(&self, buffer: &mut [MaybeUninit<u8>], args: &[Argument]) -> usize {
236 // The maximum number of digits that the type can have.
237 const MAX_DIGITS: usize = $type::MAX.ilog10() as usize + 1;
238
239 if buffer.is_empty() {
240 return 0;
241 }
242
243 match *self {
244 // Handle zero as a special case.
245 0 => {
246 // SAFETY: the buffer is checked to be non-empty.
247 unsafe {
248 buffer.get_unchecked_mut(0).write(b'0');
249 }
250 1
251 }
252 mut value => {
253 let mut digits = [UNINIT_BYTE; MAX_DIGITS];
254 let mut offset = MAX_DIGITS;
255
256 while value > 0 {
257 let remainder = value % 10;
258 value /= 10;
259 offset -= 1;
260 // SAFETY: the offset is always within the bounds of the array since
261 // `offset` is initialized with the maximum number of digits that
262 // the type can have and decremented on each iteration; `remainder`
263 // is always less than 10.
264 unsafe {
265 digits
266 .get_unchecked_mut(offset)
267 .write(b'0' + remainder as u8);
268 }
269 }
270
271 let precision = if let Some(Argument::Precision(p)) = args
272 .iter()
273 .find(|arg| matches!(arg, Argument::Precision(_)))
274 {
275 *p as usize
276 } else {
277 0
278 };
279
280 let written = MAX_DIGITS - offset;
281 let length = buffer.len();
282
283 // Space required with the specified precision. We might need
284 // to add leading zeros and a decimal point, but this is only
285 // if the precision is greater than zero.
286 let required = match precision {
287 0 => written,
288 // decimal point
289 _precision if precision < written => written + 1,
290 // decimal point + one leading zero
291 _ => precision + 2,
292 };
293 // Determines whether the value will be truncated or not.
294 let is_truncated = required > length;
295 // Cap the number of digits to write to the buffer length.
296 let digits_to_write = min(MAX_DIGITS - offset, length);
297
298 // SAFETY: the length of both `digits` and `buffer` arrays are guaranteed
299 // to be within bounds and the `digits_to_write` value is capped to the
300 // length of the `buffer`.
301 unsafe {
302 let source = digits.as_ptr().add(offset);
303 let ptr = buffer.as_mut_ptr();
304
305 // Copy the number to the buffer if no precision is specified.
306 if precision == 0 {
307 #[cfg(target_os = "solana")]
308 syscalls::sol_memcpy_(
309 ptr as *mut _,
310 source as *const _,
311 digits_to_write as u64,
312 );
313 #[cfg(not(target_os = "solana"))]
314 copy_nonoverlapping(source, ptr, digits_to_write);
315 }
316 // If padding is needed to satisfy the precision, add leading zeros
317 // and a decimal point.
318 else if precision >= digits_to_write {
319 // Prefix.
320 (ptr as *mut u8).write(b'0');
321
322 if length > 2 {
323 (ptr.add(1) as *mut u8).write(b'.');
324 let padding = min(length - 2, precision - digits_to_write);
325
326 // Precision padding.
327 #[cfg(target_os = "solana")]
328 syscalls::sol_memset_(
329 ptr.add(2) as *mut _,
330 b'0',
331 padding as u64,
332 );
333 #[cfg(not(target_os = "solana"))]
334 (ptr.add(2) as *mut u8).write_bytes(b'0', padding);
335
336 let current = 2 + padding;
337
338 // If there is still space, copy (part of) the number.
339 if current < length {
340 let remaining = min(digits_to_write, length - current);
341
342 // Number part.
343 #[cfg(target_os = "solana")]
344 syscalls::sol_memcpy_(
345 ptr.add(current) as *mut _,
346 source as *const _,
347 remaining as u64,
348 );
349 #[cfg(not(target_os = "solana"))]
350 copy_nonoverlapping(source, ptr.add(current), remaining);
351 }
352 }
353 }
354 // No padding is needed, calculate the integer and fractional
355 // parts and add a decimal point.
356 else {
357 let integer_part = digits_to_write - precision;
358
359 // Integer part of the number.
360 #[cfg(target_os = "solana")]
361 syscalls::sol_memcpy_(
362 ptr as *mut _,
363 source as *const _,
364 integer_part as u64,
365 );
366 #[cfg(not(target_os = "solana"))]
367 copy_nonoverlapping(source, ptr, integer_part);
368
369 // Decimal point.
370 (ptr.add(integer_part) as *mut u8).write(b'.');
371 let current = integer_part + 1;
372
373 // If there is still space, copy (part of) the remaining.
374 if current < length {
375 let remaining = min(precision, length - current);
376
377 // Fractional part of the number.
378 #[cfg(target_os = "solana")]
379 syscalls::sol_memcpy_(
380 ptr.add(current) as *mut _,
381 source.add(integer_part) as *const _,
382 remaining as u64,
383 );
384 #[cfg(not(target_os = "solana"))]
385 copy_nonoverlapping(
386 source.add(integer_part),
387 ptr.add(current),
388 remaining,
389 );
390 }
391 }
392 }
393
394 let written = min(required, length);
395
396 // There might not have been space.
397 if is_truncated {
398 // SAFETY: `written` is capped to the length of the buffer and
399 // the required length (`required` is always greater than zero);
400 // `buffer` is guaranteed to have a length of at least 1.
401 unsafe {
402 buffer.get_unchecked_mut(written - 1).write(TRUNCATED);
403 }
404 }
405
406 written
407 }
408 }
409 }
410 }
411 };
412}
413
414// Supported unsigned integer types.
415impl_log_for_unsigned_integer!(u8);
416impl_log_for_unsigned_integer!(u16);
417impl_log_for_unsigned_integer!(u32);
418impl_log_for_unsigned_integer!(u64);
419impl_log_for_unsigned_integer!(u128);
420impl_log_for_unsigned_integer!(usize);
421
422/// Implement the log trait for the signed integer types.
423macro_rules! impl_log_for_signed {
424 ( $type:tt ) => {
425 unsafe impl Log for $type {
426 #[inline]
427 fn write_with_args(&self, buffer: &mut [MaybeUninit<u8>], args: &[Argument]) -> usize {
428 if buffer.is_empty() {
429 return 0;
430 }
431
432 match *self {
433 // Handle zero as a special case.
434 0 => {
435 // SAFETY: the buffer is checked to be non-empty.
436 unsafe {
437 buffer.get_unchecked_mut(0).write(b'0');
438 }
439 1
440 }
441 value => {
442 let mut prefix = 0;
443
444 if *self < 0 {
445 if buffer.len() == 1 {
446 // SAFETY: the buffer is checked to be non-empty.
447 unsafe {
448 buffer.get_unchecked_mut(0).write(TRUNCATED);
449 }
450 // There is no space for the number, so just return.
451 return 1;
452 }
453
454 // SAFETY: the buffer is checked to be non-empty.
455 unsafe {
456 buffer.get_unchecked_mut(0).write(b'-');
457 }
458 prefix += 1;
459 };
460
461 prefix
462 + $type::unsigned_abs(value)
463 .write_with_args(&mut buffer[prefix..], args)
464 }
465 }
466 }
467 }
468 };
469}
470
471// Supported signed integer types.
472impl_log_for_signed!(i8);
473impl_log_for_signed!(i16);
474impl_log_for_signed!(i32);
475impl_log_for_signed!(i64);
476impl_log_for_signed!(i128);
477impl_log_for_signed!(isize);
478
479/// Implement the log trait for the `&str` type.
480unsafe impl Log for &str {
481 #[inline]
482 fn debug_with_args(&self, buffer: &mut [MaybeUninit<u8>], _args: &[Argument]) -> usize {
483 if buffer.is_empty() {
484 return 0;
485 }
486 // SAFETY: the buffer is checked to be non-empty.
487 unsafe {
488 buffer.get_unchecked_mut(0).write(b'"');
489 }
490
491 let mut offset = 1;
492 offset += self.write(&mut buffer[offset..]);
493
494 match buffer.len() - offset {
495 0 => {
496 // SAFETY: the buffer is guaranteed to be within `offset` bounds.
497 unsafe {
498 buffer.get_unchecked_mut(offset - 1).write(TRUNCATED);
499 }
500 }
501 _ => {
502 // SAFETY: the buffer is guaranteed to be within `offset` bounds.
503 unsafe {
504 buffer.get_unchecked_mut(offset).write(b'"');
505 }
506 offset += 1;
507 }
508 }
509
510 offset
511 }
512
513 #[inline]
514 fn write_with_args(&self, buffer: &mut [MaybeUninit<u8>], args: &[Argument]) -> usize {
515 // There are 4 different cases to consider:
516 //
517 // 1. No arguments were provided, so the entire string is copied to the buffer if it fits;
518 // otherwise, the buffer is filled as many characters as possible and the last character
519 // is set to `TRUNCATED`.
520 //
521 // Then cases only applicable when precision formatting is used:
522 //
523 // 2. The buffer is large enough to hold the entire string: the string is copied to the
524 // buffer and the length of the string is returned.
525 //
526 // 3. The buffer is smaller than the string, but large enough to hold the prefix and part
527 // of the string: the prefix and part of the string are copied to the buffer. The length
528 // returned is `prefix` + number of characters copied.
529 //
530 // 4. The buffer is smaller than the string and the prefix: the buffer is filled with the
531 // prefix and the last character is set to `TRUNCATED`. The length returned is the length
532 // of the buffer.
533 //
534 // The length of the message is determined by whether a precision formatting was used or
535 // not, and the length of the buffer.
536
537 let (size, truncate_end) = match args
538 .iter()
539 .find(|arg| matches!(arg, Argument::TruncateEnd(_) | Argument::TruncateStart(_)))
540 {
541 Some(Argument::TruncateEnd(size)) => (*size, Some(true)),
542 Some(Argument::TruncateStart(size)) => (*size, Some(false)),
543 _ => (buffer.len(), None),
544 };
545
546 // Handles the write of the `str` to the buffer.
547 //
548 // - `destination`: pointer to the buffer where the string will be copied. This is always
549 // the a pointer to the log buffer, but it could de in a different offset depending on
550 // whether the truncated slice is copied or not.
551 //
552 // - `source`: pointer to the string that will be copied. This could either be a pointer
553 // to the `str` itself or `TRUNCATE_SLICE`).
554 //
555 // - `length_to_write`: number of characters from `source` that will be copied.
556 //
557 // - `written_truncated_slice_length`: number of characters copied from `TRUNCATED_SLICE`.
558 // This is used to determine the total number of characters copied to the buffer.
559 //
560 // - `truncated`: indicates whether the `str` was truncated or not. This is used to set
561 // the last character of the buffer to `TRUNCATED`.
562 let (destination, source, length_to_write, written_truncated_slice_length, truncated) =
563 // No truncate arguments were provided, so the entire `str` is copied to the buffer
564 // if it fits; otherwise indicates that the `str` was truncated.
565 if truncate_end.is_none() {
566 let length = min(size, self.len());
567 (
568 buffer.as_mut_ptr(),
569 self.as_ptr(),
570 length,
571 0,
572 length != self.len(),
573 )
574 } else {
575 let max_length = min(size, buffer.len());
576 let ptr = buffer.as_mut_ptr();
577
578 // The buffer is large enough to hold the entire `str`, so no need to use the
579 // truncate args.
580 if max_length >= self.len() {
581 (ptr, self.as_ptr(), self.len(), 0, false)
582 }
583 // The buffer is large enough to hold the truncated slice and part of the string.
584 // In this case, the characters from the start or end of the string are copied to
585 // the buffer together with the `TRUNCATED_SLICE`.
586 else if max_length > TRUNCATED_SLICE.len() {
587 // Number of characters that can be copied to the buffer.
588 let length = max_length - TRUNCATED_SLICE.len();
589 // SAFETY: the `ptr` is always within `length` bounds.
590 unsafe {
591 let (offset, source, destination) = if truncate_end == Some(true) {
592 (length, self.as_ptr(), ptr)
593 } else {
594 (
595 0,
596 self.as_ptr().add(self.len() - length),
597 ptr.add(TRUNCATED_SLICE.len()),
598 )
599 };
600 // Copy the truncated slice to the buffer.
601 copy_nonoverlapping(
602 TRUNCATED_SLICE.as_ptr(),
603 ptr.add(offset) as *mut _,
604 TRUNCATED_SLICE.len(),
605 );
606
607 (destination, source, length, TRUNCATED_SLICE.len(), false)
608 }
609 }
610 // The buffer is smaller than the `PREFIX`: the buffer is filled with the `PREFIX`
611 // and the last character is set to `TRUNCATED`.
612 else {
613 (ptr, TRUNCATED_SLICE.as_ptr(), max_length, 0, true)
614 }
615 };
616
617 if length_to_write > 0 {
618 // SAFETY: the `destination` is always within `length_to_write` bounds.
619 unsafe {
620 #[cfg(target_os = "solana")]
621 syscalls::sol_memcpy_(
622 destination as *mut _,
623 source as *const _,
624 length_to_write as u64,
625 );
626 #[cfg(not(target_os = "solana"))]
627 copy_nonoverlapping(source, destination as *mut _, length_to_write);
628 }
629
630 // There might not have been space for all the value.
631 if truncated {
632 // SAFETY: the `destination` is always within `length_to_write` bounds.
633 unsafe {
634 let last = buffer.get_unchecked_mut(length_to_write - 1);
635 last.write(TRUNCATED);
636 }
637 }
638 }
639
640 written_truncated_slice_length + length_to_write
641 }
642}
643
644/// Implement the log trait for the slice type.
645macro_rules! impl_log_for_slice {
646 ( [$type:ident] ) => {
647 unsafe impl<$type> Log for &[$type]
648 where
649 $type: Log
650 {
651 impl_log_for_slice!(@generate_write);
652 }
653 };
654 ( [$type:ident; $size:ident] ) => {
655 unsafe impl<$type, const $size: usize> Log for &[$type; $size]
656 where
657 $type: Log
658 {
659 impl_log_for_slice!(@generate_write);
660 }
661 };
662 ( @generate_write ) => {
663 #[inline]
664 fn write_with_args(&self, buffer: &mut [MaybeUninit<u8>], _args: &[Argument]) -> usize {
665 if buffer.is_empty() {
666 return 0;
667 }
668
669 // Size of the buffer.
670 let length = buffer.len();
671 // SAFETY: the buffer is checked to be non-empty.
672 unsafe {
673 buffer.get_unchecked_mut(0).write(b'[');
674 }
675
676 let mut offset = 1;
677
678 for value in self.iter() {
679 if offset >= length {
680 // SAFETY: the buffer is checked to be non-empty and the `length`
681 // represents the buffer length.
682 unsafe {
683 buffer.get_unchecked_mut(length - 1).write(TRUNCATED);
684 }
685 offset = length;
686 break;
687 }
688
689 if offset > 1 {
690 if offset + 2 >= length {
691 // SAFETY: the buffer is checked to be non-empty and the `length`
692 // represents the buffer length.
693 unsafe {
694 buffer.get_unchecked_mut(length - 1).write(TRUNCATED);
695 }
696 offset = length;
697 break;
698 } else {
699 // SAFETY: the buffer is checked to be non-empty and the `offset`
700 // is smaller than the buffer length.
701 unsafe {
702 buffer.get_unchecked_mut(offset).write(b',');
703 buffer.get_unchecked_mut(offset + 1).write(b' ');
704 }
705 offset += 2;
706 }
707 }
708
709 offset += value.debug(&mut buffer[offset..]);
710 }
711
712 if offset < length {
713 // SAFETY: the buffer is checked to be non-empty and the `offset`
714 // is smaller than the buffer length.
715 unsafe {
716 buffer.get_unchecked_mut(offset).write(b']');
717 }
718 offset += 1;
719 }
720
721 offset
722 }
723 };
724}
725
726// Supported slice types.
727impl_log_for_slice!([T]);
728impl_log_for_slice!([T; N]);
729
730/// Implement the log trait for the `bool` type.
731unsafe impl Log for bool {
732 #[inline]
733 fn debug_with_args(&self, buffer: &mut [MaybeUninit<u8>], args: &[Argument]) -> usize {
734 let value = if *self { "true" } else { "false" };
735 value.debug_with_args(buffer, args)
736 }
737
738 #[inline]
739 fn write_with_args(&self, buffer: &mut [MaybeUninit<u8>], args: &[Argument]) -> usize {
740 let value = if *self { "true" } else { "false" };
741 value.write_with_args(buffer, args)
742 }
743}