/*
 * Copyright 2013-2025 The py-lmdb authors, all rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted only as authorized by the OpenLDAP
 * Public License.
 *
 * A copy of this license is available in the file LICENSE in the
 * top-level directory of the distribution or, alternatively, at
 * <http://www.OpenLDAP.org/license.html>.
 *
 * OpenLDAP is a registered trademark of the OpenLDAP Foundation.
 *
 * Individual files and/or contributed packages may be copyright by
 * other parties and/or subject to additional restrictions.
 *
 * This work also contains materials derived from public sources.
 *
 * Additional information about OpenLDAP can be obtained at
 * <http://www.openldap.org/>.
 */

#define PY_SSIZE_T_CLEAN

/* Include order matters! */
#include "Python.h"

/* Search lib/win32 first, then fallthrough to <stdint.h> as required.*/
#include "stdint.h"

#include <errno.h>
#include <stdarg.h>
#include <string.h>

#ifdef _WIN32
#   define bool int
#   define true 1
#   define false 0
#else
#   include <stdbool.h>
#endif

#include <sys/stat.h>

#include "structmember.h"

#ifdef HAVE_MEMSINK
#define USING_MEMSINK
#include "memsink.h"
#endif

#ifdef _WIN32
#include <windows.h> /* HANDLE */
#endif

#ifndef _WIN32
#include <pthread.h>
#endif

/* Sequentially-consistent atomic counter primitives.  INCR/DECR return the
 * new value.  seq_cst keeps the waiter-registration protocol (see
 * ACTIVE_OPS_DEC / ENV_WAIT_WHILE) simple to reason about, and costs the
 * same as acq_rel on x86 for the RMW operations. */
#ifdef _WIN32
typedef volatile LONG lmdb_atomic_t;
#define LMDB_ATOMIC_INCR(p) InterlockedIncrement(p)
#define LMDB_ATOMIC_DECR(p) InterlockedDecrement(p)
#define LMDB_ATOMIC_LOAD(p) InterlockedCompareExchange((p), 0, 0)
#else
typedef int lmdb_atomic_t;
#define LMDB_ATOMIC_INCR(p) __atomic_add_fetch((p), 1, __ATOMIC_SEQ_CST)
#define LMDB_ATOMIC_DECR(p) __atomic_sub_fetch((p), 1, __ATOMIC_SEQ_CST)
#define LMDB_ATOMIC_LOAD(p) __atomic_load_n((p), __ATOMIC_SEQ_CST)
#endif

#include "lmdb.h"
#include "preload.h"


/* Comment out for copious debug. */
#define NODEBUG

#ifdef NODEBUG
#   define DEBUG(s, ...)
#else
#   define DEBUG(s, ...) fprintf(stderr, \
    "lmdb.cpython: %s:%d: " s "\n", __func__, __LINE__, ## __VA_ARGS__);
#endif

#define MDEBUG(s, ...) DEBUG("%p: " s, self, ## __VA_ARGS__);


/* Inlining control for compatible compilers. */
#if (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 4))
#   define NOINLINE __attribute__((noinline))
#else
#   define NOINLINE
#endif


/* Overflow-safe size_t arithmetic for sizes derived from on-disk values.
 * LMDB files are routinely passed between hosts; any field read out of one
 * (notably MDB_val::mv_size) must be assumed attacker-controlled. */
#define SIZE_ADD_OVERFLOW(a, b) ((b) > SIZE_MAX - (a))
#define SIZE_MUL_OVERFLOW(a, b) ((a) != 0 && (b) > SIZE_MAX / (a))


/**
 * On Win32, Environment.copyfd() needs _get_osfmodule() from the C library,
 * except that function performs no input validation. So instead we import
 * msvcrt standard library module, which wraps _get_osfmodule() in a way that
 * is crash-safe.
 */
#ifdef _WIN32
static PyObject *msvcrt;
#endif


/** PyLong representing integer 0. */
static PyObject *py_zero;
/** PyLong representing INT_MAX. */
static PyObject *py_int_max;
/** PyLong representing SIZE_MAX. */
static PyObject *py_size_max;
/** lmdb.Error type. */
static PyObject *Error;
/** Global set of canonical paths for open environments. */
static PyObject *open_env_paths;
/** Cached process ID, updated after fork via pthread_atfork. */
static pid_t _cached_pid;

/** Typedefs and forward declarations. */
static PyTypeObject PyDatabase_Type;
static PyTypeObject PyEnvironment_Type;
static PyTypeObject PyTransaction_Type;
static PyTypeObject PyCursor_Type;
static PyTypeObject PyIterator_Type;

typedef struct CursorObject CursorObject;
typedef struct DbObject DbObject;
typedef struct EnvObject EnvObject;
typedef struct IterObject IterObject;
typedef struct TransObject TransObject;


#   define MOD_RETURN(mod) return mod;
#   define MODINIT_NAME PyInit_cpython

#   define MAKE_ID(id) PyCapsule_New((void *) (1 + (id)), NULL, NULL)
#   define READ_ID(obj) (((int) (long) PyCapsule_GetPointer(obj, NULL)) - 1)

struct list_head {
    struct lmdb_object *prev;
    struct lmdb_object *next;
};

#define LmdbObject_HEAD \
    PyObject_HEAD \
    struct list_head siblings; \
    struct list_head children; \
    int valid;

struct lmdb_object {
    LmdbObject_HEAD
};

#define OBJECT_INIT(o) \
    ((struct lmdb_object *)o)->siblings.prev = NULL; \
    ((struct lmdb_object *)o)->siblings.next = NULL; \
    ((struct lmdb_object *)o)->children.prev = NULL; \
    ((struct lmdb_object *)o)->children.next = NULL; \
    ((struct lmdb_object *)o)->valid = 1;


/** lmdb._Database */
struct DbObject {
    LmdbObject_HEAD
    /** Python Environment reference. Not refcounted; when the last strong ref
     * to Environment is released, DbObject.tp_clear() will be called, causing
     * DbObject.env and DbObject.dbi to be cleared. This is to prevent a
     * cyclical reference from DB->Env keeping the environment alive. */
    struct EnvObject *env;
    /** MDB database handle. */
    MDB_dbi dbi;
    /** Flags at time of creation. */
    unsigned int flags;
};

/** lmdb.Environment */
struct EnvObject {
    LmdbObject_HEAD
    /** Python-managed list of weakrefs to this object. */
    PyObject *weaklist;
    /** MDB environment object. */
    MDB_env *env;
    /** DBI for main database, opened during Environment construction. */
    DbObject *main_db;
    /**  1 if env opened read-only; transactions must always be read-only. */
    int readonly;
    /** Spare read-only transaction. */
    struct MDB_txn *spare_txn;
    /** Maximum number of spare transactions.  In cpython only 0 and 1 are supported.
     *  If process will be forked, this must be set to 0. */
    int max_spare_txns;
    /** Process ID of the process this Environment was opened in. */
    pid_t pid;
    /** Thread ID of the thread holding the write transaction, or 0. */
    unsigned long write_txn_tid;
    /** Resolved path used to track this env in open_env_paths. */
    PyObject *open_path;
    /** Count of in-flight LMDB operations (GIL released).  env_clear and
     *  set_mapsize wait for this to reach 0 before closing/remapping the
     *  env.  Modified with atomics (LMDB_ATOMIC_*) — the inc/dec is on
     *  the per-operation hot path, so it must not take ops_mutex.
     *  Issue #180. */
    lmdb_atomic_t active_ops;
    /** Count of threads inside ENV_WAIT_WHILE.  Registered under
     *  ops_mutex before the predicate check; read lock-free by
     *  ACTIVE_OPS_DEC to skip the broadcast when nobody is draining
     *  (the common case). */
    lmdb_atomic_t ops_waiters;
    /** 1 while set_mapsize() is inside its invalidate/remap critical
     *  section.  New GIL-releasing LMDB operations fail fast with EINVAL
     *  until the resize completes (see ENV_RESIZE_BLOCKED).  Issue #475. */
    int resizing;
    /** Thread ID of the thread performing the resize, so same-thread
     *  re-entry (e.g. a destructor running during invalidation, while the
     *  old map is still valid) passes through instead of failing. */
    unsigned long resize_tid;
    /** 1 once ops_mutex/ops_cond below are initialized (guards teardown
     *  on env_new error paths). */
    int ops_sync_ready;
    /** Mutex + condition variable guarding writes to active_ops,
     *  write_txn_tid and resizing, and waking their waiters: env_clear's
     *  operation drain, its issue-#465 write-txn wait and its wait for a
     *  concurrent resize, trans_abort's child-operation wait, and
     *  set_mapsize's drains.  A condition variable (rather than the
     *  previous single-slot PyThread lock handoff) supports multiple
     *  concurrent waiters: e.g. a write-transaction abort and a
     *  set_mapsize can drain simultaneously without a lost wakeup.
     *  Broadcasts happen when active_ops reaches 0, when write_txn_tid
     *  clears, and when a resize completes.  All writes to the guarded
     *  fields also happen with the GIL held, so GIL-holding readers need
     *  not take the mutex.  Issues #180, #465, #475. */
#ifdef _WIN32
    CRITICAL_SECTION ops_mutex;
    CONDITION_VARIABLE ops_cond;
#else
    pthread_mutex_t ops_mutex;
    pthread_cond_t ops_cond;
#endif
};

/** TransObject.flags bitfield values. */
enum trans_flags {
    /** Buffers should be yielded by get. */
    TRANS_BUFFERS       = 1,
    /** Transaction can be can go on freelist instead of deallocation. */
    TRANS_RDONLY        = 2,
    /** Transaction is spare, ready for mdb_txn_renew() */
    TRANS_SPARE         = 4
};

/** lmdb.Transaction */
struct TransObject {
    LmdbObject_HEAD
    /** Python-managed list of weakrefs to this object. */
    PyObject *weaklist;
    EnvObject *env;
#ifdef HAVE_MEMSINK
    /** Copy-on-invalid list head. */
    PyObject *sink_head;
#endif
    /** MDB transaction object. */
    MDB_txn *txn;
    /** Bitfield of trans_flags values. */
    int flags;
    /** Default database if none specified. */
    DbObject *db;
    /** Number of mutations occurred since start of transaction. Required to
     * know when cursor key/value must be refreshed. */
    int mutations;
};

/** lmdb.Cursor */
struct CursorObject {
    LmdbObject_HEAD
    /** Transaction cursor belongs to. */
    TransObject *trans;
    /** 1 if mdb_cursor_get() has been called and it last returned 0. */
    int positioned;
    /** MDB-level cursor object. */
    MDB_cursor *curs;
    /** mv_size==0 if positioned==0, otherwise points to current key. */
    MDB_val key;
    /** mv_size==0 if positioned==0, otherwise points to current value. */
    MDB_val val;
    /** If TransObject.mutations!=last_mutation, must MDB_GET_CURRENT to
     * refresh `key' and `val'. */
    int last_mutation;
    /** DBI flags at time of creation. */
    unsigned int dbi_flags;
};


typedef PyObject *(*IterValFunc)(CursorObject *);

/** lmdb.Iterator
 *
 * This is separate from Cursor since we want to define Cursor.next() to mean
 * MDB_NEXT, and a Python iterator's next() has different semantics.
 */
struct IterObject {
    PyObject_HEAD
    /** Cursor being iterated, or NULL for freelist iterator. */
    CursorObject *curs;
    /** 1 if iteration has started (Cursor should advance on next()). */
    int started;
    /** Operation used to advance cursor. */
    MDB_cursor_op op;
    /** Iterator value function, should be item(), key(), or value(). */
    IterValFunc val_func;
};


/**
 * Link `child` into `parent`'s list of dependent objects. Use LINK_CHILD()
 * maro to avoid casting PyObject to lmdb_object.
 */
static void link_child(struct lmdb_object *parent, struct lmdb_object *child)
{
    struct lmdb_object *sibling = parent->children.next;
    if(sibling) {
        child->siblings.next = sibling;
        sibling->siblings.prev = child;
    }
    parent->children.next = child;
}

#define LINK_CHILD(parent, child) link_child((void *)parent, (void *)child);


/**
 * Remove `child` from `parent`'s list of dependent objects. Use UNLINK_CHILD
 * macro to avoid casting PyObject to lmdb_object.
 */
static void unlink_child(struct lmdb_object *parent, struct lmdb_object *child)
{
    if(parent) {
        struct lmdb_object *prev = child->siblings.prev;
        struct lmdb_object *next = child->siblings.next;
        if(prev) {
            prev->siblings.next = next;
                 /* If double unlink_child(), this test my legitimately fail: */
        } else if(parent->children.next == child) {
            parent->children.next = next;
        }
        if(next) {
            next->siblings.prev = prev;
        }
        child->siblings.prev = NULL;
        child->siblings.next = NULL;
    }
}

#define UNLINK_CHILD(parent, child) unlink_child((void *)parent, (void *)child);


/**
 * Notify dependents of `parent` that `parent` is about to become invalid,
 * and that they should free any dependent resources.
 *
 * To save effort, tp_clear is overloaded to be the invalidation function,
 * instead of carrying a separate pointer. Objects are added to their parent's
 * list during construction and removed during deallocation.
 *
 * When the environment is closed, it walks its list calling tp_clear on each
 * child, which in turn walk their own lists. Child transactions are added to
 * their parent transaction's list. Iterators keep no significant state, so
 * they are not tracked.
 *
 * Use INVALIDATE() macro to avoid casting PyObject to lmdb_object.
 */
static void invalidate(struct lmdb_object *parent)
{
    struct lmdb_object *child = parent->children.next;
    PyObject *held_ref = NULL;
    while(child) {
        struct lmdb_object *next = child->siblings.next;
        /* tp_clear may release the GIL (e.g. during txn_abort), allowing
         * another thread to Py_DECREF both `child` and `next` to zero.
         * Hold temporary references to prevent use-after-free. */
        Py_XINCREF((PyObject *) next);
        Py_INCREF((PyObject *) child);
        DEBUG("invalidating parent=%p child %p", parent, child)
        Py_TYPE(child)->tp_clear((PyObject *) child);
        Py_DECREF((PyObject *) child);
        Py_XDECREF(held_ref);
        held_ref = (PyObject *) next;
        child = next;
    }
    Py_XDECREF(held_ref);
}

/**
 * Quickly mark `parent` and all its descendants as invalid (valid=0).
 * This is fast (no I/O, no GIL release) and prevents new operations from
 * starting on any descendant.  Must be called before the slower invalidate()
 * which does actual cleanup.  Issue #180.
 */
static void invalidate_mark(struct lmdb_object *parent)
{
    struct lmdb_object *child = parent->children.next;
    while(child) {
        child->valid = 0;
        invalidate_mark(child);
        child = child->siblings.next;
    }
}

#define INVALIDATE(parent) invalidate((void *)parent);
#define INVALIDATE_MARK(parent) invalidate_mark((void *)parent);

/**
 * Like invalidate()/invalidate_mark(), but restricted to transaction children
 * (and, recursively, their cursors).  Named- and main-database handles
 * (DbObject) are also direct children of the environment, but they hold only
 * an integer MDB_dbi that stays valid across mdb_env_set_mapsize's
 * munmap/mmap.  Only transactions and cursors hold pointers into the old
 * mapping, so only they must be invalidated on resize.  Invalidating a
 * database handle would set db->env = NULL and break the caller's still-valid
 * handle.  Issue #475.
 */
static void invalidate_mark_txns(struct lmdb_object *parent)
{
    struct lmdb_object *child = parent->children.next;
    while(child) {
        if(Py_TYPE(child) == &PyTransaction_Type) {
            child->valid = 0;
            invalidate_mark(child);
        }
        child = child->siblings.next;
    }
}

static void invalidate_txns(struct lmdb_object *parent)
{
    struct lmdb_object *child = parent->children.next;
    PyObject *held_ref = NULL;
    while(child) {
        struct lmdb_object *next = child->siblings.next;
        /* tp_clear may release the GIL, allowing another thread to
         * Py_DECREF both `child` and `next` to zero.  Hold temporary
         * references to prevent use-after-free (see invalidate()). */
        Py_XINCREF((PyObject *) next);
        Py_INCREF((PyObject *) child);
        if(Py_TYPE(child) == &PyTransaction_Type) {
            Py_TYPE(child)->tp_clear((PyObject *) child);
        }
        Py_DECREF((PyObject *) child);
        Py_XDECREF(held_ref);
        held_ref = (PyObject *) next;
        child = next;
    }
    Py_XDECREF(held_ref);
}

#define INVALIDATE_TXNS(parent) invalidate_txns((void *)parent);
#define INVALIDATE_MARK_TXNS(parent) invalidate_mark_txns((void *)parent);


/* ---------- */
/* Exceptions */
/* ---------- */

struct error_map {
    int code;
    const char *name;
};

/** Array of Error subclasses corresponding to `error_map'. */
static PyObject **error_tbl;
/** Mapping from LMDB error code to py-lmdb exception class. */
static const struct error_map error_map[] = {
    {MDB_KEYEXIST, "KeyExistsError"},
    {MDB_NOTFOUND, "NotFoundError"},
    {MDB_PAGE_NOTFOUND, "PageNotFoundError"},
    {MDB_CORRUPTED, "CorruptedError"},
    {MDB_PANIC, "PanicError"},
    {MDB_VERSION_MISMATCH, "VersionMismatchError"},
    {MDB_INVALID, "InvalidError"},
    {MDB_MAP_FULL, "MapFullError"},
    {MDB_DBS_FULL, "DbsFullError"},
    {MDB_READERS_FULL, "ReadersFullError"},
    {MDB_TLS_FULL, "TlsFullError"},
    {MDB_TXN_FULL, "TxnFullError"},
    {MDB_CURSOR_FULL, "CursorFullError"},
    {MDB_PAGE_FULL, "PageFullError"},
    {MDB_MAP_RESIZED, "MapResizedError"},
    {MDB_INCOMPATIBLE, "IncompatibleError"},
    {MDB_BAD_RSLOT, "BadRslotError"},
    {MDB_BAD_DBI, "BadDbiError"},
    {MDB_BAD_TXN, "BadTxnError"},
    {MDB_BAD_VALSIZE, "BadValsizeError"},
    {EACCES, "ReadonlyError"},
    {EINVAL, "InvalidParameterError"},
    {EAGAIN, "LockError"},
    {ENOMEM, "MemoryError"},
    {ENOSPC, "DiskError"}
};


/* ---------- */
/* Exceptions */
/* ---------- */

/**
 * Raise an exception appropriate for the given `rc` MDB error code.
 */
static void * NOINLINE
err_set(const char *what, int rc)
{
    size_t count = sizeof error_map / sizeof error_map[0];
    PyObject *klass = Error;
    size_t i;

    if(rc) {
        for(i = 0; i < count; i++) {
            if(error_map[i].code == rc) {
                klass = error_tbl[i];
                break;
            }
        }
    }

    PyErr_Format(klass, "%s: %s", what, mdb_strerror(rc));
    return NULL;
}

/**
 * Raise an exception from a format string.
 */
static void * NOINLINE
err_format(int rc, const char *fmt, ...)
{
    char buf[128];
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(buf, sizeof buf, fmt, ap);
    buf[sizeof buf - 1] = '\0';
    va_end(ap);
    return err_set(buf, rc);
}

static void * NOINLINE
err_invalid(void)
{
    PyErr_Format(Error, "Attempt to operate on closed/deleted/dropped object.");
    return NULL;
}

static void * NOINLINE
type_error(const char *what)
{
    PyErr_Format(PyExc_TypeError, "%s", what);
    return NULL;
}

/**
 * Convert a PyObject to filesystem bytes. Must call fspath_fini() when done.
 * Return 0 on success, or set an exception and return -1 on failure.
 */
static PyObject *
get_fspath(PyObject *src)
{
    if(PyBytes_CheckExact(src)) {
        Py_INCREF(src);
        return src;
    }
    if(! PyUnicode_CheckExact(src)) {
        type_error("Filesystem path must be Unicode or bytes.");
        return NULL;
    }
    return PyUnicode_AsEncodedString(src, Py_FileSystemDefaultEncoding,
                                     "strict");
}

