C++ Exports

4 minute read

Published:

LevelDB 코드를 읽다가 export.h라는 파일을 읽었습니다. 원래는 나중에 따로 읽어보려고 했는데, 먼저 읽어보고 이렇게 따로 정리하는 데에는 이유가 크게 두 가지 있습니다. 첫째는 제가 열어본 모든 헤더 파일들이 export.h 파일을 include 하고 있기 때문입니다. 둘째는 처음 시작한 db.h 파일에 이런 클래스가 있었기 때문입니다.

// db.h
#include "leveldb/export.h"

/* ... */

class LEVELDB_EXPORT Snapshot {
    protected:
        virtual ~Snapshot();
};

LEVELDB_EXPORT 매크로가 뭘까, 왜 클래스 선언에서 클래스명 앞에 위치해 있을까, 이 두 가지가 이번 포스팅을 작성하게 된 계기입니다.

The Problem

LEVELDB_EXPORT가 뭔지 찾아봤는데, 이렇게 아무것도 없이 선언만 된 매크로였습니다.

// export.h
#define LEVELDB_EXPORT

상당히 당황스러웠는데, 뭔가 그럴듯하게 있을 것처럼 매크로까지 만들어놓고서는 코드를 뜯어보고 나니 아무것도 없는 상황이었기 때문입니다. 이 문제를 알아보기 위해 C++ 책도 읽어보고 검색도 하다가 스택 오버플로우에서 이런 질문을 찾았습니다. 저와 마찬가지로 IDATA_EXPORT라는 매크로가 클래스명 앞에 달려 있고, 매크로가 선언되는 곳에 가 봐도 아무것도 없이 매크로 선언만 있다는 내용입니다. 이 질문의 답변에 한 링크가 있습니다. 좀 더 자세하게 알아보기 위해 DLL에 대해서 알아보겠습니다.

DLL

출처: https://docs.microsoft.com/ko-kr/cpp/build/dlls-in-visual-cpp?view=msvc-170

DLL이란 dynamic-link library의 약자로, 동적 링크 라이브러리로 부릅니다. 왜 이걸 여지껏 본 적이 없을까 하니, 윈도우에만 돌아가는 녀석이기 때문에 우분투 위에서 코딩을 하는 저는 만날 일이 없었던 것 같습니다. 잠깐 검색을 해 보니 DLL을 쓰는 작업은 대체로 Visual Studio에서 하는 경우가 많아 보였습니다. 아무래도 같은 마이크로소프트에서 개발한 것이다 보니 호환성이 좋아서 그런 것 같습니다.

정적 링크를 사용하면, 그러니까 우리가 통상적으로 컴파일을 할 때처럼 링크를 하게 되면 정적 라이브러리의 모든 코드를 빌드 시에 이 코드를 사용하는 실행 파일에 복사합니다. 동적 링크는 이와 다르게 애플리케이션이 로드되거나 런타임에 요청이 있을 시에 DLL의 함수 혹은 리소스를 가져와서 쓰는 것입니다. 그 장점은 위 출처에 방문하시면 많이 보실 수 있습니다. 지금은 이 DLL이 제 문제와 어떤 관계가 있는지 더 자세하게 설명하겠습니다.

dllexport, dllimport

출처: https://docs.microsoft.com/en-us/cpp/cpp/dllexport-dllimport?view=msvc-170

dllexportdllimport는 마이크로소프트에서 제공하는 C/C++에 대한 확장 기능입니다. 이 명령어들을 사용하면(명령어라고 부르는게 정확한지는 모르겠지만) 함수, 리소스, 혹은 객체를 DLL로 export하거나 DLL에서 import 할 수 있습니다.

여기서 중요하게 생각할 것이 두 가지가 있습니다. 첫째는 이 기능이 마이크로소프트에서만 제공하는 C/C++에 대한 확장 기능이기 때문에 윈도우 환경이 아니라면 작동하지 않습니다. 즉 윈도우 환경에서 돌아가는 것이 아니라면 넣을 필요가 없고 오히려 컴파일을 방해하는 요소라는 것이죠. 둘째는 이걸 일일이 선언해 줘야 한다는 것입니다. 필요한 모든 클래스, 객체, 함수에 대해서 말입니다. 실행되는 운영체제에 따라서 필요성이 달라지는데 필요할 때는 일일이 선언해 줘야 하는 번거로움을 해결해 주기 위해서 이것을 매크로로 설정한 것입니다.

