cs-binding-generator 1.0.1.dev35__tar.gz → 1.0.1.dev39__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 (67) hide show
  1. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/.gitignore +1 -1
  2. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/PKG-INFO +8 -3
  3. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/README.md +7 -2
  4. cs_binding_generator-1.0.1.dev39/cs_binding_generator/_version.py +24 -0
  5. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/config.py +11 -6
  6. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/generator.py +361 -74
  7. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/PKG-INFO +8 -3
  8. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/SOURCES.txt +1 -0
  9. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/docs/ARCHITECTURE.md +18 -8
  10. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/docs/XML_CONFIG.md +140 -15
  11. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_generator.py +5 -3
  12. cs_binding_generator-1.0.1.dev39/tests/test_macro_constants.py +501 -0
  13. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_removal.py +226 -5
  14. cs_binding_generator-1.0.1.dev35/cs_binding_generator/_version.py +0 -34
  15. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/.coverage +0 -0
  16. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/.flake8 +0 -0
  17. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/.github/workflows/publish.yml +0 -0
  18. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/COPILOT_CONTEXT.md +0 -0
  19. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/LICENSE +0 -0
  20. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/__init__.py +0 -0
  21. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/code_generators.py +0 -0
  22. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/constants.py +0 -0
  23. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/main.py +0 -0
  24. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/type_mapper.py +0 -0
  25. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/dependency_links.txt +0 -0
  26. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/entry_points.txt +0 -0
  27. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/requires.txt +0 -0
  28. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/top_level.txt +0 -0
  29. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/docs/INCLUDE_DIRECTORIES.md +0 -0
  30. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/docs/MULTI_FILE_OUTPUT.md +0 -0
  31. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/docs/RENAMING_EXAMPLE.xml +0 -0
  32. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/docs/TROUBLESHOOTING.md +0 -0
  33. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/enter_devenv.sh +0 -0
  34. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/pyproject.toml +0 -0
  35. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/run_tests.sh +0 -0
  36. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/setup.cfg +0 -0
  37. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/FreeTypeTest/FreeTypeTest.csproj +0 -0
  38. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/FreeTypeTest/bindings.cs +0 -0
  39. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/FreeTypeTest/cs-bindings.xml +0 -0
  40. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/FreeTypeTest/freetype.cs +0 -0
  41. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/LibtcodTest.csproj +0 -0
  42. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/SDL3.cs +0 -0
  43. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/bindings.cs +0 -0
  44. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/cs-bindings.xml +0 -0
  45. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/libtcod.cs +0 -0
  46. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/SDL3.cs +0 -0
  47. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/SDL3Test.csproj +0 -0
  48. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/SDL3Test.sln +0 -0
  49. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/bindings.cs +0 -0
  50. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/cs-bindings.xml +0 -0
  51. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/__init__.py +0 -0
  52. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/conftest.py +0 -0
  53. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/fixtures.py +0 -0
  54. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_cli.py +0 -0
  55. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_cli_extended.py +0 -0
  56. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_code_generators.py +0 -0
  57. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_defines.py +0 -0
  58. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_edge_cases.py +0 -0
  59. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_error_handling.py +0 -0
  60. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_flag_enums.py +0 -0
  61. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_multi_file_deduplication.py +0 -0
  62. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_opaque_typedef_underlying.py +0 -0
  63. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_renaming.py +0 -0
  64. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_type_mapper.py +0 -0
  65. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_type_mapping_extended.py +0 -0
  66. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_variadic_functions.py +0 -0
  67. {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_xml_config.py +0 -0
@@ -7,6 +7,6 @@ obj/
7
7
  dist/
8
8
  build/
9
9
  _version.py
10
-
10
+ -/
11
11
  # Local test output
12
12
  out_test/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cs_binding_generator
3
- Version: 1.0.1.dev35
3
+ Version: 1.0.1.dev39
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
@@ -48,7 +48,9 @@ The tool is configured primarily through XML configuration files, providing powe
48
48
  - **Automatic Type Mapping**: Intelligently maps C types to C# equivalents
49
49
  - **Renaming Support**: Simple and regex-based renaming rules to transform C names to C# conventions
50
50
  - **Removal Support**: Filter out unwanted functions, types, or patterns
51
- - **Macro Constants**: Extract C `#define` constants as C# enums with optional `[Flags]` attribute
51
+ - **Compiler Defines**: Pass `-D` flags to libclang via `<define>` to enable optional API blocks and gate platform-specific code paths during parsing
52
+ - **Flag Enum Marking**: Tag auto-discovered C enums with `[Flags]` via `<flags>`, with exact-name or regex matching
53
+ - **Macro Constants**: Extract C `#define` constants as C# enums (numeric mode) or as UTF-8 `ReadOnlySpan<byte>` members (string mode). Object-like and function-like macros are recursively expanded, and C-style casts in macro values are stripped before the numeric check, so chains like `SDL_BUTTON_MASK(SDL_BUTTON_LEFT)` and values like `((SDL_AudioDeviceID) 0xFFFFFFFFu)` resolve cleanly.
52
54
  - **String Handling**: Provides both raw pointer and helper string methods for `char*` returns
53
55
  - **Struct Generation**: Creates explicit layout structs with proper field offsets
54
56
  - **Union Support**: Converts C unions to C# structs with `LayoutKind.Explicit` and field offsets
@@ -449,7 +451,10 @@ CsBindingGenerator/
449
451
  ## Limitations
450
452
 
451
453
  - Variadic functions are not supported (skipped)
452
- - Complex macros with expressions are not extracted
454
+ - Macro expansion is textual, not a full C preprocessor: token-pasting (`##`),
455
+ stringizing (`#`), and multi-line backslash continuations are not handled.
456
+ Multi-token type names inside casts are recognized (`unsigned int`, `long long`),
457
+ but pointer casts (`(int*)`) are not stripped.
453
458
  - Bitfields in structs are not supported
454
459
  - Function pointers are mapped to `nint`
455
460
  - Requires manual handling of callbacks
@@ -19,7 +19,9 @@ The tool is configured primarily through XML configuration files, providing powe
19
19
  - **Automatic Type Mapping**: Intelligently maps C types to C# equivalents
20
20
  - **Renaming Support**: Simple and regex-based renaming rules to transform C names to C# conventions
21
21
  - **Removal Support**: Filter out unwanted functions, types, or patterns
22
- - **Macro Constants**: Extract C `#define` constants as C# enums with optional `[Flags]` attribute
22
+ - **Compiler Defines**: Pass `-D` flags to libclang via `<define>` to enable optional API blocks and gate platform-specific code paths during parsing
23
+ - **Flag Enum Marking**: Tag auto-discovered C enums with `[Flags]` via `<flags>`, with exact-name or regex matching
24
+ - **Macro Constants**: Extract C `#define` constants as C# enums (numeric mode) or as UTF-8 `ReadOnlySpan<byte>` members (string mode). Object-like and function-like macros are recursively expanded, and C-style casts in macro values are stripped before the numeric check, so chains like `SDL_BUTTON_MASK(SDL_BUTTON_LEFT)` and values like `((SDL_AudioDeviceID) 0xFFFFFFFFu)` resolve cleanly.
23
25
  - **String Handling**: Provides both raw pointer and helper string methods for `char*` returns
24
26
  - **Struct Generation**: Creates explicit layout structs with proper field offsets
25
27
  - **Union Support**: Converts C unions to C# structs with `LayoutKind.Explicit` and field offsets
@@ -420,7 +422,10 @@ CsBindingGenerator/
420
422
  ## Limitations
421
423
 
422
424
  - Variadic functions are not supported (skipped)
423
- - Complex macros with expressions are not extracted
425
+ - Macro expansion is textual, not a full C preprocessor: token-pasting (`##`),
426
+ stringizing (`#`), and multi-line backslash continuations are not handled.
427
+ Multi-token type names inside casts are recognized (`unsigned int`, `long long`),
428
+ but pointer casts (`(int*)`) are not stripped.
424
429
  - Bitfields in structs are not supported
425
430
  - Function pointers are mapped to `nint`
426
431
  - Requires manual handling of callbacks
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '1.0.1.dev39'
22
+ __version_tuple__ = version_tuple = (1, 0, 1, 'dev39')
23
+
24
+ __commit_id__ = commit_id = 'gb6b8a2450'
@@ -84,19 +84,24 @@ def parse_config_file(config_path):
84
84
 
85
85
  # Get global constants (macros to extract)
86
86
  # These are stored as a list of (name, pattern, type, is_flags) tuples
87
- # They will be applied to all libraries during processing
87
+ # They will be applied to all libraries during processing.
88
+ # The `name` attribute is required for numeric constants groups (which become a
89
+ # named C# enum) but optional for `type="string"` groups (which emit each macro
90
+ # as a member of the library's static class with no wrapper type).
88
91
  for const in root.findall("constants"):
89
92
  const_name = const.get("name")
90
93
  const_pattern = const.get("pattern")
91
- const_type = const.get("type", "uint") # Default to uint
92
- const_flags = const.get("flags", "false").lower() == "true" # Default to false
94
+ const_type = const.get("type", "uint").strip()
95
+ const_flags = const.get("flags", "false").lower() == "true"
93
96
 
94
- if not const_name:
95
- raise ValueError("Constants element missing 'name' attribute")
96
97
  if not const_pattern:
97
98
  raise ValueError("Constants element missing 'pattern' attribute")
99
+ if const_type != "string" and not const_name:
100
+ raise ValueError("Constants element missing 'name' attribute")
98
101
 
99
- config.global_constants.append((const_name.strip(), const_pattern.strip(), const_type.strip(), const_flags))
102
+ config.global_constants.append(
103
+ ((const_name or "").strip(), const_pattern.strip(), const_type, const_flags)
104
+ )
100
105
 
101
106
  for library in root.findall("library"):
102
107
  library_name = library.get("name")
@@ -60,51 +60,293 @@ class CSharpBindingsGenerator:
60
60
  self.source_file = None
61
61
 
62
62
  def _extract_macros_from_file(self, file_path: str, patterns: list[str]) -> dict[str, str]:
63
- """Extract #define macros from a header file that match the given patterns
63
+ """Numeric-only extractor (legacy signature, retained for internal tests).
64
64
 
65
- Args:
66
- file_path: Path to the header file
67
- patterns: List of regex patterns to match macro names
65
+ Forwards to `_extract_typed_macros_from_file` treating every pattern as
66
+ ``numeric``, and strips the kind from each entry so the return shape stays
67
+ ``{name: value}``.
68
+ """
69
+ typed = [(p, "numeric") for p in patterns]
70
+ result = self._extract_typed_macros_from_file(file_path, typed)
71
+ return {name: value for name, (value, _kind) in result.items()}
72
+
73
+ def _extract_typed_macros_from_file(
74
+ self,
75
+ file_path: str,
76
+ typed_patterns: list[tuple[str, str]],
77
+ ) -> dict[str, tuple[str, str]]:
78
+ """Extract ``#define`` macros from a header, dispatching by pattern kind.
79
+
80
+ ``typed_patterns`` is a list of ``(regex, kind)`` pairs where ``kind`` is either
81
+ ``"string"`` or ``"numeric"``. Each macro is tested against the patterns for its
82
+ kind:
83
+
84
+ - ``"string"``: the macro body must be a single C string literal (``"..."``).
85
+ No expansion is done — string macros that reference other identifiers are
86
+ out of scope. The stored value is the literal including the bounding quotes.
87
+ - ``"numeric"``: the body is expanded against the file's full macro table,
88
+ C casts are stripped, and the result must pass `_is_numeric_macro_value`.
89
+
90
+ Returns a dict ``{name: (value, kind)}`` so callers can route emit decisions
91
+ (enum vs. UTF-8 property) without re-classifying.
92
+ """
93
+ macros: dict[str, tuple[str, str]] = {}
94
+ table = self._scan_macros(file_path)
95
+
96
+ numeric_patterns = [p for p, k in typed_patterns if k != "string"]
97
+ string_patterns = [p for p, k in typed_patterns if k == "string"]
98
+
99
+ for name, (params, body) in table.items():
100
+ if params is not None:
101
+ continue # function-like macros stay in the table only as expansion targets
102
+
103
+ wants_numeric = any(re.fullmatch(p, name) for p in numeric_patterns)
104
+ wants_string = any(re.fullmatch(p, name) for p in string_patterns)
105
+
106
+ # Prefer the string check first when the macro body literally looks like a
107
+ # quoted string — that way `<constants type="string">` doesn't accidentally
108
+ # lose to a wider `numeric` pattern that happens to also match the name.
109
+ if wants_string and self._is_string_macro_value(body):
110
+ macros[name] = (body, "string")
111
+ continue
112
+
113
+ if wants_numeric:
114
+ value = self._expand_macros(body, table)
115
+ value = self._strip_c_casts(value)
116
+
117
+ # Legacy single-arg cast-macro form `WRAP(value)` (e.g. SDL_UINT64_C(0x123)).
118
+ # Run AFTER expansion so we only fall back when expansion didn't replace
119
+ # the wrapper.
120
+ cast_match = re.match(r'^\w+\((.*)\)$', value)
121
+ if cast_match:
122
+ value = cast_match.group(1).strip()
123
+
124
+ if self._is_numeric_macro_value(value):
125
+ macros[name] = (value, "numeric")
68
126
 
69
- Returns:
70
- Dict mapping macro names to their values
127
+ return macros
128
+
129
+ @staticmethod
130
+ def _is_string_macro_value(value: str) -> bool:
131
+ """True if `value` is a single C string literal (e.g. `"hello"`).
132
+
133
+ Backslash escapes are honored so that `"a\"b"` is recognized as one literal,
134
+ not a quote-balanced pair. Concatenated literals like `"a" "b"` are rejected;
135
+ we don't try to fuse them.
71
136
  """
72
- macros = {}
137
+ if len(value) < 2 or value[0] != '"' or value[-1] != '"':
138
+ return False
139
+ i = 1
140
+ end = len(value) - 1
141
+ while i < end:
142
+ if value[i] == '\\' and i + 1 < end:
143
+ i += 2
144
+ elif value[i] == '"':
145
+ return False
146
+ else:
147
+ i += 1
148
+ return True
149
+
150
+ def _scan_macros(self, file_path: str) -> dict[str, tuple[list[str] | None, str]]:
151
+ """Pass-1 scan: turn every `#define` in a file into a lookup table.
152
+
153
+ Returns a dict from macro name to `(params, body)` where:
154
+ - object-like macros (`#define NAME body`) have `params=None`
155
+ - function-like macros (`#define NAME(arg1, arg2) body`) have `params=[...]`
156
+
157
+ Bodies are stripped of trailing `/* ... */` comments and trailing commas to
158
+ match the legacy single-pass behavior. Multi-line continuations (backslash-
159
+ newline) are not handled — same limitation the previous scanner had.
160
+ """
161
+ # Function-like has to be tried first because its `NAME(` opening would otherwise
162
+ # be consumed by the object-like `\w+` group and leave `(args) body` as the value.
163
+ func_re = re.compile(r'^\s*#\s*define\s+(\w+)\(([^)]*)\)\s+(.+?)(?://.*)?$')
164
+ obj_re = re.compile(r'^\s*#\s*define\s+(\w+)\s+(.+?)(?://.*)?$')
165
+ table: dict[str, tuple[list[str] | None, str]] = {}
73
166
 
74
167
  try:
75
168
  with open(file_path, 'r', encoding='utf-8') as f:
76
169
  for line in f:
77
- # Look for #define directives with simple numeric values
78
- # Pattern: #define NAME VALUE
79
- match = re.match(r'^\s*#\s*define\s+(\w+)\s+(.+?)(?://.*)?$', line)
80
- if match:
81
- macro_name = match.group(1)
82
- macro_value = match.group(2).strip()
83
-
84
- # Strip C-style comments (/**< ... */ or /* ... */)
85
- macro_value = re.sub(r'/\*.*?\*/', '', macro_value).strip()
86
-
87
- # Strip trailing commas
88
- macro_value = macro_value.rstrip(',')
89
-
90
- # Strip C cast macros like SDL_UINT64_C(0x...) and extract the value
91
- cast_match = re.match(r'^\w+\((.*)\)$', macro_value)
92
- if cast_match:
93
- macro_value = cast_match.group(1).strip()
94
-
95
- # Only capture macros with numeric-looking values or simple expressions
96
- # Skip macros that reference other identifiers (which would need evaluation)
97
- if self._is_numeric_macro_value(macro_value):
98
- # Check if this macro matches any of the patterns
99
- for pattern in patterns:
100
- if re.fullmatch(pattern, macro_name):
101
- macros[macro_name] = macro_value
102
- break
103
- except Exception as e:
104
- # If we can't read the file, just skip it
170
+ fm = func_re.match(line)
171
+ if fm:
172
+ name = fm.group(1)
173
+ params = [p.strip() for p in fm.group(2).split(',') if p.strip()]
174
+ body = self._clean_macro_body(fm.group(3))
175
+ table[name] = (params, body)
176
+ continue
177
+ om = obj_re.match(line)
178
+ if om:
179
+ name = om.group(1)
180
+ body = self._clean_macro_body(om.group(2))
181
+ table[name] = (None, body)
182
+ except Exception:
105
183
  pass
106
184
 
107
- return macros
185
+ return table
186
+
187
+ @staticmethod
188
+ def _clean_macro_body(body: str) -> str:
189
+ body = body.strip()
190
+ body = re.sub(r'/\*.*?\*/', '', body).strip()
191
+ return body.rstrip(',')
192
+
193
+ def _expand_macros(self, value: str, macros: dict, max_depth: int = 8) -> str:
194
+ """Iteratively substitute identifier and function-call references in `value`
195
+ until the value stops changing or we hit `max_depth`.
196
+
197
+ The depth cap is a hard stop against self-referential macros (`#define FOO FOO`)
198
+ and mutually-referential pairs; we'd rather return a partially-expanded value the
199
+ numeric check rejects than hang.
200
+ """
201
+ for _ in range(max_depth):
202
+ new_value = self._expand_once(value, macros)
203
+ if new_value == value:
204
+ return value
205
+ value = new_value
206
+ return value
207
+
208
+ def _expand_once(self, value: str, macros: dict) -> str:
209
+ """One substitution pass over `value`.
210
+
211
+ Walks the string left-to-right. When we hit an identifier, decide:
212
+ - if it's followed by `(`, treat it as a function-like macro call and substitute
213
+ the body with the args bound to the parameter names;
214
+ - otherwise treat it as an object-like macro reference and substitute its body.
215
+
216
+ Identifiers inside `"..."` string literals are skipped so that string-valued
217
+ macros aren't corrupted by accidental substitution.
218
+ """
219
+ ident_re = re.compile(r'[A-Za-z_]\w*')
220
+ out: list[str] = []
221
+ i = 0
222
+ n = len(value)
223
+
224
+ while i < n:
225
+ ch = value[i]
226
+
227
+ if ch == '"':
228
+ # Copy a string literal verbatim, honoring backslash escapes so that an
229
+ # escaped `\"` doesn't terminate the string prematurely.
230
+ j = i + 1
231
+ while j < n:
232
+ if value[j] == '\\' and j + 1 < n:
233
+ j += 2
234
+ elif value[j] == '"':
235
+ j += 1
236
+ break
237
+ else:
238
+ j += 1
239
+ out.append(value[i:j])
240
+ i = j
241
+ continue
242
+
243
+ if ch.isalpha() or ch == '_':
244
+ m = ident_re.match(value, i)
245
+ assert m is not None # the leading-char check above guarantees this
246
+ name = m.group(0)
247
+ end = i + len(name)
248
+
249
+ if end < n and value[end] == '(':
250
+ # Possible function-like call.
251
+ close = self._find_matching_paren(value, end)
252
+ if close is not None and name in macros and macros[name][0] is not None:
253
+ params, body = macros[name]
254
+ args = self._split_macro_args(value[end + 1:close])
255
+ if len(args) == len(params):
256
+ out.append(self._substitute_params(body, params, args))
257
+ i = close + 1
258
+ continue
259
+ # Not a known function-like macro (or arity mismatch): leave it alone.
260
+ out.append(name)
261
+ i = end
262
+ continue
263
+
264
+ # Bare identifier.
265
+ if name in macros and macros[name][0] is None:
266
+ out.append(macros[name][1])
267
+ else:
268
+ out.append(name)
269
+ i = end
270
+ else:
271
+ out.append(ch)
272
+ i += 1
273
+
274
+ return ''.join(out)
275
+
276
+ @staticmethod
277
+ def _find_matching_paren(s: str, open_idx: int) -> int | None:
278
+ """Return the index of the `)` that closes the `(` at `open_idx`, or None
279
+ if the parens never balance."""
280
+ depth = 0
281
+ for i in range(open_idx, len(s)):
282
+ if s[i] == '(':
283
+ depth += 1
284
+ elif s[i] == ')':
285
+ depth -= 1
286
+ if depth == 0:
287
+ return i
288
+ return None
289
+
290
+ @staticmethod
291
+ def _split_macro_args(args_str: str) -> list[str]:
292
+ """Split a comma-separated function-like macro argument list, respecting paren
293
+ depth so that `(a, b)` inside an argument is not split into two args."""
294
+ if args_str.strip() == '':
295
+ return []
296
+ args: list[str] = []
297
+ buf: list[str] = []
298
+ depth = 0
299
+ for ch in args_str:
300
+ if ch == ',' and depth == 0:
301
+ args.append(''.join(buf).strip())
302
+ buf = []
303
+ else:
304
+ if ch == '(':
305
+ depth += 1
306
+ elif ch == ')':
307
+ depth -= 1
308
+ buf.append(ch)
309
+ args.append(''.join(buf).strip())
310
+ return args
311
+
312
+ @staticmethod
313
+ def _substitute_params(body: str, params: list[str], args: list[str]) -> str:
314
+ """Replace every whole-word occurrence of each parameter name in `body` with the
315
+ corresponding argument text. Identifiers inside string literals are not skipped
316
+ here because function-like macro bodies that contain strings AND reference params
317
+ in them are vanishingly rare in C and unsupported."""
318
+ mapping = dict(zip(params, args))
319
+
320
+ def repl(m: re.Match) -> str:
321
+ name = m.group(0)
322
+ return mapping[name] if name in mapping else name
323
+
324
+ return re.sub(r'\b[A-Za-z_]\w*\b', repl, body)
325
+
326
+ def _strip_c_casts(self, value: str) -> str:
327
+ """Strip C-style casts `(IDENT)` from a macro value when they sit in front of a
328
+ numeric token.
329
+
330
+ The lookahead is what makes this safe: we only remove `(name)` when the next
331
+ non-space character is a digit, opening paren, minus, or bitwise NOT — i.e. the
332
+ start of a numeric expression that the cast is converting. Parens around a bare
333
+ identifier (e.g. `(x)+1`) are left alone, and parens around a number (e.g. `(1)`)
334
+ are not casts so the leading-letter requirement on the identifier skips them.
335
+ """
336
+ # Match a `(IDENT)` or `(IDENT IDENT ...)` cast where each token is whitespace-
337
+ # separated. Covers `(uint32_t)`, `(unsigned int)`, `(long long)`, etc. We do not
338
+ # try to handle pointer casts (`(int*)`) — those contain `*` and the numeric check
339
+ # would reject the surrounding expression anyway.
340
+ cast_pattern = re.compile(
341
+ r'\(\s*[A-Za-z_]\w*(?:\s+[A-Za-z_]\w*)*\s*\)\s*(?=[\d(\-~])'
342
+ )
343
+ # Loop until stable: nested casts like `((Foo)(Bar)0)` need a couple of passes.
344
+ prev = None
345
+ result = value
346
+ while prev != result:
347
+ prev = result
348
+ result = cast_pattern.sub('', result)
349
+ return result
108
350
 
109
351
  def _is_numeric_macro_value(self, value: str) -> bool:
110
352
  """Check if a macro value looks numeric (number, cast, or simple expression)
@@ -373,6 +615,10 @@ class CSharpBindingsGenerator:
373
615
  "uintptr_t",
374
616
  "wchar_t",
375
617
  ]:
