🥷

[UE5]ユーザに秘匿したいモジュール機能をAPIマクロで他のモジュールに公開する

2024/02/28に公開

TL;DL

  1. 公開したいヘッダのクラス名の前に公開用のAPIマクロをつける
  2. 公開側モジュールを、利用側モジュールの依存情報に追加する
  3. 公開側モジュールのルートディレクトリから見たときの相対パスで、ヘッダをインクルードする

動作確認した環境

  • Unreal Engine: 5.3.2
  • OS: macOS 12.6 Monterey (M1 Apple Silicon)

背景

例を挙げて説明します。
ある UE5 プロジェクトで、仮に Awesome モジュールと Flotsum モジュールがあるとします。
Awesome モジュールでは、AwesomeAPI クラスを Private > AwesomeAPI.h で定義しています。
このとき、ヘッダファイルの内容をユーザに見られることを防ぎたい何らかの要求があると仮定し[1]Private/ に設置して秘匿するという状況について考えます。

UEProject/
├── Awesome/ (Type: Game)
│   ├── Private/
│   │   ├── AwesomeAPI.h
│   │   └── AwesomeAPI.cpp
│   └── AwesomeModule.Build.cs
└── Flotsam/ (Type: Editor)
    ├── Private/
    │   ├── FlotsamModule.h
    │   └── FlotsamModule.cpp
    └── FlotsamModule.Build.cs

いま、Flotsum モジュールから AwesomeAPI クラスを利用するとします。
そのために、モジュール依存情報を Flotsum.Build.cs で追加することで Private > AwesomeAPI.h は Flotsum モジュールでインクルード可能になりますが、クラス内の関数を呼び出すと、次のような未定義シンボルエラーが発生してしまいます。

Undefined Symbols for architecture arm64

ここで、試しに AwesomeAPI.hPublic/ に設置してインクルードすると問題なく関数を呼び出せます。
とはいえ、顧客の要求としてヘッダファイルを Public/ に設置するわけにはいきません。

別の方策として、Flotsam モジュールのコードを Awesome モジュールに統合してしまえば Private/ のヘッダにアクセスできますが、保守性が下がってしまいます。何より、これではモジュール開発の恩恵が受けられません。

そこで、冒頭の手順1を行います。

1. 公開したいヘッダのクラス名の前に公開用のAPIマクロをつける

詳しくは補足で述べますが、Unreal Build Tool が生成する公開用のマクロを用いることで、クラスのシンボルが公開されます。

Awesome.h
#pragma once
class AWESOME_API AwesomeAPI
{
public:
// 通常の定義
}

AWESOME_API が API マクロです。
API マクロの名前は、モジュール名をアッパーケースにして "_API" を付加したものです。
Flotsum モジュールなら FLOTSUM_API です。
余談として、UObject 等を継承して UCLASS() 化しても同様のやり方ができます。

2. 公開側モジュールを、利用側モジュールの依存情報に追加する

Flotsum.Build.cs
...
  PrivateDependencyModuleNames.AddRange(new string[] { "Awesome" });
...

今回は Flotsum モジュールの Private/ 下のソースコードで呼び出すと仮定するので、 PrivateDependency に依存情報を追加します。
これは通常と同様です。

3. 公開側モジュールのルートディレクトリから見たときの相対パスで、ヘッダをインクルードする

Flotsum.cpp
#include "Awesome/Private/AwesomeAPI.h"

実は私は途中まで、ファイル名のみを指定していました。
Public/ に設置すればインクルードできてしまいますが、Private/ に設置すると不可能です。

2024/02/29追記:
下の補足でもインクルードについて触れています。よろしければ読んでみてください。

以上の手順を実施したら、リビルド時に Undefined Symbols エラーが発生しないことを確認します。

補足

API マクロの実体と、機能としての信頼性が気になる人は読んでください。

1. APIマクロの実体は何か?

C++ プロジェクトのビルド時に生成される中間ファイルが入った Intermediate/ の中で、Definitions.モジュール名.h (例) が作成され、そこで API マクロが定義されています。

Definitions.Awesome.h
...
#define AWESOME_API DLLEXPORT
...

マクロ名は、Unreal Build Tool の以下のスクリプトで決定されています。

Engine > Source > Programs > UnrealBuildTool > Configuration > UEBuildModule.cs

そして、DLLEXPORT もまたマクロです。
これはプラットフォームごとに定義されていて、例えば MacOS なら Unix ベースの OS ということで UnixPlatform.h の定義が用いられます。
DLLIMPORT も、ここで定義されていますね。

UnixPlatform.h
...
// DLL export and import definitions
#define DLLEXPORT			__attribute__((visibility("default")))
#define DLLIMPORT			__attribute__((visibility("default")))
...

Engine > Source > Runtime > Core > Public > Unix > UnixPlatform.h

もうお分かりですね。これは呼出規約(call convension)です。
API マクロはプラットフォームに応じて適切な呼出規約を定義するマクロということが分かります。

2. APIマクロは推奨手順なのか?

業務開発する上で気になるのは、機能に万が一不具合があった時、Unreal Engine のせいにできるかどうかです。

今回の手順は、現在リンク切れとなっているドキュメントに記載されていたことがあるようです。
例えば、こちらの StackOverflow にはドキュメントの URL と長めの引用を書いている人がいます。
これは 2019年 6月の書き込みなので、少なくとも UE4 の時代には推奨されていた手順かと思われます。

https://stackoverflow.com/questions/56788666/what-is-it-somtemplate-vr-api/56794745?stw=2#56794745

また、上の StackOverflow の引用には興味深い事が書いてあります。

