Skip to content

Commit 310af91

Browse files
committed
[WIP] Implement PyInterface prototype
1 parent d61dda5 commit 310af91

File tree

7 files changed

+278
-0
lines changed

7 files changed

+278
-0
lines changed

Include/Python.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ __pragma(warning(disable: 4201))
8080
#include "pyatomic.h"
8181
#include "pylock.h"
8282
#include "critical_section.h"
83+
#include "pyinterface.h"
8384
#include "object.h"
8485
#include "refcount.h"
8586
#include "objimpl.h"

Include/cpython/object.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ struct _typeobject {
239239
* Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere).
240240
*/
241241
uint16_t tp_versions_used;
242+
243+
getinterfacefunc tp_getinterface;
242244
};
243245

244246
#define _Py_ATTR_CACHE_UNUSED (30000) // (see tp_versions_used)

Include/object.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,11 @@ PyAPI_FUNC(void) PyObject_ClearWeakRefs(PyObject *);
491491
*/
492492
PyAPI_FUNC(PyObject *) PyObject_Dir(PyObject *);
493493

494+
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030F0000
495+
PyAPI_FUNC(int) PyObject_GetInterface(PyObject *obj, const char *intf_name, void *intf);
496+
#endif
497+
498+
494499
/* Helpers for printing recursive container types */
495500
PyAPI_FUNC(int) Py_ReprEnter(PyObject *);
496501
PyAPI_FUNC(void) Py_ReprLeave(PyObject *);

