diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a85bf05..7862e1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,12 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['3.10', '3.11', '3.12', '3.13', 'pypy-3.10'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.13t', 'pypy-3.10'] + exclude: + # TODO: Reenable this once there's a setuptools release that sets Py_GIL_DISABLED + # correctly on Windows + - os: windows-latest + python-version: 3.13t steps: - uses: actions/checkout@v4 - name: Get history and tags for SCM versioning to work @@ -86,7 +91,7 @@ jobs: git fetch --prune --unshallow git fetch --depth=1 origin +refs/tags/*:refs/tags/* - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: Quansight-Labs/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e5ce975..d3d2c77 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,7 +106,9 @@ jobs: - name: Build wheels if: matrix.manylinux_version == 'manylinux2014' env: - CIBW_BUILD: "cp311-* cp312-* cp313-* pp310-*" + CIBW_BUILD: "cp311-* cp312-* cp313-* cp313t-* pp310-*" + CIBW_SKIP: "cp313t-win*" + CIBW_ENABLE: python-freethreading CIBW_ARCHS_MACOS: x86_64 universal2 arm64 CIBW_ARCHS_LINUX: ${{ matrix.archs }} CIBW_ARCHS_WINDOWS: auto64 diff --git a/py/src/constraint.cpp b/py/src/constraint.cpp index 7db2959..f2ac2a2 100644 --- a/py/src/constraint.cpp +++ b/py/src/constraint.cpp @@ -44,8 +44,12 @@ Constraint_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) cn->expression = reduce_expression(pyexpr); if (!cn->expression) return 0; + + ACQUIRE_GLOBAL_LOCK(); kiwi::Expression expr(convert_to_kiwi_expression(cn->expression)); new (&cn->constraint) kiwi::Constraint(expr, op, strength); + RELEASE_GLOBAL_LOCK(); + return pycn.release(); } @@ -68,7 +72,9 @@ void Constraint_dealloc(Constraint *self) { PyObject_GC_UnTrack(self); Constraint_clear(self); + ACQUIRE_GLOBAL_LOCK(); self->constraint.~Constraint(); + RELEASE_GLOBAL_LOCK(); Py_TYPE(self)->tp_free(pyobject_cast(self)); } @@ -83,11 +89,21 @@ Constraint_repr(Constraint *self) PyObject *item = PyTuple_GET_ITEM(expr->terms, i); Term *term = reinterpret_cast(item); stream << term->coefficient << " * "; - stream << reinterpret_cast(term->variable)->variable.name(); + ACQUIRE_GLOBAL_LOCK(); + std::string name = reinterpret_cast(term->variable)->variable.name(); + RELEASE_GLOBAL_LOCK(); + stream << name; stream << " + "; } stream << expr->constant; - switch (self->constraint.op()) + + ACQUIRE_GLOBAL_LOCK(); + kiwi::RelationalOperator op = self->constraint.op(); + double strength = self->constraint.strength(); + bool violated = self->constraint.violated(); + RELEASE_GLOBAL_LOCK(); + + switch (op) { case kiwi::OP_EQ: stream << " == 0"; @@ -99,8 +115,8 @@ Constraint_repr(Constraint *self) stream << " >= 0"; break; } - stream << " | strength = " << self->constraint.strength(); - if (self->constraint.violated()) + stream << " | strength = " << strength; + if (violated) { stream << " (VIOLATED)"; } @@ -117,7 +133,12 @@ PyObject * Constraint_op(Constraint *self) { PyObject *res = 0; - switch (self->constraint.op()) + + ACQUIRE_GLOBAL_LOCK(); + kiwi::RelationalOperator op = self->constraint.op(); + RELEASE_GLOBAL_LOCK(); + + switch (op) { case kiwi::OP_EQ: res = PyUnicode_FromString("=="); @@ -135,13 +156,20 @@ Constraint_op(Constraint *self) PyObject * Constraint_strength(Constraint *self) { - return PyFloat_FromDouble(self->constraint.strength()); + ACQUIRE_GLOBAL_LOCK(); + double strength = self->constraint.strength(); + RELEASE_GLOBAL_LOCK(); + return PyFloat_FromDouble(strength); } PyObject * Constraint_violated(Constraint *self) { - if (self->constraint.violated()) { + ACQUIRE_GLOBAL_LOCK(); + bool violated = self->constraint.violated(); + RELEASE_GLOBAL_LOCK(); + + if (violated) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; @@ -162,7 +190,11 @@ Constraint_or(PyObject *pyoldcn, PyObject *value) Constraint *oldcn = reinterpret_cast(pyoldcn); Constraint *newcn = reinterpret_cast(pynewcn); newcn->expression = cppy::incref(oldcn->expression); + + ACQUIRE_GLOBAL_LOCK(); new (&newcn->constraint) kiwi::Constraint(oldcn->constraint, strength); + RELEASE_GLOBAL_LOCK(); + return pynewcn; } diff --git a/py/src/expression.cpp b/py/src/expression.cpp index c9e969a..a53cc1f 100644 --- a/py/src/expression.cpp +++ b/py/src/expression.cpp @@ -89,7 +89,10 @@ Expression_repr( Expression* self ) PyObject* item = PyTuple_GET_ITEM( self->terms, i ); Term* term = reinterpret_cast( item ); stream << term->coefficient << " * "; - stream << reinterpret_cast( term->variable )->variable.name(); + ACQUIRE_GLOBAL_LOCK(); + std::string name = reinterpret_cast( term->variable )->variable.name(); + RELEASE_GLOBAL_LOCK(); + stream << name; stream << " + "; } stream << self->constant; @@ -121,7 +124,10 @@ Expression_value( Expression* self ) PyObject* item = PyTuple_GET_ITEM( self->terms, i ); Term* term = reinterpret_cast( item ); Variable* pyvar = reinterpret_cast( term->variable ); - result += term->coefficient * pyvar->variable.value(); + ACQUIRE_GLOBAL_LOCK(); + double value = pyvar->variable.value(); + RELEASE_GLOBAL_LOCK(); + result += term->coefficient * value; } return PyFloat_FromDouble( result ); } diff --git a/py/src/kiwisolver.cpp b/py/src/kiwisolver.cpp index 9eb3f56..c6f2d55 100644 --- a/py/src/kiwisolver.cpp +++ b/py/src/kiwisolver.cpp @@ -5,11 +5,18 @@ | | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ +#include #include #include #include "types.h" #include "version.h" +namespace kiwisolver +{ + +std::recursive_mutex global_lock; + +} namespace { @@ -162,6 +169,9 @@ kiwisolver_methods[] = { PyModuleDef_Slot kiwisolver_slots[] = { {Py_mod_exec, reinterpret_cast( kiwi_modexec ) }, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif {0, NULL} }; diff --git a/py/src/solver.cpp b/py/src/solver.cpp index 0dc0fdf..942bba4 100644 --- a/py/src/solver.cpp +++ b/py/src/solver.cpp @@ -26,7 +26,9 @@ Solver_new( PyTypeObject* type, PyObject* args, PyObject* kwargs ) if( !pysolver ) return 0; Solver* self = reinterpret_cast( pysolver ); + ACQUIRE_GLOBAL_LOCK(); new( &self->solver ) kiwi::Solver(); + RELEASE_GLOBAL_LOCK(); return pysolver; } @@ -34,7 +36,9 @@ Solver_new( PyTypeObject* type, PyObject* args, PyObject* kwargs ) void Solver_dealloc( Solver* self ) { + ACQUIRE_GLOBAL_LOCK(); self->solver.~Solver(); + RELEASE_GLOBAL_LOCK(); Py_TYPE( self )->tp_free( pyobject_cast( self ) ); } @@ -47,15 +51,19 @@ Solver_addConstraint( Solver* self, PyObject* other ) Constraint* cn = reinterpret_cast( other ); try { + ACQUIRE_GLOBAL_LOCK(); self->solver.addConstraint( cn->constraint ); + RELEASE_GLOBAL_LOCK(); } catch( const kiwi::DuplicateConstraint& ) { + RELEASE_GLOBAL_LOCK(); PyErr_SetObject( DuplicateConstraint, other ); return 0; } catch( const kiwi::UnsatisfiableConstraint& ) { + RELEASE_GLOBAL_LOCK(); PyErr_SetObject( UnsatisfiableConstraint, other ); return 0; } @@ -71,10 +79,13 @@ Solver_removeConstraint( Solver* self, PyObject* other ) Constraint* cn = reinterpret_cast( other ); try { + ACQUIRE_GLOBAL_LOCK(); self->solver.removeConstraint( cn->constraint ); + RELEASE_GLOBAL_LOCK(); } catch( const kiwi::UnknownConstraint& ) { + RELEASE_GLOBAL_LOCK(); PyErr_SetObject( UnknownConstraint, other ); return 0; } @@ -88,7 +99,12 @@ Solver_hasConstraint( Solver* self, PyObject* other ) if( !Constraint::TypeCheck( other ) ) return cppy::type_error( other, "Constraint" ); Constraint* cn = reinterpret_cast( other ); - return cppy::incref( self->solver.hasConstraint( cn->constraint ) ? Py_True : Py_False ); + + ACQUIRE_GLOBAL_LOCK(); + bool hasConstraint = self->solver.hasConstraint( cn->constraint ); + RELEASE_GLOBAL_LOCK(); + + return cppy::incref( hasConstraint ? Py_True : Py_False ); } @@ -107,15 +123,19 @@ Solver_addEditVariable( Solver* self, PyObject* args ) Variable* var = reinterpret_cast( pyvar ); try { + ACQUIRE_GLOBAL_LOCK(); self->solver.addEditVariable( var->variable, strength ); + RELEASE_GLOBAL_LOCK(); } catch( const kiwi::DuplicateEditVariable& ) { + RELEASE_GLOBAL_LOCK(); PyErr_SetObject( DuplicateEditVariable, pyvar ); return 0; } catch( const kiwi::BadRequiredStrength& e ) { + RELEASE_GLOBAL_LOCK(); PyErr_SetString( BadRequiredStrength, e.what() ); return 0; } @@ -131,10 +151,13 @@ Solver_removeEditVariable( Solver* self, PyObject* other ) Variable* var = reinterpret_cast( other ); try { + ACQUIRE_GLOBAL_LOCK(); self->solver.removeEditVariable( var->variable ); + RELEASE_GLOBAL_LOCK(); } catch( const kiwi::UnknownEditVariable& ) { + RELEASE_GLOBAL_LOCK(); PyErr_SetObject( UnknownEditVariable, other ); return 0; } @@ -148,7 +171,10 @@ Solver_hasEditVariable( Solver* self, PyObject* other ) if( !Variable::TypeCheck( other ) ) return cppy::type_error( other, "Variable" ); Variable* var = reinterpret_cast( other ); - return cppy::incref( self->solver.hasEditVariable( var->variable ) ? Py_True : Py_False ); + ACQUIRE_GLOBAL_LOCK(); + bool hasEditVariable = self->solver.hasEditVariable( var->variable ); + RELEASE_GLOBAL_LOCK(); + return cppy::incref( hasEditVariable ? Py_True : Py_False ); } @@ -167,10 +193,13 @@ Solver_suggestValue( Solver* self, PyObject* args ) Variable* var = reinterpret_cast( pyvar ); try { + ACQUIRE_GLOBAL_LOCK(); self->solver.suggestValue( var->variable, value ); + RELEASE_GLOBAL_LOCK(); } catch( const kiwi::UnknownEditVariable& ) { + RELEASE_GLOBAL_LOCK(); PyErr_SetObject( UnknownEditVariable, pyvar ); return 0; } @@ -181,7 +210,9 @@ Solver_suggestValue( Solver* self, PyObject* args ) PyObject* Solver_updateVariables( Solver* self ) { + ACQUIRE_GLOBAL_LOCK(); self->solver.updateVariables(); + RELEASE_GLOBAL_LOCK(); Py_RETURN_NONE; } @@ -189,7 +220,9 @@ Solver_updateVariables( Solver* self ) PyObject* Solver_reset( Solver* self ) { + ACQUIRE_GLOBAL_LOCK(); self->solver.reset(); + RELEASE_GLOBAL_LOCK(); Py_RETURN_NONE; } @@ -197,7 +230,10 @@ Solver_reset( Solver* self ) PyObject* Solver_dump( Solver* self ) { - cppy::ptr dump_str( PyUnicode_FromString( self->solver.dumps().c_str() ) ); + ACQUIRE_GLOBAL_LOCK(); + std::string dumps = self->solver.dumps(); + RELEASE_GLOBAL_LOCK(); + cppy::ptr dump_str( PyUnicode_FromString( dumps.c_str() ) ); PyObject_Print( dump_str.get(), stdout, 0 ); Py_RETURN_NONE; } @@ -205,7 +241,10 @@ Solver_dump( Solver* self ) PyObject* Solver_dumps( Solver* self ) { - return PyUnicode_FromString( self->solver.dumps().c_str() ); + ACQUIRE_GLOBAL_LOCK(); + std::string dumps = self->solver.dumps(); + RELEASE_GLOBAL_LOCK(); + return PyUnicode_FromString( dumps.c_str() ); } static PyMethodDef diff --git a/py/src/symbolics.h b/py/src/symbolics.h index 0f87212..7206f31 100644 --- a/py/src/symbolics.h +++ b/py/src/symbolics.h @@ -579,8 +579,10 @@ PyObject* makecn( T first, U second, kiwi::RelationalOperator op ) cn->expression = reduce_expression( pyexpr.get() ); if( !cn->expression ) return 0; + ACQUIRE_GLOBAL_LOCK(); kiwi::Expression expr( convert_to_kiwi_expression( cn->expression ) ); new( &cn->constraint ) kiwi::Constraint( expr, op, kiwi::strength::required ); + RELEASE_GLOBAL_LOCK(); return pycn.release(); } diff --git a/py/src/term.cpp b/py/src/term.cpp index bb26869..69fbab8 100644 --- a/py/src/term.cpp +++ b/py/src/term.cpp @@ -78,7 +78,10 @@ Term_repr( Term* self ) { std::stringstream stream; stream << self->coefficient << " * "; - stream << reinterpret_cast( self->variable )->variable.name(); + ACQUIRE_GLOBAL_LOCK(); + std::string name = reinterpret_cast( self->variable )->variable.name(); + RELEASE_GLOBAL_LOCK(); + stream << name; return PyUnicode_FromString( stream.str().c_str() ); } @@ -101,7 +104,10 @@ PyObject* Term_value( Term* self ) { Variable* pyvar = reinterpret_cast( self->variable ); - return PyFloat_FromDouble( self->coefficient * pyvar->variable.value() ); + ACQUIRE_GLOBAL_LOCK(); + double value = pyvar->variable.value(); + RELEASE_GLOBAL_LOCK(); + return PyFloat_FromDouble( self->coefficient * value ); } diff --git a/py/src/util.h b/py/src/util.h index 0eb18dc..ba6a940 100644 --- a/py/src/util.h +++ b/py/src/util.h @@ -7,6 +7,7 @@ |----------------------------------------------------------------------------*/ #pragma once #include +#include #include #include #include @@ -16,6 +17,20 @@ namespace kiwisolver { +#ifdef Py_GIL_DISABLED +extern std::recursive_mutex global_lock; +#define ACQUIRE_GLOBAL_LOCK() global_lock.lock() +#define RELEASE_GLOBAL_LOCK() global_lock.unlock() +#else +#define ACQUIRE_GLOBAL_LOCK() +#define RELEASE_GLOBAL_LOCK() +#endif + +#ifndef Py_BEGIN_CRITICAL_SECTION +#define Py_BEGIN_CRITICAL_SECTION(op) { +#define Py_END_CRITICAL_SECTION() } +#endif + inline bool convert_to_double( PyObject* obj, double& out ) { @@ -171,9 +186,15 @@ convert_to_kiwi_expression( PyObject* pyexpr ) // pyexpr must be an Expression PyObject* item = PyTuple_GET_ITEM( expr->terms, i ); Term* term = reinterpret_cast( item ); Variable* var = reinterpret_cast( term->variable ); - kterms.push_back( kiwi::Term( var->variable, term->coefficient ) ); + ACQUIRE_GLOBAL_LOCK(); + kiwi::Term t( var->variable, term->coefficient ); + RELEASE_GLOBAL_LOCK(); + kterms.push_back( t ); } - return kiwi::Expression( kterms, expr->constant ); + ACQUIRE_GLOBAL_LOCK(); + kiwi::Expression expression( kterms, expr->constant ); + RELEASE_GLOBAL_LOCK(); + return expression; } diff --git a/py/src/variable.cpp b/py/src/variable.cpp index 6c9593b..039666f 100644 --- a/py/src/variable.cpp +++ b/py/src/variable.cpp @@ -46,11 +46,15 @@ Variable_new( PyTypeObject* type, PyObject* args, PyObject* kwargs ) std::string c_name; if( !convert_pystr_to_str(name, c_name) ) return 0; // LCOV_EXCL_LINE + ACQUIRE_GLOBAL_LOCK(); new( &self->variable ) kiwi::Variable( c_name ); + RELEASE_GLOBAL_LOCK(); } else { + ACQUIRE_GLOBAL_LOCK(); new( &self->variable ) kiwi::Variable(); + RELEASE_GLOBAL_LOCK(); } return pyvar.release(); @@ -81,7 +85,9 @@ Variable_dealloc( Variable* self ) { PyObject_GC_UnTrack( self ); Variable_clear( self ); + ACQUIRE_GLOBAL_LOCK(); self->variable.~Variable(); + RELEASE_GLOBAL_LOCK(); Py_TYPE( self )->tp_free( pyobject_cast( self ) ); } @@ -89,14 +95,20 @@ Variable_dealloc( Variable* self ) PyObject* Variable_repr( Variable* self ) { - return PyUnicode_FromString( self->variable.name().c_str() ); + ACQUIRE_GLOBAL_LOCK(); + std::string name = self->variable.name(); + RELEASE_GLOBAL_LOCK(); + return PyUnicode_FromString( name.c_str() ); } PyObject* Variable_name( Variable* self ) { - return PyUnicode_FromString( self->variable.name().c_str() ); + ACQUIRE_GLOBAL_LOCK(); + std::string name = self->variable.name(); + RELEASE_GLOBAL_LOCK(); + return PyUnicode_FromString( name.c_str() ); } @@ -108,13 +120,17 @@ Variable_setName( Variable* self, PyObject* pystr ) std::string str; if( !convert_pystr_to_str( pystr, str ) ) return 0; - self->variable.setName( str ); + + ACQUIRE_GLOBAL_LOCK(); + self->variable.setName( str ); + RELEASE_GLOBAL_LOCK(); + Py_RETURN_NONE; } PyObject* -Variable_context( Variable* self ) +Variable_context_locked( Variable* self ) { if( self->context ) return cppy::incref( self->context ); @@ -123,14 +139,32 @@ Variable_context( Variable* self ) PyObject* -Variable_setContext( Variable* self, PyObject* value ) +Variable_context( Variable* self ) +{ + PyObject* context; + Py_BEGIN_CRITICAL_SECTION(self); + context = Variable_context_locked(self); + Py_END_CRITICAL_SECTION(); + return context; +} + + +void +Variable_setContext_locked( Variable* self, PyObject* value ) { if( value != self->context ) { - PyObject* temp = self->context; - self->context = cppy::incref( value ); - Py_XDECREF( temp ); + Py_XSETREF(self->context, cppy::incref( value )); } +} + + +PyObject* +Variable_setContext( Variable* self, PyObject* value ) +{ + Py_BEGIN_CRITICAL_SECTION(self); + Variable_setContext_locked(self, value); + Py_END_CRITICAL_SECTION(); Py_RETURN_NONE; } @@ -138,7 +172,10 @@ Variable_setContext( Variable* self, PyObject* value ) PyObject* Variable_value( Variable* self ) { - return PyFloat_FromDouble( self->variable.value() ); + ACQUIRE_GLOBAL_LOCK(); + double value = self->variable.value(); + RELEASE_GLOBAL_LOCK(); + return PyFloat_FromDouble( value ); }