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 for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace) {
188 files.push(GeneratedFile {
189 path: base_path.join(filename),
190 content: strip_trailing_whitespace(&content),
191 generated_header: true,
192 });
193 }
194 delete_superseded_visitor_files(&base_path)?;
198 } else {
199 delete_stale_visitor_files(&base_path)?;
202 }
203
204 if !config.trait_bridges.is_empty() {
206 let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
207 let bridges: Vec<_> = config
208 .trait_bridges
209 .iter()
210 .filter_map(|cfg| {
211 let trait_name = cfg.trait_name.clone();
212 trait_defs
213 .iter()
214 .find(|t| t.name == trait_name)
215 .map(|trait_def| (trait_name, cfg, *trait_def))
216 })
217 .collect();
218
219 if !bridges.is_empty() {
220 let (filename, content) = crate::trait_bridge::gen_trait_bridges_file(&namespace, &prefix, &bridges);
221 files.push(GeneratedFile {
222 path: base_path.join(filename),
223 content: strip_trailing_whitespace(&content),
224 generated_header: true,
225 });
226 }
227 }
228
229 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
231
232 let all_opaque_type_names: HashSet<String> = api
235 .types
236 .iter()
237 .filter(|t| t.is_opaque)
238 .map(|t| t.name.to_pascal_case())
239 .collect();
240
241 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
243 if typ.is_opaque {
244 let type_filename = typ.name.to_pascal_case();
245 files.push(GeneratedFile {
246 path: base_path.join(format!("{}.cs", type_filename)),
247 content: strip_trailing_whitespace(&types::gen_opaque_handle(
248 typ,
249 &namespace,
250 &exception_class_name,
251 &enum_names,
252 &streaming_methods,
253 &streaming_methods_meta,
254 &all_opaque_type_names,
255 )),
256 generated_header: true,
257 });
258 }
259 }
260
261 let complex_enums: HashSet<String> = HashSet::new();
265
266 let custom_converter_enums: HashSet<String> = api
272 .enums
273 .iter()
274 .filter(|e| {
275 let is_tagged_union = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
277 if is_tagged_union {
278 return false;
279 }
280 let rename_all_differs = matches!(
285 e.serde_rename_all.as_deref(),
286 Some("kebab-case") | Some("SCREAMING-KEBAB-CASE") | Some("camelCase") | Some("PascalCase")
287 );
288 if rename_all_differs {
289 return true;
290 }
291 e.variants.iter().any(|v| {
293 if let Some(ref rename) = v.serde_rename {
294 let snake = enums::apply_rename_all(&v.name, e.serde_rename_all.as_deref());
295 rename != &snake
296 } else {
297 false
298 }
299 })
300 })
301 .map(|e| e.name.to_pascal_case())
302 .collect();
303
304 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
306
307 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
309 if !typ.is_opaque {
310 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
313 if !typ.fields.is_empty() && !has_named_fields {
314 continue;
315 }
316 if has_visitor_callbacks && bridge_associated_types.contains(typ.name.as_str()) {
318 continue;
319 }
320
321 let type_filename = typ.name.to_pascal_case();
322 files.push(GeneratedFile {
323 path: base_path.join(format!("{}.cs", type_filename)),
324 content: strip_trailing_whitespace(&types::gen_record_type(
325 typ,
326 &namespace,
327 &enum_names,
328 &complex_enums,
329 &custom_converter_enums,
330 &lang_rename_all,
331 &bridge_type_aliases,
332 &exception_class_name,
333 )),
334 generated_header: true,
335 });
336 }
337 }
338
339 for enum_def in &api.enums {
341 if has_visitor_callbacks && bridge_associated_types.contains(enum_def.name.as_str()) {
343 continue;
344 }
345 let enum_filename = enum_def.name.to_pascal_case();
346 files.push(GeneratedFile {
347 path: base_path.join(format!("{}.cs", enum_filename)),
348 content: strip_trailing_whitespace(&enums::gen_enum(enum_def, &namespace)),
349 generated_header: true,
350 });
351 }
352
353 let needs_byte_array_converter = api
356 .types
357 .iter()
358 .any(|t| !t.is_opaque && t.fields.iter().any(|f| !f.optional && matches!(f.ty, TypeRef::Bytes)));
359 if needs_byte_array_converter {
360 files.push(GeneratedFile {
361 path: base_path.join("ByteArrayToIntArrayConverter.cs"),
362 content: types::gen_byte_array_to_int_array_converter(&namespace),
363 generated_header: true,
364 });
365 }
366
367 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
369
370 files.push(GeneratedFile {
374 path: PathBuf::from("packages/csharp/Directory.Build.props"),
375 content: gen_directory_build_props(),
376 generated_header: true,
377 });
378
379 Ok(files)
380 }
381
382 fn generate_public_api(
387 &self,
388 _api: &ApiSurface,
389 _config: &ResolvedCrateConfig,
390 ) -> anyhow::Result<Vec<GeneratedFile>> {
391 Ok(vec![])
393 }
394
395 fn build_config(&self) -> Option<BuildConfig> {
396 Some(BuildConfig {
397 tool: "dotnet",
398 crate_suffix: "",
399 build_dep: BuildDependency::Ffi,
400 post_build: vec![],
401 })
402 }
403}
404
405pub(super) fn is_tuple_field(field: &FieldDef) -> bool {
407 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
408 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
409}
410
411pub(super) fn strip_trailing_whitespace(content: &str) -> String {
413 let mut result: String = content
414 .lines()
415 .map(|line| line.trim_end())
416 .collect::<Vec<_>>()
417 .join("\n");
418 if !result.ends_with('\n') {
419 result.push('\n');
420 }
421 result
422}
423
424pub(super) fn csharp_file_header() -> String {
426 let mut out = hash::header(CommentStyle::DoubleSlash);
427 out.push_str("#nullable enable\n\n");
428 out
429}
430
431fn gen_directory_build_props() -> String {
434 "<!-- auto-generated by alef (generate_bindings) -->\n\
435<Project>\n \
436<PropertyGroup>\n \
437<Nullable>enable</Nullable>\n \
438<LangVersion>latest</LangVersion>\n \
439<TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n \
440</PropertyGroup>\n\
441</Project>\n"
442 .to_string()
443}
444
445fn delete_superseded_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
450 let superseded = ["IVisitor.cs", "VisitorCallbacks.cs"];
451 for filename in superseded {
452 let path = base_path.join(filename);
453 if path.exists() {
454 std::fs::remove_file(&path)
455 .map_err(|e| anyhow::anyhow!("Failed to delete superseded visitor file {}: {}", path.display(), e))?;
456 }
457 }
458 Ok(())
459}
460
461fn delete_stale_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
465 let stale_files = vec!["IVisitor.cs", "VisitorCallbacks.cs", "NodeContext.cs", "VisitResult.cs"];
466
467 for filename in stale_files {
468 let path = base_path.join(filename);
469 if path.exists() {
470 std::fs::remove_file(&path)
471 .map_err(|e| anyhow::anyhow!("Failed to delete stale visitor file {}: {}", path.display(), e))?;
472 }
473 }
474
475 Ok(())
476}
477
478use alef_core::ir::PrimitiveType;
483
484pub(super) fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
491 match ty {
492 TypeRef::Unit => "void",
493 TypeRef::Primitive(PrimitiveType::Bool) => "int",
495 TypeRef::Primitive(PrimitiveType::U8) => "byte",
497 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
498 TypeRef::Primitive(PrimitiveType::U32) => "uint",
499 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
500 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
501 TypeRef::Primitive(PrimitiveType::I16) => "short",
502 TypeRef::Primitive(PrimitiveType::I32) => "int",
503 TypeRef::Primitive(PrimitiveType::I64) => "long",
504 TypeRef::Primitive(PrimitiveType::F32) => "float",
505 TypeRef::Primitive(PrimitiveType::F64) => "double",
506 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
507 TypeRef::Primitive(PrimitiveType::Isize) => "long",
508 TypeRef::Duration => "ulong",
510 TypeRef::String
512 | TypeRef::Char
513 | TypeRef::Bytes
514 | TypeRef::Optional(_)
515 | TypeRef::Vec(_)
516 | TypeRef::Map(_, _)
517 | TypeRef::Named(_)
518 | TypeRef::Path
519 | TypeRef::Json => "IntPtr",
520 }
521}
522
523pub(super) fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
530 match ty {
531 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
532 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
534 TypeRef::Unit => "void",
535 TypeRef::Primitive(PrimitiveType::Bool) => "int",
536 TypeRef::Primitive(PrimitiveType::U8) => "byte",
537 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
538 TypeRef::Primitive(PrimitiveType::U32) => "uint",
539 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
540 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
541 TypeRef::Primitive(PrimitiveType::I16) => "short",
542 TypeRef::Primitive(PrimitiveType::I32) => "int",
543 TypeRef::Primitive(PrimitiveType::I64) => "long",
544 TypeRef::Primitive(PrimitiveType::F32) => "float",
545 TypeRef::Primitive(PrimitiveType::F64) => "double",
546 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
547 TypeRef::Primitive(PrimitiveType::Isize) => "long",
548 TypeRef::Duration => "ulong",
549 }
550}
551
552pub(super) fn is_bridge_param(
555 param: &alef_core::ir::ParamDef,
556 bridge_param_names: &HashSet<String>,
557 bridge_type_aliases: &HashSet<String>,
558) -> bool {
559 bridge_param_names.contains(¶m.name)
560 || matches!(¶m.ty, alef_core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
561}
562
563pub(super) fn returns_string(ty: &TypeRef) -> bool {
565 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
566}
567
568pub(super) fn returns_bool_via_int(ty: &TypeRef) -> bool {
570 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
571}
572
573pub(super) fn returns_json_object(ty: &TypeRef) -> bool {
575 matches!(
576 ty,
577 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
578 )
579}
580
581pub(super) fn returns_ptr(ty: &TypeRef) -> bool {
584 matches!(
585 ty,
586 TypeRef::String
587 | TypeRef::Char
588 | TypeRef::Path
589 | TypeRef::Json
590 | TypeRef::Named(_)
591 | TypeRef::Vec(_)
592 | TypeRef::Map(_, _)
593 | TypeRef::Bytes
594 | TypeRef::Optional(_)
595 )
596}
597
598pub(super) fn native_call_arg(
604 ty: &TypeRef,
605 param_name: &str,
606 optional: bool,
607 true_opaque_types: &HashSet<String>,
608) -> String {
609 match ty {
610 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
611 let bang = if optional { "!" } else { "" };
613 format!("{param_name}{bang}.Handle")
614 }
615 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
616 format!("{param_name}Handle")
617 }
618 TypeRef::Bytes => {
619 format!("{param_name}Handle.AddrOfPinnedObject()")
620 }
621 TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool) => {
622 if optional {
624 format!("({param_name}?.Value ? 1 : 0)")
625 } else {
626 format!("({param_name} ? 1 : 0)")
627 }
628 }
629 ty => {
630 if optional {
631 if let TypeRef::Primitive(prim) = ty {
639 use alef_core::ir::PrimitiveType;
640 let sentinel = match prim {
641 PrimitiveType::U8 => "byte.MaxValue",
642 PrimitiveType::U16 => "ushort.MaxValue",
643 PrimitiveType::U32 => "uint.MaxValue",
644 PrimitiveType::U64 | PrimitiveType::Usize => "ulong.MaxValue",
645 PrimitiveType::I8 => "sbyte.MaxValue",
646 PrimitiveType::I16 => "short.MaxValue",
647 PrimitiveType::I32 => "int.MaxValue",
648 PrimitiveType::I64 | PrimitiveType::Isize => "long.MaxValue",
649 PrimitiveType::F32 => "float.NaN",
650 PrimitiveType::F64 => "double.NaN",
651 PrimitiveType::Bool => unreachable!("handled above"),
652 };
653 format!("{param_name} ?? {sentinel}")
654 } else if matches!(ty, TypeRef::Duration) {
655 format!("{param_name}.GetValueOrDefault()")
656 } else {
657 format!("{param_name}!")
658 }
659 } else {
660 param_name.to_string()
661 }
662 }
663 }
664}
665
666pub(super) fn emit_named_param_setup(
671 out: &mut String,
672 params: &[alef_core::ir::ParamDef],
673 indent: &str,
674 true_opaque_types: &HashSet<String>,
675 exception_name: &str,
676) {
677 for param in params {
678 let param_name = param.name.to_lower_camel_case();
679 let json_var = format!("{param_name}Json");
680 let handle_var = format!("{param_name}Handle");
681
682 match ¶m.ty {
683 TypeRef::Named(type_name) => {
684 if true_opaque_types.contains(type_name) {
687 continue;
688 }
689 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
690
691 let is_config_param = param.name == "config";
693 let param_to_serialize = if is_config_param {
694 let type_pascal = type_name.to_pascal_case();
695 format!("({} ?? new {}())", param_name, type_pascal)
696 } else {
697 param_name.to_string()
698 };
699
700 if param.optional && !is_config_param {
701 out.push_str(&crate::template_env::render(
705 "named_param_handle_from_json_optional.jinja",
706 minijinja::context! {
707 indent,
708 handle_var => &handle_var,
709 from_json_method => &from_json_method,
710 json_var => &json_var,
711 param_name => ¶m_name,
712 exception_name => exception_name,
713 },
714 ));
715 } else {
716 out.push_str(&crate::template_env::render(
717 "named_param_json_serialize.jinja",
718 minijinja::context! { indent, json_var => &json_var, param_name => ¶m_to_serialize },
719 ));
720 out.push_str(&crate::template_env::render(
721 "named_param_handle_from_json.jinja",
722 minijinja::context! {
723 indent,
724 handle_var => &handle_var,
725 from_json_method => &from_json_method,
726 json_var => &json_var,
727 exception_name => exception_name,
728 },
729 ));
730 }
731 }
732 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
733 out.push_str(&crate::template_env::render(
735 "named_param_json_serialize.jinja",
736 minijinja::context! { indent, json_var => &json_var, param_name => ¶m_name },
737 ));
738 out.push_str(&crate::template_env::render(
739 "named_param_handle_string.jinja",
740 minijinja::context! { indent, handle_var => &handle_var, json_var => &json_var },
741 ));
742 }
743 TypeRef::Bytes => {
744 out.push_str(&crate::template_env::render(
746 "named_param_handle_pin.jinja",
747 minijinja::context! { indent, handle_var => &handle_var, param_name => ¶m_name },
748 ));
749 }
750 _ => {}
751 }
752 }
753}
754
755pub(super) fn emit_named_param_teardown(
760 out: &mut String,
761 params: &[alef_core::ir::ParamDef],
762 true_opaque_types: &HashSet<String>,
763) {
764 for param in params {
765 let param_name = param.name.to_lower_camel_case();
766 let handle_var = format!("{param_name}Handle");
767 match ¶m.ty {
768 TypeRef::Named(type_name) => {
769 if true_opaque_types.contains(type_name) {
770 continue;
772 }
773 let free_method = format!("{}Free", type_name.to_pascal_case());
774 out.push_str(&crate::template_env::render(
775 "named_param_teardown_free.jinja",
776 minijinja::context! { indent => " ", free_method => &free_method, handle_var => &handle_var },
777 ));
778 }
779 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
780 out.push_str(&crate::template_env::render(
781 "named_param_teardown_hglobal.jinja",
782 minijinja::context! { indent => " ", handle_var => &handle_var },
783 ));
784 }
785 TypeRef::Bytes => {
786 out.push_str(&crate::template_env::render(
787 "named_param_teardown_gchandle.jinja",
788 minijinja::context! { indent => " ", handle_var => &handle_var },
789 ));
790 }
791 _ => {}
792 }
793 }
794}
795
796pub(super) fn emit_named_param_teardown_indented(
798 out: &mut String,
799 params: &[alef_core::ir::ParamDef],
800 indent: &str,
801 true_opaque_types: &HashSet<String>,
802) {
803 for param in params {
804 let param_name = param.name.to_lower_camel_case();
805 let handle_var = format!("{param_name}Handle");
806 match ¶m.ty {
807 TypeRef::Named(type_name) => {
808 if true_opaque_types.contains(type_name) {
809 continue;
811 }
812 let free_method = format!("{}Free", type_name.to_pascal_case());
813 out.push_str(&crate::template_env::render(
814 "named_param_teardown_free.jinja",
815 minijinja::context! { indent, free_method => &free_method, handle_var => &handle_var },
816 ));
817 }
818 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
819 out.push_str(&crate::template_env::render(
820 "named_param_teardown_hglobal.jinja",
821 minijinja::context! { indent, handle_var => &handle_var },
822 ));
823 }
824 TypeRef::Bytes => {
825 out.push_str(&crate::template_env::render(
826 "named_param_teardown_gchandle.jinja",
827 minijinja::context! { indent, handle_var => &handle_var },
828 ));
829 }
830 _ => {}
831 }
832 }
833}
834
835use heck::ToLowerCamelCase;