From: Olivier Aubert Date: Thu, 30 Jul 2009 08:39:56 +0000 (+0200) Subject: New python bindings, using ctypes, automatically generated from include files. X-Git-Tag: 1.1.0-ff~4722 X-Git-Url: https://git.sesse.net/?a=commitdiff_plain;h=f83867bbbe87d88233098a17fc64c7c2e273d314;p=vlc New python bindings, using ctypes, automatically generated from include files. --- diff --git a/bindings/python-ctypes/Makefile b/bindings/python-ctypes/Makefile new file mode 100644 index 0000000000..0bee51b6b4 --- /dev/null +++ b/bindings/python-ctypes/Makefile @@ -0,0 +1,12 @@ +MODULE_NAME=vlc.py + +all: $(MODULE_NAME) + +$(MODULE_NAME): generate.py header.py override.py ../../include/vlc/*.h + ./generate.py ../../include/vlc/*.h > $@ + +doc: $(MODULE_NAME) + -epydoc -v -o doc $< + +clean: + -$(RM) $(MODULE_NAME) diff --git a/bindings/python-ctypes/README b/bindings/python-ctypes/README new file mode 100644 index 0000000000..e45c37b515 --- /dev/null +++ b/bindings/python-ctypes/README @@ -0,0 +1,52 @@ +* Python ctypes-based bindings + +The bindings use ctypes to directly call the libvlc dynamic lib, and +the code is generated from the include files defining the public API. + +** Building + +To generate the vlc.py module and its documentation, use +make + +Documentation building needs epydoc. + +** Layout + +The module offers two ways of accessing the API - a raw access to all +exported methods, and more convenient wrapper classes : + +- Raw access: methods are available as attributes of the vlc + module. Use their docstring (introspective shells like ipython are + your friends) to explore them. + +- Wrapper classes: most major structures of the libvlc API (Instance, + Media, MediaPlayer, etc) are wrapped as classes, with shorter method + names. + +** Using the module + +On win32, the simplest way is to put the vlc.py file in the same +directory as the libvlc.dll file (standard location: +c:\Program Files\VideoLAN\VLC ). + + +- Using raw access: + +>>> import vlc +>>> vlc.libvlc_get_version() +'1.0.0 Goldeneye' +>>> e=vlc.VLCException() +>>> i=vlc.libvlc_new(0, [], e) +>>> i + +>>> vlc.libvlc_audio_get_volume(i,e) +50 + +- Using wrapper classes: + +>>> i=vlc.Instance.new() +>>> i.audio_get_volume() +50 +>>> m=i.media_new('/tmp/foo.avi') +>>> m.get_mrl() +'/tmp/foo.avi' diff --git a/bindings/python-ctypes/TODO b/bindings/python-ctypes/TODO new file mode 100644 index 0000000000..f6e137f475 --- /dev/null +++ b/bindings/python-ctypes/TODO @@ -0,0 +1,11 @@ +* Check cross-platform (win32, MacOSX) support + +* Investigate memory management + +* Find how to properly define enums + +* Autogenerate enums from include files + +* Implement event callbacks + +* Write a test suite diff --git a/bindings/python-ctypes/generate.py b/bindings/python-ctypes/generate.py new file mode 100755 index 0000000000..41f69b3fec --- /dev/null +++ b/bindings/python-ctypes/generate.py @@ -0,0 +1,448 @@ +#! /usr/bin/python +debug=False + +# +# Code generator for python ctypes bindings for VLC +# Copyright (C) 2009 the VideoLAN team +# $Id: $ +# +# Authors: Olivier Aubert +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. +# + +"""This module parses VLC public API include files and generates +corresponding python/ctypes code. Moreover, it generates class +wrappers for most methods. +""" + +import sys +import re +import time +import operator +import itertools + +# DefaultDict from ASPN python cookbook +import copy +class DefaultDict(dict): + """Dictionary with a default value for unknown keys.""" + def __init__(self, default=None, **items): + dict.__init__(self, **items) + self.default = default + + def __getitem__(self, key): + if key in self: + return self.get(key) + else: + ## Need copy in case self.default is something like [] + return self.setdefault(key, copy.deepcopy(self.default)) + + def __copy__(self): + return DefaultDict(self.default, **self) + +# Methods not decorated/not referenced +blacklist=[ + "libvlc_exception_raise", + "libvlc_exception_raised", + "libvlc_exception_get_message", + "libvlc_get_vlc_instance", + + "libvlc_media_add_option_flag", + "libvlc_media_list_view_index_of_item", + "libvlc_media_list_view_insert_at_index", + "libvlc_media_list_view_remove_at_index", + "libvlc_media_list_view_add_item", + + # In svn but not in current 1.0.0 + 'libvlc_video_set_deinterlace', + 'libvlc_video_get_marquee_option_as_int', + 'libvlc_video_get_marquee_option_as_string', + 'libvlc_video_set_marquee_option_as_int', + 'libvlc_video_set_marquee_option_as_string', + 'libvlc_vlm_get_event_manager', + + 'mediacontrol_PlaylistSeq__free', + + # TODO + "libvlc_event_detach", + "libvlc_event_attach", + ] + +# Precompiled regexps +api_re=re.compile('VLC_PUBLIC_API\s+(\S+\s+.+?)\s*\(\s*(.+?)\s*\)') +param_re=re.compile('\s*(const\s*|unsigned\s*|struct\s*)?(\S+\s*\**)\s+(.+)') +paramlist_re=re.compile('\s*,\s*') +comment_re=re.compile('\\param\s+(\S+)') +python_param_re=re.compile('(@param\s+\S+)(.+)') +forward_re=re.compile('.+\(\s*(.+?)\s*\)(\s*\S+)') + +# Definition of parameter passing mode for types. This should not be +# hardcoded this way, but works alright ATM. +parameter_passing=DefaultDict(default=1) +parameter_passing['libvlc_exception_t*']=3 + +# C-type to ctypes/python type conversion +typ2class={ + 'libvlc_exception_t*': 'ctypes.POINTER(VLCException)', + + 'libvlc_media_player_t*': 'MediaPlayer', + 'libvlc_instance_t*': 'Instance', + 'libvlc_media_t*': 'Media', + 'libvlc_log_t*': 'Log', + 'libvlc_log_iterator_t*': 'LogIterator', + 'libvlc_log_message_t*': 'LogMessage', + 'libvlc_event_type_t': 'EventType', + 'libvlc_event_manager_t*': 'EventManager', + 'libvlc_media_discoverer_t*': 'MediaDiscoverer', + 'libvlc_media_library_t*': 'MediaLibrary', + 'libvlc_media_list_t*': 'MediaList', + 'libvlc_media_list_player_t*': 'MediaListPlayer', + 'libvlc_media_list_view_t*': 'MediaListView', + 'libvlc_track_description_t*': 'TrackDescription', + 'libvlc_audio_output_t*': 'AudioOutput', + + 'mediacontrol_Instance*': 'MediaControl', + 'mediacontrol_Exception*': 'MediaControlException', + 'mediacontrol_RGBPicture*': 'RGBPicture', + 'mediacontrol_PlaylistSeq*': 'MediaControlPlaylistSeq', + 'mediacontrol_Position*': 'MediaControlPosition', + 'mediacontrol_StreamInformation*': 'MediaControlStreamInformation', + 'mediacontrol_PositionOrigin': 'ctypes.c_uint', + 'mediacontrol_PositionKey': 'ctypes.c_uint', + 'WINDOWHANDLE': 'ctypes.c_ulong', + + 'short': 'ctypes.c_short', + 'char*': 'ctypes.c_char_p', + 'char**': 'ListPOINTER(ctypes.c_char_p)', + 'uint32_t': 'ctypes.c_uint', + 'float': 'ctypes.c_float', + 'unsigned': 'ctypes.c_uint', + 'void': 'None', + 'void*': 'ctypes.c_void_p', + 'int': 'ctypes.c_int', + '...': 'FIXMEva_list', + 'libvlc_callback_t': 'FIXMEcallback', + 'libvlc_time_t': 'ctypes.c_longlong', + 'libvlc_video_marquee_int_option_t': 'ctypes.c_int', + 'libvlc_video_marquee_string_option_t': 'ctypes.c_char_p', + # FIXME: enums -> to be processed + 'libvlc_media_option_t': 'ctypes.c_uint', + 'libvlc_meta_t': 'ctypes.c_uint', + 'libvlc_state_t': 'State', + } + +# Defined python classes, i.e. classes for which we want to generate +# class wrappers around libvlc functions +defined_classes=( + 'MediaPlayer', + 'Instance', + 'Media', + 'Log', + 'LogIterator', + #'LogMessage', + 'EventType', + 'EventManager', + 'MediaDiscoverer', + 'MediaLibrary', + 'MediaList', + 'MediaListPlayer', + 'MediaListView', + 'TrackDescription', + 'AudioOutput', + 'MediaControl', + #'RGBPicture', + #'MediaControlPosition', + #'MediaControlStreamInformation', + ) + +# Definition of prefixes that we can strip from method names when +# wrapping them into class methods +prefixes=dict( (v, k[:-2]) for (k, v) in typ2class.iteritems() if v in defined_classes ) +prefixes['MediaControl']='mediacontrol_' + +def parse_param(s): + """Parse a C parameter expression. + + It is used to parse both the type/name for methods, and type/name + for their parameters. + + It returns a tuple (type, name). + """ + s=s.strip() + s=s.replace('const', '') + if 'VLC_FORWARD' in s: + m=forward_re.match(s) + s=m.group(1)+m.group(2) + m=param_re.search(s) + if m: + const, typ, name=m.groups() + while name.startswith('*'): + typ += '*' + name=name[1:] + if name == 'const*': + # K&R definition: const char * const* + name='' + typ=typ.replace(' ', '') + return typ, name + else: + # K&R definition: only type + return s.replace(' ', ''), '' + +def generate_header(classes=None): + """Generate header code. + """ + f=open('header.py', 'r') + for l in f: + if 'build_date' in l: + print 'build_date="%s"' % time.ctime() + else: + print l, + f.close() + +def parse_include(name): + """Parse include file. + + This generates a tuple for each function: + (return_type, method_name, parameter_list, comment) + with parameter_list being a list of tuples (parameter_type, parameter_name). + """ + f=open(name, 'r') + accumulator='' + comment='' + for l in f: + # Note: lstrip() should not be necessary, but there is 1 badly + # formatted comment in vlc1.0.0 includes + if l.lstrip().startswith('/**'): + comment='' + continue + elif l.startswith(' * '): + comment = comment + l[3:] + continue + + l=l.strip() + + if accumulator: + accumulator=" ".join( (accumulator, l) ) + if l.endswith(');'): + # End of definition + l=accumulator + accumulator='' + elif l.startswith('VLC_PUBLIC_API') and not l.endswith(');'): + # Multiline definition. Accumulate until end of definition + accumulator=l + continue + + m=api_re.match(l) + if m: + (ret, param)=m.groups() + + rtype, method=parse_param(ret) + + params=[] + for p in paramlist_re.split(param): + params.append( parse_param(p) ) + + if len(params) == 1 and params[0][0] == 'void': + # Empty parameter list + params=[] + + if list(p for p in params if not p[1]): + # Empty parameter names. Have to poke into comment. + names=comment_re.findall(comment) + if len(names) < len(params): + # Bad description: all parameters are not specified. + # Generate default parameter names + badnames=[ "param%d" % i for i in xrange(len(params)) ] + # Put in the existing ones + for (i, p) in enumerate(names): + badnames[i]=names[i] + names=badnames + print "### Error ###" + print "### Cannot get parameter names from comment for %s: %s" % (method, comment.replace("\n", ' ')) + # Note: this was previously + # raise Exception("Cannot get parameter names from comment for %s: %s" % (method, comment)) + # but it prevented code generation for a minor detail (some bad descriptions). + params=[ (p[0], names[i]) for (i, p) in enumerate(params) ] + + for typ, name in params: + if not typ in typ2class: + raise Exception("No conversion for %s (from %s:%s)" % (typ, method, name)) + + # Transform Doxygen syntax into epydoc syntax + comment=comment.replace('\\param', '@param').replace('\\return', '@return') + + if debug: + print '********************' + print l + print '-------->' + print "%s (%s)" % (method, rtype) + for typ, name in params: + print " %s (%s)" % (name, typ) + print '********************' + yield (rtype, + method, + params, + comment) + +def output_ctypes(rtype, method, params, comment): + """Output ctypes decorator for the given method. + """ + if method in blacklist: + # FIXME + return + + if params: + print "prototype=ctypes.CFUNCTYPE(%s, %s)" % (typ2class.get(rtype, 'FIXME_%s' % rtype), + ",".join( typ2class[p[0]] for p in params )) + else: + print "prototype=ctypes.CFUNCTYPE(%s)" % typ2class.get(rtype, 'FIXME_%s' % rtype) + + + if not params: + flags='paramflags= tuple()' + elif len(params) == 1: + flags="paramflags=( (%d, ), )" % parameter_passing[params[0][0]] + else: + flags="paramflags=%s" % ",".join( '(%d,)' % parameter_passing[p[0]] for p in params ) + print flags + print '%s = prototype( ("%s", dll), paramflags )' % (method, method) + if '3' in flags: + # A VLCException is present. Process it. + print "%s.errcheck = check_vlc_exception" % method + print '%s.__doc__ = """%s"""' % (method, comment) + print + +def parse_override(name): + """Parse override definitions file. + + It is possible to override methods definitions in classes. + """ + res={} + + data=[] + current=None + f=open(name, 'r') + for l in f: + m=re.match('class (\S+):', l) + if m: + # Dump old data + if current is not None: + res[current]="\n".join(data) + current=m.group(1) + data=[] + continue + data.append(l) + res[current]="\n".join(data) + f.close() + return res + +def fix_python_comment(c): + """Fix comment by removing first and last parameters (self and exception) + """ + data=c.splitlines() + body=itertools.takewhile(lambda l: not '@param' in l, data) + param=[ python_param_re.sub('\\1:\\2', l) for l in itertools.ifilter(lambda l: '@param' in l, data) ] + ret=[ l.replace('@return', '@return:') for l in itertools.ifilter(lambda l: '@return' in l, data) ] + + if len(param) >= 2: + param=param[1:-1] + elif len(param) == 1: + param=[] + + return "\n".join(itertools.chain(body, param, ret)) + +def generate_wrappers(methods): + """Generate class wrappers for all appropriate methods. + + @return: the set of wrapped method names + """ + ret=set() + # Sort methods against the element they apply to. + elements=sorted( ( (typ2class.get(params[0][0]), rt, met, params, c) + for (rt, met, params, c) in methods + if params and typ2class.get(params[0][0], '_') in defined_classes + ), + key=operator.itemgetter(0)) + + overrides=parse_override('override.py') + + for classname, el in itertools.groupby(elements, key=operator.itemgetter(0)): + print """ +class %(name)s(object): + def __init__(self, pointer=None): + '''Internal method used for instanciating wrappers from ctypes. + ''' + if pointer is None: + raise Exception("Internal method. You should instanciate objects through other class methods (probably named 'new' or ending with 'new')") + self._as_parameter_=ctypes.c_void_p(pointer) + + @staticmethod + def from_param(arg): + '''(INTERNAL) ctypes parameter conversion method. + ''' + return arg._as_parameter_ +""" % {'name': classname} + + if classname in overrides: + print overrides[classname] + + prefix=prefixes.get(classname, '') + + for cl, rtype, method, params, comment in el: + if method in blacklist: + continue + # Strip prefix + name=method.replace(prefix, '').replace('libvlc_', '') + ret.add(method) + if params: + params[0]=(params[0][0], 'self') + if params and params[-1][0] in ('libvlc_exception_t*', 'mediacontrol_Exception*'): + args=", ".join( p[1] for p in params[:-1] ) + else: + args=", ".join( p[1] for p in params ) + + print " def %s(%s):" % (name, args) + print ' """%s\n"""' % fix_python_comment(comment) + if params and params[-1][0] == 'libvlc_exception_t*': + # Exception handling + print " e=VLCException()" + print " return %s(%s, e)" % (method, args) + elif params and params[-1][0] == 'mediacontrol_Exception*': + # Exception handling + print " e=MediaControlException()" + print " return %s(%s, e)" % (method, args) + else: + print " return %s(%s)" % (method, args) + print + return ret + +if __name__ == '__main__': + methods=[] + for name in sys.argv[1:]: + methods.extend(list(parse_include(name))) + if debug: + sys.exit(0) + + generate_header() + wrapped=generate_wrappers(methods) + for l in methods: + output_ctypes(*l) + + all=set( t[1] for t in methods ) + not_wrapped=all.difference(wrapped) + print "# Not wrapped methods:" + for m in not_wrapped: + print "# ", m + diff --git a/bindings/python-ctypes/header.py b/bindings/python-ctypes/header.py new file mode 100755 index 0000000000..ae2e8e0bf3 --- /dev/null +++ b/bindings/python-ctypes/header.py @@ -0,0 +1,161 @@ +#! /usr/bin/python + +# +# Python ctypes bindings for VLC +# Copyright (C) 2009 the VideoLAN team +# $Id: $ +# +# Authors: Olivier Aubert +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. +# + +import ctypes +import sys + +build_date="This will be replaced by the build date" + +if sys.platform == 'linux2': + dll=ctypes.CDLL('libvlc.so') +elif sys.platform == 'win32': + dll=ctypes.CDLL('libvlc.dll') +elif sys.platform == 'darwin': + # FIXME: should find a means to configure path + dll=ctypes.CDLL('/Applications/VLC.app/Contents/MacOS/lib/libvlc.2.dylib') + +class ListPOINTER(object): + '''Just like a POINTER but accept a list of ctype as an argument. + ''' + def __init__(self, etype): + self.etype = etype + + def from_param(self, param): + if isinstance(param, (list,tuple)): + return (self.etype * len(param))(*param) + +# From libvlc_structures.h +class VLCException(ctypes.Structure): + _fields_= [ + ('raised', ctypes.c_int), + ('code', ctypes.c_int), + ('message', ctypes.c_char_p), + ] + def init(self): + libvlc_exception_init(self) + + def clear(self): + libvlc_exception_clear(self) + +class PlaylistItem(ctypes.Structure): + _fields_= [ + ('id', ctypes.c_int), + ('uri', ctypes.c_char_p), + ('name', ctypes.c_char_p), + ] + +class LogMessage(ctypes.Structure): + _fields_= [ + ('size', ctypes.c_uint), + ('severity', ctypes.c_int), + ('type', ctypes.c_char_p), + ('name', ctypes.c_char_p), + ('header', ctypes.c_char_p), + ('message', ctypes.c_char_p), + ] + +class MediaControlPosition(ctypes.Structure): + _fields_= [ + ('origin', ctypes.c_ushort), + ('key', ctypes.c_ushort), + ('value', ctypes.c_longlong), + ] + + @staticmethod + def from_param(arg): + if isinstance(arg, (int, long)): + p=MediaControlPosition() + p.value=arg + p.key=2 + return p + else: + return arg + +class MediaControlPositionOrigin(ctypes.c_uint): + enum=( + 'AbsolutePosition', + 'RelativePosition', + 'ModuloPosition', + ) + def __repr__(self): + return self.enum[self.value] + +class State(ctypes.c_uint): + # FIXME: should be improved (State.NothingSpecial should hold the value) + # and maybe auto-generated from typedefs + enum=( + 'NothingSpecial', + 'Opening', + 'Buffering', + 'Playing', + 'Paused', + 'Stopped', + 'Ended', + 'Error', + ) + def __repr__(self): + return self.enum[self.value] + +class MediaControlException(ctypes.Structure): + _fields_= [ + ('code', ctypes.c_int), + ('message', ctypes.c_char_p), + ] + def init(self): + mediacontrol_exception_init(self) + + def clear(self): + mediacontrol_exception_free(self) + +class MediaControlStreamInformation(ctypes.Structure): + _fields_= [ + ('code', ctypes.c_int), + ('message', ctypes.c_char_p), + ] + +class RGBPicture(ctypes.Structure): + _fields_= [ + ('width', ctypes.c_int), + ('height', ctypes.c_int), + ('type', ctypes.c_uint32), + ('date', ctypes.c_longlong), + ('size', ctypes.c_int), + ('data', ctypes.c_char_p), + ] + + def free(self): + mediacontrol_RGBPicture__free(self) + +# Decorator for callback methods +callbackmethod=ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p) + +def check_vlc_exception(result, func, args): + """Error checking method for functions using an exception in/out parameter. + """ + ex=args[-1] + # Take into account both VLCException and MediacontrolException + c=getattr(ex, 'raised', getattr(ex, 'code', 0)) + if c: + raise Exception(args[-1].message) + return result diff --git a/bindings/python-ctypes/override.py b/bindings/python-ctypes/override.py new file mode 100644 index 0000000000..88db892553 --- /dev/null +++ b/bindings/python-ctypes/override.py @@ -0,0 +1,31 @@ +class Instance: + @staticmethod + def new(*p): + """Create a new Instance. + """ + e=VLCException() + return libvlc_new(len(p), p, e) + +class MediaControl: + @staticmethod + def new(*p): + """Create a new MediaControl + """ + e=MediaControlException() + return mediacontrol_new(len(p), p, e) + + @staticmethod + def new_from_instance(i): + """Create a new MediaControl from an existing Instance. + """ + e=MediaControlException() + return mediacontrol_new_from_instance(i, e) + +class MediaList: + def __len__(self): + e=VLCException() + return libvlc_media_list_count(self, e) + + def __getitem__(self, i): + e=VLCException() + return libvlc_media_list_item_at_index(self, i, e)