🔥 시스템 라이브러리 사용하기

1080자
13분

Swift 패키지 매니저에서 시스템 라이브러리를 연결하려면 패키지 매니저의 특별한 .systemLibrary라는 target과 사용할 각 시스템 라이브러리의 module.modulemap 파일이 필요해요.

그럼 libgit2 라이브러리를 실행 파일 타겟의 의존성으로 추가하는 예제를 같이 살펴보죠.

먼저 example이라는 폴더를 만들고, 실행 파일을 빌드하는 패키지로 초기화합니다.

$ mkdir example
$ cd example
example$ swift package init --type executable
text

그리고 Sources/example/main.swift 파일을 다음과 같이 고쳐 보세요.

import Clibgit
 
let options = git_repository_init_options()
print(options)
swift

import Clibgit을 사용하려면 시스템 패지지 매니저(apt, brew, yum, nuget 등)가 libgit2 라이브러리를 설치해야 해요. libgit2 시스템 패키지에서 우리가 관심 있는 파일은 다음과 같습니다.

/usr/local/lib/libgit2.dylib      # Linux에서는 .so
/usr/local/include/git2.h
text

참고: 시스템 라이브러리는 다음과 같이 시스템의 다른 곳에 있을 수 있어요.

  • /usr/ 또는 Apple Silicon Mac에서 Homebrew를 사용하는 경우 /opt/homebrew/
  • Windows에서 vcpkg를 사용하는 경우 C:\\vcpkg\\installed\\x64-windows\\include
    대부분의 유닉스 계열 시스템에서는 pkg-config를 사용하여 라이브러리가 설치된 위치를 찾을 수 있죠.
example$ pkg-config --cflags libgit2 -I/usr/local/libgit2/1.6.4/include
text

먼저 패키지 설명에서 **target**을 정의해 볼까요?

// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.
 
import PackageDescription
 
let package = Package(
    name: "example",
    targets: [
        // systemLibrary는 시스템 라이브러리를 감싸는 특수한 빌드 타겟으로,
        // 다른 타겟에서 의존성으로 요구할 수 있어요.
        .systemLibrary(
            name: "Clibgit",
            pkgConfig: "libgit2",
            providers: [
                .brew(["libgit2"]),
                .apt(["libgit2-dev"])
            ]
        )
    ]
)
swift

참고: Windows 전용 패키지라면 pkg-config를 사용할 수 없으니까 pkgConfig를 빼야 해요. pkgConfig 매개변수를 사용하지 않으려면 패키지 빌드할 때 명령줄에서 -L 플래그를 사용해서 라이브러리가 있는 폴더 경로를 알려줄 수 있습니다.

example$ swift build -Xlinker -L/usr/local/lib/
text

다음으로 example 프로젝트에 Sources/Clibgit 폴더를 만들고 module.modulemap과 헤더 파일을 추가하죠.

module Clibgit [system] {
	header "git2.h"
	link "git2"
	export *
}
text

헤더 파일은 이렇게 생겼습니다.

// git2.h
#pragma once
#include <git2.h>
c

참고: 라이브러리에서 주는 git2.h에 절대 경로를 지정하지 않도록 조심하세요. 그러면 파일 시스템 구조가 다르거나 다른 경로에 라이브러리를 설치하는 시스템 간 호환성이 깨질 수 있거든요.

우리가 커뮤니티가 채택하면 좋겠다고 생각하는 규칙은 이런 모듈 이름 앞에 C를 붙이고 Swift 모듈 이름 규칙에 따라 모듈 이름을 카멜케이스로 짓는 거예요.
그러면 커뮤니티는 원시 C 인터페이스 주변에 더 "Swifty"한 함수 래퍼를 담은 libgit이라는 또 다른 모듈에 자유롭게 이름을 지을 수 있습니다.

이제 example 폴더 구조는 이렇게 될 거예요.

 .
├── Package.swift
└── Sources
	├── Clibgit
	│   ├── git2.h
	│   └── module.modulemap
	└── main.swift