618
+ # Check if this type should be removed
619
+ if self.type_mapper.should_remove(type_name):
620
+ return
621
+
376
622
  struct_key = (type_name, str(cursor.location.file), cursor.location.line)
377
623
  # Use global deduplication
378
624
  if struct_key not in self.seen_structs:
@@ -397,15 +643,21 @@ class CSharpBindingsGenerator:
397
643
  u_name = u_name[len(prefix) :]
398
644
  break
399
645
  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)
646
+ # Check if the underlying name should be removed
647
+ if not self.type_mapper.should_remove(u_name):
648
+ u_struct_key = (u_name, str(cursor.location.file), cursor.location.line)
649
+ if u_struct_key not in self.seen_structs:
650
+ u_code = self.code_generator.generate_opaque_type(u_name)
651
+ if u_code:
652
+ self._add_to_library_collection(self.generated_structs, self.current_library, u_code)
653
+ self.seen_structs.add(u_struct_key)
654
+ self.seen_structs.add((u_name, None, None))
655
+ self.type_mapper.opaque_types.add(u_name)
408
656
  elif child.kind == CursorKind.STRUCT_DECL and not child.is_definition() and child.spelling:
657
+ # Check if this type should be removed
658
+ if self.type_mapper.should_remove(child.spelling):
659
+ return
660
+
409
661
  # Direct forward declaration
