블루프린트로만 개발할 수 있다면 정말 편하겠지만


언리얼에 없는 기능을 넣어서 써야 한다거나


성능상의 이슈로 인해서 어쩔 수 없이 써야 한다거나


결국은 c++을 쓰게 마련이고 외부 dll을 불러와야 하게 마련이다.


이 글은 c++을 약간 다룰 수 있는 사람 / 언리얼을 약간 다룰 수 있는 사람을 대상으로 한, 


dll파일을 불러오는 법에 대한 글이다.






dll 을 넣는 여러가지 방법이 있겠지만, 일단 이 글에서는 module이 아닌 plugin 방식을 택했다.

module을 안 쓰는 이유가, 아마 프로젝트 패키징 할 때 module은 dll복사 관련 해서 따로 뭔가 더 넣어줘야 하는 부분이 있었는데

plugin에서는 그게 그냥 자동으로 되어서 안 썼을 거다.




그래! 어서 "서드 파티 라이브러리" 플러그인을 만들어!





플러그인이 완성되고 나면 대충 이런 모양이 될 텐데...

Thirdparty에서 dll을 불러오고 mydll이라고 만든 플러그인에서 작업해도 되고

Thirdparty에서는 불러오는 폼만 잡아주고 모든 작업 내용은 mydll에 다 때려박아도 되고...

일단 build.cs를 한 번 까보자.



// Add the import library

PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "x64", "Release", "ExampleLibrary.lib"));


여기에 lib이 있겠구먼



있넹

다른 말로 하면, 내가 써먹을 dll이랑 lib도 여기다 박아놓으면 된다는 이야기겠지


// Delay-load the DLL, so we can load it from the right place first

PublicDelayLoadDLLs.Add("ExampleLibrary.dll");


여기에 ExampleLibrary.dll 대신에 내 dll으로 대체하면 내 dll을 불러온다는 이야기겠지


// Ensure that the DLL is staged along with the executable

RuntimeDependencies.Add("$(PluginDir)/Binaries/ThirdParty/MyDllLibrary/Win64/ExampleLibrary.dll");


이 Plugin을 사용할 때는 이 Dll이 꼭 필요하다는 이야기고, Binaries폴더 안에 있어야 한다는 이야기겠지


본인의 개발환경이 윈도우즈라면 밑에 .Mac 이랑 .Linux로 이어지는 구문들은 그냥 지워버리자. 

물론 애플이나 리눅스에서 개발한다면 다른 구문들을 지워버려야겠지만.

또한 본인이 멀티-플랫폼 게임을 만들고 싶다면 여기에도 하나하나 필요한 걸 넣어줘야겠지만...




멀티 플랫폼 상황이 아니라면, 프로젝트에서 얘들 날려버려도 된다.




원래 dll에 들어가는 헤더에서는, 


외부로 수출할(외부에서 불러서 사용할) 함수 이름 앞에다가 __declspec(dllexport)을 붙이고

외부에서는 수입할(Dll에서 불러서 사용할) 함수 이름 앞에다가 __declspec(dllimport)를 붙인다.


즉, 여기다가 얘들이 export를 붙여 놨다는 건, 이놈들이 이 함수를 DLL에서 가져온 게 아니라,

여기서 다시 "예제라이브러리함수"를 수출해서 다른 데서 쓰겠다... 라는 의미이다.



실제로 MyDll.cpp를 열어보면 '예제 라이브러리 함수"를 수입해서 쓰고 있다.

근데 우리가 하고 싶은 건 dll 내부의 함수를 수입하고 싶은 거잖아? 버려.



싹 버렸다.

사실 다 지워도 별 상관은 없지 싶다.

그냥 dll load, dependency 관련 부분만 build.cs에서 처리해 주고 Thirdparty부분은 여기서 쫑내자.





다음으로 온 곳은 Mydll.build.cs 파일이다.

이 플러그인에서는 Mydll에서 다 불러오고, 다 처리하고, 필요한 함수는 블루프린트에서 써먹을 용으로 만들어서 던져줄 생각이다.

그러기 위해서는 내부에 코드를 손대기 전에...

PublicDependencyModuleNames 안에다가 뭔가 더 넣어줘야 한다.

"CoreUObject"나 "Engine"정도는 넣어 줘야지 내가 코드를 짜면 에러를 안 내고 멀쩡하게 컴파일이 될 거다.