Include/pyinterface.h

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#ifndef Py_PYINTERFACE_H
2+
#define Py_PYINTERFACE_H
3+
#ifdef __cplusplus
4+
extern "C" {
5+
#endif
6+
7+
/*
8+
The PyInterface_Base struct is the generic type for actual interface data
9+
implementations. The intent is for callers to preallocate the specific struct
10+
and have the PyObject_GetInterface() function fill it in.
11+
12+
An example of direct use of the interface API (definitions and example without
13+
macro helpers below):
14+
15+
Py_INTERFACE_VAR(PyInterface_GetAttrWChar, attr_data);
16+
if (PyObject_GetInterface(obj, PyInterface_GetAttrWChar_Name, &attr_data) == 0) {
17+
result = Py_INTERFACE_CALL(attr_data, getattr, L"attribute_name");
18+
PyInterface_Release(&attr_data);
19+
} else {
20+
char attr_name[128];
21+
wchar_to_char(attr_name, L"attribute_name"); // hypothetical converter
22+
result = PyObject_GetAttr(obj, attr_name);
23+
}
24+
25+
Here are the key points that add value:
26+
* the PyInterface_GetAttrWChar_Name string literal is embedded in the calling
27+
module, making the value available when running against earlier releases of
28+
Python (unlike a function export, which would cause a load failure).
29+
* if the name is not available, the PyObject_GetInterface call can fail safely.
30+
Thus, new APIs can be added in later releases, and newer compiled modules can
31+
be fully binary compatible with older releases.
32+
* The attr_data value contains all the information required to eventually
33+
produce the result. It may provide fields for direct access as well as
34+
function pointers that can calculate/copy/return results as needed.
35+
* The interface is resolved by the object's type, but does not have to provide
36+
identical result for every instance. It has independent lifetime from object,
37+
(though this will often just be a new strong reference to the object).
38+
* Static/header libraries can be used to wrap up the faster APIs, so that
39+
extensions can adopt them simply by compiling with the latest release.
40+
An example is shown at the end of this file.
41+
42+
43+
Without the Py_INTERFACE_* helper macros, it would look like the below. This is
44+
primarily for the benefit of non-C developers trying to use the API without
45+
macros.
46+
47+
PyInterface_GetAttrWChar attr_data = { sizeof(PyInterface_GetAttrWChar) };
48+
if (PyObject_GetInterface(obj, PyInterface_GetAttrWChar_Name, &attr_data) == 0) {
49+
result = (*attr_data.getattr)(&attr_data, L"attribute_name");
50+
PyInterface_Release(&attr_data);
51+
} ...
52+
*/
53+
54+
#define Py_INTERFACE_VAR(t, n) t n = { sizeof(t) }
55+
#define Py_INTERFACE_CALL(data, attr, ...) ((data).attr)(&(data), __VA_ARGS__)
56+
57+
typedef struct _interfacedata {
58+
Py_ssize_t size;
59+
/* intf is 'struct _interfacedata' but expects the specific struct */
60+
int (*release)(void *intf);
61+
} PyInterface_Base;
62+
63+
64+
typedef int (*getinterfacefunc)(PyObject *o, const char *intf_name, void *intf);
65+
66+
/* Functions to get interfaces are defined on PyObject and ... TODO */
67+
int PyInterface_Release(void *intf);
68+
69+
70+
/* Some generic/example interface definitions.
71+
72+
The first (PyInterface_GetAttrWChar) shows a hypothetical new API that may be added
73+
in a later release. Rather than modifying the ABI (making newer extensions
74+
incompatible with earlier releases), it is added as a interface. Runtimes
75+
without the interface will return a runtime error, and the caller uses a
76+
fallback (the example above).
77+
*/
78+
79+
#define PyInterface_GetAttrWChar_Name "getattr-wchar"
80+
81+
typedef struct _getattrwcharinterface {
82+
PyInterface_Base base;
83+
PyObject *(*getattr)(struct _getattrwcharinterface *, const wchar_t *attr);
84+
int (*hasattr)(struct _getattrwcharinterface *, const wchar_t *attr);
85+
// Strong reference to original object, but for private use only.
86+
PyObject *_obj;
87+
} PyInterface_GetAttrWChar;
88+
89+
90+
/* This example (PyInterface_AsUTF8) shows an optionally-optimised API, where in some
91+
cases the result of the API is readily available, and can be returned to
92+
pre-allocated memory (in this case, the interface data in the stack of the caller).
93+
The caller can then either use a fast path to access it directly, or the more
94+
generic function (pointer) in the struct that will *always* produce a result,
95+
but may take more computation if the fast result was not present.
96+
97+
Py_INTERFACE_VAR(PyInterface_AsUTF8, intf);
98+
if (PyObject_GetInterface(o, PyInterface_AsUTF8, &intf) == 0) {
99+
// This check is optional, as the function calls in the interface should
100+
// always succeed. However, developers who want to avoid an additional
101+
// function call/allocation can do the check themselves.
102+
// This check is defined per-interface struct, so users reference the
103+
// docs for the specific interfaces they're using to find the check.
104+
if (intf->s[0]) {
105+
// use intf->s as the result.
106+
call_next_api(intf->s);
107+
} else {
108+
char *s = Py_INTERFACE_CALL(intf, stralloc);
109+
call_next_api(s);
110+
PyMem_Free(s);
111+
}
112+
PyInterface_Release(&intf);
113+
} else { ... }
114+
115+
*/
116+
117+
#define PyInterface_AsUTF8_Name "as-utf8"
118+
119+
typedef struct _asutf8interface {
120+
PyInterface_Base base;
121+
// Copy of contents if it was readily available. s[0] will be non-zero if set
122+
char s[16];
123+
// Copy the characters into an existing string
124+
size_t (*strcpy)(struct _asutf8interface *, char *dest, size_t dest_size);
125+
// Allocate new buffer with PyMem_Malloc (use PyMem_Free to release)
126+
char * (*stralloc)(struct _asutf8interface *);
127+
// Optional strong reference to object, if unable to use char array
128+
PyObject *_obj;
129+
} PyInterface_AsUTF8;
130+
131+
/* Example implementation of PyInterface_AsUTF8.strcpy:
132+
133+
size_t _PyUnicode_AsUTF8Interface_strcpy(PyInterface_AsUTF8 *intf, char *dest, size_t dest_size)
134+
{
135+
if (intf->_obj) {
136+
// copy/convert data from _obj...
137+
_internal_copy((PyUnicodeObject *)intf->_obj, dest, dest_size);
138+
return chars_copied;
139+
} else {
140+
return strcpy_s(dest, dest_size, intf->_reserved);
141+
}
142+
}
143+
144+
*/
145+
146+
/* API wrappers.
147+
148+
These are inline functions (or in a static import library) to embed all the
149+
implementation into the external module. We can change the implementation over
150+
releases, and by using interfaces to add optional handling, the behaviour
151+
remains compatible with earlier releases, and the sources remain compatible.
152+
153+
Note that additions will usually be at the top of the function, assuming that
154+
newer interfaces are preferred over older ones.
155+
156+
static inline PyObject *PyObject_GetAttrWChar(PyObject *o, wchar_t *attr)
157+
{
158+
PyObject *result = NULL;
159+
160+
// Added in version N+1
161+
Py_INTERFACE_VAR(PyInterface_GetAttrWChar, intf);
162+
if (PyObject_GetInterface(o, PyInterface_GetAttrWChar_Name, &intf) == 0) {
163+
result = Py_INTERFACE_CALL(intf, getattr, attr);
164+
if (PyInterface_Release(&intf) < 0) {
165+
Py_DecRef(result);
166+
return NULL;
167+
}
168+
return result;
169+
}
170+
PyErr_Clear();
171+
172+
// Original implementation in version N
173+
PyObject *attro = PyUnicode_FromWideChar(attr, -1);
174+
if (attro) {
175+
result = PyObject_GetAttr(o, attro);
176+
Py_DecRef(attro);
177+
}
178+
return result;
179+
}
180+
181+
182+
This may be defined in its own header or statically linked library, provided the
183+
implementation ends up in the external module, not in the Python library.
184+
*/
185+
186+
187+
#ifdef __cplusplus
188+
}
189+
#endif
190+
#endif

