Swift로 iOS개발을 하면서 UIKit, Foundation 등의 모듈을 import해서 사용하는데 이 import가 어떻게 수행되는지 한번 정리해보고 싶었다. 입사 동기분의 추천으로 한 유튜브 영상을 보게 되었는데, Swift에서 C & Objc, Swift 언어로 빌드된 library 또는 framework가 어떻게 import되는지 잘 설명해주고 있어 이를 정리해보고자 한다.
1. C or ObjC언어로 작성된 static library import하기.
아래 피보나치수열을 출력하는 코드를 C언어로 작성한 후 Swift에서 import해보도록 하자.
// CStaticLibrary.h
#ifndef CStaticLibrary.h
#define CStaticLibrary.h
#include <stdio.h>
#endif
void fibonacci(int n);
// CStaticLibrary.c
#import "CStaticLibrary.h" // #include "CStaticLibrary.h"와 동일
void fibonacci(int n) {
int first = 0, second = 1, next, i;
for (i = 0; i < n; i++) {
if (i <= 3) {
next = i;
} else {
next = first + second;
first = second;
second = next;
}
printf("%d, ", next);
}
printf("\n");
}
위의 코드를 빌드하기 위해 프로젝트 상위 디렉토리에 아래의 스크립트를 작성해주자. 여기에서는 buildC.sh라는 이름을 사용하였다.
#!/bin/bash
set -e
BASEDIR=$(dirname "$0")
PROJECT_NAME="CStaticLibrary"
PROJECT_PATH="$BASEDIR/$PROJECT_NAME/$PROJECT_NAME.xcodeproj"
DERIVED_DATA_PATH="$BASEDIR/.derivedData/$PROJECT_NAME"
ARCHIVE_PATH="$DERIVED_DATA_PATH/archives/$PROJECT_NAME/$PROJECT_NAME.xcarchive"
PRODUCTS_PATH="$BASEDIR/product"
echo_section() {
SECTION=$1
echo "\033[1;34m ********************************* $SECTION ********************************* \033[0m"
}
archive() {
PROJECT=$1
SCHEME=$2
CONFIGURATION=$3
SDK=$4
ARCH=$5
xcodebuild archive \
-project $PROJECT \
-scheme $SCHEME \
-derivedDataPath $DERIVED_DATA_PATH \
-archivePath $ARCHIVE_PATH \
-configuration $CONFIGURATION \
-sdk $SDK \
-arch $ARCH \
MACH_O_TYPE=staticlib \
SKIP_INSTALL=NO
}
echo_section "Cleaning: C"
rm -rf "$PRODUCTS_PATH/$PROJECT_NAME"
rm -rf "$DERIVED_DATA_PATH/$PROJECT_NAME"
mkdir -p "$PRODUCTS_PATH/$PROJECT_NAME"
mkdir -p "$DERIVED_DATA_PATH/$PROJECT_NAME"
echo_section "Archiving: C"
archive $PROJECT_PATH $PROJECT_NAME "Release" "iphonesimulator" "arm64"
echo_section "Copying files: C"
cp "$ARCHIVE_PATH/Products/usr/local/lib/lib$PROJECT_NAME.a" "$PRODUCTS_PATH/$PROJECT_NAME/lib$PROJECT_NAME.a"
cp "$BASEDIR/$PROJECT_NAME/$PROJECT_NAME/$PROJECT_NAME.h" "$PRODUCTS_PATH/$PROJECT_NAME/$PROJECT_NAME.h"
echo_section "Done: C"
작성한 쉘 스크립트를 실행하면 아래처럼 product/CStaticLibrary 디렉토리에 libCStaticLibrary.h 및 libCStaticLibrary.a파일이 생성된 것을 확인할 수 있다.
이제 생성된 헤더파일을 import하여 사용해야 하는데... Swift에서는 이 헤더파일을 바로 import할 수 없다. 대신, ObjC에서는 헤더파일을 import해서 사용할 수 있는데 https://developer.apple.com/documentation/swift/importing-objective-c-into-swift에 따르면 프로젝트에서 어떤 언어를 주로 사용하는지에 상관없이 Swift와 ObjC를 같이 사용하기 위해 bridging header를 사용하면 된다고 나와있다.
피보나치 함수를 호출하면 당연히 찾을 수 없다고 나온다. 프로젝트에 헤더 및 라이브러리를 추가 후 import할 수 있도록 해보자.
위의 이미지처럼 이전에 생성한 header 및 library를 추가해 준 후
File -> New -> File(또는 cmd n) -> Header File을 선택한 후 위의 이미지처럼 헤더파일을 추가해 주자. 브릿징 헤더를 수동으로 추가해 줄 경우 Build Settings에서 아래처럼 Objective-C Bridging Header를 세팅해주어야 한다.
그다음 이제 "libCStaticLibrary.h"를 include하도록 브릿징 헤더에 다음과 같이 코드를 추가하면 마찬가지로 헤더 파일을 찾을 수 없어 컴파일을 할 수 없다고 한다.
이를 해결하기 위해선 libCStaticLibrary.h의 경로를 명시해주어야 하는데, 헤더파일 경로는 project 디렉토리의 libraries/CStaticLibrary였으므로 Build Settings에서 다음처럼 header search path를 추가해 주자.
다시 빌드를 수행해 보면 아래처럼 undefine symbol 에러가 나면서 빌드가 실패한다.
컴파일 시 헤더파일은 잘 포함돼서 _fibonacci라는 심볼(undefined)이 있음을 확인했고, ViewController.swift의 viewDidLoad에서 호출할 수 있지만, LD(링커)의 링킹 과정에서 이 심볼을 Resolve시켜주지 못했기 때문에 위와 같은 오류가 발생한다. 이를 해결하기 위해서는 libCStaticLibrary.a파일을 링커에 전달해주어야 한다.
링커에 라이브러리를 전달해 주기 위해서는 -I(대문자 i) 플래그를 이용해 포함할 라이브러리의 경로를 지정해주어야 하고, -l(소문자 L) 플래그를 이용해 포함할 라이브러리 이름을 명시해주어야 한다. (libCStaticLibrary.a 의 경우 앞의 lib와 뒤의 .a를 제외한 CStaticLibrary를 전달하기 위해 -lCStaticLibrary)
이 과정을 Xcode에서 자동으로 수행해 주게끔 하기 위해 경로 및 이름을 Build Settings에 명시해주어야 한다.
-I(소문자 i)의 경우 Library Search Path에 전달해 주면 되고
-l(소문자 L)의 경우 Other Linker Flag에 전달해주면 된다.
이제 빌드 후 실행(Run)해보면 빌드 성공 및 피모나치 수열이 정상적으로 출력되는 것을 확인할 수 있다.
2. Swift언어로 작성된 static library import하기.
앞서 C언어에서 작성했던 피보나치수열 출력 함수를 Swift로 작성한 후 Static Library로 빌드하여 다른 Swift 프로젝트(Example)에서 import 하도록 해보자.
import Foundation
public func fibonacci(_ n: Int) {
var first: Int = 0
var second: Int = 1
var next: Int
for i in 0..<n {
if (i <= 3) {
next = i
} else {
next = first + second
first = second
second = next
}
print("\(next), ", terminator: "")
}
print("Swift")
}
프로젝트 생성(Static Library) 후 위와 같이 코드를 작성해 준 후 아래의 쉘 스크립트를 작성하여 실행해 주자(섹션 1의 C와 비슷함.)
#!/bin/bash
set -e
BASEDIR=$(dirname "$0")
PROJECT_NAME="SwiftStaticLibrary"
PROJECT_PATH="$BASEDIR/$PROJECT_NAME/$PROJECT_NAME.xcodeproj"
DERIVED_DATA_PATH="$BASEDIR/.derivedData/$PROJECT_NAME"
ARCHIVE_PATH="$DERIVED_DATA_PATH/archives/$PROJECT_NAME/$PROJECT_NAME.xcarchive"
PRODUCTS_PATH="$BASEDIR/product"
echo_section() {
SECTION=$1
echo "\033[1;34m ********************************* $SECTION ********************************* \033[0m"
}
archive() {
PROJECT=$1
SCHEME=$2
CONFIGURATION=$3
SDK=$4
ARCH=$5
xcodebuild archive \
-project $PROJECT \
-scheme $SCHEME \
-derivedDataPath $DERIVED_DATA_PATH \
-archivePath $ARCHIVE_PATH \
-configuration $CONFIGURATION \
-sdk $SDK \
-arch $ARCH \
MACH_O_TYPE=staticlib \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
SKIP_INSTALL=NO
}
echo_section "Cleaning: Swift"
rm -rf "$PRODUCTS_PATH/$PROJECT_NAME"
rm -rf "$DERIVED_DATA_PATH/$PROJECT_NAME"
mkdir -p "$PRODUCTS_PATH/$PROJECT_NAME"
mkdir -p "$DERIVED_DATA_PATH/$PROJECT_NAME"
echo_section "Archiving: Swift"
archive $PROJECT_PATH $PROJECT_NAME "Release" "iphonesimulator" "arm64"
echo_section "Copying files: Swift"
cp "$ARCHIVE_PATH/Products/usr/local/lib/lib$PROJECT_NAME.a" "$PRODUCTS_PATH/$PROJECT_NAME/lib$PROJECT_NAME.a"
cp -r "$DERIVED_DATA_PATH/Build/Intermediates.noindex/ArchiveIntermediates/$PROJECT_NAME/BuildProductsPath/Release-iphonesimulator/$PROJECT_NAME.swiftmodule" "$PRODUCTS_PATH/$PROJECT_NAME/$PROJECT_NAME.swiftmodule"
echo_section "Done: Swift"
위의 쉘 스크립트를 실행해 보면 C와 마찬가지로 product/SwiftStaticLibrary 디렉토리에 아래와 같이 파일이 생성되어 있음을 확인할 수 있다.
C(또는 ObjC)와 달리 header파일이 아닌 모듈 파일이 생성되어있음을 확인할 수 있다. ObjC는 header파일과 module을, Swift에서는 module만을 import해서 사용할 수 있는데, 이렇게 사용되는 문법은 다음과 같다.
@import UIKit; // ObjC
import UIKit // Swift
C, ObjC에서 사용되는 #include 또는 #import의 경우 전처리(preprocessing) 과정을 거치도록 되어있는데, 모듈의 경우 프레임워크 또는 라이브러리가 Clang에서 import할 수 있도록 편의를 제공한다고 한다.
C, ObjC 라이브러리를 사용할 때와 마찬가지로 libraries 디렉토리에 빌드된 SwiftStaticLibrary를 추가해 준 후 Build Settings의 값들을 세팅해주어야 한다.
1. Library Search Path: -I(대문자 i) = $(PROJECT_DIR)/libraries/SwiftStaticLibrary
2. Other Linker Flags: -l(소문자 L)SwiftStaticLibrary
3. Header Search Path: 헤더파일이 없는데....?
이렇게 세팅 후 아래와 같이 SwiftStaticLibrary를 import하면 SwiftStaticLibrary를 찾을 수 없다는 에러를 출력한다.
Swift에서 모듈을 import하기 위해서는 Header Search Path가 아닌 Build Settings의 Import Paths(SWIFT_INCLUDE_PATHS)를 아래와 같이 설정해주어야 한다.
이제 마지막으로 ViewController의 코드를 다음과 같이 실행하면
CStaticLibrary의 fibonacci 함수가 호출되지 않고 SwiftStaticLibrary의 함수가 호출된다.
먼저 CStaticLibrary.a의 심볼을 확인해 보면 다음과 같고
libCStaticLibrary.a(CStaticLibrary.o):
0000000000000080 s LC1
0000000000000000 T _fibonacci
U _printf
U _putchar
SwiftStaticLibrary.a의 심볼을 확인해보면
libSwiftStaticLibrary.a(SwiftStaticLibrary.o):
0000000000000230 s EH_Frame1
0000000000000000 T SwiftStaticLibrary.fibonacci(Swift.Int) -> ()
...
U Swift.String.append(Swift.String) -> ()
U type metadata for Swift.String
U type metadata for Swift.Int
U protocol conformance descriptor for Swift.Int : Swift.BinaryInteger in Swift
U (extension in Swift):Swift.BinaryInteger.description.getter : Swift.String
U nominal type descriptor for Swift._ContiguousArrayStorage
U Swift.print(_: Any..., separator: Swift.String, terminator: Swift.String) -> ()
...
이렇게 되어있는데... 호출의 우선순위가 있는 건지.. 아니면 심볼이 겹쳤을 경우의 문제가 있는 건지는 아직 정확히 파악을 못했다.
혹시나 아시는 분 계시면 알려주세요.
References
https://youtu.be/lGG0UPdvc54?t=1977
https://useyourloaf.com/blog/modules-and-precompiled-headers/
https://developer.apple.com/documentation/swift/importing-objective-c-into-swift
'iOS' 카테고리의 다른 글
(iOS) 유니플로거 리팩토링(2) 튜토리얼 (0) | 2023.06.07 |
---|---|
(iOS) 유니플로거 리팩토링(1) XCFramework (4) | 2023.05.07 |
(iOS) Library vs Framework(3) (0) | 2023.04.06 |
(iOS) Library vs Framework(2) (0) | 2023.03.07 |
(iOS) Library vs Framework(1) (1) | 2023.02.18 |