no_panics/
lib.rs

1//! [![github]](https://github.com/dtolnay/no-panic) [![crates-io]](https://crates.io/crates/no-panic) [![docs-rs]](https://docs.rs/no-panic)
2//!
3//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
4//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
5//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
6//!
7//! <br>
8//!
9//! A Rust attribute macro to require that the compiler prove a function can't
10//! ever panic.
11//!
12//! ```toml
13//! [dependencies]
14//! no-panic = "0.1"
15//! ```
16//!
17//! ```
18//! use no_panic::no_panic;
19//!
20//! #[no_panic]
21//! fn demo(s: &str) -> &str {
22//!     &s[1..]
23//! }
24//!
25//! fn main() {
26//!     # fn demo(s: &str) -> &str {
27//!     #     &s[1..]
28//!     # }
29//!     #
30//!     println!("{}", demo("input string"));
31//! }
32//! ```
33//!
34//! If the function does panic (or the compiler fails to prove that the function
35//! cannot panic), the program fails to compile with a linker error that
36//! identifies the function name. Let's trigger that by passing a string that
37//! cannot be sliced at the first byte:
38//!
39//! ```should_panic
40//! # fn demo(s: &str) -> &str {
41//! #     &s[1..]
42//! # }
43//! #
44//! fn main() {
45//!     println!("{}", demo("\u{1f980}input string"));
46//! }
47//! ```
48//!
49//! ```console
50//!    Compiling no-panic-demo v0.0.1
51//! error: linking with `cc` failed: exit code: 1
52//!   |
53//!   = note: /no-panic-demo/target/release/deps/no_panic_demo-7170785b672ae322.no_p
54//! anic_demo1-cba7f4b666ccdbcbbf02b7348e5df1b2.rs.rcgu.o: In function `_$LT$no_pani
55//! c_demo..demo..__NoPanic$u20$as$u20$core..ops..drop..Drop$GT$::drop::h72f8f423002
56//! b8d9f':
57//!           no_panic_demo1-cba7f4b666ccdbcbbf02b7348e5df1b2.rs:(.text._ZN72_$LT$no
58//! _panic_demo..demo..__NoPanic$u20$as$u20$core..ops..drop..Drop$GT$4drop17h72f8f42
59//! 3002b8d9fE+0x2): undefined reference to `
60//!
61//!           ERROR[no-panic]: detected panic in function `demo`
62//!           '
63//!           collect2: error: ld returned 1 exit status
64//! ```
65//!
66//! The error is not stellar but notice the ERROR\[no-panic\] part at the end
67//! that provides the name of the offending function.
68//!
69//! *Compiler support: requires rustc 1.39+*
70//!
71//! <br>
72//!
73//! ## Caveats
74//!
75//! - Functions that require some amount of optimization to prove that they do
76//!   not panic may no longer compile in debug mode after being marked
77//!   `#[no_panic]`.
78//!
79//! - Panic detection happens at link time across the entire dependency graph,
80//!   so any Cargo commands that do not invoke a linker will not trigger panic
81//!   detection. This includes `cargo build` of library crates and `cargo check`
82//!   of binary and library crates.
83//!
84//! - The attribute is useless in code built with `panic = "abort"`.
85//!
86//! If you find that code requires optimization to pass `#[no_panic]`, either
87//! make no-panic an optional dependency that you only enable in release builds,
88//! or add a section like the following to Cargo.toml to enable very basic
89//! optimization in debug builds.
90//!
91//! ```toml
92//! [profile.dev]
93//! opt-level = 1
94//! ```
95//!
96//! If the code that you need to prove isn't panicking makes function calls to
97//! non-generic non-inline functions from a different crate, you may need thin
98//! LTO enabled for the linker to deduce those do not panic.
99//!
100//! ```toml
101//! [profile.release]
102//! lto = "thin"
103//! ```
104//!
105//! If you want no_panic to just assume that some function you call doesn't
106//! panic, and get Undefined Behavior if it does at runtime, see
107//! [dtolnay/no-panic#16]; try wrapping that call in an `unsafe extern "C"`
108//! wrapper.
109//!
110//! [dtolnay/no-panic#16]: https://github.com/dtolnay/no-panic/issues/16
111//!
112//! <br>
113//!
114//! ## Acknowledgments
115//!
116//! The linker error technique is based on [Kixunil]'s crate [`dont_panic`].
117//! Check out that crate for other convenient ways to require absence of panics.
118//!
119//! [Kixunil]: https://github.com/Kixunil
120//! [`dont_panic`]: https://github.com/Kixunil/dont_panic
121
122#![allow(clippy::doc_markdown, clippy::missing_panics_doc)]
123
124extern crate proc_macro;
125
126use proc_macro::TokenStream;
127use proc_macro2::{Span, TokenStream as TokenStream2};
128use quote::quote;
129use syn::parse::{Nothing, Result};
130use syn::{parse_quote, Attribute, FnArg, Ident, ItemFn, Pat, PatType, ReturnType};
131
132#[proc_macro_attribute]
133pub fn no_panic(args: TokenStream, input: TokenStream) -> TokenStream {
134    let args = TokenStream2::from(args);
135    let input = TokenStream2::from(input);
136    let expanded = match parse(args, input.clone()) {
137        Ok(function) => expand_no_panic(function),
138        Err(parse_error) => {
139            let compile_error = parse_error.to_compile_error();
140            quote!(#compile_error #input)
141        }
142    };
143    TokenStream::from(expanded)
144}
145
146fn parse(args: TokenStream2, input: TokenStream2) -> Result<ItemFn> {
147    let function: ItemFn = syn::parse2(input)?;
148    let _: Nothing = syn::parse2::<Nothing>(args)?;
149    Ok(function)
150}
151
152fn expand_no_panic(mut function: ItemFn) -> TokenStream2 {
153    let mut move_self = None;
154    let mut arg_pat = Vec::new();
155    let mut arg_val = Vec::new();
156    for (i, input) in function.sig.inputs.iter_mut().enumerate() {
157        let numbered = Ident::new(&format!("__arg{}", i), Span::call_site());
158        match input {
159            FnArg::Typed(PatType { pat, .. })
160                if match pat.as_ref() {
161                    Pat::Ident(pat) => pat.ident != "self",
162                    _ => true,
163                } =>
164            {
165                arg_pat.push(quote!(#pat));
166                arg_val.push(quote!(#numbered));
167                *pat = parse_quote!(mut #numbered);
168            }
169            FnArg::Typed(_) | FnArg::Receiver(_) => {
170                move_self = Some(quote! {
171                    if false {
172                        loop {}
173                        #[allow(unreachable_code)]
174                        {
175                            let __self = self;
176                        }
177                    }
178                });
179            }
180        }
181    }
182
183    let is_async = function.sig.asyncness.is_some();
184
185    let has_inline = function
186        .attrs
187        .iter()
188        .flat_map(Attribute::parse_meta)
189        .any(|meta| meta.path().is_ident("inline"));
190    if !has_inline {
191        function.attrs.push(parse_quote!(#[inline]));
192    }
193
194    // NOTE: this ret value is NOT USED for async
195    let ret = match &function.sig.output {
196        ReturnType::Default => quote!(-> ()),
197        output @ ReturnType::Type(..) => quote!(#output),
198    };
199    let stmts = function.block.stmts;
200    let message = format!(
201        "\n\nERROR[no-panic]: detected panic in function `{}`\n",
202        function.sig.ident,
203    );
204
205    let part1 = if is_async { quote!() } else { quote!(move) };
206    let part2 = if is_async {
207        quote!(async move)
208    } else {
209        quote!(#ret)
210    };
211
212    let __f_call = if is_async {
213        quote!(__f().await)
214    } else {
215        quote!(__f())
216    };
217
218    function.block = Box::new(parse_quote!({
219        struct __NoPanic;
220        extern "C" {
221            #[link_name = #message]
222            fn trigger() -> !;
223        }
224        impl core::ops::Drop for __NoPanic {
225            fn drop(&mut self) {
226                unsafe {
227                    trigger();
228                }
229            }
230        }
231        let __guard = __NoPanic;
232        let mut __f = #part1 || #part2 {
233            #move_self
234            #(
235                let #arg_pat = #arg_val;
236            )*
237            #(#stmts)*
238        };
239        let __result = #__f_call;
240        core::mem::forget(__guard);
241        __result
242    }));
243
244    quote!(#function)
245}