1use std::collections::HashMap;
8
9use anyhow::Result;
10use camino::Utf8Path;
11use heck::ToUpperCamelCase;
12use serde::{Deserialize, Serialize};
13use weaveffi_core::codegen::common::{
14 emit_doc as common_emit_doc, walk_modules, walk_modules_with_path, DocCommentStyle,
15};
16use weaveffi_core::codegen::Generator;
17use weaveffi_core::utils::{
18 c_abi_struct_name, local_type_name, render_json_prelude, render_prelude, render_trailer,
19 wrapper_name, CommentStyle,
20};
21use weaveffi_ir::ir::{Api, Function, StructDef, TypeRef};
22
23#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25#[serde(default)]
26pub struct NodeConfig {
27 pub package_name: Option<String>,
29 pub strip_module_prefix: bool,
32 #[serde(skip)]
34 pub input_basename: Option<String>,
35}
36
37impl NodeConfig {
38 pub fn package_name(&self) -> &str {
39 self.package_name.as_deref().unwrap_or("weaveffi")
40 }
41
42 pub fn input_basename(&self) -> &str {
43 self.input_basename.as_deref().unwrap_or("weaveffi.yml")
44 }
45}
46
47pub struct NodeGenerator;
48
49impl NodeGenerator {
50 fn generate_impl(
51 &self,
52 api: &Api,
53 out_dir: &Utf8Path,
54 package_name: &str,
55 strip_module_prefix: bool,
56 input_basename: &str,
57 ) -> Result<()> {
58 let dir = out_dir.join("node");
59 std::fs::create_dir_all(&dir)?;
60 let dbl = CommentStyle::DoubleSlash;
61 std::fs::write(
62 dir.join("index.js"),
63 format!(
64 "{}module.exports = require('./index.node')\n\n{}",
65 render_prelude(dbl, input_basename),
66 render_trailer(dbl, "index.js"),
67 ),
68 )?;
69 std::fs::write(
70 dir.join("types.d.ts"),
71 render_node_dts(api, strip_module_prefix, input_basename),
72 )?;
73 std::fs::write(
74 dir.join("package.json"),
75 render_package_json(package_name, input_basename),
76 )?;
77 std::fs::write(dir.join("binding.gyp"), render_binding_gyp(input_basename))?;
78 std::fs::write(
79 dir.join("weaveffi_addon.c"),
80 render_addon_c(api, strip_module_prefix, input_basename),
81 )?;
82 Ok(())
83 }
84}
85
86impl Generator for NodeGenerator {
87 type Config = NodeConfig;
88
89 fn name(&self) -> &'static str {
90 "node"
91 }
92
93 fn generate(&self, api: &Api, out_dir: &Utf8Path, config: &Self::Config) -> Result<()> {
94 self.generate_impl(
95 api,
96 out_dir,
97 config.package_name(),
98 config.strip_module_prefix,
99 config.input_basename(),
100 )
101 }
102
103 fn output_files(&self, _api: &Api, out_dir: &Utf8Path, _config: &Self::Config) -> Vec<String> {
104 let mut files = vec![
105 out_dir.join("node/binding.gyp").to_string(),
106 out_dir.join("node/index.js").to_string(),
107 out_dir.join("node/package.json").to_string(),
108 out_dir.join("node/types.d.ts").to_string(),
109 out_dir.join("node/weaveffi_addon.c").to_string(),
110 ];
111 files.sort();
112 files
113 }
114}
115
116fn render_package_json(name: &str, input_basename: &str) -> String {
117 let prelude = render_json_prelude(input_basename);
118 format!(
119 "{{\n{prelude} \"name\": \"{name}\",\n \"version\": \"0.1.0\",\n \"main\": \"index.js\",\n \"types\": \"types.d.ts\",\n \"gypfile\": true,\n \"scripts\": {{\n \"install\": \"node-gyp rebuild\"\n }}\n}}\n"
120 )
121}
122
123fn render_binding_gyp(input_basename: &str) -> String {
124 let prelude = render_prelude(CommentStyle::Hash, input_basename);
125 let trailer = render_trailer(CommentStyle::Hash, "binding.gyp");
126 format!(
127 "{prelude}{{\n \"targets\": [\n {{\n \"target_name\": \"weaveffi\",\n \"sources\": [\"weaveffi_addon.c\"],\n \"include_dirs\": [\"../c\"],\n \"libraries\": [\"-lweaveffi\"]\n }}\n ]\n}}\n\n{trailer}"
128 )
129}
130
131fn is_c_ptr_type(ty: &TypeRef) -> bool {
132 matches!(
133 ty,
134 TypeRef::StringUtf8
135 | TypeRef::Bytes
136 | TypeRef::Struct(_)
137 | TypeRef::List(_)
138 | TypeRef::Map(_, _)
139 | TypeRef::Iterator(_)
140 )
141}
142
143fn c_elem_type(ty: &TypeRef, module: &str) -> String {
144 match ty {
145 TypeRef::I32 => "int32_t".into(),
146 TypeRef::U32 => "uint32_t".into(),
147 TypeRef::I64 => "int64_t".into(),
148 TypeRef::F64 => "double".into(),
149 TypeRef::Bool => "bool".into(),
150 TypeRef::TypedHandle(_) | TypeRef::Handle => "weaveffi_handle_t".into(),
151 TypeRef::StringUtf8 | TypeRef::BorrowedStr => "const char*".into(),
152 TypeRef::Bytes | TypeRef::BorrowedBytes => "const uint8_t*".into(),
153 TypeRef::Struct(s) => format!("{}*", c_abi_struct_name(s, module, "weaveffi")),
154 TypeRef::Enum(e) => format!("weaveffi_{module}_{e}"),
155 TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
156 c_elem_type(inner, module)
157 }
158 TypeRef::Map(_, _) => "void*".into(),
159 }
160}
161
162fn c_ret_type_str(ty: &TypeRef, module: &str) -> String {
163 match ty {
164 TypeRef::I32 => "int32_t".into(),
165 TypeRef::U32 => "uint32_t".into(),
166 TypeRef::I64 => "int64_t".into(),
167 TypeRef::F64 => "double".into(),
168 TypeRef::Bool => "bool".into(),
169 TypeRef::StringUtf8 | TypeRef::BorrowedStr => "const char*".into(),
170 TypeRef::Bytes | TypeRef::BorrowedBytes => "const uint8_t*".into(),
171 TypeRef::TypedHandle(_) | TypeRef::Handle => "weaveffi_handle_t".into(),
172 TypeRef::Struct(s) => format!("{}*", c_abi_struct_name(s, module, "weaveffi")),
173 TypeRef::Enum(e) => format!("weaveffi_{module}_{e}"),
174 TypeRef::Optional(inner) => {
175 if is_c_ptr_type(inner) {
176 c_ret_type_str(inner, module)
177 } else {
178 format!("{}*", c_elem_type(inner, module))
179 }
180 }
181 TypeRef::List(inner) => format!("{}*", c_elem_type(inner, module)),
182 TypeRef::Map(_, _) => "void".into(),
183 TypeRef::Iterator(_) => "void*".into(),
184 }
185}
186
187fn napi_getter(ty: &TypeRef) -> &'static str {
188 match ty {
189 TypeRef::I32 | TypeRef::Enum(_) => "napi_get_value_int32",
190 TypeRef::U32 => "napi_get_value_uint32",
191 TypeRef::I64 | TypeRef::Handle | TypeRef::TypedHandle(_) | TypeRef::Struct(_) => {
192 "napi_get_value_int64"
193 }
194 TypeRef::F64 => "napi_get_value_double",
195 TypeRef::Bool => "napi_get_value_bool",
196 _ => "napi_get_value_int64",
197 }
198}
199
200fn render_addon_c(api: &Api, strip_module_prefix: bool, input_basename: &str) -> String {
201 let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename);
202 out.push_str(
203 "#include <node_api.h>\n#include \"weaveffi.h\"\n#include <stdlib.h>\n#include <string.h>\n\n",
204 );
205
206 let has_async = walk_modules(&api.modules).any(|m| m.functions.iter().any(|f| f.r#async));
207 if has_async {
208 out.push_str("typedef struct {\n");
209 out.push_str(" napi_env env;\n");
210 out.push_str(" napi_deferred deferred;\n");
211 out.push_str("} weaveffi_napi_async_ctx;\n\n");
212 }
213
214 let mut all_exports: Vec<(String, String)> = Vec::new();
215 let structs = struct_registry(api);
216
217 for (m, path) in walk_modules_with_path(&api.modules) {
218 for f in &m.functions {
219 let c_name = format!("weaveffi_{}_{}", path, f.name);
220 let napi_name = format!("Napi_{c_name}");
221 let js_name = wrapper_name(&path, &f.name, strip_module_prefix);
222 all_exports.push((js_name, napi_name.clone()));
223
224 if f.r#async {
225 render_async_callback(&mut out, f, &c_name, &path, &structs);
226 }
227
228 out.push_str(&format!(
229 "static napi_value {napi_name}(napi_env env, napi_callback_info info) {{\n"
230 ));
231 if f.r#async {
232 render_async_napi_body(&mut out, f, &c_name, &path);
233 } else {
234 render_napi_body(&mut out, f, &c_name, &path, &structs);
235 }
236 out.push_str("}\n\n");
237 }
238 }
239
240 out.push_str("static napi_value Init(napi_env env, napi_value exports) {\n");
241 if !all_exports.is_empty() {
242 out.push_str(" napi_property_descriptor props[] = {\n");
243 for (js_name, napi_fn) in &all_exports {
244 out.push_str(&format!(
245 " {{ \"{js_name}\", NULL, {napi_fn}, NULL, NULL, NULL, napi_default, NULL }},\n"
246 ));
247 }
248 out.push_str(" };\n");
249 out.push_str(&format!(
250 " napi_define_properties(env, exports, {}, props);\n",
251 all_exports.len()
252 ));
253 }
254 out.push_str(" return exports;\n");
255 out.push_str("}\n\n");
256 out.push_str("NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)\n\n");
257 out.push_str(&render_trailer(
258 CommentStyle::DoubleSlash,
259 "weaveffi_addon.c",
260 ));
261 out
262}
263
264fn async_cb_result_params_node(ret: Option<&TypeRef>, module: &str) -> String {
265 match ret {
266 None => String::new(),
267 Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => ", const char* result".into(),
268 Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => {
269 ", const uint8_t* result, size_t result_len".into()
270 }
271 Some(TypeRef::List(inner)) => {
272 let et = c_elem_type(inner, module);
273 format!(", {et}* result, size_t result_len")
274 }
275 Some(TypeRef::Map(k, v)) => {
276 let kt = c_elem_type(k, module);
277 let vt = c_elem_type(v, module);
278 format!(", {kt}* result_keys, {vt}* result_values, size_t result_len")
279 }
280 Some(t) => format!(", {} result", c_ret_type_str(t, module)),
281 }
282}
283
284fn emit_async_resolve_value(
285 out: &mut String,
286 ret: Option<&TypeRef>,
287 module: &str,
288 structs: &HashMap<String, StructDef>,
289) {
290 out.push_str(" napi_value val;\n");
291 match ret {
292 None => out.push_str(" napi_get_undefined(ctx->env, &val);\n"),
293 Some(TypeRef::I32) => out.push_str(" napi_create_int32(ctx->env, result, &val);\n"),
294 Some(TypeRef::U32) => out.push_str(" napi_create_uint32(ctx->env, result, &val);\n"),
295 Some(TypeRef::I64) => out.push_str(" napi_create_int64(ctx->env, result, &val);\n"),
296 Some(TypeRef::F64) => out.push_str(" napi_create_double(ctx->env, result, &val);\n"),
297 Some(TypeRef::Bool) => out.push_str(" napi_get_boolean(ctx->env, result, &val);\n"),
298 Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => {
299 out.push_str(
300 " napi_create_string_utf8(ctx->env, result, NAPI_AUTO_LENGTH, &val);\n",
301 );
302 }
303 Some(TypeRef::TypedHandle(_) | TypeRef::Handle) => {
304 out.push_str(" napi_create_int64(ctx->env, (int64_t)result, &val);\n");
305 }
306 Some(TypeRef::Struct(name)) => {
307 emit_struct_to_object(
308 out, "ctx->env", name, "result", "val", module, structs, " ", true,
309 );
310 }
311 Some(TypeRef::Enum(_)) => {
312 out.push_str(" napi_create_int32(ctx->env, (int32_t)result, &val);\n");
313 }
314 Some(TypeRef::Iterator(_)) => {
315 out.push_str(" napi_create_int64(ctx->env, (int64_t)(intptr_t)result, &val);\n");
316 }
317 _ => out.push_str(" napi_get_undefined(ctx->env, &val);\n"),
318 }
319 out.push_str(" napi_resolve_deferred(ctx->env, ctx->deferred, val);\n");
320}
321
322fn render_async_callback(
323 out: &mut String,
324 f: &Function,
325 c_name: &str,
326 module: &str,
327 structs: &HashMap<String, StructDef>,
328) {
329 let cb_name = format!("{c_name}_napi_cb");
330 let cb_result = async_cb_result_params_node(f.returns.as_ref(), module);
331
332 out.push_str(&format!(
333 "static void {cb_name}(void* context, weaveffi_error* err{cb_result}) {{\n"
334 ));
335 out.push_str(" weaveffi_napi_async_ctx* ctx = (weaveffi_napi_async_ctx*)context;\n");
336 out.push_str(" if (err != NULL && err->code != 0) {\n");
337 out.push_str(" napi_value err_msg;\n");
338 out.push_str(
339 " napi_create_string_utf8(ctx->env, err->message, NAPI_AUTO_LENGTH, &err_msg);\n",
340 );
341 out.push_str(" napi_reject_deferred(ctx->env, ctx->deferred, err_msg);\n");
342 out.push_str(" } else {\n");
343 emit_async_resolve_value(out, f.returns.as_ref(), module, structs);
344 out.push_str(" }\n");
345 out.push_str(" free(ctx);\n");
346 out.push_str("}\n\n");
347}
348
349fn render_async_napi_body(out: &mut String, f: &Function, c_name: &str, module: &str) {
350 let n = f.params.len();
351 if n > 0 {
352 out.push_str(&format!(" size_t argc = {n};\n"));
353 out.push_str(&format!(" napi_value args[{n}];\n"));
354 out.push_str(" napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
355 } else {
356 out.push_str(" size_t argc = 0;\n");
357 out.push_str(" napi_get_cb_info(env, info, &argc, NULL, NULL, NULL);\n");
358 }
359
360 let mut c_args: Vec<String> = Vec::new();
361 let mut cleanups: Vec<String> = Vec::new();
362 for (i, p) in f.params.iter().enumerate() {
363 emit_param(out, &mut c_args, &mut cleanups, &p.ty, &p.name, i, module);
364 }
365
366 out.push_str(
367 " weaveffi_napi_async_ctx* ctx = (weaveffi_napi_async_ctx*)malloc(sizeof(weaveffi_napi_async_ctx));\n",
368 );
369 out.push_str(" ctx->env = env;\n");
370 out.push_str(" napi_value promise;\n");
371 out.push_str(" napi_create_promise(env, &ctx->deferred, &promise);\n");
372
373 if f.cancellable {
374 c_args.push("NULL".into());
375 }
376
377 let cb_name = format!("{c_name}_napi_cb");
378 c_args.push(cb_name);
379 c_args.push("ctx".into());
380 let args_str = c_args.join(", ");
381 out.push_str(&format!(" {c_name}_async({args_str});\n"));
382
383 for cleanup in &cleanups {
384 out.push_str(cleanup);
385 }
386
387 out.push_str(" return promise;\n");
388}
389
390fn render_napi_body(
391 out: &mut String,
392 f: &Function,
393 c_name: &str,
394 module: &str,
395 structs: &HashMap<String, StructDef>,
396) {
397 let n = f.params.len();
398 if n > 0 {
399 out.push_str(&format!(" size_t argc = {n};\n"));
400 out.push_str(&format!(" napi_value args[{n}];\n"));
401 out.push_str(" napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
402 } else {
403 out.push_str(" size_t argc = 0;\n");
404 out.push_str(" napi_get_cb_info(env, info, &argc, NULL, NULL, NULL);\n");
405 }
406
407 let mut c_args: Vec<String> = Vec::new();
408 let mut cleanups: Vec<String> = Vec::new();
409 for (i, p) in f.params.iter().enumerate() {
410 emit_param(out, &mut c_args, &mut cleanups, &p.ty, &p.name, i, module);
411 }
412
413 out.push_str(" weaveffi_error err = {0};\n");
414
415 if let Some(ret) = &f.returns {
416 emit_ret_out_params(out, &mut c_args, ret, module);
417 }
418 c_args.push("&err".to_string());
419
420 let args_str = c_args.join(", ");
421 let ret_type = f.returns.as_ref().map(|r| c_ret_type_str(r, module));
422 match &ret_type {
423 Some(rt) if rt != "void" => {
424 out.push_str(&format!(" {rt} result = {c_name}({args_str});\n"));
425 }
426 _ => {
427 out.push_str(&format!(" {c_name}({args_str});\n"));
428 }
429 }
430
431 for cleanup in &cleanups {
432 out.push_str(cleanup);
433 }
434
435 out.push_str(" if (err.code != 0) {\n");
436 out.push_str(" napi_throw_error(env, NULL, err.message);\n");
437 out.push_str(" weaveffi_error_clear(&err);\n");
438 out.push_str(" return NULL;\n");
439 out.push_str(" }\n");
440
441 match &f.returns {
442 Some(ret) => emit_ret_to_napi(out, ret, module, &f.name, structs),
443 None => {
444 out.push_str(" napi_value ret;\n");
445 out.push_str(" napi_get_undefined(env, &ret);\n");
446 out.push_str(" return ret;\n");
447 }
448 }
449}
450
451fn emit_param(
452 out: &mut String,
453 c_args: &mut Vec<String>,
454 cleanups: &mut Vec<String>,
455 ty: &TypeRef,
456 name: &str,
457 idx: usize,
458 module: &str,
459) {
460 match ty {
461 TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Bool => {
462 let ct = c_elem_type(ty, module);
463 let getter = napi_getter(ty);
464 out.push_str(&format!(" {ct} {name};\n"));
465 out.push_str(&format!(" {getter}(env, args[{idx}], &{name});\n"));
466 c_args.push(name.into());
467 }
468 TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
469 out.push_str(&format!(" size_t {name}_len;\n"));
470 out.push_str(&format!(
471 " napi_get_value_string_utf8(env, args[{idx}], NULL, 0, &{name}_len);\n"
472 ));
473 out.push_str(&format!(
474 " char* {name} = (char*)malloc({name}_len + 1);\n"
475 ));
476 out.push_str(&format!(
477 " napi_get_value_string_utf8(env, args[{idx}], {name}, {name}_len + 1, &{name}_len);\n"
478 ));
479 c_args.push(name.into());
480 cleanups.push(format!(" free({name});\n"));
481 }
482 TypeRef::TypedHandle(_) | TypeRef::Handle => {
483 out.push_str(&format!(" int64_t {name}_raw;\n"));
484 out.push_str(&format!(
485 " napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
486 ));
487 c_args.push(format!("(weaveffi_handle_t){name}_raw"));
488 }
489 TypeRef::Enum(e) => {
490 out.push_str(&format!(" int32_t {name};\n"));
491 out.push_str(&format!(
492 " napi_get_value_int32(env, args[{idx}], &{name});\n"
493 ));
494 c_args.push(format!("(weaveffi_{module}_{e}){name}"));
495 }
496 TypeRef::Struct(s) => {
497 let abi = c_abi_struct_name(s, module, "weaveffi");
498 out.push_str(&format!(" int64_t {name}_raw;\n"));
499 out.push_str(&format!(
500 " napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
501 ));
502 c_args.push(format!("(const {abi}*)(intptr_t){name}_raw"));
503 }
504 TypeRef::Optional(inner) => {
505 out.push_str(&format!(" napi_valuetype {name}_type;\n"));
506 out.push_str(&format!(" napi_typeof(env, args[{idx}], &{name}_type);\n"));
507 emit_optional_param(out, c_args, cleanups, inner, name, idx, module);
508 }
509 TypeRef::List(inner) => {
510 emit_list_param(out, c_args, cleanups, inner, name, idx, module);
511 }
512 TypeRef::Bytes | TypeRef::BorrowedBytes => {
513 out.push_str(&format!(" void* {name}_raw;\n"));
514 out.push_str(&format!(" size_t {name}_len;\n"));
515 out.push_str(&format!(
516 " napi_get_buffer_info(env, args[{idx}], &{name}_raw, &{name}_len);\n"
517 ));
518 c_args.push(format!("(const uint8_t*){name}_raw"));
519 c_args.push(format!("{name}_len"));
520 }
521 TypeRef::Map(k, v) => {
522 emit_map_param(out, c_args, cleanups, k, v, name, idx, module);
523 }
524 TypeRef::Iterator(_) => unreachable!("iterator not valid as parameter"),
525 }
526}
527
528fn emit_opt_val(
529 out: &mut String,
530 c_args: &mut Vec<String>,
531 c_type: &str,
532 napi_fn: &str,
533 name: &str,
534 idx: usize,
535) {
536 out.push_str(&format!(" {c_type} {name}_val;\n"));
537 out.push_str(&format!(" const {c_type}* {name}_ptr = NULL;\n"));
538 out.push_str(&format!(
539 " if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
540 ));
541 out.push_str(&format!(" {napi_fn}(env, args[{idx}], &{name}_val);\n"));
542 out.push_str(&format!(" {name}_ptr = &{name}_val;\n"));
543 out.push_str(" }\n");
544 c_args.push(format!("{name}_ptr"));
545}
546
547fn emit_optional_param(
548 out: &mut String,
549 c_args: &mut Vec<String>,
550 cleanups: &mut Vec<String>,
551 inner: &TypeRef,
552 name: &str,
553 idx: usize,
554 module: &str,
555) {
556 match inner {
557 TypeRef::I32 => {
558 emit_opt_val(out, c_args, "int32_t", "napi_get_value_int32", name, idx);
559 }
560 TypeRef::U32 => {
561 emit_opt_val(out, c_args, "uint32_t", "napi_get_value_uint32", name, idx);
562 }
563 TypeRef::I64 => {
564 emit_opt_val(out, c_args, "int64_t", "napi_get_value_int64", name, idx);
565 }
566 TypeRef::F64 => {
567 emit_opt_val(out, c_args, "double", "napi_get_value_double", name, idx);
568 }
569 TypeRef::Bool => {
570 emit_opt_val(out, c_args, "bool", "napi_get_value_bool", name, idx);
571 }
572 TypeRef::TypedHandle(_) | TypeRef::Handle => {
573 out.push_str(&format!(" int64_t {name}_raw = 0;\n"));
574 out.push_str(&format!(" weaveffi_handle_t {name}_val;\n"));
575 out.push_str(&format!(" const weaveffi_handle_t* {name}_ptr = NULL;\n"));
576 out.push_str(&format!(
577 " if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
578 ));
579 out.push_str(&format!(
580 " napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
581 ));
582 out.push_str(&format!(
583 " {name}_val = (weaveffi_handle_t){name}_raw;\n"
584 ));
585 out.push_str(&format!(" {name}_ptr = &{name}_val;\n"));
586 out.push_str(" }\n");
587 c_args.push(format!("{name}_ptr"));
588 }
589 TypeRef::Enum(e) => {
590 let etype = format!("weaveffi_{module}_{e}");
591 out.push_str(&format!(" int32_t {name}_raw;\n"));
592 out.push_str(&format!(" {etype} {name}_val;\n"));
593 out.push_str(&format!(" const {etype}* {name}_ptr = NULL;\n"));
594 out.push_str(&format!(
595 " if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
596 ));
597 out.push_str(&format!(
598 " napi_get_value_int32(env, args[{idx}], &{name}_raw);\n"
599 ));
600 out.push_str(&format!(" {name}_val = ({etype}){name}_raw;\n"));
601 out.push_str(&format!(" {name}_ptr = &{name}_val;\n"));
602 out.push_str(" }\n");
603 c_args.push(format!("{name}_ptr"));
604 }
605 TypeRef::StringUtf8 => {
606 out.push_str(&format!(" char* {name} = NULL;\n"));
607 out.push_str(&format!(
608 " if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
609 ));
610 out.push_str(&format!(" size_t {name}_len;\n"));
611 out.push_str(&format!(
612 " napi_get_value_string_utf8(env, args[{idx}], NULL, 0, &{name}_len);\n"
613 ));
614 out.push_str(&format!(" {name} = (char*)malloc({name}_len + 1);\n"));
615 out.push_str(&format!(
616 " napi_get_value_string_utf8(env, args[{idx}], {name}, {name}_len + 1, &{name}_len);\n"
617 ));
618 out.push_str(" }\n");
619 c_args.push(name.into());
620 cleanups.push(format!(" free({name});\n"));
621 }
622 TypeRef::Struct(s) => {
623 let abi = c_abi_struct_name(s, module, "weaveffi");
624 out.push_str(&format!(" int64_t {name}_raw = 0;\n"));
625 out.push_str(&format!(
626 " if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
627 ));
628 out.push_str(&format!(
629 " napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
630 ));
631 out.push_str(" }\n");
632 c_args.push(format!(
633 "{name}_raw ? (const {abi}*)(intptr_t){name}_raw : NULL"
634 ));
635 }
636 _ => {
637 emit_param(out, c_args, cleanups, inner, name, idx, module);
638 }
639 }
640}
641
642fn emit_list_param(
643 out: &mut String,
644 c_args: &mut Vec<String>,
645 cleanups: &mut Vec<String>,
646 inner: &TypeRef,
647 name: &str,
648 idx: usize,
649 module: &str,
650) {
651 let et = c_elem_type(inner, module);
652 out.push_str(&format!(" uint32_t {name}_count;\n"));
653 out.push_str(&format!(
654 " napi_get_array_length(env, args[{idx}], &{name}_count);\n"
655 ));
656 out.push_str(&format!(
657 " {et}* {name}_arr = ({et}*)malloc(sizeof({et}) * ({name}_count + 1));\n"
658 ));
659 out.push_str(&format!(
660 " for (uint32_t {name}_i = 0; {name}_i < {name}_count; {name}_i++) {{\n"
661 ));
662 out.push_str(&format!(" napi_value {name}_el;\n"));
663 out.push_str(&format!(
664 " napi_get_element(env, args[{idx}], {name}_i, &{name}_el);\n"
665 ));
666
667 match inner {
668 TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Bool => {
669 let getter = napi_getter(inner);
670 out.push_str(&format!(
671 " {getter}(env, {name}_el, &{name}_arr[{name}_i]);\n"
672 ));
673 }
674 TypeRef::TypedHandle(_) | TypeRef::Handle => {
675 out.push_str(&format!(" int64_t {name}_h;\n"));
676 out.push_str(&format!(
677 " napi_get_value_int64(env, {name}_el, &{name}_h);\n"
678 ));
679 out.push_str(&format!(
680 " {name}_arr[{name}_i] = (weaveffi_handle_t){name}_h;\n"
681 ));
682 }
683 TypeRef::Enum(_) => {
684 out.push_str(&format!(" int32_t {name}_ev;\n"));
685 out.push_str(&format!(
686 " napi_get_value_int32(env, {name}_el, &{name}_ev);\n"
687 ));
688 out.push_str(&format!(" {name}_arr[{name}_i] = ({et}){name}_ev;\n"));
689 }
690 TypeRef::StringUtf8 => {
691 out.push_str(&format!(" size_t {name}_sl;\n"));
692 out.push_str(&format!(
693 " napi_get_value_string_utf8(env, {name}_el, NULL, 0, &{name}_sl);\n"
694 ));
695 out.push_str(&format!(
696 " char* {name}_s = (char*)malloc({name}_sl + 1);\n"
697 ));
698 out.push_str(&format!(
699 " napi_get_value_string_utf8(env, {name}_el, {name}_s, {name}_sl + 1, &{name}_sl);\n"
700 ));
701 out.push_str(&format!(" {name}_arr[{name}_i] = {name}_s;\n"));
702 }
703 TypeRef::Struct(_) => {
704 out.push_str(&format!(" int64_t {name}_sp;\n"));
705 out.push_str(&format!(
706 " napi_get_value_int64(env, {name}_el, &{name}_sp);\n"
707 ));
708 out.push_str(&format!(
709 " {name}_arr[{name}_i] = ({et})(intptr_t){name}_sp;\n"
710 ));
711 }
712 _ => {
713 let getter = napi_getter(inner);
714 out.push_str(&format!(
715 " {getter}(env, {name}_el, &{name}_arr[{name}_i]);\n"
716 ));
717 }
718 }
719
720 out.push_str(" }\n");
721 c_args.push(format!("{name}_arr"));
722 c_args.push(format!("(size_t){name}_count"));
723
724 if matches!(inner, TypeRef::StringUtf8) {
725 cleanups.push(format!(
726 " for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_arr[{name}_j]);\n"
727 ));
728 }
729 cleanups.push(format!(" free({name}_arr);\n"));
730}
731
732#[allow(clippy::too_many_arguments)]
733fn emit_map_param(
734 out: &mut String,
735 c_args: &mut Vec<String>,
736 cleanups: &mut Vec<String>,
737 k: &TypeRef,
738 v: &TypeRef,
739 name: &str,
740 idx: usize,
741 module: &str,
742) {
743 let kt = c_elem_type(k, module);
744 let vt = c_elem_type(v, module);
745 out.push_str(&format!(" napi_value {name}_keys_napi;\n"));
746 out.push_str(&format!(
747 " napi_get_property_names(env, args[{idx}], &{name}_keys_napi);\n"
748 ));
749 out.push_str(&format!(" uint32_t {name}_count;\n"));
750 out.push_str(&format!(
751 " napi_get_array_length(env, {name}_keys_napi, &{name}_count);\n"
752 ));
753 out.push_str(&format!(
754 " {kt}* {name}_keys = ({kt}*)malloc(sizeof({kt}) * ({name}_count + 1));\n"
755 ));
756 out.push_str(&format!(
757 " {vt}* {name}_values = ({vt}*)malloc(sizeof({vt}) * ({name}_count + 1));\n"
758 ));
759 out.push_str(&format!(
760 " for (uint32_t {name}_i = 0; {name}_i < {name}_count; {name}_i++) {{\n"
761 ));
762 out.push_str(&format!(" napi_value {name}_k;\n"));
763 out.push_str(&format!(
764 " napi_get_element(env, {name}_keys_napi, {name}_i, &{name}_k);\n"
765 ));
766
767 if matches!(k, TypeRef::StringUtf8) {
768 out.push_str(&format!(" size_t {name}_kl;\n"));
769 out.push_str(&format!(
770 " napi_get_value_string_utf8(env, {name}_k, NULL, 0, &{name}_kl);\n"
771 ));
772 out.push_str(&format!(
773 " char* {name}_ks = (char*)malloc({name}_kl + 1);\n"
774 ));
775 out.push_str(&format!(
776 " napi_get_value_string_utf8(env, {name}_k, {name}_ks, {name}_kl + 1, &{name}_kl);\n"
777 ));
778 out.push_str(&format!(" {name}_keys[{name}_i] = {name}_ks;\n"));
779 } else {
780 out.push_str(&format!(" napi_value {name}_kn;\n"));
781 out.push_str(&format!(
782 " napi_coerce_to_number(env, {name}_k, &{name}_kn);\n"
783 ));
784 let kgetter = napi_getter(k);
785 out.push_str(&format!(
786 " {kgetter}(env, {name}_kn, &{name}_keys[{name}_i]);\n"
787 ));
788 }
789
790 out.push_str(&format!(" napi_value {name}_v;\n"));
791 out.push_str(&format!(
792 " napi_get_property(env, args[{idx}], {name}_k, &{name}_v);\n"
793 ));
794
795 if matches!(v, TypeRef::StringUtf8) {
796 out.push_str(&format!(" size_t {name}_vl;\n"));
797 out.push_str(&format!(
798 " napi_get_value_string_utf8(env, {name}_v, NULL, 0, &{name}_vl);\n"
799 ));
800 out.push_str(&format!(
801 " char* {name}_vs = (char*)malloc({name}_vl + 1);\n"
802 ));
803 out.push_str(&format!(
804 " napi_get_value_string_utf8(env, {name}_v, {name}_vs, {name}_vl + 1, &{name}_vl);\n"
805 ));
806 out.push_str(&format!(" {name}_values[{name}_i] = {name}_vs;\n"));
807 } else {
808 let vgetter = napi_getter(v);
809 out.push_str(&format!(
810 " {vgetter}(env, {name}_v, &{name}_values[{name}_i]);\n"
811 ));
812 }
813
814 out.push_str(" }\n");
815 c_args.push(format!("{name}_keys"));
816 c_args.push(format!("{name}_values"));
817 c_args.push(format!("(size_t){name}_count"));
818
819 if matches!(k, TypeRef::StringUtf8) {
820 cleanups.push(format!(
821 " for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_keys[{name}_j]);\n"
822 ));
823 }
824 cleanups.push(format!(" free({name}_keys);\n"));
825 if matches!(v, TypeRef::StringUtf8) {
826 cleanups.push(format!(
827 " for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_values[{name}_j]);\n"
828 ));
829 }
830 cleanups.push(format!(" free({name}_values);\n"));
831}
832
833fn emit_ret_out_params(out: &mut String, c_args: &mut Vec<String>, ty: &TypeRef, module: &str) {
834 match ty {
835 TypeRef::Bytes | TypeRef::List(_) => {
836 out.push_str(" size_t out_len;\n");
837 c_args.push("&out_len".into());
838 }
839 TypeRef::Map(k, v) => {
840 let kt = c_elem_type(k, module);
841 let vt = c_elem_type(v, module);
842 out.push_str(&format!(" {kt}* out_keys = NULL;\n"));
843 out.push_str(&format!(" {vt}* out_values = NULL;\n"));
844 out.push_str(" size_t out_len = 0;\n");
845 c_args.push("out_keys".into());
846 c_args.push("out_values".into());
847 c_args.push("&out_len".into());
848 }
849 TypeRef::Optional(inner) if is_c_ptr_type(inner) => {
850 emit_ret_out_params(out, c_args, inner, module);
851 }
852 _ => {}
853 }
854}
855
856fn struct_registry(api: &Api) -> HashMap<String, StructDef> {
860 walk_modules(&api.modules)
861 .flat_map(|m| m.structs.iter())
862 .map(|s| (s.name.clone(), s.clone()))
863 .collect()
864}
865
866#[allow(clippy::too_many_arguments)]
871fn emit_struct_to_object(
872 out: &mut String,
873 env: &str,
874 struct_name: &str,
875 ptr_expr: &str,
876 obj_var: &str,
877 module: &str,
878 structs: &HashMap<String, StructDef>,
879 indent: &str,
880 destroy: bool,
881) {
882 let abi = c_abi_struct_name(struct_name, module, "weaveffi");
883 let Some(def) = structs.get(local_type_name(struct_name)).cloned() else {
884 out.push_str(&format!(
886 "{indent}napi_create_int64({env}, (int64_t)(intptr_t){ptr_expr}, &{obj_var});\n"
887 ));
888 return;
889 };
890 let p = format!("{obj_var}_p");
891 out.push_str(&format!("{indent}{{\n"));
892 out.push_str(&format!("{indent} {abi}* {p} = ({abi}*){ptr_expr};\n"));
893 out.push_str(&format!(
894 "{indent} napi_create_object({env}, &{obj_var});\n"
895 ));
896 for field in &def.fields {
897 let getter = format!("{abi}_get_{}", field.name);
898 let fv = format!("{obj_var}_{}", field.name);
899 out.push_str(&format!("{indent} napi_value {fv};\n"));
900 emit_struct_field_to_napi(
901 out,
902 env,
903 &field.ty,
904 &getter,
905 &p,
906 &fv,
907 module,
908 structs,
909 &format!("{indent} "),
910 );
911 out.push_str(&format!(
912 "{indent} napi_set_named_property({env}, {obj_var}, \"{}\", {fv});\n",
913 field.name
914 ));
915 }
916 if destroy {
917 out.push_str(&format!("{indent} {abi}_destroy({p});\n"));
918 }
919 out.push_str(&format!("{indent}}}\n"));
920}
921
922#[allow(clippy::too_many_arguments)]
927fn emit_struct_field_to_napi(
928 out: &mut String,
929 env: &str,
930 ty: &TypeRef,
931 getter: &str,
932 pv: &str,
933 fv: &str,
934 module: &str,
935 structs: &HashMap<String, StructDef>,
936 indent: &str,
937) {
938 match ty {
939 TypeRef::I32 => out.push_str(&format!(
940 "{indent}napi_create_int32({env}, {getter}({pv}), &{fv});\n"
941 )),
942 TypeRef::U32 => out.push_str(&format!(
943 "{indent}napi_create_uint32({env}, {getter}({pv}), &{fv});\n"
944 )),
945 TypeRef::I64 => out.push_str(&format!(
946 "{indent}napi_create_int64({env}, {getter}({pv}), &{fv});\n"
947 )),
948 TypeRef::F64 => out.push_str(&format!(
949 "{indent}napi_create_double({env}, {getter}({pv}), &{fv});\n"
950 )),
951 TypeRef::Bool => out.push_str(&format!(
952 "{indent}napi_get_boolean({env}, {getter}({pv}), &{fv});\n"
953 )),
954 TypeRef::Enum(_) => out.push_str(&format!(
955 "{indent}napi_create_int32({env}, (int32_t){getter}({pv}), &{fv});\n"
956 )),
957 TypeRef::Handle | TypeRef::TypedHandle(_) => out.push_str(&format!(
958 "{indent}napi_create_int64({env}, (int64_t){getter}({pv}), &{fv});\n"
959 )),
960 TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
961 let owned = matches!(ty, TypeRef::StringUtf8);
962 out.push_str(&format!("{indent}{{\n"));
963 out.push_str(&format!(
964 "{indent} char* {fv}_s = (char*){getter}({pv});\n"
965 ));
966 out.push_str(&format!(
967 "{indent} napi_create_string_utf8({env}, {fv}_s, NAPI_AUTO_LENGTH, &{fv});\n"
968 ));
969 if owned {
970 out.push_str(&format!("{indent} weaveffi_free_string({fv}_s);\n"));
971 }
972 out.push_str(&format!("{indent}}}\n"));
973 }
974 TypeRef::Struct(name) => {
975 emit_struct_to_object(
976 out,
977 env,
978 name,
979 &format!("{getter}({pv})"),
980 fv,
981 module,
982 structs,
983 indent,
984 true,
985 );
986 }
987 TypeRef::Optional(inner)
988 if matches!(inner.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) =>
989 {
990 let owned = matches!(inner.as_ref(), TypeRef::StringUtf8);
991 out.push_str(&format!("{indent}{{\n"));
992 out.push_str(&format!(
993 "{indent} char* {fv}_s = (char*){getter}({pv});\n"
994 ));
995 out.push_str(&format!(
996 "{indent} if ({fv}_s == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
997 ));
998 out.push_str(&format!(
999 "{indent} else {{ napi_create_string_utf8({env}, {fv}_s, NAPI_AUTO_LENGTH, &{fv});"
1000 ));
1001 if owned {
1002 out.push_str(&format!(" weaveffi_free_string({fv}_s);"));
1003 }
1004 out.push_str(" }\n");
1005 out.push_str(&format!("{indent}}}\n"));
1006 }
1007 TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Struct(_)) => {
1008 let TypeRef::Struct(name) = inner.as_ref() else {
1009 unreachable!()
1010 };
1011 let abi = c_abi_struct_name(name, module, "weaveffi");
1012 out.push_str(&format!("{indent}{{\n"));
1013 out.push_str(&format!("{indent} {abi}* {fv}_sp = {getter}({pv});\n"));
1014 out.push_str(&format!(
1015 "{indent} if ({fv}_sp == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
1016 ));
1017 out.push_str(&format!("{indent} else {{\n"));
1018 emit_struct_to_object(
1019 out,
1020 env,
1021 name,
1022 &format!("{fv}_sp"),
1023 fv,
1024 module,
1025 structs,
1026 &format!("{indent} "),
1027 true,
1028 );
1029 out.push_str(&format!("{indent} }}\n"));
1030 out.push_str(&format!("{indent}}}\n"));
1031 }
1032 _ => out.push_str(&format!("{indent}napi_get_null({env}, &{fv});\n")),
1033 }
1034}
1035
1036fn emit_ret_to_napi(
1037 out: &mut String,
1038 ty: &TypeRef,
1039 module: &str,
1040 fn_name: &str,
1041 structs: &HashMap<String, StructDef>,
1042) {
1043 out.push_str(" napi_value ret;\n");
1044 match ty {
1045 TypeRef::I32 => out.push_str(" napi_create_int32(env, result, &ret);\n"),
1046 TypeRef::U32 => out.push_str(" napi_create_uint32(env, result, &ret);\n"),
1047 TypeRef::I64 => out.push_str(" napi_create_int64(env, result, &ret);\n"),
1048 TypeRef::F64 => out.push_str(" napi_create_double(env, result, &ret);\n"),
1049 TypeRef::Bool => out.push_str(" napi_get_boolean(env, result, &ret);\n"),
1050 TypeRef::StringUtf8 => {
1051 out.push_str(" napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
1052 out.push_str(" weaveffi_free_string(result);\n");
1053 }
1054 TypeRef::BorrowedStr => {
1055 out.push_str(" napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
1056 }
1057 TypeRef::TypedHandle(_) | TypeRef::Handle => {
1058 out.push_str(" napi_create_int64(env, (int64_t)result, &ret);\n");
1059 }
1060 TypeRef::Struct(name) => {
1061 emit_struct_to_object(
1062 out, "env", name, "result", "ret", module, structs, " ", true,
1063 );
1064 }
1065 TypeRef::Enum(_) => {
1066 out.push_str(" napi_create_int32(env, (int32_t)result, &ret);\n");
1067 }
1068 TypeRef::Bytes => {
1069 out.push_str(" napi_create_buffer_copy(env, out_len, result, NULL, &ret);\n");
1070 out.push_str(" weaveffi_free_bytes((uint8_t*)result, out_len);\n");
1071 }
1072 TypeRef::BorrowedBytes => {
1073 out.push_str(" napi_create_buffer_copy(env, out_len, result, NULL, &ret);\n");
1074 }
1075 TypeRef::Optional(inner) => {
1076 out.push_str(" if (result == NULL) {\n");
1077 out.push_str(" napi_get_null(env, &ret);\n");
1078 out.push_str(" } else {\n");
1079 emit_optional_ret_inner(out, inner, module, structs);
1080 out.push_str(" }\n");
1081 }
1082 TypeRef::List(inner) => emit_list_ret(out, inner, module, " ", structs),
1083 TypeRef::Map(_, _) => {
1084 out.push_str(" napi_create_object(env, &ret);\n");
1085 }
1086 TypeRef::Iterator(inner) => {
1087 let fn_pascal = fn_name.to_upper_camel_case();
1088 let iter_type = format!("weaveffi_{module}_{fn_pascal}Iterator");
1089 let et = c_elem_type(inner, module);
1090 out.push_str(" napi_create_array(env, &ret);\n");
1091 out.push_str(" uint32_t iter_idx = 0;\n");
1092 out.push_str(&format!(" {et} iter_item;\n"));
1093 out.push_str(&format!(
1094 " while ({iter_type}_next(result, &iter_item)) {{\n"
1095 ));
1096 out.push_str(" napi_value elem;\n");
1097 match inner.as_ref() {
1098 TypeRef::I32 => {
1099 out.push_str(" napi_create_int32(env, iter_item, &elem);\n");
1100 }
1101 TypeRef::U32 => {
1102 out.push_str(" napi_create_uint32(env, iter_item, &elem);\n");
1103 }
1104 TypeRef::I64 => {
1105 out.push_str(" napi_create_int64(env, iter_item, &elem);\n");
1106 }
1107 TypeRef::F64 => {
1108 out.push_str(" napi_create_double(env, iter_item, &elem);\n");
1109 }
1110 TypeRef::Bool => {
1111 out.push_str(" napi_get_boolean(env, iter_item, &elem);\n");
1112 }
1113 TypeRef::TypedHandle(_) | TypeRef::Handle => {
1114 out.push_str(" napi_create_int64(env, (int64_t)iter_item, &elem);\n");
1115 }
1116 TypeRef::StringUtf8 => {
1117 out.push_str(
1118 " napi_create_string_utf8(env, iter_item, NAPI_AUTO_LENGTH, &elem);\n",
1119 );
1120 out.push_str(" weaveffi_free_string(iter_item);\n");
1121 }
1122 TypeRef::Struct(_) | TypeRef::Enum(_) => {
1123 out.push_str(
1124 " napi_create_int64(env, (int64_t)(intptr_t)iter_item, &elem);\n",
1125 );
1126 }
1127 _ => {
1128 out.push_str(" napi_create_int64(env, (int64_t)iter_item, &elem);\n");
1129 }
1130 }
1131 out.push_str(" napi_set_element(env, ret, iter_idx++, elem);\n");
1132 out.push_str(" }\n");
1133 out.push_str(&format!(" {iter_type}_destroy(result);\n"));
1134 }
1135 }
1136 out.push_str(" return ret;\n");
1137}
1138
1139fn emit_optional_ret_inner(
1140 out: &mut String,
1141 inner: &TypeRef,
1142 module: &str,
1143 structs: &HashMap<String, StructDef>,
1144) {
1145 match inner {
1146 TypeRef::I32 => {
1147 out.push_str(" napi_create_int32(env, *result, &ret);\n");
1148 out.push_str(" free(result);\n");
1149 }
1150 TypeRef::U32 => {
1151 out.push_str(" napi_create_uint32(env, *result, &ret);\n");
1152 out.push_str(" free(result);\n");
1153 }
1154 TypeRef::I64 => {
1155 out.push_str(" napi_create_int64(env, *result, &ret);\n");
1156 out.push_str(" free(result);\n");
1157 }
1158 TypeRef::F64 => {
1159 out.push_str(" napi_create_double(env, *result, &ret);\n");
1160 out.push_str(" free(result);\n");
1161 }
1162 TypeRef::Bool => {
1163 out.push_str(" napi_get_boolean(env, *result, &ret);\n");
1164 out.push_str(" free(result);\n");
1165 }
1166 TypeRef::TypedHandle(_) | TypeRef::Handle => {
1167 out.push_str(" napi_create_int64(env, (int64_t)*result, &ret);\n");
1168 out.push_str(" free(result);\n");
1169 }
1170 TypeRef::Enum(_) => {
1171 out.push_str(" napi_create_int32(env, (int32_t)*result, &ret);\n");
1172 out.push_str(" free(result);\n");
1173 }
1174 TypeRef::StringUtf8 => {
1175 out.push_str(" napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
1176 out.push_str(" weaveffi_free_string(result);\n");
1177 }
1178 TypeRef::Struct(name) => {
1179 emit_struct_to_object(
1180 out, "env", name, "result", "ret", module, structs, " ", true,
1181 );
1182 }
1183 TypeRef::List(li) => emit_list_ret(out, li, module, " ", structs),
1184 _ => out.push_str(" napi_get_null(env, &ret);\n"),
1185 }
1186}
1187
1188fn emit_list_ret(
1189 out: &mut String,
1190 inner: &TypeRef,
1191 module: &str,
1192 ind: &str,
1193 structs: &HashMap<String, StructDef>,
1194) {
1195 out.push_str(&format!(
1196 "{ind}napi_create_array_with_length(env, out_len, &ret);\n"
1197 ));
1198 out.push_str(&format!(
1199 "{ind}for (size_t ret_i = 0; ret_i < out_len; ret_i++) {{\n"
1200 ));
1201 out.push_str(&format!("{ind} napi_value elem;\n"));
1202 match inner {
1203 TypeRef::I32 => out.push_str(&format!(
1204 "{ind} napi_create_int32(env, result[ret_i], &elem);\n"
1205 )),
1206 TypeRef::U32 => out.push_str(&format!(
1207 "{ind} napi_create_uint32(env, result[ret_i], &elem);\n"
1208 )),
1209 TypeRef::I64 => out.push_str(&format!(
1210 "{ind} napi_create_int64(env, result[ret_i], &elem);\n"
1211 )),
1212 TypeRef::F64 => out.push_str(&format!(
1213 "{ind} napi_create_double(env, result[ret_i], &elem);\n"
1214 )),
1215 TypeRef::Bool => out.push_str(&format!(
1216 "{ind} napi_get_boolean(env, result[ret_i], &elem);\n"
1217 )),
1218 TypeRef::TypedHandle(_) | TypeRef::Handle => out.push_str(&format!(
1219 "{ind} napi_create_int64(env, (int64_t)result[ret_i], &elem);\n"
1220 )),
1221 TypeRef::StringUtf8 => {
1222 out.push_str(&format!(
1223 "{ind} napi_create_string_utf8(env, result[ret_i], NAPI_AUTO_LENGTH, &elem);\n"
1224 ));
1225 out.push_str(&format!("{ind} weaveffi_free_string(result[ret_i]);\n"));
1226 }
1227 TypeRef::Enum(_) => out.push_str(&format!(
1228 "{ind} napi_create_int32(env, (int32_t)result[ret_i], &elem);\n"
1229 )),
1230 TypeRef::Struct(name) => {
1231 let elem_indent = format!("{ind} ");
1232 emit_struct_to_object(
1233 out,
1234 "env",
1235 name,
1236 "result[ret_i]",
1237 "elem",
1238 module,
1239 structs,
1240 &elem_indent,
1241 true,
1242 );
1243 }
1244 _ => out.push_str(&format!(
1245 "{ind} napi_create_int64(env, (int64_t)result[ret_i], &elem);\n"
1246 )),
1247 }
1248 out.push_str(&format!(
1249 "{ind} napi_set_element(env, ret, (uint32_t)ret_i, elem);\n"
1250 ));
1251 out.push_str(&format!("{ind}}}\n"));
1252 out.push_str(&format!("{ind}free(result);\n"));
1253}
1254
1255fn ts_type_for(ty: &TypeRef) -> String {
1256 match ty {
1257 TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 => "number".into(),
1258 TypeRef::Bool => "boolean".into(),
1259 TypeRef::StringUtf8 | TypeRef::BorrowedStr => "string".into(),
1260 TypeRef::Bytes | TypeRef::BorrowedBytes => "Buffer".into(),
1261 TypeRef::Handle => "bigint".into(),
1262 TypeRef::TypedHandle(name) => name.clone(),
1263 TypeRef::Struct(name) => local_type_name(name).to_string(),
1264 TypeRef::Enum(name) => name.clone(),
1265 TypeRef::Optional(inner) => format!("{} | null", ts_type_for(inner)),
1266 TypeRef::List(inner) => {
1267 let inner_ts = ts_type_for(inner);
1268 if matches!(inner.as_ref(), TypeRef::Optional(_)) {
1269 format!("({inner_ts})[]")
1270 } else {
1271 format!("{inner_ts}[]")
1272 }
1273 }
1274 TypeRef::Map(k, v) => format!("Record<{}, {}>", ts_type_for(k), ts_type_for(v)),
1275 TypeRef::Iterator(inner) => {
1276 let t = ts_type_for(inner);
1277 format!("{t}[]")
1278 }
1279 }
1280}
1281
1282fn emit_doc(out: &mut String, doc: &Option<String>, indent: &str) {
1285 common_emit_doc(out, doc, indent, DocCommentStyle::Javadoc);
1286}
1287
1288fn emit_fn_doc(
1291 out: &mut String,
1292 doc: &Option<String>,
1293 params: &[weaveffi_ir::ir::Param],
1294 indent: &str,
1295 extra_tags: &[String],
1296) {
1297 let has_param_docs = params.iter().any(|p| p.doc.is_some());
1298 let trimmed_doc = doc.as_ref().map(|d| d.trim()).filter(|d| !d.is_empty());
1299 if trimmed_doc.is_none() && !has_param_docs && extra_tags.is_empty() {
1300 return;
1301 }
1302 out.push_str(indent);
1303 out.push_str("/**\n");
1304 if let Some(d) = trimmed_doc {
1305 for line in d.lines() {
1306 out.push_str(indent);
1307 if line.is_empty() {
1308 out.push_str(" *\n");
1309 } else {
1310 out.push_str(" * ");
1311 out.push_str(line);
1312 out.push('\n');
1313 }
1314 }
1315 }
1316 for p in params {
1317 if let Some(pdoc) = &p.doc {
1318 let pdoc = pdoc.trim();
1319 if pdoc.is_empty() {
1320 continue;
1321 }
1322 let mut lines = pdoc.lines();
1323 if let Some(first) = lines.next() {
1324 out.push_str(indent);
1325 out.push_str(&format!(" * @param {} {}\n", p.name, first));
1326 }
1327 for line in lines {
1328 out.push_str(indent);
1329 if line.is_empty() {
1330 out.push_str(" *\n");
1331 } else {
1332 out.push_str(" * ");
1333 out.push_str(line);
1334 out.push('\n');
1335 }
1336 }
1337 }
1338 }
1339 for tag in extra_tags {
1340 out.push_str(indent);
1341 out.push_str(" * ");
1342 out.push_str(tag);
1343 out.push('\n');
1344 }
1345 out.push_str(indent);
1346 out.push_str(" */\n");
1347}
1348
1349fn render_struct_builder_dts(out: &mut String, s: &StructDef) {
1350 let name = &s.name;
1351 emit_doc(out, &s.doc, "");
1352 out.push_str(&format!("export interface {}Builder {{\n", s.name));
1353 for field in &s.fields {
1354 let method = format!("with{}", field.name.to_upper_camel_case());
1355 let ts = ts_type_for(&field.ty);
1356 emit_doc(out, &field.doc, " ");
1357 out.push_str(&format!(" {method}(value: {ts}): {name}Builder;\n"));
1358 }
1359 out.push_str(&format!(" build(): {name};\n"));
1360 out.push_str("}\n");
1361}
1362
1363fn render_node_dts(api: &Api, strip_module_prefix: bool, input_basename: &str) -> String {
1364 let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename);
1365 out.push_str("// Generated types for WeaveFFI functions\n");
1366 for (m, path) in walk_modules_with_path(&api.modules) {
1367 for s in &m.structs {
1368 emit_doc(&mut out, &s.doc, "");
1369 out.push_str(&format!("export interface {} {{\n", s.name));
1370 for field in &s.fields {
1371 emit_doc(&mut out, &field.doc, " ");
1372 out.push_str(&format!(" {}: {};\n", field.name, ts_type_for(&field.ty)));
1373 }
1374 out.push_str("}\n");
1375 if s.builder {
1376 render_struct_builder_dts(&mut out, s);
1377 }
1378 }
1379 for e in &m.enums {
1380 emit_doc(&mut out, &e.doc, "");
1381 out.push_str(&format!("export enum {} {{\n", e.name));
1382 for v in &e.variants {
1383 emit_doc(&mut out, &v.doc, " ");
1384 out.push_str(&format!(" {} = {},\n", v.name, v.value));
1385 }
1386 out.push_str("}\n");
1387 }
1388 out.push_str(&format!("// module {}\n", path));
1389 for f in &m.functions {
1390 let params: Vec<String> = f
1391 .params
1392 .iter()
1393 .map(|p| format!("{}: {}", p.name, ts_type_for(&p.ty)))
1394 .collect();
1395 let base_ret = match &f.returns {
1396 Some(ty) => ts_type_for(ty),
1397 None => "void".into(),
1398 };
1399 let ret = if f.r#async {
1400 format!("Promise<{base_ret}>")
1401 } else {
1402 base_ret
1403 };
1404 let ts_name = wrapper_name(&path, &f.name, strip_module_prefix);
1405 let mut tags = vec![format!("Maps to C function: weaveffi_{}_{}", path, f.name)];
1406 if let Some(msg) = &f.deprecated {
1407 tags.push(format!("@deprecated {}", msg));
1408 }
1409 emit_fn_doc(&mut out, &f.doc, &f.params, "", &tags);
1410 out.push_str(&format!(
1411 "export function {}({}): {}\n",
1412 ts_name,
1413 params.join(", "),
1414 ret
1415 ));
1416 }
1417 }
1418 out.push('\n');
1419 out.push_str(&render_trailer(CommentStyle::DoubleSlash, "types.d.ts"));
1420 out
1421}
1422
1423#[cfg(test)]
1424mod tests {
1425 use super::*;
1426 use weaveffi_ir::ir::{EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField};
1427
1428 fn make_api(modules: Vec<Module>) -> Api {
1429 Api {
1430 version: "0.1.0".into(),
1431 modules,
1432 generators: None,
1433 }
1434 }
1435
1436 fn make_module(name: &str) -> Module {
1437 Module {
1438 name: name.into(),
1439 functions: vec![],
1440 structs: vec![],
1441 enums: vec![],
1442 callbacks: vec![],
1443 listeners: vec![],
1444 errors: None,
1445 modules: vec![],
1446 }
1447 }
1448
1449 #[test]
1450 fn ts_type_for_primitives() {
1451 assert_eq!(ts_type_for(&TypeRef::I32), "number");
1452 assert_eq!(ts_type_for(&TypeRef::Bool), "boolean");
1453 assert_eq!(ts_type_for(&TypeRef::StringUtf8), "string");
1454 assert_eq!(ts_type_for(&TypeRef::Bytes), "Buffer");
1455 assert_eq!(ts_type_for(&TypeRef::Handle), "bigint");
1456 }
1457
1458 #[test]
1459 fn ts_type_for_struct_and_enum() {
1460 assert_eq!(ts_type_for(&TypeRef::Struct("Contact".into())), "Contact");
1461 assert_eq!(ts_type_for(&TypeRef::Enum("Color".into())), "Color");
1462 }
1463
1464 #[test]
1465 fn ts_type_for_optional() {
1466 let ty = TypeRef::Optional(Box::new(TypeRef::StringUtf8));
1467 assert_eq!(ts_type_for(&ty), "string | null");
1468 }
1469
1470 #[test]
1471 fn ts_type_for_list() {
1472 let ty = TypeRef::List(Box::new(TypeRef::I32));
1473 assert_eq!(ts_type_for(&ty), "number[]");
1474 }
1475
1476 #[test]
1477 fn ts_type_for_list_of_optional() {
1478 let ty = TypeRef::List(Box::new(TypeRef::Optional(Box::new(TypeRef::I32))));
1479 assert_eq!(ts_type_for(&ty), "(number | null)[]");
1480 }
1481
1482 #[test]
1483 fn ts_type_for_map() {
1484 let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32));
1485 assert_eq!(ts_type_for(&ty), "Record<string, number>");
1486 }
1487
1488 #[test]
1489 fn ts_type_for_optional_list() {
1490 let ty = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::I32))));
1491 assert_eq!(ts_type_for(&ty), "number[] | null");
1492 }
1493
1494 #[test]
1495 fn generate_node_dts_with_structs() {
1496 let mut m = make_module("contacts");
1497 m.structs.push(StructDef {
1498 name: "Contact".into(),
1499 doc: None,
1500 fields: vec![
1501 StructField {
1502 name: "name".into(),
1503 ty: TypeRef::StringUtf8,
1504 doc: None,
1505 default: None,
1506 },
1507 StructField {
1508 name: "age".into(),
1509 ty: TypeRef::I32,
1510 doc: None,
1511 default: None,
1512 },
1513 StructField {
1514 name: "active".into(),
1515 ty: TypeRef::Bool,
1516 doc: None,
1517 default: None,
1518 },
1519 ],
1520 builder: false,
1521 });
1522 m.enums.push(EnumDef {
1523 name: "Color".into(),
1524 doc: None,
1525 variants: vec![
1526 EnumVariant {
1527 name: "Red".into(),
1528 value: 0,
1529 doc: None,
1530 },
1531 EnumVariant {
1532 name: "Green".into(),
1533 value: 1,
1534 doc: None,
1535 },
1536 EnumVariant {
1537 name: "Blue".into(),
1538 value: 2,
1539 doc: None,
1540 },
1541 ],
1542 });
1543 m.functions.push(Function {
1544 name: "get_contact".into(),
1545 params: vec![Param {
1546 name: "id".into(),
1547 ty: TypeRef::I32,
1548 mutable: false,
1549 doc: None,
1550 }],
1551 returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
1552 "Contact".into(),
1553 )))),
1554 doc: None,
1555 r#async: false,
1556 cancellable: false,
1557 deprecated: None,
1558 since: None,
1559 });
1560 m.functions.push(Function {
1561 name: "list_contacts".into(),
1562 params: vec![],
1563 returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
1564 doc: None,
1565 r#async: false,
1566 cancellable: false,
1567 deprecated: None,
1568 since: None,
1569 });
1570
1571 let dts = render_node_dts(&make_api(vec![m]), true, "weaveffi.yml");
1572
1573 assert!(dts.contains("export interface Contact {"));
1574 assert!(dts.contains(" name: string;"));
1575 assert!(dts.contains(" age: number;"));
1576 assert!(dts.contains(" active: boolean;"));
1577 assert!(dts.contains("export enum Color {"));
1578 assert!(dts.contains(" Red = 0,"));
1579 assert!(dts.contains(" Green = 1,"));
1580 assert!(dts.contains(" Blue = 2,"));
1581 assert!(dts.contains("export function get_contact(id: number): Contact | null"));
1582 assert!(dts.contains("export function list_contacts(): Contact[]"));
1583
1584 let iface_pos = dts.find("export interface Contact").unwrap();
1585 let enum_pos = dts.find("export enum Color").unwrap();
1586 let fn_pos = dts.find("export function get_contact").unwrap();
1587 assert!(
1588 iface_pos < fn_pos,
1589 "interface should appear before functions"
1590 );
1591 assert!(enum_pos < fn_pos, "enum should appear before functions");
1592 }
1593
1594 #[test]
1595 fn node_generates_binding_gyp() {
1596 let api = make_api(vec![{
1597 let mut m = make_module("math");
1598 m.functions.push(Function {
1599 name: "add".into(),
1600 params: vec![
1601 Param {
1602 name: "a".into(),
1603 ty: TypeRef::I32,
1604 mutable: false,
1605 doc: None,
1606 },
1607 Param {
1608 name: "b".into(),
1609 ty: TypeRef::I32,
1610 mutable: false,
1611 doc: None,
1612 },
1613 ],
1614 returns: Some(TypeRef::I32),
1615 doc: None,
1616 r#async: false,
1617 cancellable: false,
1618 deprecated: None,
1619 since: None,
1620 });
1621 m
1622 }]);
1623
1624 let tmp = std::env::temp_dir().join("weaveffi_test_node_binding_gyp");
1625 let _ = std::fs::remove_dir_all(&tmp);
1626 std::fs::create_dir_all(&tmp).unwrap();
1627 let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
1628
1629 NodeGenerator
1630 .generate(&api, out_dir, &NodeConfig::default())
1631 .unwrap();
1632
1633 let gyp = std::fs::read_to_string(tmp.join("node").join("binding.gyp")).unwrap();
1634 assert!(
1635 gyp.contains("\"target_name\": \"weaveffi\""),
1636 "missing target_name: {gyp}"
1637 );
1638 assert!(
1639 gyp.contains("weaveffi_addon.c"),
1640 "missing source file: {gyp}"
1641 );
1642
1643 let addon = std::fs::read_to_string(tmp.join("node").join("weaveffi_addon.c")).unwrap();
1644 assert!(
1645 addon.contains("napi_value Init("),
1646 "missing Init function: {addon}"
1647 );
1648 assert!(
1649 addon.contains("weaveffi_math_add"),
1650 "missing C ABI call: {addon}"
1651 );
1652 assert!(
1653 addon.contains("napi_get_cb_info"),
1654 "missing napi_get_cb_info call: {addon}"
1655 );
1656
1657 let pkg = std::fs::read_to_string(tmp.join("node").join("package.json")).unwrap();
1658 assert!(pkg.contains("\"gypfile\": true"), "missing gypfile: {pkg}");
1659 assert!(
1660 pkg.contains("node-gyp rebuild"),
1661 "missing install script: {pkg}"
1662 );
1663
1664 let _ = std::fs::remove_dir_all(&tmp);
1665 }
1666
1667 #[test]
1668 fn generate_node_dts_with_structs_and_enums() {
1669 let api = make_api(vec![Module {
1670 name: "contacts".to_string(),
1671 functions: vec![
1672 Function {
1673 name: "get_contact".to_string(),
1674 params: vec![Param {
1675 name: "id".to_string(),
1676 ty: TypeRef::I32,
1677 mutable: false,
1678 doc: None,
1679 }],
1680 returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
1681 "Contact".into(),
1682 )))),
1683 doc: None,
1684 r#async: false,
1685 cancellable: false,
1686 deprecated: None,
1687 since: None,
1688 },
1689 Function {
1690 name: "list_contacts".to_string(),
1691 params: vec![],
1692 returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
1693 doc: None,
1694 r#async: false,
1695 cancellable: false,
1696 deprecated: None,
1697 since: None,
1698 },
1699 Function {
1700 name: "set_favorite_color".to_string(),
1701 params: vec![
1702 Param {
1703 name: "contact_id".to_string(),
1704 ty: TypeRef::I32,
1705 mutable: false,
1706 doc: None,
1707 },
1708 Param {
1709 name: "color".to_string(),
1710 ty: TypeRef::Optional(Box::new(TypeRef::Enum("Color".into()))),
1711 mutable: false,
1712 doc: None,
1713 },
1714 ],
1715 returns: None,
1716 doc: None,
1717 r#async: false,
1718 cancellable: false,
1719 deprecated: None,
1720 since: None,
1721 },
1722 Function {
1723 name: "get_tags".to_string(),
1724 params: vec![Param {
1725 name: "contact_id".to_string(),
1726 ty: TypeRef::I32,
1727 mutable: false,
1728 doc: None,
1729 }],
1730 returns: Some(TypeRef::List(Box::new(TypeRef::StringUtf8))),
1731 doc: None,
1732 r#async: false,
1733 cancellable: false,
1734 deprecated: None,
1735 since: None,
1736 },
1737 ],
1738 structs: vec![StructDef {
1739 name: "Contact".to_string(),
1740 doc: None,
1741 fields: vec![
1742 StructField {
1743 name: "name".to_string(),
1744 ty: TypeRef::StringUtf8,
1745 doc: None,
1746 default: None,
1747 },
1748 StructField {
1749 name: "email".to_string(),
1750 ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
1751 doc: None,
1752 default: None,
1753 },
1754 StructField {
1755 name: "tags".to_string(),
1756 ty: TypeRef::List(Box::new(TypeRef::StringUtf8)),
1757 doc: None,
1758 default: None,
1759 },
1760 ],
1761 builder: false,
1762 }],
1763 enums: vec![EnumDef {
1764 name: "Color".to_string(),
1765 doc: None,
1766 variants: vec![
1767 EnumVariant {
1768 name: "Red".to_string(),
1769 value: 0,
1770 doc: None,
1771 },
1772 EnumVariant {
1773 name: "Green".to_string(),
1774 value: 1,
1775 doc: None,
1776 },
1777 EnumVariant {
1778 name: "Blue".to_string(),
1779 value: 2,
1780 doc: None,
1781 },
1782 ],
1783 }],
1784 callbacks: vec![],
1785 listeners: vec![],
1786 errors: None,
1787 modules: vec![],
1788 }]);
1789
1790 let tmp = std::env::temp_dir().join("weaveffi_test_node_structs_and_enums");
1791 let _ = std::fs::remove_dir_all(&tmp);
1792 std::fs::create_dir_all(&tmp).unwrap();
1793 let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
1794
1795 NodeGenerator
1796 .generate(
1797 &api,
1798 out_dir,
1799 &NodeConfig {
1800 strip_module_prefix: true,
1801 ..NodeConfig::default()
1802 },
1803 )
1804 .unwrap();
1805
1806 let dts = std::fs::read_to_string(tmp.join("node").join("types.d.ts")).unwrap();
1807
1808 assert!(
1809 dts.contains("export interface Contact {"),
1810 "missing Contact interface: {dts}"
1811 );
1812 assert!(dts.contains(" name: string;"), "missing name field: {dts}");
1813 assert!(
1814 dts.contains(" email: string | null;"),
1815 "missing optional email field: {dts}"
1816 );
1817 assert!(
1818 dts.contains(" tags: string[];"),
1819 "missing list tags field: {dts}"
1820 );
1821
1822 assert!(
1823 dts.contains("export enum Color {"),
1824 "missing Color enum: {dts}"
1825 );
1826 assert!(dts.contains(" Red = 0,"), "missing Red variant: {dts}");
1827 assert!(dts.contains(" Green = 1,"), "missing Green variant: {dts}");
1828 assert!(dts.contains(" Blue = 2,"), "missing Blue variant: {dts}");
1829
1830 assert!(
1831 dts.contains("export function get_contact(id: number): Contact | null"),
1832 "missing get_contact with optional return: {dts}"
1833 );
1834 assert!(
1835 dts.contains("export function list_contacts(): Contact[]"),
1836 "missing list_contacts with list return: {dts}"
1837 );
1838 assert!(
1839 dts.contains(
1840 "export function set_favorite_color(contact_id: number, color: Color | null): void"
1841 ),
1842 "missing set_favorite_color with optional enum param: {dts}"
1843 );
1844 assert!(
1845 dts.contains("export function get_tags(contact_id: number): string[]"),
1846 "missing get_tags with list return: {dts}"
1847 );
1848
1849 let iface_pos = dts.find("export interface Contact").unwrap();
1850 let enum_pos = dts.find("export enum Color").unwrap();
1851 let fn_pos = dts.find("export function get_contact").unwrap();
1852 assert!(
1853 iface_pos < fn_pos,
1854 "interface should appear before functions"
1855 );
1856 assert!(enum_pos < fn_pos, "enum should appear before functions");
1857
1858 let _ = std::fs::remove_dir_all(&tmp);
1859 }
1860
1861 #[test]
1862 fn node_custom_package_name() {
1863 let api = make_api(vec![make_module("math")]);
1864
1865 let tmp = std::env::temp_dir().join("weaveffi_test_node_custom_pkg");
1866 let _ = std::fs::remove_dir_all(&tmp);
1867 std::fs::create_dir_all(&tmp).unwrap();
1868 let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
1869
1870 let config = NodeConfig {
1871 package_name: Some("@myorg/cool-lib".into()),
1872 ..NodeConfig::default()
1873 };
1874 NodeGenerator.generate(&api, out_dir, &config).unwrap();
1875
1876 let pkg = std::fs::read_to_string(tmp.join("node").join("package.json")).unwrap();
1877 assert!(
1878 pkg.contains("\"name\": \"@myorg/cool-lib\""),
1879 "package.json should use custom name: {pkg}"
1880 );
1881 assert!(
1882 !pkg.contains("\"name\": \"weaveffi\""),
1883 "package.json should not contain default name: {pkg}"
1884 );
1885
1886 let _ = std::fs::remove_dir_all(&tmp);
1887 }
1888
1889 #[test]
1890 fn node_dts_has_jsdoc() {
1891 let api = make_api(vec![{
1892 let mut m = make_module("math");
1893 m.functions.push(Function {
1894 name: "add".into(),
1895 params: vec![
1896 Param {
1897 name: "a".into(),
1898 ty: TypeRef::I32,
1899 mutable: false,
1900 doc: None,
1901 },
1902 Param {
1903 name: "b".into(),
1904 ty: TypeRef::I32,
1905 mutable: false,
1906 doc: None,
1907 },
1908 ],
1909 returns: Some(TypeRef::I32),
1910 doc: None,
1911 r#async: false,
1912 cancellable: false,
1913 deprecated: None,
1914 since: None,
1915 });
1916 m.functions.push(Function {
1917 name: "subtract".into(),
1918 params: vec![
1919 Param {
1920 name: "a".into(),
1921 ty: TypeRef::I32,
1922 mutable: false,
1923 doc: None,
1924 },
1925 Param {
1926 name: "b".into(),
1927 ty: TypeRef::I32,
1928 mutable: false,
1929 doc: None,
1930 },
1931 ],
1932 returns: Some(TypeRef::I32),
1933 doc: None,
1934 r#async: false,
1935 cancellable: false,
1936 deprecated: None,
1937 since: None,
1938 });
1939 m
1940 }]);
1941
1942 let dts = render_node_dts(&api, true, "weaveffi.yml");
1943
1944 assert!(
1945 dts.contains("Maps to C function: weaveffi_math_add"),
1946 "missing JSDoc for add: {dts}"
1947 );
1948 assert!(
1949 dts.contains("Maps to C function: weaveffi_math_subtract"),
1950 "missing JSDoc for subtract: {dts}"
1951 );
1952 }
1953
1954 #[test]
1955 fn node_addon_has_no_todo() {
1956 let api = make_api(vec![{
1957 let mut m = make_module("math");
1958 m.functions.push(Function {
1959 name: "add".into(),
1960 params: vec![
1961 Param {
1962 name: "a".into(),
1963 ty: TypeRef::I32,
1964 mutable: false,
1965 doc: None,
1966 },
1967 Param {
1968 name: "b".into(),
1969 ty: TypeRef::I32,
1970 mutable: false,
1971 doc: None,
1972 },
1973 ],
1974 returns: Some(TypeRef::I32),
1975 doc: None,
1976 r#async: false,
1977 cancellable: false,
1978 deprecated: None,
1979 since: None,
1980 });
1981 m
1982 }]);
1983 let addon = render_addon_c(&api, true, "weaveffi.yml");
1984 assert!(
1985 !addon.contains("// TODO: implement"),
1986 "generated addon.c should not contain TODO comments: {addon}"
1987 );
1988 }
1989
1990 #[test]
1991 fn node_addon_extracts_args() {
1992 let api = make_api(vec![{
1993 let mut m = make_module("math");
1994 m.functions.push(Function {
1995 name: "add".into(),
1996 params: vec![
1997 Param {
1998 name: "a".into(),
1999 ty: TypeRef::I32,
2000 mutable: false,
2001 doc: None,
2002 },
2003 Param {
2004 name: "b".into(),
2005 ty: TypeRef::I32,
2006 mutable: false,
2007 doc: None,
2008 },
2009 ],
2010 returns: Some(TypeRef::I32),
2011 doc: None,
2012 r#async: false,
2013 cancellable: false,
2014 deprecated: None,
2015 since: None,
2016 });
2017 m
2018 }]);
2019 let addon = render_addon_c(&api, true, "weaveffi.yml");
2020 assert!(
2021 addon.contains("napi_get_cb_info"),
2022 "generated addon.c should call napi_get_cb_info: {addon}"
2023 );
2024 }
2025
2026 #[test]
2027 fn node_addon_frees_strings() {
2028 let api = make_api(vec![{
2029 let mut m = make_module("greet");
2030 m.functions.push(Function {
2031 name: "hello".into(),
2032 params: vec![Param {
2033 name: "name".into(),
2034 ty: TypeRef::StringUtf8,
2035 mutable: false,
2036 doc: None,
2037 }],
2038 returns: Some(TypeRef::StringUtf8),
2039 doc: None,
2040 r#async: false,
2041 cancellable: false,
2042 deprecated: None,
2043 since: None,
2044 });
2045 m
2046 }]);
2047 let addon = render_addon_c(&api, true, "weaveffi.yml");
2048 assert!(
2049 addon.contains("weaveffi_free_string(result)"),
2050 "generated addon should free returned strings: {addon}"
2051 );
2052 assert!(
2053 addon.contains("#include <string.h>"),
2054 "generated addon should include string.h: {addon}"
2055 );
2056 assert!(
2057 addon.contains("#include <stdlib.h>"),
2058 "generated addon should include stdlib.h: {addon}"
2059 );
2060 assert!(
2061 addon.contains("weaveffi_error_clear(&err)"),
2062 "generated addon should clear errors: {addon}"
2063 );
2064 }
2065
2066 #[test]
2067 fn node_addon_checks_error() {
2068 let api = make_api(vec![{
2069 let mut m = make_module("math");
2070 m.functions.push(Function {
2071 name: "add".into(),
2072 params: vec![
2073 Param {
2074 name: "a".into(),
2075 ty: TypeRef::I32,
2076 mutable: false,
2077 doc: None,
2078 },
2079 Param {
2080 name: "b".into(),
2081 ty: TypeRef::I32,
2082 mutable: false,
2083 doc: None,
2084 },
2085 ],
2086 returns: Some(TypeRef::I32),
2087 doc: None,
2088 r#async: false,
2089 cancellable: false,
2090 deprecated: None,
2091 since: None,
2092 });
2093 m
2094 }]);
2095 let addon = render_addon_c(&api, true, "weaveffi.yml");
2096 assert!(
2097 addon.contains("err.code"),
2098 "generated addon.c should check err.code: {addon}"
2099 );
2100 }
2101
2102 #[test]
2103 fn node_strip_module_prefix() {
2104 let api = make_api(vec![{
2105 let mut m = make_module("contacts");
2106 m.functions.push(Function {
2107 name: "create_contact".into(),
2108 params: vec![Param {
2109 name: "name".into(),
2110 ty: TypeRef::StringUtf8,
2111 mutable: false,
2112 doc: None,
2113 }],
2114 returns: Some(TypeRef::I32),
2115 doc: None,
2116 r#async: false,
2117 cancellable: false,
2118 deprecated: None,
2119 since: None,
2120 });
2121 m
2122 }]);
2123
2124 let config = NodeConfig {
2125 strip_module_prefix: true,
2126 ..NodeConfig::default()
2127 };
2128
2129 let tmp = std::env::temp_dir().join("weaveffi_test_node_strip_prefix");
2130 let _ = std::fs::remove_dir_all(&tmp);
2131 std::fs::create_dir_all(&tmp).unwrap();
2132 let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
2133
2134 NodeGenerator.generate(&api, out_dir, &config).unwrap();
2135
2136 let dts = std::fs::read_to_string(tmp.join("node/types.d.ts")).unwrap();
2137 assert!(
2138 dts.contains("export function create_contact("),
2139 "stripped name should be create_contact: {dts}"
2140 );
2141 assert!(
2142 !dts.contains("export function contacts_create_contact("),
2143 "should not contain module-prefixed name: {dts}"
2144 );
2145
2146 let addon = std::fs::read_to_string(tmp.join("node/weaveffi_addon.c")).unwrap();
2147 assert!(
2148 addon.contains("\"create_contact\""),
2149 "JS export name should be stripped: {addon}"
2150 );
2151 assert!(
2152 addon.contains("weaveffi_contacts_create_contact"),
2153 "C ABI call should still use full name: {addon}"
2154 );
2155
2156 let no_strip = NodeConfig::default();
2157 let tmp2 = std::env::temp_dir().join("weaveffi_test_node_no_strip_prefix");
2158 let _ = std::fs::remove_dir_all(&tmp2);
2159 std::fs::create_dir_all(&tmp2).unwrap();
2160 let out_dir2 = Utf8Path::from_path(&tmp2).expect("valid UTF-8");
2161
2162 NodeGenerator.generate(&api, out_dir2, &no_strip).unwrap();
2163
2164 let dts2 = std::fs::read_to_string(tmp2.join("node/types.d.ts")).unwrap();
2165 assert!(
2166 dts2.contains("export function contacts_create_contact("),
2167 "default should use module-prefixed name: {dts2}"
2168 );
2169
2170 let _ = std::fs::remove_dir_all(&tmp);
2171 let _ = std::fs::remove_dir_all(&tmp2);
2172 }
2173
2174 #[test]
2175 fn node_typed_handle_type() {
2176 let api = make_api(vec![{
2177 let mut m = make_module("contacts");
2178 m.structs.push(StructDef {
2179 name: "Contact".into(),
2180 doc: None,
2181 fields: vec![StructField {
2182 name: "name".into(),
2183 ty: TypeRef::StringUtf8,
2184 doc: None,
2185 default: None,
2186 }],
2187 builder: false,
2188 });
2189 m.functions.push(Function {
2190 name: "get_info".into(),
2191 params: vec![Param {
2192 name: "contact".into(),
2193 ty: TypeRef::TypedHandle("Contact".into()),
2194 mutable: false,
2195 doc: None,
2196 }],
2197 returns: None,
2198 doc: None,
2199 r#async: false,
2200 cancellable: false,
2201 deprecated: None,
2202 since: None,
2203 });
2204 m
2205 }]);
2206 let dts = render_node_dts(&api, true, "weaveffi.yml");
2207 assert!(
2208 dts.contains("contact: Contact"),
2209 "TypedHandle should use class type not bigint: {dts}"
2210 );
2211 }
2212
2213 #[test]
2214 fn node_deeply_nested_optional() {
2215 let api = make_api(vec![Module {
2216 name: "edge".into(),
2217 functions: vec![Function {
2218 name: "process".into(),
2219 params: vec![Param {
2220 name: "data".into(),
2221 ty: TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
2222 Box::new(TypeRef::Struct("Contact".into())),
2223 ))))),
2224 mutable: false,
2225 doc: None,
2226 }],
2227 returns: None,
2228 doc: None,
2229 r#async: false,
2230 cancellable: false,
2231 deprecated: None,
2232 since: None,
2233 }],
2234 structs: vec![StructDef {
2235 name: "Contact".into(),
2236 doc: None,
2237 fields: vec![StructField {
2238 name: "name".into(),
2239 ty: TypeRef::StringUtf8,
2240 doc: None,
2241 default: None,
2242 }],
2243 builder: false,
2244 }],
2245 enums: vec![],
2246 callbacks: vec![],
2247 listeners: vec![],
2248 errors: None,
2249 modules: vec![],
2250 }]);
2251 let dts = render_node_dts(&api, true, "weaveffi.yml");
2252 assert!(
2253 dts.contains("(Contact | null)[] | null"),
2254 "should contain deeply nested optional type: {dts}"
2255 );
2256 }
2257
2258 #[test]
2259 fn node_map_of_lists() {
2260 let api = make_api(vec![Module {
2261 name: "edge".into(),
2262 functions: vec![Function {
2263 name: "process".into(),
2264 params: vec![Param {
2265 name: "scores".into(),
2266 ty: TypeRef::Map(
2267 Box::new(TypeRef::StringUtf8),
2268 Box::new(TypeRef::List(Box::new(TypeRef::I32))),
2269 ),
2270 mutable: false,
2271 doc: None,
2272 }],
2273 returns: None,
2274 doc: None,
2275 r#async: false,
2276 cancellable: false,
2277 deprecated: None,
2278 since: None,
2279 }],
2280 structs: vec![],
2281 enums: vec![],
2282 callbacks: vec![],
2283 listeners: vec![],
2284 errors: None,
2285 modules: vec![],
2286 }]);
2287 let dts = render_node_dts(&api, true, "weaveffi.yml");
2288 assert!(
2289 dts.contains("Record<string, number[]>"),
2290 "should contain map of lists type: {dts}"
2291 );
2292 }
2293
2294 #[test]
2295 fn node_enum_keyed_map() {
2296 let api = make_api(vec![Module {
2297 name: "edge".into(),
2298 functions: vec![Function {
2299 name: "process".into(),
2300 params: vec![Param {
2301 name: "contacts".into(),
2302 ty: TypeRef::Map(
2303 Box::new(TypeRef::Enum("Color".into())),
2304 Box::new(TypeRef::Struct("Contact".into())),
2305 ),
2306 mutable: false,
2307 doc: None,
2308 }],
2309 returns: None,
2310 doc: None,
2311 r#async: false,
2312 cancellable: false,
2313 deprecated: None,
2314 since: None,
2315 }],
2316 structs: vec![StructDef {
2317 name: "Contact".into(),
2318 doc: None,
2319 fields: vec![StructField {
2320 name: "name".into(),
2321 ty: TypeRef::StringUtf8,
2322 doc: None,
2323 default: None,
2324 }],
2325 builder: false,
2326 }],
2327 enums: vec![EnumDef {
2328 name: "Color".into(),
2329 doc: None,
2330 variants: vec![
2331 EnumVariant {
2332 name: "Red".into(),
2333 value: 0,
2334 doc: None,
2335 },
2336 EnumVariant {
2337 name: "Green".into(),
2338 value: 1,
2339 doc: None,
2340 },
2341 EnumVariant {
2342 name: "Blue".into(),
2343 value: 2,
2344 doc: None,
2345 },
2346 ],
2347 }],
2348 callbacks: vec![],
2349 listeners: vec![],
2350 errors: None,
2351 modules: vec![],
2352 }]);
2353 let dts = render_node_dts(&api, true, "weaveffi.yml");
2354 assert!(
2355 dts.contains("Record<Color, Contact>"),
2356 "should contain enum-keyed map type: {dts}"
2357 );
2358 }
2359
2360 #[test]
2361 fn node_no_double_free_on_error() {
2362 let api = make_api(vec![{
2363 let mut m = make_module("contacts");
2364 m.structs.push(StructDef {
2365 name: "Contact".into(),
2366 doc: None,
2367 fields: vec![StructField {
2368 name: "name".into(),
2369 ty: TypeRef::StringUtf8,
2370 doc: None,
2371 default: None,
2372 }],
2373 builder: false,
2374 });
2375 m.functions.push(Function {
2376 name: "find_contact".into(),
2377 params: vec![Param {
2378 name: "name".into(),
2379 ty: TypeRef::StringUtf8,
2380 mutable: false,
2381 doc: None,
2382 }],
2383 returns: Some(TypeRef::Struct("Contact".into())),
2384 doc: None,
2385 r#async: false,
2386 cancellable: false,
2387 deprecated: None,
2388 since: None,
2389 });
2390 m
2391 }]);
2392 let addon = render_addon_c(&api, true, "weaveffi.yml");
2393 assert!(
2394 addon.contains("free(name)"),
2395 "malloc'd JS string copy should be freed after the C call: {addon}"
2396 );
2397 assert!(
2398 !addon.contains("weaveffi_free_string(name)"),
2399 "input string param must not use weaveffi_free_string: {addon}"
2400 );
2401 let free_pos = addon
2402 .find("free(name)")
2403 .expect("free(name) should be present");
2404 let err_pos = addon
2405 .find("if (err.code != 0)")
2406 .expect("err.code check should be present");
2407 assert!(
2408 free_pos < err_pos,
2409 "cleanup should run before error check: free at {free_pos}, err at {err_pos}"
2410 );
2411 let err_block_start = addon
2412 .find(" if (err.code != 0) {\n")
2413 .expect("error if block should be present");
2414 let after_err = &addon[err_block_start..];
2415 let err_block_end_rel = after_err
2416 .find(" }\n napi_value ret;")
2417 .expect("napi_value ret should follow error block");
2418 let err_block = &addon[err_block_start..err_block_start + err_block_end_rel];
2419 assert!(
2420 !err_block.contains("result"),
2421 "error path should not touch result before return NULL: {err_block}"
2422 );
2423 }
2424
2425 #[test]
2426 fn node_null_check_on_optional_return() {
2427 let api = make_api(vec![{
2428 let mut m = make_module("contacts");
2429 m.structs.push(StructDef {
2430 name: "Contact".into(),
2431 doc: None,
2432 fields: vec![StructField {
2433 name: "name".into(),
2434 ty: TypeRef::StringUtf8,
2435 doc: None,
2436 default: None,
2437 }],
2438 builder: false,
2439 });
2440 m.functions.push(Function {
2441 name: "find_contact".into(),
2442 params: vec![Param {
2443 name: "id".into(),
2444 ty: TypeRef::I32,
2445 mutable: false,
2446 doc: None,
2447 }],
2448 returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
2449 "Contact".into(),
2450 )))),
2451 doc: None,
2452 r#async: false,
2453 cancellable: false,
2454 deprecated: None,
2455 since: None,
2456 });
2457 m
2458 }]);
2459 let addon = render_addon_c(&api, true, "weaveffi.yml");
2460 assert!(
2461 addon.contains("if (result == NULL)"),
2462 "optional struct return should null-check before wrapping: {addon}"
2463 );
2464 assert!(
2465 addon.contains("napi_get_null"),
2466 "optional absent should return JS null via napi_get_null: {addon}"
2467 );
2468 }
2469
2470 #[test]
2471 fn node_async_returns_promise() {
2472 let api = make_api(vec![{
2473 let mut m = make_module("tasks");
2474 m.functions.push(Function {
2475 name: "run".into(),
2476 params: vec![Param {
2477 name: "id".into(),
2478 ty: TypeRef::I32,
2479 mutable: false,
2480 doc: None,
2481 }],
2482 returns: Some(TypeRef::StringUtf8),
2483 doc: None,
2484 r#async: true,
2485 cancellable: false,
2486 deprecated: None,
2487 since: None,
2488 });
2489 m.functions.push(Function {
2490 name: "fire_and_forget".into(),
2491 params: vec![],
2492 returns: None,
2493 doc: None,
2494 r#async: true,
2495 cancellable: false,
2496 deprecated: None,
2497 since: None,
2498 });
2499 m
2500 }]);
2501 let dts = render_node_dts(&api, true, "weaveffi.yml");
2502 assert!(
2503 dts.contains("Promise<"),
2504 "async function should return Promise in .d.ts: {dts}"
2505 );
2506 assert!(
2507 dts.contains("): Promise<string>"),
2508 "async string return should be Promise<string>: {dts}"
2509 );
2510 assert!(
2511 dts.contains("): Promise<void>"),
2512 "async void return should be Promise<void>: {dts}"
2513 );
2514 }
2515
2516 #[test]
2517 fn node_addon_creates_promise() {
2518 let api = make_api(vec![{
2519 let mut m = make_module("tasks");
2520 m.functions.push(Function {
2521 name: "run".into(),
2522 params: vec![Param {
2523 name: "id".into(),
2524 ty: TypeRef::I32,
2525 mutable: false,
2526 doc: None,
2527 }],
2528 returns: Some(TypeRef::I32),
2529 doc: None,
2530 r#async: true,
2531 cancellable: false,
2532 deprecated: None,
2533 since: None,
2534 });
2535 m
2536 }]);
2537 let addon = render_addon_c(&api, true, "weaveffi.yml");
2538 assert!(
2539 addon.contains("napi_create_promise"),
2540 "async addon should call napi_create_promise: {addon}"
2541 );
2542 assert!(
2543 addon.contains("napi_resolve_deferred"),
2544 "async callback should call napi_resolve_deferred: {addon}"
2545 );
2546 assert!(
2547 addon.contains("napi_reject_deferred"),
2548 "async callback should call napi_reject_deferred: {addon}"
2549 );
2550 assert!(
2551 addon.contains("weaveffi_napi_async_ctx"),
2552 "async addon should define async context struct: {addon}"
2553 );
2554 assert!(
2555 addon.contains("weaveffi_tasks_run_async("),
2556 "async addon should call the _async C function: {addon}"
2557 );
2558 assert!(
2559 addon.contains("weaveffi_tasks_run_napi_cb"),
2560 "async addon should define the callback: {addon}"
2561 );
2562 }
2563
2564 #[test]
2570 fn node_async_pins_callback_for_lifetime() {
2571 let api = make_api(vec![{
2572 let mut m = make_module("tasks");
2573 m.functions.push(Function {
2574 name: "run".into(),
2575 params: vec![Param {
2576 name: "id".into(),
2577 ty: TypeRef::I32,
2578 mutable: false,
2579 doc: None,
2580 }],
2581 returns: Some(TypeRef::I32),
2582 doc: None,
2583 r#async: true,
2584 cancellable: false,
2585 deprecated: None,
2586 since: None,
2587 });
2588 m
2589 }]);
2590 let addon = render_addon_c(&api, true, "weaveffi.yml");
2591 let create_count = addon.matches("napi_create_promise").count();
2592 let resolve_count = addon.matches("napi_resolve_deferred").count();
2593 let reject_count = addon.matches("napi_reject_deferred").count();
2594 let malloc_count = addon
2595 .matches("malloc(sizeof(weaveffi_napi_async_ctx))")
2596 .count();
2597 let free_count = addon.matches("free(ctx);").count();
2598 assert_eq!(
2599 create_count, 1,
2600 "expected one napi_create_promise per async fn, got {create_count}: {addon}"
2601 );
2602 assert_eq!(
2603 resolve_count, 1,
2604 "expected one napi_resolve_deferred per async fn, got {resolve_count}: {addon}"
2605 );
2606 assert_eq!(
2607 reject_count, 1,
2608 "expected one napi_reject_deferred per async fn, got {reject_count}: {addon}"
2609 );
2610 assert_eq!(
2611 malloc_count, free_count,
2612 "ctx malloc / free must balance per async fn: malloc={malloc_count} free={free_count}: {addon}"
2613 );
2614 }
2615
2616 fn doc_module() -> Module {
2617 Module {
2618 name: "docs".into(),
2619 functions: vec![Function {
2620 name: "do_thing".into(),
2621 params: vec![Param {
2622 name: "x".into(),
2623 ty: TypeRef::I32,
2624 mutable: false,
2625 doc: Some("the input value".into()),
2626 }],
2627 returns: Some(TypeRef::I32),
2628 doc: Some("Performs a thing.".into()),
2629 r#async: false,
2630 cancellable: false,
2631 deprecated: None,
2632 since: None,
2633 }],
2634 structs: vec![StructDef {
2635 name: "Item".into(),
2636 doc: Some("An item we track.".into()),
2637 fields: vec![StructField {
2638 name: "id".into(),
2639 ty: TypeRef::I64,
2640 doc: Some("Stable id".into()),
2641 default: None,
2642 }],
2643 builder: false,
2644 }],
2645 enums: vec![EnumDef {
2646 name: "Kind".into(),
2647 doc: Some("Kind of item.".into()),
2648 variants: vec![EnumVariant {
2649 name: "Small".into(),
2650 value: 0,
2651 doc: Some("A small one".into()),
2652 }],
2653 }],
2654 callbacks: vec![],
2655 listeners: vec![],
2656 errors: None,
2657 modules: vec![],
2658 }
2659 }
2660
2661 #[test]
2662 fn node_emits_doc_on_function() {
2663 let dts = render_node_dts(&make_api(vec![doc_module()]), true, "weaveffi.yml");
2664 assert!(dts.contains("Performs a thing."), "{dts}");
2665 }
2666
2667 #[test]
2668 fn node_emits_doc_on_struct() {
2669 let dts = render_node_dts(&make_api(vec![doc_module()]), true, "weaveffi.yml");
2670 assert!(dts.contains("/** An item we track. */"), "{dts}");
2671 }
2672
2673 #[test]
2674 fn node_emits_doc_on_enum_variant() {
2675 let dts = render_node_dts(&make_api(vec![doc_module()]), true, "weaveffi.yml");
2676 assert!(dts.contains("/** Kind of item. */"), "{dts}");
2677 assert!(dts.contains("/** A small one */"), "{dts}");
2678 }
2679
2680 #[test]
2681 fn node_emits_doc_on_field() {
2682 let dts = render_node_dts(&make_api(vec![doc_module()]), true, "weaveffi.yml");
2683 assert!(dts.contains("/** Stable id */"), "{dts}");
2684 }
2685
2686 #[test]
2687 fn node_emits_doc_on_param() {
2688 let dts = render_node_dts(&make_api(vec![doc_module()]), true, "weaveffi.yml");
2689 assert!(dts.contains("@param x the input value"), "{dts}");
2690 }
2691}