include_transformed/
lib.rs

1//! Macros for including transformed files at compile time.
2//!
3//! The macros include a file similar to [`include_bytes!`], but transform the
4//! data before inclusion. The transformation could be, for example, assembling
5//! assembly code to machine code ([`include_nasm_bin!`]).
6//!
7//! The original purpose of this project was including assembled application
8//! processor initialization code in [libhermit-rs] using [`include_nasm_bin!`].
9//!
10//! ```
11//! [dependencies]
12//! include-transformed = "0.2"
13//! ```
14//!
15//! # Requirements
16//!
17//! This project depends on the unstable features `proc_macro_span` [#54725] and
18//! `proc_macro_expand` [#90765], which are only available on the nightly
19//! toolchain channel.
20//!
21//! [libhermit-rs]: https://github.com/hermitcore/libhermit-rs
22//! [#54725]: https://github.com/rust-lang/rust/issues/54725
23//! [#90765]: https://github.com/rust-lang/rust/issues/90765
24
25#![feature(proc_macro_expand)]
26#![feature(proc_macro_span)]
27
28use std::{io::Read, path::Path, process::Command};
29
30use proc_macro::Literal;
31use quote::quote;
32use syn::{parse_macro_input, LitStr};
33use tempfile::NamedTempFile;
34
35/// Simluates inclusion.
36///
37/// This makes sure, the proc-macro is rerun if the included file has changed.
38fn simulate_inclusion(file: &LitStr) -> Result<(), proc_macro::ExpandError> {
39    let src = quote! {
40        include_bytes!(#file)
41    };
42    proc_macro::TokenStream::from(src).expand_expr().map(drop)
43}
44
45macro_rules! expand_macro_input {
46    ($tokenstream:ident) => {
47        match $tokenstream.expand_expr() {
48            Ok(tokenstream) => tokenstream,
49            Err(err) => {
50                let tokenstream = proc_macro2::TokenStream::from($tokenstream);
51                let error = syn::Error::new_spanned(tokenstream, err);
52                return proc_macro::TokenStream::from(error.into_compile_error());
53            }
54        }
55    };
56}
57
58fn include_transformed(
59    input: proc_macro::TokenStream,
60    transform: impl FnOnce(&Path, &Path) -> Result<(), proc_macro::TokenStream>,
61) -> proc_macro::TokenStream {
62    let input = expand_macro_input!(input);
63    let file = parse_macro_input!(input as LitStr);
64
65    simulate_inclusion(&file).unwrap();
66    let mut src = proc_macro::Span::call_site().source_file().path();
67    src.set_file_name(file.value());
68    let mut dst = NamedTempFile::new().unwrap();
69
70    if let Err(output) = transform(src.as_path(), dst.path()) {
71        return output;
72    }
73
74    let output = {
75        let mut buf = Vec::new();
76        dst.read_to_end(&mut buf).unwrap();
77        Literal::byte_string(&buf)
78    };
79    proc_macro::TokenStream::from(proc_macro::TokenTree::from(output))
80}
81
82macro_rules! bail {
83    ($($arg:tt)*) => {{
84        let msg = format!($($arg)*);
85        let compile_error = quote! {
86            compile_error!(#msg)
87        };
88        return Err(proc_macro::TokenStream::from(compile_error));
89    }};
90}
91
92fn run(command: &mut Command, program: &str) -> Result<(), proc_macro::TokenStream> {
93    let status = match command.status() {
94        Ok(status) => status,
95        Err(err) => bail!("Failed to execute {program}: {err}"),
96    };
97    if !status.success() {
98        bail!("{program} finished with: {status}");
99    }
100    Ok(())
101}
102
103/// Assembles a file with [NASM] into raw binary and includes the output as a
104/// reference to a byte array.
105///
106/// The file is located relative to the current file (similarly to how modules
107/// are found). This macro will yield an expression of type `&'static [u8; N]`
108/// which is the contents of the assembled file.
109///
110/// # Dependencies
111///
112/// This macro requires [NASM] to be installed.
113///
114/// # Examples
115///
116/// Assume there is a file 'boot.asm' in the same directory as 'main.rs'.
117///
118/// ```ignore
119/// let boot_code = include_nasm_bin!("boot.asm");
120/// ```
121///
122/// This code is equivalent to running `nasm -f bin boot.asm -o boot` and
123/// including `boot` via [`include_bytes!`].
124///
125/// [NASM]: https://www.nasm.us/
126#[proc_macro]
127pub fn include_nasm_bin(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
128    include_transformed(input, |src, dst| {
129        let program = "nasm";
130        let mut command = Command::new(program);
131        command.arg("-f").arg("bin").arg(src).arg("-o").arg(dst);
132        run(&mut command, program)
133    })
134}