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