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