diff --git a/Cargo.lock b/Cargo.lock index ec554e9fb2b..3bf0d1cee5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1234,6 +1234,26 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aca749d3d3f5b87a0d6100509879f9cf486ab510803a4a4e1001da1ff61c2bd6" +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -4643,7 +4663,9 @@ dependencies = [ name = "gstd" version = "1.0.5" dependencies = [ + "arrayvec 0.7.4", "bs58 0.5.0", + "const_format", "document-features", "futures", "galloc", diff --git a/Cargo.toml b/Cargo.toml index 27500f13397..ab808732da9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -185,7 +185,7 @@ gcli = { path = "gcli" } gclient = { path = "gclient" } gsdk = { path = "gsdk" } gsdk-codegen = { path = "gsdk/codegen" } -gstd = { path = "gstd", features = [ "nightly" ] } +gstd = { path = "gstd", features = ["nightly"] } gstd-codegen = { path = "gstd/codegen" } gsys = { path = "gsys" } gtest = { path = "gtest" } diff --git a/core-backend/src/error.rs b/core-backend/src/error.rs index b4fd2e926b7..137f6dc4cd0 100644 --- a/core-backend/src/error.rs +++ b/core-backend/src/error.rs @@ -225,7 +225,7 @@ pub enum TrapExplanation { ProgramAllocOutOfBounds, #[display(fmt = "Syscall unrecoverable error: {_0}")] UnrecoverableExt(UnrecoverableExtError), - #[display(fmt = "{_0}")] + #[display(fmt = "Panic occurred: {_0}")] Panic(LimitedStr<'static>), #[display(fmt = "Stack limit exceeded")] StackLimitExceeded, diff --git a/examples/waiter/tests/utils.rs b/examples/waiter/tests/utils.rs index 108f36fa700..ba695ea6505 100644 --- a/examples/waiter/tests/utils.rs +++ b/examples/waiter/tests/utils.rs @@ -13,10 +13,6 @@ pub fn assert_panicked(result: &RunResult, panic_msg: &str) { ))) )); let payload = String::from_utf8(result.log()[0].payload().into()) - .expect("Unable to decode panic message") - .split(',') - .map(String::from) - .next() - .expect("Unable to split panic message"); - assert_eq!(payload, format!("'{}'", panic_msg)); + .expect("Unable to decode panic message"); + assert!(payload.contains(&format!("panicked with '{panic_msg}'"))); } diff --git a/gstd/Cargo.toml b/gstd/Cargo.toml index 1c7fdf490fe..be7b500a68b 100644 --- a/gstd/Cargo.toml +++ b/gstd/Cargo.toml @@ -3,6 +3,8 @@ name = "gstd" description = "Gear programs standard library" keywords = ["gear", "std", "no-std", "wasm", "smart-contracts"] categories = ["api-bindings"] +# Stable version of Rust >=1.73 is required due to new format of panic message. +rust-version = "1.73" version.workspace = true edition.workspace = true authors.workspace = true @@ -11,6 +13,8 @@ homepage.workspace = true repository.workspace = true [dependencies] +arrayvec = { version = "0.7.4", default-features = false, optional = true } +const_format = { version = "0.2.32", optional = true } document-features = { version = "0.2.8", optional = true } galloc.workspace = true gcore = { workspace = true, features = ["codec"] } @@ -27,18 +31,45 @@ futures = { workspace = true, features = ["alloc"] } [features] #! ## Default features -default = ["panic-handler"] -## When enabled, a panic handler is provided by this crate. -panic-handler = [] +default = ["panic-message"] + +#! ## Panic handler profiles +#! We currently use the following format for panic messages from Rust code: +#! `panicked with '{message}'[ at '{location}']`. Also `Panic occurred: ` +#! will be added to the beginning of the panic message by our core-backend. +#! +#! So the final panic message looks like this: +#! `Panic occurred: panicked with '{message}'[ at '{location}']`. +#! +#! You can configure which panic handler profile you need +#! by specifying one of the following functions: + +## When enabled, a minimal panic handler is provided by this crate. +## Instead of a panic message, `` is displayed. +panic-handler = ["const_format"] +## When enabled, a panic handler will also display a panic message. +panic-message = ["panic-handler", "arrayvec"] +## When enabled, a panic handler will also display a panic message and location. +## This function is not recommended for use in production environment +## because it displays the code path. +panic-location = ["panic-message"] #! ## Nightly features +#! +#! The `panic-message` and `panic-location` features gets additional +#! optimizations when using the nightly compiler. +#! +#! For example, if you don't use the `panic-location` feature, the compiler +#! will remove all locations such as `/home/username/dapp/src/lib.rs:1:2` +#! from the binary. The size of smart contract will be reduced and +#! `/home/username/...` information will not be included in the binary. ## Enables all features below. ## These features depend on unstable Rust API and require nightly toolchain. -nightly = ["panic-messages", "oom-handler"] -## When enabled, additional context information is available from -## panic messages in debug mode. Relies on [`panic_info_message`][rust-66745]. -panic-messages = [] +nightly = ["panic-info-message", "oom-handler"] +## When enabled, a panic handler will use a more efficient implementation. +## Relies on [`panic_info_message`][rust-66745]. +panic-info-message = [] ## When enabled, an OOM error handler is provided. ## Relies on [`alloc_error_handler`][rust-51540], oom-handler = [] diff --git a/gstd/src/common/handlers.rs b/gstd/src/common/handlers.rs index 071bb35947c..04b0bf0463a 100644 --- a/gstd/src/common/handlers.rs +++ b/gstd/src/common/handlers.rs @@ -25,50 +25,205 @@ //! debug and non-debug mode, for programs built in `wasm32` architecture. //! For `debug` mode it provides more extensive logging. +#[cfg(target_arch = "wasm32")] #[cfg(feature = "oom-handler")] #[alloc_error_handler] pub fn oom(_: core::alloc::Layout) -> ! { crate::ext::oom_panic() } - +/// We currently support 3 panic handler profiles: +/// - `panic-handler`: it displays `panicked with ''` +/// - `panic-message`: it displays `panicked with '{message}'` +/// - `panic-location`: it displays `panicked with '{message}' at '{location}'` +/// +/// How we get the panic message in different versions of Rust: +/// - In nightly Rust, we use `#![feature(panic_info_message)]` and the +/// [`write!`] macro. +/// - In stable Rust, we need to modify the default panic handler message +/// format. +/// +/// Default panic handler message format (according to ): +/// `panicked at {location}:\n{message}` +/// +/// We parse the output of `impl Display for PanicInfo<'_>` and +/// then convert it to custom format: +/// `panicked with '{message}'[ at '{location}']`. +#[cfg(target_arch = "wasm32")] #[cfg(feature = "panic-handler")] mod panic_handler { use crate::ext; use core::panic::PanicInfo; - #[cfg(not(feature = "debug"))] + mod constants { + /// This prefix is used before the panic message. + pub const PANIC_PREFIX: &str = "panicked with "; + /// This panic message is used in the minimal panic handler and when + /// internal errors occur. + #[cfg(any(feature = "panic-info-message", not(feature = "panic-message")))] + pub const UNKNOWN_REASON: &str = ""; + + /// This prefix is used by `impl Display for PanicInfo<'_>`. + #[cfg(all(not(feature = "panic-info-message"), feature = "panic-message"))] + pub const PANICKED_AT: &str = "panicked at "; + + /// Max amount of bytes allowed to be thrown as string explanation + /// of the error. + #[cfg(feature = "panic-message")] + pub const TRIMMED_MAX_LEN: usize = 1024; //TODO: do not duplicate + // `gear_core::str::TRIMMED_MAX_LEN` + } + + use constants::*; + + /// Minimal panic handler. + #[cfg(not(feature = "panic-message"))] #[panic_handler] pub fn panic(_: &PanicInfo) -> ! { - ext::panic("no info") + const MESSAGE: &str = const_format::formatcp!("{PANIC_PREFIX}'{UNKNOWN_REASON}'"); + + #[cfg(feature = "debug")] + let _ = ext::debug(MESSAGE); + + ext::panic(MESSAGE) } - #[cfg(feature = "debug")] + /// Panic handler for nightly Rust. + #[cfg(all(feature = "panic-info-message", feature = "panic-message"))] #[panic_handler] pub fn panic(panic_info: &PanicInfo) -> ! { - use crate::prelude::format; - #[cfg(not(feature = "panic-messages"))] - let message = None::<&core::fmt::Arguments<'_>>; - #[cfg(feature = "panic-messages")] - let message = panic_info.message(); - - let msg = match (message, panic_info.location()) { - (Some(msg), Some(loc)) => format!( - "'{:?}', {}:{}:{}", - msg, - loc.file(), - loc.line(), - loc.column() - ), - (Some(msg), None) => format!("'{msg:?}'"), - (None, Some(loc)) => { - format!("{}:{}:{}", loc.file(), loc.line(), loc.column()) + use crate::prelude::fmt::Write; + use arrayvec::ArrayString; + + let mut debug_msg = ArrayString::::new(); + let _ = debug_msg.try_push_str(PANIC_PREFIX); + + match (panic_info.message(), panic_info.location()) { + #[cfg(feature = "panic-location")] + (Some(msg), Some(loc)) => { + let _ = write!(&mut debug_msg, "'{msg}' at '{loc}'"); + } + #[cfg(not(feature = "panic-location"))] + (Some(msg), _) => { + let _ = write!(&mut debug_msg, "'{msg}'"); + } + _ => { + let _ = debug_msg.try_push_str(const_format::formatcp!("'{UNKNOWN_REASON}'")); } - _ => ext::panic("no info"), }; - crate::debug!("panic occurred: {msg}"); - ext::panic(&msg) + #[cfg(feature = "debug")] + let _ = ext::debug(&debug_msg); + + ext::panic(&debug_msg) + } + + /// Panic handler for stable Rust. + #[cfg(all(not(feature = "panic-info-message"), feature = "panic-message"))] + #[panic_handler] + pub fn panic(panic_info: &PanicInfo) -> ! { + use crate::prelude::fmt::{self, Write}; + use arrayvec::ArrayString; + + #[derive(Default)] + struct TempBuffer { + overflowed: bool, + buffer: ArrayString, + } + + impl TempBuffer { + #[inline] + fn write_str(&mut self, s: &str) { + if !self.overflowed && self.buffer.write_str(s).is_err() { + self.overflowed = true; + } + } + } + + #[derive(Default)] + struct TempOutput { + found_prefix: bool, + found_delimiter: bool, + #[cfg(feature = "panic-location")] + location: TempBuffer, + message: TempBuffer, + } + + impl fmt::Write for TempOutput { + fn write_str(&mut self, s: &str) -> fmt::Result { + if !self.found_prefix && s.len() == PANICKED_AT.len() { + self.found_prefix = true; + return Ok(()); + } + + if !self.found_delimiter { + if s == ":\n" { + self.found_delimiter = true; + return Ok(()); + } + #[cfg(feature = "panic-location")] + self.location.write_str(s); + } else { + self.message.write_str(s); + } + + Ok(()) + } + } + + let mut output = TempOutput::default(); + let _ = write!(&mut output, "{panic_info}"); + + #[cfg(feature = "panic-location")] + let location = &*output.location.buffer; + let message = &*output.message.buffer; + + let mut debug_msg = ArrayString::::new(); + let _ = debug_msg.try_push_str(PANIC_PREFIX); + + #[cfg(feature = "panic-location")] + for s in ["'", message, "' at '", location, "'"] { + if debug_msg.try_push_str(s).is_err() { + break; + } + } + + #[cfg(not(feature = "panic-location"))] + for s in ["'", message, "'"] { + if debug_msg.try_push_str(s).is_err() { + break; + } + } + + #[cfg(feature = "debug")] + let _ = ext::debug(&debug_msg); + + ext::panic(&debug_msg) + } +} + +#[cfg(test)] +mod tests { + extern crate std; + + use std::{format, panic, prelude::v1::*}; + + /// Here is a test to verify that the default panic handler message + /// format has not changed. + #[test] + fn panic_msg_format_not_changed() { + const MESSAGE: &str = "message"; + + panic::set_hook(Box::new(|panic_info| { + let location = panic_info.location().unwrap(); + assert_eq!( + panic_info.to_string(), + format!("panicked at {location}:\n{MESSAGE}") + ); + })); + + let result = panic::catch_unwind(|| { + panic!("{MESSAGE}"); + }); + assert!(result.is_err()); } } -#[cfg(feature = "panic-handler")] -pub use panic_handler::*; diff --git a/gstd/src/common/mod.rs b/gstd/src/common/mod.rs index a9e065da018..aad7044a550 100644 --- a/gstd/src/common/mod.rs +++ b/gstd/src/common/mod.rs @@ -19,6 +19,5 @@ //! Common modules for each Gear smart contract. pub mod errors; -#[cfg(target_arch = "wasm32")] mod handlers; pub mod primitives; diff --git a/gstd/src/lib.rs b/gstd/src/lib.rs index 74a408879bb..448ff67e71d 100644 --- a/gstd/src/lib.rs +++ b/gstd/src/lib.rs @@ -28,6 +28,10 @@ //! asynchronous programming primitives, arbitrary types encoding/decoding, //! providing convenient instruments for creating programs from programs, etc. //! +//! # Minimum supported Rust version +//! This crate requires **Rust >= 1.73** due to the implementation of the panic +//! handler in the stable version. +//! //! # Crate features #![cfg_attr( feature = "document-features", @@ -129,7 +133,11 @@ #![no_std] #![warn(missing_docs)] #![cfg_attr( - all(target_arch = "wasm32", feature = "panic-messages",), + all( + target_arch = "wasm32", + feature = "panic-info-message", + feature = "panic-message" + ), feature(panic_info_message) )] #![cfg_attr( diff --git a/gtest/src/program.rs b/gtest/src/program.rs index d25d0250cce..9e2e978df43 100644 --- a/gtest/src/program.rs +++ b/gtest/src/program.rs @@ -856,7 +856,9 @@ mod tests { assert!(run_result.main_failed()); let log = run_result.log(); - assert!(log[0].payload().starts_with(b"'Failed to load destination")); + let panic_msg_payload = + String::from_utf8(log[0].payload().into()).expect("Unable to decode panic message"); + assert!(panic_msg_payload.contains("panicked with 'Failed to load destination")); let run_result = prog.send(user_id, String::from("should_be_skipped")); diff --git a/pallets/gear/src/tests.rs b/pallets/gear/src/tests.rs index 0479247ef88..fcb7ce182ec 100644 --- a/pallets/gear/src/tests.rs +++ b/pallets/gear/src/tests.rs @@ -365,7 +365,7 @@ fn delayed_reservations_sending_validation() { run_to_next_block(None); let error_text = format!( - "{SENDING_EXPECT}: {:?}", + "panicked with '{SENDING_EXPECT}: {:?}'", GstdError::Core( ExtError::Message(MessageError::InsufficientGasForDelayedSending).into() ) @@ -402,7 +402,7 @@ fn delayed_reservations_sending_validation() { run_for_blocks(wait_for as u64 + 1, None); let error_text = format!( - "{SENDING_EXPECT}: {:?}", + "panicked with '{SENDING_EXPECT}: {:?}'", GstdError::Core( ExtError::Message(MessageError::InsufficientGasForDelayedSending).into() ) @@ -513,7 +513,7 @@ fn default_wait_lock_timeout() { run_to_block(expiration_block, None); let error_text = format!( - "ran into error-reply: {:?}", + "panicked with 'ran into error-reply: {:?}'", GstdError::Timeout( expiration_block.unique_saturated_into(), expiration_block.unique_saturated_into() @@ -7545,7 +7545,7 @@ fn test_create_program_with_value_lt_ed() { assert_total_dequeued(1); let error_text = format!( - "Failed to create program: {:?}", + "panicked with 'Failed to create program: {:?}'", GstdError::Core(ExtError::Message(MessageError::InsufficientValue).into()) ); @@ -7595,7 +7595,7 @@ fn test_create_program_with_exceeding_value() { assert_total_dequeued(1); let error_text = format!( - "Failed to create program: {:?}", + "panicked with 'Failed to create program: {:?}'", GstdError::Core(ExtError::Execution(ExecutionError::NotEnoughValue).into()) ); assert_failed( @@ -7681,7 +7681,7 @@ fn demo_constructor_works() { run_to_next_block(None); - let error_text = "I just panic every time".to_owned(); + let error_text = "panicked with 'I just panic every time'".to_owned(); assert_failed( message_id, @@ -8668,7 +8668,7 @@ fn mx_lock_ownership_exceedance() { let get_lock_ownership_exceeded_trap = |command_msg_id| { ActorExecutionErrorReplyReason::Trap(TrapExplanation::Panic( format!( - "Message 0x{} has exceeded lock ownership time", + "panicked with 'Message 0x{} has exceeded lock ownership time'", hex::encode(command_msg_id) ) .into(),