출처 http://cheucheu.tistory.com/47

 

         1. 컴파일러(Compiler)와 인터프리터(Interpreter)

컴파일을 하기 위하여 입력되는 프로그램을 "원시 프로그램" 이라 하고, 

이 프로그램을 기술한 언어를 "원시 언어" 라 합니다.

번역되어 출력되는 프로그램을 "목적 프로그램" 이라 하고, 

이 프로그램을 기술한 언어를 "목적 언어" 라 합니다.



       컴파일러(Compiler)


컴파일러는 원시 언어로 된 프로그램을 읽어들여서 목적 언어로 된 동일한 프로그램을

출력하여 주는 언어처리기입니다. 기계어로 번역이 쉽게 이루어질 수 있으면서 

시행 시간을 중시하는 경우에 사용합니다.

 

       인터프리터(Interpreter)


인터프리터는 원시 언어로 작성된 명령문들을 한 번에 한 줄씩 번역하고 실행하는 프로그램입니다.

프로그램이 짧고 복잡하지 않고, 대화형 프로그램에서 많이 사용됩니다.

 


컴파일러는 번역, 인터프리터는 통역이라고 이해하면 쉽습니다.

 

 

 

 

 

         3. 차이점

 



컴파일러와 인터프리터의 차이점을 위 표에 간단하게 정리해보았습니다.

 

       컴파일러(Compiler)

 컴파일러는 컴파일 과정이 복잡하고 그 시간이 많이 걸리게 되는데, 한 번 컴파일 하면 그대로 사용이 가능합니다.

전체 실행 시간 면에서 매우 효율적이며, 매번 번역할 필요가 없다는 것이 장점입니다.

하지만 한 줄의 입력 시 때로는 프로그램이 몇 백 개의 기계어로 번역되기 때문에 큰 기억용량을 요구하게 됩니다.

특히 입출력 명령은 입출력 형식을 위한 코드 외에 기계 상태 파악코드로 인해 큰 기억 장치가 요구됩니다.

 

       인터프리터(Interpreter)


인터프리터는 프로그램이 될 때까지 원시 언어의 형태를 유지하기 때문에 기억 장소가 추가로 필요하지 않습니다.

하지만 원시프로그램을 직접 실행하기 때문에 이 실행에 필요한 소프트웨어가 항상 기계 안에 상주하면서

원시 프로그램의 명령들을 받아서 처리하게 됩니다. 반복하여 실행하는 경우에는 그 때마다 원시 프로그램을

해석하여 처리하기 때문에 많은 시간이 걸리기도 합니다.

 

 

 

 

 

         4. 차이점

컴파일러는 실행 시간의 효율을 중하는 프로그래밍 언어에서, 

인터프리터는 사용자의 융통성을 중시하는 프로그래밍 언어에서 주로 사용됩니다.

 

 


 

'게임개발공부 > 무작정퍼오기' 카테고리의 다른 글

explicit 키워드에 대하여  (0) 2014.01.09
함수객체  (0) 2013.12.29
DLL만 로딩해도 컴퓨터가 뻗는경우.  (0) 2013.12.21
volatile (퍼온글)  (0) 2013.12.21
extern "C" 이건 뭔가? (퍼온글)  (0) 2013.12.21
Posted by JJOREG

이번에는 explicit 키워드에 대해 얘기해 보려고 합니다.

최근에 C++ 를 배우신 분들은 모르시겠지만 explicit 키워드는 C++의 첫번째 표준안인 98년 9월 1일 버전에 처음으로 도입된 키워드입니다. Bjarne Stroustrup 이 처음에 C++ 를 창시할 때는 없었다는 거죠. 그럼 왜 98년 C++ 표준안을 출판할 때는 explicit 라는 키워드를 넣었을까요 ? 이런 의문을 제기하지 않고 왜 그런지를 모른다면 C++의 참맛을 안다고 할 수 없겠죠.

결 론부터 말씀드린다면 explicit 키워드는 C 나 C++ 의 공통 특징 중의 하나인 암시적 형변환의 특성때문에 프로그래머가 예상치 못하는 형변환이 일어남으로 인해 정말 발견하기 어려운 버그가 생기는 걸 방지하기 위해서 도입된 키워드입니다.

결론은 그렇다 치는데, 도대체 어떤 버그이길래 그렇게 발견하기 어려운 버그일까요 ? 역시 백마디 말보다 하나의 예가 이해를 돕는데는 제격이므로 예를 들어 한 단계씩 차분히 설명해 보도록 하겠습니다.

일 단, 여러분이 Stack 이라는 클래스를 설계하려고 한다고 해 보죠. 제일 먼저 있어야 할 것은 Stack 객체를 생성하는 생성자가 있어야 할 것입니다. 우선 생각할 수 있는 생성자로는 n 개의 요소까지 가질 수 있는 Stack 을 생성하는 걸 생각해 볼 수 있을 것입니다. 다른 생성자로는 다른 Stack 을 복사해서 생성하는 것, 다른 배열을 복사해서 생성하는 것, 다른 vector 를 복사해서 생성하는 것 등을 생각할 수 있을 것입니다. 이런 생성자를 갖는 Stack 은 다음과 같이 작성할 수 있겠네요.

// 저번에 썼던 코드입니다.
template <typename T>
struct Print {
  void operator() (T v) {
    cout << v << " ";
  }
};
 
class Stack {
private:
  vector<int> _data;
  int _pos;
 
public:
  const static int MIN_ELEM = 10;
 
  // 최대 n 개 요소를 가질 수 있는 Stack 생성자
  // 기본값을 지정해 봤습니다.
  Stack(int n = MIN_ELEM): _data(n), _pos(0) {
    cout << "Stack(int) called" << endl;
  }
 
  // 다른 Stack 을 복사해서 생성하는 복사 생성자
  Stack(const Stack& other): _data(other._data), _pos(other._pos) {
    cout << "Stack(const Stack&) called" << endl;
  }
 
  // 다른 배열을 복사해서 생성하는 생성자
  Stack(int arr[], int n): _data(arr, arr+n), _pos(n) {
    cout << "Stack(int[], int) called" << endl;
  }
 
  // 다른 vector 를 복사해서 생성하는 생성자
  Stack(vector<int> v): _data(v), _pos(v.size()) {
    cout << "Stack(vector<int>) called" << endl;
  }
 
  // 그냥 어떻게 작동하는지 확인하기 위한 디버깅용 멤버 함수
  void printAll() {
    for_each (_data.begin(), _data.end(), Print<int>());
    cout << endl;
  }
};

다음 코드를 수행하면 어떤 결과가 벌어질까요 ? 여러분의 예상과 맞아 떨어지는지 한 번 보시기 바랍니다.

01: int
02: main(void)
03: {
04:   Stack s1;
05:   s1.printAll();
06:   Stack s2(20);
07:   s2.printAll();
08:   Stack s3(s2);
09:
10:   vector<int> v(10, 3);
11:   for_each(v.begin(), v.end(), Print<int>());
12:   cout << endl;
13:
14:   Stack s4(v);
15:   Stack s5 = v;
16:   s1 = v;                 // 이게 어떤 효과를 일으킬까요 ?
17:   s2 = 10;                // 버그 아닌가요 ? 좀 이상하지 않나요 ?
18:
19:   s1.printAll();          // 뭐가 찍힐까요 ?
20:   s2.printAll();          // 뭐가 찍힐까요 ?
21: }

위 코드가 컴파일될까요 ? 특히 17 line은 컴파일이 안될 것 같지 않으세요 ? stack 에 정수값을 할당한다는 게 전혀 말이 안되니 당연히 컴파일 안되어야 정상이겠죠. 자 한 번 컴파일해 볼까요 ? 어! 이게 어떻게 된 거죠 ? 아무런 에러 없이 컴파일이 되네요. 17 line 도 에러 없이 컴파일이 됩니다. 이게 어떻게 된 조화일까요 ? 컴파일러가 왜 이걸 정상적인 코드로 인식하고 컴파일을 했는지를 알려면 우리의 입장이 아니라 컴파일러의 입장에서 생각해 봐야 할 겁니다. 이른바 역지사지 해 보는 것이죠.

17 line 을 만나면 컴파일러는 어떻게 할까요 ? 컴파일러가 하는 일은 자신이 알고 있는 모든 지식을 동원해서 코드를 컴파일 해내는 것입니다. 먼저 s2 에 정수값을 할당할 수 있는 할당 연산자가 있는지 볼 것입니다. 어! 그런데 Stack 클래스 정의를 봤더니 아무리 눈씻고 찾아 봐도 없네요. 프로그래머가 정의한 할당 연산자가 없다면, 컴파일러가 생성한 복사 할당 연산자가 있을 것입니다. 자기가 생성한 복사 할당 연산자를 쓰려고 했더니, 이번에는 10 이라는 정수값이 복사 할당 연산자의 인자와 호환이 되질 않네요. 그렇다고 우리의 C++ 컴파일러는 그냥 컴파일을 포기하고 항복할 녀석이 아니죠. 이번에는 혹시 정수를 복사 할당 연산자의 인자(const Stack& 일 겁니다)로 변환할 수 있는지를 볼 겁니다. 여기에서 C++ 컴파일러는 인자가 하나인 생성자를 암시적 형변환하는데 사용한다는 것을 기억해 보세요. 죽 뒤져 보았더니 Stack(int) 가 눈에 띄네요. 그래서 결국 Stack(10) 을 호출해서 10개의 요소를 갖는 Stack 을 생성하고 그걸 복사 할당 연산자를 이용해서 s2 에 할당하게 됩니다. 이제 그 짧은 코드 안에서 어떤 일이 일어나는지 이해가 되시나요 ?

제가 쓰고 있는 컴파일러(Microsoft Visual C++ 2005)에서 컴파일 후 실행해 봤더니 다음과 같은 결과가 나옵니다.

01: Stack(int) called                         // 04 line 출력
02: 0 0 0 0 0 0 0 0 0 0                       // 05 line 출력
03: Stack(int) called                         // 06 line 출력
04: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   // 07 line 출력
05: Stack(const Stack&) called                // 08 line 출력
06: 3 3 3 3 3 3 3 3 3 3                       // 10 line 출력
07: Stack(vector<int>) called                 // 14 line 출력
08: Stack(vector<int>) called                 // 15 line 출력
09: Stack(vector<int>) called                 // 16 line 출력
10: Stack(int) called                         // 17 line 출력
11: 3 3 3 3 3 3 3 3 3 3                       // 19 line 출력
12: 0 0 0 0 0 0 0 0 0 0                       // 20 line 출력

09 ~ 12번째 라인 결과가 좀 의아하지 않나요 ? 왜 이런 결과가 나올까요 ? 눈치 빠른 분들 벌써 회심의 미소를 짓고 계시는 군요. 예, 그렇습니다. 아까 컴파일러 입장에서 살펴 본대로 C++ 의 암시적 형변환 규칙에 따르면 클래스의 생성자 중에 인자가 하나인 생성자들은 프로그래머의 의도와는 전혀 상관없이 컴파일러에 의해 암시적 형변환에 동원되도록 되어 있어서 s1 = v, s2 = 10 이라는 코드를 만나면 각각 Stack(vector<int>), Stack(int) 를 호출하여 임시 객체를 생성하고, 그 임시 객체를 가지고 복사 할당 연산자를 호출하게 됩니다(위 코드 예에서는 컴파일러에 의해 자동 생성된 할당 연산자가 호출되겠죠. 클래스 정의에서 할당 연산자를 여러분 나름대로 정의한 후, cout 으로 뭔가를 출력하게 되면 할당 연산자가 호출되는 것을 확인하실 수 있을 겁니다). 그러고 나면 s1 은 v vector의 내용으로 Stack 을 채우게 될 것이고, s2는 10개의 int 요소로 채우게 될 것입니다. 그래서 결과적으로 09 ~ 12번째같은 결과가 나오는 것이지요. s1 = v 에 대해 Stack(vector<int>) 를 호출하는 건 그래도 어느 정도 말이 된다고 하지만 s2 = 10 에 대해 Stack(int) 를 호출하는 건 좀 오버아닌가요 ? 도대체 Stack 객체에 10을 할당한다는 게 말이 되느냔 말입니다.

C++ 컴파일러는 참 잘난체 하는 데는 이골이 났나 봅니다. 프로그래머가 특별히 정의하지 않더라도 기본 복사 생성자, 복사 할당자를 생성해 버리고, 그것도 모자라 인자가 하나인 생성자를 암시적 형변환하는데 지 맘대로 써 버린다니… 그런데 나름 C++ 컴파일러도 말 못할 고민이 있다고 하네요. C++ 본래 태생이 그래서 그렇답니다. 맘에 안든다고 버릴 수도 없고, 그냥 우리가 적응해서 살아야 되지 않겠습니까 ?

아무리 적응하려고 한다지만 s2 = 10 을 만났을 때 Stack(int) 를 호출하는 건 프로그래머 입장에서는 참 예상하기 힘든 일입니다. 그래서 이런 예상치 못한 수행이 일어나면 프로그래머는 그런 버그를 정말 찾기 힘들게 됩니다. 실제로 이런 일들이 프로젝트 수행 도중 종종 일어나고, 이 버그는 찾기도 정말 힘듭니다. 그러니 아예 이런 일은 피해가는 게 상책이지요. 피해가는 방법이야 s2 = 10 과 같은 코드를 작성하지 않도록 Coding Guideline 에 넣어 두고, Code Review 도 하고 그런 방법도 있겠지만 좀 더 좋은 방법은 아예 s2 = 10 과 같은 코드를 만나면 컴파일시에 에러가 발생해 버린다면 더 좋을 것입니다(항상 기억하세요! 에러는 빨리 발견할 수록 수정하는데 드는 비용이 작다).

그래서, explicit 키워드가 C++의 첫번째 표준안에 새로 도입됐던 것입니다. explicit 키워드는 인자가 하나인 생성자가 암시적 형변환에 쓰이지 않도록 해줍니다. 암시적 형변환에 쓰이면 컴파일 에러가 발생하게 됩니다. 위에 정의한 Stack 정의에서 다음 부분을 수정하신 후에 main()함수를 다시 컴파일해 보시기 바랍니다.

  // 최대 n 개 요소를 가질 수 있는 Stack 생성자
  // 기본값을 지정해 봤습니다.
  explicit Stack(int n = MIN_ELEM): _data(n), _pos(0) {
    cout << "Stack(int) called" << endl;
  }

제가 쓰는 컴파일러에서는 다음과 같은 에러가 발생하네요.

error C2679: binary '=' : no operator found which takes a right-hand operand of type 'int' (or there is no acceptable conversion)

정확히 의도한 바와 같이 컴파일 에러가 발생하네요.

이 상에서 밝힌 바와 같이 explicit 키워드는 C++ 의 특징 중에 하나인 암시적 형변환 때문에 발생할 수 있는 문제의 소지를 미리 예방하는데 유용한 키워드입니다. 그러니, 클래스의 생성자를 설계할 때, 인자가 하나만 있는 생성자에 대해서는 암시적 형변환이 발생할 때 어떤일이 벌어질지를 생각해 보고, 암시적 형변환이 말이 안되는 의미를 갖게 될 경우 explicit 라는 키워드를 꼭 붙이시기 바랍니다. 아니면 거꾸로 생성자에 대해 일단은 무조건 explicit 키워드를 붙였다가 컴파일할 때 암시적 형변환이 허용되지 않아서 너무 불편하다 싶으면 그때가서 빼는 것도 방법입니다. explicit 키워드를 쓴 경우에는 다음과 같이 코드를 작성하시면 암시적 형변환과 같은 의미를 갖는 코드를 작성하실 수 있습니다.

