Skip to content

Commit 8c40c2e

Browse files
committed
Added initial support for platform-specific code.
1 parent e0fbe60 commit 8c40c2e

File tree

9 files changed

+199
-21
lines changed

9 files changed

+199
-21
lines changed

CHANGELOG.md

+12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ All notable changes to this project will be documented in this file.
66
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
77

88

9+
## [Unreleased]
10+
11+
### Added
12+
13+
- Experimental support for platform-dependent code. You should now define `start_platform_code` and `end_platform_code` on your generators to be run on each item inside a platform-specific code. The built-in generators already have support for these changes. Please refer to the documentation inside `visitor.py` for more.
14+
- README on the C++ generator discussing most common topics related to it.
15+
16+
### Changed
17+
18+
- Rewrote the C# generator to use functions from `utils`.
19+
20+
921
## 2024-06-28
1022

1123
### Added

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Also, before you can run the script, you need to edit `PATH_BY_UNIT` in [setup.p
1919

2020
This is a list of currently supported languages:
2121

22-
- C++ ([example](./gen/cpp/example.cpp))
22+
- C++ ([example](./gen/cpp/example.cpp)) (for SDL2 use [this](./gen/cpp/example-sdl2.cpp) instead)
2323
- C# ([example](./gen/cs/Example.cs)) (NOTE: headers must be annotated as in this [PR](https://github.com/libsdl-org/SDL/pull/9907))
2424
- JSON
2525

gen/cpp.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
}}
5555
5656
export namespace {2}
57-
{{"""
57+
{{
58+
"""
5859

5960

6061
def _cut_similarity(model: str, target: str) -> str:
@@ -121,6 +122,14 @@ def __del__(self) -> None:
121122
self._file.write("}\n")
122123
self._file.close()
123124

125+
def start_platform_code(self, platforms: list[str]):
126+
self._file.write(
127+
f"#if {' || '.join(map(lambda p: f'defined({p})', platforms))}\n"
128+
)
129+
130+
def end_platform_code(self):
131+
self._file.write("#endif\n\n")
132+
124133
def visit_function(self, rules: dict[str, Node | list[Node]]):
125134
name = rules["function.name"]
126135
ret = rules["function.return"].text.decode()
@@ -205,8 +214,7 @@ def cast_if_enum(ty: str, name: str) -> str:
205214
print(f"Note: Skipping {name.text.decode()} due to unnamed parameter")
206215
return
207216

208-
self._file.write(f"""
209-
{ret} {name.text[4:].decode()}(""")
217+
self._file.write(f"\n {ret} {name.text[4:].decode()}(")
210218

211219
self._file.write(", ".join(f"{ty} {nm}" for ty, nm in zip(ps_types, ps_name)))
212220

gen/cpp/README.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
2+
# Q&A
3+
4+
Q: How do I use the C++ generator?
5+
A: Just like how you would use the other generators, by running: `py sdl_parser.py gen.cpp`. You can specify the following parameters:
6+
`--module`: a format string for the module name. Defaults to `sdl.{ext}`.
7+
`--namespace`: a format string for the namespace containing the generated code. Defaults to `sdl::{ext}`
8+
9+
10+
Q: What compiler do I need to use the code?
11+
A: You need a compiler that supports C++20 modules.
12+
13+
14+
Q: How do I use the generated code?
15+
A: You simply import the modules you want on your code and use the functions/types as you would with any C++ code.
16+
17+
Q: How do I build the code?
18+
A: You need to consult the documentation of your compiler on how to build modules, but below there are commands showing how to build the provided example (example.cpp) with the main SDL module (`SDL.g.cppm`) on the three main compilers: GCC, Clang and MSVC. Remember to use `example-sdl2.cpp` instead of `example.cpp` if you have generated SDL2 headers.
19+
```sh
20+
# For GCC, warning: untested code
21+
# Apparently with GCC you have to build everything at once
22+
g++ -std=c++20 -fmodules-ts -o example example.cpp SDL.g.cppm -I<your-include-paths> -l<sdl-lib>
23+
24+
25+
# For Clang, warning: untested code
26+
clang -c -std=c++20 SDL.g.cppm -Xclang -emit-module-interface -o SDL.g.pcm -I<your-include-paths>
27+
# No include paths are needed when building the executable
28+
clang --std=c++20 -fprebuilt-module-path=. -fmodule-file=sdl=SDL.g.pcm example.cpp SDL.g.cppm -o example -l<sdl-lib>
29+
30+
# For MSVC
31+
cl /std:c++20 /c SDL.g.cppm /TP /interface /I <your-include-paths>
32+
# Notice no include paths are needed when building the executable
33+
cl /std:c++20 example.cpp /link SDL.g.obj <sdl-library>
34+
```
35+
36+
37+
38+
The SDL modules need to be built only once and can be used freely after that.
39+
40+
Q: What difference does the generated code have over the regular SDL code?
41+
A: Here's a list of changes that the generator applies:
42+
- Everything is located inside a namespace, depending on the unit they are part of.
43+
- The names have the prefix stripped (eg. `SDL_Init` is `sdl::Init`).
44+
- Macros are now `constexpr` functions, as macros and static variables cannot be exported from modules.
45+
- Enums and bitflags are strongly typed (as in `enum class`). Bitflags also use the alias type as underlying type.

gen/cs.py

+6
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,12 @@ def __del__(self) -> None:
220220
with open(self._out, "w") as f:
221221
f.write(self._data)
222222

223+
def start_platform_code(self, platforms: list[str]):
224+
self._file.write(f"#if {' || '.join(platforms)}\n")
225+
226+
def end_platform_code(self):
227+
self._file.write("#endif\n\n")
228+
223229
def visit_function(self, rules):
224230
name = rules["function.name"].text.decode()
225231
docs = rules["function.docs"]

query.scm

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11

22
(
3-
(comment) @function.docs
4-
.
53
(declaration
64
(storage_class_specifier)
75
type: (_) @function.return
@@ -13,8 +11,6 @@
1311
) @function
1412

1513
(
16-
(comment) @function.docs
17-
.
1814
(declaration
1915
(storage_class_specifier)
2016
type: (_) @function.return
@@ -112,3 +108,7 @@
112108

113109
(preproc_def name: (_) @const.name value: (_) @const.value) @const
114110

111+
[
112+
(preproc_if condition: (_) @cond.text)
113+
(preproc_ifdef name: (_) @cond.text)
114+
] @cond

sdl_parser.py

+38-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
from setup import PATH_BY_UNIT, SDL_ROOT
1212
import utils
1313

14+
15+
if not PATH_BY_UNIT:
16+
print("Error: no units chosen to parse. Please edit PATH_BY_UNIT in setup.py.")
17+
sys.exit(1)
18+
1419
_FILE_STR: str = """
1520
from tree_sitter import Node
1621
from visitor import visitor
@@ -90,11 +95,14 @@ def os_defines() -> list[str]:
9095
return ["-D", "_AIX"]
9196

9297
case "android":
93-
return ["-D", "__ANDROID__"]
98+
return ["-D", "__ANDROID__", "-D", "ANDROID"]
9499

95100
case "cygwin":
96101
return ["-D", "_WIN32", "-D", "__CYGWIN__"]
97102

103+
case "darwin":
104+
return ["-D", "__APPLE__", "-D", "__MACH__"]
105+
98106
case "emscripten":
99107
return ["-D", "__EMSCRIPTEN__"]
100108

@@ -104,6 +112,9 @@ def os_defines() -> list[str]:
104112
case "linux":
105113
return ["-D", "__linux__"]
106114

115+
case "wasi":
116+
return ["-D", "__wasi__"]
117+
107118
case "win32":
108119
return ["-D", "_WIN32"]
109120

@@ -126,6 +137,28 @@ def parse_file(*args, input: str, output: str):
126137
"-D",
127138
"SDL_DECLSPEC=", # save us some time and headaches
128139
"-D",
140+
"SDL_DEPRECATED=", # save us some time and headaches
141+
"-D",
142+
"SDL_UNUSED=", # save us some time and headaches
143+
"-D",
144+
"SDL_ASSERT_LEVEL=1", # save us some time and headaches
145+
"-D",
146+
"SDL_NODISCARD=", # save us some time and headaches
147+
"-D",
148+
"SDL_NORETURN=", # save us some time and headaches
149+
"-D",
150+
"SDL_ANALYZER_NORETURN=", # save us some time and headaches
151+
"-D",
152+
"SDL_MALLOC=", # save us some time and headaches
153+
"-D",
154+
"SDL_ALLOC_SIZE=", # save us some time and headaches
155+
"-D",
156+
"SDL_ALLOC_SIZE2=", # save us some time and headaches
157+
"-D",
158+
"SDL_BYTEORDER=SDL_LIL_ENDIAN", # save us some time and headaches
159+
"-D",
160+
"SDL_FLOATWORDORDER=SDL_LIL_ENDIAN", # save us some time and headaches
161+
"-D",
129162
"SDL_SLOW_MEMCPY", # save us some time and headaches
130163
"-D",
131164
"SDL_SLOW_MEMMOVE", # save us some time and headaches
@@ -150,6 +183,8 @@ def parse_file(*args, input: str, output: str):
150183
"-D",
151184
"SDL_EndThreadFunction",
152185
"-D",
186+
"SDL_platform_defines_h_", # save us some time and headaches
187+
"-D",
153188
"SDL_oldnames_h_", # save us some time and headaches
154189
"-D",
155190
"SDL_stdinc_h_", # save us some time and headaches
@@ -168,10 +203,9 @@ def parse_file(*args, input: str, output: str):
168203
(SDL_static_cast(Uint32, SDL_static_cast(Uint8, (D))) << 24))""",
169204
"-D",
170205
"SDL_static_cast(T, V)=((T)(V))", # save us some time and headaches
171-
"-N",
172-
"WINAPI_FAMILY_WINRT", # workaround
206+
# skip this as we need them to detect platform-specific code
173207
"--passthru-defines", # keep defines in output
174-
# "--passthru-unknown-exprs",
208+
"--passthru-unknown-exprs", # NOTE: this keeps the ifdef/endif blocks
175209
"--passthru-unfound-includes", # skip missing includes
176210
"--passthru-comments", # keep comments in output
177211
"--output-encoding",

setup.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import sys
2-
31
# NOTE: this is relative to `sdl_parser.py` if a relative path.
42
SDL_ROOT = "./include"
53

@@ -9,16 +7,11 @@
97
# All these paths should be reasonable defaults.
108
# Uncomment and change as needed.
119
PATH_BY_UNIT = {
12-
"SDL": "SDL2/SDL.h",
10+
# "SDL": "SDL2/SDL.h",
1311
# "SDL": "SDL3/SDL.h",
1412
# "SDL_main": "SDL3/SDL_main.h",
1513
# "SDL_vulkan": "SDL3/SDL_vulkan.h",
1614
# "SDL_image": "SDL3_image/SDL_image.h",
1715
# "SDL_mixer": "SDL3_mixer/SDL_mixer.h",
1816
# "SDL_ttf": "SDL3_ttf/SDL_ttf.h",
1917
}
20-
21-
# NOTE: do not touch :)
22-
if not PATH_BY_UNIT:
23-
print("Error: no units chosen to parse.")
24-
sys.exit(1)

visitor.py

+81-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from getopt import getopt
33
import inspect
44
import sys
5+
import re
56

67
from tree_sitter import Node
78

@@ -10,6 +11,8 @@
1011

1112
_BITFLAG_FILTER = {"preproc_def", "preproc_function_def"}
1213

14+
_PLATFORM_REGEX = re.compile(r"\bSDL_PLATFORM_\w+\b")
15+
1316

1417
class Visitor(metaclass=ABCMeta):
1518
def __init__(self, unit: str) -> None:
@@ -19,38 +22,115 @@ def __init__(self, unit: str) -> None:
1922
# ignore typedef/#defines that are used for bitflags
2023
# so we have to do it manually
2124
self._parsing_bitflag = False
25+
self._platforms = []
26+
self._platform_node_id = None
2227

2328
def __del__(self) -> None:
2429
# empty for now
2530
pass
2631

2732
def visit(self, rules: _Rules):
33+
# TODO: check if this is the child of the `cond` node, if the node is not `None`
34+
# when not the child, then the `cond` node becomes None
35+
36+
def _platform_setup(rule: str):
37+
cursor = rules[rule]
38+
39+
if self._platform_node_id:
40+
while (
41+
cursor.parent
42+
and cursor.parent.parent
43+
and cursor.parent.parent.type != "translation_unit"
44+
):
45+
cursor = cursor.parent
46+
47+
if cursor.id != self._platform_node_id:
48+
self._platform_node_id = None
49+
50+
if self._platform_node_id:
51+
self.start_platform_code(self._platforms)
52+
2853
if "function" in rules:
54+
_platform_setup("function")
2955
self.visit_function(rules)
3056
elif "bitflag" in rules:
57+
_platform_setup("bitflag")
3158
self.visit_bitflag(rules)
3259
self._parsing_bitflag = False
3360
elif "enum" in rules:
61+
_platform_setup("enum")
3462
self.visit_enum(rules)
3563
elif "opaque" in rules:
64+
_platform_setup("opaque")
3665
self.visit_opaque(rules)
3766
elif "struct" in rules:
67+
_platform_setup("struct")
3868
self.visit_struct(rules)
3969
elif "union" in rules:
70+
_platform_setup("union")
4071
self.visit_union(rules)
4172
elif "alias" in rules:
4273
if rules["alias"].next_sibling.type in _BITFLAG_FILTER:
4374
self._parsing_bitflag = True
4475
return
4576

77+
_platform_setup("alias")
4678
self.visit_alias(rules)
4779
elif "callback" in rules:
80+
_platform_setup("callback")
4881
self.visit_callback(rules)
4982
elif "fn_macro" in rules:
83+
_platform_setup("fn_macro")
5084
self.visit_fn_macro(rules)
5185
elif "const" in rules:
52-
if not self._parsing_bitflag:
86+
if not self._parsing_bitflag or self._platform_node_id:
87+
# skip constants inside bitflags and platform-specific code
88+
_platform_setup("const")
5389
self.visit_const(rules)
90+
elif "cond" in rules:
91+
self._platforms = _PLATFORM_REGEX.findall(rules["cond.text"].text.decode())
92+
if not self._platforms:
93+
return
94+
95+
self._platform_node_id = rules["cond"].id
96+
return
97+
98+
if self._platform_node_id:
99+
self.end_platform_code()
100+
101+
@abstractmethod
102+
def start_platform_code(self, platforms: list[str]):
103+
"""
104+
Start a platform-specific code block.
105+
106+
This function is called once per platform-specific data instead of once per block.
107+
Thus, in the following code block:
108+
109+
```c
110+
#ifdef SDL_PLATFORM_WINDOWS
111+
extern void SDL_foo();
112+
113+
extern void SDL_bar();
114+
#endif
115+
```
116+
117+
`start_platform_code` will be called once with `platforms` being `["SDL_PLATFORM_WINDOWS"]`
118+
for both `SDL_foo` and `SDL_bar`.
119+
120+
121+
`platforms` is a list of all the platforms that support the following code block.
122+
"""
123+
raise NotImplementedError()
124+
125+
@abstractmethod
126+
def end_platform_code(self):
127+
"""
128+
End a platform-specific code block.
129+
130+
Similarly to `start_platform_code`, this function is called once per platform-specific data.
131+
Refer to `start_platform_code` for more information.
132+
"""
133+
raise NotImplementedError()
54134

55135
@abstractmethod
56136
def visit_function(self, rules: _Rules):

0 commit comments

Comments
 (0)