410
662
  struct_key = (child.spelling, str(cursor.location.file), cursor.location.line)
411
663
  # Use global deduplication
@@ -442,10 +694,13 @@ class CSharpBindingsGenerator:
442
694
  "uintptr_t",
443
695
  "wchar_t",
444
696
  ]:
445
- self.type_mapper.opaque_types.add(type_name)
697
+ # Only register as opaque if not marked for removal
698
+ if not self.type_mapper.should_remove(type_name):
699
+ self.type_mapper.opaque_types.add(type_name)
446
700
  elif child.kind == CursorKind.STRUCT_DECL and not child.is_definition() and child.spelling:
447
- # Direct forward declaration
448
- self.type_mapper.opaque_types.add(child.spelling)
701
+ # Direct forward declaration - only register if not marked for removal
702
+ if not self.type_mapper.should_remove(child.spelling):
703
+ self.type_mapper.opaque_types.add(child.spelling)
449
704
 
450
705
  # Recurse into children
451
706
  for child in cursor.get_children():
@@ -657,10 +912,13 @@ class CSharpBindingsGenerator:
657
912
  if library_name not in self.captured_macros:
658
913
  self.captured_macros[library_name] = {}
659
914
 
660
- # Collect all patterns from global constants
661
- patterns = []
915
+ # Tag each pattern with its emission kind so the per-file scanner can
916
+ # filter accordingly. Anything other than "string" routes to the numeric
917
+ # path (existing behavior).
918
+ typed_patterns: list[tuple[str, str]] = []
662
919
  for const_name, const_pattern, const_type, const_flags in self.global_constants:
