이 문서에서는 라이브러리 추가를 위한 표준 패턴, 다이내믹 라이브러리에 대한 특별 고려 사항, 종속성 준비, 타사 라이브러리를 언리얼 프로젝트에 통합하는 동안 발생할 수 있는 오류에 대한 유용한 정보를 포함한 타사 라이브러리를 통합하는 방법에 대해 설명합니다.
언리얼 엔진(UE) 소스 코드에는 UnrealEngine/Engine/Source/ThirdParty/..
에 저장된 몇 가지 타사 라이브러리가 포함됩니다. 이는 엔진 모듈에 일반적인 방식이지만 필수는 아닙니다. 타사 라이브러리를 사용하는 플러그인을 개발할 때는 플러그인 디렉터리에 타사 소프트웨어를 포함하는 것이 더 편리합니다.
타사 플러그인 템플릿
에디터의 플러그인 브라우저에는 타사 라이브러리를 통합하기 위한 템플릿이 있습니다. 템플릿을 사용해서 새로운 플러그인을 생성하려면 플러그인 브라우저 창에서 새 플러그인(New Plugin) 을 선택하고 타사 플러그인(Third Party Plugin) 템플릿이 나올 때까지 아래로 스크롤합니다.
모듈 설정
일반 언리얼 엔진 C++ 모듈은 .build.cs
파일을 사용하여 구성되며 타사 라이브러리도 동일합니다. 소스 코드가 없고 다른 모듈에서 사용하기 위한 모듈을 만들려면 다음과 같이 플러그인에 대한 .build.cs
를 만듭니다.
using System;
using System.IO;
using UnrealBuildTool;
public class MyThirdPartyLibrary : ModuleRules
{
public MyThirdPartyLibrary(ReadOnlyTargetRules Target) : base(Target)
{
Type = ModuleType.External;
// 설정해야 하는 매크로 추가
PublicDefinitions.Add("WITH_MYTHIRDPARTYLIBRARY=1");
// 플러그인의 포함 경로 추가
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "inc"));
// 임포트 라이브러리 또는 스태틱 라이브러리 추가
PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "lib", "foo.a"));
}
}
.build.cs
는 엔진이 Engine/Source
또는 MyProject/Source
등의 모듈을 찾는 일반 폴더에 있어야 합니다.
ModuleType.External 설정은 엔진이 소스 코드를 찾거나 컴파일하지 않도록 지시합니다. 컴파일 환경에 나열된 포함 경로를 추가하고 적절한 매크로를 설정하고, 주어진 스태틱 라이브러리에 대해 링크하여 해당 파일에 정의한 다른 설정을 사용합니다.
다이내믹 라이브러리
Windows
Windows에는 DLL 로드를 위한 꽤 고정적인 모델이 있습니다. 각 실행 파일 또는 DLL은 의존하는 DLL 목록을 가져오기 테이블 에 저장하고 운영 체제는 이러한 종속성을 충족하기 위해 모듈을 로드할 때 이 목록을 스캔합니다.
종속성 DLL 이름은 경로 정보 없이 이름으로만 저장되며, 운영 체제는 경로의 짧은 목록을 검색하여 파일을 찾습니다. 애플리케이션이 종속 DLL 위치를 지정할 수 없을 때 이런 현상이 일어나므로 시작 시 모호한 오류의 원인이 될 수 있습니다.
FPlatformProcess::GetDllHandle
엔진에 의해 명시적으로 트리거된 모든 DLL 로드는 'FPlatformProcess::GetDllHandle()' 함수를 사용합니다. 함수에는 로드하기 전에 DLL 가져오기 테이블을 읽는 특수 로직이 있으며, 엔진의 검색 경로 목록의 파일(예: 모든 프로젝트, 엔진 및 플러그인 '바이너리' 디렉터리)에 대한 DLL 종속성 해결을 시도합니다.
운영 체제에서 새 DLL을 로드할 때 동일한 이름의 DLL이 이미 메모리에 있는 경우 디스크에서 새 모듈을 로드하려고 시도하지 않고 해당 DLL에 링크합니다.
'GetDllHandle' 함수는 종속성을 로드하지 못한 경우 로그에 자세한 정보 표시 출력을 생성하며, 이는 오류를 트래킹하는 데 굉장히 유용할 수 있습니다.
DLL 딜레이 로드
DLL이 엔진에서 정상적으로 검색되지 않는 경로에 있다면 이를 대체할 전략은 로드를 딜레이하는 것입니다. 운영 체제는 함수를 처음 호출할 때만 DLL을 로드하려고 시도하므로 명시적으로 로직을 실행하여 특정 위치에서 DLL을 로드할 수 있습니다. 운영 체제가 딜레이 로드를 수행하게 되는 상황이 오면 디스크에서 검색하는 대신 프로세스에 이미 로드된 동일한 이름의 기존 DLL을 찾아 해당 DLL로 해결합니다.
딜레이 로드는 가져온 함수가 실제 DLL을 로드하는 썽크 함수를 가리키게 하여 작동합니다. 실제 DLL이 로드된 후 가져오기 테이블은 썽크 함수가 아닌 실제 DLL 함수 주소를 가리키도록 수정되며 실행은 정상적으로 계속됩니다.
DLL의 변수에 액세스하게 되면 이 메커니즘은 불가능합니다. 이런 식으로 참조되는 DLL 로드를 딜레이하려고 하면 링커에서 오류가 발생합니다.
딜레이 로드된 DLL은 다음과 같은 선언을 사용해서 build.cs
파일에서 지정할 수 있습니다.
PublicDelayLoadDLLs.Add("foo.dll")
참고: 링커는 운영 체제가 가져오기 테이블에 추가하는 DLL 종속성을 해결하는 방법과 관련이 없기 때문에 DLL 경로는 필요하지 않으며, 이름만 사용됩니다.
DLL 로딩 문제 디버깅
Dependency Walker는 모듈에서 가져온 DLL 및 함수의 검사에 유용합니다. 자세한 정보 표시 엔진 로그 출력에는 'Dependency Walker' 툴도 포함됩니다.
macOS
macOS 실행 파일과 dylib는 의존하는 dylib 목록을 설치 이름 형식으로 저장합니다. 설치 이름은 dylib에 대한 절대 또는 상대 경로이거나, 혹은 다음 세 가지 설치 경로 중 하나에 대한 상대 경로일 수 있습니다: @executable_path
, @load_path
또는 @rpath
. 이러한 옵션 중 가장 유연한 옵션은 @rpath
이며, 언리얼 엔진에서 사용하는 옵션도 이것입니다.
언리얼 빌드 툴은 Engine/Source
및 MyProject/Source
하위 폴더 외부의 모든 타사 dylib에 대해 빌드하는 실행 파일 및 dylib에 RPATH
검색 경로를 자동으로 추가합니다. Source
하위 폴더에 타사 dylib를 저장하는 것은 지원은 되지만 권장되진 않습니다. 이러한 폴더는 패키지로 만든 게임이나 플러그인의 바이너리 버전이 아니므로 빌드 시스템에서 다르게 처리하고 다른 위치에 복사해야 합니다.
설치 이름은 링크하는 동안 dylib에서 읽으므로 타사 라이브러리의 설치 이름은 @rpath/libfoo.dylib
로 설정해야 합니다. 라이브러리 빌드 시 -install_name
링커 옵션을 사용하거나 다음과 같이 dylib가 생성된 후 수정하기 위해 install_name_tool
을 사용하는 두 가지 방법을 사용할 수 있습니다:
install_name_tool -id @rpath/libfoo.dylib /path/to/libfoo.dylib
프레임워크의 경우 .build.cs
파일에서 PublicAdditionalLibraries
대신 PublicFrameworks
를 사용해야 하며, 그 외에는 동일한 규칙이 적용되므로 프레임워크 설치 이름에도 @rpath
를 사용하는 것이 좋습니다.
DLL 딜레이 로드
DLL 딜레이 로드 기능은 macOS에서 완전히 지원되지 않습니다.
Windows의 /DELAYLOAD
에 해당하는 것이 없으므로 macOS에서는 런타임에 사용할 수 없는 라이브러리와 링크하는 특정 용도만 지원됩니다. 이를 구현하기 위해 언리얼 엔진은 약한 링크를 사용합니다.
dylib 로딩 문제 디버깅
명령줄에서 otool
은 실행 파일 또는 dylib를 검사하고 런타임 종속성을 확인하는 데 유용합니다. 특히 -L
및 -l
옵션은 로딩 문제를 디버깅할 때 유용합니다.
명령 | 설명 |
---|---|
otool -L libname.dylib |
실행 파일 또는 dylib가 의존하는 모든 dylib의 설치 이름 및 버전 번호를 나열합니다. dylib의 경우 목록의 첫 번째 항목은 자체 설치 이름입니다. |
otool -l libname.dylib |
파일의 모든 로드 명령을 표시합니다. LC_LOAD_DYLIB 및 LC_RPATH 를 찾아 다이내믹 링커가 자동으로 로드하려는 항목과 RPATH 검색 경로가 무엇인지 확인합니다. |
Linux
런타임에 dlopen
이 검색하는 경로를 변경할 수 있는 방법이 없으므로 Linux를 검색하는 모든 경로는 각 모듈의 RPATH
아래에 설정됩니다. readelf
를 사용하여 검색된 전체 RPATH
목록을 찾을 수 있습니다.
FPlatformProcess::GetDllHandle
모든 언리얼 엔진 모듈은 RTLD_LAZY | RTLD_LOCAL
로 dlopen 됩니다. 언리얼 엔진이 아닌 모듈은 처음에는 LAZY | LOCAL
로 로드되었다가 LAZY | RTLD_GLOBAL
로 다시 열립니다. 이로 인해 여러 언리얼 엔진 모듈이 단일 글로벌 기호 대신 로컬 기호에 링크되는 동일한 글로벌 기호를 가지는 특수한 문제가 발생할 수 있습니다. 이 경우 글로벌 시스템이 초기화되지 않은 것처럼 보이는 이상한 충돌이 발생할 수 있습니다. gdb
를 사용하여 저장했다고 생각하는 위치와 글로벌 포인터를 비교할 수 있습니다. 비교했으나 서로 다른 경우 여러 글로벌 정의 및 모듈이 서로 다른 항목에 바인딩되어 있을 가능성이 큽니다.
SO 로딩 문제 디버깅
툴 | Linux 사용자 지정 페이지 | 간략한 설명 |
---|---|---|
ldd | http://man7.org/linux/man-pages/man1/ldd.1.html | 런타임 종속성과 누락된 종속성을 알려줍니다. |
nm | http://man7.org/linux/man-pages/man1/nm.1p.html | 내보낸 기호, 또는 필요한 경우 모든 기호에 대해 알려줄 수 있는 'Dependency Walker'와 유사합니다. |
readelf | http://man7.org/linux/man-pages/man1/readelf.1.html | 내보낸 기호 및 elf 섹션 오프셋에 대한 정보를 덤프하는 또 다른 도구입니다. |
LD_DEBUG | http://man7.org/linux/man-pages/man8/ld.so.8.html | 어떤 기호가 어떤 다이내믹 라이브러리에 바인딩되어 있는지 알아낼 수 있는 또 다른 방법입니다. |
strace | http://man7.org/linux/man-pages/man1/strace.1.html | 사용 중인 런타임 시스템 호출을 확인하는 유용한 도구입니다. dlopen 을 어떤 경로에서 열거나 읽으려고 시도하는지 확인할 수 있습니다. |
런타임 종속성
게임을 패키지로 만들 때 실행 파일 옆에 타사 DLL을 준비하려면 다음과 같이 build.cs
파일에서 런타임 종속성으로 선언할 수 있습니다.
RuntimeDependencies.Add(Path.Combine(PluginDirectory, "Binaries/Win64/Foo.dll"));
여기서는 DLL이 이미 주어진 디렉터리에 존재하고 플러그인이 해당 위치에서 수동으로 로드한다고 가정합니다. 빌드 시 실행 파일과 동일한 출력 디렉터리에 DLL을 복사하려면 다음과 같이 RuntimeDependencies.Add
메서드의 오버로드를 통해 복사합니다.
RuntimeDependencies.Add("$(TargetOutputDir)/Foo.dll", Path.Combine(PluginDirectory, "Source/ThirdParty/bin/Foo.dll"));
DLL 출력 경로에 다음과 같이 다른 변수도 사용할 수 있습니다.
변수 | 설명 |
---|---|
$(EngineDir) |
엔진 디렉터리 |
$(ProjectDir) |
프로젝트 파일이 있는 디렉터리 |
$(ModuleDir) |
.build.cs 파일이 있는 디렉터리 |
$(PluginDir) |
.uplugin 파일이 있는 디렉터리 |
$(BinaryOutputDir) |
모듈이 컴파일되는 바이너리가 있는 디렉터리(예: 에디터 빌드의 경우 DLL 경로, 패키지로 만든 빌드의 경우 실행 파일(EXE) 경로) |
$(TargetOutputDir) |
실행 파일이 있는 디렉터리(에디터 빌드 포함) |
'RuntimeDependencies' 필드는 스테이징 DLL로 제한되지 않으며 스테이징 프로세스에 추가 파일을 삽입하는 데 사용할 수도 있습니다. 이러한 파일은 언리얼의 PAK 파일에 저장하거나 디스크에 느슨하게 보관할 수 있습니다. DLL은 운영 체제에서 로드하므로 일반적으로는 PAK 파일에 저장할 수 없습니다.
RuntimeDependencies.Add(Path.Combine(PluginDirectory, "Extras/..."), StagedFileType.UFS);
StagedFileType에 가능한 값은 다음과 같습니다.
값 | 설명 |
---|---|
StagedFileType.UFS |
언리얼 파일 시스템 함수를 통해서만 액세스할 수 있으며 PAK 파일에 포함될 수 있습니다. |
StagedFileType.NonUFS |
느슨한 파일 시스템 일부로 보관해야 합니다. |
StagedFileType.DebugNonUFS |
느슨한 파일 시스템 일부로 보관해야 하는 디버그 파일입니다. 디버그 파일이 스테이지되도록 설정되어 있지 않은 경우 포함되지 않습니다. |
StagedFileType.SystemNonUFS |
느슨한 파일 시스템 일부로 보관해야 하는 시스템 파일입니다. 시스템 파일은 플랫폼 레이어에 의해 자동으로 다시 매핑되거나 이름이 변경되지 않습니다. |
문제 해결
Windows.h
표준 Windows 헤더(Windows.h
)는 대부분의 언리얼 엔진 코드에 기본으로 포함되어 있지 않습니다. 타사 라이브러리에 필요한 경우 Core
모듈의 WindowsHWrapper.h
파일을 통해 포함합니다.
#include "Windows/WindowsHWrapper.h"
많은 Windows 함수는 ANSI와 유니코드 변형 간 전환을 위한 매크로로 정의되므로 관련 없는 코드가 동일한 이름의 기호를 정의할 때 문제가 발생할 수 있습니다. 이를 방지하기 위하여 Windows.h
로 정의된 많은 매크로 정의를 해제합니다. 가능한 경우 함수의 ...A
및 ...W
변형을 명시적으로 호출하기를 권장합니다(예: GetCommandLineA()
또는 GetCommandLineW()
).
TRUE
및 FALSE
에 대한 Windows 매크로는 이식이 불가능하며 사용하는 경우 컴파일 오류를 일으키는 값으로 재정의됩니다. 코드 블록에 대하여 활성화해야 하는 경우 다음과 같이 AllowWindowsPlatformTypes.h
및 HideWindowsPlatformTypes.h
를 포함하여 래핑합니다.
#include "Windows/AllowWindowsPlatformTypes.h"
int Foo = TRUE;
#include "Windows/HideWindowsPlatformTypes.h"
마찬가지로 원자성 함수에 대한 Windows 매크로는 WindowsPlatformAtomics.h
에 정의된 함수 이름과 충돌합니다. 정의를 원래 값으로 복원하려면 다음과 같이 AllowWindowsPlatformAtomics.h/HideWindowsPlatformAtomics.h
를 포함합니다.
#include "Windows/AllowWindowsPlatformAtomics.h"
//InterlockedIncrement를 사용하는 코드, ...
#include "Windows/HideWindowsPlatformAtomics.h"
C++ 경고 및 오류
언리얼 엔진 코드베이스에는 기본적으로 오류로 처리되는 많은 경고가 있습니다. 타사 코드에 대한 이러한 제한 일부를 완화하기 위해 일반 경고를 일시적으로 비활성화하는 크로스 플랫폼 매크로는 다음과 같습니다.
THIRD_PARTY_INCLUDES_START
#include <openssl.h>
THIRD_PARTY_INCLUDES_END
디폴트 압축 및 정렬
레거시로 인해 언리얼 엔진은 Win32에서 4바이트 압축을 강제 실행합니다. 이로 인해 double 또는 long과 같이 8바이트 유형을 사용하는 클래스에서 디버깅하기 어려운 정렬 문제가 발생할 수 있습니다. 일반 구조체에서 8바이트 유형을 정의하는 타사 코드 주변의 기본 압축을 복원하려면 다음 매크로를 사용하세요.
PRAGMA_PUSH_PLATFORM_DEFAULT_PACKING
#include <thirdparty.h>
PRAGMA_POP_PLATFORM_DEFAULT_PACKING
RTTI 빌드 오류
다른 RTTI(런타임 유형 정보) 플래그로 컴파일된 소스 파일에서 바이너리를 링크하면 Windows에서 빌드 오류가 발생할 수 있습니다. RTTI 빌드 오류가 발생하면 RTTI 설정/해제 모듈을 혼합하는 도우미 매크로를 정의하거나 소스에서 빌드하는 경우 TargetRules.cs
에서 bForceEnableRTTI
를 true
로 설정하여 전체 엔진에 대한 RTTI를 활성화할 수 있습니다.
Linux
Linux에서는 RTTI 모듈의 혼합 및 일치를 허용하지 않습니다. 따라서 RTTI 모듈이 있는 경우 엔진에 대한 활성화가 필요합니다.
다이내믹 캐스트 오류
RTTI가 해제되어 있을 때 UObject 유형이 아닌 오브젝트 유형에 다이내믹 캐스트를 사용하는 경우 CoreUObject/Public/Templates/Casts.h
, #define dynamic_cast
가 CUObject 모듈을 리디렉션하여 "RTTI가 비활성화되어 있을 때 dynamic_cast를 사용할 수 없습니다 오류" 가 발생합니다. UObject 유형의 경우 다이내믹 캐스트는 언리얼 엔진의 리플렉션 시스템을 사용하지만 다른 유형의 경우 일반 dynamic_cast
를 사용합니다.