diff --git a/csbindgen-tests/src/field_casing.rs b/csbindgen-tests/src/field_casing.rs new file mode 100644 index 0000000..1e13fa1 --- /dev/null +++ b/csbindgen-tests/src/field_casing.rs @@ -0,0 +1,4 @@ +#[repr(C)] +pub struct GeneratedStructFieldCasing { + pub this_is_snake_case: u32, +} diff --git a/csbindgen-tests/src/lib.rs b/csbindgen-tests/src/lib.rs index e2f3acf..2b85fc5 100644 --- a/csbindgen-tests/src/lib.rs +++ b/csbindgen-tests/src/lib.rs @@ -3,6 +3,7 @@ use std::{ }; mod counter; +mod field_casing; #[allow(dead_code)] #[allow(non_snake_case)] @@ -719,4 +720,4 @@ pub enum CResultStatus { #[no_mangle] pub extern "C" fn enum_test2(status: CResultStatus) -> i32 { status as i32 -} \ No newline at end of file +} diff --git a/csbindgen/Cargo.toml b/csbindgen/Cargo.toml index 39b7829..9dac1d9 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.11.0" diff --git a/csbindgen/src/builder.rs b/csbindgen/src/builder.rs index 7a062d4..97f1105 100644 --- a/csbindgen/src/builder.rs +++ b/csbindgen/src/builder.rs @@ -6,7 +6,7 @@ use std::{ path::Path, }; use std::convert::identity; - +use crate::Case; use crate::{generate, GenerateKind}; pub struct Builder { @@ -35,6 +35,7 @@ pub struct BindgenOptions { pub csharp_type_rename: fn(type_name: String) -> String, pub csharp_file_header: String, pub csharp_file_footer: String, + pub csharp_field_casing: Option>, pub always_included_types: Vec, } @@ -63,6 +64,7 @@ impl Default for Builder { csharp_type_rename: identity, csharp_file_header: "".to_string(), csharp_file_footer: "".to_string(), + csharp_field_casing: None, always_included_types: vec![], }, } @@ -238,6 +240,13 @@ impl Builder { self } + /// configure the casing of the generated C# field names, default is to use the input field + /// names verbatim (ie. most likely snake_case) + pub fn csharp_field_casing(mut self, field_casing: Case<'static>) -> Builder { + self.options.csharp_field_casing = Some(field_casing); + self + } + pub fn generate_csharp_file>( &self, csharp_output_path: P, diff --git a/csbindgen/src/lib.rs b/csbindgen/src/lib.rs index 3a7cef5..550b60f 100644 --- a/csbindgen/src/lib.rs +++ b/csbindgen/src/lib.rs @@ -17,6 +17,8 @@ use parser::*; use std::{collections::HashSet, error::Error}; use type_meta::{ExternMethod, RustConst, RustEnum, RustStruct, RustType}; +pub use convert_case::Case; + enum GenerateKind { InputBindgen, InputExtern, @@ -39,7 +41,7 @@ pub(crate) fn generate( for path in paths { let file_content = std::fs::read_to_string(path) - .unwrap_or_else(|_| panic!("input file not found, path: {}", path.display())); + .unwrap_or_else(|_| panic!("input file not found, path: {}", std::path::absolute(path).unwrap().display())); let file_ast = syn::parse_file(file_content.as_str())?; match generate_kind { @@ -47,7 +49,7 @@ pub(crate) fn generate( GenerateKind::InputExtern => collect_extern_method(&file_ast, options, &mut methods), }; collect_type_alias(&file_ast, &mut aliases); - collect_struct(&file_ast, &mut structs); + collect_struct(&file_ast, options, &mut structs); collect_enum(&file_ast, &mut enums); collect_const(&file_ast, &mut consts, options.csharp_generate_const_filter); @@ -159,15 +161,17 @@ mod tests { let path = std::env::current_dir().unwrap(); println!("starting dir: {}", path.display()); // csbindgen/csbindgen - std::env::set_current_dir(path.parent().unwrap()).unwrap(); + // NOTE: no longer changing the cwd here since it causes race conditions with other tests + // running at the same time + // std::env::set_current_dir(path.parent().unwrap()).unwrap(); Builder::new() - .input_bindgen_file("csbindgen-tests/src/lz4.rs") + .input_bindgen_file("../csbindgen-tests/src/lz4.rs") .csharp_class_name("LibLz4") .csharp_dll_name("csbindgen_tests") .generate_to_file( - "csbindgen-tests/src/lz4_ffi.rs", - "dotnet-sandbox/lz4_bindgen.cs", + "../csbindgen-tests/src/lz4_ffi.rs", + "../dotnet-sandbox/lz4_bindgen.cs", ) .unwrap(); } @@ -202,6 +206,41 @@ mod tests { file.flush().unwrap(); } + #[test] + fn field_casing_original() { + let original_file_path = "../dotnet-sandbox/field_casing_original.cs"; + let generated_file_path = "../dotnet-sandbox/field_casing_bindgen.cs"; + + Builder::new() + .always_included_types(["GeneratedStructFieldCasing"]) + .input_bindgen_file("../csbindgen-tests/src/field_casing.rs") + .generate_csharp_file(generated_file_path) + .unwrap(); + + compare_and_delete_files( + original_file_path, + generated_file_path, + ); + } + + #[test] + fn field_casing_upper_camel() { + let original_file_path = "../dotnet-sandbox/field_casing_original_upper_camel.cs"; + let generated_file_path = "../dotnet-sandbox/field_casing_bindgen_upper_camel.cs"; + + Builder::new() + .always_included_types(["GeneratedStructFieldCasing"]) + .input_bindgen_file("../csbindgen-tests/src/field_casing.rs") + .csharp_field_casing(Case::UpperCamel) + .generate_csharp_file(generated_file_path) + .unwrap(); + + compare_and_delete_files( + original_file_path, + generated_file_path, + ); + } + fn compare_and_delete_files(original_file_path: &str, generated_file_path: &str) { let original = fs::read_to_string(original_file_path) .expect("Should have been able to read original file"); @@ -216,15 +255,15 @@ mod tests { // #[test] // fn test_emit_without_class() { - // let generated_file_path = "dotnet-sandbox/only_enums_and_structs_bindgen.cs"; + // let generated_file_path = "../dotnet-sandbox/only_enums_and_structs_bindgen.cs"; // Builder::new() // .always_included_types(["Vec3", "Foo"]) - // .input_bindgen_file("csbindgen-tests/src/only_enums_and_structs.rs") + // .input_bindgen_file("../csbindgen-tests/src/only_enums_and_structs.rs") // .generate_csharp_file(generated_file_path) // .unwrap(); // compare_and_delete_files( - // "dotnet-sandbox/only_enums_and_structs_original.cs", + // "../dotnet-sandbox/only_enums_and_structs_original.cs", // generated_file_path, // ); // } diff --git a/csbindgen/src/parser.rs b/csbindgen/src/parser.rs index 8ea6c16..266256e 100644 --- a/csbindgen/src/parser.rs +++ b/csbindgen/src/parser.rs @@ -4,6 +4,7 @@ use crate::util::get_str_from_meta; use crate::{alias_map::AliasMap, builder::BindgenOptions, field_map::FieldMap, type_meta::*}; use regex::Regex; use std::collections::HashSet; +use convert_case::Casing; use syn::{ForeignItem, Item, Pat, ReturnType}; enum FnItem { @@ -268,12 +269,12 @@ pub fn collect_type_alias(ast: &syn::File, result: &mut AliasMap) { } } -pub fn collect_struct(ast: &syn::File, result: &mut Vec) { +pub fn collect_struct(ast: &syn::File, options: &BindgenOptions, result: &mut Vec) { // collect union or struct for item in depth_first_module_walk(&ast.items) { if let Item::Union(t) = item { let struct_name = t.ident.to_string(); - let fields = collect_fields(&t.fields); + let fields = collect_fields(&t.fields, options); result.push(RustStruct { struct_name, @@ -294,7 +295,7 @@ pub fn collect_struct(ast: &syn::File, result: &mut Vec) { if repr { if let syn::Fields::Named(f) = &t.fields { let struct_name = t.ident.to_string(); - let fields = collect_fields(f); + let fields = collect_fields(f, options); result.push(RustStruct { struct_name, fields, @@ -335,14 +336,19 @@ pub fn collect_struct(ast: &syn::File, result: &mut Vec) { } } -fn collect_fields(fields: &syn::FieldsNamed) -> Vec { +fn collect_fields(fields: &syn::FieldsNamed, options: &BindgenOptions) -> Vec { let mut result = Vec::new(); for field in &fields.named { if let Some(x) = &field.ident { + let name = match options.csharp_field_casing { + Some(case) => x.to_string().to_case(case), + None => x.to_string(), + }; + let t = parse_type(&field.ty); result.push(FieldMember { - name: x.to_string(), + name, rust_type: t, doc_comment: gather_docs(&field.attrs), }); diff --git a/dotnet-sandbox/field_casing_original.cs b/dotnet-sandbox/field_casing_original.cs new file mode 100644 index 0000000..319974c --- /dev/null +++ b/dotnet-sandbox/field_casing_original.cs @@ -0,0 +1,23 @@ +// +// This code is generated by csbindgen. +// DON'T CHANGE THIS DIRECTLY. +// +#pragma warning disable CS8500 +#pragma warning disable CS8981 +using System; +using System.Runtime.InteropServices; + + +namespace CsBindgen +{ + + + [StructLayout(LayoutKind.Sequential)] + internal unsafe partial struct GeneratedStructFieldCasing + { + public uint this_is_snake_case; + } + + + +} diff --git a/dotnet-sandbox/field_casing_original_upper_camel.cs b/dotnet-sandbox/field_casing_original_upper_camel.cs new file mode 100644 index 0000000..bcf2f02 --- /dev/null +++ b/dotnet-sandbox/field_casing_original_upper_camel.cs @@ -0,0 +1,23 @@ +// +// This code is generated by csbindgen. +// DON'T CHANGE THIS DIRECTLY. +// +#pragma warning disable CS8500 +#pragma warning disable CS8981 +using System; +using System.Runtime.InteropServices; + + +namespace CsBindgen +{ + + + [StructLayout(LayoutKind.Sequential)] + internal unsafe partial struct GeneratedStructFieldCasing + { + public uint ThisIsSnakeCase; + } + + + +}