고수들이 절대 가르쳐 주지 않는 C/C++ 프로그래밍 팁 #3 - C 에서 C++ 코드 호출

Posted at 2007.06.28 15:00 // in S/W개발/고절가주팁 // by 김윤수


고절가주팁 세번째 시간입니다. 세번째 팁을 될 수 있으면 빨리 쓰려고 했는데, 어느새 일주일이 지나버렸네요. 이번에는 지난글에서 밝힌 것처럼 C 코드에서 C++를 호출하는 방법에 대해 다뤄보도록 하겠습니다.

C 코드에서 C++ 코드 또는 라이브러리를 호출하는 방법도 결국은 C++쪽에서 extern "C" 호출 규약을 통해 C 코드가 C++쪽 코드를 볼 수 있도록 맞추어 주는 식으로 하게 됩니다. 어떻게 보면 똑똑한 C++가 약간 덜 똑똑한 C에게 맞춰주는 식이라고 할까요 ? 똑똑한 C++가 덜 똑똑한 C보고 니가 나한테 맞춰라하는 식이 아니라요.

그런데 C 코드에서 직접 C++ 코드를 호출할 수 있도록 만들어 주는 것보다 더 자주 발생하는 경우가 C++ 코드에서 어떤 C 라이브러리 API를 쓰는데 그 API 가 callback을 필요로 하는 경우라고 할 수 있습니다. 그런 대표적인 예가 pthread 라이브러리라고 할 수 있습니다.

먼저  pthread 라이브러리의 가장 기본적인 API인 pthread_create()를 살펴 보겠습니다.

       int pthread_create(pthread_t *restrict thread,
              const pthread_attr_t *restrict attr,
              void *(*start_routine)(void*), void *restrict arg);

세번째 인자 start_routine 이라는 게 생성된 thread가 실행해야할 로직을 나타내고, 네번째 인자가 그   start_routine에 전달되어야할 데이터를 뜻합니다. start_routine을 타입을 보면  void *를 입력인자로 받아들이고, void * 를 리턴하는 것을 확인할 수 있습니다. pthread_create의 네번째 인자는  start_routine 의 입력인자로 제공되는 것이죠. 이해를 돕기 위해  pthread 라이브러리를 쓰는 간단한 C 코드를 작성해서 실행시켜 볼까요 ?

/* thread.c - 일반 C 코드 */
#include <stdio.h>

#include <pthread.h>               /* pthread 라이브러리를 쓰려면 include 해야 함 */
#include <unistd.h>                /* sleep()을 쓰려면 include 해야 함 */

/* thread 가 실행시킬 로직입니다. 이 예에서는 그냥 단순히 1 ~ 20까지 셉니다. */
void*
start_routine(void* data)
{
  int i = 0;

  printf("starts to run\n");
  while (++i < 20) {
    printf("%d\n", i);
    sleep(1);
  }

  if (data != NULL)
    free(data);

  return NULL;
}

int
main(void)
{
  pthread_t thrd;
  int nr = 0;
  void* data;

  /* 생성된 thread가 start_routine 을 실행시키게 합니다 */
  nr = pthread_create(&thrd, NULL, start_routine, NULL);
  /* 이 시점에서 프로그램은 두 개의 thread로 나뉘어 수행됩니다. 이 코드를 수행하는 것은
     thread를 생성한 main thread 입니다. main thread 는  생성된 thread 가 처리를
     완료할 때
까지 기다려 줘야 합니다. 그렇지 않으면 생성된 thread가 처리를 끝내기도 전에
     프로그램
이 종료되어 버립니다. pthread 라이브러리를 처음 사용해 본 사람들이 잘
     저지르는
실수가 pthread_join()을 호출하지 않거나 main thread가 생성된 thread보다
     먼저 종료되는 걸 막지 않는 것입니다 */

  nr = pthread_join(thrd, &data);
  if (data != NULL)
    free(data);

  return 0;
}

