Files
idapython-src/tools/genhooks/genhooks.py
T
2019-10-15 15:05:24 +02:00

444 lines
16 KiB
Python

from __future__ import print_function
# TODO:
# * auto_queue_empty must return 1 by default. How should
# that be specified? In idp.hpp, or in a specific, 'recipe' file?
# WARNING: The code you are about to look into, is surprisingly messy and
# convoluted. Especially since it doesn't do that much. Perhaps a good
# refactoring is in order?
import six
from argparse import ArgumentParser
p = ArgumentParser()
p.add_argument("-i", "--input", required=True, dest="input", help="Input file")
p.add_argument("-o", "--output", required=True, dest="output", help="Output file")
p.add_argument("-x", "--xml", required=True, dest="xml", help="XML structure containing enumerations information")
p.add_argument("-c", "--hooks-class", required=True, dest="hooks_class", help="Name of the hooks class")
p.add_argument("-e", "--enum", required=True, dest="enum", help="Enumeration to look for")
p.add_argument("-m", "--marker", required=True, dest="marker", help="Marker to look for, to start generating callbacks")
p.add_argument("-r", "--default-rtype", required=True, dest="def_rtype", help="Default return type, for notifications that don't specify it")
p.add_argument("-n", "--default-rval", required=True, dest="def_rval", help="Default return value, for notifications that don't specify it")
p.add_argument("-q", "--qualifier", required=False, dest="qualifier", help="A possible qualifier, to put in front of enumerators", default="")
p.add_argument("-R", "--recipe", required=False, dest="recipe", help="Special information, needed to generate valid & compatible hooks")
p.add_argument("-d", "--discard-prefix",required=False, dest="discard_prefix",help="Discard all enum entries starting with the given prefix(es) (multiple prefixes can be specified as a comma-separated list.)")
p.add_argument("-D", "--discard-doc", required=False, dest="discard_doc", help="Typically for kernwin.hpp: ignore enum entries whose documentation starts with the specified pattern (e.g., 'ui:', which is for 'to UI' codes, and not 'from UI' codes)")
p.add_argument("-s", "--strip-prefix", required=False, dest="strip_prefix", help="Strip the given prefix from the enumerator name (e.g., 'ui_', for kernwin.hpp notifications)")
args = p.parse_args()
def warn(msg):
print("#### WARNING: %s" % msg)
if args.recipe:
exec(open(args.recipe).read())
else:
recipe = {}
from xml.dom import *
import xml.etree.ElementTree as ET
tree = ET.parse(args.xml)
# Find enum def
enum_el = tree.find(".//memberdef[@kind='enum']/[name='%s']" % args.enum)
def parse_return_type(desc):
default = None
import re
m = re.match(r".*\(default=([^\)]*)\).*", desc)
if m:
default = m.group(1)
if desc.startswith("void"):
return "void", default
else:
return parse_param_type(desc), default
def parse_param_type(desc):
if desc.startswith("("):
import re
m = re.match(r"\(([^\)]*)\)\s*.*", desc)
if m:
ptype = m.group(1)
return ptype.lstrip(":").replace(" ::", " ")
def collect_all_text(el):
bits = []
for txt in el.itertext():
bits.append(txt)
return "".join(bits)
enumerators = []
def add_enum_value(enumval_el, name, enum_name):
if not name.startswith("OBSOLETE"):
params = []
# Return type
ret_el = enumval_el.find(".//simplesect[@kind='return']")
rdata = {"name" : "<return>"}
rtype = None
rdefault = None
rexpr = None
recipe_data = recipe.get(name, {})
if "ignore" in recipe_data and recipe_data["ignore"]:
return
return_data = recipe_data["return"] if (recipe_data and "return" in recipe_data) else {}
if "type" in return_data:
rtype = return_data["type"]
elif ret_el is not None:
rdesc = ret_el.find("para").text
rtype, rdefault = parse_return_type(rdesc)
if rtype is None:
rtype = args.def_rtype
if rtype != "void" and rdefault is None:
rdefault = args.def_rval
assert(rdefault is not None)
if "default" in return_data:
rdefault = return_data["default"]
if "retexpr" in return_data:
rexpr = return_data["retexpr"]
params.append({
"name" : "<return>",
"type" : rtype,
"default" : rdefault,
"retexpr" : rexpr,
})
# Parameters
plist_el = enumval_el.find(".//parameterlist")
if plist_el is not None:
for pitem_el in plist_el.findall("./parameteritem"):
pname = pitem_el.find(".//parametername").text
if pname != "none" and pname != "...":
pdesc = collect_all_text(pitem_el.find(".//parameterdescription/para"))
ptype = parse_param_type(pdesc)
if ptype is None:
warn("Couldn't parse parameter description: \"%s\". Dropping notification \"%s\" altogether" % (pdesc, name))
return
params.append({
"name" : pname,
"type" : ptype,
})
# added parameters
if "add_params" in recipe_data:
params.extend(recipe_data["add_params"])
enumerators.append({"name" : name, "params" : params, "enum_name" : enum_name})
# for each enum value...
for enumval_el in enum_el.findall("./enumvalue"):
discarded = False
enum_name = enumval_el.find("./name").text
name = enum_name
if args.discard_prefix:
for pfx in args.discard_prefix.split(","):
if name.startswith(pfx):
discarded = True
break
if not discarded and args.discard_doc:
discarded = collect_all_text(enumval_el.find("./detaileddescription")).strip().startswith(args.discard_doc) or \
collect_all_text(enumval_el.find("./briefdescription")).strip().startswith(args.discard_doc)
if not discarded:
if args.strip_prefix:
pfxes = args.strip_prefix.split(",")
for pfx in pfxes:
if name.startswith(pfx):
name = name[len(pfx):]
break
add_enum_value(enumval_el, name, enum_name)
def dump():
for enumerator in enumerators:
print("%s:" % enumerator["name"])
rdata = enumerator["params"][0]
print("\t%s: %s (default=%s)" % (
rdata["name"], rdata["type"], rdata["default"]))
for p in enumerator["params"][1:]:
print("\t%s: %s" % (p["name"], p["type"]))
#dump()
def gen_methods(out):
for e in enumerators:
ename = e["name"]
if ename in recipe:
recipe_data = recipe[ename]
else:
recipe_data = {}
method_name = recipe_data["method_name"] if "method_name" in recipe_data else ename
params = e["params"]
rdata = params[0]
# We *must* name arguments, or typemaps won't be applied properly.
# Thus, we must 'qnotused'-them.
qnotused_decls = ""
if rdata["type"] == "void":
retbody = ""
elif rdata["retexpr"]:
retbody = "%s;" % rdata["retexpr"]
else:
retbody = "return %s;" % rdata["default"]
arg_strs = []
for p in (recipe_data["call_params"] if "call_params" in recipe_data else params[1:]):
if isinstance(p, six.string_types):
assert(p[0] == "@")
synth_info = recipe["synthetic_params"][p]
ptype = synth_info["type"]
pname = p[1:]
else:
pname = p["name"]
ptype = p["type"]
suppress_for_call = False
final_name = pname
defstr = ""
if "params" in recipe_data:
all_pdata = recipe_data["params"]
if pname in all_pdata:
pdata = all_pdata[pname]
if "type" in pdata:
ptype = pdata["type"]
if "suppress_for_call" in pdata:
suppress_for_call = pdata["suppress_for_call"]
if "rename" in pdata:
final_name = pdata["rename"]
if "default" in pdata:
defstr = "=%s" % pdata["default"]
if not suppress_for_call:
arg_strs.append("%s %s%s" % (ptype, final_name, defstr))
qnotused_decls += "qnotused(%s); " % final_name
text = "virtual %s %s(%s) {%s%s}\n" % (
rdata["type"],
method_name,
", ".join(arg_strs),
qnotused_decls,
retbody)
out.write(text)
def gen_notifications(out):
for e in enumerators:
ename = e["name"]
out.write("case %s%s:\n" % (args.qualifier, e["enum_name"]))
out.write("{\n")
params = e["params"]
rdata = params[0]
if ename in recipe:
recipe_data = recipe[ename]
else:
recipe_data = {}
method_name = recipe_data["method_name"] if "method_name" in recipe_data else ename
# first, the arguments to pop from the stack
nosynth_params = []
for p in params[1:]:
pname = p["name"]
pname = recipe_data.get("params", {}).get(pname, {}).get("rename", pname)
ptype = p["type"]
pick_type = ptype
if ptype in ["bool", "char", "uchar", "uint16", "cref_t",
"dref_t", "cm_t", "ui_notification_t", "dbg_notification_t",
"tcc_renderer_type_t", "range_kind_t", "demreq_type_t",
"ctree_maturity_t", "comp_t"]:
cast = ptype
pick_type = "int"
else:
cast = ""
out.write(" %s %s = %s%sva_arg(va, %s)%s;\n" % (
ptype,
pname,
cast,
"(" if cast else "",
pick_type,
")" if cast else ""));
nosynth_params.append(pname)
# then, let's do a second pass, this time over the arguments that need
# to be actually passed to the method (can differ; e.g., dbg.hpp
# notifications' "event" is typically scattered into N args)
argstr = [] # arguments to pass to the call, minus those explicitly suppressed
argstr_all = [] # all arguments
for p in (recipe_data["call_params"] if "call_params" in recipe_data else params[1:]):
if isinstance(p, six.string_types):
assert(p[0] == "@")
synth_info = recipe["synthetic_params"][p]
ptype = synth_info["type"]
synth = synth_info["synth"]
pname = p[1:]
else:
pname = p["name"]
ptype = p["type"]
synth = None
param_convertor = None
suppress_for_call = False
qnotused = False
clinked = None
cast_needed = False
deref = None
if "params" in recipe_data:
all_pdata = recipe_data["params"]
if pname in all_pdata:
pdata = all_pdata[pname]
if "rename" in pdata:
pname = pdata["rename"]
if "convertor" in pdata:
param_convertor = pdata["convertor"]
if "deref" in pdata:
deref = pdata["deref"]
if "suppress_for_call" in pdata:
suppress_for_call = pdata["suppress_for_call"]
if "qnotused" in pdata:
qnotused = pdata["qnotused"]
if "clinked" in pdata:
clinked = pdata["clinked"]
if "cast_needed" in pdata:
cast_needed = pdata["cast_needed"]
pass_expr = pname
if deref:
pass_expr = "%s != NULL ? *(%s) : (%s)" % (
pname,
pname,
deref["ifNULL"])
if clinked:
out.write(" ref_t clinked_%s = create_linked_class_instance(%s, %s, %s);\n" %
(pname, clinked["module_define"], clinked["class_define"], pname))
out.write(" if ( clinked_%s == NULL )\n" % pname)
out.write(" break;\n")
pass_expr = "clinked_%s.o" % pname
elif synth:
pass_expr = synth
elif cast_needed:
pass_expr = "(%s) %s" % (cast_needed, pname)
if not suppress_for_call:
if param_convertor:
argstr.append("%s(%s)" % (param_convertor, pass_expr))
else:
argstr.append("%s" % (pass_expr))
if qnotused:
out.write(" qnotused(%s);\n" % pname)
argstr_all.append(pname)
ret_convertor = None
ret_convertor_pass_args = False
ret_convertor_pass_args_nosynth = False
if "return" in recipe_data:
retdata = recipe_data["return"]
if "convertor" in retdata:
ret_convertor = retdata["convertor"]
if "convertor_pass_args" in retdata:
ret_convertor_pass_args = retdata["convertor_pass_args"]
if "convertor_pass_args_nosynth" in retdata:
ret_convertor_pass_args_nosynth = retdata["convertor_pass_args_nosynth"]
if ret_convertor:
out.write(" %s _tmp = %s(%s);\n" % (
retdata["type"],
ename,
", ".join(argstr)))
cvtargs = ["_tmp"]
if ret_convertor_pass_args:
if ret_convertor_pass_args_nosynth:
cvtargs.extend(nosynth_params)
else:
cvtargs.extend(argstr_all)
out.write(" ret = %s(%s);\n" % (ret_convertor, ", ".join(cvtargs)))
else:
if rdata["type"] == "void":
rstr = ""
else:
rstr = "ret = "
out.write(" %s%s(%s);\n" % (
rstr,
method_name,
", ".join(argstr)))
out.write("}\n")
out.write("break;\n\n")
def gen_methodsinfo_decl(out):
out.write(" static const event_code_to_method_name_t mappings[%d];\n" % len(enumerators))
out.write(" static const size_t mappings_size;\n")
def gen_methodsinfo_def(out):
out.write("""
const hooks_base_t::event_code_to_method_name_t %s::mappings[%d] =
{
""" % (args.hooks_class, len(enumerators)))
for e in enumerators:
ename = e["name"]
recipe_data = recipe.get(ename, {})
method_name = recipe_data.get("method_name", ename)
out.write("{ int(%s%s), \"%s\" },\n" % (args.qualifier, e["enum_name"], method_name))
out.write("""
};
const size_t %s::mappings_size = %d;
""" % (args.hooks_class, len(enumerators)))
def gen_safecall(out, class_name):
out.write("""
// This hook gets called from the kernel. Ensure we hold the GIL.
PYW_GIL_GET;
%s *proxy = (%s *) ud;
ssize_t ret = 0;
try
{
if ( !proxy->has_fixed_method_set() || proxy->has_nondef[int(code)] > 0 )
{
if ( proxy->call_requires_new_execution() )
{
new_execution_t exec;
ret = proxy->dispatch(code, va);
}
else
{
ret = proxy->dispatch(code, va);
}
}
}
catch ( Swig::DirectorException &e )
{
msg("Exception in %%s dispatcher function: %%s\\n", proxy->class_name, e.getMessage());
PYW_GIL_CHECK_LOCKED_SCOPE();
if ( PyErr_Occurred() )
PyErr_Print();
}
return ret;
""" % (class_name, class_name))
with open(args.input) as fin:
with open(args.output, "w") as fout:
for line in fin:
fout.write(line)
import re
m = re.match(r".*%s:([^\s]*).*" % args.marker, line)
if m:
what = m.group(1)
if what == "methods":
gen_methods(fout)
elif what == "notifications":
gen_notifications(fout)
elif what == "methodsinfo_decl":
gen_methodsinfo_decl(fout)
elif what == "methodsinfo_def":
gen_methodsinfo_def(fout)
elif what.startswith("safecall="):
gen_safecall(fout, what[9:])
else:
raise Exception("Unknown marker type: %s" % what)