PaperSloth’s diary

主にゲーム開発関連についての記事を書きます。

UE4 外部ライブラリの使用方法について

この記事はUnreal Engine 4(UE4) Advent Calendar 2019の22日目の投稿記事です。
Unreal Engine 4 (UE4)のカレンダー | Advent Calendar 2019 - Qiita


目次

環境

UE4.23.1
Visual Studio 2017 Community

結論

最初に結論だけを書きます。
Static Library, Dynamic-link Libraryどちらの場合も編集するのは .Build.cs です。
・共通(Static Libraryはこれで設定完了)

// インクルードファイルへのpath 情報を追加
// .h/.hpp ファイルを配置したディレクトリへのpathを記述
PublicSystemIncludePaths.Add(Path.Combine(LibraryPath, "include"));
// ライブラリへのpath 情報を追加
// .lib ファイルを格納したディレクトリへのpath を記述
PublicLibraryPaths.Add(Path.Combine(LibraryPath, "lib", "x64"));
// ライブラリをプロジェクトに追加
// .lib ファイルの名前を記述
PublicAdditionalLibraries.Add("LirbraryName.lib");


・Dynamic-link Library

// ライブラリをプロジェクトに追加
// .dll ファイルの名前を記述
 PublicDelayLoadDLLs.Add("LibraryName.dll");
// プロジェクトのパッケージング情報に.dll の情報を追加し、ファイルコピー
// .dll ファイルを格納したディレクトリへのpath を記述
 RuntimeDependencies.Add(Path.Combine(LibraryPath, "lib", "x64");

概要

今回はUE4で外部のライブラリ(Static Library, Dynamic-link library)の使用方法について書きます。
使用するライブラリは自作した簡単なライブラリを使用します。
Static Libraryはint の足し算を行うだけのもの : ExampleLib
Dynamic-link Libraryはint の引き算を行うだけのもの : ExampleDll
この2つを用意しました。
わざわざライブラリ化する意味は全くありませんが、使用方法の参考例として用意しました。

ライブラリの作成方法についてはUE4の話から逸れるため、作成方法などについては省略します。
下記ドキュメントを参考にしてください。
チュートリアル: スタティック ライブラリを作成して使用する (C++) | Microsoft Learn
Visual Studio で C/C++ DLL を作成する | Microsoft Learn

Static Libraryの作成

設定等は大幅に省いてざっくりとコードだけ載せます。

こちらも設定等は省いてコードだけ。

UE4でのLibraryの使用方法

ようやくUE4の話に戻ります。
まずは新規にUE4プロジェクトを作成します。


続いて使用するライブラリをそれぞれプロジェクトに紐付けるために適切なディレクトリに移します。
(ProjectRoot)\ThirdParty\ 以下にファイルを置くのが一般的でしょう。
ThirdPartyというフォルダは存在しないため、新規作成します。

例 :
(ProjectRoot)\ThirdParty\(LibraryName)\include\
(ProjectRoot)\ThirdParty\(LibraryName)\lib\(PlatformName)\

今回は以下のように配置しました。
・ Static Library
(ProjectRoot)\ThirdParty\ExampleLib\include\
(ProjectRoot)\ThirdParty\ExampleLib\lib\x64\

・Dynamic-link Library
(ProjectRoot)\ThirdParty\ExampleDll\include\
(ProjectRoot)\ThirdParty\ExampleDll\lib\x64

ライブラリに更新が入った時には
Generate Visual Studio project filesでプロジェクトを再生成しています。
これが正しいフローなのかどうかは疑問が残りますが、これでライブラリの更新は反映されます。

ライブラリのリンクについて編集するのは基本的に下記のファイルのみです。
(ProjectRoot)\Source\(ProjectName)\(ProjectName).Build.cs
今回の場合だと (ProjectRoot)\Source\LibrarySandbox\LibrarySandbox.Build.cs
コメント等を削除して、初期の記述状態は下記になります。

using UnrealBuildTool;

public class LibrarySandbox : ModuleRules
{
    public LibrarySandbox(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });

        PrivateDependencyModuleNames.AddRange(new string[] { });
    }
}

ライブラリの使用準備にあたってThirdPartyへのFullPath取得のPropertyを追加します。

// Path使用のため追加
using System.IO;

public class LibrarySandbox : ModuleRules
{
    // 追加
    private string ThirdPartyPath
    {
        get { return Path.GetFullPath(Path.Combine(ModuleDirectory, "../../ThirdParty/")); }
    }
...
}

Static Libraryの使用方法

Static Libraryを使用するにあたっては下記へのファイル追加が必要です
・PublicSystemIncludePaths
・PublicLibraryPaths
・PublicAdditionalLibraries

includeファイルのディレクトリの追加、ライブラリへのpath の追加、ライブラリの追加です
今回の例では下記のようになります。

