본문 바로가기

CS연구소👨‍💻/C++

[C++] C++로 응용 프로그램 만들기, DLL과 LIB차이, CMAKE로 DLL 생성

이런 것에 대한 정보를 담고 있다

  1. C++ 프로그래밍 언어에서 실행 가능한 파일을 만드는 법
  2. lib파일과 dll파일의 차이점
  3. dll파일을 만드는 방법
  4. dll파일을 로드하는 방법중 하나

이런 것에 대한 정보는 담겨있지 않다

  1. dll파일을 로드하는 방법중 나머지 방법들

C++ 언어에서 실행 가능한 파일을 만드는 법은 컴파일러를 이용하는 법과 CMAKE를 이용하는 법 두가지가 있다.

1.1 CMake를 사용하지 않고 실행 가능한 파일을 만드는 법

clang++ -std=c++17 -I/usr/local/include
-L/usr/local/lib 
-lboost_system 
-lboost_thread websocket.cpp -o websocket

1.2 CMake를 통해 C++ 프로그래밍 언어에서 실행 가능한 파일을 만드는 법 : add_executable

add_executable( ... ...)

CMAKE는 복잡한 C++의 빌드를 한결 단순화해주는 역할을 한다. 기본적으로 CMake는 CMakeLists.txt 파일을 통해서 실행파일을 빌드한다. 그리고 거기서 실행파일을 만들기 위해 반드시 들어가야하는 코드가 add_executable이다.

cmake_minimum_required(VERSION 3.10)

