Swift는 어떻게 빌드되고, Objective-C와 어떻게 같이 쓰이는가?

글쓴이 연유 날짜

Swift가 메인이 된 이후에 iOS 개발을 시작한 사람으로서, 항상 Objective-C와 Swift가 함께 쓰이는 게 당연하게 느껴지기도 하면서, 어떻게 서로 다른 언어를 고통 없이(신경 써야 할 부분은 있겠지만요.) 섞어서 쓸 수 있는지 궁금했습니다. 이번 문서에서는 대략적으로 어떻게 두 언어가 잘 섞여서 쓰일 수 있는지 다뤄보려고 합니다.

Xcode에서 프로젝트가 어떻게 빌드 되는가?

만약에 Swift와 Objective-C가, Java와 Swift처럼 서로 호환되지 않는 언어였다면 지금과는 달리 서로 같이 이용하기가 매우 어려웠을 것입니다. 예를 들어서, Swift에서 Java 코드를 호출하려면 (실제로 그런 식으로 이용하는 프로젝트가 있는지는 모르겠지만) 무언가 서로 통신을 하기 위해서 여러 방법으로 메모리에 파일을 쓰고 읽어야 하겠지요. 그러나, Swift와 Objective-C는 마지 한 언어인 것처럼 서로 호환하여 쓸 수 있습니다. 어떻게 이런 일이 일어날까요? Xcode가 앱을 빌드하는 과정을 보면 이해할 수 있습니다.

빌드의 단계

Xcode에서 프로젝트는 Preprocessor -> Complier -> Assembler -> Linker -> Loader 순으로 빌드됩니다.

https://www.vadimbulavin.com/xcode-build-system/

Preprocessor