s1 = Stack(v);
s2 = Stack(10);

마지막으로 다음을 기억하신다면 C++의 암시적 형변환 때문에 버그 찾느라 헤메는 일은 없을 것입니다.

'게임개발공부 > 무작정퍼오기' 카테고리의 다른 글

컴파일러 인터프린터  (0) 2014.01.13
함수객체  (0) 2013.12.29
DLL만 로딩해도 컴퓨터가 뻗는경우.  (0) 2013.12.21
volatile (퍼온글)  (0) 2013.12.21
extern "C" 이건 뭔가? (퍼온글)  (0) 2013.12.21
Posted by JJOREG
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


함수객체란
  함수 처럼 동작 하는 객체
 *사용법
   ㄱ.'()' 연산자 사용.
   ㄴ. '()'연산자는 함수 호출연산자.
   함수 호출 연산자 ()를 재정의 한다. 

두 수의 곱을 출력하는 함수 객체

  1. class CFuncObject
  2. {
  3. public :
  4.     CFuncObject( int n ) : m_n(n) {}
  5.     virtual~ CFuncObject() {}
  6.  
  7.     void operator () (int i)
  8.     {
  9.         std::cout<< m_n << "*" << i << "=" << m_n*<<std::endl;
  10.     }
  11.  
  12. private :
  13.     int m_n;
  14. };


위의 함수 객체를 둘로 나누어 출력을 담당하는 함수 객체, 함수객체와 인자 하나를 고정 시켜 주는 개체를 나누어 코딩을 해보자.

  1. using namespace std;
  2.  
  3. struct PrintMultiplication
  4. {  
  5.   void operator() (int m, int n)
  6.   {
  7.     cout << m << " * " << n << " = " << m * n << endl;
  8.   }
  9. };
  10.  
  11. class Binder
  12. {
  13. public:
  14. // 호출할 함수 객체와 고정할 인자를 기억해 놓습니다  
  15.   Binder(const PrintMultiplication& op, int arg): _op(op), _arg(arg) {}
  16.  // 첫번째 인자를 고정된 값으로 호출합니다
  17.   void operator() (int i)
  18.   {
  19.     _op(_arg, i);
  20.   }
  21. private:
  22.   PrintMultiplication _op;    //호출할 함수 객체
  23.   int _arg;                  // 고정할 인자의 값
  24. };
  25.  
  26. int main(void)
  27. {
  28.   vector<int> v;
  29.  
  30.   for (int i = 1; i < 10; i++)
  31.   {
  32.     v.push_back(i);
  33.   }
  34.  
  35.   for (vector<int>::iterator ii = ++v.begin();
  36.       ii != v.end(); ++ii)
  37.   {
  38.     cout << "<< " << *ii << "단 >>" << endl;
  39.  
    //PrintTimes(*ii)가 호출 되면서 _n이 차례로 2~9로 초기화 된다.
    // 그리고 for_each() 알고 리즘을 사용한다. 
  40.     for_each(v.begin(), v.end(), Binder(PrintMultiplication()*ii));
  41.   }
  42.  
  43.   system("pause");
  44.  
  45.   return 0;
  46. }


언뜻 보았을 때는 기능을 왜 굳이 둘로 나누었는지 모르겠지만,  Binder를 템플릿화 시켜서 사용하면, 재사용성이라는 장점이 부각되므로 꽤나 쓸만해 진다.

  1. template <class OP, typename ARG>
  2. class Binder {
  3. public:
  4.   Binder(const OP& op, ARG arg): _op(op), _arg(arg) {}
  5.  
  6.   void operator() (int i) {
  7.     _op(_arg, i);
  8.   }
  9.  
  10. private:
  11.   OP _op;                
  12.   ARG _arg;                  
  13. }

 c++표준 라이브러리에서는 기본적인 연산에 대해서 이미 함수 객체로 정의해 놓고 있다.

표현식

효과

negate<type>()

- 인자

plus<type>()

인자1 + 인자2

minus<type>()

인자1 – 인자2

multiplies<type>()

인자1 * 인자2

divides<type>()

인자1 / 인자2

modules<type>()

인자1 % 인자2

equal_to<type>()

인자1 == 인자2

not_equal_to<type>()

인자1 != 인자2

less<type>()

인자1 < 인자2

greater<type>()

인자1 > 인자2

less_equal<type>()

인자1 <= 인자2

greater_equal<type>()

인자1 >= 인자2

logical_not<type>()

! 인자

logical_and<type>()

인자1 && 인자2

logical_or<type>()

인자1 || 인자

bind1st(op,value)

op(value, 인자)

bind2nd(op,value)

op(인자, value)

not1(op)

!op(인자)

not2(op)

!op(인자1,인자2)


함수 객체의 장점

1. 함수에 특정 상태, 속성을 넣을 수 있게 되었음.
2. 1번의 경우 덕분에 보다 OOP적인 코딩이 가능해짐.
3. 함수를 동적 메모리 할당해서 쓰고 버릴 수 있음. (객체이기 때문에 가능한 것!)
4. 템플릿과 함수객체를 혼합해서 사용하며느 보다 다양한 함수 객체가 가능해짐.
5. 일반 함수보다 빠르다고 함. (테스트 안해봄...ㅡㅡ;;)

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


1. 함수 객체

1) STL 알고리즘의 모호성

 STL 알고리즘들은 전역함수가 처리하며 문제를 풀기위한 반복자 구간, 검색 대상 ,채울 값 따위의

정보들이 함수의 인자로 전달된다. 알고리즘 함수들은 입력된 정보를 바탕으로 알아서 동작하지만 어떤 함수들은

내부에서 모든 동작을 다 처리하지 않거나 할 수 없는 경우가 있다.  즉 검색하는 값이 정확하게 어떠한 조건인지, 정렬을 위해 요소를 비교할때 어떤 방식으로 비교 할 것인지를 함수가 마음대로 결정할 수 없다.

 

2) 모호성의 해결 : 함수 객체

이때 함수에게 좀더 구체적인 처리 방식을 지정하기 위해 사용자가 마리 만들어 놓은 함수 객체를 전달한다. 알고리즘 함수는 동작중에 사용자의 개입이 필요한 부분에 대해서 함수 객체를 호출하여 의사를 결정한다.

 

3) 동작방식

벡터의 순회의 경우 결과를 확인하기 위해 직접 순회하면서 벡터의 요소를 일일이 출력한다. 이때 이런 순회는 반복적으로 루프를 구성해야 하므로 번거롭다. 이때 순회를 담당하는 함수 객체를 사용하여 이러한 처리를 대신 시킬수 가 있다.  

  UniOp for_each(init first, Init last , UniOp op)

예) 함수 포인터를 이용한 함수 객체 흉내

 

  : 함수 포인터

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

void print
{
 printf("%d\n",a);
};

void main()
{
 int ari[]= { 2,3,5,1,9};
 vector<int> vi(&ari[0],&ari[5]);


 sort(vi.begin(), vi.end());

 for_each(vi.begin(),vi.end(),print());     // 함수 포인터를 객체로 
}

 

예) 함수 객체의 사용

STL 의 함수 객체는 함수 포인터에만 국한 되는 것이 아니라함수를 흉내낼 수 있는 모든 객체일 수 있다는 점이 다르다.함수 객체는 함수 호출 연산자인 ()을 오버로딩한 객체를 의미한다.  이 연산자를 통해 마치 함수를 호출하듯이 객체를 호출할 수 있다.  

 

  : iterarray

struct print
{
 void operator()(int a) const
 {
  printf("%d\n",a);
 }
};

void main()
{
 int ari[]= { 2,3,5,1,9};
 vector<int> vi(&ari[0],&ari[5]);


 sort(vi.begin(), vi.end());

 for_each(vi.begin(),vi.end(),print());        // 함수 객체의 사용 
}

 

 

4) 함수 객체의 특징 1 : 멤버 변수

 

 함수 객체는 말 그대로 객체이기 때문에 함수 연산자() 뿐만 아니라 처리에 필요한 멤버들을 추가로 가질 수 있다.  연산중에 필요한 변수가 있으면 멤버로 만들 수 있고 필요한 동작이 있다면 멤버 함수로 가질 수 있다.

 

  : 객체함수 멤버

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

struct accum
{
 int sum;
 accum() { sum=0; }
 void operator()(int a)
 {
  sum += a;
 }
};


void main()
{
 int ari[]= { 2,3,5,1,9};
 vector<int> vi(&ari[0],&ari[5]);

 sort(vi.begin(), vi.end());


 accum f;
 f = for_each(vi.begin(),vi.end(),f);        // 함수 객체의 사용

 printf("총합 : %d",f.sum);

}


 

 

5) 함수 객체의 특징 2 : 생성자, 멤버 함수 

 

 멤버뿐만 아니라 멤버 함수도 가질 수 있으며 생성자와 파괴자도 활용할 수가 있다.  특히 생성자는 멤버의 값을 원하는 대로 초기화 할 수 있다는 점에서 함수 객체에 대하여서도 실용성이 높다.

 

 

  : 생성자

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

 

struct print
{
 string mes;
 print(string &m) : mes(m){}      // 생성자의 활용 
 void operator()(int a) const
 {
  cout << mes;
  printf("%d\n",a);
 }
};

 

void main()
{
 int ari[] = { 2,8,5,1,9};
 vector<int> vi(&ari[0],&ari[5]);
    
 sort(vi.begin(),vi.end());
    
 for_each(vi.begin(),vi.end(),print(string("요소값은")));
 for_each(vi.begin(),vi.end(),print(string("숫자는")));

}


 

 

 

2. 알고리즘의 변형

1) 함수객체의 변형

 

for_each 함수는 순회 중에 할 일을 결정하기 위해 반드시 함수 객체를 부르도록 되어 있다.

순회만 할 바에야 for_each를 부를 필요가 없으므로 for_each에게 함수 객체는 필수적인 존재라고 할 수 있다. 이처럼 함수 객체를 명시적으로 요구하는 알고리즘도 있고 필요할 때만 함수 객체를 옵션으로 받는 알고리즘도 있다.

컨테이너에서 값을 검색하는 find는 순회중의 반복자 값과 세 번째 인수로 지정한 값(val)을 == 연산자로 비교하여 정확하게 일치하는 요소를 찾아낸다. 그런데 때로는==로 정확한 일치를 검색하는 것이 아니라 사용자가 정의하는 방식으로 검색할 요소를 골라야 하는 경우도 있다. 이때 함수 객체로 요소를 직접 비교할 수 있는데 이런 함수는 보통 원래 함수의 이름 끝에 _if가 붙는다. find의 함수 객체 버전은 다음과 같다.

 

InIt find_if(InIt first, InIt last, UniPred F);

 

세 번째 인수 F는 () 연산자를 오버로딩하는 함수 객체이며 요소값 하나를 인수로 전달받아 이 값이 원하는 조건이 맞는지 검사하여 bool형을 리턴한다. 찾는 조건에 맞으면 true를 리턴하고 아니면 false를 리턴할 것이다.

 

예)

 

  : find_if

#include <iostream>

#include <string>

#include <vector>

#include <algorithm>

using namespace std;

 

struct IsKim {

     bool operator()(string name) const {

          return (strncmp(name.c_str(),"김",2)==0);

     }

};

 

void main()

{

     string names[]={"김유신","이순신","성삼문","장보고","조광조",

          "신숙주","김홍도","정도전","이성계","정몽주"};

     vector<string> vs(&names[0],&names[10]);

 

     vector<string>::iterator it;

     it=find_if(vs.begin(),vs.end(),IsKim());

 

     if (it==vs.end()) {

          cout << "없다." << endl;

     } else {

          cout << *it << "이(가) 있다." << endl;

     }

}

 

 

2) 함수 객체의 장점

 

함수 객체는 고정된 의미를 가지는 알고리즘에 유연성을 부여하여 활용도를 대폭적으로 향상시킨다비교 조건을 직접 작성할 수 있으므로 정확하게 같은 것만 검색하는 것이 아니라 사용자가 원하는 어떤 조건으로도 검색할 수 있다.

사원 명부 컨테이너에서 직급이 과장 이상이고 나이는 45 ~ 49세 사이이며 가불을 한 적이 있고 입사한지 10년 이상 되었고 자택을 소유한 남자 사원을 검색하는 정도의 복잡한 동작까지도 가능해진다.

 

find는 템플릿으로 되어 있으므로 임의의 컨테이너에 대해 검색을 수행할 수 있는 일반성을 가진다. 검색 대상을 템플릿 인수로 전달받으므로 인수로 검색 대상을 지정할 수 있다. find_if는 여기에 비교 방식까지도 인수로 전달받아 검색 조건이 무엇인가까지도 사용자가 지정할 수 있다. 그래서 find보다 find_if가 훨씬 더 일반적이다.

find뿐만 아니라 대부분의 STL 알고리즘은 함수 객체를 인수로 취하는 버전이 있다. 정렬, 대체, 병합, 계산 등에 사용자가 개입할 여지가 많이 남겨져 있어 STL이 제공하는 기능대로만 사용하지 않아도 된다. 그래서 60개밖에 안되는 알고리즘으로도 엄청나게 많은 일을 처리할 수 있는 것이다.

 

 

3. 정의된 함수 객체

1) 정의됨 함수 객체

 

함수 객체는 통상 () 연산자 하나만 정의하고 그나마도 동작이 간단해 길이가 아주 짧다.

이런 짧은 클래스도 직접 선언해서 쓰자면 번거로운데 그래서STL은 자주 사용할만한 연산에 대해 미리 함수 객체를 정의하고 있다. 이런 객체들은 별다른 정의없이 그냥 사용하기만 하면 된다. 대표적으로 가장 간단한 함수 객체인 plus를 보자. 더할 피연산자의 타입 T를 인수로 받아들이는 클래스 템플릿이다.

 

struct plus : public binary_function<T, T, T> {

     T operator()(const T& x, const T& y) const { return (x+y); }

};

 

이 선언문에서 : public 이하의 내용은 다음 항의 주제이며 꼭 없어도 상관없으므로 잠시 무시하도록 하자. 본체 내용도 아주 쉬운데 T형의 x, y를 전달받아 x+y를 리턴한다. T가 아주 뚱뚱한 클래스일 수도 있으므로 값이 아닌 레퍼런스로 전달받고 피연산자를 상수로 취급한다는 정도 외에는 특별할 것도 없다. T 가 int라면 결국 a, b를 받아 a+b를 리턴하는 동작을 하는 함수 객체이다. 간단한 사용예를 보자.

 

  : plus

#include <iostream>

#include <functional>

using namespace std;

 

void main()

{

     int a=1,b=2;

     int c=plus<int>()(a,b);

     cout << c << endl;

}

 

