extern 블록

extern 블록 (External Blocks)

Rust에서 다른 언어(특히 C/C++)로 작성된 함수나 변수를 그대로 불러 써야 할 때가 있어요. 그때 extern 블록이 필요해요. extern 블록은 현재 crate에 정의되어 있지 않은 항목들의 선언을 제공하며, Rust의 외부 함수 인터페이스(FFI, Foreign Function Interface) 의 기반이 돼요. 검사 없는 import(unchecked import)에 가깝다고 볼 수 있어요.

출처: Rust Reference - External blocks

본문

구문 (Syntax)

ExternBlock → unsafe?¹ extern Abi? { InnerAttribute* ExternalItem* }

ExternalItem → OuterAttribute* ( MacroInvocationSemi | Visibility? StaticItem | Visibility? Function )

extern 블록에서는 함수(function)와 정적 변수(static) 두 종류의 항목 선언만 허용돼요.

extern 블록에 선언된 unsafe 함수를 호출하거나 unsafe static에 접근하는 것은 unsafe 컨텍스트에서만 허용돼요.

extern 블록은 자신이 위치한 모듈이나 블록의 값 네임스페이스에 자신의 함수와 static을 정의해요.

unsafe 키워드

unsafe 키워드는 extern 블록에서 extern 키워드 앞에 의미상 필수예요.

  • 2024 에디션 차이: 2024 에디션 이전에는 unsafe 키워드가 선택적이었어요. 그리고 safe/unsafe 항목 한정자(item qualifier)는 extern 블록 자체가 unsafe로 표시된 경우에만 허용돼요.

함수 (Functions)

extern 블록 안의 함수는 다른 Rust 함수와 같은 방식으로 선언되지만, 한 가지 예외가 있어요. 본문(body)을 가질 수 없고 대신 세미콜론으로 끝나야 한다는 점이에요.

매개변수에는 패턴이 허용되지 않고, IDENTIFIER_만 쓸 수 있어요.

safe/unsafe 함수 한정자는 허용되지만, 다른 함수 한정자(예: const, async, extern)는 허용되지 않아요.

extern 블록 안의 함수는 Rust에서 정의한 함수처럼 Rust 코드에서 호출할 수 있어요. Rust 컴파일러가 Rust ABI와 외부 ABI 사이를 자동으로 번역해요.

extern 블록에 선언된 함수는 safe 함수 한정자가 있지 않다면 **암묵적으로 unsafe**예요.

함수 포인터로 강제(coercion)될 때, extern 블록에 선언된 함수는 for<'l1, ..., 'lm> extern "abi" fn(A1, ..., An) -> R 타입을 가져요. 여기서 'l1, … 'lm은 라이프타임 매개변수, A1, …, An은 선언된 매개변수 타입, R은 선언된 반환 타입이에요.

정적 변수 (Statics)

extern 블록 안의 static은 extern 블록 밖의 static과 같은 방식으로 선언되지만, 값을 초기화하는 표현식이 없다는 점이 달라요.

extern 블록에 선언된 static 항목이 safe로 한정되지 않았다면, 그 항목에 접근하는 것은 **가변 여부와 관계없이 unsafe**예요. 왜냐하면 어떤 임의의(예: C) 코드가 그 static을 초기화하므로, static 메모리의 비트 패턴이 선언된 타입에 유효하다는 보장이 없기 때문이에요.

extern static은 extern 블록 밖의 static처럼 불변이거나 가변일 수 있어요.

불변 static은 어떤 Rust 코드가 실행되기 전에 초기화되어야 해요. Rust 코드가 읽기 전에 초기화되는 것만으로는 부족해요. Rust 코드가 실행되기 시작하면, (Rust 안이나 밖에서) 불변 static을 변경하는 것은 UB예요. 단 변경이 UnsafeCell 안의 바이트에서 일어난다면 예외예요.

ABI

extern 키워드 뒤에는 선택적인 ABI 문자열이 올 수 있어요. ABI는 블록 안 함수들의 호출 규약(calling convention) 을 지정해요. 호출 규약은 함수를 위한 저수준 인터페이스를 정의해요 — 인자를 레지스터나 스택에 어떻게 배치하는지, 반환 값을 어떻게 넘기는지, 누가 스택을 정리할 책임을 지는지 같은 것들이에요.

// Interface to the Windows API.
unsafe extern "system" { /* ... */ }

ABI 문자열을 지정하지 않으면 기본값은 "C"예요.

참고: 명시적 ABI가 없는 extern 구문은 단계적으로 제거(phasing out)되고 있어요. 그래서 ABI를 항상 명시적으로 쓰는 게 좋아요.

