Skip to main content

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}