본문으로 바로가기

C/C++ DLL 생성 및 C# 윈폼에서 사용하기

category 프로그래밍/C# 2024. 4. 19. 00:02
반응형

우선 라이브러리라고 함은 코드, 함수 등을 다른 프로그램에서 다시 작성하지 않고 재사용할 수 있도록 해놓은 모음이다.

DLLdynamic-link library의 약자로 동적 링크 라이브러리라고 한다. dynamic-link는 컴파일 시 함수를 생성하는 것이 아니라 실행을 하는 동안에 함수를 로드하고 생성을 한다. dll의 내부에는 함수의 주소 테이블을 포함하고 있고 마치 펌웨어 파일과 유사하다.

C#으로 윈폼을 개발하면 생산성도 좋지만 WPF 같이 시각적인 디자인을 개발하기에 적합하다. C++은 성능 및 기능 확장에 유용한데, 하드웨어를 제어하는데 적합하다. 이 둘의 장점을 합쳐서 C#으로만 구현하기 어려운 기능들을 만들 수 있게 된다.

DLL 동적 연결 라이브러리

 

Visual Studio를 실행하여 DLL 검색하여 프로젝트를 생성한다.

 

 

 

dll 프로젝트 생성

 

DLL 프로젝트를 생성하면 위에와 같이 프로젝트 코드가 생성이 된다. 크게 ATTACH DETACH가 보이는데 ATTACH는 연결이 될 때 DETACH는 분리될 때 수행이 되는 작업이다. 간단하게 Open/Close라고 생각하면 될 것 같다.

 

지금 VS2022버전을 사용하고 있는데 22버전에서는 DLL 프로젝트를 생성할 때 빈프로젝트로 만드는 기능이 없는 것으로 보인다. 각자의 취향에 맞게 위에 코드를 Block 처리하고 빈프로젝트를 사용 혹은 위에 템플릿을 사용하던지 하면 될 것 같다. 보통 나는 위에 템플릿을 그대로 사용하긴 한다.

 

 

 

솔루션 탐색기에서 프로젝트 우클릭을 하여 TEST1, TEST2, C# 윈폼을 생성한다. 여기서 TEST1, TEST2 DLL 프로젝트이다.  

 

 

3개의 프로젝트가 생성이 되었으면 여기서 윈폼 프로젝트를 우클릭하고 시작 프로젝트로 설정을 한다. 이렇게 설정을 하는 이유는 우선 dll은 실행 프로그램이 아니다. 프로그램 호출에 의해서 사용이 되기 때문에 에러 팝업이 발생을 하게 된다.

 

 

 

생성된 dll 파일

빌드 설정을 debug에 x64로 설정을 하였다고 가정하면 사용하고 있는 프로젝트에 bin 폴더에는 위에와 같이 dll이 생성이 되어있을 것이다. 여기서 주의점은 실행 파일과 dll파일 모두 아키텍처가 동일해야 한다. x86(32비트) 혹은 x64(64비트) 혼용해서 사용하면 안 된다. 테스트 코드는 아래 내용 참조.

 

dll 파일과 실행 파일

실행 파일에서 dll을 검색하는 코드를 별도로 작성하지 않았다면 실행 파일과 dll 파일은 같은 폴더 내에 존재를 해야한다. dll을 실행 파일로 움기는 작업이 필요한데, 번거로우면 프로젝트 속성에서 경로를 별도로 지정을 해주면 된다.

 

 

 

C# 윈폼

간단하게 RichTextBox와 열고 닫기를 할 수 있는 버튼 4개를 추가하였다.

 

 

 

TEST1 DLL
1
2
3
4
5
6
7
8
9
// dllmain.cpp : DLL 애플리케이션의 진입점을 정의합니다.
#include "pch.h"
 
#define DLLEXPORT extern "C" __declspec(dllexport)
 
DLLEXPORT int add(int a, int b)
{
    return (a + b);
}
 
 

 

TEST1 DLL에는 a + b를 더하여 return 값을 넘겨주는 코드.

 

TEST2 DLL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <Windows.h>
#include "pch.h"
 
#define DLLEXPORT extern "C" __declspec(dllexport)
 
HANDLE hThread;
HANDLE hEvent;
 
DWORD WINAPI ThreadTest2Ctl(LPVOID arg);
 
BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        hThread = hModule;
        break;
 
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;
 
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
 
