cs-binding-generator 1.0.1.dev38__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.dev38 → cs_binding_generator-1.0.1.dev39}/PKG-INFO +8 -3
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/README.md +7 -2
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/_version.py +3 -3
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/config.py +11 -6
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/generator.py +337 -63
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/PKG-INFO +8 -3
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/SOURCES.txt +1 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/docs/ARCHITECTURE.md +18 -8
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/docs/XML_CONFIG.md +140 -15
- {cs_binding_generator-1.0.1.dev38 → 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.dev38 → cs_binding_generator-1.0.1.dev39}/.coverage +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/.flake8 +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/.github/workflows/publish.yml +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/.gitignore +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/COPILOT_CONTEXT.md +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/LICENSE +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/__init__.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/code_generators.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/constants.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/main.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator/type_mapper.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/dependency_links.txt +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/entry_points.txt +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/requires.txt +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/cs_binding_generator.egg-info/top_level.txt +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/docs/INCLUDE_DIRECTORIES.md +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/docs/MULTI_FILE_OUTPUT.md +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/docs/RENAMING_EXAMPLE.xml +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/docs/TROUBLESHOOTING.md +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/enter_devenv.sh +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/pyproject.toml +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/run_tests.sh +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/setup.cfg +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/test_dotnet/FreeTypeTest/FreeTypeTest.csproj +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/test_dotnet/FreeTypeTest/bindings.cs +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/test_dotnet/FreeTypeTest/cs-bindings.xml +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/test_dotnet/FreeTypeTest/freetype.cs +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/LibtcodTest.csproj +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/SDL3.cs +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/bindings.cs +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/cs-bindings.xml +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/test_dotnet/LibtcodTest/libtcod.cs +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/SDL3.cs +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/SDL3Test.csproj +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/SDL3Test.sln +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/bindings.cs +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/test_dotnet/SDL3Test/cs-bindings.xml +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/__init__.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/conftest.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/fixtures.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/test_cli.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/test_cli_extended.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/test_code_generators.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/test_defines.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/test_edge_cases.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/test_error_handling.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/test_flag_enums.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/test_multi_file_deduplication.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/test_opaque_typedef_underlying.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/test_removal.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/test_renaming.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/test_type_mapper.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/test_type_mapping_extended.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → cs_binding_generator-1.0.1.dev39}/tests/test_variadic_functions.py +0 -0
- {cs_binding_generator-1.0.1.dev38 → 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
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 0, 1, '
|
|
21
|
+
__version__ = version = '1.0.1.dev39'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 1, 'dev39')
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gb6b8a2450'
|
{cs_binding_generator-1.0.1.dev38 → 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()
|
|
68
123
|
|
|
69
|
-
|
|
70
|
-
|
|
124
|
+
if self._is_numeric_macro_value(value):
|
|
125
|
+
macros[name] = (value, "numeric")
|
|
126
|
+
|
|
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.
|
|
136
|
+
"""
|
|
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.
|
|
71
160
|
"""
|
|
72
|
-
|
|
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)
|
|
@@ -670,10 +912,13 @@ class CSharpBindingsGenerator:
|
|
|
670
912
|
if library_name not in self.captured_macros:
|
|
671
913
|
self.captured_macros[library_name] = {}
|
|
672
914
|
|
|
673
|
-
#
|
|
674
|
-
|
|
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]] = []
|
|
675
919
|
for const_name, const_pattern, const_type, const_flags in self.global_constants:
|
|
676
|
-
|
|
920
|
+
kind = "string" if const_type == "string" else "numeric"
|
|
921
|
+
typed_patterns.append((const_pattern, kind))
|
|
677
922
|
|
|
678
923
|
# Extract macros from all files in the translation unit (not just the main header)
|
|
679
924
|
# This includes all #included files, which is where macros like SDL_WINDOW_* live
|
|
@@ -685,13 +930,13 @@ class CSharpBindingsGenerator:
|
|
|
685
930
|
files_set.add(file_path)
|
|
686
931
|
for child in cursor.get_children():
|
|
687
932
|
collect_files(child, files_set)
|
|
688
|
-
|
|
933
|
+
|
|
689
934
|
all_files = set()
|
|
690
935
|
collect_files(tu.cursor, all_files)
|
|
691
|
-
|
|
936
|
+
|
|
692
937
|
# Extract macros from all non-system files
|
|
693
938
|
for file_path in all_files:
|
|
694
|
-
file_macros = self.
|
|
939
|
+
file_macros = self._extract_typed_macros_from_file(file_path, typed_patterns)
|
|
695
940
|
self.captured_macros[library_name].update(file_macros)
|
|
696
941
|
|
|
697
942
|
if self.captured_macros[library_name]:
|
|
@@ -737,36 +982,65 @@ class CSharpBindingsGenerator:
|
|
|
737
982
|
"""
|
|
738
983
|
self._add_to_library_collection(self.generated_enums, library, code)
|
|
739
984
|
|
|
740
|
-
# Generate enums from captured macros using global constants
|
|
985
|
+
# Generate enums or UTF-8 string members from captured macros using global constants
|
|
741
986
|
for library_name in self.captured_macros:
|
|
742
987
|
for const_name, const_pattern, const_type, const_flags in self.global_constants:
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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()):
|
|
756
1011
|
renamed_member = self.type_mapper.apply_rename(macro_name)
|
|
757
|
-
|
|
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
|
+
)
|
|
758
1032
|
|
|
759
|
-
|
|
1033
|
+
members_str = "\n".join(members)
|
|
760
1034
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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}
|
|
765
1039
|
{{
|
|
766
1040
|
{members_str}
|
|
767
1041
|
}}
|
|
768
1042
|
"""
|
|
769
|
-
|
|
1043
|
+
self._add_to_library_collection(self.generated_enums, library_name, code)
|
|
770
1044
|
|
|
771
1045
|
return self._generate_multi_file_output(output)
|
|
772
1046
|
|
|
@@ -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
|
|
@@ -83,8 +83,19 @@ The main orchestrator that coordinates the entire generation process.
|
|
|
83
83
|
- `process_cursor()`: Recursively walks the AST
|
|
84
84
|
- `prescan_opaque_types()`: Identifies opaque struct typedefs
|
|
85
85
|
- `_build_file_depth_map()`: Tracks include hierarchy
|
|
86
|
-
- `_extract_macros_from_file()
|
|
87
|
-
|
|
86
|
+
- `_extract_macros_from_file()` / `_extract_typed_macros_from_file()`: Scan a
|
|
87
|
+
header for `#define` macros matching the configured patterns; the typed
|
|
88
|
+
variant dispatches between numeric and string emission kinds.
|
|
89
|
+
- `_scan_macros()`: Pass-1 scan that builds a per-file table of both object-like
|
|
90
|
+
and function-like macros, used as the lookup target during expansion.
|
|
91
|
+
- `_expand_macros()` / `_expand_once()`: Recursive textual substitution of
|
|
92
|
+
identifier references and function-like macro calls, depth-capped against
|
|
93
|
+
self-referential cycles. String literals are skipped.
|
|
94
|
+
- `_strip_c_casts()`: Removes `(IDENT)` and `(IDENT IDENT)` C-style casts when
|
|
95
|
+
followed by a numeric token, so `((SDL_AudioDeviceID) 0xFFFFFFFFu)` reduces
|
|
96
|
+
to `(0xFFFFFFFFu)`.
|
|
97
|
+
- `_is_numeric_macro_value()` / `_is_string_macro_value()`: Per-kind value
|
|
98
|
+
validators run after expansion/cast-strip.
|
|
88
99
|
|
|
89
100
|
### 4. Type Mapper (`type_mapper.py`)
|
|
90
101
|
|
|
@@ -428,9 +439,8 @@ Potential areas for improvement:
|
|
|
428
439
|
|
|
429
440
|
1. **Function pointer support**: Map to delegates
|
|
430
441
|
2. **Callback handling**: Generate C# delegate types
|
|
431
|
-
3. **
|
|
432
|
-
4. **
|
|
433
|
-
5. **
|
|
434
|
-
6. **
|
|
435
|
-
7. **
|
|
436
|
-
8. **Custom type maps**: User-provided type override files
|
|
442
|
+
3. **Bitfield support**: Handle bit-width fields
|
|
443
|
+
4. **Documentation comments**: Extract and preserve doxygen comments
|
|
444
|
+
5. **Source maps**: Track C header → C# line mapping
|
|
445
|
+
6. **Incremental generation**: Only regenerate changed definitions
|
|
446
|
+
7. **Custom type maps**: User-provided type override files
|