$ gcc -o thread thread.c -lpthread
$ ./thread
starts to run
1
2
......
20


일반 C 코드이니 C 컴파일러로 컴파일하면 아무 문제가 없을 것입니다. 그럼 이제 우리 본론으로 돌아와서 위에 pthread 라이브러리가 호출해 줄 로직을 C++ 클래스의 메소드로 작성하려면 어떻게 해야할까요 ? 그냥 메소드를 가리키는 포인터를 넘겨주면 될까요 ? 예를 들어, 다음과 같이 뭔가 유용한 C++ 클래스가 있다고 쳐보겠습니다(실제로 하는 일은 없어 보이지만...).

// c2cpp.h
#ifndef _C2CPP_H                      // 고절가주팁 #1
#define _C2CPP_H

#include <iostream>
#include <unistd.h>

using namespace std;

class UsefulCppClass {
public:
  UsefulCppClass(string name): _name(name) {
  }

  void DoSomeAction() {
    cout << _name << ": I'm doing some useful job. step #1" << endl;
    sleep(1);
    cout << _name << ": I'm doing some useful job. step #2" << endl;
    sleep(1);
    cout << _name << ": I'm doing some useful job. step #3" << endl;
    sleep(1);
    cout << _name << ": I'm doing some useful job. step #4" << endl;
    sleep(1);
    cout << _name << ": I'm doing some useful job. step #5" << endl;
    sleep(1);
    cout << _name << ": Exiting..." << endl;
    sleep(1);
  }

private:
  string _name;
};

#endif // _C2CPP_H

UsefulCppClass의 DoSomeAction()이 시간이 오래 걸리는 작업이라 독립된 thread 로 실행시키고 싶은 경우를 생각해 보시겠습니다. 어떻게 하면 pthread 라이브러리에서 DoSomeAction()을 호출할 수 있게 만들 수 있을까요 ? 그냥 다음과 같이 하면 될까요 ?

pthread_t thrd;
pthread_create(&thrd, NULL, &UsefulCppClass::DoSomeAction, NULL);

제 컴파일러로 컴파일했더니 다음과 같은 에러가 발생하네요.

$ g++ -o c2cpp c2cpp.cpp -lpthread
c2cpp.cpp: In function `int main()':
c2cpp.cpp:32: error: cannot convert `void (UsefulCppClass::*)()' to `void*(*)(void*)' for argument `3' to `int pthread_create(__pthread_t**, __pthread_attr_t* const*, void*(*)(void*), void*)'

