본문 바로가기

지식/VC++

extern "C"

extern "C"

 

C뿐만 아니라 모든 프로그래밍 언어는 목적 파일(obj)을 만들고 링크에 의해 목적 파일을 연결함으로써 최종 실행 파일을 만든다. 이때 목적 파일로 자신이 정의한 함수의 명칭과 주소를 공개하도록 되어 있고 이 명칭 공개 방식이 언어에 상관없는 표준 포맷으로 규정되어 있다. 그래서 C와 파스칼처럼 각각 다른 언어로 컴파일된 오브젝트 파일들이 링크될 수 있는 것이다.

초기의 C++도 마찬가지로 이런 범용 링커(linker)를 사용하도록 디자인되었다. 그런데 범용 링커는 함수를 오로지 이름으로만 찾기 때문에 C++의 오버로딩된 함수들을 제대로 구분하지 못한다. 그래서 C++은 함수의 이름을 외부로 공개할 때 인수의 개수와 타입에 대한 정보까지 함수명에 포함시키는데 이런 명칭을 작성하는 것을 이름 장식(name mangling)이라고 한다. 그래서 C++로 컴파일한 목적 파일의 함수명을 보면 함수 이름외에도 앞뒤로 이상한 기호들이 붙어 있다. 앞에서 만든 Overload.cpp의 목적 파일에는 세 개의 Add함수의 이름이 다음과 같이 작성된다.


?Add@@YANNN@Z

?Add@@YAHHHH@Z

?Add@@YAHHH@Z


Add라는 함수 이름 외에도 뒤쪽에 인수의 개수나 타입에 대한 정보들이 부호화되어 표기되어 있다. 오버로딩된 함수들의 외부 명칭이 이렇게 작성되므로 링크 단계에서는 각 함수들이 다른 함수처럼 인식되는 것이다. 그런데 모든 언어가 오버로딩을 지원하지 않기 때문에 C++의 이런 기능이 다른 언어와 혼합 프로그래밍을 할 때는 문제가 될 수 있다. 그래서 다른 언어와 링크되어야 하는 모듈은 이런 이름을 작성하지 말고 함수의 명칭을 이름만으로 외부로 공개할 필요가 있다.

이때 사용하는 지시자가 바로 extern "C"이다. 함수앞에 이 지시자를 사용하면 오버로딩 기능은 사용할 수 없지만 다른 언어와 링크될 수 있는 범용 함수를 만들 수 있다. 이렇게 만들어진 함수는 C에서도 호출 가능하다. 지금 당장 여러분들이 이 지시자를 사용할 일은 없겠지만 차후에 윈도우즈 환경에서 DLL을 만들거나 할 때는 이 지시자가 필요할 것이다. 윈도우즈의 DLL 포맷은 이름으로 함수를 찾기 때문에 오버로딩을 지원하지 않는다.

extern "C" 지시자는 C++이 C나 다른 언어를 위해 이름 장식을 하지 않도록 할 때 사용하기도 하지만 반대의 경우에도 이 지시자가 필요하다. 즉, 이미 C로 만들어진 함수를 C++에서 호출하고자 할 때 이 함수의 원형 앞에 extern "C"가 있어야 한다. 그렇지 않으면 링커가 장식된 이름으로 함수를 찾게 되므로 C의 함수를 제대로 연결하지 못한다. C++로만 함수를 작성한다면 굳이 이 지시자를 사용할 필요가 없다.


WINAPI의 의미(__stdcall)
 
function calling에 있어서 convention이 여럿 있게 마련인데요.

그런 게 있는 이유는 언어별로 다 틀리기 때문이죠.

따라서 님이 만약 어떤 dll을 만들어서 이런 저런 s/w에서 쓸 수 있게 하려면,
basic용, c++용, pascal용 등등을 따로 만들어야겠죠?

그렇게 되면 시간 및 비용이 많이 들게 됩니다.

그리고 dll의 본래 취지에서 벗어나게 되죠.

그래서 WINAPI라게 선언을 하는 거죠.

WINAPI를 찾아보시면..

#define WINAPI __stdcall

라고 되어 있는데요..

__stdcall 이라는 keyword로 선언된 함수는 함수가 return되기 전에 stack을 정리하게 됩니다.

좋은 비교 대상으로 __cdecl이라는 keyword가 있는데요..

__cdecl 이라는 keyword로 선언된 함수는 함수가 return된 후에 stack을 정리하게 됩니다. ( c/c++ )

따라서 __cdecl keyword를 사용하게 되면 stack을 정리하는 code가 필요하게 되죠.

이런 이유로, CALLBACK macro나 PASCAL macro도 찾아보면 __stdcall 이죠.
( 문자 그대로 standard calling 입니다. )


이외에도 차이점은 있을 거라고 생각됩니다만, 어쨌거나 님께서 질문하신 WINAPI,
즉 __stdcall의 용도는 서로 다른 compiler로 제작된 binary에 공통된 interface를
( - 정확히는 pascal 형식으로 알고 있습니다.. ) 제공하기 위한 것에 목적이 있다고 보시면 됩니다...