DLLEXPORT int OpenTest2Dll(HWND hWnd)
{
    int nRet = 0;
 
    hThread = CreateThread(NULL0, ThreadTest2Ctl, (LPVOID)00NULL);
 
    if (hThread == NULL) {
        nRet = GetLastError();
    }
    hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
 
    if(hEvent == NULL) {
        nRet = GetLastError();
    }
 
    return nRet;
}
 
DLLEXPORT int CloseTest2Dll()
{
    int nRet = 0;
    const DWORD dwWaitTick = 5000;
    DWORD dwStartTime = GetTickCount();
 
    if (hThread != NULL) {
        CloseHandle(hThread);
    }
    DWORD dwWaitResult = WaitForSingleObject(hThread, dwWaitTick);
 
    if (dwWaitResult == WAIT_TIMEOUT) {             // timeout
        nRet = -1;
    }
    else if (dwWaitResult == WAIT_OBJECT_0) {       // Thread closed ok
        hThread = NULL;
    }
    else {                                          // etc        
        nRet = -2;
    }
 
    if (hEvent != NULL) {
        CloseHandle(hEvent);
    }
 
    return nRet;
}
 
DWORD WINAPI ThreadTest2Ctl(LPVOID arg)
{
    int nResult;
    int cnt = 0;
 
    while (1) {
        nResult = WaitForSingleObject(hEvent, 1000); // Sleep(1000); // 1초 대기
        cnt++;
    }
    ExitThread(1);
}
 
 
 

Thread 생성, Thread 닫는 코드.

 

DLLEXPORT int CloseTest2Dll()

여기서 Close 함수는 잘못 작성이 되었기 때문에 return 값은 항상 -2가 나오게 된다.

 

 

DWORD WINAPI ThreadTest2Ctl(LPVOID arg)

해당 함수에서는 

nResult = WaitForSingleObject(hEvent, 1000);

1초마다 cnt++를 수행하고 있다.

 

C# 윈폼 테스트 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
 
namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        [DllImport("Test1.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
        public static extern int add(int a, int b);
 
        [DllImport("Test2.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
        public static extern int OpenTest2Dll(IntPtr hWnd);
 
        [DllImport("Test2.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
        public static extern int CloseTest2Dll();
 
        public Form1()
        {
            InitializeComponent();
        }
 
        private void Dll1OpenBtn_Click(object sender, EventArgs e)
        {
            MsgRtb.AppendText($"add: {add(1, 3)}\r\n");
        }
 
        private void Dll1CloseBtn_Click(object sender, EventArgs e)
        {
// 필요없는데 넣었음 ㅡㅡ;;
        }
 
        private void Dll2OpenBtn_Click(object sender, EventArgs e)
        {
            int lret = 0;
 
            lret = OpenTest2Dll(this.Handle);
 
            if (lret == 0)
            {
                MsgRtb.AppendText("Dll2이 열렸습니다.\r\n");
            }
        }
 
        private void Dll2CloseBtn_Click(object sender, EventArgs e)
        {
            int lret = 0;
 
            lret = CloseTest2Dll();
 
            if (lret == 0)
            {
                MsgRtb.AppendText("Dll2이 닫혔습니다.\r\n");
            }
        }
    }
}
 
 
 
 
 
 

 

        [DllImport("Test1.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
        public static extern int add(int a, int b);
        [DllImport("Test2.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
        public static extern int OpenTest2Dll(IntPtr hWnd);
        [DllImport("Test2.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
        public static extern int CloseTest2Dll();

윈폼에서 DLL을 사용할 수 있도록 불러오고 각 버튼에 DLL의 기능들을 넣었다.

 

참고로 DLL과 DLL을 호출하는 윈폼의 함수호출규약이 일치해야 한다.

CallingConvention.StdCall은 StdCall을 사용하겠다는 뜻이며 WinApi에서 주로 쓰인다.

자세한 내용은 이전글 참조...

2024.01.09 - [프로그래밍] - cdecl과 stdcall의 차이점 __cdecl과 __stdcall 비교

 

 

C# 윈폼 테스트

Test1 DLL의 열기 버튼을 하였을 때 1 + 3인 4가 return 값으로 들어온다.

그리고 Test2 DLL 열기 버튼을 눌렀을 때 Thread가 생성이 되었을 것이다. 닫기 버튼은 코드가 잘못 작성이 되어 항상 -2가 나오기 때문에 제대로 작동을 안 하고 있다. 사용할 수 있다는 정도만 ㅎㅎ;;

dll에서 Thread를 만들어서 사용할 일이 아마 많이 없을 것이다.

반응형