| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435 |
- #!/usr/bin/env python3
- """gir2def.py - generate GNU Modula-2 FOR "C" .def bindings from GIR XML.
- This is the optional broad-coverage companion to the hand-written bindings
- in src/. It reads a GObject-Introspection .gir file (for example
- /usr/share/gir-1.0/Gtk-4.0.gir) and emits, per class/interface/record,
- a DEFINITION MODULE FOR "C" containing its constructors, methods and
- functions, plus modules for namespace-level functions and enumerations.
- Design goals and limits
- -----------------------
- * Exact C symbol names: the procedure name is the GIR `c:identifier`,
- so the generated .def links straight against the C library.
- * One module per class/interface/record, named <Namespace><Name>
- (GtkButton, ...), mirroring the hand-written layout.
- * All instance/opaque/unknown/pointer types map to ADDRESS; scalars map
- to the closest Modula-2 type. Value structs are therefore treated as
- opaque - fine for pointers, not for by-value records.
- * GError** parameters, closure/destroy user-data and array-length
- parameters are dropped. Variadic functions are skipped.
- * Return values are always emitted as ignorable, `: [ T ]`.
- * Everything is written into the output directory (default gen/); the
- generated files are meant to be reviewed and cherry-picked, not to
- replace src/.
- Usage:
- tools/gir2def.py Gtk-4.0 -o gen
- tools/gir2def.py GLib-2.0 --gir-dir /usr/share/gir-1.0 -o gen
- """
- import argparse
- import os
- import re
- import sys
- import xml.etree.ElementTree as ET
- CORE = "http://www.gtk.org/introspection/core/1.0"
- C = "http://www.gtk.org/introspection/c/1.0"
- GLIB = "http://www.gtk.org/introspection/glib/1.0"
- def q(ns, tag):
- return "{%s}%s" % (ns, tag)
- def kind(el):
- return el.tag.split("}")[-1]
- # scalar GIR type name -> Modula-2 type
- SCALAR = {
- "gboolean": "INTEGER", "gint": "INTEGER", "int": "INTEGER",
- "gshort": "INTEGER", "short": "INTEGER",
- "gint8": "INTEGER", "gint16": "INTEGER", "gint32": "INTEGER",
- "glong": "LONGINT", "gint64": "LONGINT", "gssize": "LONGINT",
- "goffset": "LONGINT", "long": "LONGINT",
- "guint": "CARDINAL", "unsigned int": "CARDINAL", "gushort": "CARDINAL",
- "guint8": "CARDINAL", "guint16": "CARDINAL", "guint32": "CARDINAL",
- "gunichar": "CARDINAL",
- "gulong": "LONGCARD", "guint64": "LONGCARD", "gsize": "LONGCARD",
- "size_t": "LONGCARD",
- "GType": "LONGCARD", "gtype": "LONGCARD",
- "gchar": "CHAR", "guchar": "CHAR", "char": "CHAR",
- "gfloat": "SHORTREAL", "float": "SHORTREAL",
- "gdouble": "REAL", "double": "REAL",
- "gpointer": "ADDRESS", "gconstpointer": "ADDRESS", "va_list": "ADDRESS",
- }
- SCALAR_TYPES = set(SCALAR.values()) - {"ADDRESS"}
- STRING_TYPES = {"utf8", "filename"}
- # Modula-2 reserved words that cannot be used verbatim as a parameter name.
- RESERVED = {
- "and", "array", "begin", "by", "case", "const", "definition", "div",
- "do", "else", "elsif", "end", "exit", "export", "for", "from",
- "if", "implementation", "import", "in", "loop", "mod", "module",
- "not", "of", "or", "pointer", "procedure", "qualified", "record",
- "repeat", "return", "set", "then", "to", "type", "until", "var",
- "while", "with", "as", "class", "generic",
- }
- def ctype_of(t):
- if t is None:
- return ""
- return (t.get(q(C, "type")) or "").replace("const ", "").strip()
- def is_string(t):
- if t is None:
- return False
- name = t.get("name")
- if name not in STRING_TYPES:
- return False
- ct = ctype_of(t)
- return ct in ("char*", "gchar*", "gchar**", "char**") or ct.endswith("char*")
- def has_array(t):
- return t is not None and t.find(q(CORE, "array")) is not None
- class Namespace:
- def __init__(self, root):
- self.root = root
- self.ns = root.find(q(CORE, "namespace"))
- self.name = self.ns.get("name")
- self.enums = set()
- for ch in self.ns:
- if kind(ch) in ("enumeration", "bitfield"):
- self.enums.add(ch.get("name"))
- def scalar_of(self, t):
- """Map a type used as a plain value (no VAR/pointer semantics)."""
- if t is None:
- return None
- name = t.get("name")
- if name in self.enums:
- return "CARDINAL"
- return SCALAR.get(name)
- def map_in(self, t):
- if t is None:
- return "ADDRESS"
- if has_array(t):
- return "ADDRESS"
- if is_string(t):
- return "ARRAY OF CHAR"
- if ctype_of(t).endswith("*"):
- return "ADDRESS"
- name = t.get("name")
- if name in self.enums:
- return "CARDINAL"
- return SCALAR.get(name, "ADDRESS")
- def map_return(self, t):
- if t is None:
- return None
- if has_array(t) or is_string(t):
- return "ADDRESS"
- if ctype_of(t).endswith("*"):
- return "ADDRESS"
- name = t.get("name")
- if name in self.enums:
- return "CARDINAL"
- return SCALAR.get(name, "ADDRESS")
- def map_param(self, t, direction):
- if direction in ("out", "inout"):
- if t is None or has_array(t) or is_string(t):
- return "ADDRESS"
- scalar = self.scalar_of(t)
- if scalar in SCALAR_TYPES:
- return "VAR " + scalar
- return "ADDRESS"
- return self.map_in(t)
- def visible_params(params_el):
- """Return (list of parameter elements) with GIR-internal ones removed."""
- if params_el is None:
- return []
- plist = list(params_el)
- hidden = set()
- for i, p in enumerate(plist):
- if "closure" in p.attrib or "destroy" in p.attrib:
- hidden.add(i)
- arr = p.find(q(CORE, "array"))
- if arr is not None and arr.get("length"):
- hidden.add(int(arr.get("length")))
- out = []
- for i, p in enumerate(plist):
- if i in hidden:
- continue
- t = p.find(q(CORE, "type"))
- if t is not None and t.get("name") == "GLib.Error":
- continue
- out.append(p)
- return out
- def sanitize(name, used):
- if not name:
- name = "arg"
- name = re.sub(r"\W", "_", name)
- if name in RESERVED:
- name += "_"
- base = name
- k = 2
- while name in used:
- name = "%s_%d" % (base, k)
- k += 1
- used.add(name)
- return name
- def emit_callable(ns, el, self_alias):
- """Emit one PROCEDURE line for a constructor/method/function, or None."""
- cid = el.get(q(C, "identifier"))
- if not cid:
- return None
- if el.get("moved-to") is not None:
- return None
- if el.find(q(CORE, "varargs")) is not None:
- return None
- parts = []
- used = set()
- inst = el.find(q(CORE, "instance-parameter"))
- params = []
- if inst is not None:
- params.append(inst)
- params.extend(visible_params(el.find(q(CORE, "parameters"))))
- inst_el = inst
- for p in params:
- t = p.find(q(CORE, "type"))
- direction = p.get("direction", "in")
- tname = t.get("name") if t is not None else None
- if tname == ns.ns.get("name") or (self_alias and tname is not None
- and tname == self_alias[0]):
- mtype = self_alias[1]
- else:
- mtype = ns.map_param(t, direction)
- pname = sanitize(p.get("name"), used)
- if mtype.startswith("VAR "):
- parts.append("VAR %s: %s" % (pname, mtype[4:]))
- else:
- parts.append("%s: %s" % (pname, mtype))
- rv = el.find(q(CORE, "return-value"))
- rtype = None
- if rv is not None:
- t = rv.find(q(CORE, "type"))
- if t is not None and t.get("name") != "none":
- rtype = ns.map_return(t)
- sig = "PROCEDURE %s (%s)" % (cid, "; ".join(parts))
- if rtype:
- sig += " : [ %s ];" % rtype
- else:
- sig += ";"
- return cid, sig
- def emit_type_module(ns, el, outdir):
- name = el.get("name")
- if not name:
- return 0
- modname = ns.name + name
- type_name = el.get(q(GLIB, "type-name")) or modname
- procs = []
- seen = set()
- for child in el:
- if kind(child) in ("constructor", "method", "function"):
- res = emit_callable(ns, child, (name, type_name))
- if res and res[0] not in seen:
- seen.add(res[0])
- procs.append(res)
- if not procs:
- return 0
- lines = []
- lines.append('DEFINITION MODULE FOR "C" %s ;' % modname)
- lines.append("")
- lines.append("(* Generated by tools/gir2def.py from %s %s. *)"
- % (ns.name, ns.ns.get("version")))
- lines.append("")
- lines.append("FROM SYSTEM IMPORT ADDRESS;")
- lines.append("")
- lines.append("EXPORT UNQUALIFIED")
- lines.append(" %s," % type_name)
- for i, (cid, _) in enumerate(procs):
- end = "," if i < len(procs) - 1 else ";"
- lines.append(" %s%s" % (cid, end))
- lines.append("")
- lines.append("TYPE")
- lines.append(" %s = ADDRESS;" % type_name)
- lines.append("")
- for cid, sig in procs:
- lines.append("(* %s *)" % cid)
- lines.append(sig)
- lines.append("")
- lines.append("END %s." % modname)
- lines.append("")
- with open(os.path.join(outdir, modname + ".def"), "w") as f:
- f.write("\n".join(lines))
- return len(procs)
- def emit_functions_module(ns, outdir):
- procs = []
- seen = set()
- for child in ns.ns:
- if kind(child) == "function":
- res = emit_callable(ns, child, None)
- if res and res[0] not in seen:
- seen.add(res[0])
- procs.append(res)
- if not procs:
- return 0
- modname = ns.name + "Functions"
- lines = []
- lines.append('DEFINITION MODULE FOR "C" %s ;' % modname)
- lines.append("")
- lines.append("(* Generated by tools/gir2def.py: namespace-level functions. *)")
- lines.append("")
- lines.append("FROM SYSTEM IMPORT ADDRESS;")
- lines.append("")
- lines.append("EXPORT UNQUALIFIED")
- for i, (cid, _) in enumerate(procs):
- end = "," if i < len(procs) - 1 else ";"
- lines.append(" %s%s" % (cid, end))
- lines.append("")
- for cid, sig in procs:
- lines.append("(* %s *)" % cid)
- lines.append(sig)
- lines.append("")
- lines.append("END %s." % modname)
- lines.append("")
- with open(os.path.join(outdir, modname + ".def"), "w") as f:
- f.write("\n".join(lines))
- return len(procs)
- def emit_enums_module(ns, outdir):
- consts = []
- seen = set()
- for child in ns.ns:
- if kind(child) not in ("enumeration", "bitfield"):
- continue
- value = 0
- for member in child:
- if kind(member) != "member":
- continue
- cid = member.get(q(C, "identifier"))
- if not cid or cid in seen:
- continue
- seen.add(cid)
- v = member.get("value")
- if v is not None:
- value = int(v)
- consts.append((cid, value))
- value += 1
- if not consts:
- return 0
- modname = ns.name + "Enums"
- lines = []
- lines.append('DEFINITION MODULE FOR "C" %s ;' % modname)
- lines.append("")
- lines.append("(* Generated by tools/gir2def.py: enumerations and bitfields. *)")
- lines.append("")
- lines.append("EXPORT UNQUALIFIED")
- for i, (cid, _) in enumerate(consts):
- end = "," if i < len(consts) - 1 else ";"
- lines.append(" %s%s" % (cid, end))
- lines.append("")
- lines.append("CONST")
- for cid, value in consts:
- if value >= 0:
- lines.append(" %s = %d;" % (cid, value))
- else:
- lines.append(" %s = %d;" % (cid, value))
- lines.append("")
- lines.append("END %s." % modname)
- lines.append("")
- with open(os.path.join(outdir, modname + ".def"), "w") as f:
- f.write("\n".join(lines))
- return len(consts)
- def collect_identifiers(ns):
- """All C function identifiers this generator would emit for a namespace."""
- ids = []
- for child in ns.ns:
- k = kind(child)
- if k in ("class", "interface", "record", "boxed"):
- for sub in child:
- if kind(sub) in ("constructor", "method", "function"):
- cid = sub.get(q(C, "identifier"))
- if (cid and sub.get("moved-to") is None
- and sub.find(q(CORE, "varargs")) is None):
- ids.append(cid)
- elif k == "function":
- cid = child.get(q(C, "identifier"))
- if (cid and child.get("moved-to") is None
- and child.find(q(CORE, "varargs")) is None):
- ids.append(cid)
- return ids
- def main():
- ap = argparse.ArgumentParser(description=__doc__,
- formatter_class=argparse.RawDescriptionHelpFormatter)
- ap.add_argument("namespace", help="e.g. Gtk-4.0 or GLib-2.0")
- ap.add_argument("-o", "--output", default="gen", help="output directory")
- ap.add_argument("--gir-dir", default="/usr/share/gir-1.0",
- help="directory containing <namespace>.gir")
- args = ap.parse_args()
- gir_path = os.path.join(args.gir_dir, args.namespace + ".gir")
- if not os.path.exists(gir_path):
- sys.stderr.write("gir2def: not found: %s\n" % gir_path)
- return 2
- os.makedirs(args.output, exist_ok=True)
- root = ET.parse(gir_path).getroot()
- ns = Namespace(root)
- modules = 0
- procs = 0
- for child in ns.ns:
- if kind(child) in ("class", "interface", "record", "boxed"):
- n = emit_type_module(ns, child, args.output)
- if n:
- modules += 1
- procs += n
- procs += emit_functions_module(ns, args.output)
- consts = emit_enums_module(ns, args.output)
- print("gir2def: %s -> %s (%d type modules, %d procedures, %d enum constants)"
- % (args.namespace, args.output, modules, procs, consts))
- return 0
- if __name__ == "__main__":
- sys.exit(main())
|