/* ------- */
/* Helpers */
/* ------- */

/**
 * Describes the type of a struct field.
 */
enum field_type {
    /** Last field in set, stop converting. */
    TYPE_EOF,
    /** Unsigned 32bit integer. */
    TYPE_UINT,
    /** size_t */
    TYPE_SIZE,
    /** void pointer */
    TYPE_ADDR
};

/**
 * Describes a struct field.
 */
struct dict_field {
    /** Field type. */
    enum field_type type;
    /** Field name in target dict. */
    const char *name;
    /* Offset into structure where field is found. */
    int offset;
};

/**
 * Return a new reference to Py_True if the given argument is true, otherwise
 * a new reference to Py_False.
 */
static PyObject *
py_bool(int pred)
{
    PyObject *obj = pred ? Py_True : Py_False;
    Py_INCREF(obj);
    return obj;
}

/** Set a boolean value in a dict, handling the reference from py_bool. */
static int
dict_set_bool(PyObject *dict, const char *key, int pred)
{
    PyObject *val = py_bool(pred);
    int rc = PyDict_SetItemString(dict, key, val);
    Py_DECREF(val);
    return rc;
}

/**
 * Convert the structure `o` described by `fields` to a dict and return the new
 * dict.
 */
static PyObject *
dict_from_fields(void *o, const struct dict_field *fields)
{
    PyObject *dict = PyDict_New();
    if(! dict) {
        return NULL;
    }

    while(fields->type != TYPE_EOF) {
        uint8_t *p = ((uint8_t *) o) + fields->offset;
        unsigned PY_LONG_LONG l = 0;
        PyObject *lo;

        if(fields->type == TYPE_UINT) {
            l = *(unsigned int *)p;
        } else if(fields->type == TYPE_SIZE) {
            l = *(size_t *)p;
        } else if(fields->type == TYPE_ADDR) {
            l = (intptr_t) *(void **)p;
        }

        if(! ((lo = PyLong_FromUnsignedLongLong(l)))) {
            Py_DECREF(dict);
            return NULL;
        }

        if(PyDict_SetItemString(dict, fields->name, lo)) {
            Py_DECREF(lo);
            Py_DECREF(dict);
            return NULL;
        }
        Py_DECREF(lo);
        fields++;
    }
    return dict;
}

/**
 * Given an MDB_val `val`, convert it to a Python string or bytes object,
 * depending on the Python version. Returns a new reference to the object on
 * sucess, or NULL on failure.
 */
static PyObject *
obj_from_val(MDB_val *val, int as_buffer)
{
    if(val->mv_size > (size_t) PY_SSIZE_T_MAX) {
        PyErr_SetString(PyExc_OverflowError,
            "value too large to convert (corrupt database?)");
        return NULL;
    }
    if(as_buffer) {
        return PyMemoryView_FromMemory(val->mv_data,
                                       (Py_ssize_t) val->mv_size, PyBUF_READ);
    }
    return PyBytes_FromStringAndSize(val->mv_data,
                                     (Py_ssize_t) val->mv_size);
}

/* Track Py_buffer views acquired during argument parsing so they can be
 * released after the LMDB operation that consumes the data completes.
 * This prevents mutable buffer objects (bytearray, memoryview, etc.) from
 * being resized by another thread while LMDB reads from the pointer. */
#define MAX_BUF_VIEWS 2

typedef struct {
    Py_buffer views[MAX_BUF_VIEWS];
    int count;
} BufViewList;

static void
bufviewlist_init(BufViewList *bvl)
{
    bvl->count = 0;
}

static void
bufviewlist_release(BufViewList *bvl)
{
    int i;
    for(i = 0; i < bvl->count; i++) {
        PyBuffer_Release(&bvl->views[i]);
    }
    bvl->count = 0;
}

/**
 * Given some Python object, try to get at its raw data. For string or bytes
 * objects, this is the object value. For Unicode objects, this is the UTF-8
 * representation of the object value. For all other objects, use the buffer
 * protocol.
 *
 * If `bvl` is non-NULL, the acquired Py_buffer view is kept alive in the
 * BufViewList and the caller must call bufviewlist_release() after the data
 * is consumed. If `bvl` is NULL, the view is released immediately (unsafe
 * for mutable buffers, but matches the legacy behavior).
 */
static int NOINLINE
val_from_buffer(MDB_val *val, PyObject *buf, BufViewList *bvl)
{
    int ret;
    Py_buffer *view;

    if(PyBytes_CheckExact(buf)) {
        val->mv_data = PyBytes_AS_STRING(buf);
        val->mv_size = PyBytes_GET_SIZE(buf);
        return 0;
    }
    if(PyUnicode_CheckExact(buf)) {
        type_error("Won't implicitly convert Unicode to bytes; use .encode()");
        return -1;
    }

#if PY_VERSION_HEX < 0x030d0000
    if(! bvl) {
        return PyObject_AsReadBuffer(buf,
            (const void **) &val->mv_data,
            (Py_ssize_t *) &val->mv_size);
    }
#endif

    if(bvl && bvl->count < MAX_BUF_VIEWS) {
        view = &bvl->views[bvl->count];
        ret = PyObject_GetBuffer(buf, view, PyBUF_SIMPLE);
        if(ret == 0) {
            val->mv_data = view->buf;
            val->mv_size = view->len;
            bvl->count++;
        }
        return ret;
    } else {
        Py_buffer tmp;
        ret = PyObject_GetBuffer(buf, &tmp, PyBUF_SIMPLE);
        if(ret == 0) {
            val->mv_data = tmp.buf;
            val->mv_size = tmp.len;
            PyBuffer_Release(&tmp);
        }
        return ret;
    }
}

/* ------------------- */
/* Concurrency control */
/* ------------------- */

/* UNLOCKED: release GIL for a plain LMDB call (no env protection). */
#define UNLOCKED(out, e) \
    Py_BEGIN_ALLOW_THREADS \
    out = (e); \
    Py_END_ALLOW_THREADS

/* Primitive wrappers for the env's ops_mutex/ops_cond (see EnvObject).
 * The mutex is a leaf lock: held only for brief field updates or inside
 * ENV_WAIT_WHILE — never while acquiring the GIL or calling into LMDB or
 * Python, so no lock-order cycle with the GIL is possible. */
#ifdef _WIN32
#define ENV_SYNC_LOCK(_env)      EnterCriticalSection(&(_env)->ops_mutex)
#define ENV_SYNC_UNLOCK(_env)    LeaveCriticalSection(&(_env)->ops_mutex)
#define ENV_SYNC_WAIT(_env) \
    SleepConditionVariableCS(&(_env)->ops_cond, &(_env)->ops_mutex, INFINITE)
#define ENV_SYNC_BROADCAST(_env) WakeAllConditionVariable(&(_env)->ops_cond)
#else
#define ENV_SYNC_LOCK(_env)      pthread_mutex_lock(&(_env)->ops_mutex)
#define ENV_SYNC_UNLOCK(_env)    pthread_mutex_unlock(&(_env)->ops_mutex)
#define ENV_SYNC_WAIT(_env) \
    pthread_cond_wait(&(_env)->ops_cond, &(_env)->ops_mutex)
#define ENV_SYNC_BROADCAST(_env) pthread_cond_broadcast(&(_env)->ops_cond)
#endif

/* ENV_WAIT_WHILE: block (GIL released) until `cond` becomes false.  `cond`
 * is evaluated under ops_mutex, so it may only reference thread-locals,
 * fields whose writes take the mutex (write_txn_tid, resizing/resize_tid),
 * and atomics read via LMDB_ATOMIC_LOAD (active_ops).  Multiple threads
 * may wait concurrently; every state change wakes them.  The waiter is
 * counted in ops_waiters BEFORE the predicate check: ACTIVE_OPS_DEC reads
 * the count lock-free after its decrement, so (both accesses being
 * seq_cst) either this thread's check observes the count already at 0 and
 * never sleeps, or the decrementer observes our registration and
 * broadcasts — and since the broadcast is taken under ops_mutex, it
 * cannot slip between our check and the wait.  No lost wakeup.  On return
 * the GIL has been reacquired, and GIL-guarded state may have changed
 * meanwhile — callers needing a GIL-stable predicate must re-check and
 * loop.  Issues #180, #465, #475. */
#define ENV_WAIT_WHILE(_env, cond) \
    do { \
        Py_BEGIN_ALLOW_THREADS \
        ENV_SYNC_LOCK(_env); \
        LMDB_ATOMIC_INCR(&(_env)->ops_waiters); \
        while(cond) { \
            ENV_SYNC_WAIT(_env); \
        } \
        LMDB_ATOMIC_DECR(&(_env)->ops_waiters); \
        ENV_SYNC_UNLOCK(_env); \
        Py_END_ALLOW_THREADS \
    } while(0)

/* ACTIVE_OPS_INC/DEC: adjust the in-flight operation count.  This is the
 * per-operation hot path (twice per cursor step), so it is lock-free: a
 * single atomic RMW, plus — only on the 0-crossing — a lock-free read of
 * ops_waiters to decide whether any drainer needs waking.  The mutex is
 * taken only to deliver that (rare) broadcast, so a waiter between its
 * predicate check and its wait cannot miss it.  Issues #180, #475. */
#define ACTIVE_OPS_INC(_env) \
    do { \
        LMDB_ATOMIC_INCR(&(_env)->active_ops); \
    } while(0)

#define ACTIVE_OPS_DEC(_env) \
    do { \
        if(LMDB_ATOMIC_DECR(&(_env)->active_ops) == 0 && \
           LMDB_ATOMIC_LOAD(&(_env)->ops_waiters) != 0) { \
            ENV_SYNC_LOCK(_env); \
            ENV_SYNC_BROADCAST(_env); \
            ENV_SYNC_UNLOCK(_env); \
        } \
    } while(0)

/* CLEAR_WRITE_TXN_TID: record that this thread's write transaction is
 * fully released, waking env_clear's issue-#465 wait and set_mapsize's
 * drain.  MUST be called with the GIL held and only AFTER the write txn's
 * mutex has been released (i.e. after mdb_txn_abort/commit completes), so
 * a waiter does not unmap the lock file, or remap the data file, while
 * the transaction is still live.  Issues #465, #475. */
#define CLEAR_WRITE_TXN_TID(_env) \
    do { \
        ENV_SYNC_LOCK(_env); \
        (_env)->write_txn_tid = 0; \
        ENV_SYNC_BROADCAST(_env); \
        ENV_SYNC_UNLOCK(_env); \
    } while(0)

/* SET_WRITE_TXN_TID: record this thread as the write-txn owner.  Must be
 * called with the GIL held; mutex-protected for ENV_WAIT_WHILE readers. */
#define SET_WRITE_TXN_TID(_env) \
    do { \
        ENV_SYNC_LOCK(_env); \
        (_env)->write_txn_tid = (unsigned long) PyThread_get_thread_ident(); \
        ENV_SYNC_UNLOCK(_env); \
    } while(0)

/* ENV_UNLOCKED: release GIL for an LMDB call, holding the env's active_ops
 * counter to prevent env_clear from closing the env underneath us.  The
 * counter is incremented while the GIL is still held (so env_clear can't
 * start between our valid-check and the increment), and decremented after
 * we reacquire the GIL.  The _env expression is evaluated once while the
 * GIL is held and saved to a local, so pointer chains like
 * self->trans->env remain valid for ACTIVE_OPS_DEC even if another thread
 * NULLs an intermediate pointer during the GIL release.  Issue #180. */

/* ENV_RESIZE_BLOCKED: true when another thread is inside set_mapsize()'s
 * invalidate/remap critical section.  A new GIL-releasing LMDB operation
 * must not start then: the remap invalidates in-flight pointers, and any
 * txn/cursor arguments the operation captured were (or are about to be)
 * invalidated by the resize.  The operation fails fast with EINVAL instead
 * of blocking — its handles are dead either way, and failing under the GIL
 * (set_mapsize sets `resizing` under the GIL too) needs no lock protocol.
 * Same-thread re-entry (a destructor invoked during the resize's
 * invalidation phase, while the old map is still valid) passes through.
 * Issue #475. */
#define ENV_RESIZE_BLOCKED(_env) \
    ((_env)->resizing && \
     (_env)->resize_tid != (unsigned long) PyThread_get_thread_ident())

#define ENV_UNLOCKED(_env, out, e) \
    do { \
        EnvObject *_saved_env = (_env); \
        if(ENV_RESIZE_BLOCKED(_saved_env)) { \
            out = EINVAL; \
        } \
        else { \
            ACTIVE_OPS_INC(_saved_env); \
            Py_BEGIN_ALLOW_THREADS \
            out = (e); \
            Py_END_ALLOW_THREADS \
            ACTIVE_OPS_DEC(_saved_env); \
        } \
    } while(0)

#define PRELOAD_UNLOCKED(_rc, _data, _size) \
    Py_BEGIN_ALLOW_THREADS \
    preload(_rc, _data, _size); \
    Py_END_ALLOW_THREADS

/* ENV_PRELOAD_UNLOCKED: preload faults data pages in with the GIL released.
 * If a resize is pending, skip it WITHOUT releasing the GIL: _data points
 * into the current map, and the caller's subsequent GIL-held copies from it
 * are only safe as long as the resize thread (which needs the GIL for its
 * remap) cannot run.  preload is purely an optimization, so skipping is
 * always correct.  Issue #475. */
#define ENV_PRELOAD_UNLOCKED(_env, _rc, _data, _size) \
    do { \
        EnvObject *_saved_env = (_env); \
        if(! _saved_env->resizing) { \
            ACTIVE_OPS_INC(_saved_env); \
            Py_BEGIN_ALLOW_THREADS \
            preload(_rc, _data, _size); \
            Py_END_ALLOW_THREADS \
            ACTIVE_OPS_DEC(_saved_env); \
        } \
    } while(0)

/* ---------------- */
/* Argument parsing */
/* ---------------- */

#define OFFSET(k, y) offsetof(struct k, y)
#define SPECSIZE() (sizeof(argspec) / sizeof(argspec[0]))
enum arg_type {
    ARG_DB,     /** DbObject*               */
    ARG_TRANS,  /** TransObject*            */
    ARG_ENV,    /** EnvObject*              */
    ARG_OBJ,    /** PyObject*               */
    ARG_BOOL,   /** int                     */
    ARG_BUF,    /** MDB_val                 */
    ARG_STR,    /** char*                   */
    ARG_INT,    /** int                     */
    ARG_SIZE    /** size_t                  */
};
struct argspec {
    const char *string;
    unsigned short type;
    unsigned short offset;
};

static PyTypeObject *type_tbl[] = {
    &PyDatabase_Type,
    &PyTransaction_Type,
    &PyEnvironment_Type
};


static int NOINLINE
parse_ulong(PyObject *obj, uint64_t *l, PyObject *max)
{
    int rc = PyObject_RichCompareBool(obj, py_zero, Py_GE);
    if(rc == -1) {
        return -1;
    } else if(! rc) {
        PyErr_Format(PyExc_OverflowError, "Integer argument must be >= 0");
        return -1;
    }
    rc = PyObject_RichCompareBool(obj, max, Py_LE);
    if(rc == -1) {
        return -1;
    } else if(! rc) {
        PyErr_Format(PyExc_OverflowError, "Integer argument exceeds limit.");
        return -1;
    }
    *l = PyLong_AsUnsignedLongLongMask(obj);
    return 0;
}

/**
 * Parse a single argument specified by `spec` into `out`, returning 0 on
 * success or setting an exception and returning -1 on error.
 *
 * `bvl` is passed through to val_from_buffer() for ARG_BUF args.
 * For ARG_STR, NULL is passed since the string is consumed immediately.
 */
static int
parse_arg(const struct argspec *spec, PyObject *val, void *out,
          BufViewList *bvl)
{
    void *dst = ((uint8_t *)out) + spec->offset;
    int ret = 0;
    uint64_t l;

    if(val != Py_None) {
        switch((enum arg_type) spec->type) {
        case ARG_DB:
        case ARG_TRANS:
        case ARG_ENV:
            if(val->ob_type != type_tbl[spec->type]) {
                type_error("invalid type");
                return -1;
            }
            /* fallthrough */
        case ARG_OBJ:
            *((PyObject **) dst) = val;
            break;
        case ARG_BOOL:
            *((int *)dst) = PyObject_IsTrue(val);
            break;
        case ARG_BUF:
            ret = val_from_buffer((MDB_val *)dst, val, bvl);
            break;
        case ARG_STR: {
            MDB_val mv;
            if(! (ret = val_from_buffer(&mv, val, NULL))) {
                *((char **) dst) = mv.mv_data;
            }
            break;
        }
        case ARG_INT:
            if(! (ret = parse_ulong(val, &l, py_int_max))) {
                *((int *) dst) = (int)l;
            }
            break;
        case ARG_SIZE:
            if(! (ret = parse_ulong(val, &l, py_size_max))) {
                *((size_t *) dst) = (size_t)l;
            }
            break;
        }
    }
    return ret;
}

/**
 * Walk `argspec`, building a Python dictionary mapping keyword arguments to a
 * PyInt describing their offset in the array. Used to reduce keyword argument
 * parsing from O(specsize) to O(number of supplied kwargs).
 */
static int NOINLINE
make_arg_cache(int specsize, const struct argspec *argspec, PyObject **cache)
{
    Py_ssize_t i;

    if(! ((*cache = PyDict_New()))) {
        return -1;
    }

    for(i = 0; i < specsize; i++) {
        const struct argspec *spec = argspec + i;
        PyObject *key = PyUnicode_InternFromString(spec->string);
        PyObject *val = MAKE_ID(i);
        if((! (key && val)) || PyDict_SetItem(*cache, key, val)) {
            Py_XDECREF(key);
            Py_XDECREF(val);
            Py_CLEAR(*cache);
            return -1;
        }
        Py_DECREF(key);
        Py_DECREF(val);
    }
    return 0;
}

/**
 * Like PyArg_ParseTupleAndKeywords except types are specialized for this
 * module, keyword strings aren't dup'd every call and the code is >3x smaller.
 *
 * `bvl` is passed through to parse_arg() for buffer lifetime tracking.
 * May be NULL for callers with no ARG_BUF args.
 */
static int NOINLINE
parse_args(int valid, int specsize, const struct argspec *argspec,
           PyObject **cache, PyObject *args, PyObject *kwds, void *out,
           BufViewList *bvl)
{
    unsigned set = 0;
    unsigned i;

    if(! valid) {
        err_invalid();
        return -1;
    }

    if(args) {
        Py_ssize_t size = PyTuple_GET_SIZE(args);
        if(size > specsize) {
            type_error("too many positional arguments.");
            return -1;
        }
        if(specsize < size) {
            size = specsize;
        }
        for(i = 0; i < size; i++) {
            if(parse_arg(argspec + i, PyTuple_GET_ITEM(args, i), out, bvl)) {
                return -1;
            }
            set |= 1 << i;
        }
    }

    if(kwds) {
        Py_ssize_t ppos = 0;
        PyObject *pkey;
        PyObject *pvalue;

        if((! *cache) && make_arg_cache(specsize, argspec, cache)) {
            return -1;
        }

        while(PyDict_Next(kwds, &ppos, &pkey, &pvalue)) {
            PyObject *specidx;
            int i;

            if(! ((specidx = PyDict_GetItem(*cache, pkey)))) {
                type_error("unrecognized keyword argument");
                return -1;
            }

            i = READ_ID(specidx);
            if(set & (1 << i)) {
                PyErr_Format(PyExc_TypeError, "duplicate argument: %U", pkey);
                return -1;
            }

            if(parse_arg(argspec + i, pvalue, out, bvl)) {
                return -1;
            }
        }
    }
    return 0;
}

/**
 * Return 1 if `db` is associated with the given `env`, otherwise raise an
 * exception. Used to prevent DBIs from unrelated envs from being mixed
 * together (which in future, would cause one env to access another's cursor
 * pointers).
 */
static int
db_owner_check(DbObject *db, EnvObject *env)
{
    if(db->env != env) {
        err_set("Database handle belongs to another environment.", 0);
        return 0;
    }
    return 1;
}