모든 플랫폼에서 지원되는 ABI

  • unsafe extern "Rust" — Rust 함수와 클로저의 고유(native) 호출 규약. extern fn 없이 선언된 함수의 기본값이에요. Rust ABI는 안정성 보장이 없어요.
  • unsafe extern "C" — 타겟에서 지배적인 C 컴파일러가 선택하는 기본 ABI와 일치해요.
  • unsafe extern "system" — Windows x86_32에서 non-variadic 함수에는 "stdcall", variadic 함수에는 "C"와 동등하다는 점만 빼고 extern "C"와 동등해요. Windows에서 올바른 근본 ABI는 타겟마다 다르므로, 명시적으로 정의된 ABI가 없는 Windows API 함수를 연결할 때는 extern "system"을 쓰는 게 좋아요.
  • extern "C-unwind"extern "system-unwind" — 각각 "C", "system"과 동일하지만, 피호출자(callee)가 (panic하거나 C++ 스타일 예외를 던져서) unwind할 때의 동작이 다르다는 점만 달라요.

플랫폼별 ABI

  • unsafe extern "cdecl" — x86_32 C 코드에서 보통 쓰는 호출 규약. x86_32 타겟에서만 사용 가능. MSVC의 __cdecl, GCC/clang의 __attribute__((cdecl))에 대응.
  • unsafe extern "stdcall" — x86_32에서 Win32 API가 보통 쓰는 호출 규약. x86_32 타겟에서만 사용 가능. MSVC의 __stdcall, GCC/clang의 __attribute__((stdcall))에 대응.
  • unsafe extern "win64" — Windows x64 ABI. x86_64 타겟에서만 사용 가능. Windows x86_64 타겟에서 "C"와 같음. GCC/clang의 __attribute__((ms_abi))에 대응.
  • unsafe extern "sysv64" — System V ABI. x86_64 타겟에서만 사용 가능. non-Windows x86_64 타겟에서 "C"와 같음. GCC/clang의 __attribute__((sysv_abi))에 대응.
  • unsafe extern "aapcs" — ARM의 소프트플로트 ABI. ARM32 타겟에서만 사용 가능. 소프트플로트 ARM32에서 "C"와 같음. clang의 __attribute__((pcs("aapcs")))에 대응.
  • unsafe extern "fastcall" — 일부 인자를 레지스터로 넘기는 stdcall의 "빠른" 변형. x86_32 타겟에서만 사용 가능. MSVC의 __fastcall, GCC/clang의 __attribute__((fastcall))에 대응.
  • unsafe extern "thiscall" — x86_32 MSVC에서 C++ 클래스 멤버 함수에 보통 쓰는 호출 규약. x86_32 타겟에서만 사용 가능. MSVC의 __thiscall, GCC/clang의 __attribute__((thiscall))에 대응.
  • unsafe extern "efiapi" — UEFI 함수에 쓰는 ABI. x86과 ARM 타겟(32bit와 64bit)에서만 사용 가능.

"C""system"처럼, 대부분의 플랫폼별 ABI 문자열에도 대응하는 -unwind 변형이 있어요. 구체적으로는 "aapcs-unwind", "cdecl-unwind", "fastcall-unwind", "stdcall-unwind", "sysv64-unwind", "thiscall-unwind", "win64-unwind"예요.

가변 인자 함수 (Variadic functions)

extern 블록 안의 함수는 마지막 인자로 ...를 지정해서 가변 인자(variadic) 로 만들 수 있어요. 가변 인자 매개변수에는 선택적으로 식별자를 붙일 수도 있어요.

unsafe extern "C" {
    unsafe fn foo(...);
    unsafe fn bar(x: i32, ...);
    unsafe fn with_name(format: *const u8, args: ...);
    // SAFETY: This function guarantees it will not access
    // variadic arguments.
    safe fn ignores_variadic_arguments(x: i32, ...);
}

⚠️ 경고: 함수가 가변 인자에 전혀 접근하지 않는다는 보장이 없다면 extern 블록의 함수에 safe 한정자를 쓰면 안 돼요. 가변 인자 함수에 예상치 못한 개수나 타입의 인자를 넘기면 정의되지 않은 동작(UB)을 일으킬 수 있어요.

가변 인자 매개변수는 다음과 같은 ABI 문자열(또는 그에 대응하는 -unwind 변형)을 가진 extern 블록 안에서만 지정할 수 있어요.

"aapcs", "C", "cdecl", "efiapi", "system", "sysv64", "win64"

extern 블록의 속성

다음 속성들이 extern 블록의 동작을 제어해요.

link 속성은 extern 블록 안의 항목들을 위해 컴파일러가 링크할 네이티브 라이브러리의 이름을 지정해요.

