spine-cpp-lite-codegen.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  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. body += inset + inset
  188. body += "}"
  189. else:
  190. if not self.spine_function.return_type == "void":
  191. body += "return "
  192. if self.spine_function.isReturningSpineClass():
  193. function_prefix = f"{self.spine_object.name}_"
  194. function_name = self.spine_function.name.replace(function_prefix, "", 1)
  195. if "find_" in function_name or self.spine_function.returns_optional:
  196. body += function_call
  197. body += ".flatMap { .init($0"
  198. if self.spine_function.return_type in enums:
  199. body += ".rawValue"
  200. body += ") }"
  201. else:
  202. body += ".init("
  203. body += function_call
  204. if self.spine_function.return_type in enums:
  205. body += ".rawValue"
  206. body += ")"
  207. else:
  208. body += function_call
  209. if self.spine_function.return_type == "const utf8 *" or self.spine_function.return_type == "utf8 *":
  210. body += ".flatMap { String(cString: $0) }"
  211. if self.spine_function.return_type == "int32_t *" or self.spine_function.return_type == "float *":
  212. body += ".flatMap { $0.pointee }"
  213. return body
  214. def write_c_function_call(self, spine_params):
  215. function_call = ""
  216. function_call += f"{self.spine_function.name}"
  217. function_call += "("
  218. # Replace name with ivar name
  219. spine_params_with_ivar_name = spine_params
  220. if spine_params_with_ivar_name and spine_params_with_ivar_name[0].type == self.spine_object.name:
  221. spine_params_with_ivar_name[0].name = self.spine_object.var_name
  222. if self.is_setter and len(spine_params_with_ivar_name) == 2:
  223. spine_params_with_ivar_name[1].name = "newValue"
  224. if self.is_getter_optional:
  225. spine_params_with_ivar_name[1].name += "?"
  226. swift_param_names = []
  227. for idx, spine_param in enumerate(spine_params_with_ivar_name):
  228. if spine_param.isSpineClass() and idx > 0:
  229. swift_param_names.append(f"{spine_param.name}.wrappee")
  230. elif spine_param.type == "spine_bool":
  231. swift_param_names.append(f"{spine_param.name} ? -1 : 0")
  232. else:
  233. swift_param_names.append(spine_param.name)
  234. function_call += ", ".join(swift_param_names)
  235. function_call += ")"
  236. if self.spine_function.return_type == "spine_bool":
  237. function_call += " != 0"
  238. return function_call
  239. def write_array_call(self, num_function_name, function_call):
  240. array_call = f"let num = Int({num_function_name}({self.spine_object.var_name}))"
  241. array_call += "\n"
  242. array_call += inset + inset
  243. array_call += f"let ptr = {function_call}"
  244. array_call += "\n"
  245. array_call += inset + inset
  246. array_call += "return (0..<num).compactMap {"
  247. array_call += "\n"
  248. array_call += inset + inset + inset
  249. if self.spine_function.isReturningSpineClass():
  250. array_call += "ptr?[$0].flatMap { .init($0) }"
  251. else:
  252. array_call += "ptr?[$0]"
  253. array_call += "\n"
  254. return array_call
  255. def write_dispose_call(self):
  256. dispose_body = "if disposed { return }"
  257. dispose_body += "\n"
  258. dispose_body += inset + inset
  259. dispose_body += "disposed = true"
  260. dispose_body += "\n"
  261. dispose_body += inset + inset
  262. return dispose_body
  263. class SwiftFunctionWriter:
  264. def __init__(self, spine_object, spine_function, spine_setter_function):
  265. self.spine_object = spine_object
  266. self.spine_function = spine_function
  267. self.spine_setter_function = spine_setter_function
  268. def write(self):
  269. function_prefix = f"{self.spine_object.name}_"
  270. function_name = self.spine_function.name.replace(function_prefix, "", 1)
  271. is_getter = (function_name.startswith("get_") or function_name.startswith("is_")) and len(self.spine_function.parameters) < 2
  272. num_function_name = self.spine_function.name.replace("get_", "get_num_")
  273. swift_return_type_is_array = "get_" in self.spine_function.name and num_function_name in self.spine_object.function_names
  274. swift_return_type_writer = SwiftTypeWriter(type = self.spine_function.return_type)
  275. swift_return_type = swift_return_type_writer.write()
  276. if swift_return_type_is_array:
  277. swift_return_type = f"[{swift_return_type}]"
  278. function_string = inset
  279. if is_getter:
  280. function_string += self.write_computed_property_signature(function_name, swift_return_type)
  281. if self.spine_setter_function:
  282. function_string += " {\n"
  283. function_string += inset + inset
  284. function_string += "get"
  285. else:
  286. function_string += self.write_method_signature(function_name, swift_return_type)
  287. function_string += " {"
  288. function_string += "\n"
  289. function_string += inset + inset
  290. if self.spine_setter_function:
  291. function_string += inset
  292. function_string += SwiftFunctionBodyWriter(spine_object = self.spine_object, spine_function = self.spine_function, is_setter=False, is_getter_optional=False).write()
  293. if self.spine_setter_function:
  294. function_string += "\n"
  295. function_string += inset + inset + "}"
  296. function_string += "\n"
  297. function_string += inset + inset + "set {"
  298. function_string += "\n"
  299. function_string += inset + inset + inset
  300. 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()
  301. function_string += "\n"
  302. function_string += inset + inset + "}"
  303. function_string += "\n"
  304. function_string += inset + "}"
  305. function_string += "\n"
  306. return function_string
  307. def write_computed_property_signature(self, function_name, swift_return_type):
  308. property_name = snake_to_camel(function_name.replace("get_", ""))
  309. property_string = f"public var {property_name}: {swift_return_type}"
  310. if self.spine_function.returns_optional:
  311. property_string += "?"
  312. return property_string
  313. def write_method_signature(self, function_name, swift_return_type):
  314. function_string = ""
  315. if not self.spine_function.return_type == "void":
  316. function_string += "@discardableResult"
  317. function_string += "\n"
  318. function_string += inset
  319. function_string += f"public func {snake_to_camel(function_name)}"
  320. function_string += "("
  321. spine_params = self.spine_function.parameters;
  322. # Filter out ivar
  323. if spine_params and spine_params[0].type == self.spine_object.name:
  324. spine_params_without_ivar = spine_params[1:]
  325. else:
  326. spine_params_without_ivar = spine_params
  327. swift_params = [
  328. SwiftParamWriter(param = spine_param).write()
  329. for spine_param in spine_params_without_ivar
  330. ]
  331. function_string += ", ".join(swift_params)
  332. function_string += ")"
  333. if not self.spine_function.return_type == "void":
  334. function_string += f" -> {swift_return_type}"
  335. if "find_" in function_name or self.spine_function.returns_optional:
  336. function_string += "?"
  337. return function_string
  338. class SwiftObjectWriter:
  339. def __init__(self, spine_object):
  340. self.spine_object = spine_object
  341. def write(self):
  342. ivar_type = self.spine_object.name
  343. ivar_name = self.spine_object.var_name
  344. class_name = snake_to_title(self.spine_object.name.replace("spine_", ""))
  345. object_string = f"@objc(Spine{class_name})"
  346. object_string += "\n"
  347. object_string += "@objcMembers"
  348. object_string += "\n"
  349. object_string += f"public final class {class_name}: NSObject"
  350. object_string += " {"
  351. object_string += "\n"
  352. object_string += "\n"
  353. object_string += inset
  354. object_string += f"internal let {ivar_name}: {ivar_type}"
  355. object_string += "\n"
  356. if any("dispose" in function_name for function_name in self.spine_object.function_names):
  357. object_string += inset
  358. object_string += f"internal var disposed = false"
  359. object_string += "\n"
  360. object_string += "\n"
  361. object_string += inset
  362. object_string += f"internal init(_ {ivar_name}: {ivar_type})"
  363. object_string += " {"
  364. object_string += "\n"
  365. object_string += inset + inset
  366. object_string += f"self.{ivar_name} = {ivar_name}"
  367. object_string += "\n"
  368. object_string += inset + inset
  369. object_string += "super.init()"
  370. object_string += "\n"
  371. object_string += inset
  372. object_string += "}"
  373. object_string += "\n"
  374. object_string += "\n"
  375. filtered_spine_functions = [spine_function for spine_function in self.spine_object.functions if not "_get_num_" in spine_function.name]
  376. spine_functions_by_name = {}
  377. getter_names = []
  378. setter_names = []
  379. method_names = []
  380. for spine_function in filtered_spine_functions:
  381. spine_functions_by_name[spine_function.name] = spine_function
  382. if ("_get_" in spine_function.name or "_is_" in spine_function.name) and len(spine_function.parameters) == 1:
  383. getter_names.append(spine_function.name)
  384. elif "_set_" in spine_function.name and len(spine_function.parameters) == 2:
  385. setter_names.append(spine_function.name)
  386. else:
  387. method_names.append(spine_function.name)
  388. get_set_pairs = []
  389. for setter_name in setter_names:
  390. getter_name_get = setter_name.replace("_set_", "_get_")
  391. getter_name_is = setter_name.replace("_set_", "_is_")
  392. if getter_name_get in getter_names:
  393. getter_names.remove(getter_name_get)
  394. get_set_pairs.append((getter_name_get, setter_name))
  395. elif getter_name_is in getter_names:
  396. getter_names.remove(getter_name_is)
  397. get_set_pairs.append((getter_name_is, setter_name))
  398. else:
  399. method_names.append(setter_name) # Coul not find getter by name. Move to methods
  400. # print(get_set_pairs)
  401. for getter_name in getter_names:
  402. spine_function = spine_functions_by_name[getter_name]
  403. object_string += SwiftFunctionWriter(spine_object = self.spine_object, spine_function = spine_function, spine_setter_function=None).write()
  404. object_string += "\n"
  405. for get_set_pair in get_set_pairs:
  406. getter_function = spine_functions_by_name[get_set_pair[0]]
  407. setter_function = spine_functions_by_name[get_set_pair[1]]
  408. object_string += SwiftFunctionWriter(spine_object = self.spine_object, spine_function = getter_function, spine_setter_function=setter_function).write()
  409. object_string += "\n"
  410. for method_name in method_names:
  411. spine_function = spine_functions_by_name[method_name]
  412. object_string += SwiftFunctionWriter(spine_object = self.spine_object, spine_function = spine_function, spine_setter_function=None).write()
  413. object_string += "\n"
  414. object_string += "}"
  415. return object_string
  416. class SwiftEnumWriter:
  417. def __init__(self, spine_enum):
  418. self.spine_enum = spine_enum
  419. def write(self):
  420. # TODO: Consider leaving spine prefix (objc) or map whole c enum to swift/objc compatible enum
  421. return f"public typealias {snake_to_title(self.spine_enum.replace("spine_", ""))} = {self.spine_enum}"
  422. print("import Foundation")
  423. print("import SpineCppLite")
  424. print("")
  425. for enum in enums:
  426. print(SwiftEnumWriter(spine_enum=enum).write())
  427. print("")
  428. for object in objects:
  429. print(SwiftObjectWriter(spine_object = object).write())
  430. print("")