1use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
2use alef_core::config::{AdapterPattern, Language, ResolvedCrateConfig, resolve_output_dir};
3use alef_core::hash::{self, CommentStyle};
4use alef_core::ir::{ApiSurface, FieldDef, TypeRef};
5use heck::ToPascalCase;
6use std::collections::{HashMap, HashSet};
7use std::path::PathBuf;
8
9#[derive(Debug, Clone)]
13pub(super) struct StreamingMethodMeta {
14 #[allow(dead_code)]
17 pub owner_type: String,
18 pub item_type: String,
19}
20
21pub(super) mod enums;
22pub(super) mod errors;
23pub(super) mod functions;
24pub(super) mod methods;
25pub(super) mod types;
26
27pub struct CsharpBackend;
28
29impl CsharpBackend {
30 }
32
33impl Backend for CsharpBackend {
34 fn name(&self) -> &str {
35 "csharp"
36 }
37
38 fn language(&self) -> Language {
39 Language::Csharp
40 }
41
42 fn capabilities(&self) -> Capabilities {
43 Capabilities {
44 supports_async: true,
45 supports_classes: true,
46 supports_enums: true,
47 supports_option: true,
48 supports_result: true,
49 ..Capabilities::default()
50 }
51 }
52
53 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
54 let namespace = config.csharp_namespace();
55 let prefix = config.ffi_prefix();
56 let lib_name = config.ffi_lib_name();
57
58 let bridge_param_names: HashSet<String> = config
61 .trait_bridges
62 .iter()
63 .filter_map(|b| b.param_name.clone())
64 .collect();
65 let bridge_type_aliases: HashSet<String> = config
66 .trait_bridges
67 .iter()
68 .filter_map(|b| b.type_alias.clone())
69 .collect();
70 let has_visitor_callbacks = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false);
72 let bridge_associated_types = config.bridge_associated_types();
73
74 let streaming_methods: HashSet<String> = config
79 .adapters
80 .iter()
81 .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
82 .map(|a| a.name.clone())
83 .collect();
84 let streaming_methods_meta: HashMap<String, StreamingMethodMeta> = config
85 .adapters
86 .iter()
87 .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
88 .filter_map(|a| {
89 let owner_type = a.owner_type.clone()?;
90 let item_type = a.item_type.clone()?;
91 Some((a.name.clone(), StreamingMethodMeta { owner_type, item_type }))
92 })
93 .collect();
94
95 let exclude_functions: HashSet<String> = config
97 .csharp
98 .as_ref()
99 .map(|c| c.exclude_functions.iter().cloned().collect())
100 .unwrap_or_default();
101
102 let output_dir = resolve_output_dir(config.output_paths.get("csharp"), &config.name, "packages/csharp/");
103
104 let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
105
106 let mut files = Vec::new();
107
108 let exception_class_name = format!("{}Exception", api.crate_name.to_pascal_case());
110
111 files.push(GeneratedFile {
113 path: base_path.join("NativeMethods.cs"),
114 content: strip_trailing_whitespace(&functions::gen_native_methods(
115 api,
116 &namespace,
117 &lib_name,
118 &prefix,
119 &bridge_param_names,
120 &bridge_type_aliases,
121 has_visitor_callbacks,
122 &config.trait_bridges,
123 &streaming_methods,
124 &streaming_methods_meta,
125 &exclude_functions,
126 )),
127 generated_header: true,
128 });
129
130 if !api.errors.is_empty() {
132 for error in &api.errors {
133 let error_files =
134 alef_codegen::error_gen::gen_csharp_error_types(error, &namespace, Some(&exception_class_name));
135 for (class_name, content) in error_files {
136 files.push(GeneratedFile {
137 path: base_path.join(format!("{}.cs", class_name)),
138 content: strip_trailing_whitespace(&content),
139 generated_header: false, });
141 }
142 }
143 }
144
145 if api.errors.is_empty()
147 || !api
148 .errors
149 .iter()
150 .any(|e| format!("{}Exception", e.name) == exception_class_name)
151 {
152 files.push(GeneratedFile {
153 path: base_path.join(format!("{}.cs", exception_class_name)),
154 content: strip_trailing_whitespace(&errors::gen_exception_class(&namespace, &exception_class_name)),
155 generated_header: true,
156 });
157 }
158
159 let base_class_name = api.crate_name.to_pascal_case();
161 let wrapper_class_name = if namespace == base_class_name {
162 format!("{}Lib", base_class_name)
163 } else {
164 base_class_name
165 };
166 files.push(GeneratedFile {
167 path: base_path.join(format!("{}.cs", wrapper_class_name)),
168 content: strip_trailing_whitespace(&methods::gen_wrapper_class(
169 api,
170 &namespace,
171 &wrapper_class_name,
172 &exception_class_name,
173 &prefix,
174 &bridge_param_names,
175 &bridge_type_aliases,
176 has_visitor_callbacks,
177 &streaming_methods,
178 &streaming_methods_meta,
179 &exclude_functions,
180 &config.trait_bridges,
181 )),
182 generated_header: true,
183 });
184
185 if has_visitor_callbacks {
187 let visitor_bridge_cfg = config
190 .trait_bridges
191 .iter()
192 .find(|b| b.bind_via == alef_core::config::BridgeBinding::OptionsField);
193 let trait_map: std::collections::HashMap<&str, &alef_core::ir::TypeDef> = api
194 .types
195 .iter()
196 .filter(|t| t.is_trait)
197 .map(|t| (t.name.as_str(), t))
198 .collect();
199 let visitor_trait = visitor_bridge_cfg.and_then(|b| trait_map.get(b.trait_name.as_str()).copied());
200
201 if let Some(trait_def) = visitor_trait {
202 for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace, trait_def) {
203 files.push(GeneratedFile {
204 path: base_path.join(filename),
205 content: strip_trailing_whitespace(&content),
206 generated_header: true,
207 });
208 }
209 } else {
210 let placeholder = alef_core::ir::TypeDef {
212 name: String::new(),
213 rust_path: String::new(),
214 original_rust_path: String::new(),
215 fields: vec![],
216 methods: vec![],
217 is_opaque: false,
218 is_clone: false,
219 is_copy: false,
220 is_trait: true,
221 has_default: false,
222 has_stripped_cfg_fields: false,
223 is_return_type: false,
224 serde_rename_all: None,
225 has_serde: false,
226 super_traits: vec![],
227 doc: String::new(),
228 cfg: None,
229 };
230 for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace, &placeholder) {
231 files.push(GeneratedFile {
232 path: base_path.join(filename),
233 content: strip_trailing_whitespace(&content),
234 generated_header: true,
235 });
236 }
237 }
238 delete_superseded_visitor_files(&base_path)?;
242 } else {
243 delete_stale_visitor_files(&base_path)?;
246 }
247
248 if !config.trait_bridges.is_empty() {
250 let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
251 let bridges: Vec<_> = config
252 .trait_bridges
253 .iter()
254 .filter_map(|cfg| {
255 let trait_name = cfg.trait_name.clone();
256 trait_defs
257 .iter()
258 .find(|t| t.name == trait_name)
259 .map(|trait_def| (trait_name, cfg, *trait_def))
260 })
261 .collect();
262
263 if !bridges.is_empty() {
264 let visible_type_names: HashSet<&str> = api
268 .types
269 .iter()
270 .filter(|t| !t.is_trait)
271 .map(|t| t.name.as_str())
272 .chain(api.enums.iter().map(|e| e.name.as_str()))
273 .collect();
274 let (filename, content) =
275 crate::trait_bridge::gen_trait_bridges_file(&namespace, &prefix, &bridges, &visible_type_names);
276 files.push(GeneratedFile {
277 path: base_path.join(filename),
278 content: strip_trailing_whitespace(&content),
279 generated_header: true,
280 });
281 }
282 }
283
284 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
286
287 let all_opaque_type_names: HashSet<String> = api
290 .types
291 .iter()
292 .filter(|t| t.is_opaque)
293 .map(|t| t.name.to_pascal_case())
294 .collect();
295
296 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
298 if typ.is_opaque {
299 let type_filename = typ.name.to_pascal_case();
300 files.push(GeneratedFile {
301 path: base_path.join(format!("{}.cs", type_filename)),
302 content: strip_trailing_whitespace(&types::gen_opaque_handle(
303 typ,
304 &namespace,
305 &exception_class_name,
306 &enum_names,
307 &streaming_methods,
308 &streaming_methods_meta,
309 &all_opaque_type_names,
310 )),
311 generated_header: true,
312 });
313 }
314 }
315
316 let complex_enums: HashSet<String> = HashSet::new();
320
321 let custom_converter_enums: HashSet<String> = api
327 .enums
328 .iter()
329 .filter(|e| {
330 let is_tagged_union = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
332 if is_tagged_union {
333 return false;
334 }
335 let rename_all_differs = matches!(
340 e.serde_rename_all.as_deref(),
341 Some("kebab-case") | Some("SCREAMING-KEBAB-CASE") | Some("camelCase") | Some("PascalCase")
342 );
343 if rename_all_differs {
344 return true;
345 }
346 e.variants.iter().any(|v| {
348 if let Some(ref rename) = v.serde_rename {
349 let snake = enums::apply_rename_all(&v.name, e.serde_rename_all.as_deref());
350 rename != &snake
351 } else {
352 false
353 }
354 })
355 })
356 .map(|e| e.name.to_pascal_case())
357 .collect();
358
359 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
361
362 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
364 if !typ.is_opaque {
365 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
368 if !typ.fields.is_empty() && !has_named_fields {
369 continue;
370 }
371 if has_visitor_callbacks && bridge_associated_types.contains(typ.name.as_str()) {
373 continue;
374 }
375
376 let type_filename = typ.name.to_pascal_case();
377 files.push(GeneratedFile {
378 path: base_path.join(format!("{}.cs", type_filename)),
379 content: strip_trailing_whitespace(&types::gen_record_type(
380 typ,
381 &namespace,
382 &enum_names,
383 &complex_enums,
384 &custom_converter_enums,
385 &lang_rename_all,
386 &bridge_type_aliases,
387 &exception_class_name,
388 )),
389 generated_header: true,
390 });
391 }
392 }
393
394 for enum_def in &api.enums {
396 if has_visitor_callbacks && bridge_associated_types.contains(enum_def.name.as_str()) {
398 continue;
399 }
400 let enum_filename = enum_def.name.to_pascal_case();
401 files.push(GeneratedFile {
402 path: base_path.join(format!("{}.cs", enum_filename)),
403 content: strip_trailing_whitespace(&enums::gen_enum(enum_def, &namespace)),
404 generated_header: true,
405 });
406 }
407
408 let needs_byte_array_converter = api
411 .types
412 .iter()
413 .any(|t| !t.is_opaque && t.fields.iter().any(|f| !f.optional && matches!(f.ty, TypeRef::Bytes)));
414 if needs_byte_array_converter {
415 files.push(GeneratedFile {
416 path: base_path.join("ByteArrayToIntArrayConverter.cs"),
417 content: types::gen_byte_array_to_int_array_converter(&namespace),
418 generated_header: true,
419 });
420 }
421
422 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
424
425 files.push(GeneratedFile {
429 path: PathBuf::from("packages/csharp/Directory.Build.props"),
430 content: gen_directory_build_props(),
431 generated_header: true,
432 });
433
434 Ok(files)
435 }
436
437 fn generate_public_api(
442 &self,
443 _api: &ApiSurface,
444 _config: &ResolvedCrateConfig,
445 ) -> anyhow::Result<Vec<GeneratedFile>> {
446 Ok(vec![])
448 }
449
450 fn build_config(&self) -> Option<BuildConfig> {
451 Some(BuildConfig {
452 tool: "dotnet",
453 crate_suffix: "",
454 build_dep: BuildDependency::Ffi,
455 post_build: vec![],
456 })
457 }
458}
459
460pub(super) fn is_tuple_field(field: &FieldDef) -> bool {
462 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
463 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
464}
465
466pub(super) fn strip_trailing_whitespace(content: &str) -> String {
468 let mut result: String = content
469 .lines()
470 .map(|line| line.trim_end())
471 .collect::<Vec<_>>()
472 .join("\n");
473 if !result.ends_with('\n') {
474 result.push('\n');
475 }
476 result
477}
478
479pub(super) fn csharp_file_header() -> String {
481 let mut out = hash::header(CommentStyle::DoubleSlash);
482 out.push_str("#nullable enable\n\n");
483 out
484}
485
486fn gen_directory_build_props() -> String {
489 "<!-- auto-generated by alef (generate_bindings) -->\n\
490<Project>\n \
491<PropertyGroup>\n \
492<Nullable>enable</Nullable>\n \
493<LangVersion>latest</LangVersion>\n \
494<TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n \
495</PropertyGroup>\n\
496</Project>\n"
497 .to_string()
498}
499
500fn delete_superseded_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
505 let superseded = ["IVisitor.cs", "VisitorCallbacks.cs"];
506 for filename in superseded {
507 let path = base_path.join(filename);
508 if path.exists() {
509 std::fs::remove_file(&path)
510 .map_err(|e| anyhow::anyhow!("Failed to delete superseded visitor file {}: {}", path.display(), e))?;
511 }
512 }
513 Ok(())
514}
515
516fn delete_stale_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
520 let stale_files = vec!["IVisitor.cs", "VisitorCallbacks.cs", "NodeContext.cs", "VisitResult.cs"];
521
522 for filename in stale_files {
523 let path = base_path.join(filename);
524 if path.exists() {
525 std::fs::remove_file(&path)
526 .map_err(|e| anyhow::anyhow!("Failed to delete stale visitor file {}: {}", path.display(), e))?;
527 }
528 }
529
530 Ok(())
531}
532
533use alef_core::ir::PrimitiveType;
538
539pub(super) fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
546 match ty {
547 TypeRef::Unit => "void",
548 TypeRef::Primitive(PrimitiveType::Bool) => "int",
550 TypeRef::Primitive(PrimitiveType::U8) => "byte",
552 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
553 TypeRef::Primitive(PrimitiveType::U32) => "uint",
554 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
555 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
556 TypeRef::Primitive(PrimitiveType::I16) => "short",
557 TypeRef::Primitive(PrimitiveType::I32) => "int",
558 TypeRef::Primitive(PrimitiveType::I64) => "long",
559 TypeRef::Primitive(PrimitiveType::F32) => "float",
560 TypeRef::Primitive(PrimitiveType::F64) => "double",
561 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
562 TypeRef::Primitive(PrimitiveType::Isize) => "long",
563 TypeRef::Duration => "ulong",
565 TypeRef::String
567 | TypeRef::Char
568 | TypeRef::Bytes
569 | TypeRef::Optional(_)
570 | TypeRef::Vec(_)
571 | TypeRef::Map(_, _)
572 | TypeRef::Named(_)
573 | TypeRef::Path
574 | TypeRef::Json => "IntPtr",
575 }
576}
577
578pub(super) fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
585 match ty {
586 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
587 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
589 TypeRef::Unit => "void",
590 TypeRef::Primitive(PrimitiveType::Bool) => "int",
591 TypeRef::Primitive(PrimitiveType::U8) => "byte",
592 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
593 TypeRef::Primitive(PrimitiveType::U32) => "uint",
594 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
595 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
596 TypeRef::Primitive(PrimitiveType::I16) => "short",
597 TypeRef::Primitive(PrimitiveType::I32) => "int",
598 TypeRef::Primitive(PrimitiveType::I64) => "long",
599 TypeRef::Primitive(PrimitiveType::F32) => "float",
600 TypeRef::Primitive(PrimitiveType::F64) => "double",
601 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
602 TypeRef::Primitive(PrimitiveType::Isize) => "long",
603 TypeRef::Duration => "ulong",
604 }
605}
606
607pub(super) fn is_bridge_param(
610 param: &alef_core::ir::ParamDef,
611 bridge_param_names: &HashSet<String>,
612 bridge_type_aliases: &HashSet<String>,
613) -> bool {
614 bridge_param_names.contains(¶m.name)
615 || matches!(¶m.ty, alef_core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
616}
617
618pub(super) fn returns_string(ty: &TypeRef) -> bool {
620 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
621}
622
623pub(super) fn returns_bool_via_int(ty: &TypeRef) -> bool {
625 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
626}
627
628pub(super) fn returns_json_object(ty: &TypeRef) -> bool {
630 matches!(
631 ty,
632 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
633 )
634}
635
636pub(super) fn returns_ptr(ty: &TypeRef) -> bool {
639 matches!(
640 ty,
641 TypeRef::String
642 | TypeRef::Char
643 | TypeRef::Path
644 | TypeRef::Json
645 | TypeRef::Named(_)
646 | TypeRef::Vec(_)
647 | TypeRef::Map(_, _)
648 | TypeRef::Bytes
649 | TypeRef::Optional(_)
650 )
651}
652
653pub(super) fn native_call_arg(
659 ty: &TypeRef,
660 param_name: &str,
661 optional: bool,
662 true_opaque_types: &HashSet<String>,
663) -> String {
664 match ty {
665 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
666 let bang = if optional { "!" } else { "" };
668 format!("{param_name}{bang}.Handle")
669 }
670 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
671 format!("{param_name}Handle")
672 }
673 TypeRef::Bytes => {
674 format!("{param_name}Handle.AddrOfPinnedObject()")
675 }
676 TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool) => {
677 if optional {
679 format!("({param_name}?.Value ? 1 : 0)")
680 } else {
681 format!("({param_name} ? 1 : 0)")
682 }
683 }
684 ty => {
685 if optional {
686 if let TypeRef::Primitive(prim) = ty {
694 use alef_core::ir::PrimitiveType;
695 let sentinel = match prim {
696 PrimitiveType::U8 => "byte.MaxValue",
697 PrimitiveType::U16 => "ushort.MaxValue",
698 PrimitiveType::U32 => "uint.MaxValue",
699 PrimitiveType::U64 | PrimitiveType::Usize => "ulong.MaxValue",
700 PrimitiveType::I8 => "sbyte.MaxValue",
701 PrimitiveType::I16 => "short.MaxValue",
702 PrimitiveType::I32 => "int.MaxValue",
703 PrimitiveType::I64 | PrimitiveType::Isize => "long.MaxValue",
704 PrimitiveType::F32 => "float.NaN",
705 PrimitiveType::F64 => "double.NaN",
706 PrimitiveType::Bool => unreachable!("handled above"),
707 };
708 format!("{param_name} ?? {sentinel}")
709 } else if matches!(ty, TypeRef::Duration) {
710 format!("{param_name}.GetValueOrDefault()")
711 } else {
712 format!("{param_name}!")
713 }
714 } else {
715 param_name.to_string()
716 }
717 }
718 }
719}
720
721pub(super) fn emit_named_param_setup(
726 out: &mut String,
727 params: &[alef_core::ir::ParamDef],
728 indent: &str,
729 true_opaque_types: &HashSet<String>,
730 exception_name: &str,
731) {
732 for param in params {
733 let param_name = param.name.to_lower_camel_case();
734 let json_var = format!("{param_name}Json");
735 let handle_var = format!("{param_name}Handle");
736
737 match ¶m.ty {
738 TypeRef::Named(type_name) => {
739 if true_opaque_types.contains(type_name) {
742 continue;
743 }
744 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
745
746 let is_config_param = param.name == "config";
748 let param_to_serialize = if is_config_param {
749 let type_pascal = type_name.to_pascal_case();
750 format!("({} ?? new {}())", param_name, type_pascal)
751 } else {
752 param_name.to_string()
753 };
754
755 if param.optional && !is_config_param {
756 out.push_str(&crate::template_env::render(
760 "named_param_handle_from_json_optional.jinja",
761 minijinja::context! {
762 indent,
763 handle_var => &handle_var,
764 from_json_method => &from_json_method,
765 json_var => &json_var,
766 param_name => ¶m_name,
767 exception_name => exception_name,
768 },
769 ));
770 } else {
771 out.push_str(&crate::template_env::render(
772 "named_param_json_serialize.jinja",
773 minijinja::context! { indent, json_var => &json_var, param_name => ¶m_to_serialize },
774 ));
775 out.push_str(&crate::template_env::render(
776 "named_param_handle_from_json.jinja",
777 minijinja::context! {
778 indent,
779 handle_var => &handle_var,
780 from_json_method => &from_json_method,
781 json_var => &json_var,
782 exception_name => exception_name,
783 },
784 ));
785 }
786 }
787 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
788 out.push_str(&crate::template_env::render(
790 "named_param_json_serialize.jinja",
791 minijinja::context! { indent, json_var => &json_var, param_name => ¶m_name },
792 ));
793 out.push_str(&crate::template_env::render(
794 "named_param_handle_string.jinja",
795 minijinja::context! { indent, handle_var => &handle_var, json_var => &json_var },
796 ));
797 }
798 TypeRef::Bytes => {
799 out.push_str(&crate::template_env::render(
801 "named_param_handle_pin.jinja",
802 minijinja::context! { indent, handle_var => &handle_var, param_name => ¶m_name },
803 ));
804 }
805 _ => {}
806 }
807 }
808}
809
810pub(super) fn emit_named_param_teardown(
815 out: &mut String,
816 params: &[alef_core::ir::ParamDef],
817 true_opaque_types: &HashSet<String>,
818) {
819 for param in params {
820 let param_name = param.name.to_lower_camel_case();
821 let handle_var = format!("{param_name}Handle");
822 match ¶m.ty {
823 TypeRef::Named(type_name) => {
824 if true_opaque_types.contains(type_name) {
825 continue;
827 }
828 let free_method = format!("{}Free", type_name.to_pascal_case());
829 out.push_str(&crate::template_env::render(
830 "named_param_teardown_free.jinja",
831 minijinja::context! { indent => " ", free_method => &free_method, handle_var => &handle_var },
832 ));
833 }
834 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
835 out.push_str(&crate::template_env::render(
836 "named_param_teardown_hglobal.jinja",
837 minijinja::context! { indent => " ", handle_var => &handle_var },
838 ));
839 }
840 TypeRef::Bytes => {
841 out.push_str(&crate::template_env::render(
842 "named_param_teardown_gchandle.jinja",
843 minijinja::context! { indent => " ", handle_var => &handle_var },
844 ));
845 }
846 _ => {}
847 }
848 }
849}
850
851pub(super) fn emit_named_param_teardown_indented(
853 out: &mut String,
854 params: &[alef_core::ir::ParamDef],
855 indent: &str,
856 true_opaque_types: &HashSet<String>,
857) {
858 for param in params {
859 let param_name = param.name.to_lower_camel_case();
860 let handle_var = format!("{param_name}Handle");
861 match ¶m.ty {
862 TypeRef::Named(type_name) => {
863 if true_opaque_types.contains(type_name) {
864 continue;
866 }
867 let free_method = format!("{}Free", type_name.to_pascal_case());
868 out.push_str(&crate::template_env::render(
869 "named_param_teardown_free.jinja",
870 minijinja::context! { indent, free_method => &free_method, handle_var => &handle_var },
871 ));
872 }
873 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
874 out.push_str(&crate::template_env::render(
875 "named_param_teardown_hglobal.jinja",
876 minijinja::context! { indent, handle_var => &handle_var },
877 ));
878 }
879 TypeRef::Bytes => {
880 out.push_str(&crate::template_env::render(
881 "named_param_teardown_gchandle.jinja",
882 minijinja::context! { indent, handle_var => &handle_var },
883 ));
884 }
885 _ => {}
886 }
887 }
888}
889
890use heck::ToLowerCamelCase;