/* -------------------------------------------------------- */
/* Functionality shared between Transaction and Environment */
/* -------------------------------------------------------- */

static PyObject *
make_trans(EnvObject *env, DbObject *db, TransObject *parent, int write,
           int buffers)
{
    MDB_txn *parent_txn;
    MDB_txn *txn;
    TransObject *self;
    int flags;
    int rc;

    DEBUG("make_trans(env=%p, parent=%p, write=%d, buffers=%d)",
        env, parent, write, buffers)
    if(! env->valid) {
        return err_invalid();
    }

    if(! db) {
        db = env->main_db;
    } else if(! db_owner_check(db, env)) {
        return NULL;
    }

    parent_txn = NULL;
    if(parent) {
        if(parent->flags & TRANS_RDONLY) {
            return err_set("Read-only transactions cannot be nested.", EINVAL);
        }
        if(! parent->valid) {
            return err_invalid();
        }
        parent_txn = parent->txn;
    }

    if(write && env->readonly) {
        const char *msg =
            "Cannot start write transaction with read-only environment.";
        return err_set(msg, EACCES);
    }

    /* Fail fast while another thread is inside set_mapsize's critical
     * section: a txn begun now would reference the map about to be
     * unmapped.  The GIL-held read path needs this explicit check; the
     * write path would be caught by ENV_UNLOCKED anyway.  Issue #475. */
    if(ENV_RESIZE_BLOCKED(env)) {
        return err_set("mdb_txn_begin", EINVAL);
    }

    if(write && !parent &&
       env->write_txn_tid == PyThread_get_thread_ident()) {
        const char *msg =
            "Attempt to start a write transaction while another write "
            "transaction is active on the same thread. This would deadlock.";
        return err_set(msg, EBUSY);
    }

    if((!write) && env->spare_txn) {
        txn = env->spare_txn;
        DEBUG("using cached txn", txn)
        env->spare_txn = NULL;
        /* Hold GIL during mdb_txn_renew to prevent race with env_clear:
         * env->valid check above must be atomic with the LMDB operation.
         * See https://github.com/jnwatson/py-lmdb/issues/180 */
        rc = mdb_txn_renew(txn);
        if(rc) {
            mdb_txn_abort(txn);
            return err_set("mdb_txn_renew", rc);
        }
    }
    else {
        flags = write ? 0 : MDB_RDONLY;
        if(write && !parent) {
            /* Release GIL so another thread's write txn can block on the
             * LMDB mutex instead of deadlocking on the GIL.  Use active_ops
             * to prevent env_clear from closing the env underneath us.
             * Issues #180, #427. */
            ENV_UNLOCKED(env, rc,
                mdb_txn_begin(env->env, parent_txn, flags, &txn));
            if(rc) {
                return err_set("mdb_txn_begin", rc);
            }
            SET_WRITE_TXN_TID(env);
        } else {
            /* Read txns and child txns: hold GIL during mdb_txn_begin to
             * prevent race with env_clear.  Issue #180. */
            rc = mdb_txn_begin(env->env, parent_txn, flags, &txn);
            if(rc) {
                return err_set("mdb_txn_begin", rc);
            }
        }
    }

    if(! ((self = PyObject_New(TransObject, &PyTransaction_Type)))) {
        mdb_txn_abort(txn);
        if(write && !parent) {
            /* Mutex released; wakes any close() waiting on it.  #465. */
            CLEAR_WRITE_TXN_TID(env);
        }
        return NULL;
    }
    self->txn = txn;


    OBJECT_INIT(self)
    LINK_CHILD(env, self)
    self->weaklist = NULL;
    self->env = env;
    Py_INCREF(env);
    self->db = db;
    Py_INCREF(db);
#ifdef HAVE_MEMSINK
    self->sink_head = NULL;
#endif

    self->mutations = 0;
    self->flags = 0;
    if(! write) {
        self->flags |= TRANS_RDONLY;
    }
    if(buffers) {
        self->flags |= TRANS_BUFFERS;
    }
    return (PyObject *)self;
}

static PyObject *
make_cursor(DbObject *db, TransObject *trans)
{
    CursorObject *self;
    MDB_cursor *curs;
    int rc;

    if(! trans->valid) {
        return err_invalid();
    }
    if(! db) {
        db = trans->env->main_db;
    } else if(! db_owner_check(db, trans->env)) {
        return NULL;
    }

    /* Hold GIL: see make_trans comment and issue #180. */
    rc = mdb_cursor_open(trans->txn, db->dbi, &curs);
    if(rc) {
        return err_set("mdb_cursor_open", rc);
    }

    self = PyObject_New(CursorObject, &PyCursor_Type);
    if (!self) {
        mdb_cursor_close(curs);
        return NULL;
    }

    DEBUG("sizeof cursor = %d", (int) sizeof *self)
    OBJECT_INIT(self)
    LINK_CHILD(trans, self)
    self->curs = curs;
    self->positioned = 0;
    self->key.mv_size = 0;
    self->key.mv_data = NULL;
    self->val.mv_size = 0;
    self->val.mv_data = NULL;
    self->trans = trans;
    self->last_mutation = trans->mutations;
    self->dbi_flags = db->flags;
    Py_INCREF(self->trans);
    return (PyObject *) self;
}


/* -------- */
/* Database */
/* -------- */

static DbObject *
db_from_name(EnvObject *env, MDB_txn *txn, const char *name,
             unsigned int flags)
{
    MDB_dbi dbi;
    unsigned int f;
    int rc;
    DbObject *dbo;

    ENV_UNLOCKED(env, rc, mdb_dbi_open(txn, name, flags, &dbi));
    if(rc) {
        err_set("mdb_dbi_open", rc);
        return NULL;
    }
    if((rc = mdb_dbi_flags(txn, dbi, &f))) {
        err_set("mdb_dbi_flags", rc);
        mdb_dbi_close(env->env, dbi);
        return NULL;
    }

    if(! ((dbo = PyObject_New(DbObject, &PyDatabase_Type)))) {
        return NULL;
    }

    OBJECT_INIT(dbo)
    LINK_CHILD(env, dbo)
    dbo->env = env; /* no refcount */
    dbo->dbi = dbi;
    dbo->flags = f;
    DEBUG("DbObject '%s' opened at %p", name, dbo)
    return dbo;
}

/**
 * Use a temporary transaction to manufacture a new _Database object for
 * `name`.
 */
static DbObject *
txn_db_from_name(EnvObject *env, const char *name,
                 unsigned int flags)
{
    int rc;
    MDB_txn *txn;
    DbObject *dbo;

    int begin_flags = (name == NULL || env->readonly) ? MDB_RDONLY : 0;

    /* Fail fast while another thread is remapping (issue #475); the txn
     * begun below would reference the map being replaced. */
    if(ENV_RESIZE_BLOCKED(env)) {
        err_set("mdb_txn_begin", EINVAL);
        return NULL;
    }
    /* Hold GIL: see make_trans comment and issue #180. */
    rc = mdb_txn_begin(env->env, NULL, begin_flags, &txn);
    if(rc) {
        err_set("mdb_txn_begin", rc);
        return NULL;
    }

    if(! ((dbo = db_from_name(env, txn, name, flags)))) {
        int ignored;
        ENV_UNLOCKED(env, ignored, (mdb_txn_abort(txn), 0));
        (void)ignored;
        return NULL;
    }

    ENV_UNLOCKED(env, rc, mdb_txn_commit(txn));
    if(rc) {
        Py_DECREF(dbo);
        return err_set("mdb_txn_commit", rc);
    }
    return dbo;
}

static int
db_clear(DbObject *self)
{
    if(self->env) {
        UNLINK_CHILD(self->env, self)
        self->env = NULL;
    }
    self->valid = 0;
    return 0;
}

/**
 * _Database.flags()
 */
static PyObject *
db_flags(DbObject *self, PyObject *args, PyObject *kwds)
{
    PyObject *dct;
    unsigned int f;

    if (args) {
        Py_ssize_t size = PyTuple_GET_SIZE(args);
        if(size > 1) {
            return type_error("too many positional arguments.");
        }
    }

    dct = PyDict_New();
    if(! dct)
        return NULL;
    f = self->flags;
    dict_set_bool(dct, "reverse_key", f & MDB_REVERSEKEY);
    dict_set_bool(dct, "dupsort", f & MDB_DUPSORT);
    dict_set_bool(dct, "integerkey", f & MDB_INTEGERKEY);
    dict_set_bool(dct, "integerdup", f & MDB_INTEGERDUP);
    dict_set_bool(dct, "dupfixed", f & MDB_DUPFIXED);
    return dct;
}

/**
 * _Database.__del__()
 */
static void
db_dealloc(DbObject *self)
{
    db_clear(self);
    PyObject_Del(self);
}

static struct PyMethodDef db_methods[] = {
    {"flags", (PyCFunction)db_flags, METH_VARARGS|METH_KEYWORDS},
    {0, 0, 0, 0}
};

static PyTypeObject PyDatabase_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "_Database",                /*tp_name*/
    sizeof(DbObject),           /*tp_basicsize*/
    0,                          /*tp_itemsize*/
    (destructor)db_dealloc,     /*tp_dealloc*/
    0,                          /*tp_print*/
    0,                          /*tp_getattr*/
    0,                          /*tp_setattr*/
    0,                          /*tp_compare*/
    0,                          /*tp_repr*/
    0,                          /*tp_as_number*/
    0,                          /*tp_as_sequence*/
    0,                          /*tp_as_mapping*/
    0,                          /*tp_hash*/
    0,                          /*tp_call*/
    0,                          /*tp_str*/
    0,                          /*tp_getattro*/
    0,                          /*tp_setattro*/
    0,                          /*tp_as_buffer*/
    Py_TPFLAGS_DEFAULT,         /*tp_flags*/
    0,                          /*tp_doc*/
    0,                          /*tp_traverse*/
    (inquiry)db_clear,          /*tp_clear*/
    0,                          /*tp_richcompare*/
    0,                          /*tp_weaklistoffset*/
    0,                          /*tp_iter*/
    0,                          /*tp_iternext*/
    db_methods                  /*tp_methods*/
};


/* ----------- */
/* Environment */
/* ----------- */

static void
trans_dealloc(TransObject *self);

static void
txn_abort(MDB_txn *txn);

static int
env_clear(EnvObject *self)
{
    MDB_txn * txn;
    unsigned long me = (unsigned long) PyThread_get_thread_ident();

    MDEBUG("env_clear")

    /* If another thread is inside set_mapsize's critical section, wait
     * for it to finish before tearing the env down, so the two
     * invalidation walks cannot interleave.  Same-thread re-entry (a
     * destructor during the resize's invalidation phase) passes through.
     * set_mapsize never waits on close, so this cannot deadlock.  The
     * outer loop re-checks under the GIL in case a new resize started
     * between the wait ending and us reacquiring the GIL.  Issue #475. */
    while(ENV_RESIZE_BLOCKED(self)) {
        ENV_WAIT_WHILE(self, self->resizing && self->resize_tid != me);
    }

    self->valid = 0;
    /* Phase 1: quickly mark all descendants invalid (no I/O, GIL held).
     * This prevents any descendant from starting new LMDB operations. */
    INVALIDATE_MARK(self)

    /* Wait for in-flight LMDB operations to complete.  self->valid = 0
     * above (GIL held) prevents new ones from starting.  Issue #180. */
    if(LMDB_ATOMIC_LOAD(&self->active_ops) > 0) {
        ENV_WAIT_WHILE(self, LMDB_ATOMIC_LOAD(&self->active_ops) > 0);
    }

    /* LMDB requires all transactions to be closed before mdb_env_close,
     * and a write transaction belongs to the OS thread that began it
     * (with MDB_NOTLS, lmdb.h: "serialize the write transactions in an
     * OS thread, since LMDB's write locking is unaware of the user
     * threads").  On Linux the write mutex is a robust process-shared
     * pthread mutex linked into that thread's robust list, and can only
     * be unlocked by that thread.  If another thread closes the env
     * while a write txn is still open elsewhere, aborting it here (the
     * wrong thread) cannot release the mutex, and unmapping the lock
     * file underneath it strands a dangling entry on the owning thread's
     * robust list -> SIGSEGV in a later mdb_txn_begin on aarch64 (glibc
     * tolerates this on x86_64).  Honor the contract: block until the
     * owning thread releases the write txn, then tear the mapping down.
     * The owning thread broadcasts on ops_cond after its abort/commit
     * unlocks the mutex (CLEAR_WRITE_TXN_TID); the condition variable
     * re-checks the predicate under ops_mutex, so there is no lost
     * wakeup and no polling.  Issue #465. */
    if(self->write_txn_tid && self->write_txn_tid != me) {
        ENV_WAIT_WHILE(self,
            self->write_txn_tid && self->write_txn_tid != me);
    }

    /* Phase 2: actual cleanup (may release GIL for txn_abort etc.) */
    INVALIDATE(self)
    Py_CLEAR(self->main_db);

    txn = self->spare_txn;
    if(txn) {
        self->spare_txn = NULL;
        MDEBUG("killing spare txn %p", txn);
        /* Don't release GIL — env is being torn down, workers could
         * re-stash a spare_txn during the GIL release. */
        mdb_txn_abort(txn);
    }

    if(self->env) {
        MDB_env *env = self->env;
        self->env = NULL;
        DEBUG("Closing env")
        /* Release the GIL for mdb_env_close since it may msync/fsync
         * large writemap regions.  Setting self->env = NULL above tells
         * any concurrent trans_clear (from trans_dealloc in another
         * thread) to skip its mdb_txn_abort — the txn memory will be
         * freed by mdb_env_close.
         *
         * Py_INCREF prevents another thread from dropping the last
         * reference and freeing the EnvObject while mdb_env_close is
         * still running.  Issue #180, #418. */
        Py_INCREF((PyObject *)self);
        ACTIVE_OPS_INC(self);
        Py_BEGIN_ALLOW_THREADS
        mdb_env_close(env);
        Py_END_ALLOW_THREADS
        ACTIVE_OPS_DEC(self);
        Py_DECREF((PyObject *)self);
    }

    if(self->open_path) {
        PySet_Discard(open_env_paths, self->open_path);
        Py_CLEAR(self->open_path);
    }
    return 0;
}

/**
 * Environment.__del__()
 */
static void
env_dealloc(EnvObject *self)
{
    if(self->weaklist != NULL) {
        MDEBUG("Clearing weaklist..")
        PyObject_ClearWeakRefs((PyObject *) self);
    }

    /* Prevent re-entrant dealloc: env_clear Py_INCREFs self before
     * releasing the GIL for mdb_env_close, then Py_DECREFs after.
     * Without this, the DECREF would see refcount 0 and re-enter
     * env_dealloc.  Setting refcount to 1 ensures the DECREF in
     * env_clear drops it to 1, not 0. */
    Py_SET_REFCNT((PyObject *)self, 1);
    env_clear(self);
    if(self->ops_sync_ready) {
        self->ops_sync_ready = 0;
#ifdef _WIN32
        DeleteCriticalSection(&self->ops_mutex);
        /* CONDITION_VARIABLEs need no explicit destruction. */
#else
        pthread_mutex_destroy(&self->ops_mutex);
        pthread_cond_destroy(&self->ops_cond);
#endif
    }
    PyObject_Del(self);
}

/**
 * Environment.close()
 */
static PyObject *
env_close(EnvObject *self, PyObject *Py_UNUSED(ignored))
{
    env_clear(self);
    Py_RETURN_NONE;
}

/**
 * Environment() -> new object.
 */
static PyObject *
env_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    struct env_new {
        PyObject *path;
        size_t map_size;
        int subdir;
        int readonly;
        int metasync;
        int sync;
        int map_async;
        int mode;
        int create;
        int readahead;
        int writemap;
        int meminit;
        int max_readers;
        int max_dbs;
        int max_spare_txns;
        int lock;
    } arg = {NULL, 10485760, 1, 0, 1, 1, 0, 0755, 1, 1, 0, 1, 126, 0, 0, 1};

    static const struct argspec argspec[] = {
        {"path", ARG_OBJ, OFFSET(env_new, path)},
        {"map_size", ARG_SIZE, OFFSET(env_new, map_size)},
        {"subdir", ARG_BOOL, OFFSET(env_new, subdir)},
        {"readonly", ARG_BOOL, OFFSET(env_new, readonly)},
        {"metasync", ARG_BOOL, OFFSET(env_new, metasync)},
        {"sync", ARG_BOOL, OFFSET(env_new, sync)},
        {"map_async", ARG_BOOL, OFFSET(env_new, map_async)},
        {"mode", ARG_INT, OFFSET(env_new, mode)},
        {"create", ARG_BOOL, OFFSET(env_new, create)},
        {"readahead", ARG_BOOL, OFFSET(env_new, readahead)},
        {"writemap", ARG_BOOL, OFFSET(env_new, writemap)},
        {"meminit", ARG_INT, OFFSET(env_new, meminit)},
        {"max_readers", ARG_INT, OFFSET(env_new, max_readers)},
        {"max_dbs", ARG_INT, OFFSET(env_new, max_dbs)},
        {"max_spare_txns", ARG_INT, OFFSET(env_new, max_spare_txns)},
        {"lock", ARG_BOOL, OFFSET(env_new, lock)},
    };

    PyObject *fspath_obj = NULL;
    EnvObject *self;
    const char *fspath;
    int flags;
    int rc;
    int mode;

    static PyObject *cache = NULL;
    if(parse_args(1, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }

    if(! arg.path) {
        return type_error("'path' argument required");
    }

    if(! ((self = PyObject_New(EnvObject, type)))) {
        return NULL;
    }

    OBJECT_INIT(self)
    self->weaklist = NULL;
    self->main_db = NULL;
    self->env = NULL;
    self->spare_txn = NULL;
    self->open_path = NULL;
    self->max_spare_txns = arg.max_spare_txns;
    self->pid = _cached_pid;
    self->write_txn_tid = 0;
    self->active_ops = 0;
    self->ops_waiters = 0;
    self->resizing = 0;
    self->resize_tid = 0;
    self->ops_sync_ready = 0;
#ifdef _WIN32
    InitializeCriticalSection(&self->ops_mutex);
    InitializeConditionVariable(&self->ops_cond);
#else
    if(pthread_mutex_init(&self->ops_mutex, NULL)) {
        PyErr_SetString(PyExc_MemoryError, "unable to initialize mutex");
        Py_DECREF(self);
        return NULL;
    }
    if(pthread_cond_init(&self->ops_cond, NULL)) {
        pthread_mutex_destroy(&self->ops_mutex);
        PyErr_SetString(PyExc_MemoryError,
                        "unable to initialize condition variable");
        Py_DECREF(self);
        return NULL;
    }
#endif
    self->ops_sync_ready = 1;

    if((rc = mdb_env_create(&self->env))) {
        err_set("mdb_env_create", rc);
        goto fail;
    }

    if((rc = mdb_env_set_mapsize(self->env, arg.map_size))) {
        err_set("mdb_env_set_mapsize", rc);
        goto fail;
    }

    if((rc = mdb_env_set_maxreaders(self->env, arg.max_readers))) {
        err_set("mdb_env_set_maxreaders", rc);
        goto fail;
    }

    if((rc = mdb_env_set_maxdbs(self->env, arg.max_dbs))) {
        err_set("mdb_env_set_maxdbs", rc);
        goto fail;
    }

    if(! ((fspath_obj = get_fspath(arg.path)))) {
        goto fail;
    }
    fspath = PyBytes_AS_STRING(fspath_obj);

    if(arg.create && arg.subdir && !arg.readonly) {
        if(mkdir(fspath, arg.mode) && errno != EEXIST) {
            PyErr_SetFromErrnoWithFilename(PyExc_OSError, fspath);
            goto fail;
        }
    }

    {
        PyObject *os_path = PyImport_ImportModule("os.path");
        PyObject *realpath_func;
        PyObject *resolved;

        if(! os_path) {
            goto fail;
        }
        realpath_func = PyObject_GetAttrString(os_path, "realpath");
        Py_DECREF(os_path);
        if(! realpath_func) {
            goto fail;
        }
        resolved = PyObject_CallFunctionObjArgs(realpath_func, arg.path, NULL);
        Py_DECREF(realpath_func);
        if(! resolved) {
            goto fail;
        }

        /* Normalize to a string for consistent set membership. */
        if(PyBytes_Check(resolved)) {
            PyObject *tmp = PyUnicode_DecodeFSDefault(PyBytes_AS_STRING(resolved));
            Py_DECREF(resolved);
            if(! tmp) {
                goto fail;
            }
            resolved = tmp;
        }

        if(PySet_Contains(open_env_paths, resolved)) {
            PyErr_Format(Error,
                "The environment '%s' is already open in this process.",
                fspath);
            Py_DECREF(resolved);
            goto fail;
        }
        self->open_path = resolved;  /* steal reference */
    }

    flags = MDB_NOTLS;
    if(! arg.subdir) {
        flags |= MDB_NOSUBDIR;
    }
    if(arg.readonly) {
        flags |= MDB_RDONLY;
    }
    self->readonly = arg.readonly;
    if(! arg.metasync) {
        flags |= MDB_NOMETASYNC;
    }
    if(! arg.sync) {
        flags |= MDB_NOSYNC;
    }
    if(arg.map_async) {
        flags |= MDB_MAPASYNC;
    }
    if(! arg.readahead) {
        flags |= MDB_NORDAHEAD;
    }
    if(arg.writemap) {
        flags |= MDB_WRITEMAP;
    }
    if(! arg.meminit) {
        flags |= MDB_NOMEMINIT;
    }
    if(! arg.lock) {
        flags |= MDB_NOLOCK;
    }

    /* Strip +x. */
    mode = arg.mode & ~0111;

    DEBUG("mdb_env_open(%p, '%s', %d, %o);", self->env, fspath, flags, mode)
    UNLOCKED(rc, mdb_env_open(self->env, fspath, flags, mode));
    if(rc) {
        err_set(fspath, rc);
        goto fail;
    }

    self->main_db = txn_db_from_name(self, NULL, 0);
    if(self->main_db) {
        self->valid = 1;
        if(PySet_Add(open_env_paths, self->open_path)) {
            goto fail;
        }
        DEBUG("EnvObject '%s' opened at %p", fspath, self)
        Py_DECREF(fspath_obj);
        return (PyObject *) self;
    }

fail:
    DEBUG("initialization failed")
    Py_CLEAR(fspath_obj);
    Py_CLEAR(self);
    return NULL;
}

