-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Forward-compatibility for special case APIs #4
Comments
Additional point: you can #ifdef PY_SLOT_GETITEM_INT
if (PyObject_GetSlots(dict, PY_SLOT_GETITEM_INT, &dict_getitem))) {
// Obtained the fast path
value = (*((struct PySlots_GetItemInt *)dict_getitem.slots)->getitem_ssize_t)(&dict_getitem, k);
PySlots_Release(&dict_getitem);
} else
#endif
#ifdef PY_SLOT_GETITEM // wouldn't do this because it would always exist... but for example
if (PyObject_GetSlots(dict, PY_SLOT_GETITEM, &dict_getitem)) {
// Fast path not available on this version, use the slow path
PyObject *key = PyLong_FromSSizeT(k);
value = (*((struct PySlots_GetItem *)dict_getitem.slots)->getitem)(&dict_getitem, key);
PySlots_Release(&dict_getitem);
Py_DECREF(key);
} else
#endif
{
// should be unreachable unless we deprecate/remove PY_SLOT_GETITEM
} |
When you know #ifndef PY_SLOT_GETITEM_INT
#define PY_SLOT_GETITEM_INT 42
#endif or find a compat header library that does that... Would custom tpyes be expected to provide these? What would the API look like? |
You'll also need the struct layout and all the prototypes, so you'd definitely get it from our headers. The only reason you'd test for the value is to see if you have all of those defined - it's for source compatibility, not for binary compatibility.
Could do. I guess it would be a PyTypeObject member with the same signature as
If they want to actually get an item, yeah. If they'd rather refuse because it'll be "too slow," that's up to them. But I expect a helpful inline function that does the fallback would be popular. Then you compile with the latest available Python headers and get whichever is the "best"/fastest/safest behaviour available on whatever version you're running on. |
What are the advantages over only providing that "helper" -- a "caller" API like: PyObject *Py_GetItem_ssize(PyObject *obj, ssize_t *key); where Python itself would try the proper fallbacks? Refusing a fallback because it's too slow doesn't seem very compelling... |
Remember the context is compatibility over multiple releases, so I'm assuming that the specialised function doesn't exist in version N, but is added in N+1 (or later), and we want to support extension modules that can be loaded in any version from N onwards. Code written for version N is going to use a regular GetItem that takes a PyObject key, because that's the only one available in version N. Code written for N+1 could use a newly exported GetItem_ssize that has its own fallback (if If The rest is really just a more efficient way of doing polymorphism upfront. For example, if you We could even get crazy in certain circumstances. For example, we could have a slots type for "list of up to 8 integers" that just contains (Or for a more normal example, every protocol could have a slots struct - Footnotes
|
Ah, I see. It solves a problem with the limited API that you can't use newer features/optimizations if you want to support an old Python version. Given that you can compile wheels for the new & old versions separately, the complexity might not be worth it. |
Uhm... are you suggesting that people should just compile wheels for each version? What's the point of having a stable ABI/API between versions then? |
Yeah, you're right. It expands the scope of the stable ABI, but it'd be a welcome enhancement. capi-workgroup/api-evolution#1 is another possible approach to solving it. |
The two approaches come at it from different directions. This one gives us a path to design an ABI level that remains compatible over time, and then we can build helpers on top of it to make life easier for end users (similar in principle to #1 Native Interface proposal, but this would work as a component of that rather than an alternative). The other approach assumes that we'll continue to add, change and remove ABI members over time in incompatible ways, but will include shims in user's code as needed to handle the differences. This doesn't handle forward-compatibility, because we still can't predict future changes, but it does allow us to handle backwards compatibility (though, IMHO, no better than this proposal). It also doesn't allow us to safely provide API-level optimisations or an interface that can be efficient for other Python implementations. But as notable from being in separate repositories, they aren't mutually exclusive. We can invest in a shim library (probably HPy is the right place for it?) while also developing an API structure that can eventually be the new stable ABI. If anyone does think they are totally exclusive, I'd like the opportunity to sell this one better, because I don't believe they are. |
Oh, we also don't necessarily have to define every slots struct as belonging to the stable ABI if the API to get them does. It would probably be appreciated by users if we at least have baseline functionality guaranteed to not go away, but provided that we can always point at what the fallback interface ought to be, we can always remove1 more specific slots in a later version. Code will still work, which is the promise, even if it loses a bit of performance.2 Footnotes |
The more I think about this the more questoins I have :) We probably want a linear sequence of fallbacks. What happens if a user uses the wrong order? A subtle behaviour difference? Is that a bug in the exporting object (do we require that the fallbacks have equivalent behaviour)? I can see fallbacks being optional: If there's a slot to "get an int value from a dict", but it overflows, you might, but might not, want to fall back to "get an object from dict". |
The "proper order" will depend on the particular slots, so it'd have to be part of the definition. For example, If the user uses the wrong order, then one of the earlier checks will succeed and none of the later ones will occur. So if they check for
The identifier is the name, so I haven't specified any MRO logic at all here, btw, since we don't currently have that in native types. Most likely, your
Yeah, exactly. Or even just getting an int value from a long-like object. Any native type could implement a And "this should've worked, but can't" is often enough of an error condition to make it an error immediately rather than going through and trying more native APIs. |
It seems that one way to look at this is as an extension of the current sub-slot structs, but with an extra layer of indirection (which the object controls). It would be helpful to have a concrete example for how the “provider” side would be implemented -- what a type author should do to make |
In the simplest case, the implementer would do something like this: // We've defined the interface somewhere in our public headers
struct PySlot_GetSetDelItem {
struct PySlots_Base base;
PyObject * (*getitem)(PySlots *self, PyObject *key);
PyObject * (*setitem)(PySlots *self, PyObject *key, PyObject *value);
PyObject * (*delitem)(PySlots *self, PyObject *key);
};
// The type implementer defines a (potentially static) function table in their code
struct PySlots_GetItem _dict_getitem_slots = {
_PyDictSlots_Release,
&_PyDictSlots_GetItem,
&_PyDictSlots_SetItem,
&_PyDictSlots_DelItem
};
// Assuming we've defined PySlots as:
struct PySlots {
void *handle;
PySlotName name;
struct PySlots_Base *slots;
};
// Their "...Slots_Get" function returns the right slots for the requested name.
// This function is found in the PyTypeObject of 'o'
static int
_PyDictSlots_Get(PyObject *o, PySlotName name, PySlots *slots)
{
switch (name) {
case PY_SLOT_GETSETDELITEM:
slots->handle = Py_NewRef(o);
slots->name = name; // I just added this, realised we need it for Release
slots->slots = (struct PySlots_Base*)&_dict_getitem_slots;
return 0;
}
return _PySlots_GenericGet(o, name, slots);
}
static int
_PyDictSlots_Release(PySlots *slots)
{
switch (slots->name) {
case Py_SLOT_GETSETDELITEM:
Py_DECREF((PyObject *)slots->handle);
return 0;
}
return _PySlots_GenericRelease(slots);
}
// For this example we just redirect to the existing function, but this could
// contain the actual implementation. It doesn't necessarily have to just be
// a wrapper around the more obvious function.
static int
_PyDictSlots_GetItem(PySlots *self, PyObject *key)
{
return PyDict_GetItem((PyObject *)self->handle, key);
} I also wrote up a rough example of an efficient "AsLong" slots, but posted it in a Gist rather than cluttering up this thread more. It's a very hypothetical idea, but I think it shows the potential efficiency gains more than GetItem does. |
Thanks! Two questions to hash out:
Currently, Python knows about all slots so it can update them appropriately. |
Then you'll also override
I imagine for a mutable class, the slots would be generic (i.e. they'd do the This API doesn't really do a huge amount out of the box for Python user-defined classes. But in any case, those are based on our C type, and so we still know all the slots. Just that our
You mean performance-wise? Obviously there'll be a minor perf regression compared to assuming the type and calling the function directly, but by using the indirection it means core native functions can efficiently use 3rd party types, whereas today that only works when they haven't overridden our own implementations. I'm sure Cython will choose to make assumptions about internal layouts that will eventually come back to bite them (after all, that's exactly what they do today). e.g. they'll probably assume that the getitem slots for one dict will work for any dict, which would probably be true up until the point where it isn't. What they'd gain is the option to have unconditional code for using faster interfaces that still works (slower) on older versions. So if we add a |
Let me give concrete hypothetical scenarios. These involve inheritance, and exploit (perhaps too much) the mechanism that decouples the slots from the interpreter. Scenario 1
Here, Scenario 2
Here, |
Scenario 2 is easy, because your Scenario 1 is hard in general. If a subclass doesn't override a protocol, the superclass provides it, regardless of whether we're talking about this proposal or existing Python code. Just thinking out loud here, but maybe we could mark some slots as uninheritable (or alternatively, "must be implemented by the most derived class")? If they're ints, maybe the topmost bit is set. So if |
One of the issues we've seen in the existing API is that we grow more specific functions over time, for performance and other reasons.
For example,
PyObject_GetItem
is theoretically sufficient for all get-item scenarios, and yet we offer a variety of public APIs that streamline certain operations.12So I want to propose an API that lets us extend these into the future, allowing even more special/optimised cases without causing undue burden on maintainers. Names are obviously all going to be bikeshedded, and I suspect this will fit nicely into one of the other proposals going around, but this is the aspect I personally care most about so happy to see it merged into another one. But until then, I'm going to call it
PyObject_GetSlots
.The basic idea is that each object may return a struct containing function pointers that are specific to its current instance, based on the argument given by the caller. Thinking about how you might perform
__getitem__
, here's a more fully spelled out example (again, using current naming/objects, but could be adapted to fit a new API design):Key points:
PySlot_GetItemStr
if it has non-str keysThe biggest downside is that it can get to be really messy C code when used directly (I'm not 100% sure I typed it right in my example), but headers/macros and/or a static library could really help. As long as it's statically compiled into the caller, and not part of the Python runtime itself, or it will break on earlier versions.
Footnotes
Particularly for dict objects, but those are largely due to back-compat with design flaws we made in the past. ↩
I recognise many of these are implementations of type-specific handlers for
GetItem
, but even if we wanted to make them internal now, we can't and wouldn't. ↩The text was updated successfully, but these errors were encountered: