mirror of
				https://github.com/libsdl-org/SDL.git
				synced 2025-10-26 12:27:44 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			173 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			173 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env python3
 | |
| 
 | |
| import argparse
 | |
| import dataclasses
 | |
| import os
 | |
| import pathlib
 | |
| import re
 | |
| 
 | |
| ROOT = pathlib.Path(__file__).resolve().parents[1]
 | |
| SDL_ANDROID_C = ROOT / "src/core/android/SDL_android.c"
 | |
| METHOD_SOURCE_PATHS = (
 | |
|     SDL_ANDROID_C,
 | |
|     ROOT / "src/hidapi/android/hid.cpp",
 | |
| )
 | |
| JAVA_ROOT = ROOT / "android-project/app/src/main/java"
 | |
| 
 | |
| 
 | |
| BASIC_TYPE_SPEC_LUT = {
 | |
|     "char": "C",
 | |
|     "byte": "B",
 | |
|     "short": "S",
 | |
|     "int": "I",
 | |
|     "long": "J",
 | |
|     "float": "F",
 | |
|     "double": "D",
 | |
|     "void": "V",
 | |
|     "boolean": "Z",
 | |
|     "Object": "Ljava/lang/Object;",
 | |
|     "String": "Ljava/lang/String;",
 | |
| }
 | |
| 
 | |
| 
 | |
| @dataclasses.dataclass(frozen=True)
 | |
| class JniType:
 | |
|     typ: str
 | |
|     array: int
 | |
| 
 | |
| 
 | |
| def java_type_to_jni_spec_internal(type_str: str) -> tuple[int, str]:
 | |
|     for basic_type_str, basic_type_spec in BASIC_TYPE_SPEC_LUT.items():
 | |
|         if type_str.startswith(basic_type_str):
 | |
|             return len(basic_type_str), basic_type_spec
 | |
|     raise ValueError(f"Don't know how to convert {repr(type_str)} to its equivalent jni spec")
 | |
| 
 | |
| 
 | |
| def java_type_to_jni_spec(type_str: str) -> str:
 | |
|     end, type_spec = java_type_to_jni_spec_internal(type_str)
 | |
|     suffix_str = type_str[end:]
 | |
|     assert(all(c in "[] \t" for c in suffix_str))
 | |
|     suffix_str = "".join(filter(lambda v: v in "[]", suffix_str))
 | |
|     assert len(suffix_str) % 2 == 0
 | |
|     array_spec = "[" * (len(suffix_str) // 2)
 | |
|     return array_spec + type_spec
 | |
| 
 | |
| 
 | |
| def java_method_to_jni_spec(ret: str, args: list[str]) -> str:
 | |
|     return "(" + "".join(java_type_to_jni_spec(a) for a in args) +")" + java_type_to_jni_spec(ret)
 | |
| 
 | |
| 
 | |
| @dataclasses.dataclass(frozen=True)
 | |
| class JniMethodBinding:
 | |
|     name: str
 | |
|     spec: str
 | |
| 
 | |
| 
 | |
| def collect_jni_bindings_from_c() -> dict[str, set[JniMethodBinding]]:
 | |
|     bindings = {}
 | |
| 
 | |
|     sdl_android_text = SDL_ANDROID_C.read_text()
 | |
|     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):
 | |
|         kls = m["class"]
 | |
|         table = m["table"]
 | |
|         methods = set()
 | |
|         in_struct = False
 | |
|         for method_source_path in METHOD_SOURCE_PATHS:
 | |
|             method_source = method_source_path.read_text()
 | |
|             for line in method_source.splitlines(keepends=False):
 | |
|                 if re.match(f"(static )?JNINativeMethod {table}" + r"\[([0-9]+)?\] = \{", line):
 | |
|                     in_struct = True
 | |
|                     continue
 | |
|                 if in_struct:
 | |
|                     if re.match(r"\};", line):
 | |
|                         in_struct = False
 | |
|                         break
 | |
|                     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)
 | |
