Motive


최근에 Vulkan 에 관심이 좀 생겨서 tutorial 을 따라하면서 구현하고 있습니다. 그런데 이걸 추상화해서 C++ class 로 만들어 내는 데 스트레스가 심해졌습니다. 


Vulkan 은 기본적으로 C 로 구현되어 있기 때문에 오브젝트가 다 structure 로 되어 있고 member function 을 사용하고 있지 않습니다. 그래서 함수가 매우 많은데 일일이 찾아야 하고 구조체에 일일이 sType 을 넣어 줘야 합니다. ( C++ template 을 사용하는 ) Type-Traits 가 존재하지 않기 때문에 구조체의 유효성 검사를 위해서는 어쩔 수 없겠죠.



게다가 호출할 때도 파라미터 개수가 많아지고 생성하고자 하는 오브젝트가 어디에 소속되어 있는 건지 알기가 어렵습니다.



D3D 처럼 vkPhysicalDevice 와 관련한 메서드가 클래스에 모여 있으면 얼마나 좋을까요?



저는 C++ 에 익숙해져 있는 개발자다 보니 Vulkan 이 D3D 보다 좀 산만하고 어렵게 느껴집니다.


그래서 회사에서 동료분이랑 "왜 Vulkan 은 C 로 작성되었을까?" 라는 주제에 대해서 이야기해 봤는데, "개발자가 C++ 을 싫어한다", "성능 차이가 있다", "OpenGL 과 API 의 호환성을 유지하려고 한다", "별로 편하게 해줄 생각이 없는 애들이다" 등등 여러 가지 의견이 나왔지만, 납득할 만한 답을 찾지는 못했습니다.


대체 왜 C++ 이 아닌 C 를 사용하게 되었는지 알 수가 없었습니다. 그런데 "Vulkan Specification" [ 1 ] 을 읽다가 보니 단서를 찾았습니다.


2.4 절에 보면 다음과 같은 내용이 나옵니다.


ABI 는 Vulkan 이 응용프로그램을 플랫폼 혹은 구현측( implementation )에 맞춰 정의될 수 있도록 해 주는 메카니즘입니다. 다양한 플랫폼상에서, 이 명세에서 기술된 C 인터페이스는 공유 라이브러리에 의해서 제공됩니다. 공유 라이브러리들은 그것을 사용하는 응용프로그램과는 독립적으로 변경될 수 있기 때문에, 그것들은 특정한 호환성 문제를 겪게 되며, 이 명세는 그것들에 대한 요구사항을 제시합니다.


공유 라이브러리 구현은 반드시 그 플랫폼을 위한 표준 C 컴파일러의 Application Binary Interface ( ABI ) 를 사용하거나, 응용프로그램 코드가 구현측의 non-default API 를 사용하도록 만드는 customized API 헤더를 제공해야만 합니다. 이 문맥에서 ABI 는 C 데이터 타입의 size, alignment, layout 을 의미합니다; 프로시저의 호출 규약( calling convention ); 그리고 C 함수와 연관되는 공유 라이브러리 심볼의 이름 규약( naming convention ). 플랫폼에 대해 호출 규약을 커스터마이징하는 것은 보통 vk_platform.h 에 있는 호출규약 매크로를 적절히 정의함으로써 가능합니다.


Vulkan 을 공유 라이브러리로서 제공하는 플랫폼에서, 라이브러리 심볼ㄹ은 "vk" 로 시작하고 구현측에서 사용하기 위해서 예약된 숫자와 대문자가 그 다음에 오게 됩니다. Vulkan 을 사용하는 응용프로그램은 절대 이런 심볼들에 대한 정의를 제공해서는 안 됩니다. 이렇게 해야, Vulkan 공유 라이브러리가 새로운 API 나 extension 을 위해 추가적인 심볼들을 갱신할 때, 그것들이 현재 존재하는 응용프로그램의 심볼과 충돌하지 않습니다.


출처 : [ 1 ].


즉 동일한 ABI 를 사용하기 위해서 C 를 사용한다는 것을 알 수 있습니다. 하지만 C++ 도 ABI 를 제공하는 것은 아닌데 왜 굳이 C 여야 하는지 의문이 생겼습니다.


C ABI vs C++ ABI


Application Binary Interface 는 Android 개발에 의해서 cross-platform 개발이 대중화되면서 잘 알려지기 시작했습니다. 저만해도 그때까지는 Windows Platform 만 대상으로 개발했기 때문에 ABI 존재를 알지 못했습니다. 


사실 Mobile 개발에 큰 관심은 없었기에 ABI 라는 게 Android 에서만 사용하는 개념인줄 알았습니다. "armeabi" 라든가 "armeabi-v7a", "arm64-v8a" 같은 것들을 봐도 별로 느낌이 안 왔습니다 [ 3 ]. 보통 UE4 나 Unity 3D 같은 엔진들이 자동화를 통해서 빌드해 주기 때문에, 그냥 체크 몇 개만 해도 되기 때문이었습니다. 하지만 Gradle 같은 것들을 사용해서 커스터마이징을 하다가 보니 이런 개념들에 대해서 몰라서는 일을 하기가 힘들어졌습니다. 이제는 "이런 개념들에 대해서 모르고 있다가는 밥줄이 끊기겠구나" 싶더군요. 