/**
 * Environment.begin()
 */
static PyObject *
env_begin(EnvObject *self, PyObject *args, PyObject *kwds)
{
    struct env_begin {
        DbObject *db;
        TransObject *parent;
        int write;
        int buffers;
    } arg = {self->main_db, NULL, 0, 0};

    static const struct argspec argspec[] = {
        {"db", ARG_DB, OFFSET(env_begin, db)},
        {"parent", ARG_TRANS, OFFSET(env_begin, parent)},
        {"write", ARG_BOOL, OFFSET(env_begin, write)},
        {"buffers", ARG_BOOL, OFFSET(env_begin, buffers)},
    };

    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }
    return make_trans(self, arg.db, arg.parent, arg.write, arg.buffers);
}

/**
 * Environment.copy()
 */
static PyObject *
env_copy(EnvObject *self, PyObject *args, PyObject *kwds)
{
    struct env_copy {
        PyObject *path;
        int compact;
        TransObject *txn;
    } arg = {NULL, 0, NULL};

    static const struct argspec argspec[] = {
        {"path", ARG_OBJ, OFFSET(env_copy, path)},
        {"compact", ARG_BOOL, OFFSET(env_copy, compact)},
        {"txn", ARG_TRANS, OFFSET(env_copy, txn)}
    };

    PyObject *fspath_obj;
    const char *fspath_s;
    int flags;
    int rc;
#ifdef HAVE_PATCHED_LMDB
    MDB_txn *txn;
#endif

    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }
    if(! arg.path) {
        return type_error("path argument required");
    }
    if(! ((fspath_obj = get_fspath(arg.path)))) {
        return NULL;
    }

#ifdef HAVE_PATCHED_LMDB
    if (arg.txn) {
        txn = arg.txn->txn;
        if (!arg.compact) {
            Py_DECREF(fspath_obj);
            return type_error("txn argument only compatible with compact=True");
        }
    }
    else {
        txn = NULL;
    }
#else
    if (arg.txn) {
        Py_DECREF(fspath_obj);
        return type_error("Non-patched LMDB doesn't support transaction with env.copy");
    }
#endif

    fspath_s = PyBytes_AS_STRING(fspath_obj);
    flags = arg.compact ? MDB_CP_COMPACT : 0;
#ifdef HAVE_PATCHED_LMDB
    ENV_UNLOCKED(self, rc, mdb_env_copy3(self->env, fspath_s, flags, txn));
#else
    ENV_UNLOCKED(self, rc, mdb_env_copy2(self->env, fspath_s, flags));
#endif
    Py_CLEAR(fspath_obj);
    if(rc) {
#ifdef HAVE_PATCHED_LMDB
        return err_set("mdb_env_copy3", rc);
#else
        return err_set("mdb_env_copy2", rc);
#endif
    }
    Py_RETURN_NONE;
}

/**
 * Environment.copyfd(fd)
 */
static PyObject *
env_copyfd(EnvObject *self, PyObject *args, PyObject *kwds)
{
    struct env_copyfd {
        int fd;
        int compact;
        TransObject *txn;
    } arg = {-1, 0};
    int rc;
#ifdef HAVE_PATCHED_LMDB
    MDB_txn *txn;
#endif
#ifdef _WIN32
    PyObject *temp;
    Py_ssize_t handle;
#endif

    static const struct argspec argspec[] = {
        {"fd", ARG_INT, OFFSET(env_copyfd, fd)},
        {"compact", ARG_BOOL, OFFSET(env_copyfd, compact)},
        {"txn", ARG_TRANS, OFFSET(env_copyfd, txn)},
    };
    int flags;

    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }
    if(arg.fd == -1) {
        return type_error("fd argument required");
    }
    flags = arg.compact ? MDB_CP_COMPACT : 0;

#ifdef HAVE_PATCHED_LMDB
    if (arg.txn) {
        txn = arg.txn->txn;
        if (!arg.compact) {
            return type_error("txn argument only compatible with compact=True");
        }
    }
    else {
        txn = NULL;
    }
#else
    if (arg.txn) {
        return type_error("Non-patched LMDB doesn't support transaction with env.copyfd");
    }
#endif

#ifdef _WIN32
    temp = PyObject_CallMethod(msvcrt, "get_osfhandle", "i", arg.fd);
    if(! temp) {
        return NULL;
    }
    handle = PyNumber_AsSsize_t(temp, PyExc_OverflowError);
    Py_DECREF(temp);
    if(PyErr_Occurred()) {
        return NULL;
    }
    #define HANDLE_ARG (HANDLE)handle
#else
    #define HANDLE_ARG arg.fd
#endif

#ifdef HAVE_PATCHED_LMDB
    ENV_UNLOCKED(self, rc, mdb_env_copyfd3(self->env, HANDLE_ARG, flags, txn));
#else
    ENV_UNLOCKED(self, rc, mdb_env_copyfd2(self->env, HANDLE_ARG, flags));
#endif

    if(rc) {
        return err_set("mdb_env_copyfd3", rc);
    }
    Py_RETURN_NONE;
}

/**
 * Environment.info() -> dict
 */
static PyObject *
env_info(EnvObject *self, PyObject *Py_UNUSED(ignored))
{
    static const struct dict_field fields[] = {
        {TYPE_ADDR, "map_addr",    offsetof(MDB_envinfo, me_mapaddr)},
        {TYPE_SIZE, "map_size",    offsetof(MDB_envinfo, me_mapsize)},
        {TYPE_SIZE, "last_pgno",   offsetof(MDB_envinfo, me_last_pgno)},
        {TYPE_SIZE, "last_txnid",  offsetof(MDB_envinfo, me_last_txnid)},
        {TYPE_UINT, "max_readers", offsetof(MDB_envinfo, me_maxreaders)},
        {TYPE_UINT, "num_readers", offsetof(MDB_envinfo, me_numreaders)},
        {TYPE_EOF, NULL, 0}
    };
    MDB_envinfo info;
    int rc;

    if(! self->valid) {
        return err_invalid();
    }

    ENV_UNLOCKED(self, rc, mdb_env_info(self->env, &info));
    if(rc) {
        err_set("mdb_env_info", rc);
        return NULL;
    }
    return dict_from_fields(&info, fields);
}

/**
 * Environment.flags() -> dict
 */
static PyObject *
env_flags(EnvObject *self, PyObject *Py_UNUSED(ignored))
{
    PyObject *dct;
    unsigned int flags;
    int rc;

    if(! self->valid) {
        return err_invalid();
    }

    if((rc = mdb_env_get_flags(self->env, &flags))) {
        err_set("mdb_env_get_flags", rc);
        return NULL;
    }

    dct = PyDict_New();
    if(! dct)
        return NULL;
    dict_set_bool(dct, "subdir", !(flags & MDB_NOSUBDIR));
    dict_set_bool(dct, "readonly", flags & MDB_RDONLY);
    dict_set_bool(dct, "metasync", !(flags & MDB_NOMETASYNC));
    dict_set_bool(dct, "sync", !(flags & MDB_NOSYNC));
    dict_set_bool(dct, "map_async", flags & MDB_MAPASYNC);
    dict_set_bool(dct, "readahead", !(flags & MDB_NORDAHEAD));
    dict_set_bool(dct, "writemap", flags & MDB_WRITEMAP);
    dict_set_bool(dct, "meminit", !(flags & MDB_NOMEMINIT));
    dict_set_bool(dct, "lock", !(flags & MDB_NOLOCK));
    return dct;
}

/**
 * Environment.max_key_size() -> int
 */
static PyObject *
env_max_key_size(EnvObject *self, PyObject *Py_UNUSED(ignored))
{
    int key_size;
    if(! self->valid) {
        return err_invalid();
    }
    key_size = mdb_env_get_maxkeysize(self->env);
    return PyLong_FromLongLong(key_size);
}

/**
 * Environment.max_key_size() -> int
 */
static PyObject *
env_max_readers(EnvObject *self, PyObject *Py_UNUSED(ignored))
{
    unsigned int readers;
    int rc;

    if(! self->valid) {
        return err_invalid();
    }
    if((rc = mdb_env_get_maxreaders(self->env, &readers))) {
        return err_set("mdb_env_get_maxreaders", rc);
    }
    return PyLong_FromLongLong(readers);
}

/**
 * Environment.open_db() -> handle
 */
static PyObject *
env_open_db(EnvObject *self, PyObject *args, PyObject *kwds)
{
    struct env_open_db {
        const char *key;
        TransObject *txn;
        int reverse_key;
        int dupsort;
        int create;
        int integerkey;
        int integerdup;
        int dupfixed;
    } arg = {NULL, NULL, 0, 0, 1};

    static const struct argspec argspec[] = {
        {"key", ARG_STR, OFFSET(env_open_db, key)},
        {"txn", ARG_TRANS, OFFSET(env_open_db, txn)},
        {"reverse_key", ARG_BOOL, OFFSET(env_open_db, reverse_key)},
        {"dupsort", ARG_BOOL, OFFSET(env_open_db, dupsort)},
        {"create", ARG_BOOL, OFFSET(env_open_db, create)},
        {"integerkey", ARG_BOOL, OFFSET(env_open_db, integerkey)},
        {"integerdup", ARG_BOOL, OFFSET(env_open_db, integerdup)},
        {"dupfixed", ARG_BOOL, OFFSET(env_open_db, dupfixed)},
    };
    int flags;

    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }

    if (!arg.key && (arg.reverse_key || arg.dupsort || arg.integerkey ||
                     arg.integerdup || arg.dupfixed)) {
        return PyErr_Format(PyExc_ValueError,
                            "May not set flags on the main database");
    }

    flags = 0;
    if(arg.reverse_key) {
        flags |= MDB_REVERSEKEY;
    }
    if(arg.dupsort) {
        flags |= MDB_DUPSORT;
    }
    if(arg.create) {
        flags |= MDB_CREATE;
    }
    if(arg.integerkey) {
        flags |= MDB_INTEGERKEY;
    }
    if(arg.integerdup) {
        flags |= MDB_DUPSORT;
        flags |= MDB_DUPFIXED;
        flags |= MDB_INTEGERDUP;
    }
    if(arg.dupfixed) {
        flags |= MDB_DUPSORT;
        flags |= MDB_DUPFIXED;
    }

    if(arg.txn) {
        return (PyObject *) db_from_name(self, arg.txn->txn, arg.key, flags);
    } else {
        return (PyObject *) txn_db_from_name(self, arg.key, flags);
    }
}

/**
 * Environment.dbs() -> list of bytes
 */
static PyObject *
env_dbs(EnvObject *self, PyObject *args, PyObject *kwds)
{
    struct env_dbs {
        TransObject *txn;
    } arg = {NULL};

    static const struct argspec argspec[] = {
        {"txn", ARG_TRANS, OFFSET(env_dbs, txn)},
    };

    MDB_txn *txn;
    MDB_cursor *cursor;
    MDB_val key, data;
    MDB_dbi dbi;
    int rc;
    int own_txn = 0;
    PyObject *list;
    char *name;

    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }

    if(arg.txn) {
        txn = arg.txn->txn;
    } else {
        if(self->spare_txn) {
            txn = self->spare_txn;
            self->spare_txn = NULL;
            rc = mdb_txn_renew(txn);
            if(rc) {
                mdb_txn_abort(txn);
                return err_set("mdb_txn_renew", rc);
            }
        } else {
            rc = mdb_txn_begin(self->env, NULL, MDB_RDONLY, &txn);
            if(rc) {
                return err_set("mdb_txn_begin", rc);
            }
        }
        own_txn = 1;
    }

    rc = mdb_cursor_open(txn, self->main_db->dbi, &cursor);
    if(rc) {
        if(own_txn) {
            mdb_txn_reset(txn);
            self->spare_txn = txn;
        }
        return err_set("mdb_cursor_open", rc);
    }

    list = PyList_New(0);
    if(! list) {
        mdb_cursor_close(cursor);
        if(own_txn) {
            mdb_txn_reset(txn);
            self->spare_txn = txn;
        }
        return NULL;
    }

    while((rc = mdb_cursor_get(cursor, &key, &data, MDB_NEXT)) == 0) {
        if(SIZE_ADD_OVERFLOW(key.mv_size, 1)) {
            /* Sub-database name from a (possibly adversarial) on-disk record
             * is so large that the NUL-terminator allocation would wrap. */
            Py_DECREF(list);
            mdb_cursor_close(cursor);
            if(own_txn) {
                mdb_txn_reset(txn);
                self->spare_txn = txn;
            }
            PyErr_SetString(PyExc_OverflowError,
                "sub-database name size overflow (corrupt database?)");
            return NULL;
        }
        name = malloc(key.mv_size + 1);
        if(! name) {
            Py_DECREF(list);
            mdb_cursor_close(cursor);
            if(own_txn) {
                mdb_txn_reset(txn);
                self->spare_txn = txn;
            }
            return PyErr_NoMemory();
        }
        memcpy(name, key.mv_data, key.mv_size);
        name[key.mv_size] = '\0';

        rc = mdb_dbi_open(txn, name, 0, &dbi);
        free(name);

        if(rc == 0) {
            PyObject *keyobj = PyBytes_FromStringAndSize(key.mv_data,
                                                         key.mv_size);
            if(! keyobj || PyList_Append(list, keyobj)) {
                Py_XDECREF(keyobj);
                Py_DECREF(list);
                mdb_cursor_close(cursor);
                if(own_txn) {
                    mdb_txn_reset(txn);
                    self->spare_txn = txn;
                }
                return NULL;
            }
            Py_DECREF(keyobj);
        }
        /* MDB_INCOMPATIBLE means it's a regular key, not a DB. Skip it. */
    }

    mdb_cursor_close(cursor);
    if(own_txn) {
        mdb_txn_reset(txn);
        self->spare_txn = txn;
    }

    if(rc != MDB_NOTFOUND) {
        Py_DECREF(list);
        return err_set("mdb_cursor_get", rc);
    }

    return list;
}

/**
 * Environment.path() -> Unicode
 */
static PyObject *
env_path(EnvObject *self, PyObject *Py_UNUSED(ignored))
{
    const char *path;
    int rc;

    if(! self->valid) {
        return err_invalid();
    }

    if((rc = mdb_env_get_path(self->env, &path))) {
        return err_set("mdb_env_get_path", rc);
    }
    return PyUnicode_FromString(path);
}

static const struct dict_field mdb_stat_fields[] = {
    {TYPE_UINT, "psize",          offsetof(MDB_stat, ms_psize)},
    {TYPE_UINT, "depth",          offsetof(MDB_stat, ms_depth)},
    {TYPE_SIZE, "branch_pages",   offsetof(MDB_stat, ms_branch_pages)},
    {TYPE_SIZE, "leaf_pages",     offsetof(MDB_stat, ms_leaf_pages)},
    {TYPE_SIZE, "overflow_pages", offsetof(MDB_stat, ms_overflow_pages)},
    {TYPE_SIZE, "entries",        offsetof(MDB_stat, ms_entries)},
    {TYPE_EOF, NULL, 0}
};

/**
 * Environment.stat() -> dict
 */
static PyObject *
env_stat(EnvObject *self, PyObject *Py_UNUSED(ignored))
{
    MDB_stat st;
    int rc;

    if(! self->valid) {
        return err_invalid();
    }

    ENV_UNLOCKED(self, rc, mdb_env_stat(self->env, &st));
    if(rc) {
        err_set("mdb_env_stat", rc);
        return NULL;
    }
    return dict_from_fields(&st, mdb_stat_fields);
}

/**
 * Callback to receive string result for env_readers(). Return 0 on success or
 * -1 on error.
 */
static int env_readers_callback(const char *msg, void *str_)
{
    PyObject **str = str_;
    PyObject *s = PyUnicode_FromString(msg);
    PyObject *new;
    if(! s) {
        return -1;
    }
    new = PyUnicode_Concat(*str, s);
    Py_DECREF(s);
    Py_CLEAR(*str);
    *str = new;
    if(! new) {
        return -1;
    }
    return 0;
}

/**
 * Environment.readers() -> string
 */
static PyObject *
env_readers(EnvObject *self, PyObject *Py_UNUSED(ignored))
{
    PyObject *str;
    if(! self->valid) {
        return err_invalid();
    }

    if(! ((str = PyUnicode_FromString("")))) {
        return NULL;
    }

    if(mdb_reader_list(self->env, env_readers_callback, &str)) {
        Py_CLEAR(str);
    }
    return str;
}

/**
 * Environment.reader_check() -> int
 */
static PyObject *
env_reader_check(EnvObject *self, PyObject *Py_UNUSED(ignored))
{
    int rc;
    int dead;

    if(! self->valid) {
        return err_invalid();
    }

    if((rc = mdb_reader_check(self->env, &dead))) {
        return err_set("mdb_reader_check", rc);
    }
    return PyLong_FromLongLong(dead);
}

/**
 * Environment.set_mapsize(size) -> None
 */
