Skip to main content

Include/internal/pycore_function.h

cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_function.h

The private extension of PyFunctionObject. The public Include/cpython/funcobject.h exposes the struct layout for Py_LIMITED_API users; this header adds the interpreter-private details: the func_version monotone counter that the specializing interpreter uses to guard call-site caches, the PyFunction_Event enum and callback mechanism for sys.monitoring, the _PyFunction_FromConstructor constructor shortcut, and the _Py_FUNC_DISABLE_INLINING hint that prevents the tier-2 optimizer from inlining a specific function's bytecode.

Map

LinesSymbolRolegopy
1-60FUNC_VERSION_* constants, _PyFunction_SetVersion, _PyFunction_GetVersionForCurrentStateA 32-bit monotone counter per function; stamped into call-site inline caches so the specialized CALL_PY_EXACT_ARGS instruction can validate without inspecting the full function.objects/function.go
60-120PyFunction_Event, PyFunction_WatchCallback, PyFunction_AddWatcher, PyFunction_ClearWatcherEvent dispatch layer: watchers registered via sys.monitoring or C extension are called when a function is created, modified, or destroyed.objects/function.go
120-160_PyFunction_FromConstructor, _Py_FUNC_DISABLE_INLININGConstructor shortcut for type.__new__ that avoids the slower PyFunction_NewWithQualName path; and a compiler hint that suppresses tier-2 inlining for specific C-level helpers.objects/function.go

Reading

func_version counter (lines 1 to 60)

cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_function.h#L1-60

/* Upper limit: when the counter wraps here, all cached versions are
invalidated by setting every function's func_version to 0. */
#define FUNC_VERSION_MAX UINT32_MAX
#define FUNC_VERSION_UNSET 0

/* Atomically increment the interpreter-global function version
counter and assign the result to func->func_version. */
extern void _PyFunction_SetVersion(PyFunctionObject *func, uint32_t version);

/* Return the current func_version. The caller compares this against
a value stored in an inline cache; mismatch triggers despecialization. */
static inline uint32_t
_PyFunction_GetVersionForCurrentState(PyFunctionObject *func) {
return func->func_version;
}

func_version is a 32-bit integer that serves as a guard tag for call-site inline caches. When the specializing interpreter rewrites a CALL instruction to CALL_PY_EXACT_ARGS, it records the current func_version of the callee in the instruction's inline cache words. On each subsequent call the specialized instruction reads the cache and compares it against the live func->func_version. If they match, the call proceeds with no further checks. If they differ, the instruction despecializes back to the generic CALL and the slow path runs.

_PyFunction_SetVersion is called only from Objects/funcobject.c whenever a mutable attribute changes: __code__, __defaults__, __kwdefaults__, __closure__, or __annotations__. The new version value comes from an interpreter-global monotone counter that starts at 1 and never resets (wrapping to 0 effectively invalidates every cache). Setting func_version = 0 is the explicit invalidation signal: since FUNC_VERSION_UNSET = 0, any cache that was stamped with a non-zero version will fail the comparison.

The version counter is per-interpreter, not per-thread, so all threads sharing an interpreter state see a consistent versioning of all functions. In the free-threaded build the global counter is incremented with an atomic fetch-and-add.

In gopy, objects/function.go holds Version uint32 on the Function struct. Every setter (SetCode, SetDefaults, SetKwDefaults, SetClosure) zeros it, matching the CPython invalidation contract. GetVersionForCurrentState is ported as a direct one-line method. gopy does not yet run the tier-1 specializer, so no inline cache actually reads the version; the field tracks the same invariants so the specializer can be wired in later.

Specialization invalidation pattern

A changed __defaults__ invalidates every call site that was specialized against that function. The sequence in CPython is:

  1. func_set_defaults (in Objects/funcobject.c) stores the new tuple, then calls _PyFunction_SetVersion(func, 0).
  2. _PyFunction_SetVersion writes func->func_version = version.
  3. On the next call, the specialized instruction reads the cached version from its inline cache words, finds 0 != cached_version, and falls back to the generic handler, which re-specializes (or leaves it generic if the shape keeps changing).

The pattern ensures that no lock is needed: the version write is visible to any thread that subsequently reads the cache, because the specialized instruction reads func_version after the function pointer comparison, and the function pointer cannot change without going through func_set_code (which also zeroes the version).

