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