static PyObject *
env_reader_set_mapsize(EnvObject *self, PyObject *args, PyObject *kwargs)
{
    struct env_set_mapsize {
        size_t map_size;
    } arg = {0};

    static const struct argspec argspec[] = {
        {"map_size", ARG_SIZE, OFFSET(env_set_mapsize, map_size)}
    };
    int rc;
    MDB_txn *txn;

    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache,
                  args, kwargs, &arg, NULL)) {
        return NULL;
    }

    /* Reject if a write transaction is active — mdb_env_set_mapsize would
     * return EINVAL anyway, but we must also avoid invalidating a write txn
     * that the caller still holds. */
    if(self->write_txn_tid) {
        PyErr_Format(Error,
            "Cannot set_mapsize while a write transaction is active");
        return NULL;
    }

    /* Reject overlapping resizes.  Cannot be our own thread (no Python runs
     * between the flag being set and cleared except destructors during
     * invalidation, and re-entering set_mapsize from one is degenerate). */
    if(self->resizing) {
        PyErr_Format(Error,
            "Cannot set_mapsize: another set_mapsize is in progress");
        return NULL;
    }

    /* Enter the resize critical section: from here until the flag is
     * cleared, new GIL-releasing LMDB operations on this env fail fast
     * with EINVAL (see ENV_RESIZE_BLOCKED), so nothing new can run
     * concurrently with the remap.  GIL-held LMDB calls need no gate —
     * the remap below also runs with the GIL held, so they cannot
     * overlap it.  Issue #475. */
    ENV_SYNC_LOCK(self);
    self->resizing = 1;
    self->resize_tid = (unsigned long) PyThread_get_thread_ident();
    ENV_SYNC_UNLOCK(self);

    /* Phase 1: quickly mark open transactions (and their cursors) invalid so
     * no new LMDB operations can start on them.  Database handles are left
     * valid — their MDB_dbi survives the remap (issue #475).  The env itself
     * stays valid (we do NOT set self->valid = 0). */
    INVALIDATE_MARK_TXNS(self)

    /* Wait for in-flight LMDB operations to complete.  The condition
     * variable supports multiple waiters, so this cannot collide with a
     * trans_abort or env_clear draining concurrently.  The outer loop
     * re-checks under the GIL: trans_dealloc on another thread can
     * increment active_ops (it does not pass the resize gate) between the
     * wait ending and us reacquiring the GIL.  Issue #475. */
    while(LMDB_ATOMIC_LOAD(&self->active_ops) > 0) {
        ENV_WAIT_WHILE(self, LMDB_ATOMIC_LOAD(&self->active_ops) > 0);
    }

    /* Phase 2: abort all child transactions and close cursors.  This must
     * happen while the old mmap is still valid so mdb_txn_abort can
     * safely touch the pages it needs to.  Database handles are preserved. */
    INVALIDATE_TXNS(self)

    /* A write transaction may still be dying on another thread without
     * holding active_ops: trans_abort() NULLs self->txn on entry (so the
     * invalidation above skipped it) and may sit in its child-op wait
     * before performing the real mdb_txn_abort.  Such a transaction keeps
     * write_txn_tid set until the abort completes (CLEAR_WRITE_TXN_TID
     * broadcasts), and either the tid is still set or its final
     * abort/dealloc is in flight under active_ops — both observable here.
     * Wait them out, or the remap below would find the transaction undead
     * and fail with EINVAL.  New write transactions cannot start: begins
     * fail fast on the resize gate.  Issue #475. */
    while(LMDB_ATOMIC_LOAD(&self->active_ops) > 0 || self->write_txn_tid) {
        ENV_WAIT_WHILE(self,
            LMDB_ATOMIC_LOAD(&self->active_ops) > 0 || self->write_txn_tid);
    }

    /* A destructor triggered during invalidation may have closed the env
     * on this thread (same-thread re-entry passes the resize gate).
     * Don't remap a closed env. */
    if(! self->valid || ! self->env) {
        ENV_SYNC_LOCK(self);
        self->resizing = 0;
        ENV_SYNC_BROADCAST(self);
        ENV_SYNC_UNLOCK(self);
        return err_invalid();
    }

    /* Abort the spare read-only transaction — it holds references to the
     * old memory map. */
    txn = self->spare_txn;
    if(txn) {
        self->spare_txn = NULL;
        mdb_txn_abort(txn);
    }

    /* Now safe to remap: in-flight operations have drained, new ones fail
     * fast on the resize gate, and this call itself runs with the GIL
     * held. */
    rc = mdb_env_set_mapsize(self->env, arg.map_size);

    if(rc) {
        /* Remap failed — env is in an unusable state.  Mark it invalid
         * (BEFORE clearing the gate, so operations observe it and bail
         * out instead of calling into the unusable env). */
        self->valid = 0;
    }

    /* Leave the critical section; wake an env_clear waiting for the
     * resize to finish. */
    ENV_SYNC_LOCK(self);
    self->resizing = 0;
    ENV_SYNC_BROADCAST(self);
    ENV_SYNC_UNLOCK(self);

    if(rc) {
        return err_set("mdb_env_set_mapsize", rc);
    }

    Py_RETURN_NONE;
}

/**
 * Environment.sync()
 */
static PyObject *
env_sync(EnvObject *self, PyObject *args)
{
    struct env_sync {
        int force;
    } arg = {0};

    static const struct argspec argspec[] = {
        {"force", ARG_BOOL, OFFSET(env_sync, force)}
    };
    int rc;

    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, NULL, &arg, NULL)) {
        return NULL;
    }

    ENV_UNLOCKED(self, rc, mdb_env_sync(self->env, arg.force));
    if(rc) {
        return err_set("mdb_env_sync", rc);
    }
    Py_RETURN_NONE;
}

/**
 * Environment.__enter__()
 */
static PyObject *env_enter(EnvObject *self, PyObject *Py_UNUSED(ignored))
{
    Py_INCREF(self);
    return (PyObject *)self;
}

/**
 * Environment.__exit__()
 */
static PyObject *env_exit(EnvObject *self, PyObject *args)
{
    env_clear(self);
    Py_RETURN_NONE;
}

static struct PyMethodDef env_methods[] = {
    {"__enter__", (PyCFunction)env_enter, METH_NOARGS},
    {"__exit__", (PyCFunction)env_exit, METH_VARARGS},
    {"begin", (PyCFunction)env_begin, METH_VARARGS|METH_KEYWORDS},
    {"close", (PyCFunction)env_close, METH_NOARGS},
    {"copy", (PyCFunction)env_copy, METH_VARARGS|METH_KEYWORDS},
    {"dbs", (PyCFunction)env_dbs, METH_VARARGS|METH_KEYWORDS},
    {"copyfd", (PyCFunction)env_copyfd, METH_VARARGS|METH_KEYWORDS},
    {"info", (PyCFunction)env_info, METH_NOARGS},
    {"flags", (PyCFunction)env_flags, METH_NOARGS},
    {"max_key_size", (PyCFunction)env_max_key_size, METH_NOARGS},
    {"max_readers", (PyCFunction)env_max_readers, METH_NOARGS},
    {"open_db", (PyCFunction)env_open_db, METH_VARARGS|METH_KEYWORDS},
    {"path", (PyCFunction)env_path, METH_NOARGS},
    {"stat", (PyCFunction)env_stat, METH_NOARGS},
    {"readers", (PyCFunction)env_readers, METH_NOARGS},
    {"reader_check", (PyCFunction)env_reader_check, METH_NOARGS},
    {"set_mapsize", (PyCFunction)env_reader_set_mapsize,
     METH_VARARGS|METH_KEYWORDS},
    {"sync", (PyCFunction)env_sync, METH_VARARGS},
    {NULL, NULL}
};

static PyTypeObject PyEnvironment_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "Environment",              /*tp_name*/
    sizeof(EnvObject),          /*tp_basicsize*/
    0,                          /*tp_itemsize*/
    (destructor) env_dealloc,   /*tp_dealloc*/
    0,                          /*tp_print*/
    0,                          /*tp_getattr*/
    0,                          /*tp_setattr*/
    0,                          /*tp_compare*/
    0,                          /*tp_repr*/
    0,                          /*tp_as_number*/
    0,                          /*tp_as_sequence*/
    0,                          /*tp_as_mapping*/
    0,                          /*tp_hash*/
    0,                          /*tp_call*/
    0,                          /*tp_str*/
    0,                          /*tp_getattro*/
    0,                          /*tp_setattro*/
    0,                          /*tp_as_buffer*/
    Py_TPFLAGS_DEFAULT,         /*tp_flags*/
    0,                          /*tp_doc*/
    0,                          /*tp_traverse*/
    (inquiry) env_clear,        /*tp_clear*/
    0,                          /*tp_richcompare*/
    offsetof(EnvObject, weaklist), /*tp_weaklistoffset*/
    0,                          /*tp_iter*/
    0,                          /*tp_iternext*/
    env_methods,                /*tp_methods*/
    0,                          /*tp_members*/
    0,                          /*tp_getset*/
    0,                          /*tp_base*/
    0,                          /*tp_dict*/
    0,                          /*tp_descr_get*/
    0,                          /*tp_descr_set*/
    0,                          /*tp_dictoffset*/
    0,                          /*tp_init*/
    0,                          /*tp_alloc*/
    env_new,                    /*tp_new*/
};


/* ------- */
/* Cursors */
/* ------- */

static int
cursor_clear(CursorObject *self)
{
    if(self->curs) {
        MDB_cursor *curs = self->curs;
        self->valid = 0;
        self->curs = NULL;  /* Prevent double-close (issue #180). */
        INVALIDATE(self)
        UNLINK_CHILD(self->trans, self)
        /* mdb_cursor_close does no I/O — just unlinks from txn and frees.
         * No need to release the GIL; keeping it held avoids widening the
         * race window during INVALIDATE. */
        mdb_cursor_close(curs);
    }
    Py_CLEAR(self->trans);
    return 0;
}

/**
 * Cursor.__del__()
 */
static void
cursor_dealloc(CursorObject *self)
{
    DEBUG("destroying cursor")
    cursor_clear(self);
    PyObject_Del(self);
}

/**
 * Environment.Cursor(db, trans) -> new instance.
 */
static PyObject *
cursor_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    struct cursor_new {
        DbObject *db;
        TransObject *trans;
    } arg = {NULL, NULL};

    static const struct argspec argspec[] = {
        {"db", ARG_DB, OFFSET(cursor_new, db)},
        {"txn", ARG_TRANS, OFFSET(cursor_new, trans)}
    };

    static PyObject *cache = NULL;
    if(parse_args(1, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }

    if(! (arg.db && arg.trans)) {
        return type_error("db and transaction parameters required.");
    }
    return make_cursor(arg.db, arg.trans);
}

/**
 * Cursor.count() -> long
 */
static PyObject *
cursor_count(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    size_t count;
    int rc;

    if(! self->valid) {
        return err_invalid();
    }

    ENV_UNLOCKED(self->trans->env, rc, mdb_cursor_count(self->curs, &count));
    if(rc) {
        return err_set("mdb_cursor_count", rc);
    }
    return PyLong_FromUnsignedLongLong(count);
}

/**
 * Apply `op` to the cursor using the Cursor instance's `key` and `val`
 * MDB_vals. On completion, check for an error and if one occurred, set the
 * cursor to the unpositioned state. Finally record the containing
 * transaction's last mutation count, so we know if `key` and `val` become
 * invalid before the next attempt to read them.
 */
static int
_cursor_get_c(CursorObject *self, enum MDB_cursor_op op)
{
    int rc;

    ACTIVE_OPS_INC(self->trans->env);
    Py_BEGIN_ALLOW_THREADS;
    rc = mdb_cursor_get(self->curs, &self->key, &self->val, op);
    Py_END_ALLOW_THREADS;
    ACTIVE_OPS_DEC(self->trans->env);

    self->positioned = rc == 0;
    self->last_mutation = self->trans->mutations;
    if(rc) {
        self->key.mv_size = 0;
        self->val.mv_size = 0;
        if(rc != MDB_NOTFOUND) {
            if(! (rc == EINVAL && op == MDB_GET_CURRENT)) {
                err_set("mdb_cursor_get", rc);
                return -1;
            }
        }
    }
    return 0;
}

/**
 * Wrap _cursor_get_c() to return True or False depending on whether the
 * Cursor's final state is positioned.
 */
static PyObject *
_cursor_get(CursorObject *self, enum MDB_cursor_op op)
{
    if(! self->valid) {
        return err_invalid();
    }
    if(_cursor_get_c(self, op)) {
        return NULL;
    }
    return py_bool(self->positioned);
}

/**
 * Cursor.delete(dupdata=False) -> bool
 */
static PyObject *
cursor_delete(CursorObject *self, PyObject *args, PyObject *kwds)
{
    struct cursor_delete {
        int dupdata;
    } arg = {0};

    static const struct argspec argspec[] = {
        {"dupdata", ARG_BOOL, OFFSET(cursor_delete, dupdata)}
    };
    int res;

    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }

    res = 0;
    if(self->positioned) {
        int rc;
        int flags = arg.dupdata ? MDB_NODUPDATA : 0;
        DEBUG("deleting key '%.*s'",
              (int) self->key.mv_size,
              (char*) self->key.mv_data)
        ENV_UNLOCKED(self->trans->env, rc, mdb_cursor_del(self->curs, flags));
        self->trans->mutations++;
        if(rc) {
            return err_set("mdb_cursor_del", rc);
        }
        res = 1;
        _cursor_get_c(self, MDB_GET_CURRENT);
    }
    return py_bool(res);
}

/**
 * Cursor.first() -> bool
 */
static PyObject *
cursor_first(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    return _cursor_get(self, MDB_FIRST);
}

/**
 * Cursor.first_dup() -> bool
 */
static PyObject *
cursor_first_dup(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    return _cursor_get(self, MDB_FIRST_DUP);
}

static PyObject *
cursor_value(CursorObject *self, PyObject *Py_UNUSED(ignored));

/**
 * Cursor.getmulti() -> Iterable of (key, value)
 */
static PyObject *
cursor_get_multi(CursorObject *self, PyObject *args, PyObject *kwds)
{
    struct cursor_get {
        PyObject *keys;
        int dupdata;
        size_t dupfixed_bytes;
        int keyfixed;
        int values;
    } arg = {Py_None, 0, 0, 0, 1};

    int as_buffer;
    size_t i;
    PyObject *iter, *item, *tup, *key, *val;
    PyObject *pylist = NULL;
    MDB_cursor_op get_op, next_op;
    bool done, first;

    static const struct argspec argspec[] = {
        {"keys", ARG_OBJ, OFFSET(cursor_get, keys)},
        {"dupdata", ARG_BOOL, OFFSET(cursor_get, dupdata)},
        {"dupfixed_bytes", ARG_SIZE, OFFSET(cursor_get, dupfixed_bytes)},
        {"keyfixed", ARG_BOOL, OFFSET(cursor_get, keyfixed)},
        {"values", ARG_BOOL, OFFSET(cursor_get, values)}
    };

    size_t buffer_pos = 0, buffer_size = 8;
    size_t key_size = 0, val_size, item_size = 0;
    char *buffer = NULL;

    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }

    if(arg.dupfixed_bytes < 0) {
        return type_error("dupfixed_bytes must be a positive integer.");
    }else if ((arg.dupfixed_bytes > 0 || arg.keyfixed) && !arg.dupdata) {
        return type_error("dupdata is required for dupfixed_bytes/keyfixed.");
    }else if (arg.keyfixed && !arg.dupfixed_bytes){
        return type_error("dupfixed_bytes is required for keyfixed.");
    }else if (!arg.values && arg.dupdata) {
        return type_error("values=False is incompatible with dupdata.");
    }

    if(! ((iter = PyObject_GetIter(arg.keys)))) {
        return NULL;
    }

    /* Choose ops */
    if(arg.dupfixed_bytes) {
        get_op = MDB_GET_MULTIPLE;
        next_op = MDB_NEXT_MULTIPLE;
    } else {
        get_op = MDB_GET_CURRENT;
        next_op = MDB_NEXT_DUP;
    }

    as_buffer = self->trans->flags & TRANS_BUFFERS;
    val_size = arg.dupfixed_bytes;
    if (!arg.keyfixed){ /* Init list */
        pylist = PyList_New(0);
        if(! pylist) {
            Py_DECREF(iter);
            return NULL;
        }
    }
    first = true;
    while((item = PyIter_Next(iter))) {
        MDB_val mkey;
        BufViewList bvl;
        bufviewlist_init(&bvl);

        if(val_from_buffer(&mkey, item, &bvl)) {
            bufviewlist_release(&bvl);
            goto failiter;
        } /* val_from_buffer sets exception */

        self->key = mkey;
        if(_cursor_get_c(self, MDB_SET_KEY)) {
            bufviewlist_release(&bvl);
            goto failiter;
        }
        bufviewlist_release(&bvl);

        if(!arg.values) {
            /* values=False: append key only if it was found. */
            if(self->positioned) {
                key = obj_from_val(&self->key, as_buffer);
                if(key) {
                    PyList_Append(pylist, key);
                    Py_DECREF(key);
                } else {
                    goto failiter;
                }
            }
            Py_DECREF(item);
            continue;
        }

        done = false;
        while (!done) {

            if(! self->positioned) {
                done = true;
            }
            else if(_cursor_get_c(self, get_op)) {
                goto failiter;
            } else {
                key = obj_from_val(&self->key, as_buffer);
                ENV_PRELOAD_UNLOCKED(self->trans->env, 0, self->val.mv_data, self->val.mv_size);

                if(!arg.dupfixed_bytes) {
                    /* Not dupfixed, MDB_GET_CURRENT returns single item */
                    val = obj_from_val(&self->val, as_buffer);
                    tup = PyTuple_New(2);

                    if (tup && key && val) {
                        PyTuple_SET_ITEM(tup, 0, key);
                        PyTuple_SET_ITEM(tup, 1, val);
                        PyList_Append(pylist, tup);
                        Py_DECREF(tup);
                    } else {
                        Py_XDECREF(key);
                        Py_XDECREF(val);
                        Py_XDECREF(tup);
                        goto failiter;
                    }
                } else {
                    /* dupfixed, MDB_GET_MULTIPLE returns batch, iterate values */
                    size_t items = self->val.mv_size / val_size;
                    if (first) {
                        key_size = (size_t) self->key.mv_size;
                        /* key_size is from a (possibly adversarial) on-disk
                         * record; guard size arithmetic against overflow. */
                        if (SIZE_ADD_OVERFLOW(key_size, val_size)) {
                            PyErr_SetString(PyExc_OverflowError,
                                "key+value size overflow in getmulti "
                                "(corrupt database?)");
                            goto failiter;
                        }
                        item_size = key_size + val_size;
                        if (arg.keyfixed) { /* Init structured array buffer */
                            if (SIZE_MUL_OVERFLOW(buffer_size, item_size)) {
                                PyErr_NoMemory();
                                goto failiter;
                            }
                            buffer = malloc(buffer_size * item_size);
                            if (! buffer) {
                                PyErr_NoMemory();
                                goto failiter;
                            }
                        }
                        first = false;
                    }

                    for(i=0; i<items; i++) {
                        char *val_data = (char *) self->val.mv_data + (i * val_size);
                        if (arg.keyfixed) {
                            /* Add to array buffer */
                            char *k, *v;
                            if (buffer_pos >= buffer_size) { // Grow buffer
                                char *new_buffer;
                                size_t new_size;
                                if (buffer_size > SIZE_MAX / 2) {
                                    PyErr_NoMemory();
                                    goto failiter;
                                }
                                new_size = buffer_size * 2;
                                if (SIZE_MUL_OVERFLOW(new_size, item_size)) {
                                    PyErr_NoMemory();
                                    goto failiter;
                                }
                                new_buffer = realloc(buffer,
                                                     new_size * item_size);
                                if (! new_buffer) {
                                    /* Original `buffer` still valid; the
                                     * failiter cleanup frees it. */
                                    PyErr_NoMemory();
                                    goto failiter;
                                }
                                buffer = new_buffer;
                                buffer_size = new_size;
                            }
                            k = buffer + (buffer_pos * item_size);
                            v = k + key_size;
                            memcpy(k, (char *) self->key.mv_data, key_size);
                            memcpy(v, val_data, val_size);

                            buffer_pos++;
                        } else {
                            /* Add to list of tuples */
                            if(as_buffer) {
                                val = PyMemoryView_FromMemory(
                                    val_data, (size_t) arg.dupfixed_bytes, PyBUF_READ);
                            } else {
                                val = PyBytes_FromStringAndSize(
                                    val_data, (size_t) arg.dupfixed_bytes);
                            }
                            tup = PyTuple_New(2);
                            if (tup && key && val) {
                                Py_INCREF(key); // Hold key in loop
                                PyTuple_SET_ITEM(tup, 0, key);
                                PyTuple_SET_ITEM(tup, 1, val);
                                PyList_Append(pylist, tup);
                                Py_DECREF(tup);
                            } else {
                                Py_XDECREF(val);
                                Py_XDECREF(tup);
                                Py_XDECREF(key);
                                goto failiter;
                            }
                        }
                    }
                    Py_DECREF(key); // Release key
                }

                if(arg.dupdata){
                    if(_cursor_get_c(self, next_op)) {
                        goto failiter;
                    }
                }
                else {
                    done = true;
                }
            }
        }
        Py_DECREF(item);
    }

    Py_DECREF(iter);
    if(PyErr_Occurred()) {
        goto fail;
    }

    if (arg.keyfixed){
        size_t newsize;
        char *new_buffer;
        PyObject *owner, *mv;
        if (SIZE_MUL_OVERFLOW(buffer_pos, item_size)) {
            PyErr_NoMemory();
            goto fail;
        }
        newsize = buffer_pos * item_size;
        new_buffer = realloc(buffer, newsize);
        if(! new_buffer && newsize) {
            /* Original buffer still allocated; fail cleanup frees it. */
            PyErr_NoMemory();
            goto fail;
        }
        buffer = new_buffer;
        /* Wrap the buffer in a bytes object that owns the memory.
         * PyBytes_FromStringAndSize copies the data, then we free
         * the original buffer. */
        owner = PyBytes_FromStringAndSize(buffer, newsize);
        free(buffer);
        buffer = NULL;
        if(! owner) {
            goto fail;
        }
        mv = PyMemoryView_FromObject(owner);
        Py_DECREF(owner);
        return mv;
    } else {
        return pylist;
    }

