外部块

语法
ExternBlock :
   unsafe?1 extern Abi? {
      InnerAttribute*
      ExternalItem*
   }

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

1

从 2024 版本开始,语义上要求使用 unsafe 关键字。

外部块提供在当前 crate 中未 定义 的项的 声明,是 Rust 外国函数接口的基础。这类似于未检查的导入。

外部块中允许两种项的 声明函数静态变量

调用在外部块中声明的函数或访问静态变量只允许在 unsafe 上下文中进行。

外部块在其所在的模块或块的 值命名空间 中定义其函数和静态变量。

语义上要求 unsafe 关键字出现在外部块的 extern 关键字之前。

版本差异:在 2024 版本之前,unsafe 关键字是可选的。safeunsafe 项限定符只有在外部块本身标记为 unsafe 时才被允许。

函数

外部块内的函数以与其他 Rust 函数相同的方式声明,但例外的是它们不能有函数体,而是以分号结束。

参数中不允许使用模式,只能使用 标识符_

允许使用 safeunsafe 函数限定符,但不允许使用其他函数限定符(例如 constasyncextern)。

外部块内的函数可以像 Rust 中定义的函数一样被 Rust 代码调用。Rust 编译器会自动在 Rust ABI 和外部 ABI 之间进行转换。

在 extern 块中声明的函数是隐式 unsafe 的,除非存在 safe 函数限定符。

当强制转换为函数指针时,在 extern 块中声明的函数的类型为 extern "abi" for<'l1, ..., 'lm> fn(A1, ..., An) -> R,其中 'l1, … 'lm 是其生命周期参数,A1, …, An 是其参数的声明类型,R 是声明的返回类型。

静态变量

外部块内的静态变量的声明方式与外部块外的静态变量相同,只是它们没有初始化值的表达式。

除非在 extern 块中声明的静态项被限定为 safe,否则访问该项是 unsafe 的,无论它是否可变,因为无法保证静态变量内存中的位模式对于它所声明的类型是有效的,因为一些任意(例如 C)代码负责初始化该静态变量。

外部静态变量可以像外部块外的静态变量一样是不可变或可变的。

不可变静态变量在任何 Rust 代码执行之前 必须 被初始化。仅在 Rust 代码读取它之前初始化是不够的。一旦 Rust 代码运行,对不可变静态变量进行变更是 UB(从 Rust 内部或外部),除非变更发生在 UnsafeCell 内的字节上。

ABI

默认情况下,外部块假定它们调用的库在特定平台上使用标准的 C ABI。可以使用 abi 字符串指定其他 ABI,如下所示

#![allow(unused)]
fn main() {
#[cfg(any(windows, target_arch = "x86"))]
// Interface to the Windows API
unsafe extern "stdcall" { }
}

所有平台都支持以下 ABI 字符串

  • unsafe extern "Rust" – 在任何 Rust 代码中编写普通 fn foo() 时的默认 ABI。
  • unsafe extern "C" – 这与 extern fn foo() 相同;使用你的 C 编译器支持的任何默认设置。
  • unsafe extern "system" – 通常与 extern "C" 相同,但在 Win32 上除外,在这种情况下它是 "stdcall",或者你应该用来链接到 Windows API 本身的调用约定
  • extern "C-unwind"extern "system-unwind" – 分别与 "C""system" 相同,但在被调用者展开(通过 panic 或抛出 C++ 风格异常)时有不同的行为

还有一些平台特定的 ABI 字符串

  • unsafe extern "cdecl" – x86_32 C 代码的默认值。
  • unsafe extern "stdcall" – x86_32 上 Win32 API 的默认值。
  • unsafe extern "win64" – x86_64 Windows 上 C 代码的默认值。
  • unsafe extern "sysv64" – 非 Windows x86_64 上 C 代码的默认值。
  • unsafe extern "aapcs" – ARM 的默认值。
  • unsafe extern "fastcall"fastcall ABI – 对应于 MSVC 的 __fastcall 以及 GCC 和 clang 的 __attribute__((fastcall))
  • unsafe extern "thiscall" – MSVC 上 C++ 成员函数的默认值 – 对应于 MSVC 的 __thiscall 以及 GCC 和 clang 的 __attribute__((thiscall))
  • unsafe extern "efiapi" – 用于 UEFI 函数的 ABI。

"C""system" 类似,大多数平台特定的 ABI 字符串也有对应的 -unwind 变体;具体来说,它们是

  • "aapcs-unwind"
  • "cdecl-unwind"
  • "fastcall-unwind"
  • "stdcall-unwind"
  • "sysv64-unwind"
  • "thiscall-unwind"
  • "win64-unwind"

可变参数函数

外部块内的函数可以通过将 ... 指定为最后一个参数来成为可变参数函数。可变参数可以可选地与一个标识符一起指定。

#![allow(unused)]
fn main() {
unsafe extern "C" {
    safe fn foo(...);
    unsafe fn bar(x: i32, ...);
    unsafe fn with_name(format: *const u8, args: ...);
}
}

外部块上的属性

以下属性控制外部块的行为。

link 属性 指定了编译器应为 extern 块中的项链接的本地库名称。

它使用 MetaListNameValueStr 语法指定其输入。name 键是要链接的本地库名称。kind 键是一个可选值,用于指定库的类型,可能的取值如下

  • dylib — 表示动态库。如果未指定 kind,则这是默认值。
  • static — 表示静态库。
  • framework — 表示 macOS 框架。这仅对 macOS 目标有效。
  • raw-dylib — 表示一个动态库,编译器将生成一个导入库来链接(详见下面的 dylibraw-dylib)。这仅对 Windows 目标有效。