함수 객체와 그 지원 매크로, 타입 등은 모두 functional 헤더 파일에 정의되어 있으므로 이 헤더 파일을 인클루드해야 한다. main에서 정수형 변수 a와 b를 선언하고 plus 객체의 함수 ()를 호출하여 두 정수의 합을 계산했다. 여기서 plus<int>() 구문이 조금 복잡해 보이는데 앞에서 설명했다시피 디폴트 생성자 호출문이며 임시 객체를 생성한다. 생성된 임시객체로부터 ()연산자 함수를 호출하되 인수로 a, b를 넘긴 것이다. 좀 쉽게 풀어쓰면 다음 두 줄이 된다.

 

plus<int> P;

int c=P(a,b);

 

plus 클래스 템플릿으로부터 plus<int> 타입의 클래스를 구체화하고 이 클래스 타입의 객체 P를 선언한다. 그리고 P의 오버로딩된 연산자 ()를 호출했는데 이 함수가 두 인수의 합을 리턴하도록 되어 있으므로 결국 c에는 a+b인 3이 대입된다. 객체를 통해 멤버 함수를 호출했을 뿐 별로 희한할 것도 없는 예제이다. plus외에도 많은 함수 객체들이 미리 정의되어 있다.

 

함수 객체

연산

minus

 인수의 차를 계산한다.

multiplies

 인수의 곱을 계산한다.

divides

 인수를 나눈  몫을 리턴한다.

modulus

 인수를 나눈  나머지를 리턴한다.

negate

인수 하나를 전달받아 부호를 반대로 만든다.

equal_to

 인수가 같은지 비교하여 결과를 bool타입으로 리턴한다.

not_equal_to

 인수가 다른지 비교한다.

greater

 번째 인수가  번째 인수보다 큰지 조사한다.

less

 번째 인수가  번째 인수보다 작은지 조사한다.

greater_equal

 번째 인수가  번째 인수보다 크거나 같은지 조사한다.

less_equal

 번째 인수가  번째 인수보다 작거나 같은지 조사한다.

logical_and

 인수의 논리곱(&&) 결과를 리턴한다.

logical_or

 인수의 논리합(||) 결과를 리턴한다.

logical_not

인수 하나를 전달받아 논리부정(!) 리턴한다.

 

 

 2) 함수 객체의 활용

  => 기존의 sort의 올림차순이 아니라 함수 객체를 사용하여 다양한 옵션이 가능하다.

 

정렬을 위한 알고리즘 구현은sort가 하되 비교 방식만 함수 객체로 사용자가 지정할 수 있다.  좀 더 복잡한 객체 컨테이너라면 이차 정렬 조건을 둘 수 있는데 예를 들어 사원들을 이름순으로 정렬하 되 혹시 동명이인이 있으면 나이순으로 정렬하도록 세부 정렬 지침을 제공할 수 있다. 비교 구문이 인라 인으로 삽입되어 정렬 속도도 굉장히 빠른데 C의 qsort 함수보다도 훨씬 더 빠르다.

 sort 함수는 요소의 < 연산자로 대소를 비교하므로 기본적으로 올림차순으로 정렬하는데 함수 객체를 취하는 다음 버전을 사용하면 정렬 순서를 원하는대로 지정할 수 있다.

 

void sort(RanIt first, RanIt last, BinPred F);

 

마지막 인수 F는 비교할 두 요소를 전달받아 비교 결과를 리턴하는데 함수 객체의 조건을 만족하면 true를 리턴한다. bool형을 리턴하므로 F는 조건자 함수 객체이다. 다음 예제는 문자열을 정렬하는데 일반 sort 함수와 함수 객체 버전으로 각각 정렬한다.

 

  : sortdesc

#include <iostream>

#include <string>

#include <vector>

#include <algorithm>

#include <functional>

using namespace std;

 

void main()

{

     string names[]={"STL","MFC","owl","html","pascal","Ada",

          "Delphi","C/C++","Python","basic"};

     vector<string> vs(&names[0],&names[10]);

 

     //sort(vs.begin(),vs.end());

     sort(vs.begin(),vs.end(),greater<string>());

    

     vector<string>::iterator it;

     for (it=vs.begin();it!=vs.end();it++) {

          cout << *it << endl;

     }

}

 

sort의 기본 버전은 요소간의 비교를 위해 < 연산자, 즉 less 비교 함수 객체를 사용하도록 되어 있어 작은 값이 더 앞쪽에 온다. 그러나 greater 함수 객체를 사용하면 큰 값이 더 앞쪽에 오므로 정렬 순서는 반대가 된다.

 

만약 미리 제공되는 함수 객체가 아니라 사용자가 정의한 방식대로 정렬하고 싶다면 직접 함수 객체를 만들어 sort의 세 번째 인수로 전달한다. 다음 예제는 대소문자 구분없이 알파벳순으로 문자열을 오름차순 정렬한다.

 

  : sortfunctor

#include <iostream>

#include <string>

#include <vector>

#include <algorithm>

using namespace std;

 

struct compare {

     bool operator()(string a,string b) const {

          return stricmp(a.c_str(),b.c_str()) < 0;

     }

};

 

void main()

{

     string names[]={"STL","MFC","owl","html","pascal","Ada",

          "Delphi","C/C++","Python","basic"};

     vector<string> vs(&names[0],&names[10]);

 

     //sort(vs.begin(),vs.end());

     sort(vs.begin(),vs.end(),compare());

     vector<string>::iterator it;

     for (it=vs.begin();it!=vs.end();it++) {

          cout << *it << endl;

     }

}

 compare는 인수로 전달된 두 문자열 a, b를 대소문자 구분없이 비교하여 a가 더 작은지를 리턴한다.  compare를 쓰지 않는 sort는 string의 < 연산자로만 대소를 비교하므로 대문자가 항상 소문자 앞에 오 지만 compare를 사용하는 sort는 대소문자에 상관없이 알파벳순으로 정렬된다.

 

4. 함수 객체의 사용

 

  : dualinstance

#include <iostream>

#include <list>

#include <vector>

#include <algorithm>

using namespace std;

 

void functor1(int a)            // 함수 포인터

{

     printf("%d ",a);

};

 

struct functor2 {               // 함수 객체

     void operator()(double a) const {

          printf("%f\n",a);

     }

};

 

void main()

{

     int ari[]={1,2,3,4,5};

    vector<int> vi(&ari[0],&ari[5]);

     double ard[]={1.2,3.4,5.6,7.8,9,9};

     list<double> ld(&ard[0],&ard[5]);

 

     for_each(vi.begin(),vi.end(),functor1);

     cout << endl;

     for_each(ld.begin(),ld.end(),functor2());

}

 

main에서 벡터와 리스트 두 개의 컨테이너를 정의하고 for_each를 두 번 호출하여 두 컨테이너의 내용을 출력했다. 이때 각각 다른 함수 객체를 사용했는데 첫 번째 for_each는 함수 포인터를, 두 번째 for_each는 함수 객체를 사용했다. 이 둘은 원형도 다르고 값을 출력하는 방식도 다르다. 실행 결과는 다음과 같다.

 

1 2 3 4 5

1.200000

3.400000

5.600000

7.800000

9.000000

 

그렇다면 for_each 함수의 세 번째 인수는 도대체 어떤 타입이라고 설명할 수 있을까? 예제가 잘 동작하는 걸 보면 void (*)(int) 타입의 함수를 받기도 하고 void(*)(double) 타입의 () 연산자가 정의된 객체를 받기도 한다. 가변 인수도 아닌 함수가 두 개의 다른 타입을 어떻게 받아들일 수 있는가 말이다.

 

이 문제의 해답은 간단하다for_each는 함수가 아니라 함수를 만들 수 있는 템플릿일 뿐이며 호출부에서 전달되는 타입에 맞게 매번 구체화된다. 어떤 타입을 정해 놓고 받는게 아니라 들어오는대로 받아들여 구체화되는 것이다. 물론 전달된 타입은 템플릿 본체의 코드를 100% 지원하는 타입이어야 한다. 위 예에서 for_each 함수의 실체는 두 개 존재하며 각 버전이 받아들이는 타입이 다르다.

 

STL은 알고리즘이 어떤 함수를 호출할 것인지에 대한 모든 결정을 컴파일시에 수행한다. 조건만 맞다면 그게 함수건 객체건 가리지 않으며 그래서 일반적이라고 하는 것이다. 컴파일 타임에 모든 점검과 결정이 이루어지므로 컴파일 시간은 조금 더 걸리겠지만 실행시의 효율은 좋을 수밖에 없다

 

5. 함수 객체의 분류

 

함수 객체가 하는 일은 비교, 대입, 합산 등 알고리즘 구현중에 필요한 연산을 처리하는 것이라고 할 수 있다. 취하는 피연산자 개수로 연산자를 분류하듯이함수 객체도 필요한 인수의 개수로 분류할 수 있으며 리턴값의 타입도 중요한 분류 기준이다. STL은 인수와 리턴값, 즉 원형에 따라 함수 객체를 다음과 같이 분류하고 고유의 이름을 부여한다.

 

인수의 개수

bool 아닌 리턴값

bool 리턴

없음

Gen

 

단항

UniOp

UniPred

이항

BinOp

BinPred

 

UniOp는 인수 하나를 취하는 단항 함수 객체이며 BinPred는 인수 둘을 취해 bool형을 리턴하는 조건자 함수 객체이다. 피연산자를 하나도 취하지 않는 함수 객체를 생성기(Generator)라고 하는데 입력없이 혼자 무엇인가를 만들어 내는 역할만 한다. 대표적으로 난수를 생성하는 함수 객체가 생성기이다. 함수 객체를 칭하는 이 표기만 보면 필요한 함수의 원형을 쉽게 유추할 수 있다.

 

알고리즘 함수들은 예외없이 템플릿 함수로 구현되어 있는데 함수 객체에 해당하는 템플릿 인수의 이름에 어떤 종류의 함수 객체가 요구되는지 표기된다. 마치 함수의 형식 인수 이름에 의미있는 이름을 붙여 유용한 정보를 표기하는 것과 같다. 앞에서 배운 몇 개의 알고리즘 함수 원형을 살펴보면 마지막 인수인 함수 객체에 이러한 정보가 포함되어 있다.

 

InIt find_if(InIt first, InIt last, UniPred F);                 // 단항 리턴

void sort(RanIt first, RanIt last,BinPred F);           // 이항 리턴  

T accumulate(InIt first, InIt last, T val,BinOp op);   // 더하므로 두개의 인자

 

find_if의 세 번째 인수는 UniPred로 되어 있으므로 인수 하나를 취하고 bool형을 리턴하는 단항 조건자임을 쉽게 알 수 있다. find_if와 함께 사용할 수 있는 함수 또는 함수 객체의 () 연산자 원형은 다음과 같을 것이다.

 

bool Pred(T &val) { }

 

여기서 T는 물론 검색 대상 컨테이너의 요소 타입이며 함수 호출문의 실인수 타입으로 구체화된다. 검색 대상인 val 인수는 값으로 받든 레퍼런스로 받든 함수 본체에서 val을 참조하는 구문에는 영향을 주지 않으므로 아무래도 상관없다. sort 함수는 두 개의 인수를 전달받아 두 인수를 비교한 후 bool형을 리턴하는 함수 객체를 요구하며 accumulate의 함수 객체는 두 인수를 전달받아 모종의 연산을 한다는 것을 알 수 있다.

만약 알고리즘 함수가 요구하는 원형과 다른 함수 객체를 인수로 전달하면 어떻게 될까? for_each 함수를 테스트하는 functor 예제의 print 함수 객체를 다음과 같이 수정해 보자

 

 

. for_each는 단항 함수 객체(UniOp)를 요구하는데 에러를 유발시키기 위해 일부러 두 개의 인수를 받도록 했다.

 

struct print {

     void operator()(int a, int b) const {

          printf("%d\n",a);

     }

};

 

문법상의 문제는 없으므로 이 객체 정의문 자체는 에러가 아니다. 그러나 이 객체를 사용하는 곳에서 문제가 발생하는데 for_each의 본체에서, 즉 algorithm 헤더 파일에서 에러가 발생한다. for_each는 아마도 다음과 같이 구현되어 있을 것이다.

 

UniOp for_each(InIt first, InIt last, UniOp op)

{

     for (;first != last; ++first)

          op(*first);                    // 여기서 에러 발생

     return (op);

}

 

for_each는 구간을 순회하면서 매 요소마다 op 함수 객체를 호출하는데 인수는 현재 순회중인 반복자의 값 *first 하나밖에 없다. 하지만 이 값을 전달받는 객체의 () 연산자 함수와는 원형이 맞지 않으므로 호출할 수 없다는 컴파일 에러가 발생하는 것이다. 정확하게는 템플릿 함수가 구체화되는 과정의 템플릿 본체에서 구문 에러가 발생한다.

런타임 중에 발생하는 것이 아니라 컴파일중에 뭔가 잘못되었다는 것을 즉시 알 수 있으므로 위험하지는 않다. 이런 특성을 타입에 대한 안정성이라고 하는데 오동작할 소지가 있는 코드를 컴파일중에 명백한 에러로 처리하여 실생시의 버그를 최소화한다. 이번에는 다음과 같이 리턴값의 타입만 다르게 수정해 보자.

 

struct print {

     int operator()(int a) const {

          return printf("%d\n",a);

     }

};

 

for_each는 함수 객체를 호출하기만 할 뿐 리턴값을 요구하지는 않는다. 하지만 이렇게 수정해도 별 문제는 없다. 리턴값을 넘기더라도 for_each에서 이 값을 무시할 수 있고 for_each 템플릿의 본체와 충돌하는 부분이 없기 때문이다. 만약 템플릿 본체에서 리턴값을 명시적으로 요구할 때는 리턴값 타입도 항상 정확해야 한다. sortfunctor 예제의 compare 함수 객체를 다음과 같이 수정해 보자.

 

struct compare {

     void operator()(string a,string b) const {

          stricmp(a.c_str(),b.c_str()) < 0;

     }

};

 

이 함수 객체는 두 개의 정렬 대상을 전달받아 앞 뒤를 가려 주는 역할을 하므로 비교 결과를 반드시 리턴해야 하는데 void형으로 잘못 작성했다. 이렇게 되면 sort 템플릿 본체에서 비교 결과를 사용하는 부분에서 에러가 발생한다. sort의 내부에는 아마 다음과 같은 코드가 작성되어 있을 것이다. 물론 실제 코드는 컴파일러마다 다르다.

 

