Sample code

code/wraplib

A full wrapper around a fictional C library: opaque handle, explicit close, error translation, callbacks, and a capsule.

Build and run
cd code/wraplib
python -m pip install -e . --no-build-isolation -v
python -m pytest -q

Sources on disk are under code/wraplib/ and are compilable as-is. Each section below links to its original file.

Files

wraplib.c

/*
 * wraplib -- a complete wrapper around the fictional "tinylib" C library.
 *
 * Demonstrates the patterns every wrapper needs:
 *   - a heap type holding an opaque handle, with GC support
 *   - explicit close(), idempotent, plus __enter__/__exit__ and a
 *     tp_finalize backstop that emits a ResourceWarning
 *   - a handle validity check in every method (no use-after-close crashes)
 *   - one error-translation function mapping library codes to exceptions
 *   - a stored Python callback: strong reference, traversed, invoked with
 *     the thread state attached, exceptions routed to the unraisable hook
 *   - a PyCapsule exposing the raw handle without exposing a bare pointer
 *
 * Build:  pip install -e . --no-build-isolation
 * Lessons: 03, 04, 05, 06, 08, 10, 13
 */

#include <Python.h>
#include <limits.h>
#include <stddef.h>
#include <string.h>

#include "tinylib.h"

#define CAPSULE_NAME "wraplib.tiny_handle"

/* ================================================================== *
 * module state
 * ================================================================== */

typedef struct {
    PyObject *ConnType;
    PyObject *Error;        /* wraplib.Error, base of the module's errors */
} wraplib_state;

static inline wraplib_state *
get_state(PyObject *module)
{
    void *s = PyModule_GetState(module);
    assert(s != NULL);
    return (wraplib_state *)s;
}

static PyModuleDef wraplib_module;   /* completed below; needed by slots */

/* One place that turns a library return code into a Python exception.
 * Returns 0 on success, -1 with an exception set otherwise. */
static int
check_rc(wraplib_state *state, int rc)
{
    if (rc == TINY_OK) {
        return 0;
    }
    switch (rc) {
    case TINY_ENOMEM:
        PyErr_NoMemory();
        break;
    case TINY_EINVAL:
        PyErr_SetString(PyExc_ValueError, tiny_strerror(rc));
        break;
    case TINY_ERANGE:
        PyErr_SetString(PyExc_OverflowError, tiny_strerror(rc));
        break;
    default:
        PyErr_Format(state->Error, "tinylib error %d: %s", rc, tiny_strerror(rc));
    }
    return -1;
}

/* ================================================================== *
 * the Conn object
 * ================================================================== */

typedef struct {
    PyObject_HEAD
    tiny_handle *h;         /* NULL once closed */
    PyObject    *name;      /* str, for repr and warnings */
    PyObject    *callback;  /* Python callable or NULL */
} ConnObject;

/* Every method starts here. Returning NULL means "already closed". */
static tiny_handle *
get_handle(ConnObject *self)
{
    if (self->h == NULL) {
        PyErr_SetString(PyExc_ValueError, "operation on a closed connection");
        return NULL;
    }
    return self->h;
}

static wraplib_state *
state_of(PyObject *self)
{
    PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &wraplib_module);
    if (module == NULL) {
        return NULL;
    }
    return get_state(module);   /* module is a borrowed reference */
}

/* ---- the trampoline the C library calls --------------------------- */

static void
conn_trampoline(void *userdata, int value)
{
    ConnObject *self = (ConnObject *)userdata;

    /* Attaching during finalization can hang or crash; never do it. */
    if (Py_IsFinalizing()) {
        return;
    }

    PyGILState_STATE gstate = PyGILState_Ensure();

    /* Take our own strong reference: another thread could replace the
     * callback attribute while we are calling it. */
    PyObject *cb = Py_XNewRef(self->callback);
    if (cb != NULL) {
        PyObject *arg = PyLong_FromLong(value);
        PyObject *res = (arg != NULL) ? PyObject_CallOneArg(cb, arg) : NULL;
        if (res == NULL) {
            /* No caller to propagate to: report rather than swallow. */
            PyErr_FormatUnraisable("Exception ignored in %R callback",
                                   (PyObject *)self);
        }
        Py_XDECREF(res);
        Py_XDECREF(arg);
        Py_DECREF(cb);
    }

    PyGILState_Release(gstate);
}