663
- patterns.append(const_pattern)
920
+ kind = "string" if const_type == "string" else "numeric"
921
+ typed_patterns.append((const_pattern, kind))
664
922
 
665
923
  # Extract macros from all files in the translation unit (not just the main header)
666
924
  # This includes all #included files, which is where macros like SDL_WINDOW_* live
@@ -672,13 +930,13 @@ class CSharpBindingsGenerator:
672
930
  files_set.add(file_path)
673
931
  for child in cursor.get_children():
674
932
  collect_files(child, files_set)
675
-
933
+
676
934
  all_files = set()
677
935
  collect_files(tu.cursor, all_files)
678
-
936
+
679
937
  # Extract macros from all non-system files
680
938
  for file_path in all_files:
681
- file_macros = self._extract_macros_from_file(file_path, patterns)
939
+ file_macros = self._extract_typed_macros_from_file(file_path, typed_patterns)
682
940
  self.captured_macros[library_name].update(file_macros)
683
941
 
684
942
  if self.captured_macros[library_name]:
@@ -724,36 +982,65 @@ class CSharpBindingsGenerator:
724
982
  """
725
983
  self._add_to_library_collection(self.generated_enums, library, code)
726
984
 
727
- # Generate enums from captured macros using global constants
985
+ # Generate enums or UTF-8 string members from captured macros using global constants
728
986
  for library_name in self.captured_macros:
729
987
  for const_name, const_pattern, const_type, const_flags in self.global_constants:
730
- # Get all macros matching this pattern
731
- matching_macros = {}
732
- for macro_name, macro_value in self.captured_macros[library_name].items():
733
- if re.fullmatch(const_pattern, macro_name):
734
- matching_macros[macro_name] = macro_value
735
-
736
- if matching_macros:
737
- # Apply rename rules to the enum name and member names
738
- enum_name = self.type_mapper.apply_rename(const_name)
739
-
740
- # Build enum members with renamed names
741
- members = []
742
- for macro_name, macro_value in sorted(matching_macros.items()):
988
+ wants_string = const_type == "string"
989
+
990
+ # Get all macros matching this pattern, filtering by kind so that
991
+ # a numeric constants group can't accidentally pick up a string macro
992
+ # (or vice-versa) when their name patterns overlap.
993
+ matching_macros: dict[str, str] = {}
994
+ for macro_name, (macro_value, kind) in self.captured_macros[library_name].items():
995
+ if not re.fullmatch(const_pattern, macro_name):
996
+ continue
997
+ if wants_string and kind != "string":
998
+ continue
999
+ if not wants_string and kind != "numeric":
1000
+ continue
1001
+ matching_macros[macro_name] = macro_value
1002
+
1003
+ if not matching_macros:
1004
+ continue
1005
+
1006
+ if wants_string:
1007
+ # Each macro lands as a ReadOnlySpan<byte> member directly on the
1008
+ # library's static class. We use the fully-qualified type so we don't
1009
+ # need to add `using System;` to every generated file.
1010
+ for macro_name, raw_string in sorted(matching_macros.items()):
743
1011
  renamed_member = self.type_mapper.apply_rename(macro_name)
744
- members.append(f" {renamed_member} = unchecked(({const_type})({macro_value})),")
1012
+ prop = (
1013
+ f" {self.visibility} static System.ReadOnlySpan<byte> "
1014
+ f"{renamed_member} => {raw_string}u8;\n"
1015
+ )
1016
+ self._add_to_library_collection(
1017
+ self.generated_functions, library_name, prop
1018
+ )
1019
+ continue
1020
+
1021
+ # Numeric (enum) path — unchanged from before.
1022
+ # Apply rename rules to the enum name and member names
1023
+ enum_name = self.type_mapper.apply_rename(const_name)
1024
+
1025
+ # Build enum members with renamed names
1026
+ members = []
1027
+ for macro_name, macro_value in sorted(matching_macros.items()):
1028
+ renamed_member = self.type_mapper.apply_rename(macro_name)
1029
+ members.append(
1030
+ f" {renamed_member} = unchecked(({const_type})({macro_value})),"
1031
+ )
745
1032
 
746
- members_str = "\n".join(members)
1033
+ members_str = "\n".join(members)
747
1034
 
748
- # Generate enum with specified type and optional [Flags] attribute
749
- flags_attr = "[Flags]\n" if const_flags else ""
750
- type_clause = f" : {const_type}" if const_type != "int" else ""
751
- code = f"""{flags_attr}{self.visibility} enum {enum_name}{type_clause}
1035
+ # Generate enum with specified type and optional [Flags] attribute
1036
+ flags_attr = "[Flags]\n" if const_flags else ""
1037
+ type_clause = f" : {const_type}" if const_type != "int" else ""
1038
+ code = f"""{flags_attr}{self.visibility} enum {enum_name}{type_clause}
752
1039
  {{
753
1040
  {members_str}
754
1041
  }}
755
1042
  """
756
- self._add_to_library_collection(self.generated_enums, library_name, code)
1043
+ self._add_to_library_collection(self.generated_enums, library_name, code)
757
1044
 
758
1045
  return self._generate_multi_file_output(output)
759
1046