#!/usr/bin/python
# h2ctypes - converts headers to ctypes modules
# Copyright (C) 2006 Leonard Ritter (contact@leonard-ritter.com)

# 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.

#
# DESCRIPTION:
# h2ctypes generates a ctypes-compliant python file from any C header file.
# it also uses nm to lookup libraries needed to properly import
# functions.
#
# REQUIREMENTS:
# h2ctypes.py requires gccxml.
#
# SYNTAX:
# $ python h2ctypes.py <headerfile> [<headermask> [<headermask> [...]]]
#
# you need to specify a base headerfile as first parameter, and one or more 
# selective arguments to filter out which headers you want.
#
# e.g.
#
# python h2ctypes.py /usr/include/alsa/asoundlib.h seq_event.h seq.h
#
# will output python code for seq_event.h and seq.h.
#
# if you do not specify any parameters, h2ctypes will just print out
# a list of possible header files to select from.
#
# it is often possible that due to cross references the generated code
# does not work right away, but h2ctypes can still be helping when
# writing ctypes bindings for c libraries.
#
# defines are supported and will be tested prior to export.
#

from xml.dom.minidom import parse, parseString
import sys, os

if len(sys.argv) < 2:
	print "syntax:"
	print "\tpython h2ctypes.py <headerfile> [<headermask> [<headermask> [...]]]"
	raise SystemExit
hfile = sys.argv[1]
selection = sys.argv[2:]

ofile = '/tmp/h2ctypes_tmp.xml'

acceptfiles = []
id2e = {}

type2ctype = {
'bool' : 'c_bool',
'char' : 'c_char',
'signed char' : 'c_char',
'float' : 'c_float',
'complex float' : 'c_float',
'double' : 'c_double',
'complex double' : 'c_double',
'long double' : 'c_double',
'complex long double' : 'c_double',
'wchar_t' : 'c_wchar',
'unsigned char' : 'c_ubyte',
'unsigned int' : 'c_uint',
'int' : 'c_int',
'short unsigned int' : 'c_ushort',
'short int' : 'c_short',
'void' : 'None',
'long int' : 'c_long',
'long long int' : 'c_longlong',
'long long unsigned int' : 'c_ulonglong',
'long unsigned int' : 'c_ulong',
}

export2lib = {}
imported = []

predefs = {
'size_t' : 'c_ulong',
'ssize_t' : 'c_uint',
'__ssize_t' : 'c_int',
'_IO_FILE' : 'c_ulong',
}

def enumerate_exports(prefix):
	import glob, re
	re_export = re.compile(r'^[0-9A-Fa-f]+ T ([A-Za-z0-9_]+)')
	for filename in glob.glob(os.path.join(prefix,'*.so')):
		for line in os.popen('nm -D "%s"' % filename):
			m = re_export.match(line)
			if m:
				export_name = m.group(1)				
				#assert not export_name in export2lib, "%s is already in %s, now seen in %s" % (export_name, export2lib[export_name], filename)
				export2lib[export_name] = filename

