[C++] C++ 코드와 C 코드의 링킹

Seongcheol Jeon·2025년 7월 1일
0

CPP

목록 보기
42/47
post-thumbnail

C언어의 역사가 오래된 만큼 C언어로 작성된 많은 라이브러리와 소스 코드가 존재하며, 지금도 C언어는 여러 분야에서 활발히 사용되고 있다. 현재 많은 C++ 프로그램 개발자들이 기존에 작성된 C 소스 코드를 이용하거나 C 라이브러리를 사용하고 있기 때문에, C++ 프로그램에서 C 코드를 연결하여 사용하는 방법을 알아둘 필요가 있다.
서로 다른 언어로 작성된 프로그램을 연결하여 사용하는 것은 어렵고 복잡하지만, C++ 언어는 C 언의 확장이기 때문에 상대적으로 쉽다.

C/C++ 컴파일러의 이름 규칙

모든 컴파일러는 소스 코드를 컴파일하여 목적 코드(obj 파일)를 만들 때, 소스 코드에 있는 변수, 함수, 클래스의 이름을 변형하여 저장한다. 이를 흔히 이름 규칙(naming mangling) 이라고 부른다. C 컴파일러C++ 컴파일러는 서로 다른 이름 규칙을 가지고 있다.

C언어와 C++언어의 이름 규칙을 제대로 알아야, C언어로 작성된 함수를 호출하거나 C 전역 변수를 사용하는 C++ 프로그램의 링킹을 제대로 이해할 수 있다. 다음은 비주얼 C/C++의 대한 이름 규칙이다.

C 컴파일러의 이름 규칙

C 컴파일러는 C 소스 코드를 컴파일하여 목적 코드(obj파일)를 만들 때 함수 이름 앞에 밑줄표시문자(_)를 붙인다. 예를 들어 다음 C 함수를 컴파일한다고 해보자.

int f(int x, int y)
int main()

C 컴파일러는 이 두 함수가 사용되는 C 소스 코드의 모든 곳에 다음과 같이 이름을 변경하여 목적 파일에 저장한다.

_f
_main

C 컴파일러의 이름 규칙은 매개 변수의 존재나 리턴 타입은 전혀 반영하지 않는다.

📌 C 언어에서 함수 중복이 불가능한 이유

C 언어에서 함수 중복이 불가능한 이유는 바로 C 컴파일러의 이름 규칙의 한계 때문이다.
int f(int x)int f(int x, int y) 함수 모두 매개 변수의 존재 여부와 관계없이 _f라는 이름으로 컴파일되어, 같은 이름의 함수 _f가 목적 코드에 2개 존재하게 되므로 컴파일 오류나 링크 오류가 발생한다.

C++ 컴파일러의 이름 규칙

C++ 컴파일러의 이름 규칙은 C 컴파일러와 다르다. C++ 컴파일러는 목적 코드를 만들 때 함수의 매개 변수 개수와 타입, 리턴 타입 등을 참조 하여 복잡한 기호를 포함하는 이름을 붙인다.
이렇게 함으로써 중복 함수(overloaded function)들이 목적 파일 내에서 구분된다. 예를 들어 다음과 같은 이름의 C++ 함수가 있다고 해보자.

int f(int x, int y)
int f(int x)
int f()
int main()

C++ 컴파일러는 이 4개의 함수들을 컴파일하여 목적 코드 내에 다음과 같은 이름을 붙인다.

?f@@YAHHH@Z // int f(int x, int y)의 이름 f를 변환한 이름
?f@@YAXH@Z  // int f(int x)의 이름 f를 변환한 이름
?f@@YAHXZ   // int f()의 이름 f를 변환한 이름
_main       // int main()의 이름 main을 변환한 이름

3개의 f() 함수가 매개 변수 타입과 개수, 리턴 타입을 반영하여 서로 다른 이름으로 변형되어 있음을 알 수 있다. 그러나 예외적으로 main() 함수만은 항상 _main으로 이름을 붙인다.

C++ 프로그램에서 C 함수 호출시 링크 오류가 발생하는 경우

C 언어로 작성된 함수를 C++ 프로그램에서 그냥 호출하면 링크 오류가 발생한다. 그 이유는 위에서 설명한 바와 같이 CC++ 컴파일러 사이의 이름 규칙이 서로 다르기 때문이다.

C++ 컴파일러main.cpp를 컴파일할 때, C++ 이름 규칙을 사용하여 함수 f()가 등장하는 모든 곳에 아래 이름으로 main.obj에 기록한다.

?f@@YAHHH@Z

한편, f.cC 컴파일러에 의해 컴파일되고, 함수 f()의 이름이 다음과 같이 f.obj 파일에 기록된다.

_f

이제, main.objf.obj를 링킇갈 때, 링커main.obj에서 호출하는 ?f@@YAHH@Z 이름의 함수를 f.obj에서 발견할 수 없기 때문에 링크 오류를 발생시킨다.

📌 이름 규칙의 표준

이름 규칙(naming mangling)에는 표준이 없다. 컴파일러마다 목적 코드에 함수의 이름을 붙이는 방법이 서로 다르다. 그러므로 서로 다른 컴파일러로 컴파일된 목적 파일들은 링크되지 않는다. 예를 들어 볼랜드 C++로 컴파일한 목적 코드를 비주얼 C++에서 작성한 목적 코드에서 링크하여 사용할 수 없다.


정상적인 링킹, extern "C"

C++ 컴파일러에게 main.cpp 안의 등장하는 함수 f()C 언어로 작성된 것임을 알려주어, f() 이름에 대해서만 C 언어의 이름 규칙으로 컴파일 하도록 지시하면 링크 오류를 해결할 수 있다.

