diff options
Diffstat (limited to 'contrib/SDL-3.2.8/build-scripts/check_android_jni.py')
| -rwxr-xr-x | contrib/SDL-3.2.8/build-scripts/check_android_jni.py | 172 |
1 files changed, 172 insertions, 0 deletions
diff --git a/contrib/SDL-3.2.8/build-scripts/check_android_jni.py b/contrib/SDL-3.2.8/build-scripts/check_android_jni.py new file mode 100755 index 0000000..9e519fb --- /dev/null +++ b/contrib/SDL-3.2.8/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()) | ||
