1use crate::type_map::csharp_type;
2use alef_codegen::naming::to_csharp_name;
3use alef_core::backend::{Backend, BuildConfig, Capabilities, GeneratedFile};
4use alef_core::config::{AlefConfig, Language, resolve_output_dir};
5use alef_core::ir::{ApiSurface, EnumDef, FieldDef, FunctionDef, MethodDef, PrimitiveType, TypeDef, TypeRef};
6use heck::{ToLowerCamelCase, ToPascalCase};
7use std::collections::HashSet;
8use std::path::PathBuf;
9
10pub struct CsharpBackend;
11
12impl CsharpBackend {
13 }
15
16impl Backend for CsharpBackend {
17 fn name(&self) -> &str {
18 "csharp"
19 }
20
21 fn language(&self) -> Language {
22 Language::Csharp
23 }
24
25 fn capabilities(&self) -> Capabilities {
26 Capabilities {
27 supports_async: true,
28 supports_classes: true,
29 supports_enums: true,
30 supports_option: true,
31 supports_result: true,
32 ..Capabilities::default()
33 }
34 }
35
36 fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
37 let namespace = config.csharp_namespace();
38 let prefix = config.ffi_prefix();
39 let lib_name = config.ffi_lib_name();
40
41 let output_dir = resolve_output_dir(
42 config.output.csharp.as_ref(),
43 &config.crate_config.name,
44 "packages/csharp/",
45 );
46
47 let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
48
49 let mut files = Vec::new();
50
51 files.push(GeneratedFile {
53 path: base_path.join("NativeMethods.cs"),
54 content: strip_trailing_whitespace(&gen_native_methods(api, &namespace, &lib_name, &prefix)),
55 generated_header: true,
56 });
57
58 if !api.errors.is_empty() {
60 for error in &api.errors {
61 let error_files = alef_codegen::error_gen::gen_csharp_error_types(error, &namespace);
62 for (class_name, content) in error_files {
63 files.push(GeneratedFile {
64 path: base_path.join(format!("{}.cs", class_name)),
65 content: strip_trailing_whitespace(&content),
66 generated_header: false, });
68 }
69 }
70 }
71
72 let exception_class_name = format!("{}Exception", api.crate_name.to_pascal_case());
74 if api.errors.is_empty()
75 || !api
76 .errors
77 .iter()
78 .any(|e| format!("{}Exception", e.name) == exception_class_name)
79 {
80 files.push(GeneratedFile {
81 path: base_path.join(format!("{}.cs", exception_class_name)),
82 content: strip_trailing_whitespace(&gen_exception_class(&namespace, &exception_class_name)),
83 generated_header: true,
84 });
85 }
86
87 let base_class_name = api.crate_name.to_pascal_case();
89 let wrapper_class_name = if namespace == base_class_name {
90 format!("{}Lib", base_class_name)
91 } else {
92 base_class_name
93 };
94 files.push(GeneratedFile {
95 path: base_path.join(format!("{}.cs", wrapper_class_name)),
96 content: strip_trailing_whitespace(&gen_wrapper_class(
97 api,
98 &namespace,
99 &wrapper_class_name,
100 &exception_class_name,
101 &prefix,
102 )),
103 generated_header: true,
104 });
105
106 for typ in &api.types {
108 if !typ.is_opaque {
109 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
112 if !typ.fields.is_empty() && !has_named_fields {
113 continue;
114 }
115
116 let type_filename = typ.name.to_pascal_case();
117 files.push(GeneratedFile {
118 path: base_path.join(format!("{}.cs", type_filename)),
119 content: strip_trailing_whitespace(&gen_record_type(typ, &namespace)),
120 generated_header: true,
121 });
122 }
123 }
124
125 for enum_def in &api.enums {
127 let enum_filename = enum_def.name.to_pascal_case();
128 files.push(GeneratedFile {
129 path: base_path.join(format!("{}.cs", enum_filename)),
130 content: strip_trailing_whitespace(&gen_enum(enum_def, &namespace)),
131 generated_header: true,
132 });
133 }
134
135 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
137
138 Ok(files)
139 }
140
141 fn generate_public_api(&self, _api: &ApiSurface, _config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
146 Ok(vec![])
148 }
149
150 fn build_config(&self) -> Option<BuildConfig> {
151 Some(BuildConfig {
152 tool: "dotnet",
153 crate_suffix: "",
154 depends_on_ffi: true,
155 post_build: vec![],
156 })
157 }
158}
159
160fn is_tuple_field(field: &FieldDef) -> bool {
162 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
163 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
164}
165
166fn strip_trailing_whitespace(content: &str) -> String {
168 let mut result: String = content
169 .lines()
170 .map(|line| line.trim_end())
171 .collect::<Vec<_>>()
172 .join("\n");
173 if !result.ends_with('\n') {
174 result.push('\n');
175 }
176 result
177}
178
179fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
190 match ty {
191 TypeRef::Unit => "void",
192 TypeRef::Primitive(PrimitiveType::Bool) => "int",
194 TypeRef::Primitive(PrimitiveType::U8) => "byte",
196 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
197 TypeRef::Primitive(PrimitiveType::U32) => "uint",
198 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
199 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
200 TypeRef::Primitive(PrimitiveType::I16) => "short",
201 TypeRef::Primitive(PrimitiveType::I32) => "int",
202 TypeRef::Primitive(PrimitiveType::I64) => "long",
203 TypeRef::Primitive(PrimitiveType::F32) => "float",
204 TypeRef::Primitive(PrimitiveType::F64) => "double",
205 TypeRef::Primitive(PrimitiveType::Usize) => "nuint",
206 TypeRef::Primitive(PrimitiveType::Isize) => "nint",
207 TypeRef::Duration => "ulong",
209 TypeRef::String
211 | TypeRef::Char
212 | TypeRef::Bytes
213 | TypeRef::Optional(_)
214 | TypeRef::Vec(_)
215 | TypeRef::Map(_, _)
216 | TypeRef::Named(_)
217 | TypeRef::Path
218 | TypeRef::Json => "IntPtr",
219 }
220}
221
222fn returns_string(ty: &TypeRef) -> bool {
224 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
225}
226
227fn returns_bool_via_int(ty: &TypeRef) -> bool {
229 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
230}
231
232fn returns_json_object(ty: &TypeRef) -> bool {
234 matches!(
235 ty,
236 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
237 )
238}
239
240fn gen_native_methods(api: &ApiSurface, namespace: &str, lib_name: &str, prefix: &str) -> String {
245 let mut out = String::from(
246 "// This file is auto-generated by alef. DO NOT EDIT.\n\
247 using System.Runtime.InteropServices;\n\n",
248 );
249
250 out.push_str(&format!("namespace {};\n\n", namespace));
251
252 out.push_str("internal static partial class NativeMethods\n{\n");
253 out.push_str(&format!(" private const string LibName = \"{}\";\n\n", lib_name));
254
255 let mut emitted: HashSet<String> = HashSet::new();
258
259 for func in &api.functions {
261 let c_func_name = format!("{}_{}", prefix, func.name.to_lowercase());
262 if emitted.insert(c_func_name.clone()) {
263 out.push_str(&gen_pinvoke_for_func(&c_func_name, func));
264 }
265 }
266
267 for typ in &api.types {
269 for method in &typ.methods {
270 let c_method_name = format!("{}_{}", prefix, method.name.to_lowercase());
271 if emitted.insert(c_method_name.clone()) {
272 out.push_str(&gen_pinvoke_for_method(&c_method_name, method));
273 }
274 }
275 }
276
277 out.push_str(&format!(
279 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_code\")]\n"
280 ));
281 out.push_str(" internal static extern int LastErrorCode();\n\n");
282
283 out.push_str(&format!(
284 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_context\")]\n"
285 ));
286 out.push_str(" internal static extern IntPtr LastErrorContext();\n\n");
287
288 out.push_str(&format!(
289 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_free_string\")]\n"
290 ));
291 out.push_str(" internal static extern void FreeString(IntPtr ptr);\n");
292
293 out.push_str("}\n");
294
295 out
296}
297
298fn gen_pinvoke_for_func(c_name: &str, func: &FunctionDef) -> String {
299 let cs_name = to_csharp_name(&func.name);
300 let mut out =
301 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
302 out.push_str(" internal static extern ");
303
304 out.push_str(pinvoke_return_type(&func.return_type));
306
307 out.push_str(&format!(" {}(", cs_name));
308
309 if func.params.is_empty() {
310 out.push_str(");\n\n");
311 } else {
312 out.push('\n');
313 for (i, param) in func.params.iter().enumerate() {
314 out.push_str(" ");
315 if matches!(param.ty, TypeRef::String | TypeRef::Char) {
316 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
317 }
318 let param_name = param.name.to_lower_camel_case();
319 out.push_str(&format!("{} {}", csharp_type(¶m.ty), param_name));
320
321 if i < func.params.len() - 1 {
322 out.push(',');
323 }
324 out.push('\n');
325 }
326 out.push_str(" );\n\n");
327 }
328
329 out
330}
331
332fn gen_pinvoke_for_method(c_name: &str, method: &MethodDef) -> String {
333 let cs_name = to_csharp_name(&method.name);
334 let mut out =
335 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
336 out.push_str(" internal static extern ");
337
338 out.push_str(pinvoke_return_type(&method.return_type));
340
341 out.push_str(&format!(" {}(", cs_name));
342
343 if method.params.is_empty() {
344 out.push_str(");\n\n");
345 } else {
346 out.push('\n');
347 for (i, param) in method.params.iter().enumerate() {
348 out.push_str(" ");
349 if matches!(param.ty, TypeRef::String | TypeRef::Char) {
350 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
351 }
352 let param_name = param.name.to_lower_camel_case();
353 out.push_str(&format!("{} {}", csharp_type(¶m.ty), param_name));
354
355 if i < method.params.len() - 1 {
356 out.push(',');
357 }
358 out.push('\n');
359 }
360 out.push_str(" );\n\n");
361 }
362
363 out
364}
365
366fn gen_exception_class(namespace: &str, class_name: &str) -> String {
367 let mut out = String::from(
368 "// This file is auto-generated by alef. DO NOT EDIT.\n\
369 using System;\n\n",
370 );
371
372 out.push_str(&format!("namespace {};\n\n", namespace));
373
374 out.push_str(&format!("public class {} : Exception\n", class_name));
375 out.push_str("{\n");
376 out.push_str(" public int Code { get; }\n\n");
377 out.push_str(&format!(
378 " public {}(int code, string message) : base(message)\n",
379 class_name
380 ));
381 out.push_str(" {\n");
382 out.push_str(" Code = code;\n");
383 out.push_str(" }\n");
384 out.push_str("}\n");
385
386 out
387}
388
389fn gen_wrapper_class(
390 api: &ApiSurface,
391 namespace: &str,
392 class_name: &str,
393 exception_name: &str,
394 prefix: &str,
395) -> String {
396 let mut out = String::from(
397 "// This file is auto-generated by alef. DO NOT EDIT.\n\
398 using System;\n\
399 using System.Collections.Generic;\n\
400 using System.Runtime.InteropServices;\n\
401 using System.Text.Json;\n\
402 using System.Text.Json.Serialization;\n\
403 using System.Threading.Tasks;\n\n",
404 );
405
406 out.push_str(&format!("namespace {};\n\n", namespace));
407
408 out.push_str(&format!("public static class {}\n", class_name));
409 out.push_str("{\n");
410
411 for func in &api.functions {
413 out.push_str(&gen_wrapper_function(func, exception_name, prefix));
414 }
415
416 for typ in &api.types {
418 if typ.is_opaque {
420 continue;
421 }
422 for method in &typ.methods {
423 if let alef_core::ir::TypeRef::Named(ref name) = method.return_type {
425 if api.types.iter().any(|t| t.name == *name && t.is_opaque) {
426 continue;
427 }
428 }
429 out.push_str(&gen_wrapper_method(method, exception_name, prefix, &typ.name));
430 }
431 }
432
433 out.push_str(" private static ");
435 out.push_str(&format!("{} GetLastError()\n", exception_name));
436 out.push_str(" {\n");
437 out.push_str(" var code = NativeMethods.LastErrorCode();\n");
438 out.push_str(" var ctxPtr = NativeMethods.LastErrorContext();\n");
439 out.push_str(" var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n");
440 out.push_str(&format!(" return new {}(code, message);\n", exception_name));
441 out.push_str(" }\n");
442
443 out.push_str("}\n");
444
445 out
446}
447
448fn gen_wrapper_function(func: &FunctionDef, _exception_name: &str, _prefix: &str) -> String {
449 let mut out = String::with_capacity(1024);
450
451 out.push_str(" public static ");
452
453 if func.return_type == TypeRef::Unit {
455 out.push_str("void");
456 } else {
457 out.push_str(&csharp_type(&func.return_type));
458 }
459
460 out.push_str(&format!(" {}", to_csharp_name(&func.name)));
461 out.push('(');
462
463 for (i, param) in func.params.iter().enumerate() {
465 let param_name = param.name.to_lower_camel_case();
466 let mapped = csharp_type(¶m.ty);
467 if param.optional && !mapped.ends_with('?') {
468 out.push_str(&format!("{mapped}? {param_name}"));
469 } else {
470 out.push_str(&format!("{mapped} {param_name}"));
471 }
472
473 if i < func.params.len() - 1 {
474 out.push_str(", ");
475 }
476 }
477
478 out.push_str(")\n {\n");
479
480 let cs_native_name = to_csharp_name(&func.name);
482
483 if func.return_type != TypeRef::Unit {
484 out.push_str(" var result = ");
485 } else {
486 out.push_str(" ");
487 }
488
489 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
490
491 if func.params.is_empty() {
492 out.push_str(");\n");
493 } else {
494 out.push('\n');
495 for (i, param) in func.params.iter().enumerate() {
496 let param_name = param.name.to_lower_camel_case();
497 out.push_str(&format!(" {}", param_name));
498 if i < func.params.len() - 1 {
499 out.push(',');
500 }
501 out.push('\n');
502 }
503 out.push_str(" );\n");
504 }
505
506 emit_return_marshalling(&mut out, &func.return_type);
507
508 out.push_str(" }\n\n");
509
510 out
511}
512
513fn gen_wrapper_method(method: &MethodDef, _exception_name: &str, _prefix: &str, type_name: &str) -> String {
514 let mut out = String::with_capacity(1024);
515
516 out.push_str(" public static ");
518
519 if method.return_type == TypeRef::Unit {
521 out.push_str("void");
522 } else {
523 out.push_str(&csharp_type(&method.return_type));
524 }
525
526 let method_cs_name = format!("{}{}", type_name, to_csharp_name(&method.name));
528 out.push_str(&format!(" {method_cs_name}"));
529 out.push('(');
530
531 for (i, param) in method.params.iter().enumerate() {
533 let param_name = param.name.to_lower_camel_case();
534 let mapped = csharp_type(¶m.ty);
535 if param.optional && !mapped.ends_with('?') {
536 out.push_str(&format!("{mapped}? {param_name}"));
537 } else {
538 out.push_str(&format!("{mapped} {param_name}"));
539 }
540
541 if i < method.params.len() - 1 {
542 out.push_str(", ");
543 }
544 }
545
546 out.push_str(")\n {\n");
547
548 let cs_native_name = to_csharp_name(&method.name);
550
551 if method.return_type != TypeRef::Unit {
552 out.push_str(" var result = ");
553 } else {
554 out.push_str(" ");
555 }
556
557 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
558
559 if method.params.is_empty() {
560 out.push_str(");\n");
561 } else {
562 out.push('\n');
563 for (i, param) in method.params.iter().enumerate() {
564 let param_name = param.name.to_lower_camel_case();
565 out.push_str(&format!(" {}", param_name));
566 if i < method.params.len() - 1 {
567 out.push(',');
568 }
569 out.push('\n');
570 }
571 out.push_str(" );\n");
572 }
573
574 emit_return_marshalling(&mut out, &method.return_type);
575
576 out.push_str(" }\n\n");
577
578 out
579}
580
581fn emit_return_marshalling(out: &mut String, return_type: &TypeRef) {
585 if *return_type == TypeRef::Unit {
586 return;
588 }
589
590 if returns_string(return_type) {
591 out.push_str(" var str = Marshal.PtrToStringUTF8(result);\n");
593 out.push_str(" NativeMethods.FreeString(result);\n");
594 out.push_str(" return str ?? string.Empty;\n");
595 } else if returns_bool_via_int(return_type) {
596 out.push_str(" return result != 0;\n");
598 } else if returns_json_object(return_type) {
599 let cs_ty = csharp_type(return_type);
601 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
602 out.push_str(" NativeMethods.FreeString(result);\n");
603 out.push_str(&format!(
604 " return JsonSerializer.Deserialize<{}>(json ?? \"null\")!;\n",
605 cs_ty
606 ));
607 } else {
608 out.push_str(" return result;\n");
610 }
611}
612
613fn gen_record_type(typ: &TypeDef, namespace: &str) -> String {
614 let mut out = String::from(
615 "// This file is auto-generated by alef. DO NOT EDIT.\n\
616 using System;\n\
617 using System.Collections.Generic;\n\
618 using System.Text.Json.Serialization;\n\n",
619 );
620
621 out.push_str(&format!("namespace {};\n\n", namespace));
622
623 if !typ.doc.is_empty() {
625 out.push_str("/// <summary>\n");
626 for line in typ.doc.lines() {
627 out.push_str(&format!("/// {}\n", line));
628 }
629 out.push_str("/// </summary>\n");
630 }
631
632 out.push_str(&format!("public sealed class {}\n", typ.name.to_pascal_case()));
633 out.push_str("{\n");
634
635 for field in &typ.fields {
636 if is_tuple_field(field) {
638 continue;
639 }
640
641 if !field.doc.is_empty() {
643 out.push_str(" /// <summary>\n");
644 for line in field.doc.lines() {
645 out.push_str(&format!(" /// {}\n", line));
646 }
647 out.push_str(" /// </summary>\n");
648 }
649
650 let json_name = field.name.to_lower_camel_case();
652 out.push_str(&format!(" [JsonPropertyName(\"{}\")]\n", json_name));
653
654 let cs_name = to_csharp_name(&field.name);
655
656 if field.optional {
657 let mapped = csharp_type(&field.ty);
659 let field_type = if mapped.ends_with('?') {
660 mapped.to_string()
661 } else {
662 format!("{mapped}?")
663 };
664 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
665 out.push_str(" = null;\n");
666 } else if typ.has_default || field.default.is_some() {
667 let field_type = csharp_type(&field.ty).to_string();
669 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
670 if let Some(default) = &field.default {
671 out.push_str(&format!(" = {};\n", default));
672 } else {
673 let default_val = match &field.ty {
675 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"".to_string(),
676 TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
677 TypeRef::Primitive(p) => match p {
678 PrimitiveType::Bool => "false".to_string(),
679 PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
680 _ => "0".to_string(),
681 },
682 TypeRef::Vec(_) => "[]".to_string(),
683 TypeRef::Map(_, _) => "new Dictionary<>()".to_string(),
684 TypeRef::Duration => "0".to_string(),
685 _ => "null".to_string(),
686 };
687 out.push_str(&format!(" = {};\n", default_val));
688 }
689 } else {
690 let field_type = csharp_type(&field.ty).to_string();
692 out.push_str(&format!(
693 " public required {} {} {{ get; set; }}\n",
694 field_type, cs_name
695 ));
696 }
697
698 out.push('\n');
699 }
700
701 out.push_str("}\n");
702
703 out
704}
705
706fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
707 let mut out = String::from("// This file is auto-generated by alef. DO NOT EDIT.\n\n");
708
709 out.push_str(&format!("namespace {};\n\n", namespace));
710
711 if !enum_def.doc.is_empty() {
713 out.push_str("/// <summary>\n");
714 for line in enum_def.doc.lines() {
715 out.push_str(&format!("/// {}\n", line));
716 }
717 out.push_str("/// </summary>\n");
718 }
719
720 out.push_str(&format!("public enum {}\n", enum_def.name.to_pascal_case()));
721 out.push_str("{\n");
722
723 for variant in &enum_def.variants {
725 if !variant.doc.is_empty() {
726 out.push_str(" /// <summary>\n");
727 for line in variant.doc.lines() {
728 out.push_str(&format!(" /// {}\n", line));
729 }
730 out.push_str(" /// </summary>\n");
731 }
732
733 out.push_str(&format!(" {},\n", variant.name.to_pascal_case()));
734 }
735
736 out.push_str("}\n");
737
738 out
739}