1use proc_macro::TokenStream;
14use quote::{format_ident, quote};
15use serde::Deserialize;
16
17fn find_workspace_root(start: &str) -> String {
19 let mut current = std::path::PathBuf::from(start);
20 loop {
21 let cargo_toml = current.join("Cargo.toml");
22 if cargo_toml.exists() {
23 if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
24 if content.contains("[workspace]") {
25 return current.to_string_lossy().to_string();
26 }
27 }
28 }
29 if !current.pop() {
30 return std::path::PathBuf::from(start)
31 .parent()
32 .unwrap()
33 .to_string_lossy()
34 .to_string();
35 }
36 }
37}
38
39fn extract_group_name(path: &std::path::Path) -> String {
42 let raw = path
43 .parent()
44 .and_then(|p| p.file_name())
45 .and_then(|n| n.to_str())
46 .unwrap_or("unknown")
47 .to_string();
48 sanitize_ident(&raw)
49}
50
51fn sanitize_ident(s: &str) -> String {
53 let mut out = String::with_capacity(s.len());
54 for ch in s.chars() {
55 if ch.is_ascii_alphanumeric() || ch == '_' {
56 out.push(ch);
57 } else {
58 out.push('_');
59 }
60 }
61 if out.chars().next().is_some_and(|c| c.is_ascii_digit()) {
62 out = format!("_{}", out);
63 }
64 if out.is_empty() {
65 out.push_str("unknown");
66 }
67 out
68}
69
70#[derive(Debug, Deserialize)]
72struct CabiExport {
73 name: String,
74 params: Vec<CabiParam>,
75 ret_type: String,
76 has_free_func: bool,
77}
78
79#[derive(Debug, Deserialize)]
80struct CabiParam {
81 #[allow(dead_code)]
82 name: String,
83 zig_type: String,
84}
85
86fn generate_from_path(json_path: &std::path::Path, group_name: &str, span: proc_macro2::Span) -> TokenStream {
88 let json_content = match std::fs::read_to_string(json_path) {
90 Ok(s) => s,
91 Err(e) => {
92 return syn::Error::new(
93 span,
94 format!(
95 "js2rust_bridge: cannot read '{}': {}",
96 json_path.display(),
97 e
98 ),
99 )
100 .to_compile_error()
101 .into();
102 }
103 };
104
105 let exports: Vec<CabiExport> = match serde_json::from_str(&json_content) {
106 Ok(v) => v,
107 Err(e) => {
108 return syn::Error::new(
109 span,
110 format!(
111 "js2rust_bridge: failed to parse '{}': {}",
112 json_path.display(),
113 e
114 ),
115 )
116 .to_compile_error()
117 .into();
118 }
119 };
120
121 let generated = generate_bindings(&exports, group_name);
123
124 match generated.parse::<TokenStream>() {
125 Ok(ts) => ts,
126 Err(e) => syn::Error::new(span, format!("internal error: {}", e))
127 .to_compile_error()
128 .into(),
129 }
130}
131
132#[proc_macro]
139pub fn js2rust_bridge(input: TokenStream) -> TokenStream {
140 let group_name = match syn::parse::<syn::Ident>(input.clone()) {
142 Ok(ident) => ident.to_string(),
143 Err(_) => {
144 match syn::parse::<syn::LitStr>(input) {
146 Ok(s) => {
147 let json_path = s.value();
148 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
149 .expect("CARGO_MANIFEST_DIR not set");
150 let workspace_root = find_workspace_root(&manifest_dir);
151 let resolved_path = std::path::Path::new(&workspace_root).join(&json_path);
152 let group_name = extract_group_name(&resolved_path);
153 return generate_from_path(&resolved_path, &group_name, s.span());
154 }
155 Err(e) => return e.to_compile_error().into(),
156 }
157 }
158 };
159
160 let out_dir = match std::env::var("OUT_DIR") {
162 Ok(dir) => dir,
163 Err(_) => {
164 return syn::Error::new(
165 proc_macro2::Span::call_site(),
166 "js2rust_bridge: OUT_DIR not set.\n\
167 Make sure you have a build script that calls `js2zig_build::transpile()`.",
168 )
169 .to_compile_error()
170 .into();
171 }
172 };
173
174 let json_path = std::path::Path::new(&out_dir)
176 .join("js2zig")
177 .join(&group_name)
178 .join("cabi_exports.json");
179
180 generate_from_path(&json_path, &group_name, proc_macro2::Span::call_site())
181}
182
183fn generate_bindings(exports: &[CabiExport], group_suffix: &str) -> String {
185 let mut extern_fns = Vec::new();
186 let mut safe_wrappers = Vec::new();
187
188 let raw_mod = format_ident!("__js2rust_ffi_raw_{group_suffix}");
189 let safe_mod = format_ident!("__js2rust_ffi_safe_{group_suffix}");
190
191 for exp in exports {
192 let fn_name = format_ident!("{}", exp.name);
193 let free_fn_name = format_ident!("free_{}", exp.name);
194
195 let mut extern_params = Vec::new();
197 let mut safe_params = Vec::new();
198 let mut call_args = Vec::new();
199
200 for (idx, param) in exp.params.iter().enumerate() {
201 let param_ident = format_ident!("arg{}", idx);
202 let param_ty = zig_type_to_rust_ffi_type(¶m.zig_type);
203 extern_params.push(quote! { #param_ident: #param_ty });
204 safe_params.push(quote! { #param_ident: #param_ty });
205 call_args.push(quote! { #param_ident });
206 }
207
208 let ret_ty = zig_ret_type_to_rust_ffi(&exp.ret_type);
209
210 extern_fns.push(quote! {
212 pub fn #fn_name( #(#extern_params),* ) -> #ret_ty;
213 });
214
215 if exp.has_free_func {
216 extern_fns.push(quote! {
217 pub fn #free_fn_name(ptr: *mut std::ffi::c_void);
218 });
219 }
220
221 let safe_wrapper = generate_safe_wrapper(exp, &fn_name, &free_fn_name, &raw_mod, group_suffix);
223 safe_wrappers.push(safe_wrapper);
224 }
225
226 let output = quote! {
228 #[allow(non_snake_case)]
229 #[allow(dead_code)]
230 mod #raw_mod {
231 unsafe extern "C" {
232 #(#extern_fns)*
233 }
234 }
235
236 #[allow(non_snake_case)]
237 #[allow(dead_code)]
238 mod #safe_mod {
239 use super::#raw_mod;
240
241 #(#safe_wrappers)*
242 }
243
244 pub use #safe_mod::*;
246 };
247
248 output.to_string()
249}
250
251fn generate_safe_wrapper(
253 exp: &CabiExport,
254 fn_name: &syn::Ident,
255 free_fn_name: &syn::Ident,
256 raw_mod: &syn::Ident,
257 group_suffix: &str,
258) -> proc_macro2::TokenStream {
259 let wrapper_name = format_ident!("{}_{}", exp.name, group_suffix);
261 let mut safe_params = Vec::new();
262 let mut ffi_args = Vec::new();
263
264 for (idx, param) in exp.params.iter().enumerate() {
266 let param_ident = format_ident!("arg{}", idx);
267 let safe_ty = zig_type_to_rust_safe_type(¶m.zig_type);
268 safe_params.push(quote! { #param_ident: #safe_ty });
269 ffi_args.push(convert_safe_to_ffi(¶m.zig_type, ¶m_ident));
270 }
271
272 let (ret_ty, call_expr) = if exp.ret_type == "[]const u8" {
273 (
275 quote! { String },
276 quote! {
277 {
278 let ptr = unsafe { super::#raw_mod::#fn_name(#(#ffi_args),*) };
279 if ptr.is_null() {
280 String::new()
281 } else {
282 let s = unsafe {
283 std::ffi::CStr::from_ptr(ptr)
284 .to_string_lossy()
285 .into_owned()
286 };
287 unsafe { super::#raw_mod::#free_fn_name(ptr as *mut std::ffi::c_void) };
288 s
289 }
290 }
291 },
292 )
293 } else {
294 let rust_ret = zig_ret_type_to_rust_safe(&exp.ret_type);
295 (
296 rust_ret.clone(),
297 quote! {
298 unsafe { super::#raw_mod::#fn_name(#(#ffi_args),*) }
299 },
300 )
301 };
302
303 quote! {
304 #[allow(non_snake_case)]
305 pub fn #wrapper_name( #(#safe_params),* ) -> #ret_ty {
306 #call_expr
307 }
308 }
309}
310
311fn zig_type_to_rust_ffi_type(zig_type: &str) -> proc_macro2::TokenStream {
315 match zig_type {
316 "[]const u8" => quote! { *const std::ffi::c_char },
317 "i32" => quote! { i32 },
318 "i64" => quote! { i64 },
319 "f64" => quote! { f64 },
320 "bool" => quote! { bool },
321 "void" => quote! { () },
322 _ => quote! { *mut std::ffi::c_void },
323 }
324}
325
326fn zig_ret_type_to_rust_ffi(ret_type: &str) -> proc_macro2::TokenStream {
328 match ret_type {
329 "[]const u8" => quote! { *const std::ffi::c_char },
330 "i32" => quote! { i32 },
331 "i64" => quote! { i64 },
332 "f64" => quote! { f64 },
333 "bool" => quote! { bool },
334 "void" => quote! { () },
335 _ => quote! { *mut std::ffi::c_void },
336 }
337}
338
339fn zig_type_to_rust_safe_type(zig_type: &str) -> proc_macro2::TokenStream {
341 match zig_type {
342 "[]const u8" => quote! { &str },
343 "i32" => quote! { i32 },
344 "i64" => quote! { i64 },
345 "f64" => quote! { f64 },
346 "bool" => quote! { bool },
347 _ => quote! { *mut std::ffi::c_void },
348 }
349}
350
351fn convert_safe_to_ffi(zig_type: &str, ident: &syn::Ident) -> proc_macro2::TokenStream {
353 match zig_type {
354 "[]const u8" => quote! { std::ffi::CString::new(#ident).unwrap().into_raw() },
355 _ => quote! { #ident },
356 }
357}
358
359fn zig_ret_type_to_rust_safe(ret_type: &str) -> proc_macro2::TokenStream {
361 match ret_type {
362 "[]const u8" => quote! { String },
363 "i32" => quote! { i32 },
364 "i64" => quote! { i64 },
365 "f64" => quote! { f64 },
366 "bool" => quote! { bool },
367 "void" => quote! { () },
368 _ => quote! { *mut std::ffi::c_void },
369 }
370}