The API macros are mostly used on older code to allow newer modules to access it from their DLL. In newer bits of code, the API macros are used far less, instead setting up nice interface layers to expose functionality across DLL boundaries.

「APIマクロは大抵古いコードで使われていて、新しいモジュールの DLL からアクセスできるようになっています。新しいコードでは、あまり API マクロは使われなくなっていて、代わりにナイスなインタフェース層を設定し、DLL の境界を越えて機能を公開しています。」

これについて、フォーラムでツッコんでいる人がいました。

https://forums.unrealengine.com/t/how-do-i-add-a-module-looking-for-a-thorough-systematic-and-in-depth-explanation-long-post/129431/9

That’s a bit misleading really. It’s true they’ve begun to use the latter approach more, but the bulk of the engine is still reliant on exporting symbols.

「これは少々誤解を招きます。彼ら(Epic)がインタフェースによるアプローチを使い始めたのは本当ですが、Engine のコードの大部分は未だにシンボルのエクスポートに依存しています。」

これは 2019年 8月の書き込みです。
執筆時点で 4 年が経過して UE5 がリリースされていますが、実際に Engine のコードを "_API" で検索すると沢山ヒットするはずです。
GitHub の Engine ソースコードにアクセスできる人は、リポジトリで検索してみてください。
https://github.com/search?q=repo%3AEpicGames%2FUnrealEngine _API&type=code

このように API マクロは内部のレベルでは活用されています。
しかし、ドキュメントが存在しない現在、Epic の推奨手順かどうかについては確証が持てません。
UE5 で動作するのだとしても、ユーザによる利用は想定から外れている可能性もあります。
(※追記あり)

もし、Epic の推奨手順を知っている方は教えて下さい。

3. 【2024/02/29追記】続:APIマクロは推奨手順なのか?

気になって夜も眠れずに調べました。

ドキュメントの言及状況

前節で言及したリンク切れのページはこれです。
https://docs.unrealengine.com/en-US/Programming/Modules/API/index.html

API マクロについて記述のあるドキュメントは無いものかと探すと、4.27 で見つかりました。
しかも、わりと入門のページに。
https://docs.unrealengine.com/4.27/ja/ProgrammingAndScripting/ProgrammingWithCPP/Basics/

Module API
モジュール外からアクセスする必要のある関数とクラスは、*_API マクロによって公開されなければなりません。

この機能は「Module API」と呼ばれていたんですね。

さらに、5.3 の C++ 開発者向けのドキュメントには、クラスウィザードで生成できるサンプルコードがあり、しれっと *_API マクロが記載されています。

https://docs.unrealengine.com/5.3/ja/cpp-only-example/

LightSwitchCodeOnly.h
UCLASS()
    class [PROJECTNAME]_API ALightSwitchCodeOnly : public AActor
    {
    GENERATED_BODY()

    };

クラスウィザードでは、モジュールの指定や、それを Public と Private のどちらに含めるかを指定することもできます。
ただし、同じ書き方とディレクトリ構成で C++ クラスを追加しても結果は一緒のはずです。

4.27 で明示的に推奨されていた API マクロは、5.x ではクラスウィザードが生成するテンプレートに含められることによって、間接的な形で推奨されるようになったと考えるのが自然かもしれません。

Unreal Engine の組み込みプラグインで手順の実績を探す

それでも心許ないので、もう少し積極的な証拠を探します。

Unreal Engine のソースコードには始めから含まれているプラグインがあり、例えば立体音響系だと Resonance Audio や Oculus Audio などのサードパーティーライブラリが入っています。
もちろん、プラグインは開発者が独自に書けるので、どちらかと言えばシステムではなくユーザ寄りのレイヤーのコードです。
もし、Unreal が始めから含んでいるプラグインに私の示した手順が行われていれば、Unreal はその手順を暗黙にそれを認めていると言えるかもしれません。
また、それは言い過ぎであるにしても、従来のプラグインのやり方に「右へならえ」しておくのが穏便だと思います[2]

などと、前置きが長くなりましたが、結論としては上に挙げた Resonance Audio と Oculus Audio では同様の実装を確認できます。

少し違いがあるとすれば、Private Include Paths を通しているところです。
ResonanceAudio の Editor モジュールの *.Build.cs を見てみましょう。

https://github.com/EpicGames/UnrealEngine/blob/5.3.2-release/Engine/Plugins/Runtime/ResonanceAudio/Source/ResonanceAudioEditor/ResonanceAudioEditor.Build.cs

ResonanceAudioEditor.Build.cs
            PrivateIncludePaths.AddRange(
                new string[] {
					Path.Combine(GetModuleDirectory("ResonanceAudio"), "Private"),
					Path.Combine(GetModuleDirectory("ResonanceAudio"), "Private", "ResonanceAudioLibrary"),
					Path.Combine(GetModuleDirectory("ResonanceAudio"), "Private", "ResonanceAudioLibrary", "resonance_audio"),
                }
            );

このようにすることで Private/ にインクルードパスが通り、ファイル名を書くだけでよくなります。
https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Plugins/Runtime/ResonanceAudio/Source/ResonanceAudio/Private/ResonanceAudioModule.cpp#L12

ResonanceAudioModule.cpp
#include "ResonanceAudioSettings.h"

Oculus Audio でも同様の実装を見つけることができます。
具体例は割愛しますので、興味があれば確認してください。

小括

以上を踏まえると、私の手順は Unreal Engine としては明示的に推奨していないものの、Private なヘッダを公開する手順としては、そこまで不味くはないと考えています。

脚注
  1. たとえば、顧客向けのプラグインを受託開発しているような状況です。 ↩︎

  2. このアプローチは上司からアドバイスをもらいました。 ↩︎

Discussion