Skip to content

Commit 43364c1

Browse files
authored
[mypyc] Enable free threading when compiling multiple modules (#19541)
Previously we used multi-phase initialization to enable free threading on Python builds that support it, but only if a single module was compiled in a group. Implements it also for multiple modules in a group. Add support for multi-phase initialization in module shims and the shared library. It's still only used on free-threaded builds, and we fall back to the old approach on other Python versions/builds. This enables compiling mypy and mypyc on free-threaded Python builds. At least almost all mypy and mypyc tests now pass when compiled and on 3.14.0b4 with free threading (only tested on macOS so far).
1 parent 5750690 commit 43364c1

File tree

3 files changed

+146
-39
lines changed

3 files changed

+146
-39
lines changed

mypyc/build.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from mypy.util import write_junit_xml
3737
from mypyc.annotate import generate_annotated_html
3838
from mypyc.codegen import emitmodule
39-
from mypyc.common import RUNTIME_C_FILES, shared_lib_name
39+
from mypyc.common import IS_FREE_THREADED, RUNTIME_C_FILES, shared_lib_name
4040
from mypyc.errors import Errors
4141
from mypyc.ir.pprint import format_modules
4242
from mypyc.namegen import exported_name
@@ -176,9 +176,15 @@ def generate_c_extension_shim(
176176
cname = "%s.c" % full_module_name.replace(".", os.sep)
177177
cpath = os.path.join(dir_name, cname)
178178

179+
if IS_FREE_THREADED:
180+
# We use multi-phase init in free-threaded builds to enable free threading.
181+
shim_name = "module_shim_no_gil_multiphase.tmpl"
182+
else:
183+
shim_name = "module_shim.tmpl"
184+
179185
# We load the C extension shim template from a file.
180186
# (So that the file could be reused as a bazel template also.)
181-
with open(os.path.join(include_dir(), "module_shim.tmpl")) as f:
187+
with open(os.path.join(include_dir(), shim_name)) as f:
182188
shim_template = f.read()
183189

184190
write_file(

mypyc/codegen/emitmodule.py

Lines changed: 97 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -784,28 +784,15 @@ def generate_shared_lib_init(self, emitter: Emitter) -> None:
784784
assert self.group_name is not None
785785

786786
emitter.emit_line()
787+
788+
short_name = shared_lib_name(self.group_name).split(".")[-1]
789+
787790
emitter.emit_lines(
788-
"PyMODINIT_FUNC PyInit_{}(void)".format(
789-
shared_lib_name(self.group_name).split(".")[-1]
790-
),
791+
f"static int exec_{short_name}(PyObject *module)",
791792
"{",
792-
(
793-
'static PyModuleDef def = {{ PyModuleDef_HEAD_INIT, "{}", NULL, -1, NULL, NULL }};'.format(
794-
shared_lib_name(self.group_name)
795-
)
796-
),
797793
"int res;",
798794
"PyObject *capsule;",
799795
"PyObject *tmp;",
800-
"static PyObject *module;",
801-
"if (module) {",
802-
"Py_INCREF(module);",
803-
"return module;",
804-
"}",
805-
"module = PyModule_Create(&def);",
806-
"if (!module) {",
807-
"goto fail;",
808-
"}",
809796
"",
810797
)
811798

@@ -827,15 +814,26 @@ def generate_shared_lib_init(self, emitter: Emitter) -> None:
827814

828815
for mod in self.modules:
829816
name = exported_name(mod)
817+
if self.multi_phase_init:
818+
capsule_func_prefix = "CPyExec_"
819+
capsule_name_prefix = "exec_"
820+
emitter.emit_line(f"extern int CPyExec_{name}(PyObject *);")
821+
else:
822+
capsule_func_prefix = "CPyInit_"
823+
capsule_name_prefix = "init_"
824+
emitter.emit_line(f"extern PyObject *CPyInit_{name}(void);")
830825
emitter.emit_lines(
831-
f"extern PyObject *CPyInit_{name}(void);",
832-
'capsule = PyCapsule_New((void *)CPyInit_{}, "{}.init_{}", NULL);'.format(
833-
name, shared_lib_name(self.group_name), name
826+
'capsule = PyCapsule_New((void *){}{}, "{}.{}{}", NULL);'.format(
827+
capsule_func_prefix,
828+
name,
829+
shared_lib_name(self.group_name),
830+
capsule_name_prefix,
831+
name,
834832
),
835833
"if (!capsule) {",
836834
"goto fail;",
837835
"}",
838-
f'res = PyObject_SetAttrString(module, "init_{name}", capsule);',
836+
f'res = PyObject_SetAttrString(module, "{capsule_name_prefix}{name}", capsule);',
839837
"Py_DECREF(capsule);",
840838
"if (res < 0) {",
841839
"goto fail;",
@@ -861,7 +859,56 @@ def generate_shared_lib_init(self, emitter: Emitter) -> None:
861859
"",
862860
)
863861

864-
emitter.emit_lines("return module;", "fail:", "Py_XDECREF(module);", "return NULL;", "}")
862+
emitter.emit_lines("return 0;", "fail:", "return -1;", "}")
863+
864+
if self.multi_phase_init:
865+
emitter.emit_lines(
866+
f"static PyModuleDef_Slot slots_{short_name}[] = {{",
867+
f"{{Py_mod_exec, exec_{short_name}}},",
868+
"{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},",
869+
"{Py_mod_gil, Py_MOD_GIL_NOT_USED},",
870+
"{0, NULL},",
871+
"};",
872+
)
873+
874+
size = 0 if self.multi_phase_init else -1
875+
emitter.emit_lines(
876+
f"static PyModuleDef module_def_{short_name} = {{",
877+
"PyModuleDef_HEAD_INIT,",
878+
f'.m_name = "{shared_lib_name(self.group_name)}",',
879+
".m_doc = NULL,",
880+
f".m_size = {size},",
881+
".m_methods = NULL,",
882+
)
883+
if self.multi_phase_init:
884+
emitter.emit_line(f".m_slots = slots_{short_name},")
885+
emitter.emit_line("};")
886+
887+
if self.multi_phase_init:
888+
emitter.emit_lines(
889+
f"PyMODINIT_FUNC PyInit_{short_name}(void) {{",
890+
f"return PyModuleDef_Init(&module_def_{short_name});",
891+
"}",
892+
)
893+
else:
894+
emitter.emit_lines(
895+
f"PyMODINIT_FUNC PyInit_{short_name}(void) {{",
896+
"static PyObject *module = NULL;",
897+
"if (module) {",
898+
"Py_INCREF(module);",
899+
"return module;",
900+
"}",
901+
f"module = PyModule_Create(&module_def_{short_name});",
902+
"if (!module) {",
903+
"return NULL;",
904+
"}",
905+
f"if (exec_{short_name}(module) < 0) {{",
906+
"Py_DECREF(module);",
907+
"return NULL;",
908+
"}",
909+
"return module;",
910+
"}",
911+
)
865912

866913
def generate_globals_init(self, emitter: Emitter) -> None:
867914
emitter.emit_lines(
@@ -887,16 +934,22 @@ def generate_globals_init(self, emitter: Emitter) -> None:
887934
def generate_module_def(self, emitter: Emitter, module_name: str, module: ModuleIR) -> None:
888935
"""Emit the PyModuleDef struct for a module and the module init function."""
889936
module_prefix = emitter.names.private_name(module_name)
890-
self.emit_module_exec_func(emitter, module_name, module_prefix, module)
891-
if self.multi_phase_init:
892-
self.emit_module_def_slots(emitter, module_prefix)
893937
self.emit_module_methods(emitter, module_name, module_prefix, module)
894-
self.emit_module_def_struct(emitter, module_name, module_prefix)
895-
self.emit_module_init_func(emitter, module_name, module_prefix)
938+
self.emit_module_exec_func(emitter, module_name, module_prefix, module)
896939

897-
def emit_module_def_slots(self, emitter: Emitter, module_prefix: str) -> None:
940+
# If using multi-phase init and a shared lib, parts of module definition
941+
# will happen in the shim modules, so we skip some steps here.
942+
if not (self.multi_phase_init and self.use_shared_lib):
943+
if self.multi_phase_init:
944+
self.emit_module_def_slots(emitter, module_prefix, module_name)
945+
self.emit_module_def_struct(emitter, module_name, module_prefix)
946+
self.emit_module_init_func(emitter, module_name, module_prefix)
947+
948+
def emit_module_def_slots(
949+
self, emitter: Emitter, module_prefix: str, module_name: str
950+
) -> None:
898951
name = f"{module_prefix}_slots"
899-
exec_name = f"{module_prefix}_exec"
952+
exec_name = f"CPyExec_{exported_name(module_name)}"
900953

901954
emitter.emit_line(f"static PyModuleDef_Slot {name}[] = {{")
902955
emitter.emit_line(f"{{Py_mod_exec, {exec_name}}},")
@@ -951,7 +1004,7 @@ def emit_module_def_struct(
9511004
"0, /* size of per-interpreter state of the module */",
9521005
f"{module_prefix}module_methods,",
9531006
)
954-
if self.multi_phase_init:
1007+
if self.multi_phase_init and not self.use_shared_lib:
9551008
slots_name = f"{module_prefix}_slots"
9561009
emitter.emit_line(f"{slots_name}, /* m_slots */")
9571010
else:
@@ -962,15 +1015,16 @@ def emit_module_def_struct(
9621015
def emit_module_exec_func(
9631016
self, emitter: Emitter, module_name: str, module_prefix: str, module: ModuleIR
9641017
) -> None:
965-
"""Emit the module init function.
1018+
"""Emit the module exec function.
9661019
967-
If we are compiling just one module, this will be the C API init
968-
function. If we are compiling 2+ modules, we generate a shared
1020+
If we are compiling just one module, this will be the normal C API
1021+
exec function. If we are compiling 2+ modules, we generate a shared
9691022
library for the modules and shims that call into the shared
970-
library, and in this case we use an internal module initialized
971-
function that will be called by the shim.
1023+
library, and in this case the shared module defines an internal
1024+
exec function for each module and these will be called by the shims
1025+
via Capsules.
9721026
"""
973-
declaration = f"static int {module_prefix}_exec(PyObject *module)"
1027+
declaration = f"int CPyExec_{exported_name(module_name)}(PyObject *module)"
9741028
module_static = self.module_internal_static_name(module_name, emitter)
9751029
emitter.emit_lines(declaration, "{")
9761030
emitter.emit_line("PyObject* modname = NULL;")
@@ -987,6 +1041,12 @@ def emit_module_exec_func(
9871041
" goto fail;",
9881042
)
9891043

1044+
if self.multi_phase_init:
1045+
emitter.emit_lines(
1046+
f"if (PyModule_AddFunctions(module, {module_prefix}module_methods) < 0)",
1047+
" goto fail;",
1048+
)
1049+
9901050
# HACK: Manually instantiate generated classes here
9911051
type_structs: list[str] = []
9921052
for cl in module.classes:
@@ -1038,7 +1098,7 @@ def emit_module_init_func(
10381098
emitter.emit_line("}")
10391099
return
10401100

1041-
exec_func = f"{module_prefix}_exec"
1101+
exec_func = f"CPyExec_{exported_name(module_name)}"
10421102

10431103
# Store the module reference in a static and return it when necessary.
10441104
# This is separate from the *global* reference to the module that will
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#include <Python.h>
2+
3+
static int {modname}_exec(PyObject *module)
4+
{{
5+
PyObject *tmp;
6+
if (!(tmp = PyImport_ImportModule("{libname}"))) return -1;
7+
PyObject *capsule = PyObject_GetAttrString(tmp, "exec_{full_modname}");
8+
Py_DECREF(tmp);
9+
if (capsule == NULL) return -1;
10+
void *exec_func = PyCapsule_GetPointer(capsule, "{libname}.exec_{full_modname}");
11+
Py_DECREF(capsule);
12+
if (!exec_func) return -1;
13+
if (((int (*)(PyObject *))exec_func)(module) != 0) return -1;
14+
return 0;
15+
}}
16+
17+
static PyModuleDef_Slot {modname}_slots[] = {{
18+
{{Py_mod_exec, {modname}_exec}},
19+
{{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED}},
20+
{{Py_mod_gil, Py_MOD_GIL_NOT_USED}},
21+
{{0, NULL}},
22+
}};
23+
24+
static struct PyModuleDef {modname}_module = {{
25+
PyModuleDef_HEAD_INIT,
26+
.m_name = "{modname}",
27+
.m_doc = NULL,
28+
.m_methods = NULL,
29+
.m_size = 0,
30+
.m_slots = {modname}_slots,
31+
}};
32+
33+
PyMODINIT_FUNC
34+
PyInit_{modname}(void)
35+
{{
36+
return PyModuleDef_Init(&{modname}_module);
37+
}}
38+
39+
// distutils sometimes spuriously tells cl to export CPyInit___init__,
40+
// so provide that so it chills out
41+
PyMODINIT_FUNC PyInit___init__(void) {{ return PyInit_{modname}(); }}

0 commit comments

Comments
 (0)