[C++] 라이브러리를 작성해서 사용하기 (Window 환경의 lib, dll)


Development note/C , C++ , MFC  2020. 4. 21. 19:58

안녕하세요. 명월입니다.


이 글은 C+에서 라이브러리를 작성해서 사용하기(Window 환경의 lib, dll)에 대한 글입니다.


C++에서 실행 파일이 아닌 하나의 라이브러리를 작성해서 오픈 소스로 공유하거나 패키지를 나누어서 사용하게끔 만들 수가 있습니다.

이 라이브러리는 운영체제마다 사용하는 방법이 조금 다른데 lib와 dll은 보통 window 환경에서 linux환경에서는 a나 so파일를 사용합니다.


먼저 조금 개발하기 편한 윈도우 환경에서 라이브러리를 작성해서 사용해 보겠습니다. linux환경에서는 다른 글로 설명하겠습니다.

라이브러리가 lib과 dll의 종류가 있습니다. 이 두 종류는 사용하는 방법과 결과가 조금 다른데..

lib은 빌드 할 때 참조(linking)하는 라이브러리로 최종 결과는 하나의 실행 파일(.exe)로 묶여서 빌드됩니다. 장점은 역시 빌드 할 때 참조하니 에러가 발생하면 빌드 단계에서 확인이 가능한 장점이 있으니 라이브러리 수정시 재 빌드를 해야하는 단점이 있습니다.

dll은 동적 참조(linking)하는 라이브러리로 Runtime(실행 단계)시 참조가 됩니다. 장점은 dll 자체만 따로 버전 관리가 된다는 장점이 있으나 문제는 버그가 발생하면 실행하기 전까진 알 수가 없다는 점이 있습니다.


정적 라이브러리 LIB

먼저 lib파일 작성하고 실행 파일에 붙여서 작성하겠습니다.

먼저 new project로 static library를 선택합니다. 저는 기존 프로젝트에서 추가했습니다만, 바로 new Project에서 생성해도 됩니다.

프로젝트 명을 입력하고 프로젝트를 생성합니다.


기본 파일이 생성됩니다만 일단 무시하고 우리는 클래스 파일을 하나 생성합니다.

Node라는 이름의 클래스를 생성했습니다.

#pragma once
#include <stdio.h>
#include <iostream>
using namespace std;
// Node 클래스
class Node
{
private:
  // data 전역 변수
  int data;
public:
  // data값을 넣기 위한 함수
  void setData(int data);
  // 값을 콘솔에 표시하는 함수
  void print();
};
#include "pch.h"
// 해더 참조
#include "Node.h"
// data값을 넣기 위한 함수
void Node::setData(int data)
{
  // 파라미터 값을 전역 변수 data에 넣는다.
  this->data = data;
}
// 값을 콘솔에 표시하는 함수
void Node::print()
{
  // 콘솔 출력
  cout << "data = " << this->data << endl;
}

그리고 main 프로젝트에서 참조하기 쉽게 먼저 빌드 output 경로를 수정합니다.

properties 설정으로 갑니다.

output 디렉토리를 위 이미지의 Output directory에 설정 할 수 있습니다.

그러나 저는 조금 다르게 build events를 사용해서 설정하겠습니다.

그리고 빌드 후에 lib 파일과 header 파일을 프로젝트의 lib 폴더로 복사하겠습니다.

rmdir "$(SolutionDir)lib" /q /s
if not exist "$(SolutionDir)lib" mkdir "$(SolutionDir)lib"
if not exist "$(SolutionDir)lib\header" mkdir "$(SolutionDir)lib\header"
copy $(SolutionDir)$(Configuration)\*.lib $(SolutionDir)lib\.
copy $(ProjectDir)*.h $(SolutionDir)lib\header\.

이렇게 작성하고 일단 라이브러리를 빌드합니다.

그럼 lib 디렉토리에 해더파일(.h)과 lib 파일이 생성된 것을 확인할 수 있습니다.