/* ---- lifecycle ---------------------------------------------------- */

static PyObject *
Conn_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    ConnObject *self = (ConnObject *)type->tp_alloc(type, 0);
    if (self == NULL) {
        return NULL;
    }
    self->h = NULL;
    self->name = NULL;
    self->callback = NULL;
    return (PyObject *)self;
}

static int
Conn_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    ConnObject *self = (ConnObject *)op;
    static char *kwlist[] = {"name", NULL};
    PyObject *name = NULL;   /* borrowed */

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "U:Conn", kwlist, &name)) {
        return -1;
    }

    wraplib_state *state = state_of(op);
    if (state == NULL) {
        return -1;
    }

    /* PyUnicode_AsUTF8AndSize returns a pointer owned by `name`; it is
     * valid here because `name` is alive and we stay attached. */
    Py_ssize_t len;
    const char *cname = PyUnicode_AsUTF8AndSize(name, &len);
    if (cname == NULL) {
        return -1;
    }
    if ((Py_ssize_t)strlen(cname) != len) {
        PyErr_SetString(PyExc_ValueError, "name must not contain NUL");
        return -1;
    }

    tiny_handle *h = NULL;
    if (check_rc(state, tiny_open(cname, &h)) < 0) {
        return -1;
    }

    /* Re-initialization on an already-open object: close the old handle. */
    if (self->h != NULL) {
        tiny_close(self->h);
    }
    self->h = h;
    Py_XSETREF(self->name, Py_NewRef(name));

    /* We hand `self` to the library as raw userdata, which does not keep the
     * object alive. That is safe here only because the handle is owned by
     * this object and is closed (clearing the callback) before the object
     * dies. A library that could outlive the registration would require an
     * explicit Py_INCREF held for the duration. */
    tiny_set_callback(self->h, conn_trampoline, self);
    return 0;
}

/* The single close path: idempotent and safe to call from a finalizer. */
static void
conn_do_close(ConnObject *self)
{
    tiny_handle *h = self->h;
    if (h == NULL) {
        return;
    }
    self->h = NULL;          /* NULL first: makes close() re-entrant-safe */
    tiny_set_callback(h, NULL, NULL);

    Py_BEGIN_ALLOW_THREADS   /* a real library's close may block */
    tiny_close(h);
    Py_END_ALLOW_THREADS
}

static int
Conn_traverse(PyObject *op, visitproc visit, void *arg)
{
    ConnObject *self = (ConnObject *)op;
    Py_VISIT(Py_TYPE(self));
    Py_VISIT(self->name);
    Py_VISIT(self->callback);   /* a closure here can reference self */
    return 0;
}

static int
Conn_clear(PyObject *op)
{
    ConnObject *self = (ConnObject *)op;
    Py_CLEAR(self->name);
    Py_CLEAR(self->callback);
    return 0;
}

/* tp_finalize runs once, while the object is still valid. Non-Python
 * resources and user-visible warnings belong here, not in tp_clear. */
static void
Conn_finalize(PyObject *op)
{
    ConnObject *self = (ConnObject *)op;
    if (self->h == NULL) {
        return;
    }
    /* A finalizer can run with an exception in flight. */
    PyObject *exc = PyErr_GetRaisedException();

    if (PyErr_ResourceWarning(op, 1, "unclosed connection %R", op) < 0) {
        PyErr_Clear();   /* warnings-as-errors here is not actionable */
    }
    conn_do_close(self);

    PyErr_SetRaisedException(exc);
}

static void
Conn_dealloc(PyObject *op)
{
    PyTypeObject *tp = Py_TYPE(op);

    PyObject_GC_UnTrack(op);
    if (PyObject_CallFinalizerFromDealloc(op) < 0) {
        return;   /* the object was resurrected by its finalizer */
    }
    conn_do_close((ConnObject *)op);   /* belt and braces */
    (void)Conn_clear(op);
    tp->tp_free(op);
    Py_DECREF(tp);
}

/* ---- methods ------------------------------------------------------ */

