자료 저장소

본 강좌는 Visual C++ 6.0, 디버그 모드를 기준으로 작성되었습니다. Visual C++ 7.0, 8.0, 9.0 등에서도 기본적인 작성 방법은 같습니다. 어디까지나 예제이므로 릴리즈 모드는 포함하지 않았습니다. 릴리즈 모드를 원하시면 릴리즈로 컴파일하신 후 그 파일을 사용하면 됩니다.




1. DLL이란?
2. DLL 만드는 방법
3. DEF - 모듈 정의 파일
4. DllMain
5. 묵시적 연결과 명시적 연결
6. 다른 언어와의 연결





1. DLL이란?
DLL을 알기 전에 먼저 “라이브러리”라는 개념을 알아야 합니다.
라이브러리란 프로그램 개발 시 자주 필요한 함수 등을 미리 컴파일해 놓은, 말하자면 함수 모음집과 비슷합니다.
쉬 운 예로 CRT 함수들이 있죠. printf, scanf, fopen 등의 함수들은 각 컴파일러 제작사들이 라이브러리에 미리 포함시켜 놓고, 링크시에 그 코드를 가져와서 실행 파일에 합치는 것입니다. 따라서 개발자들은 printf, scanf 등의 함수를 프로젝트 하나하나마다 만들 필요가 없죠.






이렇게 실행 파일에 그 코드가 직접 삽입되는 것을 정적 연결 라이브러리라고 합니다. 정적 연결 라이브러리는 부속 파일이 필요없다는 장점이 있으나 다음과 같은 단점도 있습니다.

1. 각각의 실행 파일에 코드가 포함되므로 전체적인 시스템의 자원이 낭비됩니다.
2. 라이브러리 제작사가 라이브러리를 업그레이드하면 실행 파일을 다시 컴파일해야 합니다. 예를 들어서 printf 함수를 개선한 새로운 CRT.lib이 나왔다면 다시 컴파일하기 전까지는 그 혜택을 받을 수 없습니다.

그래서 나온 것이 바로 DLL-동적 연결 라이브러리입니다. DLL은 기본적으로 함수와 코드 등을 모아놨다는 점에서는 정적 연결 라이브러리와 동일하지만 실행 파일 안에 코드가 포함되지 않고 프로그램이 실행될 때 코드가 연결됩니다.




각 각의 실행 파일에는 CRT.DLL에 printf, scanf, fopen 등의 함수가 있다는 정보만 있고, 윈도우즈는 실행 파일을 실행할 때 CRT.DLL도 같이 읽어들여서 각각의 실행 파일과 연결시켜줍니다. 이러한 DLL에는 다음과 같은 특징이 있습니다.

1. 코드가 DLL 하나에만 들어있으므로 전체적인 시스템 자원이 절약됩니다.
2. DLL의 버전이 바뀌면 DLL만 새로 교체하면 됩니다. CRT.DLL의 printf 함수가 개선되었다면 CRT.DLL만 교체하면 그 이득을 볼 수 있다는 뜻이죠.
3. DLL에 필요한 코드가 있으므로 DLL이 없으면 실행 파일을 실행할 수 없습니다.

실 제로 DLL은 아주 많은 곳에서 사용되고 있으며 윈도우즈도 KERNEL32, USER32, GDI32 이 세 개의 핵심 DLL로 돌아가고 있습니다. 아무 생각 없이 호출했던 MessageBox, CreateWindow 등의 함수도 다 저 DLL 속에 들어있는 것들이고 그것들을 호출하는 것입니다.


2. DLL 만드는 방법
DLL을 만들기 위해서는 C/C++ 컴파일러, 키보드, 마우스, 손가락(가능하다면 발가락도 괜찮음), 생각을 위한 약간의 뇌가 필요합니다. 여기서는 Microsoft Visual C++ 6.0을 사용하도록 하겠습니다.

Microsoft Visual C++ 6.0을 켜고, File - New - Projects - Win32 Dynamic Link Library을 선택하고 적절한 이름을 지은 뒤
 OK!를 누릅니다.




DLL 예제 첫번째이므로 DLLEXAM1이라는 이름을 붙였습니다.

