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::HashSet;
7use std::path::PathBuf;
8
9pub(super) mod enums;
10pub(super) mod errors;
11pub(super) mod functions;
12pub(super) mod methods;
13pub(super) mod types;
14
15pub struct CsharpBackend;
16
17impl CsharpBackend {
18 }
20
21impl Backend for CsharpBackend {
22 fn name(&self) -> &str {
23 "csharp"
24 }
25
26 fn language(&self) -> Language {
27 Language::Csharp
28 }
29
30 fn capabilities(&self) -> Capabilities {
31 Capabilities {
32 supports_async: true,
33 supports_classes: true,
34 supports_enums: true,
35 supports_option: true,
36 supports_result: true,
37 ..Capabilities::default()
38 }
39 }
40
41 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
42 let namespace = config.csharp_namespace();
43 let prefix = config.ffi_prefix();
44 let lib_name = config.ffi_lib_name();
45
46 let bridge_param_names: HashSet<String> = config
49 .trait_bridges
50 .iter()
51 .filter_map(|b| b.param_name.clone())
52 .collect();
53 let bridge_type_aliases: HashSet<String> = config
54 .trait_bridges
55 .iter()
56 .filter_map(|b| b.type_alias.clone())
57 .collect();
58 let has_visitor_callbacks = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false);
60
61 let streaming_methods: HashSet<String> = config
64 .adapters
65 .iter()
66 .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
67 .map(|a| a.name.clone())
68 .collect();
69
70 let exclude_functions: HashSet<String> = config
72 .csharp
73 .as_ref()
74 .map(|c| c.exclude_functions.iter().cloned().collect())
75 .unwrap_or_default();
76
77 let output_dir = resolve_output_dir(config.output_paths.get("csharp"), &config.name, "packages/csharp/");
78
79 let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
80
81 let mut files = Vec::new();
82
83 let exception_class_name = format!("{}Exception", api.crate_name.to_pascal_case());
85
86 files.push(GeneratedFile {
88 path: base_path.join("NativeMethods.cs"),
89 content: strip_trailing_whitespace(&functions::gen_native_methods(
90 api,
91 &namespace,
92 &lib_name,
93 &prefix,
94 &bridge_param_names,
95 &bridge_type_aliases,
96 has_visitor_callbacks,
97 &config.trait_bridges,
98 &streaming_methods,
99 &exclude_functions,
100 )),
101 generated_header: true,
102 });
103
104 if !api.errors.is_empty() {
106 for error in &api.errors {
107 let error_files =
108 alef_codegen::error_gen::gen_csharp_error_types(error, &namespace, Some(&exception_class_name));
109 for (class_name, content) in error_files {
110 files.push(GeneratedFile {
111 path: base_path.join(format!("{}.cs", class_name)),
112 content: strip_trailing_whitespace(&content),
113 generated_header: false, });
115 }
116 }
117 }
118
119 if api.errors.is_empty()
121 || !api
122 .errors
123 .iter()
124 .any(|e| format!("{}Exception", e.name) == exception_class_name)
125 {
126 files.push(GeneratedFile {
127 path: base_path.join(format!("{}.cs", exception_class_name)),
128 content: strip_trailing_whitespace(&errors::gen_exception_class(&namespace, &exception_class_name)),
129 generated_header: true,
130 });
131 }
132
133 let base_class_name = api.crate_name.to_pascal_case();
135 let wrapper_class_name = if namespace == base_class_name {
136 format!("{}Lib", base_class_name)
137 } else {
138 base_class_name
139 };
140 files.push(GeneratedFile {
141 path: base_path.join(format!("{}.cs", wrapper_class_name)),
142 content: strip_trailing_whitespace(&methods::gen_wrapper_class(
143 api,
144 &namespace,
145 &wrapper_class_name,
146 &exception_class_name,
147 &prefix,
148 &bridge_param_names,
149 &bridge_type_aliases,
150 has_visitor_callbacks,
151 &streaming_methods,
152 &exclude_functions,
153 )),
154 generated_header: true,
155 });
156
157 if has_visitor_callbacks {
159 for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace) {
160 files.push(GeneratedFile {
161 path: base_path.join(filename),
162 content: strip_trailing_whitespace(&content),
163 generated_header: true,
164 });
165 }
166 delete_superseded_visitor_files(&base_path)?;
170 } else {
171 delete_stale_visitor_files(&base_path)?;
174 }
175
176 if !config.trait_bridges.is_empty() {
178 let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
179 let bridges: Vec<_> = config
180 .trait_bridges
181 .iter()
182 .filter_map(|cfg| {
183 let trait_name = cfg.trait_name.clone();
184 trait_defs
185 .iter()
186 .find(|t| t.name == trait_name)
187 .map(|trait_def| (trait_name, cfg, *trait_def))
188 })
189 .collect();
190
191 if !bridges.is_empty() {
192 let (filename, content) = crate::trait_bridge::gen_trait_bridges_file(&namespace, &prefix, &bridges);
193 files.push(GeneratedFile {
194 path: base_path.join(filename),
195 content: strip_trailing_whitespace(&content),
196 generated_header: true,
197 });
198 }
199 }
200
201 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
203
204 let all_opaque_type_names: HashSet<String> = api
207 .types
208 .iter()
209 .filter(|t| t.is_opaque)
210 .map(|t| t.name.to_pascal_case())
211 .collect();
212
213 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
215 if typ.is_opaque {
216 let type_filename = typ.name.to_pascal_case();
217 files.push(GeneratedFile {
218 path: base_path.join(format!("{}.cs", type_filename)),
219 content: strip_trailing_whitespace(&types::gen_opaque_handle(
220 typ,
221 &namespace,
222 &exception_class_name,
223 &enum_names,
224 &streaming_methods,
225 &all_opaque_type_names,
226 )),
227 generated_header: true,
228 });
229 }
230 }
231
232 let complex_enums: HashSet<String> = api
239 .enums
240 .iter()
241 .filter(|e| e.serde_untagged && e.variants.iter().any(|v| !v.fields.is_empty()))
242 .map(|e| e.name.to_pascal_case())
243 .collect();
244
245 let custom_converter_enums: HashSet<String> = api
251 .enums
252 .iter()
253 .filter(|e| {
254 let is_tagged_union = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
256 if is_tagged_union {
257 return false;
258 }
259 e.variants.iter().any(|v| {
261 if let Some(ref rename) = v.serde_rename {
262 let snake = enums::apply_rename_all(&v.name, e.serde_rename_all.as_deref());
263 rename != &snake
264 } else {
265 false
266 }
267 })
268 })
269 .map(|e| e.name.to_pascal_case())
270 .collect();
271
272 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
274
275 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
277 if !typ.is_opaque {
278 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
281 if !typ.fields.is_empty() && !has_named_fields {
282 continue;
283 }
284 if has_visitor_callbacks && (typ.name == "NodeContext" || typ.name == "VisitResult") {
286 continue;
287 }
288
289 let type_filename = typ.name.to_pascal_case();
290 files.push(GeneratedFile {
291 path: base_path.join(format!("{}.cs", type_filename)),
292 content: strip_trailing_whitespace(&types::gen_record_type(
293 typ,
294 &namespace,
295 &enum_names,
296 &complex_enums,
297 &custom_converter_enums,
298 &lang_rename_all,
299 &bridge_type_aliases,
300 )),
301 generated_header: true,
302 });
303 }
304 }
305
306 for enum_def in &api.enums {
308 if has_visitor_callbacks && (enum_def.name == "VisitResult" || enum_def.name == "NodeContext") {
310 continue;
311 }
312 let enum_filename = enum_def.name.to_pascal_case();
313 files.push(GeneratedFile {
314 path: base_path.join(format!("{}.cs", enum_filename)),
315 content: strip_trailing_whitespace(&enums::gen_enum(enum_def, &namespace)),
316 generated_header: true,
317 });
318 }
319
320 let needs_byte_array_converter = api
323 .types
324 .iter()
325 .any(|t| !t.is_opaque && t.fields.iter().any(|f| !f.optional && matches!(f.ty, TypeRef::Bytes)));
326 if needs_byte_array_converter {
327 files.push(GeneratedFile {
328 path: base_path.join("ByteArrayToIntArrayConverter.cs"),
329 content: types::gen_byte_array_to_int_array_converter(&namespace),
330 generated_header: true,
331 });
332 }
333
334 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
336
337 files.push(GeneratedFile {
341 path: PathBuf::from("packages/csharp/Directory.Build.props"),
342 content: gen_directory_build_props(),
343 generated_header: true,
344 });
345
346 Ok(files)
347 }
348
349 fn generate_public_api(
354 &self,
355 _api: &ApiSurface,
356 _config: &ResolvedCrateConfig,
357 ) -> anyhow::Result<Vec<GeneratedFile>> {
358 Ok(vec![])
360 }
361
362 fn build_config(&self) -> Option<BuildConfig> {
363 Some(BuildConfig {
364 tool: "dotnet",
365 crate_suffix: "",
366 build_dep: BuildDependency::Ffi,
367 post_build: vec![],
368 })
369 }
370}
371
372pub(super) fn is_tuple_field(field: &FieldDef) -> bool {
374 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
375 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
376}
377
378pub(super) fn strip_trailing_whitespace(content: &str) -> String {
380 let mut result: String = content
381 .lines()
382 .map(|line| line.trim_end())
383 .collect::<Vec<_>>()
384 .join("\n");
385 if !result.ends_with('\n') {
386 result.push('\n');
387 }
388 result
389}
390
391pub(super) fn csharp_file_header() -> String {
393 let mut out = hash::header(CommentStyle::DoubleSlash);
394 out.push_str("#nullable enable\n\n");
395 out
396}
397
398fn gen_directory_build_props() -> String {
401 "<!-- auto-generated by alef (generate_bindings) -->\n\
402<Project>\n \
403<PropertyGroup>\n \
404<Nullable>enable</Nullable>\n \
405<LangVersion>latest</LangVersion>\n \
406<TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n \
407</PropertyGroup>\n\
408</Project>\n"
409 .to_string()
410}
411
412fn delete_superseded_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
417 let superseded = ["IVisitor.cs", "VisitorCallbacks.cs"];
418 for filename in superseded {
419 let path = base_path.join(filename);
420 if path.exists() {
421 std::fs::remove_file(&path)
422 .map_err(|e| anyhow::anyhow!("Failed to delete superseded visitor file {}: {}", path.display(), e))?;
423 }
424 }
425 Ok(())
426}
427
428fn delete_stale_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
432 let stale_files = vec!["IVisitor.cs", "VisitorCallbacks.cs", "NodeContext.cs", "VisitResult.cs"];
433
434 for filename in stale_files {
435 let path = base_path.join(filename);
436 if path.exists() {
437 std::fs::remove_file(&path)
438 .map_err(|e| anyhow::anyhow!("Failed to delete stale visitor file {}: {}", path.display(), e))?;
439 }
440 }
441
442 Ok(())
443}
444
445use alef_core::ir::PrimitiveType;
450
451pub(super) fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
458 match ty {
459 TypeRef::Unit => "void",
460 TypeRef::Primitive(PrimitiveType::Bool) => "int",
462 TypeRef::Primitive(PrimitiveType::U8) => "byte",
464 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
465 TypeRef::Primitive(PrimitiveType::U32) => "uint",
466 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
467 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
468 TypeRef::Primitive(PrimitiveType::I16) => "short",
469 TypeRef::Primitive(PrimitiveType::I32) => "int",
470 TypeRef::Primitive(PrimitiveType::I64) => "long",
471 TypeRef::Primitive(PrimitiveType::F32) => "float",
472 TypeRef::Primitive(PrimitiveType::F64) => "double",
473 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
474 TypeRef::Primitive(PrimitiveType::Isize) => "long",
475 TypeRef::Duration => "ulong",
477 TypeRef::String
479 | TypeRef::Char
480 | TypeRef::Bytes
481 | TypeRef::Optional(_)
482 | TypeRef::Vec(_)
483 | TypeRef::Map(_, _)
484 | TypeRef::Named(_)
485 | TypeRef::Path
486 | TypeRef::Json => "IntPtr",
487 }
488}
489
490pub(super) fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
497 match ty {
498 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
499 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
501 TypeRef::Unit => "void",
502 TypeRef::Primitive(PrimitiveType::Bool) => "int",
503 TypeRef::Primitive(PrimitiveType::U8) => "byte",
504 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
505 TypeRef::Primitive(PrimitiveType::U32) => "uint",
506 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
507 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
508 TypeRef::Primitive(PrimitiveType::I16) => "short",
509 TypeRef::Primitive(PrimitiveType::I32) => "int",
510 TypeRef::Primitive(PrimitiveType::I64) => "long",
511 TypeRef::Primitive(PrimitiveType::F32) => "float",
512 TypeRef::Primitive(PrimitiveType::F64) => "double",
513 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
514 TypeRef::Primitive(PrimitiveType::Isize) => "long",
515 TypeRef::Duration => "ulong",
516 }
517}
518
519pub(super) fn is_bridge_param(
522 param: &alef_core::ir::ParamDef,
523 bridge_param_names: &HashSet<String>,
524 bridge_type_aliases: &HashSet<String>,
525) -> bool {
526 bridge_param_names.contains(¶m.name)
527 || matches!(¶m.ty, alef_core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
528}
529
530pub(super) fn returns_string(ty: &TypeRef) -> bool {
532 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
533}
534
535pub(super) fn returns_bool_via_int(ty: &TypeRef) -> bool {
537 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
538}
539
540pub(super) fn returns_json_object(ty: &TypeRef) -> bool {
542 matches!(
543 ty,
544 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
545 )
546}
547
548pub(super) fn returns_ptr(ty: &TypeRef) -> bool {
551 matches!(
552 ty,
553 TypeRef::String
554 | TypeRef::Char
555 | TypeRef::Path
556 | TypeRef::Json
557 | TypeRef::Named(_)
558 | TypeRef::Vec(_)
559 | TypeRef::Map(_, _)
560 | TypeRef::Bytes
561 | TypeRef::Optional(_)
562 )
563}
564
565pub(super) fn native_call_arg(
571 ty: &TypeRef,
572 param_name: &str,
573 optional: bool,
574 true_opaque_types: &HashSet<String>,
575) -> String {
576 match ty {
577 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
578 let bang = if optional { "!" } else { "" };
580 format!("{param_name}{bang}.Handle")
581 }
582 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
583 format!("{param_name}Handle")
584 }
585 TypeRef::Bytes => {
586 format!("{param_name}Handle.AddrOfPinnedObject()")
587 }
588 TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool) => {
589 if optional {
591 format!("({param_name}?.Value ? 1 : 0)")
592 } else {
593 format!("({param_name} ? 1 : 0)")
594 }
595 }
596 ty => {
597 if optional {
598 let needs_value_unwrap = matches!(ty, TypeRef::Primitive(_) | TypeRef::Duration);
602 if needs_value_unwrap {
603 format!("{param_name}.GetValueOrDefault()")
604 } else {
605 format!("{param_name}!")
606 }
607 } else {
608 param_name.to_string()
609 }
610 }
611 }
612}
613
614pub(super) fn emit_named_param_setup(
619 out: &mut String,
620 params: &[alef_core::ir::ParamDef],
621 indent: &str,
622 true_opaque_types: &HashSet<String>,
623) {
624 for param in params {
625 let param_name = param.name.to_lower_camel_case();
626 let json_var = format!("{param_name}Json");
627 let handle_var = format!("{param_name}Handle");
628
629 match ¶m.ty {
630 TypeRef::Named(type_name) => {
631 if true_opaque_types.contains(type_name) {
634 continue;
635 }
636 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
637
638 let is_config_param = param.name == "config";
640 let param_to_serialize = if is_config_param {
641 let type_pascal = type_name.to_pascal_case();
642 format!("({} ?? new {}())", param_name, type_pascal)
643 } else {
644 param_name.to_string()
645 };
646
647 if param.optional && !is_config_param {
648 out.push_str(&crate::template_env::render(
649 "named_param_json_optional.jinja",
650 minijinja::context! { indent, json_var => &json_var, param_name => ¶m_name },
651 ));
652 } else {
653 out.push_str(&crate::template_env::render(
654 "named_param_json_serialize.jinja",
655 minijinja::context! { indent, json_var => &json_var, param_name => ¶m_to_serialize },
656 ));
657 }
658 out.push_str(&crate::template_env::render(
659 "named_param_handle_from_json.jinja",
660 minijinja::context! { indent, handle_var => &handle_var, from_json_method => &from_json_method, json_var => &json_var },
661 ));
662 }
663 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
664 out.push_str(&crate::template_env::render(
666 "named_param_json_serialize.jinja",
667 minijinja::context! { indent, json_var => &json_var, param_name => ¶m_name },
668 ));
669 out.push_str(&crate::template_env::render(
670 "named_param_handle_string.jinja",
671 minijinja::context! { indent, handle_var => &handle_var, json_var => &json_var },
672 ));
673 }
674 TypeRef::Bytes => {
675 out.push_str(&crate::template_env::render(
677 "named_param_handle_pin.jinja",
678 minijinja::context! { indent, handle_var => &handle_var, param_name => ¶m_name },
679 ));
680 }
681 _ => {}
682 }
683 }
684}
685
686pub(super) fn emit_named_param_teardown(
691 out: &mut String,
692 params: &[alef_core::ir::ParamDef],
693 true_opaque_types: &HashSet<String>,
694) {
695 for param in params {
696 let param_name = param.name.to_lower_camel_case();
697 let handle_var = format!("{param_name}Handle");
698 match ¶m.ty {
699 TypeRef::Named(type_name) => {
700 if true_opaque_types.contains(type_name) {
701 continue;
703 }
704 let free_method = format!("{}Free", type_name.to_pascal_case());
705 out.push_str(&crate::template_env::render(
706 "named_param_teardown_free.jinja",
707 minijinja::context! { indent => " ", free_method => &free_method, handle_var => &handle_var },
708 ));
709 }
710 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
711 out.push_str(&crate::template_env::render(
712 "named_param_teardown_hglobal.jinja",
713 minijinja::context! { indent => " ", handle_var => &handle_var },
714 ));
715 }
716 TypeRef::Bytes => {
717 out.push_str(&crate::template_env::render(
718 "named_param_teardown_gchandle.jinja",
719 minijinja::context! { indent => " ", handle_var => &handle_var },
720 ));
721 }
722 _ => {}
723 }
724 }
725}
726
727pub(super) fn emit_named_param_teardown_indented(
729 out: &mut String,
730 params: &[alef_core::ir::ParamDef],
731 indent: &str,
732 true_opaque_types: &HashSet<String>,
733) {
734 for param in params {
735 let param_name = param.name.to_lower_camel_case();
736 let handle_var = format!("{param_name}Handle");
737 match ¶m.ty {
738 TypeRef::Named(type_name) => {
739 if true_opaque_types.contains(type_name) {
740 continue;
742 }
743 let free_method = format!("{}Free", type_name.to_pascal_case());
744 out.push_str(&crate::template_env::render(
745 "named_param_teardown_free.jinja",
746 minijinja::context! { indent, free_method => &free_method, handle_var => &handle_var },
747 ));
748 }
749 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
750 out.push_str(&crate::template_env::render(
751 "named_param_teardown_hglobal.jinja",
752 minijinja::context! { indent, handle_var => &handle_var },
753 ));
754 }
755 TypeRef::Bytes => {
756 out.push_str(&crate::template_env::render(
757 "named_param_teardown_gchandle.jinja",
758 minijinja::context! { indent, handle_var => &handle_var },
759 ));
760 }
761 _ => {}
762 }
763 }
764}
765
766use heck::ToLowerCamelCase;