참고가 되셨길..


- 네이버에서 greathjm 님의 글을 발췌했습니다.
 
 
ps.
 
#define WINAPI FAR PASCAL
#define FAR _far
#define PASCAL __stdcall
출처 : Tong - 중허니님의 API통


///////////////////////

1. 다른 파일에 선언된 변수나 함수를 선언할 때

◆ 기본적인 얘기

extern이라는 것은 저장공간에 대한 선언에 사용되는 키워드로서,
여러개의 소스파일에 대해서 작업할 때 필요한 것입니다.
무슨 얘긴고 하니... 쉽게 말해서 다른 파일에 선언된 변수가 있다
고 선언하는 겁니다.

[file1.cpp]
int x = 1;
int f() { /* do something */ }
int y = 2;
extern z;

[file2.cpp]
extern int x;
int f();
extern int y = 3;  // 링크 에러.
extern z;          // 링크 에러

void g() { x = f(); }

extern int x; 라는 줄은 정수 x가 여기서 정의(definition)되지
않는다는 것을 지시하며 단지 선언(declaration)을 해주는 일을 수
행합니다. 위에서 y는 링크시 에러가 나게 되는데, extern 키워드
를 사용하더라도 초기화를 하면 그 선언은 정의가 되기 때문입니다.
즉, y는 file1.cpp와 file2.cpp에서 모두 정의되게 됩니다. 반대
로 z는 두 파일에서 모두 선언만 되고 정의가 되지 않았기 때문에 역
시 링크에러가 나게 됩니다.

참고로 file2.c에서 f는 extern int f(); 라고 지정해도 상관없습니다.

◆ const와 typedef

const와 typedef는 기본적으로 internal linkage입니다. 즉, 다
른 파일에서 영향을 받지 않는다는 것이죠. const에서 이미 살펴봤
죠? typedef도 마찬가지입니다.

const에서 다뤘던 포인터 예제는 적절하지 못한 예였습니다. -_-;
여기서 다시 보도록 하죠.

[t1.cpp]
typedef int T;
const char* pText0 = "SCV";
char* const pText1 = "marine";
extern char* const pText2;
extern char* const pText3 = "tank";
extern char* const pText4 = "zealot";

[t2.cpp]
typedef void T;
const char* pText0 = "SCV";
char* const pText1 = "lurker";
extern char* const pText2;
extern char* const pText3;
extern char* const pText4 = "dragoon";

T : t1.cpp에서는 int, t2.cpp에서는 void로 사용할 수 있음.
pText0 : 조심! const char*의 의미는 const char에 대한 const
가 아닌 포인터. 즉, const가 아님. 따라서 external linakage. 링크 에러.


pText1 : char* const의 의미는 char에 대한 const 포인터. 즉,
internal linkage. t1.cpp에서는 "marine", t2.cpp에서
는 "lurker"으로 잘 동작함.


pText2 : 양쪽 모두 선언만 되고 정의는 안함. 실제로 사용하게 되면 링크 에러.
pText3 : t1.cpp에서 초기화를 했기 때문에 정의가 됨. "tank"
pText4 : 양쪽 모두 초기화를 했기 때문에 양쪽 모두 정의가 되서 중복 정의 에러가 생김.

◆ extern을 사용가능한 경우

그럼 마지막으로 어떤 경우에 static이, 어떤 경우에 extern이 사용
가능한지 정리한 표를 보죠. (from MSDN)

Construct                                       static   extern
Function declarations within a block      No       Yes
Formal arguments to a function            No       No
Objects in a block                              Yes      Yes
Objects outside a block                       Yes      Yes
Functions                                         Yes      Yes
Class member functions                       Yes      No
Class member data                             Yes      No
typedef names                                   No       No


2. 다른 언어로의 연결을 지시할 때

◆ 기본적인 얘기부터

가장 대표적인게
extern "C" int Calc(double, int, int);
이런 식으로 C의 함수를 선언하는 것입니다.

아시겠지만 C와 C++은 함수를 호출하는 방식이 다릅니다. 인자를 처
리하는 순서라거나 스택에서 관리하는 방법 같은게 다르죠. (자세한
건 다음 기회에...) 따라서 C++ 컴파일러에게 이 함수는 C++의 규칙
에 어긋나는 함수는 아니지만 사실은 C의 함수라고 알려줘야 C++ 컴
파일러가 실수하지 않고 처리할 수 있게 됩니다. (실제로 이 선언을
중요하게 처리해야 하는건 링커입니다.)
그리고 Fotran에서 만든 함수를 Visual C++에서 사용하기 위해서
도 extern "C"로 선언해야 하는 것 같습니다. VC++이 extern 다음
에 허용하는 문자열이 "C"와 "C++" 뿐이라네요. (제가 해본건 아니
라서...)

extern "C"
{
#include < string.h>               // C 헤더 파일을 포함
    int Calc(double, int, int);   // C 함수를 선언
    int g1;                       // 정의(definition)
    extern int g2;                // 선언(declaration, not definition)
}

