python-ctypes: generate classes for enum typedefs
[vlc.git] / bindings / python-ctypes / generate.py
1 #! /usr/bin/python
2 debug=False
3
4 #
5 # Code generator for python ctypes bindings for VLC
6 # Copyright (C) 2009 the VideoLAN team
7 # $Id: $
8 #
9 # Authors: Olivier Aubert <olivier.aubert at liris.cnrs.fr>
10 #
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program; if not, write to the Free Software
23 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
24 #
25
26 """This module parses VLC public API include files and generates
27 corresponding python/ctypes code. Moreover, it generates class
28 wrappers for most methods.
29 """
30
31 import sys
32 import re
33 import time
34 import operator
35 import itertools
36
37 # DefaultDict from ASPN python cookbook
38 import copy
39 class DefaultDict(dict):
40     """Dictionary with a default value for unknown keys."""
41     def __init__(self, default=None, **items):
42         dict.__init__(self, **items)
43         self.default = default
44
45     def __getitem__(self, key):
46         if key in self:
47             return self.get(key)
48         else:
49             ## Need copy in case self.default is something like []
50             return self.setdefault(key, copy.deepcopy(self.default))
51
52     def __copy__(self):
53         return DefaultDict(self.default, **self)
54
55 # Methods not decorated/not referenced
56 blacklist=[
57     "libvlc_exception_raise",
58     "libvlc_exception_raised",
59     "libvlc_exception_get_message",
60     "libvlc_get_vlc_instance",
61
62     "libvlc_media_add_option_flag",
63     "libvlc_media_list_view_index_of_item",
64     "libvlc_media_list_view_insert_at_index",
65     "libvlc_media_list_view_remove_at_index",
66     "libvlc_media_list_view_add_item",
67
68     # In svn but not in current 1.0.0
69     'libvlc_video_set_deinterlace',
70     'libvlc_video_get_marquee_option_as_int',
71     'libvlc_video_get_marquee_option_as_string',
72     'libvlc_video_set_marquee_option_as_int',
73     'libvlc_video_set_marquee_option_as_string',
74     'libvlc_vlm_get_event_manager',
75
76     'mediacontrol_PlaylistSeq__free',
77
78     # TODO
79     "libvlc_event_detach",
80     "libvlc_event_attach",
81     ]
82
83 # Precompiled regexps
84 api_re=re.compile('VLC_PUBLIC_API\s+(\S+\s+.+?)\s*\(\s*(.+?)\s*\)')
85 param_re=re.compile('\s*(const\s*|unsigned\s*|struct\s*)?(\S+\s*\**)\s+(.+)')
86 paramlist_re=re.compile('\s*,\s*')
87 comment_re=re.compile('\\param\s+(\S+)')
88 python_param_re=re.compile('(@param\s+\S+)(.+)')
89 forward_re=re.compile('.+\(\s*(.+?)\s*\)(\s*\S+)')
90 enum_re=re.compile('typedef\s+(enum)\s*(\S+\s*)?\{\s*(.+)\s*\}\s*(\S+);')
91
92 # Definition of parameter passing mode for types.  This should not be
93 # hardcoded this way, but works alright ATM.
94 parameter_passing=DefaultDict(default=1)
95 parameter_passing['libvlc_exception_t*']=3
96
97 # C-type to ctypes/python type conversion.
98 # Note that enum types conversions are generated (cf convert_enum_names)
99 typ2class={
100     'libvlc_exception_t*': 'ctypes.POINTER(VLCException)',
101
102     'libvlc_media_player_t*': 'MediaPlayer',
103     'libvlc_instance_t*': 'Instance',
104     'libvlc_media_t*': 'Media',
105     'libvlc_log_t*': 'Log',
106     'libvlc_log_iterator_t*': 'LogIterator',
107     'libvlc_log_message_t*': 'LogMessage',
108     'libvlc_event_type_t': 'EventType',
109     'libvlc_event_manager_t*': 'EventManager',
110     'libvlc_media_discoverer_t*': 'MediaDiscoverer',
111     'libvlc_media_library_t*': 'MediaLibrary',
112     'libvlc_media_list_t*': 'MediaList',
113     'libvlc_media_list_player_t*': 'MediaListPlayer',
114     'libvlc_media_list_view_t*': 'MediaListView',
115     'libvlc_track_description_t*': 'TrackDescription',
116     'libvlc_audio_output_t*': 'AudioOutput',
117
118     'mediacontrol_Instance*': 'MediaControl',
119     'mediacontrol_Exception*': 'MediaControlException',
120     'mediacontrol_RGBPicture*': 'RGBPicture',
121     'mediacontrol_PlaylistSeq*': 'MediaControlPlaylistSeq',
122     'mediacontrol_Position*': 'MediaControlPosition',
123     'mediacontrol_StreamInformation*': 'MediaControlStreamInformation',
124     'WINDOWHANDLE': 'ctypes.c_ulong',
125
126     'void': 'None',
127     'void*': 'ctypes.c_void_p',
128     'short': 'ctypes.c_short',
129     'char*': 'ctypes.c_char_p',
130     'char**': 'ListPOINTER(ctypes.c_char_p)',
131     'uint32_t': 'ctypes.c_uint',
132     'float': 'ctypes.c_float',
133     'unsigned': 'ctypes.c_uint',
134     'int': 'ctypes.c_int',
135     '...': 'FIXMEva_list',
136     'libvlc_callback_t': 'FIXMEcallback',
137     'libvlc_time_t': 'ctypes.c_longlong',
138     }
139
140 # Defined python classes, i.e. classes for which we want to generate
141 # class wrappers around libvlc functions
142 defined_classes=(
143     'MediaPlayer',
144     'Instance',
145     'Media',
146     'Log',
147     'LogIterator',
148     #'LogMessage',
149     'EventType',
150     'EventManager',
151     'MediaDiscoverer',
152     'MediaLibrary',
153     'MediaList',
154     'MediaListPlayer',
155     'MediaListView',
156     'TrackDescription',
157     'AudioOutput',
158     'MediaControl',
159     #'RGBPicture',
160     #'MediaControlPosition',
161     #'MediaControlStreamInformation',
162     )
163
164 # Definition of prefixes that we can strip from method names when
165 # wrapping them into class methods
166 prefixes=dict( (v, k[:-2]) for (k, v) in typ2class.iteritems() if  v in defined_classes )
167 prefixes['MediaControl']='mediacontrol_'
168
169 def parse_param(s):
170     """Parse a C parameter expression.
171
172     It is used to parse both the type/name for methods, and type/name
173     for their parameters.
174
175     It returns a tuple (type, name).
176     """
177     s=s.strip()
178     s=s.replace('const', '')
179     if 'VLC_FORWARD' in s:
180         m=forward_re.match(s)
181         s=m.group(1)+m.group(2)
182     m=param_re.search(s)
183     if m:
184         const, typ, name=m.groups()
185         while name.startswith('*'):
186             typ += '*'
187             name=name[1:]
188         if name == 'const*':
189             # K&R definition: const char * const*
190             name=''
191         typ=typ.replace(' ', '')
192         return typ, name
193     else:
194         # K&R definition: only type
195         return s.replace(' ', ''), ''
196
197 def generate_header(classes=None):
198     """Generate header code.
199     """
200     f=open('header.py', 'r')
201     for l in f:
202         if 'build_date' in l:
203             print 'build_date="%s"' % time.ctime()
204         else:
205             print l,
206     f.close()
207
208 def convert_enum_names(enums):
209     res={}
210     for (typ, name, values) in enums:
211         if typ != 'enum':
212             raise Exception('This method only handles enums')
213         pyname=re.findall('(libvlc|mediacontrol)_(.+?)(_t)?$', name)[0][1]
214         if '_' in pyname:
215             pyname=pyname.title().replace('_', '')
216         else:
217             pyname=pyname.capitalize()
218         res[name]=pyname
219     return res
220
221 def generate_enums(enums):
222     for (typ, name, values) in enums:
223         if typ != 'enum':
224             raise Exception('This method only handles enums')
225         pyname=typ2class[name]
226
227         print "class %s(ctypes.c_uint):" % pyname
228
229         conv={}
230         # Convert symbol names
231         for k, v in values:
232             n=k.split('_')[-1]
233             if len(n) == 1:
234                 # Single character. Some symbols use 1_1, 5_1, etc.
235                 n="_".join( k.split('_')[:-2] )
236             if re.match('^[0-9]', n):
237                 # Cannot start an identifier with a number
238                 n='_'+n
239             conv[k]=n
240
241         for k, v in values:
242             print "    %s=%s" % (conv[k], v)
243
244         print "    _names={"
245         for k, v in values:
246             print "        %s: '%s'," % (v, conv[k])
247         print "    }"
248
249         print """
250     def __repr__(self):
251         return ".".join((self.__class__.__module__, self.__class__.__name__, self._names[self.value]))
252 """
253
254 def parse_typedef(name):
255     """Parse include file for typedef expressions.
256
257     This generates a tuple for each typedef:
258     (type, name, value_list)
259     with type == 'enum' (for the moment) and value_list being a list of (name, value)
260     Note that values are string, since this is intended for code generation.
261     """
262     f=open(name, 'r')
263     accumulator=''
264     for l in f:
265         # Note: lstrip() should not be necessary, but there is 1 badly
266         # formatted comment in vlc1.0.0 includes
267         if l.lstrip().startswith('/**'):
268             comment=''
269             continue
270         elif l.startswith(' * '):
271             comment = comment + l[3:]
272             continue
273
274         l=l.strip()
275
276         if accumulator:
277             accumulator=" ".join( (accumulator, l) )
278             if l.endswith(';'):
279                 # End of definition
280                 l=accumulator
281                 accumulator=''
282         elif l.startswith('typedef enum') and not l.endswith(';'):
283             # Multiline definition. Accumulate until end of definition
284             accumulator=l
285             continue
286
287         m=enum_re.match(l)
288         if m:
289             values=[]
290             (typ, dummy, data, name)=m.groups()
291             for i, l in enumerate(paramlist_re.split(data)):
292                 l=l.strip()
293                 if l.startswith('/*'):
294                     continue
295                 if '=' in l:
296                     # A value was specified. Use it.
297                     values.append(re.split('\s*=\s*', l))
298                 else:
299                     if l:
300                         values.append( (l, str(i)) )
301             yield (typ, name, values)
302
303 def parse_include(name):
304     """Parse include file.
305
306     This generates a tuple for each function:
307     (return_type, method_name, parameter_list, comment)
308     with parameter_list being a list of tuples (parameter_type, parameter_name).
309     """
310     f=open(name, 'r')
311     accumulator=''
312     comment=''
313     for l in f:
314         # Note: lstrip() should not be necessary, but there is 1 badly
315         # formatted comment in vlc1.0.0 includes
316         if l.lstrip().startswith('/**'):
317             comment=''
318             continue
319         elif l.startswith(' * '):
320             comment = comment + l[3:]
321             continue
322
323         l=l.strip()
324
325         if accumulator:
326             accumulator=" ".join( (accumulator, l) )
327             if l.endswith(');'):
328                 # End of definition
329                 l=accumulator
330                 accumulator=''
331         elif l.startswith('VLC_PUBLIC_API') and not l.endswith(');'):
332             # Multiline definition. Accumulate until end of definition
333             accumulator=l
334             continue
335
336         m=api_re.match(l)
337         if m:
338             (ret, param)=m.groups()
339
340             rtype, method=parse_param(ret)
341
342             params=[]
343             for p in paramlist_re.split(param):
344                 params.append( parse_param(p) )
345
346             if len(params) == 1 and params[0][0] == 'void':
347                 # Empty parameter list
348                 params=[]
349
350             if list(p for p in params if not p[1]):
351                 # Empty parameter names. Have to poke into comment.
352                 names=comment_re.findall(comment)
353                 if len(names) < len(params):
354                     # Bad description: all parameters are not specified.
355                     # Generate default parameter names
356                     badnames=[ "param%d" % i for i in xrange(len(params)) ]
357                     # Put in the existing ones
358                     for (i, p) in enumerate(names):
359                         badnames[i]=names[i]
360                     names=badnames
361                     print "### Error ###"
362                     print "### Cannot get parameter names from comment for %s: %s" % (method, comment.replace("\n", ' '))
363                     # Note: this was previously
364                     # raise Exception("Cannot get parameter names from comment for %s: %s" % (method, comment))
365                     # but it prevented code generation for a minor detail (some bad descriptions).
366                 params=[ (p[0], names[i]) for (i, p) in enumerate(params) ]
367
368             for typ, name in params:
369                 if not typ in typ2class:
370                     raise Exception("No conversion for %s (from %s:%s)" % (typ, method, name))
371
372             # Transform Doxygen syntax into epydoc syntax
373             comment=comment.replace('\\param', '@param').replace('\\return', '@return')
374
375             if debug:
376                 print '********************'
377                 print l
378                 print '-------->'
379                 print "%s (%s)" % (method, rtype)
380                 for typ, name in params:
381                     print "        %s (%s)" % (name, typ)
382                 print '********************'
383             yield (rtype,
384                    method,
385                    params,
386                    comment)
387
388 def output_ctypes(rtype, method, params, comment):
389     """Output ctypes decorator for the given method.
390     """
391     if method in blacklist:
392         # FIXME
393         return
394
395     if params:
396         print "prototype=ctypes.CFUNCTYPE(%s, %s)" % (typ2class.get(rtype, 'FIXME_%s' % rtype),
397                                                       ",".join( typ2class[p[0]] for p in params ))
398     else:
399         print "prototype=ctypes.CFUNCTYPE(%s)" % typ2class.get(rtype, 'FIXME_%s' % rtype)
400
401
402     if not params:
403         flags='paramflags= tuple()'
404     elif len(params) == 1:
405         flags="paramflags=( (%d, ), )" % parameter_passing[params[0][0]]
406     else:
407         flags="paramflags=%s" % ",".join( '(%d,)' % parameter_passing[p[0]] for p in params )
408     print flags
409     print '%s = prototype( ("%s", dll), paramflags )' % (method, method)
410     if '3' in flags:
411         # A VLCException is present. Process it.
412         print "%s.errcheck = check_vlc_exception" % method
413     print '%s.__doc__ = """%s"""' % (method, comment)
414     print
415
416 def parse_override(name):
417     """Parse override definitions file.
418
419     It is possible to override methods definitions in classes.
420     """
421     res={}
422
423     data=[]
424     current=None
425     f=open(name, 'r')
426     for l in f:
427         m=re.match('class (\S+):', l)
428         if m:
429             # Dump old data
430             if current is not None:
431                 res[current]="\n".join(data)
432             current=m.group(1)
433             data=[]
434             continue
435         data.append(l)
436     res[current]="\n".join(data)
437     f.close()
438     return res
439
440 def fix_python_comment(c):
441     """Fix comment by removing first and last parameters (self and exception)
442     """
443     data=c.splitlines()
444     body=itertools.takewhile(lambda l: not '@param' in l, data)
445     param=[ python_param_re.sub('\\1:\\2', l) for l in  itertools.ifilter(lambda l: '@param' in l, data) ]
446     ret=[ l.replace('@return', '@return:') for l in itertools.ifilter(lambda l: '@return' in l, data) ]
447
448     if len(param) >= 2:
449         param=param[1:-1]
450     elif len(param) == 1:
451         param=[]
452
453     return "\n".join(itertools.chain(body, param, ret))
454
455 def generate_wrappers(methods):
456     """Generate class wrappers for all appropriate methods.
457
458     @return: the set of wrapped method names
459     """
460     ret=set()
461     # Sort methods against the element they apply to.
462     elements=sorted( ( (typ2class.get(params[0][0]), rt, met, params, c)
463                        for (rt, met, params, c) in methods
464                        if params and typ2class.get(params[0][0], '_') in defined_classes
465                        ),
466                      key=operator.itemgetter(0))
467
468     overrides=parse_override('override.py')
469
470     for classname, el in itertools.groupby(elements, key=operator.itemgetter(0)):
471         print """
472 class %(name)s(object):
473     def __init__(self, pointer=None):
474         '''Internal method used for instanciating wrappers from ctypes.
475         '''
476         if pointer is None:
477             raise Exception("Internal method. You should instanciate objects through other class methods (probably named 'new' or ending with 'new')")
478         self._as_parameter_=ctypes.c_void_p(pointer)
479
480     @staticmethod
481     def from_param(arg):
482         '''(INTERNAL) ctypes parameter conversion method.
483         '''
484         return arg._as_parameter_
485 """ % {'name': classname}
486
487         if classname in overrides:
488             print overrides[classname]
489
490         prefix=prefixes.get(classname, '')
491
492         for cl, rtype, method, params, comment in el:
493             if method in blacklist:
494                 continue
495             # Strip prefix
496             name=method.replace(prefix, '').replace('libvlc_', '')
497             ret.add(method)
498             if params:
499                 params[0]=(params[0][0], 'self')
500             if params and params[-1][0] in ('libvlc_exception_t*', 'mediacontrol_Exception*'):
501                 args=", ".join( p[1] for p in params[:-1] )
502             else:
503                 args=", ".join( p[1] for p in params )
504
505             print "    def %s(%s):" % (name, args)
506             print '        """%s\n"""' % fix_python_comment(comment)
507             if params and params[-1][0] == 'libvlc_exception_t*':
508                 # Exception handling
509                 print "        e=VLCException()"
510                 print "        return %s(%s, e)" % (method, args)
511             elif params and params[-1][0] == 'mediacontrol_Exception*':
512                 # Exception handling
513                 print "        e=MediaControlException()"
514                 print "        return %s(%s, e)" % (method, args)
515             else:
516                 print "        return %s(%s)" % (method, args)
517             print
518     return ret
519
520 if __name__ == '__main__':
521     enums=[]
522     for name in sys.argv[1:]:
523         enums.extend(list(parse_typedef(name)))
524     # Generate python names for enums
525     typ2class.update(convert_enum_names(enums))
526
527     methods=[]
528     for name in sys.argv[1:]:
529         methods.extend(list(parse_include(name)))
530     if debug:
531         sys.exit(0)
532
533     generate_header()
534     generate_enums(enums)
535     wrapped=generate_wrappers(methods)
536     for l in methods:
537         output_ctypes(*l)
538
539     all=set( t[1] for t in methods )
540     not_wrapped=all.difference(wrapped)
541     print "# Not wrapped methods:"
542     for m in not_wrapped:
543         print "#   ", m
544