current_filepath = ''
def translate_typeid(parent, typeid, override = False):
	global current_filepath
	node = id2e[typeid]
	fileid = node.getAttribute('file')
	if (fileid in id2e) and ((fileid in acceptfiles) or override):
		filepath = id2e[fileid].getAttribute('name')
		#~ if not filepath == current_filepath:
		#~ print '# %s' % filepath	
		current_filepath = filepath
	if node.tagName == 'FundamentalType':
		return type2ctype[node.getAttribute('name')]
	elif node.tagName == 'Typedef':
		nodename = node.getAttribute('name')
		if nodename.startswith('__'): # we get weird problems with those
			nodename = 't' + nodename
		if (not node.defined) and ((fileid in acceptfiles) or override):			
			name = translate_typeid(parent, node.getAttribute('type'), True)
			node.defined = True
			if nodename not in predefs:
				print '%s = %s' % (nodename, name)
				print
			#~ else:
				#~ sys.stderr.write('WARNING: %s is declared in %s, but it is not included.\n' % (node.getAttribute('name'),id2e[fileid].getAttribute('name')))
				#~ sys.stderr.write("would be '%s = %s'\n" % (node.getAttribute('name'), name))				
		return type2ctype.get(nodename,nodename)
	elif node.tagName == 'ArrayType':
		typename = translate_typeid(parent, node.getAttribute('type'), override)
		nmax = node.getAttribute('max')
		if nmax and (nmax != 'ffffffffffffffff'):
			num = int(nmax)+1		
			return '%s*%s' % (typename ,num)
		else:
			return 'ARRAY(%s)' % (typename)
	elif node.tagName == 'PointerType':
		typename = translate_typeid(parent, node.getAttribute('type'), override)
		subnode = id2e[node.getAttribute('type')]
		if subnode.tagName == 'FunctionType':
			return typename # 
		if subnode.tagName == 'MethodType':
			return typename # 
		if typename == 'None':
			return 'c_void_p'
		if typename == 'c_char':
			return 'c_char_p'
		assert typename, id2e[node.getAttribute('type')].tagName
		return 'POINTER(%s)' % typename
	elif node.tagName == 'Union':
		name = node.getAttribute('name')
		if not name:
			import random
			name = 'union__' + ''.join([random.choice('0123456789abcdef') for i in range(8)])
			node.setAttribute('name',name)
		if (name[0] == '.'):
			if not parent:
				# wait until next reference
				return name
			name = parent.getAttribute('name') + '_union' + name[1:]
			node.setAttribute('name',name)
		if (not node.defined) and ((fileid in acceptfiles) or override):
			node.defined = True
			structfields = []
			for member in node.getAttribute('members').split(' '):			
				if member:
					member = id2e[member]
					if member.tagName == 'Field':
						fieldname = member.getAttribute('name')
						fieldtype = translate_typeid(node, member.getAttribute('type'), True)
						structfields.append((fieldname, fieldtype))
			print 'class %s(Union):' % name		
			print '\t_fields_ = ['		
			for fieldname, fieldtype in structfields:
				print "\t\t('%s', %s)," % (fieldname, fieldtype)
			print '\t]'
			print			
		return name
	elif node.tagName == 'Struct':
		if node == parent:
			return 'None'
		name = node.getAttribute('name')
		if not name:
			import random
			name = 'struct__' + ''.join([random.choice('0123456789abcdef') for i in range(8)])
			node.setAttribute('name',name)
		if (name[0] == '.'):
			if not parent:
				# wait until next reference
				return name
			name = parent.getAttribute('name') + '_struct' + name[1:]
			node.setAttribute('name',name)
		if (not node.defined) and ((fileid in acceptfiles) or override):
			node.defined = True
			node.under_construction = True
			structfields = []
			for member in node.getAttribute('members').split(' '):			
				if member:
					member = id2e[member]
					if member.tagName == 'Field':
						fieldname = member.getAttribute('name')
						fieldtype = translate_typeid(node, member.getAttribute('type'), True)						
						structfields.append((fieldname, fieldtype))
			node.under_construction = False
			print 'class %s(Structure):' % name		
			print '\t_fields_ = ['		
			for fieldname, fieldtype in structfields:
				print "\t\t('%s', %s)," % (fieldname, fieldtype)
			print '\t]'
			print
		if node.under_construction:
			return "'%s'" % name
		return name
	elif node.tagName == 'Enumeration':
		if (not node.defined) and ((fileid in acceptfiles) or override):
			node.defined = True
			print '# enumeration %s' % (node.getAttribute('name'))		
			for child in [child for child in node.childNodes if child.nodeType == node.ELEMENT_NODE]:
				if child.tagName == 'EnumValue':
					print '%s = %s' % (child.getAttribute('name'), child.getAttribute('init'))
			print
		return 'c_int'
	elif node.tagName == 'Constructor':
		# ignore
		return ''
	elif node.tagName == 'Destructor':
		# ignore
		return ''
	elif node.tagName == 'Field':
		# ignore
		return node.getAttribute('name')
	elif node.tagName == 'CvQualifiedType':
		# constant variable
		if int(node.getAttribute('const')):
			return '%s' % translate_typeid(node, node.getAttribute('type'), override)
		else:
			return translate_typeid(node, node.getAttribute('type'), override)
	elif node.tagName == 'ReferenceType':
		# ignore
		return node.getAttribute('name')
	elif node.tagName == 'Namespace':
		# ignore
		return node.getAttribute('name')
	elif node.tagName == 'MethodType':
		# ignore
		return node.getAttribute('name')
	elif node.tagName == 'Method':
		# ignore
		return node.getAttribute('name')
	elif node.tagName == 'File':
		if (not node.defined) and ((node.getAttribute('id') in acceptfiles) or override):
			node.defined = True
			name = node.getAttribute('name')
			print '# using file "%s"' % name			
		return node.getAttribute('name')
	elif node.tagName == 'Function':
		if (not node.defined) and ((fileid in acceptfiles) or override):
			node.defined = True
			name = node.getAttribute('name')
			rettype = translate_typeid(node, node.getAttribute('returns'), True)
			args = []
			for child in [child for child in node.childNodes if child.nodeType == node.ELEMENT_NODE]:
				if child.tagName == 'Argument':
					argname = child.getAttribute('name')					
					args.append((argname,translate_typeid(node, child.getAttribute('type'), True)))
			file = id2e[fileid]
			basename = None
			if name in export2lib:
				libpath = export2lib[name]
				basename = os.path.splitext(os.path.basename(libpath))[0]
				if not basename in imported:
					imported.append(basename)
					libname = str(os.path.splitext(os.path.basename(libpath))[0])
					if os.path.splitext(os.path.basename(libpath))[1] == '.so':
						libname = libname[3:]
					print '%s = dlopen(%s)' % (basename, repr(libname))
			if not basename:
				print >> sys.stderr, "h2ctypes: cant find lib for function '%s'" % name
				basename = 'None'
			kwds = ','.join(['(%s,%s)' % (repr(str(ak) or 'arg%i' % (ai+1)).ljust(10),av.ljust(10)) for ai,(ak,av) in zip(range(len(args)),args)])
			print '%s = dlsym(%s, %s, %s, %s)' % (name.ljust(40), basename, repr(str(name)).ljust(40), rettype, kwds)
		return node.getAttribute('name')
	elif node.tagName == 'FunctionType':
		rettype = translate_typeid(node, node.getAttribute('returns'), override)
		args = []
		for child in [child for child in node.childNodes if child.nodeType == node.ELEMENT_NODE]:
			if child.tagName == 'Argument':
				argname = child.getAttribute('name')
				args.append((argname,translate_typeid(node, child.getAttribute('type'), override)))
		return 'CFUNCTYPE(%s,%s)' % (rettype, ','.join([x[1] for x in args]))
	elif node.tagName == 'Variable':
		# ignore
		return node.getAttribute('name')
	raise 'unknown:', node.tagName

