]> git.sesse.net Git - vlc/blob - bindings/python-ctypes/generate.py
python-ctypes: parse enum definition from libvlc_events.h.
[vlc] / 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 special_enum_re=re.compile('^(enum)\s*(\S+\s*)?\{\s*(.+)\s*\};')
92
93 # Definition of parameter passing mode for types.  This should not be
94 # hardcoded this way, but works alright ATM.
95 parameter_passing=DefaultDict(default=1)
96 parameter_passing['libvlc_exception_t*']=3
97
98 # C-type to ctypes/python type conversion.
99 # Note that enum types conversions are generated (cf convert_enum_names)
100 typ2class={
101     'libvlc_exception_t*': 'ctypes.POINTER(VLCException)',
102
103     'libvlc_media_player_t*': 'MediaPlayer',
104     'libvlc_instance_t*': 'Instance',
105     'libvlc_media_t*': 'Media',
106     'libvlc_log_t*': 'Log',
107     'libvlc_log_iterator_t*': 'LogIterator',
108     'libvlc_log_message_t*': 'LogMessage',
109     'libvlc_event_type_t': 'EventType',
110     'libvlc_event_manager_t*': 'EventManager',
111     'libvlc_media_discoverer_t*': 'MediaDiscoverer',
112     'libvlc_media_library_t*': 'MediaLibrary',
113     'libvlc_media_list_t*': 'MediaList',
114     'libvlc_media_list_player_t*': 'MediaListPlayer',
115     'libvlc_media_list_view_t*': 'MediaListView',
116     'libvlc_track_description_t*': 'TrackDescription',
117     'libvlc_audio_output_t*': 'AudioOutput',
118
119     'mediacontrol_Instance*': 'MediaControl',
120     'mediacontrol_Exception*': 'MediaControlException',
121     'mediacontrol_RGBPicture*': 'RGBPicture',
122     'mediacontrol_PlaylistSeq*': 'MediaControlPlaylistSeq',
123     'mediacontrol_Position*': 'MediaControlPosition',
124     'mediacontrol_StreamInformation*': 'MediaControlStreamInformation',
125     'WINDOWHANDLE': 'ctypes.c_ulong',
126
127     'void': 'None',
128     'void*': 'ctypes.c_void_p',
129     'short': 'ctypes.c_short',
130     'char*': 'ctypes.c_char_p',
131     'char**': 'ListPOINTER(ctypes.c_char_p)',
132     'uint32_t': 'ctypes.c_uint',
133     'float': 'ctypes.c_float',
134     'unsigned': 'ctypes.c_uint',
135     'int': 'ctypes.c_int',
136     '...': 'FIXMEva_list',
137     'libvlc_callback_t': 'FIXMEcallback',
138     'libvlc_time_t': 'ctypes.c_longlong',
139     }
140
141 # Defined python classes, i.e. classes for which we want to generate
142 # class wrappers around libvlc functions
143 defined_classes=(
144     'MediaPlayer',
145     'Instance',
146     'Media',
147     'Log',
148     'LogIterator',
149     #'LogMessage',
150     'EventType',
151     'EventManager',
152     'MediaDiscoverer',
153     'MediaLibrary',
154     'MediaList',
155     'MediaListPlayer',
156     'MediaListView',
157     'TrackDescription',
158     'AudioOutput',
159     'MediaControl',
160     #'RGBPicture',
161     #'MediaControlPosition',
162     #'MediaControlStreamInformation',
163     )
164
165 # Definition of prefixes that we can strip from method names when
166 # wrapping them into class methods
167 prefixes=dict( (v, k[:-2]) for (k, v) in typ2class.iteritems() if  v in defined_classes )
168 prefixes['MediaControl']='mediacontrol_'
169
170 def parse_param(s):
171     """Parse a C parameter expression.
172
173     It is used to parse both the type/name for methods, and type/name
174     for their parameters.
175
176     It returns a tuple (type, name).
177     """
178     s=s.strip()
179     s=s.replace('const', '')
180     if 'VLC_FORWARD' in s:
181         m=forward_re.match(s)
182         s=m.group(1)+m.group(2)
183     m=param_re.search(s)
184     if m:
185         const, typ, name=m.groups()
186         while name.startswith('*'):
187             typ += '*'
188             name=name[1:]
189         if name == 'const*':
190             # K&R definition: const char * const*
191             name=''
192         typ=typ.replace(' ', '')
193         return typ, name
194     else:
195         # K&R definition: only type
196         return s.replace(' ', ''), ''
197
198 def generate_header(classes=None):
199     """Generate header code.
200     """
201     f=open('header.py', 'r')
202     for l in f:
203         if 'build_date' in l:
204             print 'build_date="%s"' % time.ctime()
205         else:
206             print l,
207     f.close()
208
209 def convert_enum_names(enums):
210     res={}
211     for (typ, name, values, comment) in enums:
212         if typ != 'enum':
213             raise Exception('This method only handles enums')
214         pyname=re.findall('(libvlc|mediacontrol)_(.+?)(_t)?$', name)[0][1]
215         if '_' in pyname:
216             pyname=pyname.title().replace('_', '')
217         else:
218             pyname=pyname.capitalize()
219         res[name]=pyname
220     return res
221
222 def generate_enums(enums):
223     for (typ, name, values, comment) in enums:
224         if typ != 'enum':
225             raise Exception('This method only handles enums')
226         pyname=typ2class[name]
227
228         print "class %s(ctypes.c_uint):" % pyname
229         print '    """%s\n    """' % comment
230
231         conv={}
232         # Convert symbol names
233         for k, v in values:
234             n=k.split('_')[-1]
235             if len(n) == 1:
236                 # Single character. Some symbols use 1_1, 5_1, etc.
237                 n="_".join( k.split('_')[:-2] )
238             if re.match('^[0-9]', n):
239                 # Cannot start an identifier with a number
240                 n='_'+n
241             conv[k]=n
242
243         for k, v in values:
244             print "    %s=%s" % (conv[k], v)
245
246         print "    _names={"
247         for k, v in values:
248             print "        %s: '%s'," % (v, conv[k])
249         print "    }"
250
251         print """
252     def __repr__(self):
253         return ".".join((self.__class__.__module__, self.__class__.__name__, self._names[self.value]))
254 """
255
256 def parse_typedef(name):
257     """Parse include file for typedef expressions.
258
259     This generates a tuple for each typedef:
260     (type, name, value_list, comment)
261     with type == 'enum' (for the moment) and value_list being a list of (name, value)
262     Note that values are string, since this is intended for code generation.
263     """
264     f=open(name, 'r')
265     accumulator=''
266     for l in f:
267         # Note: lstrip() should not be necessary, but there is 1 badly
268         # formatted comment in vlc1.0.0 includes
269         if l.lstrip().startswith('/**'):
270             comment=''
271             continue
272         elif l.startswith(' * '):
273             comment = comment + l[3:]
274             continue
275
276         l=l.strip()
277         if l.startswith('/*') or l.endswith('*/'):
278             continue
279
280         if (l.startswith('typedef enum') or l.startswith('enum')) and not l.endswith(';'):
281             # Multiline definition. Accumulate until end of definition
282             accumulator=l
283             continue
284         elif accumulator:
285             accumulator=" ".join( (accumulator, l) )
286             if l.endswith(';'):
287                 # End of definition
288                 l=accumulator
289                 accumulator=''
290
291         m=enum_re.match(l)
292         if m:
293             values=[]
294             (typ, dummy, data, name)=m.groups()
295             for i, l in enumerate(paramlist_re.split(data)):
296                 l=l.strip()
297                 if l.startswith('/*'):
298                     continue
299                 if '=' in l:
300                     # A value was specified. Use it.
301                     values.append(re.split('\s*=\s*', l))
302                 else:
303                     if l:
304                         values.append( (l, str(i)) )
305             comment=comment.replace('@{', '').replace('@see', 'See').replace('\ingroup', '')
306             yield (typ, name, values, comment)
307             comment=''
308             continue
309
310         # Special case, used only for libvlc_events.h
311         m=special_enum_re.match(l)
312         if m:
313             values=[]
314             (typ, name, data)=m.groups()
315             for i, l in enumerate(paramlist_re.split(data)):
316                 l=l.strip()
317                 if l.startswith('/*') or l.startswith('#'):
318                     continue
319                 if '=' in l:
320                     # A value was specified. Use it.
321                     values.append(re.split('\s*=\s*', l))
322                 else:
323                     if l:
324                         values.append( (l, str(i)) )
325             comment=comment.replace('@{', '').replace('@see', 'See').replace('\ingroup', '')
326             yield (typ, name, values, comment)
327             comment=''
328             continue
329
330 def parse_include(name):
331     """Parse include file.
332
333     This generates a tuple for each function:
334     (return_type, method_name, parameter_list, comment)
335     with parameter_list being a list of tuples (parameter_type, parameter_name).
336     """
337     f=open(name, 'r')
338     accumulator=''
339     comment=''
340     for l in f:
341         # Note: lstrip() should not be necessary, but there is 1 badly
342         # formatted comment in vlc1.0.0 includes
343         if l.lstrip().startswith('/**'):
344             comment=''
345             continue
346         elif l.startswith(' * '):
347             comment = comment + l[3:]
348             continue
349
350         l=l.strip()
351
352         if accumulator:
353             accumulator=" ".join( (accumulator, l) )
354             if l.endswith(');'):
355                 # End of definition
356                 l=accumulator
357                 accumulator=''
358         elif l.startswith('VLC_PUBLIC_API') and not l.endswith(');'):
359             # Multiline definition. Accumulate until end of definition
360             accumulator=l
361             continue
362
363         m=api_re.match(l)
364         if m:
365             (ret, param)=m.groups()
366
367             rtype, method=parse_param(ret)
368
369             params=[]
370             for p in paramlist_re.split(param):
371                 params.append( parse_param(p) )
372
373             if len(params) == 1 and params[0][0] == 'void':
374                 # Empty parameter list
375                 params=[]
376
377             if list(p for p in params if not p[1]):
378                 # Empty parameter names. Have to poke into comment.
379                 names=comment_re.findall(comment)
380                 if len(names) < len(params):
381                     # Bad description: all parameters are not specified.
382                     # Generate default parameter names
383                     badnames=[ "param%d" % i for i in xrange(len(params)) ]
384                     # Put in the existing ones
385                     for (i, p) in enumerate(names):
386                         badnames[i]=names[i]
387                     names=badnames
388                     print "### Error ###"
389                     print "### Cannot get parameter names from comment for %s: %s" % (method, comment.replace("\n", ' '))
390                     # Note: this was previously
391                     # raise Exception("Cannot get parameter names from comment for %s: %s" % (method, comment))
392                     # but it prevented code generation for a minor detail (some bad descriptions).
393                 params=[ (p[0], names[i]) for (i, p) in enumerate(params) ]
394
395             for typ, name in params:
396                 if not typ in typ2class:
397                     raise Exception("No conversion for %s (from %s:%s)" % (typ, method, name))
398
399             # Transform Doxygen syntax into epydoc syntax
400             comment=comment.replace('\\param', '@param').replace('\\return', '@return')
401
402             if debug:
403                 print '********************'
404                 print l
405                 print '-------->'
406                 print "%s (%s)" % (method, rtype)
407                 for typ, name in params:
408                     print "        %s (%s)" % (name, typ)
409                 print '********************'
410             yield (rtype,
411                    method,
412                    params,
413                    comment)
414             comment=''
415
416 def output_ctypes(rtype, method, params, comment):
417     """Output ctypes decorator for the given method.
418     """
419     if method in blacklist:
420         # FIXME
421         return
422
423     if params:
424         print "prototype=ctypes.CFUNCTYPE(%s, %s)" % (typ2class.get(rtype, 'FIXME_%s' % rtype),
425                                                       ",".join( typ2class[p[0]] for p in params ))
426     else:
427         print "prototype=ctypes.CFUNCTYPE(%s)" % typ2class.get(rtype, 'FIXME_%s' % rtype)
428
429
430     if not params:
431         flags='paramflags= tuple()'
432     elif len(params) == 1:
433         flags="paramflags=( (%d, ), )" % parameter_passing[params[0][0]]
434     else:
435         flags="paramflags=%s" % ",".join( '(%d,)' % parameter_passing[p[0]] for p in params )
436     print flags
437     print '%s = prototype( ("%s", dll), paramflags )' % (method, method)
438     if '3' in flags:
439         # A VLCException is present. Process it.
440         print "%s.errcheck = check_vlc_exception" % method
441     print '%s.__doc__ = """%s"""' % (method, comment)
442     print
443
444 def parse_override(name):
445     """Parse override definitions file.
446
447     It is possible to override methods definitions in classes.
448     
449     It returns a tuple
450     (code, overriden_methods, docstring)
451     """
452     code={}
453
454     data=[]
455     current=None
456     f=open(name, 'r')
457     for l in f:
458         m=re.match('class (\S+):', l)
459         if m:
460             # Dump old data
461             if current is not None:
462                 code[current]="".join(data)
463             current=m.group(1)
464             data=[]
465             continue
466         data.append(l)
467     code[current]="".join(data)
468     f.close()
469
470     docstring={}
471     for k, v in code.iteritems():
472         if v.lstrip().startswith('"""'):
473             # Starting comment. Use it as docstring.
474             dummy, docstring[k], code[k]=v.split('"""', 2)
475
476     # Not robust wrt. internal methods, but this works for the moment.
477     overridden_methods=dict( (k, re.findall('^\s+def\s+(\w+)', v, re.MULTILINE)) for (k, v) in code.iteritems() )
478
479     return code, overridden_methods, docstring
480
481 def fix_python_comment(c):
482     """Fix comment by removing first and last parameters (self and exception)
483     """
484     data=c.replace('@{', '').replace('@see', 'See').splitlines()
485     body=itertools.takewhile(lambda l: not '@param' in l and not '@return' in l, data)
486     param=[ python_param_re.sub('\\1:\\2', l) for l in  itertools.ifilter(lambda l: '@param' in l, data) ]
487     ret=[ l.replace('@return', '@return:') for l in itertools.ifilter(lambda l: '@return' in l, data) ]
488
489     if len(param) >= 2:
490         param=param[1:-1]
491     elif len(param) == 1:
492         param=[]
493
494     return "\n".join(itertools.chain(body, param, ret))
495
496 def generate_wrappers(methods):
497     """Generate class wrappers for all appropriate methods.
498
499     @return: the set of wrapped method names
500     """
501     ret=set()
502     # Sort methods against the element they apply to.
503     elements=sorted( ( (typ2class.get(params[0][0]), rt, met, params, c)
504                        for (rt, met, params, c) in methods
505                        if params and typ2class.get(params[0][0], '_') in defined_classes
506                        ),
507                      key=operator.itemgetter(0))
508
509     overrides, overriden_methods, docstring=parse_override('override.py')
510
511     for classname, el in itertools.groupby(elements, key=operator.itemgetter(0)):
512         print """class %(name)s(object):""" % {'name': classname}
513         if classname in docstring:
514             print '    """%s\n    """' % docstring[classname]
515
516         print """
517     def __new__(cls, pointer=None):
518         '''Internal method used for instanciating wrappers from ctypes.
519         '''
520         if pointer is None:
521             raise Exception("Internal method. You should instanciate objects through other class methods (probably named 'new' or ending with 'new')")
522         if pointer == 0:
523             return None
524         else:
525             o=object.__new__(cls)
526             o._as_parameter_=ctypes.c_void_p(pointer)
527             return o
528
529     @staticmethod
530     def from_param(arg):
531         '''(INTERNAL) ctypes parameter conversion method.
532         '''
533         return arg._as_parameter_
534 """ % {'name': classname}
535
536         if classname in overrides:
537             print overrides[classname]
538
539         prefix=prefixes.get(classname, '')
540
541         for cl, rtype, method, params, comment in el:
542             if method in blacklist:
543                 continue
544             # Strip prefix
545             name=method.replace(prefix, '').replace('libvlc_', '')
546             ret.add(method)
547             if name in overriden_methods.get(cl, []):
548                 # Method already defined in override.py
549                 continue
550
551             if params:
552                 params[0]=(params[0][0], 'self')
553             if params and params[-1][0] in ('libvlc_exception_t*', 'mediacontrol_Exception*'):
554                 args=", ".join( p[1] for p in params[:-1] )
555             else:
556                 args=", ".join( p[1] for p in params )
557
558             print "    def %s(%s):" % (name, args)
559             print '        """%s\n        """' % fix_python_comment(comment)
560             if params and params[-1][0] == 'libvlc_exception_t*':
561                 # Exception handling
562                 print "        e=VLCException()"
563                 print "        return %s(%s, e)" % (method, args)
564             elif params and params[-1][0] == 'mediacontrol_Exception*':
565                 # Exception handling
566                 print "        e=MediaControlException()"
567                 print "        return %s(%s, e)" % (method, args)
568             else:
569                 print "        return %s(%s)" % (method, args)
570             print
571
572             # Check for standard methods
573             if name == 'count':
574                 # There is a count method. Generate a __len__ one.
575                 print "    def __len__(self):"
576                 print "        e=VLCException()"
577                 print "        return %s(self, e)" % method
578                 print
579             elif name.endswith('item_at_index'):
580                 # Indexable (and thus iterable)"
581                 print "    def __getitem__(self, i):"
582                 print "        e=VLCException()"
583                 print "        return %s(self, i, e)" % method
584                 print
585                 print "    def __iter__(self):"
586                 print "        e=VLCException()"
587                 print "        for i in xrange(len(self)):"
588                 print "            yield self[i]"
589                 print
590
591     return ret
592
593 if __name__ == '__main__':
594     enums=[]
595     for name in sys.argv[1:]:
596         enums.extend(list(parse_typedef(name)))
597     # Generate python names for enums
598     typ2class.update(convert_enum_names(enums))
599
600     methods=[]
601     for name in sys.argv[1:]:
602         methods.extend(list(parse_include(name)))
603     if debug:
604         sys.exit(0)
605
606     generate_header()
607     generate_enums(enums)
608     wrapped=generate_wrappers(methods)
609     for l in methods:
610         output_ctypes(*l)
611
612     all=set( t[1] for t in methods )
613     not_wrapped=all.difference(wrapped)
614     print "# Not wrapped methods:"
615     for m in not_wrapped:
616         print "#   ", m
617