1#!/usr/bin/env python3 2import sys 3import re 4import optparse 5from ctypes import * 6 7""" 8This script will use the prototypes from "checkdocs.py -s" to concoct 9a 1:1 Python wrapper for Allegro. 10""" 11 12 13class _AL_UTF8String: 14 pass 15 16 17class Allegro: 18 def __init__(self): 19 self.types = {} 20 self.functions = {} 21 self.constants = {} 22 23 def add_struct(self, name): 24 x = type(name, (Structure, ), {}) 25 self.types[name] = x 26 27 def add_union(self, name): 28 x = type(name, (Union, ), {}) 29 self.types[name] = x 30 31 def get_type(self, ptype): 32 conversion = { 33 "bool": c_bool, 34 "_Bool": c_bool, 35 "char": c_byte, 36 "unsignedchar": c_ubyte, 37 "int": c_int, 38 "unsigned": c_uint, 39 "unsignedint": c_uint, 40 "int16_t": c_int16, 41 "uint16_t": c_uint16, 42 "int32_t": c_int32, 43 "uint32_t": c_uint32, 44 "int64_t": c_int64, 45 "uint64_t": c_uint64, 46 "uintptr_t": c_void_p, 47 "intptr_t": c_void_p, 48 "GLuint": c_uint, 49 "unsignedlong": c_ulong, 50 "long": c_long, 51 "size_t": c_size_t, 52 "off_t": c_int64, 53 "time_t": c_int64, 54 "va_list": c_void_p, 55 "float": c_float, 56 "double": c_double, 57 "al_fixed": c_int, 58 "HWND": c_void_p, 59 "char*": _AL_UTF8String, 60 61 # hack: this probably shouldn't be in the public docs 62 "postprocess_callback_t": c_void_p, 63 } 64 65 ptype = re.sub(r"\b(struct|union)\b", "", ptype) 66 ptype = re.sub(r"\bconst\b", "", ptype) 67 ptype = re.sub(r"\bextern\b", "", ptype) 68 ptype = re.sub(r"\b__inline__\b", "", ptype) 69 ptype = re.sub(r"\s+", "", ptype) 70 71 if ptype.endswith("*"): 72 if ptype in conversion: 73 return conversion[ptype] 74 t = ptype[:-1] 75 if t in self.types: 76 return POINTER(self.types[t]) 77 return c_void_p 78 elif ptype in self.types: 79 return self.types[ptype] 80 else: 81 try: 82 return conversion[ptype] 83 except KeyError: 84 print("Type Error:" + str(ptype)) 85 return None 86 87 def parse_funcs(self, funcs): 88 """ 89 Go through all documented functions and add their prototypes 90 as Python functions. 91 92 The file should have been generated by Allegro's documentation 93 generation scripts. 94 """ 95 96 for func in funcs: 97 name, proto = func.split(":", 1) 98 if not name.startswith("al_"): 99 continue 100 proto = proto.strip() 101 name = name[:-2] 102 if proto.startswith("enum"): 103 continue 104 if proto.startswith("typedef"): 105 continue 106 if "=" in proto: 107 continue 108 if proto.startswith("#"): 109 continue 110 funcstart = proto.find(name) 111 funcend = funcstart + len(name) 112 ret = proto[:funcstart].rstrip() 113 params = proto[funcend:].strip(" ;") 114 if params[0] != "(" or params[-1] != ")": 115 print("Error:") 116 print(params) 117 continue 118 params2 = params[1:-1] 119 # remove callback argument lists 120 balance = 0 121 params = "" 122 for c in params2: 123 if c == ")": 124 balance -= 1 125 if balance == 0: 126 params += c 127 if c == "(": 128 balance += 1 129 params = params.split(",") 130 plist = [] 131 for param in params: 132 param = re.sub(r"\bconst\b", "", param) 133 param = param.strip() 134 if param == "void": 135 continue 136 if param == "": 137 continue 138 if param == "...": 139 continue 140 141 # treat arrays as a void pointer, for now 142 if param.endswith("]") or param.endswith("*"): 143 plist.append(c_void_p) 144 continue 145 146 # treat callbacks as a void pointer, for now 147 if param.endswith(")"): 148 plist.append(c_void_p) 149 continue 150 151 mob = re.match("^.*?(\w+)$", param) 152 if mob: 153 pnamepos = mob.start(1) 154 if pnamepos == 0: 155 # Seems the parameter is not named 156 pnamepos = len(param) 157 else: 158 print(params) 159 print(proto) 160 print("") 161 continue 162 ptype = param[:pnamepos] 163 ptype = self.get_type(ptype) 164 plist.append(ptype) 165 166 f = type("", (object, ), {"restype": c_int}) 167 if not ret.endswith("void"): 168 f.restype = self.get_type(ret) 169 try: 170 f.argtypes = plist 171 except TypeError as e: 172 print(e) 173 print(name) 174 print(plist) 175 self.functions[name] = f 176 177 def parse_protos(self, filename): 178 protos = [] 179 unions = [] 180 funcs = [] 181 182 # first pass: create all structs, but without fields 183 for line in open(filename): 184 name, proto = line.split(":", 1) 185 proto = proto.lstrip() 186 if name.endswith("()"): 187 funcs.append(line) 188 continue 189 # anonymous structs have no name at all 190 if name and not name.startswith("ALLEGRO_"): 191 continue 192 if name == "ALLEGRO_OGL_EXT_API": 193 continue 194 if proto.startswith("union") or\ 195 proto.startswith("typedef union"): 196 self.add_union(name) 197 unions.append((name, proto)) 198 elif proto.startswith("struct") or\ 199 proto.startswith("typedef struct"): 200 self.add_struct(name) 201 protos.append((name, proto)) 202 elif proto.startswith("enum") or\ 203 proto.startswith("typedef enum"): 204 if name: 205 self.types[name] = c_int 206 protos.append(("", proto)) 207 elif proto.startswith("#define"): 208 if not name.startswith("_") and not name.startswith("GL_"): 209 i = eval(proto.split(None, 2)[2]) 210 self.constants[name] = i 211 else: 212 # actual typedef 213 mob = re.match("typedef (.*) " + name, proto) 214 if mob: 215 t = mob.group(1) 216 self.types[name] = self.get_type(t.strip()) 217 else: 218 # Probably a function pointer 219 self.types[name] = c_void_p 220 221 protos += unions 222 223 # second pass: fill in fields 224 for name, proto in protos: 225 bo = proto.find("{") 226 if bo == -1: 227 continue 228 bc = proto.rfind("}") 229 braces = proto[bo + 1:bc] 230 231 if proto.startswith("enum") or \ 232 proto.startswith("typedef enum"): 233 234 fields = braces.split(",") 235 i = 0 236 for field in fields: 237 if "=" in field: 238 fname, val = field.split("=", 1) 239 fname = fname.strip() 240 # replace any 'X' (an integer value in C) with 241 # ord('X') to match up in Python 242 val = re.sub("('.')", "ord(\\1)", val) 243 try: 244 i = int(eval(val, globals(), self.constants)) 245 except NameError: 246 i = val 247 except Exception: 248 raise ValueError( 249 "Exception while parsing '{}'".format( 250 val)) 251 else: 252 fname = field.strip() 253 if not fname: 254 continue 255 self.constants[fname] = i 256 try: 257 i += 1 258 except TypeError: 259 pass 260 continue 261 262 balance = 0 263 fields = [""] 264 for c in braces: 265 if c == "{": 266 balance += 1 267 if c == "}": 268 balance -= 1 269 if c == ";" and balance == 0: 270 fields.append("") 271 else: 272 fields[-1] += c 273 274 flist = [] 275 for field in fields: 276 if not field: 277 continue 278 279 # add function pointer as void pointer 280 mob = re.match(".*?\(\*(\w+)\)", field) 281 if mob: 282 flist.append((mob.group(1), "c_void_p")) 283 continue 284 285 # add any pointer as void pointer 286 mob = re.match(".*?\*(\w+)$", field) 287 if mob: 288 flist.append((mob.group(1), "c_void_p")) 289 continue 290 291 # add an array 292 mob = re.match("(.*)\s+(\w+)\[(.*?)\]$", field) 293 if mob: 294 # this is all a hack 295 n = 0 296 ftype = mob.group(1) 297 if ftype.startswith("struct"): 298 if ftype == "struct {float axis[3];}": 299 t = "c_float * 3" 300 else: 301 print("Error: Can't parse " + ftype + " yet.") 302 t = None 303 else: 304 n = mob.group(3) 305 # something in A5 uses a 2d array 306 if "][" in n: 307 n = n.replace("][", " * ") 308 # something uses a division expression 309 if "/" in n: 310 n = "(" + n.replace("/", "//") + ")" 311 t = self.get_type(ftype).__name__ + " * " + n 312 fname = mob.group(2) 313 flist.append((fname, t)) 314 continue 315 316 vars = field.split(",") 317 mob = re.match("\s*(.*?)\s+(\w+)\s*$", vars[0]) 318 319 t = self.get_type(mob.group(1)) 320 vname = mob.group(2) 321 if t is not None and vname is not None: 322 flist.append((vname, t.__name__)) 323 for v in vars[1:]: 324 flist.append((v.strip(), t.__name__)) 325 else: 326 print("Error: " + str(vars)) 327 328 try: 329 self.types[name].my_fields = flist 330 except AttributeError: 331 print(name, flist) 332 333 self.parse_funcs(funcs) 334 335 336def main(): 337 p = optparse.OptionParser() 338 p.add_option("-o", "--output", help="location of generated file") 339 p.add_option("-p", "--protos", help="A file with all " + 340 "prototypes to generate Python wrappers for, one per line. " 341 "Generate it with docs/scripts/checkdocs.py -p") 342 p.add_option("-t", "--type", help="the library type to " + 343 "use, e.g. debug") 344 p.add_option("-v", "--version", help="the library version to " + 345 "use, e.g. 5.1") 346 options, args = p.parse_args() 347 348 if not options.protos: 349 p.print_help() 350 return 351 352 al = Allegro() 353 354 al.parse_protos(options.protos) 355 356 f = open(options.output, "w") if options.output else sys.stdout 357 358 release = options.type 359 version = options.version 360 f.write(r"""# Generated by generate_python_ctypes.py. 361import os, platform, sys 362from ctypes import * 363from ctypes.util import * 364 365# You must adjust this function to point ctypes to the A5 DLLs you are 366# distributing. 367_dlls = [] 368def _add_dll(name): 369 release = "%(release)s" 370 if os.name == "nt": 371 release = "%(release)s-%(version)s" 372 373 # Under Windows, DLLs are found in the current directory, so this 374 # would be an easy way to keep all your DLLs in a sub-folder. 375 376 # os.chdir("dlls") 377 378 path = find_library(name + release) 379 if not path: 380 if os.name == "mac": 381 path = name + release + ".dylib" 382 elif os.name == "nt": 383 path = name + release + ".dll" 384 elif os.name == "posix": 385 if platform.mac_ver()[0]: 386 path = name + release + ".dylib" 387 else: 388 path = "lib" + name + release + ".so" 389 else: 390 sys.stderr.write("Cannot find library " + name + "\n") 391 392 # In most cases, you actually don't want the above and instead 393 # use the exact filename within your game distribution, possibly 394 # even within a .zip file. 395 # if not os.path.exists(path): 396 # path = "dlls/" + path 397 398 try: 399 # RTLD_GLOBAL is required under OSX for some reason (?) 400 _dlls.append(CDLL(path, RTLD_GLOBAL)) 401 except OSError: 402 # No need to fail here, might just be one of the addons. 403 pass 404 405 # os.chdir("..") 406 407_add_dll("allegro") 408_add_dll("allegro_acodec") 409_add_dll("allegro_audio") 410_add_dll("allegro_primitives") 411_add_dll("allegro_color") 412_add_dll("allegro_font") 413_add_dll("allegro_ttf") 414_add_dll("allegro_image") 415_add_dll("allegro_dialog") 416_add_dll("allegro_memfile") 417_add_dll("allegro_physfs") 418_add_dll("allegro_shader") 419_add_dll("allegro_main") 420_add_dll("allegro_video") 421_add_dll("allegro_monolith") 422 423# We don't have information ready which A5 function is in which DLL, 424# so we just try them all. 425def _dll(func, ret, params): 426 for dll in _dlls: 427 try: 428 f = dll[func] 429 f.restype = ret 430 f.argtypes = params 431 if ret is _AL_UTF8String: 432 # ctypes cannot do parameter conversion of the return type for us 433 f.restype = c_char_p 434 if sys.version_info[0] > 2: return lambda *x: f(*x).decode("utf8") 435 return f 436 except AttributeError: pass 437 sys.stderr.write("Cannot find function " + func + "\n") 438 return lambda *args: None 439 440# In Python3, all Python strings are unicode so we have to convert to 441# UTF8 byte strings before passing to Allegro. 442if sys.version_info[0] > 2: 443 class _AL_UTF8String: 444 def from_param(x): 445 return x.encode("utf8") 446else: 447 _AL_UTF8String = c_char_p 448 449""" % locals()) 450 451 postpone = [] 452 453 for name, val in sorted(al.constants.items()): 454 try: 455 if isinstance(val, str): 456 val = int(eval(val, globals(), al.constants)) 457 f.write(name + " = " + str(val) + "\n") 458 except: 459 postpone.append((name, val)) 460 461 for name, val in postpone: 462 f.write(name + " = " + val + "\n") 463 464 structs = set() 465 466 # output everything except structs and unions 467 for name, x in sorted(al.types.items()): 468 if not name: 469 continue 470 base = x.__bases__[0] 471 if base != Structure and base != Union: 472 f.write(name + " = " + x.__name__ + "\n") 473 else: 474 structs.add(name) 475 476 # order structs and unions by their dependencies 477 structs_list = [] 478 479 remaining = set(structs) 480 while remaining: 481 for name in sorted(remaining): 482 ok = True 483 x = al.types[name] 484 if hasattr(x, "my_fields"): 485 for fname, ftype in x.my_fields: 486 if " " in ftype: 487 ftype = ftype.split()[0] 488 if ftype in structs and ftype in remaining: 489 ok = False 490 break 491 if ok: 492 structs_list.append(name) 493 remaining.remove(name) 494 495 for name in structs_list: 496 x = al.types[name] 497 base = x.__bases__[0] 498 f.write("class " + name + "(" + base.__name__ + "):\n") 499 500 501 if hasattr(x, "my_fields"): 502 f.write(" _fields_ = [\n") 503 for fname, ftype in x.my_fields: 504 f.write(" (\"" + fname + "\", " + ftype + "),\n") 505 f.write(" ]\n") 506 else: 507 f.write(" pass\n") 508 509 pt = POINTER(x) 510 f.write("%s = POINTER(%s)\n" % (pt.__name__, name)) 511 512 for name, x in sorted(al.functions.items()): 513 try: 514 line = name + " = _dll(\"" + name + "\", " 515 line += x.restype.__name__ + ", " 516 line += "[" + (", ".join([a.__name__ for a in x.argtypes])) +\ 517 "])\n" 518 f.write(line) 519 except AttributeError as e: 520 print("Ignoring " + name + " because of errors (" + str(e) + ").") 521 522 # some stuff the automated parser doesn't pick up 523 f.write(r""" 524ALLEGRO_VERSION_INT = \ 525 ((ALLEGRO_VERSION << 24) | (ALLEGRO_SUB_VERSION << 16) | \ 526 (ALLEGRO_WIP_VERSION << 8) | ALLEGRO_RELEASE_NUMBER) 527 """) 528 529 f.write(r""" 530# work around bug http://gcc.gnu.org/bugzilla/show_bug.cgi?id=36834 531if os.name == "nt": 532 def al_map_rgba_f(r, g, b, a): return ALLEGRO_COLOR(r, g, b, a) 533 def al_map_rgb_f(r, g, b): return ALLEGRO_COLOR(r, g, b, 1) 534 def al_map_rgba(r, g, b, a): 535 return ALLEGRO_COLOR(r / 255.0, g / 255.0, b / 255.0, a / 255.0) 536 def al_map_rgb(r, g, b): 537 return ALLEGRO_COLOR(r / 255.0, g / 255.0, b / 255.0, 1) 538 """) 539 540 f.write(""" 541def al_main(real_main, *args): 542 def python_callback(argc, argv): 543 real_main(*args) 544 return 0 545 cb = CFUNCTYPE(c_int, c_int, c_void_p)(python_callback) 546 al_run_main(0, 0, cb); 547""") 548 549 f.close() 550 551main() 552