이렇게 해서 여러개의 선언을 묶어주기도 하죠.
참고로 아랫줄은 정의가 아니라 선언입니다.
extern "C" int g3;              // 선언(declaration, not definition)

C코드를 만들면서 C와 C++에서 다 사용할 수 있도록 헤더 파일을 만
들기 위해서는 다음과 같은 방법을 사용합니다.

#ifdef __cplusplus
extern "C" {
#endif

// 실제 함수나 변수들의 선언

#ifdef __cplusplus
}
#endif

C++ 소스를 컴파일할 경우에는 __cplusplus 가 자동으로 정의되므
로 전처리기에 의해 extern "C" { } 선언이 들어가게 됩니다.

◆ 함수 포인터의 경우

case by case로 생각해보겠습니다. (책을 베끼겠습니다.)

typedef int (*FT)(const void* , const void*);                // (1)

extern "C" {
    typedef int (*CFT)(const void* , const void*);           // (2)
    void qsort(void* p, size_t n, size_t sz, CFT cmp);       // (3)
}

void isort(void* p, size_t n, size_t sz, FT cmp);            // (4)
void xsort(void* p, size_t n, size_t sz, CFT cmp);           // (5)
extern "C" void ysort(void* p, size_t n, size_t sz, FT cmp); // (6)

int compare(const void* , const void*);                      // (7)
extern "C" int ccmp(const void* , const void*);              // (8)

(1) FT는 (const void*, const void*)를 받고 int를 리턴하는 C++로 링크되는 함수.
(2) CFT는 (const void*, const void*)를 받고 int를 리턴하는 C로 링크되는 함수.
(3) cmp는 CFT 타입. 즉 C로 링크되는 함수. qsort도 C 함수.
(4) cmp는 FT 타입. 즉 C++로 링크되는 함수. isort도 C++ 함수.
(5) cmp는 CFT 타입. 즉 C로 링크되는 함수. xsort는 C++ 함수.
(6) cmp는 FT 타입. 즉 C++로 링크되는 함수. qsort도 C 함수.
(7) compare는 C++로 링크되는 함수.
(8) ccmp는 C로 링크되는 함수.

따라서... 다음과 같은 결과가 나옵니다.
void (char* v, int sz)
{
    qsort(v, sz, 1, &compare);      // error
    qsort(v, sz, 1, &ccmp);         // ok
   
    isort(v, sz, 1, &compare);      // ok
    isort(v, sz, 1, &ccmp);         // error
}

[The C++ Programming Language]의 설명에 따르면 C와 C++이 함
수를 호출하는 방법이 같을 경우에는 위의 에러들도 확장으로 인정해
줄 수 있다고 하네요. 참고하세요.

◆ 다른 언어와의 연결에 대한 조금 더 시시콜콜하고 Visual C++적인 얘기

포트란이나 파스칼 함수에 대해서는 __stdcall 를 사용해서 선언해줘야 합니다.
(모든 윈도우 API 함수들은 __stdcall을 사용해서 선언되어 있습니다.)
함수의 선언 앞에 붙이는 PASCAL, WINAPI, CALLBACK이라는 선언자
는 모두 __stdacll이라는 의미를 가지고 있죠.
반면에 C나 C++의 함수는 기본적으로 __cdecl이 됩니다.

__cdecl로 선언된 함수의 경우 인자를 전달한 스택을 정리하는건 호
출한 함수(caller)입니다.
__stdcall로 선언된 함수의 경우 인자를 전달한 스택을 정리하는건
호출된 함수(callee)입니다.
양쪽다 인자는 오른쪽 인자부터 왼쪽으로 스택에 들어가지요.

그렇다면 스택을 정리하는게 누구인지가 왜 중요할까요?
다른 언어가 다 호출된 함수가 스택을 정리하니까 C나 C++도 똑같이
하면 편할 텐데요.

일단 치명적인 부분은 vararg를 사용하는 함수입니다. 가장 대표적인게 printf죠.
printf 는 다음과 같은 형을 가집니다.
int printf(const char *format, ... );
... 는 인자의 수가 정해져 있지 않다는 뜻이죠? 하지만 인자의 갯수
나 종류같은게 정해져 있지 않기 때문에 호출된 함수가 스택을 정리하
는 __stdcall의 경우에는 제대로 청소를 할 수가 없게 됩니다. 따라
서 인자의 수가 정해져 있지 않은 함수는 __cdecl을 사용하게 됩니다.

참고로 __cdecl의 경우에는 각 함수 호출마다 스택을 정리하는 부분
이 들어가야 하기 때문에 컴파일된 코드의 크기가 더 커질 수 있다고 합니다.

그리고 코드를 C++로 작성중이더라도 라이브러리 파일을 만들어서 다
른 언어에서도 사용할 수 있게 해주려면 extern "C"와 __stdcall
을 사용해서 선언해주는게 좋겠죠?

이런 부분에 대해 자세히 알고 싶으신 분은 MSDN에서 "Mixed-
Language Programming Topics"이라는 글을 참고하시기 바랍니다

출처 : http://cafe.naver.com/prostudy12