C 언어로 코루틴 구현하기 – switch-case와 __LINE__ 트릭의 원리

//
//

🚀 들어가며

아래 링크는 C 언어로 코루틴을 구현하는 아주 유명한 글입니다.
https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html

처음 보면 굉장히 충격적입니다.

왜냐하면 C 언어에는 coroutine 문법이 없고, yield도 없고, async/await도 없는데
코루틴처럼 동작하는 구조를 만들어내기 때문입니다.

게다가 스레드도 사용하지 않고, OS context switching도 없고, 복잡한 라이브러리도 사용하지 않습니다.

그런데도 함수가 중간에서 멈췄다가 다음 호출 때 이어서 실행되는 것처럼 보입니다.

이번 글에서는 원문의 핵심 아이디어를 바탕으로

  • 코루틴이 본질적으로 무엇인지
  • 왜 switch-case로 구현 가능한지
  • __LINE__ 트릭은 어떻게 동작하는지
  • 왜 이 구조가 게임 프로그래밍과 연결되는지

쉽고 자세하게 정리해보겠습니다.


🧠 먼저 코루틴이 무엇인가?

일반 함수는 보통 이런 흐름입니다.

함수 호출

코드 실행

return

함수 완전 종료

void Func()
{
    printf("A\n");
    printf("B\n");
}

예를 들어 위의 코드에서 Func 함수를 호출하면

A
B

출력 후 함수가 끝납니다.

그리고 다음에 Func를 다시 호출하면 항상 처음부터 다시 실행됩니다.


//

🎯 그런데 코루틴은 다르다

코루틴은 👉 함수 실행 상태를 저장할 수 있습니다.

즉, 아래와 같은 실행 흐름이 가능합니다.

A 실행

잠시 멈춤 (yield)

다음 호출 때 멈춘 지점에서 이어서 실행

B 실행

예를 들어, Unity Coroutine 느낌으로 보면

IEnumerator Routine()
{
    Debug.Log("A");

    yield return null;

    Debug.Log("B");
}

첫 프레임

A

다음 프레임

B

가 실행됩니다.

즉 핵심은 이것입니다.
함수가 “완전히 종료”되는 것이 아니라, 중간 상태를 저장했다가 이어서 실행됩니다.


🚨 그런데 C 언어에는 코루틴이 없다

문제는 이 글이 작성될 당시 C 언어에는 아래의 기능이 존재하지 않았다는 점입니다.

  • coroutine
  • yield
  • async

즉, 함수 실행 위치를 저장하는 기능 자체가 없었습니다.

그런데 여기서 굉장히 재미있는 아이디어가 등장합니다.

  • 함수의 실행 위치를 숫자로 저장하면 되지 않을까?

이게 이 글의 핵심 아이디어입니다.


🧩 상태 머신(State Machine)으로 생각하기

예를 들어, 이런 함수가 있다고 생각해봅시다.

void Func()
{
    printf("A\n");

    // 여기서 잠시 멈추고 싶다

    printf("B\n");
}

일반 함수라면, A와 B가 한 번에 실행됩니다.

하지만 만약, 👉 실행 위치를 저장할 수 있다면?

상태 머신 형태로 바꿀 수 있습니다.

int state = 0;

void Func()
{
    switch (state)
    {
    case 0:

        printf("A\n");

        state = 1;
        return;

    case 1:

        printf("B\n");

        state = 2;
        return;
    }
}

🎮 실행 흐름 보기

첫 번째 호출

state = 0
↓
case 0 진입
↓
"A" 출력
↓
state = 1 저장
↓
return

두 번째 호출

state = 1
↓
case 1 진입
↓
"B" 출력

즉, 👉 함수가 중간부터 다시 실행되는 것처럼 보입니다.


🧠 이게 바로 코루틴의 핵심이다

사실 코루틴의 핵심은 굉장히 단순합니다.

현재 실행 위치(state)를 저장했다가,
다음 호출 때 그 위치부터 다시 실행한다.

즉, “코루틴 ≈ 스테이트 머신” 이라고 볼 수 있습니다.

