From 18119cdebe473d411a82385fec4119849271f9ec Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 Jan 2026 02:01:37 -0500 Subject: [PATCH] Make container/pyio pure --- av/container/{pyio.pyx => pyio.py} | 128 ++++++++++++++++++----------- 1 file changed, 81 insertions(+), 47 deletions(-) rename av/container/{pyio.pyx => pyio.py} (52%) diff --git a/av/container/pyio.pyx b/av/container/pyio.py similarity index 52% rename from av/container/pyio.pyx rename to av/container/pyio.py index c8b82f96a..ccacc01e8 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.py @@ -1,16 +1,25 @@ -cimport libav as lib -from libc.string cimport memcpy +# type: ignore +import cython +from cython import NULL +from cython.cimports import libav as lib +from cython.cimports.av.error import stash_exception +from cython.cimports.libc.stdint import int64_t, uint8_t +from cython.cimports.libc.string import memcpy -from av.error cimport stash_exception +Buf = cython.typedef(cython.pointer[uint8_t]) +BufC = cython.typedef(cython.pointer[cython.const[uint8_t]]) -ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil +seek_func_t = cython.typedef( + "int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil" +) -cdef class PyIOFile: +@cython.cclass +class PyIOFile: def __cinit__(self, file, buffer_size, writeable=None): self.file = file - cdef seek_func_t seek_func = NULL + seek_func: seek_func_t = NULL readable = getattr(self.file, "readable", None) writable = getattr(self.file, "writable", None) @@ -21,39 +30,43 @@ def __cinit__(self, file, buffer_size, writeable=None): self.ftell = getattr(self.file, "tell", None) self.fclose = getattr(self.file, "close", None) - # To be seekable the file object must have `seek` and `tell` methods. + # To be seekable, the file object must have `seek` and `tell` methods. # If it also has a `seekable` method, it must return True. if ( self.fseek is not None and self.ftell is not None and (seekable is None or seekable()) ): - seek_func = pyio_seek + seek_func: seek_func_t = pyio_seek if writeable is None: writeable = self.fwrite is not None if writeable: if self.fwrite is None or (writable is not None and not writable()): - raise ValueError("File object has no write() method, or writable() returned False.") + raise ValueError( + "File object has no write() method, or writable() returned False." + ) else: if self.fread is None or (readable is not None and not readable()): - raise ValueError("File object has no read() method, or readable() returned False.") + raise ValueError( + "File object has no read() method, or readable() returned False." + ) self.pos = 0 self.pos_is_valid = True # This is effectively the maximum size of reads. - self.buffer = lib.av_malloc(buffer_size) + self.buffer = cython.cast(cython.p_uchar, lib.av_malloc(buffer_size)) self.iocontext = lib.avio_alloc_context( self.buffer, buffer_size, writeable, - self, # User data. + cython.cast(cython.p_void, self), # User data. pyio_read, pyio_write, - seek_func + seek_func, ) if seek_func: @@ -61,30 +74,37 @@ def __cinit__(self, file, buffer_size, writeable=None): self.iocontext.max_packet_size = buffer_size def __dealloc__(self): - with nogil: + with cython.nogil: # FFmpeg will not release custom input, so it's up to us to free it. # Do not touch our original buffer as it may have been freed and replaced. if self.iocontext: - lib.av_freep(&self.iocontext.buffer) - lib.av_freep(&self.iocontext) + lib.av_freep(cython.address(self.iocontext.buffer)) + lib.av_freep(cython.address(self.iocontext)) - # We likely errored badly if we got here, and so are still - # responsible for our buffer. + # We likely errored badly if we got here, and so we are still responsible. else: - lib.av_freep(&self.buffer) + lib.av_freep(cython.address(self.buffer)) -cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) noexcept nogil: - with gil: +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def pyio_read(opaque: cython.p_void, buf: Buf, buf_size: cython.int) -> cython.int: + with cython.gil: return pyio_read_gil(opaque, buf, buf_size) -cdef int pyio_read_gil(void *opaque, uint8_t *buf, int buf_size) noexcept: - cdef PyIOFile self - cdef bytes res + +@cython.cfunc +@cython.exceptval(check=False) +def pyio_read_gil(opaque: cython.p_void, buf: Buf, buf_size: cython.int) -> cython.int: + self: PyIOFile + res: bytes try: - self = opaque + self = cython.cast(PyIOFile, opaque) res = self.fread(buf_size) - memcpy(buf, res, len(res)) + memcpy( + buf, cython.cast(cython.p_void, cython.cast(cython.p_char, res)), len(res) + ) self.pos += len(res) if not res: return lib.AVERROR_EOF @@ -93,16 +113,24 @@ def __dealloc__(self): return stash_exception() -cdef int pyio_write(void *opaque, const uint8_t *buf, int buf_size) noexcept nogil: - with gil: +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def pyio_write(opaque: cython.p_void, buf: BufC, buf_size: cython.int) -> cython.int: + with cython.gil: return pyio_write_gil(opaque, buf, buf_size) -cdef int pyio_write_gil(void *opaque, const uint8_t *buf, int buf_size) noexcept: - cdef PyIOFile self - cdef bytes bytes_to_write - cdef int bytes_written + +@cython.cfunc +@cython.exceptval(check=False) +def pyio_write_gil( + opaque: cython.p_void, buf: BufC, buf_size: cython.int +) -> cython.int: + self: PyIOFile + bytes_to_write: bytes + bytes_written: cython.int try: - self = opaque + self = cython.cast(PyIOFile, opaque) bytes_to_write = buf[:buf_size] ret_value = self.fwrite(bytes_to_write) bytes_written = ret_value if isinstance(ret_value, int) else buf_size @@ -112,19 +140,25 @@ def __dealloc__(self): return stash_exception() -cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) noexcept nogil: - # Seek takes the standard flags, but also a ad-hoc one which means that - # the library wants to know how large the file is. We are generally - # allowed to ignore this. +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def pyio_seek(opaque: cython.p_void, offset: int64_t, whence: cython.int) -> int64_t: + # Seek takes the standard flags, but also a ad-hoc one which means that the library + # wants to know how large the file is. We are generally allowed to ignore this. if whence == lib.AVSEEK_SIZE: return -1 - with gil: + with cython.gil: return pyio_seek_gil(opaque, offset, whence) -cdef int64_t pyio_seek_gil(void *opaque, int64_t offset, int whence): - cdef PyIOFile self + +@cython.cfunc +def pyio_seek_gil( + opaque: cython.p_void, offset: int64_t, whence: cython.int +) -> int64_t: + self: PyIOFile try: - self = opaque + self = cython.cast(PyIOFile, opaque) res = self.fseek(offset, whence) # Track the position for the user. @@ -144,18 +178,19 @@ def __dealloc__(self): return stash_exception() -cdef int pyio_close_gil(lib.AVIOContext *pb): +@cython.cfunc +def pyio_close_gil(pb: cython.pointer[lib.AVIOContext]) -> cython.int: try: return lib.avio_close(pb) - except Exception: stash_exception() -cdef int pyio_close_custom_gil(lib.AVIOContext *pb): - cdef PyIOFile self +@cython.cfunc +def pyio_close_custom_gil(pb: cython.pointer[lib.AVIOContext]) -> cython.int: + self: PyIOFile try: - self = pb.opaque + self = cython.cast(PyIOFile, pb.opaque) # Flush bytes in the AVIOContext buffers to the custom I/O lib.avio_flush(pb) @@ -164,6 +199,5 @@ def __dealloc__(self): self.fclose() return 0 - except Exception: stash_exception()