|                     assert n, f"'{line}' does not match regex"
 | |
|                     methods.add(JniMethodBinding(name=n["method"], spec=n["spec"]))
 | |
|                     continue
 | |
|                 if methods:
 | |
|                     break
 | |
|             if methods:
 | |
|                 break
 | |
|         assert methods, f"Could not find methods for {kls} (table={table})"
 | |
| 
 | |
|         assert not in_struct
 | |
| 
 | |
|         assert kls not in bindings, f"{kls} must be unique in C sources"
 | |
|         bindings[kls] = methods
 | |
|     return bindings
 | |
| 
 | |
| def collect_jni_bindings_from_java() -> dict[str, set[JniMethodBinding]]:
 | |
|     bindings = {}
 | |
| 
 | |
|     for root, _, files in os.walk(JAVA_ROOT):
 | |
|         for file in files:
 | |
|             file_path = pathlib.Path(root) / file
 | |
|             java_text = file_path.read_text()
 | |
|             methods = set()
 | |
|             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):
 | |
|                 name = m["method"]
 | |
|                 ret = m["ret"]
 | |
|                 args = []
 | |
|                 args_str = m["args"].strip()
 | |
|                 if args_str:
 | |
|                     for a_s in args_str.split(","):
 | |
|                         atype_str, _ = a_s.strip().rsplit(" ")
 | |
|                         args.append(atype_str.strip())
 | |
| 
 | |
|                 spec = java_method_to_jni_spec(ret=ret, args=args)
 | |
|                 methods.add(JniMethodBinding(name=name, spec=spec))
 | |
|             if methods:
 | |
|                 relative_java_path = file_path.relative_to(JAVA_ROOT)
 | |
|                 relative_java_path_without_suffix = relative_java_path.with_suffix("")
 | |
|                 kls = "/".join(relative_java_path_without_suffix.parts)
 | |
|                 assert kls not in bindings, f"{kls} must be unique in JAVA sources"
 | |
|                 bindings[kls] = methods
 | |
|     return bindings
 | |
| 
 | |
| 
 | |
| def print_error(*args):
 | |
|     print("ERROR:", *args)
 | |
| 
 | |
| 
 | |
| def main():
 | |
|     parser = argparse.ArgumentParser(allow_abbrev=False, description="Verify Android JNI bindings")
 | |
|     args = parser.parse_args()
 | |
| 
 | |
|     bindings_from_c = collect_jni_bindings_from_c()
 | |
|     bindings_from_java = collect_jni_bindings_from_java()
 | |
| 
 | |
|     all_ok = bindings_from_c == bindings_from_java
 | |
|     if all_ok:
 | |
|         print("OK")
 | |
|     else:
 | |
|         print("NOT OK")
 | |
|         kls_c = set(bindings_from_c.keys())
 | |
|         kls_java = set(bindings_from_java.keys())
 | |
|         if kls_c != kls_java:
 | |
|             only_c = kls_c - kls_java
 | |
|             for c in only_c:
 | |
|                 print_error(f"Missing class in JAVA sources: {c}")
 | |
|             only_java = kls_java - kls_c
 | |
|             for c in only_java:
 | |
|                 print_error(f"Missing class in C sources: {c}")
 | |
| 
 | |
|         klasses = kls_c.union(kls_java)
 | |
|         for kls in klasses:
 | |
|             m_c = bindings_from_c.get(kls)
 | |
|             m_j = bindings_from_java.get(kls)
 | |
|             if m_c and m_j and m_c != m_j:
 | |
|                 m_only_c = m_c - m_j
 | |
|                 for c in m_only_c:
 | |
|                     print_error(f"{kls}: Binding only in C source: {c.name} {c.spec}")
 | |
|                 m_only_j = m_j - m_c
 | |
|                 for c in m_only_j:
 | |
|                     print_error(f"{kls}: Binding only in JAVA source: {c.name} {c.spec}")
 | |
| 
 | |
|     return 0 if all_ok else 1
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     raise SystemExit(main())
 | 
