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
237 .enums
238 .iter()
239 .filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
240 .map(|e| e.name.to_pascal_case())
241 .collect();
242
243 let custom_converter_enums: HashSet<String> = api
249 .enums
250 .iter()
251 .filter(|e| {
252 let is_tagged_union = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
254 if is_tagged_union {
255 return false;
256 }
257 e.variants.iter().any(|v| {
259 if let Some(ref rename) = v.serde_rename {
260 let snake = enums::apply_rename_all(&v.name, e.serde_rename_all.as_deref());
261 rename != &snake
262 } else {
263 false
264 }
265 })
266 })
267 .map(|e| e.name.to_pascal_case())
268 .collect();
269
270 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
272
273 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
275 if !typ.is_opaque {
276 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
279 if !typ.fields.is_empty() && !has_named_fields {
280 continue;
281 }
282 if has_visitor_callbacks && (typ.name == "NodeContext" || typ.name == "VisitResult") {
284 continue;
285 }
286
287 let type_filename = typ.name.to_pascal_case();
288 files.push(GeneratedFile {
289 path: base_path.join(format!("{}.cs", type_filename)),
290 content: strip_trailing_whitespace(&types::gen_record_type(
291 typ,
292 &namespace,
293 &enum_names,
294 &complex_enums,
295 &custom_converter_enums,
296 &lang_rename_all,
297 &bridge_type_aliases,
298 )),
299 generated_header: true,
300 });
301 }
302 }
303
304 for enum_def in &api.enums {
306 if has_visitor_callbacks && (enum_def.name == "VisitResult" || enum_def.name == "NodeContext") {
308 continue;
309 }
310 let enum_filename = enum_def.name.to_pascal_case();
311 files.push(GeneratedFile {
312 path: base_path.join(format!("{}.cs", enum_filename)),
313 content: strip_trailing_whitespace(&enums::gen_enum(enum_def, &namespace)),
314 generated_header: true,
315 });
316 }
317
318 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
320
321 files.push(GeneratedFile {
325 path: PathBuf::from("packages/csharp/Directory.Build.props"),
326 content: gen_directory_build_props(),
327 generated_header: true,
328 });
329
330 Ok(files)
331 }
332
333 fn generate_public_api(
338 &self,
339 _api: &ApiSurface,
340 _config: &ResolvedCrateConfig,
341 ) -> anyhow::Result<Vec<GeneratedFile>> {
342 Ok(vec![])
344 }
345
346 fn build_config(&self) -> Option<BuildConfig> {
347 Some(BuildConfig {
348 tool: "dotnet",
349 crate_suffix: "",
350 build_dep: BuildDependency::Ffi,
351 post_build: vec![],
352 })
353 }
354}
355
356pub(super) fn is_tuple_field(field: &FieldDef) -> bool {
358 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
359 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
360}
361
362pub(super) fn strip_trailing_whitespace(content: &str) -> String {
364 let mut result: String = content
365 .lines()
366 .map(|line| line.trim_end())
367 .collect::<Vec<_>>()
368 .join("\n");
369 if !result.ends_with('\n') {
370 result.push('\n');
371 }
372 result
373}
374
375pub(super) fn csharp_file_header() -> String {
377 let mut out = hash::header(CommentStyle::DoubleSlash);
378 out.push_str("#nullable enable\n\n");
379 out
380}
381
382fn gen_directory_build_props() -> String {
385 "<!-- auto-generated by alef (generate_bindings) -->\n\
386<Project>\n \
387<PropertyGroup>\n \
388<Nullable>enable</Nullable>\n \
389<LangVersion>latest</LangVersion>\n \
390<TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n \
391</PropertyGroup>\n\
392</Project>\n"
393 .to_string()
394}
395
396fn delete_superseded_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
401 let superseded = ["IVisitor.cs", "VisitorCallbacks.cs"];
402 for filename in superseded {
403 let path = base_path.join(filename);
404 if path.exists() {
405 std::fs::remove_file(&path)
406 .map_err(|e| anyhow::anyhow!("Failed to delete superseded visitor file {}: {}", path.display(), e))?;
407 }
408 }
409 Ok(())
410}
411
412fn delete_stale_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
416 let stale_files = vec!["IVisitor.cs", "VisitorCallbacks.cs", "NodeContext.cs", "VisitResult.cs"];
417
418 for filename in stale_files {
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 stale visitor file {}: {}", path.display(), e))?;
423 }
424 }
425
426 Ok(())
427}
428
429use alef_core::ir::PrimitiveType;
434
435pub(super) fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
442 match ty {
443 TypeRef::Unit => "void",
444 TypeRef::Primitive(PrimitiveType::Bool) => "int",
446 TypeRef::Primitive(PrimitiveType::U8) => "byte",
448 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
449 TypeRef::Primitive(PrimitiveType::U32) => "uint",
450 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
451 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
452 TypeRef::Primitive(PrimitiveType::I16) => "short",
453 TypeRef::Primitive(PrimitiveType::I32) => "int",
454 TypeRef::Primitive(PrimitiveType::I64) => "long",
455 TypeRef::Primitive(PrimitiveType::F32) => "float",
456 TypeRef::Primitive(PrimitiveType::F64) => "double",
457 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
458 TypeRef::Primitive(PrimitiveType::Isize) => "long",
459 TypeRef::Duration => "ulong",
461 TypeRef::String
463 | TypeRef::Char
464 | TypeRef::Bytes
465 | TypeRef::Optional(_)
466 | TypeRef::Vec(_)
467 | TypeRef::Map(_, _)
468 | TypeRef::Named(_)
469 | TypeRef::Path
470 | TypeRef::Json => "IntPtr",
471 }
472}
473
474pub(super) fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
481 match ty {
482 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
483 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
485 TypeRef::Unit => "void",
486 TypeRef::Primitive(PrimitiveType::Bool) => "int",
487 TypeRef::Primitive(PrimitiveType::U8) => "byte",
488 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
489 TypeRef::Primitive(PrimitiveType::U32) => "uint",
490 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
491 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
492 TypeRef::Primitive(PrimitiveType::I16) => "short",
493 TypeRef::Primitive(PrimitiveType::I32) => "int",
494 TypeRef::Primitive(PrimitiveType::I64) => "long",
495 TypeRef::Primitive(PrimitiveType::F32) => "float",
496 TypeRef::Primitive(PrimitiveType::F64) => "double",
497 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
498 TypeRef::Primitive(PrimitiveType::Isize) => "long",
499 TypeRef::Duration => "ulong",
500 }
501}
502
503pub(super) fn is_bridge_param(
506 param: &alef_core::ir::ParamDef,
507 bridge_param_names: &HashSet<String>,
508 bridge_type_aliases: &HashSet<String>,
509) -> bool {
510 bridge_param_names.contains(¶m.name)
511 || matches!(¶m.ty, alef_core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
512}
513
514pub(super) fn returns_string(ty: &TypeRef) -> bool {
516 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
517}
518
519pub(super) fn returns_bool_via_int(ty: &TypeRef) -> bool {
521 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
522}
523
524pub(super) fn returns_json_object(ty: &TypeRef) -> bool {
526 matches!(
527 ty,
528 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
529 )
530}
531
532pub(super) fn returns_ptr(ty: &TypeRef) -> bool {
535 matches!(
536 ty,
537 TypeRef::String
538 | TypeRef::Char
539 | TypeRef::Path
540 | TypeRef::Json
541 | TypeRef::Named(_)
542 | TypeRef::Vec(_)
543 | TypeRef::Map(_, _)
544 | TypeRef::Bytes
545 | TypeRef::Optional(_)
546 )
547}
548
549pub(super) fn native_call_arg(
555 ty: &TypeRef,
556 param_name: &str,
557 optional: bool,
558 true_opaque_types: &HashSet<String>,
559) -> String {
560 match ty {
561 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
562 let bang = if optional { "!" } else { "" };
564 format!("{param_name}{bang}.Handle")
565 }
566 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
567 format!("{param_name}Handle")
568 }
569 TypeRef::Bytes => {
570 format!("{param_name}Handle.AddrOfPinnedObject()")
571 }
572 TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool) => {
573 if optional {
575 format!("({param_name}?.Value ? 1 : 0)")
576 } else {
577 format!("({param_name} ? 1 : 0)")
578 }
579 }
580 ty => {
581 if optional {
582 let needs_value_unwrap = matches!(ty, TypeRef::Primitive(_) | TypeRef::Duration);
586 if needs_value_unwrap {
587 format!("{param_name}.GetValueOrDefault()")
588 } else {
589 format!("{param_name}!")
590 }
591 } else {
592 param_name.to_string()
593 }
594 }
595 }
596}
597
598pub(super) fn emit_named_param_setup(
603 out: &mut String,
604 params: &[alef_core::ir::ParamDef],
605 indent: &str,
606 true_opaque_types: &HashSet<String>,
607) {
608 for param in params {
609 let param_name = param.name.to_lower_camel_case();
610 let json_var = format!("{param_name}Json");
611 let handle_var = format!("{param_name}Handle");
612
613 match ¶m.ty {
614 TypeRef::Named(type_name) => {
615 if true_opaque_types.contains(type_name) {
618 continue;
619 }
620 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
621 if param.optional {
622 out.push_str(&format!(
623 "{indent}var {json_var} = {param_name} != null ? JsonSerializer.Serialize({param_name}, JsonOptions) : \"null\";\n"
624 ));
625 } else {
626 out.push_str(&format!(
627 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
628 ));
629 }
630 out.push_str(&format!(
631 "{indent}var {handle_var} = NativeMethods.{from_json_method}({json_var});\n"
632 ));
633 }
634 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
635 out.push_str(&format!(
637 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
638 ));
639 out.push_str(&format!(
640 "{indent}var {handle_var} = Marshal.StringToHGlobalAnsi({json_var});\n"
641 ));
642 }
643 TypeRef::Bytes => {
644 out.push_str(&format!(
646 "{indent}var {handle_var} = GCHandle.Alloc({param_name}, GCHandleType.Pinned);\n"
647 ));
648 }
649 _ => {}
650 }
651 }
652}
653
654pub(super) fn emit_named_param_teardown(
659 out: &mut String,
660 params: &[alef_core::ir::ParamDef],
661 true_opaque_types: &HashSet<String>,
662) {
663 for param in params {
664 let param_name = param.name.to_lower_camel_case();
665 let handle_var = format!("{param_name}Handle");
666 match ¶m.ty {
667 TypeRef::Named(type_name) => {
668 if true_opaque_types.contains(type_name) {
669 continue;
671 }
672 let free_method = format!("{}Free", type_name.to_pascal_case());
673 out.push_str(&format!(" NativeMethods.{free_method}({handle_var});\n"));
674 }
675 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
676 out.push_str(&format!(" Marshal.FreeHGlobal({handle_var});\n"));
677 }
678 TypeRef::Bytes => {
679 out.push_str(&format!(" {handle_var}.Free();\n"));
680 }
681 _ => {}
682 }
683 }
684}
685
686pub(super) fn emit_named_param_teardown_indented(
688 out: &mut String,
689 params: &[alef_core::ir::ParamDef],
690 indent: &str,
691 true_opaque_types: &HashSet<String>,
692) {
693 for param in params {
694 let param_name = param.name.to_lower_camel_case();
695 let handle_var = format!("{param_name}Handle");
696 match ¶m.ty {
697 TypeRef::Named(type_name) => {
698 if true_opaque_types.contains(type_name) {
699 continue;
701 }
702 let free_method = format!("{}Free", type_name.to_pascal_case());
703 out.push_str(&format!("{indent}NativeMethods.{free_method}({handle_var});\n"));
704 }
705 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
706 out.push_str(&format!("{indent}Marshal.FreeHGlobal({handle_var});\n"));
707 }
708 TypeRef::Bytes => {
709 out.push_str(&format!("{indent}{handle_var}.Free();\n"));
710 }
711 _ => {}
712 }
713 }
714}
715
716use heck::ToLowerCamelCase;