이제 본 소스에서 라이브러리를 참조하겠습니다.

라이브러리에서 참조 하는 방법은 두가지가 있습니다. 소스상에서 설정하는 방법과 linker 설정을 하는 방법이 있습니다.

먼저 소스 상에서의 설정 방법입니다.

// lib 파일이 있는 곳을 참조한다.
#pragma comment(lib,"../lib/LibExample.lib");
#include <stdio.h>
#include <iostream>
#include <crtdbg.h>
// 라이브러리 폴더의 header 폴더의 Node를 참조
#include "../lib/header/Node.h"
// 메모리 릭을 콘솔에 표시하기 위한 함수
#if _DEBUG
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#define malloc(s) _malloc_dbg(s, _NORMAL_BLOCK, __FILE__, __LINE__)
#endif
using namespace std;
// 실행 함수
int main()
{
  // library의 Node 클래스 할당
  Node a;
  // 값을 넣고
  a.setData(10);
  // 콘솔 출력
  a.print();

  _CrtDumpMemoryLeaks();
  return 0;
}

lib 라이브러리는 해더를 꼭 참조 해야 합니다.

그리고 pragma 전처기문으로 lib를 연결하면 사용할 수 있습니다.


저 pragma를 사용하지 않고 참조하는 방법입니다.

실행 함수가 있는 프로젝트의 properties를 접속해서 Linker의 일반 -gt; 추가 라이브러리 디렉토리를 설정해서 lib파일이 있는 디렉토리를 설정합니다.

그런 다음 입력 탭의 추가 의존성 영역에 lib를 추가합니다.


결과는 위와 같습니다.

만약 lib 참조가 되지 않으면 Linker 에러를 만날 수 있습니다.


최종 결과는 이렇게 exe 파일만 나오게 됩니다.


동적 라이브러리 DLL

동적 라이브러리를 작성하기 위해서 먼저 프로젝트를 추가합니다.

이번에는 동적 라이브러리 Dynamic link Libarary를 추가합니다.

이번에도 적당히 프로젝트 명을 작성하고 프로젝트를 생성합니다.


기본 파일이 생성됩니다만 일단 무시하고 우리는 클래스 파일을 하나 생성합니다.

이번에도 Node 클래스를 생성하겠습니다.

#pragma once
#include <stdio.h>
#include <iostream>
using namespace std;
// Node 클래스
class Node
{
private:
  // data 전역 변수
  int data;
public:
  // data값을 넣기 위한 함수
  void setData(int data);
  // 값을 콘솔에 표시하는 함수
  void print();
};
// 외부에서 참조할 instance 함수
extern "C" _declspec(dllexport) void* instance()
{
  // Node 클래스를 생성한다.
  return new Node();
}
// 외부에서 참조할 setData 함수
extern "C" _declspec(dllexport) void setData(void* node, int data)
{
  // Node 포인터을 받아서
  Node* p = (Node*)node;
  // 값을 입력한다.
  p->setData(data);
}
// 외부에서 참조할 print 함수
extern "C" _declspec(dllexport) void print(void* node)
{
  // Node 포인터을 받아서
  Node* p = (Node*)node;
  // print 함수를 실행한다.
  p->print();
}
// 외부에서 참조할 close 함수
extern "C" _declspec(dllexport) void close(void* node)
{
  // Node 포인터을 받아서
  Node* p = (Node*)node;
  // 메모리 해제
  delete p;
}
#include "pch.h"
// 해더 참조
#include "Node.h"
// data값을 넣기 위한 함수
void Node::setData(int data)
{
  // 파라미터 값을 전역 변수 data에 넣는다.
  this->data = data;
}
// 값을 콘솔에 표시하는 함수
void Node::print()
{
  // 콘솔 출력
  cout << "data = " << this->data << endl;
}

lib와 다르게 extern "C" _declspec(dllexport) 구문이 있습니다.

