LoginSignup
1
2

More than 1 year has passed since last update.

juce::Component::SafePointer を活用して安全なGUI非同期処理を実装しよう

Last updated at Posted at 2022-11-30

本記事はJUCE Advent Calendar 2022 の12月1日向けに投稿した記事です。

対象バージョン

  • JUCE 7.0.2
  • Visual Studio 2022
  • Xcode 14.1
  • CMake 3.15以上
  • Git 2.0以上

まとめ

  • juce::Componentの弱参照をjuce::Component::SafePointerで生成できる。
  • 非同期処理でjuce::Componentのインスタンスにアクセスする時はSafePointer経由で生存確認をしよう。

非同期処理とは

非同期処理は、GUIの設計と実装において頻出する概念です。

引用: https://developer.mozilla.org/ja/docs/Glossary/Asynchronous

非同期とは、 2 つ以上の事象が同時に発生したり、関連する複数の事象が互いの完了を待たずに発生したりする概念を指します(前のものが完了するのを待たずに複数の関連するものが発生することもあります)。コンピュータの世界では、「非同期」という言葉は主に 2 つの文脈で使われています。
...

非同期処理を上手に利用することで、特定のスレッドの処理待ちによるユーザー体験の低下などの影響を抑えることができます。

非同期処理の怖いところ

前述の通り、非同期処理をアプリケーションの設計に取り入れることは、ユーザー体験を向上することに繋がります。しかしその一方で、非同期処理を実装する際には注意しなければならない点があります。非同期で行っている処理とそれ以外の処理、特に異なるスレッド同士の間で同じデータ・オブジェクトを取り扱う際には、設計段階でそれらを安全にハンドリングすることを意識する必要が出てきます。
C,C++言語では、無効なリソースを操作しようとする行為はプログラムの実行時クラッシュの原因となるため、非同期処理に苦手意識を持っている方も少なくないのではないでしょうか。

JUCEとは

