Rust panic을 C++에서 try/catch로 잡을 수 없는 이유 — FFI 경계 예외 전파의 근본 원인
검증 환경 (실제 실행 기준)
본 글의 재현·검증은 다음 환경에서 실제로 수행했다.
| 항목 | 값 |
|---|---|
| OS | Ubuntu 22.04.5 LTS, aarch64 |
| rustc / cargo | 1.98.1 (2026-09-01) |
| 컴파일러 | g++ (Ubuntu) 11.4.0-1ubuntu1~22.04.3 |
| 링커 | GNU ld (Binutils) 2.38 |
| Rust 패닉 전략 | panic = "unwind" (rustc 1.98.1 기본값) — rustc --print cfg 로 panic="unwind" 확인 |
| 정적 라이브러리 | cargo build (debug) → target/debug/libboom.a |
출처 StackOverflow 원문(2024-04) 실행 결과의 오류 메시지(
fatal runtime error: failed to initiate panic, error 5)는 최신 rustc(1.98.1)에서는 다른 형태로 출력된다. 이 차이는 아래 §4.3, §8에서 구체적으로 다룬다. 언급이 없는 한 모든 실행 결과는 위 환경에서 직접 실행해 얻은 것이다.
1. 문제 정의
Rust로 작성된 정적 라이브러리(staticlib)가 특정 조건에서 panic!()을 발생시키고, 이를 C++ main에서 사용하는 상황을 가정하자. C++ 쪽은 자연스럽게 try/catch로 잡으려고 시도한다.
Rust 쪽 라이브러리 (Cargo.toml):
| |
Rust 진입점 (src/lib.rs):
| |
C++ 호출부 (boom.cpp):
| |
기대는 exception captured가 출력되는 것이었지만, 이 코드는 그렇게 동작하지 않는다. Rust의 panic!은 C++의 std::exception 계열로 전달되지 않기 때문이다.
재현 절차 (직접 실행한 순서)
| |
2. 표면 증상 (증상 fingerprint)
이 문제를 빠르게 판별하기 위한 특징은 다음과 같다.
| 구분 | 특징 | 한눈에 보는 판단 기준 |
|---|---|---|
| 발생 단계 | Rust staticlib를 g++로 링크한 뒤 런타임 호출 시점 | C++ try/catch 블록이 발동하지 않음 |
| 직접적인 증상 | panic 메시지가 출력되지만 catch(const std::exception& e)는 실행되지 않음 | 예외가 잡히지 않고 프로세스가 abort됨 |
| 관련 도구 | cargo build, #[no_mangle] extern "C" fn, g++ -L./target/debug -lboom | C/C++ FFI(외부 함수 인터페이스) 경계 |
| 흔한 오해 | “Rust panic → C++ exception으로 잡으면 되겠다” | panic과 C++ exception은 서로 다른 메커니즘 |
| 핵심 힌트 | thread caused non-unwinding panic. aborting. (최신 rustc) / fatal runtime error: failed to initiate panic, error 5 (구버전 rustc) | panic이 FFI 경계를 넘는 시점에 abort됨 |
결론적 증상 요약: “Rust 쪽에서 던진 것이 예외처럼 보이지만, C++의 예외 처리 메커니즘과는 전혀 다른 별개의 통신 채널을 사용한다.”
3. 원인 추적 — 원인 후보 매트릭스
왜 잡히지 않는지, 가능한 원인 후보를 하나씩 점검한다.
| 원인 후보 | 확인 방법 | 후보가 맞는 경우의 증상 | 배제/채택 |
|---|---|---|---|
① #[no_mangle]/extern "C" 구문 문제로 심볼이 링크되지 않음 | `nm target/debug/libboom.a | grep boom` 실행 | 링크 에러나 undefined symbol 발생 |
| ② panic이 예외가 아니라 다른 방식으로 전달된다는 C++ 쪽 오해 | catch(...)로 모든 예외를 잡아보기 | 그래도 잡히지 않는다면 예외 채널 문제가 아님 | 채택 — panic은 예외 채널을 타지 않음 |
| ③ FFI 경계에 예외 전파 ABI가 정의되어 있지 않음 | Rust 공식 FFI가 C FFI뿐임을 확인 | panic이 경계를 넘어가며 abort(아래 실행 결과) | 채택 — 이것이 근본 원인 |
| ④ panic 자체가 복구 가능한 오류로 설계됨 | panic은 unrecoverable error로 문서화됨 | 복구가 필요한 오류는 Result<T, E> 반환이 맞음 | 참고 — 설계 관점의 근거 |
원인 ③이 핵심이다. Rust의 FFI는 C++이 아니라 C를 위한 것이다. C에는 예외(exception)라는 개념이 없고, 따라서 언어 경계를 넘는 예외 전파를 위한 ABI가 정의되어 있지 않다. 이 때문에 Rust panic이 어떻게든 C/C++ 경계를 넘는다 해도 C++의 catch 절이 그것을 인식할 수 있는 경로 자체가 존재하지 않는다.
4. 근본 원인 분석
4.1 Rust는 C++ FFI가 아니라 C FFI만 제공한다
extern "C"는 함수를 C ABI로 노출시킨다. C ABI에는 예외 정보가 없다. C++ 예외가 올바르게 전파되려면 표준화된 예외 전파 ABI(unwinding 규약, 예외 테이블 등)가 필요한데, C는 그 어떤 예외 전파 규약도 정의하지 않는다. 즉 Rust → C(또는 C → Rust)로 예외를 전파하는 표준 메커니즘은 존재하지 않는다.
4.2 C 스택 프레임을 가로지르는 예외는 Undefined Behavior
더 근본적인 층위에서, C++ 예외가 C 스택 프레임을 가로지르는 것 자체가 이미 위험한 도박(gamble) 이다. C 스택 프레임은 unwinding을 처리하는 코드를 생성하지 않기 때문에, 예외 전파 중에 unwind되더라도 정리(clean-up)가 수행되지 않아 Undefined Behavior가 된다. 실제 결과는 “우연히 잘 동작함(just works)“에서 “메모리 누수(leaky)”, “상태 손상(corrupted state)“까지 다양하다. 러스트 쪽 특유의 문제라기보다, 언어 경계를 넘는 예외 전파 자체가 툴체인이 명시적으로 보장하지 않는 한 피해야 하는 행위라는 것이 답변의 요지다.
4.3 실제 실행 결과: 두 rustc 버전에서 다른 abort 출력
아래는 위 검증 환경(rustc 1.98.1)에서 naive 코드(§1)를 직접 실행한 출력이다.
| |
exception captured는 출력되지 않았고 프로세스가 abort(exit 134) 로 종료되었다. C++try/catch는 발동하지 않았다.panic in a function that cannot unwind+thread caused non-unwinding panic. aborting.메시지는 최신 rustc(1.98.1)가extern "C"함수에서 panic이 unwind 불가 지점임을 감지하고 abort로 처리한 결과다. 이는 StackOverflow 원문(2024-04, 구버전 rustc)의fatal runtime error: failed to initiate panic, error 5와 다른 버전별 메시지이며, 둘 다 같은 근본 원인(경계 예외 전파 ABI 부재)에서 비롯된다.
이 abort는 버그가 아니라 설계된 안전장치다. RFC 2945(C-unwind ABI)가 panic이 C 경계를 넘는 것을 막고 abort로 처리하는 보호 동작과 일치한다. 만약 이 보호가 없다면 Undefined Behavior로 넘어가 “그럭저럭 동작"하거나 상태가 손상된다.
4.4 설계 관점: panic은 원래 복구 불가 오류다
러스트 공식 문서(unrecoverable errors with panic)상 panic은 잡히지 않도록 설계된 unrecoverable error다. 복구 가능한 오류는 처음부터 Result<T, E>를 반환하는 것이 러스트의 표준 관례다. “Rust panic을 C++에서 잡겠다"는 발상은 이 두 오류 수단을 혼동한 데서 시작한다.
5. 해결책 — FFI 경계에서 예외를 번역하라
정답은 panic을 언어 경계 밖으로 그대로 새지 못 하게 막고, 대신 경계에서 오류 값을 번역(translate) 하는 것이다. 방향은 두 가지로 나뉜다.
5.1 Rust → C++ (더 흔한 방향)
- Rust 쪽:
std::panic::catch_unwind로 panic을 잡아 오류 반환 값/오류 코드로 번역한다. - C++ 쪽: 그 오류 값을 처리한다 (필요하면 C++에서 다시
throw).
catch_unwind는 Rust 1.9.0부터 안정(stable) API로 표준 라이브러리에 포함되어 있다. nightly가 아니다. (doc.rust-lang.org/std/panic/fn.catch_unwind.html — “Since 1.9.0”). 단, 같은 모듈의 abort_unwind·always_abort·get_backtrace_style 등은 아직 Experimental로 표기되어 있으므로 구분해야 한다.
검증 환경에서 실제 컴파일·실행한 Rust 쪽 번역 코드 (src/lib.rs):
| |
실제 catch_unwind의 시그니처(공식 문서)는 pub fn catch_unwind<F: FnOnce() -> R + UnwindSafe, R>(f: F) -> Result<R> 이다. 즉 반환 타입이 Option이 아니라 Result<i32, Box<dyn Any + Send>> 이므로, 위처럼 match Ok/Err(또는 .unwrap_or(-1)) 패턴을 쓴다. if let Some(...) 패턴은 이 API에 맞지 않는다.
- C++ 쪽은 반환값이 정상/오류인지를 검사하고, 오류인 경우에만
throw std::runtime_error(...)하는 식으로 처리한다. 이렇게 하면 C++ 예외는 C 경계를 건너지 않는다 (C++ 스택 안에서만 발생하고 처리되기 때문).
5.2 C++ → Rust (반대 방향)
- C++ 쪽:
catch로 예외를 잡아 오류 값을 번역한다. - Rust 쪽: 오류 값을 처리한다 (필요하면 다시
panic!).
5.3 해결책 비교
| 접근법 | 동작 방식 | 경계를 건너는가 | 판정 |
|---|---|---|---|
C++ try/catch로 Rust panic 직접 캐치 | panic이 C++ 예외처럼 전파되길 기대 | 예(경계를 통과 시도) | 불가 / 진행 시 abort, UB |
catch_unwind로 Rust 쪽에서 panic → 오류 값 변환 | panic이 FFI 경계를 넘어가지 못하게 원천 차단 | 아니요(경계 안쪽에서 종결) | 권장 (Rust→C++) |
C++ catch로 예외 → 오류 값 변환 | C++ 예외가 C 경계를 건너지 못하게 차단 | 아니요(경계 안쪽에서 종결) | 권장 (C++→Rust) |
| 경계 통과를 그냥 방치 | 컴파일/런타임에 abort 또는 UB | 예 | 금지 — RFC 2945가 abort로 구원 |
6. 검증 명령 (실제 실행 결과 포함, verification_command)
해결책을 적용한 뒤 성공 여부를 확인하는 명령과, 위 검증 환경(rustc 1.98.1 / g++ 11.4.0 / GNU ld 2.38 / Ubuntu 22.04 aarch64)에서 실제로 실행해 얻은 출력이다.
| |
검증 포인트는 “panic이 발생해도 프로세스가 Undefined Behavior로 오염되지 않고, 오류 경로(exit 0)가 정상적으로 실행되는지” 이다. §5.1의 catch_unwind 코드를 넣은 실행은 panic이 오류 코드 -1로 번역된 뒤 C++에서 exception captured 출력 후 exit 0으로 정상 종료했다. 반면 naive 코드는 exit 134(abort) 로 종료되어, 두 메커니즘의 차이를 실제 출력으로 확인할 수 있다.
7. 실패한 해결책 (권장하지 않는 방법)
7.1 Rust panic을 C++ try/catch/catch(...)로 그대로 잡기
왜 안 되는가: panic은 예외 채널을 타지 않으므로 catch가 인식할 메커니즘이 없다. 설령 예외가 경계를 넘는다 해도 C 스택 프레임은 unwinding 정리를 하지 않아 Undefined Behavior다. 임시방편이 아니라 구조적으로 불가능한 경로다. (실제 실행: §6 (4)에서 exit 134로 abort.)
7.2 경계 통과를 방치하고 “그럭저럭 돈다"는 기대
왜 위험한가: 최신 rustc는 panic in a function that cannot unwind로 즉시 abort시킨다. 그러나 rustc 버전·패닉 전략·링커에 따라 언제 abort되고 언제 조용히 상태가 손상될지 보장할 수 없다. “테스트만 통과하면 괜찮겠지"는 운영 환경에서 랜덤 크래시/데이터 오염으로 이어지는 전형적인 함정이다. RFC 2945의 abort조차 우연한 보호일 뿐, 그 위에 로직을 세우면 안 된다.
7.3 서드파티 라이브러리가 panic을 반환 대신 panic으로 처리하는 경우 무턱대고 fork
질문자가 실제 사용 중인 jexl-rs 같은 라이브러리가 오류 반환 대신 panic을 던지는 경우, “라이브러리를 fork해서 고치기"는 대응이지만 FFI 경계(Fix)가 먼저다. 라이브러리 수정 전에, 호출부에서 catch_unwind로 감싸 번역하는 방식이 더 안전하고 유지보수 비용이 낮다.
8. 버전/패닉 전략 차이 (실제 확인된 기준)
| 구분 | 구체 동작 | 실제 확인 결과 (이 환경) |
|---|---|---|
catch_unwind 안정화 | Rust 1.9.0부터 stable API. nightly 아님 | 공식 문서 “Since 1.9.0” (std::panic). abort_unwind·always_abort만 Experimental |
panic 전략 panic=unwind(기본) | catch_unwind가 unwind panic을 잡아 Result::Err 반환 | rustc --print cfg → panic="unwind", run (5)에서 panic→오류 코드 번역 성공 |
panic 전략 panic=abort | catch_unwind도 아무것도 못 잡고 프로세스가 abort | RUSTFLAGS="-Cpanic=abort" cargo build 후 run: panic 메시지 출력 뒤 exit 134 — exception captured 미출력 |
extern "C" 함수에서의 최신 rustc 동작 | unwind 불가 지점 자체를 감지해 즉시 abort | naive 실행 시 panic in a function that cannot unwind ... aborting. (exit 134) |
| 오류 메시지 형식 | rustc 버전에 따라 다름 | 구버전(원문, 2024) failed to initiate panic, error 5 / 1.98.1 thread caused non-unwinding panic. aborting. |
| 정적/동적 라이브러리 | staticlib(crate-type)는 g++ -L... -lboom 직접 링크, cdylib는 런타임 로더 해석 | 본 글은 staticlib로 검증 |
핵심 판단: catch_unwind를 기대한다면 반드시 panic=unwind 설정이어야 한다. panic=abort로 빌드하면 catch_unwind가 panic을 잡지 못해 §6 (5)의 번역 경로가 동작하지 않고 abort(exit 134)로 끝난다. 이 차이는 위 환경에서 RUSTFLAGS="-Cpanic=abort"로 빌드해 직접 확인했다.
9. 재발 방지 (운영 예방책)
- FFI 경계 함수는 예외를 반환값으로만 넘긴다 —
extern "C"함수는i32/bool같은 오류 코드를 돌려주고, panic이 경계 밖으로 나가지 않게catch_unwind로 감싼다. - 모든
extern "C"진입점에 panic 차단층을 설계한다 — 라이브러리를 새로 붙일 때마다 “이 함수가 panic할 수 있나?“를 검토하고, 금지 목록에 panic-유발 함수를 기록한다. - CI에서 C 경계 abort를 테스트로 감지한다 — 프로세스가
thread caused non-unwinding panic. aborting.(또는 oldfailed to initiate panic)로 죽는 케이스를 회귀 테스트로 넣어, 새로운 panic이 경계를 넘는 순간 테스트가 실패하도록 한다. - 패닉 전략을 문서화·CI에 고정한다 —
panic=unwind(catch_unwind 동작) vspanic=abort(번역 불가)를 프로젝트에 명시하고,.cargo/config.toml의[profile]rustflags로 고정한다. 복구 가능한 실패는Result<T, E>전파를 기본 원칙으로 삼는다.
10. DevTrace 독자 결론
이 문제의 근본 원인은 “패키지가 없어서"도 아니고 “try/catch 문법이 틀려서"도 아니다. Rust의 FFI는 C를 위한 것이므로, C에는 예외가 없어 panic을 C++ 예외로 전파할 언어 경계 ABI 자체가 존재하지 않는다는 점이다. Rust panic은 C++ 쪽에서 절대 catch로 잡을 수 없으며, 반드시 catch_unwind(Rust 1.9.0부터 stable)로 경계 안쪽에서 오류 값으로 번역한 뒤 C++에서 처리(또는 재throw)해야 한다. 이는 단순히 버그가 아니라 언어 경계의 설계적 한계다.
출처
- StackOverflow 질문 “Capturing Rust generated panic as C++ exception” (78346412):
https://stackoverflow.com/questions/78346412/capturing-rust-generated-panic-as-c-exception - 관련 참고 (질문 본문 논의 링크)
- When is a panic on a Rust FFI boundary undefined behavior: https://stackoverflow.com/questions/77876748/when-is-a-panic-on-a-rust-ffi-boundary-undefined-behavior
- Rust RFC 2945 — C-unwind ABI: https://github.com/rust-lang/rfcs/blob/master/text/2945-c-unwind-abi.md
- Rust 공식 문서 — Unrecoverable Errors with panic: https://doc.rust-lang.org/book/ch09-01-unrecoverable-errors-with-panic.html
- Rust std::panic — catch_unwind (Since 1.9.0): https://doc.rust-lang.org/std/panic/fn.catch_unwind.html