python: Add initial decoder Python API
diff --git a/.clang-format b/.clang-format
index 597738d..c619094 100644
--- a/.clang-format
+++ b/.clang-format
@@ -10,3 +10,7 @@
     Priority: 3
   - Regex: '^<.*\.h>$'
     Priority: 4
+Macros:
+  - PyObject_HEAD_INIT(type)={0},
+  - PyVarObject_HEAD_INIT(type, size)={0},
+  - PyObject_HEAD=PyObject* ob_base;
diff --git a/README.md b/README.md
index d6c4fd0..60c6b2b 100644
--- a/README.md
+++ b/README.md
@@ -170,7 +170,8 @@
     - AMX instructions: no tile register may be used more than once
     - AVX-512 complex FP16 multiplication: destination must be not be equal to a source register
 - Low test coverage. (Help needed.)
-- No Python API.
+- No Python encode API.
+- No Rust API. (Help needed.)
 
 Some ISA extensions are not supported, often because they are deprecated or unsupported by recent hardware. These are unlikely to be implemented in the near future:
 
diff --git a/meson.build b/meson.build
index 5e6c258..6cacb29 100644
--- a/meson.build
+++ b/meson.build
@@ -128,3 +128,17 @@
 if cc.has_function('clock_gettime')
   executable('decode-profile', 'decode-profile.c', dependencies: fadec, c_args: ['-D_GNU_SOURCE'])
 endif