JUCE (Jules' Utility Class Extensions)は、C++言語によるマルチメディア系アプリケーションの開発を支援するフレームワークです。
ライブラリの一部モジュールとして独自のGUIシステムを実装しており、開発者はそのAPIを利用することで、クロスプラットフォームなGUIアプリケーションを統一したコードベースで実装することができます。

JUCEのGUIシステムについて

juce::Componentクラス

多くのGUIフレームワークでは、ウインドウ内にGUI要素(コンポーネント、ウィジェット、アイコン等)を配置していくことでGUIを構築する仕組みを採用しています。JUCEフレームワークにおいても考え方は同様です。
JUCEでは、GUI要素の基本実装はjuce::Componentクラス*1によって提供します。開発者は、juce::Componentクラスを継承したクラスを宣言・定義をすることで、独自のGUI要素(以下コンポーネントと呼ぶ)の実装をアプリケーションコードに追加してウインドウ内に配置することができます。

*1 juce::Componentクラス

juce::MessageManagerクラス

JUCEでは、GUIを駆動するメッセージスレッド(UIスレッド)の仕組みをjuce::MessageManagerクラス*1によって提供します。
メッセージスレッドでは、ユーザー操作をGUI要素に渡す処理や、GUI要素のイベント処理を駆動するレスポンス処理などを実行します

*1 juce::MessasgeManagerクラス

弱参照について

複雑なGUIアプリケーションを実装するプログラミングにおいて、弱参照を利用することで、リソースへの危険なアクセスを回避することができるようになります。
JUCEでは、juce::WeakReferenceテンプレートが弱参照の機能を提供します。

引用:https://ja.wikipedia.org/wiki/%E5%BC%B1%E3%81%84%E5%8F%82%E7%85%A7

弱い参照(英: weak reference、ウィークリファレンス)あるいは弱参照とは、参照先のオブジェクトをガベージコレクタから守ることのできない参照のことである。弱い参照からのみによって参照されるオブジェクトは到達不可能とみなされ、従っていつでも解放することができる。弱い参照は、通常の参照(強い参照、強参照)による諸問題を解決するために用いられる。
...

juce::Component::SafePointerを使って安全にjuce::Componentを操作する

非同期処理を実装する際に、GUI要素を操作する時点で「リソースへの危険なアクセスになってないか?」を検出・回避することが重要なポイントです。
juce::Component::SafePointerクラスは、juce::Componentインスタンスの弱参照として機能し、危険なアクセスを回避することができ、安全なGUIアプリケーションを実装することをサポートします。

Exampleコード

SafePointerを介してGUI要素にアクセスすることで、juce::Componentインスタンスの生存確認を安全に確認する事例を示します。
このExampleは、あくまでもSafePointerの機能を説明するために作成したものであり、実際のアプリケーション実装を想定するものではないため、さほど参考にはならないかと思いますが、参考事例として参照ください。

image.png

Unsafe Action ボタンの挙動

  • 関数スコープ内でjuce::Componentインスタンスを生成する
  • juce::MessageManagerにラムダ式の非同期処理タスクを渡す。この時、変数キャプチャでjuce::Componentインスタンスの生ポインタを渡す
  • 関数スコープを抜ける際にjuce::Componentインスタンスが削除される
  • juce::MessageManager内で、予め渡したタスクが実行される
  • 変数キャプチャで渡された変数ではjuce::Componentインスタンスの削除を検知できないので、無効なリソースへのアクセスをしてしまう
  • プログラムがクラッシュする

Safe Action ボタンの挙動

  • 関数スコープ内でjuce::Componentインスタンスを生成する
  • juce::MessageManagerにラムダ式の非同期処理タスクを渡す。この時、変数キャプチャでjuce::Componentインスタンスのポインタをラップするjuce::Component::SafePointerオブジェクトをコピーして渡す
  • 関数スコープを抜ける際にjuce::Componentインスタンスが削除される
  • juce::MessageManager内で、予め渡したタスクが実行される
  • 変数キャプチャで渡されたjuce::Component::SafePointer変数ではjuce::Componentインスタンスの削除を検知することができるので、無効なリソースへのアクセスを回避することができる

■ MainComponent.h

#pragma once

#include <JuceHeader.h>

//==============================================================================
class MainComponent  : public juce::Component
{
public:
    //==============================================================================
    MainComponent();
    ~MainComponent() override;

    //==============================================================================
    void paint (juce::Graphics&) override;
    void resized() override;

private:
    //==============================================================================
    std::unique_ptr<juce::TextButton> buttonUnsafeAction;
    std::unique_ptr<juce::TextButton> buttonSafeAction;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

■ MainComponent.cpp

#include "MainComponent.h"

//==============================================================================
MainComponent::MainComponent()
{
    buttonUnsafeAction = std::make_unique<juce::TextButton>("Unsafe Action");
    buttonUnsafeAction->onClick = []() {
        auto temporary_component = std::make_unique<juce::Component>("Temporary");
        // 非同期処理でGUIスレッドからタスクを実行する
        juce::MessageManager::callAsync([comp = temporary_component.get()]() {
            if (comp == nullptr)
            {
                // comp変数の値は更新されていないため non nullptr であり、ここの処理は通らない。
                juce::AlertWindow::showMessageBoxAsync(juce::AlertWindow::InfoIcon,
                    "Message",
                    "comp variable is nullptr");
                return;
            }
                
            // ここの処理を通ってしまう。
            comp->getName();
        });
    };
    addAndMakeVisible(buttonUnsafeAction.get());

    buttonSafeAction = std::make_unique<juce::TextButton>("Safe Action");
    buttonSafeAction->onClick = []() {
        auto temporary_component = std::make_unique<juce::Component>("Temporary");
        // 非同期処理でGUIスレッドからタスクを実行する
        juce::MessageManager::callAsync(
            // SafePointer で弱参照を生成して非同期処理のタスクに渡す
            [safe_comp = juce::Component::SafePointer<juce::Component>(temporary_component.get())]() {
            if (safe_comp == nullptr)
            {
                // SafePointerの機能によってsafe_comp変数の値が更新されていて nullptr であり、ここの処理を通る。
                juce::AlertWindow::showMessageBoxAsync(juce::AlertWindow::InfoIcon, 
                    "Message",
                    "safe_comp object was deleted");
                return;
            }

            // ここの処理は通らない。
            safe_comp->getName();
        });
    };
    addAndMakeVisible(buttonSafeAction.get());

    setSize (600, 400);
}

MainComponent::~MainComponent()
{
}

//==============================================================================
void MainComponent::paint (juce::Graphics& g)
{
    g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
}

void MainComponent::resized()
{
    auto area = getLocalBounds();

    const auto rect_button_unsafe = area.withHeight(80).withWidth(120).withCentre({ 200, 200 });
    const auto rect_button_safe = area.withHeight(80).withWidth(120).withCentre({ 400, 200 });

    buttonUnsafeAction->setBounds(rect_button_unsafe);
    buttonSafeAction->setBounds(rect_button_safe);
}
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2