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