[issue15870] PyType_FromSpec should take metaclass as an argument

2021-07-28 Thread Josh Haberman


Josh Haberman  added the comment:

I know this is quite an old bug that was closed almost 10 years ago.  But I am 
wishing this had been accepted; it would have been quite useful for my case.

I'm working on a new iteration of the protobuf extension for Python.  At 
runtime we create types dynamically, one for each message defined in a .proto 
file, eg. from "message Foo" we dynamically construct a "class Foo".

I need to support class variables like Foo.BAR_FIELD_NUMBER, but I don't want 
to put all these class variables into tp_dict because there are a lot of them 
and they are rarely used.  So I want to implement __getattr__ for the class, 
which requires having a metaclass.  This is where the proposed 
PyType_FromSpecEx() would have come in very handy.

The existing protobuf extension gets around this by directly calling 
PyType_Type.tp_new() to create a type with a given metaclass:

https://github.com/protocolbuffers/protobuf/blob/53365065d9b8549a5c7b7ef1e7e0fd22926dbd07/python/google/protobuf/pyext/message.cc#L278-L279

It's unclear to me if PyType_Type.tp_new() is intended to be a supported/public 
API.  But in any case, it's not available in the limited API, and I am trying 
to restrict myself to the limited API.  (I also can't use 
PyType_GetSlot(PyType_Type, Py_tp_new) because PyType_Type is not a heap type.)

Put more succinctly, I do not see any way to use a metaclass from the limited C 
API.

Possible solutions I see:

1. Add PyType_FromSpecEx() (or similar with a better name) to allow a metaclass 
to be specified.  But I want to support back to at least Python 3.6, so even if 
this were merged today it wouldn't be viable for a while.

2. Use eval from C to create the class with a metaclass, eg.
  class Foo(metaclass=MessageMeta)

3. Manually set FooType->ob_type = &MetaType, as recommended here: 
https://stackoverflow.com/a/52957978/77070 .  Since PyObject.ob_type is part of 
the limited API, I think this might be possible!

--
nosy: +jhaberman

___
Python tracker 
<https://bugs.python.org/issue15870>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue15870] PyType_FromSpec should take metaclass as an argument

2021-08-02 Thread Josh Haberman


Josh Haberman  added the comment:

> You can also call (PyObject_Call*) the metaclass with (name, bases, 
> namespace);

But won't that just call my metaclass's tp_new?  I'm trying to do this from my 
metaclass's tp_new, so I can customize the class creation process. Then Python 
code can use my metaclass to construct classes normally.

> I wouldn't recommend [setting ob_type] after PyType_Ready is called.

Why not?  What bad things will happen?  It seems to be working so far.

Setting ob_type directly actually solves another problem that I had been having 
with the limited API.  I want to implement tp_getattro on the metaclass, but I 
want to first delegate to PyType.tp_getattro to return any entry that may be 
present in the type's tp_dict.  With the full API I could call 
self->ob_type->tp_base->tp_getattro() do to the equivalent of super(), but with 
the limited API I can't access type->tp_getattro (and PyType_GetSlot() can't be 
used on non-heap types).

I find that this does what I want:

  PyTypeObject *saved_type = self->ob_type;
  self->ob_type = &PyType_Type;
  PyObject *ret = PyObject_GetAttr(self, name);
  self->ob_type = saved_type;

Previously I had tried:

   PyObject *super = PyObject_CallFunction((PyObject *)&PySuper_Type, "OO",
   self->ob_type, self);
   PyObject *ret = PyObject_GetAttr(super, name);
   Py_DECREF(super);

But for some reason this didn't work.

--

___
Python tracker 
<https://bugs.python.org/issue15870>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue15870] PyType_FromSpec should take metaclass as an argument

2021-09-14 Thread Josh Haberman


Josh Haberman  added the comment:

I found a way to use metaclasses with the limited API.

I found that I can access PyType_Type.tp_new by creating a heap type derived 
from PyType_Type:

  static PyType_Slot dummy_slots[] = {
{0, NULL}
  };

  static PyType_Spec dummy_spec = {
  "module.DummyClass", 0, 0, Py_TPFLAGS_DEFAULT, dummy_slots,
  };

  PyObject *bases = Py_BuildValue("(O)", &PyType_Type);
  PyObject *type = PyType_FromSpecWithBases(&dummy_spec, bases);
  Py_DECREF(bases);

  type_new = PyType_GetSlot((PyTypeObject*)type, Py_tp_new);
  Py_DECREF(type);

  #ifndef Py_LIMITED_API
assert(type_new == PyType_Type.tp_new);
  #endif

  // Creates a type using a metaclass.
  PyObject *uses_metaclass = type_new(metaclass, args, NULL);

PyType_GetSlot() can't be used on PyType_Type directly, since it is not a heap 
type.  But a heap type derived from PyType_Type will inherit tp_new, and we can 
call PyType_GetSlot() on that.