PyFunction_Event and watchers (lines 60 to 120)

cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_function.h#L60-120

typedef enum {
PyFunction_EVENT_CREATE,
PyFunction_EVENT_DESTROY,
PyFunction_EVENT_MODIFY_CODE,
PyFunction_EVENT_MODIFY_DEFAULTS,
PyFunction_EVENT_MODIFY_KWDEFAULTS,
} PyFunction_Event;

typedef int (*PyFunction_WatchCallback)(
PyFunction_Event event,
PyFunctionObject *func,
PyObject *new_value);

PyAPI_FUNC(int) PyFunction_AddWatcher(PyFunction_WatchCallback callback);
PyAPI_FUNC(int) PyFunction_ClearWatcher(int watcher_id);

The watcher mechanism lets C extensions and sys.monitoring observe function lifecycle events without patching CPython's internal call paths. Up to 8 watchers can be registered simultaneously. When any watched event fires, CPython iterates the registered callbacks and calls each with the event type, the function object, and (for MODIFY_* events) the incoming new value before it is installed.

PyFunction_EVENT_CREATE fires at the end of PyFunction_NewWithQualName, after all fields are initialized but before the function is returned to the caller. A watcher that stamps the version for the first time does so here.

PyFunction_EVENT_DESTROY fires at the start of func_dealloc, while the function object is still fully valid. After the callbacks return, func_version is zeroed to prevent stale cache hits if the memory is reused for another function that happens to land at the same address.

MODIFY_CODE, MODIFY_DEFAULTS, and MODIFY_KWDEFAULTS fire after the validation checks in the setter but before the new value is stored. new_value is the incoming object; if the callback returns nonzero, the setter aborts and raises the exception the callback set.

In gopy, objects/function.go does not implement the watcher registry. The mechanism is needed only when sys.monitoring is active or when a C extension explicitly calls PyFunction_AddWatcher; gopy defers both until the monitoring subsystem is ported.

_PyFunction_FromConstructor and _Py_FUNC_DISABLE_INLINING (lines 120 to 160)

cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_function.h#L120-160

/* Faster constructor used by type_new_impl to avoid keyword-argument
parsing overhead when building a function for __init__ or __new__. */
extern PyObject *_PyFunction_FromConstructor(PyFrameConstructor *constr);

/* Suppress tier-2 inlining for a specific function. Used on C-level
helpers that are too large or too polymorphic to inline profitably. */
#define _Py_FUNC_DISABLE_INLINING(func) \
do { (func)->func_version = FUNC_VERSION_MAX; } while (0)

_PyFunction_FromConstructor is the fast path that type.__new__ takes when building the implicit __init__ and __new__ wrappers for a new class. It accepts a PyFrameConstructor (a pre-filled struct of the code object, globals, and builtins pointers) and avoids the full keyword-argument dispatch that PyFunction_NewWithQualName handles.

_Py_FUNC_DISABLE_INLINING sets func_version = FUNC_VERSION_MAX (i.e. UINT32_MAX). The tier-2 optimizer reserves version UINT32_MAX as a sentinel meaning "never inline this function". By convention, any function whose version reaches UINT32_MAX is skipped by the inliner regardless of its call frequency. The macro is applied to a small set of C-level built-ins (like functools.reduce and itertools.chain) that the optimizer is not equipped to inline.

In gopy, _PyFunction_FromConstructor is not yet ported; class construction goes through the general NewFunctionWithQualName path. _Py_FUNC_DISABLE_INLINING has no counterpart because gopy does not yet run the tier-2 optimizer.

gopy mirror

The central field is Version uint32 on objects.Function. All setters that CPython invalidates by zeroing func_version do the same in gopy: SetCode, SetDefaults, SetKwDefaults, and SetClosure each assign f.Version = 0 before returning. The GetVersionForCurrentState method is a direct port.

The event watcher subsystem (PyFunction_Event, PyFunction_AddWatcher, PyFunction_ClearWatcher) is not yet implemented. When sys.monitoring is ported, the watcher registry will live alongside the function type initialization in objects/function.go, mirroring the C layout where the callback table is stored on the interpreter state struct.