Skip to content

Commit 0d1163e

Browse files
committed
tests: add shared library integration test
+ Introduced a new integration test for validating shared library functionality. + Added a new workspace member `tests/shared_library` to the `Cargo.toml`. + Implemented a shared library test suite to verify `cxx-gen`'s ability to handle DLL scenarios with bidirectional function calls. + Created `tests/shared_library/library` with `lib.rs`, `build.rs`, and `Cargo.toml` for the shared library. + Added `tests/main.cc` and `tests/test_shared_library.rs` for testing the shared library's interaction with a host executable. + Updated `.gitignore` to exclude generated files from the shared library tests.
1 parent 021bee3 commit 0d1163e

File tree

11 files changed

+569
-1
lines changed

11 files changed

+569
-1
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@
1111
/expand.rs
1212
/target/
1313
/Cargo.lock
14+
/tests/shared_library/library/target/
15+
/tests/shared_library/library/Cargo.lock

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ cxx-build = { version = "=1.0.194", path = "gen/build" }
5151
cxxbridge-cmd = { version = "=1.0.194", path = "gen/cmd" }
5252

5353
[workspace]
54-
members = ["demo", "flags", "gen/build", "gen/cmd", "gen/lib", "macro", "tests/ffi"]
54+
members = ["demo", "flags", "gen/build", "gen/cmd", "gen/lib", "macro", "tests/ffi", "tests/shared_library"]
5555

5656
[package.metadata.docs.rs]
5757
targets = ["x86_64-unknown-linux-gnu"]

tests/shared_library/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "cxx-test-shared-library"
3+
version = "0.0.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[[test]]
8+
name = "test_shared_library"
9+
harness = true
10+
11+
[dev-dependencies]
12+
cc = "1.0"
13+
cargo_metadata = "0.19"

