1
0

check_android_jni.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. #!/usr/bin/env python3
  2. import argparse
  3. import dataclasses
  4. import os
  5. import pathlib
  6. import re
  7. ROOT = pathlib.Path(__file__).resolve().parents[1]
  8. SDL_ANDROID_C = ROOT / "src/core/android/SDL_android.c"
  9. METHOD_SOURCE_PATHS = (
  10. SDL_ANDROID_C,
  11. ROOT / "src/hidapi/android/hid.cpp",
  12. )
  13. JAVA_ROOT = ROOT / "android-project/app/src/main/java"
  14. BASIC_TYPE_SPEC_LUT = {
  15. "char": "C",
  16. "byte": "B",
  17. "short": "S",
  18. "int": "I",
  19. "long": "J",
  20. "float": "F",
  21. "double": "D",
  22. "void": "V",
  23. "boolean": "Z",
  24. "Object": "Ljava/lang/Object;",
  25. "String": "Ljava/lang/String;",
  26. }
  27. @dataclasses.dataclass(frozen=True)
  28. class JniType:
  29. typ: str
  30. array: int
  31. def java_type_to_jni_spec_internal(type_str: str) -> tuple[int, str]:
  32. for basic_type_str, basic_type_spec in BASIC_TYPE_SPEC_LUT.items():
  33. if type_str.startswith(basic_type_str):
  34. return len(basic_type_str), basic_type_spec
  35. raise ValueError(f"Don't know how to convert {repr(type_str)} to its equivalent jni spec")
  36. def java_type_to_jni_spec(type_str: str) -> str:
  37. end, type_spec = java_type_to_jni_spec_internal(type_str)
  38. suffix_str = type_str[end:]
  39. assert(all(c in "[] \t" for c in suffix_str))
  40. suffix_str = "".join(filter(lambda v: v in "[]", suffix_str))
  41. assert len(suffix_str) % 2 == 0
  42. array_spec = "[" * (len(suffix_str) // 2)
  43. return array_spec + type_spec
  44. def java_method_to_jni_spec(ret: str, args: list[str]) -> str:
  45. return "(" + "".join(java_type_to_jni_spec(a) for a in args) +")" + java_type_to_jni_spec(ret)
  46. @dataclasses.dataclass(frozen=True)
  47. class JniMethodBinding:
  48. name: str
  49. spec: str
  50. def collect_jni_bindings_from_c() -> dict[str, set[JniMethodBinding]]:
  51. bindings = {}
  52. sdl_android_text = SDL_ANDROID_C.read_text()
  53. 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):
  54. kls = m["class"]
  55. table = m["table"]
  56. methods = set()
  57. in_struct = False
  58. for method_source_path in METHOD_SOURCE_PATHS:
  59. method_source = method_source_path.read_text()
  60. for line in method_source.splitlines(keepends=False):
  61. if re.match(f"(static )?JNINativeMethod {table}" + r"\[([0-9]+)?\] = \{", line):
  62. in_struct = True
  63. continue
  64. if in_struct:
  65. if re.match(r"\};", line):
  66. in_struct = False
  67. break
  68. if re.match(r"\s*(#|//)", line):
  69. continue
  70. 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)
  71. assert n, f"'{line}' does not match regex"
  72. methods.add(JniMethodBinding(name=n["method"], spec=n["spec"]))
  73. continue
  74. if methods:
  75. break
  76. if methods:
  77. break
  78. assert methods, f"Could not find methods for {kls} (table={table})"
  79. assert not in_struct
  80. assert kls not in bindings, f"{kls} must be unique in C sources"
  81. bindings[kls] = methods
  82. return bindings
  83. def collect_jni_bindings_from_java() -> dict[str, set[JniMethodBinding]]:
  84. bindings = {}
  85. for root, _, files in os.walk(JAVA_ROOT):
  86. for file in files:
  87. file_path = pathlib.Path(root) / file
  88. java_text = file_path.read_text()
  89. methods = set()
  90. 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):
  91. name = m["method"]
  92. ret = m["ret"]
  93. args = []
  94. args_str = m["args"].strip()
  95. if args_str:
  96. for a_s in args_str.split(","):
  97. atype_str, _ = a_s.strip().rsplit(" ")
  98. args.append(atype_str.strip())
  99. spec = java_method_to_jni_spec(ret=ret, args=args)
  100. methods.add(JniMethodBinding(name=name, spec=spec))
  101. if methods:
  102. relative_java_path = file_path.relative_to(JAVA_ROOT)
  103. relative_java_path_without_suffix = relative_java_path.with_suffix("")
  104. kls = "/".join(relative_java_path_without_suffix.parts)
  105. assert kls not in bindings, f"{kls} must be unique in JAVA sources"
  106. bindings[kls] = methods
  107. return bindings
  108. def print_error(*args):
  109. print("ERROR:", *args)
  110. def main():
  111. parser = argparse.ArgumentParser(allow_abbrev=False, description="Verify Android JNI bindings")
  112. args = parser.parse_args()
  113. bindings_from_c = collect_jni_bindings_from_c()
  114. bindings_from_java = collect_jni_bindings_from_java()
  115. all_ok = bindings_from_c == bindings_from_java
  116. if all_ok:
  117. print("OK")
  118. else:
  119. print("NOT OK")
  120. kls_c = set(bindings_from_c.keys())
  121. kls_java = set(bindings_from_java.keys())
  122. if kls_c != kls_java:
  123. only_c = kls_c - kls_java
  124. for c in only_c:
  125. print_error(f"Missing class in JAVA sources: {c}")
  126. only_java = kls_java - kls_c
  127. for c in only_java:
  128. print_error(f"Missing class in C sources: {c}")
  129. klasses = kls_c.union(kls_java)
  130. for kls in klasses:
  131. m_c = bindings_from_c.get(kls)
  132. m_j = bindings_from_java.get(kls)
  133. if m_c and m_j and m_c != m_j:
  134. m_only_c = m_c - m_j
  135. for c in m_only_c:
  136. print_error(f"{kls}: Binding only in C source: {c.name} {c.spec}")
  137. m_only_j = m_j - m_c
  138. for c in m_only_j:
  139. print_error(f"{kls}: Binding only in JAVA source: {c.name} {c.spec}")
  140. return 0 if all_ok else 1
  141. if __name__ == "__main__":
  142. raise SystemExit(main())