main.rs 和 lib.rs 共存时无法链接 C 库

The*_*tor 5 linker ffi rust

基本设置

一个简单的 C 函数,通过 FFI 从 Rust 调用,以及一个在 build.rs 中链接的静态库 (Windows)。

// api.c

int increment(int value) {
    return value + 1;
}
Run Code Online (Sandbox Code Playgroud)
// main.rs

extern {
    fn increment(value: i32) -> i32;
}

fn main() {
    let num = unsafe { increment(4) };
    println!("The incremented number is {}", num);
}
Run Code Online (Sandbox Code Playgroud)
// build.rs

fn main() {
    println!("cargo:rustc-link-search=src/ffi");
    println!("cargo:rustc-link-lib=static=api");
}
Run Code Online (Sandbox Code Playgroud)

有了这个目录结构:

Cargo.toml
Cargo.lock
build.rs
src
+-- main.rs
+-- ffi
    +-- api.c
    +-- api.lib
Run Code Online (Sandbox Code Playgroud)

Cargo.toml 只有板条箱名称、版本、作者和 Rust 版本。

到目前为止,效果非常好,并且按预期打印了“增量数为 5”。


问题

现在我添加一个 lib.rs文件,这样我就可以将我的板条箱用作库和二进制文件:

Cargo.toml
Cargo.lock
build.rs
src
+-- lib.rs     <-- NEW
+-- main.rs
+-- ffi
    +-- api.c
    +-- api.lib
Run Code Online (Sandbox Code Playgroud)

我不会更改 Cargo.toml。
仅此一点就会导致链接步骤失败(MSVC 链接器):

错误LNK2019:函数“_ZN16my_crate4main17h887bd80180495b7eE”中引用了无法解析的外部符号“increment”

即使我在 Cargo.toml 中明确指定库和二进制文件的存在并使用以下命令运行,错误仍然存​​在cargo run --bin my_main

[lib]
name = "my_lib"
path = "src/lib.rs"

[[bin]]
name = "my_main"
path = "src/main.rs"
Run Code Online (Sandbox Code Playgroud)

我还确保它build.rs仍然在二进制情况下执行,通过让它恐慌中止构建。

我知道我可以通过拆分板条箱来解决这个问题,但我想了解到底发生了什么。所以:


为什么空的存在会lib.rs导致链接器失败?
有没有办法让它在一个箱子里成功?

Lin*_*ope 4

当单个 crate 中同时存在二进制文件和 Rust 库时,Rust 将二进制文件静态链接到 Rust 库本身,就好像该库是任何其他外部 crate(例如来自 crates.io)。我假设是为了防止由于重复符号而导致的错误,或者可能只是为了避免代码膨胀或额外的工作,它似乎避免将任何外部工件直接链接到二进制文件,并且不努力确定有问题的代码是否可用在lib.rs做出这个判断之前。通常,这种情况会在后台发生,而您不会注意到(即,它会对 Rust 标准库、您的系统标准库和外部包以及您编译的所有内容执行此操作),但是当您引入自定义 FFI 依赖项时,它会变得显而易见。

如果你改变你lib.rs

extern {
    pub fn increment(value: i32) -> i32;
}
Run Code Online (Sandbox Code Playgroud)

还有你main.rs

fn main() {
    let num = unsafe { libname::increment(4) };
    println!("The incremented number is {}", num);
}
Run Code Online (Sandbox Code Playgroud)

其中libname列出的库名称Cargo.toml或项目名称(如果未指定)将正确编译并运行。事实上,无论你变得多么复杂,这都是如此。extern "C"您可以将 FFI 函数埋藏一百万个模块深,并从任何地方包含它,但如果您在不首先检查主库箱的情况下引用函数的定义,它将失败并出现相同的错误。

我不知道是否有任何方法(build.rs或以其他方式)坚持Rust 静态链接库两次针对两个不同的目标,或者仅针对二进制文件本身,但如果有的话,它没有很好的记录或明显。

也就是说,在大多数情况下,这不太可能成为问题,因为如果您要使功能可用作库,那么无论如何您都可能在那里拥有公共代码,包括 FFI。如果您有一个仅在二进制文件中有意义的 C 依赖项(例如用于设备访问),那么这可能是一个问题,但我认为这种情况非常罕见,如果这是您的具体情况,那么强制您使用工作区拆分板条箱是合理的。