반응형

C++에서 간단한 dll를 만들어 C++에서 사용하기

 MS사에서 제공하는 MVC++ 컴파일러는 모던 C 표준 지원을 제대로 하지 않는다. 이러한 이유 때문인지 프로그래밍 입문 과목중에 C/C++이라는 과목이 존재하는 듯 하다.


 MVC++에서는 함수를 다른 프로그램에 사용할 수 있도록 내보내거나 들여오는 매크로를 지원한다. MSDN에서는 간단한 예시를 제공한다.


내보내기 예시)

우선 MathFuncsDll 이름의 프로젝트를 생성한다. 생성할때 dll프로젝트로 생성해야 하는데, VS 2017부터는 기존 마법사로 생설할 경우 "Visual C++" 하위의 "Windows 데스크톱" 항목의 Windows 데스크톱 마법사에서 생성하면된다. 마법사가 귀찮다면, DLL로 생성하면 된다. 

// MathFuncsDll.h

#ifdef MATHFUNCSDLL_EXPORTS
#define MATHFUNCSDLL_API __declspec(dllexport)
#else
#define MATHFUNCSDLL_API __declspec(dllimport)
#endif

namespace MathFuncs
{
    // This class is exported from the MathFuncsDll.dll
    class MyMathFuncs
    {
    public:
        // Return a + b
        static MATHFUNCSDLL_API double Add(double a, double b);

        // Return a - b
        static MATHFUNCSDLL_API double Subtract(double a, double b);

        // Return a * b
        static MATHFUNCSDLL_API double Multiply(double a, double b);

        // Return a / b
        // Throws const std::invalid-argument & if b is 0
        static MATHFUNCSDLL_API double Divide(double a, double b);
    };
}


위의 매크로 함수를 정의 하는데, dll프로젝트일 경우 컴파일러의 전처리 옵션에 [대문자 프로젝트 명] + "_EXPORTS"(여기서는 "MATHFUNCSDLL_EXPORTS")의 형태의 매크로가 자동으로 추가 되어 있다. 이러한 특징을 이용해서 헤더 파일에 DLL 프로젝트의 경우 내보내는 매크로 함수"__declspec(dllexport)"가 되고 그외 프로젝트에서는 가져오는 매크로 함수 "__declspec(dllimport)"로 치환이 된다.

 이렇게 만들어진 헤더 파일은 나중에 참조하는 실행 프로그램에서도 include 해야 한다.


// MathFuncsDll.cpp

#include <iostream>
#include "mathfuncsdll.h"

using namespace std;

namespace MathFuncs
{
    double MyMathFuncs::Add(double a, double b)
    { return a + b; }

    double MyMathFuncs::Subtract(double a, double b) 
    { return a - b; }

    double MyMathFuncs::Multiply(double a, double b)
    { return a * b; }

    double MyMathFuncs::Divide(double a, double b)
    {
        if(b == 0)
        {
            throw invallid_argument("b cannot be zero");
        }
        return a / b;
    }
}

 cpp 파일까지 작성하였다면 빌드를 하면 된다. 이때 "win32"로 했는지 "x64"으로 했는지 구분하는 것이 좋다. 에러가 없다면, 가능하면 Release로 빌드하여 최적화를 하자.



들여오기 사용 예시1)

 실행하는 프로젝트의 경우 선행 작업이 조금 필요하다. 우선 새로운 프로젝트를 만들자 이름은 대략 "MyExceRefDll"로 빈프로젝트로 만들어도 무난하다. 앞의 프로젝트에서 빌드한 파일중에 .lib와 dll 파일이 생성된 것을 알 수 있다. 빌드와 링크과정에는 헤더파일과 lib 파일이 필요하다. 헤더파일은 솔루션 탐색기에 추가를 시켜야 한다. "솔루션 탐색기 -> 헤더파일 -> 우클릭 -> 추가 -> 기존항목 -> 헤더파일"


// MyExecRefsDll.cpp

#include <iostream>
#include "MathFuncsDll.h"

using namespace std;

