https://github.com/python/cpython/commit/b30d30c747df2bf9f1614df8e76db2ffdb24fcd8
commit: b30d30c747df2bf9f1614df8e76db2ffdb24fcd8
branch: main
author: Eric Snow <[email protected]>
committer: encukou <[email protected]>
date: 2024-05-23T21:15:52+02:00
summary:
gh-117398: Statically Allocate the Datetime C-API (GH-119472)
files:
A Misc/NEWS.d/next/Library/2024-05-23-11-52-36.gh-issue-117398.2FG1Mk.rst
M Modules/_datetimemodule.c
M Tools/c-analyzer/cpython/globals-to-fix.tsv
diff --git
a/Misc/NEWS.d/next/Library/2024-05-23-11-52-36.gh-issue-117398.2FG1Mk.rst
b/Misc/NEWS.d/next/Library/2024-05-23-11-52-36.gh-issue-117398.2FG1Mk.rst
new file mode 100644
index 00000000000000..ac595f1b7fc84c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-05-23-11-52-36.gh-issue-117398.2FG1Mk.rst
@@ -0,0 +1,3 @@
+Objects in the datetime C-API are now all statically allocated, which means
+better memory safety, especially when the module is reloaded. This should be
+transparent to users.
diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c
index 9a66f0358179b5..3c6d270b8d1331 100644
--- a/Modules/_datetimemodule.c
+++ b/Modules/_datetimemodule.c
@@ -1178,6 +1178,8 @@ new_time_subclass_fold_ex(int hour, int minute, int
second, int usecond,
return t;
}
+static PyDateTime_Delta * look_up_delta(int, int, int, PyTypeObject *);
+
/* Create a timedelta instance. Normalize the members iff normalize is
* true. Passing false is a speed optimization, if you know for sure
* that seconds and microseconds are already in their proper ranges. In any
@@ -1198,6 +1200,12 @@ new_delta_ex(int days, int seconds, int microseconds,
int normalize,
if (check_delta_day_range(days) < 0)
return NULL;
+ self = look_up_delta(days, seconds, microseconds, type);
+ if (self != NULL) {
+ return (PyObject *)self;
+ }
+ assert(!PyErr_Occurred());
+
self = (PyDateTime_Delta *) (type->tp_alloc(type, 0));
if (self != NULL) {
self->hashcode = -1;
@@ -1219,6 +1227,8 @@ typedef struct
PyObject *name;
} PyDateTime_TimeZone;
+static PyDateTime_TimeZone * look_up_timezone(PyObject *offset, PyObject
*name);
+
/* Create new timezone instance checking offset range. This
function does not check the name argument. Caller must assure
that offset is a timedelta instance and name is either NULL
@@ -1234,6 +1244,12 @@ create_timezone(PyObject *offset, PyObject *name)
assert(PyDelta_Check(offset));
assert(name == NULL || PyUnicode_Check(name));
+ self = look_up_timezone(offset, name);
+ if (self != NULL) {
+ return (PyObject *)self;
+ }
+ assert(!PyErr_Occurred());
+
self = (PyDateTime_TimeZone *)(type->tp_alloc(type, 0));
if (self == NULL) {
return NULL;
@@ -2892,6 +2908,25 @@ static PyTypeObject PyDateTime_DeltaType = {
0, /* tp_free */
};
+// XXX Can we make this const?
+static PyDateTime_Delta zero_delta = {
+ PyObject_HEAD_INIT(&PyDateTime_DeltaType)
+ /* Letting this be set lazily is a benign race. */
+ .hashcode = -1,
+};
+
+static PyDateTime_Delta *
+look_up_delta(int days, int seconds, int microseconds, PyTypeObject *type)
+{
+ if (days == 0 && seconds == 0 && microseconds == 0
+ && type == zero_delta.ob_base.ob_type)
+ {
+ return &zero_delta;
+ }
+ return NULL;
+}
+
+
/*
* PyDateTime_Date implementation.
*/
@@ -4184,6 +4219,23 @@ static PyTypeObject PyDateTime_TimeZoneType = {
timezone_new, /* tp_new */
};
+// XXX Can we make this const?
+static PyDateTime_TimeZone utc_timezone = {
+ PyObject_HEAD_INIT(&PyDateTime_TimeZoneType)
+ .offset = (PyObject *)&zero_delta,
+ .name = NULL,
+};
+
+static PyDateTime_TimeZone *
+look_up_timezone(PyObject *offset, PyObject *name)
+{
+ if (offset == utc_timezone.offset && name == NULL) {
+ return &utc_timezone;
+ }
+ return NULL;
+}
+
+
/*
* PyDateTime_Time implementation.
*/
@@ -6719,45 +6771,42 @@ static PyMethodDef module_methods[] = {
{NULL, NULL}
};
+
+/* The C-API is process-global. This violates interpreter isolation
+ * due to the objects stored here. Thus each of those objects must
+ * be managed carefully. */
+// XXX Can we make this const?
+static PyDateTime_CAPI capi = {
+ /* The classes must be readied before used here.
+ * That will happen the first time the module is loaded.
+ * They aren't safe to be shared between interpreters,
+ * but that's okay as long as the module is single-phase init. */
+ .DateType = &PyDateTime_DateType,
+ .DateTimeType = &PyDateTime_DateTimeType,
+ .TimeType = &PyDateTime_TimeType,
+ .DeltaType = &PyDateTime_DeltaType,
+ .TZInfoType = &PyDateTime_TZInfoType,
+
+ .TimeZone_UTC = (PyObject *)&utc_timezone,
+
+ .Date_FromDate = new_date_ex,
+ .DateTime_FromDateAndTime = new_datetime_ex,
+ .Time_FromTime = new_time_ex,
+ .Delta_FromDelta = new_delta_ex,
+ .TimeZone_FromTimeZone = new_timezone,
+ .DateTime_FromTimestamp = datetime_fromtimestamp,
+ .Date_FromTimestamp = datetime_date_fromtimestamp_capi,
+ .DateTime_FromDateAndTimeAndFold = new_datetime_ex2,
+ .Time_FromTimeAndFold = new_time_ex2,
+};
+
/* Get a new C API by calling this function.
* Clients get at C API via PyDateTime_IMPORT, defined in datetime.h.
*/
static inline PyDateTime_CAPI *
get_datetime_capi(void)
{
- datetime_state *st = get_datetime_state();
-
- PyDateTime_CAPI *capi = PyMem_Malloc(sizeof(PyDateTime_CAPI));
- if (capi == NULL) {
- PyErr_NoMemory();
- return NULL;
- }
- capi->DateType = st->date_type;
- capi->DateTimeType = st->datetime_type;
- capi->TimeType = st->time_type;
- capi->DeltaType = st->delta_type;
- capi->TZInfoType = st->tzinfo_type;
- capi->Date_FromDate = new_date_ex;
- capi->DateTime_FromDateAndTime = new_datetime_ex;
- capi->Time_FromTime = new_time_ex;
- capi->Delta_FromDelta = new_delta_ex;
- capi->TimeZone_FromTimeZone = new_timezone;
- capi->DateTime_FromTimestamp = datetime_fromtimestamp;
- capi->Date_FromTimestamp = datetime_date_fromtimestamp_capi;
- capi->DateTime_FromDateAndTimeAndFold = new_datetime_ex2;
- capi->Time_FromTimeAndFold = new_time_ex2;
- // Make sure this function is called after utc has
- // been initialized.
- assert(st->utc != NULL);
- capi->TimeZone_UTC = st->utc; // borrowed ref
- return capi;
-}
-
-static void
-datetime_destructor(PyObject *op)
-{
- void *ptr = PyCapsule_GetPointer(op, PyDateTime_CAPSULE_NAME);
- PyMem_Free(ptr);
+ return &capi;
}
static int
@@ -6955,8 +7004,7 @@ _datetime_exec(PyObject *module)
if (capi == NULL) {
goto error;
}
- PyObject *capsule = PyCapsule_New(capi, PyDateTime_CAPSULE_NAME,
- datetime_destructor);
+ PyObject *capsule = PyCapsule_New(capi, PyDateTime_CAPSULE_NAME, NULL);
if (capsule == NULL) {
PyMem_Free(capi);
goto error;
diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv
b/Tools/c-analyzer/cpython/globals-to-fix.tsv
index 8b6fe94e3afc52..711ae343a8d876 100644
--- a/Tools/c-analyzer/cpython/globals-to-fix.tsv
+++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv
@@ -304,6 +304,9 @@ Python/crossinterp_exceptions.h -
PyExc_InterpreterNotFoundError -
##-----------------------
## singletons
+Modules/_datetimemodule.c - zero_delta -
+Modules/_datetimemodule.c - utc_timezone -
+Modules/_datetimemodule.c - capi -
Objects/boolobject.c - _Py_FalseStruct -
Objects/boolobject.c - _Py_TrueStruct -
Objects/dictobject.c - empty_keys_struct -
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]