static PyObject *
Conn_push(PyObject *op, PyObject *arg)
{
    ConnObject *self = (ConnObject *)op;
    tiny_handle *h = get_handle(self);
    if (h == NULL) {
        return NULL;
    }
    long v = PyLong_AsLong(arg);
    if (v == -1 && PyErr_Occurred()) {
        return NULL;
    }
    if (v < INT_MIN || v > INT_MAX) {
        PyErr_SetString(PyExc_OverflowError, "value does not fit in a C int");
        return NULL;
    }

    wraplib_state *state = state_of(op);
    if (state == NULL) {
        return NULL;
    }
    /* NOTE: tiny_push invokes our callback synchronously, which runs Python
     * code, so we must stay attached here. A library call that does NOT call
     * back would be wrapped in Py_BEGIN_ALLOW_THREADS instead. */
    if (check_rc(state, tiny_push(h, (int)v)) < 0) {
        return NULL;
    }
    Py_RETURN_NONE;
}

static PyObject *
Conn_close(PyObject *op, PyObject *Py_UNUSED(ignored))
{
    conn_do_close((ConnObject *)op);
    Py_RETURN_NONE;
}

static PyObject *
Conn_enter(PyObject *op, PyObject *Py_UNUSED(ignored))
{
    if (get_handle((ConnObject *)op) == NULL) {
        return NULL;
    }
    return Py_NewRef(op);
}

static PyObject *
Conn_exit(PyObject *op, PyObject *const *args, Py_ssize_t nargs)
{
    conn_do_close((ConnObject *)op);
    Py_RETURN_FALSE;    /* do not suppress exceptions */
}

/* Hand the raw handle to other C code without exposing a bare pointer.
 * The capsule does NOT own the handle; the Conn object still does. */
static PyObject *
Conn_capsule(PyObject *op, PyObject *Py_UNUSED(ignored))
{
    tiny_handle *h = get_handle((ConnObject *)op);
    if (h == NULL) {
        return NULL;
    }
    return PyCapsule_New(h, CAPSULE_NAME, NULL);
}

static PyMethodDef Conn_methods[] = {
    {"push", Conn_push, METH_O,
     PyDoc_STR("push(value, /)\n--\n\nAdd a value; invokes the callback.")},
    {"close", Conn_close, METH_NOARGS,
     PyDoc_STR("close()\n--\n\nRelease the handle. Idempotent.")},
    {"__enter__", Conn_enter, METH_NOARGS, NULL},
    {"__exit__", (PyCFunction)(void (*)(void))Conn_exit, METH_FASTCALL, NULL},
    {"as_capsule", Conn_capsule, METH_NOARGS,
     PyDoc_STR("as_capsule()\n--\n\nReturn the raw handle wrapped in a capsule.")},
    {NULL}
};

/* ---- attributes ---------------------------------------------------- */

static PyObject *
Conn_get_total(PyObject *op, void *closure)
{
    tiny_handle *h = get_handle((ConnObject *)op);
    if (h == NULL) {
        return NULL;
    }
    wraplib_state *state = state_of(op);
    if (state == NULL) {
        return NULL;
    }
    long total = 0;
    if (check_rc(state, tiny_total(h, &total)) < 0) {
        return NULL;
    }
    return PyLong_FromLong(total);
}

static PyObject *
Conn_get_closed(PyObject *op, void *closure)
{
    return PyBool_FromLong(((ConnObject *)op)->h == NULL);
}

static PyObject *
Conn_get_callback(PyObject *op, void *closure)
{
    ConnObject *self = (ConnObject *)op;
    return self->callback ? Py_NewRef(self->callback) : Py_NewRef(Py_None);
}

static int
Conn_set_callback(PyObject *op, PyObject *value, void *closure)
{
    ConnObject *self = (ConnObject *)op;
    if (value == NULL || value == Py_None) {
        Py_CLEAR(self->callback);
        return 0;
    }
    if (!PyCallable_Check(value)) {
        PyErr_SetString(PyExc_TypeError, "callback must be callable or None");
        return -1;
    }
    Py_XSETREF(self->callback, Py_NewRef(value));
    return 0;
}

