1#![feature(iterator_try_collect)]
5
6use anyhow::{Result, bail};
7use std::fmt::Write as _;
8use std::fs;
9use std::path::Path;
10use syn::{Abi, FnArg, Item, ItemFn, ReturnType, Type, Visibility};
11
12#[bon::builder]
13pub fn generate(input: impl AsRef<Path>, output: impl AsRef<Path>) -> Result<()> {
14 let content = fs::read_to_string(input)?;
15 let file = syn::parse_file(&content)?;
16 let functions = collect_functions(&file);
17
18 let mut result = with_header()?;
19 writeln!(&mut result, "export const definitions = {{")?;
20
21 for function in functions {
22 let name = function.sig.ident.to_string();
23 let arguments = function
24 .sig
25 .inputs
26 .iter()
27 .map(|arg| {
28 if let FnArg::Typed(arg) = arg {
29 node_type(&arg.ty)
30 } else {
31 bail!("invalid fn argument");
32 }
33 })
34 .try_collect::<Vec<_>>()?;
35
36 let return_type = match &function.sig.output {
37 ReturnType::Default => "ffi.types.VOID",
38 ReturnType::Type(_, ty) => node_type(ty)?,
39 };
40
41 writeln!(
42 &mut result,
43 "{name}: {{ arguments: [{}], return: {return_type} }},",
44 arguments.join(", ")
45 )?;
46 }
47
48 writeln!(&mut result, "}} as const;")?;
49
50 fs::write(output, result)?;
51
52 Ok(())
53}
54
55#[rustfmt::skip]
56fn with_header() -> Result<String> {
57 let mut buf = String::new();
58 writeln!(buf, "// dprint-ignore-file\n")?;
59 writeln!(buf, "// Copyright (C) Call of Nil contributors")?;
60 writeln!(buf, "// SPDX-License-Identifier: AGPL-3.0-only\n")?;
61 writeln!(buf, "import * as ffi from \"node:ffi\";\n")?;
62 Ok(buf)
63}
64
65fn collect_functions(file: &syn::File) -> Vec<&ItemFn> {
66 file
67 .items
68 .iter()
69 .filter_map(|item| {
70 if let Item::Fn(function) = item {
71 is_ffi_function(function).then_some(function)
72 } else {
73 None
74 }
75 })
76 .collect()
77}
78
79fn is_ffi_function(function: &ItemFn) -> bool {
80 matches!(function.vis, Visibility::Public(_))
81 && function
82 .sig
83 .abi
84 .as_ref()
85 .is_some_and(is_c_abi)
86 && has_no_mangle(function)
87}
88
89fn is_c_abi(abi: &Abi) -> bool {
90 abi
91 .name
92 .as_ref()
93 .is_some_and(|name| name.value() == "C")
94}
95
96fn has_no_mangle(function: &ItemFn) -> bool {
97 function
98 .attrs
99 .iter()
100 .filter(|attr| attr.path().is_ident("unsafe"))
101 .any(|attr| {
102 let mut found = false;
103 let _ = attr.parse_nested_meta(|meta| {
104 if meta.path.is_ident("no_mangle") {
105 found = true;
106 }
107
108 Ok(())
109 });
110
111 found
112 })
113}
114
115fn node_type(ty: &Type) -> Result<&'static str> {
116 const ERR: &str = "unsupported FFI type";
117
118 let value = match ty {
119 Type::Ptr(_) => "ffi.types.POINTER",
120 Type::FnPtr(_) => "ffi.types.FUNCTION",
121 Type::Path(path) => {
122 let Some(segment) = path.path.segments.last() else { bail!(ERR) };
123
124 #[allow(clippy::match_same_arms)]
125 match segment.ident.to_string().as_str() {
126 "i8" => "ffi.types.INT_8",
127 "u8" => "ffi.types.UINT_8",
128 "i16" => "ffi.types.INT_16",
129 "u16" => "ffi.types.UINT_16",
130 "i32" => "ffi.types.INT_32",
131 "u32" => "ffi.types.UINT_32",
132 "i64" => "ffi.types.INT_64",
133 "u64" => "ffi.types.UINT_64",
134 "f32" => "ffi.types.FLOAT_32",
135 "f64" => "ffi.types.FLOAT_64",
136 "bool" => "ffi.types.BOOL",
137
138 "RequestId" => "ffi.types.UINT_32",
139 "Status" => "ffi.types.INT_32",
140 other if other.contains("Callback") => "ffi.types.FUNCTION",
141
142 _ => bail!(ERR),
143 }
144 }
145 _ => bail!(ERR),
146 };
147
148 Ok(value)
149}