about summary refs log tree commit diff
diff options
context:
space:
mode:
authorAlexey Yerin <yyp@disroot.org>2023-05-28 14:10:05 +0300
committerAlexey Yerin <yyp@disroot.org>2023-05-28 14:15:22 +0300
commit16e730f540acfe6a0015464c81831cdfacaf107b (patch)
treea2aa04bf788dc16977fc6cda2ed874938c3c13c5
Initial commit
-rw-r--r--.gitignore6
-rw-r--r--COPYING367
-rw-r--r--Makefile55
-rw-r--r--README.md59
-rw-r--r--bindings/glib/types.ha8
-rw-r--r--bindings/gobject/types.ha13
-rw-r--r--bindings/types/libc/types.ha27
-rw-r--r--cmd/demo/main.ha58
-rw-r--r--cmd/demo4/main.ha59
-rw-r--r--cmd/hare-gi/context.ha129
-rw-r--r--cmd/hare-gi/ctype.ha263
-rw-r--r--cmd/hare-gi/emit.ha704
-rw-r--r--cmd/hare-gi/ident.ha144
-rw-r--r--cmd/hare-gi/main.ha148
-rw-r--r--cmd/hare-gi/populate.ha62
-rw-r--r--cmd/xmltree/main.ha104
-rwxr-xr-xgenerate40
-rwxr-xr-xgenerate447
-rw-r--r--gir/parse.ha742
-rw-r--r--gir/types.ha363
20 files changed, 3398 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4d33af7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/xmltree
+/hare-gi
+/demo
+/demo4
+
+_generated.ha
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..c257317
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,367 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in 
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..2b41877
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,55 @@
+.POSIX:
+.SUFFIXES:
+
+BINDINGS_DIR = bindings
+CORE_MODULES = glib gobject gio gmodule atk gdkpixbuf freetype harfbuzz cairo pango xlib types/libc
+MODULES3 = $(CORE_MODULES) gdk gtk
+MODULES4 = $(CORE_MODULES) gdk4 gtk4 pangocairo graphene gsk
+
+DESTDIR =
+PREFIX = /usr/local
+SRCDIR = $(PREFIX)/src
+HARESRCDIR = $(SRCDIR)/hare
+THIRDPARTYDIR = $(HARESRCDIR)/third-party
+
+all:
+	@true
+
+install: install-core
+	@for mod in $(MODULES3); do \
+		printf "installing %s\n" "$$mod"; \
+		mkdir -p "$(DESTDIR)$(THIRDPARTYDIR)/$$mod"; \
+		install -m644 "$(BINDINGS_DIR)/$$mod"/* "$(DESTDIR)$(THIRDPARTYDIR)/$$mod"; \
+	done
+
+install4: install-core
+	@for mod in $(MODULES4); do \
+		printf "installing %s\n" "$$mod"; \
+		mkdir -p "$(DESTDIR)$(THIRDPARTYDIR)/$$mod"; \
+		install -m644 "$(BINDINGS_DIR)/$$mod"/* "$(DESTDIR)$(THIRDPARTYDIR)/$$mod"; \
+	done
+
+uninstall:
+	@for mod in $(MODULES3); do \
+		printf "uninstalling %s\n" "$$mod"; \
+		rm -r "$(DESTDIR)$(THIRDPARTYDIR)/$$mod"; \
+	done
+
+uninstall4:
+	@for mod in $(MODULES4); do \
+		printf "uninstalling %s\n" "$$mod"; \
+		rm -r "$(DESTDIR)$(THIRDPARTYDIR)/$$mod"; \
+	done
+
+demo:
+	env HAREPATH="$(BINDINGS_DIR):$$(hare version -v | grep HAREPATH | cut -f2-)" \
+		hare build $$(pkg-config --libs-only-l --static gtk+-3.0) -lbsd -o demo cmd/demo
+
+demo4:
+	env HAREPATH="$(BINDINGS_DIR):$$(hare version -v | grep HAREPATH | cut -f2-)" \
+		hare build $$(pkg-config --libs-only-l --static gtk4) -lepoxy -lXi -lxkbcommon -lwayland-client -lwayland-egl -lXfixes -lXcursor -lXdamage -lXrandr -lXinerama -lcairo-script-interpreter -lbsd -o demo4 cmd/demo4
+
+clean:
+	rm -f hare-gi xmltree demo demo4
+
+.PHONY: all install install4 uninstall uninstall4 demo demo4 clean
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..559c6cf
--- /dev/null
+++ b/README.md
@@ -0,0 +1,59 @@
+# hare-gi
+GObject Introspection code generator for Hare. In short, it creates bindings for
+GTK family of libraries (GTK+, GDK, Pango, etc).
+
+## Generating and installing bindings
+Dependencies:
+* An up-to-date [Hare] toolchain
+* [hare-fastxml]
+* Development files for the desired GTK version and its dependencies. Make sure
+`/usr/share/gir-1.0` is included.
+
+A few core libraries do not ship GIR XML files by default. They can be obtained
+from gobject-introspection:
+
+```
+git clone https://gitlab.gnome.org/GNOME/gobject-introspection.git
+meson setup build/
+ninja -C build/
+
+# The path where this is downloaded will be used later
+```
+
+### GTK 3
+```
+./generate <path to gobject-introspection>
+make install
+```
+
+### GTK 4
+```
+./generate4 <path to gobject-introspection>
+make install4
+```
+
+## Running examples
+### GTK 3
+```
+make demo
+./demo
+```
+
+### GTK 4
+```
+make demo4
+./demo4
+```
+
+## Contributing
+Please send patches to [~yerinalexey/public-inbox@lists.sr.ht][archives] using
+[git send-email] with prefix set to `hare-gi`:
+
+```shell-session
+$ git config format.subjectPrefix "PATCH hare-gi"
+```
+
+[Hare]: https://harelang.org/installation
+[hare-fastxml]: https://git.sr.ht/~yerinalexey/hare-fastxml
+[archives]: https://lists.sr.ht/~yerinalexey/public-inbox
+[git send-email]: https://git-send-email.io
diff --git a/bindings/glib/types.ha b/bindings/glib/types.ha
new file mode 100644
index 0000000..c973570
--- /dev/null
+++ b/bindings/glib/types.ha
@@ -0,0 +1,8 @@
+// Boolean type used by GLib and related libraries, equivalent to gboolean in C.
+export type boolean = uint;
+
+// True value of [[boolean]].
+export def TRUE: boolean = 1;
+
+// False value of [[boolean]].
+export def FALSE: boolean = 0;
diff --git a/bindings/gobject/types.ha b/bindings/gobject/types.ha
new file mode 100644
index 0000000..193bffd
--- /dev/null
+++ b/bindings/gobject/types.ha
@@ -0,0 +1,13 @@
+use types::c;
+
+export type ValueUnion = union {
+	v_int: int,
+	v_uint: uint,
+	v_long: c::long,
+	v_ulong: c::ulong,
+	v_int64: i64,
+	v_uint64: u64,
+	v_float: f32,
+	v_double: f64,
+	v_pointer: *void,
+};
diff --git a/bindings/types/libc/types.ha b/bindings/types/libc/types.ha
new file mode 100644
index 0000000..bfb0dc0
--- /dev/null
+++ b/bindings/types/libc/types.ha
@@ -0,0 +1,27 @@
+use types::c;
+
+export type time_t = i64;
+
+export type tm = struct {
+	tm_sec: int,
+	tm_min: int,
+	tm_hour: int,
+	tm_mday: int,
+	tm_mon: int,
+	tm_year: int,
+	tm_wday: int,
+	tm_yday: int,
+	tm_isdst: int,
+};
+
+export type FILE = void;
+
+export type passwd = struct {
+	pw_name: *c::char,
+	pw_passwd: *c::char,
+	pw_uid: uint,
+	pw_gid: uint,
+	pw_gecos: *c::char,
+	pw_dir: *c::char,
+	pw_shell: *c::char,
+};
diff --git a/cmd/demo/main.ha b/cmd/demo/main.ha
new file mode 100644
index 0000000..cf807c6
--- /dev/null
+++ b/cmd/demo/main.ha
@@ -0,0 +1,58 @@
+use gio;
+use glib;
+use gobject;
+use gtk;
+use os;
+use rt;
+use types::c;
+
+fn about_clicked(btn: *gtk::Button, data: *void) void = {
+	let dialog = gtk::about_dialog_new();
+	const authors: []*c::char = [
+		c::fromstr("Harriet?"),
+		null: *c::char,
+	];
+	gtk::about_dialog_set_license_type(
+		dialog: *gtk::AboutDialog,
+		gtk::License::MPL_2_0,
+	);
+	gtk::about_dialog_set_authors(
+		dialog: *gtk::AboutDialog,
+		authors: *[*]*c::char: **c::char,
+	);
+	gtk::about_dialog_set_website(
+		dialog: *gtk::AboutDialog,
+		c::fromstr("https://git.sr.ht/~yerinalexey/hare-gi"),
+	);
+
+	gtk::widget_show_all(dialog);
+};
+
+fn activate(app: *gio::Application, data: *void) void = {
+	const app = app: *gtk::Application;
+
+	let button = gtk::button_new_with_label(c::fromstr("About"));
+	gtk::button_connect_clicked(button: *gtk::Button, &about_clicked, null);
+
+	let headerbar = gtk::header_bar_new();
+	gtk::header_bar_set_title(headerbar: *gtk::HeaderBar, c::fromstr("hare-gi demo"));
+	gtk::header_bar_set_show_close_button(headerbar: *gtk::HeaderBar, glib::TRUE);
+	gtk::header_bar_pack_end(headerbar: *gtk::HeaderBar, button);
+
+	let window = gtk::application_window_new(app);
+	gtk::window_set_titlebar(window: *gtk::Window, headerbar);
+	gtk::widget_show_all(window);
+};
+
+export fn main() void = {
+	let app = gtk::application_new(
+		c::fromstr("hare_gi.example"),
+		gio::ApplicationFlags::FLAGS_NONE,
+	): *gio::Application;
+	gio::application_connect_activate(app, &activate, null);
+	const status = gio::application_run(
+		app, rt::argc: int, rt::argv: **c::char,
+	);
+	gobject::object_unref(app);
+	os::exit(status);
+};
diff --git a/cmd/demo4/main.ha b/cmd/demo4/main.ha
new file mode 100644
index 0000000..c46bed2
--- /dev/null
+++ b/cmd/demo4/main.ha
@@ -0,0 +1,59 @@
+use gio;
+use glib;
+use gobject;
+use gtk4;
+use os;
+use rt;
+use types::c;
+
+fn about_clicked(btn: *gtk4::Button, data: *void) void = {
+	let dialog = gtk4::about_dialog_new();
+	const authors: []*c::char = [
+		c::fromstr("Harriet?"),
+		null: *c::char,
+	];
+	gtk4::about_dialog_set_license_type(
+		dialog: *gtk4::AboutDialog,
+		gtk4::License::MPL_2_0,
+	);
+	gtk4::about_dialog_set_authors(
+		dialog: *gtk4::AboutDialog,
+		authors: *[*]*c::char: **c::char,
+	);
+	gtk4::about_dialog_set_website(
+		dialog: *gtk4::AboutDialog,
+		c::fromstr("https://git.sr.ht/~yerinalexey/hare-gi"),
+	);
+
+	gtk4::widget_show(dialog);
+};
+
+fn activate(app: *gio::Application, data: *void) void = {
+	const app = app: *gtk4::Application;
+
+	let button = gtk4::button_new_with_label(c::fromstr("About"));
+	gtk4::button_connect_clicked(button: *gtk4::Button, &about_clicked, null);
+
+	let headerbar = gtk4::header_bar_new();
+	let title = gtk4::label_new(c::fromstr("hare-gi demo"));
+	gtk4::header_bar_set_title_widget(headerbar: *gtk4::HeaderBar, title);
+	gtk4::header_bar_set_show_title_buttons(headerbar: *gtk4::HeaderBar, glib::TRUE);
+	gtk4::header_bar_pack_end(headerbar: *gtk4::HeaderBar, button);
+
+	let window = gtk4::application_window_new(app);
+	gtk4::window_set_titlebar(window: *gtk4::Window, headerbar);
+	gtk4::widget_show(window);
+};
+
+export fn main() void = {
+	let app = gtk4::application_new(
+		c::fromstr("hare_gi.example"),
+		gio::ApplicationFlags::FLAGS_NONE,
+	): *gio::Application;
+	gio::application_connect_activate(app, &activate, null);
+	const status = gio::application_run(
+		app, rt::argc: int, rt::argv: **c::char,
+	);
+	gobject::object_unref(app);
+	os::exit(status);
+};
diff --git a/cmd/hare-gi/context.ha b/cmd/hare-gi/context.ha
new file mode 100644
index 0000000..d4f47fa
--- /dev/null
+++ b/cmd/hare-gi/context.ha
@@ -0,0 +1,129 @@
+use fmt;
+use gir;
+use io;
+use os;
+use strings;
+
+type namespace = struct {
+	gir::namespace,
+	includes: []gir::include,
+
+	// C -> Hare type mappings
+	types: [](str, str),
+	// All imported modules. Populated during emit_empty()
+	imports: []ident,
+	// A list of exported symbols to make sure nothing is emitted twice
+	exports: []str,
+
+	module: ident,
+	output_file: io::file,
+	output: io::handle,
+};
+fn namespace_finish(ns: *namespace) void = {
+	gir::namespace_finish(ns);
+	if (ns.output_file != -1) {
+		io::close(ns.output_file)!;
+	};
+	for (let i = 0z; i < len(ns.includes); i += 1) {
+		gir::include_finish(&ns.includes[i]);
+	};
+	free(ns.includes);
+	for (let i = 0z; i < len(ns.types); i += 1) {
+		// types[i].0 is borrowed from GIR structures
+		free(ns.types[i].1);
+	};
+	free(ns.types);
+	for (let i = 0z; i < len(ns.imports); i += 1) {
+		strings::freeall(ns.imports[i]);
+	};
+	free(ns.exports);
+	free(ns.module);
+};
+
+type context = struct {
+	namespaces: []namespace,
+	current: *namespace,
+	stack: []str,
+	brief: bool,
+
+	// Core modules
+	glib: *namespace,
+	gobject: *namespace,
+};
+fn context_finish(ctx: *context) void = {
+	for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+		namespace_finish(&ctx.namespaces[i]);
+	};
+	free(ctx.namespaces);
+};
+
+fn add_repository(ctx: *context, repo: gir::repository) void = {
+	for (let i = 0z; i < len(repo.namespaces); i += 1) {
+		let new = namespace {
+			output_file = -1: io::file,
+			output = io::empty,
+			...
+		};
+		*(&new: *gir::namespace) = repo.namespaces[i];
+		append(new.includes, repo.includes...);
+		append(ctx.namespaces, new);
+	};
+};
+
+fn get_namespace(ctx: *context, name: str, version: str...) nullable *namespace = {
+	assert(len(version) <= 1);
+	if (len(version) == 1) {
+		for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+			if (ctx.namespaces[i].name == name
+					&& ctx.namespaces[i].version == version[0]) {
+				return &ctx.namespaces[i];
+			};
+		};
+	} else {
+		for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+			if (ctx.namespaces[i].name == name) {
+				return &ctx.namespaces[i];
+			};
+		};
+	};
+	return null;
+};
+
+fn add_import(ns: *namespace, module: ident) void = {
+	for (let i = 0z; i < len(ns.imports); i += 1) {
+		if (ident_eq(ns.imports[i], module)) {
+			return;
+		};
+	};
+	append(ns.imports, strings::dupall(module));
+};
+
+fn lookup_type(ctx: *context, name: str) ((*namespace, str) | void) = {
+	for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+		const ns = &ctx.namespaces[i];
+		match (lookup_type_in_namespace(ns, name)) {
+		case let ident: str =>
+			return (ns, ident);
+		case void =>
+			yield;
+		};
+	};
+};
+
+fn lookup_type_in_namespace(ns: *namespace, name: str) (str | void) = {
+	for (let i = 0z; i < len(ns.types); i += 1) {
+		// NOTE: sometimes the requested type doesn't have a prefix
+		// (like Gtk from GtkWindow), the second check catches that
+		if (ns.types[i].0 == name || ns.types[i].1 == name) {
+			return ns.types[i].1;
+		};
+	};
+};
+
+fn push(ctx: *context, name: str) void = {
+	append(ctx.stack, name);
+};
+fn pop(ctx: *context) void = {
+	assert(len(ctx.stack) > 0);
+	delete(ctx.stack[len(ctx.stack) - 1]);
+};
diff --git a/cmd/hare-gi/ctype.ha b/cmd/hare-gi/ctype.ha
new file mode 100644
index 0000000..f45dc9b
--- /dev/null
+++ b/cmd/hare-gi/ctype.ha
@@ -0,0 +1,263 @@
+// The world's worst C parser
+
+use ascii;
+use strings;
+use strio;
+use fmt;
+
+type ctype = ((cmodule, str) | str | cbuiltin | cpointer);
+type cpointer = *ctype;
+type cbuiltin = enum {
+	VOID,
+	INT,
+	UINT,
+	BOOL,
+	SIZE,
+	RUNE,
+	VALIST,
+	VOID_POINTER,
+
+	CHAR,
+	UCHAR,
+	SHORT,
+	USHORT,
+	LONG,
+	ULONG,
+	SSIZE,
+
+	I8, U8,
+	I16, U16,
+	I32, U32,
+	I64, U64,
+	F32, F64,
+};
+type cmodule = enum {
+	LIBC,
+	GLIB,
+	GOBJECT,
+};
+
+fn ctype_finish(type_: ctype) void = {
+	match (type_) {
+	case let pointer: cpointer =>
+		ctype_finish(*pointer);
+		free(pointer);
+	case let s: str =>
+		free(s);
+	case => yield;
+	};
+};
+
+fn cbuiltin_str(b: cbuiltin) const str = switch (b) {
+case cbuiltin::VOID => yield "void";
+case cbuiltin::INT => yield "int";
+case cbuiltin::UINT => yield "uint";
+case cbuiltin::BOOL => yield "bool";
+case cbuiltin::SIZE => yield "size";
+case cbuiltin::RUNE => yield "rune";
+case cbuiltin::VALIST => yield "valist";
+case cbuiltin::VOID_POINTER => yield "*void";
+
+case cbuiltin::CHAR => yield "c::char";
+case cbuiltin::UCHAR => yield "c::uchar";
+case cbuiltin::SSIZE => yield "c::ssize";
+case cbuiltin::SHORT => yield "c::short";
+case cbuiltin::USHORT => yield "c::ushort";
+case cbuiltin::LONG => yield "c::long";
+case cbuiltin::ULONG => yield "c::ulong";
+
+case cbuiltin::I8 => yield "i8";
+case cbuiltin::U8 => yield "u8";
+case cbuiltin::I16 => yield "i16";
+case cbuiltin::U16 => yield "u16";
+case cbuiltin::I32 => yield "i32";
+case cbuiltin::U32 => yield "u32";
+case cbuiltin::I64 => yield "i64";
+case cbuiltin::U64 => yield "u64";
+case cbuiltin::F32 => yield "f32";
+case cbuiltin::F64 => yield "f64";
+};
+
+fn cbuiltin_unsigned(b: cbuiltin) cbuiltin = switch (b) {
+case cbuiltin::INT => yield cbuiltin::UINT;
+case cbuiltin::CHAR => yield cbuiltin::UCHAR;
+case cbuiltin::SHORT => yield cbuiltin::USHORT;
+case cbuiltin::LONG => yield cbuiltin::ULONG;
+case => abort();
+};
+
+fn cbuiltin_needs_import(b: cbuiltin) bool = switch (b) {
+case cbuiltin::CHAR, cbuiltin::UCHAR, cbuiltin::SHORT, cbuiltin::USHORT,
+cbuiltin::LONG, cbuiltin::ULONG, cbuiltin::SSIZE => yield true;
+case => yield false;
+};
+
+let map: [](str, ctype) = [
+	("void", cbuiltin::VOID),
+	("int", cbuiltin::INT),
+	("char", cbuiltin::CHAR),
+	("short", cbuiltin::SHORT),
+	("long", cbuiltin::LONG),
+	("float", cbuiltin::F32),
+	("double", cbuiltin::F64),
+
+	// C11, <stdbool.h>
+	("_Bool", cbuiltin::BOOL),
+	("bool", cbuiltin::BOOL),
+
+	// <stdarg.h>
+	("va_list", cbuiltin::VALIST),
+
+	// <stdlib.h>
+	("size_t", cbuiltin::SIZE),
+	("ssize_t", cbuiltin::SSIZE),
+
+	// <stdint.h>
+	("int8_t", cbuiltin::I8), ("uint8_t", cbuiltin::U8),
+	("int16_t", cbuiltin::I16), ("uint16_t", cbuiltin::U16),
+	("int32_t", cbuiltin::I32), ("uint32_t", cbuiltin::U32),
+	("int64_t", cbuiltin::I64), ("uint64_t", cbuiltin::U64),
+
+	// <glib/gtypes.h>
+	("gchar", cbuiltin::CHAR),
+	("gshort", cbuiltin::SHORT),
+	("glong", cbuiltin::LONG),
+	("gint", cbuiltin::INT),
+	("gboolean", (cmodule::GLIB, "boolean")),
+	("guchar", cbuiltin::UCHAR),
+	("gushort", cbuiltin::USHORT),
+	("gulong", cbuiltin::ULONG),
+	("guint", cbuiltin::UINT),
+	("gfloat", cbuiltin::F32),
+	("gdouble", cbuiltin::F64),
+	("gpointer", cbuiltin::VOID_POINTER),
+	("gconstpointer", cbuiltin::VOID_POINTER),
+	("grefcount", cbuiltin::UINT),
+	("gatomicrefcount", cbuiltin::UINT),
+
+	("gint8", cbuiltin::I8), ("guint8", cbuiltin::U8),
+	("gint16", cbuiltin::I16), ("guint16", cbuiltin::U16),
+	("gint32", cbuiltin::I32), ("guint32", cbuiltin::U32),
+	("gint64", cbuiltin::I64), ("guint64", cbuiltin::U64),
+	("gsize", cbuiltin::SIZE), ("gssize", cbuiltin::SSIZE),
+	("goffset", cbuiltin::I64),
+
+	// <glib/gunicode.h>
+	("gunichar", cbuiltin::RUNE),
+	("gunichar2", cbuiltin::U16),
+
+	// <gobject/gvalue.h>
+	("_Value__data__union", (cmodule::GOBJECT, "ValueUnion")),
+
+	// <unistd.h>
+	("pid_t", cbuiltin::UINT),
+	("uid_t", cbuiltin::UINT),
+
+	// <time.h>
+	("time_t", (cmodule::LIBC, "time_t")),
+	("tm", (cmodule::LIBC, "tm")),
+
+	// <stdio.h>
+	("FILE", (cmodule::LIBC, "FILE")),
+
+	// <pwd.h>
+	("passwd", (cmodule::LIBC, "passwd")),
+
+	// freetype
+	("int32", cbuiltin::I32),
+
+	// ("utf8", ...) - see resolve()
+];
+
+fn parse_ctype(type_: str) ctype = {
+	let iter = strings::iter(type_);
+	let current: ctype = "";
+
+	let unsigned = false;
+	for (true) match (next_token(&iter)) {
+	case let s: str =>
+		if (s == "const" || s == "volatile") {
+			continue;
+		};
+		if (s == "unsigned") {
+			unsigned = true;
+		} else {
+			current = resolve(s);
+			if (unsigned) {
+				current = cbuiltin_unsigned(current as cbuiltin);
+				unsigned = false;
+			};
+		};
+	case let r: rune =>
+		switch (r) {
+		case '*' =>
+			current = alloc(current): cpointer: ctype;
+		case => abort();
+		};
+	case =>
+		break;
+	};
+	if (unsigned) {
+		return cbuiltin::UINT;
+	};
+	return current;
+};
+
+fn resolve(s: str) ctype = {
+	if (s == "utf8") {
+		return alloc(cbuiltin::CHAR: ctype): cpointer;
+	};
+	for (let i = 0z; i < len(map); i += 1) {
+		if (map[i].0 == s) {
+			return map[i].1;
+		};
+	};
+	return s;
+};
+
+fn next_token(iter: *strings::iterator) (str | rune | void) = {
+	for (true) match (_next_token(iter)) {
+	case let s: str =>
+		return s;
+	case let r: rune =>
+		return r;
+	case =>
+		break;
+	};
+};
+
+fn _next_token(iter: *strings::iterator) (str | rune | void) = {
+	let buffer = strio::dynamic();
+	let first = true;
+	for (true) match (strings::next(iter)) {
+	case let r: rune =>
+		if (ascii::isspace(r)) {
+			if (len(buffer.buf) > 0) {
+				return strio::string(&buffer);
+			};
+			continue;
+		};
+		if (is_ident(r, first)) {
+			strio::appendrune(&buffer, r)!;
+			first = false;
+		} else switch (r) {
+		case '*' =>
+			if (len(buffer.buf) > 0) {
+				strings::prev(iter);
+				return strio::string(&buffer);
+			} else {
+				return r;
+			};
+		case =>
+			fmt::fatalf("Unexpected character in a type: '{}'", r);
+		};
+	case =>
+		break;
+	};
+	if (len(buffer.buf) > 0) {
+		return strio::string(&buffer);
+	};
+};
+
+fn is_ident(r: rune, first: bool) bool
+	= ascii::isalpha(r) || r == '_' || (!first && ascii::isdigit(r));
diff --git a/cmd/hare-gi/emit.ha b/cmd/hare-gi/emit.ha
new file mode 100644
index 0000000..fd37110
--- /dev/null
+++ b/cmd/hare-gi/emit.ha
@@ -0,0 +1,704 @@
+use ascii;
+use bufio;
+use fmt;
+use gir;
+use io;
+use os;
+use strings;
+
+// Second stage of codegen: generate code for every namespace, but discard it.
+// This will fill namespace.imports list, which will be used in the next stage.
+fn emit_empty(ctx: *context) void = {
+	for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+		const ns = &ctx.namespaces[i];
+		ctx.current = ns;
+		ctx.current.output = io::empty;
+		emit_namespace(ctx, ns)!;
+	};
+};
+
+// Third stage: generate the final code for modules that have specified an
+// output file.
+fn emit(ctx: *context) (void | io::error) = {
+	for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+		const ns = &ctx.namespaces[i];
+
+		ctx.current = ns;
+
+		if (ns.output_file == -1) continue;
+
+		static let wbuf: [os::BUFSIZ]u8 = [0...];
+		const stream = bufio::buffered(ns.output_file, [], wbuf);
+		ctx.current.output = &stream;
+
+		delete(ctx.current.exports[..]);
+		emit_namespace(ctx, ns)?;
+	};
+};
+
+fn emit_namespace(ctx: *context, ns: *namespace) (void | io::error) = {
+	defer assert(len(ctx.stack) == 0);
+
+	for (let i = 0z; i < len(ns.imports); i += 1) {
+		fmt::fprint(ctx.current.output, "use ")?;
+		emit_ident(ctx.current.output, ns.imports[i])?;
+		fmt::fprintln(ctx.current.output, ";")?;
+	};
+
+	for (let i = 0z; i < len(ns.aliases); i += 1) {
+		emit_alias(ctx, &ns.aliases[i])?;
+	};
+	for (let i = 0z; i < len(ns.classes); i += 1) {
+		emit_class(ctx, &ns.classes[i])?;
+	};
+	for (let i = 0z; i < len(ns.interfaces); i += 1) {
+		emit_interface(ctx, &ns.interfaces[i])?;
+	};
+	for (let i = 0z; i < len(ns.records); i += 1) {
+		emit_record(ctx, &ns.records[i])?;
+	};
+	for (let i = 0z; i < len(ns.enums); i += 1) {
+		emit_enumeration(ctx, &ns.enums[i])?;
+	};
+	for (let i = 0z; i < len(ns.functions); i += 1) {
+		emit_function(ctx, &ns.functions[i])?;
+	};
+	for (let i = 0z; i < len(ns.unions); i += 1) {
+		emit_union(ctx, &ns.unions[i])?;
+	};
+	for (let i = 0z; i < len(ns.bitfields); i += 1) {
+		emit_bitfield(ctx, &ns.bitfields[i])?;
+	};
+	for (let i = 0z; i < len(ns.callbacks); i += 1) {
+		emit_callback(ctx, &ns.callbacks[i])?;
+	};
+	for (let i = 0z; i < len(ns.constants); i += 1) {
+		emit_constant(ctx, &ns.constants[i])?;
+	};
+};
+
+fn emit_alias(ctx: *context, alias: *gir::alias) (void | io::error) = {
+	if (is_exported(ctx, alias.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, alias.c_type);
+
+	emit_doc(ctx, alias)?;
+	fmt::fprintf(ctx.current.output, "export type {} = ",
+		fix_identifier(alias.name))?;
+	emit_type(ctx, alias.inner)?;
+	fmt::fprintln(ctx.current.output, ";")?;
+};
+
+fn emit_class(ctx: *context, class: *gir::class) (void | io::error) = {
+	if (is_exported(ctx, class.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, class.c_type);
+
+	emit_doc(ctx, class)?;
+	fmt::fprintf(ctx.current.output, "export type {} = ",
+		fix_identifier(class.name))?;
+	if (len(class.entries) == 0) {
+		// opaque
+		fmt::fprintln(ctx.current.output, "*void;")?;
+	} else {
+		fmt::fprintln(ctx.current.output, "struct {")?;
+		for (let i = 0z; i < len(class.entries); i += 1) {
+			emit_entry(ctx, &class.entries[i])?;
+		};
+		fmt::fprintln(ctx.current.output, "};")?;
+	};
+
+	// TODO: add implements into documentation
+
+	push(ctx, class.name);
+	for (let i = 0z; i < len(class.constructors); i += 1) {
+		emit_constructor(ctx, &class.constructors[i])?;
+	};
+	for (let i = 0z; i < len(class.methods); i += 1) {
+		emit_method(ctx, &class.methods[i])?;
+	};
+	for (let i = 0z; i < len(class.functions); i += 1) {
+		emit_function(ctx, &class.functions[i])?;
+	};
+	// Ignore: class.virtual_methods
+	// Ignore: class.properties
+	for (let i = 0z; i < len(class.signals); i += 1) {
+		emit_signal(ctx, &class.signals[i])?;
+	};
+	for (let i = 0z; i < len(class.constants); i += 1) {
+		emit_constant(ctx, &class.constants[i])?;
+	};
+	// Unused: class.callbacks
+	assert(len(class.callbacks) == 0);
+	pop(ctx);
+};
+
+fn emit_interface(ctx: *context, iface: *gir::interface) (void | io::error) = {
+	if (is_exported(ctx, iface.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, iface.c_type);
+
+	emit_doc(ctx, iface)?;
+	fmt::fprintf(ctx.current.output, "export type {} = ",
+		fix_identifier(iface.name))?;
+	if (len(iface.entries) == 0) {
+		// opaque
+		fmt::fprintln(ctx.current.output, "*void;")?;
+	} else {
+		fmt::fprintln(ctx.current.output, "struct {")?;
+		for (let i = 0z; i < len(iface.entries); i += 1) {
+			emit_entry(ctx, &iface.entries[i])?;
+		};
+		fmt::fprintln(ctx.current.output, "};")?;
+	};
+
+	// TODO: add implements and prerequisites into documentation
+
+	push(ctx, iface.name);
+	for (let i = 0z; i < len(iface.constructors); i += 1) {
+		emit_constructor(ctx, &iface.constructors[i])?;
+	};
+	for (let i = 0z; i < len(iface.methods); i += 1) {
+		emit_method(ctx, &iface.methods[i])?;
+	};
+	for (let i = 0z; i < len(iface.functions); i += 1) {
+		emit_function(ctx, &iface.functions[i])?;
+	};
+	// Ignore: iface.virtual_methods
+	// Ignore: iface.properties
+	for (let i = 0z; i < len(iface.signals); i += 1) {
+		emit_signal(ctx, &iface.signals[i])?;
+	};
+	for (let i = 0z; i < len(iface.constants); i += 1) {
+		emit_constant(ctx, &iface.constants[i])?;
+	};
+	// Unused: iface.callbacks
+	assert(len(iface.callbacks) == 0);
+	pop(ctx);
+};
+
+fn emit_union(ctx: *context, union_: *gir::union_) (void | io::error) = {
+	if (is_exported(ctx, union_.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, union_.c_type);
+
+	emit_doc(ctx, union_)?;
+	fmt::fprintf(ctx.current.output, "export type {} = ",
+		fix_identifier(union_.name))?;
+	if (len(union_.entries) == 0) {
+		fmt::fprintln(ctx.current.output, "void;")?; // FIXME
+	} else {
+		fmt::fprintln(ctx.current.output, "union {")?;
+		for (let i = 0z; i < len(union_.entries); i += 1) {
+			emit_entry(ctx, &union_.entries[i])?;
+		};
+		fmt::fprintln(ctx.current.output, "};")?;
+	};
+
+	push(ctx, union_.name);
+	for (let i = 0z; i < len(union_.constructors); i += 1) {
+		emit_constructor(ctx, &union_.constructors[i])?;
+	};
+	for (let i = 0z; i < len(union_.methods); i += 1) {
+		emit_method(ctx, &union_.methods[i])?;
+	};
+	for (let i = 0z; i < len(union_.functions); i += 1) {
+		emit_function(ctx, &union_.functions[i])?;
+	};
+	pop(ctx);
+};
+
+fn emit_record(ctx: *context, record: *gir::record) (void | io::error) = {
+	if (is_exported(ctx, record.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, record.c_type);
+
+	emit_doc(ctx, record)?;
+	fmt::fprintf(ctx.current.output, "export type {} = ",
+		fix_identifier(record.name))?;
+	if (record.opaque || len(record.entries) == 0) {
+		fmt::fprintln(ctx.current.output, "*void;")?;
+	} else {
+		fmt::fprintln(ctx.current.output, "struct {")?;
+		for (let i = 0z; i < len(record.entries); i += 1) {
+			emit_entry(ctx, &record.entries[i])?;
+		};
+		fmt::fprintln(ctx.current.output, "};")?;
+	};
+
+	push(ctx, record.name);
+	for (let i = 0z; i < len(record.constructors); i += 1) {
+		emit_constructor(ctx, &record.constructors[i])?;
+	};
+	for (let i = 0z; i < len(record.methods); i += 1) {
+		emit_method(ctx, &record.methods[i])?;
+	};
+	for (let i = 0z; i < len(record.functions); i += 1) {
+		emit_function(ctx, &record.functions[i])?;
+	};
+	pop(ctx);
+};
+
+fn emit_enumeration(
+	ctx: *context,
+	enumeration: *gir::enumeration,
+) (void | io::error) = {
+	if (is_exported(ctx, enumeration.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, enumeration.c_type);
+
+	emit_doc(ctx, enumeration)?;
+	fmt::fprintfln(ctx.current.output, "export type {} = enum uint {{",
+		fix_identifier(enumeration.name))?;
+	for (let i = 0z; i < len(enumeration.members); i += 1) {
+		emit_member(ctx, &enumeration.members[i])?;
+	};
+	fmt::fprintln(ctx.current.output, "};")?;
+
+	push(ctx, enumeration.name);
+	for (let i = 0z; i < len(enumeration.functions); i += 1) {
+		emit_function(ctx, &enumeration.functions[i])?;
+	};
+	pop(ctx);
+};
+
+fn emit_bitfield(ctx: *context, bitfield: *gir::bitfield) (void | io::error) = {
+	if (is_exported(ctx, bitfield.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, bitfield.c_type);
+
+	emit_doc(ctx, bitfield)?;
+	fmt::fprintfln(ctx.current.output, "export type {} = enum uint {{",
+		fix_identifier(bitfield.name))?;
+	for (let i = 0z; i < len(bitfield.members); i += 1) {
+		emit_member(ctx, &bitfield.members[i])?;
+	};
+	fmt::fprintln(ctx.current.output, "};")?;
+
+	push(ctx, bitfield.name);
+	for (let i = 0z; i < len(bitfield.functions); i += 1) {
+		emit_function(ctx, &bitfield.functions[i])?;
+	};
+	pop(ctx);
+};
+
+fn emit_member(ctx: *context, member: *gir::member) (void | io::error) = {
+	emit_indented_doc(ctx, member)?;
+	fmt::fprintfln(ctx.current.output, "\t" "{} = {},",
+		to_uppercase(fix_identifier(member.name)), member.value)?;
+};
+
+fn emit_callback(ctx: *context, callback: *gir::callback) (void | io::error) = {
+	if (is_exported(ctx, callback.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, callback.c_type);
+
+	emit_doc(ctx, callback)?;
+	fmt::fprintf(ctx.current.output, "export type {} = *fn(",
+		fix_identifier(callback.name))?;
+	emit_params(ctx, callback.params)?;
+	if (callback.throws) {
+		if (len(callback.params) > 0) {
+			fmt::fprint(ctx.current.output, ", ")?;
+		};
+		fmt::fprint(ctx.current.output, "error: nullable **")?;
+		emit_object(ctx, ctx.glib, "Error")?;
+	};
+	fmt::fprint(ctx.current.output, ") ")?;
+	emit_return_value(ctx, callback.return_value)?;
+	fmt::fprintln(ctx.current.output, ";")?;
+};
+
+fn emit_constant(ctx: *context, constant: *gir::constant) (void | io::error) = {
+	// TODO: constants
+	return;
+};
+
+fn emit_constructor(
+	ctx: *context,
+	constructor: *gir::constructor,
+) (void | io::error) = {
+	if (is_exported(ctx, constructor.c_identifier)) {
+		return;
+	};
+	append(ctx.current.exports, constructor.c_identifier);
+
+	emit_doc(ctx, constructor)?;
+	fmt::fprintf(ctx.current.output, `export @symbol("{}") fn `,
+		constructor.c_identifier)?;
+	emit_function_name(ctx, constructor.name)?;
+	fmt::fprint(ctx.current.output, "(")?;
+	emit_params(ctx, constructor.params)?;
+	if (constructor.throws) {
+		if (len(constructor.params) > 0) {
+			fmt::fprint(ctx.current.output, ", ")?;
+		};
+		fmt::fprint(ctx.current.output, "error: nullable **")?;
+		emit_object(ctx, ctx.glib, "Error")?;
+	};
+	fmt::fprint(ctx.current.output, ") ")?;
+	emit_return_value(ctx, constructor.return_value)?;
+	fmt::fprintln(ctx.current.output, ";")?;
+};
+
+fn emit_method(ctx: *context, method: *gir::method) (void | io::error) = {
+	if (is_exported(ctx, method.c_identifier)) {
+		return;
+	};
+	append(ctx.current.exports, method.c_identifier);
+
+	emit_doc(ctx, method)?;
+	fmt::fprintf(ctx.current.output, `export @symbol("{}") fn `,
+		method.c_identifier)?;
+	emit_function_name(ctx, method.name)?;
+	fmt::fprint(ctx.current.output, "(")?;
+	emit_instance_param(ctx, method.instance)?;
+	if (len(method.params) > 0) {
+		fmt::fprint(ctx.current.output, ", ")?;
+	};
+	emit_params(ctx, method.params)?;
+	if (method.throws) {
+		fmt::fprint(ctx.current.output, ", error: nullable **")?;
+		emit_object(ctx, ctx.glib, "Error")?;
+	};
+	fmt::fprint(ctx.current.output, ") ")?;
+	emit_return_value(ctx, method.return_value)?;
+	fmt::fprintln(ctx.current.output, ";")?;
+};
+
+fn emit_function(ctx: *context, function: *gir::function) (void | io::error) = {
+	if (is_exported(ctx, function.c_identifier)) {
+		return;
+	};
+	append(ctx.current.exports, function.c_identifier);
+
+	emit_doc(ctx, function)?;
+	fmt::fprintf(ctx.current.output, `export @symbol("{}") fn `,
+		function.c_identifier)?;
+	emit_function_name(ctx, function.name)?;
+	fmt::fprint(ctx.current.output, "(")?;
+	emit_params(ctx, function.params)?;
+	if (function.throws) {
+		if (len(function.params) > 0) {
+			fmt::fprint(ctx.current.output, ", ")?;
+		};
+		fmt::fprint(ctx.current.output, "error: nullable **")?;
+		emit_object(ctx, ctx.glib, "Error")?;
+	};
+	fmt::fprint(ctx.current.output, ") ")?;
+	emit_return_value(ctx, function.return_value)?;
+	fmt::fprintln(ctx.current.output, ";")?;
+};
+
+fn emit_signal(ctx: *context, signal: *gir::signal) (void | io::error) = {
+	assert(len(ctx.stack) >= 1);
+	// Example code for GtkWindow::activate:
+	//
+	// export fn window_connect_activate(
+	// 	instance: *gtk::Window,
+	// 	handler: *fn(instance: *gtk::Window, data: *void) void,
+	// 	data: nullable *void,
+	// ) u64 = gobject::signal_connect_data(
+	// 	instance,
+	// 	*(&"activate\0": []u8): *[*]u8: *c::char,
+	// 	handler: gobject::Callback,
+	// 	data: *void,
+	// 	null: gobject::ClosureNotify, 0,
+	// );
+
+	// TODO: add documentation
+
+	const function = strings::concat(
+		"connect_",
+		normalize_signal(signal.name),
+	);
+	defer free(function);
+
+	fmt::fprintf(ctx.current.output, "export fn ")?;
+	emit_function_name(ctx, function)?;
+	fmt::fprintln(ctx.current.output, "(")?;
+	fmt::fprintfln(ctx.current.output, "\t" "instance: *{},", ctx.stack[0])?;
+	fmt::fprintf(ctx.current.output,
+		"\t" "handler: *fn(instance: *{}",
+		ctx.stack[0],
+	)?;
+	if (len(signal.params) > 0) {
+		fmt::fprint(ctx.current.output, ", ")?;
+		emit_params(ctx, signal.params)?;
+	};
+	fmt::fprint(ctx.current.output, ", data: *void) ")?;
+	emit_return_value(ctx, signal.return_value)?;
+	fmt::fprintln(ctx.current.output, ",")?;
+	fmt::fprintln(ctx.current.output, "\t" "data: nullable *void,")?;
+	fmt::fprint(ctx.current.output, ") u64 = ")?;
+	emit_object(ctx, ctx.gobject, "signal_connect_data")?;
+	fmt::fprintln(ctx.current.output, "(")?;
+	fmt::fprintln(ctx.current.output,  "\t" "instance,")?;
+	fmt::fprintfln(ctx.current.output,
+		"\t" `*(&"{}\0": *[]u8): *[*]u8: *c::char,`,
+		signal.name,
+	)?;
+	fmt::fprint(ctx.current.output,    "\t" "handler: ")?;
+	emit_object(ctx, ctx.gobject, "Callback")?;
+	fmt::fprintln(ctx.current.output, ",")?;
+	fmt::fprintln(ctx.current.output,  "\t" "data: *void,")?;
+	fmt::fprint(ctx.current.output,    "\t" "null: ")?;
+	emit_object(ctx, ctx.gobject, "ClosureNotify")?;
+	fmt::fprintln(ctx.current.output, ", 0,")?;
+	fmt::fprintln(ctx.current.output, ");")?;
+};
+
+fn emit_instance_param(
+	ctx: *context,
+	instance: gir::instance_parameter,
+) (void | io::error) = {
+	fmt::fprintf(ctx.current.output, "{}: ", fix_identifier(instance.name))?;
+	emit_type(ctx, instance.type_)?;
+};
+
+fn emit_params(ctx: *context, params: []gir::callable_param) (void | io::error) = {
+	for (let i = 0z; i < len(params); i += 1) {
+		const param = params[i];
+		if (i > 0) {
+			fmt::fprint(ctx.current.output, ", ")?;
+		};
+		match (param.parameter) {
+		case let t: gir::any_type =>
+			fmt::fprintf(ctx.current.output, "{}: ",
+				fix_identifier(param.name))?;
+			emit_type(ctx, t)?;
+		case gir::varargs =>
+			fmt::fprint(ctx.current.output, "...")?;
+			assert(i == len(params) - 1);
+		};
+	};
+};
+
+fn emit_return_value(
+	ctx: *context,
+	ret: (gir::callable_return | void),
+) (void | io::error) = {
+	match (ret) {
+	case let ret: gir::callable_return =>
+		emit_type(ctx, ret.type_)?;
+	case =>
+		fmt::fprint(ctx.current.output, "void")?;
+	};
+};
+
+fn emit_function_name(ctx: *context, name: str) (void | io::error) = {
+	if (len(ctx.stack) == 0) {
+		fmt::fprint(ctx.current.output, fix_identifier(name))?;
+		return;
+	};
+
+	const path = strings::concat(ctx.stack...);
+	defer free(path);
+	const lower = swap_case(path);
+	defer free(lower);
+
+	fmt::fprintf(ctx.current.output, "{}_{}", lower, name)?;
+};
+
+fn emit_entry(ctx: *context, entry: *gir::entry) (void | io::error) = {
+	match (*entry) {
+	case let f: gir::field =>
+		emit_field(ctx, &f)?;
+	case let u: gir::union_ =>
+		emit_inline_union(ctx, &u)?;
+	case let r: gir::record =>
+		emit_inline_record(ctx, &r)?;
+	};
+};
+
+fn emit_field(ctx: *context, field: *gir::field) (void | io::error) = {
+	emit_indented_doc(ctx, field)?;
+	fmt::fprintf(ctx.current.output, "\t" "{}: ",
+		fix_identifier(field.name))?;
+	if (!field.writable) {
+		fmt::fprint(ctx.current.output, "const ")?;
+	};
+	emit_type(ctx, field.type_)?;
+	fmt::fprintln(ctx.current.output, ",")?;
+};
+
+fn emit_inline_union(ctx: *context, union_: *gir::union_) (void | io::error) = {
+	emit_indented_doc(ctx, union_)?;
+	fmt::fprint(ctx.current.output, "\t")?;
+	if (len(union_.name) > 0) {
+		fmt::fprintf(ctx.current.output, "{}: ",
+			fix_identifier(union_.name))?;
+	};
+	fmt::fprintln(ctx.current.output, "union {")?;
+	for (let i = 0z; i < len(union_.entries); i += 1) {
+		emit_entry(ctx, &union_.entries[i])?;
+	};
+	fmt::fprintln(ctx.current.output, "},")?;
+};
+
+fn emit_inline_record(ctx: *context, record: *gir::record) (void | io::error) = {
+	emit_indented_doc(ctx, record)?;
+	fmt::fprint(ctx.current.output, "\t")?;
+	if (len(record.name) > 0) {
+		fmt::fprintf(ctx.current.output, "{}: ",
+			fix_identifier(record.name))?;
+	};
+	fmt::fprintln(ctx.current.output, "struct {")?;
+	for (let i = 0z; i < len(record.entries); i += 1) {
+		emit_entry(ctx, &record.entries[i])?;
+	};
+	fmt::fprintln(ctx.current.output, "},")?;
+};
+
+fn emit_indented_doc(ctx: *context, item: *gir::documentation) (void | io::error) = {
+	if (ctx.brief) {
+		return;
+	};
+	const iter = strings::tokenize(item.doc, "\n");
+	for (true) match (strings::next_token(&iter)) {
+	case let line: str =>
+		fmt::fprintln(ctx.current.output, "\t" "//", line)?;
+	case =>
+		break;
+	};
+};
+
+fn emit_doc(ctx: *context, item: *gir::documentation) (void | io::error) = {
+	if (ctx.brief) {
+		return;
+	};
+	const iter = strings::tokenize(item.doc, "\n");
+	for (true) match (strings::next_token(&iter)) {
+	case let line: str =>
+		fmt::fprintln(ctx.current.output, "//", line)?;
+	case =>
+		break;
+	};
+};
+
+fn emit_type(ctx: *context, t: (gir::any_type | gir::callback)) (void | io::error) = {
+	match (t) {
+	case let t: gir::simple_type =>
+		if (len(t.c_type) > 0) {
+			const parsed = parse_ctype(t.c_type);
+			defer ctype_finish(parsed);
+			return emit_c_type(ctx, parsed);
+		} else {
+			const (first, second) = strings::cut(t.name, ".");
+			if (len(second) == 0) {
+				const parsed = parse_ctype(t.name);
+				defer ctype_finish(parsed);
+				return emit_c_type(ctx, parsed);
+			} else {
+				const ns = match (get_namespace(ctx, first)) {
+				case let ns: *namespace =>
+					yield ns;
+				case null =>
+					fmt::fprintf(ctx.current.output,
+						"#unresolved type {}#", t.name)?;
+					return;
+				};
+				match (lookup_type_in_namespace(ns, second)) {
+				case let id: str =>
+					emit_object(ctx, ns, id)?;
+				case =>
+					fmt::fprintf(ctx.current.output,
+						"#unresolved type {}#", t.name)?;
+				};
+			};
+		};
+	case let t: gir::array_type =>
+		if (t.fixed_size == 0) {
+			if (len(t.c_type) > 0) {
+				const parsed = parse_ctype(t.c_type);
+				defer ctype_finish(parsed);
+				return emit_c_type(ctx, parsed);
+			} else {
+				fmt::fprint(ctx.current.output, "*")?;
+				return emit_type(ctx, *t.inner)?;
+			};
+		} else {
+			fmt::fprintf(ctx.current.output, "[{}]", t.fixed_size)?;
+			return emit_type(ctx, *t.inner)?;
+		};
+	case let t: gir::callback =>
+		return emit_callback_type(ctx, &t);
+	};
+};
+
+fn emit_callback_type(ctx: *context, cb: *gir::callback) (void | io::error) = {
+	fmt::fprint(ctx.current.output, "*fn(")?;
+	emit_params(ctx, cb.params)?;
+	if (cb.throws) {
+		if (len(cb.params) > 0) {
+			fmt::fprint(ctx.current.output, ", ")?;
+		};
+		fmt::fprint(ctx.current.output, "error: nullable **")?;
+		emit_object(ctx, ctx.glib, "Error")?;
+	};
+	fmt::fprint(ctx.current.output, ") ")?;
+	emit_return_value(ctx, cb.return_value)?;
+};
+
+fn emit_c_type(ctx: *context, type_: ctype) (void | io::error) = {
+	match (type_) {
+	case let special: (cmodule, str) =>
+		const (mod, id) = special;
+		switch (mod) {
+		case cmodule::LIBC =>
+			add_import(ctx.current, ["types", "libc"]);
+			fmt::fprintf(ctx.current.output, "libc::{}", id)?;
+		case cmodule::GLIB =>
+			emit_object(ctx, ctx.glib, id)?;
+		case cmodule::GOBJECT =>
+			emit_object(ctx, ctx.gobject, id)?;
+		};
+	case let id: str =>
+		match (lookup_type(ctx, id)) {
+		case let pair: (*namespace, str) =>
+			emit_object(ctx, pair.0, pair.1)?;
+		case =>
+			fmt::fprintf(ctx.current.output, "#unresolved type {}#",
+				id)?;
+		};
+	case let b: cbuiltin =>
+		if (cbuiltin_needs_import(b)) {
+			add_import(ctx.current, ["types", "c"]);
+		};
+		fmt::fprint(ctx.current.output, cbuiltin_str(b))?;
+	case let p: cpointer =>
+		fmt::fprint(ctx.current.output, '*')?;
+		return emit_c_type(ctx, *p);
+	};
+};
+
+fn emit_object(
+	ctx: *context,
+	source: *namespace,
+	object: str,
+) (void | io::error) = {
+	if (ident_eq(source.module, ctx.current.module)) {
+		fmt::fprint(ctx.current.output, object)?;
+	} else {
+		add_import(ctx.current, source.module);
+		fmt::fprintf(ctx.current.output, "{}::{}",
+			source.module[len(source.module) - 1], object)?;
+	};
+};
+
+fn is_exported(ctx: *context, symbol: str) bool = {
+	for (let i = 0z; i < len(ctx.current.exports); i += 1) {
+		if (ctx.current.exports[i] == symbol) {
+			return true;
+		};
+	};
+	return false;
+};
diff --git a/cmd/hare-gi/ident.ha b/cmd/hare-gi/ident.ha
new file mode 100644
index 0000000..962a6b3
--- /dev/null
+++ b/cmd/hare-gi/ident.ha
@@ -0,0 +1,144 @@
+use ascii;
+use fmt;
+use io;
+use strings;
+use strio;
+
+const keywords: [_]str = [
+	"abort", "align", "alloc", "append", "as", "assert", "bool", "break",
+	"case", "const", "continue", "defer", "def", "delete", "else", "enum",
+	"export", "f32", "f64", "false", "fn", "for", "free", "i16", "i32",
+	"i64", "i8", "if", "insert", "int", "is", "len", "let", "match", "null",
+	"nullable", "offset", "return", "rune", "size", "static", "str",
+	"struct", "switch", "true", "type", "u16", "u32", "u64", "u8", "uint",
+	"uintptr", "union", "use", "vaarg", "vaend", "valist", "vastart",
+	"void", "yield",
+];
+
+// Makes an identifier suitable for use in Hare code.
+// If the identifier doesn't start with a letter or '_', an '_' is prepended.
+// If the identifier matches one of Hare's keywords, an '_' is appended.
+fn fix_identifier(s: str) str = {
+	static let buf: [128]u8 = [0...];
+
+	let iter = strings::iter(s);
+	const first = strings::next(&iter) as rune;
+	if (!ascii::isalpha(first) && first != '_') {
+		return fmt::bsprintf(buf, "_{}", s);
+	};
+
+	for (let i = 0z; i < len(keywords); i += 1) {
+		if (keywords[i] == s) {
+			return fmt::bsprintf(buf, "{}_", s);
+		};
+	};
+	return s;
+};
+
+// Normalizes a signal name into a form suitable as an identifier, replacing
+// '-' with '_'.
+fn normalize_signal(in: str) str = {
+	static let buf: [128]u8 = [0...];
+	let buf = strio::fixed(buf);
+
+	let iter = strings::iter(in);
+	for (true) match (strings::next(&iter)) {
+	case let r: rune =>
+		strio::appendrune(&buf, if (r == '-') '_' else r)!;
+	case =>
+		break;
+	};
+	return strio::string(&buf);
+};
+
+// Converts lower_case into UPPER_CASE
+fn to_uppercase(in: str) str = {
+	static let buf: [128]u8 = [0...];
+	let buf = strio::fixed(buf);
+
+	let iter = strings::iter(in);
+	for (true) match (strings::next(&iter)) {
+	case let r: rune =>
+		strio::appendrune(&buf, ascii::toupper(r))!;
+	case =>
+		break;
+	};
+	return strio::string(&buf);
+};
+
+// Converts PascalCase into snake_case
+fn swap_case(in: str) str = {
+	let parts: []str = [];
+	defer strings::freeall(parts);
+
+	let buf = strio::dynamic();
+	defer io::close(&buf)!;
+
+	let iter = strings::iter(in);
+	let prev = '!';
+	for (true) match (strings::next(&iter)) {
+	case let r: rune =>
+		if (ascii::isupper(r) && !ascii::isupper(prev)) {
+			if (len(strio::string(&buf)) > 0) {
+				append(parts,
+					strings::dup(strio::string(&buf)));
+				strio::reset(&buf);
+			};
+		};
+		strio::appendrune(&buf, ascii::tolower(r))!;
+		prev = r;
+	case void =>
+		break;
+	};
+
+	if (len(strio::string(&buf)) > 0) {
+		append(parts, strings::dup(strio::string(&buf)));
+	};
+
+	return strings::join("_", parts...);
+};
+
+@test fn swap_case() void = {
+	const tests = [
+		("Gtk", "gtk"),
+		("GtkActionBar", "gtk_action_bar"),
+		("GtkWidget", "gtk_widget"),
+		("DBus", "dbus"),
+		("cairo_t", "cairo_t"),
+	];
+	for (let i = 0z; i < len(tests); i += 1) {
+		const (in, expected) = tests[i];
+		const out = swap_case(in);
+		if (out != expected) {
+			fmt::fatalf("Fail '{}': expected '{}', got '{}'",
+				in, expected, out);
+		};
+		free(out);
+	};
+};
+
+// A fully quantified identifier.
+type ident = []str;
+
+// Returns whether two fully quantified identifiers match.
+fn ident_eq(a: ident, b: ident) bool = {
+	if (len(a) != len(b)) {
+		return false;
+	};
+	for (let i = 0z; i < len(a); i += 1) {
+		if (a[i] != b[i]) {
+			return false;
+		};
+	};
+	return true;
+};
+
+// Writes a fully quantified identifier to an [[io::handle]].
+fn emit_ident(out: io::handle, id: ident) (void | io::error) = {
+	for (let i = 0z; i < len(id); i += 1) {
+		if (i > 0) {
+			fmt::fprint(out, "::")?;
+		};
+		fmt::fprint(out, id[i])?;
+	};
+};
diff --git a/cmd/hare-gi/main.ha b/cmd/hare-gi/main.ha
new file mode 100644
index 0000000..6aa1213
--- /dev/null
+++ b/cmd/hare-gi/main.ha
@@ -0,0 +1,148 @@
+use fmt;
+use fs;
+use getopt;
+use gir;
+use io;
+use os;
+use strings;
+
+export fn main() void = {
+	const help: [_]getopt::help = [
+		"generate Hare bindings from GObject-Introspection files",
+		('c', "verify the files instead of generating code"),
+		('B', "disable documentation comments"),
+		('m', "namespace,<module>,<output", "register a module binding namespace <namespace> to Hare module <module>. If provided, also write the code to <output>"),
+		"files...",
+	];
+	const cmd = getopt::parse(os::args, help...);
+	defer getopt::finish(&cmd);
+
+	let ctx = context {
+		glib = null: *namespace,
+		gobject = null: *namespace,
+		...
+	};
+	defer context_finish(&ctx);
+
+	let check = false;
+	for (let i = 0z; i < len(cmd.opts); i += 1) {
+		const (opt, _) = cmd.opts[i];
+		switch (opt) {
+		case 'c' =>
+			check = true;
+		case 'B' =>
+			ctx.brief = true;
+		case 'm' =>
+			yield; // parsed later
+		case => abort();
+		};
+	};
+
+	for (let i = 0z; i < len(cmd.args); i += 1) {
+		const f = match (os::open(cmd.args[i])) {
+		case let f: io::file =>
+			yield f;
+		case let err: fs::error =>
+			fmt::fatalf("Failed to open {}: {}",
+				cmd.args[i], fs::strerror(err));
+		};
+		defer io::close(f)!;
+
+		match (gir::parse(f)) {
+		case let r: gir::repository =>
+			add_repository(&ctx, r);
+		case let err: gir::error =>
+			fmt::fatalf("Failed to parse {}: {}",
+				cmd.args[i], gir::strerror(err));
+		};
+	};
+
+	for (let i = 0z; i < len(cmd.opts); i += 1) {
+		const (opt, arg) = cmd.opts[i];
+		switch (opt) {
+		case 'm' =>
+			const (ns, rest) = strings::cut(arg, ",");
+			const (module, output) = strings::cut(rest, ",");
+
+			if (len(ns) == 0) {
+				fmt::fatalf("Invalid -o {}: empty namespace",
+					arg);
+			};
+			if (len(module) == 0) {
+				fmt::fatalf("Invalid -o {}: empty module", arg);
+			};
+
+			const ns = match (get_namespace(&ctx, ns)) {
+			case let ns: *namespace =>
+				yield ns;
+			case null =>
+				fmt::fatalf("No such namespace '{}'", ns);
+			};
+			ns.module = strings::split(module, "::");
+			if (len(output) > 0) {
+				match (os::create(output, fs::mode::USER_RW)) {
+				case let f: io::file =>
+					ns.output_file = f;
+				case let err: fs::error =>
+					fmt::fatalf("Failed to open {}: {}",
+						output, fs::strerror(err));
+				};
+			};
+		case =>
+			yield;
+		};
+	};
+
+	let errors = 0z;
+	for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+		const ns = &ctx.namespaces[i];
+		if (len(ns.module) == 0) {
+			fmt::errorln("A module was not provided for namespace",
+				ns.name)!;
+			errors += 1;
+		};
+		for (let j = 0z; j < len(ns.includes); j += 1) {
+			const incl = &ns.includes[j];
+			match (get_namespace(&ctx, incl.name, incl.version)) {
+			case *namespace => yield;
+			case null =>
+				fmt::errorfln(
+					"Missing namespace {} (version {}) "
+					"required by {}",
+					incl.name, incl.version, ns.name)!;
+				errors += 1;
+			};
+		};
+	};
+	if (errors > 0) {
+		os::exit(1);
+	};
+
+	if (check) {
+		return;
+	};
+
+	match (get_namespace(&ctx, "GLib")) {
+	case let ns: *namespace =>
+		ctx.glib = ns;
+	case null =>
+		fmt::fatal("GLib namespace is required but was not provided");
+	};
+
+	match (get_namespace(&ctx, "GObject")) {
+	case let ns: *namespace =>
+		ctx.gobject = ns;
+	case null =>
+		fmt::fatal("GObject namespace is required but was not provided");
+	};
+
+	populate(&ctx);
+
+	emit_empty(&ctx);
+
+	match (emit(&ctx)) {
+	case void => yield;
+	case let err: io::error =>
+		fmt::fatal("Error:", io::strerror(err));
+	};
+};
diff --git a/cmd/hare-gi/populate.ha b/cmd/hare-gi/populate.ha
new file mode 100644
index 0000000..31e663b
--- /dev/null
+++ b/cmd/hare-gi/populate.ha
@@ -0,0 +1,62 @@
+use gir;
+use strings;
+
+// First stage of codegen: enumerates all types and builds an identifier table,
+// which can be then used to get a (namespace, str) pair for a given C
+// identifier. See [[lookup_type]].
+fn populate(ctx: *context) void = {
+	for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+		populate_namespace(ctx, &ctx.namespaces[i]);
+	};
+};
+
+fn populate_namespace(ctx: *context, ns: *namespace) void = {
+	ctx.current = ns;
+
+	for (let i = 0z; i < len(ns.aliases); i += 1) {
+		const alias = &ns.aliases[i];
+		populate_type(ctx, alias.c_type, alias.name);
+	};
+	for (let i = 0z; i < len(ns.classes); i += 1) {
+		const class = &ns.classes[i];
+		if (len(class.c_type) > 0) {
+			populate_type(ctx, class.c_type, class.name);
+		} else {
+			populate_type(ctx, class.glib_type_name, class.name);
+		};
+	};
+	for (let i = 0z; i < len(ns.interfaces); i += 1) {
+		const iface = &ns.interfaces[i];
+		populate_type(ctx, iface.c_type, iface.name);
+	};
+	for (let i = 0z; i < len(ns.records); i += 1) {
+		const record = &ns.records[i];
+		populate_type(ctx, record.c_type, record.name);
+	};
+	for (let i = 0z; i < len(ns.enums); i += 1) {
+		const enumeration = &ns.enums[i];
+		populate_type(ctx, enumeration.c_type, enumeration.name);
+	};
+	for (let i = 0z; i < len(ns.unions); i += 1) {
+		const union_ = &ns.unions[i];
+		populate_type(ctx, union_.c_type, union_.name);
+	};
+	for (let i = 0z; i < len(ns.bitfields); i += 1) {
+		const bitfield = &ns.bitfields[i];
+		populate_type(ctx, bitfield.c_type, bitfield.name);
+	};
+	for (let i = 0z; i < len(ns.callbacks); i += 1) {
+		const callback = &ns.callbacks[i];
+		if (len(callback.c_type) > 0) {
+			populate_type(ctx, callback.c_type, callback.name);
+		};
+	};
+};
+
+fn populate_type(ctx: *context, c: str, name: str) void = {
+	if (len(c) == 0) {
+		return;
+	};
+	const hare = strings::dup(fix_identifier(name));
+	append(ctx.current.types, (c, hare));
+};
diff --git a/cmd/xmltree/main.ha b/cmd/xmltree/main.ha
new file mode 100644
index 0000000..5f15eaa
--- /dev/null
+++ b/cmd/xmltree/main.ha
@@ -0,0 +1,104 @@
+use xml = format::fastxml;
+use sort;
+use os;
+use fmt;
+use strings;
+
+type node = struct {
+	name: str,
+	attributes: []str,
+	has_text: bool,
+
+	children: []*node,
+};
+
+fn add_attr(n: *node, attr: xml::attribute) void = {
+	for (let i = 0z; i < len(n.attributes); i += 1) {
+		if (n.attributes[i] == attr.0) return;
+	};
+	append(n.attributes, strings::dup(attr.0));
+};
+
+fn add_text(n: *node, text: str) void = {
+	const clean = strings::trim(text);
+	n.has_text ||= len(clean) > 0;
+};
+
+fn get_node(n: *node, name: str) *node = {
+	for (let i = 0z; i < len(n.children); i += 1) {
+		if (n.children[i].name == name)
+			return n.children[i];
+	};
+	let new = alloc(node { name = strings::dup(name), ... });
+	append(n.children, new);
+	return new;
+};
+
+fn print(n: *node, indent: size) void = {
+	for (let i = 0z; i < indent; i += 1) fmt::print("> ")!;
+
+	fmt::print(n.name)!;
+	if (n.has_text) fmt::print("+")!;
+	fmt::print(" [")!;
+	for (let i = 0z; i < len(n.attributes); i += 1) {
+		fmt::printf("{}{}", if (i == 0) "" else " ", n.attributes[i])!;
+	};
+	fmt::println("]")!;
+
+	for (let i = 0z; i < len(n.children); i += 1) {
+		print(n.children[i], indent + 1);
+	};
+};
+
+export fn main() void = {
+	if (len(os::args) - 1 < 1) {
+		fmt::fatal("Usage: xmltree <file.xml>");
+	};
+
+	const file = os::open(os::args[1])!;
+	const parser = xml::parse(file)!;
+	defer xml::parser_free(parser);
+
+	let tree = null: *node;
+	defer free(tree); // memory leaks!
+
+	let stack: []*node = [];
+	defer free(stack);
+
+	for (true) {
+		const token = match (xml::scan(parser)!) {
+		case let t: xml::token =>
+			yield t;
+		case void =>
+			break;
+		};
+
+		match (token) {
+		case let start: xml::elementstart =>
+			if (len(stack) == 0) {
+				tree = alloc(node {
+					name = strings::dup(start),
+					...
+				});
+				append(stack, tree);
+			} else {
+				let current = stack[len(stack) - 1];
+				append(stack, get_node(current, start));
+			};
+		case let end: xml::elementend =>
+			//fmt::printfln("stack => {}", stack[len(stack) - 1].name)!;
+			//fmt::printfln("file => {}", end)!;
+			assert(end == stack[len(stack) - 1].name);
+			delete(stack[len(stack) - 1]);
+			if (len(stack) == 0) break;
+		case let attr: xml::attribute =>
+			let current = stack[len(stack) - 1];
+			add_attr(current, attr);
+		case let text: xml::text =>
+			let current = stack[len(stack) - 1];
+			add_text(current, text);
+		};
+	};
+
+	print(tree, 0);
+};
diff --git a/generate b/generate
new file mode 100755
index 0000000..7f62ada
--- /dev/null
+++ b/generate
@@ -0,0 +1,40 @@
+#!/bin/sh
+
+if [ $# -lt 1 ]; then
+	printf "Usage: generate <gobject introspection root>\n" >&2
+	exit 1
+fi
+GI="$1"
+BIND=./bindings
+
+mkdir -p "$BIND"/glib "$BIND"/gobject "$BIND"/gio "$BIND"/gmodule "$BIND"/atk \
+	"$BIND"/gdkpixbuf "$BIND"/freetype "$BIND"/harfbuzz "$BIND"/cairo \
+	"$BIND"/pango "$BIND"/gdk "$BIND"/xlib "$BIND"/gtk
+
+hare build cmd/hare-gi && ./hare-gi -B \
+	-m GLib,glib,"$BIND"/glib/_generated.ha \
+	-m GObject,gobject,"$BIND"/gobject/_generated.ha \
+	-m Gio,gio,"$BIND"/gio/_generated.ha \
+	-m GModule,gmodule,"$BIND"/gmodule/_generated.ha \
+	-m Atk,atk,"$BIND"/atk/_generated.ha \
+	-m GdkPixbuf,gdkpixbuf,"$BIND"/gdkpixbuf/_generated.ha \
+	-m freetype2,freetype,"$BIND"/freetype/_generated.ha \
+	-m HarfBuzz,harfbuzz,"$BIND"/harfbuzz/_generated.ha \
+	-m cairo,cairo,"$BIND"/cairo/_generated.ha \
+	-m Pango,pango,"$BIND"/pango/_generated.ha \
+	-m Gdk,gdk,"$BIND"/gdk/_generated.ha \
+	-m xlib,xlib,"$BIND"/xlib/_generated.ha \
+	-m Gtk,gtk,"$BIND"/gtk/_generated.ha \
+	"$GI"/build/gir/GLib-2.0.gir \
+	"$GI"/build/gir/GObject-2.0.gir \
+	"$GI"/build/gir/Gio-2.0.gir \
+	"$GI"/build/gir/GModule-2.0.gir \
+	/usr/share/gir-1.0/Atk-1.0.gir \
+	/usr/share/gir-1.0/GdkPixbuf-2.0.gir \
+	"$GI"/build/gir/freetype2-2.0.gir \
+	/usr/share/gir-1.0/HarfBuzz-0.0.gir \
+	"$GI"/build/gir/cairo-1.0.gir \
+	/usr/share/gir-1.0/Pango-1.0.gir \
+	/usr/share/gir-1.0/Gdk-3.0.gir \
+	"$GI"/build/gir/xlib-2.0.gir \
+	/usr/share/gir-1.0/Gtk-3.0.gir
diff --git a/generate4 b/generate4
new file mode 100755
index 0000000..8fcff42
--- /dev/null
+++ b/generate4
@@ -0,0 +1,47 @@
+#!/bin/sh
+
+if [ $# -lt 1 ]; then
+	printf "Usage: generate4 <gobject introspection root>\n" >&2
+	exit 1
+fi
+GI="$1"
+BIND=./bindings
+
+mkdir -p "$BIND"/glib "$BIND"/gobject "$BIND"/gio "$BIND"/gmodule "$BIND"/atk \
+	"$BIND"/gdkpixbuf "$BIND"/freetype "$BIND"/harfbuzz "$BIND"/cairo \
+	"$BIND"/pango "$BIND"/pangocairo "$BIND"/gdk4 "$BIND"/xlib "$BIND"/graphene \
+	"$BIND"/gsk "$BIND"/gtk4
+
+hare build cmd/hare-gi && ./hare-gi -B \
+	-m GLib,glib,"$BIND"/glib/_generated.ha \
+	-m GObject,gobject,"$BIND"/gobject/_generated.ha \
+	-m Gio,gio,"$BIND"/gio/_generated.ha \
+	-m GModule,gmodule,"$BIND"/gmodule/_generated.ha \
+	-m Atk,atk,"$BIND"/atk/_generated.ha \
+	-m GdkPixbuf,gdkpixbuf,"$BIND"/gdkpixbuf/_generated.ha \
+	-m freetype2,freetype,"$BIND"/freetype/_generated.ha \
+	-m HarfBuzz,harfbuzz,"$BIND"/harfbuzz/_generated.ha \
+	-m cairo,cairo,"$BIND"/cairo/_generated.ha \
+	-m Pango,pango,"$BIND"/pango/_generated.ha \
+	-m PangoCairo,pangocairo,"$BIND"/pangocairo/_generated.ha \
+	-m Gdk,gdk4,"$BIND"/gdk4/_generated.ha \
+	-m xlib,xlib,"$BIND"/xlib/_generated.ha \
+	-m Graphene,graphene,"$BIND"/graphene/_generated.ha \
+	-m Gsk,gsk,"$BIND"/gsk/_generated.ha \
+	-m Gtk,gtk4,"$BIND"/gtk4/_generated.ha \
+	"$GI"/build/gir/GLib-2.0.gir \
+	"$GI"/build/gir/GObject-2.0.gir \
+	"$GI"/build/gir/Gio-2.0.gir \
+	"$GI"/build/gir/GModule-2.0.gir \
+	/usr/share/gir-1.0/Atk-1.0.gir \
+	/usr/share/gir-1.0/GdkPixbuf-2.0.gir \
+	"$GI"/build/gir/freetype2-2.0.gir \
+	/usr/share/gir-1.0/HarfBuzz-0.0.gir \
+	"$GI"/build/gir/cairo-1.0.gir \
+	/usr/share/gir-1.0/Pango-1.0.gir \
+	/usr/share/gir-1.0/PangoCairo-1.0.gir \
+	/usr/share/gir-1.0/Gdk-4.0.gir \
+	"$GI"/build/gir/xlib-2.0.gir \
+	/usr/share/gir-1.0/Graphene-1.0.gir \
+	/usr/share/gir-1.0/Gsk-4.0.gir \
+	/usr/share/gir-1.0/Gtk-4.0.gir
diff --git a/gir/parse.ha b/gir/parse.ha
new file mode 100644
index 0000000..f844cd9
--- /dev/null
+++ b/gir/parse.ha
@@ -0,0 +1,742 @@
+use fmt;
+use io;
+use strconv;
+use strings;
+use xml = format::fastxml;
+
+// The provided GIR file is invalid.
+export type invalid = !void;
+
+// Any error which can occur during GIR parsing.
+export type error = !(invalid | xml::error);
+
+// Returns a human-friendly representation of an [[error]].
+export fn strerror(err: error) const str = {
+	match (err) {
+	case invalid =>
+		return "File does not follow GIR structure";
+	case let err: xml::error =>
+		return xml::strerror(err);
+	};
+};
+
+// Parses a file into a [[repository]].
+export fn parse(in: io::file) (repository | error) = {
+	const parser = xml::parse(in)?;
+	defer xml::parser_free(parser);
+
+	let repo = repository { ... };
+	parse_element(parser, "",
+		("repository", &parse_repository, &repo),
+	)?;
+	return repo;
+};
+
+fn parse_repository(parser: *xml::parser, repo: *void) (void | error) = {
+	const repo = repo: *repository;
+	return parse_element(parser, "repository",
+		("xmlns", null),
+		("xmlns:c", null),
+		("xmlns:glib", null),
+		("version", &repo.version),
+		("c:identifier-prefixes", null),
+		("c:symbol-prefixes", null),
+		("include", &parse_include, &repo.includes),
+		("c:include", null, null),
+		("package", null, null),
+		("namespace", &parse_namespace, &repo.namespaces),
+	);
+};
+
+fn parse_include(parser: *xml::parser, includes: *void) (void | error) = {
+	const includes = includes: *[]include;
+	let new = include { ... };
+	parse_element(parser, "include",
+		("name", &new.name),
+		("version", &new.version),
+	)?;
+	append(includes, new);
+};
+
+fn parse_namespace(parser: *xml::parser, namespaces: *void) (void | error) = {
+	const namespaces = namespaces: *[]namespace;
+	let new = namespace { ... };
+	parse_element(parser, "namespace",
+		("name", &new.name),
+		("version", &new.version),
+		("c:identifier-prefixes", null),
+		("c:symbol-prefixes", null),
+		("shared-library", null),
+
+		("alias", &parse_alias, &new.aliases),
+		("class", &parse_class, &new.classes),
+		("interface", &parse_interface, &new.interfaces),
+		("record", &parse_record, &new.records),
+		("enumeration", &parse_enumeration, &new.enums),
+		("function", &parse_function, &new.functions),
+		("union", &parse_union, &new.unions),
+		("bitfield", &parse_bitfield, &new.bitfields),
+		("callback", &parse_callback, &new.callbacks),
+		("constant", &parse_constant, &new.constants),
+		("annotation", null, null),
+		("glib:boxed", null, null),
+
+		// Undocumented and unsupported
+		("docsection", null, null),
+		("function-macro", null, null),
+	)?;
+	append(namespaces, new);
+};
+
+fn parse_alias(parser: *xml::parser, aliases: *void) (void | error) = {
+	const aliases = aliases: *[]alias;
+	let new = alias { ... };
+	parse_element(parser, "alias",
+		parse_info(&new),
+		("name", &new.name),
+		("c:type", &new.c_type),
+		("type", &parse_simple_type, &new.inner),
+	)?;
+	append(aliases, new);
+};
+
+fn parse_class(parser: *xml::parser, classes: *void) (void | error) = {
+	const classes = classes: *[]class;
+	let new = class { ... };
+	parse_element(parser, "class",
+		parse_info(&new),
+		("name", &new.name),
+		("glib:type-name", &new.glib_type_name),
+		("glib:get-type", &new.glib_get_type),
+		("parent", &new.parent),
+		("glib:type-struct", null),
+		("glib:ref-func", &new.glib_ref_func),
+		("glib:unref-func", &new.glib_unref_func),
+		("glib:set-value-func", &new.glib_set_value_func),
+		("glib:get-value-func", &new.glib_get_value_func),
+		("c:type", &new.c_type),
+		("c:symbol-prefix", &new.symbol_prefix),
+		("abstract", &new.abstract),
+		("glib:fundamental", &new.fundamental),
+		("final", &new.final),
+
+		("implements", &parse_implements, &new.implements),
+		("constructor", &parse_constructor, &new.constructors),
+		("method", &parse_method, &new.methods),
+		("function", &parse_function, &new.functions),
+		("virtual-method", &parse_virtual_method, &new.virtual_methods),
+		("field", &parse_field, &new.entries),
+		("property", &parse_property, &new.properties),
+		("glib:signal", &parse_signal, &new.signals),
+		("union", &parse_union_entry, &new.entries),
+		("constant", &parse_constant, &new.constants),
+		("record", &parse_record_entry, &new.entries),
+		("callback", &parse_callback, &new.callbacks),
+	)?;
+	append(classes, new);
+};
+fn parse_implements(parser: *xml::parser, implements: *void) (void | error) = {
+	const implements = implements: *[]str;
+	let name = "";
+	parse_element(parser, "implements",
+		("name", &name),
+	)?;
+	append(implements, name);
+};
+fn parse_interface(parser: *xml::parser, interfaces: *void) (void | error) = {
+	const interfaces = interfaces: *[]interface;
+	let new = interface { ... };
+	parse_element(parser, "interface",
+		parse_info(&new),
+		("name", &new.name),
+		("glib:type-name", &new.glib_type_name),
+		("glib:get-type", &new.glib_get_type),
+		("c:symbol-prefix", null),
+		("c:type", &new.c_type),
+		("glib:type-struct", null),
+
+		("prerequisite", &parse_prerequisite, &new.prerequisites),
+		("implements", &parse_implements, &new.implements),
+		("function", &parse_function, &new.functions),
+		("constructor", &parse_constructor, &new.constructors),
+		("method", &parse_method, &new.methods),
+		("virtual-method", &parse_virtual_method, &new.virtual_methods),
+		("field", &parse_field, &new.entries),
+		("property", &parse_property, &new.properties),
+		("glib:signal", &parse_signal, &new.signals),
+		("callback", &parse_callback, &new.callbacks),
+		("constant", &parse_constant, &new.constants),
+	)?;
+	append(interfaces, new);
+};
+fn parse_prerequisite(parser: *xml::parser, prerequisites: *void) (void | error) = {
+	const prerequisites = prerequisites: *[]str;
+	let name = "";
+	parse_element(parser, "prerequisite",
+		("name", &name),
+	)?;
+	append(prerequisites, name);
+};
+
+fn parse_record(parser: *xml::parser, records: *void) (void | error) = {
+	const records = records: *[]record;
+	let new = record { ... };
+	parse_element(parser, "record",
+		parse_info(&new),
+		("name", &new.name),
+		("c:type", &new.c_type),
+		("disguised", &new.disguised),
+		("opaque", &new.opaque),
+		("pointer", &new.pointer),
+		("glib:type-name", &new.glib_type_name),
+		("glib:get-type", &new.glib_get_type),
+		("c:symbol-prefix", null),
+		("foreign", &new.foreign),
+		("glib:is-gtype-struct-for", null),
+		("copy-function", &new.copy_function),
+		("free-function", &new.free_function),
+
+		("field", &parse_field, &new.entries),
+		("function", &parse_function, &new.functions),
+		("union", &parse_union_entry, &new.entries),
+		("method", &parse_method, &new.methods),
+		("constructor", &parse_constructor, &new.constructors),
+	)?;
+	append(records, new);
+};
+
+fn parse_record_entry(parser: *xml::parser, entries: *void) (void | error) = {
+	const entries = entries: *[]entry;
+	let records: []record = [];
+	defer free(records);
+	parse_record(parser, &records)?;
+	append(entries, records[0]);
+};
+
+fn parse_constructor(parser: *xml::parser, constructors: *void) (void | error) = {
+	const constructors = constructors: *[]constructor;
+	let new = constructor { ... };
+	parse_element(parser, "constructor",
+		parse_callable(&new),
+		("parameters", &parse_parameters, &new.params),
+		("return-value", &parse_return_value, &new.return_value),
+	)?;
+	append(constructors, new);
+};
+fn parse_method(parser: *xml::parser, methods: *void) (void | error) = {
+	const methods = methods: *[]method;
+	let new = method { ... };
+	parse_element(parser, "method",
+		parse_callable(&new),
+		("glib:set-property", &new.glib_set_property),
+		("glib:get-property", &new.glib_get_property),
+		("parameters", &parse_method_parameters,
+			&(&new.instance, &new.params)),
+		("return-value", &parse_return_value, &new.return_value),
+	)?;
+	append(methods, new);
+};
+fn parse_virtual_method(parser: *xml::parser, vmethods: *void) (void | error) = {
+	const vmethods = vmethods: *[]virtual_method;
+	let new = virtual_method { ... };
+	parse_element(parser, "virtual-method",
+		parse_callable(&new),
+		("invoker", &new.invoker),
+		("parameters", &parse_method_parameters,
+			&(&new.instance, &new.params)),
+		("return-value", &parse_return_value, &new.return_value),
+	)?;
+	append(vmethods, new);
+};
+fn parse_field(parser: *xml::parser, entries: *void) (void | error) = {
+	const entries = entries: *[]entry;
+	let new = field { ... };
+	let type_: any_type = simple_type { ... };
+	let cb = callback { ... };
+	parse_element(parser, "field",
+		parse_info(&new),
+		("name", &new.name),
+		("writable", &new.writable),
+		("readable", &new.readable),
+		("private", &new.private),
+		("bits", &new.bits),
+
+		("callback", &parse_one_callback, &cb),
+		parse_any_type(&type_),
+	)?;
+	if (len(cb.name) > 0) {
+		new.type_ = cb;
+	} else {
+		new.type_ = type_;
+	};
+	append(entries, new);
+};
+fn parse_property(parser: *xml::parser, properties: *void) (void | error) = {
+	const properties = properties: *[]property;
+	let new = property { ... };
+	parse_element(parser, "property",
+		parse_info(&new),
+		("name", &new.name),
+		("writable", &new.writable),
+		("readable", &new.readable),
+		("construct", &new.construct),
+		("construct-only", &new.construct_only),
+		("setter", &new.setter),
+		("getter", &new.getter),
+		("default-value", &new.default_value),
+		("transfer-ownership", &new.transfer_ownership),
+
+		parse_any_type(&new.type_),
+	)?;
+	append(properties, new);
+};
+
+fn parse_enumeration(parser: *xml::parser, enums: *void) (void | error) = {
+	const enums = enums: *[]enumeration;
+	let new = enumeration { ... };
+	parse_element(parser, "enumeration",
+		parse_info(&new),
+		("name", &new.name),
+		("c:type", &new.c_type),
+		("glib:type-name", &new.glib_type_name),
+		("glib:get-type", &new.glib_get_type),
+		("glib:error-domain", &new.glib_error_domain),
+
+		("member", &parse_member, &new.members),
+		("function", &parse_function, &new.functions),
+	)?;
+	append(enums, new);
+};
+
+fn parse_function(parser: *xml::parser, functions: *void) (void | error) = {
+	const functions = functions: *[]function;
+	let new = function { ... };
+	parse_element(parser, "function",
+		parse_callable(&new),
+		("parameters", &parse_parameters, &new.params),
+		("return-value", &parse_return_value, &new.return_value),
+	)?;
+	append(functions, new);
+};
+
+fn parse_union(parser: *xml::parser, unions: *void) (void | error) = {
+	const unions = unions: *[]union_;
+	let new = union_ { ... };
+	parse_element(parser, "union",
+		parse_info(&new),
+		("name", &new.name),
+		("c:type", &new.c_type),
+		("c:symbol-prefix", null),
+		("glib:type-name", &new.glib_type_name),
+		("glib:get-type", &new.glib_get_type),
+		("copy-function", &new.copy_function),
+		("free-function", &new.free_function),
+
+		("field", &parse_field, &new.entries),
+		("constructor", &parse_constructor, &new.constructors),
+		("method", &parse_method, &new.methods),
+		("function", &parse_function, &new.functions),
+		("record", &parse_record_entry, &new.entries),
+	)?;
+	append(unions, new);
+};
+
+fn parse_union_entry(parser: *xml::parser, entries: *void) (void | error) = {
+	const entries = entries: *[]entry;
+	let unions: []union_ = [];
+	defer free(unions);
+	parse_union(parser, &unions)?;
+	append(entries, unions[0]);
+};
+
+fn parse_bitfield(parser: *xml::parser, bitfields: *void) (void | error) = {
+	const bitfields = bitfields: *[]bitfield;
+	let new = bitfield { ... };
+	parse_element(parser, "bitfield",
+		parse_info(&new),
+		("name", &new.name),
+		("c:type", &new.c_type),
+		("glib:type-name", &new.glib_type_name),
+		("glib:get-type", &new.glib_get_type),
+
+		("member", &parse_member, &new.members),
+		("function", &parse_function, &new.functions),
+	)?;
+	append(bitfields, new);
+};
+
+fn parse_callback(parser: *xml::parser, callbacks: *void) (void | error) = {
+	const callbacks = callbacks: *[]callback;
+	let new = callback { ... };
+	parse_one_callback(parser, &new)?;
+	append(callbacks, new);
+};
+fn parse_one_callback(parser: *xml::parser, cb: *void) (void | error) = {
+	const cb = cb: *callback;
+	return parse_element(parser, "callback",
+		parse_info(cb),
+		("name", &cb.name),
+		("c:type", &cb.c_type),
+		("throws", &cb.throws),
+
+		("parameters", &parse_parameters, &cb.params),
+		("return-value", &parse_return_value, &cb.return_value),
+	);
+};
+
+fn parse_constant(parser: *xml::parser, constants: *void) (void | error) = {
+	const constants = constants: *[]constant;
+	let new = constant { ... };
+	let type_: any_type = simple_type { ... };
+	parse_element(parser, "constant",
+		parse_info(&new),
+		("name", &new.name),
+		("value", &new.value),
+		("c:type", &new.c_type),
+		("c:identifier", &new.c_identifier),
+		parse_any_type(&type_),
+	)?;
+	if (type_ is array_type || len((type_ as simple_type).name) > 0) {
+		new.type_ = type_;
+	} else {
+		new.type_ = void;
+	};
+	append(constants, new);
+};
+
+fn parse_signal(parser: *xml::parser, signals: *void) (void | error) = {
+	const signals = signals: *[]signal;
+	let new = signal { ... };
+	parse_element(parser, "glib:signal",
+		parse_info(&new),
+		("name", &new.name),
+		("detailed", &new.detailed),
+		("when", &new.when),
+		("action", &new.action),
+		("no-hooks", &new.no_hooks),
+		("no-recurse", &new.no_recurse),
+		("emitter", &new.emitter),
+
+		("parameters", &parse_parameters, &new.params),
+		("return-value", &parse_return_value, &new.return_value),
+	)?;
+	append(signals, new);
+};
+
+// enum + bitfield
+fn parse_member(parser: *xml::parser, members: *void) (void | error) = {
+	const members = members: *[]member;
+	let new = member { ... };
+	parse_element(parser, "member",
+		parse_info(&new),
+		("name", &new.name),
+		("value", &new.value),
+		("c:identifier", &new.c_identifier),
+		("glib:nick", &new.glib_nick),
+		("glib:name", &new.glib_name),
+	)?;
+	append(members, new);
+};
+
+// callable
+fn parse_callable(callable: *callable) [17]scan = [
+	("introspectable", null),
+	("deprecated", null),
+	("deprecated-version", null),
+	("version", null),
+	("stability", null),
+	("doc", &parse_doc, &callable.doc),
+	("doc-version", null, null),
+	("doc-stability", null, null),
+	("doc-deprecated", null, null),
+	("source-position", null, null),
+	("attribute", null, null),
+	("name", &callable.name),
+	("c:identifier", &callable.c_identifier),
+	("shadowed-by", &callable.shadowed_by),
+	("shadows", &callable.shadows),
+	("throws", &callable.throws),
+	("moved-to", null),
+];
+fn parse_parameters(parser: *xml::parser, parameters: *void) (void | error) = {
+	return parse_element(parser, "parameters",
+		("parameter", &parse_parameter, parameters),
+		("varargs", &parse_varargs, parameters),
+	);
+};
+fn parse_method_parameters(parser: *xml::parser, parameters: *void) (void | error) = {
+	const parameters = parameters: *(*instance_parameter, *[]callable_param);
+	return parse_element(parser, "parameters",
+		("instance-parameter", &parse_instance_parameter, parameters.0),
+		("parameter", &parse_parameter, parameters.1),
+		("varargs", &parse_varargs, parameters.1),
+	);
+};
+fn parse_instance_parameter(parser: *xml::parser, param: *void) (void | error) = {
+	const param = param: *instance_parameter;
+	return parse_element(parser, "instance-parameter",
+		parse_info(param),
+		("name", &param.name),
+		("nullable", &param.is_nullable),
+		("allow-none", &param.allow_none),
+		("direction", &param.direction),
+		("caller-allocates", &param.caller_allocates),
+		("transfer-ownership", &param.transfer_ownership),
+
+		("type", &parse_simple_type, &param.type_),
+	);
+};
+fn parse_parameter(parser: *xml::parser, parameters: *void) (void | error) = {
+	const parameters = parameters: *[]callable_param;
+	let new = callable_param { ... };
+	let type_: any_type = simple_type { ... };
+	let has_varargs = false;
+	parse_element(parser, "parameter",
+		parse_info(&new),
+		("name", &new.name),
+		("nullable", &new.is_nullable),
+		("allow-none", &new.allow_none),
+		("introspectable", &new.introspectable),
+		("closure", &new.closure),
+		("destroy", &new.destroy),
+		("scope", &new.scope),
+		("direction", &new.direction),
+		("caller-allocates", &new.caller_allocates),
+		("optional", &new.optional),
+		("skip", &new.skip),
+		("transfer-ownership", &new.transfer_ownership),
+		("varargs", &parse_varargs, &has_varargs),
+		parse_any_type(&type_),
+	)?;
+	if (has_varargs) {
+		new.parameter = varargs;
+	} else {
+		new.parameter = type_;
+	};
+	append(parameters, new);
+};
+fn parse_varargs(parser: *xml::parser, has_varargs: *void) (void | error) = {
+	*(has_varargs: *bool) = true;
+	return parse_element(parser, "varargs");
+};
+fn parse_return_value(parser: *xml::parser, return_value: *void) (void | error) = {
+	const return_value = return_value: *(callable_return | void);
+	let new = callable_return { ... };
+	parse_element(parser, "return-value",
+		parse_info(&new),
+		("introspectable", &new.introspectable),
+		("nullable", &new.is_nullable),
+		("closure", &new.closure),
+		("destroy", &new.destroy),
+		("skip", &new.skip),
+		("allow-none", &new.allow_none),
+		("transfer-ownership", &new.transfer_ownership),
+		parse_any_type(&new.type_),
+	)?;
+	*return_value = new;
+};
+
+// documentation
+fn parse_info(doc: *documentation) [11]scan = [
+	("introspectable", null),
+	("deprecated", null),
+	("deprecated-version", null),
+	("version", null),
+	("stability", null),
+	("doc", &parse_doc, &doc.doc),
+	("doc-version", null, null),
+	("doc-stability", null, null),
+	("doc-deprecated", null, null),
+	("source-position", null, null),
+	("attribute", null, null),
+];
+fn parse_doc(parser: *xml::parser, text: *void) (void | error) = {
+	const text = text: *str;
+	return parse_element(parser, "doc",
+		("xml:space", null),
+		("xml:whitespace", null),
+		("filename", null),
+		("line", null),
+		("column", null),
+		text,
+	);
+};
+
+// types
+fn parse_simple_type(parser: *xml::parser, out: *void) (void | error) = {
+	const out = out: *simple_type;
+	return parse_element(parser, "type",
+		parse_info(out),
+		("name", &out.name),
+		("c:type", &out.c_type),
+		("introspectable", &out.introspectable),
+		("type", null, null),
+		("array", null, null),
+	);
+};
+
+fn parse_array_type(parser: *xml::parser, out: *void) (void | error) = {
+	const out = out: *array_type;
+	let inner: any_type = simple_type { ... };
+	parse_element(parser, "array",
+		parse_info(out),
+		("name", &out.name),
+		("zero-terminated", &out.zero_terminated),
+		("fixed-size", &out.fixed_size),
+		("introspectable", &out.introspectable),
+		("length", &out.length),
+		("c:type", &out.c_type),
+		parse_any_type(&inner),
+	)?;
+	out.inner = alloc(inner);
+};
+
+fn parse_any_type(out: *any_type) [2]scan = [
+	("type", &parse_simple_type_union, out),
+	("array", &parse_array_type_union, out),
+];
+fn parse_simple_type_union(parser: *xml::parser, out: *void) (void | error) = {
+	const out = out: *any_type;
+	let simple = simple_type { ... };
+	parse_simple_type(parser, &simple)?;
+	*out = simple;
+};
+fn parse_array_type_union(parser: *xml::parser, out: *void) (void | error) = {
+	const out = out: *any_type;
+	let array = array_type { ... };
+	parse_array_type(parser, &array)?;
+	*out = array;
+};
+
+type parse_func = fn(parser: *xml::parser, data: *void) (void | error);
+type scan_element = (
+	str,
+	nullable *parse_func,
+	nullable *void,
+);
+type scan_attribute = (
+	str,
+	(nullable *str | *uint | *bool),
+);
+type scan_text = *str;
+type scan = (scan_element | scan_attribute | scan_text | []scan);
+
+fn parse_element(
+	parser: *xml::parser,
+	current: str,
+	scans: scan...
+) (void | error) = {
+	let elements: []scan_element = [];
+	defer free(elements);
+	let attrs: []scan_attribute = [];
+	defer free(attrs);
+	let text_out: nullable *str = null;
+
+	for (let i = 0z; i < len(scans); i += 1) {
+		match (scans[i]) {
+		case let s: []scan =>
+			for (let j = 0z; j < len(s); j += 1) match (s[j]) {
+			case let s: scan_element =>
+				append(elements, s);
+			case let s: scan_attribute =>
+				append(attrs, s);
+			case let s: scan_text =>
+				assert(text_out is null, "only one scan_text is allowed");
+				text_out = s;
+			case => abort("too deep");
+			};
+		case let s: scan_element =>
+			append(elements, s);
+		case let s: scan_attribute =>
+			append(attrs, s);
+		case let s: scan_text =>
+			assert(text_out is null, "only one scan_text is allowed");
+			text_out = s;
+		};
+	};
+
+	for (true) match (xml::scan(parser)?) {
+	case let start: xml::elementstart =>
+		let found = false;
+		for (let i = 0z; i < len(elements); i += 1) {
+			const (name, func, data) = elements[i];
+			if (start == name) {
+				match (func) {
+				case let func: *parse_func =>
+					func(parser, data: *void)?;
+				case null =>
+					ignore(parser, name)?;
+				};
+				found = true;
+				break;
+			};
+		};
+		if (!found) {
+			fmt::fatalf("Unhandled element {}->{}", current, start);
+			//return invalid;
+		};
+	case let end: xml::elementend =>
+		assert(len(end) == 0 || end == current);
+		break;
+	case let attr: xml::attribute =>
+		let found = false;
+		for (let i = 0z; i < len(attrs); i += 1) {
+			const (name, out) = attrs[i];
+			if (attr.0 == name) {
+				match (out) {
+				case let s: nullable *str =>
+					match(s) {
+					case let s: *str =>
+						*s = strings::dup(attr.1);
+					case null => yield;
+					};
+				case let b: *bool =>
+					*b = (attr.1 == "1");
+				case let u: *uint =>
+					match (strconv::stou(attr.1)) {
+					case let parsed: uint =>
+						*u = parsed;
+					case =>
+						return invalid;
+					};
+				};
+				found = true;
+				break;
+			};
+		};
+		if (!found) {
+			fmt::fatalf("Unhandled attribute {}->{}", current, attr.0);
+			//return invalid;
+		};
+	case let text: xml::text =>
+		if (len(strings::trim(text)) > 0) {
+			match (text_out) {
+			case let s: *str =>
+				*s = strings::dup(text);
+			case null =>
+				yield;
+			};
+		};
+	case void =>
+		if (len(current) == 0) {
+			break;
+		};
+		return invalid;
+	};
+};
+
+fn ignore(parser: *xml::parser, name: str) (void | error) = {
+	const name = strings::dup(name);
+	defer free(name);
+
+	for (true) match (xml::scan(parser)?) {
+	case let start: xml::elementstart =>
+		ignore(parser, start)?;
+	case let end: xml::elementend =>
+		assert(end == name || end == "");
+		break;
+	case void =>
+		return invalid;
+	case => yield;
+	};
+};
diff --git a/gir/types.ha b/gir/types.ha
new file mode 100644
index 0000000..6616d69
--- /dev/null
+++ b/gir/types.ha
@@ -0,0 +1,363 @@
+export type repository = struct {
+	version: str,
+
+	includes: []include,
+	namespaces: []namespace,
+};
+export fn repository_finish(repo: *repository) void = {
+	free(repo.version);
+	for (let i = 0z; i < len(repo.includes); i += 1) {
+		include_finish(&repo.includes[i]);
+	};
+	for (let i = 0z; i < len(repo.namespaces); i += 1) {
+		namespace_finish(&repo.namespaces[i]);
+	};
+};
+
+export type include = struct {
+	name: str,
+	version: str,
+};
+export fn include_finish(incl: *include) void = {
+	free(incl.name);
+	free(incl.version);
+};
+
+export type namespace = struct {
+	name: str,
+	version: str,
+
+	aliases: []alias,
+	classes: []class,
+	interfaces: []interface,
+	records: []record,
+	enums: []enumeration,
+	functions: []function,
+	unions: []union_,
+	bitfields: []bitfield,
+	callbacks: []callback,
+	constants: []constant,
+};
+export fn namespace_finish(ns: *namespace) void = {
+	free(ns.name);
+	free(ns.version);
+	// TODO
+};
+
+export type alias = struct {
+	documentation,
+
+	name: str,
+	c_type: str,
+
+	inner: simple_type,
+};
+
+export type class = struct {
+	documentation,
+
+	name: str,
+	glib_type_name: str,
+	glib_get_type: str,
+	parent: str,
+	glib_ref_func: str,
+	glib_unref_func: str,
+	glib_set_value_func: str,
+	glib_get_value_func: str,
+	c_type: str,
+	symbol_prefix: str,
+	abstract: bool,
+	fundamental: bool,
+	final: bool,
+
+	entries: []entry,
+	implements: []str,
+	constructors: []constructor,
+	methods: []method,
+	functions: []function,
+	virtual_methods: []virtual_method,
+	properties: []property,
+	signals: []signal,
+	constants: []constant,
+	callbacks: []callback,
+};
+export type interface = struct {
+	documentation,
+
+	name: str,
+	glib_type_name: str,
+	glib_get_type: str,
+	symbol_prefix: str,
+	c_type: str,
+
+	entries: []entry,
+	prerequisites: []str,
+	implements: []str,
+	functions: []function,
+	constructors: []constructor,
+	methods: []method,
+	virtual_methods: []virtual_method,
+	properties: []property,
+	signals: []signal,
+	callbacks: []callback,
+	constants: []constant,
+};
+export type record = struct {
+	documentation,
+
+	name: str,
+	c_type: str,
+	disguised: bool,
+	opaque: bool,
+	pointer: bool,
+	glib_type_name: str,
+	glib_get_type: str,
+	symbol_prefix: str,
+	foreign: bool,
+	copy_function: str,
+	free_function: str,
+
+	entries: []entry,
+	functions: []function,
+	methods: []method,
+	constructors: []constructor,
+};
+
+// FIXME: find a better name for this
+export type entry = (field | union_ | record);
+
+export type constructor = struct {
+	callable,
+
+	params: []callable_param,
+	return_value: (callable_return | void),
+};
+export type method = struct {
+	callable,
+
+	glib_set_property: str,
+	glib_get_property: str,
+
+	instance: instance_parameter,
+	params: []callable_param,
+	return_value: (callable_return | void),
+};
+export type virtual_method = struct {
+	callable,
+
+	invoker: str,
+
+	instance: instance_parameter,
+	params: []callable_param,
+	return_value: (callable_return | void),
+};
+export type field = struct {
+	documentation,
+
+	name: str,
+	writable: bool,
+	readable: bool,
+	private: bool,
+	bits: uint,
+
+	type_: (callback | any_type),
+};
+export type property = struct {
+	documentation,
+
+	name: str,
+	writable: bool,
+	readable: bool,
+	construct: bool,
+	construct_only: bool,
+	setter: str,
+	getter: str,
+	default_value: str,
+	transfer_ownership: str,
+
+	type_: any_type,
+};
+
+export type enumeration = struct {
+	documentation,
+
+	name: str,
+	c_type: str,
+	glib_type_name: str,
+	glib_get_type: str,
+	glib_error_domain: str,
+
+	members: []member,
+	functions: []function,
+};
+
+export type function = struct {
+	callable,
+
+	params: []callable_param,
+	return_value: (callable_return | void),
+};
+
+export type union_ = struct {
+	documentation,
+
+	name: str,
+	c_type: str,
+	symbol_prefix: str,
+	glib_type_name: str,
+	glib_get_type: str,
+	copy_function: str,
+	free_function: str,
+
+	entries: []entry,
+	constructors: []constructor,
+	methods: []method,
+	functions: []function,
+};
+
+export type bitfield = struct {
+	documentation,
+
+	name: str,
+	c_type: str,
+	glib_type_name: str,
+	glib_get_type: str,
+
+	members: []member,
+	functions: []function,
+};
+
+export type callback = struct {
+	documentation,
+
+	name: str,
+	c_type: str,
+	throws: bool,
+
+	params: []callable_param,
+	return_value: (callable_return | void),
+};
+
+export type constant = struct {
+	documentation,
+
+	name: str,
+	value: str,
+	c_type: str,
+	c_identifier: str,
+
+	type_: (any_type | void),
+};
+
+export type signal = struct {
+	documentation,
+
+	name: str,
+	detailed: bool,
+	when: str,
+	action: bool,
+	no_hooks: bool,
+	no_recurse: bool,
+	emitter: str,
+
+	params: []callable_param,
+	return_value: (callable_return | void),
+};
+
+export type member = struct {
+	documentation,
+
+	name: str,
+	value: str,
+	c_identifier: str,
+	glib_nick: str,
+	glib_name: str,
+};
+
+export type callable = struct {
+	documentation,
+
+	name: str,
+	c_identifier: str,
+	shadowed_by: str,
+	shadows: str,
+	throws: bool,
+};
+
+export type callable_param = struct {
+	documentation,
+
+	name: str,
+	is_nullable: bool,
+	allow_none: bool,
+	introspectable: bool,
+	closure: uint,
+	destroy: uint,
+	scope: str,
+	direction: str,
+	caller_allocates: bool,
+	optional: bool,
+	skip: bool,
+	transfer_ownership: str,
+
+	parameter: (any_type | varargs),
+};
+
+export type instance_parameter = struct {
+	documentation,
+
+	name: str,
+	is_nullable: bool,
+	allow_none: bool,
+	direction: str,
+	caller_allocates: str,
+	transfer_ownership: str,
+
+	type_: simple_type,
+};
+
+export type varargs = void;
+
+export type callable_return = struct {
+	documentation,
+
+	introspectable: bool,
+	is_nullable: bool,
+	closure: uint,
+	scope: str,
+	destroy: uint,
+	skip: bool,
+	allow_none: bool,
+	transfer_ownership: str,
+
+	type_: any_type,
+};
+
+export type documentation = struct {
+	doc: str,
+};
+
+export type simple_type = struct {
+	documentation,
+
+	name: str,
+	c_type: str,
+	introspectable: bool,
+
+	inner: []any_type,
+};
+
+export type array_type = struct {
+	documentation,
+
+	name: str,
+	zero_terminated: bool,
+	fixed_size: uint,
+	introspectable: bool,
+	length: uint,
+	c_type: str,
+
+	inner: *any_type,
+};
+
+export type any_type = (simple_type | array_type);