Skip to content

Commit c7153b0

Browse files
eustascopybara-github
authored andcommitted
add py free-threaded support
PiperOrigin-RevId: 830892275
1 parent 8e4d912 commit c7153b0

File tree

2 files changed

+103
-20
lines changed

2 files changed

+103
-20
lines changed

.github/workflows/build_test.yml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,18 +127,24 @@ jobs:
127127
c_compiler: clang
128128
cxx_compiler: clang++
129129

130-
- name: python3.10:clang
130+
- name: python3.14:clang
131131
build_system: python
132-
python_version: "3.10"
132+
python_version: "3.14"
133133
c_compiler: clang
134134
cxx_compiler: clang++
135135

136-
- name: python3.10-win
136+
- name: python3.14t:clang
137137
build_system: python
138-
python_version: "3.10"
138+
python_version: "3.14t"
139+
c_compiler: clang
140+
cxx_compiler: clang++
141+
142+
- name: python3.14-win
143+
build_system: python
144+
python_version: "3.14"
139145
# TODO: investigate why win-builds can't run tests
140146
py_setuptools_cmd: build_ext
141-
os: windows-2022
147+
os: windows-latest
142148

143149
- name: maven
144150
build_system: maven

python/_brotli.c

Lines changed: 92 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,32 @@
2222
#error "Only Python 3.10+ is supported"
2323
#endif
2424

25+
/*
26+
Decoder / encoder nature does not support concurrent access. Attempt to enter
27+
concurrently will result in an exception.
28+
29+
"Critical" parts used in prologues to ensure that only one thread enters.
30+
For consistency, we use them in epilogues as well. "Critical" is essential for
31+
free-threaded support and should be no-op in GIL environment (since methods are
32+
entered with GIL already held).
33+
34+
"Native" parts denote code that does not interact with Python and thus GIL could
35+
be released. "Native" are important for GIL environment (for performance), but
36+
should be no-op in free-threaded environment, since prologue/epilogue should
37+
guarantee exclusive access.
38+
*/
39+
#ifdef Py_GIL_DISABLED
40+
#define BROTLI_CRITICAL_START Py_BEGIN_CRITICAL_SECTION(self)
41+
#define BROTLI_CRITICAL_END Py_END_CRITICAL_SECTION()
42+
#define BROTLI_NATIVE_START
43+
#define BROTLI_NATIVE_END
44+
#else
45+
#define BROTLI_CRITICAL_START
46+
#define BROTLI_CRITICAL_END
47+
#define BROTLI_NATIVE_START Py_BEGIN_ALLOW_THREADS
48+
#define BROTLI_NATIVE_END Py_END_ALLOW_THREADS
49+
#endif
50+
2551
static const char kErrorAttr[] = "error";
2652
static const char kModuleAttr[] = "_module";
2753

@@ -475,7 +501,7 @@ static PyObject* compress_stream(PyBrotli_Compressor* self,
475501
goto error;
476502
}
477503

478-
Py_BEGIN_ALLOW_THREADS;
504+
BROTLI_NATIVE_START;
479505
while (1) {
480506
ok = BrotliEncoderCompressStream(enc, op, &available_in, &next_in,
481507
&buffer.avail_out, &buffer.next_out, NULL);
@@ -492,7 +518,7 @@ static PyObject* compress_stream(PyBrotli_Compressor* self,
492518
}
493519
break;
494520
}
495-
Py_END_ALLOW_THREADS;
521+
BROTLI_NATIVE_END;
496522

