고수들이 절대 가르쳐 주지 않는 C/C++ 프로그래밍 팁 #2 - C/C++ Mixed Programming
C/C++ 섞어 프로그래밍하기에 대해 글을 써 봐야겠다고 생각하게 된 건 다음 글 때문이었습니다.
- C에서 C++로 작성한 라이브러리 쓰기
- 먼저 name mangling이라는것을 알아보았다. 하여튼 이넘때문에 C에서 C++에서 작성한 라이브러리를 사용하고 싶을때는 C++ 라이브러리를 작성할때 애초에 C에서 사용될수 있다는것을 염두해 두고 작성해야 한다는 말이다...
위 글을 읽고 났더니 옛날에 C/C++ 두 언어를 다 써서 개발을 해야 했던 기억이 나더군요. 그래서 제가 도움이 될만한 게 있겠다 싶어 이 글을 쓰기로 결심을 하게 됐습니다.
왜 C++ 와 C 가 함께 쓰기가 어려울까요 ? C++야 C에서 나왔으니까 당연히 두 언어는 함께 섞어 써도 아무런 문제가 없어야 되는거 아냐 ? 라고 생각하실지도 모르겠습니다. 그렇지만 C++와 C 를 섞어 쓰는 게 생각만큼 그리 쉽진 않습니다. 개선된 C로서 C++ 특징 중 하나가 함수 재정의(function override) 가 가능하다는 것이고, 이것을 처리하기 위해서는 함수 심볼명을 코드에 나온 그대로 생성하는 게 아니라 컴파일러가 조정할 필요가 있게 됩니다. 이렇게 컴파일러가 함수 심볼명을 재정의하는 것을 name mangling 이라고 하는데요, 이것 때문에 C와 C++를 섞어 쓰는게 쉽지 않습니다. 어떻게 name mangling 이루어 지는지 아래 코드를 통해 눈으로 확인해 보겠습니다.
다음 코드를 한 번 컴파일 해 보시겠어요 ? max 라는 함수를 재정의 하고 있습니다.
#include <iostream>
using namespace std;
double
max(double a, double b)
{
cout << "double max(double, double) called" << endl;
if (a > b)
return a;
else
return b;
}
int
max(int a, int b)
{
cout << "int max(int, int) called" << endl;
if (a > b)
return a;
else
return b;
}
int
main(void)
{
double dm = max(23.7, 35.1);
int im = max(10,5);
cout << "double max = " << dm << endl
<< "int max = " << im << endl;
}
$ g++ -o mixed mixed.cpp
실행해 본 결과는 다음과 같습니다.
$ ./mixed
double max(double, double) called
int max(int, int) called
double max = 35.1
int max = 10
실행한 결과가 위와 같이 나온다는 건 다 이해하실테구요. name mangling 이 어떤 식으로 이루어지는지 확인해 보기 위해 함수 심볼 테이블을 살펴 보겠습니다. 제가 이 글을 쓰면서 사용하고 있는 컴파일러는 g++(4.1.0 20060304) 이고, 실행파일에서 함수 심볼명을 보기 위해 nm 유틸리티를 활용했습니다.
$ g++ --version
g++ (GCC) 4.1.0 20060304 (Red Hat 4.1.0-3)
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ nm mixed
08049ac0 d _DYNAMIC
08049ba4 d _GLOBAL_OFFSET_TABLE_
080486ea t _GLOBAL__I__Z3maxdd
0804892c R _IO_stdin_used
w _Jv_RegisterClasses
0804875c T _Z3maxdd
08048716 T _Z3maxii
080486a4 t _Z41__static_initialization_and_destruction_0ii
......
제가 파랗게 표시한 부분에 주목해 보시기 바랍니다. 소스 코드에서는 max 라고 이름지어진 함수 두 개의 컴파일 후 심볼명은 _Z3maxdd, _Z3maxii와 같이 생성되어 있는 걸 확인하실 수 있습니다. 제가 추측하건데, Z 다음 숫자(3)는 함수 본래 이름 길이를 뜻하는 것 같고, 그 다음에 오는 dd, ii 는 함수 인자의 타입을 뜻하는 것 같습니다. 그러니까 double max(double, double) 에 대해서는 _Z3maxdd 라는 심볼명이 생성되고, int max(int, int)에 대해서는 _Z3maxii 라는 심볼명이 생성된 것으로 보입니다. 컴파일러 입장에서는 당연히 두 함수를 구분해야 하므로 이렇게 서로 다른 심볼명을 생성하는 건 당연지사입니다. C++ 에서는 함수 재정의만 지원되는 게 아니라 namespace 도 지원됩니다. test 라는 namespace 에 max 라는 함수를 정의하면 어떻게 될까요 ?
// 이전 내용은 동일
namespace test {
char
max(char c1, char c2)
{
cout << "int test::max(char, char) called" << endl;
if (c1 > c2)
return c1;
else
return c2;
}
};
using namespace test;
// main 함수는 다음과 같이 수정합니다.
int
main(void)
{
double dm = max(23.7, 35.1);
int im = max(10,5);
char cm = max('a', 'z');
cout << "double max = " << dm << endl
<< "int max = " << im << endl
<< "char max = '" << cm << "'" << endl;
}
새롭게 컴파일한 실행 파일의 심볼 테이블을 보면 다음과 같이 나오네요.
$ nm mixed | grep max
0804875a t _GLOBAL__I__Z3maxdd
08048820 T _Z3maxdd
080487da T _Z3maxii
08048786 T _ZN4test3maxEcc
앞의 코드를 컴파일했을 때는 나타나지 않던 _ZN4test3maxEcc 가 새로 생긴 걸 보니 test 라는 namespace 내의 max 함수인가 봅니다.
이상과 같이 함수 재정의나 네임스페이스 같은 것들을 지원하기 위해서 C++ 컴파일러는 name mangling 을 하게 됩니다. 그럼 어떻게 이런 문제를 피하면서 C 와 C++ 를 섞어서 프로그래밍할 수 있을까요 ?
지금까지 장황하게 얘기를 해서 그렇지 섞어 프로그래밍하기를 위한 방법은 알고 보면 그리 복잡하지 않습니다. name mangling 때문에 섞어 프로그래밍하기가 어려워진 것이므로 C++ 코드를 컴파일할 때 name mangling 을 하지 않도록 하면 되는 것이겠죠. 그렇게 되면 C 코드에서도 C++ 코드를 불러 쓸 수 있고, C++ 코드에서도 C 코드를 불러 쓸 수 있게 될 것입니다. 그럼 C/C++ 섞어 프로그래밍하기 문제는 C++ name mangling 을 어떻게 막느냐의 문제로 환원될 것입니다. 어떻게 name mangling을 막느냐 ? 간단합니다. C++ 코드 컴파일 시에 extern "C" 라는 호출 규약을 쓰도록 하면 됩니다. 자~ 다음 코드를 한 번 컴파일해 보신 후에 심볼 테이블을 살펴 보시겠어요 ?
#include <iostream>
using namespace std;
extern "C" int
max(int a, int b)
{
if (a > b)
return a;
else
return b;
}
int
main(void)
{
int im = max(100,30);
cout << "int max = " << im << endl;
}
$ g++ -o externc externc.cpp
$ nm externc | grep max
080486dc t _GLOBAL__I_max
08048674 T max
name mangling 이 일어나지 않은 걸 확인하실 수 있습니다. 이 지식을 바탕으로 실제 대규모 프로젝트에서는 어떻게 C/C++ 섞어 프로그래밍하기를 할 수 있는지 풀어가 보겠습니다.
그럼 첫번째로 기존에 이미 존재하는 C 로 작성된 라이브러리가 있는데, 그걸 C++ 에서 사용하고 싶은 경우 어떻게 해야할지 알아 보겠습니다.
솔직히 C++ 가 아무리 언어적으로 뛰어 나다고 하더라도 기존에 C 로 이미 구현되어 있는 것들에 대응하는 C++ 라이브러리가 나오기는 힘들 것입니다. 그러니 현실적으로 C++ 코드에서 C로 구현된 라이브러리를 쓰는 건 매우 일상적인 개발 현실이라고 할 수 있습니다. 예를 들어, 우리가 거의 매일 같이 쓰는 문자열 관련 C 라이브러리는 C 코드, C++ 코드 상관 없이 일상적으로 쓰이고 있죠.
아까 예제 코드가 다음과 같이 구성되어 있다고 상상해 보시죠.
/* max.h */
int max(int a, int b);
/* max.c - 이미 C 컴파일러로 컴파일된 기존 C 라이브러리라고 가정 */
int
max(int a, int b)
{
if (a > b)
return a;
else
return b;
}
/* cppmain.cpp - 기존 C 라이브러리를 쓰고자 하는 C++ 프로그램 */
#include <iostream>
#include "max.h"
using namespace std;
int
main(void)
{
int im = max(100,30);
cout << "int max = " << im << endl;
}
자~ 그럼 이 코드를 컴파일하고 링크시켜 볼까요 ?
$ gcc -c max.c # object 파일만 만듭니다
$ g++ -c cppmain.cpp # object 파일만 만듭니다
$ g++ -o cppmax cppmain.o max.o # 링크합니다.
그랬더니 다음과 같은 링크 에러가 발생하네요.
cppmain.o:cppmain.cpp:(.text+0x13a): undefined reference to `max(int, int)'
collect2: ld returned 1 exit status
당연하겠지요 ? cppmain.o 에서는 name mangling 된 max(g++ 에서는 _Z3maxii)를 찾는데, C에 의해 컴파일된 max.o 에는 그냥 max 만 정의되어 있을 것이기 때문입니다. 이 문제를 해결하는 게 extern "C" 였구요, 이걸 max.h 에 적용하면 문제가 해결됩니다.
/* max.h */
extern "C" int max(int a, int b);
근데, 보통은 이렇게 문제가 간단하질 않죠 ? 라이브러리 헤더 파일에 수많은 함수들이 정의되어 있는데, 그 함수 선언 하나 하나에 extern "C"를 붙일 생각을 하시니 갑갑하시죠. 그럴 경우 쓸 수 있는 방법이 물론 있습니다. 이런식으로 하면 됩니다.
/* max.h - max() 함수 말고도 상당히 많은 함수가 선언되어 있다고 가정해
보겠습니다 */
extern "C" {
int max(int a, int b);
...... /* 다른 함수 정의 */
}
이제 다 문제가 해결됐나요 ? 아니죠~ 한가지 문제가 더 있습니다. 말 그대로 우리가 목표로 하는 것이 C/C++ 섞어 프로그래밍하기 인데... C++ 쪽 문제를 해결하다 보니 C 쪽에서 문제가 발생합니다. 바로 extern "C" 라는 건 C++ 컴파일러만 이해할 수 있는 키워드입니다. C 컴파일러는 이해하지 못하기 때문에 max.h 를 컴파일할 수 없습니다. 어디 한 번 확인해 보시죠.
/* cmain.c */
#include <stdio.h>
#include "max.h"
int
main(void)
{
int im = max(100,30);
printf("int max = %d\n", im);
}
$ gcc -c cmain.c
In file included from cmain.c:2:
max.h:2: error: parse error before string constant
그럼 또 이 문제를 어떻게 해결해야 하나요 ? 음... 골치 아프군요. 골치 아프더라도 한 번 곰곰히 생각해 보겠습니다. C 컴파일러용과 C++ 컴파일러용 헤더 파일을 따로 만들까요 ? C 컴파일러는 max.h 를 include 하고, C++ 컴파일러는 max.hpp 를 include 하라고 하는 거죠. extern "C" 가 있고 없고 차이밖에 없는 데 그렇게 한다는 건 왠지 무식해 보이네요 그죠~? 그렇담 좀 더 우아한 해결책이 없을까요 ?
C 컴파일러에게는 extern "C" { } 이 안 보이게 하고, C++ 컴파일러에게만 보이게 하는 방법이 없을까요 ? 만약 헤더 파일을 컴파일하고 있는 컴파일러가 C 컴파일러인지 C++ 컴파일러인지 구분할 수 있다면 이런 방법이 가능하지 않을까요 ? 그러니까 다음과 같은 조건부 컴파일이 가능할 것 같은데 말이죠.
#if (C++ 컴파일러라면)
extern "C" {
#endif
/* 헤더 파일 본 내용 */
#if (C++ 컴파일러라면)
}
#endif
그럼 우리의 문제는 어떻게 하면 컴파일러 종류를 알아낼 수 있느냐 하는 문제로 귀착됐네요. 뭐, 가장 쉬운 방법은 컴파일을 하는 사람이 C++ 컴파일러를 뜻하는 매크로를 정의하는 것이겠죠. 그런데, 이 C/C++ 섞어 프로그래밍하기는 워낙 근본적인 문제이기 때문에 개발자가 일일이 매번 나름의 C++ 컴파일러 매크로를 정의할 필요가 없도록 모든 C++ 컴파일러는 __cplusplus 라는 매크로를 정의하도록 되어 있습니다. 그러니까 max.h 를 다음과 같이 수정하면 만사형통이라는 얘기입니다.
/* max.h - max() 함수 말고도 상당히 많은 함수가 선언되어 있다고 가정해
보겠습니다 */
#ifdef __cplusplus
extern "C" {
#endif
int max(int a, int b);
...... /* 다른 함수 정의 */
#ifdef __cplusplus
}
#endif
자~ 그렇다면 여기서 한 가지 얻을 수 있는 포인트는
"C 라이브러리를 작성하려거든 나중에 C++ 에서 활용될 가능성을 미리 염두해 두고, 헤더 파일 처음과 마지막에 extern "C" {} 가 조건부 컴파일되게 하시라"
라는 것입니다. 시스템에 설치되어 있는 C 라이브러리 헤더 파일들은 죄다 위와 같은 조건부 컴파일 구문이 들어가 있는 걸 확인할 수 있습니다. 여기에 한 술 더 떠서 g++ 헤더 파일 중 _ansi.h 에는 다음과 같이 정의되어 있네요.(이해를 돕기 위해 상당히 간소화시킨 것입니다)
#ifdef __cplusplus
# define _BEGIN_STD_C extern "C" {
# define _END_STD_C }
#else
# define _BEGIN_STD_C
# define _END_STD_C
#endif
위와 같이 정의하고 모든 헤더 파일의 처음과 끝에 _BEGIN_STD_C, _END_STD_C 와 같은 매크로를 붙이는 것이지요. 다음과 같이요.
/* max.h - max() 함수 말고도 상당히 많은 함수가 선언되어 있다고 가정해
보겠습니다 */
#include "_ansi.h"
_BEGIN_STD_C
int max(int a, int b);
...... /* 다른 함수 정의 */
_END_STD_C
이번 글에서는 주로 name mangling과 C++ 에서 C 코드를 호출하는 방법에 대해 주로 알아 봤는데요, 다음에는 거꾸로 C에서 C++ 를 호출하는 방법에 대해 알아보도록 하겠습니다. 고절가주팁이 쭉~ 갈 수 있도록 많은 관심 부탁드립니다. 여러분이 알고 있는 팁을 트랙백이나 댓글로 좀 알려 주시면 더할 나위 없이 좋겠네요.
블로그를 구독하는 방법을 잘 모르시는 분은 2. RSS 활용을 클릭하세요.
RSS에 대해 잘 모르시는 분은 1. RSS란 무엇인가를 클릭하세요.