failiter:
    Py_DECREF(item);
    Py_DECREF(iter);
fail:
    Py_XDECREF(pylist);
    if (buffer) {
        free(buffer);
    }
    return NULL;
}

/**
 * Cursor.get() -> result
 */
static PyObject *
cursor_get(CursorObject *self, PyObject *args, PyObject *kwds)
{
    struct cursor_get {
        MDB_val key;
        PyObject *default_;
    } arg = {{0, 0}, Py_None};

    static const struct argspec argspec[] = {
        {"key", ARG_BUF, OFFSET(cursor_get, key)},
        {"default", ARG_OBJ, OFFSET(cursor_get, default_)}
    };
    BufViewList bvl;
    PyObject *ret = NULL;

    bufviewlist_init(&bvl);
    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, &bvl)) {
        goto out;
    }

    if(! arg.key.mv_data) {
        type_error("key must be given.");
        goto out;
    }

    self->key = arg.key;
    if(_cursor_get_c(self, MDB_SET_KEY)) {
        goto out;
    }
    bufviewlist_release(&bvl);
    if(! self->positioned) {
        Py_INCREF(arg.default_);
        return arg.default_;
    }
    return cursor_value(self, NULL);

out:
    bufviewlist_release(&bvl);
    return ret;
}

/**
 * Cursor.item() -> (key, value)
 */
static PyObject *
cursor_item(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    int as_buffer;
    PyObject *key;
    PyObject *val;
    PyObject *tup;
    int rc = 0;

    if(! self->valid) {
        return err_invalid();
    }
    /* Must refresh `key` and `val` following mutation. */
    if(self->last_mutation != self->trans->mutations &&
       _cursor_get_c(self, MDB_GET_CURRENT)) {
        return NULL;
    }

    as_buffer = self->trans->flags & TRANS_BUFFERS;
    key = obj_from_val(&self->key, as_buffer);
    ENV_PRELOAD_UNLOCKED(self->trans->env, rc, self->val.mv_data, self->val.mv_size);
    val = obj_from_val(&self->val, as_buffer);
    tup = PyTuple_New(2);
    if(tup && key && val) {
        PyTuple_SET_ITEM(tup, 0, key);
        PyTuple_SET_ITEM(tup, 1, val);
        return tup;
    }
    Py_CLEAR(key);
    Py_CLEAR(val);
    Py_CLEAR(tup);
    return NULL;
}

/**
 * Cursor.key() -> result
 */
static PyObject *
cursor_key(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    if(! self->valid) {
        return err_invalid();
    }
    /* Must refresh `key` and `val` following mutation. */
    if(self->last_mutation != self->trans->mutations &&
       _cursor_get_c(self, MDB_GET_CURRENT)) {
        return NULL;
    }
    return obj_from_val(&self->key, self->trans->flags & TRANS_BUFFERS);
}

/**
 * Cursor.last() -> bool
 */
static PyObject *
cursor_last(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    return _cursor_get(self, MDB_LAST);
}

/**
 * Cursor.last_dup() -> bool
 */
static PyObject *
cursor_last_dup(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    return _cursor_get(self, MDB_LAST_DUP);
}

/**
 * Cursor.next() -> bool
 */
static PyObject *
cursor_next(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    return _cursor_get(self, MDB_NEXT);
}

/**
 * Cursor.next_dup() -> bool
 */
static PyObject *
cursor_next_dup(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    return _cursor_get(self, MDB_NEXT_DUP);
}

/**
 * Cursor.next_nodup() -> bool
 */
static PyObject *
cursor_next_nodup(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    return _cursor_get(self, MDB_NEXT_NODUP);
}

/**
 * Cursor.prev() -> bool
 */
static PyObject *
cursor_prev(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    return _cursor_get(self, MDB_PREV);
}

/**
 * Cursor.prev_dup() -> bool
 */
static PyObject *
cursor_prev_dup(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    return _cursor_get(self, MDB_PREV_DUP);
}

/**
 * Cursor.prev_nodup() -> bool
 */
static PyObject *
cursor_prev_nodup(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    return _cursor_get(self, MDB_PREV_NODUP);
}

/**
 * Cursor.putmulti(iter|dict) -> (consumed, added)
 */
static PyObject *
cursor_put_multi(CursorObject *self, PyObject *args, PyObject *kwds)
{
    struct cursor_put {
        PyObject *items;
        int dupdata;
        int overwrite;
        int append;
    } arg = {Py_None, 1, 1, 0};

    PyObject *iter;
    PyObject *item;

    static const struct argspec argspec[] = {
        {"items", ARG_OBJ, OFFSET(cursor_put, items)},
        {"dupdata", ARG_BOOL, OFFSET(cursor_put, dupdata)},
        {"overwrite", ARG_BOOL, OFFSET(cursor_put, overwrite)},
        {"append", ARG_BOOL, OFFSET(cursor_put, append)}
    };
    int flags;
    int rc;
    Py_ssize_t consumed;
    Py_ssize_t added;
    PyObject *ret = NULL;

    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }

    flags = 0;
    if(! arg.dupdata) {
        flags |= MDB_NODUPDATA;
    }
    if(! arg.overwrite) {
        flags |= MDB_NOOVERWRITE;
    }
    if(arg.append) {
        flags |= (self->trans->db->flags & MDB_DUPSORT) ? MDB_APPENDDUP : MDB_APPEND;
    }

    if(! ((iter = PyObject_GetIter(arg.items)))) {
        return NULL;
    }

    consumed = 0;
    added = 0;
    while((item = PyIter_Next(iter))) {
        MDB_val mkey, mval;
        BufViewList bvl;
        bufviewlist_init(&bvl);

        if(! (PyTuple_CheckExact(item) && PyTuple_GET_SIZE(item) == 2)) {
            PyErr_SetString(PyExc_TypeError,
                            "putmulti() elements must be 2-tuples");
            Py_DECREF(item);
            Py_DECREF(iter);
            return NULL;
        }

        if(val_from_buffer(&mkey, PyTuple_GET_ITEM(item, 0), &bvl) ||
           val_from_buffer(&mval, PyTuple_GET_ITEM(item, 1), &bvl)) {
            bufviewlist_release(&bvl);
            Py_DECREF(item);
            Py_DECREF(iter);
            return NULL; /* val_from_buffer sets exception */
        }

        ENV_UNLOCKED(self->trans->env, rc, mdb_cursor_put(self->curs, &mkey, &mval, flags));
        bufviewlist_release(&bvl);
        self->trans->mutations++;
        switch(rc) {
        case MDB_SUCCESS:
            added++;
            break;
        case MDB_KEYEXIST:
            break;
        default:
            Py_DECREF(item);
            Py_DECREF(iter);
            return err_format(rc, "mdb_cursor_put() element #%zd", consumed);
        }

        Py_DECREF(item);
        consumed++;
    }

    Py_DECREF(iter);
    if(! PyErr_Occurred()) {
        ret = Py_BuildValue("(nn)", consumed, added);
    }
    return ret;
}

/**
 * Cursor.put() -> bool
 */
static PyObject *
cursor_put(CursorObject *self, PyObject *args, PyObject *kwds)
{
    struct cursor_put {
        MDB_val key;
        MDB_val val;
        int dupdata;
        int overwrite;
        int append;
    } arg = {{0, 0}, {0, 0}, 1, 1, 0};

    static const struct argspec argspec[] = {
        {"key", ARG_BUF, OFFSET(cursor_put, key)},
        {"value", ARG_BUF, OFFSET(cursor_put, val)},
        {"dupdata", ARG_BOOL, OFFSET(cursor_put, dupdata)},
        {"overwrite", ARG_BOOL, OFFSET(cursor_put, overwrite)},
        {"append", ARG_BOOL, OFFSET(cursor_put, append)}
    };
    BufViewList bvl;
    int flags;
    int rc;

    bufviewlist_init(&bvl);
    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, &bvl)) {
        bufviewlist_release(&bvl);
        return NULL;
    }

    flags = 0;
    if(! arg.dupdata) {
        flags |= MDB_NODUPDATA;
    }
    if(! arg.overwrite) {
        flags |= MDB_NOOVERWRITE;
    }
    if(arg.append) {
        flags |= (self->trans->db->flags & MDB_DUPSORT) ? MDB_APPENDDUP : MDB_APPEND;
    }

    ENV_UNLOCKED(self->trans->env, rc, mdb_cursor_put(self->curs, &arg.key, &arg.val, flags));
    bufviewlist_release(&bvl);
    self->trans->mutations++;
    if(rc) {
        if(rc == MDB_KEYEXIST) {
            Py_RETURN_FALSE;
        }
        return err_set("mdb_put", rc);
    }
    Py_RETURN_TRUE;
}

/**
 * Shared between Cursor.replace() and Transaction.replace()
 */
static PyObject *
do_cursor_replace(CursorObject *self, MDB_val *key, MDB_val *val)
{
    int rc = 0;
    PyObject *old;
    MDB_val newval = *val;

    if(self->dbi_flags & MDB_DUPSORT) {
        self->key = *key;
        if(_cursor_get_c(self, MDB_SET_KEY)) {
            return NULL;
        }
        if(self->positioned) {
            ENV_PRELOAD_UNLOCKED(self->trans->env, rc, self->val.mv_data, self->val.mv_size);
            if(! ((old = obj_from_val(&self->val, 0)))) {
                return NULL;
            }
            ENV_UNLOCKED(self->trans->env, rc, mdb_cursor_del(self->curs, MDB_NODUPDATA));
            self->trans->mutations++;
            if(rc) {
                Py_CLEAR(old);
                return err_set("mdb_cursor_del", rc);
            }
        } else {
            old = Py_None;
            Py_INCREF(old);
        }
    } else {
        /* val is updated if MDB_KEYEXIST. */
        int flags = MDB_NOOVERWRITE;
        ENV_UNLOCKED(self->trans->env, rc, mdb_cursor_put(self->curs, key, val, flags));
        self->trans->mutations++;
        if(! rc) {
            Py_RETURN_NONE;
        } else if(rc != MDB_KEYEXIST) {
            return err_set("mdb_put", rc);
        }

        if(! ((old = obj_from_val(val, 0)))) {
            return NULL;
        }
    }

    ENV_UNLOCKED(self->trans->env, rc, mdb_cursor_put(self->curs, key, &newval, 0));
    if(rc) {
        Py_DECREF(old);
        return err_set("mdb_put", rc);
    }
    return old;
}

/**
 * Cursor.replace() -> None|result
 */
static PyObject *
cursor_replace(CursorObject *self, PyObject *args, PyObject *kwds)
{
    struct cursor_replace {
        MDB_val key;
        MDB_val val;
    } arg = {{0, 0}, {0, 0}};

    static const struct argspec argspec[] = {
        {"key", ARG_BUF, OFFSET(cursor_replace, key)},
        {"value", ARG_BUF, OFFSET(cursor_replace, val)}
    };
    BufViewList bvl;
    PyObject *ret;

    bufviewlist_init(&bvl);
    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, &bvl)) {
        bufviewlist_release(&bvl);
        return NULL;
    }

    ret = do_cursor_replace(self, &arg.key, &arg.val);
    bufviewlist_release(&bvl);
    return ret;
}

/**
 * Cursor.pop() -> None|result
 */
static PyObject *
cursor_pop(CursorObject *self, PyObject *args, PyObject *kwds)
{
    struct cursor_pop {
        MDB_val key;
    } arg = {{0, 0}};

    static const struct argspec argspec[] = {
        {"key", ARG_BUF, OFFSET(cursor_pop, key)},
    };
    BufViewList bvl;
    PyObject *old;
    int rc = 0;

    bufviewlist_init(&bvl);
    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, &bvl)) {
        goto out;
    }

    self->key = arg.key;
    if(_cursor_get_c(self, MDB_SET_KEY)) {
        goto out;
    }
    bufviewlist_release(&bvl);
    if(! self->positioned) {
        Py_RETURN_NONE;
    }
    ENV_PRELOAD_UNLOCKED(self->trans->env, rc, self->val.mv_data, self->val.mv_size);
    if(! ((old = obj_from_val(&self->val, 0)))) {
        return NULL;
    }

    ENV_UNLOCKED(self->trans->env, rc, mdb_cursor_del(self->curs, 0));
    self->trans->mutations++;
    if(rc) {
        Py_DECREF(old);
        return err_set("mdb_cursor_del", rc);
    }
    return old;

out:
    bufviewlist_release(&bvl);
    return NULL;
}

/**
 * Cursor.set_key(key) -> bool
 */
static PyObject *
cursor_set_key(CursorObject *self, PyObject *arg)
{
    BufViewList bvl;
    PyObject *ret;

    if(! self->valid) {
        return err_invalid();
    }
    bufviewlist_init(&bvl);
    if(val_from_buffer(&self->key, arg, &bvl)) {
        bufviewlist_release(&bvl);
        return NULL;
    }
    ret = _cursor_get(self, MDB_SET_KEY);
    bufviewlist_release(&bvl);
    return ret;
}

/**
 * Cursor.set_key_dup(key, value) -> bool
 */
static PyObject *
cursor_set_key_dup(CursorObject *self, PyObject *args, PyObject *kwds)
{
    struct cursor_set_key_dup {
        MDB_val key;
        MDB_val value;
    } arg = {{0, 0}, {0, 0}};

    static const struct argspec argspec[] = {
        {"key", ARG_BUF, OFFSET(cursor_set_key_dup, key)},
        {"value", ARG_BUF, OFFSET(cursor_set_key_dup, value)}
    };
    BufViewList bvl;
    PyObject *ret;

    bufviewlist_init(&bvl);
    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, &bvl)) {
        bufviewlist_release(&bvl);
        return NULL;
    }
    self->key = arg.key;
    self->val = arg.value;
    ret = _cursor_get(self, MDB_GET_BOTH);
    bufviewlist_release(&bvl);
    return ret;
}

/**
 * Cursor.set_range(key) -> bool
 */
static PyObject *
cursor_set_range(CursorObject *self, PyObject *arg)
{
    BufViewList bvl;
    PyObject *ret;

    if(! self->valid) {
        return err_invalid();
    }
    bufviewlist_init(&bvl);
    if(val_from_buffer(&self->key, arg, &bvl)) {
        bufviewlist_release(&bvl);
        return NULL;
    }
    if(self->key.mv_size) {
        ret = _cursor_get(self, MDB_SET_RANGE);
    } else {
        ret = _cursor_get(self, MDB_FIRST);
    }
    bufviewlist_release(&bvl);
    return ret;
}

/**
 * Cursor.set_range_dup(key, value) -> bool
 */
static PyObject *
cursor_set_range_dup(CursorObject *self, PyObject *args, PyObject *kwds)
{
    BufViewList bvl;
    PyObject *ret;
    struct cursor_set_range_dup {
        MDB_val key;
        MDB_val value;
    } arg = {{0, 0}, {0, 0}};

    static const struct argspec argspec[] = {
        {"key", ARG_BUF, OFFSET(cursor_set_range_dup, key)},
        {"value", ARG_BUF, OFFSET(cursor_set_range_dup, value)}
    };

    bufviewlist_init(&bvl);
    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, &bvl)) {
        bufviewlist_release(&bvl);
        return NULL;
    }

    self->key = arg.key;
    self->val = arg.value;
    ret = _cursor_get(self, MDB_GET_BOTH_RANGE);
    bufviewlist_release(&bvl);

    /* issue #126: MDB_GET_BOTH_RANGE does not satisfy its documentation, and
     * fails to update `key` and `value` on success. Therefore explicitly call
     * MDB_GET_CURRENT after MDB_GET_BOTH_RANGE. */
    _cursor_get_c(self, MDB_GET_CURRENT);

    return ret;
}

/**
 * Cursor.value() -> result
 */
static PyObject *
cursor_value(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    if(! self->valid) {
        return err_invalid();
    }
    /* Must refresh `key` and `val` following mutation. */
    if(self->last_mutation != self->trans->mutations &&
        _cursor_get_c(self, MDB_GET_CURRENT)) {
        return NULL;
    }
    ENV_PRELOAD_UNLOCKED(self->trans->env, 0, self->val.mv_data, self->val.mv_size);

    return obj_from_val(&self->val, self->trans->flags & TRANS_BUFFERS);
}

static PyObject *
new_iterator(CursorObject *cursor, IterValFunc val_func, MDB_cursor_op op)
{
    IterObject *iter = PyObject_New(IterObject, &PyIterator_Type);
    if (!iter) {
        return NULL;
    }

    iter->val_func = val_func;
    iter->curs = cursor;
    Py_INCREF(cursor);
    iter->started = 0;
    iter->op = op;

    DEBUG("new_iterator: %p", (void *)iter)
    return (PyObject *) iter;
}

static PyObject *
iter_from_args(CursorObject *self, PyObject *args, PyObject *kwds,
               signed int pos_op, enum MDB_cursor_op op,
               int keys_default, int values_default)
{
    struct iter_from_args {
        int keys;
        int values;
    } arg = {keys_default, values_default};

    static const struct argspec argspec[] = {
        {"keys", ARG_BOOL, OFFSET(iter_from_args, keys)},
        {"values", ARG_BOOL, OFFSET(iter_from_args, values)}
    };
    void *val_func;

    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }

    if(pos_op != -1 && !self->positioned) {
        if(_cursor_get_c(self, (enum MDB_cursor_op) pos_op)) {
            return NULL;
        }
    }

    if(! arg.values) {
        val_func = cursor_key;
    } else if(! arg.keys) {
        val_func = cursor_value;
    } else {
        val_func = cursor_item;
    }
    return new_iterator(self, val_func, op);
}

static PyObject *
cursor_iter(CursorObject *self)
{
    return iter_from_args(self, NULL, NULL, MDB_FIRST, MDB_NEXT, 1, 1);
}

/**
 * Cursor.iternext() -> Iterator
 */
static PyObject *
cursor_iternext(CursorObject *self, PyObject *args, PyObject *kwargs)
{
    return iter_from_args(self, args, kwargs, MDB_FIRST, MDB_NEXT, 1, 1);
}

/**
 * Cursor.iternext_dup() -> Iterator
 */
static PyObject *
cursor_iternext_dup(CursorObject *self, PyObject *args, PyObject *kwargs)
{
    return iter_from_args(self, args, kwargs, -1, MDB_NEXT_DUP, 0, 1);
}

/**
 * Cursor.iternext_nodup() -> Iterator
 */
static PyObject *
cursor_iternext_nodup(CursorObject *self, PyObject *args, PyObject *kwargs)
{
    return iter_from_args(self, args, kwargs, MDB_FIRST, MDB_NEXT_NODUP, 1, 0);
}

