Workspace link static c/c++ library - release vs debug

I'm having issues linking a static C/C++ library into a rust library, and then that library into an executable, all inside one workspace. The strange part is that it works in debug mode (default, just cargo build) but not in release mode (with cargo build --release). This is on Windows 10.

I've built the static library in C/C++, and have tested it with a console application in C++. It also works directly from a Rust console application in either release or debug mode. There's something about trying to link together a Rust console application, to a rust library, to a static C library that's breaking but only in release mode.

My general workspace layout is the following:

workspace_test
--> Cargo.toml
--> console_test
  --> Cargo.toml
  --> build.rs
  --> src
    --> main.rs
|
--> link_to_c
  --> Cargo.toml
  --> build.rs
  --> RustTestStatic - (C library, I built this)
    --> x64
      --> debug
        --> RustTestStatic.lib
      --> release
        --> RustTestStatic.lib
  --> src
    --> lib.rs

The top-level workspace Cargo.toml is:

[workspace]
members = [
    "console_test",
    "link_to_c"
]

The Cargo.toml in console_test is:

[package]
name = "console_test"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
link_to_c = { path = "../link_to_c" }

The main.rs there is:

use std::error::Error;

fn main()-> Result<(), Box<dyn Error>> {
    println!("Hello, world!");
    let print_result = link_to_c::print_int(55);
    println!("Got back {} from C/C++", print_result);
    Ok(())
}

In link_to_c the Cargo.toml is:

[package]
name = "link_to_c"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
libc = "0.2"

The build.rs there is:

use std::error::Error;

fn main()-> Result<(), Box<dyn Error>> {
    // Get the proper directory for this based on the build profile
    let profile = std::env::var("PROFILE").unwrap();
    let subdir = match profile.as_str() {
        "debug" => "Debug",
        "release" => "Release",
        _ => panic!("Unknown cargo profile:"),
    };
    let cur_path = std::env::current_dir()?;
    println!("cargo:rustc-link-search={}\\RustTestStatic\\x64\\{subdir}", cur_path.display());

    // didn't do anything, not needed on debug either!
    //println!("cargo:rustc-link-lib=static=RustTestStatic");

    Ok(())
}

In link_to_c lib.rs it is:

use libc::size_t;

#[link(name = "RustTestStatic", kind = "static")]
extern {
    fn print_int_val(value: i32) -> size_t;
}

