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