spine-runtimes/spine-cpp/spine-cpp-lite/spine-cpp-lite-codegen.py
Byeong Gwan 6e5da45fa3
[iOS] update Swift inteface for more fine grained control(like external atlas loading) (#2772)
* update Swift inteface for more fine grained control

- move instance method to static method which does not require spine cpp pointer
- implement equality and hashing based on cpp pointer so that it can be stored in collection and compare it
- expose Atlas image count property so that the user can load the atlas resource lazily

- User can now Create Atlas by using Altas static function, and fetch whole resource path by iterating count of atlas page count

* [iOS] fix wrong y-axis alignment
2025-04-08 11:51:32 +02:00

611 lines
22 KiB
Python

import re
import os
script_directory = os.path.dirname(os.path.abspath(__file__))
input_path = os.path.join(script_directory, 'spine-cpp-lite.h')
with open(input_path, 'r') as file:
file_contents = file.read()
supported_types_to_swift_types = {
'void *': 'UnsafeMutableRawPointer',
'const utf8 *': 'String?',
'uint64_t': 'UInt64',
'float *': 'Float?',
'float': 'Float',
'int32_t': 'Int32',
'utf8 *': 'String?',
'int32_t *': 'Int32?',
'uint16_t *': 'UInt16',
'spine_bool': 'Bool'
}
def read_spine_types(data):
types_start = data.find('// @start: opaque_types') + len('// @start: opaque_types')
types_end = data.find('// @end: paque_types')
types_section = data[types_start:types_end]
return re.findall(r'SPINE_OPAQUE_TYPE\(([^)]+)\)', types_section)
def read_spine_function_declarations(data):
declarations_start = data.find('// @start: function_declarations') + len('// @start: function_declarations')
declarations_end = data.find('// @end: function_declarations')
declarations_section = data[declarations_start:declarations_end]
lines = declarations_section.split('\n')
filtered_lines = []
ignore_next = False
next_returns_optional = False
for line in lines:
if ignore_next:
ignore_next = False
continue
line = line.strip()
if next_returns_optional:
next_returns_optional = False
line = line + "?"
if not line.strip().startswith('//') and line.strip() != '':
filtered_lines.append(line)
if line.startswith('//') and '@ignore' in line:
ignore_next = True
elif line.startswith('//') and '@optional' in line:
next_returns_optional = True
function_declaration = [
line.replace('SPINE_CPP_LITE_EXPORT', '').strip()
for line in filtered_lines
]
return function_declaration
def read_spine_enums(data):
enums_start = data.find('// @start: enums') + len('// @start: enums')
enums_end = data.find('// @end: enums')
enums_section = data[enums_start:enums_end]
return re.findall(r"typedef enum (\w+) \{", enums_section)
class SpineObject:
def __init__(self, name, functions):
self.name = name
self.functions = functions
self.function_names = {function.name for function in functions}
self.var_name = "wrappee"
def __str__(self):
return f"SpineObject: name: {self.name}, functions: {self.functions}"
class SpineFunction:
def __init__(self, return_type, name, parameters, returns_optional):
self.return_type = return_type
self.name = name
self.parameters = parameters
self.returns_optional = returns_optional
def isReturningSpineClass(self):
return self.return_type.startswith("spine_") and self.return_type != "spine_bool" and self.return_type not in enums
def __str__(self):
return f"SpineFunction(return_type: {self.return_type}, name: {self.name}, parameters: {self.parameters}, returns_optional: {self.returns_optional})"
def __repr__(self):
return self.__str__()
class SpineParam:
def __init__(self, type, name):
self.type = type
self.name = name
def isSpineClass(self):
return self.type.startswith("spine_") and self.type != "spine_bool" and self.type not in enums
def __str__(self):
return f"SpineParam(type: {self.type}, name: {self.name})"
def __repr__(self):
return self.__str__()
def parse_function_declaration(declaration):
returns_optional = declaration.endswith("?")
# Strip semicolon and extra whitespace
declaration = declaration.strip('?').strip(';').strip()
# Use regex to split the declaration into parts
# Regex explanation:
# ^([\w\s\*]+?)\s+ - Capture the return type, possibly including spaces and asterisks (non-greedy)
# ([\w]+) - Capture the function name (alphanumeric and underscores)
# \((.*)\) - Capture the argument list in entirety
match = re.match(r'^(\S.+?\s*\*?\s*)([\w]+)\s*\((.*)\)$', declaration)
if not match:
return "Invalid function declaration"
return_type, function_name, params = match.groups()
params = params.strip()
parameters = []
if params:
# Splitting each argument on comma
param_list = params.split(',')
for param in param_list:
param_parts = []
if '*' in param: # Split at the pointer and add it as a suffix to the type
param_parts = param.rsplit('*', 1)
param_parts[0] = param_parts[0] + '*'
else: # Assuming type and name are separated by space and taking the last space as the separator
param_parts = param.rsplit(' ', 1)
param_type, param_name = param_parts
spine_param = SpineParam(type = param_type.strip(), name = param_name.strip())
parameters.append(spine_param)
return SpineFunction(
return_type = return_type.strip(),
name = function_name.strip(),
parameters = parameters,
returns_optional = returns_optional
)
types = read_spine_types(file_contents)
function_declarations = read_spine_function_declarations(file_contents)
enums = read_spine_enums(file_contents)
sorted_types = sorted(types, key=len, reverse=True) # Sorted by legth descending so we can match longest prefix.
spine_functions = [
parse_function_declaration(function_declaration)
for function_declaration in function_declarations
]
objects = []
for type in sorted_types:
object_functions = []
hits = set() ## Keep track of hits and remove them for next object
for function_declaration in function_declarations:
spine_function = parse_function_declaration(function_declaration)
if spine_function.name.startswith(type):
hits.add(function_declaration);
object_functions.append(spine_function);
object = SpineObject(name = type, functions = object_functions);
objects.append(object)
function_declarations = [item for item in function_declarations if item not in hits]
def snake_to_camel(snake_str):
# Split the string by underscore
parts = snake_str.split('_')
# Return the first part lowercased and concatenate capitalized subsequent parts
return parts[0] + ''.join(word.capitalize() for word in parts[1:])
def snake_to_title(snake_str):
# Split the string at underscores
words = snake_str.split('_')
# Capitalize the first letter of each word
words = [word.capitalize() for word in words]
# Join the words into a single string without any separator
title_str = ''.join(words)
return title_str
inset = " "
class SwiftTypeWriter:
def __init__(self, type):
self.type = type
def write(self):
parameter_type = supported_types_to_swift_types.get(self.type)
if parameter_type is None:
parameter_type = snake_to_title(self.type.replace("spine_", ""))
if parameter_type.endswith(" *"):
parameter_type = f"{parameter_type[:-2]}"
return parameter_type
class SwiftParamWriter:
def __init__(self, param):
self.param = param
def write(self):
type = SwiftTypeWriter(type = self.param.type).write()
return f"{snake_to_camel(self.param.name)}: {type}"
class SwiftFunctionBodyWriter:
def __init__(self, spine_object, spine_function, is_setter, is_getter_optional):
self.spine_object = spine_object
self.spine_function = spine_function
self.is_setter = is_setter
self.is_getter_optional = is_getter_optional
def write(self):
body = ""
num_function_name = self.spine_function.name.replace("get_", "get_num_")
swift_return_type_is_array = "get_" in self.spine_function.name and num_function_name in self.spine_object.function_names
spine_params = self.spine_function.parameters;
body = ""
if "dispose" in self.spine_function.name:
body += self.write_dispose_call()
function_call = self.write_c_function_call(spine_params)
if swift_return_type_is_array:
body += self.write_array_call(num_function_name, function_call)
else:
if not self.spine_function.return_type == "void":
body += "return "
if self.spine_function.isReturningSpineClass():
function_prefix = f"{self.spine_object.name}_"
function_name = self.spine_function.name.replace(function_prefix, "", 1)
if "find_" in function_name or self.spine_function.returns_optional:
body += function_call
body += ".flatMap { .init($0"
if self.spine_function.return_type in enums:
body += ".rawValue"
body += ") }"
else:
body += ".init("
body += function_call
if self.spine_function.return_type in enums:
body += ".rawValue"
body += ")"
else:
body += function_call
if self.spine_function.return_type == "const utf8 *" or self.spine_function.return_type == "utf8 *":
body += ".flatMap { String(cString: $0) }"
if self.spine_function.return_type == "int32_t *" or self.spine_function.return_type == "float *":
body += ".flatMap { $0.pointee }"
return body
def write_c_function_call(self, spine_params):
function_call = ""
function_call += f"{self.spine_function.name}"
function_call += "("
# Replace name with ivar name
spine_params_with_ivar_name = spine_params
if spine_params_with_ivar_name and spine_params_with_ivar_name[0].type == self.spine_object.name:
spine_params_with_ivar_name[0].name = self.spine_object.var_name
if self.is_setter and len(spine_params_with_ivar_name) == 2:
spine_params_with_ivar_name[1].name = "newValue"
if self.is_getter_optional:
spine_params_with_ivar_name[1].name += "?"
swift_param_names = []
for idx, spine_param in enumerate(spine_params_with_ivar_name):
if spine_param.isSpineClass() and idx > 0:
swift_param_names.append(f"{spine_param.name}.wrappee")
elif spine_param.type == "spine_bool":
swift_param_names.append(f"{spine_param.name} ? -1 : 0")
else:
swift_param_names.append(spine_param.name)
function_call += ", ".join(swift_param_names)
function_call += ")"
if self.spine_function.return_type == "spine_bool":
function_call += " != 0"
return function_call
def write_array_spine_class(self, num_function_name, function_call):
array_call = f"let ptr = {function_call}"
array_call += "\n"
array_call += inset + inset
array_call += "guard let validPtr = ptr else { return [] }"
array_call += "\n"
array_call += inset + inset
array_call += f"let num = Int({num_function_name}({self.spine_object.var_name}))"
array_call += "\n"
array_call += inset + inset
array_call += "let buffer = UnsafeBufferPointer(start: validPtr, count: num)"
array_call += "\n"
array_call += inset + inset
array_call += "return buffer.compactMap {"
array_call += "\n"
array_call += inset + inset + inset
array_call += "$0.flatMap { .init($0) }"
array_call += "\n"
array_call += inset + inset
array_call += "}"
return array_call
def write_array_call(self, num_function_name, function_call):
if self.spine_function.isReturningSpineClass():
return self.write_array_spine_class(num_function_name, function_call)
array_call = f"let ptr = {function_call}"
array_call += "\n"
array_call += inset + inset
array_call += "guard let validPtr = ptr else { return [] }"
array_call += "\n"
array_call += inset + inset
array_call += f"let num = Int({num_function_name}({self.spine_object.var_name}))"
array_call += "\n"
array_call += inset + inset
array_call += "let buffer = UnsafeBufferPointer(start: validPtr, count: num)"
array_call += "\n"
array_call += inset + inset
array_call += "return Array(buffer)"
return array_call
def write_dispose_call(self):
dispose_body = "if disposed { return }"
dispose_body += "\n"
dispose_body += inset + inset
dispose_body += "disposed = true"
dispose_body += "\n"
dispose_body += inset + inset
return dispose_body
class SwiftFunctionWriter:
def __init__(self, spine_object, spine_function, spine_setter_function):
self.spine_object = spine_object
self.spine_function = spine_function
self.spine_setter_function = spine_setter_function
def write(self):
function_prefix = f"{self.spine_object.name}_"
function_name = self.spine_function.name.replace(function_prefix, "", 1)
is_getter = (function_name.startswith("get_") or function_name.startswith("is_")) and len(self.spine_function.parameters) < 2
num_function_name = self.spine_function.name.replace("get_", "get_num_")
swift_return_type_is_array = "get_" in self.spine_function.name and num_function_name in self.spine_object.function_names
swift_return_type_writer = SwiftTypeWriter(type = self.spine_function.return_type)
swift_return_type = swift_return_type_writer.write()
if swift_return_type_is_array:
swift_return_type = f"[{swift_return_type}]"
function_string = inset
if is_getter:
function_string += self.write_computed_property_signature(function_name, swift_return_type)
if self.spine_setter_function:
function_string += " {\n"
function_string += inset + inset
function_string += "get"
else:
function_string += self.write_method_signature(function_name, swift_return_type)
function_string += " {"
function_string += "\n"
function_string += inset + inset
if self.spine_setter_function:
function_string += inset
function_string += SwiftFunctionBodyWriter(spine_object = self.spine_object, spine_function = self.spine_function, is_setter=False, is_getter_optional=False).write()
if self.spine_setter_function:
function_string += "\n"
function_string += inset + inset + "}"
function_string += "\n"
function_string += inset + inset + "set {"
function_string += "\n"
function_string += inset + inset + inset
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()
function_string += "\n"
function_string += inset + inset + "}"
function_string += "\n"
function_string += inset + "}"
function_string += "\n"
return function_string
def write_computed_property_signature(self, function_name, swift_return_type):
property_name = snake_to_camel(function_name.replace("get_", ""))
property_string = f"public var {property_name}: {swift_return_type}"
if self.spine_function.returns_optional:
property_string += "?"
return property_string
def write_method_signature(self, function_name, swift_return_type):
function_string = ""
if not self.spine_function.return_type == "void":
function_string += "@discardableResult"
function_string += "\n"
function_string += inset
function_string += f"public func {snake_to_camel(function_name)}"
function_string += "("
spine_params = self.spine_function.parameters;
# Filter out ivar
if spine_params and spine_params[0].type == self.spine_object.name:
spine_params_without_ivar = spine_params[1:]
else:
function_string = function_string.replace("public func ", "public static func ")
spine_params_without_ivar = spine_params
swift_params = [
SwiftParamWriter(param = spine_param).write()
for spine_param in spine_params_without_ivar
]
function_string += ", ".join(swift_params)
function_string += ")"
if not self.spine_function.return_type == "void":
function_string += f" -> {swift_return_type}"
if "find_" in function_name or self.spine_function.returns_optional:
function_string += "?"
return function_string
class SwiftObjectWriter:
def __init__(self, spine_object):
self.spine_object = spine_object
def write(self):
ivar_type = self.spine_object.name
ivar_name = self.spine_object.var_name
class_name = snake_to_title(self.spine_object.name.replace("spine_", ""))
object_string = f"@objc(Spine{class_name})"
object_string += "\n"
object_string += "@objcMembers"
object_string += "\n"
object_string += f"public final class {class_name}: NSObject"
object_string += " {"
object_string += "\n"
object_string += "\n"
object_string += inset
object_string += f"internal let {ivar_name}: {ivar_type}"
object_string += "\n"
if any("dispose" in function_name for function_name in self.spine_object.function_names):
object_string += inset
object_string += f"internal var disposed = false"
object_string += "\n"
object_string += "\n"
object_string += inset
object_string += f"internal init(_ {ivar_name}: {ivar_type})"
object_string += " {"
object_string += "\n"
object_string += inset + inset
object_string += f"self.{ivar_name} = {ivar_name}"
object_string += "\n"
object_string += inset + inset
object_string += "super.init()"
object_string += "\n"
object_string += inset
object_string += "}"
object_string += "\n"
object_string += "\n"
object_string += inset
object_string += "public override func isEqual(_ object: Any?) -> Bool"
object_string += " {"
object_string += "\n"
object_string += inset + inset
object_string += f"guard let other = object as? {class_name} else {{ return false }}"
object_string += "\n"
object_string += inset + inset
object_string += f"return self.{ivar_name} == other.{ivar_name}"
object_string += "\n"
object_string += inset
object_string += "}"
object_string += "\n"
object_string += "\n"
object_string += inset
object_string += "public override var hash: Int"
object_string += " {"
object_string += "\n"
object_string += inset + inset
object_string += "var hasher = Hasher()"
object_string += "\n"
object_string += inset + inset
object_string += f"hasher.combine(self.{ivar_name})"
object_string += "\n"
object_string += inset + inset
object_string += "return hasher.finalize()"
object_string += "\n"
object_string += inset
object_string += "}"
object_string += "\n"
object_string += "\n"
filtered_spine_functions = [spine_function for spine_function in self.spine_object.functions if not "_get_num_" in spine_function.name]
spine_functions_by_name = {}
getter_names = []
setter_names = []
method_names = []
for spine_function in filtered_spine_functions:
spine_functions_by_name[spine_function.name] = spine_function
if ("_get_" in spine_function.name or "_is_" in spine_function.name) and len(spine_function.parameters) == 1:
getter_names.append(spine_function.name)
elif "_set_" in spine_function.name and len(spine_function.parameters) == 2:
setter_names.append(spine_function.name)
else:
method_names.append(spine_function.name)
get_set_pairs = []
for setter_name in setter_names:
getter_name_get = setter_name.replace("_set_", "_get_")
getter_name_is = setter_name.replace("_set_", "_is_")
if getter_name_get in getter_names:
getter_names.remove(getter_name_get)
get_set_pairs.append((getter_name_get, setter_name))
elif getter_name_is in getter_names:
getter_names.remove(getter_name_is)
get_set_pairs.append((getter_name_is, setter_name))
else:
method_names.append(setter_name) # Coul not find getter by name. Move to methods
# print(get_set_pairs)
for getter_name in getter_names:
spine_function = spine_functions_by_name[getter_name]
object_string += SwiftFunctionWriter(spine_object = self.spine_object, spine_function = spine_function, spine_setter_function=None).write()
object_string += "\n"
for get_set_pair in get_set_pairs:
getter_function = spine_functions_by_name[get_set_pair[0]]
setter_function = spine_functions_by_name[get_set_pair[1]]
object_string += SwiftFunctionWriter(spine_object = self.spine_object, spine_function = getter_function, spine_setter_function=setter_function).write()
object_string += "\n"
for method_name in method_names:
spine_function = spine_functions_by_name[method_name]
object_string += SwiftFunctionWriter(spine_object = self.spine_object, spine_function = spine_function, spine_setter_function=None).write()
object_string += "\n"
object_string += "}"
return object_string
class SwiftEnumWriter:
def __init__(self, spine_enum):
self.spine_enum = spine_enum
def write(self):
# TODO: Consider leaving spine prefix (objc) or map whole c enum to swift/objc compatible enum
return f"public typealias {snake_to_title(self.spine_enum.replace("spine_", ""))} = {self.spine_enum}"
print("import Foundation")
print("import SpineCppLite")
print("")
for enum in enums:
print(SwiftEnumWriter(spine_enum=enum).write())
print("")
for object in objects:
print(SwiftObjectWriter(spine_object = object).write())
print("")