use crate::type_map::csharp_type;
use alef_codegen::doc_emission;
use alef_codegen::naming::to_csharp_name;
use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
use alef_core::config::{AdapterPattern, AlefConfig, Language, resolve_output_dir};
use alef_core::hash::{self, CommentStyle};
use alef_core::ir::{ApiSurface, EnumDef, FieldDef, FunctionDef, MethodDef, PrimitiveType, TypeDef, TypeRef};
use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
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 bridge_param_names: HashSet<String> = config
.trait_bridges
.iter()
.filter_map(|b| b.param_name.clone())
.collect();
let bridge_type_aliases: HashSet<String> = config
.trait_bridges
.iter()
.filter_map(|b| b.type_alias.clone())
.collect();
let has_visitor_callbacks = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false);
let streaming_methods: HashSet<String> = config
.adapters
.iter()
.filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
.map(|a| a.name.clone())
.collect();
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,
&bridge_param_names,
&bridge_type_aliases,
has_visitor_callbacks,
&config.trait_bridges,
&streaming_methods,
)),
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,
&bridge_param_names,
&bridge_type_aliases,
has_visitor_callbacks,
&streaming_methods,
)),
generated_header: true,
});
if has_visitor_callbacks {
for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace) {
files.push(GeneratedFile {
path: base_path.join(filename),
content: strip_trailing_whitespace(&content),
generated_header: true,
});
}
} else {
delete_stale_visitor_files(&base_path)?;
}
if !config.trait_bridges.is_empty() {
let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
let bridges: Vec<_> = config
.trait_bridges
.iter()
.filter_map(|cfg| {
let trait_name = cfg.trait_name.clone();
trait_defs
.iter()
.find(|t| t.name == trait_name)
.map(|trait_def| (trait_name, cfg, *trait_def))
})
.collect();
if !bridges.is_empty() {
let (filename, content) = crate::trait_bridge::gen_trait_bridges_file(&namespace, &prefix, &bridges);
files.push(GeneratedFile {
path: base_path.join(filename),
content: strip_trailing_whitespace(&content),
generated_header: true,
});
}
}
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
if typ.is_opaque {
let type_filename = typ.name.to_pascal_case();
files.push(GeneratedFile {
path: base_path.join(format!("{}.cs", type_filename)),
content: strip_trailing_whitespace(&gen_opaque_handle(typ, &namespace)),
generated_header: true,
});
}
}
let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
let complex_enums: HashSet<String> = api
.enums
.iter()
.filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
.map(|e| e.name.to_pascal_case())
.collect();
let custom_converter_enums: HashSet<String> = api
.enums
.iter()
.filter(|e| {
(e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty()))
|| e.variants.iter().any(|v| {
if let Some(ref rename) = v.serde_rename {
let snake = apply_rename_all(&v.name, e.serde_rename_all.as_deref());
rename != &snake
} else {
false
}
})
})
.map(|e| e.name.to_pascal_case())
.collect();
let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
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;
}
if has_visitor_callbacks && (typ.name == "NodeContext" || typ.name == "VisitResult") {
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,
&enum_names,
&complex_enums,
&custom_converter_enums,
&lang_rename_all,
)),
generated_header: true,
});
}
}
for enum_def in &api.enums {
if has_visitor_callbacks && (enum_def.name == "VisitResult" || enum_def.name == "NodeContext") {
continue;
}
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)?;
files.push(GeneratedFile {
path: PathBuf::from("packages/csharp/Directory.Build.props"),
content: gen_directory_build_props(),
generated_header: true,
});
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: "",
build_dep: BuildDependency::Ffi,
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 csharp_file_header() -> String {
let mut out = hash::header(CommentStyle::DoubleSlash);
out.push_str("#nullable enable\n\n");
out
}
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) => "ulong",
TypeRef::Primitive(PrimitiveType::Isize) => "long",
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 pinvoke_param_type(ty: &TypeRef) -> &'static str {
match ty {
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
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) => "ulong",
TypeRef::Primitive(PrimitiveType::Isize) => "long",
TypeRef::Duration => "ulong",
}
}
fn is_bridge_param(
param: &alef_core::ir::ParamDef,
bridge_param_names: &HashSet<String>,
bridge_type_aliases: &HashSet<String>,
) -> bool {
bridge_param_names.contains(¶m.name)
|| matches!(¶m.ty, alef_core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
}
#[allow(clippy::too_many_arguments)]
fn gen_native_methods(
api: &ApiSurface,
namespace: &str,
lib_name: &str,
prefix: &str,
bridge_param_names: &HashSet<String>,
bridge_type_aliases: &HashSet<String>,
has_visitor_callbacks: bool,
trait_bridges: &[alef_core::config::TraitBridgeConfig],
streaming_methods: &HashSet<String>,
) -> String {
let mut out = csharp_file_header();
out.push_str("using System;\n");
out.push_str("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();
let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
let mut opaque_param_types: HashSet<String> = HashSet::new();
let mut opaque_return_types: HashSet<String> = HashSet::new();
for func in &api.functions {
for param in &func.params {
if let TypeRef::Named(name) = ¶m.ty {
opaque_param_types.insert(name.clone());
}
}
if let TypeRef::Named(name) = &func.return_type {
if !enum_names.contains(name) {
opaque_return_types.insert(name.clone());
}
}
}
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
for method in &typ.methods {
if streaming_methods.contains(&method.name) {
continue;
}
for param in &method.params {
if let TypeRef::Named(name) = ¶m.ty {
opaque_param_types.insert(name.clone());
}
}
if let TypeRef::Named(name) = &method.return_type {
if !enum_names.contains(name) {
opaque_return_types.insert(name.clone());
}
}
}
}
let true_opaque_types: HashSet<String> = api
.types
.iter()
.filter(|t| t.is_opaque)
.map(|t| t.name.clone())
.collect();
let mut sorted_param_types: Vec<&String> = opaque_param_types.iter().collect();
sorted_param_types.sort();
for type_name in sorted_param_types {
let snake = type_name.to_snake_case();
if !true_opaque_types.contains(type_name) {
let from_json_entry = format!("{prefix}_{snake}_from_json");
let from_json_cs = format!("{}FromJson", type_name.to_pascal_case());
if emitted.insert(from_json_entry.clone()) {
out.push_str(&format!(
" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{from_json_entry}\")]\n"
));
out.push_str(&format!(
" internal static extern IntPtr {from_json_cs}([MarshalAs(UnmanagedType.LPStr)] string json);\n\n"
));
}
}
let free_entry = format!("{prefix}_{snake}_free");
let free_cs = format!("{}Free", type_name.to_pascal_case());
if emitted.insert(free_entry.clone()) {
out.push_str(&format!(
" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
));
out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
}
}
let mut sorted_return_types: Vec<&String> = opaque_return_types.iter().collect();
sorted_return_types.sort();
for type_name in sorted_return_types {
let snake = type_name.to_snake_case();
if !true_opaque_types.contains(type_name) {
let to_json_entry = format!("{prefix}_{snake}_to_json");
let to_json_cs = format!("{}ToJson", type_name.to_pascal_case());
if emitted.insert(to_json_entry.clone()) {
out.push_str(&format!(
" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{to_json_entry}\")]\n"
));
out.push_str(&format!(
" internal static extern IntPtr {to_json_cs}(IntPtr ptr);\n\n"
));
}
}
let free_entry = format!("{prefix}_{snake}_free");
let free_cs = format!("{}Free", type_name.to_pascal_case());
if emitted.insert(free_entry.clone()) {
out.push_str(&format!(
" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
));
out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
}
}
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,
bridge_param_names,
bridge_type_aliases,
));
}
}
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
let type_snake = typ.name.to_snake_case();
for method in &typ.methods {
if streaming_methods.contains(&method.name) {
continue;
}
let c_method_name = format!("{}_{}_{}", prefix, type_snake, method.name.to_lowercase());
let cs_method_name = format!("{}{}", typ.name.to_pascal_case(), to_csharp_name(&method.name));
if emitted.insert(c_method_name.clone()) {
out.push_str(&gen_pinvoke_for_method(&c_method_name, &cs_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");
if has_visitor_callbacks {
out.push('\n');
out.push_str(&crate::gen_visitor::gen_native_methods_visitor(
namespace, lib_name, prefix,
));
}
if !trait_bridges.is_empty() {
let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
let bridges: Vec<_> = trait_bridges
.iter()
.filter_map(|config| {
let trait_name = config.trait_name.clone();
trait_defs
.iter()
.find(|t| t.name == trait_name)
.map(|trait_def| (trait_name, config, *trait_def))
})
.collect();
if !bridges.is_empty() {
out.push('\n');
out.push_str(&crate::trait_bridge::gen_native_methods_trait_bridges(
namespace, prefix, &bridges,
));
}
}
out.push_str("}\n");
out
}
fn gen_pinvoke_for_func(
c_name: &str,
func: &FunctionDef,
bridge_param_names: &HashSet<String>,
bridge_type_aliases: &HashSet<String>,
) -> 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));
let visible_params: Vec<_> = func
.params
.iter()
.filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
.collect();
if visible_params.is_empty() {
out.push_str(");\n\n");
} else {
out.push('\n');
for (i, param) in visible_params.iter().enumerate() {
out.push_str(" ");
let pinvoke_ty = pinvoke_param_type(¶m.ty);
if pinvoke_ty == "string" {
out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
}
let param_name = param.name.to_lower_camel_case();
out.push_str(&format!("{pinvoke_ty} {param_name}"));
if i < visible_params.len() - 1 {
out.push(',');
}
out.push('\n');
}
out.push_str(" );\n\n");
}
out
}
fn gen_pinvoke_for_method(c_name: &str, cs_name: &str, method: &MethodDef) -> String {
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(" ");
let pinvoke_ty = pinvoke_param_type(¶m.ty);
if pinvoke_ty == "string" {
out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
}
let param_name = param.name.to_lower_camel_case();
out.push_str(&format!("{pinvoke_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 = csharp_file_header();
out.push_str("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
}
#[allow(clippy::too_many_arguments)]
fn gen_wrapper_class(
api: &ApiSurface,
namespace: &str,
class_name: &str,
exception_name: &str,
prefix: &str,
bridge_param_names: &HashSet<String>,
bridge_type_aliases: &HashSet<String>,
has_visitor_callbacks: bool,
streaming_methods: &HashSet<String>,
) -> String {
let mut out = csharp_file_header();
out.push_str("using System;\n");
out.push_str("using System.Collections.Generic;\n");
out.push_str("using System.Runtime.InteropServices;\n");
out.push_str("using System.Text.Json;\n");
out.push_str("using System.Text.Json.Serialization;\n");
out.push_str("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");
out.push_str(" private static readonly JsonSerializerOptions JsonOptions = new()\n");
out.push_str(" {\n");
out.push_str(" Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) },\n");
out.push_str(" DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault\n");
out.push_str(" };\n\n");
let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
let true_opaque_types: HashSet<String> = api
.types
.iter()
.filter(|t| t.is_opaque)
.map(|t| t.name.clone())
.collect();
for func in &api.functions {
out.push_str(&gen_wrapper_function(
func,
exception_name,
prefix,
&enum_names,
&true_opaque_types,
bridge_param_names,
bridge_type_aliases,
));
}
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
if typ.is_opaque {
continue;
}
for method in &typ.methods {
if streaming_methods.contains(&method.name) {
continue;
}
out.push_str(&gen_wrapper_method(
method,
exception_name,
prefix,
&typ.name,
&enum_names,
&true_opaque_types,
bridge_param_names,
bridge_type_aliases,
));
}
}
if has_visitor_callbacks {
out.push_str(&crate::gen_visitor::gen_convert_with_visitor_method(
exception_name,
prefix,
));
}
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 emit_named_param_setup(
out: &mut String,
params: &[alef_core::ir::ParamDef],
indent: &str,
true_opaque_types: &HashSet<String>,
) {
for param in params {
let param_name = param.name.to_lower_camel_case();
let json_var = format!("{param_name}Json");
let handle_var = format!("{param_name}Handle");
match ¶m.ty {
TypeRef::Named(type_name) => {
if true_opaque_types.contains(type_name) {
continue;
}
let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
if param.optional {
out.push_str(&format!(
"{indent}var {json_var} = {param_name} != null ? JsonSerializer.Serialize({param_name}, JsonOptions) : \"null\";\n"
));
} else {
out.push_str(&format!(
"{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
));
}
out.push_str(&format!(
"{indent}var {handle_var} = NativeMethods.{from_json_method}({json_var});\n"
));
}
TypeRef::Vec(_) | TypeRef::Map(_, _) => {
out.push_str(&format!(
"{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
));
out.push_str(&format!(
"{indent}var {handle_var} = Marshal.StringToHGlobalAnsi({json_var});\n"
));
}
TypeRef::Bytes => {
out.push_str(&format!(
"{indent}var {handle_var} = GCHandle.Alloc({param_name}, GCHandleType.Pinned);\n"
));
}
_ => {}
}
}
}
fn returns_ptr(ty: &TypeRef) -> bool {
matches!(
ty,
TypeRef::String
| TypeRef::Char
| TypeRef::Path
| TypeRef::Json
| TypeRef::Named(_)
| TypeRef::Vec(_)
| TypeRef::Map(_, _)
| TypeRef::Bytes
| TypeRef::Optional(_)
)
}
fn native_call_arg(ty: &TypeRef, param_name: &str, optional: bool, true_opaque_types: &HashSet<String>) -> String {
match ty {
TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
let bang = if optional { "!" } else { "" };
format!("{param_name}{bang}.Handle")
}
TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
format!("{param_name}Handle")
}
TypeRef::Bytes => {
format!("{param_name}Handle.AddrOfPinnedObject()")
}
TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool) => {
if optional {
format!("({param_name}?.Value ? 1 : 0)")
} else {
format!("({param_name} ? 1 : 0)")
}
}
ty => {
if optional {
let needs_value_unwrap = matches!(ty, TypeRef::Primitive(_) | TypeRef::Duration);
if needs_value_unwrap {
format!("{param_name}.GetValueOrDefault()")
} else {
format!("{param_name}!")
}
} else {
param_name.to_string()
}
}
}
}
fn emit_named_param_teardown(
out: &mut String,
params: &[alef_core::ir::ParamDef],
true_opaque_types: &HashSet<String>,
) {
for param in params {
let param_name = param.name.to_lower_camel_case();
let handle_var = format!("{param_name}Handle");
match ¶m.ty {
TypeRef::Named(type_name) => {
if true_opaque_types.contains(type_name) {
continue;
}
let free_method = format!("{}Free", type_name.to_pascal_case());
out.push_str(&format!(" NativeMethods.{free_method}({handle_var});\n"));
}
TypeRef::Vec(_) | TypeRef::Map(_, _) => {
out.push_str(&format!(" Marshal.FreeHGlobal({handle_var});\n"));
}
TypeRef::Bytes => {
out.push_str(&format!(" {handle_var}.Free();\n"));
}
_ => {}
}
}
}
fn emit_named_param_teardown_indented(
out: &mut String,
params: &[alef_core::ir::ParamDef],
indent: &str,
true_opaque_types: &HashSet<String>,
) {
for param in params {
let param_name = param.name.to_lower_camel_case();
let handle_var = format!("{param_name}Handle");
match ¶m.ty {
TypeRef::Named(type_name) => {
if true_opaque_types.contains(type_name) {
continue;
}
let free_method = format!("{}Free", type_name.to_pascal_case());
out.push_str(&format!("{indent}NativeMethods.{free_method}({handle_var});\n"));
}
TypeRef::Vec(_) | TypeRef::Map(_, _) => {
out.push_str(&format!("{indent}Marshal.FreeHGlobal({handle_var});\n"));
}
TypeRef::Bytes => {
out.push_str(&format!("{indent}{handle_var}.Free();\n"));
}
_ => {}
}
}
}
fn gen_wrapper_function(
func: &FunctionDef,
_exception_name: &str,
_prefix: &str,
enum_names: &HashSet<String>,
true_opaque_types: &HashSet<String>,
bridge_param_names: &HashSet<String>,
bridge_type_aliases: &HashSet<String>,
) -> String {
let mut out = String::with_capacity(1024);
let visible_params: Vec<alef_core::ir::ParamDef> = func
.params
.iter()
.filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
.cloned()
.collect();
doc_emission::emit_csharp_doc(&mut out, &func.doc, " ");
for param in &visible_params {
if !func.doc.is_empty() {
out.push_str(&format!(
" /// <param name=\"{}\">{}</param>\n",
param.name.to_lower_camel_case(),
if param.optional { "Optional." } else { "" }
));
}
}
out.push_str(" public static ");
if func.is_async {
if func.return_type == TypeRef::Unit {
out.push_str("async Task");
} else {
out.push_str(&format!("async Task<{}>", csharp_type(&func.return_type)));
}
} else 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 visible_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 < visible_params.len() - 1 {
out.push_str(", ");
}
}
out.push_str(")\n {\n");
for param in &visible_params {
if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
let param_name = param.name.to_lower_camel_case();
out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
}
}
emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
let cs_native_name = to_csharp_name(&func.name);
if func.is_async {
if func.return_type == TypeRef::Unit {
out.push_str(" await Task.Run(() =>\n {\n");
} else {
out.push_str(" return await Task.Run(() =>\n {\n");
}
if func.return_type != TypeRef::Unit {
out.push_str(" var nativeResult = ");
} else {
out.push_str(" ");
}
out.push_str(&format!("NativeMethods.{}(", cs_native_name));
if visible_params.is_empty() {
out.push_str(");\n");
} else {
out.push('\n');
for (i, param) in visible_params.iter().enumerate() {
let param_name = param.name.to_lower_camel_case();
let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
out.push_str(&format!(" {arg}"));
if i < visible_params.len() - 1 {
out.push(',');
}
out.push('\n');
}
out.push_str(" );\n");
}
if func.return_type != TypeRef::Unit {
out.push_str(
" if (nativeResult == IntPtr.Zero)\n {\n var err = GetLastError();\n if (err.Code != 0)\n {\n throw err;\n }\n }\n",
);
}
emit_return_marshalling_indented(
&mut out,
&func.return_type,
" ",
enum_names,
true_opaque_types,
);
emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
emit_return_statement_indented(&mut out, &func.return_type, " ");
out.push_str(" });\n");
} else {
if func.return_type != TypeRef::Unit {
out.push_str(" var nativeResult = ");
} else {
out.push_str(" ");
}
out.push_str(&format!("NativeMethods.{}(", cs_native_name));
if visible_params.is_empty() {
out.push_str(");\n");
} else {
out.push('\n');
for (i, param) in visible_params.iter().enumerate() {
let param_name = param.name.to_lower_camel_case();
let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
out.push_str(&format!(" {arg}"));
if i < visible_params.len() - 1 {
out.push(',');
}
out.push('\n');
}
out.push_str(" );\n");
}
if func.return_type != TypeRef::Unit && returns_ptr(&func.return_type) {
out.push_str(
" if (nativeResult == IntPtr.Zero)\n {\n var err = GetLastError();\n if (err.Code != 0)\n {\n throw err;\n }\n }\n",
);
}
emit_return_marshalling(&mut out, &func.return_type, enum_names, true_opaque_types);
emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
emit_return_statement(&mut out, &func.return_type);
}
out.push_str(" }\n\n");
out
}
#[allow(clippy::too_many_arguments)]
fn gen_wrapper_method(
method: &MethodDef,
_exception_name: &str,
_prefix: &str,
type_name: &str,
enum_names: &HashSet<String>,
true_opaque_types: &HashSet<String>,
bridge_param_names: &HashSet<String>,
bridge_type_aliases: &HashSet<String>,
) -> String {
let mut out = String::with_capacity(1024);
let visible_params: Vec<alef_core::ir::ParamDef> = method
.params
.iter()
.filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
.cloned()
.collect();
doc_emission::emit_csharp_doc(&mut out, &method.doc, " ");
for param in &visible_params {
if !method.doc.is_empty() {
out.push_str(&format!(
" /// <param name=\"{}\">{}</param>\n",
param.name.to_lower_camel_case(),
if param.optional { "Optional." } else { "" }
));
}
}
out.push_str(" public static ");
if method.is_async {
if method.return_type == TypeRef::Unit {
out.push_str("async Task");
} else {
out.push_str(&format!("async Task<{}>", csharp_type(&method.return_type)));
}
} else 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 visible_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 < visible_params.len() - 1 {
out.push_str(", ");
}
}
out.push_str(")\n {\n");
for param in &visible_params {
if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
let param_name = param.name.to_lower_camel_case();
out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
}
}
emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
let cs_native_name = format!("{}{}", type_name.to_pascal_case(), to_csharp_name(&method.name));
if method.is_async {
if method.return_type == TypeRef::Unit {
out.push_str(" await Task.Run(() =>\n {\n");
} else {
out.push_str(" return await Task.Run(() =>\n {\n");
}
if method.return_type != TypeRef::Unit {
out.push_str(" var nativeResult = ");
} else {
out.push_str(" ");
}
out.push_str(&format!("NativeMethods.{}(", cs_native_name));
if visible_params.is_empty() {
out.push_str(");\n");
} else {
out.push('\n');
for (i, param) in visible_params.iter().enumerate() {
let param_name = param.name.to_lower_camel_case();
let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
out.push_str(&format!(" {arg}"));
if i < visible_params.len() - 1 {
out.push(',');
}
out.push('\n');
}
out.push_str(" );\n");
}
emit_return_marshalling_indented(
&mut out,
&method.return_type,
" ",
enum_names,
true_opaque_types,
);
emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
emit_return_statement_indented(&mut out, &method.return_type, " ");
out.push_str(" });\n");
} else {
if method.return_type != TypeRef::Unit {
out.push_str(" var nativeResult = ");
} else {
out.push_str(" ");
}
out.push_str(&format!("NativeMethods.{}(", cs_native_name));
if visible_params.is_empty() {
out.push_str(");\n");
} else {
out.push('\n');
for (i, param) in visible_params.iter().enumerate() {
let param_name = param.name.to_lower_camel_case();
let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
out.push_str(&format!(" {arg}"));
if i < visible_params.len() - 1 {
out.push(',');
}
out.push('\n');
}
out.push_str(" );\n");
}
emit_return_marshalling(&mut out, &method.return_type, enum_names, true_opaque_types);
emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
emit_return_statement(&mut out, &method.return_type);
}
out.push_str(" }\n\n");
out
}
fn emit_return_marshalling(
out: &mut String,
return_type: &TypeRef,
enum_names: &HashSet<String>,
true_opaque_types: &HashSet<String>,
) {
if *return_type == TypeRef::Unit {
return;
}
if returns_string(return_type) {
out.push_str(" var returnValue = Marshal.PtrToStringUTF8(nativeResult) ?? string.Empty;\n");
out.push_str(" NativeMethods.FreeString(nativeResult);\n");
} else if returns_bool_via_int(return_type) {
out.push_str(" var returnValue = nativeResult != 0;\n");
} else if let TypeRef::Named(type_name) = return_type {
let pascal = type_name.to_pascal_case();
if true_opaque_types.contains(type_name) {
out.push_str(&format!(" var returnValue = new {pascal}(nativeResult);\n"));
} else if !enum_names.contains(&pascal) {
let to_json_method = format!("{pascal}ToJson");
let free_method = format!("{pascal}Free");
let cs_ty = csharp_type(return_type);
out.push_str(&format!(
" var jsonPtr = NativeMethods.{to_json_method}(nativeResult);\n"
));
out.push_str(" var json = Marshal.PtrToStringUTF8(jsonPtr);\n");
out.push_str(" NativeMethods.FreeString(jsonPtr);\n");
out.push_str(&format!(" NativeMethods.{free_method}(nativeResult);\n"));
out.push_str(&format!(
" var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
cs_ty
));
} else {
let cs_ty = csharp_type(return_type);
out.push_str(" var json = Marshal.PtrToStringUTF8(nativeResult);\n");
out.push_str(" NativeMethods.FreeString(nativeResult);\n");
out.push_str(&format!(
" var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
cs_ty
));
}
} else if returns_json_object(return_type) {
let cs_ty = csharp_type(return_type);
out.push_str(" var json = Marshal.PtrToStringUTF8(nativeResult);\n");
out.push_str(" NativeMethods.FreeString(nativeResult);\n");
out.push_str(&format!(
" var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
cs_ty
));
} else {
out.push_str(" var returnValue = nativeResult;\n");
}
}
fn emit_return_statement(out: &mut String, return_type: &TypeRef) {
if *return_type != TypeRef::Unit {
out.push_str(" return returnValue;\n");
}
}
fn emit_return_marshalling_indented(
out: &mut String,
return_type: &TypeRef,
indent: &str,
enum_names: &HashSet<String>,
true_opaque_types: &HashSet<String>,
) {
if *return_type == TypeRef::Unit {
return;
}
if returns_string(return_type) {
out.push_str(&format!(
"{indent}var returnValue = Marshal.PtrToStringUTF8(nativeResult) ?? string.Empty;\n"
));
out.push_str(&format!("{indent}NativeMethods.FreeString(nativeResult);\n"));
} else if returns_bool_via_int(return_type) {
out.push_str(&format!("{indent}var returnValue = nativeResult != 0;\n"));
} else if let TypeRef::Named(type_name) = return_type {
let pascal = type_name.to_pascal_case();
if true_opaque_types.contains(type_name) {
out.push_str(&format!("{indent}var returnValue = new {pascal}(nativeResult);\n"));
} else if !enum_names.contains(&pascal) {
let to_json_method = format!("{pascal}ToJson");
let free_method = format!("{pascal}Free");
let cs_ty = csharp_type(return_type);
out.push_str(&format!(
"{indent}var jsonPtr = NativeMethods.{to_json_method}(nativeResult);\n"
));
out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(jsonPtr);\n"));
out.push_str(&format!("{indent}NativeMethods.FreeString(jsonPtr);\n"));
out.push_str(&format!("{indent}NativeMethods.{free_method}(nativeResult);\n"));
out.push_str(&format!(
"{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
cs_ty
));
} else {
let cs_ty = csharp_type(return_type);
out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(nativeResult);\n"));
out.push_str(&format!("{indent}NativeMethods.FreeString(nativeResult);\n"));
out.push_str(&format!(
"{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
cs_ty
));
}
} else if returns_json_object(return_type) {
let cs_ty = csharp_type(return_type);
out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(nativeResult);\n"));
out.push_str(&format!("{indent}NativeMethods.FreeString(nativeResult);\n"));
out.push_str(&format!(
"{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
cs_ty
));
} else {
out.push_str(&format!("{indent}var returnValue = nativeResult;\n"));
}
}
fn emit_return_statement_indented(out: &mut String, return_type: &TypeRef, indent: &str) {
if *return_type != TypeRef::Unit {
out.push_str(&format!("{indent}return returnValue;\n"));
}
}
fn gen_opaque_handle(typ: &TypeDef, namespace: &str) -> String {
let mut out = csharp_file_header();
out.push_str("using System;\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");
}
let class_name = typ.name.to_pascal_case();
out.push_str(&format!("public sealed class {} : IDisposable\n", class_name));
out.push_str("{\n");
out.push_str(" internal IntPtr Handle { get; }\n\n");
out.push_str(&format!(" internal {}(IntPtr handle)\n", class_name));
out.push_str(" {\n");
out.push_str(" Handle = handle;\n");
out.push_str(" }\n\n");
out.push_str(" public void Dispose()\n");
out.push_str(" {\n");
out.push_str(" // Native free will be called by the runtime\n");
out.push_str(" }\n");
out.push_str("}\n");
out
}
fn gen_record_type(
typ: &TypeDef,
namespace: &str,
enum_names: &HashSet<String>,
complex_enums: &HashSet<String>,
custom_converter_enums: &HashSet<String>,
_lang_rename_all: &str,
) -> String {
let mut out = csharp_file_header();
out.push_str("using System;\n");
out.push_str("using System.Collections.Generic;\n");
out.push_str("using System.Text.Json;\n");
out.push_str("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 field_base_type = match &field.ty {
TypeRef::Named(n) => Some(n.to_pascal_case()),
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Named(n) => Some(n.to_pascal_case()),
_ => None,
},
_ => None,
};
if let Some(ref base) = field_base_type {
if custom_converter_enums.contains(base) {
out.push_str(&format!(" [JsonConverter(typeof({base}JsonConverter))]\n"));
}
}
let json_name = field.name.clone();
out.push_str(&format!(" [JsonPropertyName(\"{}\")]\n", json_name));
let cs_name = to_csharp_name(&field.name);
let is_complex = matches!(&field.ty, TypeRef::Named(n) if complex_enums.contains(&n.to_pascal_case()));
if field.optional {
let mapped = if is_complex {
"JsonElement".to_string()
} else {
csharp_type(&field.ty).to_string()
};
let field_type = if mapped.ends_with('?') {
mapped
} 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() {
use alef_core::ir::DefaultValue;
let base_type = if is_complex {
"JsonElement".to_string()
} else {
csharp_type(&field.ty).to_string()
};
if matches!(&field.ty, TypeRef::Duration) {
let nullable_type = if base_type.ends_with('?') {
base_type.clone()
} else {
format!("{}?", base_type)
};
out.push_str(&format!(
" public {} {} {{ get; set; }} = null;\n",
nullable_type, cs_name
));
out.push('\n');
continue;
}
let default_val = match &field.typed_default {
Some(DefaultValue::BoolLiteral(b)) => b.to_string(),
Some(DefaultValue::IntLiteral(n)) => n.to_string(),
Some(DefaultValue::FloatLiteral(f)) => {
let s = f.to_string();
let s = if s.contains('.') { s } else { format!("{s}.0") };
match &field.ty {
TypeRef::Primitive(PrimitiveType::F32) => format!("{}f", s),
_ => s,
}
}
Some(DefaultValue::StringLiteral(s)) => {
let escaped = s
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t");
format!("\"{}\"", escaped)
}
Some(DefaultValue::EnumVariant(v)) => {
if base_type == "string" || base_type == "string?" {
format!("\"{}\"", v.to_pascal_case())
} else {
format!("{}.{}", base_type, v.to_pascal_case())
}
}
Some(DefaultValue::None) => "null".to_string(),
Some(DefaultValue::Empty) | None => match &field.ty {
TypeRef::Vec(_) => "[]".to_string(),
TypeRef::Map(k, v) => format!("new Dictionary<{}, {}>()", csharp_type(k), csharp_type(v)),
TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
TypeRef::Json => "null".to_string(),
TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
TypeRef::Primitive(p) => match p {
PrimitiveType::Bool => "false".to_string(),
PrimitiveType::F32 => "0.0f".to_string(),
PrimitiveType::F64 => "0.0".to_string(),
_ => "0".to_string(),
},
TypeRef::Named(name) => {
let pascal = name.to_pascal_case();
if complex_enums.contains(&pascal) {
"null".to_string()
} else if enum_names.contains(&pascal) {
"null".to_string()
} else {
"default!".to_string()
}
}
_ => "default!".to_string(),
},
};
let field_type = if (default_val == "null" && !base_type.ends_with('?')) || is_complex {
format!("{}?", base_type)
} else {
base_type
};
out.push_str(&format!(
" public {} {} {{ get; set; }} = {};\n",
field_type, cs_name, default_val
));
} else {
let field_type = if is_complex {
"JsonElement".to_string()
} else {
csharp_type(&field.ty).to_string()
};
if matches!(&field.ty, TypeRef::Duration) {
out.push_str(&format!(
" public {} {} {{ get; set; }} = null;\n",
field_type, cs_name
));
} else {
let default_val = match &field.ty {
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"",
TypeRef::Vec(_) => "[]",
TypeRef::Bytes => "Array.Empty<byte>()",
TypeRef::Primitive(PrimitiveType::Bool) => "false",
TypeRef::Primitive(PrimitiveType::F32) => "0.0f",
TypeRef::Primitive(PrimitiveType::F64) => "0.0",
TypeRef::Primitive(_) => "0",
_ => "default!",
};
out.push_str(&format!(
" public {} {} {{ get; set; }} = {};\n",
field_type, cs_name, default_val
));
}
}
out.push('\n');
}
out.push_str("}\n");
out
}
fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
match rename_all {
Some("snake_case") => name.to_snake_case(),
Some("camelCase") => name.to_lower_camel_case(),
Some("PascalCase") => name.to_pascal_case(),
Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
Some("lowercase") => name.to_lowercase(),
Some("UPPERCASE") => name.to_uppercase(),
_ => name.to_lowercase(),
}
}
fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
let mut out = csharp_file_header();
out.push_str("using System.Text.Json.Serialization;\n\n");
let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
if enum_def.serde_tag.is_some() && has_data_variants {
return gen_tagged_union(enum_def, namespace);
}
let needs_custom_converter = enum_def.variants.iter().any(|v| {
if let Some(ref rename) = v.serde_rename {
let snake = apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref());
rename != &snake
} else {
false
}
});
let enum_pascal = enum_def.name.to_pascal_case();
let variants: Vec<(String, String)> = enum_def
.variants
.iter()
.map(|v| {
let json_name = v
.serde_rename
.clone()
.unwrap_or_else(|| apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref()));
let pascal_name = v.name.to_pascal_case();
(json_name, pascal_name)
})
.collect();
out.push_str("using System;\n");
out.push_str("using System.Text.Json;\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");
}
if needs_custom_converter {
out.push_str(&format!("[JsonConverter(typeof({enum_pascal}JsonConverter))]\n"));
}
out.push_str(&format!("public enum {enum_pascal}\n"));
out.push_str("{\n");
for (json_name, pascal_name) in &variants {
if let Some(v) = enum_def
.variants
.iter()
.find(|v| v.name.to_pascal_case() == *pascal_name)
{
if !v.doc.is_empty() {
out.push_str(" /// <summary>\n");
for line in v.doc.lines() {
out.push_str(&format!(" /// {}\n", line));
}
out.push_str(" /// </summary>\n");
}
}
out.push_str(&format!(" [JsonPropertyName(\"{json_name}\")]\n"));
out.push_str(&format!(" {pascal_name},\n"));
}
out.push_str("}\n");
if needs_custom_converter {
out.push('\n');
out.push_str(&format!(
"/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that respects explicit variant names.</summary>\n"
));
out.push_str(&format!(
"internal sealed class {enum_pascal}JsonConverter : JsonConverter<{enum_pascal}>\n"
));
out.push_str("{\n");
out.push_str(&format!(
" public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
));
out.push_str(" {\n");
out.push_str(" var value = reader.GetString();\n");
out.push_str(" return value switch\n");
out.push_str(" {\n");
for (json_name, pascal_name) in &variants {
out.push_str(&format!(
" \"{json_name}\" => {enum_pascal}.{pascal_name},\n"
));
}
out.push_str(&format!(
" _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
));
out.push_str(" };\n");
out.push_str(" }\n\n");
out.push_str(&format!(
" public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
));
out.push_str(" {\n");
out.push_str(" var str = value switch\n");
out.push_str(" {\n");
for (json_name, pascal_name) in &variants {
out.push_str(&format!(
" {enum_pascal}.{pascal_name} => \"{json_name}\",\n"
));
}
out.push_str(&format!(
" _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
));
out.push_str(" };\n");
out.push_str(" writer.WriteStringValue(str);\n");
out.push_str(" }\n");
out.push_str("}\n");
}
out
}
fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
let enum_pascal = enum_def.name.to_pascal_case();
let converter_name = format!("{enum_pascal}JsonConverter");
let ns = namespace;
let mut out = csharp_file_header();
out.push_str("using System;\n");
out.push_str("using System.Collections.Generic;\n");
out.push_str("using System.Text.Json;\n");
out.push_str("using System.Text.Json.Serialization;\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!("[JsonConverter(typeof({converter_name}))]\n"));
out.push_str(&format!("public abstract record {enum_pascal}\n"));
out.push_str("{\n");
let variant_names: std::collections::HashSet<String> =
enum_def.variants.iter().map(|v| v.name.to_pascal_case()).collect();
for variant in &enum_def.variants {
let pascal = variant.name.to_pascal_case();
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");
}
if variant.fields.is_empty() {
out.push_str(&format!(" public sealed record {pascal}() : {enum_pascal};\n\n"));
} else {
let is_copy_ctor_clash = variant.fields.len() == 1 && {
let field_cs_type = csharp_type(&variant.fields[0].ty);
field_cs_type.as_ref() == pascal
};
if is_copy_ctor_clash {
let cs_type = csharp_type(&variant.fields[0].ty);
let qualified_cs_type = format!("global::{ns}.{cs_type}");
out.push_str(&format!(" public sealed record {pascal} : {enum_pascal}\n"));
out.push_str(" {\n");
out.push_str(&format!(
" public required {qualified_cs_type} Value {{ get; init; }}\n"
));
out.push_str(" }\n\n");
} else {
out.push_str(&format!(" public sealed record {pascal}(\n"));
for (i, field) in variant.fields.iter().enumerate() {
let cs_type = csharp_type(&field.ty);
let cs_type = if field.optional && !cs_type.ends_with('?') {
format!("{cs_type}?")
} else {
cs_type.to_string()
};
let comma = if i < variant.fields.len() - 1 { "," } else { "" };
if is_tuple_field(field) {
out.push_str(&format!(" {cs_type} Value{comma}\n"));
} else {
let json_name = field.name.trim_start_matches('_');
let cs_name = to_csharp_name(json_name);
let clashes = cs_name == pascal || cs_name == cs_type || variant_names.contains(&cs_name);
if clashes {
out.push_str(&format!(
" [property: JsonPropertyName(\"{json_name}\")] {cs_type} Value{comma}\n"
));
} else {
out.push_str(&format!(
" [property: JsonPropertyName(\"{json_name}\")] {cs_type} {cs_name}{comma}\n"
));
}
}
}
out.push_str(&format!(" ) : {enum_pascal};\n\n"));
}
}
}
out.push_str("}\n\n");
out.push_str(&format!(
"/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that reads the \"{tag_field}\" discriminator from any position.</summary>\n"
));
out.push_str(&format!(
"internal sealed class {converter_name} : JsonConverter<{enum_pascal}>\n"
));
out.push_str("{\n");
out.push_str(&format!(
" public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
));
out.push_str(" {\n");
out.push_str(" using var doc = JsonDocument.ParseValue(ref reader);\n");
out.push_str(" var root = doc.RootElement;\n");
out.push_str(&format!(
" if (!root.TryGetProperty(\"{tag_field}\", out var tagEl))\n"
));
out.push_str(&format!(
" throw new JsonException(\"{enum_pascal}: missing \\\"{tag_field}\\\" discriminator\");\n"
));
out.push_str(" var tag = tagEl.GetString();\n");
out.push_str(" var json = root.GetRawText();\n");
out.push_str(" return tag switch\n");
out.push_str(" {\n");
for variant in &enum_def.variants {
let discriminator = variant
.serde_rename
.clone()
.unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
let pascal = variant.name.to_pascal_case();
let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
let f = &variant.fields[0];
let cs_type = csharp_type(&f.ty);
let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
cs_name == pascal || cs_name == cs_type
};
let is_newtype = is_tuple_newtype || is_named_clash_newtype;
if is_newtype {
let inner_cs_type = csharp_type(&variant.fields[0].ty);
if inner_cs_type == pascal {
out.push_str(&format!(
" \"{discriminator}\" => new {enum_pascal}.{pascal} {{ Value = JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
));
out.push_str(&format!(
" ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\") }},\n"
));
} else {
out.push_str(&format!(
" \"{discriminator}\" => new {enum_pascal}.{pascal}(\n"
));
out.push_str(&format!(
" JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
));
out.push_str(&format!(
" ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\")),\n"
));
}
} else {
out.push_str(&format!(
" \"{discriminator}\" => JsonSerializer.Deserialize<{enum_pascal}.{pascal}>(json, options)!\n"
));
out.push_str(&format!(
" ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}\"),\n"
));
}
}
out.push_str(&format!(
" _ => throw new JsonException($\"Unknown {enum_pascal} discriminator: {{tag}}\")\n"
));
out.push_str(" };\n");
out.push_str(" }\n\n");
out.push_str(&format!(
" public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
));
out.push_str(" {\n");
out.push_str(" // Serialize the concrete type, then inject the discriminator\n");
out.push_str(" switch (value)\n");
out.push_str(" {\n");
for variant in &enum_def.variants {
let discriminator = variant
.serde_rename
.clone()
.unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
let pascal = variant.name.to_pascal_case();
let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
let f = &variant.fields[0];
let cs_type = csharp_type(&f.ty);
let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
cs_name == pascal || cs_name == cs_type
};
let is_newtype = is_tuple_newtype || is_named_clash_newtype;
out.push_str(&format!(" case {enum_pascal}.{pascal} v:\n"));
out.push_str(" {\n");
if is_newtype {
out.push_str(" var doc = JsonSerializer.SerializeToDocument(v.Value, options);\n");
} else {
out.push_str(" var doc = JsonSerializer.SerializeToDocument(v, options);\n");
}
out.push_str(" writer.WriteStartObject();\n");
out.push_str(&format!(
" writer.WriteString(\"{tag_field}\", \"{discriminator}\");\n"
));
out.push_str(" foreach (var prop in doc.RootElement.EnumerateObject())\n");
out.push_str(&format!(
" if (prop.Name != \"{tag_field}\") prop.WriteTo(writer);\n"
));
out.push_str(" writer.WriteEndObject();\n");
out.push_str(" break;\n");
out.push_str(" }\n");
}
out.push_str(&format!(
" default: throw new JsonException($\"Unknown {enum_pascal} subtype: {{value.GetType().Name}}\");\n"
));
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str("}\n");
out
}
fn gen_directory_build_props() -> String {
"<!-- auto-generated by alef (generate_bindings) -->\n\
<Project>\n \
<PropertyGroup>\n \
<Nullable>enable</Nullable>\n \
<LangVersion>latest</LangVersion>\n \
</PropertyGroup>\n\
</Project>\n"
.to_string()
}
fn delete_stale_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
let stale_files = vec!["IVisitor.cs", "VisitorCallbacks.cs", "NodeContext.cs", "VisitResult.cs"];
for filename in stale_files {
let path = base_path.join(filename);
if path.exists() {
std::fs::remove_file(&path)
.map_err(|e| anyhow::anyhow!("Failed to delete stale visitor file {}: {}", path.display(), e))?;
}
}
Ok(())
}