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
73 let streaming_methods: HashSet<String> = config
78 .adapters
79 .iter()
80 .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
81 .map(|a| a.name.clone())
82 .collect();
83 let streaming_methods_meta: HashMap<String, StreamingMethodMeta> = config
84 .adapters
85 .iter()
86 .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
87 .filter_map(|a| {
88 let owner_type = a.owner_type.clone()?;
89 let item_type = a.item_type.clone()?;
90 Some((a.name.clone(), StreamingMethodMeta { owner_type, item_type }))
91 })
92 .collect();
93
94 let exclude_functions: HashSet<String> = config
96 .csharp
97 .as_ref()
98 .map(|c| c.exclude_functions.iter().cloned().collect())
99 .unwrap_or_default();
100
101 let output_dir = resolve_output_dir(config.output_paths.get("csharp"), &config.name, "packages/csharp/");
102
103 let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
104
105 let mut files = Vec::new();
106
107 let exception_class_name = format!("{}Exception", api.crate_name.to_pascal_case());
109
110 files.push(GeneratedFile {
112 path: base_path.join("NativeMethods.cs"),
113 content: strip_trailing_whitespace(&functions::gen_native_methods(
114 api,
115 &namespace,
116 &lib_name,
117 &prefix,
118 &bridge_param_names,
119 &bridge_type_aliases,
120 has_visitor_callbacks,
121 &config.trait_bridges,
122 &streaming_methods,
123 &streaming_methods_meta,
124 &exclude_functions,
125 )),
126 generated_header: true,
127 });
128
129 if !api.errors.is_empty() {
131 for error in &api.errors {
132 let error_files =
133 alef_codegen::error_gen::gen_csharp_error_types(error, &namespace, Some(&exception_class_name));
134 for (class_name, content) in error_files {
135 files.push(GeneratedFile {
136 path: base_path.join(format!("{}.cs", class_name)),
137 content: strip_trailing_whitespace(&content),
138 generated_header: false, });
140 }
141 }
142 }
143
144 if api.errors.is_empty()
146 || !api
147 .errors
148 .iter()
149 .any(|e| format!("{}Exception", e.name) == exception_class_name)
150 {
151 files.push(GeneratedFile {
152 path: base_path.join(format!("{}.cs", exception_class_name)),
153 content: strip_trailing_whitespace(&errors::gen_exception_class(&namespace, &exception_class_name)),
154 generated_header: true,
155 });
156 }
157
158 let base_class_name = api.crate_name.to_pascal_case();
160 let wrapper_class_name = if namespace == base_class_name {
161 format!("{}Lib", base_class_name)
162 } else {
163 base_class_name
164 };
165 files.push(GeneratedFile {
166 path: base_path.join(format!("{}.cs", wrapper_class_name)),
167 content: strip_trailing_whitespace(&methods::gen_wrapper_class(
168 api,
169 &namespace,
170 &wrapper_class_name,
171 &exception_class_name,
172 &prefix,
173 &bridge_param_names,
174 &bridge_type_aliases,
175 has_visitor_callbacks,
176 &streaming_methods,
177 &streaming_methods_meta,
178 &exclude_functions,
179 )),
180 generated_header: true,
181 });
182
183 if has_visitor_callbacks {
185 for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace) {
186 files.push(GeneratedFile {
187 path: base_path.join(filename),
188 content: strip_trailing_whitespace(&content),
189 generated_header: true,
190 });
191 }
192 delete_superseded_visitor_files(&base_path)?;
196 } else {
197 delete_stale_visitor_files(&base_path)?;
200 }
201
202 if !config.trait_bridges.is_empty() {
204 let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
205 let bridges: Vec<_> = config
206 .trait_bridges
207 .iter()
208 .filter_map(|cfg| {
209 let trait_name = cfg.trait_name.clone();
210 trait_defs
211 .iter()
212 .find(|t| t.name == trait_name)
213 .map(|trait_def| (trait_name, cfg, *trait_def))
214 })
215 .collect();
216
217 if !bridges.is_empty() {
218 let (filename, content) = crate::trait_bridge::gen_trait_bridges_file(&namespace, &prefix, &bridges);
219 files.push(GeneratedFile {
220 path: base_path.join(filename),
221 content: strip_trailing_whitespace(&content),
222 generated_header: true,
223 });
224 }
225 }
226
227 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
229
230 let all_opaque_type_names: HashSet<String> = api
233 .types
234 .iter()
235 .filter(|t| t.is_opaque)
236 .map(|t| t.name.to_pascal_case())
237 .collect();
238
239 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
241 if typ.is_opaque {
242 let type_filename = typ.name.to_pascal_case();
243 files.push(GeneratedFile {
244 path: base_path.join(format!("{}.cs", type_filename)),
245 content: strip_trailing_whitespace(&types::gen_opaque_handle(
246 typ,
247 &namespace,
248 &exception_class_name,
249 &enum_names,
250 &streaming_methods,
251 &streaming_methods_meta,
252 &all_opaque_type_names,
253 )),
254 generated_header: true,
255 });
256 }
257 }
258
259 let complex_enums: HashSet<String> = HashSet::new();
263
264 let custom_converter_enums: HashSet<String> = api
270 .enums
271 .iter()
272 .filter(|e| {
273 let is_tagged_union = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
275 if is_tagged_union {
276 return false;
277 }
278 let rename_all_differs = matches!(
283 e.serde_rename_all.as_deref(),
284 Some("kebab-case") | Some("SCREAMING-KEBAB-CASE") | Some("camelCase") | Some("PascalCase")
285 );
286 if rename_all_differs {
287 return true;
288 }
289 e.variants.iter().any(|v| {
291 if let Some(ref rename) = v.serde_rename {
292 let snake = enums::apply_rename_all(&v.name, e.serde_rename_all.as_deref());
293 rename != &snake
294 } else {
295 false
296 }
297 })
298 })
299 .map(|e| e.name.to_pascal_case())
300 .collect();
301
302 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
304
305 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
307 if !typ.is_opaque {
308 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
311 if !typ.fields.is_empty() && !has_named_fields {
312 continue;
313 }
314 if has_visitor_callbacks && (typ.name == "NodeContext" || typ.name == "VisitResult") {
316 continue;
317 }
318
319 let type_filename = typ.name.to_pascal_case();
320 files.push(GeneratedFile {
321 path: base_path.join(format!("{}.cs", type_filename)),
322 content: strip_trailing_whitespace(&types::gen_record_type(
323 typ,
324 &namespace,
325 &enum_names,
326 &complex_enums,
327 &custom_converter_enums,
328 &lang_rename_all,
329 &bridge_type_aliases,
330 &exception_class_name,
331 )),
332 generated_header: true,
333 });
334 }
335 }
336
337 for enum_def in &api.enums {
339 if has_visitor_callbacks && (enum_def.name == "VisitResult" || enum_def.name == "NodeContext") {
341 continue;
342 }
343 let enum_filename = enum_def.name.to_pascal_case();
344 files.push(GeneratedFile {
345 path: base_path.join(format!("{}.cs", enum_filename)),
346 content: strip_trailing_whitespace(&enums::gen_enum(enum_def, &namespace)),
347 generated_header: true,
348 });
349 }
350
351 let needs_byte_array_converter = api
354 .types
355 .iter()
356 .any(|t| !t.is_opaque && t.fields.iter().any(|f| !f.optional && matches!(f.ty, TypeRef::Bytes)));
357 if needs_byte_array_converter {
358 files.push(GeneratedFile {
359 path: base_path.join("ByteArrayToIntArrayConverter.cs"),
360 content: types::gen_byte_array_to_int_array_converter(&namespace),
361 generated_header: true,
362 });
363 }
364
365 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
367
368 files.push(GeneratedFile {
372 path: PathBuf::from("packages/csharp/Directory.Build.props"),
373 content: gen_directory_build_props(),
374 generated_header: true,
375 });
376
377 Ok(files)
378 }
379
380 fn generate_public_api(
385 &self,
386 _api: &ApiSurface,
387 _config: &ResolvedCrateConfig,
388 ) -> anyhow::Result<Vec<GeneratedFile>> {
389 Ok(vec![])
391 }
392
393 fn build_config(&self) -> Option<BuildConfig> {
394 Some(BuildConfig {
395 tool: "dotnet",
396 crate_suffix: "",
397 build_dep: BuildDependency::Ffi,
398 post_build: vec![],
399 })
400 }
401}
402
403pub(super) fn is_tuple_field(field: &FieldDef) -> bool {
405 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
406 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
407}
408
409pub(super) fn strip_trailing_whitespace(content: &str) -> String {
411 let mut result: String = content
412 .lines()
413 .map(|line| line.trim_end())
414 .collect::<Vec<_>>()
415 .join("\n");
416 if !result.ends_with('\n') {
417 result.push('\n');
418 }
419 result
420}
421
422pub(super) fn csharp_file_header() -> String {
424 let mut out = hash::header(CommentStyle::DoubleSlash);
425 out.push_str("#nullable enable\n\n");
426 out
427}
428
429fn gen_directory_build_props() -> String {
432 "<!-- auto-generated by alef (generate_bindings) -->\n\
433<Project>\n \
434<PropertyGroup>\n \
435<Nullable>enable</Nullable>\n \
436<LangVersion>latest</LangVersion>\n \
437<TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n \
438</PropertyGroup>\n\
439</Project>\n"
440 .to_string()
441}
442
443fn delete_superseded_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
448 let superseded = ["IVisitor.cs", "VisitorCallbacks.cs"];
449 for filename in superseded {
450 let path = base_path.join(filename);
451 if path.exists() {
452 std::fs::remove_file(&path)
453 .map_err(|e| anyhow::anyhow!("Failed to delete superseded visitor file {}: {}", path.display(), e))?;
454 }
455 }
456 Ok(())
457}
458
459fn delete_stale_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
463 let stale_files = vec!["IVisitor.cs", "VisitorCallbacks.cs", "NodeContext.cs", "VisitResult.cs"];
464
465 for filename in stale_files {
466 let path = base_path.join(filename);
467 if path.exists() {
468 std::fs::remove_file(&path)
469 .map_err(|e| anyhow::anyhow!("Failed to delete stale visitor file {}: {}", path.display(), e))?;
470 }
471 }
472
473 Ok(())
474}
475
476use alef_core::ir::PrimitiveType;
481
482pub(super) fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
489 match ty {
490 TypeRef::Unit => "void",
491 TypeRef::Primitive(PrimitiveType::Bool) => "int",
493 TypeRef::Primitive(PrimitiveType::U8) => "byte",
495 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
496 TypeRef::Primitive(PrimitiveType::U32) => "uint",
497 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
498 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
499 TypeRef::Primitive(PrimitiveType::I16) => "short",
500 TypeRef::Primitive(PrimitiveType::I32) => "int",
501 TypeRef::Primitive(PrimitiveType::I64) => "long",
502 TypeRef::Primitive(PrimitiveType::F32) => "float",
503 TypeRef::Primitive(PrimitiveType::F64) => "double",
504 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
505 TypeRef::Primitive(PrimitiveType::Isize) => "long",
506 TypeRef::Duration => "ulong",
508 TypeRef::String
510 | TypeRef::Char
511 | TypeRef::Bytes
512 | TypeRef::Optional(_)
513 | TypeRef::Vec(_)
514 | TypeRef::Map(_, _)
515 | TypeRef::Named(_)
516 | TypeRef::Path
517 | TypeRef::Json => "IntPtr",
518 }
519}
520
521pub(super) fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
528 match ty {
529 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
530 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
532 TypeRef::Unit => "void",
533 TypeRef::Primitive(PrimitiveType::Bool) => "int",
534 TypeRef::Primitive(PrimitiveType::U8) => "byte",
535 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
536 TypeRef::Primitive(PrimitiveType::U32) => "uint",
537 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
538 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
539 TypeRef::Primitive(PrimitiveType::I16) => "short",
540 TypeRef::Primitive(PrimitiveType::I32) => "int",
541 TypeRef::Primitive(PrimitiveType::I64) => "long",
542 TypeRef::Primitive(PrimitiveType::F32) => "float",
543 TypeRef::Primitive(PrimitiveType::F64) => "double",
544 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
545 TypeRef::Primitive(PrimitiveType::Isize) => "long",
546 TypeRef::Duration => "ulong",
547 }
548}
549
550pub(super) fn is_bridge_param(
553 param: &alef_core::ir::ParamDef,
554 bridge_param_names: &HashSet<String>,
555 bridge_type_aliases: &HashSet<String>,
556) -> bool {
557 bridge_param_names.contains(¶m.name)
558 || matches!(¶m.ty, alef_core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
559}
560
561pub(super) fn returns_string(ty: &TypeRef) -> bool {
563 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
564}
565
566pub(super) fn returns_bool_via_int(ty: &TypeRef) -> bool {
568 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
569}
570
571pub(super) fn returns_json_object(ty: &TypeRef) -> bool {
573 matches!(
574 ty,
575 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
576 )
577}
578
579pub(super) fn returns_ptr(ty: &TypeRef) -> bool {
582 matches!(
583 ty,
584 TypeRef::String
585 | TypeRef::Char
586 | TypeRef::Path
587 | TypeRef::Json
588 | TypeRef::Named(_)
589 | TypeRef::Vec(_)
590 | TypeRef::Map(_, _)
591 | TypeRef::Bytes
592 | TypeRef::Optional(_)
593 )
594}
595
596pub(super) fn native_call_arg(
602 ty: &TypeRef,
603 param_name: &str,
604 optional: bool,
605 true_opaque_types: &HashSet<String>,
606) -> String {
607 match ty {
608 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
609 let bang = if optional { "!" } else { "" };
611 format!("{param_name}{bang}.Handle")
612 }
613 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
614 format!("{param_name}Handle")
615 }
616 TypeRef::Bytes => {
617 format!("{param_name}Handle.AddrOfPinnedObject()")
618 }
619 TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool) => {
620 if optional {
622 format!("({param_name}?.Value ? 1 : 0)")
623 } else {
624 format!("({param_name} ? 1 : 0)")
625 }
626 }
627 ty => {
628 if optional {
629 if let TypeRef::Primitive(prim) = ty {
637 use alef_core::ir::PrimitiveType;
638 let sentinel = match prim {
639 PrimitiveType::U8 => "byte.MaxValue",
640 PrimitiveType::U16 => "ushort.MaxValue",
641 PrimitiveType::U32 => "uint.MaxValue",
642 PrimitiveType::U64 | PrimitiveType::Usize => "ulong.MaxValue",
643 PrimitiveType::I8 => "sbyte.MaxValue",
644 PrimitiveType::I16 => "short.MaxValue",
645 PrimitiveType::I32 => "int.MaxValue",
646 PrimitiveType::I64 | PrimitiveType::Isize => "long.MaxValue",
647 PrimitiveType::F32 => "float.NaN",
648 PrimitiveType::F64 => "double.NaN",
649 PrimitiveType::Bool => unreachable!("handled above"),
650 };
651 format!("{param_name} ?? {sentinel}")
652 } else if matches!(ty, TypeRef::Duration) {
653 format!("{param_name}.GetValueOrDefault()")
654 } else {
655 format!("{param_name}!")
656 }
657 } else {
658 param_name.to_string()
659 }
660 }
661 }
662}
663
664pub(super) fn emit_named_param_setup(
669 out: &mut String,
670 params: &[alef_core::ir::ParamDef],
671 indent: &str,
672 true_opaque_types: &HashSet<String>,
673 exception_name: &str,
674) {
675 for param in params {
676 let param_name = param.name.to_lower_camel_case();
677 let json_var = format!("{param_name}Json");
678 let handle_var = format!("{param_name}Handle");
679
680 match ¶m.ty {
681 TypeRef::Named(type_name) => {
682 if true_opaque_types.contains(type_name) {
685 continue;
686 }
687 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
688
689 let is_config_param = param.name == "config";
691 let param_to_serialize = if is_config_param {
692 let type_pascal = type_name.to_pascal_case();
693 format!("({} ?? new {}())", param_name, type_pascal)
694 } else {
695 param_name.to_string()
696 };
697
698 if param.optional && !is_config_param {
699 out.push_str(&crate::template_env::render(
703 "named_param_handle_from_json_optional.jinja",
704 minijinja::context! {
705 indent,
706 handle_var => &handle_var,
707 from_json_method => &from_json_method,
708 json_var => &json_var,
709 param_name => ¶m_name,
710 exception_name => exception_name,
711 },
712 ));
713 } else {
714 out.push_str(&crate::template_env::render(
715 "named_param_json_serialize.jinja",
716 minijinja::context! { indent, json_var => &json_var, param_name => ¶m_to_serialize },
717 ));
718 out.push_str(&crate::template_env::render(
719 "named_param_handle_from_json.jinja",
720 minijinja::context! {
721 indent,
722 handle_var => &handle_var,
723 from_json_method => &from_json_method,
724 json_var => &json_var,
725 exception_name => exception_name,
726 },
727 ));
728 }
729 }
730 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
731 out.push_str(&crate::template_env::render(
733 "named_param_json_serialize.jinja",
734 minijinja::context! { indent, json_var => &json_var, param_name => ¶m_name },
735 ));
736 out.push_str(&crate::template_env::render(
737 "named_param_handle_string.jinja",
738 minijinja::context! { indent, handle_var => &handle_var, json_var => &json_var },
739 ));
740 }
741 TypeRef::Bytes => {
742 out.push_str(&crate::template_env::render(
744 "named_param_handle_pin.jinja",
745 minijinja::context! { indent, handle_var => &handle_var, param_name => ¶m_name },
746 ));
747 }
748 _ => {}
749 }
750 }
751}
752
753pub(super) fn emit_named_param_teardown(
758 out: &mut String,
759 params: &[alef_core::ir::ParamDef],
760 true_opaque_types: &HashSet<String>,
761) {
762 for param in params {
763 let param_name = param.name.to_lower_camel_case();
764 let handle_var = format!("{param_name}Handle");
765 match ¶m.ty {
766 TypeRef::Named(type_name) => {
767 if true_opaque_types.contains(type_name) {
768 continue;
770 }
771 let free_method = format!("{}Free", type_name.to_pascal_case());
772 out.push_str(&crate::template_env::render(
773 "named_param_teardown_free.jinja",
774 minijinja::context! { indent => " ", free_method => &free_method, handle_var => &handle_var },
775 ));
776 }
777 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
778 out.push_str(&crate::template_env::render(
779 "named_param_teardown_hglobal.jinja",
780 minijinja::context! { indent => " ", handle_var => &handle_var },
781 ));
782 }
783 TypeRef::Bytes => {
784 out.push_str(&crate::template_env::render(
785 "named_param_teardown_gchandle.jinja",
786 minijinja::context! { indent => " ", handle_var => &handle_var },
787 ));
788 }
789 _ => {}
790 }
791 }
792}
793
794pub(super) fn emit_named_param_teardown_indented(
796 out: &mut String,
797 params: &[alef_core::ir::ParamDef],
798 indent: &str,
799 true_opaque_types: &HashSet<String>,
800) {
801 for param in params {
802 let param_name = param.name.to_lower_camel_case();
803 let handle_var = format!("{param_name}Handle");
804 match ¶m.ty {
805 TypeRef::Named(type_name) => {
806 if true_opaque_types.contains(type_name) {
807 continue;
809 }
810 let free_method = format!("{}Free", type_name.to_pascal_case());
811 out.push_str(&crate::template_env::render(
812 "named_param_teardown_free.jinja",
813 minijinja::context! { indent, free_method => &free_method, handle_var => &handle_var },
814 ));
815 }
816 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
817 out.push_str(&crate::template_env::render(
818 "named_param_teardown_hglobal.jinja",
819 minijinja::context! { indent, handle_var => &handle_var },
820 ));
821 }
822 TypeRef::Bytes => {
823 out.push_str(&crate::template_env::render(
824 "named_param_teardown_gchandle.jinja",
825 minijinja::context! { indent, handle_var => &handle_var },
826 ));
827 }
828 _ => {}
829 }
830 }
831}
832
833use heck::ToLowerCamelCase;