컴파일을 하기 전에 전처리기 단계가 실행됩니다. 전처리기 단계에서는 다음과 같은 작업을 합니다.

  • 매크로를 실제 정의로 바꾼다. (eg. #Define 문)
  • 파일 참조 관계(종속성)를 파악한다. (eg. #include )
  • 컴파일 조건문을 파악한다. (eg. #if ~, #if else, #endif)

그러나, 스위프트를 빌드할 때는 전처리기 작업이 없습니다. 따라서 스위프트에는 매크로가 존재하지 않죠. 그런데, 매크로는 그렇다 쳐도, 파일 참조 관계와 컴파일 조건문은 필요하기도 하고, 실제로 쓰고 있잖아요? 어떻게 되는 걸까요?

먼저 파일 참조 관계에 대해서 llbuild(low-level build system)이라는 것을 사용하고 있습니다. 말씀드렸다시피, Objective-C에서는 전처리 단계가 존재하기 때문에, 프로젝트도 Swift-llbuild라고 이름지어져 있네요.

또한, 컴파일 조건문을 파악하기 위해서 Active Compilation Conditions 과 같은 설정을 스위프트 컴파일러에서 추가적으로 제공하고 있습니다.

위와 같은 Build setting이 되어 있기 때문에 DEBUG 타겟일 때, #If DEBUG와 같은 문구로 특정 코드를 삽입할 수 있겠죠.

Compiler

https://stackoverflow.com/questions/2354725/what-exactly-is-llvm

Xcode에서 컴파일러 단계는 LLVM을 거쳐서 실행되게 됩니다. LLVM은 프론트엔드->(미들엔드)->백엔드의 단계를 거쳐서 컴파일을 진행하게 되는데요, Xcode에서는 LLVM이 어셈블리어를 만들 목적코드를 생성하기 위해 Swift ComplierClang의 프론트엔드를 가지고 있습니다. 이 두 가지 프론트엔드는, 심볼 테이블을 가지고 코드가 가진 의미를 잃지 않으면서 LLVM이 어셈블리어로 번역을 해줄 중간 코드인 Intermediate Representation을 생성합니다. 즉, 제일 처음 궁금했던 점인, 어떻게 스위프트와 Objective-C가 개발자 입장에서 별 고생없이 쓰이냐는 질문은 결국엔 두 언어 모두 LLVM을 위한 중간 언어로 변환된 후에, 실제 어셈블리어로 변환되기 때문이라고 이해할 수 있겠네요.

Swift Complier의 경우

Swift Complier 페이지에 각 컴파일 단계에 대한 간단한 설명이 적혀있습니다. 프론트엔드단인 Swift Compiler에서는 크게 보면 내가 작성한 Swift 코드가 분석되어 Swift Intermediate Language로 변환되고, 다시 한 번, LLVM의 컴파일 대상인 LLVM IR로 변환되어 백엔드에 넘어간다고 할 수 있습니다.

  • Parsing
    문구 분석을 통해서 추상 문법 트리(AST)를 만듭니다. 이 과정에서는 따로 의미나 type을 추출하지는 않습니다. 입력에 대한 문법적 경고나 에러를 출력할 수 있습니다.
  • Semantic analysis
    위에서 파싱한 AST를 가지고, 정돈되고 타입이 확인된 AST를 만듭니다. 입력에 대해 의미적인 경고나 에러를 출력할 수 있습니다. 타입 추론을 포함하며, 이 과정이 성공하였다는 것은 타입이 확인된 AST에서 안전하게 코드(SIL)를 생성할 수 있다는 것을 의미합니다.
  • Clang importer
    C와 Objective-C API를 Swift API에 매핑합니다. (주: bridge로 Objective-C의 모듈을 임포트할 때, Swift 형식의 API로 쓸 수 있게 되는 데, 이 과정에 의해 그렇게 되는 것 같습니다.)
  • SIL generation
    타입이 확인된 AST로 스위프트를 위한 고수준의 중간 언어(Swift Intermediate Language)를 만듭니다. SIL는 분석이나 최적화에 적합한 스위프트 코드입니다. https://github.com/apple/swift/blob/master/docs/SIL.rst
  • SIL guaranteed transformations
    이 과정에서 프로그램의 정확성에 대한 추가적인 데이터 플로우 진단을 할 수 있습니다. 결과적으로 표준 SIL이 되게 됩니다.
  • SIL Optimizations
    SIL을 이용하여 Swift를 위해 고수준에서 최적화를 진행합니다. ARC와 devirtualization, Generic specialization 등을 포함합니다.
    (주: ARC의 경우에는 다들 아는 개념일 것 같고, Devirtualization은 상속에 따른 V-table 참조에 대한 최적화입니다. 예를 들어서 final keyword를 이용하여 더 이상 상속이 불가능함을 명시하면 이 절차에 의해 클래스임에도 더 이상의 서브클래스가 없기 때문에 function, variable 등을 직접 참조하는 최적화를 할 수 있게 됩니다. 또한, Generic spetialization은 프로토콜의 Witness table의 최적화로 보입니다.)
  • LLVM IR Generation
    SIL가 바로 어셈블리어가 되지 않기 때문에, 백엔드에 줄 LLVM IR를 만들고, 최종 어셈블리어 생성을 LLVM 백엔드에 위임합니다.

Assembler

어셈블러의 역할은 어셈블리어로 작성된 코드를 기계어로 번역하는 것입니다. 결과적으로 Mach-O 파일이 생성되는데, 이는 코드와 데이터를 포함한 Darwin 기반 OS(MacOS, iOS, ..)의 바이너리 파일입니다. 이때, Xcode assembler는 절대 주소에 대한 기계어가 아닌, 상대 주소를 지원할 수 있는 기계어 파일을 만듭니다.

Linker

링커는 다양한 객체 파일과 라이브러리를 하나의 Darwin 기반 OS 에서 실행될 수 있는 Mach-O 실행파일로 만들어줍니다. 오브젝트 파일은 어셈블러 과정에서의 산물이거나 다양한 타입(.dylib, .tbd, .a)의 라이브러리로 구성됩니다. 어셈블러와 링커 모두 Mach-O 파일을 만드는데요, 링커의 경우에는 어셈블러가 만든 Mach-O 파일에 다른 파일 또는 라이브러리의 참조 관계에 따라 최종 결합을 해서 완성본인 Mach-O 파일을 만들어준다고 보면 될 것 같습니다.

Loader

로더는 빌드 단계라기 보다는 실행의 단계인데요. 운영체제 단에서 프로그램을 메모리에 올리고 레지스터를 초기화합니다.

두 언어를 어떻게 섞어서 쓰는가?

Swift에서 Objective-C 모듈을 사용하기

Swift 프로젝트에서 Objective-C 파일을 추가하거나 반대의 경우에 Objective-C 브릿지 헤더를 만들거냐고 물어봅니다. (만들지 않아도 수동으로 만들고, build setting에서 설정해줄 수 있습니다.) 이 파일을 만들고 난 후에, 내가 작성한 Objective-C 파일 헤더를 브릿지 헤더에 추가하면 스위프트에서도 Objective-C 모듈을 불러올 수 있습니다. (브릿지 헤더와 Swift 코드가 같은 타겟일 경우에 별도로 Objective-C 브릿지 헤더를 임포트할 필요는 없어요.) (브릿지 헤더에 불러오고자 하는 Objective-C 헤더를 추가하지 않으면, Swift 코드에서 알 수가 없습니다.)

Importing Objective-C into Swift

Objective-C에서 Swift 모듈 사용하기

Xcode의 타겟의 Build Setting에서 Objective-C GeneratedInterface Header Name을 설정할 수 있습니다. 스위프트 모듈들의 인터페이스가 Objective-C에서 쓰일 수 있도록 헤더로 작성이 되고, 원하는 Objective-C 파일에서 해당 파일을 import 해주면 됩니다. (타겟의 해당 파일에서 자동으로 관리되니까, swift 파일마다 헤더를 만들지 않아도 되네요!)

또한 같은 타겟 안에서, Objective-C 헤더에 Swift 코드의 클래스나 프로토콜을 이용할 경우 순환 참조가 생기게 되어 Forward Declarations으로 @클래스명, @프로토콜명과 같이 먼저 선언을 해주어야 한다고 합니다.

Importing Swift into Objective-C

참고자료

Xcode build system

Mach-O

C나 C++로 파이썬 확장하기

마샬링 (컴퓨터 과학) – 위키백과, 우리 모두의 백과사전

업데이트 중

카테고리: 미분류