Skip to content

frontend: Use qobject_cast when casting QObjects#11784

Open
cg2121 wants to merge 1 commit into
obsproject:masterfrom
cg2121:replace-dynamic-cast
Open

frontend: Use qobject_cast when casting QObjects#11784
cg2121 wants to merge 1 commit into
obsproject:masterfrom
cg2121:replace-dynamic-cast

Conversation

@cg2121
Copy link
Copy Markdown
Contributor

@cg2121 cg2121 commented Jan 28, 2025

Description

When casting between QObjects, qobject_cast should always be used. This increases performance as there is no RTTI (Run Time Type Information) with qobject_cast, like there is with dynamic_cast.

Using reinterpret_cast is bad practice, as you should use it only in very specific cases.

Motivation and Context

Better code

How Has This Been Tested?

compiled and ran OBS

Types of changes

  • Bug fix (non-breaking change which fixes an issue) -->
  • Performance enhancement (non-breaking change which improves efficiency)

Checklist:

  • My code has been run through clang-format.
  • I have read the contributing document.
  • My code is not on the master branch.
  • The code has been tested.
  • All commit messages are properly formatted and commits squashed where appropriate.
  • I have included updates to all appropriate documentation.

@cg2121 cg2121 force-pushed the replace-dynamic-cast branch from eb3f82d to 5582ea1 Compare January 28, 2025 11:49
@cg2121 cg2121 changed the title frontend: Use qobject_cast instead of dynamic_cast frontend: Use qobject_cast when casting QObjects Jan 28, 2025
@cg2121 cg2121 added kind/bug Categorizes issue or PR as related to a bug. area/ui-ux Anything to do with changes or additions to UI/UX elements. labels Jan 28, 2025
@cg2121 cg2121 marked this pull request as draft January 28, 2025 12:50
@cg2121
Copy link
Copy Markdown
Contributor Author

cg2121 commented Jan 28, 2025

Converting to draft, as #11785 needs to be merged first.

@cg2121 cg2121 marked this pull request as ready for review May 2, 2025 05:39
@cg2121 cg2121 force-pushed the replace-dynamic-cast branch from 5582ea1 to 7808d32 Compare May 2, 2025 05:41
@cg2121 cg2121 force-pushed the replace-dynamic-cast branch from 7808d32 to a9342f8 Compare May 5, 2025 18:34
@cg2121
Copy link
Copy Markdown
Contributor Author

cg2121 commented May 5, 2025

This PR now builds correctly

@cg2121 cg2121 requested a review from PatTheMav May 5, 2025 18:47
@cg2121 cg2121 force-pushed the replace-dynamic-cast branch 2 times, most recently from 3623034 to c2398aa Compare May 5, 2025 18:51
@RytoEX RytoEX self-assigned this May 5, 2025
Copy link
Copy Markdown
Member

@PatTheMav PatTheMav left a comment

Choose a reason for hiding this comment

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

Overall I'm fine with this change, though it obviously touches a whole lot casts in the code that touch a whole lot of aspects of our frontend code, so this might be too late for the next version but should possibly merged after the next version is stabilised to give it some time to exist on the master branch.

@RytoEX WDYT?

@RytoEX
Copy link
Copy Markdown
Member

RytoEX commented Nov 19, 2025

Overall I'm fine with this change, though it obviously touches a whole lot casts in the code that touch a whole lot of aspects of our frontend code, so this might be too late for the next version but should possibly merged after the next version is stabilised to give it some time to exist on the master branch.

@RytoEX WDYT?

I think this should be merged for 32.1, merge timing TBD. It does have a merge conflict though.

@Warchamp7 Warchamp7 force-pushed the replace-dynamic-cast branch from c2398aa to 6789390 Compare November 27, 2025 22:09
@Warchamp7
Copy link
Copy Markdown
Member

I've rebased this PR

