1.1Basis
1.1.1Features
- 내부적으로 C++ 클래스를 사용할 수 있고, C 함수 Wrapper만을 Export 할 수 있다. 따라서 내부적인 C++ Class에 대한 변경은 DLL의 호출에 영향을 주지 않는다.
- MFC를 사용할 수 없으며, 별도의 MFC Library가 필요없다.
- DLL을 사용하는 Client는 DLL 호출을 지원하는 어떠한 Language로 작성될 수 있다.
- AppWizard를 이용하여 자동으로 Project를 생성할 수 있다.
1.1.2 Function Export
DLL 내에서 정의된 Function을 export하기 위해서는 “__declspec(dllexport)” 를 사용한다. “__declspec”은 MS만의 C, C++의 확장된 syntax로서, 확장된 storage-class 정보를 정의한다. “dllexport”는 storage-class의 한 속성으로, DLL의 Function, Data, Object를 export할 수 있도록 하여준다. 반대로 DLL내의 Function을 import하기 위해서는 “dllimport” 속성을 사용한다. Win32 환경에서는 Function Export/Import를 위하여 이것을 이용하며, Win32 이전의 Windows 환경에서 사용되던 module-definition file (.DEF)을 사용하지 않는다. 단, .Net 이전의 VB 등의 툴과 호환가능한 DLL을 제작하는 경우, module-definition file을 사용하도록 한다.
· export / import
함수 export/import를 위하여 아래와 같이 함수를 선언한다. Coding의 편의를 위하여 export선언을 #define으로 간략화시킨다.
#define DLLImport __declspec(dllimport)
#define DLLExport __declspec(dllexport)
DLLExport void somefunc();
· export/import Tips
위 방법으로 export/import 함수를 정의하면, DLL 내에서의 함수 정의와 DLL을 사용하는 Client에서의 함수정의를 다르게 해야 하는 불편이 생긴다. DLL과 Client에서 동일한 Header File을 사용할 수 있도록 하기 위하여 아래와 같이 export/import 함수를 정의한다.
#ifdef DLLTEST_EXPORTS
#define DLLFunction __declspec(dllexport)
#elseif
#define DLLFunction __declspec(dllimport)
#endif
DLLFunction void somefunc();
“DLLTEST3_EXPORTS” 은 DLL의 Project Settings에 Preprocessor definitions에 “프로젝트명_EXPORTS”의 형식으로 정의 되어 있다. 따라서 DLL Project에서는 export로, Client Project에서는 import로 동작한다.
· Adjusting Naming Convention
C++은 C와 다른 Naming Convention을 사용한다. 따라서 export되는 함수명은 Compile시에 기존의 정의한 이름과 다르게 해석된다. 따라서 Naming Convention에 대한 조정과정이 없으면, export된 함수는 C++ 이외의 Language로 작성되는 프로그램에서는 호출될 수 없다. extern “C”는 함수가 C naming convention을 사용하도록 만들어주며, 이를 통하여 C++로 작성되어 export되는 함수를 다른 Language에서도 사용가능하도록 하여 준다. VC는 기본적으로 프로젝트생성시에 C++을 사용하도록 구성되므로 모든 export함수는 Naming Convention의 조정이 필요하다.
#ifdef _USRDLL
#define DLLFunction __declspec(dllexport)
#elseif
#define DLLFunction __declspec(dllimport)
#endif
#ifdef __cplusplus
extern “C” {
#endif
DLLFunction void somefunc();
#ifdef __cplusplus
}
#endif
· Adjusting Calling Convention
이전의 Visual Basic과 같은 C 이외의 다른 언어는 C와 다른 Calling Convention을 사용하므로 다른언어에서 사용될 DLL을 작성하는 경우, 이를 조정해주어야 한다. VC는 기본적으로 “__cdecl” 방식을 사용하며, 다른 언어는 표준방식인 “__stdcall” 방식을 사용한다. 따라서 Project 속성페이지에서 “구성속성 – C/C++” 페이지의 “고급” 항목을 선택하여 “호출 규칙”을 “__stdcall”로 설정한다.
속성을 사용하지 않고 코드내에서 직접 지정하고 싶다면 아래와 같이 함수 선언과 구현시 함수명 앞에 Calling Convention을 지정해 준다.
DLLFunction void __stdcall somefunc(); |
DLLFunction void __stdcall someFunc() { } |
만약 DLL을 .Net 이전의 Visual Basic 과 같은 언어에서 사용하고자 한다면, module-definition file(.Def)을 작성하여 프로젝트에 포함시킨다. VC로 작성한 DLL을 예전의 Visual Basic에서 사용하는 경우, Calling Convention이 다르다는 에러를 자주 볼 수 있는데, 이는 모두 Calling Convention을 조정과정을 거치지 않아서 발생하는 문제이다.
Win32DLLSample.def |
LIBRARY "Win32DLLSample.DLL" EXPORTS somefunc @1 |
1.2 Making DLL
1.2.1 목표
간단한 DLL을 만들기 위하여 아래와 같은 두가지의 기능만을 가지는 DLL을 만들기로 한다.
- int 형의 두 숫자를 parameter로 받아 그 합을 return하는 함수
- 두개의 string을 받아 연결된 string을 넘겨주고, 그 총 길이를 return하는 함수
1.2.2 구현방법
- 새 프로젝트를 선택한 후, 프로젝트 유형을 아래와 같이 Visual C++ 카테고리의 “Win32 프로젝트”로 선택한다.
- 응용 프로그램 마법사에서 아래와 같이 “DLL”을 선택 후, 마침을 클릭하여 프로젝트를 생성한다.
- export할 함수의 정의를 위하여 Header File을 생성한다. 이는 후에 import 측에서도 공용으로 사용될 것이다. “SimpleDll.h”의 이름으로 Header를 생성하고, 두개의 함수를 위한 함수정의를 만든다.
[Win32DLLSample.h]
#pragma once
#ifdef WIN32DLLSAMPLE_EXPORTS
#define DLLFunction __declspec(dllexport)
#else
#define DLLFunction __declspec(dllimport)
#endif
extern "C" {
DLLFunction int __stdcall addint(int n1, int n2);
DLLFunction int __stdcall addchar(char* s1, char* s2, char* added);
}
[Win32DLLSample.cpp]
#include "stdafx.h"
#include "stdio.h"
#include "Win32DLLSample.h"
DLLFunction int __stdcall addint(int n1, int n2)
{
return n1 + n2;
}
DLLFunction int __stdcall addchar(char* s1, char* s2, char* added)
{
sprintf(added, "%s%s", s1, s2);
return strlen(added);
}
1.3 Using DLL
앞에서 작성한 코드를 컴파일하여 Win32DLLSample.dll 파일을 얻는다. 이것을 테스트하기 위하여 VC++ 및 VC#에서 DLL내의 함수를 호출하는 기능을 작성한다.
1.3.1 Using DLL with Visual C++ (Link Implicitly)
Dialog-Based 프로젝트를 생성하여 아래와 같은 순서로 DLL의 함수를 호출하는 기능을 작성한다.
- 앞에서 작성한 “Win32DLLSample.h”를 프로젝트에 추가한다.
- 프로젝트 속성페이지에서 “구성속성 – 링커 – 입력”을 선택한 후, “추가종속성” 항목에 “Win32DLLSample.lib”를 추가한다. “Win32DLLSample.lib” 파일은 VC++의 환경설정의 Directories에 존재하는 폴더 또는 Project 폴더에 복사하도록 하거나 “구성속성 – VC++ 디렉터리”를 선택하여 “라이브러리 디렉터리” 항목에 Lib 파일이 존재하는 폴더를 추가한다.
- Dialog를 다음과 같이 만들고 각 컨트롤에 멤버변수를 추가한다.
- DLL 함수를 호출할 Button에 대한 Handler를 작성한다.
[Win32DLLSampleTestDlg.cpp]
#include "Win32DLLSample.h"
…
void CWin32DLLSampleTestDlg::OnBnClickedButtonInt()
{
UpdateData(TRUE);
m_nIntSum = addint(m_nInt1, m_nInt2);
UpdateData(FALSE);
}
void CWin32DLLSampleTestDlg::OnBnClickedButtonChar()
{
char s1[9];
char s2[9];
char sSum[17];
UpdateData(TRUE);
lstrcpy(s1, (LPCTSTR)m_sChar1);
lstrcpy(s2, (LPCTSTR)m_sChar2);
addchar(s1, s2, sSum);
m_sCharSum = sSum;
UpdateData(FALSE);
}
위의 방법으로 DLL 함수를 호출하는 프로그램을 작성하여 실행하면 각 함수들이 정확하게 호출되고 있음을 확인할 수 있다.
1.3.2 Using DLL with VC++ (Link Explicitly)
Explicit Link를 사용하여 DLL 함수를 호출하는 경우, Header 파일과 Library 파일은 필요하지 않다. 단지 그 함수의 원형만을 알고 있으면 된다. Explicit Link를 사용하기 위하여 아래와 같은 순서로 DLL 함수를 호출하는 기능을 작성한다.
- 1.3.1에서와 같이 Dialog-Based 프로젝트를 생성한 후, 같은 모양으로 Dialog를 만들고 멤버변수를 연결한다.
- DLL 함수호출을 위한 Button의 Handler를 만들고 아래와 같이 코드를 작성한다.
[SimpleDllTest2.Dlg.cpp]
void CSimpleDllTest2Dlg::OnAddint()
{
int (*lpfnAddInt)(int, int);
HINSTANCE hLib = LoadLibrary("Win32DLLSample.dll");
if(hLib == NULL)
return;
lpfnAddInt = (int(*)(int, int))GetProcAddress(hLib, "addint");
if(lpfnAddInt == NULL)
{
FreeLibrary(hLib);
return;
}
UpdateData(TRUE);
m_nSumInt = lpfnAddInt(m_nInt1, m_nInt2);
UpdateData(FALSE);
FreeLibrary(hLib);
}
위와 같이 LoadLibrary를 이용하여 Explicit Link로 DLL 함수를 호출하도록 하면, 별도의 Library와 Header가 필요하지 않으며, 실행 시간에 DLL의 Load와 Unload의 시점을 결정할 수 있는 장점이 있다.
1.3.3 Using DLL with C#
C#에서는 DLL의 함수 호출이 매우 간단하다. 단지 어떤 Library의 어떤 함수를 사용할 것인지에 대한 선언만 정확이 명시하면 된다. 아래와 같이 “addint” 와 “addchar” 함수를 선언한다.
C#은 Calling Convention 등을 모두 속성으로 설정 가능하므로, 대부분의 DLL을 그대로 호출할 수 있다.
[DllImport("Win32DLLSample.dll")]
public static extern int addint(int n1, int n2);
[DllImport("Win32DLLSample.dll")]
public static extern int addchar(string s1, string s2, StringBuilder sum);
함수선언 후, 실행을 위한 Handler에서 아래와 같이 호출한다.
private void btnInt_Click(object sender, EventArgs e)
{
int n1, n2;
n1 = int.Parse(txtN1.Text);
n2 = int.Parse(txtN2.Text);
int sum = addint(n1, n2);
txtNSum.Text = sum.ToString();
}
private void btnString_Click(object sender, EventArgs e)
{
var sb = new StringBuilder(100);
addchar(txtS1.Text, txtS2.Text, sb);
txtSSum.Text = sb.ToString();
}
위에서 사용된 DllImport Attribute는 몇가지 옵션필드를 가질 수 있다. 이 필드 중 주요한 항목은 아래와 같다. 자세한 내용은 DllImportAttribute Class를 참조한다.
필드 | 설명 |
Calling Convention | DLL 내의 Export 함수에 대한 Calling Convention을 지정할 수 있다. Cdecl, Winapi, StdCall 을 지원하며, 기본값은 StdCall 이다. |
CharSet | 문자열에 사용할 Character Set을 설정한다. None(자동), Unicode 값을 가질 수 있다. |
Entry Point | DLL 내의 함수가 호출되는 이름을 나타낸다. 이를 이용하면 함수진입점을 지정하여, 선언시 다른 이름으로 별칭을 이용할 수도 있다. |
아래는 DllImport Attribute를 사용하여 함수에 별칭을 부여한 예이다.
[DllImport(“user32”, CharSet = CharSet.UniCode, EntryPoint = “MessageBoxW”)]
public static extern int MsgBox(int hWnd, String pText, String pCaption, int uType);
Unmanaged Code의 Data Type은 C#(Managed Code)에서 아래와 같이 변환한다.
C (Unmanaged Code) | C# (Managed Code) |
HANDLE, void* 또는 일반 pointer | IntPtr |
BYTE, unsigned char | Byte |
short | Short |
WORD, unsigned short | Ushort |
int | int |
UINT, unsigned int | uint |
long | int |
BOOL, long | int |
DWORD, unsigned long | uint |
char | char |
LPSTR, char* | string 또는 StringBuilder |
LPCSTR, const char* | string 또는 StringBuilder |
BSTR | string |
float | float |
double | double |
HRESULT | int |
VARIANT | object |