From 2ead1f767f62d6ced53be35d6e37c1db689e2437 Mon Sep 17 00:00:00 2001 From: Brandon Grimshaw Date: Fri, 6 Jun 2025 19:26:43 +1000 Subject: [PATCH 1/7] Convert field names to UpperCamelCase --- csbindgen/Cargo.toml | 1 + csbindgen/src/parser.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/csbindgen/Cargo.toml b/csbindgen/Cargo.toml index 39b7829..ec47f97 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.8.0" diff --git a/csbindgen/src/parser.rs b/csbindgen/src/parser.rs index 8ea6c16..9dccd1d 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::{Case, Casing}; use syn::{ForeignItem, Item, Pat, ReturnType}; enum FnItem { @@ -342,7 +343,7 @@ fn collect_fields(fields: &syn::FieldsNamed) -> Vec { if let Some(x) = &field.ident { let t = parse_type(&field.ty); result.push(FieldMember { - name: x.to_string(), + name: x.to_string().to_case(Case::UpperCamel), rust_type: t, doc_comment: gather_docs(&field.attrs), }); From e9d8cb2e74b08751410f0158949e33cbe2641de4 Mon Sep 17 00:00:00 2001 From: Brandon Grimshaw Date: Mon, 4 May 2026 15:31:47 +1000 Subject: [PATCH 2/7] Make field casing configurable --- csbindgen/src/builder.rs | 11 ++++++++++- csbindgen/src/lib.rs | 4 +++- csbindgen/src/parser.rs | 15 ++++++++++----- 3 files changed, 23 insertions(+), 7 deletions(-) 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..b6258c0 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, @@ -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); diff --git a/csbindgen/src/parser.rs b/csbindgen/src/parser.rs index 9dccd1d..d06d408 100644 --- a/csbindgen/src/parser.rs +++ b/csbindgen/src/parser.rs @@ -269,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, @@ -295,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, @@ -336,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().to_case(Case::UpperCamel), + name, rust_type: t, doc_comment: gather_docs(&field.attrs), }); From 3672b0ae140ab48f81b93cbb970e562720620b8f Mon Sep 17 00:00:00 2001 From: Brandon Grimshaw Date: Mon, 4 May 2026 15:59:19 +1000 Subject: [PATCH 3/7] Fix unused use statement warning --- csbindgen/src/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csbindgen/src/parser.rs b/csbindgen/src/parser.rs index d06d408..266256e 100644 --- a/csbindgen/src/parser.rs +++ b/csbindgen/src/parser.rs @@ -4,7 +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::{Case, Casing}; +use convert_case::Casing; use syn::{ForeignItem, Item, Pat, ReturnType}; enum FnItem { From c15975003c1bfea22491bd8cb30db7b4f3531699 Mon Sep 17 00:00:00 2001 From: Brandon Grimshaw Date: Mon, 4 May 2026 16:42:11 +1000 Subject: [PATCH 4/7] Add tests for generated field casing Very barebones, only tests one non-original case type. --- csbindgen-tests/src/field_casing.rs | 4 ++ csbindgen/src/lib.rs | 53 ++++++++++++++++--- dotnet-sandbox/field_casing_original.cs | 23 ++++++++ .../field_casing_original_upper_camel.cs | 23 ++++++++ 4 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 csbindgen-tests/src/field_casing.rs create mode 100644 dotnet-sandbox/field_casing_original.cs create mode 100644 dotnet-sandbox/field_casing_original_upper_camel.cs diff --git a/csbindgen-tests/src/field_casing.rs b/csbindgen-tests/src/field_casing.rs new file mode 100644 index 0000000..c3d2c61 --- /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/src/lib.rs b/csbindgen/src/lib.rs index b6258c0..550b60f 100644 --- a/csbindgen/src/lib.rs +++ b/csbindgen/src/lib.rs @@ -41,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 { @@ -161,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(); } @@ -204,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"); @@ -218,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/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; + } + + + +} From 410ca3ce86a3454e6fc6fa498279b57dcbe07b85 Mon Sep 17 00:00:00 2001 From: Brandon Grimshaw Date: Mon, 4 May 2026 16:42:25 +1000 Subject: [PATCH 5/7] Update `convert_case` to v0.11.0 --- csbindgen/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csbindgen/Cargo.toml b/csbindgen/Cargo.toml index ec47f97..9dac1d9 100644 --- a/csbindgen/Cargo.toml +++ b/csbindgen/Cargo.toml @@ -14,4 +14,4 @@ repository = "https://github.com/Cysharp/csbindgen/" [dependencies] syn = { version = "2.0.68", features = ["full", "parsing"] } regex = "1.10.5" -convert_case = "0.8.0" +convert_case = "0.11.0" From 5ae43d7be50b9745eb6c92f332b7cb5aa5ddd681 Mon Sep 17 00:00:00 2001 From: Brandon Grimshaw Date: Mon, 4 May 2026 16:51:41 +1000 Subject: [PATCH 6/7] Add missing change --- csbindgen-tests/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 +} From 7dac9d0128e56c4beb6328008c9c3d21676ca95b Mon Sep 17 00:00:00 2001 From: Brandon Grimshaw Date: Mon, 4 May 2026 16:52:07 +1000 Subject: [PATCH 7/7] Normalise indentation in field casing test input --- csbindgen-tests/src/field_casing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csbindgen-tests/src/field_casing.rs b/csbindgen-tests/src/field_casing.rs index c3d2c61..1e13fa1 100644 --- a/csbindgen-tests/src/field_casing.rs +++ b/csbindgen-tests/src/field_casing.rs @@ -1,4 +1,4 @@ #[repr(C)] pub struct GeneratedStructFieldCasing { - pub this_is_snake_case: u32, + pub this_is_snake_case: u32, }