실제로 현대 coroutine 컴파일러도 내부적으로는

  • 상태(state)
  • resume point
  • suspend point

기반 state machine 코드로 변환합니다.

즉, 이 글은 👉 코루틴의 본질을 보여주는 글입니다.


🔥 이제 원문의 핵심 트릭 등장

지금까지 설명한 coroutine 상태 저장 구조를 그림으로 정리해보면 다음과 같습니다.

핵심은 Coroutine의 본질은 “현재 실행 위치(state)를 저장했다가 다음 호출 때 이어 실행하는 것”이라는 점입니다.

C 언어에서 switch-case와 __LINE__ 매크로를 이용해 coroutine처럼 동작하는 상태 머신 구조를 설명하는 인포그래픽
C 언어에서 switch-case와 __LINE__ 매크로를 이용해 coroutine처럼 동작하는 상태 머신 구조를 설명하는 인포그래픽

위 구조처럼 원문의 coroutine 구현은

  • switch-case 기반 상태 머신
  • __LINE__ 매크로를 이용한 실행 위치 저장
  • yield 지점 복원

을 조합해 함수가 중간부터 다시 실행되는 것처럼 동작하게 만듭니다.

즉 이 구조는 단순한 매크로 트릭이 아니라,
Coroutine이 실제로 어떤 원리로 동작하는지를 아주 낮은 수준에서 보여주는 예제라고 볼 수 있습니다.

원문에서 가장 유명한 부분은 바로 이것입니다.

#define crBegin static int state = 0; switch(state) { case 0:
#define crReturn(x) do { state = __LINE__; return x; case __LINE__:; } while (0)
#define crFinish }

처음 보면 굉장히 이상합니다.

특히

case __LINE__:

부분이 충격적입니다.


🧠 __LINE__이 무엇인가?

__LINE__은 현재 코드 라인 번호를 의미하는 매크로입니다.

예를 들어, 아래 코드와 같이 __LINE__을 출력하면 현재 코드의 줄 번호(라인)가 출력됩니다.

printf("%d", __LINE__);

즉, __LINE__을 통해 각 코드 위치마다 고유 번호를 얻을 수 있습니다.

원문은 이걸 coroutine 상태 번호로 사용합니다.


🎯 실제 동작 흐름

예제를 보겠습니다.

int Coroutine()
{
    crBegin;

    printf("A\n");
    crReturn(1);

    printf("B\n");
    crReturn(2);

    crFinish;
}

매크로를 개념적으로 펼쳐보면 대략 이런 느낌입니다.

int Coroutine()
{
    static int state = 0;

    switch(state)
    {
    case 0:

        printf("A\n");

        state = 10;
        return 1;

    case 10:

        printf("B\n");

        state = 20;
        return 2;

    case 20:
        break;
    }
}

즉, 👉 yield 위치를 라인 번호로 저장하는 것입니다.


🎮 실행 흐름 다시 보기

첫 번째 호출

A 출력
↓
현재 라인 번호 저장
↓
return 1

두 번째 호출

저장된 라인 번호의 case로 점프
↓
B 출력
↓
return 2

즉, 함수가 👉 중간부터 이어서 실행되는 것처럼 보입니다.


🔥 왜 이게 충격적이었을까?

이 글이 유명해진 이유는 언어 기능 없이 coroutine 느낌을 구현했기 때문입니다.

특히, 스택 저장 없음 / 스레드 없음 / OS 스케줄링 없없음에도 “함수 중간에서 멈췄다가 이어서 실행”하는 것처럼 보입니다.

즉, Coroutine의 핵심이 “스택 저장”이 아니라 “실행 위치 저장”이라는 점을 보여줍니다.


⚠️ 하지만 한계도 크다

물론 이 방식은 진짜 coroutine과는 차이가 있습니다.


1. 지역 변수 문제가 있다

예를 들어, 아래 코드는 문제가 발생합니다.

void Func()
{
    int x = 10;

    crReturn();

    printf("%d", x);
}

왜냐하면, “함수가 return되면 지역 변수는 사라지기 때문“입니다.