OK를 누르면 뭔가 쎈 척 하는 창이 뜨는데 empty 들어간 걸 선택해서 Finish, OK를 차례대로 누릅니다. 그러면 프로젝트가 만들어집니다.
첫번째 예제이므로 두 정수를 입력받아 그 합을 리턴하는 함수를 포함하는 DLL을 만들어봅시다.
프로젝트에 cpp 파일을 하나 추가하고 다음과 같이 입력합니다.




혹시 여기까지 작성하실 수 없는 분이라면 지금 전혀 엉뚱한 강좌를 잘못 보고 있는 것이므로 C에 대한 기본적인 것을 먼저 마스터하고 오실 것을 권하겠습니다.

어쨌든 여기까지 작성하고 빌드를 하면 성공일겁니다.



프로젝트 폴더의 debug 폴더에 보면 DLLEXAM1.dll이라는 파일이 만들어져 있을 겁니다.
근데 이 파일을 수동으로 시스템 폴더에 옮기기엔 너무 귀찮습니다. 그래서 컴파일 시 자동으로 c:\windows\system32에 dll이 복사되도록 해 보겠습니다.

메뉴의 Project - Settings - Post-build step(화살표를 눌러 탭을 옮기다 보면 나옵니다.)에 다음과 같은 문을 추가합니다.
copy debug\dllexam1.dll c:\windows\system32



다시 컴파일해보시면 출력창에 다음과 같이 1개 파일이 복사되었습니다. 라고 나올 겁니다.



여기까지 따라하셨다면 여러분은 첫 번째 DLL을 성공적으로 작성한 것입니다. 축하합니다!
(라고 썼지만 실제로 따라하는 사람이 없을 거라는 걸 잘 알죠.)

ps. 아까 DLL은 함수의 집합이라고 했지만 꼭 함수만을 작성할 필요는 없고 전역변수, 클래스, C/C++ 런타임 함수, API 함수등을 마음대로 사용해도 괜찮습니다. 결국엔 DLL이나 실행파일이나 코드와 데이터로 이루어져 있는 건 동일하니까요.

그 러면 이제 이 DLL을 사용하는 간단한 프로그램을 만들어보겠습니다. 콘솔 프로젝트를 작성하고 DLLEXAM1의 debug 폴더에 있는 DLLEXAM1.lib을 해당 프로젝트 폴더에 복사합니다(이게 무엇인지는 잠시 후에 설명합니다) 코드는 다음과 같습니다.




실행 결과는 다음과 같습니다.



DLLEXAM1.dll에 있는 addinteger 함수를 호출해서 덧셈을 하고 있습니다.
그러면 어떻게 이런 결과가 나온 건지 분석해보죠.

다음은 DLLEXAM1의 소스입니다.

extern "C" __declspec(dllexport) int addinteger(int a,int b)
{
 return a+b;
}

extern "C"는 함수 이름을 C 형식으로 하라는 지시자입니다. DLL에는 하나의 이름에 하나의 함수만 대응될 수 있는데, C++은 같은 이름을 가지는 함수를 여러 벌 작성하는 오버로딩이 가능합니다. 따라서 C++로 DLL을 작성하면 실제 DLL에 기록되는 이름에 여러 장식이 붙습니다. 이것은 인수 정보 등의 정보를 담고 있는 표식입니다. 저 extern "C" 문을 지우고 컴파일한 후, Visual Studio 도구 중 하나인 Depends로 DLL을 열어 보면 그 실체를 알 수 있습니다. 여기서는 생략하겠습니다. 그냥 붙이면 좋다-고 알아두세요.

__declspec(dllexport)는 컴파일러에게 이 함수를 export하겠다고 알려줍니다.


그리고 DLLEXAM2의 소스입니다.

#pragma comment(lib, "dllexam1.lib")
extern "C" __declspec(dllimport) int addinteger(int,int);

extern "C"는 위에서와 같고, __declspec(dllimport)는 컴파일러에게 이 함수는 DLL에서 import되는 함수라고 알려줍니다.
그렇다면 위의 #pragma comment(lib, "dllexam1.lib")문은 무엇일까요???