/**
 * Cursor.iterprev() -> Iterator
 */
static PyObject *
cursor_iterprev(CursorObject *self, PyObject *args, PyObject *kwargs)
{
    return iter_from_args(self, args, kwargs, MDB_LAST, MDB_PREV, 1, 1);
}

/**
 * Cursor.iterprev_dup() -> Iterator
 */
static PyObject *
cursor_iterprev_dup(CursorObject *self, PyObject *args, PyObject *kwargs)
{
    return iter_from_args(self, args, kwargs, -1, MDB_PREV_DUP, 0, 1);
}

/**
 * Cursor.iterprev_nodup() -> Iterator
 */
static PyObject *
cursor_iterprev_nodup(CursorObject *self, PyObject *args, PyObject *kwargs)
{
    return iter_from_args(self, args, kwargs, MDB_LAST, MDB_PREV_NODUP, 1, 0);
}

/**
 * Cursor._iter_from() -> Iterator
 */
static PyObject *
cursor_iter_from(CursorObject *self, PyObject *args)
{
    struct cursor_iter_from {
        MDB_val key;
        int reverse;
    } arg = {{0, 0}, 0};

    static const struct argspec argspec[] = {
        {"key", ARG_BUF, OFFSET(cursor_iter_from, key)},
        {"reverse", ARG_BOOL, OFFSET(cursor_iter_from, reverse)}
    };
    BufViewList bvl;
    enum MDB_cursor_op op;
    int rc;

    bufviewlist_init(&bvl);
    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, NULL, &arg, &bvl)) {
        bufviewlist_release(&bvl);
        return NULL;
    }

    if((! arg.key.mv_size) && (! arg.reverse)) {
        rc = _cursor_get_c(self, MDB_FIRST);
    } else {
        self->key = arg.key;
        rc = _cursor_get_c(self, MDB_SET_RANGE);
    }
    bufviewlist_release(&bvl);

    if(rc) {
        return NULL;
    }

    op = MDB_NEXT;
    if(arg.reverse) {
        op = MDB_PREV;
        if(! self->positioned) {
            if(_cursor_get_c(self, MDB_LAST)) {
                return NULL;
            }
        }
    }

    DEBUG("positioned? %d", self->positioned)
    return new_iterator(self, (void *)cursor_item, op);
}

/**
 * Cursor.__enter__()
 */
static PyObject *cursor_enter(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    Py_INCREF(self);
    return (PyObject *)self;
}

/**
 * Cursor.__exit__()
 */
static PyObject *cursor_exit(CursorObject *self, PyObject *args)
{
    cursor_clear(self);
    Py_RETURN_NONE;
}

/**
 * Cursor.close()
 */
static PyObject *cursor_close(CursorObject *self, PyObject *Py_UNUSED(ignored))
{
    cursor_clear(self);
    Py_RETURN_NONE;
}

static struct PyMethodDef cursor_methods[] = {
    {"__enter__", (PyCFunction)cursor_enter, METH_NOARGS},
    {"__exit__", (PyCFunction)cursor_exit, METH_VARARGS},
    {"close", (PyCFunction)cursor_close, METH_NOARGS},
    {"count", (PyCFunction)cursor_count, METH_NOARGS},
    {"delete", (PyCFunction)cursor_delete, METH_VARARGS|METH_KEYWORDS},
    {"first", (PyCFunction)cursor_first, METH_NOARGS},
    {"first_dup", (PyCFunction)cursor_first_dup, METH_NOARGS},
    {"get", (PyCFunction)cursor_get, METH_VARARGS|METH_KEYWORDS},
    {"getmulti", (PyCFunction)cursor_get_multi, METH_VARARGS|METH_KEYWORDS},
    {"item", (PyCFunction)cursor_item, METH_NOARGS},
    {"iternext", (PyCFunction)cursor_iternext, METH_VARARGS|METH_KEYWORDS},
    {"iternext_dup", (PyCFunction)cursor_iternext_dup, METH_VARARGS|METH_KEYWORDS},
    {"iternext_nodup", (PyCFunction)cursor_iternext_nodup, METH_VARARGS|METH_KEYWORDS},
    {"iterprev", (PyCFunction)cursor_iterprev, METH_VARARGS|METH_KEYWORDS},
    {"iterprev_dup", (PyCFunction)cursor_iterprev_dup, METH_VARARGS|METH_KEYWORDS},
    {"iterprev_nodup", (PyCFunction)cursor_iterprev_nodup, METH_VARARGS|METH_KEYWORDS},
    {"key", (PyCFunction)cursor_key, METH_NOARGS},
    {"last", (PyCFunction)cursor_last, METH_NOARGS},
    {"last_dup", (PyCFunction)cursor_last_dup, METH_NOARGS},
    {"next", (PyCFunction)cursor_next, METH_NOARGS},
    {"next_dup", (PyCFunction)cursor_next_dup, METH_NOARGS},
    {"next_nodup", (PyCFunction)cursor_next_nodup, METH_NOARGS},
    {"prev", (PyCFunction)cursor_prev, METH_NOARGS},
    {"prev_dup", (PyCFunction)cursor_prev_dup, METH_NOARGS},
    {"prev_nodup", (PyCFunction)cursor_prev_nodup, METH_NOARGS},
    {"put", (PyCFunction)cursor_put, METH_VARARGS|METH_KEYWORDS},
    {"putmulti", (PyCFunction)cursor_put_multi, METH_VARARGS|METH_KEYWORDS},
    {"replace", (PyCFunction)cursor_replace, METH_VARARGS|METH_KEYWORDS},
    {"pop", (PyCFunction)cursor_pop, METH_VARARGS|METH_KEYWORDS},
    {"set_key", (PyCFunction)cursor_set_key, METH_O},
    {"set_key_dup", (PyCFunction)cursor_set_key_dup, METH_VARARGS|METH_KEYWORDS},
    {"set_range", (PyCFunction)cursor_set_range, METH_O},
    {"set_range_dup", (PyCFunction)cursor_set_range_dup, METH_VARARGS|METH_KEYWORDS},
    {"value", (PyCFunction)cursor_value, METH_NOARGS},
    {"_iter_from", (PyCFunction)cursor_iter_from, METH_VARARGS},
    {NULL, NULL}
};

static PyTypeObject PyCursor_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "Cursor",                   /*tp_name*/
    sizeof(CursorObject),       /*tp_basicsize*/
    0,                          /*tp_itemsize*/
    (destructor) cursor_dealloc,/*tp_dealloc*/
    0,                          /*tp_print*/
    0,                          /*tp_getattr*/
    0,                          /*tp_setattr*/
    0,                          /*tp_compare*/
    0,                          /*tp_repr*/
    0,                          /*tp_as_number*/
    0,                          /*tp_as_sequence*/
    0,                          /*tp_as_mapping*/
    0,                          /*tp_hash*/
    0,                          /*tp_call*/
    0,                          /*tp_str*/
    0,                          /*tp_getattro*/
    0,                          /*tp_setattro*/
    0,                          /*tp_as_buffer*/
    Py_TPFLAGS_DEFAULT,         /*tp_flags*/
    0,                          /*tp_doc*/
    0,                          /*tp_traverse*/
    (inquiry) cursor_clear,     /*tp_clear*/
    0,                          /*tp_richcompare*/
    0,                          /*tp_weaklistoffset*/
    (getiterfunc)cursor_iter,   /*tp_iter*/
    0,                          /*tp_iternext*/
    cursor_methods,             /*tp_methods*/
    0,                          /*tp_members*/
    0,                          /*tp_getset*/
    0,                          /*tp_base*/
    0,                          /*tp_dict*/
    0,                          /*tp_descr_get*/
    0,                          /*tp_descr_set*/
    0,                          /*tp_dictoffset*/
    0,                          /*tp_init*/
    0,                          /*tp_alloc*/
    cursor_new,                 /*tp_new*/
    0,                          /*tp_free*/
};


/* --------- */
/* Iterators */
/* --------- */


/**
 * Iterator.__del__()
 */
static void
iter_dealloc(IterObject *self)
{
    DEBUG("destroying iterator")
    Py_CLEAR(self->curs);
    PyObject_Del(self);
}

/**
 * Iterator.__iter__() -> self
 */
static PyObject *
iter_iter(IterObject *self)
{
    Py_INCREF(self);
    return (PyObject *)self;
}

/**
 * Iterator.next() -> result
 */
static PyObject *
iter_next(IterObject *self)
{
    if(! self->curs->valid) {
        return err_invalid();
    }
    if(! self->curs->positioned) {
        return NULL;
    }

    if(self->started) {
        if(_cursor_get_c(self->curs, self->op)) {
            return NULL;
        }
        if(! self->curs->positioned) {
            return NULL;
        }
    }

    self->started = 1;
    return self->val_func(self->curs);
}

static PyTypeObject PyIterator_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "_Iterator",                 /*tp_name*/
    sizeof(IterObject),         /*tp_basicsize*/
    0,                          /*tp_itemsize*/
    (destructor) iter_dealloc,  /*tp_dealloc*/
    0,                          /*tp_print*/
    0,                          /*tp_getattr*/
    0,                          /*tp_setattr*/
    0,                          /*tp_compare*/
    0,                          /*tp_repr*/
    0,                          /*tp_as_number*/
    0,                          /*tp_as_sequence*/
    0,                          /*tp_as_mapping*/
    0,                          /*tp_hash*/
    0,                          /*tp_call*/
    0,                          /*tp_str*/
    0,                          /*tp_getattro*/
    0,                          /*tp_setattro*/
    0,                          /*tp_as_buffer*/
    Py_TPFLAGS_DEFAULT,         /*tp_flags*/
    0,                          /*tp_doc*/
    0,                          /*tp_traverse*/
    0,                          /*tp_clear*/
    0,                          /*tp_richcompare*/
    0,                          /*tp_weaklistoffset*/
    (getiterfunc)iter_iter,     /*tp_iter*/
    (iternextfunc)iter_next,    /*tp_iternext*/
    0,                          /*tp_methods*/
    0,                          /*tp_members*/
    0,                          /*tp_getset*/
    0,                          /*tp_base*/
    0,                          /*tp_dict*/
    0,                          /*tp_descr_get*/
    0,                          /*tp_descr_set*/
    0,                          /*tp_dictoffset*/
    0,                          /*tp_init*/
    0,                          /*tp_alloc*/
    0,                          /*tp_new*/
    0,                          /*tp_free*/
};



/* ------------ */
/* Transactions */
/* ------------ */

static void
txn_abort(MDB_txn *self)
{
    Py_BEGIN_ALLOW_THREADS
    MDEBUG("aborting")
    mdb_txn_abort(self);
    Py_END_ALLOW_THREADS
}


static int
trans_clear(TransObject *self)
{
    MDEBUG("clearing trans")
    self->valid = 0;
    INVALIDATE(self)
#ifdef HAVE_MEMSINK
    ms_notify((PyObject *) self, &self->sink_head);
#endif

    if(self->txn) {
        MDB_txn *txn = self->txn;
        self->txn = NULL;  /* Prevent double-abort from concurrent
                            * trans_clear calls (issue #180). */
        if(self->env && !(self->flags & TRANS_RDONLY)) {
            /* The abort below runs with the GIL held, so a #465 waiter
             * (which needs the GIL to proceed) cannot act on the cleared
             * tid before the write mutex is actually released. */
            CLEAR_WRITE_TXN_TID(self->env);
        }
        /* Don't release the GIL here — trans_clear is called from
         * invalidate() while iterating the children list, and releasing
         * the GIL would allow other threads to modify the list or free
         * objects we're still using.  The explicit trans_abort() path
         * releases the GIL for performance.  Issue #180.
         *
         * Skip the abort if the env's MDB_env* has been NULLed — this
         * means env_clear is inside mdb_env_close (GIL released) and
         * the txn's backing memory is being freed.  Issue #418. */
        if(!self->env || self->env->env) {
            mdb_txn_abort(txn);
        }
    }
    MDEBUG("db is/was %p", self->db)
    Py_CLEAR(self->db);
    if(self->env) {
        UNLINK_CHILD(self->env, self)
        Py_CLEAR(self->env);
    }
    return 0;
}

/**
 * Transaction.__del__()
 */
static void
trans_dealloc(TransObject *self)
{
    MDB_txn * txn = self->txn;
    if(self->weaklist != NULL) {
        MDEBUG("Clearing weaklist..")
        PyObject_ClearWeakRefs((PyObject *) self);
    }

    if(self->env && self->env->pid == _cached_pid) {
        if(self->env->valid && self->env->env && txn &&
                !self->env->spare_txn &&
                self->env->max_spare_txns && (self->flags & TRANS_RDONLY)) {
            MDEBUG("caching trans")
            mdb_txn_reset(txn);
            self->env->spare_txn = txn;
            self->txn = NULL;
        } else if(txn && !(self->flags & TRANS_RDONLY)) {
            /* Abort write txn with GIL released — mdb_txn_abort may do I/O
             * flushing dirty pages.  Safe here because trans_dealloc is the
             * normal refcount-to-zero path, not called from invalidate()'s
             * child list iteration.  trans_clear will see txn==NULL and skip
             * the abort.  Issue #180.
             *
             * Skip if env's MDB_env* is NULL — env_clear is inside
             * mdb_env_close and the txn memory is being freed.  #418.
             *
             * active_ops keeps env_clear from calling mdb_env_close while
             * this abort is in flight.  Issue #180. */
            self->txn = NULL;
            if(self->env) {
                if(self->env->env) {
                    ACTIVE_OPS_INC(self->env);
                    txn_abort(txn);
                    ACTIVE_OPS_DEC(self->env);
                }
                /* Mutex released on this (owning) thread; wakes a
                 * close() blocked waiting for it.  Cleared after the
                 * abort so waiters never observe tid == 0 while the
                 * transaction is still live.  Issues #465, #475. */
                CLEAR_WRITE_TXN_TID(self->env);
            }
        }
        MDEBUG("deleting trans")
        trans_clear(self);
    }
    else {
       MDEBUG("In forked process, not deleting trans");
       /* Can't touch LMDB handles after fork, but still need to
        * release Python references to avoid leaking env/db. */
       Py_CLEAR(self->db);
       if(self->env) {
           UNLINK_CHILD(self->env, self)
           Py_CLEAR(self->env);
       }
    }

    PyObject_Del(self);
}

/**
 * Transaction(env, db) -> new instance.
 */
static PyObject *
trans_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    struct trans_new {
        EnvObject *env;
        DbObject *db;
        TransObject *parent;
        int write;
        int buffers;
    } arg = {NULL, NULL, NULL, 0, 0};

    static const struct argspec argspec[] = {
        {"env", ARG_ENV, OFFSET(trans_new, env)},
        {"db", ARG_DB, OFFSET(trans_new, db)},
        {"parent", ARG_TRANS, OFFSET(trans_new, parent)},
        {"write", ARG_BOOL, OFFSET(trans_new, write)},
        {"buffers", ARG_BOOL, OFFSET(trans_new, buffers)}
    };

    static PyObject *cache = NULL;
    if(parse_args(1, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }
    if(! arg.env) {
        return type_error("'env' argument required");
    }
    return make_trans(arg.env, arg.db, arg.parent, arg.write, arg.buffers);
}

/**
 * Transaction.abort()
 */
static PyObject *
trans_abort(TransObject *self, PyObject *Py_UNUSED(ignored))
{
    if(self->valid) {
        self->valid = 0;  /* Prevent concurrent trans_clear (issue #180). */
        /* Save txn and env before INVALIDATE, which may release the GIL
         * and allow concurrent trans_clear to NULL them. */
        MDB_txn *txn = self->txn;
        EnvObject *env = self->env;
        self->txn = NULL;  /* Prevent double-abort (issue #180). */
        Py_XINCREF((PyObject *) env);
        DEBUG("invalidate")
        INVALIDATE(self)
#ifdef HAVE_MEMSINK
        ms_notify((PyObject *) self, &self->sink_head);
#endif
        if(self->flags & TRANS_RDONLY) {
            DEBUG("resetting")
            /* Reset to spare state, ready for _dealloc to freelist it. */
            if(txn) {
                mdb_txn_reset(txn);
                self->txn = txn;  /* Restore for spare_txn caching. */
            }
            self->flags |= TRANS_SPARE;
        } else {
            if(txn && env) {
                DEBUG("aborting")
                /* Wait for child ops (e.g. child commit) to finish before
                 * aborting, since LMDB doesn't allow concurrent access to
                 * parent+child txns.  Issue #180.
                 *
                 * write_txn_tid stays set until the abort completes: the
                 * waits below release the GIL while the MDB txn (and the
                 * writer mutex) is still live, and clearing the tid early
                 * would let a concurrent set_mapsize() pass its write-txn
                 * check and then find the txn undead at remap time (its
                 * invalidation pass skips this txn because self->txn was
                 * already NULLed above).  Issues #465, #475. */
                if(LMDB_ATOMIC_LOAD(&env->active_ops) > 0) {
                    ENV_WAIT_WHILE(env, LMDB_ATOMIC_LOAD(&env->active_ops) > 0);
                }
                ACTIVE_OPS_INC(env);
                Py_BEGIN_ALLOW_THREADS
                mdb_txn_abort(txn);
                Py_END_ALLOW_THREADS
                ACTIVE_OPS_DEC(env);
                /* Mutex released on this (owning) thread; wakes a close()
                 * blocked waiting for it.  Issue #465. */
                CLEAR_WRITE_TXN_TID(env);
            } else {
                if(txn) {
                    mdb_txn_abort(txn);
                }
                if(env) {
                    CLEAR_WRITE_TXN_TID(env);
                }
            }
        }
        Py_XDECREF((PyObject *) env);
    }
    Py_RETURN_NONE;
}

/**
 * Transaction.commit()
 */
static PyObject *
trans_commit(TransObject *self, PyObject *Py_UNUSED(ignored))
{
    int rc;

    if(! self->valid) {
        return err_invalid();
    }
    self->valid = 0;  /* Prevent new operations from starting (issue #180). */
    DEBUG("invalidate")
    INVALIDATE(self)
#ifdef HAVE_MEMSINK
    ms_notify((PyObject *) self, &self->sink_head);
#endif
    if(self->flags & TRANS_RDONLY) {
        DEBUG("resetting")
        /* Reset to spare state, ready for _dealloc to freelist it. */
        mdb_txn_reset(self->txn);
        self->flags |= TRANS_SPARE;
    } else {
        /* Save txn/env locally before releasing the GIL.  mdb_txn_commit
         * frees the MDB_txn, so self->txn must be NULLed first to prevent
         * concurrent trans_clear from calling mdb_txn_abort on freed memory.
         * Similarly, Py_INCREF the env since trans_clear may Py_CLEAR it
         * while the GIL is released.  Issue #180. */
        MDB_txn *txn = self->txn;
        EnvObject *env = self->env;
        self->txn = NULL;
        Py_INCREF((PyObject *) env);
        DEBUG("committing")
        ACTIVE_OPS_INC(env);
        Py_BEGIN_ALLOW_THREADS
        rc = mdb_txn_commit(txn);
        Py_END_ALLOW_THREADS
        ACTIVE_OPS_DEC(env);
        /* Mutex released on this (owning) thread; wakes a close() blocked
         * waiting for it.  Cleared after the commit so waiters never
         * observe tid == 0 while the transaction is still live.
         * Issues #465, #475. */
        CLEAR_WRITE_TXN_TID(env);
        Py_DECREF((PyObject *) env);
        if(rc) {
            return err_set("mdb_txn_commit", rc);
        }
    }
    Py_RETURN_NONE;
}

/**
 * Transaction.cursor() -> Cursor
 */
static PyObject *
trans_cursor(TransObject *self, PyObject *args, PyObject *kwds)
{
    struct trans_cursor {
        DbObject *db;
    } arg = {self->db};

    static const struct argspec argspec[] = {
        {"db", ARG_DB, OFFSET(trans_cursor, db)}
    };

    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }
    return make_cursor(arg.db, self);
}

/**
 * Transaction.delete() -> bool
 */
