Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Each reads `if (!PHPQT_PROBE_OFF("<id>") && (<check>))`, marks its throw line `/

| Site | Calls | Outcome |
|---|---|---|
| `qapp-widget`, `qapp-text-widget`, `qapp-slider`, `qapp-table`, `qapp-splitter`, `qapp-gl-painter`, `qapp-menubar`, `qapp-menu` | widget constructors before a `QApplication` | qFatal abort |
| `qapp-widget`, `qapp-text-widget`, `qapp-slider`, `qapp-table`, `qapp-tree-widget`, `qapp-splitter`, `qapp-gl-painter`, `qapp-menubar`, `qapp-menu` | widget constructors before a `QApplication` | qFatal abort |
| `qguiapp-window`, `qguiapp-pixmap`, `qguiapp-pixmap-from-image` | `QWindow`, `QPixmap`, `QPixmap::fromImage` before a `QGuiApplication` | qFatal abort |
| `qguiapp-vulkan-create` | `QVulkanInstance::create()` before a `QGuiApplication` | SIGSEGV |
| `main-thread-macos` | an application, widget or `QWindow` constructor off the process main thread, macOS only | AppKit's uncaught NSException |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ One PHP object per QObject, guarded by QPointer; PHP deletes what it created unl

## Values

Qt's value classes (`QPoint`, `QPointF`, `QSize`, `QRect`, `QDate`, `QKeySequence`, `QUrl`, `QByteArray`, `QFont`, `QPixmap`, `QImage`, `QColor`, `QBrush`, the text values) and `QTableWidgetItem` are not QObjects. They use `phpqt_value_object { void *ptr; bool constructed; bool owned; void (*destroy)(void *); …; zend_object std; }`: a heap copy the PHP object owns and deletes through `destroy`. Copies in, copies out: `QWidget::font()` returns a new wrapper around a copy; `setFont()` copies the wrapped value in. No identity map; two reads give two objects. A second `__construct()` is refused by `phpqt_value_fresh`.[^values]
Qt's value classes (`QPoint`, `QPointF`, `QSize`, `QRect`, `QDate`, `QKeySequence`, `QUrl`, `QByteArray`, `QFont`, `QPixmap`, `QImage`, `QColor`, `QBrush`, the text values), `QTableWidgetItem` and `QTreeWidgetItem` are not QObjects. They use `phpqt_value_object { void *ptr; bool constructed; bool owned; void (*destroy)(void *); …; zend_object std; }`: a heap copy the PHP object owns and deletes through `destroy`. Copies in, copies out: `QWidget::font()` returns a new wrapper around a copy; `setFont()` copies the wrapped value in. No identity map; two reads give two objects. A second `__construct()` is refused by `phpqt_value_fresh`.[^values]

A `QImage` made from an address borrows that buffer, as Qt does: the image holds no copy, and the buffer (a `QByteArray`'s `pointer()`, a framebuffer's) must outlive every image sharing it. Its cleanup function is a `PhpSlot` holding the callable and the bound `cleanupInfo`; Qt's `cleanupInfo` is a `QPointer` to that slot, so after request end, when the slot is gone, nothing runs.

A value may also be borrowed from an owner (`phpqt_value_borrow`): a `QLayoutItem` read from a layout reads as deleted once the layout no longer lists it (`QLayout::indexOf`).

`QTableWidgetItem` is the one value with a second owner. Once `QTableWidget::setItem()` stored the item (`item->tableWidget()` is that table), the table owns it (`owned = false`); an item another table holds stays that table's, as Qt warns. The item is a `PhpTableWidgetItem` subclass whose destructor clears its wrapper's `ptr`, so a call after `clearContents()` or a row removal throws `QtException("QTableWidgetItem has been deleted by Qt")` instead of reading freed memory. `item(r, c)` and `horizontalHeaderItem(c)` return the wrapper the item already has, or a new table-owned one. Items Qt makes itself (header labels, a cell a user types into) are clones of an item prototype the `QTableWidget` constructor installs, so their wrappers learn of deletion the same way. A cell outside the table is refused (site `table-item-range`: Qt drops the item unstored, its view pointer on the table).[^values]
`QTableWidgetItem` and `QTreeWidgetItem` are the values with a second owner. Once `QTableWidget::setItem()` stored the item (`item->tableWidget()` is that table), the table owns it (`owned = false`); an item another table holds stays that table's, as Qt warns. The item is a `PhpTableWidgetItem` subclass whose destructor clears its wrapper's `ptr`, so a call after `clearContents()` or a row removal throws `QtException("QTableWidgetItem has been deleted by Qt")` instead of reading freed memory. `item(r, c)` and `horizontalHeaderItem(c)` return the wrapper the item already has, or a new table-owned one. Items Qt makes itself (header labels, a cell a user types into) are clones of an item prototype the `QTableWidget` constructor installs, so their wrappers learn of deletion the same way. A cell outside the table is refused (site `table-item-range`: Qt drops the item unstored, its view pointer on the table).[^values]

`QTreeWidgetItem` follows the same pattern. Once `QTreeWidget::addTopLevelItem()` or `QTreeWidgetItem::addChild()` stored the item (afterwards `item->treeWidget()` is that tree with no parent, or `child->parent()` is that item), the tree or parent owns it. Qt ignores an item that already has a tree or a parent, and that item stays where it was. The item is a `PhpTreeWidgetItem` whose destructor clears its wrapper's `ptr`. Qt's base destructor then deletes the children, and each child clears its own wrapper. So after `clear()`, after the tree goes, or after the last reference to a PHP-owned parent is dropped, a call on any of those items throws `QtException("QTreeWidgetItem has been deleted by Qt")`. `child()`, `parent()`, `currentItem()`, `selectedItems()` and the signal arguments return the wrapper the item already has, or a new tree-owned one. Only PHP adds items: Qt's header item is not bound, and Qt clones items only for drag and drop, which no bound call turns on.

`addChild()` passes through Qt's own behaviour for a cycle. An item added under itself or under one of its descendants, while neither belongs to a tree, makes Qt's subtree walk (`propagateDisabled`) loop forever. Probed 2026-10-18: the process spins with flat memory and does not crash, so this is not a refusal under rule 3.

`QTextEdit\ExtraSelection` is a plain object, the struct: typed public `cursor` and `format` properties holding PHP values, copied into Qt's list by `setExtraSelections()`.

Expand Down
4 changes: 2 additions & 2 deletions ecosystem/standalone-extensions/qt/architecture/signals.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ String-signature connect through a moc-free dynamic slot; argument marshalling;

`PhpSlot` = a `QObject` subclass holding a PHP callable. `QObject::connect()` normalises the signature (`QMetaObject::normalizedSignature`), finds the signal with `indexOfSignal`, and calls `QMetaObject::connect(sender, index, slot, QObject::staticMetaObject.methodCount(), Qt::DirectConnection)`. Activation arrives in `PhpSlot::qt_metacall`; after `QObject::qt_metacall` subtracts QObject's own methods the id is 0, which invokes the callable. No moc, no generated metaobject. A signal the class lacks gets what Qt's own string connect gives: Qt's warning and the invalid connection.[^qobject]

Arguments, typed by the signal's `QMetaMethod::parameterMetaType`: bool, integer and float types as PHP scalars; `QString`/`QByteArray` as strings; `QDate`, `QPoint`, `QPointF`, `QSize`, `QRect`, `QUrl` and `QKeySequence` as copies in their PHP classes (`QDateEdit::dateChanged` hands a `QDate` over); `QSocketDescriptor` as the int fd; enums as ints (`QMediaPlayer::MediaStatus` arrives as the `QMediaPlayer\MediaStatus` case value); `QObject*` boxed. A `QObject*` that is the emitting sender maps to the sender's own wrapper even during `destroyed()`, when Qt has already cleared its QPointers.[^runtime]
Arguments, typed by the signal's `QMetaMethod::parameterMetaType`: bool, integer and float types as PHP scalars; `QString`/`QByteArray` as strings; `QDate`, `QPoint`, `QPointF`, `QSize`, `QRect`, `QUrl`, `QKeySequence`, `QBrush`, `QColor`, `QFont` and `QPixmap` as copies in their PHP classes (`QDateEdit::dateChanged` hands a `QDate` over); `QSocketDescriptor` as the int fd; enums as ints (`QMediaPlayer::MediaStatus` arrives as the `QMediaPlayer\MediaStatus` case value); `QObject*` boxed; `QTreeWidgetItem*` as the wrapper PHP holds for the item (a new tree-owned one if none), null as null. The same conversion (`phpqt_variant_to_zval`) reads a `QVariant`: `QTreeWidgetItem::data()` returns what it holds, null when it holds nothing or a type with no PHP class. A `QObject*` that is the emitting sender maps to the sender's own wrapper even during `destroyed()`, when Qt has already cleared its QPointers.[^runtime]

Lifetime:

Expand All @@ -17,7 +17,7 @@ Lifetime:
* `QImage`'s cleanup function: a slot with one bound argument (`cleanupInfo`), fired once on its own thread (queued there from another), then freed.
* RSHUTDOWN deletes every remaining slot (disconnecting it) and frees its callable while the engine can still free it.

`QEventFilter` is a `PhpSlot` subclass overriding `eventFilter(watched, event)`: it shares the slot's call path and request-end teardown, hands PHP every event as `(watched, event)` and returns PHP's bool to Qt. The callable reads `$event->type()` itself.
`QEventFilter` is a `PhpSlot` subclass overriding `eventFilter(watched, event)`: it shares the slot's call path and request-end teardown, hands PHP every event as `(watched, event)` and returns PHP's bool to Qt. The callable reads `$event->type()` itself. `phpqt_box_event` boxes the most specific bound class by `dynamic_cast`, in order: `QContextMenuEvent`, `QMouseEvent`, `QKeyEvent`, `QWheelEvent` (a `QSinglePointEvent`, not a `QMouseEvent`, so it needs its own branch), `QInputEvent`, else `QEvent`.

`QMetaObject::invokeMethod()` converts PHP arguments the other way, to the parameter types of the member of that name and arity (`phpqt_zval_to_variant`), the value classes included, and makes the native call with the connection type given.

Expand Down
22 changes: 22 additions & 0 deletions ecosystem/standalone-extensions/qt/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,30 @@

What changed in this package, newest first.

### 2026-10-23

* `QWidget::clearFocus`. No new site. Shipped in tag v0.10.5 (`PHP_QT_VERSION` 0.10.5; there are no v0.10.3 or v0.10.4 tags). [surface](/docs/{{version}}/ecosystem/standalone-extensions/qt/guide/binding-surface)

### 2026-10-22

* `QWidget` `focusPolicy` / `setFocusPolicy` (`Qt\FocusPolicy`, generated with its checks) and static `setTabOrder`; `QKeyEvent` `text`, `isAutoRepeat`, `nativeScanCode`, `nativeVirtualKey`. No new site. Shipped in tag v0.10.5 (`PHP_QT_VERSION` 0.10.5; there are no v0.10.3 or v0.10.4 tags). [surface](/docs/{{version}}/ecosystem/standalone-extensions/qt/guide/binding-surface)
* Coverage: `scripts/support.php` writes `SUPPORT.md` (every public member function name of QtCore, QtGui and QtWidgets classes, from clang++'s AST dump of each public class header; signals found through `QT_ANNOTATE_ACCESS_SPECIFIER`; a stub method on a subclass counts for the base that declares it, through every public base) and the README API badge; `--check` compares without the date. 570 / 11435 on Qt 6.11.2.
* `QMessageBox` `addButton`, `setDefaultButton`, `setEscapeButton`, `clickedButton`; `QFileDialog` (its constructor at site `qapp-file-dialog`), `setFileMode`, `setAcceptMode`, `setOption`, `setDirectory`, `selectFile`, `selectedFiles`; `QGuiApplication::clipboard` (`?QClipboard`: Qt warns and returns nullptr before an application, and lives, so no refusal), `QClipboard` `setText`, `text`; the generated `QMessageBox\ButtonRole`, `QFileDialog\FileMode`, `AcceptMode`, `Option`, `QClipboard\Mode`; shipped in tag v0.10.5 (`PHP_QT_VERSION` 0.10.5; there are no v0.10.3 or v0.10.4 tags). An unshown native `QFileDialog` keeps no selection; Qt's own dialog (`DONT_USE_NATIVE_DIALOG`) does. [surface](/docs/{{version}}/ecosystem/standalone-extensions/qt/guide/binding-surface)

### 2026-10-20

* `QWidget` `window`, `windowFlags`, `setWindowFlags`, `windowModality`, `setWindowModality`; the generated `Qt\WindowModality`; `QPushButton` `isDefault`, `setDefault`; shipped in tag v0.10.5 (`PHP_QT_VERSION` 0.10.5; there are no v0.10.3 or v0.10.4 tags). `setParent` keeps its one-argument form: PHP holds it to `QObject::setParent(?QObject)`, so the flags of a child window go through `setWindowFlags`. [surface](/docs/{{version}}/ecosystem/standalone-extensions/qt/guide/binding-surface)

### 2026-10-19

* `QWheelEvent` (constructor, `pixelDelta`, `angleDelta`, `position`, `globalPosition`, `source`; filters box wheels as it), the generated `Qt\ScrollPhase` and `Qt\MouseEventSource`, and `QStyleHints::mouseDoubleClickInterval` / `mouseDoubleClickDistance`, shipped in tag v0.10.5 (`PHP_QT_VERSION` 0.10.5; there are no v0.10.3 or v0.10.4 tags). [surface](/docs/{{version}}/ecosystem/standalone-extensions/qt/guide/binding-surface), [signals](/docs/{{version}}/ecosystem/standalone-extensions/qt/architecture/signals)

### 2026-10-18

* `QTreeWidget` and `QTreeWidgetItem`. Shipped in tag v0.10.5 (`PHP_QT_VERSION` 0.10.5; there are no v0.10.3 or v0.10.4 tags). Item lifetime as for table items: `PhpTreeWidgetItem`, owned by the tree or parent once Qt stored it. `QTreeWidgetItem*` signal arguments arrive as the item's wrapper. `data()` reads a QVariant through the signal-argument conversion. `QBrush`, `QColor`, `QFont` and `QPixmap` now cross both ways as QVariants and as signal arguments. Also `QAbstractItemView\ScrollHint`, `QTreeWidgetItem\ChildIndicatorPolicy`, `QKeyEvent` (filters box key presses and releases as it), and the generated `Qt\ItemDataRole` and `Qt\Key`. The constructor carries the `qapp-tree-widget` site. [surface](/docs/{{version}}/ecosystem/standalone-extensions/qt/guide/binding-surface), [object model](/docs/{{version}}/ecosystem/standalone-extensions/qt/architecture/object-model), [signals](/docs/{{version}}/ecosystem/standalone-extensions/qt/architecture/signals)
* Event constructors refuse ints wider than their C type instead of truncating them: `QKeyEvent` key (int) and modifiers, `QMouseEvent` button, buttons and modifiers, `QContextMenuEvent` modifiers (32-bit masks). [surface](/docs/{{version}}/ecosystem/standalone-extensions/qt/guide/binding-surface)
* `scripts/gen-enum.php` handles three more cases: deprecation messages that hold commas, digit names (`KEY_0`), and names that differ only in case (`DEAD_a` / `DEAD_A`). Its output for the enums already generated is unchanged. [generating enums](/docs/{{version}}/ecosystem/standalone-extensions/qt/runbooks/generating-enums)

* Qt 1:1 (0.10.2): every method is one native call under Qt's name, parameters and defaults. Value classes `QPoint`, `QPointF`, `QSize`, `QRect`, `QDate`, `QKeySequence`, `QByteArray` replace arrays and strings; `QThread`, `QLayoutItem`, `QPointingDevice`, `QCalendarWidget`; window flags on the widget constructors; applications take `(int &$argc, array $argv, int $flags)`; `invokeMethod` takes the connection type; `QTextEdit\ExtraSelection` is the struct; `QImage` borrows its buffer with Qt's cleanup function; `QMenu::popup(QPoint)`, `addMenu(title)`, `QUrl::toString(options)`; abstract classes are abstract. `QSurfaceFormat::setDefaultFormat` and `QCoreApplication::setAttribute`/`testAttribute` with `Qt\ApplicationAttribute`: a GL widget composites only when its context shares with the window's, which on macOS means the same profile as Qt's default. [surface](/docs/{{version}}/ecosystem/standalone-extensions/qt/guide/binding-surface), [layout](/docs/{{version}}/ecosystem/standalone-extensions/qt/guide/layout-and-geometry), [object model](/docs/{{version}}/ecosystem/standalone-extensions/qt/architecture/object-model)
* Qt answers for itself: refusals stay only where a probe showed a crash, each with its `bindings.json` site and a test in `tests/Probes` (`--enable-qt-probes`); the main-thread and second-application checks are macOS-only. [errors and threads](/docs/{{version}}/ecosystem/standalone-extensions/qt/architecture/errors-and-threads), [build](/docs/{{version}}/ecosystem/standalone-extensions/qt/runbooks/build-install-test)
* Signal arguments of the value types reach PHP in their classes; `QEventFilter` hands over every event as `(watched, event)`. [signals](/docs/{{version}}/ecosystem/standalone-extensions/qt/architecture/signals)
Expand Down
Loading
Loading