You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

502 lines
21 KiB
Python

# -*- coding: utf-8 -*-
"""
CLibrary.py - Provides CLibrary class
Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
Proxy to both CHeader and ctypes, allowing automatic type conversion and
function calling based on C header definitions.
"""
from ctypes import *
import sys
class CLibrary:
"""The CLibrary class is intended to automate much of the work in using ctypes by integrating
header file definitions from CParser. Ths class serves as a proxy to a ctypes, adding
a few features:
- allows easy access to values defined via CParser
- automatic type conversions for function calls using CParser function signatures
- creates ctype classes based on type definitions from CParser
Initialize using a ctypes shared object and a CParser:
headers = CParser.winDefs()
lib = CLibrary(windll.User32, headers)
There are 3 ways to access library elements:
lib(type, name) - type can be one of 'values', 'functions', 'types', 'structs', 'unions', or 'enums'.
Returns an object matching name. For values, the value from the headers is
returned. For functions, a callable object is returned that handles automatic
type conversion for arguments and return values. for structs, types, and enums,
a ctypes class is returned matching the type specified.
lib.name - searches in order through values, functions, types, structs, unions, and enums from
header definitions and returns an object for the first match found. The object
returned is the same as returned by lib(type, name). This is the preferred way to access
elements from CLibrary, but may not work in some situations (for example, if
a struct and variable share the same name).
lib[type] - Accesses the header definitions directly, returns definition dictionaries
based on the type requested. This is equivalent to headers.defs[type].
"""
Null = object()
cTypes = {
'char': c_char,
'wchar': c_wchar,
'unsigned char': c_ubyte,
'short': c_short,
'short int': c_short,
'unsigned short': c_ushort,
'unsigned short int': c_ushort,
'int': c_int,
'unsigned': c_uint,
'unsigned int': c_uint,
'long': c_long,
'long int': c_long,
'unsigned long': c_ulong,
'unsigned long int': c_ulong,
'__int64': c_longlong,
'long long': c_longlong,
'long long int': c_longlong,
'unsigned __int64': c_ulonglong,
'unsigned long long': c_ulonglong,
'unsigned long long int': c_ulonglong,
'float': c_float,
'double': c_double,
'long double': c_longdouble
}
cPtrTypes = {
'char': c_char_p,
'wchar': c_wchar_p,
'void': c_void_p
}
def __init__(self, lib, headers, prefix=None):
## name everything using underscores to avoid name collisions with library
self._lib_ = lib
self._headers_ = headers
self._defs_ = headers.defs
if prefix is None:
self._prefix_ = []
elif type(prefix) is list:
self._prefix_ = prefix
else:
self._prefix_ = [prefix]
self._objs_ = {}
for k in ['values', 'functions', 'types', 'structs', 'unions', 'enums']:
self._objs_[k] = {}
self._allObjs_ = {}
self._structs_ = {}
self._unions_ = {}
def __call__(self, typ, name):
if typ not in self._objs_:
typs = self._objs_.keys()
raise Exception("Type must be one of %s" % str(typs))
if name not in self._objs_[typ]:
self._objs_[typ][name] = self._mkObj_(typ, name)
return self._objs_[typ][name]
def _allNames_(self, name):
return [name] + [p + name for p in self._prefix_]
def _mkObj_(self, typ, name):
names = self._allNames_(name)
for n in names:
if n in self._objs_:
return self._objs_[n]
for n in names: ## try with and without prefix
if n not in self._defs_[typ] and not (typ in ['structs', 'unions', 'enums'] and n in self._defs_['types']):
continue
if typ == 'values':
return self._defs_[typ][n]
elif typ == 'functions':
return self._getFunction(n)
elif typ == 'types':
obj = self._defs_[typ][n]
return self._ctype(obj)
elif typ == 'structs':
return self._cstruct('structs', n)
elif typ == 'unions':
return self._cstruct('unions', n)
elif typ == 'enums':
## Allow automatic resolving of typedefs that alias enums
if n not in self._defs_['enums']:
if n not in self._defs_['types']:
raise Exception('No enums named "%s"' % n)
typ = self._headers_.evalType([n])[0]
if typ[:5] != 'enum ':
raise Exception('No enums named "%s"' % n)
n = self._defs_['types'][typ][1] ## look up internal name of enum
obj = self._defs_['enums'][n]
return obj
else:
raise Exception("Unknown type %s" % typ)
raise NameError(name)
def __getattr__(self, name):
"""Used to retrieve any type of definition from the headers. Searches for the name in this order:
values, functions, types, structs, unions, enums."""
if name not in self._allObjs_:
names = self._allNames_(name)
for k in ['values', 'functions', 'types', 'structs', 'unions', 'enums', None]:
if k is None:
raise NameError(name)
obj = None
for n in names:
if n in self._defs_[k]:
obj = self(k, n)
break
if obj is not None:
break
self._allObjs_[name] = obj
return self._allObjs_[name]
def __getitem__(self, name):
"""Used to retrieve a specific dictionary from the headers."""
return self._defs_[name]
def __repr__(self):
return "<CLibrary instance: %s>" % str(self._lib_)
def _getFunction(self, funcName):
try:
func = getattr(self._lib_, funcName)
except:
raise Exception("Function name '%s' appears in headers but not in library!" % func)
#print "create function %s," % (funcName), self._defs_['functions'][funcName]
return CFunction(self, func, self._defs_['functions'][funcName], funcName)
def _ctype(self, typ, pointers=True):
"""return a ctype object representing the named type.
If pointers is True, the class returned includes all pointer/array specs provided.
Otherwise, the class returned is just the base type with no pointers."""
try:
typ = self._headers_.evalType(typ)
mods = typ[1:][:]
## Create the initial type
## Some types like ['char', '*'] have a specific ctype (c_char_p)
## (but only do this if pointers == True)
if pointers and len(typ) > 1 and typ[1] == '*' and typ[0] in CLibrary.cPtrTypes:
cls = CLibrary.cPtrTypes[typ[0]]
mods = typ[2:]
## If the base type is in the list of existing ctypes:
elif typ[0] in CLibrary.cTypes:
cls = CLibrary.cTypes[typ[0]]
## structs, unions, enums:
elif typ[0][:7] == 'struct ':
cls = self._cstruct('structs', self._defs_['types'][typ[0]][1])
elif typ[0][:6] == 'union ':
cls = self._cstruct('unions', self._defs_['types'][typ[0]][1])
elif typ[0][:5] == 'enum ':
cls = c_int
## void
elif typ[0] == 'void':
cls = None
else:
#print typ
raise Exception("Can't find base type for %s" % str(typ))
if not pointers:
return cls
## apply pointers and arrays
while len(mods) > 0:
m = mods.pop(0)
if isinstance(m, basestring): ## pointer or reference
if m[0] == '*' or m[0] == '&':
for i in m:
cls = POINTER(cls)
elif type(m) is list: ## array
for i in m:
if i == -1: ## -1 indicates an 'incomplete type' like "int variable[]"
cls = POINTER(cls) ## which we should interpret like "int *variable"
else:
cls = cls * i
elif type(m) is tuple: ## Probably a function pointer
## Find pointer and calling convention
isPtr = False
conv = '__cdecl'
if len(mods) == 0:
raise Exception("Function signature with no pointer:", m, mods)
for i in [0,1]:
if len(mods) < 1:
break
if mods[0] == '*':
mods.pop(0)
isPtr = True
elif mods[0] in ['__stdcall', '__cdecl']:
conv = mods.pop(0)
else:
break
if not isPtr:
raise Exception("Not sure how to handle type (function without single pointer): %s" % str(typ))
if conv == '__stdcall':
mkfn = WINFUNCTYPE
else:
mkfn = CFUNCTYPE
#print "Create function pointer (%s)" % conv
args = [self._ctype(arg[1]) for arg in m]
cls = mkfn(cls, *args)
else:
raise Exception("Not sure what to do with this type modifier: '%s'" % str(p))
return cls
except:
print "Error while processing type", typ
raise
def _cstruct(self, strType, strName):
if strName not in self._structs_:
## Resolve struct name--typedef aliases allowed.
if strName not in self._defs_[strType]:
if strName not in self._defs_['types']:
raise Exception('No struct/union named "%s"' % strName)
typ = self._headers_.evalType([strName])[0]
if typ[:7] != 'struct ' and typ[:6] != 'union ':
raise Exception('No struct/union named "%s"' % strName)
strName = self._defs_['types'][typ][1]
## Pull struct definition
defn = self._defs_[strType][strName]
## create ctypes class
defs = defn['members'][:]
if strType == 'structs':
class s(Structure):
def __repr__(self):
return "<ctypes struct '%s'>" % strName
elif strType == 'unions':
class s(Union):
def __repr__(self):
return "<ctypes union '%s'>" % strName
## must register struct here to allow recursive definitions.
self._structs_[strName] = s
if defn['pack'] is not None:
s._pack_ = defn['pack']
## assign names to anonymous members
members = []
anon = []
for i in range(len(defs)):
if defs[i][0] is None:
c = 0
while True:
name = 'anon_member%d' % c
if name not in members:
defs[i][0] = name
anon.append(name)
break
members.append(defs[i][0])
s._anonymous_ = anon
s._fields_ = [(m[0], self._ctype(m[1])) for m in defs]
s._defaults_ = [m[2] for m in defs]
return self._structs_[strName]
class CFunction:
def __init__(self, lib, func, sig, name):
self.lib = lib
self.func = func
#print sig
self.sig = list(sig) # looks like [return_type, [(argName, type, default), (argName, type, default), ...]]
self.sig[1] = [s for s in sig[1] if s[1] != ['void']] ## remove void args from list
for conv in ['__stdcall', '__cdecl']:
if conv in self.sig[0]:
self.sig[0].remove(conv)
self.name = name
self.restype = lib._ctype(self.sig[0])
#func.restype = self.restype
self.argTypes = [lib._ctype(s[1]) for s in self.sig[1]]
func.argtypes = self.argTypes
self.reqArgs = [x[0] for x in self.sig[1] if x[2] is None]
self.argInds = dict([(self.sig[1][i][0], i) for i in range(len(self.sig[1]))]) ## mapping from argument names to indices
#print "created func", self, sig, self.argTypes
def argCType(self, arg):
"""Return the ctype required for the specified argument.
arg can be either an integer or the name of the argument.
"""
if isinstance(arg, basestring):
arg = self.argInds[arg]
return self.lib._ctype(self.sig[1][arg][1])
def __call__(self, *args, **kwargs):
"""Invoke the SO or dll function referenced, converting all arguments to the correct type.
Keyword arguments are allowed as long as the header specifies the argument names.
Arguments which are passed byref may be omitted entirely, and will be automaticaly generated.
To pass a NULL pointer, give None as the argument.
Returns the return value of the function call as well as all of the arguments (so that objects passed by reference can be retrieved)"""
#print "CALL: %s(%s)" % (self.name, ", ".join(map(str, args) + ["%s=%s" % (k, str(kwargs[k])) for k in kwargs]))
#print " sig:", self.sig
argList = [None] * max(len(self.reqArgs), len(args)) ## We'll need at least this many arguments.
## First fill in args
for i in range(len(args)):
#argList[i] = self.argTypes[i](args[i])
if args[i] is None:
argList[i] = self.lib.Null
else:
argList[i] = args[i]
## Next fill in kwargs
for k in kwargs:
#print " kw:", k
if k not in self.argInds:
print "Function signature:", self.prettySignature()
raise Exception("Function signature has no argument named '%s'" % k)
ind = self.argInds[k]
if ind >= len(argList): ## stretch argument list if needed
argList += [None] * (ind - len(argList) + 1)
#argList[ind] = self.coerce(kwargs[k], self.argTypes[ind])
if kwargs[k] is None:
argList[ind] = self.lib.Null
else:
argList[ind] = kwargs[k]
guessedArgs = []
## Finally, fill in remaining arguments if they are pointers to int/float/void*/struct values
## (we assume these are to be modified by the function and their initial value is not important)
for i in range(len(argList)):
if argList[i] is None or argList[i] is self.lib.Null:
try:
sig = self.sig[1][i][1]
argType = self.lib._headers_.evalType(sig)
if argList[i] is self.lib.Null: ## request to build a null pointer
if len(argType) < 2:
raise Exception("Can not create NULL for non-pointer argument type: %s" % str(argType))
argList[i] = self.lib._ctype(sig)()
#elif argType == ['char', '*']: ## pass null pointer if none was specified. This is a little dangerous, but some functions will expect it.
#argList[i] = c_char_p() ## On second thought: let's just require the user to explicitly ask for a NULL pointer.
else:
if argType == ['void', '**'] or argType == ['void', '*', '*']:
cls = c_void_p
else:
assert len(argType) == 2 and argType[1] == '*' ## Must be 2-part type, second part must be '*'
cls = self.lib._ctype(sig, pointers=False)
argList[i] = pointer(cls(0))
guessedArgs.append(i)
except:
if sys.exc_info()[0] is not AssertionError:
raise
#sys.excepthook(*sys.exc_info())
print "Function signature:", self.prettySignature()
raise Exception("Function call '%s' missing required argument %d '%s'. (See above for signature)" % (self.name, i, self.sig[1][i][0]))
#print " args:", argList
try:
res = self.func(*argList)
except:
print "Function call failed. Signature is:", self.prettySignature()
print "Arguments:", argList
print "Argtypes:", self.func.argtypes
raise
#print " result:", res
cr = CallResult(res, argList, self.sig, guessed=guessedArgs)
return cr
def prettySignature(self):
return "%s %s(%s)" % (''.join(self.sig[0]), self.name, ', '.join(["%s %s" % ("".join(map(str, s[1])), s[0]) for s in self.sig[1]]))
class CallResult:
"""Class for bundling results from C function calls. Allows access to the function
return value as well as all of the arguments, since the function call will often return
extra values via these arguments.
- Original ctype objects can be accessed via result.rval or result.args
- Python values carried by these objects can be accessed using ()
To access values:
- The return value: ()
- The nth argument passed: [n]
- The argument by name: ['name']
- All values that were auto-generated: .auto()
The class can also be used as an iterator, so that tuple unpacking is possible:
ret, arg1, arg2 = lib.runSomeFunction(...)
"""
def __init__(self, rval, args, sig, guessed):
self.rval = rval ## return value of function call
self.args = args ## list of arguments to function call
self.sig = sig ## function signature
self.guessed = guessed ## list of arguments that were generated automatically (usually byrefs)
def __call__(self):
#print "Clibrary:", type(self.rval), self.mkVal(self.rval)
if self.sig[0] == ['void']:
return None
return self.mkVal(self.rval)
def __getitem__(self, n):
if type(n) is int:
return self.mkVal(self.args[n])
elif type(n) is str:
ind = self.findArg(n)
return self.mkVal(self.args[ind])
else:
raise Exception("Index must be int or str.")
def __setitem__(self, n, val):
if type(n) is int:
self.args[n] = val
elif type(n) is str:
ind = self.findArg(n)
self.args[ind] = val
else:
raise Exception("Index must be int or str.")
def mkVal(self, obj):
while not hasattr(obj, 'value'):
if not hasattr(obj, 'contents'):
return obj
try:
obj = obj.contents
except ValueError:
return None
return obj.value
def findArg(self, arg):
for i in range(len(self.sig[1])):
if self.sig[1][i][0] == arg:
return i
raise Exception("Can't find argument '%s' in function signature. Arguments are: %s" % (arg, str([a[0] for a in self.sig[1]])))
def __iter__(self):
yield self()
for i in range(len(self.args)):
yield(self[i])
def auto(self):
return [self[n] for n in self.guessed]