static PyGetSetDef Conn_getset[] = {
    {"total", Conn_get_total, NULL, PyDoc_STR("running total"), NULL},
    {"closed", Conn_get_closed, NULL, PyDoc_STR("True once closed"), NULL},
    {"callback", Conn_get_callback, Conn_set_callback,
     PyDoc_STR("callable invoked on each push, or None"), NULL},
    {NULL}
};

static PyObject *
Conn_repr(PyObject *op)
{
    ConnObject *self = (ConnObject *)op;
    return PyUnicode_FromFormat("<%s %R%s>", Py_TYPE(self)->tp_name,
                                self->name ? self->name : Py_None,
                                self->h ? "" : " (closed)");
}

static PyType_Slot Conn_slots[] = {
    {Py_tp_doc,      (void *)PyDoc_STR("Conn(name)\n--\n\nA tinylib connection.")},
    {Py_tp_new,      Conn_new},
    {Py_tp_init,     Conn_init},
    {Py_tp_dealloc,  Conn_dealloc},
    {Py_tp_finalize, Conn_finalize},
    {Py_tp_traverse, Conn_traverse},
    {Py_tp_clear,    Conn_clear},
    {Py_tp_repr,     Conn_repr},
    {Py_tp_methods,  Conn_methods},
    {Py_tp_getset,   Conn_getset},
    {0, NULL}
};

static PyType_Spec Conn_spec = {
    .name = "wraplib.Conn",
    .basicsize = sizeof(ConnObject),
    .itemsize = 0,
    .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,   /* no BASETYPE: the
                 close/finalize invariants are not designed for subclassing */
    .slots = Conn_slots,
};

/* ================================================================== *
 * module
 * ================================================================== */

static int
wraplib_exec(PyObject *module)
{
    wraplib_state *st = get_state(module);

    st->Error = PyErr_NewExceptionWithDoc("wraplib.Error",
                                          "Base error for wraplib.", NULL, NULL);
    if (st->Error == NULL) {
        return -1;
    }
    if (PyModule_AddObjectRef(module, "Error", st->Error) < 0) {
        return -1;
    }

    st->ConnType = PyType_FromModuleAndSpec(module, &Conn_spec, NULL);
    if (st->ConnType == NULL) {
        return -1;
    }
    if (PyModule_AddObjectRef(module, "Conn", st->ConnType) < 0) {
        return -1;
    }

    /* Export the library's constants under their own C names. */
    if (PyModule_AddIntMacro(module, TINY_OK) < 0) return -1;
    if (PyModule_AddIntMacro(module, TINY_EINVAL) < 0) return -1;
    if (PyModule_AddIntMacro(module, TINY_ENOMEM) < 0) return -1;
    if (PyModule_AddIntMacro(module, TINY_ERANGE) < 0) return -1;
    return 0;
}

static int
wraplib_traverse(PyObject *module, visitproc visit, void *arg)
{
    wraplib_state *st = get_state(module);
    Py_VISIT(st->ConnType);
    Py_VISIT(st->Error);
    return 0;
}

static int
wraplib_clear(PyObject *module)
{
    wraplib_state *st = get_state(module);
    Py_CLEAR(st->ConnType);
    Py_CLEAR(st->Error);
    return 0;
}

static void
wraplib_free(void *module)
{
    (void)wraplib_clear((PyObject *)module);
}