text

여기까지 잘 따라오셨나요? 어렵지 않죠?

이제 시스템 라이브러리 타겟이 완전히 정의되었으니, Package.swift의 다른 타겟에서 이 타겟을 의존성으로 사용할 수 있어요.

 
import PackageDescription
 
let package = Package(
    name: "example",
    targets: [
        .executableTarget(
            name: "example",
 
            // example 실행 파일은 "Clibgit" 타겟을 의존성으로 요구해요.
            // 아래에 정의된 systemLibrary 타겟이죠.
            dependencies: ["Clibgit"],
            path: "Sources"
        ),
 
        // systemLibrary는 시스템 라이브러리를 감싸는 특수한 빌드 타겟으로,
        // 다른 타겟에서 의존성으로 요구할 수 있어요.
        .systemLibrary(
            name: "Clibgit",
            pkgConfig: "libgit2",
            providers: [
                .brew(["libgit2"]),
                .apt(["libgit2-dev"])
            ]
        )
    ]
)
swift

자, 이제 예제 앱 폴더에서 swift build를 입력하면 실행 파일이 만들어질 거예요.

example$ swift build

example$ .build/debug/example
git_repository_init_options(version: 0, flags: 0, mode: 0, workdir_path: nil, description: nil, template_path: nil, initial_head: nil, origin_url: nil)
example$
text

pkg-config 없이 시스템 라이브러리 사용하기

실행 파일에서 IJG의 JPEG 라이브러리를 사용하는 또 다른 예제를 살펴보겠습니다. 이 예제에는 좀 주의해야 할 점이 있어요.

example이라는 폴더를 만들고, 실행 파일을 빌드하는 패키지로 초기화합니다.

$ mkdir example
$ cd example
example$ swift package init --type executable
text

Sources/main.swift를 이렇게 고쳐 봅시다.

import CJPEG
 
let jpegData = jpeg_common_struct()
print(jpegData)
swift

macOS에서는 Homebrew 패지지 매니저로 JPEG 라이브러리를 설치할 수 있어요: brew install jpeg.
jpeg는 keg 전용 수식이라서 /usr/local/lib에 연결되지 않으니까 빌드할 때 직접 연결해 줘야 합니다.

이전 예제처럼 mkdir Sources/CJPEG를 실행하고 다음 module.modulemap을 추가하세요.

module CJPEG [system] {
	header "shim.h"
	header "/usr/local/opt/jpeg/include/jpeglib.h"
	link "jpeg"
	export *
}
text

같은 폴더에 shim.h 파일을 만들고 #include <stdio.h>를 추가하죠.

$ echo '#include <stdio.h>' > shim.h
text

jpeglib.h가 온전한 모듈이 아니기 때문이에요. 필요한 #include <stdio.h> 줄이 없거든요. 아니면 shim.h 파일을 안 만들고 jpeglib.h 맨 위에 #include <stdio.h>를 추가해도 돼요.

이제 CJPEG 패키지를 쓰려면 예제 앱의 Package.swift에 의존성을 선언해야 해요.

 
import PackageDescription
 
let package = Package(
    name: "example",
    targets: [
        .executableTarget(
            name: "example",
            dependencies: ["CJPEG"],
            path: "Sources"
            ),
        .systemLibrary(
            name: "CJPEG",
            providers: [
                .brew(["jpeg"])
            ])
    ]
)
swift

자, 이제 예제 앱 폴더에서 swift build를 실행해 보면 실행 파일이 만들어집니다.

example$ swift build -Xlinker -L/usr/local/jpeg/lib

example$ .build/debug/example
jpeg_common_struct(err: nil, mem: nil, progress: nil, client_data: nil, is_decompressor: 0, global_state: 0)
example$
text

libjpeg용 pkg-config 파일이 없어서 -Xlinker로 libjpeg가 있는 경로를 알려줘야 해요. 나중에 명령줄에서 플래그를 안 써도 되는 방법을 제공할 계획이에요.