@Warchamp7 Warchamp7 moved this from In Review to Ready For Review in OBS Studio 32.1 PR Considerations Nov 27, 2025
@Warchamp7 Warchamp7 moved this from Ready For Review to Ready For Merge in OBS Studio 32.1 PR Considerations Nov 27, 2025
@RytoEX RytoEX requested review from Warchamp7 and gxalpha December 18, 2025 01:37
@RytoEX RytoEX added this to the OBS Studio 32.1 milestone Dec 18, 2025
@RytoEX RytoEX force-pushed the replace-dynamic-cast branch from 6789390 to 4b832ef Compare December 18, 2025 01:38
@RytoEX RytoEX self-requested a review December 18, 2025 02:43
Copy link
Copy Markdown
Member

@gxalpha gxalpha left a comment

Choose a reason for hiding this comment

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

Just looking at it, I think most of these should use static_cast instead of either dynamic_cast or qobject_cast. qobject_cast is better than dynamic_cast if you want the dynamic behavior (i.e. have the object be null if the cast fails), but if you already know/assume that the cast will succeed (and dont check for new_obj == nullptr), you can just use static_cast.

Also, original git commit authorship appears to have gotten lost, that should probably be reverted.

@RytoEX RytoEX moved this from Ready For Merge to In Review in OBS Studio 32.1 PR Considerations Dec 18, 2025
@RytoEX RytoEX moved this from In Review to Requires Changes in OBS Studio 32.1 PR Considerations Dec 18, 2025
@Warchamp7 Warchamp7 force-pushed the replace-dynamic-cast branch from 4b832ef to 7b82eec Compare December 25, 2025 16:56
@Warchamp7
Copy link
Copy Markdown
Member

Just looking at it, I think most of these should use static_cast instead of either dynamic_cast or qobject_cast. qobject_cast is better than dynamic_cast if you want the dynamic behavior (i.e. have the object be null if the cast fails), but if you already know/assume that the cast will succeed (and dont check for new_obj == nullptr), you can just use static_cast.

Also, original git commit authorship appears to have gotten lost, that should probably be reverted.

Adjusted the PR for this, as well as removed a macro in the AutoConfig files that were casting the wizard() method in favor of an inline method on those pages.

@Warchamp7 Warchamp7 force-pushed the replace-dynamic-cast branch from 7b82eec to f6596c5 Compare December 25, 2025 17:12
@Warchamp7 Warchamp7 moved this from Requires Changes to Ready For Review in OBS Studio 32.1 PR Considerations Jan 6, 2026
Comment thread frontend/wizards/AutoConfigStreamPage.cpp Outdated
@Warchamp7 Warchamp7 added the kind/cleanup Non-breaking change which makes code smaller or more readable label May 6, 2026
@RytoEX RytoEX requested review from PatTheMav and gxalpha May 7, 2026 20:57
@RytoEX
Copy link
Copy Markdown
Member

RytoEX commented May 7, 2026

This requires a rebase.

@Warchamp7 Warchamp7 force-pushed the replace-dynamic-cast branch from f6596c5 to 5c91477 Compare May 8, 2026 02:51
@Warchamp7
Copy link
Copy Markdown
Member

This requires a rebase.

Rebased and conflicts resolved, however I've also added a to-be-squashed commit with some additional casts that were either introduced in the rebase, or missed originally.

@Warchamp7 Warchamp7 force-pushed the replace-dynamic-cast branch from 5c91477 to f6cc03e Compare May 8, 2026 02:54
Copy link
Copy Markdown
Member

@gxalpha gxalpha left a comment

Choose a reason for hiding this comment

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

I haven't gone through all the files (only about one third) but the idea should be clear.
There are still a lot of qobject_casts that should be static_casts. qobject_cast is only needed where it isn't clear (or assumed) that the cast succeeds. Especially in cases where the pointer is dereferenced immediately after the cast, qobject_cast doesn't make sense. Also in some cases, instead of casting from the main window it should just be OBSBasic::Get().
Also commit authorship is still changed.

