Cross-platform shared library with google test using CMake

Created:

목표

  1. Windows와 Linux 환경에서 모두 빌드와 실행이 가능한 C++ 공유 라이브러리 프로젝트를 생성하고자 한다.
  2. 빌드시 Windows에서는 .dll이, Linux에서는 .so가 생성되게 할 것이다.
  3. 또한 생성된 공유 라이브러리를 테스트 할 수 있는 환경 역시 구축할 것이다.

프로젝트 구성

  • 평소 같으면 Visual Studio에서 솔루션을 생성하겠지만, cross-platform이 목적인 이상 그럴 수는 없다.
    • Visual Studio는 Windows에서만 사용 가능하기 때문이다.
    • 모든 플랫폼에서 작동 가능한 프로젝트를 위해서는 특정 IDE에 종속되는 프로젝트를 구성해서는 안된다.
  • 때문에 여러 IDE들이 지원하며, 심지어 터미널에서도 쉽게 사용 가능한 CMake를 사용하여 프로젝트를 구성하기로 결정했다.

    본 글은 CMake 자체에 대한 정보는 다루지 않는다. CMake에 대한 초보적인 이해가 있다는 가정 하에 글을 작성할 것이다.

CMake

  • CMake는 makefile을 자동으로 생성해주는 빌드 시스템이다.
    • makefile은 터미널에서 make 명령어를 사용할 때 참조하는 텍스트 파일이다.
    • 프로젝트를 구성하는 파일들의 의존성, 컴파일러에 입력되는 변수 등을 정의할 수 있다.
    • CMake 역시 makefile에 정의되는 여러 정보들을 정의할 수 있다.
  • Visual Studio에 CMake 기반 프로젝트를 생성하는 기능이 존재하기 때문에 아래의 방법으로 프로젝트를 생성하였다.

  • 위의 방법으로 프로젝트를 생성하면 아래와 같은 구조가 생성된다.
    • 프로젝트에 필요한 몇 개의 파일들을 추가로 생성하였다.

공유 라이브러리 빌드 설정

  • 상위 디랙터리의 CMakeLists.txt는 당장 수정할 것이 없다.
    • 프로젝트가 커져 추가적인 하위 디렉터리를 생성할 일이 발생했을 때 수정하면 된다.
  • 하위 디랙터리의 CMakeLists.txt의 초기 모습은 다음과 같다.
# CMakeList.txt : CMake project for Math, include source and define
# project specific logic here.
#

# Add source to this project's executable.
add_executable(Math "Math.cpp")

if (CMAKE_VERSION VERSION_GREATER 3.12)
  set_property(TARGET Math PROPERTY CXX_STANDARD 20)
endif()

# TODO: Add tests and install targets if needed.
  • 먼저 해야할 것은 add_executable 부분을 수정하는 것이다.
    • 우리는 해당 소스코드를 실행 파일이 아닌 shared library로 빌드하고자 한다.
    • 따라서 add_library(Math SHARED "Math.cpp")로 수정해야한다.
    • 수정을 마친 CMakeLists.txt는 아래와 같다.
# CMakeList.txt : CMake project for Math, include source and define
# project specific logic here.
#

# Add source to this project's executable.
add_library(Math SHARED "Math.cpp")

if (CMAKE_VERSION VERSION_GREATER 3.12)
  set_property(TARGET Math PROPERTY CXX_STANDARD 20)
endif()

# TODO: Add tests and install targets if needed.
  • 그럼 이제 shared library 생성을 위한 소스코드들을 작성해보자.
/* Math.h */
#pragma once

// Shared library import/export macro
#ifdef _WIN32
	#ifdef MATH_EXPORTS
	#define MATH_API __declspec(dllexport)
	#else
	#define MATH_API __declspec(dllimport)
	#endif
#elif __linux__
	#define MATH_API
#endif

extern "C" MATH_API double Add(double a, double b);
extern "C" MATH_API double Subtract(double a, double b);


/* Math.cpp */
#include "Math.h"

double Add(double a, double b) {
    return a + b;
}

