]> git.sesse.net Git - vlc/blob - bindings/python-ctypes/generate.py
New python bindings, using ctypes, automatically generated from include files.
[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
91 # Definition of parameter passing mode for types.  This should not be
92 # hardcoded this way, but works alright ATM.
93 parameter_passing=DefaultDict(default=1)
94 parameter_passing['libvlc_exception_t*']=3
95
96 # C-type to ctypes/python type conversion
97 typ2class={
98     'libvlc_exception_t*': 'ctypes.POINTER(VLCException)',
99
100     'libvlc_media_player_t*': 'MediaPlayer',
101     'libvlc_instance_t*': 'Instance',
102     'libvlc_media_t*': 'Media',
103     'libvlc_log_t*': 'Log',
104     'libvlc_log_iterator_t*': 'LogIterator',
105     'libvlc_log_message_t*': 'LogMessage',
106     'libvlc_event_type_t': 'EventType',
107     'libvlc_event_manager_t*': 'EventManager',
108     'libvlc_media_discoverer_t*': 'MediaDiscoverer',
109     'libvlc_media_library_t*': 'MediaLibrary',
110     'libvlc_media_list_t*': 'MediaList',
111     'libvlc_media_list_player_t*': 'MediaListPlayer',
112     'libvlc_media_list_view_t*': 'MediaListView',
113     'libvlc_track_description_t*': 'TrackDescription',
114     'libvlc_audio_output_t*': 'AudioOutput',
115
116     'mediacontrol_Instance*': 'MediaControl',
117     'mediacontrol_Exception*': 'MediaControlException',
118     'mediacontrol_RGBPicture*': 'RGBPicture',
119     'mediacontrol_PlaylistSeq*': 'MediaControlPlaylistSeq',
120     'mediacontrol_Position*': 'MediaControlPosition',
121     'mediacontrol_StreamInformation*': 'MediaControlStreamInformation',
122     'mediacontrol_PositionOrigin': 'ctypes.c_uint',
123     'mediacontrol_PositionKey': 'ctypes.c_uint',
124     'WINDOWHANDLE': 'ctypes.c_ulong',
125
126     'short': 'ctypes.c_short',
127     'char*': 'ctypes.c_char_p',
128     'char**': 'ListPOINTER(ctypes.c_char_p)',
129     'uint32_t': 'ctypes.c_uint',
130     'float': 'ctypes.c_float',
131     'unsigned': 'ctypes.c_uint',
132     'void': 'None',
133     'void*': 'ctypes.c_void_p',
134     'int': 'ctypes.c_int',
135     '...': 'FIXMEva_list',
136     'libvlc_callback_t': 'FIXMEcallback',
137     'libvlc_time_t': 'ctypes.c_longlong',
138     'libvlc_video_marquee_int_option_t': 'ctypes.c_int',
139     'libvlc_video_marquee_string_option_t': 'ctypes.c_char_p',
140     # FIXME: enums -> to be processed
141     'libvlc_media_option_t': 'ctypes.c_uint',
142     'libvlc_meta_t': 'ctypes.c_uint',
143     'libvlc_state_t': 'State',
144     }
145
146 # Defined python classes, i.e. classes for which we want to generate
147 # class wrappers around libvlc functions
148 defined_classes=(
149     'MediaPlayer',
150     'Instance',
151     'Media',
152     'Log',
153     'LogIterator',
154     #'LogMessage',
155     'EventType',
156     'EventManager',
157     'MediaDiscoverer',
158     'MediaLibrary',
159     'MediaList',
160     'MediaListPlayer',
161     'MediaListView',
162     'TrackDescription',
163     'AudioOutput',
164     'MediaControl',
165     #'RGBPicture',
166     #'MediaControlPosition',
167     #'MediaControlStreamInformation',
168     )
169
170 # Definition of prefixes that we can strip from method names when
171 # wrapping them into class methods
172 prefixes=dict( (v, k[:-2]) for (k, v) in typ2class.iteritems() if  v in defined_classes )
173 prefixes['MediaControl']='mediacontrol_'
174
175 def parse_param(s):
176     """Parse a C parameter expression.
177
178     It is used to parse both the type/name for methods, and type/name
179     for their parameters.
180
181     It returns a tuple (type, name).
182     """
183     s=s.strip()
184     s=s.replace('const', '')
185     if 'VLC_FORWARD' in s:
186         m=forward_re.match(s)
187         s=m.group(1)+m.group(2)
188     m=param_re.search(s)
189     if m:
190         const, typ, name=m.groups()
191         while name.startswith('*'):
192             typ += '*'
193             name=name[1:]
194         if name == 'const*':
195             # K&R definition: const char * const*
196             name=''
197         typ=typ.replace(' ', '')
198         return typ, name
199     else:
200         # K&R definition: only type
201         return s.replace(' ', ''), ''
202
203 def generate_header(classes=None):
204     """Generate header code.
205     """
206     f=open('header.py', 'r')
207     for l in f:
208         if 'build_date' in l:
209             print 'build_date="%s"' % time.ctime()
210         else:
211             print l,
212     f.close()
213
214 def parse_include(name):
215     """Parse include file.
216
217     This generates a tuple for each function:
218     (return_type, method_name, parameter_list, comment)
219     with parameter_list being a list of tuples (parameter_type, parameter_name).
220     """
221     f=open(name, 'r')
222     accumulator=''
223     comment=''
224     for l in f:
225         # Note: lstrip() should not be necessary, but there is 1 badly
226         # formatted comment in vlc1.0.0 includes
227         if l.lstrip().startswith('/**'):
228             comment=''
229             continue
230         elif l.startswith(' * '):
231             comment = comment + l[3:]
232             continue
233
234         l=l.strip()
235
236         if accumulator:
237             accumulator=" ".join( (accumulator, l) )
238             if l.endswith(');'):
239                 # End of definition
240                 l=accumulator
241                 accumulator=''
242         elif l.startswith('VLC_PUBLIC_API') and not l.endswith(');'):
243             # Multiline definition. Accumulate until end of definition
244             accumulator=l
245             continue
246
247         m=api_re.match(l)
248         if m:
249             (ret, param)=m.groups()
250
251             rtype, method=parse_param(ret)
252
253             params=[]
254             for p in paramlist_re.split(param):
255                 params.append( parse_param(p) )
256
257             if len(params) == 1 and params[0][0] == 'void':
258                 # Empty parameter list
259                 params=[]
260
261             if list(p for p in params if not p[1]):
262                 # Empty parameter names. Have to poke into comment.
263                 names=comment_re.findall(comment)
264                 if len(names) < len(params):
265                     # Bad description: all parameters are not specified.
266                     # Generate default parameter names
267                     badnames=[ "param%d" % i for i in xrange(len(params)) ]
268                     # Put in the existing ones
269                     for (i, p) in enumerate(names):
270                         badnames[i]=names[i]
271                     names=badnames
272                     print "### Error ###"
273                     print "### Cannot get parameter names from comment for %s: %s" % (method, comment.replace("\n", ' '))
274                     # Note: this was previously
275                     # raise Exception("Cannot get parameter names from comment for %s: %s" % (method, comment))
276                     # but it prevented code generation for a minor detail (some bad descriptions).
277                 params=[ (p[0], names[i]) for (i, p) in enumerate(params) ]
278
279             for typ, name in params:
280                 if not typ in typ2class:
281                     raise Exception("No conversion for %s (from %s:%s)" % (typ, method, name))
282
283             # Transform Doxygen syntax into epydoc syntax
284             comment=comment.replace('\\param', '@param').replace('\\return', '@return')
285
286             if debug:
287                 print '********************'
288                 print l
289                 print '-------->'
290                 print "%s (%s)" % (method, rtype)
291                 for typ, name in params:
292                     print "        %s (%s)" % (name, typ)
293                 print '********************'
294             yield (rtype,
295                    method,
296                    params,
297                    comment)
298
299 def output_ctypes(rtype, method, params, comment):
300     """Output ctypes decorator for the given method.
301     """
302     if method in blacklist:
303         # FIXME
304         return
305
306     if params:
307         print "prototype=ctypes.CFUNCTYPE(%s, %s)" % (typ2class.get(rtype, 'FIXME_%s' % rtype),
308                                                       ",".join( typ2class[p[0]] for p in params ))
309     else:
310         print "prototype=ctypes.CFUNCTYPE(%s)" % typ2class.get(rtype, 'FIXME_%s' % rtype)
311
312
313     if not params:
314         flags='paramflags= tuple()'
315     elif len(params) == 1:
316         flags="paramflags=( (%d, ), )" % parameter_passing[params[0][0]]
317     else:
318         flags="paramflags=%s" % ",".join( '(%d,)' % parameter_passing[p[0]] for p in params )
319     print flags
320     print '%s = prototype( ("%s", dll), paramflags )' % (method, method)
321     if '3' in flags:
322         # A VLCException is present. Process it.
323         print "%s.errcheck = check_vlc_exception" % method
324     print '%s.__doc__ = """%s"""' % (method, comment)
325     print
326
327 def parse_override(name):
328     """Parse override definitions file.
329
330     It is possible to override methods definitions in classes.
331     """
332     res={}
333
334     data=[]
335     current=None
336     f=open(name, 'r')
337     for l in f:
338         m=re.match('class (\S+):', l)
339         if m:
340             # Dump old data
341             if current is not None:
342                 res[current]="\n".join(data)
343             current=m.group(1)
344             data=[]
345             continue
346         data.append(l)
347     res[current]="\n".join(data)
348     f.close()
349     return res
350
351 def fix_python_comment(c):
352     """Fix comment by removing first and last parameters (self and exception)
353     """
354     data=c.splitlines()
355     body=itertools.takewhile(lambda l: not '@param' in l, data)
356     param=[ python_param_re.sub('\\1:\\2', l) for l in  itertools.ifilter(lambda l: '@param' in l, data) ]
357     ret=[ l.replace('@return', '@return:') for l in itertools.ifilter(lambda l: '@return' in l, data) ]
358
359     if len(param) >= 2:
360         param=param[1:-1]
361     elif len(param) == 1:
362         param=[]
363
364     return "\n".join(itertools.chain(body, param, ret))
365
366 def generate_wrappers(methods):
367     """Generate class wrappers for all appropriate methods.
368
369     @return: the set of wrapped method names
370     """
371     ret=set()
372     # Sort methods against the element they apply to.
373     elements=sorted( ( (typ2class.get(params[0][0]), rt, met, params, c)
374                        for (rt, met, params, c) in methods
375                        if params and typ2class.get(params[0][0], '_') in defined_classes
376                        ),
377                      key=operator.itemgetter(0))
378
379     overrides=parse_override('override.py')
380
381     for classname, el in itertools.groupby(elements, key=operator.itemgetter(0)):
382         print """
383 class %(name)s(object):
384     def __init__(self, pointer=None):
385         '''Internal method used for instanciating wrappers from ctypes.
386         '''
387         if pointer is None:
388             raise Exception("Internal method. You should instanciate objects through other class methods (probably named 'new' or ending with 'new')")
389         self._as_parameter_=ctypes.c_void_p(pointer)
390
391     @staticmethod
392     def from_param(arg):
393         '''(INTERNAL) ctypes parameter conversion method.
394         '''
395         return arg._as_parameter_
396 """ % {'name': classname}
397
398         if classname in overrides:
399             print overrides[classname]
400
401         prefix=prefixes.get(classname, '')
402
403         for cl, rtype, method, params, comment in el:
404             if method in blacklist:
405                 continue
406             # Strip prefix
407             name=method.replace(prefix, '').replace('libvlc_', '')
408             ret.add(method)
409             if params:
410                 params[0]=(params[0][0], 'self')
411             if params and params[-1][0] in ('libvlc_exception_t*', 'mediacontrol_Exception*'):
412                 args=", ".join( p[1] for p in params[:-1] )
413             else:
414                 args=", ".join( p[1] for p in params )
415
416             print "    def %s(%s):" % (name, args)
417             print '        """%s\n"""' % fix_python_comment(comment)
418             if params and params[-1][0] == 'libvlc_exception_t*':
419                 # Exception handling
420                 print "        e=VLCException()"
421                 print "        return %s(%s, e)" % (method, args)
422             elif params and params[-1][0] == 'mediacontrol_Exception*':
423                 # Exception handling
424                 print "        e=MediaControlException()"
425                 print "        return %s(%s, e)" % (method, args)
426             else:
427                 print "        return %s(%s)" % (method, args)
428             print
429     return ret
430
431 if __name__ == '__main__':
432     methods=[]
433     for name in sys.argv[1:]:
434         methods.extend(list(parse_include(name)))
435     if debug:
436         sys.exit(0)
437
438     generate_header()
439     wrapped=generate_wrappers(methods)
440     for l in methods:
441         output_ctypes(*l)
442
443     all=set( t[1] for t in methods )
444     not_wrapped=all.difference(wrapped)
445     print "# Not wrapped methods:"
446     for m in not_wrapped:
447         print "#   ", m
448