497523
if (oom) goto error;
498524
if (ok) {
@@ -522,7 +548,9 @@ static PyObject* brotli_Compressor_process(PyBrotli_Compressor* self,
522548
PyObject* ret = NULL;
523549
PyObject* input_object = NULL;
524550
Py_buffer input;
551+
int ok = 1;
525552

553+
BROTLI_CRITICAL_START;
526554
if (self->healthy == 0) {
527555
set_brotli_exception(self_type, kCompressUnhealthyError);
528556
return NULL;
@@ -531,6 +559,13 @@ static PyObject* brotli_Compressor_process(PyBrotli_Compressor* self,
531559
set_brotli_exception(self_type, kCompressConcurrentError);
532560
return NULL;
533561
}
562+
if (ok) {
563+
self->processing = 1;
564+
}
565+
BROTLI_CRITICAL_END;
566+
if (!ok) {
567+
return NULL;
568+
}
534569

535570
if (!PyArg_ParseTuple(args, "O:process", &input_object)) {
536571
return NULL;
@@ -539,49 +574,76 @@ static PyObject* brotli_Compressor_process(PyBrotli_Compressor* self,
539574
return NULL;
540575
}
541576

542-
self->processing = 1;
543577
ret = compress_stream(self, BROTLI_OPERATION_PROCESS, (uint8_t*)input.buf,
544578
input.len);
545579
PyBuffer_Release(&input);
580+
581+
BROTLI_CRITICAL_START;
546582
self->processing = 0;
583+
BROTLI_CRITICAL_END;
584+
547585
return ret;
548586
}
549587

550588
static PyObject* brotli_Compressor_flush(PyBrotli_Compressor* self) {
551589
PyObject* self_type = (PyObject*)Py_TYPE((PyObject*)self);
552590
PyObject* ret = NULL;
591+
int ok = 1;
553592

593+
BROTLI_CRITICAL_START;
554594
if (self->healthy == 0) {
555595
set_brotli_exception(self_type, kCompressUnhealthyError);
556-
return NULL;
596+
ok = 0;
557597
}
558598
if (self->processing != 0) {
559599
set_brotli_exception(self_type, kCompressConcurrentError);
600+
ok = 0;
601+
}
602+
if (ok) {
603+
self->processing = 1;
604+
}
605+
BROTLI_CRITICAL_END;
606+
if (!ok) {
560607
return NULL;
561608
}
562609

563-
self->processing = 1;
564610
ret = compress_stream(self, BROTLI_OPERATION_FLUSH, NULL, 0);
611+
612+
BROTLI_CRITICAL_START;
565613
self->processing = 0;
614+
BROTLI_CRITICAL_END;
615+
566616
return ret;
567617
}
568618

569619
static PyObject* brotli_Compressor_finish(PyBrotli_Compressor* self) {
570620
PyObject* self_type = (PyObject*)Py_TYPE((PyObject*)self);
571621
PyObject* ret = NULL;
622+
int ok = 1;
572623

624+
BROTLI_CRITICAL_START;
573625
if (self->healthy == 0) {
574626
set_brotli_exception(self_type, kCompressUnhealthyError);
575-
return NULL;
627+
ok = 0;
576628
}
577629
if (self->processing != 0) {
578630
set_brotli_exception(self_type, kCompressConcurrentError);
631+
ok = 0;
632+
}
633+
if (ok) {
634+
self->processing = 1;
635+
}
636+
BROTLI_CRITICAL_END;
637+
if (!ok) {
579638
return NULL;
580639
}
581640

582-
self->processing = 1;
583641
ret = compress_stream(self, BROTLI_OPERATION_FINISH, NULL, 0);
642+
643+
BROTLI_CRITICAL_START;
584644
self->processing = 0;
645+
BROTLI_CRITICAL_END;
646+
585647
if (ret != NULL) {
586648
assert(BrotliEncoderIsFinished(self->enc));
587649
}
@@ -664,13 +726,22 @@ static PyObject* brotli_Decompressor_process(PyBrotli_Decompressor* self,
664726
uint8_t* new_tail = NULL;
665727
size_t new_tail_length = 0;
666728
int oom = 0;
729+
int ok = 1;
667730

731+
BROTLI_CRITICAL_START;
668732
if (self->healthy == 0) {
669733
set_brotli_exception(self_type, kDecompressUnhealthyError);
670-
return NULL;
734+
ok = 0;
671735
}
672736
if (self->processing != 0) {
673737
set_brotli_exception(self_type, kDecompressConcurrentError);
738+
ok = 0;
739+
}
740+
if (ok) {
741+
self->processing = 1;
742+
}
743+
BROTLI_CRITICAL_END;
744+
if (!ok) {
674745
return NULL;
675746
}
676747

@@ -683,7 +754,6 @@ static PyObject* brotli_Decompressor_process(PyBrotli_Decompressor* self,
683754
}
684755

685756
Buffer_Init(&buffer);
686-
self->processing = 1;
687757

688758
if (self->unconsumed_data_length > 0) {
689759
if (input.len > 0) {
@@ -703,7 +773,7 @@ static PyObject* brotli_Decompressor_process(PyBrotli_Decompressor* self,
703773
goto finally;
704774
}
705775

706-
Py_BEGIN_ALLOW_THREADS;
776+
BROTLI_NATIVE_START;
707777
while (1) {
708778
result = BrotliDecoderDecompressStream(self->dec, &avail_in, &next_in,
709779
&buffer.avail_out, &buffer.next_out,
@@ -721,7 +791,7 @@ static PyObject* brotli_Decompressor_process(PyBrotli_Decompressor* self,
721791
}
722792
break;
723793
}
724-
Py_END_ALLOW_THREADS;
794+
BROTLI_NATIVE_END;
725795

726796
if (oom) {
727797
goto finally;
@@ -759,17 +829,22 @@ static PyObject* brotli_Decompressor_process(PyBrotli_Decompressor* self,
759829
}
760830
PyBuffer_Release(&input);
761831
Buffer_Cleanup(&buffer);
832+
833+
ok = (PyErr_Occurred() == NULL) ? 1 : 0;
834+
BROTLI_CRITICAL_START;
762835
if (self->unconsumed_data) {
763836
free(self->unconsumed_data);
764837
self->unconsumed_data = NULL;
765838
}
766839
self->unconsumed_data = new_tail;
767840
self->unconsumed_data_length = new_tail_length;
768-
if (PyErr_Occurred() != NULL) {
841+
if (!ok) {
769842
assert(ret == NULL);
770843
self->healthy = 0;
771844
}
772845
self->processing = 0;
846+
BROTLI_CRITICAL_END;
847+
773848
return ret;
774849
}
775850

@@ -848,7 +923,7 @@ static PyObject* brotli_decompress(PyObject* m, PyObject* args,
848923
goto finally;
849924
}
850925

851-
Py_BEGIN_ALLOW_THREADS;
926+
BROTLI_NATIVE_START;
852927
while (1) {
853928
result = BrotliDecoderDecompressStream(
854929
state, &available_in, &next_in, &buffer.avail_out, &buffer.next_out, 0);
@@ -862,7 +937,7 @@ static PyObject* brotli_decompress(PyObject* m, PyObject* args,
862937
}
863938
break;
864939
}
865-
Py_END_ALLOW_THREADS;
940+
BROTLI_NATIVE_END;
866941

867942
if (oom) {
868943
goto finally;
@@ -1003,7 +1078,9 @@ static PyMethodDef brotli_methods[] = {
10031078

10041079
static PyModuleDef_Slot brotli_mod_slots[] = {
10051080
{Py_mod_exec, brotli_init_mod},
1006-
#if (PY_MAJOR_VERSION > 3) || (PY_MINOR_VERSION >= 12)
1081+
#ifdef Py_GIL_DISABLED
1082+
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
1083+
#elif (PY_MAJOR_VERSION > 3) || (PY_MINOR_VERSION >= 12)
10071084
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
10081085
#endif
10091086
{0, NULL}};

0 commit comments

Comments
 (0)