icon: cpu title: 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.
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.
| 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.
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.
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.