+
+if components.contains('decode')
+  py = import('python').find_installation(required: false, disabler: true)
+  # For Meson 0.63+, this can be dropped.
+  py_dep = dependency('python3', required: false, disabler: true)
+  py_mod = py.extension_module('fadec', 'python.c', dependencies: [fadec, py_dep],
+                               c_args: ['-Wno-missing-field-initializers', '-Wno-missing-prototypes'],
+                               install: true)
+  env = environment()
+  env.set('PYTHONPATH', meson.current_build_dir())
+  test('python', python3, args: ['python-test.py'], depends: [py_mod],
+       env: ['PYTHONPATH=' + meson.current_build_dir()],
+       workdir: meson.current_source_dir())
+endif
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..2066422
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,18 @@
+[project]
+name = 'fadec'
+version = '1.0.0'
+description = 'Fast Decoder for x86-32 and x86-64 and Encoder for x86-64'
+requires-python = '>=3.9'
+readme = 'README.md'
+license = {file = 'LICENSE'}
+authors = [
+  {name = 'Alexis Engelke', email = 'mail@aengelke.net'},
+]
+
+[build-system]
+build-backend = 'mesonpy'
+requires = ['meson-python']
+
+[tool.meson-python.wheel]
+exclude = ['*']
+include = ['{py_platlib}/*']
diff --git a/python-test.py b/python-test.py
new file mode 100644
index 0000000..acc9183
--- /dev/null
+++ b/python-test.py
@@ -0,0 +1,55 @@
+import fadec
+
+nop = fadec.decode64(b"\x90")
+assert str(nop) == "nop"
+assert repr(nop) == '<fadec.Inst "nop",size=1>'
+assert nop.size == 1
+assert nop.type == fadec.Inst.NOP
+assert nop.addrsize == 8
+assert nop.has_rep is False
+assert nop.has_repnz is False
+assert nop.has_lock is False
+assert nop.has_3e is False
+assert nop.maskreg is None
+assert nop.ops == ()
+
+add_rr = fadec.decode64(b"\x01\xc1")
+assert str(add_rr) == "add ecx, eax"
+assert repr(add_rr) == '<fadec.Inst "add ecx, eax",size=2>'
+assert add_rr.size == 2
+assert add_rr.type == fadec.Inst.ADD
+assert len(add_rr.ops) == 2
+assert repr(add_rr.ops[0]) == '<fadec.Op "add ecx, eax",idx=0>'
+assert add_rr.ops[0].is_reg
+assert not add_rr.ops[0].is_imm
+assert not add_rr.ops[0].is_mem
+assert not add_rr.ops[0].is_off
+assert not add_rr.ops[0].is_membcst
+assert add_rr.ops[0].size == 4
+assert str(add_rr.ops[0].reg) == "ecx"
+assert repr(add_rr.ops[0].reg) == f'<fadec.Reg "ecx",size=2,type={fadec.Reg.GPL},idx=1>'
+assert add_rr.ops[0].reg.sizelg == 2
+assert add_rr.ops[0].reg.type == fadec.Reg.GPL
+assert add_rr.ops[0].reg.idx == 1
+assert repr(add_rr.ops[1]) == '<fadec.Op "add ecx, eax",idx=1>'
+assert add_rr.ops[1].is_reg
+assert add_rr.ops[1].size == 4
+assert str(add_rr.ops[1].reg) == "eax"
+assert repr(add_rr.ops[1].reg) == f'<fadec.Reg "eax",size=2,type={fadec.Reg.GPL},idx=0>'
+assert add_rr.ops[1].reg.sizelg == 2
+assert add_rr.ops[1].reg.type == fadec.Reg.GPL
+assert add_rr.ops[1].reg.idx == 0
+
+mov_mi = fadec.decode64(b"\x66\xc7\x00\x42\x00")
+assert str(mov_mi) == "mov word ptr [rax], 0x42"
+assert mov_mi.size == 5
+assert mov_mi.type == fadec.Inst.MOV
+assert len(mov_mi.ops) == 2
+assert mov_mi.ops[0].size == 2
+assert mov_mi.ops[0].is_mem
+assert str(mov_mi.ops[0].base) == "rax"
+assert mov_mi.ops[0].index is None
+assert mov_mi.ops[0].disp == 0
+assert mov_mi.ops[1].size == 2
+assert mov_mi.ops[1].is_imm
+assert mov_mi.ops[1].imm == 0x42
diff --git a/python.c b/python.c
new file mode 100644
index 0000000..23b687e
--- /dev/null
+++ b/python.c
@@ -0,0 +1,506 @@
+
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+#include <fadec.h>
+#include <string.h>
+
+static PyObject* DecodeError;
+
+static unsigned Reg_FromPair(unsigned idx, unsigned ty) {
+  return idx == FD_REG_NONE ? 0 : idx | (ty << 8);
+}
+static PyObject* Reg_AsLong(unsigned reg) {
+  return Py_BuildValue(&"I"[!reg], reg);
+}
+
+typedef struct {
+  PyObject_HEAD
+
+  uint8_t size;
+  uint8_t type;
+  uint8_t idx;
+} RegObject;
+
+typedef struct {
+  PyObject_HEAD
+
+  FdInstr fdi;
+} InstObject;
+
+typedef struct {
+  PyObject_HEAD
+
+  InstObject* inst;
+  unsigned idx;
+} OpObject;
+
+static PyObject* Reg_str(RegObject* self) {
+  char buf[16];
+  fd_format_reg(self->type, self->idx, self->size, buf);
+  return Py_BuildValue("s", buf);
+}
+
+static PyObject* Reg_repr(RegObject* self) {
+  char buf[16];
+  fd_format_reg(self->type, self->idx, self->size, buf);
+  char repr[256];
+  sprintf(repr, "<fadec.Reg \"%s\",size=%u,type=%u,idx=%u>", buf, self->size,
+          self->type, self->idx);
+  return Py_BuildValue("s", repr);
+}
+
+static PyMemberDef RegType_members[] = {
+    {"sizelg", Py_T_UBYTE, offsetof(RegObject, size), Py_READONLY,
+     "Register size (logarithmic)."},
+    {"type", Py_T_UBYTE, offsetof(RegObject, type), Py_READONLY,
+     "Register type."},
+    {"idx", Py_T_UBYTE, offsetof(RegObject, idx), Py_READONLY,
+     "Register index."},
+    {0}};
+
+static PyTypeObject RegType = {
+    PyVarObject_HEAD_INIT(NULL, 0)
+    .tp_name = "fadec.Reg",
+    .tp_doc = "A register",
+    .tp_basicsize = sizeof(RegObject),
+    .tp_itemsize = 0,
+    .tp_flags = Py_TPFLAGS_DEFAULT,
+    .tp_str = (reprfunc)Reg_str,
+    .tp_repr = (reprfunc)Reg_repr,
+    .tp_members = RegType_members,
+};
+
+static void Op_dealloc(PyObject* self) {
+  Py_XDECREF(((OpObject*)self)->inst);
+  Py_TYPE(self)->tp_free(self);
+}
+
+static PyObject* Op_repr(OpObject* self) {
+  char buf[128];
+  fd_format(&self->inst->fdi, buf, sizeof buf);
+  char repr[256];
+  sprintf(repr, "<fadec.Op \"%s\",idx=%u>", buf, self->idx);
+  return Py_BuildValue("s", repr);
+}
+
+static PyObject* Op_getis_type(OpObject* self, void* closure) {
+  return PyBool_FromLong(FD_OP_TYPE(&self->inst->fdi, self->idx) ==
+                         (uintptr_t)closure);
+}
+static PyObject* Op_getsize(OpObject* self, void* Py_UNUSED(closure)) {
+  return PyLong_FromLong(FD_OP_SIZE(&self->inst->fdi, self->idx));
+}
+static PyObject* Op_getreg(OpObject* self, void* Py_UNUSED(closure)) {
+  if (FD_OP_TYPE(&self->inst->fdi, self->idx) != FD_OT_REG) {
+    PyErr_SetString(PyExc_AttributeError, "reg");
+    return NULL;
+  }
+  RegObject* reg = (RegObject*)PyType_GenericAlloc(&RegType, 0);
+  if (reg) {
+    reg->size = FD_OP_SIZELG(&self->inst->fdi, self->idx);
+    reg->type = FD_OP_REG_TYPE(&self->inst->fdi, self->idx);
+    reg->idx = FD_OP_REG(&self->inst->fdi, self->idx);
+  }
+  return (PyObject*)reg;
+}
+static PyObject* Op_getimm(OpObject* self, void* closure) {
+  FdOpType exp_ty = *(const char*)closure == 'o' ? FD_OT_OFF : FD_OT_IMM;
+  if (FD_OP_TYPE(&self->inst->fdi, self->idx) != exp_ty) {
+    PyErr_SetString(PyExc_AttributeError, (const char*)closure);
+    return NULL;
+  }
+  return PyLong_FromLong(FD_OP_IMM(&self->inst->fdi, self->idx));
+}
+static PyObject* Op_getbase(OpObject* self, void* Py_UNUSED(closure)) {
+  FdOpType ot = FD_OP_TYPE(&self->inst->fdi, self->idx);
+  if (ot != FD_OT_MEM && ot != FD_OT_MEMBCST) {
+    PyErr_SetString(PyExc_AttributeError, "base");
+    return NULL;
+  }
+  if (FD_OP_BASE(&self->inst->fdi, self->idx) == FD_REG_NONE)
+    Py_RETURN_NONE;
+  RegObject* reg = (RegObject*)PyType_GenericAlloc(&RegType, 0);
+  if (reg) {
+    reg->size = FD_ADDRSIZELG(&self->inst->fdi);
+    reg->type = FD_RT_GPL;
+    reg->idx = FD_OP_BASE(&self->inst->fdi, self->idx);
+  }
+  return (PyObject*)reg;
+}
+static PyObject* Op_getindex(OpObject* self, void* Py_UNUSED(closure)) {
+  FdOpType ot = FD_OP_TYPE(&self->inst->fdi, self->idx);
+  if (ot != FD_OT_MEM && ot != FD_OT_MEMBCST) {
+    PyErr_SetString(PyExc_AttributeError, "base");
+    return NULL;
+  }
+  if (FD_OP_INDEX(&self->inst->fdi, self->idx) == FD_REG_NONE)
+    Py_RETURN_NONE;
+  RegObject* reg = (RegObject*)PyType_GenericAlloc(&RegType, 0);
+  if (reg) {
+    // TODO: VSIB
+    reg->size = FD_ADDRSIZELG(&self->inst->fdi);
+    reg->type = FD_RT_GPL;
+    reg->idx = FD_OP_INDEX(&self->inst->fdi, self->idx);
+  }
+  return (PyObject*)reg;
+}
+static PyObject* Op_getscale(OpObject* self, void* Py_UNUSED(closure)) {
+  FdOpType ot = FD_OP_TYPE(&self->inst->fdi, self->idx);
+  if ((ot != FD_OT_MEM && ot != FD_OT_MEMBCST) ||
+      FD_OP_INDEX(&self->inst->fdi, self->idx) == FD_REG_NONE) {
+    PyErr_SetString(PyExc_AttributeError, "scale");
+    return NULL;
+  }
+  return PyLong_FromLong(FD_OP_SCALE(&self->inst->fdi, self->idx));
+}
+static PyObject* Op_getdisp(OpObject* self, void* Py_UNUSED(closure)) {
+  FdOpType ot = FD_OP_TYPE(&self->inst->fdi, self->idx);
+  if ((ot != FD_OT_MEM && ot != FD_OT_MEMBCST)) {
+    PyErr_SetString(PyExc_AttributeError, "disp");
+    return NULL;
+  }
+  return PyLong_FromLong(FD_OP_DISP(&self->inst->fdi, self->idx));
+}
+static PyObject* Op_getbcstsz(OpObject* self, void* Py_UNUSED(closure)) {
+  if (FD_OP_TYPE(&self->inst->fdi, self->idx) != FD_OT_MEMBCST) {
+    PyErr_SetString(PyExc_AttributeError, "bcstsz");
+    return NULL;
+  }
+  return PyLong_FromLong(FD_OP_BCSTSZ(&self->inst->fdi, self->idx));
+}
+
+static PyGetSetDef OpType_getset[] = {
+    {"is_reg", (getter)Op_getis_type, NULL, "Whether operand is a register",
+     (void*)FD_OT_REG},
+    {"is_imm", (getter)Op_getis_type, NULL, "Whether operand is an immediate",
+     (void*)FD_OT_IMM},
+    {"is_mem", (getter)Op_getis_type, NULL,
+     "Whether operand refers to a memory location", (void*)FD_OT_MEM},
+    {"is_off", (getter)Op_getis_type, NULL,
+     "Whether operand is relative offset", (void*)FD_OT_OFF},
+    {"is_membcst", (getter)Op_getis_type, NULL,
+     "Whether operand refers to a broadcasted memory location",
+     (void*)FD_OT_MEMBCST},
+    {"size", (getter)Op_getsize, NULL, "Operand size (in bytes, or zero)",
+     NULL},
+    {"reg", (getter)Op_getreg, NULL, "Register value (for register operands)",
+     NULL},
+    {"imm", (getter)Op_getimm, NULL,
+     "Immediate operand (for immediate operands)", (void*)"imm"},
+    {"off", (getter)Op_getimm, NULL, "Offset operand (for offset operands)",
+     (void*)"off"},
+    {"base", (getter)Op_getbase, NULL, "Base register (for memory operands)",
+     NULL},
+    {"index", (getter)Op_getindex, NULL, "Index register (for memory operands)",
+     NULL},
+    {"scale", (getter)Op_getscale, NULL, "Scale (for memory operands)", NULL},
+    {"disp", (getter)Op_getdisp, NULL, "Displacement (for memory operands)",
+     NULL},
+    {"bcstsz", (getter)Op_getbcstsz, NULL, "Broadcast size (in bytes)", NULL},
+    {0}};
+
+static PyTypeObject OpType = {
+    PyVarObject_HEAD_INIT(NULL, 0)
+    .tp_name = "fadec.Op",
+    .tp_doc = "An instruction operand",
+    .tp_basicsize = sizeof(OpObject),
+    .tp_itemsize = 0,
+    .tp_dealloc = Op_dealloc,
+    .tp_flags = Py_TPFLAGS_DEFAULT,
+    .tp_repr = (reprfunc)Op_repr,
+    .tp_getset = OpType_getset,
+};
+
+static PyObject* Inst_str(InstObject* self) {
+  char buf[128];
+  fd_format(&self->fdi, buf, sizeof buf);
+  return Py_BuildValue("s", buf);
+}
+
+static PyObject* Inst_repr(InstObject* self) {
+  char buf[128];
+  fd_format(&self->fdi, buf, sizeof buf);
+  char repr[256];
+  sprintf(repr, "<fadec.Inst \"%s\",size=%u>", buf, FD_SIZE(&self->fdi));
+  return Py_BuildValue("s", repr);
+}
+
+static PyObject* Inst_gettype(InstObject* self, void* Py_UNUSED(closure)) {
+  return PyLong_FromLong(FD_TYPE(&self->fdi));
+}
+
+static PyObject* Inst_getsize(InstObject* self, void* Py_UNUSED(closure)) {
+  return PyLong_FromLong(FD_SIZE(&self->fdi));
+}
+
+static PyObject* Inst_getsegment(InstObject* self, void* Py_UNUSED(closure)) {
+  return Reg_AsLong(Reg_FromPair(FD_SEGMENT(&self->fdi), FD_RT_SEG));
+}
+
+static PyObject* Inst_getopsize(InstObject* self, void* Py_UNUSED(closure)) {
+  return PyLong_FromLong(FD_OPSIZE(&self->fdi));
+}
+
+static PyObject* Inst_getaddrsize(InstObject* self, void* Py_UNUSED(closure)) {
+  return PyLong_FromLong(FD_ADDRSIZE(&self->fdi));
+}
+
+static PyObject* Inst_gethas_rep(InstObject* self, void* Py_UNUSED(closure)) {
+  return PyBool_FromLong(FD_HAS_REP(&self->fdi));
+}
+
+static PyObject* Inst_gethas_repnz(InstObject* self, void* Py_UNUSED(closure)) {
+  return PyBool_FromLong(FD_HAS_REPNZ(&self->fdi));
+}
+
+static PyObject* Inst_gethas_lock(InstObject* self, void* Py_UNUSED(closure)) {
+  return PyBool_FromLong(FD_HAS_LOCK(&self->fdi));
+}
+
+static PyObject* Inst_gethas_3e(InstObject* self, void* Py_UNUSED(closure)) {
+  return PyBool_FromLong(FD_HAS_3E(&self->fdi));
+}
+
+static PyObject* Inst_getmaskreg(InstObject* self, void* Py_UNUSED(closure)) {
+  unsigned maskreg =
+      FD_MASKREG(&self->fdi) ? FD_MASKREG(&self->fdi) : FD_REG_NONE;
+  return Reg_AsLong(Reg_FromPair(maskreg, FD_RT_MASK));
+}
+
+static PyObject* Inst_getmaskzero(InstObject* self, void* Py_UNUSED(closure)) {
+  if (FD_MASKREG(&self->fdi) == 0) {
+    PyErr_SetString(PyExc_AttributeError, "maskzero");
+    return NULL;
+  }
+  return PyBool_FromLong(FD_MASKZERO(&self->fdi));
+}
+
+static PyObject* Inst_getops(InstObject* self, void* Py_UNUSED(closure)) {
+  const FdInstr* fdi = &self->fdi;
+  unsigned num_ops = 0;
+  for (; num_ops < 4; num_ops++)
+    if (FD_OP_TYPE(fdi, num_ops) == FD_OT_NONE)
+      break;
+
+  PyObject* res = PyTuple_New(num_ops);
+  for (unsigned idx = 0; idx < num_ops; idx++) {
+    OpObject* op = (OpObject*)PyType_GenericAlloc(&OpType, 0);
+    if (op == NULL) {
+      Py_DECREF(res);
+      return NULL;
+    }
+    Py_INCREF(self);
+    op->inst = self;
+    op->idx = idx;
+    PyTuple_SetItem(res, idx, (PyObject*)op);
+  }
+  return res;
+}
+
+static PyObject* Inst_getroundcontrol(InstObject* self,
+                                      void* Py_UNUSED(closure)) {
+  return PyLong_FromLong(FD_ROUNDCONTROL(&self->fdi));
+}
+
+static PyGetSetDef InstType_getset[] = {
+    {"type", (getter)Inst_gettype, NULL,
+     "The type/mnemonic of the instruction.", NULL},
+    {"size", (getter)Inst_getsize, NULL,
+     "The size of the instruction in bytes.", NULL},
+    {"segment", (getter)Inst_getsegment, NULL,
+     "The specified segment override register, or None.", NULL},
+    {"addrsize", (getter)Inst_getaddrsize, NULL,
+     "The address size attribute of the instruction in bytes.", NULL},
+    {"opsize", (getter)Inst_getopsize, NULL,
+     "The operation width in bytes of the instruction if this is not encoded "
+     "in the operands, for example for the string instruction (e.g. MOVS).",
+     NULL},
+    {"has_rep", (getter)Inst_gethas_rep, NULL,
+     "Whether the instruction was encoded with a REP prefix. Needed for: "
+     "(1) Handling the instructions MOVS, STOS, LODS, INS and OUTS properly. "
+     "(2) Handling the instructions SCAS and CMPS, for which this means REPZ.",
+     NULL},
+    {"has_repnz", (getter)Inst_gethas_repnz, NULL,
+     "Whether the instruction was encoded with a REPNZ prefix.", NULL},
+    {"has_lock", (getter)Inst_gethas_lock, NULL,
+     "Whether the instruction was encoded with a LOCK prefix.", NULL},
+    {"has_3e", (getter)Inst_gethas_3e, NULL,
+     "whether there is a meaningful 3E prefix used for indirect JMP (notrack "
+     "prefix) and conditional branches (hint-taken prefix).",
+     NULL},
+    {"maskreg", (getter)Inst_getmaskreg, NULL,
+     "The opmask register for EVEX-encoded instructions; None for no mask.",
+     NULL},
+    {"maskzero", (getter)Inst_getmaskzero, NULL,
+     "Whether zero masking shall be used.", NULL},
+    {"roundcontrol", (getter)Inst_getroundcontrol, NULL,
+     "The rounding mode for EVEX-encoded instructions.", NULL},
+    {"ops", (getter)Inst_getops, NULL, "The instruction operands.", NULL},
+    {0}};
+
+static PyTypeObject InstType = {
+    PyVarObject_HEAD_INIT(NULL, 0)
+    .tp_name = "fadec.Inst",
+    .tp_doc = "A decoded instruction",
+    .tp_basicsize = sizeof(InstObject),
+    .tp_itemsize = 0,
+    .tp_flags = Py_TPFLAGS_DEFAULT,
+    .tp_str = (reprfunc)Inst_str,
+    .tp_repr = (reprfunc)Inst_repr,
+    .tp_getset = InstType_getset,
+};
+
+static PyObject* decode_generic(PyObject* Py_UNUSED(self), PyObject* args,
+                                int mode) {
+  Py_buffer buf;
+  if (!PyArg_ParseTuple(args, "y*", &buf))
+    return NULL;
+
+  InstObject* inst = (InstObject*)PyType_GenericAlloc(&InstType, 0);
+  if (inst != NULL) {
+    int res = fd_decode(buf.buf, buf.len, mode, 0, &inst->fdi);
+    if (res < 0) {
+      PyErr_SetString(DecodeError, "Decoding failed");
+      Py_DECREF(inst);
+      PyBuffer_Release(&buf);
+      return NULL;
+    }
+  }
+
+  PyBuffer_Release(&buf);
+  return (PyObject*)inst;
+}
+
+static PyObject* decode32(PyObject* self, PyObject* args) {
+  return decode_generic(self, args, 32);
+}
+
+static PyObject* decode64(PyObject* self, PyObject* args) {
+  return decode_generic(self, args, 64);
+}
+
+static PyMethodDef FadecMethods[] = {
+    {"decode32", decode32, METH_VARARGS, "Decode for 32-bit mode."},
+    {"decode64", decode64, METH_VARARGS, "Decode for 64-bit mode."},
+    {0}};
+
+static struct PyModuleDef fadecmodule = {
+    PyModuleDef_HEAD_INIT,
+    "fadec",
+    "Fadec: fast decoder/encoder for x86(-64)",
+    -1,
+    FadecMethods,
+};
+
+static int add_const(PyObject* dict, const char* name, unsigned value) {
+  PyObject* val = PyLong_FromLong(value);
+  if (val == NULL)
+    return -1;
+  if (PyDict_SetItemString(dict, name, val) < 0)
+    return -1;
+  Py_DECREF(val);
+  return 0;
+}
+
+static PyObject* create_reg_dict(void) {
+  // TODO: reduce code size
+  PyObject* reg_dict = PyDict_New();
+#define REG_TY(ty)                                                             \
+  if (add_const(reg_dict, #ty, FD_RT_##ty) < 0)                                \
+    goto fail;
+  REG_TY(VEC)
+  REG_TY(GPL)
+  REG_TY(GPH)
+  REG_TY(SEG)
+  REG_TY(FPU)
+  REG_TY(MMX)
+  REG_TY(TMM)
+  REG_TY(MASK)
+  REG_TY(BND)
+  REG_TY(CR)
+  REG_TY(DR)
+#undef REG_TY
+  return reg_dict;
+
+fail:
+  Py_DECREF(reg_dict);
+  return NULL;
+}
+
+static PyObject* create_inst_dict(void) {
+  // TODO: reduce code size
+  PyObject* inst_dict = PyDict_New();
+#define FD_MNEMONIC(name, value)                                               \
+  if (add_const(inst_dict, #name, value) < 0)                                  \
+    goto fail;
+#include <fadec-decode-public.inc>
+#undef FD_MNEMONIC
+  return inst_dict;
+
+fail:
+  Py_DECREF(inst_dict);
+  return NULL;
+}
+
+PyMODINIT_FUNC PyInit_fadec(void) {
+  if (PyType_Ready(&OpType) < 0)
+    return NULL;
+
+  Py_XDECREF(RegType.tp_dict);
+  RegType.tp_dict = create_reg_dict();
+  if (!RegType.tp_dict)
+    return NULL;
+  if (PyType_Ready(&RegType) < 0) {
+    Py_CLEAR(RegType.tp_dict);
+    return NULL;
+  }
+
+  Py_XDECREF(InstType.tp_dict);
+  InstType.tp_dict = create_inst_dict();
+  if (!InstType.tp_dict)
+    return NULL;
+  if (PyType_Ready(&InstType) < 0) {
+    Py_CLEAR(InstType.tp_dict);
+    return NULL;
+  }
+
+  PyObject* m = PyModule_Create(&fadecmodule);
+  if (m == NULL)
+    return NULL;
+
+  Py_INCREF(&InstType);
+  if (PyModule_AddObject(m, "Inst", (PyObject*)&InstType) < 0) {
+    Py_DECREF(&InstType);
+    Py_DECREF(m);
+    return NULL;
+  }
+
+  Py_INCREF(&OpType);
+  if (PyModule_AddObject(m, "Op", (PyObject*)&OpType) < 0) {
+    Py_DECREF(&OpType);
+    Py_DECREF(m);
+    return NULL;
+  }
+
+  Py_INCREF(&RegType);
+  if (PyModule_AddObject(m, "Reg", (PyObject*)&RegType) < 0) {
+    Py_DECREF(&RegType);
+    Py_DECREF(m);
+    return NULL;
+  }
+
+  DecodeError = PyErr_NewException("fadec.DecodeError", NULL, NULL);
+  if (DecodeError == NULL) {
+    Py_DECREF(m);
+    return NULL;
+  }
+  Py_XINCREF(DecodeError);
+  if (PyModule_AddObject(m, "DecodeError", DecodeError) < 0) {
+    Py_XDECREF(DecodeError);
+    Py_CLEAR(DecodeError);
+    Py_DECREF(m);
+    return NULL;
+  }
+
+  return m;
+}