We have a legacy C/C++ library in which we are trying to integrate some Rust code as a C style library. However, this C/C++ library should also be usable in Rust through a wrapper written in Rust.
The rust and C/C++ library works as expected. The build of the rust wrapper is successful, however we are unable to run the tests with cargo test, the linker is complaining that some symbols are defined multiple times (__rust_alloc etc.). There was no issue previously when the C/C++ library was not integrating Rust code.
There is no issue in Windows, only in Linux.
We made several unsuccessful attempts to fix this issue:
- Use the
ltoandstripoptions for the different profiles - Use the
cxxcreate - Use the
striplinux command line
Using the --allow-multiple-definition linker flag is working, but this looks like a dirty workaround.
Here is a small GitHub project mimicking what we would want to do. Below is an extract of the readme:
Description
This example is composed of several projects in C/Rust that depends on each other in this sequence:
rust_end_user->rust_wrap->c_lib->rust_lib
rust_lib:
- Rust project generating a C style static library
- Exposes a function
rust_addc_lib
- C static library which is using
rust_lib- Exposes a function
c_addwhich is callingrust_addrust_wrap
- Rust wrapper around
c_lib- Exposes a function
wrap_addwhich is callingc_addrust_end_user
- An "end user application" that is using
rust_wrapas a dependency in the rust project- Calls
wrap_addExpected result:
rust_lib:
- Run binary: OK
- Run unit test: OK
- Run integration test: OK
- Run example: OK
c_lib
- Run binary: OK
rust_wrap
- Run binary: OK
- Run unit test: NOK
- Link error about several symbols defined multiple times (
__rust_alloc,__rust_dealloc,__rust_no_alloc_shim_is_unstable, etc.)- If the
--allow-multiple-definitionis provided to the linker, it works- Run integration test: OK
- Run example: OK
rust_end_user
- Run binary: OK
- Run unit test: OK
- Run integration test: OK
- Run example: OK
And below are the most interesting code extracts:
rust_lib
Cargo.toml
[package]
name = "rust_lib"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "rust_lib.rs"
crate-type = ["staticlib", "lib"]
[...]
rust_lib.rs
#[no_mangle]
pub extern "C" fn rust_add(a: u8, b:u8) -> u8 {
let r = a + b;
println!("rust_add: {a} + {b} -> {r}");
r
}
[...]
rust_lib.h
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
uint8_t rust_add(uint8_t a, uint8_t b);
#ifdef __cplusplus
}
#endif
c_lib
c_lib.h
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
uint8_t c_add(uint8_t a, uint8_t b);
#ifdef __cplusplus
}
#endif
c_lib.cpp
#include <stdio.h>
#include "c_lib.h"
#include "../rust_lib/rust_lib.h"
uint8_t c_add(uint8_t a, uint8_t b) {
uint8_t r = rust_add(a, b);
printf("c_add: %d + %d -> %d\n", a, b, r);
return r;
}
compilation
[...]
gcc -c -o $BUILD_DIR/c_lib.o c_lib/c_lib.c
ar rcs $BUILD_DIR/libc_lib.a $BUILD_DIR/c_lib.o
[...]
rust_wrap
rust_wrap.rs
extern "C" {
fn c_add(a: u8, b: u8) -> u8;
}
pub fn wrap_add(a: u8, b: u8) -> u8 {
let r = unsafe { c_add(a, b) };
println!("wrap_add: {a} + {b} -> {r}");
r
}
[...]
build.rs
use std::process::Command;
fn main() {
[...]
println!("cargo::rustc-link-lib=static=c_lib");
println!("cargo::rustc-link-search=native=../_build/c_lib");
println!("cargo::rustc-link-lib=static=rust_lib");
println!("cargo::rustc-link-search=native=../_build/rust_lib/debug");
}
rust_end_user
Cargo.toml
[package]
name = "rust_end_user"
version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
name = "rust_end_user"
path = "main.rs"
[...]
main.rs
use rust_wrap;
fn main() {
println!("RUST END USER MAIN");
assert_eq!(rust_wrap::wrap_add(50, 77), 127);
println!();
}
[...]
The initial tests were made with rust 1.80 (where it seems to indeed be an issue). However it works fine with rust 1.85.
.afiles and keep only the object files of your (sub)project