아마 사용하는 코드에 따라서 뭔가 더 넣어줘야 할 수도 있고...




원래는 18행까지가 끝인데 뭔가 더 적어 봤다.

저게 돌아가려면 Dll에 


__declspec(dllexport) void Init();

__declspec(dllexport) int GetValue(int);

__declspec(dllexport) bool SetValue(int, float);

typedef void(*CALLBACK_FUNC)(int);

__declspec(dllexport) void RegisterCallback(CALLBACK_FUNC);

__declspec(dllexport) ReleaseCallback();


이 정의되어 있어야 쓸 수 있다. 



사실 함수 이름은 달라도 상관 없지만 앞에 'dll_' 같은 prefix를 붙여 주거나, 아예 함수 이름이 같으면 안 헷갈린다.

함수 이름을 불러오는 부분을 .h 파일이 아니라 다른 식으로 불러 왔기 때문에 unreal쪽에서 쓰는 함수 이름이 같아도 된다.




MyDll 내부에서는...

위에서 경로를 찾아내고, 파일 이름을 적절히 정해준 다음에...

LibraryHandle에 dll관련된걸 붙여놓고,

여기서 내부 함수를 불러와서 쓰면 된다.


Init = (void(*)())FPlatformProcess::GetDllExport(ExampleLibraryHandle, TEXT("Init"));

가 함수를 불러와서 함수 포인터에 붙여주는 부분이고,

TEXT("Init")안에는 DLL에서 정의한 정확한 함수 이름을 넣어줘야 한다.


* 주의사항으로는, bool은 되지만 BOOL은 안된다.

윈도우 환경에서 dll을 만들다 보면 내부 규칙으로 만들어서 

int 대신 BOOL이 들어가고 unsigned char 대신 BYTE가 들어가고 이런 경우가 생기는데,

언리얼에서는 변수의 기본형이 아닌 다른 타입으로 불러오는 방법을 권장하지 않으며,

굳이 윈도우에서 재정의한 변수의 타입으로 불러오길 원한다면 

#include <Windows/WindowsHWrapper.h> 

헤더를 활용할 수 있다.


블루프린트에서 편하게 함수를 불러오기 위해서 따로 클래스를 하나 더 만들었다.




빨간줄은 intellisence가 병신이라 그런거고, 멀쩡하게 돌아가는 물건이다.













++덤


Callback은 하다 보니 넣게 된 건데, 이게 dll 내부의 함수를 언리얼에서만 부르는 게 아니라 

dll 내부에서 쿠쿠 밥 다했어요 언리얼님 찾아가세요 하고 메시지를 보낼 일이 생기게 마련이다.




이벤트 말고 Delegate를 사용하는 데는 아주 큰 이유가 있는데,

이벤트는 object에 붙어 다녀야 하는 객체지향적인 놈이라서 지금처럼 static이 붕붕 날아다니는 환경에선 못 쓴다더라.

근데 어차피 DynamicDelegate도 object에 매달려야 하는 건 같지만...





구조를 보면, 

MyInit에서 static한 MyDllLib::CallbackFromDll 함수를 Dll내부에 register시켜 줬고

myCallback을 정의해 준 다음에(얘는 global하게 쓸 수 있다)

MyDllLib::CallbackFromDll 에서 myCallback 방송을 때린다.




오브젝트에 달려 있는 CallbackFromDll을 이 오브젝트가 생성될 때 myCallback 에 붙여줬고

CallbackFromDll 에서는 MyOnCallback.Broadcast를 날려줬다.

MyOnCallback은 블루프린트 친화적으로 밖에서 찾아올 수 있기 때문에, 

블루프린트 상에서 MyOnCallback 에다가 이벤트를 달아주면 활용이 가능하다.


굳이 2중 구조를 쓴 이유는 

앞에서 static한 init을 쓰면서 그 때 RegisterCallback으로 dll내부에 함수를 등록시켰는데,

이 때 


쓸 수 있는 함수<DLL에 링크 된>와 

그 안에 들어가는 delegate<함수에서 Call 할 수 있는 callback>는 


당연히 static 해야 한다.

그리고 static한 놈은 UPROPERTY를 달아줄 수 없고, UPROPERTY가 안 달려 있으면 블루프린트에서 못 찾아본다.