| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| /* |
| * \file tvm_ffi_python_object.h |
| * \brief PyObject-tying state machine: binds one Python wrapper to one C++ FFI object |
| * ("chandle") for the object's lifetime so identity is stable (``a.x is a.x``, |
| * stable ``id()`` across drop+refetch, ``f(x) is x`` for FFI returns). |
| * |
| * Split out of tvm_ffi_python_helpers.h. The design overview is the banner comment below. |
| */ |
| #ifndef TVM_FFI_PYTHON_OBJECT_H_ |
| #define TVM_FFI_PYTHON_OBJECT_H_ |
| |
| #include <Python.h> |
| #include <tvm/ffi/c_api.h> |
| #include <tvm/ffi/memory.h> |
| |
| // Define here to avoid dependencies on non-c headers for now |
| #ifndef TVM_FFI_INLINE |
| #if defined(_MSC_VER) |
| #define TVM_FFI_INLINE [[msvc::forceinline]] inline |
| #else |
| #define TVM_FFI_INLINE [[gnu::always_inline]] inline |
| #endif |
| #endif |
| |
| // Managed-dict (`__slots__ = ("__dict__",)` without an explicit dictoffset) |
| // is a CPython 3.11+ feature. On 3.9/3.10 such types instead use a regular |
| // ``tp_dictoffset != 0``, which the inactive-eligibility check catches anyway, |
| // so defining the flag as 0 here yields the correct (no-op) behavior. |
| #ifndef Py_TPFLAGS_MANAGED_DICT |
| #define Py_TPFLAGS_MANAGED_DICT 0 |
| #endif |
| |
| #include <atomic> |
| #include <cassert> |
| #include <cstring> |
| #include <utility> |
| |
| // ``_Interlocked*`` intrinsics for the MSVC arm of the spin-lock leaves below. <intrin.h>, not |
| // <windows.h>, to keep min/max etc. macros out of the Cython TU. |
| #if defined(_MSC_VER) && defined(Py_GIL_DISABLED) |
| #include <intrin.h> |
| #endif |
| |
| //================================================================================ |
| // PyObject-tying state machine |
| // |
| // Ties one Python wrapper to one C++ FFI object ("chandle") for the chandle's |
| // lifetime, so Python identity is stable: |
| // - ``a.x is a.x`` while the wrapper is live; |
| // - ``id(a.x)`` is stable across drop+refetch (while another C++ holder keeps |
| // the chandle alive); |
| // - ``f(x) is x`` whenever an FFI call returns a chandle that already has a |
| // canonical wrapper. |
| // Works on both the GIL and free-threaded (``Py_GIL_DISABLED``) builds. |
| // |
| // Wrapper vs native object |
| // ------------------------ |
| // A "wrapper" is the Python object that represents a native TVM-FFI object; it is |
| // not the native object itself. For ``x = outer.x``: |
| // |
| // Python wrapper W Native TVM-FFI object H |
| // |
| // +----------------------+ +----------------------+ |
| // | PyObject header | | refcounts | |
| // | ob_refcnt | | type_index | |
| // | ob_type | | actual object fields | |
| // | CObject.chandle --------------------> +----------------------+ |
| // +----------------------+ | |
| // ^ | |
| // +--------- tagged_pyobj in header <------+ |
| // |
| // ``W`` is a Cython ``CObject`` subclass (a container, reflected dataclass, |
| // ``Function``, or other registered class). It provides Python identity, methods, |
| // and Python reference counting; its ``chandle`` points to the native object |
| // ``H``, which holds the real FFI data and TVM-FFI's own strong/weak refcounts. |
| // |
| // Canonical wrapper |
| // ----------------- |
| // Without tying, one chandle could back several wrappers -- ``a = outer.x; |
| // b = outer.x`` gives ``a.same_as(b)`` but ``a is not b``. Tying designates ONE |
| // wrapper as canonical and records its address in the native allocation's |
| // ``tagged_pyobj`` field, so ``a is b``. That field is a RAW pointer, not a |
| // Python reference: storing ``W`` there does not bump ``W.ob_refcnt`` and does |
| // not keep ``W`` alive. |
| // |
| // Memory layout |
| // ------------- |
| // Every Object allocated through the registered Python allocator |
| // (``TVMFFIPyAllocate``) is preceded by a fixed 16-byte ``PyCustomAllocHeader``: |
| // |
| // malloc start |
| // +-------------------+--------------------------+--------+ |
| // | tagged_pyobj | TVMFFIObjectAllocHeader | T | |
| // | (offset 0..8) | delete_space (8..16) | | |
| // +-------------------+--------------------------+--------+ |
| // ^ ptr = malloc + 16 |
| // |
| // The body ``T`` starts at ``malloc + 16``, and ``base.delete_space`` sits at |
| // ``ptr - sizeof(TVMFFIObjectAllocHeader)`` so the generic C++ deleter (which |
| // knows nothing about Python) can find it. Objects NOT allocated through this |
| // allocator (e.g. some C++-static objects) have no header and never tie. |
| // |
| // Pointer tagging |
| // --------------- |
| // A wrapper allocation is >= 16-byte aligned, so ``tagged_pyobj``'s low bits are |
| // free to encode lifecycle state without growing the header. If ``W = 0x1000``: |
| // |
| // Active: 0x1000 # W |
| // Inactive: 0x1001 # W | 1 (bit 0) |
| // InTransit: 0x1003 # W | 1 | 2 (bits 0 and 1) |
| // Locked: 0x1004 # W | 4 (bit 2, free-threaded only) |
| // |
| // Tagged values are not dereferenceable ``PyObject*``; ``TVMFFIPyRemoveTag`` masks |
| // every tag bit to recover ``W``. |
| // |
| // The four states |
| // --------------- |
| // ``tagged_pyobj`` encodes four lifecycle states (Locked, bit 2, is a separate |
| // free-threaded overlay described below, not a fifth state): |
| // |
| // Detached (``NULL``) |
| // No canonical wrapper for this chandle: freshly created and not yet returned |
| // to Python, moved-from, an ineligible wrapper that died and was genuinely |
| // freed, or allocated-but-not-yet-wrapped. The next FFI return makes a fresh |
| // wrapper: Detached -> Active. |
| // |
| // Active (``W``) |
| // ``W`` is the live canonical wrapper: ``W.chandle == H`` owns one strong |
| // native ref, while the header's back-pointer owns no Python ref. When an FFI |
| // call returns ``H``: find ``W``, ``Py_INCREF`` it, drop the redundant native |
| // ref from the return, return ``W`` (hence ``f(x) is x``). Exits: -> Inactive |
| // (last Python ref gone, another native owner keeps ``H`` alive); -> Detached |
| // (move, ineligible-wrapper death, or rebind); -> freed (wrapper and ``H`` |
| // both end, via the InTransit handshake). |
| // |
| // Inactive (``W | 1``) |
| // ``W``'s Python lifetime ended (refcount 0, dealloc ran, untracked from GC, |
| // no strong native ref, not yet ``PyObject_GC_Del``'d) but its raw allocation |
| // is retained because ``H`` is still alive. A later fetch revives it in place |
| // -- zero fields -> ``PyObject_Init`` -> ``PyObject_GC_Track`` -> restore |
| // chandle -- giving Inactive -> Active at the SAME address (stable ``id()``). |
| // This is a new object lifetime at that address, not resurrection of the old |
| // one. If ``H`` dies first, its deleter frees both the cached storage and |
| // ``H``. |
| // |
| // InTransit (``W | 1 | 2``) |
| // A short-lived teardown baton (see "The dealloc handshake"). It overlays a |
| // non-live binding while the Python-side and C++-side teardown settle |
| // ownership; it never overlays a live Active wrapper. |
| // |
| // State flow: |
| // |
| // move / ineligible death |
| // +------------------------------+ |
| // | v |
| // Detached ------> Active ------> InTransit ------> Detached/freed |
| // ^ | | |
| // | | | native object survives |
| // | | v |
| // | +----------> Inactive |
| // | | |
| // +---------------------------------+ |
| // native object returned again |
| // |
| // Invariants |
| // ---------- |
| // I1. When a wrapper goes out of scope, its +1 on the chandle is always released |
| // (in ``__dealloc__`` -> ``TVMFFIPyTpDealloc``). |
| // I2. When a chandle is destroyed, its cached allocation (if any) is reclaimed. |
| // I3. ``wrapper.chandle`` is only ever a real C++ object pointer or NULL, never a |
| // sentinel. A non-NULL chandle owns +1, except inside the wrapper's own |
| // dealloc window (where it is kept only as a header locator). |
| // I4. Every ``PyObject*`` the Cython side passes to a helper here is a live |
| // wrapper (tag bits 0); only this header sets or clears the tag bits. |
| // I5. InTransit is the dealloc handshake's baton and nothing else: it overlays a |
| // non-live binding (Inactive(W) or Detached(NULL)) while the two teardown |
| // sides settle, never the live Active wrapper. Any reader that sees it -- a |
| // peer settler, or make_ret's classify -- waits the transition out. |
| // |
| // The dealloc handshake |
| // --------------------- |
| // An allocation can be torn down from two directions, and the handshake stops them |
| // from racing into a double free or a leak: |
| // * from Python -- the wrapper's refcount hits 0, so ``tp_dealloc`` -> |
| // ``tp_free`` run; |
| // * from C++ -- the chandle's weak count hits 0, so its Weak deleter fires |
| // ``TVMFFIPyDeleteSpace``. |
| // ``tp_dealloc`` cannot know which side is last (an FFI ``DecRef`` may race it from |
| // another thread), so it pre-tags ``Inactive | InTransit`` and ``DecRef``s |
| // unconditionally. The InTransit bit is a baton: the FIRST settler clears it and |
| // defers, so the SECOND finds it clear and performs the free. |
| // |
| // Flow 1 -- wrapper dies, chandle outlives it (cache the allocation): |
| // tp_dealloc : Active -> Inactive, InTransit 0 -> 1, |
| // DecRef (chandle still has refs, so no deleter fires) |
| // tp_free : InTransit 1 -> 0, keep ``self`` cached Inactive |
| // ... later, the chandle dies: |
| // delete_space : InTransit == 0, reclaim the cached wrapper and free the block |
| // |
| // Flow 2 -- wrapper held the last ref (free the allocation now): |
| // tp_dealloc : Active -> Inactive, InTransit 0 -> 1, |
| // DecRef (last ref dropped, so reentrantly fires delete_space) |
| // delete_space : InTransit 1 -> 0, defer the free back to tp_free |
| // tp_free : InTransit == 0, free the C++ block here |
| // |
| // Where transitions happen |
| // ------------------------ |
| // ``TVMFFIPyMakeRetObject`` (behind ``make_ret_object``, object.pxi) owns the |
| // whole return-object transition in one frame: |
| // Detached / Active / Inactive -> Active (fresh / revived-in-place / cached). |
| // ``TVMFFIPyTpDealloc`` (CObject.__dealloc__), when the wrapper refcount hits 0: |
| // Active -> Inactive (eligible; tag Inactive | InTransit, then DecRef); |
| // Active -> Detached (ineligible; detach first, then DecRef). |
| // ``TVMFFIPyArgSetterObjectRValueRef_`` (function.pxi) and |
| // ``__move_handle_from__`` (object.pxi): |
| // Active -> Detached (detach before a move nulls the source chandle). |
| // ``TVMFFIPyDeleteSpace`` (Weak deleter), when the chandle weak count hits 0: |
| // Inactive | InTransit -> defer both frees to ``tp_free``; |
| // Inactive (settled) -> reclaim the cached wrapper and free the block. |
| // |
| // Slot install |
| // ------------ |
| // Two slot families, each unmissable over a different scope: |
| // * ``tp_dealloc`` (correctness, I1): installed once on ``CObject`` and inherited |
| // by every subtype -- nothing to install per type. |
| // * ``tp_alloc`` / ``tp_free`` (the cache-&-revive optimization): installed per |
| // registered type at the sole registration choke point ``_update_registry`` |
| // (object.pxi), because these slots are not inherited by dynamic subtypes. An |
| // unregistered subtype simply fails eligibility and genuine-frees (losing |
| // stable-id-across-drop, not correctness). |
| // ``tp_dealloc`` works with either pairing: with the custom ``tp_alloc`` / |
| // ``tp_free`` it caches (eligible), and with the generic ones it detaches + |
| // genuine-frees -- the eligibility gate keys on the same ``tp_free``, so the two |
| // can never disagree. |
| // |
| // Shutdown guard |
| // -------------- |
| // ``TVMFFIPyMarkPythonFinalizing`` is wired to atexit from Cython module init. |
| // After it fires, inactive cached allocations on still-live chandles are |
| // intentionally leaked (the process is exiting; the OS reclaims) rather than |
| // reaching for ``PyGILState_Ensure`` on a teardown interpreter. |
| // |
| // Free-threaded builds (``Py_GIL_DISABLED``) |
| // ------------------------------------------ |
| // Without the GIL the bare ``tagged_pyobj`` reads/writes race -- an Active-hit read |
| // is a use-after-free (``make_ret`` reads the wrapper; a concurrent dealloc frees |
| // it before the IncRef). The tie stays enabled; two FT-only mechanisms close the |
| // gap, both behind ``#ifdef Py_GIL_DISABLED`` so the GIL build is byte-for-byte |
| // unchanged: |
| // * The word is its own spin-lock: the Locked bit (bit 2) is CAS-acquired via the |
| // portable pointer-atomic leaves (``__atomic_*`` on GCC/Clang, ``_Interlocked*`` |
| // on MSVC), so every transition serializes its word edits. A holder sets bit 2, |
| // reasons about the underlying state with the bit masked off, then publishes the |
| // new state without it. The GIL build never sets it. |
| // * The Active hit uses ``PyUnstable_TryIncRef`` (inc-if-nonzero), not |
| // ``Py_INCREF``, so it fails on a wrapper a concurrent dealloc is collecting. |
| // The Active lookup then: lock the word; ``TryIncRef``; if alive, return the |
| // wrapper; if its refcount already hit 0, wait for the dealloc to transition the |
| // state and retry as Inactive or Detached. |
| //================================================================================ |
| |
| /*! |
| * \brief Python-side derived allocation header, sitting immediately before the |
| * object body (see "Memory layout" in the banner above). |
| * |
| * \c tagged_pyobj is the tagged pointer to the canonical wrapper (Detached / |
| * Active / Inactive / InTransit, plus the free-threaded Locked overlay -- see |
| * "Pointer tagging" and "The four states" in the banner). \c base is the generic |
| * ``TVMFFIObjectAllocHeader``; its \c delete_space sits at |
| * ``ptr - sizeof(TVMFFIObjectAllocHeader)`` so the C++ deleter (which knows |
| * nothing about Python) can find it. The ``TVMFFIPyTag*`` helpers below only |
| * inspect or transform the encoded word -- they never touch refcounts, allocate, |
| * or free. |
| */ |
| struct PyCustomAllocHeader { |
| PyObject* tagged_pyobj; |
| TVMFFIObjectAllocHeader base; |
| }; |
| |
| static_assert(sizeof(PyCustomAllocHeader) == 16, |
| "header must be 16 bytes so T at ptr = malloc + 16 is naturally " |
| "aligned for alignof(T) up to alignof(max_align_t)"); |
| static_assert(offsetof(PyCustomAllocHeader, base) == |
| sizeof(PyCustomAllocHeader) - sizeof(TVMFFIObjectAllocHeader), |
| "base must sit at ptr - sizeof(TVMFFIObjectAllocHeader) for the " |
| "C++ deleter to find it"); |
| |
| /*! \brief Recover the ``PyCustomAllocHeader`` sitting immediately before an object body. |
| * \param ptr The object (``T``) pointer returned by the allocator. |
| * \return The header at ``ptr - sizeof(PyCustomAllocHeader)``. */ |
| TVM_FFI_INLINE PyCustomAllocHeader* TVMFFIPyHeader(void* ptr) { |
| return reinterpret_cast<PyCustomAllocHeader*>(static_cast<char*>(ptr) - |
| sizeof(PyCustomAllocHeader)); |
| } |
| |
| // The tag bits on ``tagged_pyobj`` (see "Pointer tagging" in the banner): bit 0 Inactive, bit 1 |
| // InTransit, bit 2 Locked (free-threaded spin-lock only; the GIL build never sets it). |
| // ``TVMFFIPyRemoveTag`` masks every defined bit. |
| constexpr uintptr_t kPyCachedInactiveTagBit = 1; |
| constexpr uintptr_t kPyInTransitTagBit = 2; |
| #ifdef Py_GIL_DISABLED |
| constexpr uintptr_t kPyLockedTagBit = 4; |
| constexpr uintptr_t kPyTagBitMask = kPyCachedInactiveTagBit | kPyInTransitTagBit | kPyLockedTagBit; |
| #else |
| constexpr uintptr_t kPyTagBitMask = kPyCachedInactiveTagBit | kPyInTransitTagBit; |
| #endif |
| |
| /*! \brief True iff the Inactive bit is set on ``tagged``. |
| * \param tagged The raw ``tagged_pyobj`` word. |
| * \return Whether bit 0 (Inactive) is set. */ |
| TVM_FFI_INLINE bool TVMFFIPyTagIsInactive(PyObject* tagged) { |
| return (reinterpret_cast<uintptr_t>(tagged) & kPyCachedInactiveTagBit) != 0; |
| } |
| /*! \brief True iff the InTransit bit is set on ``tagged``. |
| * \param tagged The raw ``tagged_pyobj`` word. |
| * \return Whether bit 1 (InTransit) is set. */ |
| TVM_FFI_INLINE bool TVMFFIPyTagInTransit(PyObject* tagged) { |
| return (reinterpret_cast<uintptr_t>(tagged) & kPyInTransitTagBit) != 0; |
| } |
| /*! \brief Strip every tag bit, yielding the bare wrapper pointer. |
| * \param tagged The raw ``tagged_pyobj`` word. |
| * \return ``tagged`` with all defined tag bits cleared. */ |
| TVM_FFI_INLINE PyObject* TVMFFIPyRemoveTag(PyObject* tagged) { |
| return reinterpret_cast<PyObject*>(reinterpret_cast<uintptr_t>(tagged) & ~kPyTagBitMask); |
| } |
| /*! \brief Clear ONLY the InTransit bit (Inactive|InTransit -> Inactive, settled). |
| * \param tagged The raw ``tagged_pyobj`` word. |
| * \return ``tagged`` with bit 1 cleared, others kept. */ |
| TVM_FFI_INLINE PyObject* TVMFFIPyTagClearInTransit(PyObject* tagged) { |
| return reinterpret_cast<PyObject*>(reinterpret_cast<uintptr_t>(tagged) & ~kPyInTransitTagBit); |
| } |
| |
| //--------------------------------------------------------------- |
| // Word-access leaves: the ONE place the GIL / free-threaded divergence lives. Every transition |
| // body below (make_ret, the dealloc family, Rebind) is written once against the small vocabulary |
| // below, so the logic reads identically on both builds and the build difference is confined here. |
| // |
| // Vocabulary -- two layers, two naming rules, never mixed: |
| // * Public ``TVMFFIPy*`` leaves name LOCK SEMANTICS -- what you hold on return: |
| // ``Lock...`` -> returns HOLDING the lock + the prior binding (``LockWord``, |
| // ``LockClassifyActive``); |
| // ``Unlock...`` -> returns having RELEASED it, publishing the named new state |
| // (``UnlockWord`` = arbitrary, ``UnlockKeep`` = unchanged); |
| // ``Peek...`` -> never touches the lock (``PeekWord``, a lock-free read). |
| // So "Acquire" as a lock verb never appears up here -- ``PeekWord`` is the lock-free read even |
| // though its body uses an acquire-ordered load. Two non-lock helpers complete the set: |
| // ``EnableTryIncRef`` (arm a wrapper for a racing reader's TryIncRef before publish) and |
| // ``SpinYield`` (GC-safe back-off on a wait; free-threaded only). |
| // * Detail ``pyobj_detail::Word*`` leaves name the MEMORY-ORDER MECHANISM -- |
| // ``Load{Relaxed,Acquire}`` / ``StoreRelease`` / ``CASAcquire``. "Acquire"/"Release" mean the |
| // C++ memory order here, and live ONLY in this layer. |
| // |
| // Consumers -- three operations. Each ``lock #N`` ... ``unlock #N`` pair brackets one critical |
| // section; indented branches are the mutually exclusive paths, each "condition: actions". |
| // |
| // Return -- make_ret: |
| // LockClassifyActive <- lock #1 (returns HELD) |
| // ├─ Active hit : UnlockKeep <- unlock #1; drop +1; return live |
| // └─ miss : NewWrapper, then |
| // UnlockWord(obj on ok / cur on OOM) <- unlock #1 (publish Active / undo) |
| // |
| // Rebind -- CompareAndRebindPyObject (move / construct / detach): |
| // LockWord <- lock #2 |
| // ├─ cur == expect or Detached : UnlockWord(new) <- unlock #2 (swap the binding) |
| // └─ else (not ours / busy) : UnlockKeep <- unlock #2 (release unchanged) |
| // |
| // Teardown -- the two-direction handshake (Flow 1 / Flow 2 above); three participants, each its |
| // own bracket. tp_dealloc opens (sets InTransit); tp_free / delete_space settle it. |
| // tp_dealloc: LockWord <- lock #3 |
| // ├─ ours + eligible : UnlockWord(Inactive|InTransit) <- unlock #3 (then DecRef) |
| // ├─ ours, ineligible: UnlockWord(NULL) <- unlock #3 (detach; then DecRef) |
| // └─ not ours : UnlockKeep <- unlock #3 (then DecRef) |
| // tp_free: LockWord <- lock #4 |
| // ├─ InTransit == 1 : UnlockWord(clear it) <- unlock #4 (keep self cached) |
| // └─ InTransit == 0 : UnlockKeep ; AlignedFree <- unlock #4 (we free the block) |
| // delete_space: PeekWord <- lock-free peek |
| // ├─ Detached : AlignedFree <- no bracket (fast path) |
| // └─ Inactive : LockWord <- lock #5 |
| // ├─ InTransit == 1 : UnlockWord(clear it) <- unlock #5 (defer to tp_free) |
| // └─ InTransit == 0 : UnlockWord(NULL) ; reclaim wrapper + free <- unlock #5 |
| // |
| // On the GIL build the GIL already serializes every transition, so there is no spin-lock: each |
| // leaf collapses to a plain field access (load / store / no-op), a lock-free simplification of the |
| // free-threaded structure that emits byte-for-byte unchanged. |
| //--------------------------------------------------------------- |
| |
| #ifdef Py_GIL_DISABLED |
| /*! \brief True iff the Locked spin-lock bit is set on ``tagged`` (free-threaded only). |
| * \param tagged The raw ``tagged_pyobj`` word. |
| * \return Whether bit 2 (Locked) is set. */ |
| TVM_FFI_INLINE bool TVMFFIPyTagIsLocked(PyObject* tagged) { |
| return (reinterpret_cast<uintptr_t>(tagged) & kPyLockedTagBit) != 0; |
| } |
| |
| /*! \brief GC-safe back-off for any wait on the word (the spin-loop's yield -- not a lock op |
| * itself). Must run with an attached thread state and WITHOUT the word lock held. */ |
| TVM_FFI_INLINE void TVMFFIPySpinYield() { |
| PyThreadState* tstate = PyEval_SaveThread(); |
| PyEval_RestoreThread(tstate); |
| } |
| |
| // Raw pointer-atomics on the word -- the only bare loads/stores/CAS (transitions go through |
| // Lock/Unlock/Peek). Dual-coded ``_Interlocked*`` on MSVC / ``__atomic_*`` elsewhere. |
| namespace tvm { |
| namespace ffi { |
| namespace pyobj_detail { |
| |
| /*! \brief Relaxed load of the word -- only ever the CAS seed, so a stale value just retries. |
| * \param h The allocation header. |
| * \return The current ``tagged_pyobj`` (relaxed). */ |
| TVM_FFI_INLINE PyObject* WordLoadRelaxed(PyCustomAllocHeader* h) { |
| #if defined(_MSC_VER) |
| return reinterpret_cast<PyObject* const volatile*>(&h->tagged_pyobj)[0]; // NOLINT(*) |
| #else |
| return __atomic_load_n(&h->tagged_pyobj, __ATOMIC_RELAXED); |
| #endif |
| } |
| |
| /*! \brief Acquire load of the word (standalone sync edge). On MSVC a CAS NULL/NULL: an |
| * acquire-ordered read that never writes. |
| * \param h The allocation header. |
| * \return The current ``tagged_pyobj`` (acquire). */ |
| TVM_FFI_INLINE PyObject* WordLoadAcquire(PyCustomAllocHeader* h) { |
| #if defined(_MSC_VER) |
| return reinterpret_cast<PyObject*>(_InterlockedCompareExchangePointer( |
| reinterpret_cast<void* volatile*>(&h->tagged_pyobj), nullptr, nullptr)); |
| #else |
| return __atomic_load_n(&h->tagged_pyobj, __ATOMIC_ACQUIRE); |
| #endif |
| } |
| |
| /*! \brief Release store of the word, publishing ``v`` to a later acquire. |
| * \param h The allocation header. |
| * \param v The new ``tagged_pyobj`` value to publish. */ |
| TVM_FFI_INLINE void WordStoreRelease(PyCustomAllocHeader* h, PyObject* v) { |
| #if defined(_MSC_VER) |
| _InterlockedExchangePointer(reinterpret_cast<void* volatile*>(&h->tagged_pyobj), v); |
| #else |
| __atomic_store_n(&h->tagged_pyobj, v, __ATOMIC_RELEASE); |
| #endif |
| } |
| |
| /*! \brief CAS, acquire-on-success (not acq_rel -- nothing synchronizes on the Locked-bit store it |
| * publishes). |
| * \param h The allocation header. |
| * \param expect In: value to match; on failure, reloaded to the current word. |
| * \param desired The value to store on a match. |
| * \return True if ``*expect`` matched and ``desired`` stored; else false, ``*expect`` reloaded. */ |
| TVM_FFI_INLINE bool WordCASAcquire(PyCustomAllocHeader* h, PyObject** expect, PyObject* desired) { |
| #if defined(_MSC_VER) |
| PyObject* prev = reinterpret_cast<PyObject*>(_InterlockedCompareExchangePointer( |
| reinterpret_cast<void* volatile*>(&h->tagged_pyobj), desired, *expect)); |
| if (prev == *expect) return true; |
| *expect = prev; |
| return false; |
| #else |
| return __atomic_compare_exchange_n(&h->tagged_pyobj, expect, desired, /*weak=*/true, |
| __ATOMIC_ACQUIRE, __ATOMIC_RELAXED); |
| #endif |
| } |
| |
| } // namespace pyobj_detail |
| } // namespace ffi |
| } // namespace tvm |
| |
| /*! \brief Acquire the per-word spin-lock (CAS on the Locked bit); release it via |
| * ``TVMFFIPyUnlockWord`` / ``TVMFFIPyUnlockKeep``. |
| * \param h The allocation header whose word to lock. |
| * \return The prior binding with the Locked bit cleared (the state to reason about while held). */ |
| TVM_FFI_INLINE PyObject* TVMFFIPyLockWord(PyCustomAllocHeader* h) { |
| for (;;) { |
| PyObject* cur = ::tvm::ffi::pyobj_detail::WordLoadRelaxed(h); |
| if (!TVMFFIPyTagIsLocked(cur)) { |
| PyObject* locked = |
| reinterpret_cast<PyObject*>(reinterpret_cast<uintptr_t>(cur) | kPyLockedTagBit); |
| // Acquire on success so the locked section happens-after the matching release. |
| if (::tvm::ffi::pyobj_detail::WordCASAcquire(h, &cur, locked)) { |
| return cur; |
| } |
| // CAS failed (lost the race or spurious); ``cur`` reloaded -- retry without |
| // yielding, the word was not Locked so contention is brief. |
| continue; |
| } |
| TVMFFIPySpinYield(); |
| } |
| } |
| |
| /*! \brief Release the lock, transitioning the binding to ``new_state``. |
| * \param h The allocation header whose lock to release. |
| * \param new_state The binding to publish (Active wrapper, Inactive|InTransit, or NULL). */ |
| TVM_FFI_INLINE void TVMFFIPyUnlockWord(PyCustomAllocHeader* h, PyObject* new_state) { |
| ::tvm::ffi::pyobj_detail::WordStoreRelease(h, new_state); |
| } |
| |
| /*! \brief Release the lock leaving the binding unchanged (a no-op on the GIL arm). |
| * \param h The allocation header whose lock to release. |
| * \param cur The unchanged binding to republish (as returned by ``TVMFFIPyLockWord``). */ |
| TVM_FFI_INLINE void TVMFFIPyUnlockKeep(PyCustomAllocHeader* h, PyObject* cur) { |
| TVMFFIPyUnlockWord(h, cur); |
| } |
| |
| /*! \brief Read the binding WITHOUT taking the lock (a lock-free peek). Acquire-ordered: this |
| * standalone read is the sync edge, so an observed state happens-after its publishing |
| * unlock. (The "acquire" is the memory order, not the lock; hence ``Peek``.) |
| * \param h The allocation header. |
| * \return The current binding (tag bits intact). */ |
| TVM_FFI_INLINE PyObject* TVMFFIPyPeekWord(PyCustomAllocHeader* h) { |
| return ::tvm::ffi::pyobj_detail::WordLoadAcquire(h); |
| } |
| |
| /*! \brief Arm ``obj`` for a concurrent reader's ``TryIncRef``; sequence before publishing it Active |
| * (no-op for NULL). ``PyUnstable_EnableTryIncRef`` is 3.14+ free-threading API, compiled |
| * only on this arm. |
| * \param obj The wrapper about to be published Active, or NULL. */ |
| TVM_FFI_INLINE void TVMFFIPyEnableTryIncRef(PyObject* obj) { |
| if (obj != nullptr) PyUnstable_EnableTryIncRef(obj); |
| } |
| |
| /*! \brief Settle the binding and acquire the right to transition it. Returns with the lock |
| * HELD in both outcomes: |
| * (true, cur) Active -- ``cur`` is the live wrapper, already inc-ref'd. |
| * (false, W|Inactive) Inactive -- ``cur`` is a revivable cached allocation. |
| * (false, NULL) Detached -- no wrapper bound. |
| * (true, NULL) cannot occur. |
| * Waits out any in-flight dealloc handshake (marked InTransit) before settling. |
| * \param h The allocation header. |
| * \param out_pyobj Out: the settled binding (inc-ref'd live wrapper / cached alloc / NULL). |
| * \return True on an Active hit (``*out_pyobj`` inc-ref'd); false for Inactive or Detached. */ |
| TVM_FFI_INLINE bool TVMFFIPyLockClassifyActive(PyCustomAllocHeader* h, PyObject** out_pyobj) { |
| for (;;) { |
| PyObject* cur = TVMFFIPyLockWord(h); |
| if (TVMFFIPyTagInTransit(cur)) { // (1) a dealloc handshake is mid-transition |
| TVMFFIPyUnlockKeep(h, cur); |
| TVMFFIPySpinYield(); |
| continue; |
| } |
| if (cur != nullptr && !TVMFFIPyTagIsInactive(cur)) { // (2) Active candidate |
| // Branch (1) ruled out InTransit and this guard rules out Inactive, so no tag bits are |
| // set: ``cur`` is a bare, valid PyObject* -- safe to hand to TryIncRef. |
| if (PyUnstable_TryIncRef(cur)) { |
| *out_pyobj = cur; |
| return true; // Active hit -- lock HELD, cur inc-ref'd |
| } |
| TVMFFIPyUnlockKeep(h, cur); // dying: let its dealloc settle the word, then retry |
| TVMFFIPySpinYield(); |
| continue; |
| } |
| *out_pyobj = cur; // (3) Inactive(W) clean, or (4) Detached(NULL) |
| return false; // lock HELD |
| } |
| } |
| |
| #else |
| // GIL build: the word is a plain field; the GIL is the lock. Each leaf is the exact field access |
| // the pre-merge code performed (or a no-op where it did nothing); see the free-threaded arm above |
| // for the full per-function contracts these mirror. |
| /*! \brief GIL arm of ``TVMFFIPyLockWord``: a plain field read (the GIL is the lock). */ |
| TVM_FFI_INLINE PyObject* TVMFFIPyLockWord(PyCustomAllocHeader* h) { return h->tagged_pyobj; } |
| /*! \brief GIL arm of ``TVMFFIPyUnlockWord``: a plain field store. */ |
| TVM_FFI_INLINE void TVMFFIPyUnlockWord(PyCustomAllocHeader* h, PyObject* new_state) { |
| h->tagged_pyobj = new_state; |
| } |
| /*! \brief GIL arm of ``TVMFFIPyUnlockKeep``: unchanged binding, so a no-op (no store). */ |
| TVM_FFI_INLINE void TVMFFIPyUnlockKeep(PyCustomAllocHeader*, PyObject*) {} |
| /*! \brief GIL arm of ``TVMFFIPyPeekWord``: a plain field read. */ |
| TVM_FFI_INLINE PyObject* TVMFFIPyPeekWord(PyCustomAllocHeader* h) { return h->tagged_pyobj; } |
| /*! \brief GIL arm of ``TVMFFIPyEnableTryIncRef``: no TryIncRef synchronizer on the GIL, a no-op. */ |
| TVM_FFI_INLINE void TVMFFIPyEnableTryIncRef(PyObject*) {} |
| |
| /*! \brief GIL arm of ``TVMFFIPyLockClassifyActive``: a plain field read + Py_INCREF on an Active |
| * hit. The FT arm's TryIncRef + InTransit-wait loop has no GIL analog (the GIL serializes |
| * everything), so this leaf stays split. |
| * \param h The allocation header. |
| * \param out_pyobj Out: the binding (INCREF'd live wrapper / cached alloc / NULL). |
| * \return True on an Active hit; false for Inactive or Detached. */ |
| TVM_FFI_INLINE bool TVMFFIPyLockClassifyActive(PyCustomAllocHeader* h, PyObject** out_pyobj) { |
| PyObject* cur = h->tagged_pyobj; |
| if (cur != nullptr && !TVMFFIPyTagIsInactive(cur)) { // Active: live canonical wrapper |
| Py_INCREF(cur); |
| *out_pyobj = cur; |
| return true; |
| } |
| *out_pyobj = cur; // Inactive(W) or Detached(NULL) |
| return false; |
| } |
| #endif // Py_GIL_DISABLED |
| |
| /*! |
| * \brief Per-thread vehicle carrying the inactive cached allocation address from |
| * ``make_ret_object`` (which knows the chandle) down into |
| * ``TVMFFIPyTpAlloc`` (which is handed only ``type`` and an item count). |
| * |
| * The slot's sole access primitive is a swap: store ``next``, return the prior |
| * value. Taking the block is thus ``TVMFFIPyTLSReviveSlot(nullptr)`` -- |
| * read-and-clear in one step, with no separate clear to forget. Per-thread -> |
| * free-threading safe. |
| * |
| * \param next The value to store into the slot (a cached alloc to arm, or NULL to clear). |
| * \return The prior slot value (the armed block on a take, else NULL). |
| */ |
| inline PyObject* TVMFFIPyTLSReviveSlot(PyObject* next) { |
| static thread_local PyObject* slot = nullptr; |
| std::swap(slot, next); |
| return next; |
| } |
| |
| /*! \brief Arm the cached allocation to be reused by the next ``tp_alloc`` on |
| * this thread. Called by ``make_ret_object`` immediately before ``cls.__new__``. |
| * \param cached_alloc The inactive cached allocation to revive on the next ``tp_alloc``. */ |
| TVM_FFI_INLINE void TVMFFIPySetReviveBlock(PyObject* cached_alloc) { |
| TVMFFIPyTLSReviveSlot(cached_alloc); |
| } |
| |
| // Forward decl; defined below. |
| // |
| // NOTE: deliberately *not* TVM_FFI_INLINE. TVM_FFI_INLINE expands to |
| // [[gnu::always_inline]] which forbids taking the function's address as |
| // a stable, callable pointer — and we hand the address to the C++ side |
| // (stored in PyCustomAllocHeader::base.delete_space at allocate time). |
| inline void TVMFFIPyDeleteSpace(void* ptr); |
| |
| // Atexit-driven shutdown guard. ``TVMFFIPyMarkPythonFinalizing`` flips |
| // the flag to false from an atexit hook registered in Cython module init; |
| // ``TVMFFIPyDeleteSpace`` reads it via ``TVMFFIPyIsPythonAlive`` before |
| // ``PyGILState_Ensure`` to avoid touching a teardown interpreter. |
| /*! \brief The process-wide "Python still alive" flag storage (function-static). |
| * \return Reference to the atomic flag (true until finalization begins). */ |
| inline std::atomic<bool>& TVMFFIPyAliveFlagStorage() { |
| static std::atomic<bool> flag{true}; |
| return flag; |
| } |
| |
| /*! \brief Whether Python is still alive (finalization has not begun). |
| * \return True until ``TVMFFIPyMarkPythonFinalizing`` has fired. */ |
| inline bool TVMFFIPyIsPythonAlive() noexcept { |
| return TVMFFIPyAliveFlagStorage().load(std::memory_order_acquire); |
| } |
| |
| /*! \brief Mark Python as finalizing (atexit hook); after this, ``delete_space`` skips the GIL. */ |
| inline void TVMFFIPyMarkPythonFinalizing() noexcept { |
| TVMFFIPyAliveFlagStorage().store(false, std::memory_order_release); |
| } |
| |
| /*! |
| * \brief Whether ``chandle`` participates in Python object tying. |
| * |
| * Object tying is dormant during the compatibility rollout: public object |
| * constructors still use the legacy no-prefix allocation layout, so probing |
| * memory before an arbitrary object body would be invalid. Keep the complete |
| * tying implementation compiled, but route every object through the ordinary |
| * fresh-wrapper path until allocation is activated in a later release. |
| * |
| * \param chandle The FFI object handle to test. |
| * \return False while object tying remains dormant. |
| */ |
| TVM_FFI_INLINE bool TVMFFIPyIsCanonical(void* chandle) { |
| (void)chandle; |
| return false; |
| } |
| |
| //--------------------------------------------------------------- |
| // Forward declarations shared by SECTION A (make_ret) and the lifecycle sections. |
| //--------------------------------------------------------------- |
| |
| /*! \brief Address of a CObject wrapper's ``chandle`` field (defined in object.pxi). |
| * \param ptr The wrapper object. |
| * \return Pointer to its ``chandle`` field. */ |
| __PYX_EXTERN_C void** TVMFFICyObjectGetCHandlePtr(PyObject* ptr); |
| |
| inline void TVMFFIPyTpFree(void* self); |
| |
| //--------------------------------------------------------------- |
| // SECTION A -- alloc / revival / make_ret (HOT: per construction / per FFI return). |
| //--------------------------------------------------------------- |
| |
| /*! |
| * \brief Allocator entry registered with TVMFFISetCustomAllocator at |
| * Cython module init. Allocates ``sizeof(PyCustomAllocHeader) + size`` bytes |
| * with ``alignment``, zero-inits the header to the Detached |
| * state, wires ``base.delete_space = &TVMFFIPyDeleteSpace``, and |
| * returns the T location. |
| * |
| * Handler::New static_asserts ``alignof(T) <= alignof(max_align_t)``, so |
| * the runtime ``alignment`` is bounded and ``base + sizeof(PyCustomAllocHeader)`` |
| * (= ``base + 16``) lands T naturally aligned for any T we allocate. |
| * |
| * \param size The object body (``T``) size requested by the core allocator. |
| * \param alignment The required alignment of ``T``. |
| * \param type_index (unused) The FFI type index of the allocation. |
| * \param context (unused) The allocator context registered alongside this entry. |
| * \return Pointer to the ``T`` body (header prepended, Detached). |
| */ |
| inline void* TVMFFIPyAllocate(size_t size, size_t alignment, int32_t /*type_index*/, |
| void* /*context*/) { |
| void* base_alloc = |
| ::tvm::ffi::details::AlignedAlloc(sizeof(PyCustomAllocHeader) + size, alignment); |
| auto* h = static_cast<PyCustomAllocHeader*>(base_alloc); |
| h->tagged_pyobj = nullptr; // Detached |
| h->base.delete_space = &TVMFFIPyDeleteSpace; |
| return static_cast<char*>(base_alloc) + sizeof(PyCustomAllocHeader); |
| } |
| |
| /*! \brief Allocate (fresh) or revive (in place, at ``revive``'s address) a wrapper of |
| * type ``tp`` via ``tp_new``. Build-agnostic: touches no word state, only the per-thread |
| * revive slot + ``tp_new``; shared by both builds' ``TVMFFIPyMakeRetObject``. |
| * |
| * On return ``*out_revive_consumed`` reports whether ``revive`` was taken out of the slot by a |
| * ``tp_alloc`` (revived in place). The caller needs this on the failure path: a consumed block no |
| * longer exists (the failed instance's ``tp_free`` freed it), so the word must NOT be republished |
| * as ``Inactive(revive)`` -- that would dangle. May be NULL when ``revive`` is NULL. |
| * |
| * \param tp The wrapper type to instantiate. |
| * \param revive An inactive cached allocation to revive in place, or NULL for a fresh alloc. |
| * \param out_revive_consumed Out: set true iff ``revive`` (non-NULL) was consumed by a tp_alloc. |
| * \return A new reference (refcount 1), or NULL with a Python error set. */ |
| inline PyObject* TVMFFIPyNewWrapper(PyTypeObject* tp, PyObject* revive, bool* out_revive_consumed) { |
| if (out_revive_consumed != nullptr) *out_revive_consumed = false; |
| if (revive != nullptr) TVMFFIPySetReviveBlock(revive); |
| PyObject* args = PyTuple_New(0); |
| // Near-dead (() is an immortal singleton) but required: tp_new does PyTuple_GET_SIZE(args) |
| // unchecked, so a NULL here would segfault rather than report. |
| if (args == nullptr) { |
| TVMFFIPySetReviveBlock(nullptr); // disarm: tp_new will not run |
| return nullptr; |
| } |
| PyObject* obj = tp->tp_new(tp, args, nullptr); |
| Py_DECREF(args); |
| // Clear the slot and observe whether tp_alloc consumed the armed block: a cleared slot |
| // (leftover != revive) means the block was revived in place -- and on a tp_new failure its bytes |
| // were then freed by tp_free -- so the caller must not republish it as Inactive(revive). |
| PyObject* leftover = TVMFFIPyTLSReviveSlot(nullptr); |
| if (out_revive_consumed != nullptr) { |
| *out_revive_consumed = (revive != nullptr && leftover != revive); |
| } |
| return obj; |
| } |
| |
| /*! |
| * \brief Set ``chandle``'s cached canonical PyObject to ``new_object``, but only if the word is |
| * still what the caller expected (``cur == expect``, or Detached); otherwise leave it |
| * untouched. ``new_object == NULL`` clears the binding. No-op for non-canonical chandles. |
| * |
| * All three callers are the same conditional set -- "make ``new_object`` canonical iff the word is |
| * still ``expect``" -- differing only in the arguments: |
| * - construct (object.pxi, ``expect=NULL, new=self``): a fresh wrapper claims a just-constructed |
| * chandle; Detached is expected because nothing else holds this brand-new chandle yet. |
| * - move (object.pxi ``__move_handle_from__``, ``expect=other, new=self``): hand canonical status |
| * from ``other`` to ``self``. |
| * - detach (function.pxi rvalue-ref setter, ``expect=src, new=NULL``): clear the binding before a |
| * move nulls the source chandle. |
| * |
| * If the word is NOT ``expect`` (a concurrent make_ret or move rebound it first), the set simply |
| * no-ops -- ``new_object`` stays a valid wrapper that owns the chandle but is not the canonical |
| * one. The only cost is a missed identity share; its ``tp_dealloc`` sees ``cur != wrapper``, takes |
| * the not-ours branch, and genuine-frees without touching the cache. |
| * |
| * \param chandle FFI object handle whose cached-wrapper word is (re)bound; non-canonical: no-op. |
| * \param expect Rebind only if the current binding equals this (Detached also matches). |
| * \param new_object Wrapper to publish as canonical, or NULL to detach. |
| */ |
| TVM_FFI_INLINE void TVMFFIPyCompareAndRebindPyObject(void* chandle, PyObject* expect, |
| PyObject* new_object) { |
| if (!TVMFFIPyIsCanonical(chandle)) return; |
| PyCustomAllocHeader* h = TVMFFIPyHeader(chandle); |
| // Arm before publishing Active, so a racing Active-hit make_ret can safely TryIncRef |
| // ``new_object`` (no-op for new_object == NULL and on the GIL build). |
| TVMFFIPyEnableTryIncRef(new_object); |
| PyObject* cur = TVMFFIPyLockWord(h); |
| if (cur == expect || cur == nullptr) { |
| TVMFFIPyUnlockWord(h, new_object); // publish new_object (Active, or Detached when NULL) |
| } else { |
| TVMFFIPyUnlockKeep(h, cur); // not ours / busy: release unchanged (GIL: no store) |
| } |
| } |
| |
| /*! |
| * \brief Wrap a returned ``chandle`` into its canonical Python wrapper -- the |
| * whole Detached / Active / Inactive transition in one frame, behind |
| * Cython's ``make_ret_object``. |
| * |
| * The caller owns +1 (strong) on ``chandle``; ownership transfers to the |
| * returned wrapper. Returns a new owned reference, or NULL with a Python error |
| * set (the Cython side declares this ``object``, so NULL propagates as an |
| * exception). |
| * |
| * \param chandle The returned object handle (caller owns +1 strong). |
| * \param cls_type The wrapper class to instantiate (a ``PyTypeObject*``). |
| * \return New owned wrapper reference, or NULL with a Python error set. |
| */ |
| // make_ret: one shared body over the four word states, written against the make_ret leaves. |
| // Non-canonical -> fresh wrapper, no tie (FT cannot even locate a header here). |
| // Active -> return the live canonical wrapper (classify inc-ref'd it). |
| // Inactive(W) -> revive W in place at the same address (stable id()). |
| // Detached(NULL) -> fresh wrapper, bound canonical. |
| inline PyObject* TVMFFIPyMakeRetObject(void* chandle, PyObject* cls_type) { |
| PyTypeObject* tp = reinterpret_cast<PyTypeObject*>(cls_type); |
| // Non-canonical chandle (no Python alloc header, e.g. a C++-static registry object): |
| // never tied -- wrap fresh, transferring the caller's +1. |
| if (!TVMFFIPyIsCanonical(chandle)) { |
| PyObject* obj = TVMFFIPyNewWrapper(tp, nullptr, nullptr); |
| if (obj == nullptr) { // live OOM/tp_new failure: release the caller's +1 before propagating |
| TVMFFIObjectDecRef(chandle); |
| return nullptr; |
| } |
| *TVMFFICyObjectGetCHandlePtr(obj) = chandle; |
| return obj; |
| } |
| PyCustomAllocHeader* h = TVMFFIPyHeader(chandle); |
| PyObject* cur; |
| // Active hit: return the live canonical wrapper (classify inc-ref'd it); drop caller's +1. |
| if (TVMFFIPyLockClassifyActive(h, &cur)) { |
| TVMFFIPyUnlockKeep(h, cur); |
| TVMFFIObjectDecRef(chandle); |
| return cur; |
| } |
| // Inactive(W) -> revive at W's address, Detached(NULL) -> fresh. ``classify`` returned the lock |
| // HELD and we KEEP it across the alloc, so a peer make_ret just spins on ``LockWord`` until we |
| // publish -- one critical section, no InTransit needed here (like the GIL build). |
| PyObject* reused_pyobj_space = TVMFFIPyRemoveTag(cur); |
| // NOTE: alloc runs UNDER the lock, so a tied ``__cinit__`` must not re-enter the tie on this same |
| // chandle (would self-deadlock). Holds today: tied ``__cinit__`` only nulls the chandle field. |
| bool revive_consumed = false; |
| PyObject* obj = TVMFFIPyNewWrapper(tp, reused_pyobj_space, |
| &revive_consumed); // alloc / revive WITH LOCK HELD |
| if (obj == nullptr) { |
| // OOM/tp_new failure. If a tp_alloc already revived (and thus, on failure, freed) the cached |
| // block, ``reused_pyobj_space`` no longer exists: publish Detached, NOT Inactive(W), or the |
| // word would dangle at freed bytes (double free when the chandle later dies). If it was |
| // untouched (failure before tp_alloc), restore the original word (Inactive(W) / Detached). |
| TVMFFIPyUnlockWord(h, revive_consumed ? nullptr : cur); |
| TVMFFIObjectDecRef(chandle); |
| return nullptr; |
| } |
| *TVMFFICyObjectGetCHandlePtr(obj) = chandle; // caller's +1 transfers to obj |
| TVMFFIPyEnableTryIncRef(obj); |
| TVMFFIPyUnlockWord(h, obj); // release, publish Active(obj) |
| return obj; |
| } |
| |
| /*! |
| * \brief True iff a wrapper of ``wrapper``'s type may be cached & revived. |
| * |
| * Requirements (all must hold, else we genuinely free and lose only |
| * stable-id-across-drop): |
| * - GC type: revival re-tracks and genuine free / reclaim use GC_Del. |
| * - our custom ``tp_free`` is installed: otherwise the generic free would |
| * reclaim the block while ``tagged_pyobj`` still points at it (UAF). |
| * - no instance ``__dict__`` (plain or managed): reusing a cached allocation whose dict |
| * region was cleared would need dict re-init we do not perform. Lean |
| * wrappers (the common, tested case) have ``tp_dictoffset == 0``. |
| * - no finalizer (``tp_finalize``, i.e. a Python ``__del__``): CPython sets |
| * a permanent GC-finalized bit the first time ``tp_finalize`` runs and |
| * never runs it again on that block. Reusing an inactive cached allocation would silently |
| * suppress ``__del__`` for every revived generation (the cached allocation's bit is |
| * already set and there is no public API to clear it). Excluding these |
| * types makes ``__del__`` fire correctly once per drop (genuine free each |
| * time); the only cost is no stable-id-across-drop. |
| * |
| * \param wrapper A live wrapper whose type is examined. |
| * \return True iff its type may be cached and revived (all requirements above hold). |
| */ |
| TVM_FFI_INLINE bool TVMFFIPyIsInactiveEligible(PyObject* wrapper) { |
| PyTypeObject* tp = Py_TYPE(wrapper); |
| if (!PyType_IS_GC(tp)) return false; |
| if (tp->tp_free != &TVMFFIPyTpFree) return false; |
| if (tp->tp_dictoffset != 0) return false; |
| if ((tp->tp_flags & Py_TPFLAGS_MANAGED_DICT) != 0) return false; |
| if (tp->tp_finalize != nullptr) return false; |
| return true; |
| } |
| |
| /*! |
| * \brief Custom ``tp_alloc``. On the revival path (an inactive cached allocation was handed |
| * to this thread via ``TVMFFIPySetReviveBlock``) it revives the cached allocation |
| * in place — same address, so ``id()`` is stable — re-initializing it |
| * to match ``PyType_GenericAlloc``'s contract. Otherwise (miss) it |
| * forwards to ``PyType_GenericAlloc`` for a fresh, tracked object. |
| * |
| * Revive-path contract (must match what ``tp_new`` expects from |
| * ``PyType_GenericAlloc``): |
| * 1. zero the body ``[sizeof(PyObject), tp_basicsize)`` so ``__cinit__`` |
| * sees clean fields; |
| * 2. ``PyObject_Init`` -> ob_refcnt = 1, ob_type, INCREF(type); |
| * 3. ``PyObject_GC_Track`` -> GenericAlloc returns a *tracked* object and |
| * ``tp_new`` does not track again, so the revive path must track. |
| * (No stale GC-finalized bit to clear: the design uses no tp_finalize.) |
| * |
| * Fixed-size only: ``PyObject_Init`` resets ``ob_refcnt``/``ob_type`` but not |
| * ``ob_size``. Every registered FFI wrapper is a fixed-size cdef class |
| * (``tp_itemsize == 0``), asserted below; a future variable-sized type would |
| * need ``PyObject_InitVar(.., nitems)`` here and a matching basicsize check. |
| * |
| * \param type The wrapper type being allocated. |
| * \param nitems Item count for variable-sized types (0 for our fixed-size wrappers); |
| * forwarded to ``PyType_GenericAlloc`` on the miss path. |
| * \return The revived cached allocation (same address) on a hit, else a fresh tracked object. |
| */ |
| inline PyObject* TVMFFIPyTpAlloc(PyTypeObject* type, Py_ssize_t nitems) { |
| // Take the revive block and leave the slot NULL in one step (per-thread). |
| PyObject* blk = TVMFFIPyTLSReviveSlot(nullptr); |
| if (blk != nullptr) { |
| // REVIVAL: revive the inactive cached allocation at the same address. The body memset |
| // below assumes ``type->tp_basicsize`` equals the cached allocation's original |
| // basicsize. This holds because a chandle's ``type_index`` maps to one |
| // stable wrapper class for the life of the process, and ``make_ret_object`` |
| // derives both the cached allocation (from the chandle) and ``type`` (= cls for that |
| // same type_index) from the very same chandle on the revival path. |
| assert(type->tp_itemsize == 0 && |
| "cache-&-revive supports only fixed-size wrappers; a variable-sized " |
| "type needs PyObject_InitVar and a per-instance basicsize check"); |
| std::memset(reinterpret_cast<char*>(blk) + sizeof(PyObject), 0, |
| static_cast<size_t>(type->tp_basicsize) - sizeof(PyObject)); |
| PyObject_Init(blk, type); |
| PyObject_GC_Track(blk); |
| return blk; |
| } |
| return PyType_GenericAlloc(type, nitems); // MISS: fresh (already tracked) |
| } |
| |
| //--------------------------------------------------------------- |
| // SECTION B -- dealloc (HOT: per wrapper death). |
| //--------------------------------------------------------------- |
| |
| /*! |
| * \brief Custom ``tp_free``, the second step of the dealloc handshake. The InTransit bit |
| * (read here) says whether the chandle's deleter fired during the |
| * ``TVMFFIPyTpDealloc`` DecRef: still set => the chandle outlived us, settle to |
| * Inactive and keep ``self`` cached; cleared => free the C++ block too. The |
| * ``chandle == NULL`` / non-canonical path is a plain genuine free, dispatching on |
| * GC-ness like CPython's default. |
| * |
| * \param self The wrapper being freed (CPython's ``tp_free`` argument). |
| */ |
| inline void TVMFFIPyTpFree(void* self) { |
| void** chandle_ptr = TVMFFICyObjectGetCHandlePtr(static_cast<PyObject*>(self)); |
| void* chandle = *chandle_ptr; |
| if (chandle != nullptr && TVMFFIPyIsCanonical(chandle)) { |
| PyCustomAllocHeader* h = TVMFFIPyHeader(chandle); // header read BEFORE any free below |
| PyObject* cur = TVMFFIPyLockWord(h); |
| if (TVMFFIPyTagInTransit(cur)) { |
| // Case 0 (Flow 1): chandle outlived us -- settle to stable Inactive, keep ``self`` cached. |
| // continuation: delete_space reclaims this block when the chandle later dies. |
| // ``*chandle_ptr = nullptr`` MUST precede the publish (still under the lock): else a |
| // make_ret revive could grab the Inactive word and re-set the chandle, only for this |
| // stale NULL to clobber it (Active wrapper with chandle == NULL -> crash). |
| *chandle_ptr = nullptr; |
| TVMFFIPyUnlockWord(h, TVMFFIPyTagClearInTransit(cur)); |
| return; |
| } |
| // Case 1 (Flow 2): deleter already fired and deferred the block free to us. |
| // continuation: none -- we are the last settler and free the block here. |
| *chandle_ptr = nullptr; |
| TVMFFIPyUnlockKeep(h, cur); |
| ::tvm::ffi::details::AlignedFree(static_cast<char*>(chandle) - sizeof(PyCustomAllocHeader)); |
| } |
| PyObject* op = static_cast<PyObject*>(self); |
| if (PyObject_IS_GC(op)) { |
| PyObject_GC_Del(op); |
| } else { |
| PyObject_Free(op); |
| } |
| } |
| |
| /*! |
| * \brief delete_space callback (installed by TVMFFIPyAllocate), invoked from the C++ Weak |
| * deleter when the chandle's block's weak count hits 0. Detached => free the block |
| * (lock-free fast path, the common C++-only-object case). Inactive => read InTransit: |
| * set => an in-flight ``tp_free`` will free the block, so defer; clear => reclaim the |
| * cached wrapper and free the block here. |
| * |
| * At weak->0 there are no live refs, so the binding is only ever Detached or |
| * Inactive(|InTransit) -- never Active/Locked. |
| * |
| * \param ptr The object (``T``) pointer whose C++ block reached weak-count 0. |
| */ |
| inline void TVMFFIPyDeleteSpace(void* ptr) { |
| void* base_alloc = static_cast<char*>(ptr) - sizeof(PyCustomAllocHeader); |
| auto* h = static_cast<PyCustomAllocHeader*>(base_alloc); |
| PyObject* cur0 = TVMFFIPyPeekWord(h); |
| if (!TVMFFIPyTagIsInactive(cur0)) { // Detached: lock-free free |
| ::tvm::ffi::details::AlignedFree(base_alloc); |
| return; |
| } |
| if (TVMFFIPyIsPythonAlive()) { |
| PyGILState_STATE gstate = PyGILState_Ensure(); |
| if (TVMFFIPyIsPythonAlive()) { |
| PyObject* cur = TVMFFIPyLockWord(h); |
| if (TVMFFIPyTagInTransit(cur)) { |
| // In-flight: defer both frees to tp_free; keep the block. |
| TVMFFIPyUnlockWord(h, TVMFFIPyTagClearInTransit(cur)); |
| PyGILState_Release(gstate); |
| return; |
| } |
| // Settled: detach, reclaim the cached wrapper (outside the word lock), then |
| // free the block below. |
| PyObject* wrapper = TVMFFIPyRemoveTag(cur); |
| TVMFFIPyUnlockWord(h, nullptr); |
| PyObject_GC_Del(wrapper); |
| PyGILState_Release(gstate); |
| } else { |
| PyGILState_Release(gstate); |
| } |
| } else if (TVMFFIPyTagInTransit(cur0)) { |
| // Teardown, same-thread in-flight: defer the block free. No lock is held here (Python is |
| // finalizing single-threaded, no thread state to lock under) -- this is the one word |
| // store in the file with no matching TVMFFIPyLockWord; the release-store is still correct. |
| TVMFFIPyUnlockWord(h, TVMFFIPyTagClearInTransit(cur0)); |
| return; |
| } |
| ::tvm::ffi::details::AlignedFree(base_alloc); |
| } |
| |
| /*! |
| * \brief ``__dealloc__`` hook (Cython's ``CObject.__dealloc__`` calls it). Build-agnostic: the same |
| * binding transition runs on both the GIL and free-threaded builds. |
| * |
| * Releases the wrapper's +1 on the chandle and opens the cache-vs-free handshake for an eligible |
| * canonical wrapper. Four cases, keyed on the wrapper's relationship to the chandle's binding word: |
| * - chandle already NULL: an eager move (``__move_handle_from__`` / rvalue-ref setter) already |
| * detached and released this wrapper's ref -- nothing owed, return. |
| * - eligible canonical Active binding (``cur == wrapper``): Active -> Inactive | InTransit, keep |
| * the cached allocation for in-place revival, then DecRef (``tp_free`` settles the handshake). |
| * - ineligible canonical Active binding (``cur == wrapper``): Active -> Detached before the |
| * DecRef, then fall through to the genuine-free tail. |
| * - not our binding (``cur != wrapper``) or non-canonical chandle: some other wrapper is (or no |
| * wrapper is) canonical for this chandle -- leave the word untouched and fall through to the |
| * genuine-free tail. |
| * The DecRef MUST run outside the lock: it can fire the chandle deleter (``TVMFFIPyDeleteSpace``), |
| * which re-locks the same non-reentrant word. |
| * |
| * \param ptr_to_chandle Address of the wrapper's ``chandle`` field (nulled here before the DecRef). |
| * \param wrapper The dying wrapper (used to check it is still the canonical binding). |
| */ |
| TVM_FFI_INLINE void TVMFFIPyTpDealloc(void** ptr_to_chandle, PyObject* wrapper) { |
| void* chandle = *ptr_to_chandle; |
| // Case: chandle already NULL. Released by an eager move (detached to NULL); nothing to do. NULL |
| // is the only released state here -- the transit marker lives in ``tagged_pyobj``, not here. |
| if (chandle == nullptr) return; |
| if (TVMFFIPyIsCanonical(chandle)) { |
| PyCustomAllocHeader* h = TVMFFIPyHeader(chandle); |
| PyObject* cur = TVMFFIPyLockWord(h); |
| if (cur == wrapper) { |
| // We ARE the canonical wrapper for this chandle. |
| if (TVMFFIPyIsInactiveEligible(wrapper)) { |
| // Case: eligible canonical binding. Active -> Inactive | InTransit, then DecRef outside the |
| // lock. continuation: the DecRef either leaves the chandle alive (-> tp_free keeps it |
| // cached, delete_space frees later: Flow 1) or drops its last ref (-> delete_space fires |
| // reentrantly and defers, tp_free frees: Flow 2). See the handshake flows up top. |
| TVMFFIPyUnlockWord( |
| h, reinterpret_cast<PyObject*>(reinterpret_cast<uintptr_t>(wrapper) | |
| kPyCachedInactiveTagBit | kPyInTransitTagBit)); |
| TVMFFIObjectDecRef(chandle); |
| return; |
| } |
| // Case: ineligible canonical binding. Active -> Detached. Publish NULL before the DecRef so a |
| // deleter firing inside it sees no stale Active binding; then fall to the genuine-free tail. |
| TVMFFIPyUnlockWord(h, nullptr); |
| } else { |
| // Case: not our binding (``cur != wrapper``). A concurrent make_ret / move made a DIFFERENT |
| // wrapper canonical for this chandle (or it is Detached/Inactive). Leave the word as-is -- |
| // this wrapper only owns its +1 -- and fall to the genuine-free tail. (GIL: no store.) |
| TVMFFIPyUnlockKeep(h, cur); |
| } |
| } |
| // Tail (ineligible / not-ours / non-canonical): no handshake -- genuine free. Null |
| // ``wrapper.chandle`` BEFORE the DecRef so the deleter chain observes a consistent |
| // (chandle == NULL, no +1 owed) state. tp_free then genuine-frees the wrapper storage. |
| *ptr_to_chandle = nullptr; |
| TVMFFIObjectDecRef(chandle); |
| } |
| |
| //--------------------------------------------------------------- |
| // SECTION C -- installation (COLD: once per registered type / once per process). |
| //--------------------------------------------------------------- |
| |
| /*! \brief Install the custom ``tp_alloc`` / ``tp_free`` (cache-&-revive) slots on ``type_obj``. |
| * Called once per registered FFI type from ``_update_registry`` (object.pxi). |
| * \param type_obj The registered wrapper type (must be a type object; ignored otherwise). */ |
| TVM_FFI_INLINE void TVMFFIPyInstallTypeSlots(PyObject* type_obj) { |
| if (type_obj == nullptr || !PyType_Check(type_obj)) return; |
| PyTypeObject* tp = reinterpret_cast<PyTypeObject*>(type_obj); |
| tp->tp_alloc = &TVMFFIPyTpAlloc; |
| tp->tp_free = &TVMFFIPyTpFree; |
| } |
| |
| /*! |
| * \brief Install ``TVMFFIPyAllocate`` as the process-wide custom allocator. |
| * Storage for the registered entry is a function-static so the |
| * address is process-stable. |
| * \return The status code from ``TVMFFISetCustomAllocator`` (0 on success). |
| */ |
| TVM_FFI_INLINE int TVMFFIPyRegisterDefaultAllocator() { |
| // Installed on both the GIL and free-threaded builds; on free-threaded builds the |
| // header state machine is lock-synchronized (see "Free-threaded builds" above). |
| static TVMFFICustomAllocator allocator{&TVMFFIPyAllocate, /*context=*/nullptr}; |
| return TVMFFISetCustomAllocator(&allocator); |
| } |
| |
| #endif // TVM_FFI_PYTHON_OBJECT_H_ |