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