18.2.3.3 다중 메시지 의존성(Nested Messages) 처리 시 헤더 인클루드(Include) 순서 보장 로직
복잡한 항공 제어 시스템에서 모든 데이터를 단일 .msg 파일 안에 플랫(Flat)하게 구겨 넣는 것은 객체 지향적 설계 원칙에 위배되며, 재사용성을 극도로 떨어뜨린다.
따라서 uORB 생태계는 하나의 토픽 구조체 안에 다른 토픽 구조체를 통째로 포함시키는 다중 메시지 의존성(Nested Messages) 패턴을 매우 흔하게 사용한다.
예를 들어, 글로벌 궤적 제어점(trajectory_setpoint.msg) 내부에는 3차원 위치(position_setpoint.msg)와 3차원 속도(velocity_setpoint.msg) 타입의 변수들이 중첩되어 선언될 수 있다.
1. C++ 컴파일러의 “불완전한 타입(Incomplete Type)” 에러
문제는 자동 생성된 코드를 C++ 컴파일러로 밀어 넣을 때 발생한다.
만약 trajectory.h 파일에서 position_s 구조체를 변수로 사용하려면, C++ 컴파일러는 trajectory.h의 구조체 선언부를 읽기 이전에 반드시 position.h를 먼저 읽어 그 구조체의 덩치(sizeof)가 얼마인지 완벽하게 알고 있어야만 한다. 그렇지 않으면 컴파일러는 “불완전한 타입(Incomplete Type)을 사용할 수 없다“며 악명 높은 컴파일 에러를 뱉어내고 파업에 돌입한다.
만약 개발자 수십 명이 중구난방으로 메시지들을 서로 가져다 쓰며 얽히고설킨 100여 개의 .msg 생태계에서, 파이썬 제너레이터가 가나다순 등 임의의 순서로 C++ 헤더 파일을 뱉어낸다면, 빌드 시스템은 영원한 컴파일 에러의 늪에 빠지게 될 것이다.
2. 방향성 비순환 그래프(DAG) 기반 위상 정렬(Topological Sort)
이 위협적인 의존성 지옥을 타개하기 위해, px4_generate_messages.py 스크립트는 단순 구문 분석을 넘어 컴파일러 수준의 정적 분석(Static Analysis) 알고리즘을 수행한다.
- DAG(Directed Acyclic Graph) 빌드: 스크립트는
msg/폴더 내의 모든 토픽들을 스캔하면서, 어떤 토픽이 어떤 자식 토픽을 포함(Include)하는지를 파악하여 메모리 상에 거대한 추상 그래프(Graph)를 그려낸다. - 순환 참조(Circular Dependency) 차단: 이 과정에서
A.msg가B.msg를 포함하는데, 실수로B.msg가 다시A.msg를 포함하는 사이클(Cycle) 구조가 발견되면, 스크립트는 무한 루프를 방지하기 위해 즉각적으로 “Circular Dependency Detected” 예외를 던지고 컴파일을 중단시킨다. - 위상 정렬(Topological Sort): 의존성 그래프에 사이클이 없다는 것이 입증되면, 트리의 가장 깊은 뿌리(자식이 하나도 없는 기본 메시지)부터 순차적으로 헤더 포함 순서를 결정 짓는 위상 정렬 알고리즘을 가동한다.
3. 템플릿 코드 렌더링에서의 위력
이 혹독한 검증과 정렬 연산을 통과하여 도출된 includes 리스트 배열은 비로소 Jinja2 엔진(msg.h.jinja)으로 넘겨진다.
// msg.h.jinja 내부의 의존성 전개 로직
{% for include in includes %}
#include <uORB/topics/{{ include }}.h>
{% endfor %}
이 템플릿루프가 돌 때마다 찍혀져 나오는 #include 지시문들은 절대 무작위 순서가 아니다. DAG 위상 정렬을 거쳐 철저하게 계산된 방탄(Bullet-proof) 순열이다. 따라서 C++ 컴파일러는 이 순서대로 헤더 파일 체인을 타고 내려가기만 하면, 단 한 번의 에러나 멈춤 없이 수백 개의 복합 의존성 구조체들을 매끄럽게 컴파일해 낼 수 있게 된다.
결과적으로 PX4의 메시지 생성 파이프라인은 개발자에게는 파이썬 스크립트처럼 자유롭게 .msg를 조립할 수 있는 유연성을 제공하면서도, 백엔드 기계실에서는 C++ 컴파일러의 깐깐한 메모리 레이아웃 요구사항을 빈틈없이 맞춰주는 완벽한 중재자 역할을 수행하고 있는 것이다.