Skip to main content

aranya_capi_codegen/
generate.rs

1use std::{fs::File, io::Write as _};
2
3use proc_macro2::TokenStream;
4use quote::{format_ident, quote};
5use syn::{Ident, Path, parse_quote};
6use tracing::{debug, info, instrument};
7
8use super::{
9    ast::Ast,
10    ctx::Ctx,
11    error::BuildError,
12    syntax::{Fn, Item, Node, Trimmed},
13};
14
15/// Configures code generation.
16#[derive(Clone, Debug)]
17pub struct Config {
18    /// The error type to use.
19    ///
20    /// Must implement `aranya_capi_core::ErrorCode`.
21    pub err_ty: Path,
22    /// The extended error type to use.
23    ///
24    /// Must implement `aranya_capi_core::ExtendedError`.
25    pub ext_err_ty: Path,
26    /// Function identifier prefix.
27    ///
28    /// E.g., `os` converts `fn foo` to `fn os_foo`.
29    pub fn_prefix: Ident,
30    /// Type identifier prefix.
31    ///
32    /// E.g., `Os` converts `struct Foo` to `struct OsFoo`.
33    pub ty_prefix: Ident,
34    /// Path to the module where defs live.
35    pub defs: Path,
36    /// The `TARGET` environment variable.
37    pub target: String,
38}
39
40impl Config {
41    /// Generates code.
42    #[instrument(skip_all)]
43    pub fn generate(self, source: &str) -> Result<TokenStream, BuildError> {
44        let file = syn::parse_file(source)?;
45        generate(self, file.items.into_iter().map(Item::from).collect())
46    }
47}
48
49fn generate(cfg: Config, items: Vec<Item>) -> Result<TokenStream, BuildError> {
50    info!(target = cfg.target, items = items.len(), "generating C API");
51
52    let capi = format_ident!("__capi");
53    let mut ctx = Ctx {
54        capi: capi.clone(),
55        conv: parse_quote!(#capi::internal::conv),
56        util: parse_quote!(#capi::internal::util),
57        error: parse_quote!(#capi::internal::error),
58        err_ty: cfg.err_ty,
59        ext_err_ty: cfg.ext_err_ty,
60        ty_prefix: cfg.ty_prefix,
61        fn_prefix: cfg.fn_prefix,
62        defs: cfg.defs,
63        hidden: format_ident!("__hidden"),
64        imports: format_ident!("__imports"),
65        errs: Default::default(),
66    };
67    let ast = Ast::parse(&mut ctx, items)?;
68    ctx.propagate()?;
69
70    let capi = &ctx.capi;
71    let err_ty = &ctx.err_ty;
72    let ext_err_ty = &ctx.ext_err_ty;
73
74    debug!(
75        capi = %Trimmed(capi),
76        err_ty = %Trimmed(err_ty),
77        ext_err_ty = %Trimmed(ext_err_ty),
78        ty_prefix = %ctx.ty_prefix,
79        fn_prefix = %ctx.fn_prefix,
80        "generating code",
81    );
82
83    // `use ...` items.
84    let mut imports = Vec::new();
85    // Type defs.
86    let mut defs = Vec::new();
87    // Rust and exported FFI fns.
88    let mut fns = Vec::<Fn>::new();
89    // Other items from the AST.
90    let mut other = Vec::new();
91
92    for node in ast.nodes {
93        match node {
94            n @ (Node::Alias(_) | Node::Enum(_) | Node::Struct(_) | Node::Union(_)) => defs.push(n),
95            Node::FfiFn(f) => fns.push(f.into()),
96            Node::RustFn(f) => fns.push(f.into()),
97            Node::Other(Item::Use(mut v)) => {
98                v.vis = parse_quote!(pub(super));
99                imports.push(v);
100            }
101            Node::Other(v) => other.push(v),
102        }
103    }
104    let hidden = &ast.hidden;
105
106    let mod_imports = &ctx.imports;
107    let mod_hidden = &ctx.hidden;
108
109    let code = quote! {
110        /// This code is @generated by `aranya-capi-codegen`. DO NOT EDIT.
111        const _: () = ();
112
113        extern crate aranya_capi_core as #capi;
114
115        use #capi::Builder;
116        use #capi::internal::tracing;
117
118        mod #mod_imports {
119            #(#imports)*
120        }
121
122        #(#defs)*
123        #(#fns)*
124        #(#other)*
125
126        #[allow(deprecated)]
127        mod #mod_hidden {
128            #[allow(clippy::wildcard_imports)]
129            use super::*;
130
131            #hidden
132        }
133    };
134
135    dump(&code, "/tmp/expand-capi-codegen.rs");
136
137    Ok(code)
138}
139
140/// Formats a [`TokenStream`] as a string.
141pub fn format(tokens: &TokenStream) -> String {
142    let mut data = tokens.to_string();
143    if let Ok(file) = syn::parse_file(&data) {
144        data = prettyplease::unparse(&file);
145    }
146    data
147}
148
149#[doc(hidden)]
150#[allow(clippy::panic, reason = "This is debugging `build.rs` code")]
151pub fn dump(code: &TokenStream, path: &str) {
152    if !cfg!(feature = "debug") {
153        return;
154    }
155    let data = format(code);
156    File::create(path)
157        .unwrap_or_else(|_| panic!("unable to create `{path}`"))
158        .write_all(data.as_bytes())
159        .unwrap_or_else(|_| panic!("unable to write all data to `{path}`"));
160}