이 속성은 입력을 지정하기 위해 MetaListNameValueStr 구문을 사용해요.

  • name 키 — 링크할 네이티브 라이브러리의 이름이에요.
  • kind 키 — 라이브러리의 종류를 지정하는 선택값이에요. 가능한 값은 다음과 같아요.
    • dylib — 동적 라이브러리. kind를 지정하지 않으면 이 값이 기본이에요.
    • static — 정적 라이브러리.
    • framework — macOS 프레임워크. macOS 타겟에서만 유효해요.
    • raw-dylib — 컴파일러가 링크할 import 라이브러리를 생성하는 동적 라이브러리 (자세한 건 dylib vs raw-dylib 참고). Windows 타겟에서만 유효해요.

kind를 지정했다면 name 키는 반드시 포함되어야 해요.

선택적인 modifiers 인자는 링크할 라이브러리의 링킹 수정자(link modifier) 를 지정하는 방법이에요. 수정자는 콤마로 구분된 문자열로 지정되고, 각 수정자 앞에는 + 또는 -가 붙어 해당 수정자를 켜거나 끔을 나타내요.

하나의 link 속성에 modifiers 인자를 여러 번 지정하거나, 같은 modifiers 인자 안에 동일한 수정자를 여러 개 지정하는 것은 현재 지원되지 않아요. 예: #[link(name = "mylib", kind = "static", modifiers = "+whole-archive")].

wasm_import_module 키는 호스트 환경에서 심볼을 import할 때 extern 블록 안 항목들의 WebAssembly 모듈 이름을 지정하는 데 쓸 수 있어요. wasm_import_module을 지정하지 않으면 기본 모듈 이름은 env예요.

#[link(name = "crypto")]
unsafe extern {
    // …
}

#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern {
    // …
}

#[link(wasm_import_module = "foo")]
unsafe extern {
    // …
}

빈 extern 블록에 link 속성을 붙이는 것도 유효해요. 코드의 다른 곳(상위 crate 포함)에 있는 extern 블록의 링킹 요구사항을, 모든 블록에 속성을 넣는 대신 이렇게 한 번에 충족시킬 수 있어요.

링킹 수정자: bundle

이 수정자는 static 링킹 종류와만 호환돼요. 다른 종류를 쓰면 컴파일 에러가 나요.

  • rlib/staticlib을 빌드할 때 +bundle은 네이티브 정적 라이브러리를 rlib/staticlib 아카이브 안에 넣고, 최종 바이너리 링킹 때 거기서 꺼내 쓰는 걸 뜻해요.
  • rlib을 빌드할 때 -bundle은 네이티브 정적 라이브러리를 "이름으로" 그 rlib의 의존성으로 등록하고, 그 오브젝트 파일은 최종 바이너리 링킹 때만 포함되는 걸 뜻해요. 그 이름으로 파일 검색도 최종 링킹 때 수행돼요. staticlib을 빌드할 때 -bundle은 네이티브 정적 라이브러리를 아카이브에 그냥 포함하지 않는 걸 뜻해요. 대신 더 높은 수준의 빌드 시스템이 나중에 최종 바이너리 링킹 때 추가해야 해요.
  • 실행 파일이나 동적 라이브러리 같은 다른 타겟을 빌드할 때는 이 수정자가 효과가 없어요.
  • 이 수정자의 기본값은 +bundle이에요.

링킹 수정자: whole-archive

이 수정자는 static 링킹 종류와만 호환돼요. 다른 종류를 쓰면 컴파일 에러가 나요.

+whole-archive는 정적 라이브러리를 오브젝트 파일을 버리지 않고 전체 아카이브로 링크하는 걸 뜻해요. 기본값은 -whole-archive예요.

링킹 수정자: verbatim

이 수정자는 모든 링킹 종류와 호환돼요.

  • +verbatim은 rustc 자신이 라이브러리 이름에 타겟 지정 접두사/접미사(예: lib, .a)를 추가하지 않고, 링커에도 같은 걸 요청하려 시도하는 걸 뜻해요.
  • -verbatim은 rustc가 타겟 지정 접두사와 접미사를 라이브러리 이름에 추가한 뒤 링커에 넘기거나, 링커가 암묵적으로 추가하는 것을 막지 않는 걸 뜻해요.
  • 기본값은 -verbatim이에요.

dylibraw-dylib

Windows에서 동적 라이브러리에 링크하려면 링커에 import 라이브러리를 제공해야 해요. import 라이브러리는 동적 라이브러리가 export한 모든 심볼을 "런타임에 동적으로 로드되어야 한다"고 링커가 알 수 있도록 선언하는 특수한 정적 라이브러리예요.

