고수들이 절대 가르쳐 주지 않는 C/C++ 프로그래밍 팁 #2 - C/C++ Mixed Programming

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


고절가주팁 두번째 시간입니다. 저번 글에서 말씀드린 대로 C/C++ 섞어 프로그래밍하기 기법에 대해 말씀 드리겠습니다.

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란 무엇인가를 클릭하세요.

신고
  1. 시퍼

    2007.06.17 17:46 신고 [수정/삭제] [답글]

    무슨소리인지 하나도 모르겠습니다. ^^.. 좋은글 잘보고 갑니다. ^^

  2. 마래바

    2007.06.17 18:44 신고 [수정/삭제] [답글]

    저도 무슨 소린지는 하나도 모르겠습니다만.. 본인이 가지고 있는 나만의 지식을 이렇게 공개한다는 것도 쉽지않은 결정이겠죠? ^^

    예전에 컴퓨터 관심을 가지고 처음 배울 때 익혔던 파스칼이 잠깐 생각 나는군요.
    거의 프로그래밍 logic 부분만 배우고 말았지만.. :P

  3. 김찬우

    2007.06.17 18:56 신고 [수정/삭제] [답글]

    정말 쉽게 깔끔하게 잘 정리되어 있네요.
    나중에 책 내셔도 되겠어요.

    함수심볼 바뀌는 부분까지 알려주셔서 감사합니다. ^ㅡ^

    • 김윤수

      2007.06.17 19:06 신고 [수정/삭제]

      그렇게 말씀해 주시니 참 감사합니다. 김찬우 님께서도 혹시 공유하고 있으신 팁 있으시면 블로그에 올리시고 트랙백 날려 주심 감사하겠습니다 ^_^

  4. Heart

    2007.06.17 21:17 신고 [수정/삭제] [답글]

    깔끔한 정리군요 ^^
    extern "C"가 범위가 지정될 수 있는 것과, g++의 ansi.h 파일의 define은 몰랐던 정보네요.
    C/C++을 섞어서 쓸 일이 없다보니...
    C, C++ 언어를 사용하시는 분이라면 쉽게 이해할 수 있는 좋은 강의라고 생각합니다 ^^
    RSS 리더에 추가해야겠네요. 앞으로도 좋은 포스팅 부탁드립니다.

  5. A2

    2007.06.26 00:34 신고 [수정/삭제] [답글]

    좋은 팁 잘 보았습니다.
    C코딩을 하면서 C++까지 고려한다니 미처 생각하지 못한 부분이네요.
    _BEGIN_STD_C 를 선언하는 이 부분도 감탄이 나왔습니다.
    쭉 좋은 정보 부탁드립니다. ^^

  6. Mgun

    2008.03.10 20:25 신고 [수정/삭제] [답글]

    ^^ 상당히 잘 읽었습니다.
    보통 c++로 다시만들어 쓰거나 하는데
    이젠 구현할때 좀 더생각해 봐야겠네요. ^^a.

  7. jargon

    2010.01.04 19:23 신고 [수정/삭제] [답글]

    글 내용이 유익하고 쏙쏙 이해할 수 있게 되 있어서 그냥 가기 미안한 마음이 들어 댓글남기고 갑니다. 고수의 가르침을 공짜로 받을 수 있으니 블로그의 위력에 감탄하고 고수님께 감사할 따름입니다.

  8. Maxico

    2011.03.31 00:22 신고 [수정/삭제] [답글]

    유익하게 잘 읽었습니다.

  9. 조형호

    2013.02.19 04:03 신고 [수정/삭제] [답글]

    엄청나게 좋은 글 감사합니다.

  10. 서동연

    2013.04.04 19:09 신고 [수정/삭제] [답글]

    와 정말 이해하기 쉽고 쏙쏙 들어오는 포스팅을 쓰시네요 ㅎㅎ
    앞으로도 잘 부탁드립니다 ^

  11. 호빗코더

    2015.04.23 01:24 신고 [수정/삭제] [답글]

    안녕하세요. 좋은정보 정말 감사합니다. 저는 루아 공부하다 extern "C" 라는 것을 공부하게 되었는데요. 여기 와서
    필자님의 글을 보니 더 쏙쏙 이해가 잘 가네요 ^^ 감사합니다.

    오늘도 좋은하루보내세요. ^^

댓글을 남겨주세요.