int main(void)
{
    double a = 7.4;
    int b = 99;

    cout << "a + b = " << MathFuncs::MyMathfuncs::Add(a, b) << endl;
    cout << "a - b = " << MathFuncs::MyMathfuncs::Subtract(a, b) << endl;
    cout << "a * b = " << MathFuncs::MyMathfuncs::Multiply(a, b) << endl;
    cout << "a / b = " << MathFuncs::MyMathfuncs::Divide(a, b) << endl;

    try
    {
        cout << "a / 0 = " << MathFuncs::MyMathFuncs::Divide(a, 0) << endl;
    }
    catch(const invalid_argument & e)
    {
        cout << "Caught exception: " << e.what() << endl;
    }

    return 0;
}

 lib파일은 "솔루션 탐색기 -> 프로젝트 -> 우클릭 -> 속성" 에서 "링커 -> 일반" 항목중 "추가 라이브러리 디렉토리"에 lib파일이 있는 경로를 추가 시킨다. 이후 "링커 -> 입력" 항목중 "추가 종속성"에 추가할 lib 파일명을 적어주면 된다. 이 예시에서는 "MathFuncsDll.lib"를 적어주면 된다.


 링크 옵션을 마치면, 컴파일을 한다. 이때 공부할때 습관적으로 F5키로 실행까지 하는데, 실행파일이 있는 폴더에 dll 파일(여기서는 "MathFuncsDll.dll")을 넣고 실행해야 한다. 별다른 오류 메시지 창이 안보이면, 잘 된거다.



들여오기 사용 예시2)

 앞의 예시는 MVC++ 컴파일러의 옵션 값을 넣어서 dll을 참조했다면, 이번에는 옵션 값을 수정하지 않고 참조 해보자. 앞의 예시와 동일하게 헤더파일을 솔루션 탐색기에서 인식할 수 있도록 추가 해야 한다.

 실행 프로그램의 소스 코드중에 MVC++에서 사용하느 매크로 함수 "#pragma comment()"를 사용하여 참조할 lib 파일의 경로와 파일명을 알려주면 된다. 그럼, 앞의 "예시1"의 컴파일 옵션을 컴파일러가 해당 소스를 읽을때 컴파일 옵션을 설정하기 때문에 따로 속성창을 열어서 수정하지 않아도 된다.


// MyExecrefsDll.cpp
// ...
#pragma comment(lib, "../../MathFuncsDll/Debug/MathFuncsDll.lib");
// ...



정리

 원리상 컴파일 옵션으로 작성이 되는 것이고 그 역할을 하는 매크로 함수를 컴파일러 수준에서 제공을 해주는 것을 이용하는 것이다. 이는 헤더파일이 없어도 참조함수를 선언해서 사용할 수 있지만, 복잡하고 많은 수의 함수를 일일히 선언해주는 것은 비 생산적인 일이 때문에 헤더파일은 그대로 사용하는데, 이 때문에 타사의 상용 라이브러리(흔히 dll)를 사용할 때 헤더파일은 대부분 같이 제공해준다.


 여기서는 간단하게 작성했지만, 가능하면 정신건강을 위해서 정리를 하는게 좋다. 들여오기 예시1, 2는 각각 장단점이 있으니 자신이 우선시 하거나 취향대로 사용하면 될 것 같다.


참조자료

MSDN VS2015(한글)

MSDN VS2015(영문)

DLL 작성 및 사용하기(한글 블로그)




반응형
반응형

윈도우즈 라이브러리 개념(LIB, DLL)


이글을 정리하게 된 경우 당연히 사용해야 할 상황이 닥쳤기(?) 때문에 적는다.


라이브러리 개념

 라이브러리는 일반적으로 운영체제에 많은 부분은 의지하는 경우가 있다. 따라서 리눅스와 윈도우의 라이브러리 확장자 이름이 다르다. 심지어 일부는 정의 하는 이름도 다르다.

 윈도우즈에서 개념적으로 정적 라이브러리와 동적 라이브러리로 분류가 되며, 각 확장자는 순서대로 lib, dll로 붙는다.


정적 라이브러리와 동적 라이브러리

 정적 라이브러리(Static Library)의 경우 종속적인(해당 라이브러리를 사용하는) 프로그램이 컴파일(빌드와 링크 포함)되는 시점에 필요로 하고 실행에는 이미 라이브러리의 내용이 포함되어 있기 때문에 정적 라이브러리 파일이 필요로 하지 않는다. 확장자는 .lib를 사용한다.

리눅스에서는 동일한 개념으로 확장자 .o(Object의 약자)를 사용한다.


 동적 라이브러리(Dybamic Library)는 종속적인 프로그램이 컴파일시 링크만 하게 된다. 이 때문에 실행 프로그램이 실행되는 시점에 동적 라이브러리 파일이 있어야 한다. 이 때 운영체제는 실행 프로그램에서 요청하는 dll 파일을 찾게 되는데, 첫째는 실행 프로그램의 같은 폴더내에서 찾게 되고, 그 다음은 하위 폴더, 마지막으로는 환경변수를 통해서 찾게 된다. 이러한 과정으로도 필요한 dll 파일을 찾지 못하게 되면, 오류 메시지를 보여주고 프로그램 실행이 중단된다.

