diff options
author | 3gg <3gg@shellblade.net> | 2025-08-30 16:53:58 -0700 |
---|---|---|
committer | 3gg <3gg@shellblade.net> | 2025-08-30 16:53:58 -0700 |
commit | 6aaedb813fa11ba0679c3051bc2eb28646b9506c (patch) | |
tree | 34acbfc9840e02cb4753e6306ea7ce978bf8b58e /src/contrib/SDL-3.2.20/build-scripts/check_android_jni.py | |
parent | 8f228ade99dd3d4c8da9b78ade1815c9adf85c8f (diff) |
Update to SDL3
Diffstat (limited to 'src/contrib/SDL-3.2.20/build-scripts/check_android_jni.py')
-rwxr-xr-x | src/contrib/SDL-3.2.20/build-scripts/check_android_jni.py | 172 |
1 files changed, 172 insertions, 0 deletions
diff --git a/src/contrib/SDL-3.2.20/build-scripts/check_android_jni.py b/src/contrib/SDL-3.2.20/build-scripts/check_android_jni.py new file mode 100755 index 0000000..9e519fb --- /dev/null +++ b/src/contrib/SDL-3.2.20/build-scripts/check_android_jni.py | |||
@@ -0,0 +1,172 @@ | |||
1 | #!/usr/bin/env python | ||
2 | |||
3 | import argparse | ||
4 | import dataclasses | ||
5 | import os | ||
6 | import pathlib | ||
7 | import re | ||
8 | |||
9 | ROOT = pathlib.Path(__file__).resolve().parents[1] | ||
10 | SDL_ANDROID_C = ROOT / "src/core/android/SDL_android.c" | ||
11 | METHOD_SOURCE_PATHS = ( | ||
12 | SDL_ANDROID_C, | ||
13 | ROOT / "src/hidapi/android/hid.cpp", | ||
14 | ) | ||
15 | JAVA_ROOT = ROOT / "android-project/app/src/main/java" | ||
16 | |||
17 | |||
18 | BASIC_TYPE_SPEC_LUT = { | ||
19 | "char": "C", | ||
20 | "byte": "B", | ||
21 | "short": "S", | ||
22 | "int": "I", | ||
23 | "long": "J", | ||
24 | "float": "F", | ||
25 | "double": "D", | ||
26 | "void": "V", | ||
27 | "boolean": "Z", | ||
28 | "Object": "Ljava/lang/Object;", | ||
29 | "String": "Ljava/lang/String;", | ||
30 | } | ||
31 | |||
32 | |||
33 | @dataclasses.dataclass(frozen=True) | ||
34 | class JniType: | ||
35 | typ: str | ||
36 | array: int | ||
37 | |||
38 | |||
39 | def java_type_to_jni_spec_internal(type_str: str) -> tuple[int, str]: | ||
40 | for basic_type_str, basic_type_spec in BASIC_TYPE_SPEC_LUT.items(): | ||
41 | if type_str.startswith(basic_type_str): | ||
42 | return len(basic_type_str), basic_type_spec | ||
43 | raise ValueError(f"Don't know how to convert {repr(type_str)} to its equivalent jni spec") | ||
44 | |||
45 | |||
46 | def java_type_to_jni_spec(type_str: str) -> str: | ||
47 | end, type_spec = java_type_to_jni_spec_internal(type_str) | ||
48 | suffix_str = type_str[end:] | ||
49 | assert(all(c in "[] \t" for c in suffix_str)) | ||
50 | suffix_str = "".join(filter(lambda v: v in "[]", suffix_str)) | ||
51 | assert len(suffix_str) % 2 == 0 | ||
52 | array_spec = "[" * (len(suffix_str) // 2) | ||
53 | return array_spec + type_spec | ||
54 | |||
55 | |||
56 | def java_method_to_jni_spec(ret: str, args: list[str]) -> str: | ||
57 | return "(" + "".join(java_type_to_jni_spec(a) for a in args) +")" + java_type_to_jni_spec(ret) | ||
58 | |||
59 | |||
60 | @dataclasses.dataclass(frozen=True) | ||
61 | class JniMethodBinding: | ||
62 | name: str | ||
63 | spec: str | ||
64 | |||
65 | |||
66 | def collect_jni_bindings_from_c() -> dict[str, set[JniMethodBinding]]: | ||
67 | bindings = {} | ||
68 | |||
69 | sdl_android_text = SDL_ANDROID_C.read_text() | ||
70 | for m in re.finditer(r"""register_methods\((?:[A-Za-z0-9]+),\s*"(?P<class>[a-zA-Z0-9_/]+)",\s*(?P<table>[a-zA-Z0-9_]+),\s*SDL_arraysize\((?P=table)\)\)""", sdl_android_text): | ||
71 | kls = m["class"] | ||
72 | table = m["table"] | ||
73 | methods = set() | ||
74 | in_struct = False | ||
75 | for method_source_path in METHOD_SOURCE_PATHS: | ||
76 | method_source = method_source_path.read_text() | ||
77 | for line in method_source.splitlines(keepends=False): | ||
78 | if re.match(f"(static )?JNINativeMethod {table}" + r"\[([0-9]+)?\] = \{", line): | ||
79 | in_struct = True | ||
80 | continue | ||
81 | if in_struct: | ||
82 | if re.match(r"\};", line): | ||
83 | in_struct = False | ||
84 | break | ||
85 | n = re.match(r"""\s*\{\s*"(?P<method>[a-zA-Z0-9_]+)"\s*,\s*"(?P<spec>[()A-Za-z0-9_/;[]+)"\s*,\s*(\(void\*\))?(HID|SDL)[_A-Z]*_JAVA_[_A-Z]*INTERFACE[_A-Z]*\((?P=method)\)\s*\},?""", line) | ||
86 | assert n, f"'{line}' does not match regex" | ||
87 | methods.add(JniMethodBinding(name=n["method"], spec=n["spec"])) | ||
88 | continue | ||
89 | if methods: | ||
90 | break | ||
91 | if methods: | ||
92 | break | ||
93 | assert methods, f"Could not find methods for {kls} (table={table})" | ||
94 | |||
95 | assert not in_struct | ||
96 | |||
97 | assert kls not in bindings, f"{kls} must be unique in C sources" | ||
98 | bindings[kls] = methods | ||
99 | return bindings | ||
100 | |||
101 | def collect_jni_bindings_from_java() -> dict[str, set[JniMethodBinding]]: | ||
102 | bindings = {} | ||
103 | |||
104 | for root, _, files in os.walk(JAVA_ROOT): | ||
105 | for file in files: | ||
106 | file_path = pathlib.Path(root) / file | ||
107 | java_text = file_path.read_text() | ||
108 | methods = set() | ||
109 | for m in re.finditer(r"(?:(?:public|private)\s+)?(?:static\s+)?native\s+(?P<ret>[A-Za-z0-9_]+)\s+(?P<method>[a-zA-Z0-9_]+)\s*\(\s*(?P<args>[^)]*)\);", java_text): | ||
110 | name = m["method"] | ||
111 | ret = m["ret"] | ||
112 | args = [] | ||
113 | args_str = m["args"].strip() | ||
114 | if args_str: | ||
115 | for a_s in args_str.split(","): | ||
116 | atype_str, _ = a_s.strip().rsplit(" ") | ||
117 | args.append(atype_str.strip()) | ||
118 | |||
119 | spec = java_method_to_jni_spec(ret=ret, args=args) | ||
120 | methods.add(JniMethodBinding(name=name, spec=spec)) | ||
121 | if methods: | ||
122 | relative_java_path = file_path.relative_to(JAVA_ROOT) | ||
123 | relative_java_path_without_suffix = relative_java_path.with_suffix("") | ||
124 | kls = "/".join(relative_java_path_without_suffix.parts) | ||
125 | assert kls not in bindings, f"{kls} must be unique in JAVA sources" | ||
126 | bindings[kls] = methods | ||
127 | return bindings | ||
128 | |||
129 | |||
130 | def print_error(*args): | ||
131 | print("ERROR:", *args) | ||
132 | |||
133 | |||
134 | def main(): | ||
135 | parser = argparse.ArgumentParser(allow_abbrev=False, description="Verify Android JNI bindings") | ||
136 | args = parser.parse_args() | ||
137 | |||
138 | bindings_from_c = collect_jni_bindings_from_c() | ||
139 | bindings_from_java = collect_jni_bindings_from_java() | ||
140 | |||
141 | all_ok = bindings_from_c == bindings_from_java | ||
142 | if all_ok: | ||
143 | print("OK") | ||
144 | else: | ||
145 | print("NOT OK") | ||
146 | kls_c = set(bindings_from_c.keys()) | ||
147 | kls_java = set(bindings_from_java.keys()) | ||
148 | if kls_c != kls_java: | ||
149 | only_c = kls_c - kls_java | ||
150 | for c in only_c: | ||
151 | print_error(f"Missing class in JAVA sources: {c}") | ||
152 | only_java = kls_java - kls_c | ||
153 | for c in only_java: | ||
154 | print_error(f"Missing class in C sources: {c}") | ||
155 | |||
156 | klasses = kls_c.union(kls_java) | ||
157 | for kls in klasses: | ||
158 | m_c = bindings_from_c.get(kls) | ||
159 | m_j = bindings_from_java.get(kls) | ||
160 | if m_c and m_j and m_c != m_j: | ||
161 | m_only_c = m_c - m_j | ||
162 | for c in m_only_c: | ||
163 | print_error(f"{kls}: Binding only in C source: {c.name} {c.spec}") | ||
164 | m_only_j = m_j - m_c | ||
165 | for c in m_only_j: | ||
166 | print_error(f"{kls}: Binding only in JAVA source: {c.name} {c.spec}") | ||
167 | |||
168 | return 0 if all_ok else 1 | ||
169 | |||
170 | |||
171 | if __name__ == "__main__": | ||
172 | raise SystemExit(main()) | ||