1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
//! Macros for including transformed files at compile time.
//!
//! The macros include a file similar to [`include_bytes!`], but transform the
//! data before inclusion (e.g. runs an assembler).
//!
//! The original purpose of this project was including assembled application
//! processor initialization code in [libhermit-rs] using [`include_nasm_bin!`].
//!
//! ```
//! [dependencies]
//! include-transformed = "0.1"
//! ```
//! 
//! # Requirements
//!
//! This project depends on the unstable features `proc_macro_span` [#54725] and
//! `proc_macro_expand` [#90765], which are only available on the nightly
//! toolchain channel.
//!
//! [libhermit-rs]: https://github.com/hermitcore/libhermit-rs
//! [#54725]: https://github.com/rust-lang/rust/issues/54725
//! [#90765]: https://github.com/rust-lang/rust/issues/90765

#![feature(proc_macro_expand)]
#![feature(proc_macro_span)]

use std::{io::Read, path::Path, process::Command};

use proc_macro::Literal;
use quote::quote;
use syn::{parse_macro_input, LitStr};
use tempfile::NamedTempFile;

/// Simluates inclusion.
///
/// This makes sure, the proc-macro is rerun if the included file has changed.
fn simulate_inclusion(file: &LitStr) -> Result<(), proc_macro::ExpandError> {
    let src = quote! {
        include_bytes!(#file)
    };
    let _ = proc_macro::TokenStream::from(src).expand_expr()?;
    Ok(())
}

macro_rules! expand_macro_input {
    ($tokenstream:ident) => {
        match $tokenstream.expand_expr() {
            Ok(tokenstream) => tokenstream,
            Err(err) => {
                let tokenstream = proc_macro2::TokenStream::from($tokenstream);
                let error = syn::Error::new_spanned(tokenstream, err);
                return proc_macro::TokenStream::from(error.into_compile_error());
            }
        }
    };
}

fn include_transformed(
    input: proc_macro::TokenStream,
    transform: impl FnOnce(&Path, &Path) -> Command,
) -> proc_macro::TokenStream {
    let input = expand_macro_input!(input);
    let file = parse_macro_input!(input as LitStr);

    simulate_inclusion(&file).unwrap();
    let mut src = proc_macro::Span::call_site().source_file().path();
    src.set_file_name(file.value());
    let mut dst = NamedTempFile::new().unwrap();

    let status = transform(src.as_path(), dst.path()).status().unwrap();
    assert!(status.success());

    let output = {
        let mut buf = Vec::new();
        dst.read_to_end(&mut buf).unwrap();
        Literal::byte_string(&buf)
    };
    proc_macro::TokenStream::from(proc_macro::TokenTree::from(output))
}

/// Assembles a file with [NASM] into raw binary and includes the output as a
/// reference to a byte array.
///
/// The file is located relative to the current file (similarly to how modules
/// are found). This macro will yield an expression of type `&'static [u8; N]`
/// which is the contents of the assembled file.
///
/// # Dependencies
///
/// This macro requires [NASM] to be installed.
///
/// # Examples
///
/// ```ignore
/// let boot_code = include_nasm_bin!("boot.asm");
/// ```
///
/// [NASM]: https://www.nasm.us/
#[proc_macro]
pub fn include_nasm_bin(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    include_transformed(input, |src_path, dst_path| {
        let mut nasm = Command::new("nasm");
        nasm.arg("-f")
            .arg("bin")
            .arg(src_path)
            .arg("-o")
            .arg(dst_path);
        nasm
    })
}