즉, 👉 상태를 유지하려면 static 또는 별도 struct에 저장해야 합니다.


2. switch-case 트릭이다

실제로는 switch-case 진입 위치 조작을 이용한 꼼수입니다.

따라서, 디버깅 어려움 / 가독성 낮음 / IDE 친화적이지 않음 등의 문제가 있습니다.


3. Stackless Coroutine이다

이 구조는 👉 현재 함수 상태만 저장합니다.

즉, 함수 호출 스택 전체를 저장하지 않습니다.

그래서 Stackless Coroutine이라고 부릅니다.

반면, 일부 coroutine 시스템은 전체 call stack을 저장하기도 합니다.


🎮 그런데 게임 프로그래밍에서는 굉장히 중요하다

게임 로직은 원래 👉 여러 프레임에 걸쳐 실행되는 경우가 많습니다.

예를 들어, 아래와 같은 실행 흐름을 구현해야 할 때가 많습니다.

3초 대기

문 열기

애니메이션 재생

적 생성

즉, “중간 상태 저장 후 이어 실행” 구조가 매우 중요합니다.

그래서 게임을 제작할 때 아래에 나열된 기능을 자주 사용하게 됩니다.

  • Coroutine
  • Latent Action
  • Async Task
  • FSM(State Machine)

🧠 Unity Coroutine과도 연결된다

Unity의 yield return null; 도 결국 👉 상태 머신(state machine)입니다.

컴파일러가 내부적으로

  • 현재 실행 위치
  • yield 지점
  • 다음 resume 위치

를 저장하는 구조로 변환합니다.

즉, Unity의 코루틴도 실제로는 상태 머신 기반의 코드입니다.

이 점에서 원문의 아이디어와 굉장히 비슷합니다.


🔥 현대 C++ Coroutine과 비교하면?

지금은 C++20 coroutine이 존재합니다.

  • co_await
  • co_yield
  • co_return

같은 문법이 있습니다.

하지만, 내부 원리는 여전히 비슷합니다.

컴파일러는 coroutine 코드를 👉 state machine 기반 구조로 변환합니다.

즉, Coroutine의 본질은 여전히 “실행 위치 저장”입니다.


🎯 이 글의 진짜 핵심

사실 이 글의 핵심은 “코루틴 구현법” 자체보다 코루틴이 실제로 무엇인가?를 보여주는 데 있습니다.

즉, 코루틴 = 현재 실행 위치 저장 + 다음 호출 때 이어 실행이라는 점을 아주 직관적으로 보여줍니다.


🎯 핵심 정리

  • 일반 함수는 return 시 완전히 종료된다
  • Coroutine은 실행 상태를 저장하고 이어 실행할 수 있다
  • 원문은 switch-case와 __LINE__을 이용해 coroutine처럼 동작하는 구조를 구현했다
  • 핵심은 현재 실행 위치(state)를 저장하는 것이다
  • 이 구조는 결국 state machine과 매우 유사하다
  • Unity Coroutine과 현대 C++ coroutine도 내부적으로 state machine 기반이다

🧩 마무리

처음 보면 이 글은 단순한 C 매크로 꼼수처럼 보일 수 있습니다.

하지만 조금 더 깊게 보면 👉 코루틴의 본질을 아주 직관적으로 설명하는 글입니다.

특히, 게임 프로그래밍에서는 아래의 내용이 모두 연결됩니다.

  • FSM
  • Coroutine
  • 비동기 처리
  • 게임 루프

즉, “현재 상태를 저장했다가, 다음 프레임에 이어 실행한다”는 개념이 게임 로직의 핵심 구조 중 하나입니다.

이 글은 그 구조를 아주 낮은 수준에서 직접 보여준다는 점에서 지금 봐도 굉장히 흥미로운 글이라고 생각합니다.

👉 게임 엔진 구조를 더 깊이 이해하고 싶다면

아래 강의를 통해 직접 구현해보는 것을 추천드립니다.

C++로 만드는 게임 엔진 프레임워크 강의

👉 C++로 만드는 게임 엔진 프레임워크 강의 바로가기

//
   

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다