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
+