Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES/1328.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fixed global counter system using an atomic variable.
-- by :user:`Vizonex`.
10 changes: 8 additions & 2 deletions multidict/_multilib/state.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
extern "C" {
#endif

#include <stdatomic.h>

/* State of the _multidict module */
typedef struct {
PyTypeObject *IStrType;
Expand All @@ -26,7 +28,7 @@ typedef struct {
PyObject *str_lower;
PyObject *str_name;

uint64_t global_version;
_Atomic uint64_t global_version;
} mod_state;

static inline mod_state *
Expand Down Expand Up @@ -128,7 +130,11 @@ get_mod_state_by_def(PyObject *self)
static inline uint64_t
NEXT_VERSION(mod_state *state)
{
return ++state->global_version;
/* relaxed is fine here as we only care about the atomicity of the RMW
* itself */
return atomic_fetch_add_explicit(
&state->global_version, 1, memory_order_relaxed) +
1;
}

#ifdef __cplusplus
Expand Down
50 changes: 33 additions & 17 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,51 @@
import os
import platform
import sys

from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext

NO_EXTENSIONS = bool(os.environ.get("MULTIDICT_NO_EXTENSIONS"))
DEBUG_BUILD = bool(os.environ.get("MULTIDICT_DEBUG_BUILD"))

if sys.implementation.name != "cpython":
NO_EXTENSIONS = True

CFLAGS = ["-O0", "-g3", "-UNDEBUG"] if DEBUG_BUILD else ["-O3", "-DNDEBUG"]

if platform.system() != "Windows":
CFLAGS.extend(
[
"-std=c11",
"-Wall",
"-Wsign-compare",
"-Wconversion",
"-fno-strict-aliasing",
"-Wno-conversion",
"-Werror",
]
)
BASE_CFLAGS = ["O0", "g3", "UNDEBUG"] if DEBUG_BUILD else ["O3", "DNDEBUG"]

UNIX_CFLAGS = [
"-std=c11",
"-Wall",
"-Wsign-compare",
"-Wconversion",
"-fno-strict-aliasing",
"-Wno-conversion",
"-Werror",
]

MSVC_CFLAGS = ["/std:c11", "/experimental:c11atomics"]


class BuildExt(build_ext):
def build_extensions(self):
if self.compiler.compiler_type == "msvc":
for ext in self.extensions:
ext.extra_compile_args.extend(MSVC_CFLAGS)
for flag in BASE_CFLAGS:
# XXX: MSVC Doesn't have a /O3 flag only O2 is possible...
ext.extra_compile_args.append("/O2" if flag == "O3" else f"/{flag}")
else:
Comment on lines +33 to +36
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MSVC handling currently prefixes BASE_CFLAGS values with / (e.g. O0 -> /O0, g3 -> /g3). These aren't valid MSVC flags (/Od and /Zi//Z7 are the usual equivalents), so Windows builds with MULTIDICT_DEBUG_BUILD=1 will fail. Consider defining a separate MSVC-specific debug/release flag list instead of reusing BASE_CFLAGS verbatim.

Copilot uses AI. Check for mistakes.
for ext in self.extensions:
ext.extra_compile_args.extend(UNIX_CFLAGS)
for flag in BASE_CFLAGS:
ext.extra_compile_args.append(f"-{flag}")
super().build_extensions()


extensions = [
Extension(
"multidict._multidict",
["multidict/_multidict.c"],
Comment thread
Vizonex marked this conversation as resolved.
extra_compile_args=CFLAGS,
extra_compile_args=[],
),
]

Expand All @@ -38,7 +54,7 @@
print("*********************")
print("* Accelerated build *")
print("*********************")
setup(ext_modules=extensions)
setup(ext_modules=extensions, cmdclass={"build_ext": BuildExt})
else:
print("*********************")
print("* Pure Python build *")
Expand Down
33 changes: 33 additions & 0 deletions tests/isolated/multidict_global_counter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import sysconfig
import threading

import multidict

Check notice

Code scanning / CodeQL

Module is imported with 'import' and 'import from' Note test

Module 'multidict' is imported with both 'import' and 'import from'.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
bdraco marked this conversation as resolved.
Dismissed
from multidict import MultiDict

FREETHREADED = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))


md: MultiDict[int] = MultiDict()
N, M = 3, 100
baseline = multidict.getversion(md) # type: ignore[arg-type]


def worker(tid: int) -> None:
for i in range(M):
md[f"k{tid}_{i}"] = i


if (__name__ == "__main__") and FREETHREADED:
threads = [threading.Thread(target=worker, args=(tid,)) for tid in range(N)]
for t in threads:
t.start()
for t in threads:
t.join()

observed = multidict.getversion(md) - baseline # type: ignore[arg-type]
expected = N * M
assert expected == observed, (
f"expected delta: {expected}"
f" observed: {observed} "
f"lost: {expected - observed}"
)
1 change: 1 addition & 0 deletions tests/test_leaks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"multidict_type_leak.py",
"multidict_type_leak_items_values.py",
"multidict_pop.py",
"multidict_global_counter.py",
),
)
@pytest.mark.leaks
Expand Down
Loading