tests/shared_library/README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Shared Library Test
2+
3+
This test validates cxx-gen's ability to generate code for shared library/DLL scenarios with bidirectional function calls.
4+
5+
## What This Tests
6+
7+
This test specifically validates the cxx_gen functionality for generating:
8+
9+
1. **export_symbols** - List of mangled symbols the shared library exports (Rust functions callable from C++)
10+
2. **import_symbols** - List of mangled symbols the EXE must export (C++ functions callable from Rust)
11+
3. **import_thunks** - C++ code compiled into the DLL to dynamically load EXE's exports via GetProcAddress (Windows only)
12+
13+
### Platform-Specific Behavior
14+
15+
**Windows:**
16+
- Uses `.def` files to control symbol exports/imports
17+
- Generates thunks that use `GetProcAddress` to dynamically resolve symbols from the executable
18+
- Thunks are compiled into the DLL
19+
20+
**Linux:**
21+
- Uses a version script to control symbol visibility in the shared library
22+
- Uses a dynamic list to export only the required symbols from the executable (more precise than `--export-dynamic`)
23+
- The shared library resolves imported symbols directly (no thunks needed)
24+
- Uses `rpath` with `$ORIGIN` for library loading
25+
26+
**macOS:**
27+
- Uses `-undefined dynamic_lookup` to allow undefined symbols
28+
- The executable makes symbols available to the shared library at runtime
29+
30+
## Structure
31+
32+
- `library/lib.rs` - Defines the cxx bridge with exported Rust functions and imported C++ functions
33+
- `library/build.rs` - Uses `cxx_gen::generate_header_and_cc()` to generate .def files and thunks
34+
- `tests/main.cc` - Test executable that implements C++ functions and calls library functions
35+
- `tests/exe_functions.{h,cc}` - Implementation of functions exported by the executable
36+
- `tests/test_shared_library.rs` - Integration test that verifies the generated files and builds/runs test exe
37+
38+
## How It Works
39+
40+
1. **Library Build** (`library/build.rs`):
41+
- Parses `lib.rs` using cxx_gen to extract bridge declarations
42+
- Generates `library.def` with export_symbols (functions DLL exports) on Windows
43+
- Generates `exe.def` with import_symbols (functions EXE must export) on Windows
44+
- Generates `thunks.cc` with import_thunks (GetProcAddress-based loaders on Windows)
45+
- On Linux, generates a version script to control symbol visibility
46+
- On macOS, generates a response file with `-U` flags for each import symbol
47+
- Compiles everything into `test_library.dll` (Windows) or `libtest_library.so` (Linux/macOS)
48+
49+
2. **Test Execution** (`tests/test_shared_library.rs`):
50+
- Detects the test's target platform and builds the library with the same target
51+
- Builds the library with `cargo build --manifest-path library/Cargo.toml --target <target>`
52+
- Verifies .def files contain correct mangled symbols (Windows only)
53+
- Compiles a test executable linking main.cc + generated lib.cc + shared library
54+
- **Windows**: Uses .def files, DLL import library, and MSVC linker
55+
- **Linux**: Uses `--export-dynamic` to export symbols from executable, and `rpath` for library loading
56+
- **macOS**: Uses `@loader_path` rpath and dynamic symbol lookup
57+
- Runs the executable to validate bidirectional calling works
58+
59+
3. **Test Executable** (`tests/main.cc`):
60+
- Implements `exe_callback()` and `exe_get_constant()` in `tests/exe_functions.cc`
61+
- Calls library's `get_magic_number()`, `multiply_values()`, `library_entry_point()`
62+
- All calls go through cxx bridge (not direct mangled names)
63+
64+
## Testing
65+
66+
The tests verify that:
67+
- The library builds successfully on Windows, Linux, and macOS
68+
- Exported functions can be called and return correct values
69+
- Imported functions are called correctly by the library
70+
- The entry point demonstrates the full round-trip
71+
- Platform-specific symbol resolution mechanisms work correctly
72+
73+
Run with:
74+
```bash
75+
# Test for your current platform
76+
cargo test
77+
78+
# Test for a specific target
79+
cargo test --target x86_64-unknown-linux-gnu
80+
cargo test --target x86_64-pc-windows-msvc
81+
cargo test --target x86_64-apple-darwin
82+
```
83+
84+
## Expected Behavior
85+
86+
- `get_magic_number()` calls `exe_get_constant()` (returns 1000) and adds 42, returning 1042
87+
- `multiply_values(3, 4)` computes 3*4=12, then calls `exe_callback(12)` which doubles it to 24
88+
- `library_entry_point()` calls both `exe_callback(100)` and `exe_get_constant()`, returning 200 + 1000 = 1200
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "test-library"
3+
version = "0.0.0"
4+
edition = "2021"
5+
publish = false
6+
7+
# Exclude from parent workspace - this is a standalone test library
8+
[workspace]
9+
10+
[lib]
11+
crate-type = ["cdylib"]
12+
path = "lib.rs"
13+
14+
[dependencies]
15+
cxx = { path = "../../.." }
16+
17+
[build-dependencies]
18+
cxx-gen = { path = "../../../gen/lib" }
19+
cc = "1.0"
20+
proc-macro2 = "1.0"
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use std::env;
2+
use std::fs;
3+
use std::io::Write;
4+
use std::path::PathBuf;
5+
6+
fn main() {
7+
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
8+
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
9+
let profile = env::var("PROFILE").unwrap();
10+
let test_artifacts_dir = manifest_dir.join(format!("target/cxxbridge/{}", profile));
11+
fs::create_dir_all(&test_artifacts_dir).unwrap();
12+
13+
// generate the bridge using cxx_gen to get symbols and thunks
14+
let lib_source = fs::read_to_string(manifest_dir.join("lib.rs")).unwrap();
15+
let lib_tokens: proc_macro2::TokenStream = lib_source.parse().unwrap();
16+
let generated = cxx_gen::generate_header_and_cc(lib_tokens, &cxx_gen::Opt::default()).unwrap();
17+
18+
// write header and implementation
19+
fs::write(out_dir.join("lib.h"), &generated.header).unwrap();
20+
fs::write(out_dir.join("lib.cc"), &generated.implementation).unwrap();
21+
22+
// create EXE symbols file in OS-appropriate format
23+
// Use TARGET env var to get the target OS, not the host OS
24+
let target = env::var("TARGET").unwrap();
25+
let target_os = TargetOs::from(target.as_str());
26+
27+
let exe_symbols_content = cxx_gen::format_import_symbols_for_linker(
28+
&generated.import_symbols(),
29+
target_os.as_str(),
30+
);
31+
32+
let exe_symbols_path = match target_os {
33+
TargetOs::Windows => out_dir.join("exe.def"),
34+
TargetOs::Macos => out_dir.join("exe_undefined.txt"),
35+
TargetOs::Linux => out_dir.join("exe.dynamic"),
36+
};
37+
fs::write(&exe_symbols_path, exe_symbols_content).unwrap();
38+
39+
// compile C++ code needed by the library
40+
// On Windows: compile thunks (which call back to the exe via GetProcAddress)
41+
// On Unix: no C++ code needed in the library (generate_import_thunks returns empty string)
42+
let thunks = generated.generate_import_thunks(target_os.as_str());
43+
if !thunks.is_empty() {
44+
// write thunks that will be compiled into the DLL
45+
let thunks_path = out_dir.join("thunks.cc");
46+
fs::write(&thunks_path, &thunks).unwrap();
47+
48+
let mut build = cc::Build::new();
49+
build
50+
.cpp(true)
51+
.flag("/EHsc")
52+
.file(&thunks_path)
53+
.include(&out_dir)
54+
.include(manifest_dir.parent().unwrap().join("tests")); // for exe_functions.h
55+
build.compile("cxx-test-shared-library");
56+
}
57+
58+
// on Windows, use the .def file for exports
59+
if target_os == TargetOs::Windows {
60+
// create DLL .def file with export symbols (functions the DLL exports)
61+
let dll_def_content = cxx_gen::format_export_symbols_for_linker(
62+
&generated.export_symbols(),
63+
"windows",
64+
);
65+
let dll_def_path = out_dir.join("library.def");
66+
fs::write(&dll_def_path, dll_def_content).unwrap();
67+
68+
println!("cargo:rustc-cdylib-link-arg=/DEF:{}", dll_def_path.display());
69+
70+
// copy for the test to use
71+
fs::copy(&dll_def_path, test_artifacts_dir.join("library.def")).unwrap();
72+
} else if target_os == TargetOs::Macos {
73+
// Per ld(1) man page: "-U symbol_name: Specified that it is ok for symbol_name to
74+
// have no definition. With -two_levelnamespace, the resulting symbol will be marked
75+
// dynamic_lookup which means dyld will search all loaded images."
76+
//
77+
// The Rust code in the library calls the cxxbridge wrapper functions (import_symbols),
78+
// which are implemented in lib.cc that's compiled into the executable.
79+
println!("cargo:rustc-cdylib-link-arg=-Wl,@{}", exe_symbols_path.display());
80+
} else {
81+
// on Linux, create a version script to export symbols and allow undefined symbols
82+
let mut version_script = Vec::new();
83+
writeln!(version_script, "{{").unwrap();
84+
writeln!(version_script, " global:").unwrap();
85+
for sym in &generated.export_symbols() {
86+
writeln!(version_script, " {};", sym).unwrap();
87+
}
88+
writeln!(version_script, " local: *;").unwrap();
89+
writeln!(version_script, "}};").unwrap();
90+
let version_script_path = out_dir.join("libtest_library.version");
91+
fs::write(&version_script_path, version_script).unwrap();
92+
93+
println!("cargo:rustc-cdylib-link-arg=-Wl,--version-script={}", version_script_path.display());
94+
println!("cargo:rustc-cdylib-link-arg=-Wl,--allow-shlib-undefined");
95+
}
96+
97+
// expose paths for the test to use
98+
println!("cargo:rustc-env=EXE_SYMBOLS_PATH={}", exe_symbols_path.display());
99+
100+
// copy generated files to a predictable location for the test to find
101+
fs::copy(&exe_symbols_path, test_artifacts_dir.join(exe_symbols_path.file_name().unwrap())).unwrap();
102+
fs::copy(out_dir.join("lib.h"), test_artifacts_dir.join("lib.h")).unwrap();
103+
fs::copy(out_dir.join("lib.cc"), test_artifacts_dir.join("lib.cc")).unwrap();
104+
105+
println!("cargo:rerun-if-changed=lib.rs");
106+
}
107+
108+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109+
enum TargetOs {
110+
Windows,
111+
Macos,
112+
Linux,
113+
}
114+
115+
impl From<&str> for TargetOs {
116+
fn from(target: &str) -> Self {
117+
if target.contains("windows") {
118+
TargetOs::Windows
119+
} else if target.contains("darwin") || target.contains("ios") {
120+
TargetOs::Macos
121+
} else {
122+
TargetOs::Linux
123+
}
124+
}
125+
}
126+
127+
impl TargetOs {
128+
fn as_str(self) -> &'static str {
129+
match self {
130+
TargetOs::Windows => "windows",
131+
TargetOs::Macos => "macos",
132+
TargetOs::Linux => "linux",
133+
}
134+
}
135+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#[cxx::bridge]
2+
pub mod ffi {
3+
extern "Rust" {
4+
// functions exported by the cdylib, imported by test exe
5+
fn get_magic_number() -> i32;
6+
fn multiply_values(a: i32, b: i32) -> i32;
7+
fn library_entry_point() -> i32;
8+
}
9+
10+
unsafe extern "C++" {
11+
include!("exe_functions.h");
12+
13+
// functions exported by test exe, imported by cdylib
14+
fn exe_callback(value: i32) -> i32;
15+
fn exe_get_constant() -> i32;
16+
}
17+
}
18+
19+
pub fn get_magic_number() -> i32 {
20+
// call back to the exe to get a constant, then add our magic
21+
let exe_value = ffi::exe_get_constant();
22+
exe_value + 42
23+
}
24+
25+
pub fn multiply_values(a: i32, b: i32) -> i32 {
26+
// use exe callback to process the result
27+
let product = a * b;
28+
ffi::exe_callback(product)
29+
}
30+
31+
pub fn library_entry_point() -> i32 {
32+
// test that we can call exe functions from the library
33+
ffi::exe_callback(100) + ffi::exe_get_constant()
34+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#include "exe_functions.h"
2+
3+
// Implementation of exe callback functions
4+
int exe_callback(int value) {
5+
// doubles the value
6+
return value * 2;
7+
}
8+
9+
int exe_get_constant() {
10+
return 1000;
11+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#pragma once
2+
3+
// Functions exported by the executable that the library can call back to
4+
int exe_callback(int value);
5+
int exe_get_constant();

tests/shared_library/tests/main.cc

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#include <iostream>
2+
#include <cstdint>
3+
#include "lib.h"
4+
5+
int main() {
6+
std::cout << "Testing shared library interaction..." << std::endl;
7+
8+
std::int32_t magic = get_magic_number();
9+
std::cout << "magic number: " << magic << std::endl;
10+
if (magic != 1042) {
11+
std::cerr << "ERROR: expected 1042, got " << magic << std::endl;
12+
return 1;
13+
}
14+
15+
std::int32_t product = multiply_values(3, 4);
16+
std::cout << "multiply result: " << product << std::endl;
17+
if (product != 24) {
18+
std::cerr << "ERROR: expected 24, got " << product << std::endl;
19+
return 1;
20+
}
21+
22+
std::int32_t entry_result = library_entry_point();
23+
std::cout << "entry point result: " << entry_result << std::endl;
24+
if (entry_result != 1200) {
25+
std::cerr << "ERROR: expected 1200, got " << entry_result << std::endl;
26+
return 1;
27+
}
28+
29+
std::cout << "All tests passed!" << std::endl;
30+
return 0;
31+
}

0 commit comments

Comments
 (0)