Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Include/Python.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 *);
Expand Down
202 changes: 202 additions & 0 deletions Include/pyinterface.h
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions Include/typeslots.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 14 additions & 0 deletions Lib/test/test_capi/test_interfaces.py
Original file line number Diff line number Diff line change
@@ -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, 'ϼўТλФЙ')
42 changes: 42 additions & 0 deletions Modules/_testcapi/interfaces.c
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 1 addition & 0 deletions Modules/_testcapi/parts.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading