This post is auto-generated from RSS feed
The Rust Programming Language Forum - Latest topics . Source:
Panic-free-friendly standard library
When working with a code which is supposed to be panic-free (that is with zero panic machinery linked into the final binary), I periodically encounter functionality from the standard (core) library which does bring this machinery. When examining it in depth it usually becomes clear that this is totally unnecessary and can be rewritten in a panic-free-friendly fashion. Most of the issues are coming and recurring from the core::fmt, here are few examples:
opened 04:25PM - 03 Feb 26 UTC
A-LLVM
P-medium
regression-from-stable-to-stable
A-LTO
S-has-mcve
C-optimization
S-has-bisection
### Code
<details>
<summary> Code (MCVE) </summary>
Build with
```bash
cargo r… ustc --release -- --emit=llvm-ir
```
This will likely fail on linking step, but will still generate the relevant LLVM-IR output. To pass the linking step a no_std target can be passed as in
```bash
cargo rustc --release --target=riscv32imc-unknown-none-elf -- --emit=llvm-ir
```
#### Cargo.toml
```toml
[package]
name = "test_disp"
version = "0.1.0"
edition = "2024"
[dependencies]
[profile.release]
panic = "abort"
lto = "fat"
opt-level = "z"
debug = true
codegen-units = 1
```
#### src/main.rs
```rust
#![no_std]
#![no_main]
use core::fmt::{Arguments, Write};
use core::ptr;
pub struct Writer {
cout: *mut char,
}
const WRITE_INST: Writer = Writer {
// Just for illustration
cout: 0x1234 as *mut char,
};
impl Write for Writer {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
for c in s.chars() {
unsafe { ptr::write_volatile(self.cout, c); }
}
Ok(())
}
}
macro_rules! print {
($($arg:tt)*) => ($crate::_print(format_args!($($arg)*)));
}
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
pub fn _print(args: Arguments) {
let _ = WRITE_INST.write_fmt(args);
}
#[unsafe(no_mangle)]
pub fn foo(x: usize) {
print!("{:#X?}", x);
}
#[unsafe(no_mangle)]
unsafe extern "C" fn _start() -> ! {
let mut x: usize = 0;
loop {
foo(x);
x = x+1;
}
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
```
</details>
### Version it worked on
This code compiles without generating any panicking machinery on v1.92.0 *and the nightly*, including toolchain built from `main` (79a1e77fe34b70de8b9b9e7c44ff251003b70aaf).
### Versions with regression
Stable 1.93.0 and beta :
`rustc +beta --version --verbose`:
```
rustc 1.94.0-beta.2 (23a44d3c7 2026-01-25)
binary: rustc
commit-hash: 23a44d3c70448c08dc6a2fc13c1afceab49f2bb9
commit-date: 2026-01-25
host: x86_64-unknown-linux-gnu
release: 1.94.0-beta.2
LLVM version: 21.1.8
```
`rustc +stable --version --verbose`:
```
rustc 1.93.0 (254b59607 2026-01-19)
binary: rustc
commit-hash: 254b59607d4417e9dffbc307138ae5c86280fe4c
commit-date: 2026-01-19
host: x86_64-unknown-linux-gnu
release: 1.93.0
LLVM version: 21.1.8
```
Resulting LLVM-IR
[beta.txt](https://github.com/user-attachments/files/25051351/beta.txt)
[nightly.txt](https://github.com/user-attachments/files/25051350/nightly.txt)
Note the portion in beta.txt which does not present in `nightly.txt`
```llvm-ir
.preheader: ; preds = %.preheader.preheader, %61
%24 = phi i64 [ %27, %61 ], [ 20, %.preheader.preheader ]
%25 = phi i64 [ %65, %61 ], [ %19, %.preheader.preheader ]
%26 = icmp ne i64 %24, 0, !dbg !191
tail call void @llvm.assume(i1 %26), !dbg !192
%27 = add nsw i64 %24, -4, !dbg !197
%28 = icmp ult i64 %27, 20, !dbg !183
br i1 %28, label %61, label %60, !dbg !183
//.........
60: ; preds = %.preheader
; call core::panicking::panic_bounds_check
tail call fastcc void @_ZN4core9panicking18panic_bounds_check17hb0c8fc4be015a51bE(i64 noundef -4) #13, !dbg !183
unreachable, !dbg !183
```
The offending code is *unchanged* between the versions:
https://github.com/rust-lang/rust/blob/79a1e77fe34b70de8b9b9e7c44ff251003b70aaf/library/core/src/fmt/num.rs#L198-L217
If replacing the `buf[offset + ..]` with `buf.get_unchecked_mut(offset + ...)` the issue goes away.
### Update:
Removing `lto="fat"` from `Cargo.toml` also changes this behavior.
opened 08:12PM - 24 Nov 25 UTC
A-macros
T-compiler
C-optimization
It looks like in some cases `#[derive(Debug)]` is implemented via `debug_struct_… fields_finish()` which contains the following assertion:
https://github.com/rust-lang/rust/blob/42ec52babac2cbf2bb2b9d794f980cbcb3ebe413/library/core/src/fmt/mod.rs#L2565
This breaks an otherwise panic-free code.
**Update with MCVE**
_Cargo.toml:_
```toml
[package]
name = "debug_struct"
version = "0.1.0"
edition = "2024"
[dependencies]
[profile.release]
lto = "thin"
debug = true
opt-level="z"
panic="abort"
```
_main.rs_
```rust
#![no_std]
#![no_main]
use core::fmt::Write;
#[repr(C)]
#[derive(Debug)]
struct Test {
a0: u32,
a1: u32,
a2: u32,
a3: u32,
a4: u32,
a5: [u32; 33],
}
impl Default for Test {
fn default() -> Self {
Self {
a5: [0; 33],
.. Default::default()
}
}
}
struct FakeWriter;
impl core::fmt::Write for FakeWriter {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
for c in s.bytes() {
unsafe {
core::ptr::write_volatile(0x1000_0000 as *mut u8, c);
}
}
Ok(())
}
}
#[unsafe(no_mangle)]
extern "C" fn _start() {
let test = Test::default();
let _ = writeln!(FakeWriter, "{:#X?}", test);
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe extern "C" {
#[link_name = "\n\nError: panic is possible on some code path\n\n"]
unsafe fn never_panic() -> !;
}
unsafe { never_panic() }
}
```
Build with command:
```bash
cargo build --release --target=riscv32imc-unknown-none-elf
```
Will fail with linking error due to the panic handler referencing undefined function (credits to the [no-panics-whatsoever](https://docs.rs/no-panics-whatsoever/latest/no_panics_whatsoever/) crate authors.
_Observations:_
1. Will build with `opt-level=3`
2. Will build with `Test::a5` shorter than 33 elements (curiously the same threshold as for `#[derive(Default)]`
3. Will build with number of fields 5 or less (likely because in that case `debug_struct_fields_finish()` is not being called, but `debug_struct_fields_finishX` is called instead
opened 06:51PM - 13 Jun 24 UTC
closed 10:42PM - 05 Nov 24 UTC
C-enhancement
T-libs
A-panic
When writing non-panicking code, it is impossible to use the `"{:X}"` format spe… cifier as the `impl core::fmt::UpperHex for usize` and similar code might panic. It seems that it is because the [GenericRadix::fmt_int](https://github.com/rust-lang/rust/blob/f1586001ace26df7cafeb6534eaf76fb2c5513e5/library/core/src/fmt/num.rs#L71) method is using slice indexing notation [here](https://github.com/rust-lang/rust/blob/f1586001ace26df7cafeb6534eaf76fb2c5513e5/library/core/src/fmt/num.rs#L105C18-L105C19):
let buf = &buf[curr..];
This seem to be easily avoidable by replacing it with `buf.get(curr..)` and some error handling.
Another one relating to async/await functionality (which can't be easily fixed because of how it is specified):
opened 05:09PM - 25 Sep 25 UTC
T-lang
A-async-await
C-discussion
It appears that `async fn() {...}` desugars into `Future` state machine with a p… ossible `panic!()` path. I understand that this path is taken if `poll()` is executed on a finished task. This is a problem when trying to write a panic-free code (that is with no panic-related code linked to the final artifact).
I was not able to write a minimal reproducing example, so I will put here the excerpts from my full code (`no_std`, attempting to implement custom executor):
```rust
pub async fn async_number() -> u32 {
42
}
pub async fn example_task() {
let num = async_number().await;
let _ = writeln!(dbg_print::DebugPrint, "Async number: {}", num);
}
```
This produces the following llvm-ir:
```llvm
; test_futures::example_task::{{closure}}
; Function Attrs: inlinehint minsize nounwind optsize
define internal noundef zeroext i1 @"_ZN12test_futures12example_task28_$u7b$$u7b$closure$u7d$$u7d$17h69ea2006b28af955E"(ptr nocapture noundef nonnull align 1 %_1, ptr noalias nocapture readnone align 4 %_2) unnamed_addr #4 !dbg !1446 {
start:
%_14 = alloca [0 x i8], align 1
%args = alloca [8 x i8], align 4
%_15 = alloca [24 x i8], align 4
%num = alloca [4 x i8], align 4
// ...................
%0 = load i8, ptr %_1, align 1, !dbg !1477, !range !1478, !noundef !17
switch i8 %0, label %bb7 [
i8 0, label %bb4.thread
i8 1, label %panic
i8 3, label %bb4
], !dbg !1477
// ........
panic.i: ; preds = %bb4
; call core::panicking::panic_const::panic_const_async_fn_resumed
tail call fastcc void @_ZN4core9panicking11panic_const28panic_const_async_fn_resumed17h3b19b0997d8ed2ccE() #12, !dbg !1505
unreachable, !dbg !1505
```
In the final disassembly the call to `panic_const_async_fn_resumed()` appears as well.
I am not sure what would be the right way to solve this. Perhaps introducing `Future::try_poll()` ? Or adding a "Finished" variant to the `Poll` enum.
**UPDATE:**
I believe the code responsible for generating the panic block is this:
https://github.com/rust-lang/rust/blob/a8858111044a9391ac7558f969d3bf62ef43222d/compiler/rustc_mir_transform/src/coroutine.rs#L1254-L1264
It is also documented:
https://github.com/rust-lang/rust/blob/a8858111044a9391ac7558f969d3bf62ef43222d/compiler/rustc_mir_transform/src/coroutine.rs#L43-L46
However it does not change the fact that `async/await` is not panic-free-friendly.
Now, I am not asking how to fix these specific issues, I wonder how we can change the approach the library functionality is written in a way that allows the downstream developer to decide how to handle their errors? On which level these guidelines are usually documented and how these can be influenced?
1 post - 1 participant
Read full topic
🏷️ Rust_feed