Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-117398: Statically Allocate the Datetime C-API #119472

Merged
merged 6 commits into from
May 23, 2024
Merged
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
Original file line number Diff line number Diff line change
@@ -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.
118 changes: 83 additions & 35 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions Tools/c-analyzer/cpython/globals-to-fix.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -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 -
Expand Down
Loading