lib의 경우는 lib 파일과 해더 파일이 함께 참조가 되는 데 동적 dll의 경우는 해더 파일 없이 dll 파일만 참조됩니다.


여기서 문제가 소스에서 사용할 Node클래스를 선언하지 못한다는 게 문제입니다. 즉, Node 함수는 dll 라이브러리 안에서만 사용되어야 하고 외부 함수로 포인터를 주고 받는 식으로 설정할 수 밖에 없습니다.


여기도 output 파일(dll)를 옮기겠습니다.

rmdir "$(SolutionDir)dll" /q /s
if not exist "$(SolutionDir)dll" mkdir "$(SolutionDir)dll"
copy $(SolutionDir)$(Configuration)\*.dll $(SolutionDir)dll\.

이제 빌드를 하면 dll 폴더에 dll파일이 생성된 것을 확인할 수 있습니다.

이제 생성된 이 dll를 실행 소스에서 참조하겠습니다.

#include <stdio.h>
#include <iostream>
#include <crtdbg.h>
#include <Windows.h>
// 메모리 릭을 콘솔에 표시하기 위한 함수
#if _DEBUG
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#define malloc(s) _malloc_dbg(s, _NORMAL_BLOCK, __FILE__, __LINE__)
#endif
using namespace std;
// 실행 함수
int main()
{
  // dll 파일을 로드한다.
  HMODULE dll = LoadLibrary(L"../dll/DllExample.dll");
  // 로드가 실패하면 종료.
  if (dll == NULL) {
    cout << "Load failt!" << endl;
    return 1;
  }
  // 외부로 참조된 instance 함수를 취득
  PVOID ptr = GetProcAddress(dll, "instance");
  // 함수 포인터를 이용하여 포인터를 캐스팅한다
  void*(__cdecl * instance)() = (void* (__cdecl*)())ptr;
  // Node 클래스를 생성. 여기서 Node 지시자를 사용할 수 없기 때문에 void*로 참조한다.
  void* node = instance();
  // setData 함수를 취득
  ptr = GetProcAddress(dll, "setData");
  // 함수 포인터를 이용하여 포인터를 캐스팅한다
  void (__cdecl * setData)(void*, int) = (void(__cdecl*)(void*, int))ptr;
  // Node 클래스에 10의 값을 넣는다.
  setData(node, 10);
  // print 함수를 취득
  ptr = GetProcAddress(dll, "print");
  // 함수 포인터를 이용하여 포인터를 캐스팅한다.
  void(__cdecl * print)(void*) = (void(__cdecl*)(void*))ptr;
  // Node 클래스의 print함수를 호출
  print(node);
  // close 함수를 취득
  ptr = GetProcAddress(dll, "close");
  // 함수 포인터를 이용하여 포인터를 캐스팅한다.
  void(__cdecl * close)(void*) = (void(__cdecl*)(void*))ptr;
  // Node 클래스의 메모리를 해지한다.
  close(node);

  _CrtDumpMemoryLeaks();
  return 0;
}

dll이 lib에 비해 사용하는 방법이 복잡해 보입니다만, 조금만 소스 정리하면 소스 상에서 dll과 lib의 참조하는 소스 스탭은 거의 차이가 없을 것입니다.


막상 작성해 보니 dll에서는 클래스를 직접 참조가 불가능하네요. 전체적으로 완성된 라이브러리, 즉, 함수를 하나 호출해서 결과를 받는 식의 라이브러리라면 dll이 더 좋을 듯 싶은데..

클래스부터 결합성이 높은 라이브러리라면 lib 라이브러리가 더 편하겠습니다.


그리고 dll의 경우는 c++뿐 아니라 C#이나 Java에서도 참조가 가능한 동적 라이브러리입니다. 그것에 관해서 다른 글로 설명하겠습니다.


여기까지 C+에서 라이브러리를 작성해서 사용하기 (Window 환경의 lib, dll)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.