gh-117398: Statically Allocate the Datetime C-API (GH-119472)

This commit is contained in:
Eric Snow 2024-05-23 15:15:52 -04:00 committed by GitHub
parent 6e012ced6c
commit b30d30c747
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 89 additions and 35 deletions

View File

@ -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.

View File

@ -1178,6 +1178,8 @@ new_time_subclass_fold_ex(int hour, int minute, int second, int usecond,
return t; return t;
} }
static PyDateTime_Delta * look_up_delta(int, int, int, PyTypeObject *);
/* Create a timedelta instance. Normalize the members iff normalize is /* Create a timedelta instance. Normalize the members iff normalize is
* true. Passing false is a speed optimization, if you know for sure * true. Passing false is a speed optimization, if you know for sure
* that seconds and microseconds are already in their proper ranges. In any * 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) if (check_delta_day_range(days) < 0)
return NULL; 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)); self = (PyDateTime_Delta *) (type->tp_alloc(type, 0));
if (self != NULL) { if (self != NULL) {
self->hashcode = -1; self->hashcode = -1;
@ -1219,6 +1227,8 @@ typedef struct
PyObject *name; PyObject *name;
} PyDateTime_TimeZone; } PyDateTime_TimeZone;
static PyDateTime_TimeZone * look_up_timezone(PyObject *offset, PyObject *name);
/* Create new timezone instance checking offset range. This /* Create new timezone instance checking offset range. This
function does not check the name argument. Caller must assure function does not check the name argument. Caller must assure
that offset is a timedelta instance and name is either NULL 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(PyDelta_Check(offset));
assert(name == NULL || PyUnicode_Check(name)); 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)); self = (PyDateTime_TimeZone *)(type->tp_alloc(type, 0));
if (self == NULL) { if (self == NULL) {
return NULL; return NULL;
@ -2892,6 +2908,25 @@ static PyTypeObject PyDateTime_DeltaType = {
0, /* tp_free */ 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. * PyDateTime_Date implementation.
*/ */
@ -4184,6 +4219,23 @@ static PyTypeObject PyDateTime_TimeZoneType = {
timezone_new, /* tp_new */ 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. * PyDateTime_Time implementation.
*/ */
@ -6719,45 +6771,42 @@ static PyMethodDef module_methods[] = {
{NULL, NULL} {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. /* Get a new C API by calling this function.
* Clients get at C API via PyDateTime_IMPORT, defined in datetime.h. * Clients get at C API via PyDateTime_IMPORT, defined in datetime.h.
*/ */
static inline PyDateTime_CAPI * static inline PyDateTime_CAPI *
get_datetime_capi(void) get_datetime_capi(void)
{ {
datetime_state *st = get_datetime_state(); return &capi;
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);
} }
static int static int
@ -6955,8 +7004,7 @@ _datetime_exec(PyObject *module)
if (capi == NULL) { if (capi == NULL) {
goto error; goto error;
} }
PyObject *capsule = PyCapsule_New(capi, PyDateTime_CAPSULE_NAME, PyObject *capsule = PyCapsule_New(capi, PyDateTime_CAPSULE_NAME, NULL);
datetime_destructor);
if (capsule == NULL) { if (capsule == NULL) {
PyMem_Free(capi); PyMem_Free(capi);
goto error; goto error;

View File

@ -304,6 +304,9 @@ Python/crossinterp_exceptions.h - PyExc_InterpreterNotFoundError -
##----------------------- ##-----------------------
## singletons ## singletons
Modules/_datetimemodule.c - zero_delta -
Modules/_datetimemodule.c - utc_timezone -
Modules/_datetimemodule.c - capi -
Objects/boolobject.c - _Py_FalseStruct - Objects/boolobject.c - _Py_FalseStruct -
Objects/boolobject.c - _Py_TrueStruct - Objects/boolobject.c - _Py_TrueStruct -
Objects/dictobject.c - empty_keys_struct - Objects/dictobject.c - empty_keys_struct -

Can't render this file because it has a wrong number of fields in line 4.