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

config.h가 두 개인 이유

~15 min · c, swiftpm, header-search-path, licensing

Level 0빈 캔버스
0 XP0/37 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

이름이 같은 파일 둘

벤더링한 두 라이브러리는 각자 비공개 config.h를 기대해. 근데 둘은 생판 달라. libmypaint 쪽은 Ariandel이 손으로 쓴 세 줄이야(아래에 파일 전체가 있어). json-c 쪽은 업스트림 CMake가 iOS용으로 뽑아낸 결과물이고. 236줄에 #define 지시문이 64개, HAVE_DLFCN_H부터 버전 문자열까지 다 들어 있어.

충돌은 눈앞에 뻔히 있는데 잘 안 보여. libmypaint 파일엔 #define PACKAGE "libmypaint"가 있고, json-c 파일엔 #define PACKAGE "json-c"가 있어. 매크로는 같고 값은 둘. 어느 파일도 틀리지 않았어. 각자 라이브러리 하나를 빌드하는 데 필요한 사실일 뿐이야. 틀어지는 길은 하나뿐이야. 한 라이브러리가 다른 라이브러리의 사실을 보게 두는 것.

libmypaint는 이 선을 이미 스스로 그어놨어. 공개 조정값(MYPAINT_TILE_SIZE 64, 최대 스레드 수, 밉맵 단계)은 include/에 있는 공개 헤더 mypaint-config.h에 살아. 비공개 빌드 스위치는 config.h에 살고. 파일 이름이 누굴 위한 건지 말해줘.

각자 사는 곳

두 파일 다 자기 타깃 루트, .c 파일 바로 옆에 있어. include/엔 절대 안 들어가. 이 배치가 설계의 전부야. SwiftPM에서 그 두 디렉터리는 뜻이 완전히 다르거든.

  • C 타깃의 include/는 공개된 얼굴이야. 그 타깃에 의존하는 모든 타깃의 헤더 검색 경로에 올라가고, Swift 코드가 import하는 Clang 모듈이 돼.
  • 타깃 루트는 비공개야. .headerSearchPath(".")는 루트를 그 타깃 자신의 검색 경로에만 넣고, 다른 누구한테도 안 줘.

C 전처리기도 거들어. 따옴표 형식 #include "config.h"는 include하는 파일이 있는 디렉터리부터 먼저 뒤지고, 그다음에야 -I 목록으로 내려가. 자기 config.h 옆에 앉은 .c 파일은 다른 경로가 끼어들기 전에 자기 걸 찾아. (CMyPaint는 autotools 관례인 HAVE_CONFIG_H도 정의해. 근데 d5a88fb의 libmypaint 소스는 config.h를 조건 없이 include하니까, 어느 파일이 걸릴지는 결국 위치가 정해.)

Ariandel 하우스 사인엔 이게 함정 항목으로 적혀 있어. "libmypaint의 config.h와 json-c의 config.h는 서로 다른 타깃의 서로 다른 파일이다. 어느 쪽도 타깃의 공개 include/에 두지 마라. 그러면 Swift가 엉뚱한 매크로를 import한다."

망가지는 걸 직접 보기

이런 규칙은 한 번 깨지는 걸 봐야 지키기 쉬워. 그래서 아래 스케치는 일부러 실수를 만들어. C 타깃 둘, CJson과 CPaint가 있고 CPaint가 CJson에 의존해. 둘 다 PACKAGE를 정의하는 config.h가 있고, 그 값을 돌려주는 함수를 하나씩 내놔. 스크립트는 배치 두 개를 빌드해.

  • 누수(각 config.h가 자기 타깃의 include/에)는 json-c json-c를 찍어. CPaint 자기 코드가 json-c 값으로 컴파일된 거야. 에러도 경고도 없이.
  • 수정(각 config.h가 타깃 루트에)은 libmypaint json-c를 찍어.

