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}