Skip to main content

nil_ffi_node/
lib.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4#![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}