feat: zero-copy buffer-protocol subject support (mmap, bytearray, array) - #98
Conversation
Port the e08f2ab buffer support from the iforapsy fork: - Add buffer_view_from_object() helper in pcre_ext/util.c to obtain a contiguous memoryview over bytes-like objects without copying. - Wire the zero-copy path into Pattern_execute() and Pattern_create_finditer() so mmap.mmap, bytearray, array.array, and other buffer-protocol objects can be matched directly. - Broaden is_bytes_like() in pcre/re_compat.py to accept buffer-protocol exporters by attempting PyMemoryView_FromObject() (with BufferError handled). - Add UTF-8 validation for buffer-protocol subjects when the pattern has PCRE2_UTF, preventing PCRE2_NO_UTF_CHECK from passing malformed UTF-8 to PCRE2. - Add tests/test_mmap.py exercising match/fullmatch/search/finditer/split/sub against mmap and bytearray subjects, non-contiguous buffer rejection, and lifetime behavior (mmap cannot be closed while a match is alive). The GIL=0 object-lifetime contract is safe because PyMemoryView_FromObject holds the exporter alive and pins its buffer for the lifetime of the match or finditer iterator. Concurrent mutation of writable buffers remains a caller contract, not something the library can prevent; this is documented in the README section added by the original c8e8e4b commit and already exists in the repo's README.
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
||
| def test_split_on_mmap(self): | ||
| pattern = pcre.compile(rb"\s+") | ||
| parts = pattern.split(self.mm[:20]) |
There was a problem hiding this comment.
🟡 Test claims to cover mmap splitting but actually tests plain bytes
The split test passes an already-sliced value (self.mm[:20] at tests/test_mmap.py:81) which is a plain bytes object rather than the memory-mapped file, so the new zero-copy path is never exercised for splitting.
Impact: The feature area appears covered but has no real test, so a future regression in splitting directly on a buffer object would go undetected.
Why the sliced value is bytes, not mmap
Slicing an mmap.mmap (self.mm[:20]) returns a bytes object, not another mmap or memoryview. Therefore pattern.split(self.mm[:20]) splits over a bytes subject and never enters the buffer-protocol branch added in pcre_ext/pcre2.c. To actually exercise the new path the test should split over self.mm directly (e.g. pattern.split(self.mm)), which routes through Pattern.split -> finditer(self.mm) -> the new buffer branch.
| parts = pattern.split(self.mm[:20]) | |
| parts = pattern.split(self.mm) |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good catch — self.mm[:20] returns plain bytes, so the zero-copy buffer path was not exercised. Fixed in 9710449 by passing self.mm directly to pattern.split(). Test still passes and now routes through Pattern.split -> finditer(self.mm) -> the new buffer branch.
Summary
Allow
Pattern.match(),search(),fullmatch(),finditer(),findall(),split(), andsub()to operate directly on any object that implements the Python buffer protocol (e.g.mmap.mmap,bytearray,array.array) without first copying it into abytesobject. The change is safe under GIL=0 because the memoryview that exposes the buffer pins the exporter's storage for the lifetime of the match/finditer iterator.Upstream credit: The buffer-protocol subject support is based on Jack Wong's work in
iforapsy/PyPcre(commite08f2ab). This port rebases it ontomain, adds UTF-8 validation for the zero-copy buffer path, and includes expanded tests.What changed
pcre_ext/util.caddsbuffer_view_from_object(obj, &data, &len), which usesPyMemoryView_FromObjectto borrow a contiguous, single-byte C pointer into the exporter's storage. Callers keep the returned memoryview alive asutf8_owner, which bothPattern_execute()andPattern_create_finditer()now do.pcre_ext/pcre2.cadds aPyObject_CheckBuffer(subject_obj)branch inPattern_execute()andPattern_create_finditer(). It uses the helper above, treats the subject as raw byte data (byte offsets,bytesgroup values), and callsensure_valid_utf8_for_bytes_subject()before thePCRE2_NO_UTF_CHECKfast-path so UTF-8 patterns cannot receive invalid bytes.pcre/re_compat.pybroadensis_bytes_like()to accept other buffer-protocol objects by attemptingmemoryview(value), while still returningFalseforstr.tests/test_mmap.pyadds coverage formmap.mmapandbytearraysubjects acrosspcremodule-level functions and pattern methods, includingfindall/finditer,split/sub,pos/endposbyte offsets, non-contiguous buffer rejection, and the lifetime contract that preventsmmap.close()while a match object is alive.GIL=0 / safety notes
PyMemoryView_FromObjectincrements the exporter's buffer export count and holds the exporter alive. The CMatchObject/FinditerObjectstores the memoryview asutf8_owner, so the buffer stays pinned until the match/finditer is freed.PCRE2_UTFpatterns, matching the existingbytesbehavior, soPCRE2_NO_UTF_CHECKis safe to use.Example