-
-
Notifications
You must be signed in to change notification settings - Fork 52
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
Support metaclasses in HPy. #335
Conversation
From my POV this looks good modulo the failing CI jobs and is an important step forward for NumPy porting effort. It would be great if someone else could take a look too (@antocuni, @hodgestar, @rlamy, ... :-)) |
Yes, I'm sorry. MSVC is a bit more pedantic than |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wow, this was a tremendous amount of work, good job @fangerer and @steve-s 👍.
The PR looks good in general, but before merging I would like to explore whether there are other ways to avoid paying the penalty in HPy_AsStruct
.
HPy_AsStruct
will be called a lot, and we should try at all costs to avoid making it unnecessarily slow, especially considering that it's a penalty that we are going to pay also in CPython mode, which in theory should be 0 costs.
I also have the feeling that we are going in circles, because when we proposed #83, we did it specifically to avoid paying the cost of this kind of "dynamic cast".
I think there is a possible solution which came to my mind while reading this PR, so let me explain it here although it might be wrong.
If I understand correctly, the problem comes when subclassing a builtin type which is not PyObject
. But the key realization is that there is only a finite number of those: PyHeapTypeObject
, PyLongObject
, PyDictObject
and probably a handful more. The relevant issue is #169.
Currently, we distinguish between "pure types" and "legacy types". I propose to treat specially all built-in type that can be subclassed (or at least, the ones for which we care about performance). So, we will have:
HPy_AsStruct(); // works only for pure types
HPy_AsStruct_Generic(); // this is the "slow" version, which dynamically looks up the payload_offset. Maybe we should call it HPy_AsStruct_Dynamic? Or _Slow?
HPy_AsStruct_Legacy_PyObject();
HPy_AsStruct_Legacy_PyHeapTypeObject();
...
HPyType_DEFINE_HELPERS
will be able to use the most appropriate version, and probably we can teach the debug mode to detect the case in which we are using a specialized version which does not match what's written in payload_offset
.
Note that we still need the "generic" slow version for cases in which we don't know upfront the type, but this is already the case in the current master: we have _pyobj_as_struct
which does exactly that and it's called from a couple of internal places:
hpy/hpy/devel/src/runtime/ctx_type.c
Lines 73 to 81 in d262f43
static void *_pyobj_as_struct(PyObject *obj) | |
{ | |
if (_is_pure_HPyType(Py_TYPE(obj))) { | |
return _HPy_PyObject_Payload(obj); | |
} | |
else { | |
return obj; | |
} | |
} |
Also, if we decide to go down that route, we can probably remove the is_pure
flag:
typedef struct {
uint16_t magic;
HPyFunc_traverseproc tp_traverse_impl;
HPyFunc_destroyfunc tp_destroy_impl;
bool is_pure;
HPy_ssize_t payload_offset;
char name[];
} HPyType_Extra_t;
I think that payload_offset
should be enough: a type is pure IIF payload_offset == 0
.
hpy/devel/src/runtime/ctx_type.c
Outdated
ALIGN_SIZE(16, 11) == 16. Picking the right alignment is crucial! */ | ||
#define ALIGN_SIZE(_align, _size) \ | ||
((HPy_ssize_t)((((size_t)(_size)) - 1u + (_align)) & (~(_align) + 1))) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as you have already noticed, we already have a mechanism to align an arbitrary payload:
https://github.com/fangerer/hpy.git/blob/f337049cb17d2538b0439c5ac45beca0f0470f08/hpy/devel/include/hpy/runtime/ctx_type.h#L25-40
I'm happy to use this new logic, but then we should probably kill _HPy_FullyAlignedSpaceForPyObject_HEAD
and use the new logic also there?
hpy/devel/src/runtime/ctx_type.c
Outdated
@@ -51,10 +81,10 @@ static inline HPyType_Extra_t *_HPyType_EXTRA(PyTypeObject *tp) { | |||
} | |||
|
|||
static inline bool _is_pure_HPyType(PyTypeObject *tp) { | |||
return _is_HPyType(tp) && _HPyType_EXTRA(tp)->is_pure; | |||
return _is_HPyType(tp) && _HPyType_EXTRA_IS_PURE(_HPyType_EXTRA(tp)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this might be nitpick, but was wrong with _HPyType_EXTRA(tp)->is_pure
? It looked more natural and easier to read to me, and it doesn't require to introduce a new long macro for every field that we will add to HPyType_Extra_t
.
Am I missing something obvious for why the new macro was needed?
hpy/devel/src/runtime/ctx_type.c
Outdated
return _HPy_PyObject_OFFSET(obj, | ||
_HPyType_EXTRA_PAYLOAD_OFFSET(_HPyType_EXTRA(type))); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand why you ended up with this solution of using a payload, but I would really like to avoid this at all costs.
I will write a more extensive explanation in the main thread, because it's better to discuss there than in a small side comment.
the heap type to the manually allocated one. Then we clear some key slots | ||
and call 'PyType_Ready' on it to re-initialize everything. The temporary | ||
heap type is then expired. */ | ||
static PyObject* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to think more to really understand what's happening here, but a temporary hackish solution for "old" CPython versions sounds good to me.
I wonder whether we should move it to its own file so that we can easily exclude it from compilation with newer Pythons.
I know that in theory we could guard it with #ifdef
also in this case, but I think it's too long and would be confusing.
I though the problem here was that we have also custom subclasses. However, now I realized we do not have this problem here. At the same time, we still need to know the right offset, and even for pure types (or only for pure types actually). Let's have some example for discussion:
To recap what is going on there: With HPy pure types the The suggested solution looks good if we really fear that fishing the offset from the type would be unacceptably slow. However, I think we'll need to have
in fact, I think that the legacy ones are not necessary, because there the user should already embed the right struct inside their struct, so we do not have to calculate any offset, but such a difference between pure and legacy types would make porting harder imo. For the helpers: one would have to specify the base type as an argument of the macro, or we'd have different macros:
For "dynamic" Completely different thing is inheritance of custom user defined types (and their structs). I believe that inheritance of custom types must be actually treated differently from builtin types, because builtin types must stay opaque, while custom types should be accessible to be actually useful. There I'd agree with what we discussed on the last call, that the user should actually embed structs, example:
here we will need to distinguish that in this case Last situation that comes to my mind is inheriting from a type from another extension, which is however not exposed via some shared header file or such, so its struct cannot be embedded. In that case I would say that setting |
This is a relevant thread that proposes similar APIs for C API: https://mail.python.org/archives/list/[email protected]/thread/SIP3VP7JU4OBWP62KBOYGOYCVIOTXEFH/ |
FWIW, turning that proposal into s PEP is now almost on top of my CPython TODO list (after python/cpython#95992 and a bit of non-CPython work) |
Discussion at sprint: simplify this to not dynamically calculate the payload, but instead just solve the issue specifically for In a second PR, we will add support to inherit from other HPy types - the offset is still For inheriting from other builtin types, we need just an enum to specify the subset of builtin types you can inherit from and the specific macros. We need to grep the top packages to see which ones are needed first. |
I've proposed PEP 697. Could you please check it out, and comment on Discourse? |
Will do so. |
c9748cb
to
bc8f9b2
Compare
This adds a new member of HPyType_SpecParam_Kind and its handling in HPyType_FromSpec. CPython does not support metaclass in PyType_FromSpecWithBases and this PR relies on patched CPython version that provides PyType_FromSpecWithBasesAndMeta, which is very simple extension of PyType_FromSpecWithBases (diff ~10 lines), but adds a new API. Doing all the work on HPy side would require pulling PyType_FromSpecWithBases code to HPy including all the static helper methods. Not only this would be a large amount of code, but some of the helper methods use internal APIs.
bc8f9b2
to
07f25c0
Compare
This PR resolves #296 and is one of the big pieces that is required for migration of NumPy.
Goal of this PR: Support metaclasses for HPy types.
Before version 3.12, CPython does not support specifying metaclasses for heap types but only for static types.
We can still support that by creating a temporary heap type using
PyType_FromSpecWithBases
and then we manually allocate another heap type using the provided metaclass'tp_alloc
. We then copy everything from the temporary heap type to the final one, reset some carefully picked slots and initialize it withPyType_Ready
.The temporary heap type is then trashed.
However, making that work with pure HPy types wasn't that trivial because a metaclass usually embeds the heap type struct like this:
The problem is that if we specify a pure HPy type and use it as metaclass, we would still just have following struct:
Since from an HPy point of view, the runtime's heap type is opaque, you cannot embed that in your structure and HPy assumes that it's just a bare
PyObject
and so the used offset is justsizeof(PyObject)
.However, each metaclass needs to inherit from
PyType_Type
(directly or indirectly). I use that fact and iterate over the bases to determine the largest basicsize. That is then used as an offset.Furthermore, we now don't have a fixed offset for the pure HPy data pointer and so we need to store that in the HPy type's extra info.
This makes the
*_AsStruct
calls slightly more expensive because we need to load the offset from the object type's extra info.I think, this change is not only a fix for metaclasses but in general a fix to be able to inherit from any opaque type. For example, an HPy extension could define a type that inherits from a foreign HPy extension's type which also specifies a basicsize.
Furthermore, as far as I know, the situation is similar in the C API when using the limited API. The structs are opaque and if one wants to inherit from a type, he needs to add the base type's basicsize.