public class LibrarySandbox : ModuleRules
{
    ...
    public LibrarySandbox(ReadOnlyTargetRules Target) : base(Target)
    {
        ...
        PrivateDependencyModuleNames.AddRange(new string[] { });

        // 下記を追加
        string ExampleLibPath = Path.Combine(ThirdPartyPath, "ExampleLib");
        PublicSystemIncludePaths.Add(Path.Combine(ExampleLibPath, "include"));

        PublicLibraryPaths.Add(Path.Combine(ExampleLibPath, "lib", "x64"));
        PublicAdditionalLibraries.Add("ExampleLib.lib");
...
}

では実際に使用できるかコードを書いて確かめてみます。
GameModeのctorから呼び出してみます。

#include "LibrarySandboxGameModeBase.h"
#if PLATFORM_WINDOWS
#include "ExampleLib.h"
#include <memory>
#endif

ALibrarySandboxGameModeBase::ALibrarySandboxGameModeBase()
{
#if PLATFORM_WINDOWS
    const auto helloLib(std::make_shared<ExampleLib::HelloLib>());
    UE_LOG(LogTemp, Log, TEXT("Hello Static Library World : 1 + 2 = %d"), helloLib->Add(1, 2));
#endif
}

実行してみると無事に動作することが確認できました。

LogInit: FAudioDevice initialized.
LogLoad: Game class is 'LibrarySandboxGameModeBase'
LogTemp: Hello Static Library World : 1 + 2 = 3

手順としてはPublicSystemIncludePaths, PublicLibraryPaths, PublicAdditionalLibrariesに設定を記述するだけでプロジェクトから使用できるため比較的お手軽です。

Dynamic-link Libraryを使用するにあたっては下記へのファイル追加が必要です
・PublicSystemIncludePaths
・PublicLibraryPaths
・PublicAdditionalLibraries
・PublicDelayLoadDLLs
・RuntimeDependencies

includeファイルのディレクトリの追加、ライブラリへのpath の追加、ライブラリの追加、ライブラリのパッケージング時のコピー

今回の例では下記のようになります。

public class LibrarySandbox : ModuleRules
{
    ...
    public LibrarySandbox(ReadOnlyTargetRules Target) : base(Target)
    {
        ...
        PrivateDependencyModuleNames.AddRange(new string[] { });

        // 下記を追加
        string exampleDllRootPath = Path.Combine(ThirdPartyPath, "ExampleDll");
        PublicSystemIncludePaths.Add(Path.Combine(exampleDllRootPath, "include"));

        string exampleDllPath = Path.Combine(exampleDllRootPath, "lib", "x64");
        PublicLibraryPaths.Add(exampleDllPath);
        PublicAdditionalLibraries.Add("ExampleDll.lib");

        string exampleDllName = "ExampleDll.dll";
        string exampleDllFullPath = Path.Combine(exampleDllPath, exampleDllName);
        PublicDelayLoadDLLs.Add(exampleDllName);
        RuntimeDependencies.Add(exampleDllFullPath);
...
}

実際に使用できるかコードを書いて確かめてみます。
GameModeのctorから呼び出してみます。

#include "LibrarySandboxGameModeBase.h"
#if PLATFORM_WINDOWS
#include "ExampleDll.h"
#include <memory>
#endif

ALibrarySandboxGameModeBase::ALibrarySandboxGameModeBase()
{
#if PLATFORM_WINDOWS
    const auto helloDll(std::make_shared<ExampleDll::HelloDll>());
    UE_LOG(LogTemp, Log, TEXT("Hello Dynamic-link Library World : 2 - 1 = %d"), helloDll->Sub(2, 1));
#endif
}

実行してみるとエラーが出ます。
delayhlp.cpp の中でKernelBase.dllで例外が発生したと表示されます。
これはBinaries/Win64/ 以下にdll が存在せずEditorから実行できません。
ですのでdll をBinaries/(Platforms)/以下にコピーするコードを追加します。

using System;
using System.IO;

public class LibrarySandbox : ModuleRules
{
    ...
    public LibrarySandbox(ReadOnlyTargetRules Target) : base(Target)
    {
        ...
        string exampleDllName = "ExampleDll.dll";
        string exampleDllFullPath = Path.Combine(exampleDllPath, exampleDllName);
        PublicDelayLoadDLLs.Add(exampleDllName);
        RuntimeDependencies.Add(exampleDllFullPath);
        CopyDll(exampleDllName, exampleDllFullPath);
    }

    // Binaries以下にdll をコピーする
    private void CopyDll(string dllName, string dllFullPath)
    {
        if (!File.Exists(dllFullPath))
        {
            Console.WriteLine("file {0} does not exist", dllName);
            return;
        }
        string binariesDir = Path.Combine(ModuleDirectory, "../../Binaries/Win64/");
        if (!Directory.Exists(binariesDir))
        {
            Directory.CreateDirectory(binariesDir);
        }
        string binariesDllFullPath = Path.Combine(binariesDir, dllName);
        if (File.Exists(binariesDllFullPath))
        {
            File.SetAttributes(binariesDllFullPath, File.GetAttributes(binariesDllFullPath) & ~FileAttributes.ReadOnly);
        }
        // try catchでくくっていないとパッケージ作成時にこけます
        try
        {
            File.Copy(dllFullPath, binariesDllFullPath, true);
        }
        catch (Exception ex)
        {
            Console.WriteLine("failed to copy file: {0}", dllName);
        }
    }
}


これで無事に動作することが確認できました。

LogInit: FAudioDevice initialized.
LogLoad: Game class is 'LibrarySandboxGameModeBase'
LogTemp: Hello Dynamic-link Library : 2 - 1 = 1

手順としてはPublicSystemIncludePaths, PublicLibraryPaths, PublicAdditionalLibraries, に設定を記述することに加えて
PublicDelayLoadDLLs, RuntimeDependenciesが追加されました。
RuntimeDependenciesは記述がなくとも動作しますが
これを設定していないとプロジェクトのパッケージング時にそのフォルダにDLLがコピーされないため
DLLがなくて動作しないという問題を避けることができます。

パッケージング後のディレクトリはWindows PC向けパッケージでは下記のようになります。
WindowsNoEditor\LibraySandbox.exe
WindowsNoEditor\LibrarySandbox\ThirdParty\ExampleDll\lib\x64\ExampleDll.dll


最後にbuild.cs と使用例を全文載せておきます。