본문 바로가기
C.W.K.
Stream
Lesson 02 of 06 · published

Swift가 C를 불러야 할 때

~15 min · packages-projects, c-interop, system-libraries, swiftpm

Level 0번들 열어본 사람
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"import부터 해봐. C 타깃은 Swift가 정말 못 보는 것에 쓰는 거야."

C가 아직도 나오는 이유

프레임워크 아래로 내려가면 macOS는 상당 부분이 C API야. 프로세스 정보는 libproc, 압축 파일은 libarchive가 맡고, sqlite3, POSIX 소켓, forkpty도 다 C야. Swift는 SDK의 모듈 맵을 통해 C 헤더를 많이 바로 가져와서, import Darwin만으로도 꽤 많은 게 돼. 근데 예외가 있어. 깔끔한 Swift 모듈로 안 드러나는 헤더도 있고, 함수처럼 생긴 매크로는 아예 안 들어와(WIFEXITED는 Swift에서 그냥 안 보여). 고정 크기 C 배열은 다루기 어색한 튜플로 들어오고. 아예 서드파티인 라이브러리도 있지. 이럴 때 쓰라고 SwiftPM은 패키지 안에 Swift 타깃과 나란히 C 타깃을 둘 수 있게 해줘.

C 타깃의 구조

  • Sources/CSparkProc/ 같은 타깃 폴더에 .c 파일들을 넣고, 공개 헤더는 include/ 밑에 둬(publicHeadersPath 기본값이야).
  • SwiftPM이 모듈 맵을 만들어주니까, CSparkProc에 의존하는 Swift 타깃은 그냥 import CSparkProc라고 쓰면 돼.
  • C 쪽으로 드러나는 부분은 작고 밋밋하게 둬. 함수도 타입도 평범하게, 실패는 반환 코드로 알리고, 해석은 전부 Swift 쪽에서 해.

가족의 터미널 앱은 새 창을 같은 위치에서 열려고, 창마다 도는 셸의 작업 디렉터리를 알아야 했어. 답은 proc_pidinfoPROC_PIDVNODEPATHINFO를 주는 거였고, 앱은 libproc를 Swift에서 import할 수 없다고 믿고 공개 함수 하나짜리 C 파일 타깃으로 감쌌어. 근데 Swift 6.3.3과 macOS 26.5 SDK로 재보니 그 믿음은 틀렸어. import Darwin만으로 두 이름이 다 보이고, 호출도 컴파일돼서 작업 디렉터리를 돌려줘. 그 C 타깃은 가상 터미널의 전경 프로세스 그룹을 찾는 호출까지 담은, 작고 테스트하기 쉬운 C 표면으로 여전히 쓸모가 있어. 그래도 배울 건 순서야. import부터 해보고, 안 되는 것만 C로 써. 파일 작업대 앱도 같은 방식으로 libarchive를 감쌌고, 시스템 라이브러리는 linkerSettings: [.linkedLibrary("archive")]로 링크했어.

링크, 그리고 여기서 넘지 말아야 할 선

macOS에 딸려 오는 시스템 라이브러리는 .linkedLibrary(sqlite3, archive)나 .linkedFramework(AppKit, PDFKit)로 링크해. Homebrew로 깐 서드파티 라이브러리는 얘기가 달라. 그걸 링크해서 swift build가 통과해도, 증명되는 건 Mac에 그 라이브러리가 있다는 것뿐이야. 배포하려면 의존성에 딸린 dylib를 전부 앱 안에 넣고, 불러오는 경로를 고치고, 하나하나 서명해야 해. 가족의 네이티브 비디오 플레이어가 딱 그렇게 하는데, 자세한 이야기는 그 앱의 퀘스트(/cwk-quests/ashen-reel-quest)에 나와. 이 레슨은 OS가 이미 주는 C까지만 다뤄.

반대 방향

Swift 6.3에 @c 속성이 생겼어. Swift 함수나 enum을 C에 내보내고, 거기 맞는 헤더 선언까지 만들어줘. 앱에서 쓸 일은 드물지만, 같은 프로젝트의 C 코드에 Swift 구현을 넘기는 공식적인 방법이 이제 이거야.

Code

Swift 타깃 옆에 둔 파일 하나짜리 C 타깃·text
SparkKit/
  Package.swift                  .target(name: "CSparkProc")
  Sources/
    CSparkProc/
      include/CSparkProc.h       the public header Swift will see
      CSparkProc.c               the only file that includes <libproc.h>
    SparkMac/
      ProcessDirectory.swift     import CSparkProc
C 쪽은 작게, 해석은 Swift 쪽에서·swift
// Sources/CSparkProc/include/CSparkProc.h
//   #include <sys/types.h>
//   #include <stddef.h>
//   int spark_process_cwd(pid_t pid, char *buffer, size_t length);
//
// Sources/CSparkProc/CSparkProc.c
//   #include "CSparkProc.h"
//   #include <libproc.h>
//   #include <string.h>
//   int spark_process_cwd(pid_t pid, char *buffer, size_t length) {
//       struct proc_vnodepathinfo info;
//       int size = proc_pidinfo(pid, PROC_PIDVNODEPATHINFO, 0, &info, sizeof(info));
//       if (size != sizeof(info) || length == 0) { return -1; }
//       strncpy(buffer, info.pvi_cdir.vip_path, length - 1);
//       buffer[length - 1] = '\0';
//       return 0;
//   }

// Sources/SparkMac/ProcessDirectory.swift
#if os(macOS)
import Foundation
import CSparkProc

public enum ProcessDirectory {
    /// The working directory of another process, through a one-file C shim over libproc.
    public static func current(of pid: pid_t) -> String? {
        var buffer = [CChar](repeating: 0, count: Int(MAXPATHLEN))
        guard spark_process_cwd(pid, &buffer, buffer.count) == 0 else { return nil }
        return buffer.withUnsafeBufferPointer { String(cString: $0.baseAddress!) }
    }
}
#endif

External links

Exercise

SparkKit에 CSparkProc 타깃이랑 ProcessDirectory를 넣어. 그리고 ProcessDirectory.current(of: getpid())FileManager.default.currentDirectoryPath를 나란히 찍는 작은 cwd-probe 실행 파일을 만들어. 서로 다른 폴더 두 곳에서 돌려서 두 값이 같은지 확인해. 이어서 다른 터미널 창에 있는 셸의 pid를 넘기고, 그 셸에서 cd하면 값이 따라 바뀌는지 봐. 끝으로 import Darwin만 두고 Swift에서 proc_pidinfo를 바로 불러서 같은 디렉터리가 나오는지 확인하고, WIFEXITED도 불러서 에러를 읽어봐.
Hint
pgrep -n zsh로 제일 최근 zsh pid를 얻을 수 있어. 다른 사용자의 프로세스에 대해 C 호출이 -1을 돌려주면, 래퍼 버그가 아니라 커널이 거절한 거야. 짐작으로 넘기지 말고, Swift 반환값이 그렇다고 말하게 해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.