#![cfg(feature = "codegen")]
use std::path::{Path, PathBuf};
use std::process::Command;
use dlsym::codegen::{Builder, parse};
fn skip(why: &str) {
if std::env::var("DLSYM_REQUIRE_E2E").is_ok_and(|v| !v.is_empty() && v != "0") {
panic!("end-to-end test required but {why}");
}
eprintln!("skipping end-to-end test: {why}");
}
fn compiler(env: &str, names: [&str; 3]) -> Option<String> {
let candidates = std::env::var(env)
.into_iter()
.chain(names.map(String::from));
candidates.into_iter().find(|cc| {
Command::new(cc)
.arg("--version")
.output()
.is_ok_and(|o| o.status.success())
})
}
fn fixtures() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/demo")
}
fn compile(cc: &str, source: &Path, lib: &Path) {
let mut cmd = Command::new(cc);
if cfg!(target_vendor = "apple") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
if cfg!(unix) {
cmd.arg("-fPIC");
}
let status = cmd
.arg("-o")
.arg(lib)
.arg(source)
.status()
.expect("run the compiler");
assert!(status.success(), "{cc} failed on {}", source.display());
}
fn run(name: &str, work: &Path, code: &str, main: &Path, lib: &Path) {
std::fs::create_dir_all(work.join("src")).unwrap();
std::fs::write(work.join("src/bindings.rs"), code).unwrap();
std::fs::copy(main, work.join("src/main.rs")).unwrap();
std::fs::write(
work.join("Cargo.toml"),
format!(
"[package]\nname = \"{name}-e2e\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\
[dependencies]\ndlsym = {{ path = {:?} }}\n\n[workspace]\n",
env!("CARGO_MANIFEST_DIR")
),
)
.unwrap();
let out = Command::new(env!("CARGO"))
.args(["run", "--quiet", "--manifest-path"])
.arg(work.join("Cargo.toml"))
.arg("--")
.arg(lib)
.env(
"CARGO_TARGET_DIR",
Path::new(env!("CARGO_TARGET_TMPDIR")).join("e2e-target"),
)
.env(
"RUSTFLAGS",
std::env::var("DLSYM_E2E_RUSTFLAGS").unwrap_or_default(),
)
.output()
.expect("run cargo");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success() && stdout.contains(&format!("{name} ok")),
"generated bindings failed:\n--- stdout\n{stdout}\n--- stderr\n{stderr}\n--- bindings\n{code}"
);
}
#[test]
fn demo_library() {
if cfg!(target_pointer_width = "32") {
eprintln!("skipping end-to-end test on a 32-bit target");
return;
}
let Some(cc) = compiler("CC", ["cc", "clang", "gcc"]) else {
return skip("no C compiler found");
};
let work = Path::new(env!("CARGO_TARGET_TMPDIR")).join("e2e-demo");
let _ = std::fs::remove_dir_all(&work);
std::fs::create_dir_all(&work).unwrap();
let lib = work.join(dlsym::library_filename("demo"));
compile(&cc, &fixtures().join("demo.c"), &lib);
let generated = match Builder::new()
.header(fixtures().join("demo.h"))
.struct_name("Demo")
.safe("demo_add")
.safe("demo_is_even")
.owned("demo_handle", "demo_handle_free", ["demo_handle_new"])
.safe_handle("demo_handle_get")
.wrap("demo_strlen:str,safe")
.wrap("demo_sum_bytes:slice=data/len,safe")
.wrap("demo_fill:slice=buf/len")
.wrap("demo_parse:str,out=out,ret=nonzero,safe")
.wrap("demo_checked:ret=negative,safe")
.wrap("demo_fail_errno:ret=errno,safe")
.wrap("demo_version:ret=static-str,safe")
.wrap("demo_handle_label:slice=buf/len,ret=negative,safe")
.wrap("demo_greet:str?,safe")
.wrap("demo_handle_open:out=out,ret=nonzero,safe")
.library_name(lib.to_str().unwrap())
.generate()
{
Ok(g) => g,
Err(parse::Error::LibClang(e)) => return skip(&e),
Err(e) => panic!("{e}"),
};
assert!(
generated
.warnings
.iter()
.any(|w| w.contains("demo_long_double")),
"{:?}",
generated.warnings
);
assert!(
generated
.warnings
.iter()
.any(|w| w.contains("`demo_not_implemented` is not exported")),
"{:?}",
generated.warnings
);
let code = generated.code;
for needle in [
"#[repr(C, packed)]\n#[derive(Clone, Copy, Debug)]\npub struct demo_packed",
"#[repr(C, align(16))]",
"#[repr(C, packed(2))]",
"pub union demo_number",
"pub struct demo_handle {\n _private: [u8; 0],",
"pub const DEMO_VERSION: &::core::ffi::CStr = c\"1.2.3\";",
"pub fn demo_sum(count: ::core::ffi::c_int, ...) -> ::core::ffi::c_int;",
"pub safe fn demo_add(",
"pub struct DemoHandle<'lib> {",
"pub fn demo_add_ten(&self, x: ::core::ffi::c_int) -> ::core::ffi::c_int {",
"#[optional]\n pub fn demo_not_implemented()",
] {
assert!(code.contains(needle), "missing {needle:?}");
}
assert!(
!code.contains("demo_long_double"),
"long double is unsupported"
);
assert!(
!code.contains("demo_twice"),
"only macros wrapping a call are bound"
);
assert!(
!code.contains("demo_inline"),
"static inline functions have no symbol"
);
assert!(!code.contains("DEMO_EMPTY") && !code.contains("DEMO_FUNC_LIKE"));
run("demo", &work, &code, &fixtures().join("main.rs"), &lib);
}
#[test]
fn cpp_library() {
if cfg!(target_pointer_width = "32") {
eprintln!("skipping end-to-end test on a 32-bit target");
return;
}
let Some(cxx) = compiler("CXX", ["c++", "clang++", "g++"]) else {
return skip("no C++ compiler found");
};
let fixtures = fixtures().with_file_name("cpp");
let work = Path::new(env!("CARGO_TARGET_TMPDIR")).join("e2e-cpp");
let _ = std::fs::remove_dir_all(&work);
std::fs::create_dir_all(&work).unwrap();
let lib = work.join(dlsym::library_filename("cpp"));
compile(&cxx, &fixtures.join("cpp.cpp"), &lib);
let generated = match Builder::new()
.header(fixtures.join("cpp.hpp"))
.struct_name("Cpp")
.safe_handle("geo_Counter_get")
.library_name(lib.to_str().unwrap())
.generate()
{
Ok(g) => g,
Err(parse::Error::LibClang(e)) => return skip(&e),
Err(e) => panic!("{e}"),
};
let code = generated.code;
let warnings = generated.warnings.join("\n");
assert!(
warnings.contains("non-trivial C++ type"),
"make_name returns a non-trivial type by value:\n{warnings}"
);
assert!(
!warnings.contains("not exported"),
"every mangled name resolves:\n{warnings}"
);
for needle in [
"unsafe extern \"C-unwind\"",
"pub fn geo_checked_div(",
"pub fn geo_name_len(n: *const geo_Name)",
"pub fn cpp_plain(",
"pub const geo_shape_square: geo_shape",
"pub struct geo_Registry {\n v: ::core::ffi::c_int,",
"#[derive(Debug)]\npub struct geo_Name {\n _opaque: [u8; 8],",
"pub struct GeoCounter<'lib> {",
"pub fn get(&self) -> ::core::ffi::c_int {",
"pub unsafe fn add_int(&mut self, n: ::core::ffi::c_int)",
"pub fn geo_Registry_value(this: *const geo_Registry)",
] {
assert!(code.contains(needle), "missing {needle:?} in\n{code}");
}
for absent in ["secret", "make_name", "fn geo_Registry_Registry"] {
assert!(
!code.contains(absent),
"{absent:?} must not be bound:\n{code}"
);
}
assert!(
!code.contains("#[link_name = \"cpp_plain\"]"),
"extern \"C\" functions keep their plain names"
);
run("cpp", &work, &code, &fixtures.join("main.rs"), &lib);
}