컴파일과 동시에 같이 컴파일 되는 정적 라이브러리가 있고 이와 반대되는 개념인 동적 라이브러리가 있다.
동적 라이브러리는 바이러리 파일로 *.dll로 존재한다.
동적 라이브러리는 윈도우 운영체제의 서비스에 의해 올라가서 응용 프로그램이 운영체제 서비스에 DLL 함수 혹은 변수를 요청할 때 함수 혹은 변수를 호출해주는 방식이다.
동적 라이브러리는 2가지로 구분할 수 있다.
1. 암시적 연결
2. 명시적 연결
명시적 연결은 그때그때 필요할 때마다 운영체제에게 dll 파일을 올려달라고 하고, 함수의 주소값을 얻어와 동적 라이브러리의 함수를 실행시키는 방법이다.
이에 반해 암시적 연결은 미리 프로그램을 만들 때 dll 파일을 불러오는 암시적 링킹을 위한 심볼이 들어있는 *lib를 함께 사용해서 컴파일 한다.
이를 통해 프로그램을 실행 시 자동으로 운영체제에 dll 파일을 올리고 함수의 주소 값을 불러오지 않아도 더 편하게 동적 라이브러리를 호출할 수 있다.
하지만 암시적 연결은 편리하고, 프로그램 실행 시 미리 dll을 불러오기에 함수 실행 시 즉각 반응하지만, 명시적 연결은 그때그때 필요한 dll의 함수를 불러들이기 때문에 더 효율적으로 사용할 수 있다.
여기서 만드는 동적 링킹은 암시적 연결 DLL을 할 것이다.
만약 명시적 연결이 궁금하다면 해당 사이트를 참조 바란다.
https://learn.microsoft.com/ko-kr/cpp/build/linking-an-executable-to-a-dll?view=msvc-170
간단히 말하면
위의 3가지 함수를 통해서 동적 라이브러리를 로드시키고, 동적 라이브러리의 함수 주소 값을 얻어내고, 동적 라이브러리 연결을 끊는 것이 끝이다.
먼저 비주얼 스튜디오에서 DLL(동적 연결 라이브러리)를 선택하여 만든다.
// dllmain.cpp : DLL 애플리케이션의 진입점을 정의합니다.
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
프로젝트를 만들 시 다음과 같이 DLLMain이 있을 것이다.
어느 DLL이든 DLLMain과 같은 초기화 및 종료 코드에 대한 진입점 함수가 있는 경우 프로세스나 쓰레드가 시작 또는 종료할 때 호출된다.
만약 진입점 함수에서 TRUE를 반환하지 않는 경우 시스템은 프로세스를 종료하고 오류를 발생시킨다.
마지막으로 시스템은 프로세스의 실행 코드를 수정하여 DLL 함수에 대한 시작 주소를 제공한다.
(참고로 DLL을 구현할 때 진입점 함수를 반드시 구현하지 않아도 된다. 만약 DLLMain 함수가 존재하지 않으면 C/C++ 런타일 라이브러리 내에 자체적으로 구현되어 있는 DLLMain함수가 호출 된다.)
DLL_PROCESS_ATTACH는 loadLibrary로 호출 됐을 시 호출할 때,
DLL_THREAD_ATTACH는 프로세스 내에 새로운 쓰레드가 생성될 때, (이미 DLL이 호출되어있어야 한다.)
DLL_THREAD_DETACH는 프로세스 내에 쓰레드가 종료 되기 이전에 호출 된다.
DLL_PROCESS_DETACH는 프로세스 종료 혹은 FreeLibrary 호출 시 호출된다.
먼저 *.cpp 파일과 *.h 파일을 만들어주겠다.
// TestComponent.h
#ifdef _WINDLL
#define StudyDLL_API __declspec(dllexport)
#else
#define StudyDLL_API __declspec(dllimport)
#endif
class TestClass{
public:
static int Value;
void AddValue(int a);
};
extern "C" StudyDLL_API TestClass * MakeInstance();
extern "C" StudyDLL_API void DeleteInstance(TestClass* testClass);
extern "C" StudyDLL_API void AddValue(TestClass * testClass, int a);
우리는 헤더파일을 DLL 파일을 만드는데 헤더파일로도 사용할 것이고 해당 헤더 파일을 재사용해서 DLL 파일을 사용할 때에도 이 헤더 파일을 쓸 것이다.
위와같이 매크로를 사용하지 않는다면 아래와 같이 분리해서 작성해야 한다.
extern "C" __declspec(dllexport) TestClass * MakeInstance();
extern "C" __declspec(dllexport) void DeleteInstance(TestClass* testClass);
extern "C" __declspec(dllexport) void AddValue(TestClass * testClass, int a);
위는 DLL 파일을 빌드할 때 함수 앞에 사용하는 매크로이고,
아래는 DLL 파일을 사용할 때 작성해야 하는 매크로이다.
extern "C" __declspec(dllimport) TestClass * MakeInstance();
extern "C" __declspec(dllimport) void DeleteInstance(TestClass* testClass);
extern "C" __declspec(dllimport) void AddValue(TestClass * testClass, int a);
이를 해결하기 위해서 매크로를 만들어준 것이다.
_WINDLL은 어디서 나왔냐면, [프로젝트 속성] -> [C/C++] -> [전처리기] -> [전처리기 정의] 에 있는 내용이다.
우리가 프로젝트를 생성했을 때 DLL(동적 연결 라이브러리)로 만들었기 때문에 자동으로 추가되어있는 매크로이다.
extern "C"을 쓴 이유는 다음과 같다.
c++은 객체지향이 포함되면서 함수명을 내부적으로 클래스명과 함께 복잡한 방식으로 이름을 네이밍한다. 이를 C언어 스타일로 즉, 내부적으로 이름을 다시 바꾸지 않게 하기 위해서이다.
// TestComponent.cpp
#include "pch.h"
#include "TestComponent.h"
#include <stdio.h>
int TestClass::Value = 0;
void TestClass::AddValue(int a) {
Value += a;
printf("Value is %d\n", Value);
}
TestClass* MakeInstance()
{
printf("Instance Create\n");
return new TestClass();
}
void DeleteInstance(TestClass* testClass) {
if (testClass == nullptr)
return;
printf("Instance Delete\n");
delete testClass;
}
void AddValue(TestClass* testClass,int a) {
testClass->AddValue(a);
}
구현부는 일반적인 것 처럼 구현해주면 된다.
프로젝트를 우클릭해서 빌드를 해준다.
그리고 파일 탐색기에 폴더 열기를 한다.
그리고 뒤로 가기를 누른 후, 빌드 버전에 따라서 들어가주면 다음과 같이 dll 파일과 lib 파일이 생성된다.
일반적인 프로그래밍하듯이 프로젝트를 생성해준다.
그리고 생성한 프로젝트에 아까 만들었던 lib과 dll을 모두 넣는다.
(귀찮아서 이렇게 한 것이지 실제로는 정리가 필수이다.)
그리고 위에 DLL을 만든 프로젝트에 가서 헤더 파일 또한 옮긴다.
#include "TestComponent.h"
int main(void) {
TestClass* testClass = MakeInstance();
AddValue(testClass, 0);
AddValue(testClass, 2);
AddValue(testClass, 3);
DeleteInstance(testClass);
}
일단 다음과 같이 코드를 사용한 Main 함수를 만들어주겠다.
코드야 헤더 파일을 추가했으니 사용가능한 것이고, 나머지 dll과 lib 또한 연결해줘야 된다.
[프로젝트 속성]->[VC++ 디렉터리]->[포함 디렉터리]
포함 디렉터리는 헤더파일을 외부에 있을 경우, 혹은 내부에 폴더를 만들었을 때 사용하면 된다. 하지만 프로젝트 폴더에 바로 넣었으므로 생략해도 된다.
[프로젝트 속성]->[VC++ 디렉터리]->[참조 디렉터리]
참조 디렉터리는 lib이 있는 폴더를 연결해주면 된다.
이것 또한 프로젝트 폴더에 바로 넣었으므로 생략해도 된다.
그리고 [프로젝트 속성]->[링커]->[입력]->[추가 종속성] 에 라이브러리 파일 이름을 작성해야 한다.
여기서 나는 파일명이 StudyDLL.lib이므로 이것으로 적겠다.
그리고 실행해보면 문제없이 작동하는 것을 확인할 수 있다.
추가적으로 실행파일을 배포할 때는 같은 폴더 내에 *.dll 파일도 같이 배포해야 한다.
추가 Tip. [프로젝트 속성]->[링커]->[입력]->[추가 종속성]에 일일히 넣는게 귀찮다면 다음과 같이 코드를 통해서 링커에게 이 파일도 링킹해 달라고 요청할 수 있다.
#pragma comment(lib,"./StudyDLL.lib")