리눅스에서는 비슷한(사실상 같은) 역할을 하는 라이브러리를 공유 라이브러리(Shared Library)라 부르고 확장자는 .so(Shared Object의 약자)를 사용한다.


정적 라이브러리 VS 동적 라이브러리

 vs는 실제 둘의 차이를 비교하기 위해서 많이 사용하는 검색 키워드이기도 하다. 두 방법이 각각 장단점이 있는데, 성능(흔시 실행 속도)는 정적 라이브러리가 앞서지만, 재사용성은 동적 라이브러리가 앞선다. 이는 동적 라이브러리를 많이 사용할 수록 여러 파일로 나누어져있기 때문에 발생하는 문제이기도 하다.


제작 및 사용 방법

 운영체제에 따라서 파일 확장자 부터 다르며, 제작 및 사용 방법도 컴파일의 종류에 영향을 받는다. 제작 및 사용 방법은 사용하는 컴파일러에 대해서 조사를 해야 한다. 다만, 대부분이 매크로와 컴파일러에서 제공하는 옵션 사용하기 때문에 원래 소스코드에 손을 덜 대거나 호환성이 높은 코드를 작성한다.



반응형
반응형

gcc C++ : 유닉스 라이브러리 만들기 입문

알게된 배경

 리눅스 환경에서 처음에는 간단한 프로그램을 제작을 했지만, 점차 커기게 되자 상당히 많은 양의 파일들이 한폴더에 어지럽게 있자 더 이상 모듈화를 하지 않으면 코드관리가 안될만한 상황이 되어 라이브러리 작성법에 대해서 공부하게 되었다.


염두해야 될 것

 윈도우 개발환경과 달리 리눅스환경의 경우 gcc의 옵션을 활용하는 방법을 알아야 했다. 그리고 편하게 하려면, makefile을 활용해야 하고, 더 편하게 makefile을 만들기 위해서는 makefile을 어느정도 알고 있는 상태에서 cmake를 사용할 줄 알아야 한다.


테스트 샘플 코드

// ~/lib_test/include/sample.h
#pragma once
#inlcude <iostream>

class Sample
{
public:
    Sample(void);
    ~Sample(void);

    void print(void);
};
// ~/lib_test/src/sample.cpp
#include "../include/sample.h"

using namespace std;

Sample::Sample(void)
{}
Sample::~Sample(void)
{}
void Sample::print(void)
{
    cout << "hello snupy?!" << endl;
}
// ~/lib_test/main.cpp
#include "include/sample.h"

int main(int argc, char* argv[])
{
    Sample sam;
    sam.print();
    return 0;
}



정적 라이브러리

 파일 확장자는 a(Archives의 앞글자)이며, object(.o) 파일과 큰 차이는 없다. 정적라이브러리를 통해서 빌드된 프로그램에 정적라이브러리가 포함되기 때문에 프로그램의 용량이 커지는 특징이 있다. 라이브러리에 수정할 내용이 있어서 수정하게 되면, 프로그램을 다시 빌드해야 하는 단점이 있다.


만드는 방법

# ~/lib_test/ 에서 실행할 경우
g++ -c ./src/sample.cpp -o ./obj/sample.o
ar rc ./lib/libsample.a ./obj/sample.o

 첫 줄은 sample.cpp를 sample.o 로 빌드가 되며, 두번째 줄은 libsample.a로 정적 라이브러리로 만들어준다. ar의 명령어는 아카이브(Archives)의 앞의 두글자를 딴 것이다. 또한 리눅스에서는 파일이름앞에 lib를 붙여야 나중에 실행 파일을 빌드할 때 lib로 인식을 하니 붙여줘야 한다.

 만약 해당 코드에 C++11이상 같은 표준라이브러리을 사용한다면, 오브젝트(.o)파일로 빌드할 때 -std=c++11 같은 옵션을 추가해주어야 한다.


링크된 실행파일 만들기

# ~/lib_test/ 에서 실행할 경우
g++ -o test main.cpp -Llib -lsample

 위의 명령어는 main.cpp라는 소스코드를 test라는 이름으로 빌드를 하게 된다. 이때 lib라는 폴더에 있는 sample이라는 이름의 라이브러리를 참조하라는 명령이 된다. 앞에서 작성한 libsample.a 파일을 sample이라는 라이브러리로 인식을 하게 된다.

 정적 라이브러리는 빌드 이후에는 실행파일에 포함이 되기 때문에 빌드이후 sample.a가 없어도 실행이 된다.