Comment thread frontend/settings/OBSBasicSettings.cpp Outdated
Comment thread frontend/components/VolumeControl.cpp
Comment thread frontend/dialogs/OBSBasicSourceSelect.cpp Outdated
Comment thread frontend/importer/ImporterEntryPathItemDelegate.cpp Outdated
Comment thread frontend/utility/audio-encoders.cpp Outdated
Comment thread frontend/widgets/OBSBasic.cpp Outdated
Comment thread frontend/widgets/OBSBasic_Clipboard.cpp Outdated
Comment thread frontend/widgets/OBSBasic_ContextToolbar.cpp Outdated
@Warchamp7
Copy link
Copy Markdown
Member

I haven't gone through all the files (only about one third) but the idea should be clear. There are still a lot of qobject_casts that should be static_casts. qobject_cast is only needed where it isn't clear (or assumed) that the cast succeeds. Especially in cases where the pointer is dereferenced immediately after the cast, qobject_cast doesn't make sense. Also in some cases, instead of casting from the main window it should just be OBSBasic::Get(). Also commit authorship is still changed.

Commit authorship is intentional since at this point it contains changes from both of us.

Will address all the feedback.

@Warchamp7 Warchamp7 force-pushed the replace-dynamic-cast branch 2 times, most recently from 15aa946 to 87c6774 Compare May 8, 2026 18:48
Comment thread shared/properties-view/properties-view.cpp Outdated
When casting between QObjects, qobject_cast should always be used for dynamic
casts. This increases performance as there is no RTTI (Run Time Type
Information) with qobject_cast, like there is with dynamic_cast. In other cases
where we are sure or assume the cast is valid, use static_cast.

Using reinterpret_cast is bad practice, as you should use it in very
specific cases.

Co-Authored-By: Clayton Groeneveld <19962531+cg2121@users.noreply.github.com>
@Warchamp7 Warchamp7 force-pushed the replace-dynamic-cast branch from 87c6774 to 677a0d8 Compare May 10, 2026 18:55
Copy link
Copy Markdown
Member

@gxalpha gxalpha left a comment

Choose a reason for hiding this comment

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

Seems correct

Copy link
Copy Markdown
Member

@PatTheMav PatTheMav left a comment

Choose a reason for hiding this comment

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

Even though it might seem superfluous to use qobject_cast or dynamic_cast without checking the result for nullptr after, even a missing nullptr check will create deterministic crash points (mainly when that pointer is dereferenced).

But as the crash by UpdateContextBar demonstrates, static_cast will produce a valid pointer and thus dereferencing will succeed and if both types happen to have identical member or method names (like GetSource) code might potentially succeed and produce corrupted data or crash at arbitrary points later.

Otherwise I agree that where no checks and balances are used (and code is pretty blasé about safety so far anyway) using static_cast directly seems more "honest".