Once we have PyType_Type.tp_new, we can use it to create a new type using a 
metaclass. This avoids any of the class-switching tricks I was trying before.  
We can also get other slots of PyType_Type like tp_getattro to do the 
equivalent of super().

The PyType_FromSpecEx() function proposed in this bug would still be a nicer 
solution to my problem.  Calling type_new() doesn't let you specify object size 
or slots.  To work around this, I derive from a type I created with 
PyType_FromSpec(), relying on the fact that the size and slots will be 
inherited.  This works, but it introduces an extra class into the hierarchy 
that ideally could be avoided.

But I do have a workaround that appears to work, and avoids the problems 
associated with setting ob_type directly (like PyPy incompatibility).

--
nosy: +haberman2

___
Python tracker 
<https://bugs.python.org/issue15870>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue15870] PyType_FromSpec should take metaclass as an argument

2021-09-23 Thread Josh Haberman


Josh Haberman  added the comment:

> Passing the metaclass as a slot seems like the right idea for this API, 
> though I recall there being some concern about the API (IIRC, mixing function 
> pointers and data pointers doesn't work on some platforms?)

PyType_Slot is defined as a void* (not a function pointer): 
https://github.com/python/cpython/blob/8492b729ae97737d22544f2102559b2b8dd03a03/Include/object.h#L223-L226

So putting a PyTypeObject* into a slot would appear to be more kosher than 
function pointers.

Overall, a slot seems like a great first approach.  It doesn't require any new 
functions, which seems like a plus.  If the any linking issues a la tp_base are 
seen, a new function could be added later.

--

___
Python tracker 
<https://bugs.python.org/issue15870>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue15870] PyType_FromSpec should take metaclass as an argument

2021-09-23 Thread Josh Haberman


Josh Haberman  added the comment:

> It's better to pass the metaclass as a function argument, as with bases. I'd 
> prefer adding a new function that using a slot.

Bases are available both as a slot (Py_tp_bases) and as an argument 
(PyType_FromSpecWithBases).  I don't see why this has to be an either/or 
proposition.  Both can be useful.

Either would satisfy my use case.  I'm constructing N such classes, so the spec 
won't be statically initialized anyway and the initialization issues on Windows 
don't apply.

--

___
Python tracker 
<https://bugs.python.org/issue15870>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue15870] PyType_FromSpec should take metaclass as an argument

2021-09-27 Thread Josh Haberman


Josh Haberman  added the comment:

> I consider Py_tp_bases to be a mistake: it's an extra way of doing things 
> that doesn't add any extra functionality

I think it does add one extra bit of functionality: Py_tp_bases allows the 
bases to be retrieved with PyType_GetSlot().

This isn't quite as applicable to the metaclass, since that can easily be 
retrieved with Py_TYPE(type).

> but is sometimes not correct (and it might not be obvious when it's not 
> correct).

Yes I guess that most all slots are ok to share across sub-interpreters.  I can 
see the argument for aiming to keep slots sub-interpreter-agnostic.

As a tangential point, I think that the DLL case on Windows may be a case where 
Windows is not compliant with the C standard: 
https://mail.python.org/archives/list/python-...@python.org/thread/2WUFTVQA7SLEDEDYSRJ75XFIR3EUTKKO/

Practically speaking this doesn't change anything (extensions that want to be 
compatible with Windows DLLs will still want to avoid this kind of 
initialization) but I think the docs may be incorrect on this point when they 
describe Windows as "strictly standard conforming in this particular behavior."

--

___
Python tracker 
<https://bugs.python.org/issue15870>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue15870] PyType_FromSpec should take metaclass as an argument

2021-09-27 Thread Josh Haberman


Josh Haberman  added the comment:

> "static" anything in C is completely irrelevant to how symbols are looked up 
> and resolved between modules

That is not true.  On ELF/Mach-O the "static" storage-class specifier in C will 
prevent a symbol from being added to the dynamic symbol table, which will make 
it unavailable for use across modules.

> I wasn't aware the C standard covered dynamic symbol resolution?

Well the Python docs invoke the C standard to justify the behavior of DLL 
symbol resolution on Windows, using incorrect arguments about what the standard 
says: https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_base

Fixing those docs would be a good first step.

--

___
Python tracker 
<https://bugs.python.org/issue15870>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue15870] PyType_FromSpec should take metaclass as an argument

2021-09-27 Thread Josh Haberman


Josh Haberman  added the comment:

> On ELF/Mach-O...

nvm, I just realized that you were speaking about Windows specifically here.  I 
believe you that on Windows "static" makes no difference in this case.

The second point stands: if you consider LoadLibrary()/dlopen() to be outside 
the bounds of what the C standard speaks to, then the docs shouldn't invoke the 
C standard to explain the behavior.