pub fn print_int(num: i32) -> usize {
    unsafe {
        print_int_val(num)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn print_works() {
        let result = print_int(54);
        assert_eq!(result, 13);
    }
}

And for reference, the library's .h and .cpp files are:

#pragma once

#include <cstdint>

extern "C"
{
    extern size_t print_int_val(int value);
}

.cpp

// RustTestStatic.cpp : Defines the functions for the static library.
//

#include "pch.h"
#include "framework.h"
#include "RustTestStatic.h"
#include <cstdio>

size_t print_int_val(int value)
{
    auto num_printed = printf("Value is: %d\n", value);
    return num_printed;
}

Again, I can take this static library, link it from a console application, works with the same build.rs in either release or debug mode. And in the workspace, I can even do cargo test and that's fine. And just cargo build or cargo run and it's fine. But add --release and it fails.

The message I get from cargo on failure is:

  = note: liblink_to_c-f5eb5c4be5a6058d.rlib(link_to_c-f5eb5c4be5a6058d.link_to_c.7ca51d5a-cgu.0.rcgu.o) : error LNK2019: unresolved external symbol print_int_val referenced in function _ZN9link_to_c9print_int17h48d39a375ccf86f1E
          C:\Users\Kevin\Source\Rust\workspace_test\target\release\deps\console_test.exe : fatal error LNK1120: 1 unresolved externals

I omitted the full link.exe command, but it includes "/LIBPATH:C:\\Users\\Kevin\\Source\\Rust\\workspace_test\\link_to_c\\RustTestStatic\\x64\\Release" so it's definitely getting to the right directory.

Why is this working in debug, but not release? And again, same build.rs works just fine for a console application, but not "via" a library. And I'd prefer everything statically linked (at least for now).

You have two versions of your C library lying around and the release version does not contain the print_int_val function from how I interpret the error message. Could you try link the debug\RustTestStatic.lib instead of release\RustTestStatic.lib when you compile your binary package with cargo build --release and see if the error is resolved? Or just copy the debug version to the release folder would probably be easier.


Edit: if that doesn't work, you can also try to disable the optimizations applied by the release profile one by one and see which one is causing the problem. I.e. you could write something like this in the Cargo.toml of your workspace:

[profile.release]
opt-level = 2

This will decrease the level of optimization done when you compile with the --release flag.

@jofas - Neither worked. I have two versions, because one is debug, and the other is release.

As for the opt-level stuff, that didn't do anything either.

I did get one thing to work: I added a build.rs file to the console_test project and had the following line in main:

use std::error::Error;

fn main()-> Result<(), Box<dyn Error>> {
    println!("cargo:rustc-link-lib=static=RustTestStatic");
    Ok(())
}

So that "fixes" it, but why doesn't that line do anything in the library's build.rs file? I want the library to be a static library just like I'm familiar with in C/C++. I want it to link statically what I need into it, so that the consuming libraries just need to link against it, and nothing else.

It's maddening that apparently this is happening in debug, but not release mode. Or is something else entirely at play here?

Maybe adding a [package.links] to the Cargo.toml of your link_to_c will help?

[package]
name = "link_to_c"
version = "0.1.0"
edition = "2021"
links = "RustTestStatic"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
libc = "0.2"

Fresh information: copying the debug library DOES work, but I had to cargo clean first. Sorry for misreporting.

But what I said above is also true. Adding that line to build.rs in the console_test directory also works, but I don't need the "debug" version in the release directory then!

I also did dumpbin on the .lib files. On the "debug" version of the .lib, "print_int_vals" comes up no problem with dumpbin /SYMBOLS RustTestStatic.lib but you need dumpbin /ALL RustTestStatic.lib for it to show up - but it does show up.

Also, previously tried links - did nothing. Thanks for trying though!

There is also a great compilation of resources for linking from an older topic, maybe you will find an answer there:

I find this section very interesting: Using C libraries in Rust: make a sys crate

I appreciate the link to the other thread, but that article sucks for me because it has no examples!!!! It says "do this!" and then doesn't actually have a functioning working example that it shows. It has a few linux-specific crates linked at the top, but that's it.

If I don't get any more success here, I'll try and create a minimal github repository showing my problems, and link that, with "works" and "doesn't work" examples.

Given that I can get it to work with either A) the debug library, or B) the executable linking via build.rs, I think it's something wrong with how the Rust library is made, but I'm really not sure.

I'm not terribly familiar with C++, but are you sure your header file is correct? It looks to me like you're saying print_int_val is being provided by another library rather than that your library is providing it. AIUI extern "C" and extern do very different things.

I'm not sure why that behavior would be different between debug and release though

@semicoleon - extern "C" just makes sure name-mangling doesn't occur. And given I can link it fine from a C++ console application, the symbols are there.

I have uploaded the entire repository (cleanly) up onto GitHub: workspace_test_public

My goals are the following:

  • Make a rust library that statically links a C/C++ library
    • And making it two different library names, such as "cpp_library_d.lib" for debug, and "cpp_library.lib" for release is also desirable, but not in the C++ library either right now. A lot of 3rd-party stuff uses this kind of convention.
  • Make a rust executable that only needs a simple entry in the [dependencies] section of its cargo.toml file for it to "just work". Or a thorough explanation as to why that isn't possible, and how other libraries which link against C/C++ do this.

Long-term:

  • Make a rust library that dynamically links a C/C++ library, same as above.
    • I acknowledge the possibility there will have to be a "copy the .dll into the output directory" step somewhere, but if that's in the rust library part of it, that's better than in the .exe part.

I need to be able to demonstrate everything above so I can "pitch" Rust appropriately at work. If I can't support 3rd-party libraries cleanly then it's just a non-starter. Lots of custom hardware with libraries we don't control.

This problem looks very much like the "creating a *-sys crate" topic, but that article does not give a "Minimal, Reproducible Example". Not even close. That's what I'm trying to create here. Isn't this a solved problem? @kornel - you made that article. Do you have such a workspace?

1 Like

Hi, just to say something as this is interesting to me, I think that maybe since you haven’t “forced” cargo to treat your library as a Rust dynamic library, then maybe it’s building the release as a static rust lib, which wouldn’t link to another static lib… I’ll try asserting that since it is said that a lib would be compiled as preferably, and you don’t have in your cargo file that you expect to link to a native lib.

OK @drehren - how does one force it as a DLL? This doesn't quite make sense to me, as in C/C++ if you make a simple .lib (which is static) you definitely can link in other statics (in Visual Studio, look in Librarian->General->Additional Dependencies to do this), so that this would be weird to be required for Rust.

