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}