Linux에서 초록빛이 된 열여덟 개의 DLL
크로스플랫폼 빌드는 단지 도달 범위를 넓히는 것만이 아니다 — 코드베이스가 한 컴파일러의 조용한 배려에 의존하는 것을 멈추게 만든다. 추가하는 플랫폼마다 이미 갖고 있던 플랫폼들 위에서 런타임을 더 정직하게 만든다.
Linux 빌드는 몇 주 동안 부분적으로만 통과한다고 알려져 있었다. 열여덟 개의 네이티브 런타임 DLL(엔진은 초점이 맞춰진 공유 라이브러리들의 집합으로 구조화되어 있다) 대부분은 깨끗하게 빌드되고 있었다. 몇 개는 그렇지 않았다. 실패는 매번 같은 형태였다. 코드베이스에 명백히 존재하는 함수들에 대해 링크 시점에 undefined-symbol 에러가 나는 것이었다. C++로 Linux 공유 라이브러리를 작업해본 사람이라면 이것이 어디로 흘러가는지 안다.
이번 토요일 나는 그 일을 끝낼 작정으로 자리에 앉았다. 토요일이 끝날 무렵, 열여덟 개의 DLL 모두가 깨끗하게 빌드되고 있었고 Linux CI 파이프라인은 완전한 초록빛을 보고하고 있었다.
이 글은 실제 문제가 무엇이었는지, 수정이 어떤 모습이었는지, 그리고 세 플랫폼(Linux, macOS, Windows MSVC)에서 빌드하는 것이 왜 값을 치를 가치가 있는 규율이라고 생각하는지에 대한 글이다.
실패는 어떤 모습이었나
패턴은 항상 이런 식이었다. 예를 들어 libraku_runtime.so에 링크하는 테스트 바이너리가 다음과 같은 에러와 함께 링크에 실패하는 것이었다.
undefined reference to `raku_runtime_create_session(...)'
undefined reference to `raku_runtime_initialize(...)'
undefined reference to `raku_runtime_shutdown(...)'
그 함수들은 존재했다. 작성되어 있었다. 소스 파일 안에 있었다. 컴파일되어 있었다. 오브젝트 파일에도 들어 있었다. 그런데 nm -D libraku_runtime.so로 살펴본 공유 라이브러리는 그것들을 익스포트하고 있지 않았다.
이것은 전형적인 Linux 공유 라이브러리 심볼 가시성 문제다. 런타임은 소스 파일에 CMake 플래그 -fvisibility=hidden을 적용해 빌드되어 있었는데, 이는 완벽하게 괜찮은 기본값이다. -fvisibility=hidden의 의도는 내부 심볼을 공유 라이브러리의 공개 심볼 테이블 밖에 두는 것이다. 공개되어야 하는 것은 무엇이든 __attribute__((visibility("default")))나 그것으로 전개되는 매크로로 명시적으로 태그되어야 한다.
런타임의 빌드 시스템에는 각 플랫폼에서 올바른 가시성 속성으로 전개되어야 하는 RAKU_API라는 매크로가 있었다. Windows에서 RAKU_API는 DLL을 빌드할 때는 __declspec(dllexport)로, 그것을 소비할 때는 __declspec(dllimport)로 전개됐다. Linux에서는 빌드할 때 __attribute__((visibility("default")))로, 소비할 때는 아무것도 아닌 것으로 전개되어야 했다.
문제는 이 매크로의 Linux 쪽이 모든 공개 함수에 적용되어 있지 않았다는 것이다. 일부 함수는 태그되어 있었다. 많은 함수는 그렇지 않았다. 태그되지 않은 함수들은 -fvisibility=hidden 기본값에 의해 숨겨져 공개 심볼 테이블에서 사라지고 있었다.
수정은 어떤 모습이었나
세 단계, 모두 기계적이었고, 모두 적어둘 가치가 있다.
하나: 모든 공개 API 헤더를 감사해 누락된 RAKU_API 태그를 찾는다. 공개 API의 각 .h 파일을 파싱해서 공개되어야 하는데 태그되지 않은 모든 함수 선언을 찾는 스크립트를 작성했다. 이 스크립트는 함수, 파일, 줄을 보고한다. 스크립트를 실행했다. 열여덟 개의 DLL에 걸쳐 태그가 필요한 함수 삼백사십칠 개의 목록을 얻었다.
둘: 태그를 일괄 추가한다. 이것은 정확히 에이전트가 잘하는 종류의 기계적인 리팩터다. 이슈의 프레이밍은 이랬다. “아래 나열된 각 함수에 대해 선언 줄에 RAKU_API를 추가하라. 함수 본문은 수정하지 마라. 다른 어떤 줄도 수정하지 마라. 각 서브시스템 묶음 이후 빌드를 실행하고 Linux에서 여전히 빌드되는지 확인하라.” 에이전트는 이것을 서브시스템별로 깔끔하게 해냈다. 각 서브시스템 PR은 독립적으로 리뷰 가능했고 Linux 빌드는 머지될 때마다 더 초록빛이 되었다.
셋: 회귀를 막는 CI 체크. PR 시점에 실행되는 체크를 추가했다. 이 체크는 Linux에서 런타임을 빌드하고, 생성된 각 .so에 nm -D를 실행하고, 익스포트된 심볼 목록을 공개 API 헤더에 선언된 기대 세트와 비교하고, 기대되는 심볼이 하나라도 없으면 PR을 실패시킨다. 이것이야말로 “새 함수 태그하는 걸 잊었다”는 다음 사례가 몇 달 뒤가 아니라 그것이 들어오는 바로 그날 잡아내는 종류의 가드레일이다.
토요일이 끝날 무렵, 열여덟 개의 DLL 모두가 자신의 공개 표면 전체를 익스포트하고 있었다. 테스트 바이너리는 링크되었다. 테스트는 실행되었다. Linux 빌드는 초록빛이었다.
세 플랫폼이 값을 치를 가치가 있는 이유
당연한 질문이 있다. 애초에 왜 Linux까지 신경 써야 하는가? 이 런타임은 AR 글래스를 타깃으로 하는데, 그것은 개발자 머신에서와 같은 방식으로는 Linux도, Windows도, macOS도 아닌 특수한 OS에서 돌아간다. 실제 타깃 하드웨어를 지원하는 비용 위에 크로스플랫폼 비용까지 왜 치러야 하는가?
세 가지 이유가 있다.
하나: 서버 사이드 AI 추론과 클라우드 렌더링은 Linux에서 일어난다. AR 경험의 어떤 클라우드 측 구성 요소든(모델 서빙, 월드 상태 동기화, 영속성) Linux에서 실행되고 있다. 런타임은 클라우드 쪽이 호출하는 훅을 가지고 있다. 그 훅들은 클라우드 쪽이 통합되려면 Linux에서 빌드되고 실행되어야 한다. 런타임이 Windows 전용 짐승이라면, 클라우드 쪽은 별도의 통신 심을 만들거나 Linux 호환 레이어 아래에서 런타임을 실행해야 한다. 둘 다 내가 파트너들이 하기를 원하는 것이 아니다.
둘: Linux에서의 CI는 Windows에서의 CI보다 빠르고 저렴하다. 내가 머지하는 모든 PR은 CI를 거친다. Linux CI 러너는 Windows CI 러너보다 더 작고, 더 빠르고, 더 저렴하다. CI 루프가 빠를수록 에이전트들과 내가 하루의 토요일 동안 돌릴 수 있는 반복 횟수가 늘어난다. Linux를 일급 빌드 타깃으로 삼는 것은 전체 개발 워크플로우를 가속한다.
셋: 크로스플랫폼 규율은 버그를 잡아낸다. 이것이 가장 깊은 이유다. 코드베이스가 하나의 플랫폼에서만 빌드될 때, 개발자들이 손을 뻗는 패턴은 그 플랫폼에서만 작동하는 패턴이다. 크로스플랫폼 빌드는 그 패턴이 이식 가능하도록 강제한다. 암묵적인 것 대신 명시적인 가시성 속성, “이 컴파일러에서는 long이 32비트”라는 것 대신 명시적인 크로스플랫폼 타입 너비, “이건 Windows에서 작동해”라는 것 대신 명시적인 스레딩 시맨틱스. 오늘의 심볼 가시성 수정이 정확히 그 패턴이다. 이 수정 이후의 코드베이스는 그 이전보다 모든 플랫폼에서 더 강해졌다. 그 수정이 가시성 계약을 명시적으로 만들었기 때문이다.
이것이 일반화되는 것
몇 가지 정직한 패턴이 있다.
Linux에서의 심볼 가시성은 모두가 한 번 치르는 세금이다. 프로젝트가 처음 이 문제에 부딪힐 때, 그것은 신비롭고 좌절스럽다. 수정이 자리 잡고 나면(일관되게 적용된 RAKU_API 매크로에 CI 가드까지), 그것은 보이지 않게 된다. 비용은 첫 번째 순간에 든다. 일찍 치러라.
심볼 가시성 회귀를 막는 CI 가드는 선택 사항이 아니다. 아직 아무도 쓰고 있지 않은 함수가 태그를 빠뜨린 채로 몇 달이 지나서야 드러나는 종류의 버그는, 정확히 CI 가드가 그것이 들어오는 바로 그날 잡아내는 종류의 버그다. 그 가드를 설정하라.
크로스플랫폼 빌드는 코드베이스를 더 정직하게 만든다. 코드베이스가 한 플랫폼의 암묵적인 동작에 의존하는 곳이라면 어디든, 크로스플랫폼 포팅은 그 동작을 명시적으로 만들도록 강제한다. 코드베이스를 새로운 플랫폼으로 포팅할 때마다, 그 새로운 플랫폼은 원래 플랫폼이 조용히 흡수하고 있던 버그들을 드러냈다. 그 수정들은 새로운 플랫폼뿐 아니라 모든 플랫폼에서의 개선이다.
파트너와 빌더가 여기서 가져가야 할 것
크로스플랫폼 AR 제품을 위해 어떤 엔진 위에 빌드할지 결정하는 파트너라면, 그 팀에게 그들의 빌드 매트릭스가 어떤 모습인지 물어보라. Linux, macOS, Windows에서 빌드하는 팀은 그 세 플랫폼 사이의 차이로부터 규율이 잡힌 코드베이스를 가진 팀이다. 하나에서만 빌드하는 팀은 그렇지 않은 코드베이스를 가진 팀이다.
당신이 자신만의 크로스플랫폼 공유 라이브러리 프로젝트를 작업하는 개발자라면, 심볼 가시성 감사는 진단하는 데 사흘이 걸리는 이유로 테스트가 실패하기 시작하는 3월이 아니라 오늘 실행해야 하는 감사다. 당신의 공유 라이브러리에 nm -D를 실행하라. 당신의 헤더와 비교하라. 그 불일치가 곧 감사다.
당신이 코딩 에이전트가 크로스플랫폼 네이티브 코드를 작성하는 AI 랩이라면, 가시성 속성 매크로는 그 에이전트의 작업 지식에 있어야 하는 종류의 것이다. 새로운 공개 함수를 작성하고 태그하는 것을 잊는 에이전트는 후속 PR 비용을 치르게 하는 에이전트다. 모든 공개 함수를 일관되게 태그하는 에이전트는 스스로 값을 하는 에이전트다.
토요일 마무리. 열여덟 개의 DLL이 Linux에서 초록빛이다. CI는 이제 더 빠르다. 코드베이스는 더 정직하다. 빌드 매트릭스는 세 플랫폼으로 넓어졌다.
다시 빌드하러 간다.
닿는 모든 플랫폼에 의해 규율이 잡힌 런타임
RakuAI는 AR 글래스, 클라우드 추론, 그리고 그 사이의 모든 것을 위한 크로스플랫폼 공간 런타임이다 — Linux, macOS, Windows 모두 초록빛이다. 파트너들이 왜 이식 가능한 기반 위에 빌드하는지 확인하라.