static PyModuleDef_Slot wraplib_slots[] = {
    {Py_mod_exec, wraplib_exec},
    {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
#if PY_VERSION_HEX >= 0x030D0000
    {Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
    {0, NULL}
};

static PyModuleDef wraplib_module = {
    .m_base     = PyModuleDef_HEAD_INIT,
    .m_name     = "wraplib",
    .m_doc      = PyDoc_STR("Wrapper around the fictional tinylib."),
    .m_size     = sizeof(wraplib_state),
    .m_slots    = wraplib_slots,
    .m_traverse = wraplib_traverse,
    .m_clear    = wraplib_clear,
    .m_free     = wraplib_free,
};

PyMODINIT_FUNC
PyInit_wraplib(void)
{
    return PyModuleDef_Init(&wraplib_module);
}

Original source: code/wraplib/wraplib.c

tinylib.h

/*
 * tinylib -- a deliberately small stand-in for "some C library you did not
 * write". It has the three features that make wrapping interesting: an
 * opaque handle with an explicit close, integer error codes, and a callback.
 */
#ifndef TINYLIB_H
#define TINYLIB_H

#define TINY_OK      0
#define TINY_EINVAL  1
#define TINY_ENOMEM  2
#define TINY_ERANGE  3

typedef struct tiny_handle tiny_handle;

/* Invoked by tiny_push, on the calling thread. */
typedef void (*tiny_cb)(void *userdata, int value);

int  tiny_open(const char *name, tiny_handle **out);
void tiny_close(tiny_handle *h);
int  tiny_push(tiny_handle *h, int value);
int  tiny_total(tiny_handle *h, long *out);
void tiny_set_callback(tiny_handle *h, tiny_cb cb, void *userdata);
const char *tiny_strerror(int rc);

#endif /* TINYLIB_H */

Original source: code/wraplib/tinylib.h

tinylib.c

#include "tinylib.h"

#include <stdlib.h>
#include <string.h>

struct tiny_handle {
    char     name[64];
    long     total;
    tiny_cb  cb;
    void    *userdata;
};

int
tiny_open(const char *name, tiny_handle **out)
{
    if (name == NULL || out == NULL || name[0] == '\0') {
        return TINY_EINVAL;
    }
    size_t len = strlen(name);
    if (len >= sizeof(((struct tiny_handle *)0)->name)) {
        return TINY_ERANGE;
    }
    struct tiny_handle *h = calloc(1, sizeof(*h));
    if (h == NULL) {
        return TINY_ENOMEM;
    }
    memcpy(h->name, name, len + 1);
    *out = h;
    return TINY_OK;
}

void
tiny_close(tiny_handle *h)
{
    free(h);
}

int
tiny_push(tiny_handle *h, int value)
{
    if (h == NULL) {
        return TINY_EINVAL;
    }
    if (value < -1000 || value > 1000) {
        return TINY_ERANGE;
    }
    h->total += value;
    if (h->cb != NULL) {
        h->cb(h->userdata, value);
    }
    return TINY_OK;
}

int
tiny_total(tiny_handle *h, long *out)
{
    if (h == NULL || out == NULL) {
        return TINY_EINVAL;
    }
    *out = h->total;
    return TINY_OK;
}

void
tiny_set_callback(tiny_handle *h, tiny_cb cb, void *userdata)
{
    if (h != NULL) {
        h->cb = cb;
        h->userdata = userdata;
    }
}

const char *
tiny_strerror(int rc)
{
    switch (rc) {
    case TINY_OK:     return "ok";
    case TINY_EINVAL: return "invalid argument";
    case TINY_ENOMEM: return "out of memory";
    case TINY_ERANGE: return "value out of range";
    default:          return "unknown error";
    }
}

Original source: code/wraplib/tinylib.c

setup.py

import sys

from setuptools import Extension, setup

warn_flags = ["/W4"] if sys.platform == "win32" else ["-Wall", "-Wextra", "-Wno-unused-parameter"]

setup(
    ext_modules=[
        Extension(
            name="wraplib",
            sources=["wraplib.c", "tinylib.c"],
            include_dirs=["."],
            extra_compile_args=warn_flags,
        ),
    ],
)

Original source: code/wraplib/setup.py

pyproject.toml

[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

[project]
name = "wraplib"
version = "0.1.0"
description = "Wrapper around a fictional C library"
requires-python = ">=3.13"

[tool.pytest.ini_options]
testpaths = ["tests"]
filterwarnings = ["always::ResourceWarning"]

Original source: code/wraplib/pyproject.toml

tests/test_wraplib.py

import gc
import sys
import warnings

import pytest

import wraplib


def test_basic_use():
    with wraplib.Conn("session") as c:
        c.push(5)
        c.push(7)
        assert c.total == 12
    assert c.closed


def test_repr():
    c = wraplib.Conn("session")
    assert repr(c) == "<wraplib.Conn 'session'>"
    c.close()
    assert repr(c) == "<wraplib.Conn 'session' (closed)>"


def test_close_is_idempotent():
    c = wraplib.Conn("x")
    c.close()
    c.close()
    assert c.closed


def test_use_after_close_raises_rather_than_crashing():
    c = wraplib.Conn("x")
    c.close()
    with pytest.raises(ValueError, match="closed"):
        c.push(1)
    with pytest.raises(ValueError, match="closed"):
        _ = c.total
    with pytest.raises(ValueError, match="closed"):
        c.as_capsule()


def test_error_translation():
    with wraplib.Conn("x") as c:
        with pytest.raises(OverflowError):
            c.push(10_000)  # TINY_ERANGE
        with pytest.raises(TypeError):
            c.push("five")
        with pytest.raises(OverflowError):
            c.push(2**40)  # does not fit in a C int


def test_constructor_validation():
    with pytest.raises(TypeError):
        wraplib.Conn(123)
    with pytest.raises(ValueError):
        wraplib.Conn("")  # TINY_EINVAL
    with pytest.raises(OverflowError):
        wraplib.Conn("n" * 100)  # TINY_ERANGE


def test_callback_is_invoked():
    seen = []
    with wraplib.Conn("cb") as c:
        c.callback = seen.append
        c.push(3)
        c.push(4)
    assert seen == [3, 4]


def test_callback_validation():
    with wraplib.Conn("cb") as c:
        with pytest.raises(TypeError):
            c.callback = 42
        c.callback = None
        assert c.callback is None


def test_callback_exception_goes_to_unraisable_hook():
    def boom(_value):
        raise RuntimeError("from the callback")

    caught = []
    previous = sys.unraisablehook
    sys.unraisablehook = caught.append
    try:
        with wraplib.Conn("cb") as c:
            c.callback = boom
            c.push(1)  # must not propagate, must not be silent
            assert c.total == 1
    finally:
        sys.unraisablehook = previous

    assert len(caught) == 1
    assert caught[0].exc_type is RuntimeError
    assert str(caught[0].exc_value) == "from the callback"


def test_callback_cycle_is_collected():
    def live():
        return sum(1 for o in gc.get_objects() if type(o) is wraplib.Conn)

    gc.collect()
    before = live()
    for _ in range(100):
        c = wraplib.Conn("cyclic")
        c.callback = lambda v, c=c: None  # closure referring back to c
        c.close()
        del c
    gc.collect()
    assert live() == before


def test_unclosed_connection_warns():
    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        wraplib.Conn("forgotten")
        gc.collect()
    assert any(issubclass(w.category, ResourceWarning) for w in caught)


def test_capsule_carries_a_checked_name():
    with wraplib.Conn("x") as c:
        cap = c.as_capsule()
        assert "wraplib.tiny_handle" in repr(cap)


def test_constants_exported():
    assert wraplib.TINY_OK == 0
    assert wraplib.TINY_ERANGE == 3

Original source: code/wraplib/tests/test_wraplib.py

README.md

# wraplib

A complete wrapper around `tinylib`, a fictional C library with the three
features that make wrapping interesting: an opaque handle with an explicit
close, integer error codes, and a callback.

```bash
python -m pip install -e . --no-build-isolation -v
python -m pytest -q
```

```python
import wraplib

with wraplib.Conn("session") as c:
    c.callback = print
    c.push(5)
    c.push(7)
    print(c.total)     # 12
```

## What to look for in `wraplib.c`

| Pattern | Where |
|---|---|
| One error-translation point | `check_rc` |
| Handle validity check in every method | `get_handle` |
| Idempotent close, `__enter__`/`__exit__` | `conn_do_close`, `Conn_enter`, `Conn_exit` |
| `ResourceWarning` backstop | `Conn_finalize` |
| Callback trampoline from a foreign thread | `conn_trampoline` |
| Module state reached from a slot function | `state_of` via `PyType_GetModuleByDef` |
| Capsule with a checked name | `Conn_capsule` |

## Things to try

1. Delete the `get_handle` check from `Conn_push` and call `push` after `close`.
2. Remove `Py_VISIT(self->callback)` from `Conn_traverse`, then run
   `test_callback_cycle_is_collected`.
3. Remove the `Py_XNewRef(self->callback)` in the trampoline and reason about
   what a concurrent `c.callback = None` could now do.
4. Move `conn_do_close` from `Conn_finalize` into `Conn_clear` and explain why
   that is wrong.

Original source: code/wraplib/README.md