"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_pidinfo에 PROC_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 구현을 넘기는 공식적인 방법이 이제 이거야.