1use alef_codegen::naming::{csharp_type_name, to_csharp_name};
2use alef_codegen::shared::binding_fields;
3use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
4use alef_core::config::{AdapterPattern, Language, ResolvedCrateConfig, resolve_output_dir};
5use alef_core::hash::{self, CommentStyle};
6use alef_core::ir::{ApiSurface, FieldDef, TypeDef, TypeRef};
7use std::collections::{HashMap, HashSet};
8use std::path::PathBuf;
9
10#[derive(Debug, Clone)]
14pub(super) struct StreamingMethodMeta {
15 #[allow(dead_code)]
18 pub owner_type: String,
19 pub item_type: String,
20}
21
22pub(super) mod enums;
23pub(super) mod errors;
24pub(super) mod functions;
25pub(super) mod methods;
26pub(super) mod types;
27
28pub(crate) fn sanitize_rust_syntax_for_csharp(doc: &str) -> String {
35 alef_codegen::doc_emission::sanitize_rust_idioms(doc, alef_codegen::doc_emission::DocTarget::CSharpDoc)
36}
37
38pub(crate) fn sanitize_doc_lines_for_csharp(doc: &str) -> Vec<String> {
45 if doc.is_empty() {
46 return Vec::new();
47 }
48 let sanitized = sanitize_rust_syntax_for_csharp(doc);
49 if sanitized.trim().is_empty() {
50 return Vec::new();
51 }
52 sanitized.lines().map(ToString::to_string).collect()
53}
54
55pub struct CsharpBackend;
56
57impl CsharpBackend {
58 }
60
61fn effective_exclude_types(config: &ResolvedCrateConfig) -> HashSet<String> {
62 let mut exclude_types: HashSet<String> = config
63 .ffi
64 .as_ref()
65 .map(|ffi| ffi.exclude_types.iter().cloned().collect())
66 .unwrap_or_default();
67 if let Some(csharp) = &config.csharp {
68 exclude_types.extend(csharp.exclude_types.iter().cloned());
69 }
70 exclude_types
71}
72
73fn references_excluded_type(ty: &TypeRef, exclude_types: &HashSet<String>) -> bool {
74 exclude_types.iter().any(|name| ty.references_named(name))
75}
76
77fn signature_references_excluded_type(
78 params: &[alef_core::ir::ParamDef],
79 return_type: &TypeRef,
80 exclude_types: &HashSet<String>,
81) -> bool {
82 references_excluded_type(return_type, exclude_types)
83 || params
84 .iter()
85 .any(|param| references_excluded_type(¶m.ty, exclude_types))
86}
87
88fn api_without_excluded_types(api: &ApiSurface, exclude_types: &HashSet<String>) -> ApiSurface {
89 let mut filtered = api.clone();
90 filtered.types.retain(|typ| !exclude_types.contains(&typ.name));
91 for typ in &mut filtered.types {
92 typ.fields
93 .retain(|field| !references_excluded_type(&field.ty, exclude_types));
94 typ.methods
95 .retain(|method| !signature_references_excluded_type(&method.params, &method.return_type, exclude_types));
96 }
97 filtered
98 .enums
99 .retain(|enum_def| !exclude_types.contains(&enum_def.name));
100 for enum_def in &mut filtered.enums {
101 for variant in &mut enum_def.variants {
102 variant
103 .fields
104 .retain(|field| !references_excluded_type(&field.ty, exclude_types));
105 }
106 }
107 filtered
108 .functions
109 .retain(|func| !signature_references_excluded_type(&func.params, &func.return_type, exclude_types));
110 filtered.errors.retain(|error| !exclude_types.contains(&error.name));
111 filtered
112}
113
114impl Backend for CsharpBackend {
115 fn name(&self) -> &str {
116 "csharp"
117 }
118
119 fn language(&self) -> Language {
120 Language::Csharp
121 }
122
123 fn capabilities(&self) -> Capabilities {
124 Capabilities {
125 supports_async: true,
126 supports_classes: true,
127 supports_enums: true,
128 supports_option: true,
129 supports_result: true,
130 ..Capabilities::default()
131 }
132 }
133
134 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
135 let exclude_types = effective_exclude_types(config);
136 let filtered_api;
137 let api = if exclude_types.is_empty() {
138 api
139 } else {
140 filtered_api = api_without_excluded_types(api, &exclude_types);
141 &filtered_api
142 };
143 let namespace = config.csharp_namespace();
144 let prefix = config.ffi_prefix();
145 let lib_name = config.ffi_lib_name();
146
147 let bridge_param_names: HashSet<String> = config
150 .trait_bridges
151 .iter()
152 .filter_map(|b| b.param_name.clone())
153 .collect();
154 let bridge_type_aliases: HashSet<String> = config
155 .trait_bridges
156 .iter()
157 .filter_map(|b| b.type_alias.clone())
158 .collect();
159 let has_visitor_callbacks = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false);
161 let bridge_associated_types = config.bridge_associated_types();
162
163 let streaming_methods: HashSet<String> = config
168 .adapters
169 .iter()
170 .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
171 .map(|a| a.name.clone())
172 .collect();
173 let streaming_methods_meta: HashMap<String, StreamingMethodMeta> = config
174 .adapters
175 .iter()
176 .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
177 .filter_map(|a| {
178 let owner_type = a.owner_type.clone()?;
179 let item_type = a.item_type.clone()?;
180 Some((a.name.clone(), StreamingMethodMeta { owner_type, item_type }))
181 })
182 .collect();
183
184 let mut exclude_functions: HashSet<String> = config
186 .csharp
187 .as_ref()
188 .map(|c| c.exclude_functions.iter().cloned().collect())
189 .unwrap_or_default();
190 if let Some(ffi) = &config.ffi {
191 exclude_functions.extend(ffi.exclude_functions.iter().cloned());
192 }
193
194 let output_dir = resolve_output_dir(config.output_paths.get("csharp"), &config.name, "packages/csharp/");
195
196 let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
197
198 let mut files = Vec::new();
199
200 let exception_class_name = format!("{}Exception", to_csharp_name(&api.crate_name));
202
203 files.push(GeneratedFile {
205 path: base_path.join("NativeMethods.cs"),
206 content: strip_trailing_whitespace(&functions::gen_native_methods(
207 api,
208 &namespace,
209 &lib_name,
210 &prefix,
211 &bridge_param_names,
212 &bridge_type_aliases,
213 has_visitor_callbacks,
214 &config.trait_bridges,
215 &streaming_methods,
216 &streaming_methods_meta,
217 &exclude_functions,
218 &config.client_constructors,
219 &config.adapters,
220 )),
221 generated_header: true,
222 });
223
224 if !api.errors.is_empty() {
239 let mut seen_exception_files: HashSet<String> = HashSet::new();
240 for error in &api.errors {
241 let error_files =
242 alef_codegen::error_gen::gen_csharp_error_types(error, &namespace, Some(&exception_class_name));
243 for (class_name, content) in error_files {
244 if !seen_exception_files.insert(class_name.clone()) {
245 continue;
250 }
251 files.push(GeneratedFile {
252 path: base_path.join(format!("{}.cs", class_name)),
253 content: strip_trailing_whitespace(&content),
254 generated_header: false, });
256 }
257 }
258 }
259
260 if api.errors.is_empty()
262 || !api
263 .errors
264 .iter()
265 .any(|e| format!("{}Exception", e.name) == exception_class_name)
266 {
267 files.push(GeneratedFile {
268 path: base_path.join(format!("{}.cs", exception_class_name)),
269 content: strip_trailing_whitespace(&errors::gen_exception_class(&namespace, &exception_class_name)),
270 generated_header: true,
271 });
272 }
273
274 let all_opaque_type_names: HashSet<String> = api
276 .types
277 .iter()
278 .filter(|t| t.is_opaque)
279 .map(|t| csharp_type_name(&t.name))
280 .collect();
281
282 let base_class_name = to_csharp_name(&api.crate_name);
284 let wrapper_class_name = if namespace == base_class_name {
285 format!("{}Lib", base_class_name)
286 } else {
287 base_class_name
288 };
289 files.push(GeneratedFile {
290 path: base_path.join(format!("{}.cs", wrapper_class_name)),
291 content: strip_trailing_whitespace(&methods::gen_wrapper_class(
292 api,
293 &namespace,
294 &wrapper_class_name,
295 &exception_class_name,
296 &prefix,
297 &bridge_param_names,
298 &bridge_type_aliases,
299 has_visitor_callbacks,
300 &streaming_methods,
301 &streaming_methods_meta,
302 &exclude_functions,
303 &config.trait_bridges,
304 &all_opaque_type_names,
305 &config.adapters,
306 )),
307 generated_header: true,
308 });
309
310 if has_visitor_callbacks {
312 let visitor_bridge_cfg = config
315 .trait_bridges
316 .iter()
317 .find(|b| b.bind_via == alef_core::config::BridgeBinding::OptionsField);
318 let trait_map: std::collections::HashMap<&str, &alef_core::ir::TypeDef> = api
319 .types
320 .iter()
321 .filter(|t| t.is_trait)
322 .map(|t| (t.name.as_str(), t))
323 .collect();
324 let visitor_trait = visitor_bridge_cfg.and_then(|b| trait_map.get(b.trait_name.as_str()).copied());
325
326 if let Some(trait_def) = visitor_trait {
327 for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace, trait_def) {
328 files.push(GeneratedFile {
329 path: base_path.join(filename),
330 content: strip_trailing_whitespace(&content),
331 generated_header: true,
332 });
333 }
334 } else {
335 let placeholder = alef_core::ir::TypeDef {
337 name: String::new(),
338 rust_path: String::new(),
339 original_rust_path: String::new(),
340 fields: vec![],
341 methods: vec![],
342 is_opaque: false,
343 is_clone: false,
344 is_copy: false,
345 is_trait: true,
346 has_default: false,
347 has_stripped_cfg_fields: false,
348 is_return_type: false,
349 serde_rename_all: None,
350 has_serde: false,
351 super_traits: vec![],
352 doc: String::new(),
353 cfg: None,
354 binding_excluded: false,
355 binding_exclusion_reason: None,
356 };
357 for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace, &placeholder) {
358 files.push(GeneratedFile {
359 path: base_path.join(filename),
360 content: strip_trailing_whitespace(&content),
361 generated_header: true,
362 });
363 }
364 }
365 delete_superseded_visitor_files(&base_path)?;
369 } else {
370 delete_stale_visitor_files(&base_path)?;
373 }
374
375 if !config.trait_bridges.is_empty() {
377 let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
378 let bridges: Vec<_> = config
379 .trait_bridges
380 .iter()
381 .filter_map(|cfg| {
382 let trait_name = cfg.trait_name.clone();
383 trait_defs
384 .iter()
385 .find(|t| t.name == trait_name)
386 .map(|trait_def| (trait_name, cfg, *trait_def))
387 })
388 .collect();
389
390 if !bridges.is_empty() {
391 let visible_type_names: HashSet<&str> = api
395 .types
396 .iter()
397 .filter(|t| !t.is_trait)
398 .map(|t| t.name.as_str())
399 .chain(api.enums.iter().map(|e| e.name.as_str()))
400 .collect();
401 let (filename, content) =
402 crate::trait_bridge::gen_trait_bridges_file(&namespace, &prefix, &bridges, &visible_type_names);
403 files.push(GeneratedFile {
404 path: base_path.join(filename),
405 content: strip_trailing_whitespace(&content),
406 generated_header: true,
407 });
408 }
409 }
410
411 let enum_names: HashSet<String> = api.enums.iter().map(|e| csharp_type_name(&e.name)).collect();
413
414 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
416 if typ.is_opaque {
417 let type_filename = csharp_type_name(&typ.name);
418 let client_ctor = config.client_constructors.get(&typ.name);
419 files.push(GeneratedFile {
420 path: base_path.join(format!("{}.cs", type_filename)),
421 content: strip_trailing_whitespace(&types::gen_opaque_handle(
422 typ,
423 &api.types,
424 &namespace,
425 &exception_class_name,
426 &enum_names,
427 &streaming_methods,
428 &streaming_methods_meta,
429 &all_opaque_type_names,
430 client_ctor,
431 )),
432 generated_header: true,
433 });
434 }
435 }
436
437 let complex_enums: HashSet<String> = HashSet::new();
441
442 let tagged_union_enums: HashSet<String> = api
448 .enums
449 .iter()
450 .filter(|e| e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty()))
451 .map(|e| csharp_type_name(&e.name))
452 .collect();
453
454 let custom_converter_enums: HashSet<String> = api
460 .enums
461 .iter()
462 .filter(|e| {
463 let is_tagged_union = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
465 if is_tagged_union {
466 return false;
467 }
468 let rename_all_differs = matches!(
473 e.serde_rename_all.as_deref(),
474 Some("kebab-case") | Some("SCREAMING-KEBAB-CASE") | Some("camelCase") | Some("PascalCase")
475 );
476 if rename_all_differs {
477 return true;
478 }
479 e.variants.iter().any(|v| {
481 if let Some(ref rename) = v.serde_rename {
482 let snake = enums::apply_rename_all(&v.name, e.serde_rename_all.as_deref());
483 rename != &snake
484 } else {
485 false
486 }
487 })
488 })
489 .map(|e| csharp_type_name(&e.name))
490 .collect();
491
492 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
494
495 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
497 if !typ.is_opaque {
498 let has_visible_fields = binding_fields(&typ.fields).next().is_some();
501 let has_named_fields = binding_fields(&typ.fields).any(|f| !is_tuple_field(f));
502 if has_visible_fields && !has_named_fields {
503 continue;
504 }
505 if has_visitor_callbacks && bridge_associated_types.contains(typ.name.as_str()) {
507 continue;
508 }
509
510 let type_filename = csharp_type_name(&typ.name);
511 let excluded_types: HashSet<String> =
512 api.excluded_type_paths.keys().map(|n| csharp_type_name(n)).collect();
513 files.push(GeneratedFile {
514 path: base_path.join(format!("{}.cs", type_filename)),
515 content: strip_trailing_whitespace(&types::gen_record_type(
516 typ,
517 &api.types,
518 &namespace,
519 &prefix,
520 &enum_names,
521 &complex_enums,
522 &custom_converter_enums,
523 &lang_rename_all,
524 &bridge_type_aliases,
525 &exception_class_name,
526 &excluded_types,
527 &tagged_union_enums,
528 &all_opaque_type_names,
529 )),
530 generated_header: true,
531 });
532 }
533 }
534
535 for enum_def in &api.enums {
537 if has_visitor_callbacks && bridge_associated_types.contains(enum_def.name.as_str()) {
539 continue;
540 }
541 let enum_filename = csharp_type_name(&enum_def.name);
542 files.push(GeneratedFile {
543 path: base_path.join(format!("{}.cs", enum_filename)),
544 content: strip_trailing_whitespace(&enums::gen_enum(enum_def, &namespace)),
545 generated_header: true,
546 });
547 }
548
549 let needs_byte_array_converter = api
552 .types
553 .iter()
554 .any(|t| !t.is_opaque && t.fields.iter().any(|f| !f.optional && matches!(f.ty, TypeRef::Bytes)));
555 if needs_byte_array_converter {
556 files.push(GeneratedFile {
557 path: base_path.join("ByteArrayToIntArrayConverter.cs"),
558 content: types::gen_byte_array_to_int_array_converter(&namespace),
559 generated_header: true,
560 });
561 }
562
563 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
565
566 files.push(GeneratedFile {
570 path: PathBuf::from("packages/csharp/Directory.Build.props"),
571 content: gen_directory_build_props(),
572 generated_header: true,
573 });
574
575 Ok(files)
576 }
577
578 fn generate_public_api(
583 &self,
584 _api: &ApiSurface,
585 _config: &ResolvedCrateConfig,
586 ) -> anyhow::Result<Vec<GeneratedFile>> {
587 Ok(vec![])
589 }
590
591 fn build_config(&self) -> Option<BuildConfig> {
592 Some(BuildConfig {
593 tool: "dotnet",
594 crate_suffix: "",
595 build_dep: BuildDependency::Ffi,
596 post_build: vec![],
597 })
598 }
599}
600
601pub(super) fn is_tuple_field(field: &FieldDef) -> bool {
603 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
604 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
605}
606
607pub(super) fn strip_trailing_whitespace(content: &str) -> String {
609 let mut result: String = content
610 .lines()
611 .map(|line| line.trim_end())
612 .collect::<Vec<_>>()
613 .join("\n");
614 if !result.ends_with('\n') {
615 result.push('\n');
616 }
617 result
618}
619
620pub(super) fn csharp_file_header() -> String {
622 let mut out = hash::header(CommentStyle::DoubleSlash);
623 out.push_str("#nullable enable\n\n");
624 out
625}
626
627fn gen_directory_build_props() -> String {
630 "<!-- auto-generated by alef (generate_bindings) -->\n\
631<Project>\n \
632<PropertyGroup>\n \
633<Nullable>enable</Nullable>\n \
634<LangVersion>latest</LangVersion>\n \
635<TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n \
636</PropertyGroup>\n\
637</Project>\n"
638 .to_string()
639}
640
641fn delete_superseded_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
646 let superseded = ["IVisitor.cs", "VisitorCallbacks.cs"];
647 for filename in superseded {
648 let path = base_path.join(filename);
649 if path.exists() {
650 std::fs::remove_file(&path)
651 .map_err(|e| anyhow::anyhow!("Failed to delete superseded visitor file {}: {}", path.display(), e))?;
652 }
653 }
654 Ok(())
655}
656
657fn delete_stale_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
661 let stale_files = vec!["IVisitor.cs", "VisitorCallbacks.cs", "NodeContext.cs", "VisitResult.cs"];
662
663 for filename in stale_files {
664 let path = base_path.join(filename);
665 if path.exists() {
666 std::fs::remove_file(&path)
667 .map_err(|e| anyhow::anyhow!("Failed to delete stale visitor file {}: {}", path.display(), e))?;
668 }
669 }
670
671 Ok(())
672}
673
674use alef_core::ir::PrimitiveType;
679
680pub(super) fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
687 match ty {
688 TypeRef::Unit => "void",
689 TypeRef::Primitive(PrimitiveType::Bool) => "int",
691 TypeRef::Primitive(PrimitiveType::U8) => "byte",
693 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
694 TypeRef::Primitive(PrimitiveType::U32) => "uint",
695 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
696 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
697 TypeRef::Primitive(PrimitiveType::I16) => "short",
698 TypeRef::Primitive(PrimitiveType::I32) => "int",
699 TypeRef::Primitive(PrimitiveType::I64) => "long",
700 TypeRef::Primitive(PrimitiveType::F32) => "float",
701 TypeRef::Primitive(PrimitiveType::F64) => "double",
702 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
703 TypeRef::Primitive(PrimitiveType::Isize) => "long",
704 TypeRef::Duration => "ulong",
706 TypeRef::String
708 | TypeRef::Char
709 | TypeRef::Bytes
710 | TypeRef::Optional(_)
711 | TypeRef::Vec(_)
712 | TypeRef::Map(_, _)
713 | TypeRef::Named(_)
714 | TypeRef::Path
715 | TypeRef::Json => "IntPtr",
716 }
717}
718
719pub(super) fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
726 match ty {
727 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
728 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
730 TypeRef::Unit => "void",
731 TypeRef::Primitive(PrimitiveType::Bool) => "int",
732 TypeRef::Primitive(PrimitiveType::U8) => "byte",
733 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
734 TypeRef::Primitive(PrimitiveType::U32) => "uint",
735 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
736 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
737 TypeRef::Primitive(PrimitiveType::I16) => "short",
738 TypeRef::Primitive(PrimitiveType::I32) => "int",
739 TypeRef::Primitive(PrimitiveType::I64) => "long",
740 TypeRef::Primitive(PrimitiveType::F32) => "float",
741 TypeRef::Primitive(PrimitiveType::F64) => "double",
742 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
743 TypeRef::Primitive(PrimitiveType::Isize) => "long",
744 TypeRef::Duration => "ulong",
745 }
746}
747
748pub(super) fn is_bridge_param(
751 param: &alef_core::ir::ParamDef,
752 bridge_param_names: &HashSet<String>,
753 bridge_type_aliases: &HashSet<String>,
754) -> bool {
755 bridge_param_names.contains(¶m.name)
756 || matches!(¶m.ty, alef_core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
757}
758
759pub(super) fn returns_string(ty: &TypeRef) -> bool {
761 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
762}
763
764pub(super) fn returns_bool_via_int(ty: &TypeRef) -> bool {
766 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
767}
768
769pub(super) fn returns_json_object(ty: &TypeRef) -> bool {
771 matches!(
772 ty,
773 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
774 )
775}
776
777pub(super) fn returns_ptr(ty: &TypeRef) -> bool {
780 matches!(
781 ty,
782 TypeRef::String
783 | TypeRef::Char
784 | TypeRef::Path
785 | TypeRef::Json
786 | TypeRef::Named(_)
787 | TypeRef::Vec(_)
788 | TypeRef::Map(_, _)
789 | TypeRef::Bytes
790 | TypeRef::Optional(_)
791 )
792}
793
794pub(super) fn native_call_arg(
800 ty: &TypeRef,
801 param_name: &str,
802 optional: bool,
803 true_opaque_types: &HashSet<String>,
804) -> String {
805 match ty {
806 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
807 let bang = if optional { "!" } else { "" };
809 format!("{param_name}{bang}.Handle")
810 }
811 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
812 format!("{param_name}Handle")
813 }
814 TypeRef::Bytes => {
815 format!("{param_name}Handle.AddrOfPinnedObject()")
816 }
817 TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool) => {
818 if optional {
826 format!("({param_name} ?? false)")
827 } else {
828 param_name.to_string()
829 }
830 }
831 ty => {
832 if optional {
833 if let TypeRef::Primitive(prim) = ty {
841 use alef_core::ir::PrimitiveType;
842 let sentinel = match prim {
843 PrimitiveType::U8 => "byte.MaxValue",
844 PrimitiveType::U16 => "ushort.MaxValue",
845 PrimitiveType::U32 => "uint.MaxValue",
846 PrimitiveType::U64 | PrimitiveType::Usize => "ulong.MaxValue",
847 PrimitiveType::I8 => "sbyte.MaxValue",
848 PrimitiveType::I16 => "short.MaxValue",
849 PrimitiveType::I32 => "int.MaxValue",
850 PrimitiveType::I64 | PrimitiveType::Isize => "long.MaxValue",
851 PrimitiveType::F32 => "float.NaN",
852 PrimitiveType::F64 => "double.NaN",
853 PrimitiveType::Bool => unreachable!("handled above"),
854 };
855 format!("{param_name} ?? {sentinel}")
856 } else if matches!(ty, TypeRef::Duration) {
857 format!("{param_name}.GetValueOrDefault()")
858 } else {
859 format!("{param_name}!")
860 }
861 } else {
862 param_name.to_string()
863 }
864 }
865 }
866}
867
868fn type_has_required_fields(type_def: &TypeDef) -> bool {
870 type_def
871 .fields
872 .iter()
873 .any(|f| !f.optional && f.default.is_none() && f.typed_default.is_none())
874}
875
876pub(super) fn emit_named_param_setup(
881 out: &mut String,
882 params: &[alef_core::ir::ParamDef],
883 indent: &str,
884 true_opaque_types: &HashSet<String>,
885 exception_name: &str,
886 types: &[TypeDef],
887) {
888 for param in params {
889 let param_name = param.name.to_lower_camel_case();
890 let json_var = format!("{param_name}Json");
891 let handle_var = format!("{param_name}Handle");
892
893 match ¶m.ty {
894 TypeRef::Named(type_name) => {
895 if true_opaque_types.contains(type_name) {
898 continue;
899 }
900 let from_json_method = format!("{}FromJson", csharp_type_name(type_name));
901
902 let is_config_param = param.name == "config";
906 let type_pascal = csharp_type_name(type_name);
907
908 let type_def = types.iter().find(|t| &t.name == type_name);
910 let has_required = type_def.is_some_and(type_has_required_fields);
911
912 let param_to_serialize = if is_config_param {
913 if has_required {
914 format!(
916 "({} ?? throw new ArgumentNullException(nameof({param_name}), \"{} has required members; null is not accepted.\"))",
917 param_name, type_pascal
918 )
919 } else {
920 format!("({} ?? new {}())", param_name, type_pascal)
922 }
923 } else {
924 param_name.to_string()
925 };
926
927 if param.optional && !is_config_param {
928 out.push_str(&crate::template_env::render(
932 "named_param_handle_from_json_optional.jinja",
933 minijinja::context! {
934 indent,
935 handle_var => &handle_var,
936 from_json_method => &from_json_method,
937 json_var => &json_var,
938 param_name => ¶m_name,
939 exception_name => exception_name,
940 },
941 ));
942 } else {
943 out.push_str(&crate::template_env::render(
944 "named_param_json_serialize.jinja",
945 minijinja::context! { indent, json_var => &json_var, param_name => ¶m_to_serialize },
946 ));
947 out.push_str(&crate::template_env::render(
948 "named_param_handle_from_json.jinja",
949 minijinja::context! {
950 indent,
951 handle_var => &handle_var,
952 from_json_method => &from_json_method,
953 json_var => &json_var,
954 exception_name => exception_name,
955 },
956 ));
957 }
958 }
959 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
960 out.push_str(&crate::template_env::render(
962 "named_param_json_serialize.jinja",
963 minijinja::context! { indent, json_var => &json_var, param_name => ¶m_name },
964 ));
965 out.push_str(&crate::template_env::render(
966 "named_param_handle_string.jinja",
967 minijinja::context! { indent, handle_var => &handle_var, json_var => &json_var },
968 ));
969 }
970 TypeRef::Bytes => {
971 out.push_str(&crate::template_env::render(
973 "named_param_handle_pin.jinja",
974 minijinja::context! { indent, handle_var => &handle_var, param_name => ¶m_name },
975 ));
976 }
977 _ => {}
978 }
979 }
980}
981
982pub(super) fn emit_named_param_teardown(
987 out: &mut String,
988 params: &[alef_core::ir::ParamDef],
989 true_opaque_types: &HashSet<String>,
990) {
991 for param in params {
992 let param_name = param.name.to_lower_camel_case();
993 let handle_var = format!("{param_name}Handle");
994 match ¶m.ty {
995 TypeRef::Named(type_name) => {
996 if true_opaque_types.contains(type_name) {
997 continue;
999 }
1000 let free_method = format!("{}Free", csharp_type_name(type_name));
1001 out.push_str(&crate::template_env::render(
1002 "named_param_teardown_free.jinja",
1003 minijinja::context! { indent => " ", free_method => &free_method, handle_var => &handle_var },
1004 ));
1005 }
1006 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
1007 out.push_str(&crate::template_env::render(
1008 "named_param_teardown_hglobal.jinja",
1009 minijinja::context! { indent => " ", handle_var => &handle_var },
1010 ));
1011 }
1012 TypeRef::Bytes => {
1013 out.push_str(&crate::template_env::render(
1014 "named_param_teardown_gchandle.jinja",
1015 minijinja::context! { indent => " ", handle_var => &handle_var },
1016 ));
1017 }
1018 _ => {}
1019 }
1020 }
1021}
1022
1023pub(super) fn emit_named_param_teardown_indented(
1025 out: &mut String,
1026 params: &[alef_core::ir::ParamDef],
1027 indent: &str,
1028 true_opaque_types: &HashSet<String>,
1029) {
1030 for param in params {
1031 let param_name = param.name.to_lower_camel_case();
1032 let handle_var = format!("{param_name}Handle");
1033 match ¶m.ty {
1034 TypeRef::Named(type_name) => {
1035 if true_opaque_types.contains(type_name) {
1036 continue;
1038 }
1039 let free_method = format!("{}Free", csharp_type_name(type_name));
1040 out.push_str(&crate::template_env::render(
1041 "named_param_teardown_free.jinja",
1042 minijinja::context! { indent, free_method => &free_method, handle_var => &handle_var },
1043 ));
1044 }
1045 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
1046 out.push_str(&crate::template_env::render(
1047 "named_param_teardown_hglobal.jinja",
1048 minijinja::context! { indent, handle_var => &handle_var },
1049 ));
1050 }
1051 TypeRef::Bytes => {
1052 out.push_str(&crate::template_env::render(
1053 "named_param_teardown_gchandle.jinja",
1054 minijinja::context! { indent, handle_var => &handle_var },
1055 ));
1056 }
1057 _ => {}
1058 }
1059 }
1060}
1061
1062use heck::ToLowerCamelCase;