cs-binding-generator 1.0.1.dev21__tar.gz → 1.0.1.dev35__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/.gitignore +3 -0
  2. cs_binding_generator-1.0.1.dev35/COPILOT_CONTEXT.md +149 -0
  3. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/PKG-INFO +1 -1
  4. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/cs_binding_generator/_version.py +3 -3
  5. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/cs_binding_generator/code_generators.py +79 -7
  6. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/cs_binding_generator/config.py +51 -34
  7. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/cs_binding_generator/generator.py +47 -2
  8. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/cs_binding_generator/main.py +22 -33
  9. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/cs_binding_generator/type_mapper.py +69 -3
  10. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/cs_binding_generator.egg-info/PKG-INFO +1 -1
  11. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/cs_binding_generator.egg-info/SOURCES.txt +3 -0
  12. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/test_code_generators.py +2 -0
  13. cs_binding_generator-1.0.1.dev35/tests/test_defines.py +247 -0
  14. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/test_edge_cases.py +25 -25
  15. cs_binding_generator-1.0.1.dev35/tests/test_flag_enums.py +280 -0
  16. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/test_generator.py +111 -0
  17. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/test_multi_file_deduplication.py +5 -5
  18. cs_binding_generator-1.0.1.dev35/tests/test_opaque_typedef_underlying.py +32 -0
  19. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/test_removal.py +34 -41
  20. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/test_renaming.py +33 -33
  21. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/test_type_mapper.py +70 -2
  22. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/test_xml_config.py +79 -79
  23. cs_binding_generator-1.0.1.dev21/COPILOT_CONTEXT.md +0 -439
  24. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/.coverage +0 -0
  25. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/.flake8 +0 -0
  26. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/.github/workflows/publish.yml +0 -0
  27. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/LICENSE +0 -0
  28. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/README.md +0 -0
  29. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/cs_binding_generator/__init__.py +0 -0
  30. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/cs_binding_generator/constants.py +0 -0
  31. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/cs_binding_generator.egg-info/dependency_links.txt +0 -0
  32. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/cs_binding_generator.egg-info/entry_points.txt +0 -0
  33. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/cs_binding_generator.egg-info/requires.txt +0 -0
  34. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/cs_binding_generator.egg-info/top_level.txt +0 -0
  35. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/docs/ARCHITECTURE.md +0 -0
  36. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/docs/INCLUDE_DIRECTORIES.md +0 -0
  37. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/docs/MULTI_FILE_OUTPUT.md +0 -0
  38. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/docs/RENAMING_EXAMPLE.xml +0 -0
  39. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/docs/TROUBLESHOOTING.md +0 -0
  40. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/docs/XML_CONFIG.md +0 -0
  41. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/enter_devenv.sh +0 -0
  42. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/pyproject.toml +0 -0
  43. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/run_tests.sh +0 -0
  44. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/setup.cfg +0 -0
  45. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/test_dotnet/FreeTypeTest/FreeTypeTest.csproj +0 -0
  46. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/test_dotnet/FreeTypeTest/bindings.cs +0 -0
  47. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/test_dotnet/FreeTypeTest/cs-bindings.xml +0 -0
  48. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/test_dotnet/FreeTypeTest/freetype.cs +0 -0
  49. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/test_dotnet/LibtcodTest/LibtcodTest.csproj +0 -0
  50. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/test_dotnet/LibtcodTest/SDL3.cs +0 -0
  51. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/test_dotnet/LibtcodTest/bindings.cs +0 -0
  52. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/test_dotnet/LibtcodTest/cs-bindings.xml +0 -0
  53. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/test_dotnet/LibtcodTest/libtcod.cs +0 -0
  54. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/test_dotnet/SDL3Test/SDL3.cs +0 -0
  55. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/test_dotnet/SDL3Test/SDL3Test.csproj +0 -0
  56. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/test_dotnet/SDL3Test/SDL3Test.sln +0 -0
  57. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/test_dotnet/SDL3Test/bindings.cs +0 -0
  58. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/test_dotnet/SDL3Test/cs-bindings.xml +0 -0
  59. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/__init__.py +0 -0
  60. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/conftest.py +0 -0
  61. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/fixtures.py +0 -0
  62. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/test_cli.py +0 -0
  63. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/test_cli_extended.py +0 -0
  64. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/test_error_handling.py +0 -0
  65. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/test_type_mapping_extended.py +0 -0
  66. {cs_binding_generator-1.0.1.dev21 → cs_binding_generator-1.0.1.dev35}/tests/test_variadic_functions.py +0 -0
