@@ -142,6 +142,7 @@ typedef struct {
142142 */
143143 char x_buffer [BUFSIZE ]; /* buffer for Py_buffer */
144144 Py_ssize_t x_exports ; /* how many buffer are exported */
145+ PyThread_type_lock x_lock ; /* Lock for thread safety */
145146} XxoObject_Data ;
146147
147148// Get the `XxoObject_Data` structure for a given instance of our type.
@@ -204,6 +205,15 @@ Xxo_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
204205 Py_DECREF (self );
205206 return NULL ;
206207 }
208+
209+ // Set up a per-instance lock for thread safety.
210+ xxo_data -> x_lock = PyThread_allocate_lock ();
211+ if (xxo_data -> x_lock == NULL ) {
212+ PyErr_SetString (PyExc_SystemError , "could not allocate lock" );
213+ Py_DECREF (self );
214+ return NULL ;
215+ }
216+
207217 xxo_data -> x_attr = NULL ;
208218 memset (xxo_data -> x_buffer , 0 , BUFSIZE );
209219 xxo_data -> x_exports = 0 ;
@@ -257,13 +267,23 @@ Xxo_finalize(PyObject *self)
257267}
258268
259269// dealloc: drop all remaining references and free memory
260- // This function must preserve currently raised exception, if any.
261270static void
262271Xxo_dealloc (PyObject * self )
263272{
273+ // This function must preserve currently raised exception, if any.
264274 PyObject * exc = PyErr_GetRaisedException ();
275+
265276 PyObject_GC_UnTrack (self );
266277 Xxo_finalize (self );
278+
279+ // Free x_lock. This is not a Python object so it cannot
280+ // form reference cycles, so it's only handled here, not in
281+ // the traverse, clear and finalize handlers.
282+ XxoObject_Data * data = Xxo_get_data (self );
283+ if (data && data -> x_lock ) {
284+ PyThread_free_lock (data -> x_lock );
285+ }
286+
267287 PyTypeObject * tp = Py_TYPE (self );
268288 freefunc free = PyType_GetSlot (tp , Py_tp_free );
269289 free (self );
@@ -314,13 +334,23 @@ Xxo_setattro(PyObject *self, PyObject *name, PyObject *v)
314334 if (data == NULL ) {
315335 return -1 ;
316336 }
337+
338+ // If the attribute dict is not created yet, make one.
339+ // This needs to be protected by a lock to avoid another thread
340+ // creating a duplicate dict.
341+ PyThread_acquire_lock (data -> x_lock , 1 );
317342 if (data -> x_attr == NULL ) {
318343 // prepare the attribute dict
319344 data -> x_attr = PyDict_New ();
345+ PyThread_release_lock (data -> x_lock );
320346 if (data -> x_attr == NULL ) {
321347 return -1 ;
322348 }
323349 }
350+ else {
351+ PyThread_release_lock (data -> x_lock );
352+ }
353+
324354 if (v == NULL ) {
325355 // delete an attribute
326356 int rv = PyDict_DelItem (data -> x_attr , name );
@@ -589,6 +619,11 @@ xx_free(void *module)
589619{
590620 // allow xx_modexec to omit calling xx_clear on error
591621 (void )xx_clear ((PyObject * )module );
622+
623+ xx_state * state = PyModule_GetState (module );
624+ if (state == NULL ) {
625+ return ;
626+ }
592627}
593628
594629// Information that CPython uses to prevent loading incompatible extenstions
@@ -625,9 +660,6 @@ static PyModuleDef_Slot xx_slots[] = {
625660 * Without this slot, free-threaded builds of CPython will enable
626661 * the GIL when this module is loaded.
627662 */
628- // TODO: This is not quite true yet: there is a race in Xxo_setattro,
629- // for example.
630- // We include this for CPython's internal testing.
631663 {Py_mod_gil , Py_MOD_GIL_NOT_USED },
632664
633665 {0 , NULL }
0 commit comments