static PyObject *
trans_delete(TransObject *self, PyObject *args, PyObject *kwds)
{
    struct trans_delete {
        MDB_val key;
        MDB_val val;
        DbObject *db;
    } arg = {{0, 0}, {0, 0}, self->db};

    static const struct argspec argspec[] = {
        {"key", ARG_BUF, OFFSET(trans_delete, key)},
        {"value", ARG_BUF, OFFSET(trans_delete, val)},
        {"db", ARG_DB, OFFSET(trans_delete, db)}
    };
    BufViewList bvl;
    MDB_val *val_ptr;
    int rc;

    bufviewlist_init(&bvl);
    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, &bvl)) {
        goto out;
    }
    if(! db_owner_check(arg.db, self->env)) {
        goto out;
    }
    val_ptr = arg.val.mv_size ? &arg.val : NULL;
    self->mutations++;
    ENV_UNLOCKED(self->env, rc, mdb_del(self->txn, arg.db->dbi, &arg.key, val_ptr));
    bufviewlist_release(&bvl);
    if(rc) {
        if(rc == MDB_NOTFOUND) {
             Py_RETURN_FALSE;
        }
        return err_set("mdb_del", rc);
    }
    Py_RETURN_TRUE;

out:
    bufviewlist_release(&bvl);
    return NULL;
}

/**
 * Transaction.drop(db)
 */
static PyObject *
trans_drop(TransObject *self, PyObject *args, PyObject *kwds)
{
    struct trans_drop {
        DbObject *db;
        int delete;
    } arg = {NULL, 1};

    static const struct argspec argspec[] = {
        {"db", ARG_DB, OFFSET(trans_drop, db)},
        {"delete", ARG_BOOL, OFFSET(trans_drop, delete)}
    };
    int rc;

    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }
    if(! arg.db) {
        return type_error("'db' argument required.");
    } else if(! db_owner_check(arg.db, self->env)) {
        return NULL;
    }

    ENV_UNLOCKED(self->env, rc, mdb_drop(self->txn, arg.db->dbi, arg.delete));
    self->mutations++;
    if(rc) {
        return err_set("mdb_drop", rc);
    }
    Py_RETURN_NONE;
}

/**
 * Transaction.get() -> result
 */
static PyObject *
trans_get(TransObject *self, PyObject *args, PyObject *kwds)
{
    struct trans_get {
        MDB_val key;
        PyObject *default_;
        DbObject *db;
    } arg = {{0, 0}, Py_None, self->db};

    static const struct argspec argspec[] = {
        {"key", ARG_BUF, OFFSET(trans_get, key)},
        {"default", ARG_OBJ, OFFSET(trans_get, default_)},
        {"db", ARG_DB, OFFSET(trans_get, db)}
    };
    BufViewList bvl;
    MDB_val val;
    int rc;

    bufviewlist_init(&bvl);
    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, &bvl)) {
        goto out;
    }
    if(! db_owner_check(arg.db, self->env)) {
        goto out;
    }

    if(! arg.key.mv_data) {
        type_error("key must be given.");
        goto out;
    }

    ACTIVE_OPS_INC(self->env);
    Py_BEGIN_ALLOW_THREADS
    rc = mdb_get(self->txn, arg.db->dbi, &arg.key, &val);
    preload(rc, val.mv_data, val.mv_size);
    Py_END_ALLOW_THREADS
    ACTIVE_OPS_DEC(self->env);
    bufviewlist_release(&bvl);

    if(rc) {
        if(rc == MDB_NOTFOUND) {
            Py_INCREF(arg.default_);
            return arg.default_;
        }
        return err_set("mdb_get", rc);
    }
    return obj_from_val(&val, self->flags & TRANS_BUFFERS);

out:
    bufviewlist_release(&bvl);
    return NULL;
}

/**
 * Transaction.put() -> bool
 */
static PyObject *
trans_put(TransObject *self, PyObject *args, PyObject *kwds)
{
    struct trans_put {
        MDB_val key;
        MDB_val value;
        int dupdata;
        int overwrite;
        int append;
        DbObject *db;
    } arg = {{0, 0}, {0, 0}, 1, 1, 0, self->db};

    static const struct argspec argspec[] = {
        {"key", ARG_BUF, OFFSET(trans_put, key)},
        {"value", ARG_BUF, OFFSET(trans_put, value)},
        {"dupdata", ARG_BOOL, OFFSET(trans_put, dupdata)},
        {"overwrite", ARG_BOOL, OFFSET(trans_put, overwrite)},
        {"append", ARG_BOOL, OFFSET(trans_put, append)},
        {"db", ARG_DB, OFFSET(trans_put, db)}
    };
    BufViewList bvl;
    int flags;
    int rc;

    bufviewlist_init(&bvl);
    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, &bvl)) {
        goto out;
    }
    if(! db_owner_check(arg.db, self->env)) {
        goto out;
    }

    flags = 0;
    if(! arg.dupdata) {
        flags |= MDB_NODUPDATA;
    }
    if(! arg.overwrite) {
        flags |= MDB_NOOVERWRITE;
    }
    if(arg.append) {
        flags |= MDB_APPEND;
    }

    DEBUG("inserting '%.*s' (%d) -> '%.*s' (%d)",
        (int)arg.key.mv_size, (char *)arg.key.mv_data,
        (int)arg.key.mv_size,
        (int)arg.value.mv_size, (char *)arg.value.mv_data,
        (int)arg.value.mv_size)

    self->mutations++;
    ENV_UNLOCKED(self->env, rc, mdb_put(self->txn, (arg.db)->dbi,
                         &arg.key, &arg.value, flags));
    bufviewlist_release(&bvl);
    if(rc) {
        if(rc == MDB_KEYEXIST) {
            Py_RETURN_FALSE;
        }
        return err_set("mdb_put", rc);
    }
    Py_RETURN_TRUE;

out:
    bufviewlist_release(&bvl);
    return NULL;
}

static PyObject *
make_cursor(DbObject *db, TransObject *trans);
static PyObject *
do_cursor_replace(CursorObject *self, MDB_val *key, MDB_val *val);

/**
 * Transaction.replace() -> None|result
 */
static PyObject *
trans_replace(TransObject *self, PyObject *args, PyObject *kwds)
{
    struct trans_replace {
        MDB_val key;
        MDB_val value;
        DbObject *db;
    } arg = {{0, 0}, {0, 0}, self->db};

    static const struct argspec argspec[] = {
        {"key", ARG_BUF, OFFSET(trans_replace, key)},
        {"value", ARG_BUF, OFFSET(trans_replace, value)},
        {"db", ARG_DB, OFFSET(trans_replace, db)}
    };
    BufViewList bvl;
    PyObject *ret = NULL;
    CursorObject *cursor;

    bufviewlist_init(&bvl);
    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, &bvl)) {
        goto out;
    }
    if(! db_owner_check(arg.db, self->env)) {
        goto out;
    }

    cursor = (CursorObject *) make_cursor(arg.db, self);
    if(cursor) {
        ret = do_cursor_replace(cursor, &arg.key, &arg.value);
        Py_DECREF(cursor);
    }

out:
    bufviewlist_release(&bvl);
    return ret;
}

static int
_cursor_get_c(CursorObject *self, enum MDB_cursor_op op);

/**
 * Transaction.pop() -> None|result
 */
static PyObject *
trans_pop(TransObject *self, PyObject *args, PyObject *kwds)
{
    struct trans_pop {
        MDB_val key;
        DbObject *db;
    } arg = {{0, 0}, self->db};

    static const struct argspec argspec[] = {
        {"key", ARG_BUF, OFFSET(trans_pop, key)},
        {"db", ARG_DB, OFFSET(trans_pop, db)}
    };
    BufViewList bvl;
    CursorObject *cursor;
    PyObject *old;
    int rc = 0;

    bufviewlist_init(&bvl);
    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, &bvl)) {
        goto out;
    }
    if(! db_owner_check(arg.db, self->env)) {
        goto out;
    }

    if(! ((cursor = (CursorObject *) make_cursor(arg.db, self)))) {
        goto out;
    }

    cursor->key = arg.key;
    if(_cursor_get_c(cursor, MDB_SET_KEY)) {
        bufviewlist_release(&bvl);
        Py_DECREF((PyObject *)cursor);
        return NULL;
    }
    bufviewlist_release(&bvl);
    if(! cursor->positioned) {
        Py_DECREF((PyObject *)cursor);
        Py_RETURN_NONE;
    }

    ENV_PRELOAD_UNLOCKED(self->env, rc, cursor->val.mv_data, cursor->val.mv_size);
    if(! ((old = obj_from_val(&cursor->val, 0)))) {
        Py_DECREF((PyObject *)cursor);
        return NULL;
    }

    ENV_UNLOCKED(self->env, rc, mdb_cursor_del(cursor->curs, 0));
    Py_DECREF((PyObject *)cursor);
    self->mutations++;
    if(rc) {
        Py_DECREF(old);
        return err_set("mdb_cursor_del", rc);
    }
    return old;

out:
    bufviewlist_release(&bvl);
    return NULL;
}

/**
 * Transaction.__enter__()
 */
static PyObject *trans_enter(TransObject *self, PyObject *Py_UNUSED(ignored))
{
    if(! self->valid) {
        return err_invalid();
    }
    Py_INCREF(self);
    return (PyObject *)self;
}

/**
 * Transaction.__exit__()
 */
static PyObject *trans_exit(TransObject *self, PyObject *args)
{
    if(! self->valid) {
        return err_invalid();
    }
    if(PyTuple_GET_ITEM(args, 0) == Py_None) {
        return trans_commit(self, NULL);
    } else {
        return trans_abort(self, NULL);
    }
}

/**
 * Transaction.id() -> int
 */
static PyObject *trans_id(TransObject *self, PyObject *Py_UNUSED(ignored))
{
    size_t id;

    if(! self->valid) {
        return err_invalid();
    }

    id = mdb_txn_id(self->txn);
    return PyLong_FromUnsignedLong(id);
}

/**
 * Transaction.stat() -> dict
 */
static PyObject *
trans_stat(TransObject *self, PyObject *args, PyObject *kwds)
{
    struct trans_stat {
        DbObject *db;
    } arg = {self->db};

    static const struct argspec argspec[] = {
        {"db", ARG_DB, OFFSET(trans_stat, db)}
    };
    MDB_stat st;
    int rc;

    static PyObject *cache = NULL;
    if(parse_args(self->valid, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }
    if(! db_owner_check(arg.db, self->env)) {
        return NULL;
    }

    ENV_UNLOCKED(self->env, rc, mdb_stat(self->txn, arg.db->dbi, &st));
    if(rc) {
        return err_set("mdb_stat", rc);
    }
    return dict_from_fields(&st, mdb_stat_fields);
}

static struct PyMethodDef trans_methods[] = {
    {"__enter__", (PyCFunction)trans_enter, METH_NOARGS},
    {"__exit__", (PyCFunction)trans_exit, METH_VARARGS},
    {"abort", (PyCFunction)trans_abort, METH_NOARGS},
    {"commit", (PyCFunction)trans_commit, METH_NOARGS},
    {"cursor", (PyCFunction)trans_cursor, METH_VARARGS|METH_KEYWORDS},
    {"delete", (PyCFunction)trans_delete, METH_VARARGS|METH_KEYWORDS},
    {"drop", (PyCFunction)trans_drop, METH_VARARGS|METH_KEYWORDS},
    {"get", (PyCFunction)trans_get, METH_VARARGS|METH_KEYWORDS},
    {"put", (PyCFunction)trans_put, METH_VARARGS|METH_KEYWORDS},
    {"replace", (PyCFunction)trans_replace, METH_VARARGS|METH_KEYWORDS},
    {"pop", (PyCFunction)trans_pop, METH_VARARGS|METH_KEYWORDS},
    {"id", (PyCFunction)trans_id, METH_NOARGS},
    {"stat", (PyCFunction)trans_stat, METH_VARARGS|METH_KEYWORDS},
    {NULL, NULL}
};

static PyTypeObject PyTransaction_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "Transaction",              /*tp_name*/
    sizeof(TransObject),        /*tp_basicsize*/
    0,                          /*tp_itemsize*/
    (destructor) trans_dealloc, /*tp_dealloc*/
    0,                          /*tp_print*/
    0,                          /*tp_getattr*/
    0,                          /*tp_setattr*/
    0,                          /*tp_compare*/
    0,                          /*tp_repr*/
    0,                          /*tp_as_number*/
    0,                          /*tp_as_sequence*/
    0,                          /*tp_as_mapping*/
    0,                          /*tp_hash*/
    0,                          /*tp_call*/
    0,                          /*tp_str*/
    0,                          /*tp_getattro*/
    0,                          /*tp_setattro*/
    0,                          /*tp_as_buffer*/
    Py_TPFLAGS_DEFAULT,         /*tp_flags*/
    0,                          /*tp_doc*/
    0,                          /*tp_traverse*/
    (inquiry) trans_clear,      /*tp_clear*/
    0,                          /*tp_richcompare*/
    offsetof(TransObject, weaklist), /*tp_weaklistoffset*/
    0,                          /*tp_iter*/
    0,                          /*tp_iternext*/
    trans_methods,              /*tp_methods*/
    0,                          /*tp_members*/
    0,                          /*tp_getset*/
    0,                          /*tp_base*/
    0,                          /*tp_dict*/
    0,                          /*tp_descr_get*/
    0,                          /*tp_descr_set*/
    0,                          /*tp_dictoffset*/
    0,                          /*tp_init*/
    0,                          /*tp_alloc*/
    trans_new,                  /*tp_new*/
    0,                          /*tp_free*/
};


/**
 * Construct a PyString and append it to a list, returning 0 on success or -1
 * on error.
 */
static int
append_string(PyObject *list, const char *s)
{
    PyObject *o = PyUnicode_FromString(s);

    if(! o) {
        return -1;
    }
    if(PyList_Append(list, o)) {
        Py_DECREF(o);
        return -1;
    }
    Py_DECREF(o);
    return 0;
}


/**
 * lmdb.enable_drop_gil()
 */
static PyObject *
enable_drop_gil(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(ignored))
{
    Py_RETURN_NONE;
}

/**
 * lmdb.get_version() -> tuple
 */
static PyObject *
get_version(PyObject *mod, PyObject *args, PyObject *kwds)
{
    struct version_args {
        int subpatch;
    } arg = {0};

    static const struct argspec argspec[] = {
        {"subpatch", ARG_BOOL, OFFSET(version_args, subpatch)},
    };

    static PyObject *cache = NULL;
    if(parse_args(1, SPECSIZE(), argspec, &cache, args, kwds, &arg, NULL)) {
        return NULL;
    }

    if (arg.subpatch) {
#ifdef HAVE_PATCHED_LMDB
        const int subpatch = 1;
#else
        const int subpatch = 0;
#endif
        return Py_BuildValue("iiii", MDB_VERSION_MAJOR,
            MDB_VERSION_MINOR, MDB_VERSION_PATCH, subpatch);
    }
    return Py_BuildValue("iii", MDB_VERSION_MAJOR,
        MDB_VERSION_MINOR, MDB_VERSION_PATCH);
}

static struct PyMethodDef module_methods[] = {
    {"enable_drop_gil", (PyCFunction) enable_drop_gil, METH_NOARGS, ""},
    {"version", (PyCFunction) get_version, METH_VARARGS|METH_KEYWORDS, ""},
    {0, 0, 0, 0}
};

static struct PyModuleDef moduledef = {
    PyModuleDef_HEAD_INIT,
    "cpython",
    NULL,
    -1,
    module_methods,
    NULL,
    NULL,
    NULL,
    NULL
};

/**
 * Initialize and publish the LMDB built-in types.
 */
static int init_types(PyObject *mod, PyObject *__all__)
{
    static PyTypeObject *types[] = {
        &PyEnvironment_Type,
        &PyCursor_Type,
        &PyTransaction_Type,
        &PyIterator_Type,
        &PyDatabase_Type,
        NULL
    };

    int i;
    for(i = 0; types[i]; i++) {
        PyTypeObject *type = types[i];
        char const * name = type->tp_name;

        if(PyType_Ready(type)) {
            return -1;
        }
        if(PyObject_SetAttrString(mod, name, (PyObject *)type)) {
            return -1;
        }

        /* As a special case, export _Database */
        if((name[0] != '_' || 0 == strcmp(name, "_Database")) &&
           append_string(__all__, name)) {
            return -1;
        }
    }

    return 0;
}

/**
 * Initialize a bunch of constants used to ease number compares.
 */
static int init_constants(PyObject *mod)
{
    if(! ((py_zero = PyLong_FromUnsignedLongLong(0)))) {
        return -1;
    }
    if(! ((py_int_max = PyLong_FromUnsignedLongLong(INT_MAX)))) {
        return -1;
    }
    if(! ((py_size_max = PyLong_FromUnsignedLongLong(SIZE_MAX)))) {
        return -1;
    }
    return 0;
}

/**
 * Create lmdb.Error exception class, and one subclass for each entry in
 * `error_map`.
 */
static int init_errors(PyObject *mod, PyObject *__all__)
{
    size_t count;
    char qualname[64];
    size_t i;

    Error = PyErr_NewException("lmdb.Error", NULL, NULL);
    if(! Error) {
        return -1;
    }
    if(PyObject_SetAttrString(mod, "Error", Error)) {
        return -1;
    }
    if(append_string(__all__, "Error")) {
        return -1;
    }

    count = (sizeof error_map / sizeof error_map[0]);
    error_tbl = malloc(sizeof(PyObject *) * count);
    if(! error_tbl) {
        return -1;
    }

    for(i = 0; i < count; i++) {
        const struct error_map *error = &error_map[i];
        PyObject *klass;

        snprintf(qualname, sizeof qualname, "lmdb.%s", error->name);
        qualname[sizeof qualname - 1] = '\0';

        if(! ((klass = PyErr_NewException(qualname, Error, NULL)))) {
            return -1;
        }

        error_tbl[i] = klass;
        if(PyObject_SetAttrString(mod, error->name, klass)) {
            return -1;
        }
        if(append_string(__all__, error->name)) {
            return -1;
        }
    }
    return 0;
}

/**
 * Update cached PID in the child process after fork().
 */
#ifndef _WIN32
static void
_atfork_child(void)
{
    _cached_pid = getpid();
}
#endif

/**
 * Do all required to initialize the lmdb.cpython module.
 */
PyMODINIT_FUNC
MODINIT_NAME(void)
{
    PyObject *__all__;
    PyObject *mod = PyModule_Create(&moduledef);
    if(! mod) {
        MOD_RETURN(NULL);
    }

    if(! ((__all__ = PyList_New(0)))) {
        MOD_RETURN(NULL);
    }

    if(! ((open_env_paths = PySet_New(NULL)))) {
        MOD_RETURN(NULL);
    }

    _cached_pid = getpid();
#ifndef _WIN32
    if(pthread_atfork(NULL, NULL, _atfork_child)) {
        PyErr_SetString(PyExc_RuntimeError,
            "lmdb: pthread_atfork() failed");
        MOD_RETURN(NULL);
    }
#endif

    if(init_types(mod, __all__)) {
        MOD_RETURN(NULL);
    }
    if(append_string(__all__, "enable_drop_gil")) {
        MOD_RETURN(NULL);
    }
    if(append_string(__all__, "version")) {
        MOD_RETURN(NULL);
    }

#ifdef HAVE_MEMSINK
    MemSink_IMPORT;
    if(ms_init_source(&PyTransaction_Type, offsetof(TransObject, sink_head))) {
        MOD_RETURN(NULL);
    }
#endif

#ifdef _WIN32
    if(! ((msvcrt = PyImport_ImportModule("msvcrt")))) {
        MOD_RETURN(NULL);
    }
#endif

    if(init_constants(mod)) {
        MOD_RETURN(NULL);
    }
    if(init_errors(mod, __all__)) {
        MOD_RETURN(NULL);
    }
    if(PyObject_SetAttrString(mod, "open", (PyObject *)&PyEnvironment_Type)) {
        MOD_RETURN(NULL);
    }

    if(PyObject_SetAttrString(mod, "__all__", __all__)) {
        MOD_RETURN(NULL);
    }
    Py_DECREF(__all__);

    MOD_RETURN(mod);
}
