1use proc_macro::TokenStream;
22use quote::{format_ident, quote};
23use serde::Deserialize;
24use syn::{
25 braced, parenthesized, parse::{Parse, ParseStream}, Ident, LitStr, Token
26};
27
28#[derive(Debug, Deserialize)]
31struct CabiExport {
32 name: String,
33 params: Vec<CabiParam>,
34 ret_type: String,
35 has_free_func: bool,
36}
37
38#[derive(Debug, Deserialize)]
39struct CabiParam {
40 #[allow(dead_code)]
41 name: String,
42 zig_type: String,
43}
44
45struct HostFnDecl {
49 name: String,
50 params: Vec<String>,
51 return_type: String,
52 is_async: bool,
53 async_return_fields: Vec<(String, String)>,
55}
56
57struct MacroInput {
59 js_file: String,
61 group: String,
63 host_fns: Vec<HostFnDecl>,
64}
65
66impl Parse for MacroInput {
67 fn parse(input: ParseStream) -> syn::Result<Self> {
68 let js_file_lit: LitStr = input.parse()?;
70 let js_file = js_file_lit.value();
71
72 let stem = std::path::Path::new(&js_file)
74 .file_stem()
75 .and_then(|s| s.to_str())
76 .unwrap_or("main");
77 let group = js2zig_core::analyzer::sanitize_module_name(stem);
78
79 let mut host_fns = Vec::new();
81 while input.peek(Token![,]) {
82 input.parse::<Token![,]>()?;
83 if input.is_empty() {
84 break;
85 }
86
87 let is_async = input.peek(Token![async]);
89 if is_async {
90 input.parse::<Token![async]>()?;
91 }
92 let name: Ident = input.parse()?;
93 let name_str = name.to_string();
94
95 let paren_content;
97 parenthesized!(paren_content in input);
98 let mut params = Vec::new();
99 while !paren_content.is_empty() {
100 let ty: Ident = paren_content.parse()?;
101 params.push(ty.to_string());
102 if paren_content.peek(Token![,]) {
103 paren_content.parse::<Token![,]>()?;
104 }
105 }
106
107 input.parse::<Token![->]>()?;
109
110 let (return_type, async_fields) = if input.peek(syn::token::Brace) {
112 let struct_content;
113 braced!(struct_content in input);
114 let mut fields = Vec::new();
115 while !struct_content.is_empty() {
116 let field_name: Ident = struct_content.parse()?;
117 struct_content.parse::<Token![:]>()?;
118 let field_type: Ident = struct_content.parse()?;
119 fields.push((field_name.to_string(), field_type.to_string()));
120 if struct_content.peek(Token![,]) {
121 struct_content.parse::<Token![,]>()?;
122 }
123 }
124 ("void".to_string(), fields)
126 } else {
127 let ret: Ident = input.parse()?;
128 (ret.to_string(), Vec::new())
129 };
130
131 host_fns.push(HostFnDecl {
132 name: name_str,
133 params,
134 return_type,
135 is_async,
136 async_return_fields: async_fields,
137 });
138 }
139
140 Ok(MacroInput {
141 js_file,
142 group,
143 host_fns,
144 })
145 }
146}
147
148fn type_name_to_host_type(name: &str) -> Result<js2zig_core::HostType, String> {
152 match name {
153 "i64" => Ok(js2zig_core::HostType::I64),
154 "i32" => Ok(js2zig_core::HostType::I32),
155 "f64" => Ok(js2zig_core::HostType::F64),
156 "bool" => Ok(js2zig_core::HostType::Bool),
157 "str" => Ok(js2zig_core::HostType::Str),
158 "void" => Ok(js2zig_core::HostType::Void),
159 other => Err(format!("js2rust_bridge: unknown host type '{}'. \
160 Valid types: i64, i32, f64, bool, str, void", other)),
161 }
162}
163
164#[proc_macro]
171pub fn js2rust_bridge(input: TokenStream) -> TokenStream {
172 let input_tokens: proc_macro2::TokenStream = input.into();
173
174 match syn::parse2::<MacroInput>(input_tokens) {
175 Ok(parsed) => generate(&parsed),
176 Err(e) => e.to_compile_error().into(),
177 }
178}
179
180fn generate(input: &MacroInput) -> TokenStream {
183 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
184 .unwrap_or_else(|_| ".".to_string());
185
186 let js_file_path = std::path::Path::new(&manifest_dir).join(&input.js_file);
188
189 let cache_dir = std::path::Path::new(&manifest_dir)
191 .join(".js2zig-cache");
192
193 let mut host_functions = Vec::new();
195 for hf in &input.host_fns {
196 let params: Result<Vec<_>, _> = hf.params.iter()
197 .map(|t| type_name_to_host_type(t))
198 .collect();
199 let params = match params {
200 Ok(p) => p,
201 Err(e) => return syn::Error::new(proc_macro2::Span::call_site(), e)
202 .to_compile_error().into(),
203 };
204
205 let return_type = match type_name_to_host_type(&hf.return_type) {
206 Ok(js2zig_core::HostType::Void) => None,
207 Ok(t) => Some(t),
208 Err(e) => return syn::Error::new(proc_macro2::Span::call_site(), e)
209 .to_compile_error().into(),
210 };
211
212 let async_return_fields: Result<Vec<_>, _> = hf.async_return_fields.iter()
214 .map(|(name, ty)| {
215 type_name_to_host_type(ty).map(|t| (name.clone(), t))
216 })
217 .collect();
218 let async_return_fields = match async_return_fields {
219 Ok(v) => v,
220 Err(e) => return syn::Error::new(proc_macro2::Span::call_site(), e)
221 .to_compile_error().into(),
222 };
223
224 host_functions.push(js2zig_core::HostFunction {
225 name: hf.name.clone(),
226 params,
227 return_type,
228 is_async: hf.is_async,
229 async_return_fields,
230 });
231 }
232
233 let config = js2zig_core::ProjectConfig {
235 name: input.group.clone(),
236 js_file: js_file_path.clone(),
237 out_dir: cache_dir.clone(),
238 host_config: if host_functions.is_empty() {
239 None
240 } else {
241 Some(js2zig_core::HostConfig {
242 functions: host_functions,
243 })
244 },
245 force_rebuild: false,
246 run_zig_build: false,
247 };
248
249 let project_result = match js2zig_core::transpile_project(&config) {
251 Ok(result) => result,
252 Err(e) => {
253 return syn::Error::new(
254 proc_macro2::Span::call_site(),
255 format!("js2rust_bridge: transpilation failed: {}", e),
256 )
257 .to_compile_error()
258 .into();
259 }
260 };
261
262 let group_result = project_result.groups.first();
264
265 let group_result = match group_result {
266 Some(g) => g,
267 None => {
268 return syn::Error::new(
269 proc_macro2::Span::call_site(),
270 "js2rust_bridge: no groups found in transpilation result",
271 )
272 .to_compile_error()
273 .into();
274 }
275 };
276
277 let exports: Vec<CabiExport> = match serde_json::from_str(&group_result.cabi_exports_json) {
279 Ok(v) => v,
280 Err(e) => {
281 return syn::Error::new(
282 proc_macro2::Span::call_site(),
283 format!("js2rust_bridge: failed to parse cabi_exports: {}", e),
284 )
285 .to_compile_error()
286 .into();
287 }
288 };
289
290 let zig_project_dir = cache_dir.join(&input.group);
292 if zig_project_dir.join("build.zig").exists() {
293 let _ = std::process::Command::new("zig")
294 .arg("build")
295 .current_dir(&zig_project_dir)
296 .status();
297 }
298
299 let generated = generate_bindings(&exports, &input.group);
301
302 match generated.parse::<TokenStream>() {
303 Ok(ts) => ts,
304 Err(e) => syn::Error::new(
305 proc_macro2::Span::call_site(),
306 format!("internal error: {}", e),
307 )
308 .to_compile_error()
309 .into(),
310 }
311}
312
313fn generate_bindings(exports: &[CabiExport], group_suffix: &str) -> String {
316 let mut extern_fns = Vec::new();
317 let mut safe_wrappers = Vec::new();
318
319 let raw_mod = format_ident!("__js2rust_ffi_raw_{group_suffix}");
320 let safe_mod = format_ident!("__js2rust_ffi_safe_{group_suffix}");
321
322 for exp in exports {
323 let fn_name = format_ident!("{}", exp.name);
324 let free_fn_name = format_ident!("free_{}", exp.name);
325
326 let mut extern_params = Vec::new();
327 for (idx, param) in exp.params.iter().enumerate() {
328 let param_ident = format_ident!("arg{}", idx);
329 let param_ty = zig_type_to_rust_ffi_type(¶m.zig_type);
330 extern_params.push(quote! { #param_ident: #param_ty });
331 }
332
333 let ret_ty = zig_ret_type_to_rust_ffi(&exp.ret_type);
334
335 extern_fns.push(quote! {
336 pub fn #fn_name( #(#extern_params),* ) -> #ret_ty;
337 });
338
339 if exp.has_free_func {
340 extern_fns.push(quote! {
341 pub fn #free_fn_name(ptr: *mut std::ffi::c_void);
342 });
343 }
344
345 let safe_wrapper = generate_safe_wrapper(exp, &fn_name, &free_fn_name, &raw_mod, group_suffix);
346 safe_wrappers.push(safe_wrapper);
347 }
348
349 let runtime_init = quote! {
351 pub fn js2rust_init() {
354 extern "C" {
355 #[link_name = "js2rust_init"]
356 fn _js2rust_init();
357 }
358 unsafe { _js2rust_init() };
359 }
360 pub fn js2rust_deinit() {
362 extern "C" {
363 #[link_name = "js2rust_deinit"]
364 fn _js2rust_deinit();
365 }
366 unsafe { _js2rust_deinit() };
367 }
368 };
369 safe_wrappers.push(runtime_init);
370
371 let output = quote! {
372 #[allow(non_snake_case)]
373 #[allow(dead_code)]
374 mod #raw_mod {
375 unsafe extern "C" {
376 #(#extern_fns)*
377 }
378 }
379
380 #[allow(non_snake_case)]
381 #[allow(dead_code)]
382 mod #safe_mod {
383 use super::#raw_mod;
384
385 #(#safe_wrappers)*
386 }
387
388 pub use #safe_mod::*;
389 };
390
391 output.to_string()
392}
393
394fn generate_safe_wrapper(
395 exp: &CabiExport,
396 fn_name: &syn::Ident,
397 free_fn_name: &syn::Ident,
398 raw_mod: &syn::Ident,
399 group_suffix: &str,
400) -> proc_macro2::TokenStream {
401 let wrapper_name = format_ident!("{}_{}", exp.name, group_suffix);
402 let mut safe_params = Vec::new();
403 let mut ffi_args = Vec::new();
404
405 for (idx, param) in exp.params.iter().enumerate() {
406 let param_ident = format_ident!("arg{}", idx);
407 let safe_ty = zig_type_to_rust_safe_type(¶m.zig_type);
408 safe_params.push(quote! { #param_ident: #safe_ty });
409 ffi_args.push(convert_safe_to_ffi(¶m.zig_type, ¶m_ident));
410 }
411
412 let (ret_ty, call_expr) = if exp.ret_type == "[]const u8" {
413 (
414 quote! { String },
415 quote! {
416 {
417 let ptr = unsafe { super::#raw_mod::#fn_name(#(#ffi_args),*) };
418 if ptr.is_null() {
419 String::new()
420 } else {
421 let s = unsafe {
422 std::ffi::CStr::from_ptr(ptr)
423 .to_string_lossy()
424 .into_owned()
425 };
426 unsafe { super::#raw_mod::#free_fn_name(ptr as *mut std::ffi::c_void) };
427 s
428 }
429 }
430 },
431 )
432 } else {
433 let rust_ret = zig_ret_type_to_rust_safe(&exp.ret_type);
434 (
435 rust_ret.clone(),
436 quote! {
437 unsafe { super::#raw_mod::#fn_name(#(#ffi_args),*) }
438 },
439 )
440 };
441
442 quote! {
443 #[allow(non_snake_case)]
444 pub fn #wrapper_name( #(#safe_params),* ) -> #ret_ty {
445 #call_expr
446 }
447 }
448}
449
450fn zig_type_to_rust_ffi_type(zig_type: &str) -> proc_macro2::TokenStream {
453 match zig_type {
454 "[]const u8" => quote! { *const std::ffi::c_char },
455 "i32" => quote! { i32 },
456 "i64" => quote! { i64 },
457 "f64" => quote! { f64 },
458 "bool" => quote! { bool },
459 "void" => quote! { () },
460 _ => quote! { *mut std::ffi::c_void },
461 }
462}
463
464fn zig_ret_type_to_rust_ffi(ret_type: &str) -> proc_macro2::TokenStream {
465 match ret_type {
466 "[]const u8" => quote! { *const std::ffi::c_char },
467 "i32" => quote! { i32 },
468 "i64" => quote! { i64 },
469 "f64" => quote! { f64 },
470 "bool" => quote! { bool },
471 "void" => quote! { () },
472 _ => quote! { *mut std::ffi::c_void },
473 }
474}
475
476fn zig_type_to_rust_safe_type(zig_type: &str) -> proc_macro2::TokenStream {
477 match zig_type {
478 "[]const u8" => quote! { &str },
479 "i32" => quote! { i32 },
480 "i64" => quote! { i64 },
481 "f64" => quote! { f64 },
482 "bool" => quote! { bool },
483 _ => quote! { *mut std::ffi::c_void },
484 }
485}
486
487fn convert_safe_to_ffi(zig_type: &str, ident: &syn::Ident) -> proc_macro2::TokenStream {
488 match zig_type {
489 "[]const u8" => quote! { std::ffi::CString::new(#ident).unwrap().into_raw() },
490 _ => quote! { #ident },
491 }
492}
493
494fn zig_ret_type_to_rust_safe(ret_type: &str) -> proc_macro2::TokenStream {
495 match ret_type {
496 "[]const u8" => quote! { String },
497 "i32" => quote! { i32 },
498 "i64" => quote! { i64 },
499 "f64" => quote! { f64 },
500 "bool" => quote! { bool },
501 "void" => quote! { () },
502 _ => quote! { *mut std::ffi::c_void },
503 }
504}