如果指定了 kind,则必须包含 name 键。

可选的 modifiers 参数是一种为要链接的库指定链接修饰符的方式。

修饰符指定为逗号分隔的字符串,每个修饰符前缀为 +-,分别表示修饰符已启用或禁用。

目前不支持在单个 link 属性中指定多个 modifiers 参数,或在同一个 modifiers 参数中指定多个相同的修饰符。
示例:#[link(name = "mylib", kind = "static", modifiers = "+whole-archive")]

wasm_import_module 键可用于在从宿主环境导入符号时,指定 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 属性是有效的。你可以使用它来满足代码中其他地方(包括上游 crates)的 extern 块的链接要求,而不是将该属性添加到每个 extern 块中。

链接修饰符:bundle

此修饰符仅与 static 链接类型兼容。使用任何其他类型将导致编译器错误。

构建 rlib 或 staticlib 时,+bundle 意味着本地静态库将被打包到 rlib 或 staticlib 存档中,然后在链接最终二进制文件时从那里检索。

构建 rlib 时,-bundle 意味着本地静态库被“按名称”注册为该 rlib 的依赖项,并且其中的目标文件仅在链接最终二进制文件时包含,按名称的文件搜索也在最终链接时执行。
构建 staticlib 时,-bundle 意味着本地静态库根本不包含在存档中,并且一些更高级的构建系统需要在稍后链接最终二进制文件时添加它。

构建可执行文件或动态库等其他目标时,此修饰符无效。

此修饰符的默认值为 +bundle

有关此修饰符的更多实现细节,请参阅 rustc 的 bundle 文档

链接修饰符:whole-archive

此修饰符仅与 static 链接类型兼容。使用任何其他类型将导致编译器错误。

+whole-archive 意味着静态库作为一个完整的存档链接,不丢弃任何目标文件。

此修饰符的默认值为 -whole-archive

有关此修饰符的更多实现细节,请参阅 rustc 的 whole-archive 文档

链接修饰符:verbatim

此修饰符兼容所有链接类型。

+verbatim 意味着 rustc 本身不会给库名称添加任何目标特定的前缀或后缀(例如 lib.a),并且会尽最大努力要求链接器也这样做。

-verbatim 意味着 rustc 会在将库名称传递给链接器之前添加目标特定的前缀和后缀,或者不会阻止链接器隐式地添加它们。

此修饰符的默认值为 -verbatim

有关此修饰符的更多实现细节,请参阅 rustc 的 verbatim 文档

dylib 对比 raw-dylib

在 Windows 上,链接动态库需要向链接器提供一个导入库:这是一个特殊的静态库,它以一种方式声明了动态库导出的所有符号,以便链接器知道这些符号必须在运行时动态加载。

指定 kind = "dylib" 指示 Rust 编译器基于 name 键链接一个导入库。然后链接器会使用其正常的库解析逻辑来查找该导入库。或者,指定 kind = "raw-dylib" 指示编译器在编译期间生成一个导入库并将其提供给链接器。

raw-dylib 仅在 Windows 上受支持。在定位其他平台时使用它将导致编译器错误。

import_name_type

在 x86 Windows 上,函数名称会被“修饰”(即添加特定的前缀和/或后缀)以指示其调用约定。例如,一个名为 fn1 且没有参数的 stdcall 调用约定函数将被修饰为 _fn1@0。然而,PE 格式也允许名称没有前缀或不被修饰。此外,MSVC 和 GNU 工具链对相同的调用约定使用不同的修饰方式,这意味着默认情况下,无法通过 GNU 工具链使用 raw-dylib 链接类型调用某些 Win32 函数。

为了允许这些差异,当使用 raw-dylib 链接类型时,你还可以指定 import_name_type 键,使用以下值之一来改变在生成的导入库中函数名称的方式

  • decorated:函数名称将使用 MSVC 工具链格式进行完全修饰。
  • noprefix:函数名称将使用 MSVC 工具链格式进行修饰,但跳过开头的 ?@ 或可选的 _
  • undecorated:函数名称将不被修饰。

如果未指定 import_name_type 键,则函数名称将使用目标工具链的格式进行完全修饰。

变量从不被修饰,因此 import_name_type 键对其在生成的导入库中的命名方式没有影响。

import_name_type 键仅在 x86 Windows 上受支持。在定位其他平台时使用它将导致编译器错误。

可以在 extern 块内的声明上指定 link_name 属性,以指示要为给定函数或静态变量导入的符号。

它使用 MetaNameValueStr 语法来指定符号名称。

#![allow(unused)]
fn main() {
unsafe extern {
    #[link_name = "actual_symbol_name"]
    safe fn name_in_rust();
}
}

将此属性与 link_ordinal 属性一起使用将导致编译器错误。

link_ordinal 属性 可以应用于 extern 块内的声明,以指示生成导入库时要使用的数字序号。序号是 Windows 上动态库导出的每个符号的唯一编号,可以在加载库时用来查找该符号,而无需按名称查找。

警告

link_ordinal 只应在已知符号序号稳定的情况下使用:如果符号在其包含二进制文件构建时未明确设置序号,则会自动为其分配一个序号,并且该分配的序号可能会在二进制文件的不同构建之间发生变化。

#![allow(unused)]
fn main() {
#[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 属性一起使用将导致编译器错误。

函数参数上的属性

外部函数参数上的属性遵循与 常规函数参数 相同的规则和限制。