4
votes

Basic setup

A simple C function that is called from Rust via FFI, and a static library (Windows) that is linked in build.rs.

// api.c

int increment(int value) {
    return value + 1;
}
// main.rs

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

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

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

With this directory structure:

Cargo.toml
Cargo.lock
build.rs
src
+-- main.rs
+-- ffi
    +-- api.c
    +-- api.lib

Cargo.toml has nothing but a crate name, version, author and Rust edition.

So far, this works perfectly, and "The incremented number is 5" is printed as expected.


The problem

Now I add an empty lib.rs file, so I can use my crate as both a library and a binary:

Cargo.toml
Cargo.lock
build.rs
src
+-- lib.rs     <-- NEW
+-- main.rs
+-- ffi
    +-- api.c
    +-- api.lib

I do not change Cargo.toml.
This alone makes the link step fail (MSVC linker):

error LNK2019: Unresolved external symbol "increment" referenced in function "_ZN16my_crate4main17h887bd80180495b7eE"

The error persists even if I explicitly specify the presence of both a library and binary in Cargo.toml and run using cargo run --bin my_main:

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

[[bin]]
name = "my_main"
path = "src/main.rs"

I also made sure that build.rs is still executed in the binary case, by letting it panic to abort the build.

I'm aware that I could solve it by splitting the crate, but I'd like to understand what exactly happens. So:


Why does the presence of an empty lib.rs cause the linker to fail?
And is there a way to make it succeed, in a single crate?

1
I don't specifically know your issue, but in the past when I wanted both a bin and a lib in the same crate I had to change the crate to be a lib (src/lib.rs) with no src/main.rs, and add binaries to it that used the lib (src/bin/bin1.rs, src/bin/bin2.rs, etc.).NovaDenizen

1 Answers

2
votes

In the presence of both a binary and a Rust library in a single crate, Rust statically links the binary against the Rust library itself, as if the library were any other external crate (e.g. from crates.io). I assume to prevent errors due to duplicate symbols, or possibly just to avoid code bloat or extra work, it appears to avoid linking any external artifacts directly against the binary, and does not make an effort to determine if the code in question is even available in lib.rs before making this judgment. Normally this happens in the background without you noticing (i.e. it's doing this to the Rust standard library, your system standard libraries, and external crates as well for everything you compile), but when you introduce custom FFI dependencies it becomes obvious.

If you change your lib.rs to

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

And your main.rs to

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

Where libname is either the name of your library as listed in Cargo.toml or the project name if that's not specificed, it will compile and run properly. Indeed, this follows no matter how complex you get. You can bury the FFI function a million modules deep and include it from wherever, but it will fail with the same error if you reference a definition of an extern "C" function without going through the master library crate first.

I don't know if there's any way (in build.rs or otherwise) to insist that Rust statically links a library either twice against the two different targets, or else only against the binary itself, but if there is it's not well documented or obvious.

That said, in most cases this is unlikely to be a problem, since if you're making your functionality usable as a library, it's likely you'll have the common code, including FFI, in there anyway. It could conceivably be an issue if you have a C dependency (e.g. for device access) that only makes sense in the binary, but I think that case is rare enough it's reasonable to force you to split the crates using workspaces if that's your specific situation.