本記事は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要素(以下コンポーネントと呼ぶ)の実装をアプリケーションコードに追加してウインドウ内に配置することができます。
juce::MessageManager
クラス
JUCEでは、GUIを駆動するメッセージスレッド(UIスレッド)の仕組みをjuce::MessageManager
クラス*1によって提供します。
メッセージスレッドでは、ユーザー操作をGUI要素に渡す処理や、GUI要素のイベント処理を駆動するレスポンス処理などを実行します
弱参照について
複雑な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
の機能を説明するために作成したものであり、実際のアプリケーション実装を想定するものではないため、さほど参考にはならないかと思いますが、参考事例として参照ください。
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);
}