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,
}