project(WebSocketProject VERSION 1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# include 코드
include_directories(include)

# 라이브러리 만드는 코드
# add_library(WebsocketDll SHARED WebsocketDll.cpp)

# 실행가능한 파일 만드는 코드
add_executable(websocket_test websocket_import.cpp include/WebsocketDll.cpp)

add_executable은 해석하자면

add_executable(완성된 실행파일의 이름, {옵션}, 모든 소스파일의 나열)

으로 나타낼 수 있으며 options는 윈도우냐(WIN32), 맥이냐 (MACOSX_BUNDLE) 자동이냐 ( EXCLUDE_FROM_ALL) 정도이다. 경험상 옵션부분은 제거하고 실행해도 무방했다.

2. lib파일과 dll파일의 차이점.

한 문장으로 설명하자면 lib파일은 정적 라이브러리 파일이고 dll은 동적 라이브러리 파일이라는 것이 다르다.

라이브러리란 외부의 완성된 파일을 내 소스파일에 합치는 일이다. 컴파일러는 오브젝트 파일을 실행가능한 파일로 만들면서 라이브러리도 같이 묶은 실행 파일을 생성한다. 따라서 실행파일에는 라이브러리의 내용이 모두 들어가게 된다. 그러다보니 라이브러리의 용량이 클 수록 실행파일의 용량이 커진다.

반대로 dll파일은 실행파일을 만들때 라이브러리의 내용이 전부 들어가지 않는다. 프로세스가 실행되는 런타임에 동적으로 로드가 가능하기 때문이다.

이런 링킹을 구현하는 방법에는 두가지가 있는데 원하는 객체, 함수의 포인터만을 불러오는 명시적 링킹과 내 실행파일에 특정 dll 함수를 사용하겠다는 것을 포함시키고 실행하는 방법 두가지가 있다.

이 명시적 링킹과 암시적 링킹중 암시적 링킹이 많이 사용된다고 한다.

암시적 링킹을 사용하기 위해선 dll과 lib 파일이 각각 하나씩 필요하다. 여기서 lib 파일은 dll을 통해 불러올 함수가 들어있다.

이 생성된 부수적 lib 파일은 컴파일시 사용되며 링커에게 lib의 경로를 알려주고 헤더파일의 경로를 알려주면 컴파일에 성공한다. 하지만 컴파일이 성공해도 dll파일이 없다면 실행에는 실패한다.

실행시에 exe 파일에 맞는 dll 파일이 존재한다면 실행에 성공할 수 있다.

3. dll 파일을 만드는 방법

lib파일은 오브젝트 파일을 정렬한 형태이며 링커에 의해서 소스가 합쳐지는 것과 같다. 그렇기에 합쳐진 lib 파일을 사용하는 것은 일반적인 #include와 같다. 반면에 dll은 런타임에 함수와 객체를 읽는 것이기 때문에 dll파일을 생성하고 읽는데 다른 접근이 필요하다.

  1. 소스 파일 내에 함수와 클래스 앞에 exportdll 선언
  2. 빌드 파일 설정
     cmake_minimum_required(VERSION 3.10)
    
     project(WebSocketProject VERSION 1.0 LANGUAGES CXX)
    
     set(CMAKE_CXX_STANDARD 17)
     set(CMAKE_CXX_STANDARD_REQUIRED True)
    
     # include 코드
     include_directories(include)
    
     # 라이브러리 만드는 코드
     add_library(WebsocketDll SHARED WebsocketDll.cpp)
    
     # 실행가능한 파일 만드는 코드
     # add_executable(websocket_test websocket.cpp)
    실행파일을 만들 때와 다른 것은 add_executable 대신 add_library를 사용한다는 정도이다.
  3. 이제 export할 소스파일을 만들었으면 라이브러리를 빌드해줄 요소를 찾는다. 앞에서 사용했던 cmake를 이용하면 간단하게 라이브러리도 만들 수 있다.

3.1 __declspec(dllexport)

__declspec는 스토리지 클래스 정보를 지정하기 위한 확장 특성 구문이다. 윈도우 공식 설명에서는 이를 static과 extern과 비교했다. 하지만 static은 데이터를 코드 영역에 저장하는 역할을 하는데 비해 이 키워드는 데이터를 dll외부에서 접근할 수 있도록 도와주는 역할을 하지 데이터를 특정 영역에 저장하지 않는다

__declspec이 대상으로 삼을 수 있는 것은 함수와 클래스, 클래스 멤버 등이다 클래스를 내보내게 되면 모든 멤버함수와 정적데이터가 같이 보내진다.

3.2 extern

extern도 어떤 접근 가능성을 관리하는 코드라고 할 수 있다. extern은 컴파일 단계에서 대사의 정의가 외부에 열려있다는 것을 알린다. 그럼으로써 외부의 코드는 이 대상에 접근할 수 있게 해준다.

4. dll파일을 로드하는 법

이렇게 완성된 dll파일을 로드하기 위해서는 가장 러프하게 말해 LoadLibrary함수와 GetProcAddress함수를 사용한다. 다음은 위에서 완성된 Dll파일을 import하는 과정이다,

// #include <include\WebsocketDll.h> // 불필요한 헤더 파일 포함 방지
#include "WebsocketDll.h"
#include <iostream>

using namespace std;
typedef WebsocketServer* (*CreateServer)();

int main()
{
    HINSTANCE hInst = LoadLibraryA("../../WebsocketDll.dll");
    if (!hInst)
    {   
        cout << "can't load DLL  \n";
    }

    CreateServer createServer = (CreateServer) GetProcAddress(hInst, "CreateWebsocketServer");
    if (!createServer)
    {
        cout << "함수 주소 가져오기 실패 \n";
        FreeLibrary(hInst);
        return -1;
    }
    WebsocketServer* websocketServer = createServer();
    if (websocketServer)
    {
        websocketServer -> init();
        websocketServer -> run();
    }
    FreeLibrary(hInst);
}

LoadLibrary

LoadLibraryA는 존재하는 DLL의 위치를 알아내서 그 파일에 접근할 수 있는 HINSTANCE(HMODULE) 객체를 리턴한다.

GetProcAddress 는 dll에서 정의된 대상들을 가져오는데 쓰인다 예시 코드는 Dll안 객체를 얻기 위한 함수를 load하는 과정을 보여준다.

  1. 함수 타입 정의
     typedef WebsocketServer* (*CreateServer)();
  2. 함수도 결국 어떤 크기를 갖는 변수이자 대상이다. C++은 함수의 포인터 타입을 설정할 수 있다.
  3. GetProcAddress 사용하기createServer 객체는 함수 자체의 포인터가 된다 그리고 GetProcAddress 함수는
  4. GetProcAddress(Loadlibrary의 핸들, “함수의 이름”) 형태를 이름에 맞는 대상을 가져오고 우리는 그것을 CreateServer로 다시 캐스팅할 수 있다.
  5. CreateServer createServer = (CreateServer) GetProcAddress(hInst, "CreateWebsocketServer");
  6. 가져온 함수 사용가져온 함수는 ()를 붙이면 원래 함수처럼 사용할 수 있게 된다. 이런 과정을 통해서 우리는 dll의 함수를 불러올 수 있게 됐다
  7. WebsocketServer* websocketServer = createServer();

'CS연구소👨‍💻 > C++' 카테고리의 다른 글

[C++] WCHAR, TCHAR, cstring 정리  (0) 2024.11.16
[C++] 포인터, 참조  (0) 2024.05.03
[C++] RTTI  (1) 2024.04.26