diff --git a/Cargo.lock b/Cargo.lock index dae8a8276..4119d0440 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -732,6 +732,7 @@ version = "0.0.1" dependencies = [ "cairo-lang-macro-attributes", "cairo-lang-macro-stable", + "linkme", ] [[package]] @@ -739,6 +740,7 @@ name = "cairo-lang-macro-attributes" version = "0.0.1" dependencies = [ "quote", + "scarb-stable-hash", "syn 2.0.52", ] @@ -3332,6 +3334,26 @@ dependencies = [ "redox_syscall 0.4.1", ] +[[package]] +name = "linkme" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2cfee0de9bd869589fb9a015e155946d1be5ff415cb844c2caccc6cc4b5db9" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf157a4dc5a29b7b464aa8fe7edeff30076e07e13646a1c3874f58477dc99f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "linux-raw-sys" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 592f3877f..c3ea32c83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,7 @@ io_tee = "0.1" itertools = "0.12" libc = "0.2" libloading = "0.8.3" +linkme = "0.3" log = "0.4" ntest = "0.9" num-bigint = { version = "0.4", features = ["rand"] } diff --git a/plugins/cairo-lang-macro-attributes/Cargo.toml b/plugins/cairo-lang-macro-attributes/Cargo.toml index cd5b4a640..0b313378e 100644 --- a/plugins/cairo-lang-macro-attributes/Cargo.toml +++ b/plugins/cairo-lang-macro-attributes/Cargo.toml @@ -18,3 +18,4 @@ proc-macro = true [dependencies] quote.workspace = true syn = { workspace = true, features = ["full", "extra-traits"] } +scarb-stable-hash = { path = "../../utils/scarb-stable-hash" } diff --git a/plugins/cairo-lang-macro-attributes/src/lib.rs b/plugins/cairo-lang-macro-attributes/src/lib.rs index 63e3d8463..5463dfe60 100644 --- a/plugins/cairo-lang-macro-attributes/src/lib.rs +++ b/plugins/cairo-lang-macro-attributes/src/lib.rs @@ -1,5 +1,6 @@ use proc_macro::TokenStream; use quote::quote; +use scarb_stable_hash::short_hash; use syn::{parse_macro_input, ItemFn}; /// Inline macro helper. @@ -28,3 +29,40 @@ pub fn attribute_macro(_args: TokenStream, input: TokenStream) -> TokenStream { }; TokenStream::from(expanded) } + +/// This macro can be used to construct the auxiliary data collection callback. +/// +/// The procedural macro can emit additional auxiliary data alongside the generated [`TokenStream`] +/// during the code expansion. This data can be used to collect additional information from the +/// source code of a project that is being compiled during the macro execution. +/// For instance, you can create a procedural macro that collects some information stored by +/// the Cairo programmer as attributes in the project source code. +/// +/// This should be used to implement a collection callback for the auxiliary data. +/// This callback will be called after the source code compilation (and thus after all the procedural +/// macro executions). All auxiliary data emitted by the procedural macro during source code compilation +/// will be passed to the callback as an argument. +/// +/// The callback can be used to process or persist the data collected during the compilation. +/// +/// This macro hides the conversion to stable ABI structs from the user. +/// +/// # Safety +/// Note that AuxData deserialization may fail. +#[proc_macro_attribute] +pub fn aux_data_collection_callback(_args: TokenStream, input: TokenStream) -> TokenStream { + let mut item: ItemFn = parse_macro_input!(input as ItemFn); + // Rename item to hide it from the macro source code. + let id = short_hash(item.sig.ident.to_string()); + let item_name = format!("{}_{}", item.sig.ident, id); + item.sig.ident = syn::Ident::new(item_name.as_str(), item.sig.ident.span()); + + let item_name = &item.sig.ident; + let expanded = quote! { + #item + + #[linkme::distributed_slice(cairo_lang_macro::AUX_DATA_CALLBACKS)] + static AUX_DATA_CALLBACK_DESERIALIZE: fn(Vec) = #item_name; + }; + TokenStream::from(expanded) +} diff --git a/plugins/cairo-lang-macro/Cargo.toml b/plugins/cairo-lang-macro/Cargo.toml index 73a14dac0..a91d013b9 100644 --- a/plugins/cairo-lang-macro/Cargo.toml +++ b/plugins/cairo-lang-macro/Cargo.toml @@ -15,3 +15,4 @@ repository.workspace = true [dependencies] cairo-lang-macro-attributes = { path = "../cairo-lang-macro-attributes" } cairo-lang-macro-stable = { path = "../cairo-lang-macro-stable" } +linkme.workspace = true diff --git a/plugins/cairo-lang-macro/src/lib.rs b/plugins/cairo-lang-macro/src/lib.rs index fbee1b56a..6d60cf367 100644 --- a/plugins/cairo-lang-macro/src/lib.rs +++ b/plugins/cairo-lang-macro/src/lib.rs @@ -3,6 +3,7 @@ use cairo_lang_macro_stable::ffi::StableSlice; use cairo_lang_macro_stable::{ StableAuxData, StableDiagnostic, StableProcMacroResult, StableSeverity, StableTokenStream, }; +use linkme::distributed_slice; use std::ffi::{c_char, CStr, CString}; use std::fmt::Display; use std::num::NonZeroU8; @@ -24,6 +25,28 @@ pub unsafe extern "C" fn free_result(result: StableProcMacroResult) { ProcMacroResult::from_owned_stable(result); } +#[distributed_slice] +pub static AUX_DATA_CALLBACKS: [fn(Vec)]; + +#[no_mangle] +#[doc(hidden)] +pub unsafe extern "C" fn aux_data_callback( + stable_aux_data: StableSlice, +) -> StableSlice { + if !AUX_DATA_CALLBACKS.is_empty() { + // Callback has been defined, applying the aux data collection. + let fun = AUX_DATA_CALLBACKS[0]; + let (ptr, n) = stable_aux_data.raw_parts(); + let aux_data: &[StableAuxData] = slice::from_raw_parts(ptr, n); + let aux_data = aux_data + .iter() + .filter_map(|a| AuxData::from_stable(a)) + .collect::>(); + fun(aux_data); + } + stable_aux_data +} + #[derive(Debug)] pub enum ProcMacroResult { /// Plugin has not taken any action. diff --git a/scarb/src/compiler/plugin/proc_macro/ffi.rs b/scarb/src/compiler/plugin/proc_macro/ffi.rs index 3f80b653c..436f2c2ac 100644 --- a/scarb/src/compiler/plugin/proc_macro/ffi.rs +++ b/scarb/src/compiler/plugin/proc_macro/ffi.rs @@ -1,8 +1,10 @@ use crate::core::{Config, Package, PackageId}; use anyhow::{Context, Result}; use cairo_lang_defs::patcher::PatchBuilder; -use cairo_lang_macro::{ProcMacroResult, TokenStream}; -use cairo_lang_macro_stable::{StableProcMacroResult, StableResultWrapper, StableTokenStream}; +use cairo_lang_macro::{AuxData, ProcMacroResult, TokenStream}; +use cairo_lang_macro_stable::{ + StableAuxData, StableProcMacroResult, StableResultWrapper, StableTokenStream, +}; use cairo_lang_syntax::node::db::SyntaxGroup; use cairo_lang_syntax::node::{ast, TypedSyntaxNode}; use camino::Utf8PathBuf; @@ -10,6 +12,8 @@ use libloading::{Library, Symbol}; use std::fmt::Debug; use crate::compiler::plugin::proc_macro::compilation::SharedLibraryProvider; +use crate::compiler::plugin::proc_macro::ProcMacroAuxData; +use cairo_lang_macro_stable::ffi::StableSlice; #[cfg(not(windows))] use libloading::os::unix::Symbol as RawSymbol; #[cfg(windows)] @@ -91,14 +95,32 @@ impl ProcMacroInstance { // Return obtained result. result } + + pub(crate) fn aux_data_callback(&self, aux_data: Vec) { + // Convert to stable aux data. + let aux_data: Vec = aux_data.into_iter().map(Into::into).collect(); + let aux_data = aux_data + .into_iter() + .map(|a| a.into_stable()) + .collect::>(); + // Create stable slice representation from vector. + // Note this needs to be freed manually. + let aux_data = StableSlice::new(aux_data); + // Actual call to FFI interface for aux data callback. + let aux_data = (self.plugin.vtable.aux_data_callback)(aux_data); + // Free the memory allocated by vec. + let _ = aux_data.into_owned(); + } } type ExpandCode = extern "C" fn(StableTokenStream) -> StableResultWrapper; type FreeResult = extern "C" fn(StableProcMacroResult); +type AuxDataCallback = extern "C" fn(StableSlice) -> StableSlice; struct VTableV0 { expand: RawSymbol, free_result: RawSymbol, + aux_data_callback: RawSymbol, } impl VTableV0 { @@ -111,9 +133,14 @@ impl VTableV0 { .get(b"free_result\0") .context("failed to load free_result function for procedural macro")?; let free_result = free_result.into_raw(); + let aux_data_callback: Symbol<'_, AuxDataCallback> = library + .get(b"aux_data_callback\0") + .context("failed to load aux_data_callback function for procedural macro")?; + let aux_data_callback = aux_data_callback.into_raw(); Ok(VTableV0 { expand, free_result, + aux_data_callback, }) } } diff --git a/scarb/src/compiler/plugin/proc_macro/host.rs b/scarb/src/compiler/plugin/proc_macro/host.rs index 38ccfdc3e..e42fbe131 100644 --- a/scarb/src/compiler/plugin/proc_macro/host.rs +++ b/scarb/src/compiler/plugin/proc_macro/host.rs @@ -17,6 +17,7 @@ use itertools::Itertools; use smol_str::SmolStr; use std::any::Any; use std::sync::Arc; +use tracing::{debug, trace_span}; /// A Cairo compiler plugin controlling the procedural macro execution. /// @@ -171,7 +172,19 @@ impl ProcMacroHostPlugin { } } } - let _aux_data = data.into_iter().into_group_map_by(|d| d.macro_package_id); + let aux_data = data.into_iter().into_group_map_by(|d| d.macro_package_id); + for instance in self.macros.iter() { + let _ = trace_span!( + "aux_data_collection_callback", + instance = instance.package_id().to_string() + ) + .enter(); + let data = aux_data.get(&instance.package_id()).cloned(); + if let Some(data) = data { + debug!("calling aux data callback with: {:?}", data); + instance.aux_data_callback(data.clone()); + } + } Ok(()) } } diff --git a/scarb/tests/build_cairo_plugin.rs b/scarb/tests/build_cairo_plugin.rs index b825cacf3..00dda7514 100644 --- a/scarb/tests/build_cairo_plugin.rs +++ b/scarb/tests/build_cairo_plugin.rs @@ -84,6 +84,9 @@ fn simple_project_with_code(t: &impl PathChild, code: impl ToString) { [dependencies] cairo-lang-macro = {{ path = {macro_lib_path}}} cairo-lang-macro-stable = {{ path = {macro_stable_lib_path}}} + serde = {{ version = "*", features = ["derive"] }} + serde_json = "*" + linkme = "0.3" "#}, ) .build(t); @@ -452,3 +455,82 @@ fn can_replace_original_node() { Run completed successfully, returning [34] "#}); } + +#[test] +fn can_return_aux_data_from_plugin() { + let temp = TempDir::new().unwrap(); + let t = temp.child("some"); + simple_project_with_code( + &t, + indoc! {r##" + use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro, AuxData, aux_data_collection_callback}; + use serde::{Serialize, Deserialize}; + + #[derive(Debug, Serialize, Deserialize)] + struct SomeMacroDataFormat { + msg: String + } + + #[attribute_macro] + pub fn some_macro(token_stream: TokenStream) -> ProcMacroResult { + let token_stream = TokenStream::new( + token_stream + .to_string() + // Remove macro call to avoid infinite loop. + .replace("#[some]", "") + .replace("12", "34") + ); + let value = SomeMacroDataFormat { msg: "Hello from some macro!".to_string() }; + let value = serde_json::to_string(&value).unwrap(); + let value: Vec = value.into_bytes(); + let aux_data = AuxData::new(value); + + ProcMacroResult::Replace { + token_stream, + aux_data: Some(aux_data), + diagnostics: Vec::new() + } + } + + #[aux_data_collection_callback] + pub fn callback(aux_data: Vec) { + let aux_data = aux_data.into_iter() + .map(|aux_data| { + let value: Vec = aux_data.into(); + let aux_data: SomeMacroDataFormat = serde_json::from_slice(&value).unwrap(); + aux_data + }) + .collect::>(); + println!("{:?}", aux_data); + } + "##}, + ); + + let project = temp.child("hello"); + ProjectBuilder::start() + .name("hello") + .version("1.0.0") + .dep_starknet() + .dep("some", &t) + .lib_cairo(indoc! {r#" + #[some] + fn main() -> felt252 { 12 } + "#}) + .build(&project); + + Scarb::quick_snapbox() + .arg("cairo-run") + // Disable output from Cargo. + .env("CARGO_TERM_QUIET", "true") + .current_dir(&project) + .assert() + .success() + .stdout_matches(indoc! {r#" + [..]Compiling some v1.0.0 ([..]Scarb.toml) + [..]Compiling hello v1.0.0 ([..]Scarb.toml) + [SomeMacroDataFormat { msg: "Hello from some macro!" }] + [..]Finished release target(s) in [..] + [..]Running hello + [..]Run completed successfully, returning [..] + "#}); +}