고수들이 절대 가르쳐 주지 않는 C/C++ 프로그래밍 팁 #3 - C 에서 C++ 코드 호출
고절가주팁 세번째 시간입니다. 세번째 팁을 될 수 있으면 빨리 쓰려고 했는데, 어느새 일주일이 지나버렸네요. 이번에는 지난글에서 밝힌 것처럼 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란 무엇인가를 클릭하세요.