누수 쪽은 왜 그렇게 될까. 내가 돌린 툴체인(Swift 6.4, Xcode 27)에선 CPaint 컴파일 줄에 의존 타깃의 공개 include/가 제일 먼저 오고, 그다음 자기 include/, 그다음 자기 루트 순서야. 누수 배치에선 paint.c 옆에 config.h가 없으니까 -I 순서가 승부를 내고, 의존 타깃이 이겨.

Swift도 그걸 봐. Swift 파일에 print(PACKAGE)를 넣어봐. 누수 배치에선 "ambiguous use of 'PACKAGE'"로 빌드가 실패해. 두 모듈이 다 그 매크로를 내보내니까. 수정 배치에선 "cannot find 'PACKAGE' in scope"로 실패하는데, 이게 바로 원하는 결과야. 비공개 빌드 사실은 Swift한테 안 보여야 하거든.

무서운 건 데모에서 조용히 넘어간 쪽이야. 아무것도 안 깨졌고 프로그램도 돌았는데, 답이 틀렸어. PACKAGE 자리에 기능을 켜는 매크로를 넣으면, 똑같이 아무 소리 없이 다른 프로그램이 나가.

검색 경로는 설계 대상이야

include/를 API처럼 다뤄. SwiftPM이 그렇게 다루니까. 거기 든 건 앱의 Swift까지 포함해서 모든 의존자한테 한 약속이야. Ariandel 자체 브리지가 그 절제를 보여줘. CAriandelSurface/include/엔 헤더가 딱 넷(ariandel_surface.h, ariandel_kernels.h, ariandel_pixels.h, ariandel_shader_types.h) 있고, 전부 Swift 엔진이 부르거나 같이 쓰라고 둔 거야. 빌드에만 필요한 사실은 자기 집, 타깃 루트에 있어.

라이선스 셋, GPL 코드는 없어

  • libmypaint: ISC. 관대한 라이선스야. 고지만 같이 따라가면 쓰고, 복사하고, 고치고, 배포해도 돼. COPYING이 벤더링 디렉터리에 들어 있어.
  • json-c: MIT. 같은 정신, 같은 종류의 고지.
  • 브러시: CC0. 앱엔 08da4a4 시점의 mypaint-brushes 라이브러리가 들어 있어. 일곱 세트에 프리셋 196개, 각자 미리보기, 그리고 order.conf. 파일 393개가 업스트림과 바이트까지 같아.

브러시는 한 번 더 들여다볼 만해. 라이선스 파일에 GPL이라는 글자가 있거든. 그건 업스트림의 기계 판독용 저작권 파일이고, 데비안 DEP-5 형식이야. 앱엔 MYPAINT-BRUSHES-LICENSES.dep5라는 이름으로 복사돼 있어. 맨 처음이 Files: *에 GPL-2+, 그다음이 Files: brushes/*에 CC0-1.0, 그 뒤로 작가별 브러시 디렉터리가 역시 CC0로 이어져. DEP-5 규칙은 이거야. 한 파일에 맞는 단락(stanza)이 여럿이면 마지막 걸 적용해. 그래서 Ariandel이 싣는 프리셋과 미리보기는 전부 CC0야. 포괄 조항인 GPL-2+가 덮는 건 업스트림 빌드 스크립트인데, 그건 한 번도 복사된 적이 없어. 벤더링한 libmypaint와 json-c 파일엔 GPL이라는 말 자체가 안 나와. Package.swift의 exclude 목록은 COPYING과 PROVENANCE.md를 컴파일에서 빼(소스가 아니라 문서니까). 그러면서도 자기가 덮는 코드 옆에 그대로 남겨둬.

헤더가 어느 디렉터리에 있느냐가 곧 누구한테 하는 말이냐야. include/는 "모든 의존자와 Swift에게", 타깃 루트는 "이 타깃에게만". 사실마다 그걸 들어야 할 쪽에 두면, 두 라이브러리가 같은 매크로 이름을 갖고도 한 번도 안 마주쳐.

Code

Ariandel의 Sources/CMyPaint/config.h, 파일 전체·c
#define MYPAINT_CONFIG_USE_GLIB 0
#define PACKAGE "libmypaint"
#define GETTEXT_PACKAGE "libmypaint"
Ariandel의 Sources/CJSONC/config.h에서 줄여 발췌: 로컬 가드 하나, 그리고 두 파일이 공유하는 매크로·c
/* Define to 1 if you have the <endian.h> header file. */
#if __has_include(<endian.h>)
#define HAVE_ENDIAN_H
#endif