그 답은 바로 이렇습니다..
저 addinteger라는 함수는 dll에서 import한다고만 나와 있지 어떤 dll인지는 알려주지 않습니다. KERNEL32인지 USER32인지 GDI32인지 ADVAPI32인지 DLLEXAM1인지 어떤 dll이 저 함수를 가지는지 컴파일러는 알지 못하죠. 저 dllexam1.lib이라는 파일 안에는 dllexam1이 export하는 함수들에 대한 정보가 들어있습니다. 컴파일러는 lib파일들을 읽고 각 함수와 비교해서 맞는 함수를 골라주는거죠. 즉 kernel32.lib, gdi32.lib 따위도 있다는 말이 되겠군요. 이것들은 각각 kernel32.dll, gdi32.dll의 함수들에 대한 정보가 들어있겠고요. 이것은 후에 나올 묵시적 연결과 연결되는 내용(?)입니다.
그럼 저 문이 뭔지 아실 수 있겠죠? dllexam1.lib을 참조하라는 문입니다.


3. DEF - 모듈 정의 파일
옛날에는 DLL을 만들 때 DEF파일이 반드시 필요했습니다. 그러나 요즘에는 링커의 스위치로 대부분 대체되어서 별 실용성이 없습니다. 그래서 제가 DEF에 대해 쓸 내용은 한 가지밖에 없습니다.

위에서 extern "C"가 함수 이름의 장식을 없애준다고 썼었습니다. 실제로 위 코드의 DLLEXAM1.dll을 depends로 열어 보면 다음과 같이 보입니다.



함수 이름이 addinteger로 깔끔하네요.
그런데 저 코드를 다시 한번 살펴보죠.

extern "C" __declspec(dllexport) int addinteger(int a,int b)

