Skip to content
Closed
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;

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, const char *intf_name, void *intf);
#endif


/* Helpers for printing recursive container types */
PyAPI_FUNC(int) Py_ReprEnter(PyObject *);
PyAPI_FUNC(void) Py_ReprLeave(PyObject *);
Expand Down
190 changes: 190 additions & 0 deletions Include/pyinterface.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#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, PyInterface_GetAttrWChar_Name, &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 string literal 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.
* 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 = { sizeof(PyInterface_GetAttrWChar) };
if (PyObject_GetInterface(obj, PyInterface_GetAttrWChar_Name, &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) }
#define Py_INTERFACE_CALL(data, attr, ...) ((data).attr)(&(data), __VA_ARGS__)

typedef struct _interfacedata {
Py_ssize_t size;
/* intf is 'struct _interfacedata' but expects the specific struct */
int (*release)(void *intf);
} PyInterface_Base;


typedef int (*getinterfacefunc)(PyObject *o, const char *intf_name, void *intf);

/* Functions to get interfaces are defined on PyObject and ... TODO */
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 "getattr-wchar"

typedef struct _getattrwcharinterface {
PyInterface_Base base;
PyObject *(*getattr)(struct _getattrwcharinterface *, const wchar_t *attr);
int (*hasattr)(struct _getattrwcharinterface *, 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, PyInterface_AsUTF8, &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 "as-utf8"

typedef struct _asutf8interface {
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 _asutf8interface *, char *dest, size_t dest_size);
// Allocate new buffer with PyMem_Malloc (use PyMem_Free to release)
char * (*stralloc)(struct _asutf8interface *);
// 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.

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, PyInterface_GetAttrWChar_Name, &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;
}


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.
*/


#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
75 changes: 75 additions & 0 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -2180,6 +2180,81 @@
return (obj == NULL) ? _dir_locals() : _dir_object(obj);
}


static int
_PyInterface_GetAttrWChar_Release(PyInterface_GetAttrWChar *intf)
{
Py_XDECREF(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, const char *intf_name, void *intf)
{
if (0 == strcmp(intf_name, PyInterface_GetAttrWChar_Name)) {
PyInterface_GetAttrWChar *gawc = (PyInterface_GetAttrWChar *)intf;
if (gawc->base.size != sizeof(PyInterface_GetAttrWChar)) {
PyErr_SetString(PyExc_SystemError, "Invalid size struct passed to PyObject_GetInterface");
return -1;
}
gawc->base.release = _PyInterface_GetAttrWChar_Release;

Check warning on line 2229 in Objects/object.c

View workflow job for this annotation

GitHub Actions / Address sanitizer (ubuntu-24.04)

assignment to ‘int (*)(void *)’ from incompatible pointer type ‘int (*)(PyInterface_GetAttrWChar *)’ {aka ‘int (*)(struct _getattrwcharinterface *)’} [-Wincompatible-pointer-types]

Check warning on line 2229 in Objects/object.c

View workflow job for this annotation

GitHub Actions / Hypothesis tests on Ubuntu

assignment to ‘int (*)(void *)’ from incompatible pointer type ‘int (*)(PyInterface_GetAttrWChar *)’ {aka ‘int (*)(struct _getattrwcharinterface *)’} [-Wincompatible-pointer-types]

Check warning on line 2229 in Objects/object.c

View workflow job for this annotation

GitHub Actions / Cross build Linux

assignment to ‘int (*)(void *)’ from incompatible pointer type ‘int (*)(PyInterface_GetAttrWChar *)’ {aka ‘int (*)(struct _getattrwcharinterface *)’} [-Wincompatible-pointer-types]

Check warning on line 2229 in Objects/object.c

View workflow job for this annotation

GitHub Actions / Ubuntu / build and test (ubuntu-24.04)

assignment to ‘int (*)(void *)’ from incompatible pointer type ‘int (*)(PyInterface_GetAttrWChar *)’ {aka ‘int (*)(struct _getattrwcharinterface *)’} [-Wincompatible-pointer-types]

Check warning on line 2229 in Objects/object.c

View workflow job for this annotation

GitHub Actions / Ubuntu (free-threading) / build and test (ubuntu-24.04-arm)

assignment to ‘int (*)(void *)’ from incompatible pointer type ‘int (*)(PyInterface_GetAttrWChar *)’ {aka ‘int (*)(struct _getattrwcharinterface *)’} [-Wincompatible-pointer-types]

Check warning on line 2229 in Objects/object.c

View workflow job for this annotation

GitHub Actions / Ubuntu (bolt) / build and test (ubuntu-24.04)

assignment to ‘int (*)(void *)’ from incompatible pointer type ‘int (*)(PyInterface_GetAttrWChar *)’ {aka ‘int (*)(struct _getattrwcharinterface *)’} [-Wincompatible-pointer-types]

Check warning on line 2229 in Objects/object.c

View workflow job for this annotation

GitHub Actions / Ubuntu / build and test (ubuntu-24.04-arm)

assignment to ‘int (*)(void *)’ from incompatible pointer type ‘int (*)(PyInterface_GetAttrWChar *)’ {aka ‘int (*)(struct _getattrwcharinterface *)’} [-Wincompatible-pointer-types]

Check warning on line 2229 in Objects/object.c

View workflow job for this annotation

GitHub Actions / Ubuntu (free-threading) / build and test (ubuntu-24.04)

assignment to ‘int (*)(void *)’ from incompatible pointer type ‘int (*)(PyInterface_GetAttrWChar *)’ {aka ‘int (*)(struct _getattrwcharinterface *)’} [-Wincompatible-pointer-types]
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, const char *intf_name, void *intf)
{
if (!obj || !intf_name || !intf) {
PyErr_SetString(PyExc_SystemError, "NULL argument passed to PyObject_GetInterface");
return -1;
}
getinterfacefunc fn = Py_TYPE(obj)->tp_getinterface;
if (fn) {
return fn(obj, intf_name, intf);
}
return PyObject_GenericGetInterface(obj, intf_name, intf);
}


/*
None is a non-NULL undefined value.
There is (and should be!) no way to create other objects of this type,
Expand Down
1 change: 1 addition & 0 deletions Objects/typeslots.inc

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading