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 111 112 113 114 115 116 117 118 119 120
//! Prototype proc-macro crate for parsing a DTrace provider definition into Rust code.
// Copyright 2021 Oxide Computer Company
use std::iter::FromIterator;
use std::{fs, path::Path};
use quote::quote;
use syn::{parse_macro_input, Lit};
use usdt_impl::compile_provider_source;
/// Generate DTrace probe macros from a provider definition file.
///
/// This macro parses a DTrace provider.d file, given as a single literal string path. It then
/// generates a Rust macro for each of the DTrace probe definitions.
///
/// For example, let's say the file `"test.d"` has the following contents:
///
/// ```ignore
/// provider test {
/// probe start(uint8_t);
/// probe stop(char *, uint8_t);
/// };
/// ```
///
/// In a Rust library or application, write:
///
/// ```ignore
/// dtrace_provider!("test.d");
/// ```
///
/// The macro looks for the file relative to the root of the package, so `"test.d"`
/// in this case would be in the same directory as `"Cargo.toml"`.
///
/// By default probe macros are named `{provider}_{probe}!`. Arguments are passed
/// via a closure that returns a tuple. Note that the provided closure is only
/// evaluated when the probe is enabled. One can then add points of instrumentation
/// by invoking the macro:
///
/// ```ignore
/// fn do_stuff(count: u8, name: String) {
/// // doing stuff
/// test_stop!(|| (name, count));
/// }
/// ```
///
/// The probe macro names can be customized by adding `, format =
/// my_prefix_{provider}_{probe}` to the macro invocation where `{provider}` and
/// `{probe}` are optional and will be substituted with the actual provider and
/// probe names:
///
/// ```ignore
/// dtrace_provider!("test.d", format = "dtrace_{provider}_{probe}");
/// ```
///
/// Note
/// ----
/// The only supported types are integers of specific bit-width (e.g., `uint16_t`) and `char *`.
#[proc_macro]
pub fn dtrace_provider(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let mut tokens = item.into_iter().collect::<Vec<proc_macro::TokenTree>>();
let comma_index = tokens
.iter()
.enumerate()
.find_map(|(i, token)| match token {
proc_macro::TokenTree::Punct(p) if p.as_char() == ',' => Some(i),
_ => None,
});
// Split off the tokens after the comma if there is one.
let rest = if let Some(index) = comma_index {
let mut rest = tokens.split_off(index);
let _ = rest.remove(0);
rest
} else {
Vec::new()
};
// Parse the config from the remaining tokens.
let config: usdt_impl::CompileProvidersConfig = serde_tokenstream::from_tokenstream(
&proc_macro2::TokenStream::from(proc_macro::TokenStream::from_iter(rest)),
)
.unwrap();
let first_item = proc_macro::TokenStream::from_iter(tokens);
let tok = parse_macro_input!(first_item as Lit);
let filename = match tok {
Lit::Str(f) => f.value(),
_ => panic!("DTrace provider must be a single literal string filename"),
};
let source = if filename.ends_with(".d") {
let dir = std::env::var("CARGO_MANIFEST_DIR").map_or_else(
|_| std::env::current_dir().unwrap(),
|s| Path::new(&s).to_path_buf(),
);
let path = dir.join(&filename);
fs::read_to_string(path).expect(&format!(
"Could not read D source file \"{}\" in {:?}",
&filename, dir,
))
} else {
filename.clone()
};
match compile_provider_source(&source, &config) {
Ok(provider) => provider.into(),
Err(e) => {
let message = format!(
"Error building provider definition in \"{}\"\n\n{}",
filename,
e.to_string()
);
let out = quote! {
compile_error!(#message);
};
out.into()
}
}
}