diff --git a/Include/Python.h b/Include/Python.h index 261b4d316bdf85..dd524def535a59 100644 --- a/Include/Python.h +++ b/Include/Python.h @@ -80,6 +80,7 @@ __pragma(warning(disable: 4201)) #include "pyatomic.h" #include "pylock.h" #include "critical_section.h" +#include "pyinterface.h" #include "object.h" #include "refcount.h" #include "objimpl.h" diff --git a/Include/cpython/object.h b/Include/cpython/object.h index 4e6f86f29d8473..cdad0812c12d45 100644 --- a/Include/cpython/object.h +++ b/Include/cpython/object.h @@ -239,6 +239,8 @@ struct _typeobject { * Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere). */ uint16_t tp_versions_used; + + Py_getinterfacefunc tp_getinterface; }; #define _Py_ATTR_CACHE_UNUSED (30000) // (see tp_versions_used) diff --git a/Include/object.h b/Include/object.h index 9585f4a1d67a52..83cb8e80c7fdab 100644 --- a/Include/object.h +++ b/Include/object.h @@ -491,6 +491,11 @@ PyAPI_FUNC(void) PyObject_ClearWeakRefs(PyObject *); */ PyAPI_FUNC(PyObject *) PyObject_Dir(PyObject *); +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030F0000 +PyAPI_FUNC(int) PyObject_GetInterface(PyObject *obj, void *intf); +#endif + + /* Helpers for printing recursive container types */ PyAPI_FUNC(int) Py_ReprEnter(PyObject *); PyAPI_FUNC(void) Py_ReprLeave(PyObject *); diff --git a/Include/pyinterface.h b/Include/pyinterface.h new file mode 100644 index 00000000000000..892e329c09de1e --- /dev/null +++ b/Include/pyinterface.h @@ -0,0 +1,202 @@ +#ifndef Py_PYINTERFACE_H +#define Py_PYINTERFACE_H +#ifdef __cplusplus +extern "C" { +#endif + +/* +The PyInterface_Base struct is the generic type for actual interface data +implementations. The intent is for callers to preallocate the specific struct +and have the PyObject_GetInterface() function fill it in. + +An example of direct use of the interface API (definitions and example without +macro helpers below): + + Py_INTERFACE_VAR(PyInterface_GetAttrWChar, attr_data); + if (PyObject_GetInterface(obj, &attr_data) == 0) { + result = Py_INTERFACE_CALL(attr_data, getattr, L"attribute_name"); + PyInterface_Release(&attr_data); + } else { + char attr_name[128]; + wchar_to_char(attr_name, L"attribute_name"); // hypothetical converter + result = PyObject_GetAttr(obj, attr_name); + } + +Here are the key points that add value: + * the PyInterface_GetAttrWChar_Name constant is embedded in the calling + module, making the value available when running against earlier releases of + Python (unlike a function export, which would cause a load failure). + * if the name is not available, the PyObject_GetInterface call can fail safely. + Thus, new APIs can be added in later releases, and newer compiled modules can + be fully binary compatible with older releases. + * "names" are arbitrary ints (for fast comparison/switch statements). Core + values have high 32-bits zero - others should use a unique value as their top + 32 bits. The intent is that they are always referred to as a constant, hence, + "names" rather than "index" or similar. + * The attr_data value contains all the information required to eventually + produce the result. It may provide fields for direct access as well as + function pointers that can calculate/copy/return results as needed. + * The interface is resolved by the object's type, but does not have to provide + identical result for every instance. It has independent lifetime from object, + (though this will often just be a new strong reference to the object). + * Static/header libraries can be used to wrap up the faster APIs, so that + extensions can adopt them simply by compiling with the latest release. + An example is shown at the end of this file. + + +Without the Py_INTERFACE_* helper macros, it would look like the below. This is +primarily for the benefit of non-C developers trying to use the API without +macros. + + PyInterface_GetAttrWChar attr_data = { + .size = sizeof(PyInterface_GetAttrWChar), + .name = PyInterface_GetAttrWChar_Name + }; + if (PyObject_GetInterface(obj, &attr_data) == 0) { + result = (*attr_data.getattr)(&attr_data, L"attribute_name"); + PyInterface_Release(&attr_data); + } ... +*/ + +#define Py_INTERFACE_VAR(t, n) t n = { sizeof(t), t ## _Name } +#define Py_INTERFACE_CALL(data, attr, ...) ((data).attr)(&(data), __VA_ARGS__) + +typedef struct PyInterface_Base { + Py_ssize_t size; + uint64_t name; + /* intf is 'PyInterface_Base' but will be passed the full struct */ + int (*release)(struct PyInterface_Base *intf); + + /* Possibly: void *func_table; ??? + Pro: would allow function tables to be static, so less copying (though + could do this on a case-by-case basis anyway) + Con: additional indirection and more lossy typing */ +} PyInterface_Base; + + +typedef int (*Py_getinterfacefunc)(PyObject *o, PyInterface_Base *intf); + +/* Functions to get interfaces are defined on PyObject and ... TODO */ +PyAPI_FUNC(int) PyInterface_Release(void *intf); + + +/* Some generic/example interface definitions. + +The first (PyInterface_GetAttrWChar) shows a hypothetical new API that may be added +in a later release. Rather than modifying the ABI (making newer extensions +incompatible with earlier releases), it is added as a interface. Runtimes +without the interface will return a runtime error, and the caller uses a +fallback (the example above). +*/ + +#define PyInterface_GetAttrWChar_Name 1 + +typedef struct PyInterface_GetAttrWChar { + PyInterface_Base base; + PyObject *(*getattr)(struct PyInterface_GetAttrWChar *, const wchar_t *attr); + int (*hasattr)(struct PyInterface_GetAttrWChar *, const wchar_t *attr); + // Strong reference to original object, but for private use only. + PyObject *_obj; +} PyInterface_GetAttrWChar; + + +/* This example (PyInterface_AsUTF8) shows an optionally-optimised API, where in some +cases the result of the API is readily available, and can be returned to +pre-allocated memory (in this case, the interface data in the stack of the caller). +The caller can then either use a fast path to access it directly, or the more +generic function (pointer) in the struct that will *always* produce a result, +but may take more computation if the fast result was not present. + + Py_INTERFACE_VAR(PyInterface_AsUTF8, intf); + if (PyObject_GetInterface(o, &intf) == 0) { + // This check is optional, as the function calls in the interface should + // always succeed. However, developers who want to avoid an additional + // function call/allocation can do the check themselves. + // This check is defined per-interface struct, so users reference the + // docs for the specific interfaces they're using to find the check. + if (intf->s[0]) { + // use intf->s as the result. + call_next_api(intf->s); + } else { + char *s = Py_INTERFACE_CALL(intf, stralloc); + call_next_api(s); + PyMem_Free(s); + } + PyInterface_Release(&intf); + } else { ... } + +*/ + +#define PyInterface_AsUTF8_Name 2 + +typedef struct PyInterface_AsUTF8 { + PyInterface_Base base; + // Copy of contents if it was readily available. s[0] will be non-zero if set + char s[16]; + // Copy the characters into an existing string + size_t (*strcpy)(struct PyInterface_AsUTF8 *, char *dest, size_t dest_size); + // Allocate new buffer with PyMem_Malloc (use PyMem_Free to release) + char * (*stralloc)(struct PyInterface_AsUTF8 *); + // Optional strong reference to object, if unable to use char array + PyObject *_obj; +} PyInterface_AsUTF8; + +/* Example implementation of PyInterface_AsUTF8.strcpy: + +size_t _PyUnicode_AsUTF8Interface_strcpy(PyInterface_AsUTF8 *intf, char *dest, size_t dest_size) +{ + if (intf->_obj) { + // copy/convert data from _obj... + _internal_copy((PyUnicodeObject *)intf->_obj, dest, dest_size); + return chars_copied; + } else { + return strcpy_s(dest, dest_size, intf->_reserved); + } +} + +*/ + +/* API wrappers. + +These are inline functions (or in a static import library) to embed all the +implementation into the external module. We can change the implementation over +releases, and by using interfaces to add optional handling, the behaviour +remains compatible with earlier releases, and the sources remain compatible. + +Note that additions will usually be at the top of the function, assuming that +newer interfaces are preferred over older ones. +This may be defined in its own header or statically linked library, provided the +implementation ends up in the external module, not in the Python library. + +static inline PyObject *PyObject_GetAttrWChar(PyObject *o, wchar_t *attr) +{ + PyObject *result = NULL; + + // Added in version N+1 + Py_INTERFACE_VAR(PyInterface_GetAttrWChar, intf); + if (PyObject_GetInterface(o, &intf) == 0) { + result = Py_INTERFACE_CALL(intf, getattr, attr); + if (PyInterface_Release(&intf) < 0) { + Py_DecRef(result); + return NULL; + } + return result; + } + PyErr_Clear(); + + // Original implementation in version N + PyObject *attro = PyUnicode_FromWideChar(attr, -1); + if (attro) { + result = PyObject_GetAttr(o, attro); + Py_DecRef(attro); + } + return result; +} + +*/ + + +#ifdef __cplusplus +} +#endif +#endif diff --git a/Include/typeslots.h b/Include/typeslots.h index a7f3017ec02e92..7019dada5caa8c 100644 --- a/Include/typeslots.h +++ b/Include/typeslots.h @@ -94,3 +94,7 @@ /* New in 3.14 */ #define Py_tp_token 83 #endif +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030F0000 +/* New in 3.15 */ +#define Py_tp_getinterface 84 +#endif diff --git a/Lib/test/test_capi/test_interfaces.py b/Lib/test/test_capi/test_interfaces.py new file mode 100644 index 00000000000000..14f1cbce13b0b8 --- /dev/null +++ b/Lib/test/test_capi/test_interfaces.py @@ -0,0 +1,14 @@ +import unittest +from test.support import import_helper + + +_testcapi = import_helper.import_module('_testcapi') + + +class InterfacesTest(unittest.TestCase): + def test_interface_getattrwchar(self): + fn = _testcapi.interface_getattrwchar(_testcapi, "interface_getattrwchar") + self.assertIs(fn, _testcapi.interface_getattrwchar) + + with self.assertRaises(AttributeError): + _testcapi.interface_getattrwchar(_testcapi, 'ϼўТλФЙ') diff --git a/Modules/_testcapi/interfaces.c b/Modules/_testcapi/interfaces.c new file mode 100644 index 00000000000000..3b8886b8749e96 --- /dev/null +++ b/Modules/_testcapi/interfaces.c @@ -0,0 +1,42 @@ +#define PYTESTCAPI_NEED_INTERNAL_API + +#include "parts.h" +#include "util.h" + + +static PyObject * +interface_getattrwchar(PyObject *Py_UNUSED(module), PyObject *args) +{ + PyObject *obj = NULL; + PyObject *attro = NULL; + if (!PyArg_ParseTuple(args, "OO", &obj, &attro)) { + return NULL; + } + wchar_t *attr = PyUnicode_AsWideCharString(attro, NULL); + if (!attr) { + return NULL; + } + + Py_INTERFACE_VAR(PyInterface_GetAttrWChar, attr_data); + if (PyObject_GetInterface(obj, &attr_data) < 0) { + PyMem_Free(attr); + return NULL; + } + PyObject *result = Py_INTERFACE_CALL(attr_data, getattr, attr); + PyInterface_Release(&attr_data); + PyMem_Free(attr); + return result; +} + + +static PyMethodDef test_methods[] = { + {"interface_getattrwchar", interface_getattrwchar, METH_VARARGS}, + {NULL}, +}; + + +int +_PyTestCapi_Init_Interfaces(PyObject *m) +{ + return PyModule_AddFunctions(m, test_methods); +} diff --git a/Modules/_testcapi/parts.h b/Modules/_testcapi/parts.h index 32915d04bd3635..8eabaab0d09b50 100644 --- a/Modules/_testcapi/parts.h +++ b/Modules/_testcapi/parts.h @@ -66,5 +66,6 @@ int _PyTestCapi_Init_Import(PyObject *mod); int _PyTestCapi_Init_Frame(PyObject *mod); int _PyTestCapi_Init_Type(PyObject *mod); int _PyTestCapi_Init_Function(PyObject *mod); +int _PyTestCapi_Init_Interfaces(PyObject *mod); #endif // Py_TESTCAPI_PARTS_H diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index c80a780e22ca34..95fb5490c116ee 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -3506,6 +3506,9 @@ PyInit__testcapi(void) if (_PyTestCapi_Init_Function(m) < 0) { return NULL; } + if (_PyTestCapi_Init_Interfaces(m) < 0) { + return NULL; + } PyState_AddModule(m, &_testcapimodule); return m; diff --git a/Objects/object.c b/Objects/object.c index 1f10c2531fead1..e92bad567aeca5 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -2180,6 +2180,99 @@ PyObject_Dir(PyObject *obj) return (obj == NULL) ? _dir_locals() : _dir_object(obj); } + +static int +_PyInterface_GetAttrWChar_Release(PyInterface_Base *intf) +{ + Py_XDECREF(((PyInterface_GetAttrWChar*)intf)->_obj); + return 0; +} + + +static PyObject * +_PyInterface_GetAttrWChar_GetAttr(PyInterface_GetAttrWChar *intf, const wchar_t *attr) +{ + // TODO: Optimise the implementation + PyObject *result = NULL; + PyObject *attro = PyUnicode_FromWideChar(attr, -1); + if (attro) { + result = PyObject_GetAttr(intf->_obj, attro); + Py_DECREF(attro); + } + return result; +} + + +static int +_PyInterface_GetAttrWChar_HasAttr(PyInterface_GetAttrWChar *intf, const wchar_t *attr) +{ + // TODO: Optimise this implementation + int result = -1; + PyObject *attro = PyUnicode_FromWideChar(attr, -1); + if (attro) { + result = PyObject_HasAttrWithError(intf->_obj, attro); + Py_DECREF(attro); + } + return result; +} + + +static int +PyObject_GenericGetInterface(PyObject *obj, PyInterface_Base *intf) +{ + switch (intf->name) { + case PyInterface_GetAttrWChar_Name: + if (intf->size != sizeof(PyInterface_GetAttrWChar)) { + PyErr_SetString(PyExc_SystemError, "Invalid size struct passed to PyObject_GetInterface"); + return -1; + } + intf->release = _PyInterface_GetAttrWChar_Release; + PyInterface_GetAttrWChar *gawc = (PyInterface_GetAttrWChar *)intf; + gawc->getattr = _PyInterface_GetAttrWChar_GetAttr; + gawc->hasattr = _PyInterface_GetAttrWChar_HasAttr; + gawc->_obj = Py_NewRef(obj); + return 0; + } + PyErr_SetString(PyExc_TypeError, "Interface not supported"); + return -1; +} + + +/* Abstract API for getting an interface. Delegates through the type object, + or uses PyObject_GenericGetInterface if not set. +*/ +int +PyObject_GetInterface(PyObject *obj, void *intf) +{ + if (!obj || !intf) { + PyErr_SetString(PyExc_SystemError, "NULL argument passed to PyObject_GetInterface"); + return -1; + } + + Py_getinterfacefunc fn = NULL; + if (PyType_Check(obj)) { + fn = ((PyTypeObject *)obj)->tp_getinterface; + } else { + fn = Py_TYPE(obj)->tp_getinterface; + } + if (fn) { + return fn(obj, (PyInterface_Base *)intf); + } + return PyObject_GenericGetInterface(obj, (PyInterface_Base *)intf); +} + + +int +PyInterface_Release(void *intf) +{ + PyInterface_Base *intf_base = (PyInterface_Base *)intf; + if (intf_base && intf_base->release) { + return (*intf_base->release)(intf_base); + } + return 0; +} + + /* None is a non-NULL undefined value. There is (and should be!) no way to create other objects of this type, diff --git a/Objects/typeslots.inc b/Objects/typeslots.inc index 642160fe0bd8bc..8e1b41cf2f2128 100644 --- a/Objects/typeslots.inc +++ b/Objects/typeslots.inc @@ -82,3 +82,4 @@ {offsetof(PyAsyncMethods, am_send), offsetof(PyTypeObject, tp_as_async)}, {-1, offsetof(PyTypeObject, tp_vectorcall)}, {-1, offsetof(PyHeapTypeObject, ht_token)}, +{-1, offsetof(PyTypeObject, tp_getinterface)}, diff --git a/PCbuild/_testcapi.vcxproj b/PCbuild/_testcapi.vcxproj index a355a5fc25707a..e8204c7024a890 100644 --- a/PCbuild/_testcapi.vcxproj +++ b/PCbuild/_testcapi.vcxproj @@ -132,6 +132,7 @@ + diff --git a/PCbuild/_testcapi.vcxproj.filters b/PCbuild/_testcapi.vcxproj.filters index 05128d3ac36efc..ecf32cc1187e74 100644 --- a/PCbuild/_testcapi.vcxproj.filters +++ b/PCbuild/_testcapi.vcxproj.filters @@ -129,6 +129,9 @@ Source Files + + Source Files +