--

___
Python tracker 
<https://bugs.python.org/issue15870>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue15870] PyType_FromSpec should take metaclass as an argument

2021-09-27 Thread Josh Haberman

Josh Haberman  added the comment:

This behavior is covered by the standard.  The following C translation unit is 
valid according to C99:

  struct PyTypeObject;
  extern struct PyTypeObject Foo_Type;
  struct PyTypeObject *ptr = &Foo_Type;

Specifically, &Foo_Type is an "address constant" per the standard because it is 
a pointer to an object of static storage duration (6.6p9).

The Python docs contradict this with the following incorrect statement:

> However, the unary ‘&’ operator applied to a non-static variable like 
> PyBaseObject_Type() is not required to produce an address constant.

This statement is incorrect:

1. PyBaseObject_Type is an object of static storage duration.  (Note, this is 
true even though it does not use the "static" keyword -- the "static" 
storage-class specifier and "static storage duration" are separate concepts).

2. It follows that &PyBaseObject_Type is required to produce an address 
constant. because it is a pointer to an object of static storage duration.

MSVC rejects this standard-conforming TU when __declspec(dllimport) is added: 
https://godbolt.org/z/GYrfTqaGn  I am pretty sure this is out of compliance 
with C99.

--

___
Python tracker 
<https://bugs.python.org/issue15870>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue45306] Docs are incorrect re: constant initialization in the C99 standard

2021-09-27 Thread Josh Haberman

New submission from Josh Haberman :

I believe the following excerpt from the docs is incorrect 
(https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_base):

> Slot initialization is subject to the rules of initializing
> globals. C99 requires the initializers to be “address
> constants”. Function designators like PyType_GenericNew(),
> with implicit conversion to a pointer, are valid C99 address
> constants.
>
> However, the unary ‘&’ operator applied to a non-static
> variable like PyBaseObject_Type() is not required to produce
> an address constant. Compilers may support this (gcc does),
> MSVC does not. Both compilers are strictly standard
> conforming in this particular behavior.
>
> Consequently, tp_base should be set in the extension module’s init function.

I explained why in 
https://mail.python.org/archives/list/python-...@python.org/thread/2WUFTVQA7SLEDEDYSRJ75XFIR3EUTKKO/
 and on https://bugs.python.org/msg402738.

The short version: &foo is an "address constant" according to the standard 
whenever "foo" has static storage duration.  Variables declared "extern" have 
static storage duration. Therefore strictly conforming implementations should 
accept &PyBaseObject_Type as a valid constant initializer.

I believe the text above could be replaced by something like:

> MSVC does not support constant initialization of of an address
> that comes from another DLL, so extensions should be set in the
> extension module's init function.

--
assignee: docs@python
components: Documentation
messages: 402752
nosy: docs@python, jhaberman
priority: normal
severity: normal
status: open
title: Docs are incorrect re: constant initialization in the C99 standard

___
Python tracker 
<https://bugs.python.org/issue45306>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue15870] PyType_FromSpec should take metaclass as an argument

2021-09-27 Thread Josh Haberman


Josh Haberman  added the comment:

> Windows/MSVC defines DLLs as separate programs, with their own lifetime and 
> entry point (e.g. you can reload a DLL multiple times and it will be 
> reinitialised each time).

All of this is true of so's in ELF also.  It doesn't mean that the 
implementation needs to reject standards-conforming programs.

I still think the Python documentation is incorrect on this point.  I filed 
https://bugs.python.org/issue45306 to track this separately.

--

___
Python tracker 
<https://bugs.python.org/issue15870>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com



[issue15870] PyType_FromSpec should take metaclass as an argument

2021-09-28 Thread Josh Haberman


Josh Haberman  added the comment:

> Everything is copied by `_FromSpec` after all.

One thing I noticed isn't copied is the string pointed to by tp_name: 
https://github.com/python/cpython/blob/0c50b8c0b8274d54d6b71ed7bd21057d3642f138/Objects/typeobject.c#L3427

This isn't an issue if tp_name is initialized from a string literal.  But if 
tp_name is created dynamically, it could lead to a dangling pointer.  If the 
general message is that "everything is copied by _FromSpec", it might make 
sense to copy the tp_name string too.

> However, I suppose that would replace a safe-by-design API with a "best 
> practice" to never define the spec/slots statically (a best practice that is 
> probably not generally followed or even advertised currently, I guess).

Yes that seems reasonable.  I generally prefer static declarations, since they 
will end up in .data instead of .text and will avoid a copy to the stack at 
runtime.  But these are very minor differences, especially for code that only 
runs once at startup, and a safe-by-default recommendation of always 
initializing PyType_* on the stack makes sense.

--

___
Python tracker 
<https://bugs.python.org/issue15870>
___
___
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com