Exposing public APIs for immortal objects

I’ve been playing with own-GIL subinterpreters recently, and effectively the only way to safely transfer objects between threads is to immortalize them, which is a little unfortunate considering the only way to do that is via the private API. Similarly, immortalization is also a convenient way to avoid reference count contention on the free-threaded build (user-defined objects are still pretty susceptible to that problem, see #123619 for example). So, it would be nice to expose some public APIs for making an object immortal.

Now, exposing implementation details is bad, but considering a recent discussion regarding omitting reference counting for known immortals (e.g. Py_None), I’m almost certain that people are already doing this in public extensions – so, at this point, removing immortal objects in the future would probably be a breaking change. With that in mind, it would be nice if third-parties could take advantage of immortality (or really, object thread safety) without the private API.

Is something like the following feasible? (I’ve developed a proof of concept for this!)

static PyObject *
my_function(PyObject *self, PyObject *obj)
{
    if (Py_Immortalize(obj) < 0)
    {
        return NULL;
    }

    Py_RETURN_NONE;
}

I won’t get too technical (you can see my POC for details), but basically, obj is immortalized via _Py_SetImmortal, and then stored in a list on the interpreter state to be deallocated during finalization.

1 Like

I tried your PoC in a debug build of CPython:

>>> import _testcapi
>>> _testcapi.immortalize_object('a string')
python: Objects/object.c:2451: _Py_SetImmortalUntracked: Assertion `PyUnicode_CHECK_INTERNED(op) == SSTATE_INTERNED_IMMORTAL || PyUnicode_CHECK_INTERNED(op) == SSTATE_INTERNED_IMMORTAL_STATIC' failed.
Aborted (core dumped)

It’s currently not safe to immortalize arbitrary objects. Even if we exclude the types that can’t currently be immortalized, by adding a public Py_Immortalize, people would expect that the ones that currently work will keep working in the future. I don’t think we can promise this now.

See PEP 734 for Eric’s plan about sharing data between subinterpreters. (That PEP is deferred, but that’s just so the public API can be proven in a third-party module first.)

3 Likes

I wasn’t aware of this :frowning:

Could changing it to PyUnstable_Immortalize (and disallowing certain types, as you mentioned) be a solution? That gives a little more wiggle room when it comes to changing things.

I’ve updated the POC to no-op for existing immortals to prevent the double free during finalization, so the interned string problem should be fixed. Does that fix the extent of “unsafe-ness” that exists?

Does that fix the extent of “unsafe-ness” that exists?

No. Try the reproducer again.

But it’s not really about unsafeness that currently exists. Allowing immortalization of arbitrary objects would prevent possible future changes we might want to make. IMO, the feature would need a PEP to get everyone to agree we won’t need to change the internals, and can go looking for all the edge cases so we can stabilize the feature. I don’t think that PEP would make it to 3.14.

You might instead want to help with PEP 734, with its smaller list of shareable types.

Oh, you ran it in the REPL. That’s very difficult to test, no fair

I’d be happy to, but that does leave out the problem of reference count contention in free-threading – I came up with this idea to sort of light the way for a future Python API (think sys.immortalize), because it would be much more difficult to try and add something like that for DRC.

I guess if we do need a larger proposal it could follow in the footsteps of what we’re doing for PyUnstable_Object_SetDeferredRefcount, and let PyUnstable_Immortalize hint that the object should be immortal. Though, I don’t really feel like writing a separate proposal if Eric has a solid plan on how to implement this for subinterpreters. I guess we can revisit this if some more use-cases come up.

1 Like