QLayoutItem *la = ui->emptySpace->layout()->itemAt(0);
if (la) {
if (SourceToolbar *toolbar = dynamic_cast<SourceToolbar *>(la->widget())) {
if (SourceToolbar *toolbar = static_cast<SourceToolbar *>(la->widget())) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This change crashes OBS with a media source because the cast always succeeds and then toolbar->GetSource() will interpret arbitrary memory in the MediaControls object as an OBSWeakSource and crash deep within libobs's reference counting system.

As can be seen below, this entire code depends on this cast failing if the widget is indeed a MediaSource and not a SourceToolbar (hence the branch below), so dynamic_cast or qobject_cast is necessary here.

: QDialog(parent),
ui(new Ui::OBSYoutubeActions),
apiYouTube(dynamic_cast<YoutubeApiWrappers *>(auth)),
apiYouTube(static_cast<YoutubeApiWrappers *>(auth)),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will introduce a potential crash if a TwitchAuth or any other object with the common Auth base class is passed into the constructor.

I'd prefer to keep the dynamic_cast here and add a TODO to refactor this (and open a new GitHub issue to keep track).

IMO the constructor should require a YoutubeApiWrappers object here and should not even allow a base class object to be passed in (it doesn't really make sense from an API design point of view).

It's the caller's responsibility to properly upcast the generic Auth object OBSBasic provides into the YoutubeApiWrappers pointer and potentially abort if that isn't possible.

Auth *auth = OBSBasic::Get()->GetAuth();
if (auth) {
YoutubeApiWrappers *apiYouTube(dynamic_cast<YoutubeApiWrappers *>(auth));
YoutubeApiWrappers *apiYouTube(static_cast<YoutubeApiWrappers *>(auth));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm unsure about this, as Auth can be any other derived auth object apart from YoutubeApiWrappers which just inherits from Auth and adds a whole lot of additional functionality.

Indeed this whole function's purpose is to return the "API object" (so probably a whole lot of stuff other than OAuth-related functionality), so it needs to fail if the object in memory cannot be cast into YoutubeApiWrappers (and otherwise the if (apiYouTube) conditional doesn't make much sense).

Once again I'd probably opt for keeping dynamic_cast here and add a TODO for refactoring (probably decoupling the API implementation from the OAuth implementation to make this safer).

void ImporterEntryPathItemDelegate::updateText()
{
QLineEdit *lineEdit = dynamic_cast<QLineEdit *>(sender());
QLineEdit *lineEdit = static_cast<QLineEdit *>(sender());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there value to add a small comment here (and throughout this PR) to explain and point out why the type can be assumed safely, referencing the base types in an associated UI file or where the associated widget is initialised?

In cases like these it would be helpful to establish the logical connection between this event handler and the code that sets up the handler, in a way pointing out that

if that code over there is changed, this code over here will crash

which might otherwise get lost in the thousands of lines of code we have.

This would apply to all correct uses of static_cast throughout, like:

  • Casting to a specific layout type from generic QLayout - this creates an implicit link between the UI file (or explicit UI setup code) creating and setting the specific layout sub-type and the static_cast.
  • Casting to a specific model type from QAbstractItemModel - QWidgets have a setModel method that allow new models to be set, so if any API method outside of the abstract item model is used, why can we be "sure" that it's the specific item model we're casting to (and are we sure that nobody else calls setModel ever?).
  • Casting any generic QWidget to a more specific type - similar situation to the cast to a specific QLayout.

The main purpose would be to create a paper trail for these connections that are non-obvious in code, as the compiler will not be able to catch these mistakes.

OBSBasicSettings::OBSBasicSettings(QWidget *parent)
: QDialog(parent),
main(qobject_cast<OBSBasic *>(parent)),
main(static_cast<OBSBasic *>(parent)),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This change will allow creation of OBSBasicSettings objects with arbitrary QWidgets as parent pointers, leading to potential crashes at arbitrary points when main is interacted with in a way that is only possible with an actual OBSBasic instance.

The qobject_cast could at least ensure that main is a nullptr, but then the constructor body should actually check if main is not nulled and throw an exception otherwise. Instead it currently calls EnableOutputs unconditionally.

So if it is indeed required that OBSBasicSettings can only ever be instantiated with an OBSBasic parent, that should be explicitly communicated by the API instead of accepting an arbitrary QWidget pointer. This is a class of errors and constraints that we can and should enforce at compile time.

void OBSBasicSettings::OnOAuthStreamKeyConnected()
{
OAuthStreamKey *a = reinterpret_cast<OAuthStreamKey *>(auth.get());
OAuthStreamKey *a = static_cast<OAuthStreamKey *>(auth.get());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess it's "fine" for now because every OAuth implementation in the program currently uses OAuthStreamKey.

But wouldn't it make more sene if the pointer is passed into the function directly, like so:

	if (type == Auth::Type::OAuth_StreamKey || type == Auth::Type::OAuth_LinkedAccount) {
        OAuthStreamKey *streamKey = static_cast<OAuthStreamKey *>(auth.get());

		OnOAuthStreamKeyConnected(streamKey);
	}

At least OBSBasicSettings::OnAuthConnected runs some checks and balances here, but it also requires that whatever ui->service->currentText() returns as the UI label for the service selection is already in sync with the global auth object (again there is no other subclass of it yet, but as designed there could be). 🫠

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

Labels

area/ui-ux Anything to do with changes or additions to UI/UX elements. kind/bug Categorizes issue or PR as related to a bug. kind/cleanup Non-breaking change which makes code smaller or more readable

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants