본문 바로가기
컴퓨터/C++_STL

동적 라이브러리 만들고 사용하기

by adnoctum 2010. 5. 25.

환경: GCC 4.1.2 20080704 (Red Hat 4.1.2-46) on CentOS 5.4

   프로그램이 실행 중에, 필요한 기능이 구현되어 있는 파일을 찾아서 그 기능을 사용할 수 있도록 하는, 일반적으로 말하는 plug-in 형식을 구현하기 위해서 동적 라이브러리(주로 .so 나 .dll 파일)를 이용한다.  Windows의 DLL과 같은 개념인데, C++의 경우 MFC라면 확장 DLL을 이용하여 class 를 다룰 수 있지만 linux의 경우 일반적으로는 class 를 다루지 못하는 것으로 보인다. 대신, 돌아 가서, 라이브러리에서 class로 구현된 객체를 생성/파괴하는 함수를 만들고 C 로 extern 한 이후 사용하면 된다.

(이 글을 읽기 위해서는 C++의 상속/가상함수, 함수 포인터 정도의 개념이 배경지식으로 필요함)

   만약 C++ 을 사용하지 않는다면, host 프로그램[각주:1]에서 함수 포인터를 이용하여 기능들을 호출할텐데, C++ 을 사용할 경우에는 class의 member function 을 virtual 로 해 놓은 후, 클래스를 상속받아서 overriding 시킨다. 그 후, host 프로그램에서는 부모 class의 pointer로 자식 클래스를 가리킨 후, virtual 로 된 함수를 호출하면 되겠다. 그림으로 보면 다음과 같다.

동적 라이브러리 사용 프로그램의 모식도

동적 library 를 사용하는 프로그램의 모식도.


host 프로그램은 compile 시 순수추상함수 (pure virtual function)를 호출한다. 동적으로 올라갈 library는 순수추상함수를 갖고 있는 클래스를 상속받은 후, host 프로그램에서 호출된 순수추상함수를 overriding 함으로써 기능을 제공한다. 그렇게 되면 host 프로그램에 의해 호출되는 '실제' 함수는 라이브러리에 있는 함수가 될 것이다.


이제 실제 구현을 살펴 보자. 이 글은 간단한 toy-example 로 살펴 본다. 보다 현실적 예는, 공개할 수 있을 때 별도의 글로 공개한다.

우선 동적 라이브러리를 만들어 보자. 실제 라이브러리 파일인 .so 파일을 만들기 전에, 위의 구조상 필요한 가장 상위의 부모 클래스, 즉 plug-in 으로 제공될 기능을 담당하는 함수를 pure virtual function 으로 갖고 있는 클래스를 정의한다.


가장 최상위 부모 클래스의 이름은 ABC 이고, show_message 라는 함수를 member function 으로 갖는다. 이 함수는 이 클래스를 상속받은 클래스에서 구현해야만 하는 순수가상함수이다.

다음으로 ABC를 상속받고, 실제로 show_message 함수에 기능을 구현할 클래스 ABCC 를 다음과 같이 정의/구현한다.




중요한 것은 abcc.cpp 파일에서, C++의 클래스를 동적 라이브러리를 호출할 때[각주:2] 밖에서 보일 수 있도록 하기 위해 다음과 같은 함수를 만들어 주어야 한다는 것.


extern "C"

{

        ABC* create()

        {

                return new ABCC();

        }

 

        void destroy(ABC* p)

        {

                delete p;

        }

}


즉, class ABC를 가리키는 포인터를 반환하되, 실제로 반환하는 것은 ABC를 상속받은 ABCC 를 할당해서 그 포인터를 넘기는 것이다. 이것은 C++에서 그 유명한, 부모 클래스의 포인터는 자식 클래스를 가리킬 수 있다, 가 실제로 사용된 예. ㅋ 여하튼 그처럼 ABCC 를 생성하는 함수와 파괴하는 함수를 C 형식으로 부를 수 있도록 함수 2개를 제공하는데, 후에 host 프로그램에서 이 함수 이름으로 각 개체를 접근할 것이므로 이 함수 이름은 ABC 를 상속받은 모든 클래스에서 동일하게 제공되어야 한다.

ABCC와 비슷하게 ABC에서 상속받은 ABCD 라는 클래스를 다음과 같이 정의/구현한다.



지금까지의 작업을 잠깐 정리하면, ABC 라는 클래스가 있고, ABC::show_message 라는 함수가 있다. 이 때 ABC::show_message 라는 함수가 실제로 plug-in 으로 구현될 기능을 담당하는 함수이다. 이 함수는 ABC에서는 순수추상함수로 구현되어 있다. host 프로그램에서는 ABC의 포인터로 show_message 를 호출할 것이다. 이 때, ABC를 상속받고 show_message를 적당한 것으로 구현한 클래스의 포인터를 host 프로그램에서 받는다면, host 프로그램에서 실제로 호출되는 show_message 라는 함수가 제대로 정의된 함수인 것이다.

이제 ABCC와 ABCD 를 제공하기 위한 동적 라이브러리를 만들어 보자. gcc 에 의한 compile 은 다음과 같이 한다.

class ABCC 를 접근하기 위한 라이브러리는 다음과 같이 컴파일.
gcc -fPIC -c abcc.cpp
gcc -shared -Wl,-soname,libabcc.so.1.0 -o libabcc.so.1.0 abcc.o

class ABCD 를 접근하기 위한 라이브러리는 다음과 같이 컴파일.
gcc -fPIC -c abcd.cpp
gcc -shared -Wl,-soname,libabcd.so.1.0 -o libabcd.so.1.0 abcd.o

-fPIC, -shared, -Wl(소문자 L), -soname, -o 에 대한 설명은 gcc manual 을 참고한다. 위에서는 libabcc.so.1.0 과 libabcd.so.1.0 으로 동적 라이브러리 파일을 만들고 있다.

    이제 이렇게 만든 동적 라이브러리 파일을 실제로 사용하는 host 프로그램을 살펴 보자. plug-in 의 가장 큰 장점이라면 host program 을 재컴파일하지 않고 라이브러리 파일만 특정 위치에 추가시키는 것으로 host program의 기능을 확장할 수 있다는 것이다. 지금 만드는 host 프로그램은 비록 라이브러리 파일 경로를 hard-coding 해 놓았으나, 실제로는 지정된 경로에 있는 라이브러리 파일을 모두 불러가도록 해 놓고 사용할 수 있을 것이다.

    1 // main.cpp

    2 // 동적 라이브러리를 사용하는 host 프로그램의 예.

    3 #include "abc.h"

    4 #include <iostream>

    5 #include <dlfcn.h>

    6 

    7 int main(int argc, const char* argv[])

    8 {

    9         const char* lib_path[] = { "/home/adnoctum/test/libabcd.so.1.0",

   10                                    "/home/adnoctum/test/libabcc.so.1.0"};

   11         int i = 0;

   12         for(i = 0; i<2; i++){

   13                 void* lib_handle = dlopen(lib_path[i], RTLD_NOW);

   14                 if(lib_handle == NULL){

   15                         std::cout << "load error." << std::endl;

   16                         std::cout << dlerror() << std::endl;

   17                         return -1;

   18                 }

   19 

   20                 ABC* (*creator)() = (ABC*(*)())dlsym(lib_handle, "create");

   21                 ABC* p = (*creator)();

   22                 p->show_message();

   23                 void (*destructor)(ABC*) = (void (*)(ABC*))dlsym(lib_handle, "destroy");

   24                 (*destructor)(p);

   25                 dlclose(lib_handle);

   26         }

   27

   28         return 0;

   29 }



    컴파일은 다음과 같이 한다.

 g++ main.cpp -o test -ldl

-ldl(엘디엘) 에 대한 설명 역시 gcc manual 을 참고한다.

   차근차근 살펴 보자. 우선 가장 최상위 클래스만 있으면 되므로[각주:3] abc.h 만 include 하면 된다 (줄번호 3). 동적 라이브러리를 열고, 함수를 찾기 위해 필요한 함수는 dlfcn.h 파일에 정의되어 있으므로 이 파일을 include 한다 (줄번호 5). 현재는 라이브러리 파일 경로를 직접 적어 준다 (줄번호 9~10). 이 때, 전체 경로를 지정해 주어야 하며, 그렇지 않을 경우 library 를 찾는 옵션에 따라 그 경로들만 찾게 된다. systemic한 위치에 위의 테스트 라이브러리를 놓고 싶지 않았기 때문에 위처럼 전체 경로를 입력해 주는 방식으로 test host program 을 작성했다.

   동적 라이브러리 파일을 열기 위해 dlopen 함수를 이용한다 (줄번호 13). RTLD_NOW나 그 이외의 옵션은 참고 Sites를 참고한다. 열려진 라이브러리 파일에서 'create'라는 함수를 찾는 부분이 줄번호 20에 있다. abcc.cpp 나 abcd.cpp 파일에 C 스타일로 extern 했던 create, destroy 함수가 이 때 찾아지는 것이다. dlsym 함수는 반환값의 type이 void* 인데, 실제로는 함수 포인터를 반환하는 것이므로, create와 prototype이 같은 형태의 함수 포인터 형태로 casting 해준다. 줄번호 20 에 의하여, 동적 라이브러리에 구현되어 있는 create 라는 함수를 creator 라는 변수로 접근할 수 있게 되었다 (요 부분이 조금 혼잡하니 뒤에서 다시 설명).


   이제 동적 라이브러리에서 받아 온 create 함수를 호출해 보자. 이 때, create 함수는 ABCC 또는 ABCD의 포인터를 반환하게끔 되어 있었으므로 우리는 이 값을 ABC의 포인터에 저장하자, 왜냐 하면, 부모 클래스의 포인터는 자식 클래스를 가리킬 수 있으므로. 이 부분이 줄번호 21 번이다. create를 creator 로 접근하고 있으므로 줄번호 21번의 = 오른쪽과 같이 호출하고, 반환값은 ABC의 포인터에 저장한다.

   이제 실제로 필요했던 기능이 구현된 show_message 함수를 호출해 보자. 그 부분이 바로 줄번호 22. 물론 host 프로그램에서는 ABC::show_message() 로 호출되는 것처럼 되어 있으나, ㅋ, p 가 가리키는 것은 ABCC 혹은 ABCD 이고, 따라서 줄번호 22 에서 호출되는 show_message() 는 ABCC::show_message(), ABCD::show_message() 인 것이다. 이것이 바로 가상함수의 힘!

   그 후, 동적으로 할당된 객체를 메모리에서 해제하기 위하여 동적 라이브러리 파일에 구현해 놓은 destroy 함수를 찾아서 (줄번호 23), 객체를 해제한다 (줄번호 24). 바로 이러한 경우 때문에 class의 파괴자는 virtual 로 해야 한다. 즉, destroy 함수는 parameter로 ABC의 포인터를 받아서 delete 를 하고 있지만, 이 때 실제로 호출되는 것은 ABCD와 ABCC도 포함이 되는데, 만약 ABC::~ABC() 가 virtual 이 아니었다면 이와 같은 일이 불가능한 것이다.

   메모리가 깔끔하게 해제되었는지 확인해 보기 위하여 destroy 함수를 호출했을 때와 하지 않았을 때의 차이를 valgrind 로 확인해 보자. 우선 24 번째 줄을 주석처리한 후 valgrind를 호출해 보면, 다음과 같은 부분이 있다.

==7995== 8 bytes in 2 blocks are definitely lost in loss record 1 of 1
==7995==    at 0x4005B65: operator new(unsigned) (vg_replace_malloc.c:163)
==7995==    by 0x4009BC8: ???
==7995==    by 0x8048884: main (in /home/adnoctum/test/test)

위에서 보는 바와 같이 leak 이 생겼다. 이젠 24번째 줄을 주석처리 하지 않고 해보면,

==8009== malloc/free: in use at exit: 0 bytes in 0 blocks.
==8009== malloc/free: 12 allocs, 12 frees, 1,642 bytes allocated.
==8009==
==8009== All heap blocks were freed -- no leaks are possible.

언제 봐도 기분 좋은 All heap blocks were freed -- no leaks are possible 가 있다. ㅋ.

   위에서같이 만들어진 test 를 실행시켜 보면

[adnoctum@bioism test]$ ./test
ABCD
ABCC
[adnoctum@bioism test]$

과 같이 결과가 나오는 것을 알 수 있다. 즉, 실제로 실행된 코드는 ABCC::show_message() 와 ABCD::show_message() 인 것이다.

 

  조금 복잡했던, 함수 포인터로 casting 하는 부분을 살펴 보자.

   20                 ABC* (*creator)() = (ABC*(*)())dlsym(lib_handle, "create");

   21                 ABC* p = (*creator)();

   22                 p->show_message();

   23                 void (*destructor)(ABC*) = (void (*)(ABC*))dlsym(lib_handle, "destroy");

   24                 (*destructor)(p);


abcc.cpp (abcd.cpp) 에 있는 create/destroy 함수는 다음과 같다.

ABC* create()

{

        return new ABCC();

}


void destroy(ABC* p)

{

        delete p;

}


함수 포인터형 변수는
 
return_type (*variable_name)(parameter_specification)

으로 선언한다. 즉, int 를 반환하고, parameter로 double, double 을 받는 함수

int divide(double a, double b);

와 같은 함수를 가리키기 위한 함수 포인터형 변수 fpr 은

int (*fpr)(double,double)

로 선언하는 것이다. 변수가 포인터라는 것은 (*fpr) 에 표시되어 있다. 그런데 변수 이름이 아닌, 함수 타입을 나타내기 위해서는 위에서와 같이 함수 포인터형 변수를 선언하는 것과 똑같고 변수 이름만 없애면 되겠다. 실제로 변수란

type variable_name

으로 되어 있는 것이므로 variable_name 을 제거하면 정확히 type 이 된다. 따라서

int (*fpr)(double,double)

에서 frp 을 제거하면 위의 divide 함수에 대한 type과 정확히 같아지는 것이다.

int (*)(double,double)

위의 library 사용의 경우, create는 반환 type 이 ABC* 이고, parameter 가 없으므로, 함수 create 의 type은

ABC* (*)()

이 되는 것이다. 또한 destroy 는 반환 type 이 void 이고, parameter 가 ABC* 형 이므로, 함수 destroy의 type은

void (*)(ABC*)

가 되는 것이다.


참고 Sites:
1. 동적 라이브러리 on GGAM+Log
2. 공유 라이브러리 on KLDP HOWTO
3. Static, Shared Dynamic and Loadable Linux Libraries on YOLINUX
4. Dynamic Class Loading for C++ on Linux
5. Options for Linking on GCC Manual
6. Dynamic Loading on The Linux GCC HOWTO
7. Shared libraries and static libraries

  1. plug-in 으로 구현될 모듈들을 사용하는 프로그램을 host 프로그램이라고 하자. [본문으로]
  2. 이 방법 자체가 C++ 이 고려가 되지 않은, C 형식이기 때문에 약간의 추가 작업이 필요하다. [본문으로]
  3. 부모 클래스의 포인터로 자식 클래스를 가리킬 것이므로 우리는 자식 클래스의 자세한 내용을 몰라도 된다. [본문으로]

'컴퓨터 > C++_STL' 카테고리의 다른 글

const pointer  (0) 2011.11.23
isnan과 isinf  (0) 2010.12.06
Strict weak ordering  (4) 2010.05.11
에지(edge) 객체 구현해 보기  (0) 2010.05.08
파일 목록 가져 오기 일반화 시키기  (0) 2010.01.09