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.
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/.gitignore +1 -1
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/PKG-INFO +8 -3
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/README.md +7 -2
- cs_binding_generator-1.0.1.dev39/cs_binding_generator/_version.py +24 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/config.py +11 -6
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/generator.py +361 -74
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/PKG-INFO +8 -3
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/SOURCES.txt +1 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/docs/ARCHITECTURE.md +18 -8
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/docs/XML_CONFIG.md +140 -15
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_generator.py +5 -3
- cs_binding_generator-1.0.1.dev39/tests/test_macro_constants.py +501 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_removal.py +226 -5
- cs_binding_generator-1.0.1.dev35/cs_binding_generator/_version.py +0 -34
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/.coverage +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/.flake8 +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/.github/workflows/publish.yml +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/COPILOT_CONTEXT.md +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/LICENSE +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/__init__.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/code_generators.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/constants.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/main.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/type_mapper.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/dependency_links.txt +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/entry_points.txt +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/requires.txt +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/top_level.txt +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/docs/INCLUDE_DIRECTORIES.md +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/docs/MULTI_FILE_OUTPUT.md +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/docs/RENAMING_EXAMPLE.xml +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/docs/TROUBLESHOOTING.md +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/enter_devenv.sh +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/pyproject.toml +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/run_tests.sh +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/setup.cfg +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/FreeTypeTest/FreeTypeTest.csproj +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/FreeTypeTest/bindings.cs +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/FreeTypeTest/cs-bindings.xml +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/FreeTypeTest/freetype.cs +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/LibtcodTest.csproj +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/SDL3.cs +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/bindings.cs +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/cs-bindings.xml +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/libtcod.cs +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/SDL3.cs +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/SDL3Test.csproj +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/SDL3Test.sln +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/bindings.cs +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/cs-bindings.xml +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/__init__.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/conftest.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/fixtures.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_cli.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_cli_extended.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_code_generators.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_defines.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_edge_cases.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_error_handling.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_flag_enums.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_multi_file_deduplication.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_opaque_typedef_underlying.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_renaming.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_type_mapper.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_type_mapping_extended.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_variadic_functions.py +0 -0
- {cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/tests/test_xml_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cs_binding_generator
|
|
3
|
-
Version: 1.0.1.
|
|
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
|
-
- **
|
|
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
|
-
-
|
|
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
|
-
- **
|
|
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
|
-
-
|
|
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'
|
{cs_binding_generator-1.0.1.dev35 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/config.py
RENAMED
|
@@ -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")
|
|
92
|
-
const_flags = const.get("flags", "false").lower() == "true"
|
|
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(
|
|
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
|
-
"""
|
|
63
|
+
"""Numeric-only extractor (legacy signature, retained for internal tests).
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|
-
|
|
401
|
-
if
|
|
402
|
-
|
|
403
|
-
if
|
|
404
|
-
self.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
#
|
|
661
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1033
|
+
members_str = "\n".join(members)
|
|
747
1034
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
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
|
|