@@ -7,3 +7,6 @@ obj/
7
7
  dist/
8
8
  build/
9
9
  _version.py
10
+
11
+ # Local test output
12
+ out_test/
@@ -0,0 +1,149 @@
1
+ # CsBindingGenerator - AI Assistant Context
2
+
3
+ **PURPOSE**: Essential context for AI assistants. For change history, see `git log`.
4
+
5
+ ## Project Overview
6
+
7
+ Generates C# P/Invoke bindings from C headers using libclang. Modern LibraryImport attributes, multi-file output, regex renames.
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ C Headers → libclang → Generator → TypeMapper → CodeGenerator → C# Output
13
+ ```
14
+
15
+ - **Generator** (`generator.py`): Parses C headers, manages deduplication, filters system headers
16
+ - **TypeMapper** (`type_mapper.py`): Maps C→C# types, applies renames, context-aware (is_struct_field, is_return_type)
17
+ - **CodeGenerator** (`code_generators.py`): Generates C# code, handles anonymous unions/structs, applies attributes
18
+ - **Config** (`config.py`): Parses XML configuration
19
+ - **Main** (`main.py`): CLI entry point (minimal args - most config in XML)
20
+
21
+ ## Critical Concepts
22
+
23
+ ### Deduplication (Multi-File Mode - Always Enabled)
24
+
25
+ Global deduplication by name only:
26
+ - Functions: First library wins by `cursor.spelling`
27
+ - Structs/Unions: First library wins by `(name, file, line)`
28
+
29
+ **CRITICAL**: Library order in XML matters - foundation libraries first.
30
+
31
+ ### Type Mapping Context Awareness
32
+
33
+ - **`is_struct_field=True`**: Maps C `bool` → C# `byte` (avoids CS8500 managed type errors)
34
+ - **`is_return_type=True`**: Affects pointer type mapping
35
+
36
+ ### Anonymous Unions/Structs
37
+
38
+ - Detected by "anonymous" in `cursor.spelling`
39
+ - NOT generated as standalone types
40
+ - Members flattened into parent struct
41
+ - Use `cursor.type.get_offset(field_name)` for parent-relative offsets (returns bits ÷ 8 = bytes)
42
+
43
+ ### Variadic Functions
44
+
45
+ - **Default**: `...` parameter omitted (works with calling conventions, may cause stack issues)
46
+ - **`--use-variadic` flag**: Uses DllImport + `__arglist` (non-AOT compatible)
47
+
48
+ ### Rename Rules
49
+
50
+ Stored as list: `[(pattern, replacement, is_regex), ...]`
51
+ - Applied in order, first match wins
52
+ - User writes `$1`, `$2` (converted to `\1`, `\2` internally)
53
+ - Uses `re.fullmatch()` for precise matching
54
+ - Post-processing uses word boundaries
55
+
56
+ **Conflict resolution**: Place specific rules before general ones:
57
+ ```xml
58
+ <rename from="SDL_strcasecmp" to="SDL_strcasecmp"/> <!-- Keep -->
59
+ <rename from="SDL_(.*)" to="$1" regex="true"/> <!-- Strip rest -->
60
+ ```
61
+
62
+ ### System Header Filtering
63
+
64
+ `_is_system_header()` filters:
65
+ - `/usr/include/` direct files (no subdirectory)
66
+ - `/usr/include/{sys,bits,etc}/` subdirectories
67
+ - C standard library headers (c_std_headers set)
68
+ - `/usr/include/c++` paths
69
+
70
+ ## XML Configuration
71
+
72
+ ```xml
73
+ <bindings visibility="internal">
74
+ <include_directory path="/custom/path"/>
75
+ <rename from="PREFIX_(.*)" to="$1" regex="true"/>
76
+ <remove pattern="DEPRECATED_.*" regex="true"/>
77
+ <constants name="Flags" pattern="FLAG_.*" type="uint" flags="true"/>
78
+
79
+ <library name="libname" namespace="Namespace" class="ClassName">
80
+ <using namespace="System.Runtime.CompilerServices"/>
81
+ <include file="/path/to/header.h"/>
82
+ </library>
83
+ </bindings>
84
+ ```
85
+
86
+ **Key attributes**:
87
+ - `<bindings>`: `visibility="public|internal"`
88
+ - `<rename>`: `from`, `to`, `regex="true|false"`
89
+ - `<remove>`: `pattern`, `regex="true|false"`
90
+ - `<constants>`: `name`, `pattern`, `type`, `flags="true|false"`
91
+ - `<library>`: `name`, `namespace`, `class` (default: "NativeMethods")
92
+ - `<using>`: `namespace`
93
+ - `<include>`: `file`
94
+
95
+ **Note**: Don't specify `/usr/include` - clang auto-detects it.
96
+
97
+ ## CLI Arguments (Limited by Design)
98
+
99
+ - `-C/--config`: XML config file (default: cs-bindings.xml)
100
+ - `-o/--output`: Output directory (default: current)
101
+ - `--ignore-missing`: Continue if headers missing
102
+ - `--use-variadic`: Generate variadic functions with __arglist
103
+ - `--clang-path`: Path to libclang
104
+
105
+ ## Known Edge Cases
106
+
107
+ - **Duplicate symbols**: Ensure library order in XML (foundation first) for global deduplication
108
+ - **Bool in structs**: Type mapper automatically converts bool→byte when `is_struct_field=True` to avoid managed type errors
109
+ - **System header leakage**: `_is_system_header()` must filter direct `/usr/include/` files and subdirectories
110
+ - **Anonymous unions**: Must detect "anonymous" in `cursor.spelling` and flatten members with parent-relative offsets
111
+ - **Incomplete arrays**: Handle `TypeKind.INCOMPLETEARRAY` → `nuint` for char*, `T*` for others
112
+
113
+ ## libclang Key Concepts
114
+
115
+ - `cursor.spelling`: Entity name
116
+ - `cursor.location.file`: Source file
117
+ - `cursor.is_definition()`: True if definition (not declaration)
118
+ - `cursor.type.get_offset(field)`: Offset in **bits** (÷ 8 for bytes)
119
+ - `TypeKind.INCOMPLETEARRAY`: Array parameters like `items[]`
120
+
121
+ ## Testing
122
+
123
+ Run: `source enter_devenv.sh && ./run_tests.sh`
124
+ - pytest 9.0.2, Python 3.13.11
125
+ - Current: 179 tests
126
+ - Test with real C code using `temp_header_file` fixture
127
+ - Verify struct offsets in bytes
128
+
129
+ ## Development Workflow
130
+
131
+ 1. Source env: `source enter_devenv.sh`
132
+ 2. Make changes
133
+ 3. Run: `./run_tests.sh`
134
+ 4. Test projects in `test_dotnet/`: SDL3Test, LibtcodTest, FreeTypeTest
135
+ 5. Each has `regenerate_bindings.sh` + `dotnet build`
136
+
137
+ ## User Interaction Rules
138
+
139
+ 1. Never assume pre-existing bugs
140
+ 2. Don't try random fixes - ask if uncertain
141
+ 3. Read error messages carefully
142
+ 4. Check actual implementation (e.g., argparse in main.py) before documenting
143
+ 5. Update this file only for architectural insights, not change history
144
+
145
+ ---
146
+
147
+ **For change history: `git log`**
148
+
149
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cs_binding_generator
3
- Version: 1.0.1.dev21
3
+ Version: 1.0.1.dev35
4
4
  Summary: Generate C# bindings from C headers using libclang with modern LibraryImport
5
5
  Author-email: Robin 'Ruadeil' Degen <mail@ruadeil.lgbt>
6
6
  License-Expression: MIT
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.0.1.dev21'
32
- __version_tuple__ = version_tuple = (1, 0, 1, 'dev21')
31
+ __version__ = version = '1.0.1.dev35'
32
+ __version_tuple__ = version_tuple = (1, 0, 1, 'dev35')
33
33
 
34
- __commit_id__ = commit_id = 'gcee9644ab'
34
+ __commit_id__ = commit_id = 'g0db8f9507'
@@ -187,11 +187,16 @@ class CodeGenerator:
187
187
  # Add helper function for struct return types (skip for variadic functions)
188
188
  if is_struct_return and not is_variadic:
189
189
  # Build parameters for helper (same as original)
190
+ # Escape parameter names for the function call
191
+ param_names = [self._escape_keyword(arg.spelling) if arg.spelling else f"param{i}"
192
+ for i, arg in enumerate(cursor.get_arguments())]
193
+ params_call_str = ", ".join(param_names) if param_names else ""
194
+
190
195
  code += f"""
191
196
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
192
197
  {self.visibility} static unsafe {struct_return_type} {func_name}Struct({params_str})
193
198
  {{
194
- var ptr = {func_name}({", ".join(arg.spelling or f"param{i}" for i, arg in enumerate(cursor.get_arguments())) if cursor.get_arguments() else ""});
199
+ var ptr = {func_name}({params_call_str});
195
200
  return Marshal.PtrToStructure<{struct_return_type}>((nint)ptr);
196
201
  }}
197
202
  """
@@ -237,10 +242,69 @@ class CodeGenerator:
237
242
  # Collect fields with their offsets
238
243
  fields = []
239
244
  for field in cursor.get_children():
245
+ # Handle anonymous unions/structs by flattening their members into the parent struct
246
+ if field.kind == CursorKind.UNION_DECL and field.spelling and "anonymous" in field.spelling.lower() and field.is_definition():
247
+ # This is an anonymous union - flatten its members into the struct
248
+ for union_member in field.get_children():
249
+ if union_member.kind == CursorKind.FIELD_DECL:
250
+ member_name = union_member.spelling
251
+ if not member_name:
252
+ continue
253
+
254
+ # Escape C# keywords
255
+ member_name = self._escape_keyword(member_name)
256
+
257
+ # Get the offset using parent struct's type.get_offset()
258
+ # This gives us the actual offset in the parent struct, not relative to the union
259
+ try:
260
+ offset_bits = cursor.type.get_offset(union_member.spelling)
261
+ offset_bytes = offset_bits // 8
262
+ except Exception:
263
+ # Fallback to field offset (will be wrong for anonymous unions)
264
+ offset_bits = union_member.get_field_offsetof()
265
+ offset_bytes = offset_bits // 8
266
+
267
+ # Map the type
268
+ member_type = self.type_mapper.map_type(union_member.type, is_struct_field=True)
269
+ if not member_type or "unnamed" in member_type or "::" in member_type:
270
+ continue
271
+
272
+ fields.append(f" [FieldOffset({offset_bytes})]\n {self.visibility} {member_type} {member_name};")
273
+ continue
274
+
275
+ # Handle anonymous structs by flattening their members
276
+ if field.kind == CursorKind.STRUCT_DECL and field.spelling and "anonymous" in field.spelling.lower() and field.is_definition():
277
+ # This is an anonymous struct - flatten its members into the parent struct
278
+ for struct_member in field.get_children():
279
+ if struct_member.kind == CursorKind.FIELD_DECL:
280
+ member_name = struct_member.spelling
281
+ if not member_name:
282
+ continue
283
+
284
+ # Escape C# keywords
285
+ member_name = self._escape_keyword(member_name)
286
+
287
+ # Get the offset using parent struct's type.get_offset()
288
+ try:
289
+ offset_bits = cursor.type.get_offset(struct_member.spelling)
290
+ offset_bytes = offset_bits // 8
291
+ except Exception:
292
+ # Fallback to field offset
293
+ offset_bits = struct_member.get_field_offsetof()
294
+ offset_bytes = offset_bits // 8
295
+
296
+ # Map the type
297
+ member_type = self.type_mapper.map_type(struct_member.type, is_struct_field=True)
298
+ if not member_type or "unnamed" in member_type or "::" in member_type:
299
+ continue
300
+
301
+ fields.append(f" [FieldOffset({offset_bytes})]\n {self.visibility} {member_type} {member_name};")
302
+ continue
303
+
240
304
  if field.kind == CursorKind.FIELD_DECL:
241
305
  field_name = field.spelling
242
306
 
243
- # Skip unnamed fields (anonymous unions/structs)
307
+ # Skip unnamed fields (anonymous unions/structs without definitions are already handled above)
244
308
  if not field_name:
245
309
  continue
246
310
 
@@ -255,7 +319,7 @@ class CodeGenerator:
255
319
  if field.type.kind == TypeKind.CONSTANTARRAY:
256
320
  element_type = field.type.get_array_element_type()
257
321
  array_size = field.type.get_array_size()
258
- element_csharp = self.type_mapper.map_type(element_type)
322
+ element_csharp = self.type_mapper.map_type(element_type, is_struct_field=True)
259
323
 
260
324
  # Skip if element type cannot be mapped
261
325
  if not element_csharp:
@@ -265,6 +329,7 @@ class CodeGenerator:
265
329
  element_size = element_type.get_size() # size in bytes
266
330
 
267
331
  # Check if element type is a primitive type (can use fixed keyword)
332
+ # Note: bool is excluded because fixed bool arrays make structs managed
268
333
  primitive_types = {
269
334
  "byte",
270
335
  "sbyte",
@@ -276,7 +341,6 @@ class CodeGenerator:
276
341
  "ulong",
277
342
  "float",
278
343
  "double",
279
- "bool",
280
344
  "char",
281
345
  }
282
346
 
@@ -355,7 +419,7 @@ class CodeGenerator:
355
419
  if field.type.kind == TypeKind.CONSTANTARRAY:
356
420
  element_type = field.type.get_array_element_type()
357
421
  array_size = field.type.get_array_size()
358
- element_csharp = self.type_mapper.map_type(element_type)
422
+ element_csharp = self.type_mapper.map_type(element_type, is_struct_field=True)
359
423
 
360
424
  # Skip if element type cannot be mapped
361
425
  if not element_csharp:
@@ -365,6 +429,7 @@ class CodeGenerator:
365
429
  element_size = element_type.get_size()
366
430
 
367
431
  # Check if element type is a primitive type (can use fixed keyword)
432
+ # Note: bool is excluded because fixed bool arrays make structs managed
368
433
  primitive_types = {
369
434
  "byte",
370
435
  "sbyte",
@@ -376,7 +441,6 @@ class CodeGenerator:
376
441
  "ulong",
377
442
  "float",
378
443
  "double",
379
- "bool",
380
444
  "char",
381
445
  }
382
446
 
@@ -555,9 +619,14 @@ class CodeGenerator:
555
619
  if csharp_type and csharp_type != "int":
556
620
  inheritance_clause = f" : {csharp_type}"
557
621
 
622
+ # Check if this enum should have [Flags] attribute
623
+ flags_attribute = ""
624
+ if self.type_mapper.is_flag_enum(enum_name):
625
+ flags_attribute = "[Flags]\n"
626
+
558
627
  values_str = "\n".join(values)
559
628
 
560
- code = f"""{self.visibility} enum {enum_name}{inheritance_clause}
629
+ code = f"""{flags_attribute}{self.visibility} enum {enum_name}{inheritance_clause}
561
630
  {{
562
631
  {values_str}
563
632
  }}
@@ -640,6 +709,7 @@ class OutputBuilder:
640
709
  argv_copy[0] = "cs_binding_generator"
641
710
  command_line = " ".join(argv_copy)
642
711
 
712
+ parts.append("// <auto-generated />")
643
713
  parts.append("//")
644
714
  parts.append("// This file was automatically generated by cs-binding-generator")
645
715
  parts.append("// https://github.com/cs-binding-generator/cs-binding-generator")
@@ -648,6 +718,8 @@ class OutputBuilder:
648
718
  parts.append("// Do not modify this file directly")
649
719
  parts.append("//")
650
720
  parts.append("")
721
+ parts.append("#nullable enable")
722
+ parts.append("")
651
723
 
652
724
  # Usings (non-global)
653
725
  from .constants import REQUIRED_USINGS
@@ -3,10 +3,27 @@ XML configuration file parsing for C# bindings generator
3
3
  """
4
4
 
5
5
  import xml.etree.ElementTree as ET
6
+ from dataclasses import dataclass, field
7
+
8
+
9
+ @dataclass
10
+ class BindingConfig:
11
+ """Configuration for C# bindings generation"""
12
+ header_library_pairs: list[tuple[str, str]] = field(default_factory=list)
13
+ include_dirs: list[str] = field(default_factory=list)
14
+ renames: list[tuple[str, str, bool]] = field(default_factory=list)
15
+ removals: list[tuple[str, bool]] = field(default_factory=list)
16
+ flag_enums: list[tuple[str, bool]] = field(default_factory=list)
17
+ library_class_names: dict[str, str] = field(default_factory=dict)
18
+ library_namespaces: dict[str, str] = field(default_factory=dict)
19
+ library_using_statements: dict[str, list[str]] = field(default_factory=dict)
20
+ visibility: str = "public"
21
+ global_constants: list[tuple[str, str, str, bool]] = field(default_factory=list)
22
+ global_defines: list[tuple[str, str | None]] = field(default_factory=list)
6
23
 
7
24
 
8
25
  def parse_config_file(config_path):
9
- """Parse XML configuration file and return header-library pairs, include directories, and renames"""
26
+ """Parse XML configuration file and return BindingConfig object"""
10
27
  try:
11
28
  tree = ET.parse(config_path)
12
29
  root = tree.getroot()
@@ -14,20 +31,13 @@ def parse_config_file(config_path):
14
31
  if root.tag != "bindings":
15
32
  raise ValueError(f"Expected root element 'bindings', got '{root.tag}'")
16
33
 
17
- header_library_pairs = []
18
- include_dirs = []
19
- renames = [] # Changed to list of (from, to, is_regex) tuples
20
- removals = [] # List of (pattern, is_regex) tuples
21
- library_class_names = {} # Map library name to class name
22
- library_namespaces = {} # Map library name to namespace
23
- library_using_statements = {} # Map library name to list of using statements
24
- library_constants = {} # Map library name to list of (name, pattern, type) tuples
34
+ config = BindingConfig()
25
35
 
26
36
  # Get global visibility setting (default to "public")
27
- visibility = root.get("visibility", "public").strip().lower()
28
- if visibility not in ("public", "internal"):
37
+ config.visibility = root.get("visibility", "public").strip().lower()
38
+ if config.visibility not in ("public", "internal"):
29
39
  import sys
30
- print(f"Error: Invalid visibility value '{visibility}'. Must be 'public' or 'internal'.", file=sys.stderr)
40
+ print(f"Error: Invalid visibility value '{config.visibility}'. Must be 'public' or 'internal'.", file=sys.stderr)
31
41
  sys.exit(1)
32
42
 
33
43
  # Get global include directories
@@ -35,7 +45,7 @@ def parse_config_file(config_path):
35
45
  path = include_dir.get("path")
36
46
  if not path:
37
47
  raise ValueError("Include directory element missing 'path' attribute")
38
- include_dirs.append(path.strip())
48
+ config.include_dirs.append(path.strip())
39
49
 
40
50
  # Get global renames (support both simple and regex)
41
51
  for rename in root.findall("rename"):
@@ -44,7 +54,7 @@ def parse_config_file(config_path):
44
54
  if not from_name or not to_name:
45
55
  raise ValueError("Rename element missing 'from' or 'to' attribute")
46
56
  is_regex = rename.get("regex", "false").lower() == "true"
47
- renames.append((from_name.strip(), to_name.strip(), is_regex))
57
+ config.renames.append((from_name.strip(), to_name.strip(), is_regex))
48
58
 
49
59
  # Get global removals (support both simple and regex)
50
60
  for remove in root.findall("remove"):
@@ -52,12 +62,29 @@ def parse_config_file(config_path):
52
62
  if not pattern:
53
63
  raise ValueError("Remove element missing 'pattern' attribute")
54
64
  is_regex = remove.get("regex", "false").lower() == "true"
55
- removals.append((pattern.strip(), is_regex))
65
+ config.removals.append((pattern.strip(), is_regex))
66
+
67
+ # Get global flag enums (enum patterns that should have [Flags] attribute)
68
+ for flag_enum in root.findall("flags"):
69
+ pattern = flag_enum.get("pattern")
70
+ if not pattern:
71
+ raise ValueError("Flags element missing 'pattern' attribute")
72
+ is_regex = flag_enum.get("regex", "false").lower() == "true"
73
+ config.flag_enums.append((pattern.strip(), is_regex))
74
+
75
+ # Get global compiler defines
76
+ for define in root.findall("define"):
77
+ name = define.get("name")
78
+ if not name:
79
+ raise ValueError("Define element missing 'name' attribute")
80
+ value = define.get("value") # Optional, can be None
81
+ if value is not None:
82
+ value = value.strip()
83
+ config.global_defines.append((name.strip(), value))
56
84
 
57
85
  # Get global constants (macros to extract)
58
86
  # These are stored as a list of (name, pattern, type, is_flags) tuples
59
87
  # They will be applied to all libraries during processing
60
- global_constants = []
61
88
  for const in root.findall("constants"):
62
89
  const_name = const.get("name")
63
90
  const_pattern = const.get("pattern")
@@ -69,7 +96,7 @@ def parse_config_file(config_path):
69
96
  if not const_pattern:
70
97
  raise ValueError("Constants element missing 'pattern' attribute")
71
98
 
72
- global_constants.append((const_name.strip(), const_pattern.strip(), const_type.strip(), const_flags))
99
+ config.global_constants.append((const_name.strip(), const_pattern.strip(), const_type.strip(), const_flags))
73
100
 
74
101
  for library in root.findall("library"):
75
102
  library_name = library.get("name")
@@ -78,12 +105,12 @@ def parse_config_file(config_path):
78
105
 
79
106
  # Get class name (default to NativeMethods if not specified)
80
107
  class_name = library.get("class", "NativeMethods")
81
- library_class_names[library_name.strip()] = class_name.strip()
108
+ config.library_class_names[library_name.strip()] = class_name.strip()
82
109
 
83
110
  # Get namespace from library attribute
84
111
  library_namespace = library.get("namespace")
85
112
  if library_namespace is not None:
86
- library_namespaces[library_name.strip()] = library_namespace.strip()
113
+ config.library_namespaces[library_name.strip()] = library_namespace.strip()
87
114
 
88
115
  # Get using statements
89
116
  using_statements = []
@@ -92,33 +119,23 @@ def parse_config_file(config_path):
92
119
  if using_namespace:
93
120
  using_statements.append(using_namespace.strip())
94
121
  if using_statements:
95
- library_using_statements[library_name.strip()] = using_statements
122
+ config.library_using_statements[library_name.strip()] = using_statements
96
123
 
97
124
  # Get library-specific include directories
98
125
  for include_dir in library.findall("include_directory"):
99
126
  path = include_dir.get("path")
100
127
  if not path:
101
128
  raise ValueError(f"Include directory element in library '{library_name}' missing 'path' attribute")
102
- include_dirs.append(path.strip())
129
+ config.include_dirs.append(path.strip())
103
130
 
104
131
  # Get include files
105
132
  for include in library.findall("include"):
106
133
  header_path = include.get("file")
107
134
  if not header_path:
108
135
  raise ValueError(f"Include element in library '{library_name}' missing 'file' attribute")
109
- header_library_pairs.append((header_path.strip(), library_name.strip()))
110
-
111
- return (
112
- header_library_pairs,
113
- include_dirs,
114
- renames,
115
- removals,
116
- library_class_names,
117
- library_namespaces,
118
- library_using_statements,
119
- visibility,
120
- global_constants,
121
- )
136
+ config.header_library_pairs.append((header_path.strip(), library_name.strip()))
137
+
138
+ return config
122
139
 
123
140
  except ET.ParseError as e:
124
141
  raise ValueError(f"XML parsing error: {e}")
@@ -265,6 +265,9 @@ class CSharpBindingsGenerator:
265
265
 
266
266
  elif cursor.kind == CursorKind.STRUCT_DECL:
267
267
  if cursor.is_definition():
268
+ # Skip anonymous structs - they are handled inline by their parent struct
269
+ if cursor.spelling and "anonymous" in cursor.spelling.lower():
270
+ return
268
271
  # Only generate code for non-system headers
269
272
  if cursor.location.file:
270
273
  file_path = str(Path(cursor.location.file.name).resolve())
@@ -292,6 +295,9 @@ class CSharpBindingsGenerator:
292
295
 
293
296
  elif cursor.kind == CursorKind.UNION_DECL:
294
297
  if cursor.is_definition():
298
+ # Skip anonymous unions - they are handled inline by their parent struct
299
+ if cursor.spelling and "anonymous" in cursor.spelling.lower():
300
+ return
295
301
  # Only generate code for non-system headers
296
302
  if cursor.location.file:
297
303
  file_path = str(Path(cursor.location.file.name).resolve())
@@ -377,6 +383,28 @@ class CSharpBindingsGenerator:
377
383
  self.seen_structs.add((type_name, None, None))
378
384
  # Register as opaque type for pointer handling
379
385
  self.type_mapper.opaque_types.add(type_name)
386
+ # Also generate an opaque type for the underlying struct name
387
+ # e.g. when typedef struct _XDisplay Display; we also want _XDisplay
388
+ try:
389
+ underlying_spelling = str(child.type.spelling)
390
+ except Exception:
391
+ underlying_spelling = None
392
+ if underlying_spelling:
393
+ # strip 'struct ' prefix if present
394
+ u_name = underlying_spelling
395
+ for prefix in ["const ", "volatile ", "struct ", "union ", "class "]:
396
+ if u_name.startswith(prefix):
397
+ u_name = u_name[len(prefix) :]
398
+ break
399
+ if u_name and u_name != type_name:
400
+ u_struct_key = (u_name, str(cursor.location.file), cursor.location.line)
401
+ if u_struct_key not in self.seen_structs:
402
+ u_code = self.code_generator.generate_opaque_type(u_name)
403
+ if u_code:
404
+ self._add_to_library_collection(self.generated_structs, self.current_library, u_code)
405
+ self.seen_structs.add(u_struct_key)
406
+ self.seen_structs.add((u_name, None, None))
407
+ self.type_mapper.opaque_types.add(u_name)
380
408
  elif child.kind == CursorKind.STRUCT_DECL and not child.is_definition() and child.spelling:
381
409
  # Direct forward declaration
382
410
  struct_key = (child.spelling, str(cursor.location.file), cursor.location.line)
@@ -495,6 +523,7 @@ class CSharpBindingsGenerator:
495
523
  library_using_statements: Optional[dict[str, list[str]]] = None,
496
524
  visibility: str = "public",
497
525
  global_constants: Optional[list[tuple[str, str, str, bool]]] = None,
526
+ global_defines: Optional[list[tuple[str, Optional[str]]]] = None,
498
527
  ) -> dict[str, str]:
499
528
  """Generate C# bindings from C header file(s)
500
529
 
@@ -503,12 +532,13 @@ class CSharpBindingsGenerator:
503
532
  output: Output directory for generated files (required)
504
533
  include_dirs: List of directories to search for included headers
505
534
  ignore_missing: Continue processing even if some header files are not found
506
- skip_variadic: Skip generating bindings for variadic functions
535
+ skip_variadic: Skip generating bindings for variadic functions (default: True)
507
536
  library_class_names: Dict mapping library names to custom class names (defaults to NativeMethods)
508
537
  library_namespaces: Dict mapping library names to custom namespaces
509
538
  library_using_statements: Dict mapping library names to lists of using statements
510
539
  visibility: Visibility modifier for generated code ("public" or "internal")
511
540
  global_constants: List of (name, pattern, type) tuples for macro extraction, applied to all libraries
541
+ global_defines: List of (name, value) tuples for compiler defines, applied to all headers
512
542
  """
513
543
  # Store visibility setting
514
544
  self.visibility = visibility
@@ -528,6 +558,9 @@ class CSharpBindingsGenerator:
528
558
  # Store global constants
529
559
  self.global_constants = global_constants or []
530
560
 
561
+ # Store global defines
562
+ self.global_defines = global_defines or []
563
+
531
564
  # Clear previous state
532
565
  self._clear_state()
533
566
 
@@ -539,6 +572,13 @@ class CSharpBindingsGenerator:
539
572
  for include_dir in include_dirs:
540
573
  clang_args.append(f"-I{include_dir}")
541
574
 
575
+ # Add global compiler defines
576
+ for name, value in self.global_defines:
577
+ if value is None or value == "":
578
+ clang_args.append(f"-D{name}")
579
+ else:
580
+ clang_args.append(f"-D{name}={value}")
581
+
542
582
  # Add system include paths so clang can find standard headers
543
583
  # These paths are typical locations for system headers
544
584
  import subprocess
@@ -671,8 +711,13 @@ class CSharpBindingsGenerator:
671
711
  if underlying_type and underlying_type != "int":
672
712
  inheritance_clause = f" : {underlying_type}"
673
713
 
714
+ # Check if this enum should have [Flags] attribute
715
+ flags_attribute = ""
716
+ if self.type_mapper.is_flag_enum(enum_name):
717
+ flags_attribute = "[Flags]\n"
718
+
674
719
  values_str = "\n".join([f" {name} = {value}," for name, value in members])
675
- code = f"""{self.visibility} enum {enum_name}{inheritance_clause}
720
+ code = f"""{flags_attribute}{self.visibility} enum {enum_name}{inheritance_clause}
676
721
  {{
677
722
  {values_str}
678
723
  }}