여러 라이브러리를 제공하는 패키지

어떤 시스템 패키지는 여러 개의 라이브러리(.so.dylib 파일)를 제공합니다. 이럴 때는 그 Swift 모듈맵 패키지의 .modulemap 파일에 모든 라이브러리를 추가해야 해요.

module CFoo [system] {
	header "/usr/local/include/foo/foo.h"
	link "foo"
	export *
}

module CFooBar [system] {
	header "/usr/include/foo/bar.h"
	link "foobar"
	export *
}

module CFooBaz [system] {
	header "/usr/include/foo/baz.h"
	link "foobaz"
	export *
}
text

foobarfoobazfoo에 연결되어 있어요. 헤더 foo/bar.hfoo/baz.hfoo/foo.h가 모두 들어 있으니까 모듈맵에 이 정보를 안 적어도 돼요. 그런데 이 헤더들이 의존하는 헤더를 포함하는 게 아주 중요합니다. 그렇지 않으면 모듈을 Swift로 가져올 때 의존하는 모듈이 자동으로 가져와지지 않고 링크 오류가 나요. 패키지를 쓰는 사람한테 이런 링크 오류가 나면 디버깅하기 매우 어려울 수 있거든요.

크로스 플랫폼 모듈 맵

모듈 맵에는 절대 경로가 들어가야 해서 크로스 플랫폼이 안 돼요. 우리는 패지지 매니저에서 이 문제를 해결할 방법을 제공할 계획이에요. 장기적으로는 시스템 라이브러리와 시스템 패키저가 모듈 맵을 제공해서 패지지 매니저의 이 부분이 필요 없어지길 바라고 있어요.

주목할 점은 Homebrew로 JPEG와 JasPer를 설치하면 위의 단계가 안 된다는 거예요. Intel Mac에서는 파일이 /usr/local에 설치되고 Apple Silicon Mac에서는 /opt/homebrew에 설치되거든요. 지금은 경로를 맞춰 주세요. 하지만 앞에서 말했듯 이런 기본적인 재배치를 지원할 계획이에요.

모듈 맵 버전 관리

모듈 맵의 버전을 의미 있게 관리하세요. 여기서 의미 있는 버전의 의미가 좀 덜 명확하니까 여러분의 최선의 판단을 사용하세요. 모듈 맵이 나타내는 시스템 라이브러리의 버전을 따라가지 마세요. 모듈 맵의 버전을 독립적으로 관리하세요.

시스템 패키저의 규칙을 따르세요. 예를 들어 python3용 데비안 패키지는 python3라고 불려요. python용 단일 패키지가 없고 python이 나란히 설치되도록 설계되었거든요. python3의 모듈 맵을 만든다면 CPython3라고 이름 지어야 해요.

선택적 의존성이 있는 시스템 라이브러리

지금으로서는 선택적 의존성으로 빌드된 시스템 패키지를 나타내려면 다른 모듈 맵 패키지를 만들어야 합니다.

예를 들어 libarchivexz에 선택적으로 의존해요. 즉, xz 지원을 사용해 컴파일할 수는 있지만 꼭 필요한 건 아니에요. xz를 사용하는 libarchive를 제공하는 패키지를 만들려면 CXz에 의존하고 CArchive를 제공하는 CArchive+CXz 패키지를 만들어야 해요.

lecture image

이렇게 해서 시스템 라이브러리를 Swift 패키지에서 사용하는 방법을 배웠습니다. 처음에는 좀 복잡해 보일 수 있지만, 차근차근 따라가다 보면 강력한 시스템 라이브러리를 Swift 코드에서 쉽게 활용할 수 있어요.

특히 크로스 플랫폼 모듈 맵이나 선택적 의존성 처리 같이 앞으로 더 개선되어야 할 부분도 있죠. 하지만 Swift 패지지 매니저 팀이 열심히 발전시켜 나갈 거예요.