21.6.1.1.1. work_s 구조체의 함수 포인터와 Run() 메서드 주소 간의 바인딩 원리
PX4의 심층부에서 운영체제(NuttX 커널)와 C++ 객체 지향의 철학이 가장 치열하게 맞붙는 지점이 바로 여기다.
NuttX 커널의 스케줄러는 철저하게 C 언어로 작성되어 있다. 이 커널은 우리가 만든 CustomApp 이라는 화려한 C++ 클래스는 알지도 못하고 관심도 없다. 커널이 워크 큐(Work Queue)에서 꺼내어 실행할 수 있는 유일한 매개체는 work_s 라는 아주 단순한 C 구조체와 그 안에 든 함수 포인터뿐이다.
// NuttX 커널이 이해하는 순수 C 구조체 (nuttx/wqueue.h)
struct work_s {
dq_entry_t dq; // 큐 자료구조 연결 리스트
worker_t worker; // ★ 실행할 함수의 포인터 (함수 주소)
FAR void *arg; // ★ 그 함수에 넘겨줄 인자 (주로 객체의 주소)
clock_t qtime; // 큐에 들어간 시간
uint32_t delay; // 지연 시간
};
커널은 버스(Work Queue Thread)가 돌 때마다 저 구조체를 꺼내서 worker(arg); 라고 무심하게 C 함수를 호출할 뿐이다.
그렇다면 C++ 객체의 Run() 메서드는 도대체 ఎలా(어떻게) 저 worker 포인터 안으로 비집고 들어갈 수 있었을까?
1. 정적(Static) 트램펄린(Trampoline) 패러다임
C++의 일반적인 멤버 함수(Method)인 void CustomApp::Run() 은, 사실 눈에 보이지 않는 this 포인터를 첫 번째 인자로 몰래 전달받는 특수한 함수다. 따라서 순수한 C 언어의 함수 포인터 타입인 worker_t 와는 메모리 시그니처가 달라 절대 배열(Casting)할 수 없다.
이 거대한 언어적 장벽을 넘기 위해 px4::WorkItem 클래스는 내부적으로 **‘트램펄린(Trampoline) 함수’**라는 해킹 기법을 도입했다.
WorkItem의 소스 코드를 까보면 아래와 같은 징검다리 로직이 숨어있다.
// px4::WorkItem 구현부 내부 (단순화)
// 1. 순수한 C 함수 포인터와 호환되는 'static' 트램펄린 함수를 하나 만든다.
static void WorkItem::ScheduleTrampoline(void *arg)
{
// 3. 커널이 넘겨준 void* arg 인자를 우격다짐으로 WorkItem C++ 포인터로 캐스팅한다!
WorkItem *work_item = static_cast<WorkItem *>(arg);
// 4. 이제 C++ 객체의 영역으로 들어왔으니, 우아하게 다형성 가상 함수 Run()을 호출한다!
work_item->Run();
}
// WorkItem의 생성자
WorkItem::WorkItem(...)
{
// 2. 내 몸속의 깡통 C 구조체(_work)에 세팅을 건다.
// 실행할 함수 포인터에는 징검다리 '정적 함수'의 주소를 넣고,
// 그 함수에 전달할 인자(arg)에는 나 자신(C++ 객체)의 'this' 주소를 우겨 넣는다.
_work.worker = &WorkItem::ScheduleTrampoline; // 함수 포인터 바인딩
_work.arg = this; // 내 객체 주소 바인딩
}
2. 바인딩의 완성: 커널에서 객체로의 점프
이 기막힌 트램펄린(Trampoline) 설계 덕분에 흐름은 다음과 같이 완성된다.
- 커널의 호출: NuttX 커널은 큐에서
work_s덩어리를 꺼낸다. 아무것도 모르는 커널은 그저 C 언어 규칙에 따라 보따리 안에 있던 징검다리 함수를 호출하며, 인자로 저장되어 있던 메모리 주소(arg)를 넘겨준다. - 트램펄린 점프:
ScheduleTrampoline정적 함수가 발동한다. 이 함수는 넘어온void*인자가 사실 내 자식 객체(CustomApp)의 포인터라는 것을 굳게 믿고static_cast로 변환(Casting)한다. - 다형성의 발현:
work_item->Run()이 호출되는 순간, C++의 가상 함수 테이블(V-Table)에 엮여 있는 다형성 규칙에 따라,WorkItem의 껍데기를 쓰고 있던 진짜 자식 클래스, 즉 여러분이 수 백 줄의 제어 코드를 쏟아부었던CustomApp::Run()이 찬란하게 폭발(Execute)하는 것이다.
이 한 편의 첩보 영화 같은 메모리 바인딩 예술 덕분에, 픽스호크의 수많은 C++ 모듈들은 최소한의 오버헤드만으로 낡은 C 커널의 스케줄러 위를 자유롭게 뛰어다닐 수 있게 되었다.
이제 모듈이 커널 위에서 춤출 완벽한 준비를 마쳤다. 그렇다면 내가 짠 이 모듈을 어떤 버스(Work Queue)에 태우는 것이 가장 최선의 설계일까? 다음 장(21.6.2)에서 타깃 스레드 풀(Target Thread Pool)의 전략적 선택 기준을 낱낱이 파헤쳐보겠다.