Files
base/plugins/pyvm/pyvm_bridge.h
zeekay a308147a5f feat(base): pyvm runtime — cgo CPython 3.13 (-tags pyvm)
Fifth extension runtime alongside native/goja/wazero/v8go. Default
build links the stub; real impl behind `-tags pyvm` requires
pkg-config python-3.13-embed.

Architecture: one process-wide CPython init, per-module pool of PEP
684 OWN_GIL sub-interpreters (default 4 / BASE_PYVM_POOL_SIZE).
Each Invoke pins to OS thread, attaches a sub-interpreter, runs
__main__.fn(json.loads(payload)), returns json.dumps(result).
Free-threading (PEP 703) detected at runtime via Py_GIL_DISABLED.

Honest limitations documented in README + capability flags:
- HardSandbox=false (C extension segfault kills the host)
- SupportsAbort=false (no primitive to abort a running sub-interp
  from another OS thread without holding its GIL)

Direct cgo bridge (~150 lines), no DataDog/go-python3 or cpy3
dependency — both fail to compile against CPython 3.12+ because
they reference removed PyFloat_ClearFreeList /
PyDict_ClearFreeList. JSON marshaling stays inside Python's
stdlib json module so the cgo surface stays narrow.

Tests: 7 table tests pass on darwin/arm64 + Python 3.13.13.
2026-05-18 19:58:19 -07:00

51 lines
2.0 KiB
C

// Package pyvm — C bridge declarations shared across pyvm cgo files.
// Implementation lives in pyvm_bridge.c so multiple .go files can call
// the same helpers without cgo dropping duplicate-symbol errors.
#ifndef PYVM_BRIDGE_H
#define PYVM_BRIDGE_H
#include <Python.h>
// pyvm_init initializes CPython (idempotent). Returns the saved main
// thread state, leaves the GIL released. Sets the global GIL-disabled
// flag retrievable via pyvm_gil_disabled.
PyThreadState* pyvm_init(void);
// pyvm_gil_disabled returns 1 if CPython was built with --disable-gil
// (free-threading PEP 703), 0 otherwise.
int pyvm_gil_disabled(void);
// pyvm_new_sub creates an OWN_GIL sub-interpreter (PEP 684). On
// success returns its thread state and leaves the caller thread
// without holding any GIL. On failure returns NULL and writes a heap-
// allocated error string to *err_out (caller frees).
PyThreadState* pyvm_new_sub(char **err_out);
// pyvm_end_sub destroys a sub-interpreter. Caller must NOT hold GIL.
// Safe to pass NULL.
void pyvm_end_sub(PyThreadState *ts);
// pyvm_enter / pyvm_leave swap the calling OS thread to/from the
// given sub-interpreter. Must be paired and called from the same OS
// thread.
void pyvm_enter(PyThreadState *ts);
void pyvm_leave(void);
// pyvm_load_source runs source as __main__ of the current
// sub-interpreter. Caller must hold the GIL. Returns 0 on success;
// on failure writes a heap-allocated error string to *err_out.
int pyvm_load_source(const char *src, char **err_out);
// pyvm_invoke calls __main__.fn(json.loads(payload_json)) and returns
// json.dumps(result) as a heap-allocated C string (caller frees).
// Caller must hold the GIL of the target sub-interpreter.
//
// On error returns NULL and writes the error to *err_out. If the
// failure is "function not found / not callable", *was_unknown is
// set to 1 — the Go layer maps that to ErrUnknownFn.
char* pyvm_invoke(const char *fn, const char *payload_json,
int *was_unknown, char **err_out);
#endif // PYVM_BRIDGE_H