Somebody out there (maybe a bunch of somebodies) is doing this already. Heck, virtually every "*-sys crate" is probably doing this. But what's the "magic" for that? I understand why this is not "natural" to exist, since if you had a "trivial" C/C++ library, you'd just reimplement it in Rust, and not link it in. But once you get non-trivial, then the example ceases to be useful to others, as it's too complicated.

I just want a Minimal, Reproducible Example of making a "*-sys" crate that I can put into the [dependencies] area of Cargo (with no other changes necessary) for a different binary output crate (not just linking C/C++ from an application directly). And it's maddening that this works as-is for Debug mode, but not Release. If both were broken, this would be less frustrating. But that's not what's happening.

I played around with your repo a bit yesterday and wasn't able to get anywhere, I suspect that there's something specific that your Visual Studio project is (or isn't doing) that is causing problems.

Following the instructions for setting up the cc crate to compile your C++ file directly does appear to work in both debug and release

I created a fork here with those changes so they can be tested easily. Unfortunately I have basically no familiarity with Visual Studio projects so I'm not able to pinpoint what you might need to change to make the VS builds work for you too.

I appreciate your attempts @semicoleon, but unfortunately as a requirement for me is the ability to integrate 3rd-party libraries of which I cannot compile (binary-only), this capability isn't useful to me. Also you compiled it as a C++ file, which the .h file had as an extern "C" which had the original linkage as C (no name-mangling), so it's a bit of a different API too. Still, I appreciate you investigating.

The fact that debug works without changes, but release doesn't (but that release library DOES work with the pure C++ console application) makes me suspect "magic" in the cargo toolchain somewhere in debug mode. Basically, I don't believe that the problem is on the C/C++ side, because there's no problem linking there, only in Rust. Probably the library crate, but not sure what even to concentrate on.

Right, I assumed building it in the build script wasn't going to solve your problem. I just wanted to rule out that something about the source code was causing the problem

You said the linking works in a Rust console app too though. To rule out anything on the C++ side, I think you'd need to create another C++ static library that links to the pre-built library and then link that into the C++ console app. Otherwise you're comparing apples and oranges.

1 Like

Hi again, I downloaded your repo, and noticed the problem.

It is caused by building the RELEASE lib with "Link Time Code Generation". (In Visual Studio, look in Advanced->Whole Program Optimization)

This rust issue could have more useful information for you.

1 Like

@drehren - That was it! Tyvm! Note for others: change that to "No Whole Program Optimization" and it works. I've pushed the change up to master so others can see it as well. That's why the debug build worked, because debugging doesn't have that optimization!

Worth noting that the issue you link definitely looks promising, but the RFC for it was rejected due to inactivity, so the "solution" there of having a different link "kind" does not exist. I would be fascinated to know if that would have solved this as well.

So @semicoleon, you were right that something was "off" with the library that Visual Studio was able to work with, but Rust was not. Additionally, I did some tests with "redirecting" the library via another library, and I got down a rabbit hole of pre-compiled headers shenanigans that apparently linking via #pragma comment(lib, "Libraryname.lib") solves, but adding it to Additional Dependencies in the linker does not! But that's not relevant to this issue, as that didn't affect anything Rust-side (there or not, didn't matter).

Either way, thank you everybody who replied for all your help. This is a huge step for me, as 3rd-party that can not be controlled/recompiled is huge for my use-case at work. I probably have a bit more to do to make it 100% "nice" (the package.links think in cargo might be needed for correctness), but I'll work on that as time goes on.

It looks like the "static-nobundle" option got switched to #[link(..., kind = "static", modifiers = "-bundle")] in Linking modifiers for native libraries by petrochenkov · Pull Request #2951 · rust-lang/rfcs · GitHub

Changing your link_to_c.rs to use that seems to work without modifying the VS project at all

#[link(name = "RustTestStatic", kind = "static", modifiers = "-bundle")]
extern "C" {
    fn print_int_val(value: i32) -> size_t;
}
1 Like

That's cool @semicoleon - Also of note, I figured out how to do a different library name for debug/release:

#[cfg_attr(debug_assertions, link(name = "RustTestStatic_d", kind = "static"))]
#[cfg_attr(not(debug_assertions), link(name = "RustTestStatic", kind = "static"))]
extern {
    fn print_int_val(value: i32) -> size_t;
}

I renamed the library in the directories, and obviously you could key on 3rd-party that had that naming convention.

Assuming this answer on Stackoverflow is still accurate. It works, but I don't know if that's still best practice or not.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.