spine-cpp-lite-codegen.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. import re
  2. import os
  3. script_directory = os.path.dirname(os.path.abspath(__file__))
  4. input_path = os.path.join(script_directory, 'spine-cpp-lite.h')
  5. with open(input_path, 'r') as file:
  6. file_contents = file.read()
  7. supported_types_to_swift_types = {
  8. 'void *': 'UnsafeMutableRawPointer',
  9. 'const utf8 *': 'String?',
  10. 'uint64_t': 'UInt64',
  11. 'float *': 'Float?',
  12. 'float': 'Float',
  13. 'int32_t': 'Int32',
  14. 'utf8 *': 'String?',
  15. 'int32_t *': 'Int32?',
  16. 'uint16_t *': 'UInt16',
  17. 'spine_bool': 'Bool'
  18. }
  19. def read_spine_types(data):
  20. types_start = data.find('// @start: opaque_types') + len('// @start: opaque_types')
  21. types_end = data.find('// @end: paque_types')
  22. types_section = data[types_start:types_end]
  23. return re.findall(r'SPINE_OPAQUE_TYPE\(([^)]+)\)', types_section)
  24. def read_spine_function_declarations(data):
  25. declarations_start = data.find('// @start: function_declarations') + len('// @start: function_declarations')
  26. declarations_end = data.find('// @end: function_declarations')
  27. declarations_section = data[declarations_start:declarations_end]
  28. lines = declarations_section.split('\n')
  29. filtered_lines = []
  30. ignore_next = False
  31. next_returns_optional = False
  32. for line in lines:
  33. if ignore_next:
  34. ignore_next = False
  35. continue
  36. line = line.strip()
  37. if next_returns_optional:
  38. next_returns_optional = False
  39. line = line + "?"
  40. if not line.strip().startswith('//') and line.strip() != '':
  41. filtered_lines.append(line)
  42. if line.startswith('//') and '@ignore' in line:
  43. ignore_next = True
  44. elif line.startswith('//') and '@optional' in line:
  45. next_returns_optional = True
  46. function_declaration = [
  47. line.replace('SPINE_CPP_LITE_EXPORT', '').strip()
  48. for line in filtered_lines
  49. ]
  50. return function_declaration
  51. def read_spine_enums(data):
  52. enums_start = data.find('// @start: enums') + len('// @start: enums')
  53. enums_end = data.find('// @end: enums')
  54. enums_section = data[enums_start:enums_end]
  55. return re.findall(r"typedef enum (\w+) \{", enums_section)
  56. class SpineObject:
  57. def __init__(self, name, functions):
  58. self.name = name
  59. self.functions = functions
  60. self.function_names = {function.name for function in functions}
  61. self.var_name = "wrappee"
  62. def __str__(self):
  63. return f"SpineObject: name: {self.name}, functions: {self.functions}"
  64. class SpineFunction:
  65. def __init__(self, return_type, name, parameters, returns_optional):
  66. self.return_type = return_type
  67. self.name = name
  68. self.parameters = parameters
  69. self.returns_optional = returns_optional
  70. def isReturningSpineClass(self):
  71. return self.return_type.startswith("spine_") and self.return_type != "spine_bool" and self.return_type not in enums
  72. def __str__(self):
  73. return f"SpineFunction(return_type: {self.return_type}, name: {self.name}, parameters: {self.parameters}, returns_optional: {self.returns_optional})"
  74. def __repr__(self):
  75. return self.__str__()
  76. class SpineParam:
  77. def __init__(self, type, name):
  78. self.type = type
  79. self.name = name
  80. def isSpineClass(self):
  81. return self.type.startswith("spine_") and self.type != "spine_bool" and self.type not in enums
  82. def __str__(self):
  83. return f"SpineParam(type: {self.type}, name: {self.name})"
  84. def __repr__(self):
  85. return self.__str__()
  86. def parse_function_declaration(declaration):
  87. returns_optional = declaration.endswith("?")
  88. # Strip semicolon and extra whitespace
  89. declaration = declaration.strip('?').strip(';').strip()
  90. # Use regex to split the declaration into parts
  91. # Regex explanation:
  92. # ^([\w\s\*]+?)\s+ - Capture the return type, possibly including spaces and asterisks (non-greedy)
  93. # ([\w]+) - Capture the function name (alphanumeric and underscores)
  94. # \((.*)\) - Capture the argument list in entirety
  95. match = re.match(r'^(\S.+?\s*\*?\s*)([\w]+)\s*\((.*)\)$', declaration)
  96. if not match:
  97. return "Invalid function declaration"
  98. return_type, function_name, params = match.groups()
  99. params = params.strip()
  100. parameters = []
  101. if params:
  102. # Splitting each argument on comma
  103. param_list = params.split(',')
  104. for param in param_list:
  105. param_parts = []
  106. if '*' in param: # Split at the pointer and add it as a suffix to the type
  107. param_parts = param.rsplit('*', 1)
  108. param_parts[0] = param_parts[0] + '*'
  109. else: # Assuming type and name are separated by space and taking the last space as the separator
  110. param_parts = param.rsplit(' ', 1)
  111. param_type, param_name = param_parts
  112. spine_param = SpineParam(type = param_type.strip(), name = param_name.strip())
  113. parameters.append(spine_param)
  114. return SpineFunction(
  115. return_type = return_type.strip(),
  116. name = function_name.strip(),
  117. parameters = parameters,
  118. returns_optional = returns_optional
  119. )
  120. types = read_spine_types(file_contents)
  121. function_declarations = read_spine_function_declarations(file_contents)
  122. enums = read_spine_enums(file_contents)
  123. sorted_types = sorted(types, key=len, reverse=True) # Sorted by legth descending so we can match longest prefix.
  124. spine_functions = [
  125. parse_function_declaration(function_declaration)
  126. for function_declaration in function_declarations
  127. ]
  128. objects = []
  129. for type in sorted_types:
  130. object_functions = []
  131. hits = set() ## Keep track of hits and remove them for next object
  132. for function_declaration in function_declarations:
  133. spine_function = parse_function_declaration(function_declaration)
  134. if spine_function.name.startswith(type):
  135. hits.add(function_declaration);
  136. object_functions.append(spine_function);
  137. object = SpineObject(name = type, functions = object_functions);
  138. objects.append(object)
  139. function_declarations = [item for item in function_declarations if item not in hits]
  140. def snake_to_camel(snake_str):
  141. # Split the string by underscore
  142. parts = snake_str.split('_')
  143. # Return the first part lowercased and concatenate capitalized subsequent parts
  144. return parts[0] + ''.join(word.capitalize() for word in parts[1:])
  145. def snake_to_title(snake_str):
  146. # Split the string at underscores
  147. words = snake_str.split('_')
  148. # Capitalize the first letter of each word
  149. words = [word.capitalize() for word in words]
  150. # Join the words into a single string without any separator
  151. title_str = ''.join(words)
  152. return title_str
  153. inset = " "
  154. class SwiftTypeWriter:
  155. def __init__(self, type):
  156. self.type = type
  157. def write(self):
  158. parameter_type = supported_types_to_swift_types.get(self.type)
  159. if parameter_type is None:
  160. parameter_type = snake_to_title(self.type.replace("spine_", ""))
  161. if parameter_type.endswith(" *"):
  162. parameter_type = f"{parameter_type[:-2]}"
  163. return parameter_type
  164. class SwiftParamWriter:
  165. def __init__(self, param):
  166. self.param = param
  167. def write(self):
  168. type = SwiftTypeWriter(type = self.param.type).write()
  169. return f"{snake_to_camel(self.param.name)}: {type}"
  170. class SwiftFunctionBodyWriter:
  171. def __init__(self, spine_object, spine_function, is_setter, is_getter_optional):
  172. self.spine_object = spine_object
  173. self.spine_function = spine_function
  174. self.is_setter = is_setter
  175. self.is_getter_optional = is_getter_optional
  176. def write(self):
  177. body = ""
  178. num_function_name = self.spine_function.name.replace("get_", "get_num_")
  179. swift_return_type_is_array = "get_" in self.spine_function.name and num_function_name in self.spine_object.function_names
  180. spine_params = self.spine_function.parameters;
  181. body = ""
  182. if "dispose" in self.spine_function.name:
  183. body += self.write_dispose_call()
  184. function_call = self.write_c_function_call(spine_params)
  185. if swift_return_type_is_array:
  186. body += self.write_array_call(num_function_name, function_call)
  187. else:
  188. if not self.spine_function.return_type == "void":
  189. body += "return "
  190. if self.spine_function.isReturningSpineClass():
  191. function_prefix = f"{self.spine_object.name}_"
  192. function_name = self.spine_function.name.replace(function_prefix, "", 1)
  193. if "find_" in function_name or self.spine_function.returns_optional:
  194. body += function_call
  195. body += ".flatMap { .init($0"
  196. if self.spine_function.return_type in enums:
  197. body += ".rawValue"
  198. body += ") }"
  199. else:
  200. body += ".init("
  201. body += function_call
  202. if self.spine_function.return_type in enums:
  203. body += ".rawValue"
  204. body += ")"
  205. else:
  206. body += function_call
  207. if self.spine_function.return_type == "const utf8 *" or self.spine_function.return_type == "utf8 *":
  208. body += ".flatMap { String(cString: $0) }"
  209. if self.spine_function.return_type == "int32_t *" or self.spine_function.return_type == "float *":
  210. body += ".flatMap { $0.pointee }"
  211. return body
  212. def write_c_function_call(self, spine_params):
  213. function_call = ""
  214. function_call += f"{self.spine_function.name}"
  215. function_call += "("
  216. # Replace name with ivar name
  217. spine_params_with_ivar_name = spine_params
  218. if spine_params_with_ivar_name and spine_params_with_ivar_name[0].type == self.spine_object.name:
  219. spine_params_with_ivar_name[0].name = self.spine_object.var_name
  220. if self.is_setter and len(spine_params_with_ivar_name) == 2:
  221. spine_params_with_ivar_name[1].name = "newValue"
  222. if self.is_getter_optional:
  223. spine_params_with_ivar_name[1].name += "?"
  224. swift_param_names = []
  225. for idx, spine_param in enumerate(spine_params_with_ivar_name):
  226. if spine_param.isSpineClass() and idx > 0:
  227. swift_param_names.append(f"{spine_param.name}.wrappee")
  228. elif spine_param.type == "spine_bool":
  229. swift_param_names.append(f"{spine_param.name} ? -1 : 0")
  230. else:
  231. swift_param_names.append(spine_param.name)
  232. function_call += ", ".join(swift_param_names)
  233. function_call += ")"
  234. if self.spine_function.return_type == "spine_bool":
  235. function_call += " != 0"
  236. return function_call
  237. def write_array_spine_class(self, num_function_name, function_call):
  238. array_call = f"let ptr = {function_call}"
  239. array_call += "\n"
  240. array_call += inset + inset
  241. array_call += "guard let validPtr = ptr else { return [] }"
  242. array_call += "\n"
  243. array_call += inset + inset
  244. array_call += f"let num = Int({num_function_name}({self.spine_object.var_name}))"
  245. array_call += "\n"
  246. array_call += inset + inset
  247. array_call += "let buffer = UnsafeBufferPointer(start: validPtr, count: num)"
  248. array_call += "\n"
  249. array_call += inset + inset
  250. array_call += "return buffer.compactMap {"
  251. array_call += "\n"
  252. array_call += inset + inset + inset
  253. array_call += "$0.flatMap { .init($0) }"
  254. array_call += "\n"
  255. array_call += inset + inset
  256. array_call += "}"
  257. return array_call
  258. def write_array_call(self, num_function_name, function_call):
  259. if self.spine_function.isReturningSpineClass():
  260. return self.write_array_spine_class(num_function_name, function_call)
  261. array_call = f"let ptr = {function_call}"
  262. array_call += "\n"
  263. array_call += inset + inset
  264. array_call += "guard let validPtr = ptr else { return [] }"
  265. array_call += "\n"
  266. array_call += inset + inset
  267. array_call += f"let num = Int({num_function_name}({self.spine_object.var_name}))"
  268. array_call += "\n"
  269. array_call += inset + inset
  270. array_call += "let buffer = UnsafeBufferPointer(start: validPtr, count: num)"
  271. array_call += "\n"
  272. array_call += inset + inset
  273. array_call += "return Array(buffer)"
  274. return array_call
  275. def write_dispose_call(self):
  276. dispose_body = "if disposed { return }"
  277. dispose_body += "\n"
  278. dispose_body += inset + inset
  279. dispose_body += "disposed = true"
  280. dispose_body += "\n"
  281. dispose_body += inset + inset
  282. return dispose_body
  283. class SwiftFunctionWriter:
  284. def __init__(self, spine_object, spine_function, spine_setter_function):
  285. self.spine_object = spine_object
  286. self.spine_function = spine_function
  287. self.spine_setter_function = spine_setter_function
  288. def write(self):
  289. function_prefix = f"{self.spine_object.name}_"
  290. function_name = self.spine_function.name.replace(function_prefix, "", 1)
  291. is_getter = (function_name.startswith("get_") or function_name.startswith("is_")) and len(self.spine_function.parameters) < 2
  292. num_function_name = self.spine_function.name.replace("get_", "get_num_")
  293. swift_return_type_is_array = "get_" in self.spine_function.name and num_function_name in self.spine_object.function_names
  294. swift_return_type_writer = SwiftTypeWriter(type = self.spine_function.return_type)
  295. swift_return_type = swift_return_type_writer.write()
  296. if swift_return_type_is_array:
  297. swift_return_type = f"[{swift_return_type}]"
  298. function_string = inset
  299. if is_getter:
  300. function_string += self.write_computed_property_signature(function_name, swift_return_type)
  301. if self.spine_setter_function:
  302. function_string += " {\n"
  303. function_string += inset + inset
  304. function_string += "get"
  305. else:
  306. function_string += self.write_method_signature(function_name, swift_return_type)
  307. function_string += " {"
  308. function_string += "\n"
  309. function_string += inset + inset
  310. if self.spine_setter_function:
  311. function_string += inset
  312. function_string += SwiftFunctionBodyWriter(spine_object = self.spine_object, spine_function = self.spine_function, is_setter=False, is_getter_optional=False).write()
  313. if self.spine_setter_function:
  314. function_string += "\n"
  315. function_string += inset + inset + "}"
  316. function_string += "\n"
  317. function_string += inset + inset + "set {"
  318. function_string += "\n"
  319. function_string += inset + inset + inset
  320. function_string += SwiftFunctionBodyWriter(spine_object = self.spine_object, spine_function = self.spine_setter_function, is_setter=True, is_getter_optional=self.spine_function.returns_optional).write()
  321. function_string += "\n"
  322. function_string += inset + inset + "}"
  323. function_string += "\n"
  324. function_string += inset + "}"
  325. function_string += "\n"
  326. return function_string
  327. def write_computed_property_signature(self, function_name, swift_return_type):
  328. property_name = snake_to_camel(function_name.replace("get_", ""))
  329. property_string = f"public var {property_name}: {swift_return_type}"
  330. if self.spine_function.returns_optional:
  331. property_string += "?"
  332. return property_string
  333. def write_method_signature(self, function_name, swift_return_type):
  334. function_string = ""
  335. if not self.spine_function.return_type == "void":
  336. function_string += "@discardableResult"
  337. function_string += "\n"
  338. function_string += inset
  339. function_string += f"public func {snake_to_camel(function_name)}"
  340. function_string += "("
  341. spine_params = self.spine_function.parameters;
  342. # Filter out ivar
  343. if spine_params and spine_params[0].type == self.spine_object.name:
  344. spine_params_without_ivar = spine_params[1:]
  345. else:
  346. function_string = function_string.replace("public func ", "public static func ")
  347. spine_params_without_ivar = spine_params
  348. swift_params = [
  349. SwiftParamWriter(param = spine_param).write()
  350. for spine_param in spine_params_without_ivar
  351. ]
  352. function_string += ", ".join(swift_params)
  353. function_string += ")"
  354. if not self.spine_function.return_type == "void":
  355. function_string += f" -> {swift_return_type}"
  356. if "find_" in function_name or self.spine_function.returns_optional:
  357. function_string += "?"
  358. return function_string
  359. class SwiftObjectWriter:
  360. def __init__(self, spine_object):
  361. self.spine_object = spine_object
  362. def write(self):
  363. ivar_type = self.spine_object.name
  364. ivar_name = self.spine_object.var_name
  365. class_name = snake_to_title(self.spine_object.name.replace("spine_", ""))
  366. object_string = f"@objc(Spine{class_name})"
  367. object_string += "\n"
  368. object_string += "@objcMembers"
  369. object_string += "\n"
  370. object_string += f"public final class {class_name}: NSObject"
  371. object_string += " {"
  372. object_string += "\n"
  373. object_string += "\n"
  374. object_string += inset
  375. object_string += f"internal let {ivar_name}: {ivar_type}"
  376. object_string += "\n"
  377. if any("dispose" in function_name for function_name in self.spine_object.function_names):
  378. object_string += inset
  379. object_string += f"internal var disposed = false"
  380. object_string += "\n"
  381. object_string += "\n"
  382. object_string += inset
  383. object_string += f"internal init(_ {ivar_name}: {ivar_type})"
  384. object_string += " {"
  385. object_string += "\n"
  386. object_string += inset + inset
  387. object_string += f"self.{ivar_name} = {ivar_name}"
  388. object_string += "\n"
  389. object_string += inset + inset
  390. object_string += "super.init()"
  391. object_string += "\n"
  392. object_string += inset
  393. object_string += "}"
  394. object_string += "\n"
  395. object_string += "\n"
  396. object_string += inset
  397. object_string += "public override func isEqual(_ object: Any?) -> Bool"
  398. object_string += " {"
  399. object_string += "\n"
  400. object_string += inset + inset
  401. object_string += f"guard let other = object as? {class_name} else {{ return false }}"
  402. object_string += "\n"
  403. object_string += inset + inset
  404. object_string += f"return self.{ivar_name} == other.{ivar_name}"
  405. object_string += "\n"
  406. object_string += inset
  407. object_string += "}"
  408. object_string += "\n"
  409. object_string += "\n"
  410. object_string += inset
  411. object_string += "public override var hash: Int"
  412. object_string += " {"
  413. object_string += "\n"
  414. object_string += inset + inset
  415. object_string += "var hasher = Hasher()"
  416. object_string += "\n"
  417. object_string += inset + inset
  418. object_string += f"hasher.combine(self.{ivar_name})"
  419. object_string += "\n"
  420. object_string += inset + inset
  421. object_string += "return hasher.finalize()"
  422. object_string += "\n"
  423. object_string += inset
  424. object_string += "}"
  425. object_string += "\n"
  426. object_string += "\n"
  427. filtered_spine_functions = [spine_function for spine_function in self.spine_object.functions if not "_get_num_" in spine_function.name]
  428. spine_functions_by_name = {}
  429. getter_names = []
  430. setter_names = []
  431. method_names = []
  432. for spine_function in filtered_spine_functions:
  433. spine_functions_by_name[spine_function.name] = spine_function
  434. if ("_get_" in spine_function.name or "_is_" in spine_function.name) and len(spine_function.parameters) == 1:
  435. getter_names.append(spine_function.name)
  436. elif "_set_" in spine_function.name and len(spine_function.parameters) == 2:
  437. setter_names.append(spine_function.name)
  438. else:
  439. method_names.append(spine_function.name)
  440. get_set_pairs = []
  441. for setter_name in setter_names:
  442. getter_name_get = setter_name.replace("_set_", "_get_")
  443. getter_name_is = setter_name.replace("_set_", "_is_")
  444. if getter_name_get in getter_names:
  445. getter_names.remove(getter_name_get)
  446. get_set_pairs.append((getter_name_get, setter_name))
  447. elif getter_name_is in getter_names:
  448. getter_names.remove(getter_name_is)
  449. get_set_pairs.append((getter_name_is, setter_name))
  450. else:
  451. method_names.append(setter_name) # Coul not find getter by name. Move to methods
  452. # print(get_set_pairs)
  453. for getter_name in getter_names:
  454. spine_function = spine_functions_by_name[getter_name]
  455. object_string += SwiftFunctionWriter(spine_object = self.spine_object, spine_function = spine_function, spine_setter_function=None).write()
  456. object_string += "\n"
  457. for get_set_pair in get_set_pairs:
  458. getter_function = spine_functions_by_name[get_set_pair[0]]
  459. setter_function = spine_functions_by_name[get_set_pair[1]]
  460. object_string += SwiftFunctionWriter(spine_object = self.spine_object, spine_function = getter_function, spine_setter_function=setter_function).write()
  461. object_string += "\n"
  462. for method_name in method_names:
  463. spine_function = spine_functions_by_name[method_name]
  464. object_string += SwiftFunctionWriter(spine_object = self.spine_object, spine_function = spine_function, spine_setter_function=None).write()
  465. object_string += "\n"
  466. object_string += "}"
  467. return object_string
  468. class SwiftEnumWriter:
  469. def __init__(self, spine_enum):
  470. self.spine_enum = spine_enum
  471. def write(self):
  472. # TODO: Consider leaving spine prefix (objc) or map whole c enum to swift/objc compatible enum
  473. return f"public typealias {snake_to_title(self.spine_enum.replace("spine_", ""))} = {self.spine_enum}"
  474. print("import Foundation")
  475. print("import SpineCppLite")
  476. print("")
  477. for enum in enums:
  478. print(SwiftEnumWriter(spine_enum=enum).write())
  479. print("")
  480. for object in objects:
  481. print(SwiftObjectWriter(spine_object = object).write())
  482. print("")