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

좁은 C 경계

~14 min · embed-engine, c-interop, boundary, swift

Level 0릴 입문자
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Sources/CLibMPV/ 는 좁은 C 모듈 경계. 제품 결정은 없음."

위험이 사는 곳

Swift 앱에 C 라이브러리를 embed 하면, 날카로운 모서리가 죄다 이음매에 몰려. 원시 포인터, 손으로 관리하는 메모리, NULL 로 끝나는 배열, 정수 에러 코드, 핸들 잘못 쥐면 터지는 undefined behavior. 이 위험은 못 피해. 재사용의 값이니까. 네가 통제할 수 있는 건 하나야. 앱의 얼마만큼이 그걸 만져야 하느냐.

Ashen Reel 의 답은 CLibMPV 라는 모듈이야. 하는 일이라곤 Swift 가 libmpv 의 C API 를 안전하게 부르게 해주는 것뿐인, 작고 지루한 경계 하나. C 헤더를 매핑하고, 앱이 필요한 몇 안 되는 호출을 안전한 Swift 시그니처로 감싸고, 딱 거기서 멈춰. 그 위로는 전부 Swift 로 말하고, 원시 포인터는 구경도 안 해.

이음매엔 제품 결정 없음

이 경계를 건강하게 지키는 규칙. 제품 로직은 C 모듈에 안 산다. seek 을 정확히 할까 빠르게 할까? resume 가 딥링크를 이겨야 할까? 뭘 성공한 seek 으로 칠까? 그중 아무것도 C 이음매에 안 속해. AshenReelCore 의, 순수하고 테스트되는 Swift 에 속하지. C 모듈은 libmpv 한테 한마디 건네고 답을 되받아 옮기는 것만 알아.

위험한 경계는 얇고, 멍청하고, 한 가지 일만 하게 유지해. interop 층에 로직을 새어들게 할수록, 네 제품의 뇌가 테스트하기 제일 어렵고 크래시나기 제일 쉬운 그 한 곳에서 더 많이 돌아가.

철학 전체가 함수 하나에 담겨. 원시 mpv 포인터를 만지는 유일한 곳이야.

이건 Cinder 의 batchPlay 격리랑 같은 모양이야. 아빠의 Photoshop 사이드카에서 Pippa 는 원시 Photoshop descriptor 를 절대 안 내보내. 위험한 원시 API 는 얇은 번역 층 뒤에 봉인되고, 그 위 똑똑한 층은 안전한 intent 로만 말하지. 여기도 같은 본능이야. 원시 C 엔진은 CLibMPV 뒤에 봉인되고, 그 위 똑똑한 Swift 는 포인터를 절대 안 만져.

왜 '지루함' 이 목표인가

좋은 interop 경계는 일부러 지루해. 함수 몇 개 없고, 각각 한 번 호출하고, 각각 뒤처리하고, 아무도 판단 안 해. 재생 깊은 데서 뭐가 잘못되면, 넌 진짜 타입이랑 테스트로 Swift 로직을 디버깅하고 싶지, 포인터랑 제품 결정이 엉켜서 어디서 터졌는지도 모를 크래시를 뒤지고 싶진 않거든. 이음매가 지루한 게 다른 데서 다 정신 건강을 벌어줘.

Code

원시 mpv 포인터를 만지는 유일한 곳. 안전한 표면만 있고 제품 로직은 없음·swift
import CLibMPV   // umbrella module map over <mpv/client.h>

struct MPVError: Error { let code: Int32; let message: String }

/// Send one command to libmpv. This is interop plumbing, not a decision:
/// it knows how to CALL, never whether the call was the right thing to do.
func mpvCommand(_ handle: OpaquePointer, _ args: [String]) throws {
    // libmpv wants a NULL-terminated array of C strings.
    var cargs: [UnsafeMutablePointer<CChar>?] = args.map { strdup($0) }
    cargs.append(nil)
    defer { cargs.forEach { free($0) } }   // clean up across the seam, always

    let rc = mpv_command(handle, &cargs)
    guard rc >= 0 else {
        throw MPVError(code: rc, message: String(cString: mpv_error_string(rc)))
    }
}

// Callers say: try mpvCommand(handle, ["seek", "41.0", "absolute+exact"])
// WHETHER to seek exactly, and what 'success' means, is decided in Swift
// above this line -- never in here.

External links

Exercise

네 코드에서 뭔가 '원시' 한 걸 부르는 곳을 찾아. C 라이브러리, 쉘 명령, 저수준 소켓, DB 드라이버 같은 거. 물어봐. 결정이 그 경계 바로 거기서 얼마나 일어나고, 그 위 테스트되는 코드에선 얼마나 일어나? 호출만 하고 딱 그것뿐인 제일 얇은 래퍼를 그려보고, 판단은 죄다 한 층 위로 올려. 이음매가 멍청해지니까 뭐가 테스트하기 쉬워졌어?
Hint
건강한 이음매는 '정확히 한 번 호출하고 뒤처리함' 이라고 말할 수 있는 함수들로 돼 있어. 래퍼 함수가 호출을 '할지 말지' 까지 정하거나, 이상한 결과에 뭘 할지까지 정하면, 그 결정은 plumbing 에 엉키는 게 아니라 네 테스트되는 로직에 살고 싶어 하는 거야.

Progress

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

댓글 0

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

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