칸텐츠 개발자라면 모르겠지만 엔진 개발자들은 이런 주제에 대해서 공부해야 할 기회( 당위성 )가 생기면 간단하게라도 개념을 이해하고 넘어가야 겠다는 생각이 들었습니다. 그래서 이번 기회에 ABI 가 뭔지 확인해 보기로 했습니다.


Wikipedia 에서는 ABI 를 다음과 같이 정의합니다.


컴퓨터 소프트웨어에서, application binary interface( ABI ) 는 두 이진 프로그램 모듈간의 인터페이스를 의미합니다; 보통 이러한 모듈들 중 하나는 라이브러리나 운영체제 기능이며, 다른 하나는 사용자에 의해서 실행되고 있는 프로그램입니다.


ABI 는 데이터 구조와 계산 루틴( computational routine )들이 머신 코드에서 어떻게 접근되어야 하는지를 정의합니다. 이것은 저수준( low-level )이며 하드웨어 의존적인 포맷입니다; 반면에 API 는 이 접근이 소스 코드에서 이루어지고, 그것은 상대적으로 고수준( high-level )이며 상대적으로 하드웨어에 비의존적이며, 보통 인간이 읽을 수 있는 포맷입니다. ABI 에 대한 일반적인 관점은 호출 규약이며, 이는 데이터가 입력이나 계산 루틴으로부터의 출력으로서 어떻게 제공되어야 하는지를 정의합니다; 예를 들면 x86 호출 규약이 있습니다.


출처 : [ 4 ].


그런데 문제는 C++ ABI 의 경우에는 호환성 보장이 어렵다는 데 있습니다. 


같은 플랫폼에서 컴파일러 간에 C++ name mangling, exception propagation, calling convention 같은 세부사항을 표준화하는 ABI 들이 있기는 하지만 cross-platform 에서의 호환성을 요구하지는 마십시오.


C++ 은 method overloading 을 지원하기 때문에 같은 클래스 내에 같은 이름이 존재하는 것이 가능하죠. 그렇기 때문에 C++ 컴파일러는 name mangling 이라는 것을 수행하게 되는데, 이게 컴파일러마다 다릅니다. 아래 이미지는 [ 2 ] 에서 가지고 왔습니다. [ 2 ] 에서는 여러 가지 예제를 만들어서 C 와 C++ 에서 심볼 이름을 만들어 내는 과정에 대해서 자세히 설명하고 있습니다.



보시면 알겠지만 컴파일러마다 너무 이름이 달라서 심볼이름으로 검색할 때 차이가 발생할 수밖에 없음을 알 수 있습니다. 예를 들어서 Vulkan 에서 validation layer 에 대한 모니터 개체를 생성하려면 다음과 같이 해야 합니다.



만약 "vkCreateDebugUtilsMessengerEXT" 함수가 C++ mangling 규칙을 따르게 된다면 어떤 규칙으로 이름을 찾아야 하는지 알 수가 없게 되었을 겁니다.


하지만 C 는 mangling 규칙이라는 것이 존재하지 않습니다. 그냥 함수 이름 앞에 "_" 가 하나 붙게 되는 형식입니다. 이에 대해서 자세하게 알고자 한다면 [ 2 ] 를 확인하시는 것이 좋습니다.


Vulkan.hpp


호환성을 위해서 C 를 사용하는 것까지는 좋지만, 너무 불편합니다. 


그래서 Khronos 에서는 "Vulkan.hpp" 헤더를 통해서 C++ 을 지원하고 있습니다. 다음과 같이 header 를 include 할 수 있습니다.



만약 VULKAN_HPP_NAMESPACE 를 지정하지 않으면 기본값이 "vk" 가 됩니다. 왠지 멋이 없어서 저는 그냥 "Vulkan" 이라고 해 봤습니다.


다음과 같이 클래스로 메서드들이 encapsulation 되어 있으니 매우 편합니다.



그리고 enumeration 들도 wrapping 되서 짧아졌더군요.



매우 편하게 가지고 놀면 될 것 같습니다.


하지만 Extension 을 사용할 때는 external symbol link error 가 나는 경우가 있는 것 같더군요. 그쪽에서는 C++ 쪽 대응을 하지 않은 것으로 보입니다. 그래서 저같은 경우에는 DebugUtilMessenger 같은 걸 사용할 때는 그냥 C-style 로 작업을 했습니다.


Conclusion


Cross-Platform 개발을 위해서는 호환성을 유지하는 것이 매우 중요하므로, Vulkan 은 기본 컴파일러의 C ABI 를 사용한다는 결론을 내릴 수 있었습니다. 또한 개발의 편의성을 위해 C++ 래퍼 클래스를 제공하고 있기 때문에 C++ 을 선호하는 사람은 그것을 사용하면 편리합니다. 단점이 있다면 거의 대부분의 예제가 C API 를 사용한다는 것입니다.


References


[ 1 ] Vulkan 1.1.86 Specification, Khronos.


[ 2 ] C++ 상에서 발생하는 name mangling 에 관한 내용, 미카 프로젝트.


[ 3 ] ABI 관리, Android Developers.


[ 4 ] Application Binary Interface, Wikepedia.

+ Recent posts