bindings.md 4.0 KB


icon: cpu title: C Bindings

order: 17

C bindings

A binding converts Python arguments to native values, calls your C code, and writes a Python result. All native functions use the py_CFunction signature:

typedef bool (*py_CFunction)(int argc, py_StackRef argv);

argc is the argument count and py_arg(i) accesses argument i. The parameter names argc and argv are required by the convenience macros.

A complete binding

This program exports native.add(a, b=1):

#include "pocketpy.h"
#include <stdint.h>

static bool native_add(int argc, py_Ref argv) {
    PY_CHECK_ARGC(2);
    PY_CHECK_ARG_TYPE(0, tp_int);
    PY_CHECK_ARG_TYPE(1, tp_int);

    py_i64 a = py_toint(py_arg(0));
    py_i64 b = py_toint(py_arg(1));
    if((b > 0 && a > INT64_MAX - b) || (b < 0 && a < INT64_MIN - b)) {
        return ValueError("sum is outside the signed 64-bit range");
    }

    py_newint(py_retval(), a + b);
    return true;
}

int main(void) {
    py_initialize();

    py_GlobalRef mod = py_newmodule("native");
    py_bind(mod, "add(a, b=1)", native_add);

    bool ok = py_exec("from native import add\n"
                      "assert add(3, 7) == 10\n"
                      "assert add(4) == 5\n"
                      "assert add(4, b=2) == 6\n",
                      "app.py", EXEC_MODE, NULL);
    if(!ok) py_printexc();

    py_finalize();
    return ok ? 0 : 1;
}

The signature passed to py_bind() resolves defaults before the callback runs, so the callback receives two arguments even for add(4). Required parameters such as a are positional in pocketpy; see argument compatibility.

Use py_i64 for Python integers and validate the native library's accepted range before narrowing to int or another smaller type. The example checks addition overflow to avoid undefined C arithmetic.

Choose a binding helper

Helper Use
py_bind(obj, "name(args...)", callback) Signature-based binding with names and defaults.
py_bindfunc(module, "name", callback) Simple positional-argument function; the callback validates the count.
py_bindmethod(type, "name", callback) Instance method; self is argument 0 and included in argc.
py_bindstaticmethod(type, "name", callback) Method without an implicit self.
py_bindproperty(type, "name", getter, setter) Property; a NULL setter makes it read-only.
py_bindmagic(type, py_name("__len__"), callback) Special method on a native type.

For a global function, bind to py_getmodule("__main__"). For a reusable API, create a named module.

Results and errors

On success, write a value to py_retval() and return true. A function with no meaningful result must still call py_newnone(py_retval()).

On failure, set an exception and return false. For example:

static bool require_positive(int argc, py_Ref argv) {
    PY_CHECK_ARGC(1);
    PY_CHECK_ARG_TYPE(0, tp_int);
    if(py_toint(py_arg(0)) <= 0) return ValueError("value must be positive");
    py_newnone(py_retval());
    return true;
}

Type-check macros return immediately when validation fails. If an API you call fails, propagate its error rather than replacing it with a generic exception. See execution and exceptions for the host's recovery path.

Native object lifetimes

py_newtype() registers a Python type. py_newobject() allocates an instance with native userdata and optional Python slots or an attribute dictionary.

Store references to Python objects in Python slots or another GC-visible container. A raw pointer inside userdata is not automatically a GC root. Keep a native resource alive for as long as a bound object can access it, and use the type's destructor callback to release native allocations.

For class-heavy C++ libraries, the C++ binding layer handles much of this plumbing. The function reference documents the lower-level type, slot, and binding operations.