if (op(*first, *(first-1))

 

op 함수 객체로 두 요소를 넘겨 비교하도록 하고 그 결과에 따라 요소를 재배치해야 하는데 op의 결과가 없으므로 if문에 사용할 수 없는 것이다. compare 객체의 () 연산자가 int를 리턴하도록 수정하는 것은 가능하다. int는 bool형과 호환 타입이고 if문의 조건절로 사용될 수 있기 때문이다.

 

어떤 건 되고 어떤 건 안되고 함수 객체의 올바른 형태를 결정하는 것이 굉장히 어려운 규칙인 것 같지만 원칙은 지극히 간단하다.

 

템플릿의 타입은 본체의 모든 조건을 만족해야 한다는 동일한 알고리즘 조건이라는 것이 있는데 바로 이 원칙에만 맞게 작성하면 된다. for_each의 본체에 맞는 함수 객체이기만 하면 되고 sort가 구현하는 코드를 제대로 실행할 수 있으면 되는 것이다. 알고리즘의 목적과 동작 과정을 잘 생각해 보면 아주 상식적이다. 비교 함수는 bool을 리턴하는게 당연하고 for_each의 인수는 하나일 수밖에 없다.


Posted by JJOREG
본 글은, Best Practices for Creating DLLs를 번역하였으며, 원문인 DLL_bestprac.doc를 통해 직접 내려받으실 수 있습니다. 본 글에 워낙 이론적인 내용이 많다 보니 이해가 어려울 수 있는데, 실제 코드와 디버깅을 통한 설명을 다음 기회에 공유하도록 하겠습니다.


dynamic-link library(DLL)은 응용프로그램이 실행중에 로드하고 호출할 수 있는 공유된 코드와 데이터로 정의할 수 있습니다. 전형적인 DLL은 응용프로그램을 위해 루틴들을 노출(=Export)시키며, 그 내부(즉, DLL)에서 사용할(internal use) 루틴도 역시 포함되어 있습니다. 이러한 기술은 여러 응용프로그램에서 공통 기능으로 공유할 수 있게 라이브러리 형태로 재사용 가능하게 하여 필요시 로드를 할 수 있도록 해줍니다. DLL 사용의 장점은 코드가 차지하는 공간(code footprint)을 줄이고, 단일 복사본을 공유함으로서 메모리 사용량을 낮추며, 개발과 테스트가 용이하게 하고 모듈화를 가능하게 합니다.

DLL을 만드는 일은 개발자에게 많은 도전을 가져다 줍니다. DLL은 시스템을 통한 강제적인 버전관리가 이뤄지지 않습니다. 즉, 여러개의 DLL이 한 시스템에 있을때, 이러한 버전관리 체크의 부족으로 인한 overwrite는 의존성과 API 충돌을 야기합니다. 개발 환경, 로더 구현 그리고 의존성의 복잡성은 로드 순서와 응용프로그램 행위에 취약성을 만듭니다. 그래도 많은 응용프로그램들은 복잡한 의존성을 가지는 DLL에 의지하고 있습니다. 이 문서는 DLL 개발자들을 위해 가이드라인을 제공하여 견고하고 이식성 있으며 확장성 있는 DLL로 만드는데 도움을 줄 것입니다.

■ 3개의 주요한 DLL 컴포넌트 개발 모델은 다음과 같습니다.

  1. Library Loader
    DLL은 가끔 복잡한 내부의존성(interdependency)을 자니는데, 이는 그들이 로드되어야 하는 순서를 정의합니다. Library Loader는 효과적으로 이러한 의존성을 분석하고, 정확한 로드 순서를 계산한뒤 그 순서대로 로드를 합니다.
  2. DLLMain entry-point function
    이 함수는 Loader에 의해 호출되며, 그 시점은 DLL의 Load 혹은 Unload일때 입니다. Loader는 한 시점에 단 하나의 DLLMain만 호출하도록 연속으로 호출합니다. 더 많은 정보
  3. Loader Lock
    Loader가 순서대로 로드할때 사용되는 프로세스 단위의 동기화 객체입니다. 프로세스 단위의 Library Loader Data를 반드시 읽거나 써야 하는 함수는 반드시 이 Lock을 획득해야 합니다. 물론 이러한 operation을 수행하기 전에 이뤄져야 합니다. Loader Lock은 recursive이며, 이는 같은 쓰레드에서 다시 Lock의 획득이 가능함을 의미합니다.

그림 1

그림 1. DLL 로드시 어떤일이 이뤄지는가?

DLLMain에서의 부적합한 동기화 시도는 응용프로그램에게 deadlock 혹은 초기화 되지 않은 DLL의 data와 code의 접근을 야기하게 됩니다. DLLMain에서의 특정 함수 호출은 이러한 문제를 잃으킵니다.

일반적인 최고의 습관


DLLMain은 Loader Lock이 획득되었을때 호출됩니다. 따라서, DLLMain 내부에서의 호출은 중요한 제약이 강요됩니다. DLLMain은 최소의 초기화 작업을 수행하도록 디자인 되었는데, 이는 Windows API의 몇몇 함수군 호출에 의해서 입니다. DLLMain에서 직접적이든 간접적이든 Loader Lock 획득을 시도하는 어떤 함수도 호출할 수 없습니다. 다시 말해, 이 경우가 발생하면 당신은 deadlock 혹은 crash를 경험하게 됩니다. DLLMain 구현에서의 에러는 해당 프로세스와 그 내부의 쓰레드를 위험에 빠트리게 됩니다.

이상적인 DLLMain은 "그냥 비우는것" 입니다. 그러나, 많은 응용 프로그램의 복잡도를 고려할때 이는 너무한 제약이 됩니다. DLLMain을 다루는 좋은 방법은 많은 초기화 과정을 가능한 뒤로 미뤄라라는 것입니다. 이러한 미뤄진 초기화는 응용프로그램을 더욱더 견고히 해주는데, 그 이유는 Loader Lock가 획득된 동안의 초기화가 이뤄지지 않았기 때문입니다. 역시 이러한 방법은 Windows API의 사용에도 훨씬 많은 안정성을 제공합니다.

몇몇 초기화 작업은 뒤로 미룰순 없을 것입니다. 예를 들어, 설정 파일에 의존성이 있는 DLL이 있는데, 해당 파일이 좋지 않거나 쓰레기 내용이 포함되었을때 그 DLL의 Load가 실패되야 하는 경우가 있을 것입니다. 이런 종류의 초기화 방식은, 다른 작업의 자원 낭비를 하느니 DLL이 그 행위를 시도해보고 빨리 실패하는 것이 좋다고 개념이라 보여집니다.

■ 다음과 같은 작업을 절대로 DLLMain에서 수행해서는 안됩니다.

  • LoadLibrary 혹은 LibraryEx의 직접적 혹은 간접적 호출. 이는 deadlock 혹은 crash를 유발한다.
  • 다른 쓰레드와의 동기화 시도. 이는 deadlock을 유발한다.
  • Loader Lock을 획득하기 위해 기다리는 코드가 획득한 다른 사설 동기화 객제를 획득하려고 하는 시도. 이는 deadlock을 유발한다.
  • CoInitializeEx 사용에 의한 COM 쓰레드 초기화. 특정 상황이 되면 이 함수는 LoadLibrary를 호출한다.
  • 레지스트리 함수군의 호출. 이 함수는 Advapi32.dll에 구현되어 있는데, 만약 AdvApi32.dll이 아직 당신 DLL에서 초기화되지 않았다면, 그 DLL은 메모리를 초기화해제 하며, crash를 유발한다.
  • CreateProces 호출. 이는 다른 DLL을 Load할 수 있다.
  • ExitThread 호출. DLL Detach 과정에서 Exit가 진행중인 Thread는 Loader Lock을 다시 획득하려고 하는 시도가 발생하여 deadlock 혹은 crash가 발생할 수 있다.
  • CreateThread 호출. 다른 Thread와 동기화 작업을 하지 않는다면, 생성중인 Thread가 할 수 있는데, 이는 위험할 수 있다.
  • Named Pipe 혹은 다른 Named Object의 생성(Windows 2000만 해당). Windows 200에서는 Named Object는 Terminal Service DLL에 의해 제공되는데, 만일, 이 DLL이 초기화되지 않았다면, DLL을 로드하게 되어 crash가 유발될 수 있다.
  • 메모리 관리 CRT 함수 호출. 만약 CRT DLL이 초기화 되지 않았다면, crash가 유발된다.
  • User32.dll 혹은 Gdi32.dll 함수 호출. 몇몇 함수들은 아직 초기화되지 않은 DLL을 로드한다.
  • 관리 코드의 사용

■ DLLMain 내에서 안전한 작업은 다음과 같습니다.

  • compile time의 static data의 초기화
  • 동기화 객체의 생성과 초기화
  • 메모리 할당과 dynamic data의 초기화 (위 금지 함수 이외)
  • Thread local storage(TLS) 초기화
  • File의 열기/읽기/쓰기
  • kernel32.dll 함수의 호출 (위 금지 함수 제외)
  • 전역 포인터 변수를 NULL로 할당

Lock 순서의 역(Lock order inversion)에 의한 deadlock



Lock과 같은 다중 동기화 객체 사용을 구현할 때, Lock 순서를 따르는 것은 굉장히 중요합니다. 어느 시점에서 한개 이상의 Lock을 획득하는 것이 필요할때 반드시 Lock hierachy 혹은 Lock 순서라 불리는 명시적인 순서를 정의해야 합니다. 예를 들어, Lock A가 Lock B이전에 획득되었고, Lock C 이전에 Lock B가 획득되었다면 Lock 순서는 A,B,C가 되고, 이 순서는 코드에서 지켜줘야 됩니다. 만약 Lock 순서가 역으로 되는경우가 발생했다면, 예를 들어, Lock A를 획득 하기 전에 Lock B가 획득되었을 경우, 이는 Lock 순서의 역에 의한 deadlock이 발생하게 됩니다. 이렇게 발생한 deadlock은 디버깅하기 힘든 면이 있습니다. 이것을 방지하기 위해 모든 쓰레드에서는 같은 순서대로 Lock을 획득해야만 합니다.

Loader는 이미 획득한 Loader Lock으로 DLLMain을 호출한다는 사실은 굉장히 중요합니다. 그래서 Loader Lock은 Locking hierachy의 가장 높은 우선순위가 되어야 합니다. 그와 마찬가지로 적합한 동기화를 위해 요구된 Lock을 획득하해야 하는 것도 알아야 합니다. 물론 hierachy에 정의된 모든 단일 Lock을 획득해야 할 필요는 없습니다. 예를 들어, A와 C를 적합한 동기화를 위해 획득하였다면, C를 획득하기 이전에 A를 획득해야 하며, B를 획득할 필요는 없습니다. 더 나아가 설명하자면, 프로그램 코드에서는 Loader Lock을 명시적으로 획득 할 수 없습니다. 만약 사적인 Lock을 획득한 상황에서 Loader Lock을 간접적으로 획득하려는 ::GetModuleFileName(...)과 같은 API를 호출해야 한다면, 사설 Lock을 획득하기 이전에 ::GetModuleFileName(...)을 획득해야만 합니다. 이는 Load 순서를 따르게 하기 위함입니다.

그림 2

그림 2. Lock 순서의 역에 의한 deadlock



그림 2. 는 이러한 Lock 순서의 역을 보여주고 있습니다. DLLMain을 포함하는 Main 쓰레드를 가지는 DLL을 생각해 보십시요. Library Loader는 Lock L을 획득했으며, DLLMain을 호출하려고 합니다. Main 쓰레드에서는 동기화 객체인 A, B 그리고 공유 데이터를 접근하기 위해 필요한 G를 생성하고 G를 획득하기 위해 시도하려고 합니다. 그와 별도로, Worker 쓰레드에서는 이미 G를 획득한 상황이며 ::GetModuleHandle(...)를 호출하여 Loader Lock인 L을 획득하려고 시도할 것입니다. 그러면, Worker 쓰레드는 L에 의해 Block 되며, Main 쓰레드는 G에 의해 Block 되며, 이로 인해 deadlock이 발생하게 됩니다.

이러한 상황을 방지하기 위해서는, 모든 쓰레드에서는 항상 순서에 맞게 동기화 객체를 획득하도록 시도해야 합니다.

동기화를 위한 최고의 습관



초기화의 한 부분으로 DLL이 Worker 쓰레드를 생성해야 하는 경우를 생각해 보십시요. DLL이 Cleanup되면 data의 무결성(consistent)을 확신하기 위해 모든 Worker 쓰레드의 동기화가 필요하며, 그 다음 Worker 쓰레드는 종료하게 됩니다. 오늘날, 멀티쓰레드 환경의 DLL을 종료하고 동기화하는데에는 완벽하고 정확한 방법은 없습니다. 다음은 DLL 종료를 하는 동안 이루어질 쓰레드 동기화를 위해 현재까지 나와있는 최고의 습관을 설명하고 있습니다.

■ 프로세스 종료시 DLLMain에서의 쓰레드 동기화
  • 프로세스 종료시 DLLMain이 호출되었다면 모든 프로세스의 쓰레드들은 Clean up이 이뤄지며 주소 공간(Address Space)는 더이상 유지되지 않습니다. 동기화는 이런 경우에는 필요하지 않습니다. 다시 말해 DLL_PROCESS_DETACH는 비워둬도 됩니다.
  • Windows Vista에서는 핵심 data들(환경 변수, 현재 디렉토리, 프로세스 힙, ...)의 유지가 보장됩니다. 그러나 다른 동적 할당된 사설 data는 망가져서 더이상 안전하지 않습니다.
  • 저장이 필요한 영구 유지될 상태들은 저장 매체에 플러쉬되어야 합니다.

■ DLL UnLoad시의 DLL_THREAD_DETACH를 위한 DLLMain의 쓰레드 동기화

  • DLL이 UnLoad될때 주소 공간(Address Space)는 사라지는 것은 아닙니다. 따라서 DLL은 Clean될 예정인 상태입니다. 이것은 쓰레드 동기화, Open된 핸들, 영구 유지해야 하는 상태 그리고 할당된 자원들을 포함합니다.
  • 쓰레드 동기화는 종잡을 수 없는데, DLLMain에서 쓰레드의 종료를 기다리는 것은 deadlock을 유발할 수 있기 때문입니다. 예를 들어, DLL A가 Loader Lock을 획득했습니다. 그리고 Thread T를 종료시키기 위해 Signal을 보냈고 종료를 기다리도록 합니다. 쓰레드 T는 종료가 되며, Loader는 DLL A의 DLL_THREAD_DETACH 호출을 위해 Loader Lock 획득을 시도할 것입니다. 이것이 deadlock을 유발시키게 됩니다. 이러한 리스크를 최소화 하는 방법은 다음과 같습니다.
    • DLL A는 DLLMain에서 DLL_THREAD_DETACH 메시지를 받고, 쓰레드 T에게 종료해라는 Signal을 보냅니다.
    • Thread T는 현재의 작업을 마치고 스스로 상태를 유지하며 DLL A에게 Signal을 보냅니다. 단, 이러한 유지된 상태 체크를 위해서 DLLMain의 deadlock 회피를 위한 제약을 지켜야 합니다.
    • DLL A가 쓰레드 T를 종료 시켰으며, 그것이 아직 유지된 상태임을 알수 있습니다.

만약 DLL이 그것의 모든 쓰레드를 생성하고 나서 UnLoad되었고 실행이 시작되기 전이었다면, 그 쓰레드들은 crash가 발생할 수 있습니다. 만약 DLL이 초기화의 단계로 DLLMain에서 Thread를 생성하였다면, 몇몇 쓰레드들이 아직 초기화가 완료되지 못했고 그들의 DLL_THREAD_ATTACH 메시지가 여전히 DLL에게 전달되기를 기다리고 있을 것입니다. 이런 상황에서 DLL이 UnLoad된다면 쓰레드들의 Terminate가 시작될 것입니다. 그러나 몇몇 쓰레드들은 Loader Lock에 의해 block되어 있을 겁니다. 그들의 DLL_THREAD_ATTACH 메시지들은 DLL이 unmap된 이후 진행될 것이며, 이는 crash를 유발할 것입니다.



///////////// 두번째


오랜만에 블로그에 글을 쓰고 있습니다. 일종의 생존신고랄까요? 
VS2005 로 처음 작업할때도 이런 비슷한 문제로 필요없는 삽질을 꽤 했었습니다.  여기 ㅜ.ㅜ
요즘 VS2008 로 해야할 작업이 있어서 쪼물딱 거리는 와중에 이상한 현상이 있어서 삽질하고, 구글링한 결과를 정리해 볼까 합니다.
어떤 X 같은 일이 생겼나면.. 

개발머신   : VS2008, SP1 
테스트머신: 윈도우 XP SP3 

이렇게 PC 가 두대가 있습니다. 
당연히 VS2005 에서 작업하던 대로 Debug 빌드시에는 
Microsoft Visual Studio 9.0\VC\redist\Debug_NonRedist\x86\Microsoft.VC90.DebugCRT
아래 있는 파일들을 실행파일과 함께 테스트 머신에 복사하고, 실행을 했습니다. (private assembly 형태의 배포죠)
그런데 실행이 안되는겁니다.

뭐 요딴 메세지만 나오는 군요. 
아래 그림에서처럼 DLL 이 뭔가 없는것도 아니고요. 아무 문제 없습니다. (VLdr.exe 는 VLdr.dll 을 사용합니다)


Manifest 파일과 배포해야 하는 DLL 들도 잘 복사되어있고요.


대체 뭐가 문제란 말입니까.. ㅠ.ㅠ

한참을 원인을 찾던 중.. (뭐 딱히 구글링할 키워드도 떠오르지 않더군요. -_-)
뭐 급한대로 VLdr.exe.config 파일을 만들어서 dll 들을 redirect 시켜서 대충 해결이 되긴 하더군요. 
하지만 dll 이 앞으로 한두개도 아니고.. 3rd party library 라도 사용하게 되면 대략 난감해 지겠죠. 
config 파일로 해결이 된다는것은 결국 바인딩 되는 dll 의 버전 문제 아니겠는가 라는 결론이 나오죠.

VS 링커 옵션에 보면 minifest 를 자동생성하도록 되어있습니다.(디폴트로)
역시 평소에 VS 각 옵션 설정을 꼼꼼히 보아두었던 것이 도움이 되는군요 ^^.
바인딩되는 dll 들은 링크타임에 manifest 형태로 만들어지는 것이고요. 생성된 manifest 는 exe 나 dll 에 내장되는것이겠죠. (아님 말구요)

결국 debug 디렉토리(중간 오브젝트 생성경로)에 생성된 ****.exe.embed.manifest 를 살펴보니 (****.exe.intermediate.manifest 는 뭐 중간에 잠깐 만들어지는 넘 같으니 패스) 아래와 같은 내용이 있네요.


그런데 C:\Program Files\Microsoft Visual Studio 9.0\VC\redist\Debug_NonRedist\x86\Microsoft.VC90.DebugCRT 에 있는 minifest 파일을 열어보면 (private assembly 로 배포하던 파일이죠)



이런 ...ㅅㅂ... 욕나옵니다. -_-;; 
결국 SP 1 이 설치되었는데도 VS 는 예전 버전(시스템 런타임 버전)으로 바인딩을 하고 있는겁니다. 
이러니 당연히 실행이 안될 수 밖에요 (이해가 안되시는 분들은 WinSxS 나 Strong named assembly 라는 키워드로 검색을.. ).

결국 이런 저런 삽질과 구글링을 통해 알아낸 결과
 - VS 2008 SP1 설치 미디어에 있는 VC_x86Runtime.exe 을 디버기에서 실행하거나
 - 아래 전처리기를 stdafx.h 같은 곳에 (가능하면 가장 위에) 정의해 주는 겁니다.
#define _BIND_TO_CURRENT_MFC_VERSION 1
#define _BIND_TO_CURRENT_CRT_VERSION 1
프로젝트 속성 -> C/C++ -> 명령줄에 추가하는 것이 더 좋은것 같습니다. 
/D "_BIND_TO_CURRENT_MFC_VERSION=1"
/D "_BIND_TO_CURRENT_CRT_VERSION=1"
MFC 프로젝트에서 stdafx.h 에 추가했더니 여전히 잘못된 manifest 를 만들어내는군요. -_-;;

이 전처리기를 이용하면 정상적으로 ***.exe.embed.manifest  에 버전이 SP 1 용 MFC, CRT 의 버전으로 만들어집니다.
이게 VS 2005 와 VS2008 에서 이렇게 요상하게 다르게 동작하는 원인은 고객들의 요구에 의한거라고 하는군요. -_-




'게임개발공부 > 무작정퍼오기' 카테고리의 다른 글

explicit 키워드에 대하여  (0) 2014.01.09
함수객체  (0) 2013.12.29
volatile (퍼온글)  (0) 2013.12.21
extern "C" 이건 뭔가? (퍼온글)  (0) 2013.12.21
dll의 기본. (퍼온글)  (0) 2013.12.21
Posted by JJOREG

volatile 키워드는 간단히 '코드 최적화를 막아주는 키워드'라고 설명할 수 있습니다. 즉 컴파일러의 최적화와 관련된 키워드이고 CPU 내/외부 캐시와 같은 하드웨어 최적화와도 관계가 있습니다. 지금처럼 임베디드 기반 프로그래밍이 보편화 되지 않았던 시절에는 volatile 키워드의 활용 빈도가 매우 낮았으나, 최근 임베디드 시스템이나 멀티 스레드를 고려한 프로그램이 늘어 가면서 volatile 키워드의 사용이 많아 지고 있습니다.

volatile 키워드는 주로 memory-maped I/O에서 사용됩니다. 임베디드 시스템에서는 MCU의 각종 레지스터가 메모리에 매핑되어 있는 경우가 많고 프로그램은 매핑된 메모리 주소에 값을 반복적으로 쓰게 되는데 이때 volatile 키워드가 사용됩니다. 다음의 코드로 volatile 키워드가 어떻게 동작하는지 살펴봅시다.

 

unsigned int *uart = 0x40700000;
*uart = 0x00080001;
*uart = 0x00080002;
*uart = 0x00080003;
*uart = 0x00080004;
*uart = 0x00080005;


위 코드를 보면 다섯 번째 메모리 쓰기가 모두 0x4070000번지에 행해집니다. 일반적인 상황에서 위 코드를 수행하고 나면 0x40700000 번지에는 가장 마지막 값인 0x00080005만 남게 될 것입니다. 따라서 똑똑한 컴파일러는 위 코드를 아래 코드처럼 최적화합니다.

 

unsigned int *uart = 0x40700000;
*uart = 0x00080005;

 

어차피 변수에는 가장 마지막 값만 남으므로 이렇게 고치는 것입니다. 일반적인 프로르매에서라면 아무런 문제 없이 동작하고 속도도 빨라집니다. 하지만 이 코드가 MMIO(Memory-Maped I/O) 상황에서 사용된다면 이것은 분명히 잘모된 최적화입니다. 각각의 메모리 쓰기 작업이 하드웨어에 특정한 작업을 명령하기 때문에 저렇게 최적화를 해 버리면 하드웨어는 오작동을 하게 됩니다. 그러므로 컴파일러에 최적화를 하지 말라는 '지시'를 내려야 합니다. 이러한 지시는 내리는 키워드가 바로 volatile입니다.

 

volatile unsigned int *uart = 0x40700000;
*uart = 0x00080001;
*uart = 0x00080002;
*uart = 0x00080003;
*uart = 0x00080004;
*uart = 0x00080005;

 

위와 같이 uart변수를 volatile로 선언하면 컴파일러는 uart변수에 최적화를 하지 않고모든 읽기 쓰기 작업을 메모리에서 직접하게 됩니다. 위 코드의 쓰기 작업 뿐만 아니라 읽기 작업도 마찬가지 입니다.

 

unsigned int *uart = 0x40700000;
char ch;
int i;
for(i = 0;i < 5;i++)ch = *uart;

 

위와 같은 코드 역시 최적화를 하면 *uart의 값을 한번만 읽어서 캐시에 저장한 다음 이것을 반복해서 사용합니다. 하지만 아래 코드처럼 uart 변수가 volatile로 선언되면 루프안에서 uart변수를 요구할 때마다 매번 메모리에서 값을 가져오기 때문에 그때 그때 변경되는 값을 읽을 수 있습니다.


volatile unsigned int *uart = 0x40700000;
char ch;
int i;
for(i = 0;i < 5;i++)ch = *uart;

 

---------------



'게임개발공부 > 무작정퍼오기' 카테고리의 다른 글

함수객체  (0) 2013.12.29
DLL만 로딩해도 컴퓨터가 뻗는경우.  (0) 2013.12.21
extern "C" 이건 뭔가? (퍼온글)  (0) 2013.12.21
dll의 기본. (퍼온글)  (0) 2013.12.21
컨테이너 종류 복습  (0) 2013.12.21
Posted by JJOREG

가끔 일하다보면 extern "C"를 왜 써야 되는지도 모르고
사용하는 신입 코더들을 볼 수 있다. 때론 귀찮게 물어보기도 하고...
나중에 또 물어보면 이 페이지의 주소만 날려주리라. ㅋ

컴파일러는 link 작업시 오브젝트간 함수 이용 및 위치를 파악할 수 있도록,
컴파일시 사용된 함수에 관련된 정보를 오브젝트 파일에 기록하며, 이를 linkage라고 한다.

C++ 컴파일러는 컴파일 과정에서 Name mangling 이란 작업을 한다.
이는 정의되어 있는 함수명을 정해진 규칙에 의해 바꿔버리는 것이다.
(링크 에러날 때 본 적 있는가? 함수명 앞뒤에 붙은 이상한 기호와 숫자들을?)
(그리고 C++ 컴파일러가 왜 바꾸냐고? 그거 설명하려면 두꺼운 책 가져와야 된다... ㅈㅈ
굳이 목적 중에 하나를 설명하자면 override된 함수들의 구별을 위해... 이 정도)

참고로, C는 함수명 앞에 _ (underbar) 하나를 붙인다.

중요한 사실은 "코딩시 함수명과 컴파일 완료된 바이너리 코드에서의 함수명은 다르다" 라는 것이다.
이름을 바꾸는 규칙은 컴파일러에 따라 다르기 때문에 C++ 컴파일러가 다르면 호환이 되지 않는다.
즉, linkage type이 달라져 함수를 찾을 수 없게 되는 것이다.

이는 C++ 컴파일러 간에도 발생하는 문제이며 C와 C++ 컴파일러간에도 발생한다.
(당연하지 않은가, 컴파일러가 다르다니까 ㅋ)

공용의 라이브러리를 제작하였고 이 라이브러리를 사용하는 코더의 계층이 다양하다고 가정해보자.
C 환경에서도 사용을 할 것이고, 다른 C++ 환경에서도 사용할 것이다.
라이브러리 개발&배포자 입장으로써 링크 에러로 아우성치는 유저들 당황스럽다.

이를 피하기 위해 C++ 문법에서는 네임 맹글링을 막기 위해 아래 문장이 제공된다.
즉, C의 네임 형식으로 함수를 컴파일하는 것이다.

extern "C"

따라서 범용적인 라이브러리 헤더라면 아래와 같이 작성되는 것이 좋을 것이다.

// 상략
#ifdef __cplusplus
extern "C" {
#endif

// 중략
#ifdef __cplusplus
}
#endif
// 하략

자 이제 함수를 C 형식으로 컴파일 했다.
그러면 어떻게 되겠는가? name mangling 회피로 인한 link 문제는 해결이 되었다.

하지만 C 형식이므로 class의 멤버 변수가 될 수도, override가 될 수도 없다.
즉, C++ 고유 특성을 모두 잃어버리게 된다.

출처:extern "C"


'게임개발공부 > 무작정퍼오기' 카테고리의 다른 글

DLL만 로딩해도 컴퓨터가 뻗는경우.  (0) 2013.12.21
volatile (퍼온글)  (0) 2013.12.21
dll의 기본. (퍼온글)  (0) 2013.12.21
컨테이너 종류 복습  (0) 2013.12.21
텍스처 아틀라스  (0) 2013.12.20
Posted by JJOREG

1. DLL의 주요 장점


Dynamic Link Libray(DLL)에 대해 설명하기 전에, 
이것이 어떠한 장점이 있고 어떠한 경우 사용하기 좋은지 살펴보도록 하자.

단순히 라이브러리화 함으로써 얻는 이점이 아닌, DLL만이 가질 수 있는 내용으로 정리하였다.

1. 애플리케이션의 기능 확장

DLL은 프로세스의 주소 공간에 동적으로 로드될 수 있기 때문에,
애플리케이션이 수행 중이더라도 수행해야 할 작업이 결정되면 해당 작업을 수행할 수 있는 코드를 로드할 수 있다.

어떤 회사가 제품을 개발하고, 다른 회사가 이 제품의 기능을 
확장하거나 보강할 수 있도록 하려는 경우에도 DLL은 상당히 유용하게 사용될 수 있다.

2. 메모리 절약

두 개 혹은 그 이상의 애플리케이션이 동일한 DLL 파일을 사용할 경우, DLL을 램에 단 한 번만 로드하고, 
이 DLL을 필요로 하는 애플리케이션들은 앞서 로드한 내용을 공유할 수 있다.

C/C++ 런타임 라이브러리의 경우가 대표적인 예라고 할 수 있다.

매우 많은 애플리케이션들이 CRT 라이브러리를 사용하는데, 
모든 애플리케이션들이 이를 정적으로 링크하게 되면, 동일 기능들이 메모리에 여러 번 로드될 것이다.

하지만, DLL 형태의 CRT 라이브러리를 링크하게 되면, 이러한 기능들은 메모리상에 
단 한번만 로드될 것이므로 메모리를 좀 더 효율적으로 사용할 수 있게 된다.

3. 지역화 촉진

Localization을 위해 DLL을 사용하곤 한다.

예를 들어, 코드로만 구성되어 있어서, 어떠한 UI 컴포넌트도 포함하고 있지 않은 형태로 구동한 뒤,
해당 지역에 맞는 DLL를 로드하여, 지역화된 UI 컴포넌트를 사용할 수 있는 것이다.

4. 플랫폼 차별성 해소

다양한 윈도우 버전들은 각기 서로 다른 함수들을 제공하고 있다.

개발자들은 운영체제가 제공하는 최신의 기능을 사용하고 싶어 하지만,
그 기능이 제공되지 않는 낮은 버전의 운영체제에서는 프로그램이 실행조차 되지 않을 수 있다.

이러한 기능을 DLL 파일로 분리해 두면, OS 버전에 맞는 DLL을 로드하여
OS가 지원하는 최대한의 신 기능을 활용하도록 환경을 구성할 수 있게 된다.

5. 특수한 목적 달성

윈도우는 단지 DLL에서만 사용 가능한 몇몇 기능들을 가지고 있다.

Hook을 설치하는 것과 같은 작업(SetWindowsHookEx, SetWinEventHook)이 그 예가 될 수 있는데,
이 때 사용하는 훅 통지 함수는 반드시 DLL 내에 존재해야 한다.

윈도우 탐색기의 shell은 DLL 파일로 작성된 COM 오브젝트를 구성함으로써 그 기능을 확장할 수 있으며,

웹 브라우저는 DLL을 이용하여 ActiveX 컨트롤을 사용, 다양한 기능을 웹 환경에 제공하고 있다.


2. DLL과 프로세스 주소 공간

DLL 파일의 이미지는 실행 파일이나 다른 DLL이 DLL 내 포함되어 있는 함수를 호출하기 전에
반드시 프로세스의 주소 공간에 매핑되어 있어야 한다.

이를 위해 다음 두 가지 방법 중 하나를 선택할 수 있다.
  • 암시적 로드타임 링킹
  • 명시적 런타임 링킹
위 방법들에 대해선 아래 챕터에서 각각 자세히 설명하도록 하겠다.

DLL 파일 이미지가 프로세스의 주소 공간에 매핑되고 나면,
DLL이 가지고 있는 모든 함수들은 프로세스 내의 모든 쓰레드에 의해 호출될 수 있게 된다.

사실 이렇게 로드가 완료되고 나면, DLL 고유의 특성은 거의 없어진다고 볼 수 있다.

프로세스 내의 쓰레드 관점에서는 DLL이 가지고 있는 코드와 데이터들은
단순히 프로세스의 주소 공간에 로드된 추가적인 코드와 데이터들로 여겨질 뿐이다.

쓰레드가 DLL에 포함되어 있는 함수를 호출하게 되면, 
호출된 DLL 함수는 호출한 쓰레드의 스택으로부터 전달된 인자 값을 얻어내고,
호출한 쓰레드의 스택을 이용하여 지역변수를 할당하게 된다.

뿐만 아니라 DLL 함수 내부에서 생성하는 모든 오브젝트들도 
DLL 함수를 호출하는 쓰레드나 프로세스가 소유하게 되며, DLL 자체가 소유하는 오브젝트는 존재하지 않는다.

예를 들어, DLL 내의 특정 함수가 VirtualAlloc 함수를 호출하게 되면,
해당 함수를 호출한 쓰레드가 속해 있는 프로세스의 주소 공간 내에 영역이 예약된다.

만일 DLL이 프로세스의 주소 공간으로부터 내려간다 하더라도
앞서 프로세스의 주소 공간에 예약했던 영역은 그대로 남게 되는데,
이는 시스템이 해당 영역이 DLL로부터 예약되었다는 사실을 특별히 관리하지 않기 때문이다.

하지만, 단일의 주소 공간은 하나의 실행 모듈과 다수의 DLL 모듈로 구성되어 있음을 반드시 알아두어야 한다.

이 중 일부 모듈은 C/C++ 런타임 라이브러리를 정적으로 링크하고 있을 수도 있으며,
또 다른 모듈은 C/C++ 런타임 라이브러리를 동적으로 링크하고 있을 수도 있다.

따라서, 단일의 주소 공간 내에 C/C++ 런타임 라이브러리가 여러 번 로드될 수 있다는 사실을 잊어버리면 곤란한다.
아래 예제를 살펴보자.

  1. -- 실행 파일의 함수 --
  2. void EXEFunc()
  3. {
  4.     void* pv = DLLFunc();
  5.  
  6.     // pv가 가리키는 저장소를 사용한다.
  7.  
  8.     // pv가 EXE의 C/C++ 런타임 힙 내에 있을 것이라고 가정한다.
  9.     free(pv);
  10. }
  11.  
  12. -- DLL의 함수 --
  13. void* DLLFunc()
  14. {
  15.     // DLL의 C/C++ 런타임 힙으로부터 메모리를 할당받는다.
  16.     return (malloc(100));
  17. }

이 코드가 정상적으로 동작할 것인가?
DLL 함수 내에서 할당받은 메모리 블럭을 EXE 함수 내에서 정상적으로 해제할 수 있는가?

위 예제는 제대로 동작할수도 있고, 그렇지 않을 수도 있다.

만일 EXE와 DLL이 모두 DLL로 구성된 C/C++ 런타임 라이브러리를 사용하고 있다면, 위 예제는 정상 동작한다.

하지만, 둘 중 하나라도 C/C++ 런타임 라이브러리를 정적으로 링크하고 있다면, free 호출 과정에서 문제가 발생할 것이다.

이러한 문제는 애초에 습관을 제대로 들이면 된다.
DLL 내 메모리를 할당하는 함수가 있다면, 해제하는 함수도 DLL에 만들고 그걸 사용하는 것이다.

  1. -- 실행 파일의 함수 --
  2. void EXEFunc()
  3. {
  4.     // DLL 함수에서 할당한 메모리 블럭의 주소를 얻어온다.
  5.     void* pv = DLLAllocFunc();
  6.  
  7.     // pv가 가리키는 메모리 블럭을 사용한다.
  8.  
  9.     // DLL 함수를 이용해 pv를 메모리로 반환한다.
  10.     DLLFreeFunc(pv);
  11. }
  12.  
  13. -- DLL의 할당 함수 --
  14. void* DLLAllocFunc()
  15. {
  16.     // DLL의 C/C++ 런타임 힙으로부터 메모리를 할당받는다.
  17.     return (malloc(100));
  18. }
  19.  
  20. -- DLL의 해제 함수 --
  21. void DLLFreeFunc(void* p)
  22. {
  23.     // DLL의 C/C++ 런타임 힙에서 메모리를 해제한다.
  24.     free(p);
  25. }

그리고, 실행 파일 내에 전역으로 선언된 정적 변수는 동일한 실행 파일이
여러 번 실행될 경우라도 Copy-on-write 메커니즘에 의해 공유되지 않는다.

DLL 파일 내에 전역으로 선언된 정적 변수 역시 이와 동일한 메커니즘이 적용된다.

프로세스가 DLL 이미지 파일을 자신의 주소 공간 내에 매핑하는 경우
실행 파일의 경우와 동일하게 전역으로 선언된 정적변수의 새로운 인스턴스가 생성된다.

이를 공유하게 하는 방법에 대해선 PE/COFF의 Section 문서의 챕터 4. 공유 섹션을 보기 바란다.


3. DLL과 실행 파일 작성법

이제, DLL을 생성하는 방법과 실행 파일이 어떻게 DLL 파일의 함수나 변수를 사용해야 하는지에 대해 알아보자.

1. DLL 모듈 생성

DLL은 변수, 함수, C++ 클래스를 다른 모듈에 export 할 수 있다.

하지만, 코드의 계층적 추상화를 유지하고 DLL 코드를 좀 더 쉽게 유지/관리하기 위해 
변수는 가급적 익스포트 하지 않는 것이 좋다.

또한, C++ 클래스는 export 한 C++ 클래스를 사용하는 모듈을 
동일한 회사의 컴파일러를 사용하는 컴파일한 경우에만 사용할 수 있으므로 주의하도록 하자.

DLL을 작성할 때 export 하고자 하는 변수나 함수를 포함하고 있는 헤더 파일을 먼저 작성하는 것이 좋다.
이러한 헤더 파일에는 export 할 함수나 변수가 사용하는 심벌이나 데이터 구조체도 반드시 정의되어 있어야 한다.

이 헤더는 DLL과 함께 배포되어야 하며, 이 DLL을 사용하는 모듈은 이 헤더를 반드시 인클루드 해야 한다.

또한, 유지보수의 편의성을 위해 DLL 하나당 헤더 파일 1개씩 페어로 작성하는 것이 좋다.

이제 간단한 DLL 헤더의 예제를 살펴보도록 하자.
(아래 예제는 vs2010에서 DLLTest라는 DLL 프로젝트를 생성하면 기본적으로 생성시켜주는 헤더 파일이다)


  1. #ifdef DLLTEST_EXPORTS
  2. #define DLLTEST_API __declspec(dllexport)
  3. #else
  4. #define DLLTEST_API __declspec(dllimport)
  5. #endif
     
  6. // 이 변수는 DLLTest.dll에서 내보낸 것입니다.
  7. extern DLLTEST_API int nDLLTest;
  8.  
  9. // 이 함수는 DLLTest.dll에서 내보낸 것입니다.
  10. DLLTEST_API int fnDLLTest(void);

DLLTest 프로젝트의 전처리기에는 DLLTEST_API 가 선언되어 있다.
즉, 함수나 변수, 클래스 앞에 __declspec(dllexport) 선언 지정자가 붙는 것이다.

그리고 아래 예제는 역시 DLLTest 프로젝트가 기본 생성해준 cpp 파일이다.

  1. // DLLTest.cpp : DLL 응용 프로그램을 위해 내보낸 함수를 정의합니다.
  2. //
  3. #include "stdafx.h"
  4. #include "DLLTest.h"
  5.  
  6. // 내보낸 변수의 예제입니다.
  7. DLLTEST_API int nDLLTest=0;
  8.  
  9. // 내보낸 함수의 예제입니다.
  10. DLLTEST_API int fnDLLTest(void)
  11. {
  12.     return 42;
  13. }

위 cpp 예제에서 nDLLTest 변수와 fnDLLTest 함수 앞에 DLLTEST_API 매크로가 붙어 있지만,
cpp에서는 DLLTEST_API를 붙이지 않아도 무방하다.

자, 이제 위와 같이 기본 생성된 DLL 프로젝트를 빌드하고, DLL을 생성해 보자.
Output 폴더에 가보면, 다음 두 개의 파일이 생성되어 있다.
  • DLLTest.lib
  • DLLTest.dll
DLL 파일을 컴파일하면, 컴파일이 끝난 후 링커는 DLL의 소스코드가 
적어도 하나 이상의 함수/변수를 export 하고 있는지를 확인하고, 그 경우 .lib 파일을 생성한다.

.lib 파일은 어떠한 함수나 변수도 포함하고 있지 않기 때문에 그 크기가 매우 작으며,
단순히 DLL 파일이 export 하고 있는 함수나 변수의 심벌 이름만을 유지하고 있다.
이 파일은 DLL 파일이 export 하는 심볼을 참조하는 실행 모듈을 링크하는 과정에서 반드시 필요하다.
(이 내용은 DLL을 암시적 로드타임 링크할 때만 유효하다)

.lib 파일을 생성하는 것 외에도 링커는 DLL 파일 내에 해당 파일이 
export 하고 있는 심볼에 대한 정보를 테이블 형태로 포함시켜 준다.

export section이라고 불리는 이 테이블은 export 하고 있는 
변수, 함수, 클래스 심볼에 대한 목록을 (알파벳순으로) 가지고 있다.

링커는 이 외에도 상대 가상 주소(RVA:Relative Virtual Address)를 DLL 파일 내에 포함시키는데,
이 값은 각각의 심볼들이 DLL 모듈 내의 어느 위치에 있는지를 가리키는 값이다.

그럼 이제 DLLTest에서 export 하고자 한 변수와 함수가 제대로 export 되어 있는지를 dumpbin으로 확인해 보자.
꾸엑~ 
함수명 fnDLLTest은 ?fnDLLTest@@YAHZX로 
변수명 nDLLTest은 ?nDLLTest@@3HA로 naming 되어 있다.

이것은 C++ 컴파일러가 C++ 특성(override나 등등의)을 위해 컴파일시 함수나 변수명에 name mangling을 하기 때문이다.
물론 DLL도 C++로 작성하고, 사용 모듈 역시 C++로 작성된다면 실행에 아무런 방해가 되지 않는다.

하지만, DLL은 C++로 작성하고, 사용 모듈이 C로 작성될 수도 있는 상황이라면,
C로 작성한 사용 모듈에서 DLL 함수를 호출하면 링크 과정에서 존재하지 않는 심볼 참조 에러가 발생하게 된다.

이를 해결하기 위해선, extern "C" 를 사용하는 방법이 있다.

즉, 위 헤더를 다음과 같이 변경하는 것이다.

  1. #ifdef DLLTEST_EXPORTS
  2. #define DLLTEST_API extern "C" __declspec(dllexport)
  3. #else
  4. #define DLLTEST_API extern "C" __declspec(dllimport)
  5. #endif
  6.  
  7. // 이 변수는 DLLTest.dll에서 내보낸 것입니다.
  8. DLLTEST_API int nDLLTest;
  9.  
  10. // 이 함수는 DLLTest.dll에서 내보낸 것입니다.
  11. DLLTEST_API int fnDLLTest(void);

      하지만, extern "C" 는 C++ 클래스 사용시엔 문제가 발생하므로, 클래스는 조심해서 한정자를 사용해야 한다.
      자세한 내용은 http://sweeper.egloos.com/1792976 참고

      자 이제, 다시 한번 DLL 프로젝트를 빌드하고, dumpbin으로 확인해 보자.

      오~ 깨끗하게 나온다.
      헌데, 이 상태에서 fnDLLTest 함수의 calling convention을 __stdcall로 바꾸고 싶어졌다.

      문제는 M$ C 컴파일러가 __stdcall 함수에 대해선 C++를 사용하지 않다 하더라도,
      함수의 이름을 멋대로 바꾸는 작업을 수행한다는 것이다.

      함수의 이름 앞에 _ (underbar)를 붙이고, 함수의 이름 뒤엔 매개변수 크기의 총합을 @와 함께 표시한다.

      위 예제의 fnDLLTest 함수를 __stdcall로 호출하면, dumpbin 결과가 아래와 같이 나온다.
      역시 fnDLLTest 함수명이 바뀌었기에, 
      DLL을 사용하는 모듈이 타사의 컴파일러를 사용하는 경우 링크 에러를 발생시키게 된다.

      다른 회사의 컴파일러에서 사용될 DLL 파일을 M$ 컴파일러를 사용해 컴파일하려 하면,
      컴파일러에게 이름 변환을 수행하지 않도록 확실한 명령을 주어야만 한다.

      두 가지 방법이 있으나, 두 번째 방법은 별로이므로, 한가지만 소개하겠다.

      바로 .def 파일을 프로젝트에 추가하고 EXPORTS 섹션을 구성하는 것이다.
      .def 파일은 DLL의 ouput name과 동일하게 맞추어야 한다.

      1. LIBRARY DLLTest
      2.  
      3. EXPORTS
      4.         fnDLLTest
      5.         nDLLTest

      M$ 링커는 .def 파일을 분석하는 과정에서 fnDLLTest와 _fnDLLTest@0 이라는 두 개의 함수가 export 되었음을 인지하게 된다.
      그러나 이 두 함수의 이름이 서로 일치하기 때문에 (이름 변환 과정을 배제할 경우)
      .def 파일에 정의되어 있는 fnDLLTest 라는 함수만 export 하고, _fnDLLTest@0 는 export 하지 않는다.

      사실 .def 방식이 가장 확실하다.

      .def 파일을 사용하면, 
      • extern "C"를 붙이든 그렇지 않든,
      • 함수의 calling convention으로 __stdcall을 사용하든
      naming에 있어 아무런 문제가 되지 않는다.

      만약, 자신이 만든 DLL이 널리 배포될 용도로 제작되어야 한다면,
      조금 귀찮더라도 .def 파일을 프로젝트에 추가하고, EXPORTS 섹션을 구성함을 가장 추천한다.

      물론, M$ 컴파일러로 DLL을 만들고, 사용 모듈 역시 M$ 컴파일러로 만들고, 둘의 환경 모두 C++이라면
      아무 것도 하지 않아도 문제가 되진 않는다.


      2. 실행 모듈 생성

      우선, 아래 내용은 DLL을 Implicit Loadtime Link 할 경우에 초점이 맞추어져 있다.

      실행 파일의 소스 코드를 개발할 때에는 반드시 DLL과 함께 제공되는 헤더 파일을 인클루드해 주어야 한다.

      실행 파일의 소스 코드에서는 DLL 헤더 파일을 인클루드할 때, DLLTEST_EXPORTS 를 정의해선 안 된다.
      정의하지 않은 채 코드를 컴파일하게 되면,
      DLLTest.h 파일 내에서 DLLTEST_EXPORTS를 __declspec(dllimport)로 정의하게 된다.

      1. // DLL 헤더 인클루드
      2. #include "DLLTest.h"
      3.  
      4. // 테스트를 위한 암시적 로드타임 링킹
      5. #pragma comment(lib, "DLLTest.lib")
      6.  
      7. int _tmain(int argc, _TCHAR* argv[])
      8. {
      9.         // DLL의 변수에 값 설정
      10.         nDLLTest = 1;
      11.  
      12.         // DLL의 함수 호출
      13.         int num = fnDLLTest();
      14.  
      15.         return 0;
      16. }

      컴파일러가 변수, 함수, C++ 클래스에 대해 __declspec(dllimport) 한정자를 발견하게 되면, 
      이러한 심볼들이 다른 DLL 모듈에서 임포트된 것임을 알게 된다.

      다음 단계로 링커는 실행 파일의 소스 코드에서 사용되고 있는 심볼들이 어떤 DLL로부터 export 된 것인지를 확인해야 한다.
      이를 위해 링커에게 .lib 파일을 전달해 주어야 한다.

      앞서 말한 것과 같이 .lib 파일은 단순히 DLL 모듈이 export 하고 있는 심볼들에 대한 목록만을 가지고 있다.

      링커는 import 된 심볼을 찾는 과정에서 import section이라는 특수한 섹션을 실행 파일 내에 추가한다.
      이 import section은 실행 파일이 필요로 하는 DLL 모듈과 해당 DLL 모듈에서 참조되는 심볼에 대한 목록이 포함되어 있다.

      이제 실행 파일을 생성하고, dumpbin으로 내용을 확인해 보자.
      DLLTester.exe의 import 섹션에서 DLLTest.dll의 fnDLLTest 함수와 nDLLTest 변수의 심볼을 확인할 수 있다.

      이제 실행 파일을 실행해 보자.

      실행 파일이 실행되면...
      1. 운영체제의 로더는 프로세스를 위한 가상 주소 공간을 생성한다.
      2. 이후, 로더는 실행 모듈을 프로세스의 주소 공간에 매핑한다.
      3. 로더는 실행 파일의 import section을 확인하여 필요한 DLL 파일들을 찾아서 프로세스의 주소 공간에 추가 매핑한다.
      Import section 내에 포함된 DLL 이름은 전체 경로명을 포함하고 있지 않기 때문에,
      로더는 사용자의 디스크 드라이브로부터 DLL을 검색해야 한다.

      아래에 로더의 검색 순서를 나타내었다.
      1. 실행 파일 이미지가 있는 디렉토리
      2. GetSystemDirectory 함수의 반환 값인 윈도우 시스템 디렉토리 (Windows\System32)
      3. 16비트 시스템 디렉토리 (Windows\System)
      4. GetWindowsDirectory 함수의 반환 값인 윈도우 디렉토리 (Windows\)
      5. 프로세스의 현재 디렉토리
      6. PATH 환경 변수에 포함된 디렉토리
      DLL 모듈이 프로세스의 주소 공간에 매핑되면, 로더는 각 DLL 파일의 import section을 조사한다.
      (DLL 역시 다른 DLL을 링크할 수 있기 때문에...)

      만일 import section이 존재한다면, 로더는 계속해서 프로세스의 주소 공간에 
      추가적으로 필요한 DLL 파일들을 매핑시켜 나단다.

      로더는 DLL 모듈을 지속적으로 추적하여, 설사 여라 차례 참조되는 모듈이라 하더라도,
      단 한번만 로드되고 매핑될 수 있도록 한다.

      모든 DLL 모듈들이 프로세스의 주소 공간에 로드되고 매핑되면,
      로더는 import 된 심볼의 모든 참조 정보를 수정해 나간다.
      이를 위해 각 모듈의 import section을 다시 한번 살펴보게 된다.

      로더는 각각의 심볼에 대해 관련 DLL의 export section을 검토하고 심볼이 실제로 존재하는지 확인한다.

      로더는 심볼의 RVA 정보를 가져와서 DLL 모듈이 로드되어 있는 가상 주소 공간에 그 값을 더한다.
      이후, 실행 모듈의 import section 내에 계산된 가상 주소 값을 기록한다.

      이제 코드에서 import 된 심볼을 참조하게 되면 호출 모듈의 import section으로부터
      import 된 심볼의 위치 정보를 가져와서 import 된 변수, 함수, C++ 클래스의 멤버 함수에 성공적으로 접근할 수 있게 된다.

      애플리케이션이 새로 실행될 때마다 위와 같은 과정을 거쳐야 하므로, 초기 구동에 많은 비용을 소비하게 된다.

      애플리케이션의 로딩 속도를 향상시키기 위해 실행 파일과 DLL 모듈에 대해
      시작 위치 변경(rebase)과 바인딩(binding) 작업을 수행하는 것이 좋다.

      Rebase와 binding에 대해서는 추후 별도로 글을 하나 더 쓰는 것이 좋을 듯 하다.


      4. Implicit Loadtime Link

      암시적 로드타임 링크는 static libaray를 링크하는 방법과 완전히 동일한 방법으로 수행할 수 있다.
      또한, static library를 링크하는 것처럼 DLL의 export된 모든 변수와 함수, C++ 클래스가 
      프로세스의 주소 공간에 매핑된다.

      암시적 로드타임 링크시 어떻게 프로세스 주소 공간에 매핑되고,
      프로세스에서 DLL 내 심볼에 접근하는지에 대한 과정은 위 3-2. 실행 모듈 작성 챕터에 자세히 설명되어 있다.

      암시적 로드타임 링크는 역시 편안함에 기인한다.

      프로세스의 주 쓰레드가 돌기 시작한 이후
      별도로 DLL을 로드/해제할 필요가 없고, 심볼 역시 별도의 시작 주소를 얻어오는 과정 없이 바로 사용할 수 있기 때문이다.

      Static library와 링크하는 방법이 똑같지만, 이왕 정리하는 김에 
      암시적 로드타임 링크를 하는 두 가지 방법에 대해 다시 한번 소개하겠다.

      1. 프로젝트 속성의 링커-입력에 .lib 추가

      프로젝트 속성 - 링커 - 입력에 DLL의 .lib 파일을 추가하는 것이다.

      2. comment 지시어 사용

      위 3-2 예제에 나왔듯이 comment 지시어를 사용하여, 암시적 로드타임 링크를 시킬 수 있다.

      1. // 암시적 로드타임 링킹
      2. #pragma comment(lib, "DLLTest.lib")

      개인적으로는 2번 방법을 선호하는 편이다.

      위에서도 썼듯이, 프로젝트 속성에서 제어하는 방법은
      공동 작업자가 꼼꼼히 속성을 살피지 않으면 지나치기 일수라,
      코드에서 명백하게 링크하는 것이 경험상 훨씬 더 직관적인 것 같다.


      5. Explicit Runtime Link

      암시적 로드타임 링크가 프로세스가 시작되는 과정에서 
      암묵적으로 로더가 DLL을 프로세스의 주소 공간에 로드/매핑 시켜주는 것이라면,

      명시적 런타임 링크는 애플리케이션이 수행 중인 상황에서 필요한 심볼을 명시적으로 링크하는 방법을 말한다.

      다르게 표현하면, 애플리케이션이 수행되고 있는 상황에서 
      특정 쓰레드가 DLL 내에 포함되어 있는 함수를 호출하기로 결정한 경우
      프로세스의 주소 공간에 필요한 DLL 파일을 명시적으로 로드하여
      DLL 내에 포함되어 있는 함수의 가상 메모리 주소를 획득한 후, 이 값을 이용하여 함수를 호출하는 것을 말한다.

      이 방식의 매력은 모든 과정들이 애플리케이션이 수행 중인 상황에서 이루어진다는 것이다.

      또한, 암시적 로드타임 링크는 프로세스 시작 과정에서 DLL 로드/매핑까지 함께 이루어지기에,
      많은 수의 DLL을 링크할 경우 프로세스 구동 자체가 오래 걸릴 수 있는 문제가 있으나.
      명시적 런타임 링크의 경우 이 비용을 분산시킬 수 있다.

      그리고, DLL 사용 후 더 이상 필요치 않을 경우 
      가상 메모리에서 해제시킬수도 있기에 메모리도 효율적으로 관리할 수 있다.

      또한, 암시적 로드타임 링크와 다르게 명시적 런타임 링크시에는 .lib 파일에 사용되지 않는다.


      이제 명시적 런타임 링크를 사용하는 방법에 대해 본격적으로 설명하겠다.

      명시적 런타임 링크에 사용되는 세 가지 함수는 아래와 같다.
      • LoadLibrary
      • GetProcAddress
      • FreeLibrary

      1. LoadLibrary

      프로세스 내의 쓰레드는 LoadLibrary 함수를 통해 프로세스의 주소 공간에 DLL을 런타임에 매핑할 수 있다.

      1. HMODULE LoadLibrary(PCTSTR pszDLLPathName);
      2.  
      3. // 또는...
      4.  
      5. HMODULE LoadLibraryEx(
      6.     PCTSTR pszDLLPathName,
      7.     HANDLE hFile,    // reserved. 반드시 NULL을 넘겨야 한다.
      8.     DWORD flags);   // 0을 넘기거나 다양한 flag를 OR해서 넘기면 된다.

      이 함수들은 사용자의 시스템에서 파일 이미지를 검색하고 (검색 순서는 3-2. 로더의 검색 순서와 동일하다)
      함수를 호출한 프로세스의 주소 공간에 DLL 파일 이미지를 매핑하려고 시도한다.

      함수 호출이 성공하면, 파일 이미지가 매핑된 가상 메모리 주소를 나타내는 HMODULE 값을 반환한다.
      (HMODULE은 HINSTANCE와 완전히 동일한 의미이며, 혼용 또한 가능하다)

      함수 호출이 실패하면? NULL을 반환한다.

      LoadLibraryEx는 flags 변수를 통해 다양한 옵션을 줄 수 있다.
      자세한 설명은 생략한다 ㅋ 가 아니라, MSDN 페이지에서 확인하기 바란다.

      다만, 이 둘을 혼용하는 것은 원하지 않은 결과를 초래할 수 있으므로, 추천하지 않는다.

      디스크 상에 동일한 DLL을 이용하여 로드하였다 하여도,
      LoadLibrary와 LoadLibraryEx 함수가 반환하는 매핑 주소 값은 LoadLibraryEx의 flag에 따라 달라질 수 있다.

      LoadLibrary(Ex) 함수를 사용하게 되면, 프로세스별로 DLL에 대한 usage count를 증가시킨다.

      예를 들어, DLL을 로드하기 위해 LoadLibrary 함수를 최초로 수행한 경우
      시스템은 DLL 파일 이미지를 프로세스의 주소 공간에 매핑하고, DLL의 usage count를 1로 설정한다.

      만일 동일 프로세스 내의 쓰레드가 동일한 DLL 파일 이미지에 대해 LoadLibrary를 또다시 호출하게 되면,
      시스템은 DLL 파일 이미지를 프로세스의 주소 공간에 두 번 매핑하지 않고, DLL의 usage count만 2로 증가시킨다.

      이후 설명할 FreeLibrary를 호출하면, 해당 프로세스의 usage count가 감소되며,
      usage count가 0 이 되면, 시스템은 프로세스의 주소 공간으로부터 DLL 파일 이미지를 매핑 해제한다.

      위에서도 썼지만, 시스템은 DLL의 usage count를 프로세스별로 유지한다.

      A 프로세스에서와 B 프로세스가 XXX.dll을 로드하게 되면,
      A 프로세스 / B 프로세스 모두 XXX.dll의 usage count는 1로 설정된다.

      이후 B 프로세스가 FreeLibrary를 호출하여, usage count를 0으로 감소시키면,
      B 프로세스의 주소 공간으로부터 DLL 파일 이미지를 매핑 해제시키겠지만
      A 프로세스는 여전히 usage count가 1이며, 프로세스 주소 공간에 DLL이 매핑되어 있다.

      2. GetProcAddress

      쓰레드가 DLL 모듈을 명시적으로 로드하였다면, 
      이제 GetProcAddress를 통해 export 된 심볼에 대한 시작 주소를 얻어와야 한다.

      FARPROC GetProcAddress(HMODULE hInstDll, PCSTR pszSymbolName);

      첫번째 인자인 hInstDll은 LoadLibrary(Ex) 반환 값을 넘겨주면 된다.

      두번째 인자인 pszSymbolName이 ANSI 문자열임에 주목하라.
      이는 컴파일러/링커가 심볼의 이름을 DLL의 export section에 기록할 때, 항상 ANSI 문자열로 기록하기 때문이다.

      두번째 인자로 문자열 대신 숫자를 넘기는 방법도 있으나,
      이 방법은 M$가 deprecate 시킬 것이니 그만 사용할 것을 강조하고 있으니, 패스하겠다.

      GetProcAddress로 함수 심볼을 얻어오려 할 때엔,
      GetProcAddress로 얻은 주소를 적절한 원형의 함수 포인터로 형변환을 해 주어야 한다.

      즉, DLL 내의 함수가 다음과 같은 원형이라면

      bool GetParrity(int type);

      다음과 같이 함수 포인터 타입을 만들어 놓고,

      typedef bool (*pfnGetParrity)(int);

      GetProcAddress의 반환값을 함수 포인터 타입으로 형변환을 해 주어야 하는 것이다.

      1. pfnGetParrity fnGetParrity = (pfnGetParrity)GetProcAddress(hInstDll, "fnGetParrity");
      2.  
      3. bool result = fnGetParrity(1);

      3. FreeLibrary

      프로세스 내의 쓰레드에서 더 이상 DLL 파일 내의 심볼을 사용할 필요가 없게 되면,
      FreeLibrary 함수를 호출하여 프로세스의 주소 공간으로부터 DLL 파일을 명시적으로 unload 할 수 있다.

      BOOL FreeLibrary(HMODULE hInstDll);

      이 함수의 인자인 hInstDll은 LoadLibrary(Ex)의 반환값을 넘겨주면 된다.

      위 LoadLibrary 설명에서도 썼듯이, FreeLibrary는 프로세스의 DLL usage count를 감소시킨다.
      usage count가 0 이 되면, 시스템은 프로세스의 주소 공간으로부터 DLL 파일 이미지를 매핑 해제한다.

      4. 정리 예제

      마지막으로 지금까지 알아본 LoadLibrary, GetProcAddress, FreeLibrary 함수들을 사용하는 간단한 예제를 첨부하겠다.

      1. #include <Windows.h>
      2. // DLL 헤더 인클루드
      3. #include "DLLTest.h"
      4.  
      5. // fnDLL 함수 포인터 타입데푸
      6. typedef int (*pfnDLLTest)(void);
      7.  
      8. int _tmain(int argc, _TCHAR* argv[])
      9. {
      10.         ///////////////////////////////////////////////////////////////////
      11.         // DLLTest.dll을 명시적으로 로드한다.
      12.         // 프로세스의 DLL usage count 증가
      13.         HMODULE hModule = LoadLibrary(_T("DLLTest.dll"));
      14.         if (nullptr == hModule)
      15.         {
      16.                 return -1;
      17.         }
      18.  
      19.         ///////////////////////////////////////////////////////////////////
      20.         // DLL의 변수 심볼의 주소 얻어오기
      21.         // GetProcAddress의 두번째 인자는 ANSI 스트링만 넘길 수 있다.
      22.         // 이는 export section에 심볼 이름이 ANSI 스트링으로 저장되기 때문이다.
      23.         int* pDllNum = (int*)GetProcAddress(hModule, "nDLLTest");
      24.         if (nullptr == pDllNum)
      25.         {
      26.                 return -1;
      27.         }
      28.         // DLL 변수 참조
      29.         int numInDLL = *pDllNum;
      30.  
      31.         ///////////////////////////////////////////////////////////////////
      32.         // DLL의 함수 심볼의 주소 얻어오기
      33.         pfnDLLTest pfnDLLTestFunc = (pfnDLLTest)GetProcAddress(hModule, "fnDLLTest");
      34.         if (nullptr == pfnDLLTestFunc)
      35.         {
      36.                 return -1;
      37.         }
      38.  
      39.         // DLL 함수 호출
      40.         int dllFuncNum = pfnDLLTestFunc();
      41.  
      42.         ///////////////////////////////////////////////////////////////////
      43.         // DLLTest.dll을 명시적으로 해제한다
      44.         // 프로세스의 DLL usage count 감소
      45.         // DLL의 reference count가 0이면, 해당 프로세스의 주소 공간에서 영역 해제
      46.         FreeLibrary(hModule);
      47.        
      48.         return 0;
      49. }


      '게임개발공부 > 무작정퍼오기' 카테고리의 다른 글

      volatile (퍼온글)  (0) 2013.12.21
      extern "C" 이건 뭔가? (퍼온글)  (0) 2013.12.21
      컨테이너 종류 복습  (0) 2013.12.21
      텍스처 아틀라스  (0) 2013.12.20
      맴버함수를 쓰레드로 쓰는법  (0) 2013.11.27
      Posted by JJOREG

      시퀸스(Sequence) 컨테이너----------------------------------------------------------------------------------------------------------------------


      시퀀스 컨테이너는 삽입된 요소의 원래 순서를 유지합니다. 이로 인해 여러분은 컨테이너의 어느 곳에 요소를 삽입할 것인지 지정할 수 있습니다.

      deque (양방향 큐) 컨테이너는, 컨테이너의 시작과 끝에 빠르게 삽입 및 삭제를 할 수 있게 합니다. 또한 어떤 요소건 신속하게 임의 액세스를 할 수 있습니다.

      list 컨테이너는, 컨테이너의 어느 위치건 빠르게 삽입 및 삭제를 할 수 있게 하지만, 컨테이너의 요소에 임의 액세스를 할 수 없습니다.

      vector 컨테이너는, 배열처럼 동작하지만, 필요하면 자동으로 증가합니다.

      시퀀스 컨테이너에 대한 더 자세한 내용은 다음 표를 참고하십시오.


      연관(또는 결합형)(associative) 연관 컨테이너------------------------------------------------------------------------------------------------------


      결합형 컨테이너를 구분짓는 특징은, 오름차순 정렬과 같이, 미리 정의된 순서대로 요소가 삽입되는 것입니다.

      결합형 컨테이너는 다음 두 개의 하위 집합으로 그룹화 할 수 있습니다: map 과 set. map은 때때로 dictionary라고 불리는데, 키/값 쌍으로 구성됩니다. 키는 시퀀스를 정렬하는 데 사용되고, 값은 어떤 식으로든 해당 키와 연관되어 있습니다. 예를 들면, map이 텍스트의 모든 고유 단어를 나타내는 키와, 해당 단어가 텍스트에 나타나는 횟수를 표현하는 값을 포함할 수 있습니다. set은 단순히 고유 요소의 오름차순 컨테이너입니다.

      map 및 set 둘 다, 컨테이너에 삽입될 키 또는 요소의 인스턴스를 하나만 허용합니다. 요소의 다중 인스턴스가 필요한 경우 multimap 또는 multiset를 사용하십시오.

      map과 set 둘 다 양방향 반복기를 지원합니다. 반복기에 대한 더 자세한 내용은 반복기를 참고하십시오.

      STL 표준의 공식적인 부분은 아니지만, hash_map 및 hash_set은 검색 시간을 향상시키는 데 널리 사용됩니다. 이 컨테이너는 요소를 해시 테이블로 저장하며, 각 테이블은 요소들의 양방향 연결 목록을 포함합니다. 검색 시간을 가장 빠르게 하려면, 여러분의 요소에 대한 해시 알고리즘이 균등하게 분산된 해시 값을 반환해야 합니다.

      결합형 컨테이너에 대한 더 자세한 내용은, 다음 표를 참고하십시오.

      '게임개발공부 > 무작정퍼오기' 카테고리의 다른 글

      extern "C" 이건 뭔가? (퍼온글)  (0) 2013.12.21
      dll의 기본. (퍼온글)  (0) 2013.12.21
      텍스처 아틀라스  (0) 2013.12.20
      맴버함수를 쓰레드로 쓰는법  (0) 2013.11.27
      assert 사용법  (0) 2013.11.27
      Posted by JJOREG

      Texture Atlases (텍스쳐 아틀라스)

      A texture atlas image from Penguflip


      이전의 모든 예제에서 임베디드된 비트맵을 통해 바로 텍스쳐로 만들었더라도 , 실제 애플리케이션에서는 그래선 안됩니다. 그 이유는..

      Even if all the previous samples created textures directly from embedded bitmaps, a real application should not do that. Here's why.


          - 스탈링은  한번의 GPU 호출로 가능한한 많은  Quad / Image 렌더링을 일괄처리합니다.  이러한 배칭 프로세싱(일괄처리)은 텍스쳐가 바뀔때 마다 중단됩니다.
             Starling batches the rendered Quads/Images to draw as many as possible with only one GPU call. Batch processing has to be stopped whenever the texture chang

          - Stage3D 에서 사용되는 텍스쳐는,  높이와 너비가 각각 2제곱이어야 합니다.
           스탈링은 이 제한을 숨기고 있긴 하지만 그럼에도 불구하고 이 규칙을 따르지 않는다면 당신은 많은 메모리 사용하게 될것입니다.
            To use a texture in Stage3D, its height and width must each be a power of 2.
            Starling hides this limitation from you, but you will nevertheless use more memory if you do not follow that rule.



      텍스쳐 아틀라스 를 사용을 통해, 텍스쳐 전환 과 2제곱제한 둘 모두 방지할 수 있습니다.

      By using a texture atlas, you avoid both the texture switches and the power-of-two limitation.


      모든 텍스쳐들은 큰 하나의 슈퍼 텍스쳐에 포함되어 있으며, 이 텍스쳐의 적절한 부분을 스탈링이 담당하여 표시하게 됩니다.

      (몇몇 프레임워크에서는 이 기능을 스프라이트시트 라고 부릅니다.)

      All textures are within one big “super-texture”, and Starling takes care that the correct part of this texture is displayed.

      (Some other frameworks call this feature “Sprite Sheets”.)


      텍스쳐 아틀라스 의 장점에 대한 더 많은 정보를 원한다면 TexturePacker 의 저자 Andreas Löw 가 제작한

      훌륭하고 (그리고 재미있는)  이  video 를 보기를 권장합니다!

      To find out more about the advantages of texture atlases, I recommend you have look at this great (and funny!)

       video created by Andreas Löw, the author of TexturePacker.


      How to use them (사용방법)


      스탈링에서, 텍스쳐 아틀라스 는 매우 사용하기 쉽고, 그래서 당신은 분명히 이 기능을 활용할 것입니다.

      In Starling, a texture atlas is very easy to use, so you should definitely take advantage of this feature.


      각 서브 텍스쳐들의 위치는 아래처럼 XML 파일에 정의 되어 있습니다.

      The positions of each sub-texture are defined in an XML file like this one:


      <TextureAtlas imagePath="atlas.png">
       <SubTexture name="moon" x="0" y="0" width="30" height="30"/>;
       <SubTexture name="jupiter" x="30" y="0" width="65" height="78"/>;
       ...
      </TextureAtlas>;

      하지만 수동으로 텍스쳐 아틀라스를 만들 필요는 없습니다. 아래의 툴중에 하나를 사용할 수 있습니다.

      You don't have to create a texture atlas manually, though. You can use one of the following tools:


          - TexturePacker 는 GUI 와 커맨드라인이 사용가능한 훌륭한 소프트웨어중 하나입니다.( 상업적으료 이용 가능하고 무료)

            TexturePacker is a great piece of Software that can be used with a GUI or via the command line (available in a free and commercial version).

          - 스탈링의 형제 프레임워크인 Sparrow 에 포함된 심플한 아틀라스 생성기. Sparrow 를 다운로드 하면 sparrow/util/atlas_generator 폴더에 위치합니다.
            같은 폴더에 있는 README 파일에서 인스톨방법 및 사용법을 알 수 있습니다.
            
              Starling's sister framework Sparrow contains a simple atlas generator. Download Sparrow and locate it in the folder sparrow/util/atlas_generator.
              The README file in the same folder shows you how to install and use it.



      Andreas Löw 는 스탈링 사용자가 그의 툴중 하나를 구입할 때 우리와 수익의 일부를 공유하며 우리를 지원합니다. 

      만약 TexturePacker 를 구입하기로 결정했다면, 다음 링크를 이용하면 좋을거에요 : Purchase TexturePacker

      컴퓨터의 부하를 줄이기 위해 하나의 큰 텍스쳐에 작은 텍스쳐를 모두 넣고 원하는 UV 좌표를 사용해 보도록 하자.


       SpaceRocksAtlas.png 첨부이미지 미리보기


      자 위와같이 한 장의 시트지를 스프라이트 시트(Sprite Sheet) 또느 텍스쳐 아틀라스(Texture Atlas)라고 한다.

      그리고 각 이미지 사이에 최소한 2픽셀은 간격이 있어야 한다.

      그러면 아래와 같이 .plist 파일에 이미지 정보를 넣도록 한다. 

      먼저 위의 파일을 드래그하여 리소스에 넣어준 다음 아래와 같이 파일을 만든다.

      아래와 같이 선택하고

      파일이름도 지정하면

      빈 화면이 나오고 이제 밉맵핑 데이타를 기록한다.

      아이템을 추가하여

      제목과 종류를 넣고

      이제부터는 +버튼을 눌러

      계속 만들어갈 수 있다.

      다음과 같이 아틀라스 내의 좌표와 사이즈, 이름 등 6개씩 들어있는 아이템을


      총 20개를 만들어 주어야 하는데,


       SpaceRocksAtlas.plist


      파일을 다운로드 받아 폴더에 복사하시면 된다.

      새로 프로젝트 열면 내용이 다 들어온 것을 알 수 있다.


      이렇게 준비가 되면 프로그램에 들어가 보도록 하겠다.

      오늘은 여기까지 입니다. (별 내용도 아닌데 저는 엄청 시간걸리고 고생했습니다. ㅠㅠ)



      '게임개발공부 > 무작정퍼오기' 카테고리의 다른 글

      dll의 기본. (퍼온글)  (0) 2013.12.21
      컨테이너 종류 복습  (0) 2013.12.21
      맴버함수를 쓰레드로 쓰는법  (0) 2013.11.27
      assert 사용법  (0) 2013.11.27
      <소켓 서버> sockaddr  (0) 2013.11.25
      Posted by JJOREG

      오늘은 간단한 C++ 프로그래밍 기법에 대해 하나 써볼까 합니다.

       

      간단한 기교를 부려볼 겸 클래스의 멤버함수를 쓰레드 함수로 작성하는 방법을 배워 보도록 하겠습니다.

      (간단히 쓰레드 사용법도 배우고 일석이조! 야호! )

       

      아직까진 그런 적은 없지만, 쓰레드를 돌릴 때 간간히 멤버함수를 쓰레드 함수로 제작하고플 때가 있더군요.

      혹, 그런 분들을 위해 알려 드리겠습니다.

       

       class TestClass {

       

           INT B;

       

           VOID TestFunction( INT A ) { B = A };                       //이 함수를 쓰레드로 돌리고자 합니다.

           VOID Start();                                                          //이 함수에서 TestFunction을 호출하지요.

       

      };

       

       Start() 멤버 함수에서 TestFunction()을 쓰레드로 돌리고자 한다고 해보죠.

       

       VOID TestClass ::Start() {

       

             HANDLE hThread = ( HANDLE ) __beginthread( NULL, 0, &TestFunction, NULL, NULL, NULL );

             // ........ 무엇가의 작업을 하고.

       

             WaitForSingleObject( hThread, INFINITE );

             CloseHandle( hThread );

      }

        

      물론 안됩니다. 쓰레드로 돌아갈  함수는 정적으로 선언되어야 하며 정적 함수여야 하죠. 따라서,

       
       class TestClass {

       

           static VOID WINAPI TestFunction( INT A );                    // 이제 원래 의도대로 이 함수를 정적 함수로 제작합니다.

           VOID Start();

       

      };

       

      역시 물론 안됩니다. 직접 닥쳐보시면 알겠지만, 우리가 돌리고자 했던 TestFunction 함수에서는 B라는 비정적 멤버를 참조하고 있습니다.

       

      결국은 다른 방법을 찾아야 합니다.

       

       class TestClass {

       

           VOID TestFunction( INT A );                                             //다시 원래대로 돌리고

           static VOID WINAPI TestFunctionThread( LPVOID );            //대신 쓰레드가 돌아갈 정적 함수를 만듭니다.

           VOID Start();

       

      };

       

      VOID Start() {

       

             HANDLE hThread = ( HANDLE ) __beginthread( NULL, 0, &TestFunctionThread, this, NULL, NULL );

             // ........ 무엇가의 작업을 하고.

       

             WaitForSingleObject( hThread, INFINITE );

             CloseHandle( hThread );

       

      }

       

      VOID WINAPI TestFunctionThread( LPVOID p ) {

            (  (TestClass* ) p )->TestFunction( 3 );

      }

       

       

      자, 위 예를 통해 해결했습니다. 포인터. 즉 동적 호출을 통해서 정적 멤버 함수를 호출하였습니다.

       

      TestFunction의 인자 값도 동적으로 주고 싶다면, 

       

       struct Arg {

            TestClass* p;

             INT A;

      };

       

      VOID Start() {

       

              Arg arg = { this, 3 };

       

             HANDLE hThread = ( HANDLE ) __beginthread( NULL, 0, &TestFunctionThread, &arg, NULL, NULL );

             // ........ 무엇가의 작업을 하고.

       

             WaitForSingleObject( hThread, INFINITE );

             CloseHandle( hThread );

       

      }

       

      VOID WINAPI TestFunctionThread( LPVOID p ) {

            Arg* pArg = ( Arg* ) p;

            pArg->p->TestFunction( p->A );     

      }

       


      '게임개발공부 > 무작정퍼오기' 카테고리의 다른 글

      컨테이너 종류 복습  (0) 2013.12.21
      텍스처 아틀라스  (0) 2013.12.20
      assert 사용법  (0) 2013.11.27
      <소켓 서버> sockaddr  (0) 2013.11.25
      무작정 쳐보자 <CSTREAM>  (0) 2013.11.24
      Posted by JJOREG
      이전버튼 1 2 이전버튼