Problem Solved

조금 더 자세하게 export.h 파일을 살펴보겠습니다. export.h의 전문은 다음과 같습니다. 들여쓰기만 보기 좋게 바꾸고 내용은 원문 그대로입니다.

// export.h

// Copyright (c) 2017 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.

#ifndef STORAGE_LEVELDB_INCLUDE_EXPORT_H_
#define STORAGE_LEVELDB_INCLUDE_EXPORT_H_

    #if !defined(LEVELDB_EXPORT)

        #if defined(LEVELDB_SHARED_LIBRARY)
            #if defined(_WIN32)

                #if defined(LEVELDB_COMPILE_LIBRARY)
                    #define LEVELDB_EXPORT __declspec(dllexport)
                #else
                    #define LEVELDB_EXPORT __declspec(dllimport)
                #endif  // defined(LEVELDB_COMPILE_LIBRARY)

            #else  // defined(_WIN32)
                #if defined(LEVELDB_COMPILE_LIBRARY)
                    #define LEVELDB_EXPORT __attribute__((visibility("default")))
                #else
                    #define LEVELDB_EXPORT
                #endif
            #endif  // defined(_WIN32)

        #else  // defined(LEVELDB_SHARED_LIBRARY)
            #define LEVELDB_EXPORT
        #endif

    #endif  // !defined(LEVELDB_EXPORT)

#endif  // STORAGE_LEVELDB_INCLUDE_EXPORT_H_

처음부터 천천히 살펴보겠습니다.

#if !defined(LEVELDB_EXPORT)

LEVELDB_EXPORT 매크로가 이미 선언되었는지 확인합니다. 이 줄로 인해 이 코드의 뒷부분은 최초 한 번만 실행되게 됩니다.

#if defined(LEVELDB_SHARED_LIBRARY)

LevelDB를 빌드할 때, 이것을 shared library로 사용할 것인지 확인하는 과정입니다. LEVELDB_SHARED_LIBRARY는 언제 켜지냐면 CMakeLists에서 다음과 같이 선언됩니다.

# CMakeLists.txt

if(BUILD_SHARED_LIBS)
    target_compile_definitions(leveldb
        PUBLIC
            # Used by include/export.h
            LEVELDB_SHARED_LIBRARY
    )
endif(BUILD_SHARED_LIBS)

여기서 BUILD_SHARED_LIBS는 CMake에서 기본적으로 제공하는 전역 변수같은 친구입니다. 만약 true라면 add_library() 함수가 공유 라이브러리를 만들게 합니다. (참고)

#if defined(_WIN32)

빌드되는 환경이 윈도우인지를 확인합니다. 앞서 살펴본 dllexportdllimport가 위치한 것을 확인할 수 있습니다. 즉, 만약 윈도우 환경에서 LevelDB가 빌드된다면 이 LEVELDB_EXPORTdllexport 혹은 dllimport로 선언되었을 것입니다. (LEVELDB_COMPILE_LIBRARY도 선언이 되어야 하지만, 지금은 넘어가도록 하겠습니다. ) 그리고 LEVELDB_EXPORT는 모든 클래스와 함수 등에 붙어 있기 때문에, 윈도우 환경에서 빌드를 한다면 자동으로 모든 클래스와 함수 등이 export 혹은 import 되는 것입니다.

#define LEVELDB_EXPORT

가장 아래에 위치한 #define문입니다. 제가 의문을 가졌던 코드이기도 합니다. 보면 LEVELDB_EXPORT 매크로 뒤에 아무런 값도 할당되지 않았는데, 지금까지 살펴본 것을 토대로 하면 이것이 매우 당연한 것임을 알 수 있습니다. 저는 지금 윈도우 기반에서 빌드를 하지 않고 있기 때문에 DLL과 같은 기능을 사용하지 않을 것이기 때문입니다.

Conclusion

이번 포스팅에서는 비어있는 LEVELDB_EXPORT 매크로에 대해 알아봤습니다. 이 매크로를 사용하는 이유는 다음과 같이 정리할 수 있겠습니다.

  1. LEVELDB_EXPORT 매크로를 export할 모든 클래스, 객체, 함수 앞에 선언함으로써
  2. 특정 운영체제에 대한 최적화를 CMake를 할 당시에 변수를 설정하는 것 만으로 할 수 있게 해 주며
  3. 특정 운영체제를 사용하지 않는다면 그냥 비워놓으면 되기 때문에 문제가 되지 않는다.