왜 컴파일 에러가 발생할까요 ? 에러 메시지를 자세히 보면 아시겠지만 위 코드에서 넘겨주는  메소드의 타입과 pthread_create()에서 요구하는 start_routine의 타입이 틀려서이겠죠. pthread_create() 에서는 void* (*)(void*) 타입-void*를 입력인자로 받는 함수-을 요구하는데 void (UsefulCppClass::*)() 타입을 넘겨 줬기 때문입니다(C++ 이야기 열다섯번째: 함수 객체 #3, 멤버 함수 어댑터 참조). 그리고, 메소드를 호출하려면 객체 인스턴스가 하나 있어야 하겠지만 pthread 라이브러리는 객체라는 개념을 전혀 모르는 녀석이니 어떤 객체 인스턴스를 대상으로 메소드를 호출해야 하는지를 알 턱이 없습니다. C 라이브러리인 pthread 라이브러리가 C++ 의 메소드를 어떻게 호출해야 하는지를 알 수 없는 건 당연지사입니다.

그러니 pthread 라이브러리에 메소드를 그냥 callback 으로 넘겨줄 수는 없는 노릇입니다. 대신 pthread 가 요구하는 거랑 UsefulCppClass의 인터페이스랑 어떻게든 맞추어주는 뭔가가 필요한 것이죠. 일종의 wrapper 내지는 adapter 같은 것이 필요하다는 그런 말이지요. 일단 객체 인스턴스를 어떻게 넘길 수 있을지를 생각해 볼까요 ? pthread_create()의 네 번째 인자가 start_routine()의 입력인자로 제공된다고 했던 것 기억하시죠 ? 이걸 이용하면 일단 객체 인스턴스는 start_routine()으로 넘길 수 있겠네요. 객체 인스턴스를 강제로 void * 형으로 형변환하면 넘길 수 있을 것입니다.

그 다음에는 어떻게 start_routine()이 UsefulCppClass의 메소드를 호출할 수 있게 할 것이냐 문제가 남아 있습니다. 일단 객체 인스턴스가 void * 로 넘어오므로 이걸 다시 원래대로 객체 인스턴스 타입으로 형변환한 후에 그 인스턴스에 대해 메소드를 호출한다면 우리가 원하는 소기의 목적이 달성될 수 있을 듯 하네요.

// C++ 코드
void* UsefulCppClassWrappingFunction(void* pInst)
{
  cout << "UsefulCppClassWrappingFunction() called" << endl;
  // UsefulCppClass 인스턴스로 형변환합니다.
  UsefulCppClass* pInstance = static_cast<UsefulCppClass*>(pInst);
  // 해당 객체에 대해 메소드를 호출합니다.
  pInstance->DoSomeAction();
}

int
main()
{
  pthread_t thrd;
  int nr;
  UsefulCppClass* pInst = new UsefulCppClass("01");
  void* pData;

  // pInst 를 넘겨주는 것에 주목하세요
  nr = pthread_create(&thrd, NULL, &UsefulCppClassWrappingFunction, pInst);
  pthread_join(thrd, &pData);
}

위 코드를 컴파일하고 실행했더니 아무런 문제 없이 잘 되네요.

$ g++ -o c2cpp c2cpp.cpp -lpthread
$ ./c2cpp.exe
UsefulCppClassWrappingFunction() called
01: I'm doing some useful job. step #1
01: I'm doing some useful job. step #2
01: I'm doing some useful job. step #3
01: I'm doing some useful job. step #4
01: I'm doing some useful job. step #5
01: Exiting...

C++ 안에서 컴파일된 UserfulCppClassWrappingFunction() 은 name mangling이 되긴 하지만 pthread_create()에게 넘어가는 건 name mangling된 심볼명이 넘어가는 것이 아니라 함수 주소가 넘어가는 것이므로 pthread 라이브러리에서 호출할 수가 있는 것입니다. 이런 방법을 보고 나니 다른 좋은 방법들도 떠 오르시지 않나요 ? 예를 들어 다음 방법은 어떤가요 ?

class UsefulCppClassWrapper {
public:
  // 클래스 메소드 선언
  static void* DoIt(void* pInst) {
    cout << "UsefulCppClassWrapper::DoIt() called" << endl;
    UsefulCppClass* pInstance = static_cast<UsefulCppClass*>(pInst);
    pInstance->DoSomeAction();

    return NULL;
  }
};

int
main()
{
  pthread_t thrd;
  int nr;
  UsefulCppClass* pInst = new UsefulCppClass("02");
  void* pData;

  // &UsefulCppClassWrapper::DoIt과 pInst 를 넘겨주는 것에 주목하세요
  nr = pthread_create(&thrd, NULL, &UsefulCppClassWrapper::DoIt, pInst);
  pthread_join(thrd, &pData);
}


UsefulCppClassWrapper::DoIt() 은 인스턴스 메소드가 아니라 클래스 메소드이기 때문에 일반 함수와 똑같이 '&' 연산자를 적용하면 함수 포인터를 얻을 수 있게 됩니다. 당연히 호출 대상이 되어야할 객체 인스턴스도 필요 없게 되구요.

이번에는 한 단계 더 나아가 진짜 C 코드에서 pthread 라이브러리를 통해 UsefulCppClass::DoSomeAction()을 호출하려면 어떻게 해야할까요 ? 위에서 정의해 봤던 UsefulCppClassWrappingFunction()을 C 코드에서 호출할 수 있도록 해주면 될 것 같습니다. C 코드에서 C++ 코드를 볼 수 있게 하려면 이 글의 서두에 밝힌 것처럼 C++ 쪽에서 C 쪽에 공개하고 싶은 함수에 대해 extern "C" 호출 규약을 쓰도록 하면 됩니다. 다음과 같이 말이죠.

// callable.h
#ifndef _CALLABLE_H                      //
고절가주팁 #1
#define _CALLABLE_H

#ifdef __cplusplus                       // 고절가주팁 #2
extern "C" {
#endif

void* UsefulCppClassWrappingFunction(void* pInst);

#ifdef __cplusplus
}
#endif
#endif

// callable.cpp
#include <iostream>
#include "c2cpp.h"

using namespace std;

extern "C" {

void* UsefulCppClassWrappingFunction(void* pInst)
{
  cout << "UsefulCppClassWrappingFunction() called" << endl;
  UsefulCppClass* pInstance = static_cast<UsefulCppClass*>(pInst);
  pInstance->DoSomeAction();

  return NULL;
}

}

위와 같이 정의해 놓고, C 코드에서는 다음과 같이 호출하면 되지 않을까요 ?

#include <stdio.h>
#include <pthread.h>
#include "callable.h"

int
main(void)
{
  pthread_t thrd;
  int nr;
  void* pInst;                // 어? UsefulCppClass 인스턴스를 어떻게 생성하지 ?

  nr = pthread_create(&thrd, NULL, &UsefulCppClassWrappingFunction, pInst);
  pthread_join(thrd, &pData);
}

Wrapping 함수를 extern "C"로 선언하고 나서 C 코드에서 그걸 호출해 보려고 했더니, UsefulCppClass 인스턴스를 생성할 방법이 없네요. 이를 어쩌죠 ? 정말 첩첩산중이네요. 그렇담 C++ 클래스 인스턴스를 생성해줄 수 있는 함수도 따로 만들어야 겠네요.

// callable.cpp 에 다음 추가. callable.h 에도 함수 선언 추가
extern "C" {

void* CreateUsefulCppClass(char* name)
{
    return new UsefulCppClass(name);
}

}

C 에서는 UsefulCppClass 라는 타입을 알 길이 없으니 리턴값을 void* 로 만들어 버립니다. 그리고 나서 나중에 UsefulCppClassWrappingFunction() 안에서 원래 타입으로 다시 변환해 주는 것이죠. 여기까지 하고 나면 C 코드에서는 다음과 같이 UsefulCppClass::DoSomeAction() 을 호출할 수 있을 것입니다.

#include <stdio.h>
#include <pthread.h>
#include "callable.h"

int
main(void)
{
  pthread_t thrd;
  int nr;
  void* pInst = CreateUsefulCppClass("01");

  nr = pthread_create(&thrd, NULL, &UsefulCppClassWrappingFunction, pInst);
  pthread_join(thrd, &pData);
}

그럼 어디 컴파일하고 실행시켜볼까요 ?

$ g++ -c callable.cpp
$ gcc -c calling.c
$ g++ -o calling calling.o callable.o -lpthread
$ ./calling
UsefulCppClassWrappingFunction() called
01: I'm doing some useful job. step #1
01: I'm doing some useful job. step #2
01: I'm doing some useful job. step #3
01: I'm doing some useful job. step #4
01: I'm doing some useful job. step #5
01: Exiting...

이제 완벽하게 C 코드에서 C++ 클래스를 쓸 수 있는 방법을 알아 봤습니다. 이번 글에 대한 A/S글로 이번 글에서 제시한 팁을 바탕으로 C++ 용 thread 클래스 라이브러리-Java Thread 라이브러리 참조-를 핵심 아이디어만 작성해 보도록 하겠습니다.

제 글이 유익하셨다면 오른쪽 버튼을 눌러 제 블로그를 구독하세요. ->
블로그를 구독하는 방법을 잘 모르시는 분은 2. RSS 활용을 클릭하세요.
RSS에 대해 잘 모르시는 분은 1. RSS란 무엇인가를 클릭하세요.

신고
  1. 요요

    2007.06.29 05:18 신고 [수정/삭제] [답글]

    보는 것만으로도 어려워 눈이 빙빙;;언젠간 프로그래밍을 기초라도 배워보고 싶은 마음이 점점 위축되고 있어요>_<ㅋ좋은 정보 올려주시는 멋진 블로거시네요!!^^

    • 김윤수

      2007.06.29 06:20 신고 [수정/삭제]

      아무래도 제가 쓰는 글들이 기본적으로 프로그래밍 지식을 가지고 있는 사람을 대상으로 하다 보니 프로그래밍 입문자에게는 어렵게 느껴질 수도 있을 것 같네요. 제 글보고 위축되지 마시구요. 입문서들을 보시면 좀 도움이 되리라고 생각합니다. 너무 급하게 생각하진 마시구요. 프로그래밍을 배우시려는 목적이 전문적으로 하려는 경우와 그냥 취미삼아 하는 경우가 약간 다를 수 있으니 그 목적에 따라 서로 다르게 접근하셔야 할 듯 합니다.

  2. 정성채

    2007.07.24 17:33 신고 [수정/삭제] [답글]

    어느 책에서도 보지 못했던 내용들을 짚어주시네요 ^^
    혹시 ACE(Adaptive Communication Environment)에 관련된 내용들을 다뤄주실 수 있나요?
    C++에서 네트워크 프로그래밍하는데 ACE를 많이 사용하는 것 같아서요.
    요즘 제가 공부하고 있지만, 역시 깊이 파고드는 능력이 모자라서 수박 겉핡기 식이네요 ㅠ.ㅠ
    너무 방대해서 다뤄야할 내용이 많을 듯 싶지만, 혹 기회가 된다면 부탁드릴께요 ~

    • 김윤수

      2007.07.24 17:47 신고 [수정/삭제]

      저도 솔직히 ACE 는 읽어보질 않아서 잘 모르겠네요. 제 경험상 C로 네트워크 프로그래밍 경험이 있어서 그걸 다룰 순 있을 것 같긴한데... ACE 는 언제 다룰 수 있을지 장담을 못하겠네요. ACE 가 C++ 네트워크 프로그래밍하는데 있어서는 거의 고전에 가까운 책이라는 건 아는데, 제가 쓰는 글들은 되도록이면 그냥 책에 있는 것들을 옮기는 것이 아니라 제 경험에서 우러나온 것들을 쓰려고 하고 있어서 그렇습니다. 제 글의 철학이랄까 하는 것이 "Be myself" 입니다. 저와 동일한 경험을 한 사람들은 없을테니까요. 그럼 저만의 차별화된 글들을 쓸 수 있지 않을까 하는 생각으로 접근하고 있습니다.

  3. 지호

    2007.08.05 13:10 신고 [수정/삭제] [답글]

    당연히 가정하고 들어가야되는거지만 사용하는 C컴파일러와 C++컴파일러가 호환가능한가를 미리 따져봐야 하지 않을까요? 예를들어 c++컴파일러는 vc를 사용하고 c컴파일러는 gcc를 사용하는데 경우에 따라 함수 호출 방식이나 데이터 크기, 정렬을 다른 방식으로 구현해서 호환이 안되면 이런 멋진 코드를 사용하지 못하고 눈물 흘리며 손을 오그릴 수 밖에 없으니까요.

    • 김윤수

      2010.05.12 18:44 신고 [수정/삭제]

      한 OS 안에서는 보통 컴파일러들이 C ABI는 지켜주기 때문에 실제 개발시에는 별 문제 없지 않을까 합니다. C++ ABI는 아직 서로 호환되지 않는 경우가 있는 것 같더군요.

댓글을 남겨주세요.