mirror of
https://github.com/hanzoai/cpy3.git
synced 2026-10-30 18:42:30 +00:00
* feat: Python 3.12+/3.13 compat + sub-interpreter wrapper
Hanzo fork of go-python/cpy3. See HANZO_CHANGES.md for full diff.
- Polyfill APIs removed in 3.12 (per-type ClearFreeList helpers) and
3.13 (PyEval_InitThreads, Py_SetPath/PythonHome/ProgramName,
PySys_SetArgv/AddWarnOption/AddXOption/SetPath,
Py_SetStandardStreamEncoding, PyEval_ThreadsInitialized) so existing
call sites compile against modern CPython. Polyfills route to the
PyConfig API where there's a meaningful replacement, return 0/no-op
otherwise.
- New PEP 684 SubInterpreter wrapper (subinterp.go) — recommended
primitive for tenant/per-worker isolation on 3.12+. Defaults follow
PEP 684 (own GIL, threads on, no daemon threads, refuse non-isolated
C extensions). Falls back to Py_NewInterpreter on 3.11 and older.
- Free-threading awareness (PEP 703) — IsGILDisabled() reports at
runtime whether the linked CPython was built with --disable-gil.
- pkg-config switched from "python3" to "python3-embed" so libpython
links cleanly on Linux distros that split the embed flags.
- Module path: github.com/go-python/cpy3 -> github.com/hanzoai/cpy3.
Go 1.21+. Package name (python3) unchanged.
- CI matrix: Python {3.11, 3.12, 3.13} x Go {1.21, 1.22} on Ubuntu.
License remains MIT. Original go-python team credited in HANZO_CHANGES.md
and every modified file's header.
* feat: JSON hot-path helper + 3.13t build script
- LoadSource + CallJSONFunctionByName: single-cgo-crossing helpers
for the common "JSON-in, JSON-out" extension embedding contract.
Roughly 2x faster than the long form via individual wrappers and
stack-safer under sustained load (macOS SIGBUS at ~400k iters of
the 9-call long form is mitigated by collapsing to 1 cgo call).
- ErrUnknownFunction: sentinel error for missing/non-callable
lookups so callers can errors.Is.
- scripts/build-python313t.sh: idempotent local build of CPython
3.13.x with --disable-gil. Writes a python3-embed.pc so callers
steer via PKG_CONFIG_PATH.
- HANZO_CHANGES.md + README updated.
- .gitignore: exclude python313t-build/ python313t-src/.
* ci: split test runs by Python global state
TestSubInterpreter_* and TestCallJSONFunctionByName_* / TestLoadSource_*
share global Py state with the data-type/error tests. Running them
in the same `go test` invocation can hang on darwin after the
data-type tests have already touched the main interpreter. Split
into two test steps so each starts from a fresh process.
* test: move json_hotpath tests in-package + share Py init
Move from package python3_test to package python3 so they share
Py_Initialize() state with subinterp_test.go. Each test pins its
own OS thread, calls Py_Initialize (idempotent), creates a fresh
sub-interp, and unpins on return. No PyEval_SaveThread in teardown
— the sub-interp's Close already released the calling thread's GIL.
This eliminates a SIGSEGV-on-second-test pattern caused by trying
to PyEval_SaveThread a state that's already been saved.
* docs: README example for the JSON hot path
Show LoadSource + CallJSONFunctionByName end-to-end with luxfi/log
for context. The helpers themselves take no logger dependency.
229 lines
7.5 KiB
Go
229 lines
7.5 KiB
Go
/*
|
|
SPDX-License-Identifier: MIT
|
|
Hanzo additions, 2026-05-19.
|
|
|
|
Sub-interpreter wrapper (PEP 684 — OWN_GIL sub-interpreters). The
|
|
recommended primitive on 3.12+ for tenant / per-worker isolation.
|
|
|
|
Use one SubInterpreter per concurrent worker. Each has its own GIL on
|
|
3.12+, so two SubInterpreters from two goroutines truly run in parallel
|
|
on two OS threads.
|
|
|
|
API discipline:
|
|
- Each call into SubInterpreter MUST be made from a goroutine pinned
|
|
to its OS thread (runtime.LockOSThread). The wrapper pins on Enter
|
|
and unpins on Leave; do not Enter again on a different OS thread
|
|
between paired Enter/Leave calls.
|
|
- Free-threaded builds (PEP 703, --disable-gil) skip the GIL ceremony
|
|
silently; the wrapper still works because PyGILState_Ensure is a
|
|
no-op there.
|
|
*/
|
|
|
|
package python3
|
|
|
|
/*
|
|
#include "Python.h"
|
|
#include "macro.h"
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
// Compat: PyInterpreterConfig + Py_NewInterpreterFromConfig only exist on 3.12+.
|
|
#if PY_VERSION_HEX >= 0x030C0000
|
|
static PyThreadState *_hz_new_subinterp(int allow_threads,
|
|
int allow_daemon_threads,
|
|
int check_multi_interp_extensions,
|
|
int own_gil,
|
|
char **err) {
|
|
PyInterpreterConfig cfg = {
|
|
.use_main_obmalloc = 0,
|
|
.allow_fork = 0,
|
|
.allow_exec = 0,
|
|
.allow_threads = allow_threads,
|
|
.allow_daemon_threads = allow_daemon_threads,
|
|
.check_multi_interp_extensions = check_multi_interp_extensions,
|
|
.gil = own_gil ? PyInterpreterConfig_OWN_GIL : PyInterpreterConfig_SHARED_GIL,
|
|
};
|
|
PyThreadState *ts = NULL;
|
|
PyStatus st = Py_NewInterpreterFromConfig(&ts, &cfg);
|
|
if (PyStatus_Exception(st)) {
|
|
const char *m = st.err_msg ? st.err_msg : "Py_NewInterpreterFromConfig failed";
|
|
*err = strdup(m);
|
|
return NULL;
|
|
}
|
|
return ts;
|
|
}
|
|
#else
|
|
static PyThreadState *_hz_new_subinterp(int allow_threads,
|
|
int allow_daemon_threads,
|
|
int check_multi_interp_extensions,
|
|
int own_gil,
|
|
char **err) {
|
|
(void)allow_threads; (void)allow_daemon_threads;
|
|
(void)check_multi_interp_extensions; (void)own_gil;
|
|
// Fall back to the legacy SHARED_GIL Py_NewInterpreter on 3.11 and older.
|
|
PyThreadState *ts = Py_NewInterpreter();
|
|
if (ts == NULL) {
|
|
*err = strdup("Py_NewInterpreter returned NULL");
|
|
}
|
|
return ts;
|
|
}
|
|
#endif
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"errors"
|
|
gort "runtime"
|
|
"unsafe"
|
|
)
|
|
|
|
// SubInterpreterConfig configures a new sub-interpreter.
|
|
type SubInterpreterConfig struct {
|
|
// AllowThreads — Python threads enabled inside this interpreter.
|
|
// Default: true.
|
|
AllowThreads bool
|
|
|
|
// AllowDaemonThreads — daemon threads (created but never joined)
|
|
// allowed. Default: false. PEP 684 recommends false.
|
|
AllowDaemonThreads bool
|
|
|
|
// CheckMultiInterpExtensions — refuse C extensions that don't
|
|
// declare multi-interpreter support. Default: true. PEP 684
|
|
// recommends true.
|
|
CheckMultiInterpExtensions bool
|
|
|
|
// OwnGIL — give this sub-interpreter its own GIL (PEP 684 default
|
|
// on 3.12+). Required for true parallelism with another interpreter.
|
|
// On 3.11 and older this flag is ignored (only SHARED_GIL exists).
|
|
// Default: true.
|
|
OwnGIL bool
|
|
}
|
|
|
|
// DefaultSubInterpreterConfig returns the recommended PEP 684 defaults:
|
|
// own GIL, threads on, no daemon threads, refuse non-isolated C exts.
|
|
func DefaultSubInterpreterConfig() SubInterpreterConfig {
|
|
return SubInterpreterConfig{
|
|
AllowThreads: true,
|
|
AllowDaemonThreads: false,
|
|
CheckMultiInterpExtensions: true,
|
|
OwnGIL: true,
|
|
}
|
|
}
|
|
|
|
// SubInterpreter is one CPython sub-interpreter.
|
|
//
|
|
// Lifecycle:
|
|
//
|
|
// si, err := NewSubInterpreter(DefaultSubInterpreterConfig())
|
|
// defer si.Close()
|
|
// si.Enter()
|
|
// // ... call any python3.* function — they target si now
|
|
// si.Leave()
|
|
//
|
|
// CPython requires that NewSubInterpreter is called on a thread that
|
|
// holds the main interpreter's GIL. The recommended pattern is:
|
|
//
|
|
// Py_Initialize()
|
|
// // ... do main interpreter setup ...
|
|
// mainState := PyEval_SaveThread() // release main GIL
|
|
// gort.LockOSThread()
|
|
// // reacquire main GIL on this OS thread before NewSubInterpreter:
|
|
// PyEval_RestoreThread(mainState)
|
|
// si, err := NewSubInterpreter(DefaultSubInterpreterConfig())
|
|
//
|
|
// The wrapper handles the GIL save/restore inside NewSubInterpreter
|
|
// itself: it expects the caller to hold the main GIL before the call
|
|
// and returns with the caller's OS thread no longer holding any GIL.
|
|
type SubInterpreter struct {
|
|
ts *C.PyThreadState
|
|
}
|
|
|
|
// NewSubInterpreter creates a sub-interpreter and returns it. Caller
|
|
// must hold the main interpreter's GIL before calling; the call returns
|
|
// with no GIL held on the calling OS thread.
|
|
//
|
|
// Returns an error if the sub-interpreter could not be created. The
|
|
// most common failure on 3.12+ is "interpreter creation failed" when
|
|
// the caller is not pinned to an OS thread or did not actually hold
|
|
// the main GIL.
|
|
func NewSubInterpreter(cfg SubInterpreterConfig) (*SubInterpreter, error) {
|
|
var cErr *C.char
|
|
ts := C._hz_new_subinterp(
|
|
boolC(cfg.AllowThreads),
|
|
boolC(cfg.AllowDaemonThreads),
|
|
boolC(cfg.CheckMultiInterpExtensions),
|
|
boolC(cfg.OwnGIL),
|
|
&cErr,
|
|
)
|
|
if ts == nil {
|
|
msg := "unknown error"
|
|
if cErr != nil {
|
|
msg = C.GoString(cErr)
|
|
C.free(unsafe.Pointer(cErr))
|
|
}
|
|
return nil, errors.New("cpy3: NewSubInterpreter: " + msg)
|
|
}
|
|
// Release the new sub-interp's GIL — caller must Enter to use it.
|
|
C.PyEval_SaveThread()
|
|
return &SubInterpreter{ts: ts}, nil
|
|
}
|
|
|
|
// Enter acquires this sub-interpreter's GIL on the calling OS thread.
|
|
// Caller MUST have called runtime.LockOSThread() first. Pair with Leave.
|
|
func (s *SubInterpreter) Enter() {
|
|
gort.LockOSThread()
|
|
C.PyEval_RestoreThread(s.ts)
|
|
}
|
|
|
|
// Leave releases this sub-interpreter's GIL and unpins the OS thread.
|
|
func (s *SubInterpreter) Leave() {
|
|
C.PyEval_SaveThread()
|
|
gort.UnlockOSThread()
|
|
}
|
|
|
|
// ThreadState returns the underlying PyThreadState pointer. For
|
|
// interop with code that needs the raw value. Most callers should not
|
|
// need this.
|
|
func (s *SubInterpreter) ThreadState() *PyThreadState {
|
|
return (*PyThreadState)(s.ts)
|
|
}
|
|
|
|
// Close tears down the sub-interpreter. Caller must NOT hold any GIL;
|
|
// Close acquires the target sub-interp's GIL itself.
|
|
//
|
|
// After Close, the SubInterpreter is unusable and further Enter/Leave
|
|
// calls will crash. Idempotent — second Close is a no-op.
|
|
func (s *SubInterpreter) Close() error {
|
|
if s == nil || s.ts == nil {
|
|
return nil
|
|
}
|
|
gort.LockOSThread()
|
|
defer gort.UnlockOSThread()
|
|
C.PyEval_RestoreThread(s.ts)
|
|
C.Py_EndInterpreter(s.ts)
|
|
s.ts = nil
|
|
// We don't swap back to a known main state — the caller must
|
|
// PyEval_RestoreThread their own main state on this OS thread if
|
|
// they need it. Most callers spawn a goroutine per sub-interp and
|
|
// let it die.
|
|
return nil
|
|
}
|
|
|
|
// IsGILDisabled reports whether the linked CPython was built with the
|
|
// free-threading (--disable-gil, PEP 703) flag. On such builds, the
|
|
// GIL functions in this package become no-ops but remain safe to call;
|
|
// OWN_GIL sub-interpreters are still useful for state isolation but
|
|
// no longer required for parallelism.
|
|
//
|
|
// Returns false on Python 3.12 and older (free-threading is 3.13+).
|
|
func IsGILDisabled() bool {
|
|
return C._go_hz_gil_disabled() != 0
|
|
}
|
|
|
|
func boolC(b bool) C.int {
|
|
if b {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|