import ctypes_wrap.cparser
import ctypes_wrap.cparser_config

includepath = os.path.normpath(os.path.join(os.getcwd(),'include'))
gccxmlcmd = 'gccxml "%s" --gccxml-cxxflags "-DXP_UNIX -I%s -I%s" --gccxml-compiler gcc "-fxml=%s"' % (hfile, includepath, os.getcwd(), ofile)
print >> sys.stderr, "executing %s" % gccxmlcmd
result = os.system(gccxmlcmd)
if not result:
	result = ''
	dom = parse(ofile)
	#os.remove(ofile)
	gccxml = dom.getElementsByTagName('GCC_XML')[0]
	elements = [node for node in gccxml.childNodes if node.nodeType == node.ELEMENT_NODE]
	for node in elements:
		id2e[node.getAttribute('id')] = node
	for node in id2e.values():
		node.defined = False
		node.under_construction = False
		
	includes = []
		
	ignorefiles = []
	for node in elements:
		if node.tagName == 'File':
			accepted = False
			for s in selection:
				if s in node.getAttribute('name'):
					#print node.getAttribute('name')
					includes.append(node.getAttribute('name'))
					acceptfiles.append(node.getAttribute('id'))
					accepted = True
					break
			if not accepted:	
				ignorefiles.append(node.getAttribute('name'))
	if not acceptfiles:
		print 'following headers are available:'
		print '\n'.join(ignorefiles)
		raise SystemExit
		
	enumerate_exports('/usr/lib')
	enumerate_exports('.')
	
	class HasAny:
		def __getattr__(self,name):
			return None
	class IncludeParser(ctypes_wrap.cparser.IncludeParser):
		def __init__(self):
			self.options = HasAny()
			self.excluded = []
			self.excluded_re = []
			self.gccxml_args = ['--gccxml-cxxflags','-DXP_UNIX -I%s -I%s' % (includepath, os.getcwd())]
	ip = IncludeParser()	
	aliases, functions, excluded, defines = ip.filter_definitions(ip.get_defines(includes))		

	print '# generated by h2ctypes, do not touch'
	print
	print 'from ctypes import *'
	print 'from procwrap import *'
	print
	print '# predefines'
	for k,v in predefs.iteritems():
		print '%s = %s' % (k,v)
	print
	
	print '# defines'
	import ctypes
	class DefinePrinter:
		definelines = ''
		definelocals = {}
		defineglobals = dict([(k,getattr(ctypes,k)) for k in dir(ctypes)])	
		
		def print_define(self, k):
			v = self.defines[k]
			for k2 in sorted(self.defines.keys()):
				if (k2 in v) and (k2 in self.defines) and (k2 != k): # could have been deleted already
					self.print_define(k2)
			if not k.startswith('_') and k.isupper():
				for tk, tv in type2ctype.iteritems():
					v = v.replace(tk,tv)
				defineline = '%s = %s' % (k,v)
				try:
					exec(defineline, self.definelocals, self.defineglobals)
					print defineline
				except:
					import traceback
					traceback.print_exc()
					print '# ' + defineline
			del self.defines[k]
			
		def print_defines(self):
			for k in sorted(self.defines.keys()):
				if k in self.defines: # could have been deleted already
					dp.print_define(k)		
	dp = DefinePrinter()
	dp.defines = defines
	dp.print_defines()
	print
	
	for node in reversed(elements):
		translate_typeid(None, node.getAttribute('id'))
else:
	raise SystemExit,255
