#!/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 (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 .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())