extern "C" int f(int x, int y);

이 지시문에 의해 C++ 컴파일러는 함수 f()의 이름을 C 이름 규칙으로 컴파일한다.
만일 C 언어로 작성된 함수가 여러 개 있다면 다음과 같이 묶어서 선언해도 된다.

extern "C" {
  int f(int x, int y);
  void g();
  char s(int []);
}

혹은 다음과 같이 여러 개의 C 함수 원형이 선언된 헤더 파일을 통째로 지정할 수 있다.

👏 실제로 C++ 표준 라이브러리의 헤더 파일은 이런 지시문을 많이 사용하고 있다.

extern "C" {
  #include "MyCLangFunc.h"

extern "C" 지시문을 사용하여 C++ 프로그램에서 C 함수를 호출할 때 링크 오류가 발생하지 않는 코드 사례이다.
아래 그림에서 int f(int x, int y)f.c에 작성된 C 언어 함수이므로, main.cpp에는 int f(int x, int y) 함수만을 extern "C"로 선언하였다. 그 결과 main.obj에는 f() 함수와 g() 함수의 이름이 각각 다음과 같이 컴파일 된다.

_f
?g@@YAHHH@Z

링커main.obj에서 찾고자 하는 함수 _f?g@@YAHHH@Zf.objg.obj에서 정확히 찾을 수 있기 때문에, 링크가 성공적으로 이루어진다.


다음의 예제를 보자.

#include "pch.h"
#include <iostream>

using namespace std;

void __declspec(dllexport) hello()
{
    cout << "Hello World!" << endl;
}

extern "C"
{
    void __declspec(dllexport) externHello()
    {
        cout << "Hello World!" << endl;
    }
}

라이브러리를 빌드 후, dumpbin을 활용하여 dll 파일을 확인해 보면 아래와 같이 extern "C"와 그렇지 않은 함수 이름의 차이를 알 수 있다.

  • 1extern "C"를 적용하지 않은 함수로써 외부에서 호출하려면 ?hello@@YAXXZ로 호출해야 한다.
  • 2extern "C"를 적용한 함수로써 C언어 환경에서처럼 externHello 함수 이름으로 호출이 가능하다.

실행파일을 만들어서 직접 확인해보자! extern "C"가 적용되지 않은 hello() 함수를 호출하면 아래의 결과와 같이 호출이 불가능하다. 그 이유는 name mangling이 적용된 함수이기 때문이다.

#include <iostream>
#include <Windows.h>

using namespace std;

//typedef void (*helloPtr)();
using helloPtr = void(*)();

//typedef void (*externHelloPtr)();
using externHelloPtr = void(*)();


int main()
{
    HINSTANCE hInst;
    helloPtr helloFuncPtr;
    externHelloPtr externHelloFuncPtr;


    // library load
    hInst = LoadLibrary(L"C:\\Users\\seongcheoljeon\\workspace\\cpp\\testproj\\x64\\Debug\\mydll.dll");

    if (hInst == nullptr)
    {
        return 0;
    }

/* function load
    // 이것을 호출 가능!
    helloFuncPtr = (helloPtr)GetProcAddress(hInst, "?hello@@YAXXZ");

    // 아래는 name mangling으로 인하여, 함수 이름이 변경되어 hello 함수를 찾지 못하여 호출 불가!
*/
    helloFuncPtr = (helloPtr)GetProcAddress(hInst, "hello");

    if (helloFuncPtr == nullptr)
    {
        cout << "function load failed. <hello()>" << endl;
    }
    else
    {
        helloFuncPtr();
    }

    // extern "C"로 선언된 함수는 name mangling이 되지 않으므로, 아래와 같이 호출 가능!
    externHelloFuncPtr = (externHelloPtr)GetProcAddress(hInst, "externHello");

    if (externHelloFuncPtr == nullptr)
    {
        cout << "function load failed. <externHello()>" << endl;
    }
    else
    {
        externHelloFuncPtr();
    }

    // library unload
    FreeLibrary(hInst);

    return 0;
}

/* 결과
function load failed. <hello()>
Hello World!
*/

이번에는 hello() 함수가 name mangling이 진행된 이름으로 호출해 보자.

#include <iostream>
#include <Windows.h>

using namespace std;

using helloPtr = void(*)();

using externHelloPtr = void(*)();


int main()
{
    HINSTANCE hInst;
    helloPtr helloFuncPtr;
    externHelloPtr externHelloFuncPtr;


    // library load
    hInst = LoadLibrary(L"C:\\Users\\seongcheoljeon\\workspace\\cpp\\testproj\\x64\\Debug\\mydll.dll");

    if (hInst == nullptr)
    {
        return 0;
    }

    helloFuncPtr = (helloPtr)GetProcAddress(hInst, "?hello@@YAXXZ");

    if (helloFuncPtr == nullptr)
    {
        cout << "function load failed. <hello()>" << endl;
    }
    else
    {
        helloFuncPtr();
    }

    // extern "C"로 선언된 함수는 name mangling이 되지 않으므로, 아래와 같이 호출 가능!
    externHelloFuncPtr = (externHelloPtr)GetProcAddress(hInst, "externHello");

    if (externHelloFuncPtr == nullptr)
    {
        cout << "function load failed. <externHello()>" << endl;
    }
    else
    {
        externHelloFuncPtr();
    }

    // library unload
    FreeLibrary(hInst);

    return 0;
}

/* 결과
Hello World!
Hello World!
*/

Mangled Name인 ?hello@@YAXXZ로 호출하니까 정상적으로 dll에 구현된 hello() 함수가 호출되어 Hello World!가 출력되는 것을 확인해볼 수 있다.

0개의 댓글