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