From 6e5394ed9f4b1023981b3002c601e55e264b7df3 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Mon, 28 Apr 2025 12:04:37 -0400 Subject: [PATCH 1/5] Explicitly specify --no-build-isolation as that's our expectation in these tests Previously we relied on pip to build the packages in non-PEP517 mode, which implied no build isolation. The latest `virtualenv` (with pypa/virtualenv#2868) won't include `wheel` in the virtualenv, which will mean that pip uses PEP-517 mode, which is isolated by default. (cherry picked from commit 24e42cbe9f567106ac353e50de83e7e9a5f525a1) --- testing/cffi0/test_zintegration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/cffi0/test_zintegration.py b/testing/cffi0/test_zintegration.py index 8d71d81b..600e2cf9 100644 --- a/testing/cffi0/test_zintegration.py +++ b/testing/cffi0/test_zintegration.py @@ -98,7 +98,7 @@ def remove(dir): # there's a setuptools/easy_install bug that causes this to fail when the build/install occur together and # we're in the same directory with the build (it tries to look up dependencies for itself on PyPI); # subsequent runs will succeed because this test doesn't properly clean up the build- use pip for now. - subprocess.check_call((vp, '-m', 'pip', 'install', '.'), env=env) + subprocess.check_call((vp, '-m', 'pip', 'install', '.', '--no-build-isolation'), env=env) subprocess.check_call((vp, str(python_f)), env=env) finally: os.chdir(olddir) From 28f60cb69526da396f1632f2f1b823484f4b8c96 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 3 Sep 2025 09:33:11 -0600 Subject: [PATCH 2/5] Add CFFI thread safety docs (#188) * Add CFFI thread safety docs * Delete incorrect statements * Add more links, examples, and suggestions about TSan * fix indentation in code example * Update doc/source/overview.rst Co-authored-by: Matti Picus --------- Co-authored-by: Matti Picus (cherry picked from commit e94a7b68060e736229457eecc50df7a4ff2397fa) --- doc/source/overview.rst | 172 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/doc/source/overview.rst b/doc/source/overview.rst index ee8b0df3..fb78249f 100644 --- a/doc/source/overview.rst +++ b/doc/source/overview.rst @@ -595,6 +595,178 @@ with C code to initialize global variables. The actual ``lib.*()`` function calls should be obvious: it's like C. +.. _thread-safety: + +Thread Safety +------------- + +Multithreading can be a powerful but tricky way to exploit the many cores on +modern CPUs. Combining CFFI with the Python `threading` module is a convenient +way to use multithreaded parallelism with a C library. + +On the GIL-enabled build, CFFI will release the GIL before calling into a C +library. That means that it is possible to get multithreaded speedups using CFFI +on both the free-threaded and GIL-enabled builds of Python. However, that also +means that the GIL does not protect multithreaded shared use of C data +structures exposed via FFI. + +If the C library you are wrapping is not thread-safe, then it is not thread-safe +to use the library via Python without adding some kind of locking. If the +library *is* thread-safe, then no additional locking is necessary to ensure the +thread safety of CFFI itself. + +Let's make that concrete by wrapping some code that is not thread-safe due to +use of a C global variable: + +.. code-block:: python + + from cffi import FFI + ffibuilder = FFI() + + ffibuilder.set_source("_thread_safety_example", + r""" + #include + + static int64_t value = 0; + static int64_t increment(void) { + value++; + return value; + } + """, + libraries=[] + ) + + ffibuilder.cdef(r""" + int64_t increment(void); + """ + ) + + if __name__ == "__main__": + ffibuilder.compile(verbose=True) + +The way that the ``increment`` uses the ``value`` global variable is not +thread-safe. `Data races +`_ are possible if two +threads simultaneously call ``increment``. We can engineer that situation with a +Python script that calls into the wrapper like so: + +.. code-block:: python + + import sys + + from concurrent.futures import ThreadPoolExecutor, wait + import threading + + from _thread_safety_example import ffi, lib + + # Make races more likely by switching threads more often + # on the GIL-enabled build. This has no effect on the + # free-threaded build. + sys.setswitchinterval(.0000001) + + N_WORKERS = 4 + + l = threading.Lock() + + def work(): + lib.increment() + + def run_thread_pool(): + with ThreadPoolExecutor(max_workers=N_WORKERS) as tpe: + try: + futures = [tpe.submit(work) for _ in range(100000)] + # block until all work finishes + wait(futures) + finally: + # check for exceptions in worker threads + [f.result() for f in futures] + + + run_thread_pool() + + print(lib.increment()) + +On the system used to run this example by the author, this script prints random +results, with possible result values ranging from 99960 to 99980, indicating +that, on average, races happen a few dozen times over the hundred thousand loop +iterations. The results you get will depend on your hardware, system +configuration, and Python interpreter version. + +Note that races are relatively rare. The CFFI bindings and Python interpreter +add enough overhead that it is not very likely for two threads to simultaneously +increment the static integer. This can make code *appear* to be sequentially +consistent for small sample sizes, when it is in fact not consistent. See `this +tutorial +`_ +for more examples of how the GIL and Python overhead can mask thread safety +issues that only manifest under production load. + +We can make the above example script thread-safe by using a lock: + +.. code-block:: python + + l = threading.Lock() + + def work(): + l.acquire() + lib.increment() + l.release() + +The `threading.Lock` ensures only one thread can call into the wrapped C library +at a time. Any thread that calls ``l.acquire()`` while another thread has +already acquired the lock will block until the lock is released. + +Using a global lock like this is necessary if it is not safe for more than one +thread to simultaneously call into any part of the library. This is the case if +the library relies on global state that does not have any explicit +synchronization. Libraries like this are not `re-entrant +`_. + +Libraries that are re-entrant but not thread-safe are usually structured such +that two threads can simultaneously use the library so long as the threads do +not simultaneously mutate shared references to an object. For libraries like +this you will want to use a per-object lock instead of a global lock. Keep in +mind in this case that any program with more than one lock can lead to a +`deadlock `_ and care +must be taken to avoid situations where two threads can deadlock. + +If it is a programming error for two threads to simultaneously share an object, +you might acquire a `threading.Lock` object named ``l`` like this: + +.. code-block:: python + + if not l.acquire(blocking=False): + raise RuntimeError("Multithreaded use is not supported") + + # call into the unsafe library or use an unsafe object + + l.release() + +This prevents deadlocks, since `l.acquire(blocking=False)` returns `False` +immediately if the lock is already acquired by another thread. + +If you know that the C library you are wrapping is thread-safe, no additional +locking is necessary to make the CFFI bindings thread-safe. Please report thread +safety bugs that you suspect are due to issues in the generated CFFI bindings. + +If you publish CFFI bindings for a library, you should document the thread +safety guarantees of your bindings. It may make sense to add locking into the +bindings but it might also make sense to clearly document the bindings are not +thread-safe and it is up to users to ensure appropriate synchronization or +exclusive access if users do want to use the bindings in a thread pool. + +See the Python free-threading guide page on `improving the thread safety of +Python code +`_ +for more information about updating a Python library with thread safety in mind. + +You can validate the thread safety of your library by running multithreaded +tests using `Thread Sanitizer +`_. See the Python +free-threading guide page on `using Thread Sanitizer to detect thread safety +issues `_ for more +details. + .. _abi-versus-api: ABI versus API From b408f315b00f1e87f2e9c87fffa3d6c63dd7a896 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 5 Sep 2025 12:17:30 -0600 Subject: [PATCH 3/5] Explicitly specify manylinux2014 in wheel building config (#184) (cherry picked from commit 078820cfe8e0f61efedb2c31797367b34ffd9e77) --- .github/workflows/ci.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e91e720d..2814564b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -174,11 +174,11 @@ jobs: cd .. && \ rm -rf libffi-3.4.6 CIBW_ENVIRONMENT_PASS_LINUX: CFLAGS # ensure that the build container can see our overridden build config - CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux_img || '' }} - CIBW_MANYLINUX_I686_IMAGE: ${{ matrix.manylinux_img || '' }} - CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux_img || '' }} - CIBW_MANYLINUX_PPC64LE_IMAGE: ${{ matrix.manylinux_img || '' }} - CIBW_MANYLINUX_S390X_IMAGE: ${{ matrix.manylinux_img || '' }} + CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux_img || 'manylinux2014' }} + CIBW_MANYLINUX_I686_IMAGE: ${{ matrix.manylinux_img || 'manylinux2014' }} + CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux_img || 'manylinux2014' }} + CIBW_MANYLINUX_PPC64LE_IMAGE: ${{ matrix.manylinux_img || 'manyinux2014' }} + CIBW_MANYLINUX_S390X_IMAGE: ${{ matrix.manylinux_img || 'manylinux2014' }} CIBW_MUSLLINUX_X86_64_IMAGE: ${{ matrix.musllinux_img || 'musllinux_1_2' }} CIBW_MUSLLINUX_I686_IMAGE: ${{ matrix.musllinux_img || 'musllinux_1_2' }} CIBW_MUSLLINUX_AARCH64_IMAGE: ${{ matrix.musllinux_img || 'musllinux_1_2' }} From fbb59f95f9142db68aacc1455c272145d5632e2b Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 5 Sep 2025 12:19:01 -0600 Subject: [PATCH 4/5] Enable more Windows pytest-run-parallel CI (#189) * enable windows pytest-run-parallel CI * pass skip-thread-unsafe * remove OS conditional (cherry picked from commit 3c61e14ca95fde884ae4b229b5b712af5072cb63) --- .github/workflows/ci.yaml | 8 +------- testing/cffi1/test_function_args.py | 5 +++++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2814564b..05f5c70f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -429,14 +429,8 @@ jobs: python -m pip install . - name: run tests under pytest-run-parallel - if: runner.os == 'Windows' run: | - python -m pytest --parallel-threads=4 src/c - - - name: run tests under pytest-run-parallel - if: runner.os != 'Windows' - run: | - python -m pytest --parallel-threads=4 + python -m pytest --parallel-threads=4 --skip-thread-unsafe=True clang_TSAN: runs-on: ubuntu-latest diff --git a/testing/cffi1/test_function_args.py b/testing/cffi1/test_function_args.py index 30c6feda..4e7e8c3b 100644 --- a/testing/cffi1/test_function_args.py +++ b/testing/cffi1/test_function_args.py @@ -1,4 +1,9 @@ import pytest, sys + +pytestmark = [ + pytest.mark.thread_unsafe(reason="Workers would share a build directory"), +] + try: # comment out the following line to run this test. # the latest on x86-64 linux: https://github.com/libffi/libffi/issues/574 From 7cf16ecf1e4358f71d5dba7fc9e995aabcfc7926 Mon Sep 17 00:00:00 2001 From: Matt Davis <6775756+nitzmahone@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:14:19 -0700 Subject: [PATCH 5/5] Misc CI env stabilization (#194) * Misc CI env stabilization * Specify explicit runner major image versions instead of `latest`. * Test only against versioned Python releases. Installing from arbitrary source commits with `-dev` is rarely worth the potential instability between runs. Specifying X.Y with `allow-prereleases: true` will use the latest packaged X.Y.Z release, falling back to the newest X.Y.0 pre-release if X.Y.0 has not yet been released. * correct manylinux image name typo (cherry picked from commit 51e276e823fcc7487c5cc2504f8737cb7fea3a69) --- .github/workflows/ci.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 05f5c70f..3d208e6e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -177,7 +177,7 @@ jobs: CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux_img || 'manylinux2014' }} CIBW_MANYLINUX_I686_IMAGE: ${{ matrix.manylinux_img || 'manylinux2014' }} CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux_img || 'manylinux2014' }} - CIBW_MANYLINUX_PPC64LE_IMAGE: ${{ matrix.manylinux_img || 'manyinux2014' }} + CIBW_MANYLINUX_PPC64LE_IMAGE: ${{ matrix.manylinux_img || 'manylinux2014' }} CIBW_MANYLINUX_S390X_IMAGE: ${{ matrix.manylinux_img || 'manylinux2014' }} CIBW_MUSLLINUX_X86_64_IMAGE: ${{ matrix.musllinux_img || 'musllinux_1_2' }} CIBW_MUSLLINUX_I686_IMAGE: ${{ matrix.musllinux_img || 'musllinux_1_2' }} @@ -402,9 +402,9 @@ jobs: with: matrix_yaml: | include: - - { runner: ubuntu-latest, python-version: 3.14t-dev } - - { runner: macos-latest, python-version: 3.14t-dev } - - { runner: windows-latest, python-version: 3.14t-dev } + - { runner: ubuntu-24.04, python-version: 3.14t } + - { runner: macos-15, python-version: 3.14t } + - { runner: windows-2025, python-version: 3.14t } pytest-run-parallel: @@ -422,6 +422,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: build and install run: | @@ -433,7 +434,7 @@ jobs: python -m pytest --parallel-threads=4 --skip-thread-unsafe=True clang_TSAN: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 container: ghcr.io/nascheme/numpy-tsan:3.14t steps: - uses: actions/checkout@v4