use crate::type_map::csharp_type;
use alef_codegen::naming::to_csharp_name;
use alef_core::backend::{Backend, BuildConfig, Capabilities, GeneratedFile};
use alef_core::config::{AlefConfig, Language, resolve_output_dir};
use alef_core::ir::{ApiSurface, EnumDef, FieldDef, FunctionDef, MethodDef, PrimitiveType, TypeDef, TypeRef};
use heck::{ToLowerCamelCase, ToPascalCase};
use std::collections::HashSet;
use std::path::PathBuf;
pub struct CsharpBackend;
impl CsharpBackend {
}
impl Backend for CsharpBackend {
fn name(&self) -> &str {
"csharp"
}
fn language(&self) -> Language {
Language::Csharp
}
fn capabilities(&self) -> Capabilities {
Capabilities {
supports_async: true,
supports_classes: true,
supports_enums: true,
supports_option: true,
supports_result: true,
..Capabilities::default()
}
}
fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let namespace = config.csharp_namespace();
let prefix = config.ffi_prefix();
let lib_name = config.ffi_lib_name();
let output_dir = resolve_output_dir(
config.output.csharp.as_ref(),
&config.crate_config.name,
"packages/csharp/",
);
let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
let mut files = Vec::new();
files.push(GeneratedFile {
path: base_path.join("NativeMethods.cs"),
content: strip_trailing_whitespace(&gen_native_methods(api, &namespace, &lib_name, &prefix)),
generated_header: true,
});
if !api.errors.is_empty() {
for error in &api.errors {
let error_files = alef_codegen::error_gen::gen_csharp_error_types(error, &namespace);
for (class_name, content) in error_files {
files.push(GeneratedFile {
path: base_path.join(format!("{}.cs", class_name)),
content: strip_trailing_whitespace(&content),
generated_header: false, });
}
}
}
let exception_class_name = format!("{}Exception", api.crate_name.to_pascal_case());
if api.errors.is_empty()
|| !api
.errors
.iter()
.any(|e| format!("{}Exception", e.name) == exception_class_name)
{
files.push(GeneratedFile {
path: base_path.join(format!("{}.cs", exception_class_name)),
content: strip_trailing_whitespace(&gen_exception_class(&namespace, &exception_class_name)),
generated_header: true,
});
}
let base_class_name = api.crate_name.to_pascal_case();
let wrapper_class_name = if namespace == base_class_name {
format!("{}Lib", base_class_name)
} else {
base_class_name
};
files.push(GeneratedFile {
path: base_path.join(format!("{}.cs", wrapper_class_name)),
content: strip_trailing_whitespace(&gen_wrapper_class(
api,
&namespace,
&wrapper_class_name,
&exception_class_name,
&prefix,
)),
generated_header: true,
});
for typ in &api.types {
if !typ.is_opaque {
let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
if !typ.fields.is_empty() && !has_named_fields {
continue;
}
let type_filename = typ.name.to_pascal_case();
files.push(GeneratedFile {
path: base_path.join(format!("{}.cs", type_filename)),
content: strip_trailing_whitespace(&gen_record_type(typ, &namespace)),
generated_header: true,
});
}
}
for enum_def in &api.enums {
let enum_filename = enum_def.name.to_pascal_case();
files.push(GeneratedFile {
path: base_path.join(format!("{}.cs", enum_filename)),
content: strip_trailing_whitespace(&gen_enum(enum_def, &namespace)),
generated_header: true,
});
}
let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
Ok(files)
}
fn generate_public_api(&self, _api: &ApiSurface, _config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
Ok(vec![])
}
fn build_config(&self) -> Option<BuildConfig> {
Some(BuildConfig {
tool: "dotnet",
crate_suffix: "",
depends_on_ffi: true,
post_build: vec![],
})
}
}
fn is_tuple_field(field: &FieldDef) -> bool {
(field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
|| field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
}
fn strip_trailing_whitespace(content: &str) -> String {
let mut result: String = content
.lines()
.map(|line| line.trim_end())
.collect::<Vec<_>>()
.join("\n");
if !result.ends_with('\n') {
result.push('\n');
}
result
}
fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
match ty {
TypeRef::Unit => "void",
TypeRef::Primitive(PrimitiveType::Bool) => "int",
TypeRef::Primitive(PrimitiveType::U8) => "byte",
TypeRef::Primitive(PrimitiveType::U16) => "ushort",
TypeRef::Primitive(PrimitiveType::U32) => "uint",
TypeRef::Primitive(PrimitiveType::U64) => "ulong",
TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
TypeRef::Primitive(PrimitiveType::I16) => "short",
TypeRef::Primitive(PrimitiveType::I32) => "int",
TypeRef::Primitive(PrimitiveType::I64) => "long",
TypeRef::Primitive(PrimitiveType::F32) => "float",
TypeRef::Primitive(PrimitiveType::F64) => "double",
TypeRef::Primitive(PrimitiveType::Usize) => "nuint",
TypeRef::Primitive(PrimitiveType::Isize) => "nint",
TypeRef::Duration => "ulong",
TypeRef::String
| TypeRef::Char
| TypeRef::Bytes
| TypeRef::Optional(_)
| TypeRef::Vec(_)
| TypeRef::Map(_, _)
| TypeRef::Named(_)
| TypeRef::Path
| TypeRef::Json => "IntPtr",
}
}
fn returns_string(ty: &TypeRef) -> bool {
matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
}
fn returns_bool_via_int(ty: &TypeRef) -> bool {
matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
}
fn returns_json_object(ty: &TypeRef) -> bool {
matches!(
ty,
TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
)
}
fn gen_native_methods(api: &ApiSurface, namespace: &str, lib_name: &str, prefix: &str) -> String {
let mut out = String::from(
"// This file is auto-generated by alef. DO NOT EDIT.\n\
using System.Runtime.InteropServices;\n\n",
);
out.push_str(&format!("namespace {};\n\n", namespace));
out.push_str("internal static partial class NativeMethods\n{\n");
out.push_str(&format!(" private const string LibName = \"{}\";\n\n", lib_name));
let mut emitted: HashSet<String> = HashSet::new();
for func in &api.functions {
let c_func_name = format!("{}_{}", prefix, func.name.to_lowercase());
if emitted.insert(c_func_name.clone()) {
out.push_str(&gen_pinvoke_for_func(&c_func_name, func));
}
}
for typ in &api.types {
for method in &typ.methods {
let c_method_name = format!("{}_{}", prefix, method.name.to_lowercase());
if emitted.insert(c_method_name.clone()) {
out.push_str(&gen_pinvoke_for_method(&c_method_name, method));
}
}
}
out.push_str(&format!(
" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_code\")]\n"
));
out.push_str(" internal static extern int LastErrorCode();\n\n");
out.push_str(&format!(
" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_context\")]\n"
));
out.push_str(" internal static extern IntPtr LastErrorContext();\n\n");
out.push_str(&format!(
" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_free_string\")]\n"
));
out.push_str(" internal static extern void FreeString(IntPtr ptr);\n");
out.push_str("}\n");
out
}
fn gen_pinvoke_for_func(c_name: &str, func: &FunctionDef) -> String {
let cs_name = to_csharp_name(&func.name);
let mut out =
format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
out.push_str(" internal static extern ");
out.push_str(pinvoke_return_type(&func.return_type));
out.push_str(&format!(" {}(", cs_name));
if func.params.is_empty() {
out.push_str(");\n\n");
} else {
out.push('\n');
for (i, param) in func.params.iter().enumerate() {
out.push_str(" ");
if matches!(param.ty, TypeRef::String | TypeRef::Char) {
out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
}
let param_name = param.name.to_lower_camel_case();
out.push_str(&format!("{} {}", csharp_type(¶m.ty), param_name));
if i < func.params.len() - 1 {
out.push(',');
}
out.push('\n');
}
out.push_str(" );\n\n");
}
out
}
fn gen_pinvoke_for_method(c_name: &str, method: &MethodDef) -> String {
let cs_name = to_csharp_name(&method.name);
let mut out =
format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
out.push_str(" internal static extern ");
out.push_str(pinvoke_return_type(&method.return_type));
out.push_str(&format!(" {}(", cs_name));
if method.params.is_empty() {
out.push_str(");\n\n");
} else {
out.push('\n');
for (i, param) in method.params.iter().enumerate() {
out.push_str(" ");
if matches!(param.ty, TypeRef::String | TypeRef::Char) {
out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
}
let param_name = param.name.to_lower_camel_case();
out.push_str(&format!("{} {}", csharp_type(¶m.ty), param_name));
if i < method.params.len() - 1 {
out.push(',');
}
out.push('\n');
}
out.push_str(" );\n\n");
}
out
}
fn gen_exception_class(namespace: &str, class_name: &str) -> String {
let mut out = String::from(
"// This file is auto-generated by alef. DO NOT EDIT.\n\
using System;\n\n",
);
out.push_str(&format!("namespace {};\n\n", namespace));
out.push_str(&format!("public class {} : Exception\n", class_name));
out.push_str("{\n");
out.push_str(" public int Code { get; }\n\n");
out.push_str(&format!(
" public {}(int code, string message) : base(message)\n",
class_name
));
out.push_str(" {\n");
out.push_str(" Code = code;\n");
out.push_str(" }\n");
out.push_str("}\n");
out
}
fn gen_wrapper_class(
api: &ApiSurface,
namespace: &str,
class_name: &str,
exception_name: &str,
prefix: &str,
) -> String {
let mut out = String::from(
"// This file is auto-generated by alef. DO NOT EDIT.\n\
using System;\n\
using System.Collections.Generic;\n\
using System.Runtime.InteropServices;\n\
using System.Text.Json;\n\
using System.Text.Json.Serialization;\n\
using System.Threading.Tasks;\n\n",
);
out.push_str(&format!("namespace {};\n\n", namespace));
out.push_str(&format!("public static class {}\n", class_name));
out.push_str("{\n");
for func in &api.functions {
out.push_str(&gen_wrapper_function(func, exception_name, prefix));
}
for typ in &api.types {
if typ.is_opaque {
continue;
}
for method in &typ.methods {
if let alef_core::ir::TypeRef::Named(ref name) = method.return_type {
if api.types.iter().any(|t| t.name == *name && t.is_opaque) {
continue;
}
}
out.push_str(&gen_wrapper_method(method, exception_name, prefix, &typ.name));
}
}
out.push_str(" private static ");
out.push_str(&format!("{} GetLastError()\n", exception_name));
out.push_str(" {\n");
out.push_str(" var code = NativeMethods.LastErrorCode();\n");
out.push_str(" var ctxPtr = NativeMethods.LastErrorContext();\n");
out.push_str(" var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n");
out.push_str(&format!(" return new {}(code, message);\n", exception_name));
out.push_str(" }\n");
out.push_str("}\n");
out
}
fn gen_wrapper_function(func: &FunctionDef, _exception_name: &str, _prefix: &str) -> String {
let mut out = String::with_capacity(1024);
out.push_str(" public static ");
if func.return_type == TypeRef::Unit {
out.push_str("void");
} else {
out.push_str(&csharp_type(&func.return_type));
}
out.push_str(&format!(" {}", to_csharp_name(&func.name)));
out.push('(');
for (i, param) in func.params.iter().enumerate() {
let param_name = param.name.to_lower_camel_case();
let mapped = csharp_type(¶m.ty);
if param.optional && !mapped.ends_with('?') {
out.push_str(&format!("{mapped}? {param_name}"));
} else {
out.push_str(&format!("{mapped} {param_name}"));
}
if i < func.params.len() - 1 {
out.push_str(", ");
}
}
out.push_str(")\n {\n");
let cs_native_name = to_csharp_name(&func.name);
if func.return_type != TypeRef::Unit {
out.push_str(" var result = ");
} else {
out.push_str(" ");
}
out.push_str(&format!("NativeMethods.{}(", cs_native_name));
if func.params.is_empty() {
out.push_str(");\n");
} else {
out.push('\n');
for (i, param) in func.params.iter().enumerate() {
let param_name = param.name.to_lower_camel_case();
out.push_str(&format!(" {}", param_name));
if i < func.params.len() - 1 {
out.push(',');
}
out.push('\n');
}
out.push_str(" );\n");
}
emit_return_marshalling(&mut out, &func.return_type);
out.push_str(" }\n\n");
out
}
fn gen_wrapper_method(method: &MethodDef, _exception_name: &str, _prefix: &str, type_name: &str) -> String {
let mut out = String::with_capacity(1024);
out.push_str(" public static ");
if method.return_type == TypeRef::Unit {
out.push_str("void");
} else {
out.push_str(&csharp_type(&method.return_type));
}
let method_cs_name = format!("{}{}", type_name, to_csharp_name(&method.name));
out.push_str(&format!(" {method_cs_name}"));
out.push('(');
for (i, param) in method.params.iter().enumerate() {
let param_name = param.name.to_lower_camel_case();
let mapped = csharp_type(¶m.ty);
if param.optional && !mapped.ends_with('?') {
out.push_str(&format!("{mapped}? {param_name}"));
} else {
out.push_str(&format!("{mapped} {param_name}"));
}
if i < method.params.len() - 1 {
out.push_str(", ");
}
}
out.push_str(")\n {\n");
let cs_native_name = to_csharp_name(&method.name);
if method.return_type != TypeRef::Unit {
out.push_str(" var result = ");
} else {
out.push_str(" ");
}
out.push_str(&format!("NativeMethods.{}(", cs_native_name));
if method.params.is_empty() {
out.push_str(");\n");
} else {
out.push('\n');
for (i, param) in method.params.iter().enumerate() {
let param_name = param.name.to_lower_camel_case();
out.push_str(&format!(" {}", param_name));
if i < method.params.len() - 1 {
out.push(',');
}
out.push('\n');
}
out.push_str(" );\n");
}
emit_return_marshalling(&mut out, &method.return_type);
out.push_str(" }\n\n");
out
}
fn emit_return_marshalling(out: &mut String, return_type: &TypeRef) {
if *return_type == TypeRef::Unit {
return;
}
if returns_string(return_type) {
out.push_str(" var str = Marshal.PtrToStringUTF8(result);\n");
out.push_str(" NativeMethods.FreeString(result);\n");
out.push_str(" return str ?? string.Empty;\n");
} else if returns_bool_via_int(return_type) {
out.push_str(" return result != 0;\n");
} else if returns_json_object(return_type) {
let cs_ty = csharp_type(return_type);
out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
out.push_str(" NativeMethods.FreeString(result);\n");
out.push_str(&format!(
" return JsonSerializer.Deserialize<{}>(json ?? \"null\")!;\n",
cs_ty
));
} else {
out.push_str(" return result;\n");
}
}
fn gen_record_type(typ: &TypeDef, namespace: &str) -> String {
let mut out = String::from(
"// This file is auto-generated by alef. DO NOT EDIT.\n\
using System;\n\
using System.Collections.Generic;\n\
using System.Text.Json.Serialization;\n\n",
);
out.push_str(&format!("namespace {};\n\n", namespace));
if !typ.doc.is_empty() {
out.push_str("/// <summary>\n");
for line in typ.doc.lines() {
out.push_str(&format!("/// {}\n", line));
}
out.push_str("/// </summary>\n");
}
out.push_str(&format!("public sealed class {}\n", typ.name.to_pascal_case()));
out.push_str("{\n");
for field in &typ.fields {
if is_tuple_field(field) {
continue;
}
if !field.doc.is_empty() {
out.push_str(" /// <summary>\n");
for line in field.doc.lines() {
out.push_str(&format!(" /// {}\n", line));
}
out.push_str(" /// </summary>\n");
}
let json_name = field.name.to_lower_camel_case();
out.push_str(&format!(" [JsonPropertyName(\"{}\")]\n", json_name));
let cs_name = to_csharp_name(&field.name);
if field.optional {
let mapped = csharp_type(&field.ty);
let field_type = if mapped.ends_with('?') {
mapped.to_string()
} else {
format!("{mapped}?")
};
out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
out.push_str(" = null;\n");
} else if typ.has_default || field.default.is_some() {
let field_type = csharp_type(&field.ty).to_string();
out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
if let Some(default) = &field.default {
out.push_str(&format!(" = {};\n", default));
} else {
let default_val = match &field.ty {
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"".to_string(),
TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
TypeRef::Primitive(p) => match p {
PrimitiveType::Bool => "false".to_string(),
PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
_ => "0".to_string(),
},
TypeRef::Vec(_) => "[]".to_string(),
TypeRef::Map(_, _) => "new Dictionary<>()".to_string(),
TypeRef::Duration => "0".to_string(),
_ => "null".to_string(),
};
out.push_str(&format!(" = {};\n", default_val));
}
} else {
let field_type = csharp_type(&field.ty).to_string();
out.push_str(&format!(
" public required {} {} {{ get; set; }}\n",
field_type, cs_name
));
}
out.push('\n');
}
out.push_str("}\n");
out
}
fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
let mut out = String::from("// This file is auto-generated by alef. DO NOT EDIT.\n\n");
out.push_str(&format!("namespace {};\n\n", namespace));
if !enum_def.doc.is_empty() {
out.push_str("/// <summary>\n");
for line in enum_def.doc.lines() {
out.push_str(&format!("/// {}\n", line));
}
out.push_str("/// </summary>\n");
}
out.push_str(&format!("public enum {}\n", enum_def.name.to_pascal_case()));
out.push_str("{\n");
for variant in &enum_def.variants {
if !variant.doc.is_empty() {
out.push_str(" /// <summary>\n");
for line in variant.doc.lines() {
out.push_str(&format!(" /// {}\n", line));
}
out.push_str(" /// </summary>\n");
}
out.push_str(&format!(" {},\n", variant.name.to_pascal_case()));
}
out.push_str("}\n");
out
}