Skip to content

fix(crc32c): accept any contiguous bytes-like object in value() and extend() - #18626

Open
yuxin00j wants to merge 5 commits into
googleapis:mainfrom
yuxin00j:crc32c-buffer-protocol
Open

yuxin00j wants to merge 5 commits into
googleapis:mainfrom
yuxin00j:crc32c-buffer-protocol

Conversation

@yuxin00j

Copy link
Copy Markdown
Contributor

Summary

google_crc32c.value(), extend() and Checksum.update() accept any contiguous bytes-like object (memoryview, bytearray, array, bytes) instead of bytes only, and the C extension releases the GIL for inputs of 1 MiB or more whose memory belongs to a bytes object (a bytes or a memoryview of one). No API change.

Motivation: the storage client wants to verify chunks it receives as memoryview slices of the gRPC message without copying them into bytes first; today value(memoryview(...)) raises TypeError: argument 1 must be read-only bytes-like object.

Changes

  • src/google_crc32c/_crc32c.c: parse the data argument with y* into a Py_buffer (released after hashing) instead of y#, which rejects every exporter with a release function. The 1 MiB GIL release applies when the exported memory is owned by a bytes object; a bytearray or other writable owner keeps the GIL, since another thread could write to it mid-hash.
  • src/google_crc32c/python.py: the pure-Python implementation hashes the raw bytes of the same inputs (a multi-byte array or cast view is hashed byte for byte, as the C extension does, instead of item by item).
  • Docstrings in cext.py, _checksum.py and python.py say bytes-like.
  • tests/test___init__.py: value/extend on bytes, bytearray, memoryview (sliced, writable, read-only, cast, 2-D) and array inputs, BufferError on non-contiguous views, and GIL probes: held for bytearray and read-only views of it, released for bytes, memoryview(bytes) and a slice of it.

Tests

nox -s unit lint cover

180 passed, 4 skipped (numpy not installed), 100% coverage, with the compiled extension and with CRC32C_PURE_PYTHON=1.

Notes

  • Behavior change in the pure-Python implementation for multi-byte array/cast inputs (previously coerced item by item). It now matches the C extension.
  • The GIL rule (bytes-owned memory only) is a choice: hashlib and zlib.crc32 release the GIL for any buffer. Holding it for writable owners avoids hashing memory another thread may be writing to; happy to drop that restriction if you prefer parity with hashlib.

…xtend()

`google_crc32c.value()`, `extend()` and `Checksum.update()` parsed their
chunk with `PyArg_ParseTuple("y#")`, which only accepts objects without a
`releasebuffer` slot, so `memoryview`, `bytearray` and `array.array` were
rejected with "argument must be read-only bytes-like object". Callers that
hold a view into a larger buffer had to copy it into `bytes` first just to
checksum it.

Parse the chunk as a `Py_buffer` ("y*") instead, which accepts any
C-contiguous object supporting the buffer protocol, and release it once the
checksum is computed. The GIL is still released for chunks of at least
1 MiB, now for any read-only buffer (`bytes`, or a read-only view) rather
than only `bytes`; mutable buffers such as `bytearray` keep the GIL as
before. Non-contiguous views raise `BufferError`, as with `hashlib`.
Add a non-zero-offset memoryview slice and a writable bytearray-backed
view to the bytes-like parametrization so the extension is shown to hash
the exported slice rather than the exporter's whole buffer, test the
Checksum constructor with every bytes-like type, check that extend()
rejects non-contiguous views like value() does, and align the
_checksum.py / python.py docstrings with cext.py.
Checksum.update() converted its input with array.array("B", data),
which iterates items rather than bytes. Multi-byte buffers such as
array("H") or memoryview.cast("I") therefore hashed differently from the
C extension (or raised OverflowError), and mmap, 'c'-format and N-D
buffers raised, although the C extension hashes their raw bytes.

Hash memoryview(data).cast("B") for buffer objects, raise BufferError
for views that are not C-contiguous as the C extension does, and keep
array.array("B", data) only for iterables of ints.
The extension released the GIL for any read-only chunk of at least
1 MiB, but a read-only buffer is not necessarily immutable: the owner
of memoryview(bytearray).toreadonly(), or of a numpy array with
writeable=False, can still write to it from another thread, so the
checksum could cover a mix of old and new contents.

Release the GIL only when the memory belongs to a bytes object, passed
directly or through a memoryview (including slices) of it, so slices of
large bytes payloads are still hashed without the GIL. All other
buffers are hashed with the GIL held, as bytearray already was.
The tests proved only the negative half of the GIL rule: value() keeps the
GIL for a bytearray and for read-only views of one. The property the storage
SDK's off-loop checksum depends on, that hashing a bytes object or a
memoryview into one of 1 MiB or more runs with the GIL released, had no test.
The same probe thread now checks that it runs while value() is in progress for
bytes, a memoryview of bytes and a slice of that view.

The numpy-only line of the Fortran-order helper is excluded from coverage; the
unit session does not install numpy, so it skips before that line and the
cover session would otherwise stop at 99%.

Test plan: with the extension built from this worktree
(google_crc32c.implementation == "c") and with CRC32C_PURE_PYTHON=1:

    python -m pytest packages/google-crc32c/tests --cov=google_crc32c --cov=tests \
        --cov-config=.coveragerc --cov-report=term-missing

180 pass, 4 skipped (numpy), coverage 100%; the three new cases pass on three
consecutive runs. ruff check/format (0.14.14) and flake8 are clean.

Authored with an AI assistant.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request adds support for bytes-like objects (such as bytearray, memoryview, array.array, and mmap) in both the C extension and pure-Python implementations of the CRC32C checksum. It updates argument parsing to accept any C-contiguous object supporting the buffer protocol and refactors GIL release logic to safely verify buffer immutability. Additionally, comprehensive tests are introduced to validate these changes. Feedback on the C extension suggests using a recursive loop to traverse nested memoryview wrappers to ensure the GIL is correctly released for multi-layered views.

Comment thread packages/google-crc32c/src/google_crc32c/_crc32c.c

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants