use std::{fs::File, io::Write as _};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Ident, Path, parse_quote};
use tracing::{debug, info, instrument};
use super::{
ast::Ast,
ctx::Ctx,
error::BuildError,
syntax::{Fn, Item, Node, Trimmed},
};
#[derive(Clone, Debug)]
pub struct Config {
pub err_ty: Path,
pub ext_err_ty: Path,
pub fn_prefix: Ident,
pub ty_prefix: Ident,
pub defs: Path,
pub target: String,
}
impl Config {
#[instrument(skip_all)]
pub fn generate(self, source: &str) -> Result<TokenStream, BuildError> {
let file = syn::parse_file(source)?;
generate(self, file.items.into_iter().map(Item::from).collect())
}
}
fn generate(cfg: Config, items: Vec<Item>) -> Result<TokenStream, BuildError> {
info!(target = cfg.target, items = items.len(), "generating C API");
let capi = format_ident!("__capi");
let mut ctx = Ctx {
capi: capi.clone(),
conv: parse_quote!(#capi::internal::conv),
util: parse_quote!(#capi::internal::util),
error: parse_quote!(#capi::internal::error),
err_ty: cfg.err_ty,
ext_err_ty: cfg.ext_err_ty,
ty_prefix: cfg.ty_prefix,
fn_prefix: cfg.fn_prefix,
defs: cfg.defs,
hidden: format_ident!("__hidden"),
imports: format_ident!("__imports"),
errs: Default::default(),
};
let ast = Ast::parse(&mut ctx, items)?;
ctx.propagate()?;
let capi = &ctx.capi;
let err_ty = &ctx.err_ty;
let ext_err_ty = &ctx.ext_err_ty;
debug!(
capi = %Trimmed(capi),
err_ty = %Trimmed(err_ty),
ext_err_ty = %Trimmed(ext_err_ty),
ty_prefix = %ctx.ty_prefix,
fn_prefix = %ctx.fn_prefix,
"generating code",
);
let mut imports = Vec::new();
let mut defs = Vec::new();
let mut fns = Vec::<Fn>::new();
let mut other = Vec::new();
for node in ast.nodes {
match node {
n @ (Node::Alias(_) | Node::Enum(_) | Node::Struct(_) | Node::Union(_)) => defs.push(n),
Node::FfiFn(f) => fns.push(f.into()),
Node::RustFn(f) => fns.push(f.into()),
Node::Other(Item::Use(mut v)) => {
v.vis = parse_quote!(pub(super));
imports.push(v);
}
Node::Other(v) => other.push(v),
}
}
let hidden = &ast.hidden;
let mod_imports = &ctx.imports;
let mod_hidden = &ctx.hidden;
let code = quote! {
const _: () = ();
extern crate aranya_capi_core as #capi;
use #capi::Builder;
use #capi::internal::tracing;
mod #mod_imports {
#(#imports)*
}
#(#defs)*
#(#fns)*
#(#other)*
#[allow(deprecated)]
mod #mod_hidden {
#[allow(clippy::wildcard_imports)]
use super::*;
#hidden
}
};
dump(&code, "/tmp/expand-capi-codegen.rs");
Ok(code)
}
pub fn format(tokens: &TokenStream) -> String {
let mut data = tokens.to_string();
if let Ok(file) = syn::parse_file(&data) {
data = prettyplease::unparse(&file);
}
data
}
#[doc(hidden)]
#[allow(clippy::panic, reason = "This is debugging `build.rs` code")]
pub fn dump(code: &TokenStream, path: &str) {
if !cfg!(feature = "debug") {
return;
}
let data = format(code);
File::create(path)
.unwrap_or_else(|_| panic!("unable to create `{path}`"))
.write_all(data.as_bytes())
.unwrap_or_else(|_| panic!("unable to write all data to `{path}`"));
}