본문 바로가기
Engineering/C/C++

[펌] C와 C++ 함께 쓰기

by Humaneer 2009. 4. 10.

왜 C++ 와 C 가 함께 쓰기가 어려울까요 ? C++야 C에서 나왔으니까 당연히 두 언어는 함께 섞어 써도 아무런 문제가 없어야 되는거 아냐 ? 라고 생각하실지도 모르겠습니다. 그렇지만  C++와 C 를 섞어 쓰는 게 생각만큼 그리 쉽진 않습니다. 개선된 C로서 C++ 특징 중 하나가 함수 재정의(function override) 가 가능하다는 것이고, 이것을 처리하기 위해서는 함수 심볼명을 코드에 나온 그대로 생성하는 게 아니라 컴파일러가 조정할 필요가 있게 됩니다. 이렇게 컴파일러가 함수 심볼명을 재정의하는 것을 name mangling 이라고 하는데요, 이것 때문에 C와 C++를 섞어 쓰는게 쉽지 않습니다. 어떻게 name mangling 이루어 지는지 아래 코드를 통해 눈으로 확인해 보겠습니다.

Name Mangling

다음 코드를 한 번 컴파일 해 보시겠어요 ? max 라는 함수를 재정의 하고 있습니다.


실행해 본 결과는 다음과 같습니다.


 

$ 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 라는 함수를 정의하면 어떻게 될까요 ?





새롭게 컴파일한 실행 파일의 심볼 테이블을 보면 다음과 같이 나오네요.

$  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" 라는 호출 규약을 쓰도록 하면 됩니다. 자~ 다음 코드를 한 번 컴파일해 보신 후에 심볼 테이블을 살펴 보시겠어요 ?


 


$ 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 라이브러리는 C 코드, C++ 코드 상관 없이 일상적으로 쓰이고 있죠.

아까 예제 코드가 다음과 같이 구성되어 있다고 상상해 보시죠.


자~ 그럼 이 코드를 컴파일하고 링크시켜 볼까요 ?


$ 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 를 컴파일할 수 없습니다. 어디 한 번 확인해 보시죠.


 


$ 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++ 를 호출하는 방법에 대해 알아보도록 하겠습니다.


제가 마지막에 정리하길...

C 라이브러리를 작성하려거든 나중에 C++ 에서 활용될 가능성을 미리 염두해 두고, 헤더 파일 처음과 마지막에 extern "C" {} 가 조건부 컴파일되게 하시라

라고 했던 것 기억하시죠 ? 근데 솔직히 세상이 우리 생각대로만 돌아간다면 얼마나 좋겠습니까 ? 다들 제가 말한 팁을 알고 있다면 처음 작성할 때부터 C++ 고려해서 프로그래밍했겠지만, 실제로는 그렇지 않은 경우가 더 많기 마련입니다.

지금 우리 수중에 C++를 고려하지 않은 괜찮은 C 라이브러리가 있는데, 이걸 C++ 에서 쓰고 싶을 경우는 어떻게 해야할까요 ? 그 C 라이브러리의 헤더 파일을 편집해서 함수 선언에다가 일일이 extern "C"를 붙여댈까요 ? 그렇게라도 해서 그 C 라이브러리를 쓰시겠다면 말리진 않겠지만, 머리가 똑바로 박힌 사람이라면 한 열개쯤 extern "C" 를 붙이다가 이걸 어떻게 쉽게 해결할 방법이 없을까 하고 고민하기 시작할 겁니다. 이른바 창조적인 귀차니즘이 시작되는 거죠. 이런 창조적인 귀차니즘은 매우 바람직한 현상이니, 개발하는 동안에는 반복적인 작업을 얼마든지 귀찮아 하시기 바랍니다.

저번 글에서 extern "C" { ... } 이런식으로 { ... } 내용 전체에 대해 extern "C"를 붙이는 방법이 있다는 걸 소개해 드렸습니다. 이걸 한 번 더 응용하시면 됩니다. 다음 C 라이브러리가 C++ 를 고려하지 않고 다음과 같이 작성되어 있다고 치겠습니다.

/* max.h */
int max(int a, int b);
그럼 이걸 include 하는 C++ 소스 코드에서는 다음과 같이 하시면 됩니다.

/* cppmain.cpp */
#include <iostream>
extern "C" {
#include "max.h"
}

using namespace std;
// 나머지 내용은 동일
컴파일 해 보시면 컴파일/링크도 잘 되고 예상대로 실행도 잘되는 걸 확인하실 수 있을 겁니다. 이번글은 두 번째 팁의 A/S 글이니 여기에서 짧게 마칩니다.