Simple Directmedia Layer
at main 172 lines 6.1 kB view raw
1#!/usr/bin/env python 2 3import argparse 4import dataclasses 5import os 6import pathlib 7import re 8 9ROOT = pathlib.Path(__file__).resolve().parents[1] 10SDL_ANDROID_C = ROOT / "src/core/android/SDL_android.c" 11METHOD_SOURCE_PATHS = ( 12 SDL_ANDROID_C, 13 ROOT / "src/hidapi/android/hid.cpp", 14) 15JAVA_ROOT = ROOT / "android-project/app/src/main/java" 16 17 18BASIC_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) 34class JniType: 35 typ: str 36 array: int 37 38 39def 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 46def 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 56def 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) 61class JniMethodBinding: 62 name: str 63 spec: str 64 65 66def 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 101def 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 130def print_error(*args): 131 print("ERROR:", *args) 132 133 134def 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 171if __name__ == "__main__": 172 raise SystemExit(main())