호출 규약에 대한 정의가 없고, 따라서 호출 규약은 C++ 기본값인 cdecl입니다. (호출 규약에 대한 자세한 내용은http://www.winapi.co.kr/clec/cpp2/16-1-3.htm에 가면 보실 수 있습니다.)
그런데 윈도우즈의 기본 호출 규약은 stdcall이고, VB에서도 stdcall을 사용합니다. cdecl보다는 stdcall을 사용하는 게 더 좋아 보입니다. 그래서 호출 규약을 stdcall로 바꿔보고 컴파일해보겠습니다.



컴파일한 결과를 depends로 열어보았더니, 앗!



대체 저 _, @8은 뭘까여... 이대로 가다가는 addinteger라는 이름만으로는 저 함수를 찾기 힘들 텐데...
저 장식까지도 완벽하게 지우기 위해서 DEF파일이 필요한겁니다.
적절한 이름으로 DEF파일을 만들어 프로젝트에 추가하고 다음과 같이 넣으세요.



DLLEXAM1 라이브러리에서 addinteger라는 이름을 가지는 함수를 export하겠다... 라는 뜻입니다.
이제 다시 depends로 보면 장식이 없어져 있을 겁니다.. 그건 직접 확인해보세요.


4. DllMain

보통 프로그램은 main이나 WinMain 함수를 진입점으로 가집니다. DLL도 진입점을 가질 수 있고 그것이 바로 DllMain 함수입니다.
DllMain함수는 다음과 같은 원형을 가집니다.
BOOL WINAPI DllMain(HINSTANCE hInst,DWORD dwReason,LPVOID lpRes);

hInst: DLL의 인스턴스 핸들
dwReason: 함수가 호출된 이유
lpRes는 예약되어 있고 사용되지 않습니다.

여기서 가장 중요한 인수는 dwReason으로 다음 4가지 값을 가집니다.
DLL_PROCESS_ATTACH: (프로세스 단위로) DLL이 로드될 때
DLL_PROCESS_DETACH: (프로세스 단위로) DLL이 언로드될 때
DLL_THREAD_ATTACH: (스레드 단위로) DLL이 로드될 때
DLL_THREAD_DETACH: (스레드 단위로) DLL이 언로드될 때

DllMain 함수는 대체로 다음과 같은 모양을 가집니다.

switch (dwReason)
{
case DLL_PROCESS_ATTACH:
    처리;
case DLL_PROCESS_DETACH:
    처리;
case DLL_THREAD_ATTACH:
    처리;
case DLL_THREAD_DETACH:
    처리;
}

초기화에 성공했으면 TRUE를 리턴하고, 실패했으면 FALSE를 리턴하면 되는데, 리턴값은 dwReason이 DLL_PROCESS_ATTACH일 때만 유효하며 다른 때는 무시됩니다.
일반적으로 DLL_PROCESS_ATTACH에 메모리 할당 같은 초기화 코드를, DLL_PROCESS_DETACH에 메모리 해제 같은 뒤처리 코드를 넣습니다.

다음은 DllMain에서 메모리를 할당하고 해제하는 예제입니다.




5. 묵시적 연결과 명시적 연결

DLL의 함수를 연결하는 방법에는 2가지가 있는데 아까 lib파일을 설명하는 곳에서 나왔던 묵시적 연결이라는 방법과 명시적 연결이라는 방법이 있습니다.
묵시적 연결은 위에서처럼 함수가 어떤 DLL에 있는지 밝히지 않고 쓰는 방법이고, 명시적 연결은 함수를 직접 로드하고 함수의 주소를 직접 얻어오는 방법입니다.
명시적 연결은 함수 포인터에 대한 개념도 필요하고, 코딩이 좀 귀찮지만 여러 유연한 처리가 가능하다는 장점이 있습니다.(dll이 없으면 받아온다던지 하는)

명시적 연결을 하는 절차는 다음과 같습니다.

1. LoadLibrary(혹은 이미 로드되어있다면 GetModuleHandle) 함수를 사용하여 DLL을 로드합니다.
2. GetProcAddress 함수로 원하는 함수의 주소를 얻습니다.
3. 얻어온 함수의 주소를 지져먹고 쪄먹고 구워먹고 삶아먹습니다.

다음은 예제입니다.



LoadLibrary 함수의 원형은 다음과 같습니다.
HMODULE LoadLibrary(LPCTSTR lpFileName);
HMODULE은 HINSTANCE와 동일한 자료형입니다. lpFileName이 지정하는 DLL을 로드해 성공하면 핸들을, 실패하면 NULL을 리턴합니다.

GetProcAddress 함수의 원형은 다음과 같습니다.
FARPROC GetProcAddress(HMODULE hModule,LPCSTR lpProcName);
hModule이 지정하는 DLL의 함수 중 lpProcName의 이름을 가진 것을 찾아서 그 포인터를 돌려줍니다.

즉, 다음과 같은 에러 처리가 가능합니다.

if (!(hMod=LoadLibrary(DLL)))
{
printf("DLL이 없는데요?\n");
}
if (!(proc=GetProcAddress(DLL,Name)))
{
printf("함수를 찾을 수 없다는데?\n");
}

이런 것은 묵시적 연결에서는 불가능합니다. 그렇다고 묵시적 연결이 안 좋은 것은 아닙니다. 특수한 경우를 제외하고는 묵시적 연결로 사용하는 것이 편하고 좋습니다.


6. 다른 언어와의 연결

DLL 을 사용할 만한 가장 현실적인 이유가 바로 이것입니다. 분명 C++는 강력하고 빠르지만 개발 기간이 너무 오래 걸립니다. 그래서 프로그램의 비주얼 부분은 VB 등의 언어로 만들고, 성능과 속도가 중요한 부분만 C++ DLL로 만들어 합치는 것입니다. 그러면 프로그램의 성능과 개발자의 편의가 동시에 만족될 수 있습니다.

여기서는 VB 6.0과 C#에 각각 연결시켜보겠습니다.


-VB-

프로젝트를 만들고 폼에 텍스트박스 2개와 버튼을 올려놓습니다. 모듈을 하나 만들어 다음 코드를 써넣습니다.

Declare Function addinteger Lib "dllexam1" (ByVal a As Long, ByVal b As Long) As Long

폼의 버튼 클릭 이벤트에는 다음 코드를 넣습니다.

Private Sub Command1_Click()
MsgBox addinteger(CLng(Text1.Text), CLng(Text2.Text))
End Sub

다음은 실행 결과입니다.



-C#-

프로젝트를 만들고 폼에 텍스트박스 2개와 버튼 하나를 올려놓고 폼에 다음과 같은 코드를 추가합니다. (System.Runtime.InteropServices 네임스페이스를 미리 using 선언해놓아야 합니다.)

[DllImport("dllexam1")]
public static extern int addinteger(int a, int b);

버튼 클릭 이벤트에 다음 코드를 추가합니다.

private void button1_Click(object sender, EventArgs e)
{
    MessageBox.Show(addinteger(int.Parse(textBox1.Text), int.Parse(textBox2.Text)).ToString());
}

실행 결과는 다음과 같습니다.




댓글 로드 중…

최근에 게시된 글