/* ... 160-odd lines of CMake output (trimmed) ... */

/* Name of package */
#define PACKAGE "json-c"
최소 스케치: 누수와 수정을 나란히 빌드하기 ("json-c json-c"를 찍고, 다음에 "libmypaint json-c"를 찍어)·bash
#!/bin/bash
# Two C targets, each with a private config.h that defines PACKAGE.
#   leak:  config.h sits in each target's public include/
#   fixed: config.h sits at the target root, beside its .c file
build() {
  local dir=/tmp/config-$1 json=Sources/CJson paint=Sources/CPaint
  rm -rf "$dir"; mkdir -p "$dir"/Sources/{CJson,CPaint}/include "$dir"/Sources/app; cd "$dir"
  cat > Package.swift <<'SWIFT'
// swift-tools-version:6.0
import PackageDescription
let package = Package(name: "ConfigDemo", targets: [
    .target(name: "CJson", path: "Sources/CJson", cSettings: [.headerSearchPath(".")]),
    .target(name: "CPaint", dependencies: ["CJson"], path: "Sources/CPaint",
            cSettings: [.headerSearchPath(".")]),
    .executableTarget(name: "app", dependencies: ["CPaint"], path: "Sources/app"),
])
SWIFT
  [ "$1" = leak ] && { json=$json/include; paint=$paint/include; }
  echo '#define PACKAGE "json-c"'     > $json/config.h
  echo '#define PACKAGE "libmypaint"' > $paint/config.h
  echo 'const char *cjson_name(void);'  > Sources/CJson/include/json_api.h
  echo 'const char *cpaint_name(void);' > Sources/CPaint/include/paint_api.h
  printf '#include "config.h"\nconst char *cjson_name(void) { return PACKAGE; }\n'  > Sources/CJson/json.c
  printf '#include "config.h"\nconst char *cpaint_name(void) { return PACKAGE; }\n' > Sources/CPaint/paint.c
  printf 'import CPaint\nimport CJson\nprint(String(cString: cpaint_name()), String(cString: cjson_name()))\n' > Sources/app/main.swift
  echo "== $1 =="
  swift build 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -E '\.c:[0-9]+:[0-9]+: error'
  [ -x .build/debug/app ] && .build/debug/app
}
build leak
build fixed

External links

Exercise

C 타깃이 하나 이상 있는 Swift 패키지를 골라(네 거든, 연습용으로 새로 만든 거든). 각 C 타깃의 include/에 있는 헤더를 전부 적고, 하나마다 질문 하나에 답해. 이게 모든 의존자와 Swift한테 하는 약속이야? 아닌 건(config 헤더, 비공개 헬퍼) 타깃 루트로 옮기고 .headerSearchPath(".")를 붙여. 그다음 위 스케치를 돌리고 고쳐봐. 두 config.h 중 한쪽에만 FEATURE_X 같은 매크로를 하나 더 넣고, 빌드하기 전에 어느 배치에서 그게 다른 타깃으로 새어 나갈지 먼저 예측해.
Hint
배치마다 예측은 두 규칙에서 나와. 따옴표 형식은 include하는 파일 옆부터 보고, 의존 타깃의 include/는 내 컴파일 줄에서 내 것보다 앞에 와. .c 파일마다 어느 파일을 찾을지 적어두고 빌드해서 맞춰봐. Swift가 그 매크로 이름을 부를 수 있으면, 네 의도가 뭐였든 그건 공개야.

Progress

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

댓글 0

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

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