diff --git a/mypy/nativeparse.py b/mypy/nativeparse.py index d7e660b470ffb..3a8796cf03d8c 100644 --- a/mypy/nativeparse.py +++ b/mypy/nativeparse.py @@ -19,7 +19,6 @@ from __future__ import annotations import os -import time from typing import Final, cast import ast_serialize @@ -280,13 +279,6 @@ def parse_to_binary_ast( source: str | bytes | None = None, skip_function_bodies: bool = False, ) -> tuple[bytes, list[ParseError], TypeIgnores, bytes, bool, bool, str, list[tuple[int, str]]]: - # This is a horrible hack to work around a mypyc bug where imported - # module may be not ready in a thread sometimes. - t0 = time.time() - while ast_serialize is None: - time.sleep(0.0001) # type: ignore[unreachable] - if time.time() - t0 > 10.0: - raise ImportError("Cannot import ast_serialize") ast_bytes, errors, ignores, import_bytes, ast_data = ast_serialize.parse( filename, source, diff --git a/mypyc/codegen/emitmodule.py b/mypyc/codegen/emitmodule.py index 9130007e3f6ef..2442624f5c430 100644 --- a/mypyc/codegen/emitmodule.py +++ b/mypyc/codegen/emitmodule.py @@ -47,7 +47,9 @@ from mypyc.codegen.literals import Literals from mypyc.common import ( EXT_SUFFIX, + IMPORT_STATE_PREFIX, IS_FREE_THREADED, + MODULE_LOCK_API_PREFIX, MODULE_PREFIX, PREFIX, RUNTIME_C_FILES, @@ -656,6 +658,13 @@ def __init__( # probably want to enable it always, but we'll wait until it's stable. self.multi_phase_init = IS_FREE_THREADED + def import_state_name(self, module_name: str) -> str: + return f"{IMPORT_STATE_PREFIX}{exported_name(module_name)}" + + def module_lock_api_name(self) -> str: + assert self.group_name is not None + return f"{MODULE_LOCK_API_PREFIX}{exported_name(self.group_name)}" + @property def group_suffix(self) -> str: return "_" + exported_name(self.group_name) if self.group_name else "" @@ -692,6 +701,9 @@ def generate_c_for_modules(self) -> list[tuple[str, str]]: base_emitter.emit_line(f'#include "__native_internal{self.short_group_suffix}.h"') emitter = base_emitter + if self.use_shared_lib: + self.declare_module_lock_api() + self.generate_literal_tables() for module_name, module in self.modules.items(): @@ -947,6 +959,16 @@ def generate_shared_lib_init(self, emitter: Emitter) -> None: "", ) + lock_api = self.module_lock_api_name() + emitter.emit_lines( + f"if ({lock_api} == NULL) {{", + f"{lock_api} = CPyModuleLockAPI_Alloc();", + f"if ({lock_api} == NULL) goto fail;", + "}", + "if (CPyGlobalsInit() < 0) goto fail;", + "", + ) + if self.compiler_options.separate: emitter.emit_lines( 'capsule = PyCapsule_New(&exports, "{}.exports", NULL);'.format( @@ -1265,8 +1287,9 @@ def emit_module_exec_func( exec_name = f"CPyExec_{exported_name(module_name)}" declaration = f"int {exec_name}(PyObject *module)" emitter.context.declarations[exec_name] = HeaderDeclaration(declaration + ";") + impl_name = f"{exec_name}__impl" module_static = self.module_internal_static_name(module_name, emitter) - emitter.emit_lines(declaration, "{") + emitter.emit_lines(f"static int {impl_name}(PyObject *module)", "{") emitter.emit_line("intern_strings();") if self.compiler_options.depends_on_librt_internal: emitter.emit_line("if (import_librt_internal() < 0) {") @@ -1333,7 +1356,10 @@ def emit_module_exec_func( name_prefix = cl.name_prefix(emitter.names) emitter.emit_line(f"CPyDef_{name_prefix}_trait_vtable_setup();") - emitter.emit_lines("if (CPyGlobalsInit() < 0)", " goto fail;") + if not self.use_shared_lib: + # With shared lib we initialize globals in its init function in case + # modules are executed concurrently. + emitter.emit_lines("if (CPyGlobalsInit() < 0)", " goto fail;") self.generate_top_level_call(module, emitter) @@ -1357,6 +1383,14 @@ def emit_module_exec_func( emitter.emit_line("return -1;") emitter.emit_line("}") + state = self.import_state_name(module_name) + emitter.emit_lines( + declaration, + "{", + f'return CPyImport_Exec(module, "{module_name}", {impl_name}, &{state});', + "}", + ) + def emit_init_only_func(self, emitter: Emitter, module_name: str, module_prefix: str) -> None: """Emit CPyInitOnly_* which creates the module object without executing the body. @@ -1386,10 +1420,12 @@ def emit_module_init_func( ) -> None: if not self.use_shared_lib: declaration = f"PyMODINIT_FUNC PyInit_{module_name}(void)" + impl_declaration = declaration else: n = f"CPyInit_{exported_name(module_name)}" declaration = f"PyObject *{n}(void)" emitter.context.declarations[n] = HeaderDeclaration(declaration + ";") + impl_declaration = declaration if self.multi_phase_init: emitter.emit_lines(declaration, "{") @@ -1404,7 +1440,7 @@ def emit_module_init_func( self.emit_init_only_func(emitter, module_name, module_prefix) # Emit CPyInit_* / PyInit_* which creates the module and executes the body. - emitter.emit_lines(declaration, "{") + emitter.emit_lines(impl_declaration, "{") module_static = self.module_internal_static_name(module_name, emitter) emitter.emit_line("PyObject* modname = NULL;") @@ -1453,15 +1489,19 @@ def emit_module_init_func( emitter.emit_line("Py_DECREF(shared_lib_file);") emitter.emit_line("if (rv < 0) goto fail;") - # Register in sys.modules early so that circular imports via - # CPyImport_ImportNative can detect that this module is already - # being initialized and avoid re-executing the module body. + # Mark the module as initializing before publishing it so that CPython's + # import fast path waits on the module lock. Publishing early also lets + # CPyImport_ImportNative detect circular imports. + emitter.emit_line(f"if (CPyImport_SetInitializing({module_static}, 1) < 0)") + emitter.emit_line(" goto fail;") emitter.emit_line( f"if (PyObject_SetItem(PyImport_GetModuleDict(), modname, {module_static}) < 0)" ) emitter.emit_line(" goto fail;") emitter.emit_line("Py_CLEAR(modname);") emitter.emit_lines(f"if ({exec_func}({module_static}) != 0)", " goto fail;") + emitter.emit_line(f"if (CPyImport_SetInitializing({module_static}, 0) < 0)") + emitter.emit_line(" goto fail;") emitter.emit_line(f"return {module_static};") emitter.emit_lines("fail:") # Clean up on failure: remove from sys.modules and clear the static @@ -1469,6 +1509,12 @@ def emit_module_init_func( emitter.emit_line("{") emitter.emit_line(" PyObject *exc_type, *exc_val, *exc_tb;") emitter.emit_line(" PyErr_Fetch(&exc_type, &exc_val, &exc_tb);") + emitter.emit_line(f" if ({module_static} != NULL) {{") + emitter.emit_line(f" CPyImport_SetInitializing({module_static}, 0);") + emitter.emit_line(" PyErr_Clear();") + emitter.emit_line(" }") + state = self.import_state_name(module_name) + emitter.emit_line(f" CPyImport_SetInitialized(&{state}, 0);") emitter.emit_line(" if (modname == NULL) {") emitter.emit_line(f' modname = PyUnicode_FromString("{module_name}");') emitter.emit_line(" if (modname == NULL) CPyError_OutOfMemory();") @@ -1558,10 +1604,22 @@ def declare_module(self, module_name: str, emitter: Emitter) -> None: if module_name in self.modules: internal_static_name = self.module_internal_static_name(module_name, emitter) self.declare_global("CPyModule *", internal_static_name, initializer="NULL") + state_name = self.import_state_name(module_name) + if state_name not in self.context.declarations: + self.context.declarations[state_name] = HeaderDeclaration( + f"CPyImportState {state_name};", defn=[f"CPyImportState {state_name} = {{0}};"] + ) static_name = emitter.static_name(module_name, None, prefix=MODULE_PREFIX) self.declare_global("CPyModule *", static_name) self.simple_inits.append((static_name, "Py_None")) + def declare_module_lock_api(self) -> None: + name = self.module_lock_api_name() + if name not in self.context.declarations: + self.context.declarations[name] = HeaderDeclaration( + f"CPyModuleLockAPI *{name};", defn=[f"CPyModuleLockAPI *{name} = NULL;"] + ) + def declare_imports(self, imps: Iterable[str], emitter: Emitter) -> None: for imp in imps: self.declare_module(imp, emitter) diff --git a/mypyc/common.py b/mypyc/common.py index fa34647c5c729..61780f31bf055 100644 --- a/mypyc/common.py +++ b/mypyc/common.py @@ -14,6 +14,8 @@ STATIC_PREFIX: Final = "CPyStatic_" # Static variables (for literals etc.) TYPE_PREFIX: Final = "CPyType_" # Type object struct MODULE_PREFIX: Final = "CPyModule_" # Cached modules +IMPORT_STATE_PREFIX: Final = "CPyImportState_" # Native module initialization state +MODULE_LOCK_API_PREFIX: Final = "CPyModuleLockAPI_" # CPython module-lock API cache TYPE_VAR_PREFIX: Final = "CPyTypeVar_" # Type variables when using new-style Python 3.12 syntax ATTR_PREFIX: Final = "_" # Attributes FAST_PREFIX: Final = "__mypyc_fast_" # Optimized methods in non-extension classes @@ -92,6 +94,7 @@ "tuple_ops.c", "exc_ops.c", "misc_ops.c", + "locks.c", "generic_ops.c", "pythonsupport.c", "function_wrapper.c", diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index f4e3745836cf5..2f75b393acaa8 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -70,9 +70,11 @@ BITMAP_BITS, EXT_SUFFIX, GENERATOR_ATTRIBUTE_PREFIX, + IMPORT_STATE_PREFIX, IS_FREE_THREADED, KEEP_ALIVE_SHORT_LIVED, KEEP_ALIVE_WHOLE_EXPRESSION, + MODULE_LOCK_API_PREFIX, MODULE_PREFIX, SELF_NAME, TEMP_ATTR_NAME, @@ -168,6 +170,7 @@ check_unpack_count_op, get_module_dict_op, import_op, + native_import_is_initialized_op, native_import_op, ) from mypyc.primitives.registry import CFunctionDescription, function_ops @@ -523,10 +526,36 @@ def gen_import(self, module: str, line: int) -> None: self.imports[module] = None needs_import, out = BasicBlock(), BasicBlock() - self.check_if_module_loaded(module, line, needs_import, out) + is_native_module = self.is_native_module(module) + is_same_group_native = is_native_module and self.is_same_group_module(module) + import_state: Value | None = None + module_lock_api: Value | None = None + if is_same_group_native: + import_state = self.add( + LoadAddress(c_pointer_rprimitive, f"{IMPORT_STATE_PREFIX}{exported_name(module)}") + ) + group_name = self.mapper.group_map.get(self.module_name) + if group_name is not None: + module_lock_api = self.add( + LoadGlobal( + c_pointer_rprimitive, + f"{MODULE_LOCK_API_PREFIX}{exported_name(group_name)}", + ) + ) + else: + module_lock_api = Integer(0, c_pointer_rprimitive) + if is_native_module and not is_same_group_native: + # A sys.modules entry may still be executing in another thread. For + # native modules in another compilation group, use CPython's import + # path so that its per-module lock waits for initialization. + self.goto(needs_import) + else: + self.check_if_module_loaded(module, line, needs_import, out, import_state) self.activate_block(needs_import) - if self.is_native_module(module) and self.is_same_group_module(module): + if is_same_group_native: + assert import_state is not None + assert module_lock_api is not None # Use custom import machinery for native-to-native imports in the same group init_only_func = self.add( LoadGlobal(c_pointer_rprimitive, f"CPyInitOnly_{exported_name(module)}") @@ -559,6 +588,8 @@ def gen_import(self, module: str, line: int) -> None: init_only_func, exec_func, module_static, + import_state, + module_lock_api, shared_lib_file, ext_suffix, Integer(1 if is_pkg else 0, c_pyssize_t_rprimitive), @@ -572,7 +603,12 @@ def gen_import(self, module: str, line: int) -> None: self.goto_and_activate(out) def check_if_module_loaded( - self, id: str, line: int, needs_import: BasicBlock, out: BasicBlock + self, + id: str, + line: int, + needs_import: BasicBlock, + out: BasicBlock, + import_state: Value | None = None, ) -> None: """Generate code that checks if the module `id` has been loaded yet. @@ -583,7 +619,14 @@ def check_if_module_loaded( out: the BasicBlock that is run if the module has already been loaded""" first_load = self.load_module(id) comparison = self.translate_is_op(first_load, self.none_object(line), "is not", line) - self.add_bool_branch(comparison, out, needs_import) + if import_state is None: + self.add_bool_branch(comparison, out, needs_import) + else: + check_initialized = BasicBlock() + self.add_bool_branch(comparison, check_initialized, needs_import) + self.activate_block(check_initialized) + initialized = self.call_c(native_import_is_initialized_op, [import_state], line) + self.add_bool_branch(initialized, out, needs_import) def get_module(self, module: str, line: int) -> Value: # Python 3.7 has a nice 'PyImport_GetModule' function that we can't use :( diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index e2b4ff0e8c750..73603a6c2420d 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -22,6 +22,29 @@ extern "C" { #define CPYTHON_LARGE_INT_ERRMSG "Python int too large to convert to C ssize_t" +// Native module import synchronization + +typedef struct CPyModuleLockAPI CPyModuleLockAPI; + +typedef struct { + int32_t initialized; +} CPyImportState; + +enum { + CPY_LOCK_ERROR = -1, + CPY_LOCK_ACQUIRED = 0, + CPY_LOCK_DEADLOCK = 1, +}; + +CPyModuleLockAPI *CPyModuleLockAPI_Alloc(void); +void CPyModuleLockAPI_Free(CPyModuleLockAPI *api); +int CPyImport_AcquireLock(CPyModuleLockAPI *api, PyObject *module_name, + PyObject **module_lock); +int CPyImport_ReleaseLock(PyObject *module_lock); +bool CPyImport_IsInitialized(const CPyImportState *state); +void CPyImport_SetInitialized(CPyImportState *state, bool initialized); + + // Naming conventions: // // Tagged: tagged int @@ -1043,8 +1066,12 @@ PyObject *CPyImport_ImportNative(PyObject *module_name, PyObject *(*init_only_fn)(void), int (*exec_fn)(PyObject *), CPyModule **module_static, + CPyImportState *state, CPyModuleLockAPI *lock_api, PyObject *shared_lib_file, PyObject *ext_suffix, Py_ssize_t is_package); +int CPyImport_Exec(PyObject *module, const char *module_name, + int (*exec_fn)(PyObject *), CPyImportState *state); +int CPyImport_SetInitializing(PyObject *module, bool initializing); int CPyImport_SetDunderAttrs(PyObject *module, PyObject *module_name, PyObject *shared_lib_file, PyObject *ext_suffix, Py_ssize_t is_package); diff --git a/mypyc/lib-rt/locks.c b/mypyc/lib-rt/locks.c new file mode 100644 index 0000000000000..16ded3aa983fc --- /dev/null +++ b/mypyc/lib-rt/locks.c @@ -0,0 +1,98 @@ +#include "CPy.h" + +#ifdef _WIN32 +#include +#endif + +struct CPyModuleLockAPI { + PyObject *get_module_lock; + PyObject *deadlock_error; +}; + +CPyModuleLockAPI *CPyModuleLockAPI_Alloc(void) { + CPyModuleLockAPI *api = PyMem_Calloc(1, sizeof(CPyModuleLockAPI)); + if (api == NULL) { + PyErr_NoMemory(); + return NULL; + } + + PyObject *bootstrap = PyImport_ImportModule("importlib._bootstrap"); + if (bootstrap == NULL) { + PyMem_Free(api); + return NULL; + } + api->get_module_lock = PyObject_GetAttrString(bootstrap, "_get_module_lock"); + api->deadlock_error = PyObject_GetAttrString(bootstrap, "_DeadlockError"); + Py_DECREF(bootstrap); + if (api->get_module_lock == NULL || api->deadlock_error == NULL) { + CPyModuleLockAPI_Free(api); + return NULL; + } + return api; +} + +void CPyModuleLockAPI_Free(CPyModuleLockAPI *api) { + if (api == NULL) { + return; + } + Py_XDECREF(api->get_module_lock); + Py_XDECREF(api->deadlock_error); + PyMem_Free(api); +} + +int CPyImport_AcquireLock(CPyModuleLockAPI *api, PyObject *module_name, + PyObject **acquired_lock) { + *acquired_lock = NULL; + if (api == NULL) { + return CPY_LOCK_ACQUIRED; + } + + PyObject *module_lock = PyObject_CallOneArg(api->get_module_lock, module_name); + if (module_lock == NULL) { + return CPY_LOCK_ERROR; + } + PyObject *result = PyObject_CallMethod(module_lock, "acquire", NULL); + if (result == NULL) { + if (PyErr_ExceptionMatches(api->deadlock_error)) { + PyErr_Clear(); + Py_DECREF(module_lock); + return CPY_LOCK_DEADLOCK; + } + Py_DECREF(module_lock); + return CPY_LOCK_ERROR; + } + Py_DECREF(result); + *acquired_lock = module_lock; + return CPY_LOCK_ACQUIRED; +} + +int CPyImport_ReleaseLock(PyObject *module_lock) { + if (module_lock == NULL) { + return 0; + } + + PyObject *result = PyObject_CallMethod(module_lock, "release", NULL); + Py_DECREF(module_lock); + if (result == NULL) { + return -1; + } + Py_DECREF(result); + return 0; +} + +bool CPyImport_IsInitialized(const CPyImportState *state) { +#ifdef _WIN32 + return InterlockedCompareExchange( + (volatile LONG *)&state->initialized, 0, 0) != 0; +#else + return __atomic_load_n(&state->initialized, __ATOMIC_ACQUIRE) != 0; +#endif +} + +void CPyImport_SetInitialized(CPyImportState *state, bool initialized) { +#ifdef _WIN32 + InterlockedExchange((volatile LONG *)&state->initialized, initialized); +#else + __atomic_store_n(&state->initialized, initialized, __ATOMIC_RELEASE); +#endif +} diff --git a/mypyc/lib-rt/misc_ops.c b/mypyc/lib-rt/misc_ops.c index 392dba0deca4c..03a8906bd3534 100644 --- a/mypyc/lib-rt/misc_ops.c +++ b/mypyc/lib-rt/misc_ops.c @@ -1506,46 +1506,143 @@ static int CPyImport_SetModuleSpec(PyObject *modobj, PyObject *module_name, return 0; } +// Set module.__spec__._initializing for CPython's import machinery. +int CPyImport_SetInitializing(PyObject *module, bool initializing) { + PyObject *spec = PyObject_GetAttrString(module, "__spec__"); + if (spec == NULL) { + return -1; + } + int result = PyObject_SetAttrString(spec, "_initializing", + initializing ? Py_True : Py_False); + Py_DECREF(spec); + return result; +} + +// Import module_name's parent and return owned parent and child-name references. +// Both outputs are NULL for a top-level module. +static int CPyImport_ImportParent(PyObject *module_name, PyObject **parent_module, + PyObject **child_name) { + *parent_module = NULL; + *child_name = NULL; + Py_ssize_t name_len = PyUnicode_GetLength(module_name); + if (name_len < 0) { + return -1; + } + Py_ssize_t dot = PyUnicode_FindChar(module_name, '.', 0, name_len, -1); + if (dot < 0) { + return 0; + } + PyObject *parent_name = PyUnicode_Substring(module_name, 0, dot); + *child_name = PyUnicode_Substring(module_name, dot + 1, name_len); + if (parent_name == NULL || *child_name == NULL) { + Py_XDECREF(parent_name); + Py_CLEAR(*child_name); + return -1; + } + *parent_module = PyImport_Import(parent_name); + Py_DECREF(parent_name); + if (*parent_module == NULL) { + Py_CLEAR(*child_name); + return -1; + } + return 0; +} + +// Import module_name's parent and bind the module under its child name. +static int CPyImport_SetParentAttr(PyObject *module, PyObject *module_name) { + PyObject *parent_module; + PyObject *child_name; + if (CPyImport_ImportParent(module_name, &parent_module, &child_name) < 0) { + return -1; + } + if (parent_module == NULL) { + return 0; + } + int result = PyObject_SetAttr(parent_module, child_name, module); + Py_DECREF(parent_module); + Py_DECREF(child_name); + return result; +} + +static int CPyImport_ReleaseLockPreservingException(PyObject *module_lock) { + PyObject *exc_type, *exc_val, *exc_tb; + PyErr_Fetch(&exc_type, &exc_val, &exc_tb); + int result = CPyImport_ReleaseLock(module_lock); + if (result < 0) { + PyErr_Clear(); + } + PyErr_Restore(exc_type, exc_val, exc_tb); + return result; +} + +// Execute a module once and publish completion; caller holds the module lock. +int CPyImport_Exec(PyObject *module, const char *module_name, + int (*exec_fn)(PyObject *), CPyImportState *state) { + if (CPyImport_IsInitialized(state)) { + return 0; + } + PyObject *name = PyUnicode_FromString(module_name); + if (name == NULL) { + return -1; + } + + int result = exec_fn(module); + if (result == 0) { + // Match CPython import semantics: publish parent.child only after the + // child module finished executing successfully. + result = CPyImport_SetParentAttr(module, name); + } + if (result == 0) { + CPyImport_SetInitialized(state, true); + } + Py_DECREF(name); + return result; +} + PyObject *CPyImport_ImportNative(PyObject *module_name, PyObject *(*init_only_fn)(void), int (*exec_fn)(PyObject *), CPyModule **module_static, + CPyImportState *state, CPyModuleLockAPI *lock_api, PyObject *shared_lib_file, PyObject *ext_suffix, Py_ssize_t is_package) { PyObject *parent_module = NULL; PyObject *child_name = NULL; PyObject *exc_type, *exc_val, *exc_tb; - Py_ssize_t name_len = PyUnicode_GetLength(module_name); - if (name_len < 0) { + // Import the parent package first to preserve import ordering semantics. + if (CPyImport_ImportParent(module_name, &parent_module, &child_name) < 0) { return NULL; } - Py_ssize_t dot = PyUnicode_FindChar(module_name, '.', 0, name_len, -1); - if (dot >= 0) { - // Import the parent package first to preserve import ordering semantics. - PyObject *parent_name = PyUnicode_Substring(module_name, 0, dot); - if (parent_name == NULL) { - CPyError_OutOfMemory(); - } - child_name = PyUnicode_Substring(module_name, dot + 1, name_len); - if (child_name == NULL) { - CPyError_OutOfMemory(); - } - parent_module = PyImport_Import(parent_name); - Py_DECREF(parent_name); - if (parent_module == NULL) { - Py_DECREF(child_name); - return NULL; - } - } + Py_XDECREF(parent_module); + Py_XDECREF(child_name); // Create the module object without executing the module body. // CPyInitOnly_* uses an internal static to cache the module object. // We then check sys.modules to determine whether the module body // has already been executed (or is being executed in a circular import). + PyObject *module_lock; + int lock_result = CPyImport_AcquireLock(lock_api, module_name, &module_lock); + if (lock_result == CPY_LOCK_DEADLOCK) { + PyObject *partial = PyDict_GetItemWithError(PyImport_GetModuleDict(), module_name); + if (partial != NULL && + (*module_static == NULL || partial == (PyObject *)*module_static)) { + Py_INCREF(partial); + return partial; + } + if (!PyErr_Occurred()) { + PyErr_Format(PyExc_ImportError, + "import deadlock for native module '%U' without a partial module", + module_name); + } + return NULL; + } + if (lock_result == CPY_LOCK_ERROR) { + return NULL; + } + PyObject *module_dict = PyImport_GetModuleDict(); if (module_dict == NULL) { - Py_XDECREF(parent_module); - Py_XDECREF(child_name); + CPyImport_ReleaseLockPreservingException(module_lock); return NULL; } @@ -1554,33 +1651,36 @@ PyObject *CPyImport_ImportNative(PyObject *module_name, if (*module_static != NULL) { if (existing == (PyObject *)*module_static) { Py_INCREF(existing); - Py_XDECREF(parent_module); - Py_XDECREF(child_name); + if (CPyImport_ReleaseLock(module_lock) < 0) { + Py_DECREF(existing); + existing = NULL; + } return existing; } PyErr_Format(PyExc_ImportError, "native module '%U' in sys.modules was replaced after initialization", module_name); - Py_XDECREF(parent_module); - Py_XDECREF(child_name); + CPyImport_ReleaseLockPreservingException(module_lock); return NULL; } } if (PyErr_Occurred()) { - Py_XDECREF(parent_module); - Py_XDECREF(child_name); + CPyImport_ReleaseLockPreservingException(module_lock); return NULL; } - PyObject *modobj = init_only_fn(); - if (modobj == NULL) { - Py_XDECREF(parent_module); - Py_XDECREF(child_name); + if (CPyImport_IsInitialized(state)) { + PyErr_Format(PyExc_ImportError, + "initialized native module '%U' is missing from sys.modules", + module_name); + CPyImport_ReleaseLockPreservingException(module_lock); return NULL; } - if (PyObject_SetItem(module_dict, module_name, modobj) < 0) { - goto fail; + PyObject *modobj = init_only_fn(); + if (modobj == NULL) { + CPyImport_ReleaseLockPreservingException(module_lock); + return NULL; } if (*module_static != (CPyModule *)modobj) { @@ -1594,31 +1694,41 @@ PyObject *CPyImport_ImportNative(PyObject *module_name, goto fail; } + if (CPyImport_SetInitializing(modobj, true) < 0) { + goto fail; + } + + if (PyObject_SetItem(module_dict, module_name, modobj) < 0) { + goto fail; + } + // Now execute the module body, with __file__ and __package__ already set. if (exec_fn(modobj) != 0) { goto fail; } - // Match CPython import semantics: publish parent.child only after the - // child module finished executing successfully. - if (parent_module != NULL && PyObject_SetAttr(parent_module, child_name, modobj) < 0) { + if (CPyImport_SetInitializing(modobj, false) < 0) { goto fail; } - Py_XDECREF(parent_module); - Py_XDECREF(child_name); + if (CPyImport_ReleaseLock(module_lock) < 0) { + Py_DECREF(modobj); + return NULL; + } return modobj; fail: // Clean up on failure so that a subsequent import attempt will retry // initialization. PyErr_Fetch(&exc_type, &exc_val, &exc_tb); + CPyImport_SetInitializing(modobj, false); + PyErr_Clear(); PyObject_DelItem(module_dict, module_name); PyErr_Clear(); PyErr_Restore(exc_type, exc_val, exc_tb); - Py_XDECREF(parent_module); - Py_XDECREF(child_name); Py_CLEAR(*module_static); + CPyImport_SetInitialized(state, false); + CPyImport_ReleaseLockPreservingException(module_lock); return NULL; } diff --git a/mypyc/primitives/misc_ops.py b/mypyc/primitives/misc_ops.py index 7b78b61f50e26..52da7ae512e7b 100644 --- a/mypyc/primitives/misc_ops.py +++ b/mypyc/primitives/misc_ops.py @@ -141,12 +141,15 @@ # Import a native same-group module directly via C-level init/exec functions. native_import_op = custom_op( # (module name, init-only function, exec function, module static, - # shared lib __file__, ext suffix, is_package) + # import state, compilation-unit lock, shared lib __file__, ext suffix, + # is_package) arg_types=[ str_rprimitive, c_pointer_rprimitive, c_pointer_rprimitive, object_pointer_rprimitive, + c_pointer_rprimitive, + c_pointer_rprimitive, object_rprimitive, str_rprimitive, c_pyssize_t_rprimitive, @@ -156,6 +159,13 @@ error_kind=ERR_MAGIC, ) +native_import_is_initialized_op = custom_op( + arg_types=[c_pointer_rprimitive], + return_type=bit_rprimitive, + c_function_name="CPyImport_IsInitialized", + error_kind=ERR_NEVER, +) + # Table-driven import op. import_many_op = custom_op( arg_types=[ diff --git a/mypyc/test-data/run-multimodule.test b/mypyc/test-data/run-multimodule.test index fed4d3606ed90..a6a7219383551 100644 --- a/mypyc/test-data/run-multimodule.test +++ b/mypyc/test-data/run-multimodule.test @@ -2048,3 +2048,481 @@ from mypy_extensions import mypyc_attr class CompiledBase: def value(self) -> int: raise NotImplementedError + +[case testCallFunctionInLazilyImportedModule] +import my_lib + +def get_version() -> str: + return my_lib.version() + +[file my_lib.py] +def version() -> str: + return "1.0.0" + +[file other.py] +def lazy_import() -> str: + import native + + return native.get_version() + +[file driver.py] +from threading import Barrier, Thread + +import other + +def test_lazy_import() -> None: + barrier = Barrier(2) + + def run_once() -> None: + barrier.wait() + assert other.lazy_import() == "1.0.0" + + threads = [] + for _ in range(2): + t = Thread(target = run_once) + threads.append(t) + t.start() + + for t in threads: + t.join() + +test_lazy_import() + +[case testConcurrentLazyNativeImportForms] +# separate: [(["native.py", "other_dependency.py", "other_importer.py"], "testgroup")] +from time import sleep + +sleep(0.05) +import other_dependency + +def get_value() -> int: + return other_dependency.value + +[file other_dependency.py] +value = 42 + +[file other_importer.py] +def by_module() -> int: + import native + return native.get_value() + +def by_from() -> int: + from native import get_value + return get_value() + +def by_from_alias() -> int: + from native import get_value as load_value + return load_value() + +[file driver.py] +from concurrent.futures import ThreadPoolExecutor +from threading import Barrier + +import other_importer + +functions = [ + other_importer.by_module, + other_importer.by_from, + other_importer.by_from_alias, +] +barrier = Barrier(len(functions)) + +def run(function): + barrier.wait() + return function() + +with ThreadPoolExecutor(max_workers=len(functions)) as executor: + futures = [executor.submit(run, function) for function in functions] + assert [future.result() for future in futures] == [42] * len(functions) + +[case testConcurrentLazyNativePackageImportForms] +# separate: [(["other_pkg/__init__.py", "other_pkg/other_target.py", "other_pkg/other_importer.py"], "testgroup")] +pass + +[file other_pkg/__init__.py] + +[file other_pkg/other_target.py] +from time import sleep + +sleep(0.05) +import py_dependency + +def get_value() -> int: + return py_dependency.value + +[file other_pkg/other_importer.py] +def by_dotted_module() -> int: + import other_pkg.other_target + return other_pkg.other_target.get_value() + +def by_dotted_alias() -> int: + import other_pkg.other_target as target + return target.get_value() + +def by_from_package() -> int: + from other_pkg import other_target + return other_target.get_value() + +def by_from_module() -> int: + from other_pkg.other_target import get_value + return get_value() + +def by_relative_package() -> int: + from . import other_target + return other_target.get_value() + +def by_relative_module() -> int: + from .other_target import get_value + return get_value() + +[file py_dependency.py] +value = 42 + +[file driver.py] +from concurrent.futures import ThreadPoolExecutor +from threading import Barrier + +from other_pkg import other_importer + +functions = [ + other_importer.by_dotted_module, + other_importer.by_dotted_alias, + other_importer.by_from_package, + other_importer.by_from_module, + other_importer.by_relative_package, + other_importer.by_relative_module, +] +barrier = Barrier(len(functions)) + +def run(function): + barrier.wait() + return function() + +with ThreadPoolExecutor(max_workers=len(functions)) as executor: + futures = [executor.submit(run, function) for function in functions] + assert [future.result() for future in futures] == [42] * len(functions) + +[case testConcurrentLazyNativeImportFromInterpretedModules] +from time import sleep + +sleep(0.05) +import py_dependency + +def get_value() -> int: + return py_dependency.value + +[file py_dependency.py] +value = 42 + +[file py_importer.py] +def by_module(): + import native + return native.get_value() + +def by_from(): + from native import get_value + return get_value() + +[file driver.py] +from concurrent.futures import ThreadPoolExecutor +from threading import Barrier + +import py_importer + +functions = [py_importer.by_module, py_importer.by_from] +barrier = Barrier(len(functions)) + +def run(function): + barrier.wait() + return function() + +with ThreadPoolExecutor(max_workers=len(functions)) as executor: + futures = [executor.submit(run, function) for function in functions] + assert [future.result() for future in futures] == [42] * len(functions) + +[case testConcurrentLazyNativeImportFromNativeAndInterpretedModules] +# separate: [(["native.py", "other_importer.py"], "testgroup")] +from time import sleep + +sleep(0.05) +import py_dependency + +def get_value() -> int: + return py_dependency.value + +[file other_importer.py] +def load() -> int: + import native + return native.get_value() + +[file py_dependency.py] +value = 42 + +[file py_importer.py] +def load(): + from native import get_value + return get_value() + +[file driver.py] +from concurrent.futures import ThreadPoolExecutor +from threading import Barrier + +import other_importer +import py_importer + +functions = [other_importer.load, py_importer.load] +barrier = Barrier(len(functions)) + +def run(function): + barrier.wait() + return function() + +with ThreadPoolExecutor(max_workers=len(functions)) as executor: + futures = [executor.submit(run, function) for function in functions] + assert [future.result() for future in futures] == [42] * len(functions) + +[case testConcurrentLazyNativeImportAcrossCompilationGroups] +# separate: [(["native.py", "other_import_a.py"], "target"), (["other_import_b.py"], "importer_b")] +from time import sleep + +sleep(0.05) + +def get_value() -> int: + return 42 + +[file other_import_a.py] +def load() -> int: + import native + return native.get_value() + +[file other_import_b.py] +def load() -> int: + from native import get_value + return get_value() + +[file driver.py] +from concurrent.futures import ThreadPoolExecutor +from threading import Barrier + +import other_import_a +import other_import_b + +functions = [other_import_a.load, other_import_b.load] +barrier = Barrier(len(functions)) + +def run(function): + barrier.wait() + return function() + +with ThreadPoolExecutor(max_workers=len(functions)) as executor: + futures = [executor.submit(run, function) for function in functions] + assert [future.result() for future in futures] == [42] * len(functions) + +[case testRegularImportWaitsForDirectNativeImport] +# separate: [(["native.py", "other_importer.py"], "testgroup")] +from time import sleep + +import import_sync + +import_sync.direct_started.set() +assert import_sync.regular_started.wait(timeout=5) +sleep(0.05) + +def get_value() -> int: + return 42 + +[file import_sync.py] +from threading import Event + +direct_started = Event() +regular_started = Event() + +[file other_importer.py] +def load() -> int: + import native + return native.get_value() + +[file py_importer.py] +import import_sync + +def load(): + import_sync.regular_started.set() + import native + return native.get_value() + +[file driver.py] +from concurrent.futures import ThreadPoolExecutor + +import import_sync +import other_importer +import py_importer + +with ThreadPoolExecutor(max_workers=2) as executor: + direct_future = executor.submit(other_importer.load) + assert import_sync.direct_started.wait(timeout=5) + regular_future = executor.submit(py_importer.load) + assert direct_future.result(timeout=10) == 42 + assert regular_future.result(timeout=10) == 42 + +[case testConcurrentFailedLazyNativeImportRetries] +# separate: [(["native.py", "other_importer.py"], "testgroup")] +from time import sleep + +import failure_state + +failure_state.attempts += 1 +sleep(0.05) +if failure_state.attempts == 1: + raise RuntimeError("first initialization failed") + +def get_value() -> int: + return 42 + +[file failure_state.py] +attempts = 0 + +[file other_importer.py] +def load() -> int: + from native import get_value + return get_value() + +[file driver.py] +from concurrent.futures import ThreadPoolExecutor +from threading import Barrier + +import failure_state +import other_importer + +barrier = Barrier(2) + +def run(): + barrier.wait() + return other_importer.load() + +with ThreadPoolExecutor(max_workers=2) as executor: + futures = [executor.submit(run) for _ in range(2)] + +values = [] +errors = [] +for future in futures: + try: + values.append(future.result()) + except RuntimeError as error: + errors.append(str(error)) + +assert values == [42] +assert errors == ["first initialization failed"] +assert failure_state.attempts == 2 +assert other_importer.load() == 42 + +[case testConcurrentCircularNativeImports] +# separate: [(["other_a.py", "other_b.py"], "testgroup")] +pass + +[file import_sync.py] +from threading import Barrier + +barrier = Barrier(2) + +[file other_a.py] +from import_sync import barrier + +def value() -> str: + return "a" + +barrier.wait(timeout=5) +import other_b +other_value = other_b.value() + +[file other_b.py] +from import_sync import barrier + +def value() -> str: + return "b" + +barrier.wait(timeout=5) +import other_a +other_value = other_a.value() + +[file driver.py] +from concurrent.futures import ThreadPoolExecutor +import importlib + +with ThreadPoolExecutor(max_workers=2) as executor: + future_a = executor.submit(importlib.import_module, "other_a") + future_b = executor.submit(importlib.import_module, "other_b") + other_a = future_a.result(timeout=10) + other_b = future_b.result(timeout=10) + +assert other_a.other_value == "b" +assert other_b.other_value == "a" + +[case testTopLevelThreadImportsNativeModuleFromSameGroup] +# separate: [(["native.py", "other_target.py"], "testgroup")] +from threading import Thread + +results: list[int] = [] + +def load_target() -> None: + import other_target + results.append(other_target.value) + +thread = Thread(target=load_target) +thread.start() +thread.join(timeout=5) + +assert not thread.is_alive(), "native import blocked on its compilation unit" +assert results == [42] + +[file other_target.py] +value = 42 + +[file driver.py] +import native + +assert native.results == [42] + +[case testCallFunctionInLazilyImportedModuleThroughAny] +[file my_lib.py] +def version() -> str: + return "1.0.0" + +[file other_pkg/__init__.py] + +[file other_pkg/other_callmylib.py] +import my_lib + +def get_version() -> str: + return my_lib.version() + +[file other_pkg/other_import.py] +from typing import Any + +def lazy_import() -> str: + import other_pkg.other_callmylib + + pkg: Any = other_pkg + return pkg.other_callmylib.get_version() + +[file driver.py] +from threading import Barrier, Thread + +import other_pkg.other_import + +def test_lazy_import() -> None: + barrier = Barrier(2) + + def run_once() -> None: + barrier.wait() + assert other_pkg.other_import.lazy_import() == "1.0.0" + + threads = [] + for _ in range(2): + t = Thread(target = run_once) + threads.append(t) + t.start() + + for t in threads: + t.join() + +test_lazy_import()