double Subtract(double a, double b) {
    return a - b;
}

  • Math.cpp 파일은 별 내용 없다.
  • 핵심은 Math.h이다.
    • Windows의 경우 dll로 export/import 하기 위해서 __declspec(dllexport) 혹은 __declspec(dllimport)를 함수 시그니쳐 앞에 추가해야 한다.
      • __declspec(dllexport): dll을 빌드 할 경우 추가해야 한다.
      • __declspec(dllimport): dll을 사용할 경우 추가해야 한다.
    • Linux의 경우 그러한 작업이 필요하지 않다. 하지만 위의 키워드들은 msvc에서만 사용 가능하기 때문에 위의 키워드가 포함될 경우 컴파일 에러가 발생할 것이다.
    • 때문에 전처리문을 통해서 MATH_API 매크로를 Windows에서 빌드할 경우 해당 키워드로 선언하고, Linux 환경에서는 공란으로 선언할 것이다.
      • _WIN32는 Windows 32bit/64bit 환경에서, __linux__는 linux 환경에서 빌드할 경우 선언되는 매크로이다.
      • <대문자_파일명>_EXPORTS는 해당 파일이 dll 등으로 export될 때 선언되는 매크로이다.
        • 즉 헤더 파일이 export될 경우에는 __declspec(dllexport)가, 그 외의 헤더 파일을 다른 프로젝트에 include 하는 등의 경우에는 __declspec(dllimport)가 선언된다.
  • 헤더 파일의 extern "C" 키워드는 해당 함수를 링킹 과정에서 C 함수의 링킹 규칙을 따라 처리하라는 의미이다.
    • 즉, Name Mangling을 수행하지 말라는 뜻이다. 자세한 내용은 생략한다.
    • 해당 키워드를 추가하면 빌드된 라이브러리를 C에서도 사용할 수 있다.
      • C++에서만 사용될 라이브러리라면 해당 키워드를 붙이지 않아도 된다.

Google Test

Google Test의 공식 문서를 참고하면 CMake 프로젝트에서 Google Test를 사용하기 위해서는 다음과 같이 CMakeLists.txt 파일을 수정하면 된다. TODO: 아래가 추가된 코드이다.

# CMakeList.txt : CMake project for Math, include source and define
# project specific logic here.
#

# Add source to this project's executable.
add_library(Math SHARED "Math.cpp")

if (CMAKE_VERSION VERSION_GREATER 3.12)
  set_property(TARGET Math PROPERTY CXX_STANDARD 20)
endif()

# TODO: Add tests and install targets if needed.
include(FetchContent)
FetchContent_Declare(
  googletest
  URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)

# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

enable_testing()
add_executable(MathTest "MathTest.cpp")
target_link_libraries(MathTest GTest::gtest_main Math)

include(GoogleTest)
gtest_discover_tests(MathTest)

  • 위의 코드를 추가하면 CMake가 작동하는 그 시점에 Google Test 라이브러리를 다운로드 받아 MathTest.cpp에 링크하게 된다. 이제 MathTest.cpp를 작성해보자.
#include <gtest/gtest.h>

#include "Math.h"

TEST(MathLibraryTest, AddTest) {
    EXPECT_EQ(Add(1, 2), 3);
    EXPECT_EQ(Add(-1, 2), 1);
}

TEST(MathLibraryTest, SubtractTest) {
    EXPECT_EQ(Subtract(2, 1), 1);
    EXPECT_EQ(Subtract(2, -1), 3);
}

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

  • 위와 같이 작성하고 Visual Studio에서 Startup item을 테스트 실행 파일로 설정한다.

  • 이후 실행 버튼을 누르면 dll과 테스트 프로그램을 포함한 모든 소스코드가 잘 빌드되고 google test 실행 결과를 확인할 수 있다.

  • 같은 프로젝트를 Ubuntu에 설치된 Eclipse IDE로 빌드 후 실행한 결과 역시 .so 라이브러리가 잘 빌드되며 google test 결과를 확인할 수 있다.