kind = "dylib"를 지정하면 Rust 컴파일러는 name 키에 기반해 import 라이브러리를 링크하도록 지시해요. 링커는 보통의 라이브러리 해석 로직으로 그 import 라이브러리를 찾아요. 반면 kind = "raw-dylib"를 지정하면 컴파일러가 컴파일 중에 import 라이브러리를 생성해서 링커에 제공해요.

raw-dylibWindows에서만 지원돼요. 다른 플랫폼을 타겟으로 하면 컴파일 에러가 나요.

import_name_type

x86 Windows에서는 함수 이름이 "꾸며져(decorated)" 있어요 (즉 호출 규약을 나타내기 위해 특정 접두사/접미사가 붙어요). 예를 들어 인자가 없는 stdcall 호출 규약 함수 fn1_fn1@0으로 꾸며져요. 하지만 PE 포맷은 이름에 접두사가 없거나 undecorated인 것도 허용해요. 게다가 MSVC와 GNU 툴체인은 같은 호출 규약에 대해 다른 장식을 사용해요. 그래서 기본적으로 일부 Win32 함수는 GNU 툴체인으로 raw-dylib 링킹 종류를 써서 호출할 수 없어요.

이 차이를 허용하기 위해, raw-dylib 링킹 종류를 쓸 때 다음 값 중 하나로 import_name_type 키를 지정해서 생성된 import 라이브러리에서 함수 이름이 어떻게 지어질지 바꿀 수 있어요.

  • decorated — 함수 이름이 MSVC 툴체인 형식으로 완전히 꾸며져요.
  • noprefix — 함수 이름이 MSVC 툴체인 형식으로 꾸며지지만, 앞의 ?, @ 또는 선택적으로 _가 빠져요.
  • undecorated — 함수 이름이 꾸며지지 않아요.

import_name_type 키가 지정되지 않으면 함수 이름은 타겟 툴체인 형식으로 완전히 꾸며져요. 변수(variable)는 절대 꾸며지지 않으므로, import_name_type 키는 변수의 이름 지정에 영향을 주지 않아요. 이 키는 x86 Windows에서만 지원돼요. 다른 플랫폼을 타겟으로 하면 컴파일 에러가 나요.

link_name 속성

link_name 속성은 extern 블록 안의 선언에 적용해서, 주어진 함수나 static에 대해 import할 심볼을 지정할 수 있어요.

unsafe extern "C" {
    #[link_name = "actual_symbol_name"]
    safe fn name_in_rust();
}
  • 구문: link_name 속성은 MetaNameValueStr 구문을 사용해요.
  • 유효한 이름: 심볼 이름은 빈 문자열이거나 U+0000(NUL) 바이트를 포함해서는 안 돼요.
  • 허용 위치: link_name 속성은 extern 블록 안의 함수나 static 항목에만 적용할 수 있어요. rustc는 다른 위치에서의 사용을 무시하지만 린트로 경고해요. 미래에 에러가 될 수 있어요.
  • 반복 사용: 항목에서 link_name첫 번째 사용만 효과가 있어요. rustc는 첫 번째 이후의 사용에 대해 future-compatibility 경고로 린트해요. 미래에 에러가 될 수 있어요.
  • link_name 속성은 link_ordinal 속성과 함께 쓸 수 없어요.

link_ordinal 속성

link_ordinal 속성은 extern 블록 안의 선언에 적용해서, 링크할 import 라이브러리를 생성할 때 사용할 숫자 순번(ordinal) 을 나타낼 수 있어요. ordinal은 Windows에서 동적 라이브러리가 export하는 심볼당 고유한 번호인데, 라이브러리를 로드할 때 이름으로 찾는 대신 ordinal로 심볼을 찾는 데 쓸 수 있어요.

⚠️ 경고: link_ordinal은 심볼의 ordinal이 안정적이라고 알려진 경우에만 써야 해요. 포함하는 바이너리를 빌드할 때 심볼의 ordinal이 명시적으로 설정되지 않으면 자동으로 하나가 할당되는데, 그 할당된 ordinal은 바이너리를 빌드할 때마다 바뀔 수 있어요.

#[cfg(all(windows, target_arch = "x86"))]
#[link(name = "exporter", kind = "raw-dylib")]
unsafe extern "stdcall" {
    #[link_ordinal(15)]
    safe fn imported_function_stdcall(i: i32);
}
  • 이 속성은 raw-dylib 링킹 종류에서만 사용돼요. 다른 종류를 쓰면 컴파일 에러가 나요.
  • link_name 속성과 함께 쓰면 컴파일 에러가 나요.

함수 매개변수의 속성

extern 함수 매개변수의 속성은 일반 함수 매개변수의 속성과 같은 규칙과 제약을 따라요.

더 알아보기 (Learn more)