include_cstr/
lib.rs

1//! A macro for getting `&'static CStr` from a file.
2//!
3//! This macro checks whether the content of the given file is valid for `CStr`
4//! at compile time, and returns a static reference of `CStr`.
5//!
6//! This macro can be used to to initialize constants on Rust 1.46 and above.
7//!
8//! It currently requires nightly compiler for [`proc_macro_span`][proc_macro_span] feature
9//! for resolving relative path to the file,
10//! so that it can be used in a similar way as `include_str!` and `include_bytes!` macro.
11//!
12//! [proc_macro_span]: https://doc.rust-lang.org/unstable-book/library-features/proc-macro-span.html
13//!
14//! ## Example
15//!
16//! ```rust
17//! use include_cstr::include_cstr;
18//! use std::ffi::CStr;
19//!
20//! let example = include_cstr!("example.txt");
21//! assert_eq!(example, CStr::from_bytes_with_nul(b"content in example.txt\0").unwrap());
22//! ```
23
24#![feature(proc_macro_span)]
25
26use crate::parse::parse_input;
27use proc_macro::{Span, TokenStream};
28use quote::{quote, quote_spanned};
29use std::borrow::Cow;
30use std::ffi::CString;
31use std::fs;
32
33mod parse;
34
35struct Error(Span, Cow<'static, str>);
36
37#[proc_macro]
38pub fn include_cstr(input: TokenStream) -> TokenStream {
39    let tokens = match check_file(input) {
40        Ok((path, bytes)) => {
41            // Use `include_bytes!()` to ensure that the source file using this macro gets
42            // re-compiled when the content of the included file is changed.
43            // We can't use `&*ptr` to convert the raw pointer to reference, because as of Rust
44            // 1.46, dereferencing raw pointer in constants is unstable. This is being tracked in
45            // https://github.com/rust-lang/rust/issues/51911
46            // So we explicitly disable the clippy lint for this expression.
47            quote!({
48                const _: &[u8] = include_bytes!(#path);
49                unsafe{
50                    #[allow(clippy::transmute_ptr_to_ref)]
51                    ::std::mem::transmute::<_, &::std::ffi::CStr>(
52                        &[#(#bytes),*] as *const [u8] as *const ::std::ffi::CStr
53                    )
54                }
55            })
56        }
57        Err(Error(span, msg)) => {
58            let span = span.into();
59            quote_spanned!(span => compile_error!(#msg))
60        }
61    };
62    tokens.into()
63}
64
65fn check_file(input: TokenStream) -> Result<(String, Vec<u8>), Error> {
66    let (path, literal) = parse_input(input)?;
67    let span = literal.span();
68    // Safety: the path comes from a valid str literal input from rustc, so it should be a
69    // valid UTF-8 string.
70    let path = unsafe { String::from_utf8_unchecked(path) };
71    let full_path = span.source_file().path().parent().unwrap().join(&path);
72    let content = fs::read(&full_path).map_err(|e| Error(span, format!("{}", e).into()))?;
73    let content =
74        CString::new(content).map_err(|_| Error(span, "nul byte found in the file".into()))?;
75    Ok((path, content.into_bytes_with_nul()))
76}