Include/typeslots.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,7 @@
9494
/* New in 3.14 */
9595
#define Py_tp_token 83
9696
#endif
97+
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030F0000
98+
/* New in 3.15 */
99+
#define Py_tp_getinterface 84
100+
#endif

Objects/object.c

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2180,6 +2180,81 @@ PyObject_Dir(PyObject *obj)
21802180
return (obj == NULL) ? _dir_locals() : _dir_object(obj);
21812181
}
21822182

2183+
2184+
static int
2185+
_PyInterface_GetAttrWChar_Release(PyInterface_GetAttrWChar *intf)
2186+
{
2187+
Py_XDECREF(intf->_obj);
2188+
return 0;
2189+
}
2190+
2191+
2192+
static PyObject *
2193+
_PyInterface_GetAttrWChar_GetAttr(PyInterface_GetAttrWChar *intf, const wchar_t *attr)
2194+
{
2195+
// TODO: Optimise the implementation
2196+
PyObject *result = NULL;
2197+
PyObject *attro = PyUnicode_FromWideChar(attr, -1);
2198+
if (attro) {
2199+
result = PyObject_GetAttr(intf->_obj, attro);
2200+
Py_DECREF(attro);
2201+
}
2202+
return result;
2203+
}
2204+
2205+
2206+
static int
2207+
_PyInterface_GetAttrWChar_HasAttr(PyInterface_GetAttrWChar *intf, const wchar_t *attr)
2208+
{
2209+
// TODO: Optimise this implementation
2210+
int result = -1;
2211+
PyObject *attro = PyUnicode_FromWideChar(attr, -1);
2212+
if (attro) {
2213+
result = PyObject_HasAttrWithError(intf->_obj, attro);
2214+
Py_DECREF(attro);
2215+
}
2216+
return result;
2217+
}
2218+
2219+
2220+
static int
2221+
PyObject_GenericGetInterface(PyObject *obj, const char *intf_name, void *intf)
2222+
{
2223+
if (0 == strcmp(intf_name, PyInterface_GetAttrWChar_Name)) {
2224+
PyInterface_GetAttrWChar *gawc = (PyInterface_GetAttrWChar *)intf;
2225+
if (gawc->base.size != sizeof(PyInterface_GetAttrWChar)) {
2226+
PyErr_SetString(PyExc_SystemError, "Invalid size struct passed to PyObject_GetInterface");
2227+
return -1;
2228+
}
2229+
gawc->base.release = _PyInterface_GetAttrWChar_Release;
2230+
gawc->getattr = _PyInterface_GetAttrWChar_GetAttr;
2231+
gawc->hasattr = _PyInterface_GetAttrWChar_HasAttr;
2232+
gawc->_obj = Py_NewRef(obj);
2233+
return 0;
2234+
}
2235+
PyErr_SetString(PyExc_TypeError, "Interface not supported");
2236+
return -1;
2237+
}
2238+
2239+
2240+
/* Abstract API for getting an interface. Delegates through the type object,
2241+
or uses PyObject_GenericGetInterface if not set.
2242+
*/
2243+
int
2244+
PyObject_GetInterface(PyObject *obj, const char *intf_name, void *intf)
2245+
{
2246+
if (!obj || !intf_name || !intf) {
2247+
PyErr_SetString(PyExc_SystemError, "NULL argument passed to PyObject_GetInterface");
2248+
return -1;
2249+
}
2250+
getinterfacefunc fn = Py_TYPE(obj)->tp_getinterface;
2251+
if (fn) {
2252+
return fn(obj, intf_name, intf);
2253+
}
2254+
return PyObject_GenericGetInterface(obj, intf_name, intf);
2255+
}
2256+
2257+
21832258
/*
21842259
None is a non-NULL undefined value.
21852260
There is (and should be!) no way to create other objects of this type,

Objects/typeslots.inc

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)