[PyQt] Bug when PyQt5 sends Python bytes object via signal between threads

Rudolf Cardinal rudolf at pobox.com
Thu Dec 1 11:48:17 GMT 2016


Apologies, the code in the previous message had some of its spaces 
removed. Here's another attempt, below and as an attachment.


#!/usr/bin/env python
# pyqt5_signal_with_bytes.py

import signal
import sys
from PyQt5.QtCore import (
     QCoreApplication, QObject, QThread, pyqtSignal, pyqtSlot
)

THREADED = True
WITH_TEMPORARY_VARIABLES = True

VERBOSE = False

SOURCE_STR_FOR_TEMP = "ab cd ef"
SOURCE_BYTES_FOR_TEMP = b"gh ij kl"

SOURCE_STR_STATIC = ["mn", "op", "qr"]
SOURCE_BYTES_STATIC = [b"st", b"uv", b"wx"]


class Sender(QObject):
     send_bytes = pyqtSignal(bytes)
     send_str = pyqtSignal(str)

     def __init__(self, **kwargs) -> None:
         super().__init__(**kwargs)

     @pyqtSlot()
     def act(self) -> None:
         while True:
             if VERBOSE:
                 print("Sending from thread {}".format(
                     int(QThread.currentThreadId())))
             if WITH_TEMPORARY_VARIABLES:
                 # By iterating through x.split(), we create and send
                 # temporary variables, which have the potential to be
                 # garbage-collected.
                 for s in SOURCE_STR_FOR_TEMP.split(" "):
                     self.send_str.emit(s)
                 for b in SOURCE_BYTES_FOR_TEMP.split(b" "):
                     self.send_bytes.emit(b)
             else:
                 # Here, the objects already exist
                 for s in SOURCE_STR_STATIC:
                     self.send_str.emit(s)
                 for b in SOURCE_BYTES_STATIC:
                     self.send_bytes.emit(b)


class Receiver(QObject):
     def __init__(self, **kwargs) -> None:
         super().__init__(**kwargs)

     @pyqtSlot(bytes)
     def on_bytes(self, data: bytes) -> None:
         if VERBOSE:
             print("on_bytes received: {} [thread {}]".format(
                 repr(data), int(QThread.currentThreadId())))
         if ((WITH_TEMPORARY_VARIABLES and
                 data not in SOURCE_BYTES_FOR_TEMP.split(b" ")) or
                 (not WITH_TEMPORARY_VARIABLES and
                     data not in SOURCE_BYTES_STATIC)):
             print("FAILURE: on_bytes received {}".format(repr(data)))

     @pyqtSlot(str)
     def on_str(self, data: str) -> None:
         if VERBOSE:
             print("on_str received: {} [thread {}]".format(
                 repr(data), int(QThread.currentThreadId())))
         if ((WITH_TEMPORARY_VARIABLES and
                 data not in SOURCE_STR_FOR_TEMP.split(" ")) or
                 (not WITH_TEMPORARY_VARIABLES and
                     data not in SOURCE_STR_STATIC)):
             print("FAILURE: on_str received {}".format(repr(data)))


def main() -> None:
     signal.signal(signal.SIGINT, signal.SIG_DFL)  # respond to CTRL-C
     app = QCoreApplication(sys.argv)
     sender = Sender(parent=app)
     if THREADED:
         receiver = Receiver()
         thread = QThread(app)
         receiver.moveToThread(thread)
         thread.start()
     else:
         receiver = Receiver(parent=app)
     sender.send_bytes.connect(receiver.on_bytes)
     sender.send_str.connect(receiver.on_str)
     if THREADED:
         thread.started.connect(sender.act)
     else:
         sender.act()
     sys.exit(app.exec_())


if __name__ == '__main__':
     main()


"""
- Test environment:
         Python 3.5.2 [GCC 5.4.0 20160609] on linux
         pip install PyQt5==5.7

- FAILS with only this combination:
         bytes (not str)
         THREADED = True
         WITH_TEMPORARY_VARIABLES = True

- The failure messages have included:
         FAILURE: on_bytes received b'\xb0C\x8a\x01'
         FAILURE: on_bytes received b''

- This suggests that bytes objects are being corrupted in transit when 
they are
   created as temporary variables and passed through threads, which 
suggests,
   perhaps, that they are being garbage-collected before/during receipt?

- Why?
     - PyQt5 translates signal parameters into C++ objects
http://pyqt.sourceforge.net/Docs/PyQt5/signals_slots.html

     - It looks like a Python 3 bytes object is translated into "const 
char*"
         ... line 419 of 
https://github.com/baoboa/pyqt5/blob/master/qpy/QtCore/qpycore_chimera.cpp

         ... note that QMetaType::UnknownType is 0
             - http://doc.qt.io/qt-5/qmetatype.html#Type-enum
         ... whereas lines 421-2 assigns _metatype = -1; _name = "const 
char*";

         ... anyway, I'm not sure how the PyQt5 system should be managing
             reference counts here (INCREF etc.), and it looks like it 
does try:
                 Py_INCREF((PyObject *)_py_type);

     - The other places of interest look like:

         - transmission: qpycore_pyqtsignal*.cpp

         - receipt: line 139 of 
https://github.com/baoboa/pyqt5/blob/master/qpy/QtCore/qpycore_pyqtslot.cpp
             PyObject *arg = (*it)->toPyObject(*++qargs);

             - calls Chimera::toPyObject
                 ? relevant bit is line 1318 of qypycore_chimera.cpp:
                 return toPyObject(const_cast<void *>(var.data()));

- Anyway, not sure, but there's a bug.

"""  # noqa

-------------- next part --------------
A non-text attachment was scrubbed...
Name: pyqt5_signal_with_bytes.py
Type: text/x-python
Size: 4876 bytes
Desc: not available
URL: <https://www.riverbankcomputing.com/pipermail/pyqt/attachments/20161201/09e70659/attachment.py>


More information about the PyQt mailing list