diff --git a/README.md b/README.md index fdcb45c..857db93 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +# Fork notes + +This fork tries to extend the code generation for a particular use case. +The goal is to generate actual C# classes based on function name prefixes. + # csbindgen [![Crates](https://img.shields.io/crates/v/csbindgen.svg)](https://crates.io/crates/csbindgen) [![Api Rustdoc](https://img.shields.io/badge/api-rustdoc-blue)](https://docs.rs/csbindgen) diff --git a/csbindgen/Cargo.toml b/csbindgen/Cargo.toml index 82656bd..d256554 100644 --- a/csbindgen/Cargo.toml +++ b/csbindgen/Cargo.toml @@ -14,3 +14,4 @@ repository = "https://github.com/Cysharp/csbindgen/" [dependencies] syn = { version = "2.0.68", features = ["full", "parsing"] } regex = "1.10.5" +convert_case = "0.6.0" diff --git a/csbindgen/src/builder.rs b/csbindgen/src/builder.rs index a185041..205cc97 100644 --- a/csbindgen/src/builder.rs +++ b/csbindgen/src/builder.rs @@ -1,3 +1,4 @@ +use std::convert::identity; use std::path::PathBuf; use std::{ error::Error, @@ -5,7 +6,6 @@ use std::{ io::{self, Write}, path::Path, }; -use std::convert::identity; use crate::{generate, GenerateKind}; @@ -37,6 +37,14 @@ pub struct BindgenOptions { pub csharp_file_header: String, pub csharp_file_footer: String, pub always_included_types: Vec, + pub csharp_method_groups: Vec, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct MethodGroup { + pub rust_prefix: String, + pub csharp_class: String, + pub rust_type: String, } impl Default for Builder { @@ -66,6 +74,7 @@ impl Default for Builder { csharp_file_header: "".to_string(), csharp_file_footer: "".to_string(), always_included_types: vec![], + csharp_method_groups: vec![], }, } } @@ -78,7 +87,9 @@ impl Builder { /// Add an input .rs file(such as generated from bindgen) to generate binding. pub fn input_bindgen_file>(mut self, input_bindgen_file: T) -> Builder { - self.options.input_bindgen_files.push(input_bindgen_file.as_ref().to_path_buf()); + self.options + .input_bindgen_files + .push(input_bindgen_file.as_ref().to_path_buf()); self } @@ -99,9 +110,13 @@ impl Builder { /// Adds a list of types that will always be considered to be included in the /// generated bindings, even if not part of any function signature pub fn always_included_types(mut self, always_included_types: I) -> Builder - where I: IntoIterator, S: ToString + where + I: IntoIterator, + S: ToString, { - self.options.always_included_types.extend(always_included_types.into_iter().map(|v| v.to_string())); + self.options + .always_included_types + .extend(always_included_types.into_iter().map(|v| v.to_string())); self } @@ -214,17 +229,27 @@ impl Builder { /// equivalent to csharp_generate_const_filter(|_| csharp_generate_const) #[deprecated(note = "User csharp_generate_const_filter instead")] pub fn csharp_generate_const(self, csharp_generate_const: bool) -> Builder { - self.csharp_generate_const_filter(if csharp_generate_const { |_| true } else { |_| false }) + self.csharp_generate_const_filter(if csharp_generate_const { + |_| true + } else { + |_| false + }) } /// configure C# generate const filter, default `|_| false` - pub fn csharp_generate_const_filter(mut self, csharp_generate_const_filter: fn(const_name: &str) -> bool) -> Builder { + pub fn csharp_generate_const_filter( + mut self, + csharp_generate_const_filter: fn(const_name: &str) -> bool, + ) -> Builder { self.options.csharp_generate_const_filter = csharp_generate_const_filter; self } /// configure the mappings that C# type name from rust original type name, default `|x| x` - pub fn csharp_type_rename(mut self, csharp_type_rename: fn(rust_type_name: String) -> String) -> Builder { + pub fn csharp_type_rename( + mut self, + csharp_type_rename: fn(rust_type_name: String) -> String, + ) -> Builder { self.options.csharp_type_rename = csharp_type_rename; self } @@ -241,6 +266,25 @@ impl Builder { self } + pub fn csharp_group_methods( + mut self, + rust_prefix: T1, + csharp_class: T2, + rust_type: T3, + ) -> Builder + where + T1: Into, + T2: Into, + T3: Into, + { + self.options.csharp_method_groups.push(MethodGroup { + csharp_class: csharp_class.into(), + rust_prefix: rust_prefix.into(), + rust_type: rust_type.into(), + }); + self + } + pub fn generate_csharp_file>( &self, csharp_output_path: P, diff --git a/csbindgen/src/emitter.rs b/csbindgen/src/emitter.rs index 32596d7..11e01a8 100644 --- a/csbindgen/src/emitter.rs +++ b/csbindgen/src/emitter.rs @@ -1,8 +1,10 @@ use crate::alias_map::AliasMap; -use crate::builder::BindgenOptions; +use crate::builder::{BindgenOptions, MethodGroup}; use crate::type_meta::ExportSymbolNaming::{ExportName, NoMangle}; use crate::type_meta::*; use crate::util::*; +use convert_case::{Case, Casing}; +use std::collections::HashMap; pub fn emit_rust_method(list: &Vec, options: &BindgenOptions) -> String { // configure @@ -110,97 +112,531 @@ pub fn emit_csharp( dll_name = "".to_string(); } + let mut method_groups = HashMap::with_capacity(options.csharp_method_groups.len()); + for prefix in &options.csharp_method_groups { + method_groups.insert(prefix, Vec::new()); + } + let mut method_list_string = String::new(); for item in methods { - let mut method_name = &item.method_name; - let method_name_temp: String; - if method_prefix.is_empty() { - method_name_temp = escape_csharp_name(method_name); - method_name = &method_name_temp; + if let Some((_, group)) = method_groups + .iter_mut() + .find(move |(prefix, _)| item.method_name.starts_with(&prefix.rust_prefix)) + { + group.push(item); + continue; } - if let Some(x) = &item.return_type { - if let Some(delegate_method) = build_method_delegate_if_required( - x, - options, - aliases, - method_name, - &"return".to_string(), - ) { - method_list_string.push_str( - format!(" [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n") - .as_str(), - ); - method_list_string - .push_str(format!(" {accessibility} {delegate_method};\n\n").as_str()); - } - } + emit_csharp_native_method( + aliases, + options, + method_prefix, + accessibility, + &mut method_list_string, + &item, + false, + ); + } - for p in item.parameters.iter() { - if let Some(delegate_method) = build_method_delegate_if_required( - &p.rust_type, - options, - aliases, - method_name, - &p.name, - ) { - method_list_string.push_str( - format!(" [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n") - .as_str(), - ); - method_list_string - .push_str(format!(" {accessibility} {delegate_method};\n\n").as_str()); - } + let structs_string = emit_csharp_structs(aliases, structs, options, accessibility); + let enum_string = emit_csharp_enums(enums, options, accessibility); + let const_string = emit_csharp_consts(aliases, consts, options, accessibility); + + // use empty string if the generated class is empty. + let class_string = if method_list_string.is_empty() && const_string.is_empty() { + String::new() + } else { + format!( + " {accessibility} static unsafe partial class {class_name} + {{ +{dll_name} + +{const_string} + +{method_list_string} + }}\n" + ) + }; + + let group_classes = + emit_csharp_method_groups(aliases, options, accessibility, &dll_name, method_groups); + + let file_header = if options.csharp_file_header.len() > 0 { + options.csharp_file_header.to_string() + "\n" + } else { + "".to_string() + } + "// +// This code is generated by csbindgen. +// DON'T CHANGE THIS DIRECTLY. +// +#pragma warning disable CS8500 +#pragma warning disable CS8981"; + let file_footer = &options.csharp_file_footer; + + let mut imported_namespaces = String::new(); + for name in &options.csharp_imported_namespaces { + imported_namespaces.push_str_ln(format!("using {name};").as_str()); + } + + let result = format!( + "{file_header} +using System; +using System.Runtime.InteropServices; +{imported_namespaces} + +namespace {namespace} +{{ +{class_string} +{group_classes} +{structs_string} +{enum_string} +}} +{file_footer}" + ); + + result +} + +fn emit_csharp_method_groups( + aliases: &AliasMap, + options: &BindgenOptions, + accessibility: &str, + dll_name: &str, + method_groups: HashMap<&MethodGroup, Vec<&ExternMethod>>, +) -> String { + let is_mapped_type = |rust_type: &RustType| -> Option<&str> { + match &rust_type.type_kind { + TypeKind::Pointer(_, r) => method_groups.keys().find_map(move |group| { + if group.rust_type == r.type_name { + Some(&*group.csharp_class) + } else { + None + } + }), + _ => None, } + }; - let entry_point = match &item.export_naming { - NoMangle => &item.method_name, - ExportName(export_name) => export_name, + let emit_constructor = |method: &ExternMethod, group: &MethodGroup| -> String { + match &method.return_type { + None => panic!("cannot return void from constructor {method:?}"), + Some(x) => match &x.type_kind { + TypeKind::Pointer(_, r) => { + let actual_type = r.to_csharp_string( + options, + aliases, + false, + &method.method_name, + &"return".to_string(), + ); + assert_eq!(actual_type, group.rust_type); + } + _ => panic!("cannot return non-pointer type from constructor {method:?}"), + }, }; - let entry_point = match options.csharp_entry_point_prefix.as_str() { - "" => format!("{method_prefix}{entry_point}"), - x => format!("{x}{entry_point}"), + let parameters_list = method + .parameters + .iter() + .map(|p| { + let type_name = match is_mapped_type(&p.rust_type) { + None => p.rust_type.to_csharp_string( + options, + aliases, + false, + &method.method_name, + &p.name, + ), + Some(csharp_type) => csharp_type.to_string(), + }; + format!("{} {}", type_name, escape_csharp_name(p.name.as_str())) + }) + .collect::>() + .join(", "); + + let parameters = method + .parameters + .iter() + .map(|p| escape_csharp_name(p.name.as_str())) + .collect::>() + .join(", "); + + let doc_comment = match method.escape_doc_comment(" ") { + None => String::new(), + Some(escaped) => escaped + "\n", }; - let return_type = match &item.return_type { - Some(x) => { - x.to_csharp_string(options, aliases, false, method_name, &"return".to_string()) - } - None => "void".to_string(), + + let class_name = &group.csharp_class; + let method_name = &method.method_name; + format!( + r#"{doc_comment} {accessibility} {class_name}({parameters_list}) : this({method_name}({parameters})) {{}}"# + ) + }; + + let emit_method = |method: &ExternMethod, group: &MethodGroup| -> String { + let class_name = &group.csharp_class; + let csharp_method_name = method.method_name[group.rust_prefix.len()..] + .to_string() + .to_case(Case::UpperCamel); + + let first_param_is_instance = method.parameters.first().is_some_and(move |first_param| { + is_mapped_type(&first_param.rust_type) + .is_some_and(move |csharp_type| csharp_type == class_name) + }); + + let into_params = { + const TAG: &str = "servicepoint_csbindgen_consumes: "; + method + .doc_comment + .iter() + .find(move |line| line.trim_start().starts_with(TAG)) + .map(move |line| { + line[TAG.len()..] + .split(',') + .map(move |item| item.trim().to_string()) + .collect::>() + }) + .unwrap_or(vec![]) }; - let parameters = item + let native_parameters = method .parameters .iter() - .map(|p| { - let mut type_name = - p.rust_type - .to_csharp_string(options, aliases, false, method_name, &p.name); - if type_name == "bool" { - type_name = "[MarshalAs(UnmanagedType.U1)] bool".to_string(); + .enumerate() + .map(|(index, p)| { + let mut result = if index == 0 && first_param_is_instance { + "this".to_string() + } else { + escape_csharp_name(p.name.as_str()) + }; + + if into_params.contains(&p.name) { + result.push_str(".Into()"); + } else if is_mapped_type(&p.rust_type).is_some() { + result.push_str(".Instance"); } - format!("{} {}", type_name, escape_csharp_name(p.name.as_str())) + result + }) + .collect::>() + .join(", "); + + let native_name = &method.method_name; + let mut native_call = format!("{class_name}.{native_name}({native_parameters})"); + + let return_type = match &method.return_type { + None => { + native_call.push(';'); + "void".to_string() + } + Some(x) => match is_mapped_type(x) { + Some(mapped_type) => { + let is_not_null = if let TypeKind::Pointer(pointer_type, _) = &x.type_kind { + matches!(pointer_type, PointerType::NonNull) + } else { + false + }; + + let mut return_type = mapped_type.to_string(); + native_call = if is_not_null { + format!("return new {mapped_type}({native_call});") + } else { + return_type.push('?'); + format!( + r#"var native = {native_call}; + return native == null ? null : new {mapped_type}(native);"# + ) + }; + return_type + } + None => { + native_call = format!("return {native_call};"); + x.to_csharp_string( + options, + aliases, + false, + &method.method_name, + &"return".to_string(), + ) + } + }, + }; + + let parameter_list = method + .parameters + .iter() + .skip(if first_param_is_instance { 1 } else { 0 }) + .map(|p| { + let type_name = match is_mapped_type(&p.rust_type) { + None => p.rust_type.to_csharp_string( + options, + aliases, + false, + &method.method_name, + &p.name, + ), + Some(csharp_type) => csharp_type.to_string(), + }; + let param_name = escape_csharp_name(p.name.as_str()); + format!("{type_name} {param_name}") }) .collect::>() .join(", "); - if let Some(x) = item.escape_doc_comment(" ") { + let doc_comment = match method.escape_doc_comment(" ") { + None => String::new(), + Some(escaped) => escaped + "\n", + }; + + let modifiers = if first_param_is_instance { + "" + } else { + "static " + }; + let inline_attr = " [System.Runtime.CompilerServices.MethodImplAttribute(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]"; + format!( + r#"{doc_comment}{inline_attr} + {accessibility} {modifiers}{return_type} {csharp_method_name}({parameter_list}) + {{ + {native_call} + }}"# + ) + }; + + method_groups + .iter() + .filter_map(move |(group, methods)| { + if methods.is_empty() { + None + } else { + let class_name = &group.csharp_class; + let rust_prefix = &group.rust_prefix; + let rust_type = &group.rust_type; + + let mut csharp_methods = String::new(); + let mut native_methods = String::new(); + let constructor_rust_name = format!("{rust_prefix}new"); + let destructor_rust_name = format!("{rust_prefix}free"); + for method in methods { + emit_csharp_native_method( + aliases, + options, + &"", // TODO: handle method prefix + "private", + &mut native_methods, + &method, + true, + ); + + let method_name = &method.method_name; + if *method_name == constructor_rust_name { + csharp_methods.push_str_ln(&emit_constructor(method, group)); + csharp_methods.push('\n'); + continue; + } else if *method_name == destructor_rust_name { + continue; + } + + csharp_methods.push_str_ln(&emit_method(method, group)); + csharp_methods.push('\n'); + } + + let machinery = format!( + r#" private {rust_type}* _instance; + internal {rust_type}* Instance + {{ + get + {{ + if (_instance == null) + throw new NullReferenceException("instance is null"); + return _instance; + }} + }} + + private {class_name}({rust_type}* instance) + {{ + ArgumentNullException.ThrowIfNull(instance); + _instance = instance; + }} + + internal {rust_type}* Into() + {{ + var instance = Instance; + _instance = null; + return instance; + }} + + private void Free() + {{ + if (_instance != null) + {class_name}.{rust_prefix}free(Into()); + }} + + public void Dispose() + {{ + Free(); + GC.SuppressFinalize(this); + }} + + ~{class_name}() => Free(); + "# + ); + + // TODO: use Interlocked.Exchange so this also catches issues with multi threading + Some(format!( + r#" {accessibility} unsafe sealed partial class {class_name}: IDisposable + {{ +#nullable enable +{csharp_methods} +#region internal machinery +{machinery} +#endregion + +#nullable restore +#region native methods +{dll_name} +{native_methods} +#endregion + }} +"# + )) + } + }) + .collect::>() + .join("\n") +} + +fn emit_csharp_native_method( + aliases: &AliasMap, + options: &BindgenOptions, + method_prefix: &str, + accessibility: &str, + method_list_string: &mut String, + method: &&ExternMethod, + suppress_docs: bool, +) { + let mut method_name = &method.method_name; + let method_name_temp: String; + if method_prefix.is_empty() { + method_name_temp = escape_csharp_name(method_name); + method_name = &method_name_temp; + } + if let Some(x) = &method.return_type { + if let Some(delegate_method) = build_method_delegate_if_required( + x, + options, + aliases, + method_name, + &"return".to_string(), + ) { + method_list_string.push_str( + " [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n" + .to_string() + .as_str(), + ); + method_list_string + .push_str(format!(" {accessibility} {delegate_method};\n\n").as_str()); + } + } + + for p in method.parameters.iter() { + if let Some(delegate_method) = + build_method_delegate_if_required(&p.rust_type, options, aliases, method_name, &p.name) + { + method_list_string.push_str( + " [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n" + .to_string() + .as_str(), + ); + method_list_string + .push_str(format!(" {accessibility} {delegate_method};\n\n").as_str()); + } + } + + let entry_point = match &method.export_naming { + NoMangle => &method.method_name, + ExportName(export_name) => export_name, + }; + + let entry_point = match options.csharp_entry_point_prefix.as_str() { + "" => format!("{method_prefix}{entry_point}"), + x => format!("{x}{entry_point}"), + }; + let return_type = match &method.return_type { + Some(x) => x.to_csharp_string(options, aliases, false, method_name, &"return".to_string()), + None => "void".to_string(), + }; + + let parameters = method + .parameters + .iter() + .map(|p| { + let mut type_name = + p.rust_type + .to_csharp_string(options, aliases, false, method_name, &p.name); + if type_name == "bool" { + type_name = "[MarshalAs(UnmanagedType.U1)] bool".to_string(); + } + + format!("{} {}", type_name, escape_csharp_name(p.name.as_str())) + }) + .collect::>() + .join(", "); + + if !suppress_docs { + if let Some(x) = method.escape_doc_comment(" ") { method_list_string.push_str_ln(&x); } + } - method_list_string.push_str_ln( - format!(" [DllImport(__DllName, EntryPoint = \"{entry_point}\", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]").as_str(), - ); - if return_type == "bool" { - method_list_string.push_str_ln(" [return: MarshalAs(UnmanagedType.U1)]"); + method_list_string.push_str_ln( + format!(" [DllImport(__DllName, EntryPoint = \"{entry_point}\", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]").as_str(), + ); + if return_type == "bool" { + method_list_string.push_str_ln(" [return: MarshalAs(UnmanagedType.U1)]"); + } + method_list_string.push_str_ln( + format!(" {accessibility} static extern {return_type} {method_prefix}{method_name}({parameters});").as_str(), + ); + method_list_string.push('\n'); +} + +fn emit_csharp_enums( + enums: &Vec, + options: &BindgenOptions, + accessibility: &String, +) -> String { + let mut enum_string = String::new(); + for item in enums { + let repr = match &item.repr { + Some(x) => format!(" : {}", convert_token_enum_repr(x)), + None => "".to_string(), + }; + let name = (options.csharp_type_rename)(escape_csharp_name(&item.enum_name)); + if item.is_flags { + enum_string.push_str_ln(" [Flags]"); } - method_list_string.push_str_ln( - format!(" {accessibility} static extern {return_type} {method_prefix}{method_name}({parameters});").as_str(), - ); - method_list_string.push('\n'); + enum_string.push_str_ln(format!(" {accessibility} enum {name}{repr}").as_str()); + enum_string.push_str_ln(" {"); + for (name, value) in &item.fields { + let value = match value { + Some(x) => format!(" = {x},"), + None => ",".to_string(), + }; + enum_string.push_str_ln(format!(" {name}{value}").as_str()); + } + enum_string.push_str_ln(" }"); + enum_string.push('\n'); } + enum_string +} +fn emit_csharp_structs( + aliases: &AliasMap, + structs: &Vec, + options: &BindgenOptions, + accessibility: &String, +) -> String { let mut structs_string = String::new(); for item in structs { let name = (options.csharp_type_rename)(escape_csharp_name(&item.struct_name)); @@ -272,30 +708,15 @@ pub fn emit_csharp( structs_string.push_str_ln(" }"); structs_string.push('\n'); } + structs_string +} - let mut enum_string = String::new(); - for item in enums { - let repr = match &item.repr { - Some(x) => format!(" : {}", convert_token_enum_repr(x)), - None => "".to_string(), - }; - let name = (options.csharp_type_rename)(escape_csharp_name(&item.enum_name)); - if item.is_flags { - enum_string.push_str_ln(" [Flags]"); - } - enum_string.push_str_ln(format!(" {accessibility} enum {name}{repr}").as_str()); - enum_string.push_str_ln(" {"); - for (name, value) in &item.fields { - let value = match value { - Some(x) => format!(" = {x},"), - None => ",".to_string(), - }; - enum_string.push_str_ln(format!(" {name}{value}").as_str()); - } - enum_string.push_str_ln(" }"); - enum_string.push('\n'); - } - +fn emit_csharp_consts( + aliases: &AliasMap, + consts: &Vec, + options: &BindgenOptions, + accessibility: &String, +) -> String { let mut const_string: String = String::new(); for item in consts { let mut type_name = item.rust_type.to_csharp_string( @@ -342,57 +763,7 @@ pub fn emit_csharp( ); } } - - // use empty string if the generated class is empty. - let class_string = if method_list_string.is_empty() && const_string.is_empty() { - String::new() - } else { - format!( - "{accessibility} static unsafe partial class {class_name} - {{ -{dll_name} - -{const_string} - -{method_list_string} - }}" - ) - }; - - let file_header = if options.csharp_file_header.len() > 0 { - options.csharp_file_header.to_string() + "\n" - } else { - "".to_string() - } + "// -// This code is generated by csbindgen. -// DON'T CHANGE THIS DIRECTLY. -// -#pragma warning disable CS8500 -#pragma warning disable CS8981"; - let file_footer = &options.csharp_file_footer; - - let mut imported_namespaces = String::new(); - for name in &options.csharp_imported_namespaces { - imported_namespaces.push_str_ln(format!("using {name};").as_str()); - } - - let result = format!( - "{file_header} -using System; -using System.Runtime.InteropServices; -{imported_namespaces} - -namespace {namespace} -{{ - {class_string} - -{structs_string} -{enum_string} -}} -{file_footer}" - ); - - result + const_string } fn convert_token_enum_repr(repr: &str) -> &str { diff --git a/csbindgen/src/lib.rs b/csbindgen/src/lib.rs index b970461..44c8f2f 100644 --- a/csbindgen/src/lib.rs +++ b/csbindgen/src/lib.rs @@ -14,7 +14,7 @@ use emitter::*; use field_map::FieldMap; use parser::*; use std::{collections::HashSet, error::Error}; -use type_meta::{ExternMethod, RustEnum, RustStruct, RustType, RustConst}; +use type_meta::{ExternMethod, RustConst, RustEnum, RustStruct, RustType}; enum GenerateKind { InputBindgen, @@ -49,8 +49,7 @@ pub(crate) fn generate( collect_struct(&file_ast, &mut structs); collect_enum(&file_ast, &mut enums); - collect_const(&file_ast, &mut consts,options.csharp_generate_const_filter); - + collect_const(&file_ast, &mut consts, options.csharp_generate_const_filter); } // collect using_types @@ -203,7 +202,7 @@ mod tests { } fn compare_and_delete_files(original_file_path: &str, generated_file_path: &str) { - let original = fs::read_to_string(original_file_path) + let original = fs::read_to_string(original_file_path) .expect("Should have been able to read original file"); let generated = fs::read_to_string(generated_file_path)