공유 라이브러리

 윈도우의 dll(동적 라이브러리)와 비슷한 개념이다. 때문에 공유 라이브러리를 동적 라이브러리라 부르는 곳도 있다. 확장자는 .so(Share Object의 각 앞글자 머리) 공유 라이브러리의 가장 큰 특징은 정적 라이브러리와 달리 특정 공유 라이브러리 파일만 교체를 해도 수정된 내용이 적용이 된다. 즉, 실행파일을 재 빌드할 필요가 없다(물론 파일이 삭제되거나 추가되면 어쩔 수 없이 재컴파일해야 한다).

 이러한 특징으로 나온 개념이 플러그인 이다(라고 많은 책이나 참고자료가 언급한다).


만드는 방법

# ~/lib_test/ 에서 실행할 경우
g++ -fPIC -c ./src/sample.cpp -o ./obj/sample.o
g++ -shared -o ./lib/libsample.so ./obj/sample.o

 첫줄은 고정된 파일로 컴파일을 하여 오브젝트 파일로 빌드를 한다. 이후 두번째 명령에서는 공유 오브젝트 파일을 생성한다. 여기서는 간단히 확장자를 so로 작성했지만, 보통 프로그램 버전을 관리 할 경우 so.1.0.0 이런식으로 숫자를 뒤에 붙여서 관리를 한다. 또한 -Wl 옵션을 사용하여 추가 옵션을 붙이는 경우가 많다(입문자에게는 이러한 부분이 오히려 진입장벽을 높이는 결과가 되는 것 같다)


링크된 실행 파일 만들기

# ~/lib_test/ 에서 실행할 경우
g++ -o test main.cpp -Llib -lsample

 앞의 링크된 실행파일을 만드는 것과 큰 차이가 없다. 하지만, 이렇게 만들어진 실행파일은 바로 실행할 수 없는데, 이유는 정적 라이브러리와 달리 공유 라이브러리는 실행파일에 포함이 안되어 있기 때문에 실행에 필요한 파일을 알려줘야 한다. 여기서 -L옵션은 빌드할때만 사용되는 라이브러리 경로이다.


 다만, 윈도우즈의 동적 라이브러리인 dll의 경우 보통 실행파일이 있는 폴더와 하위 폴더, 그리고 환경변수에 등록된 폴더에서 실행에 필요한 dll파일이 있는지 찾아서 실행한다.


 반면 리눅스 환경에서는 그냥 환경변수만 찾아 본다(어떤이는 이를 버그라 생각을 한다). 따라서 환경변수에 있는 폴더 경로에 공유 라이브러리를 복사하는 방법과 환경변수를 만들어서 등록해줘야 된다.

 때문에 실행하기 전에 ldd 명령어를 통해 의존성 검사로 파일을 실행하는데 필요한 공유 라이브러리가 연결되어 있는지 확인 할 수 있다.

# ~/lib_test/ 에서 실행할 경우
ldd ./test

 이렇게 확인한 의존성 검사에서 "not found"가 없어지도록 해야 실행이 가능해진다.

 공유 라이브러리를 이용해 배포했을 경우 해당 프로그램을 유지 및 삭제 관리를 할 때 어떻게 할 것인지도 고민을 해야 한다.


환경변수 추가 등록

export LD_LIBRARY_PATH = [라이브러리절대경로]


 환경변수를 통해서 라이브러리 위치를 찾을 수 있다면, ldd로 검사했을 때 not found가 더 이상 보이지 않을 것이다. 실행했을 프로그램의 결과가 보이면 성공적으로 라이브러리를 생성하고 실행을 해본 것이다.


 하지만, LD_LIBRARY_PATH라는 환경변수는 디버그용(배포 전단계의 실행 테스트)에서 사용할 것을 권하지 배포하는 방법으로는 적합하지 않다.


보충해야 될 내용

 여기까지 혼자서 테스트 해보는데 성공했다면, gcc에 대한 옵션에 대해서 추가로 살펴봐야 한다. 당연한 소리지만, 테스트 결과 -l 옵션을 여러번 사용해서 여러개의 라이브러리를 참조하는 것이 가능하다. 또한 옵션을 사용하는 경우가 많으니 gcc 옵션에 대해서도 어느 정도 숙지를 하고 있어야 한다.


참고자료

우분투 환경에서 C언어로 배우는 리눅스 프로그래밍(서적)

옵션에 대한 정리(영문)

옵션에 대해 한글로 자주 사용하는 것만 정리(간혹 오역도 보인다)



반응형

+ Recent posts