diff --git a/.gitignore b/.gitignore index 3dfe7323e..337af2d92 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ /expand.rs /target/ /Cargo.lock +/tests/shared_library/library/target/ +/tests/shared_library/library/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index de521ffc1..74f72ba70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ cxx-build = { version = "=1.0.194", path = "gen/build" } cxxbridge-cmd = { version = "=1.0.194", path = "gen/cmd" } [workspace] -members = ["demo", "flags", "gen/build", "gen/cmd", "gen/lib", "macro", "tests/ffi"] +members = ["demo", "flags", "gen/build", "gen/cmd", "gen/lib", "macro", "tests/ffi", "tests/shared_library"] [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/build.rs b/build.rs index e7872d52a..c565ccde5 100644 --- a/build.rs +++ b/build.rs @@ -10,6 +10,7 @@ fn main() { cc::Build::new() .file(manifest_dir.join("src/cxx.cc")) + .include(manifest_dir.join("include")) .cpp(true) .cpp_link_stdlib(None) // linked via link-cplusplus crate .std(cxxbridge_flags::STD) diff --git a/gen/lib/src/lib.rs b/gen/lib/src/lib.rs index 0092e82c3..58dd9e27c 100644 --- a/gen/lib/src/lib.rs +++ b/gen/lib/src/lib.rs @@ -47,7 +47,7 @@ mod gen; mod syntax; pub use crate::error::Error; -pub use crate::gen::include::{Include, HEADER}; +pub use crate::gen::include::{Include, HEADER, IMPLEMENTATION}; pub use crate::gen::{CfgEvaluator, CfgResult, GeneratedCode, Opt}; pub use crate::syntax::IncludeKind; use proc_macro2::TokenStream; @@ -60,3 +60,92 @@ pub fn generate_header_and_cc(rust_source: TokenStream, opt: &Opt) -> Result String { + match target_os { + "windows" => { + let mut result = String::from("EXPORTS\n"); + for sym in symbols { + result.push_str(" "); + result.push_str(sym); + result.push('\n'); + } + result + } + "macos" => { + let mut result = String::new(); + for sym in symbols { + result.push_str("-U _"); + result.push_str(sym); + result.push('\n'); + } + result + } + _ => { + // Linux and other Unix-like systems + let mut result = String::from("{\n"); + for sym in symbols { + result.push_str(" "); + result.push_str(sym); + result.push_str(";\n"); + } + result.push_str("};"); + result + } + } +} + +/// Format symbols that a shared library imports from an executable into the appropriate linker file format. +/// +/// When a shared library calls functions defined in the executable that loads it, those symbols +/// must be declared as available to the linker. This function generates the platform-specific +/// files needed: +/// - **Windows**: `.def` file format (EXPORTS section) - used with `/DEF:` linker flag +/// - **macOS**: Linker arguments (`-U _symbol` for each) - marks symbols as dynamic_lookup +/// - **Linux**: Dynamic list format (`{ symbol; }`) - used with `--dynamic-list` +/// +/// # Arguments +/// * `symbols` - The list of symbol names the library imports from the executable +/// * `target_os` - The target operating system ("windows", "macos", or "linux") +/// +/// # Example +/// ``` +/// let import_symbols = vec!["exe_callback".to_string(), "exe_get_constant".to_string()]; +/// let content = cxx_gen::format_import_symbols_for_linker(&import_symbols, "linux"); +/// // content will be "{\n exe_callback;\n exe_get_constant;\n};" +/// ``` +pub fn format_import_symbols_for_linker(symbols: &[String], target_os: &str) -> String { + format_symbols_for_linker(symbols, target_os) +} + +/// Format symbols that a shared library exports into a Windows `.def` file format. +/// +/// On Windows, shared libraries (DLLs) use `.def` files to explicitly list exported symbols. +/// This function generates the EXPORTS section needed for the library's `.def` file. +/// +/// Note: This is Windows-specific. On Unix systems, exports are typically controlled via +/// version scripts (Linux) or visibility attributes, not separate export files. +/// +/// # Arguments +/// * `symbols` - The list of symbol names the library exports +/// * `target_os` - Must be "windows" +/// +/// # Panics +/// Panics if `target_os` is not "windows" +/// +/// # Example +/// ``` +/// let export_symbols = vec!["lib_process".to_string(), "lib_get_data".to_string()]; +/// let content = cxx_gen::format_export_symbols_for_linker(&export_symbols, "windows"); +/// // content will be "EXPORTS\n lib_process\n lib_get_data\n" +/// ``` +pub fn format_export_symbols_for_linker(symbols: &[String], target_os: &str) -> String { + if target_os != "windows" { + panic!( + "format_export_symbols_for_linker is only supported for Windows targets, got: {}", + target_os + ); + } + format_symbols_for_linker(symbols, target_os) +} diff --git a/gen/src/include.rs b/gen/src/include.rs index 71bb201dc..9cd72f3bf 100644 --- a/gen/src/include.rs +++ b/gen/src/include.rs @@ -5,6 +5,10 @@ use std::ops::{Deref, DerefMut}; /// The complete contents of the "rust/cxx.h" header. pub static HEADER: &str = include_str!("include/cxx.h"); +/// The complete contents of the "rust/cxx.cc" implementation. +#[allow(dead_code)] +pub static IMPLEMENTATION: &str = include_str!("src/cxx.cc"); + /// A header to #include. /// /// The cxxbridge tool does not parse or even require the given paths to exist; diff --git a/gen/src/mod.rs b/gen/src/mod.rs index 312bd630a..701e80528 100644 --- a/gen/src/mod.rs +++ b/gen/src/mod.rs @@ -93,6 +93,23 @@ pub struct GeneratedCode { pub header: Vec, /// The bytes of a C++ implementation file (e.g. .cc, cpp etc.) pub implementation: Vec, + /// Export and import data (unused in cxx_build). + #[allow(dead_code)] + ctx: GenContext, +} + +/// Export and import data used by cxx_gen but not cxx_build. +#[derive(Default)] +pub struct GenContext { + /// Symbols exported by the library. Access via `export_symbols()`. + exports: Vec, + /// Import information for symbols the library needs from the executable. + /// Access symbols via `import_symbols()` or generate thunks via `generate_import_thunks()`. + imports: Vec, + /// Prefix to prepend to generated thunks (includes, pragmas, builtins). + thunk_prefix: String, + /// Postfix to append to generated thunks (pragma end). + thunk_postfix: String, } impl Default for Opt { @@ -188,14 +205,81 @@ pub(super) fn generate(syntax: File, opt: &Opt) -> Result { // same token stream to avoid parsing twice. Others only need to generate // one or the other. let (mut header, mut implementation) = Default::default(); + let mut ctx = GenContext::default(); if opt.gen_header { - header = write::gen(apis, types, opt, true); + let mut out_file = write::gen(apis, types, opt, true); + header = out_file.content(); } if opt.gen_implementation { - implementation = write::gen(apis, types, opt, false); + let mut out_file = write::gen(apis, types, opt, false); + implementation = out_file.content(); + ctx.exports = out_file.exports(); + ctx.imports = out_file.imports(); + ctx.thunk_prefix = out_file.thunk_prefix(); + ctx.thunk_postfix = out_file.thunk_postfix(); } Ok(GeneratedCode { header, implementation, + ctx, }) } + +impl GeneratedCode { + /// Get the list of symbols exported by the library. + /// + /// These are the functions and types that the library provides to other code. + #[allow(dead_code)] + pub fn export_symbols(&self) -> Vec { + self.ctx.exports.clone() + } + + /// Get the list of symbols imported by the library from the executable. + /// + /// These are the functions that the library calls which are defined in the + /// executable that loads it. + #[allow(dead_code)] + pub fn import_symbols(&self) -> Vec { + self.ctx.imports.iter().map(|import| import.symbol.clone()).collect() + } + + /// Generate Windows-specific import thunks for runtime symbol resolution. + /// + /// This is only needed when building shared libraries on Windows where the library + /// imports functions from the executable. The thunks use GetProcAddress to resolve + /// symbols at runtime. + /// + /// Returns an empty string if there are no imports or if the target OS is not Windows. + #[allow(dead_code)] + pub fn generate_import_thunks(&self, target_os: &str) -> String { + if target_os != "windows" || self.ctx.imports.is_empty() { + return String::new(); + } + + let mut out = String::new(); + out.push_str(&self.ctx.thunk_prefix); + if !self.ctx.thunk_prefix.is_empty() { + out.push('\n'); + } + out.push_str("#include \n"); + out.push_str("#include \n"); + out.push_str("#include \n"); + + for import in &self.ctx.imports { + let out::ImportInfo { symbol, return_type, signature_args, noexcept, call_args } = import; + out.push_str(&format!( + r#"extern "C" {return_type}{symbol}({signature_args}){noexcept} {{ + static auto fn = reinterpret_cast<{return_type}(*)({signature_args})>( + reinterpret_cast(GetProcAddress(GetModuleHandle(NULL), "{symbol}"))); + if (fn) return fn({call_args}); + fprintf(stderr, "FATAL: Host EXE missing required export: {symbol}\n"); + std::terminate(); +}} +"#, + )); + } + + out.push_str(&self.ctx.thunk_postfix); + out + } +} diff --git a/gen/src/out.rs b/gen/src/out.rs index 007bff3df..561febb88 100644 --- a/gen/src/out.rs +++ b/gen/src/out.rs @@ -16,6 +16,7 @@ pub(crate) struct OutFile<'a> { pub pragma: Pragma<'a>, pub builtin: Builtins<'a>, content: RefCell>, + write_override: RefCell>, } #[derive(Default)] @@ -26,6 +27,8 @@ pub(crate) struct Content<'a> { suppress_next_section: bool, section_pending: bool, blocks_pending: usize, + imports: Vec, + exports: Vec, } #[derive(Copy, Clone, PartialEq, Debug)] @@ -34,6 +37,16 @@ enum BlockBoundary<'a> { End(Block<'a>), } +#[derive(Clone)] +#[allow(dead_code)] +pub(crate) struct ImportInfo { + pub symbol: String, + pub return_type: String, + pub signature_args: String, + pub noexcept: String, + pub call_args: String, +} + impl<'a> OutFile<'a> { pub(crate) fn new(header: bool, opt: &'a Opt, types: &'a Types) -> Self { OutFile { @@ -44,6 +57,7 @@ impl<'a> OutFile<'a> { pragma: Pragma::new(), builtin: Builtins::new(), content: RefCell::new(Content::new()), + write_override: RefCell::new(None), } } @@ -108,6 +122,68 @@ impl<'a> OutFile<'a> { self.content.get_mut().flush(); self.pragma.end.flush(); } + + pub(crate) fn add_export(&mut self, export: String) { + if self.header { return; } + + self.content.get_mut().exports.push(export); + } + + pub(crate) fn exports(&mut self) -> Vec { + std::mem::take(&mut self.content.get_mut().exports) + } + + pub(crate) fn add_import(&mut self, symbol: String, return_type: &str, signature_args: &str, noexcept: &str, call_args: &str) { + if self.header { return; } + + self.content.get_mut().imports.push(ImportInfo { + symbol, + return_type: return_type.to_string(), + signature_args: signature_args.to_string(), + noexcept: noexcept.to_string(), + call_args: call_args.to_string(), + }); + } + + pub(crate) fn imports(&mut self) -> Vec { + std::mem::take(&mut self.content.get_mut().imports) + } + + pub(crate) fn thunk_prefix(&mut self) -> String { + self.flush(); + + let include = &self.include.content.bytes; + let pragma_begin = &self.pragma.begin.bytes; + let builtin = &self.builtin.content.bytes; + + let mut out = String::new(); + out.push_str(include); + if !out.is_empty() && !pragma_begin.is_empty() { + out.push('\n'); + } + out.push_str(pragma_begin); + if !out.is_empty() && !builtin.is_empty() { + out.push('\n'); + } + out.push_str(builtin); + out + } + + pub(crate) fn thunk_postfix(&mut self) -> String { + self.flush(); + self.pragma.end.bytes.clone() + } + + /// Temporarily redirect writes to a buffer, returning the result and captured output + pub(crate) fn with_buffer(&mut self, f: F) -> (R, String) + where + F: FnOnce(&mut Self) -> R, + { + *self.write_override.borrow_mut() = Some(String::new()); + let result = f(self); + let buffer = self.write_override.borrow_mut().take().unwrap(); + (result, buffer) + } } impl<'a> Write for Content<'a> { @@ -169,7 +245,6 @@ impl<'a> Content<'a> { self.bytes.push_str(b); self.suppress_next_section = false; self.section_pending = false; - self.blocks_pending = 0; } } @@ -218,6 +293,7 @@ impl<'a> Content<'a> { } self.blocks.truncate(write); + self.blocks_pending = 0; } } @@ -248,6 +324,10 @@ impl<'a> InfallibleWrite for Content<'a> { impl<'a> InfallibleWrite for OutFile<'a> { fn write_fmt(&mut self, args: Arguments) { - InfallibleWrite::write_fmt(self.content.get_mut(), args); + if let Some(buf) = self.write_override.borrow_mut().as_mut() { + Write::write_fmt(buf, args).unwrap(); + } else { + InfallibleWrite::write_fmt(self.content.get_mut(), args); + } } } diff --git a/gen/src/src b/gen/src/src new file mode 120000 index 000000000..929cb3dc9 --- /dev/null +++ b/gen/src/src @@ -0,0 +1 @@ +../../src \ No newline at end of file diff --git a/gen/src/write.rs b/gen/src/write.rs index 283258fa4..d32af5d40 100644 --- a/gen/src/write.rs +++ b/gen/src/write.rs @@ -17,7 +17,7 @@ use crate::syntax::{ Trait, Type, TypeAlias, Types, Var, }; -pub(super) fn gen(apis: &[Api], types: &Types, opt: &Opt, header: bool) -> Vec { +pub(super) fn gen<'a>(apis: &'a [Api], types: &'a Types<'a>, opt: &'a Opt, header: bool) -> OutFile<'a> { let mut out_file = OutFile::new(header, opt, types); let out = &mut out_file; @@ -34,7 +34,7 @@ pub(super) fn gen(apis: &[Api], types: &Types, opt: &Opt, header: bool) -> Vec (String, String, String) { + let (_, return_type) = out.with_buffer(|out| { + if efn.throws { + out.builtin.ptr_len = true; + write!(out, "::rust::repr::PtrLen "); + } else { + write_extern_return_type_space(out, efn, efn.lang); + } + }); + + let (_, signature_args) = out.with_buffer(|out| { + if let FnKind::Method(receiver) = &efn.kind { + write!( + out, + "{}", + out.types.resolve(&receiver.ty).name.to_fully_qualified(), + ); + if !receiver.mutable { + write!(out, " const"); + } + write!(out, " &self"); + } + for (i, arg) in efn.args.iter().enumerate() { + if i > 0 || matches!(efn.kind, FnKind::Method(_)) { + write!(out, ", "); + } + if arg.ty == RustString { + write_type_space(out, &arg.ty); + write!(out, "const *{}", arg.name.cxx); + } else if let Type::RustVec(_) = arg.ty { + write_type_space(out, &arg.ty); + write!(out, "const *{}", arg.name.cxx); + } else { + write_extern_arg(out, arg); + } + } + let indirect_return = indirect_return(efn, out.types, efn.lang); + if indirect_return { + if !efn.args.is_empty() || matches!(efn.kind, FnKind::Method(_)) { + write!(out, ", "); + } + write_indirect_return_type_space(out, efn.ret.as_ref().unwrap()); + write!(out, "*return$"); + } + }); + + let noexcept = match efn.lang { + Lang::Cxx => " noexcept", + Lang::CxxUnwind => "", + Lang::Rust => unreachable!(), + }; + + (return_type, signature_args, noexcept.to_string()) +} + +fn get_extern_function_call_args(out: &OutFile, efn: &ExternFn) -> String { + let mut args = Vec::new(); + if matches!(efn.kind, FnKind::Method(_)) { + args.push("self".to_string()); + } + for arg in &efn.args { + args.push(arg.name.cxx.to_string()); + } + if indirect_return(efn, out.types, efn.lang) { + args.push("return$".to_string()); + } + args.join(", ") +} + fn write_cxx_function_shim<'a>(out: &mut OutFile<'a>, efn: &'a ExternFn) { out.pragma.dollar_in_identifier = true; out.pragma.missing_declarations = true; @@ -851,54 +920,10 @@ fn write_cxx_function_shim<'a>(out: &mut OutFile<'a>, efn: &'a ExternFn) { out.set_namespace(&efn.name.namespace); out.begin_block(Block::ExternC); begin_function_definition(out); - if efn.throws { - out.builtin.ptr_len = true; - write!(out, "::rust::repr::PtrLen "); - } else { - write_extern_return_type_space(out, efn, efn.lang); - } let mangled = mangle::extern_fn(efn, out.types); - write!(out, "{}(", mangled); - if let FnKind::Method(receiver) = &efn.kind { - write!( - out, - "{}", - out.types.resolve(&receiver.ty).name.to_fully_qualified(), - ); - if !receiver.mutable { - write!(out, " const"); - } - write!(out, " &self"); - } - for (i, arg) in efn.args.iter().enumerate() { - if i > 0 || matches!(efn.kind, FnKind::Method(_)) { - write!(out, ", "); - } - if arg.ty == RustString { - write_type_space(out, &arg.ty); - write!(out, "const *{}", arg.name.cxx); - } else if let Type::RustVec(_) = arg.ty { - write_type_space(out, &arg.ty); - write!(out, "const *{}", arg.name.cxx); - } else { - write_extern_arg(out, arg); - } - } + let (return_type, signature_args, noexcept) = get_extern_function_signature_parts(out, efn); let indirect_return = indirect_return(efn, out.types, efn.lang); - if indirect_return { - if !efn.args.is_empty() || matches!(efn.kind, FnKind::Method(_)) { - write!(out, ", "); - } - write_indirect_return_type_space(out, efn.ret.as_ref().unwrap()); - write!(out, "*return$"); - } - write!(out, ")"); - match efn.lang { - Lang::Cxx => write!(out, " noexcept"), - Lang::CxxUnwind => {} - Lang::Rust => unreachable!(), - } - writeln!(out, " {{"); + writeln!(out, "{return_type}{mangled}({signature_args}){noexcept} {{"); write!(out, " "); write_return_type(out, &efn.ret); match efn.receiver() { @@ -1021,6 +1046,9 @@ fn write_cxx_function_shim<'a>(out: &mut OutFile<'a>, efn: &'a ExternFn) { } } out.end_block(Block::ExternC); + + let call_args = get_extern_function_call_args(out, efn); + out.add_import(mangled.to_string(), &return_type, &signature_args, &noexcept, &call_args); } fn write_function_pointer_trampoline(out: &mut OutFile, efn: &ExternFn, var: &Pair, f: &Signature) { @@ -1052,6 +1080,7 @@ fn write_rust_function_decl<'a>(out: &mut OutFile<'a>, efn: &'a ExternFn) { let indirect_call = false; write_rust_function_decl_impl(out, &link_name, efn, indirect_call); out.end_block(Block::ExternC); + out.add_export(link_name.to_string()); } fn write_rust_function_decl_impl( @@ -1590,21 +1619,15 @@ fn write_rust_box_extern(out: &mut OutFile, key: &NamedImplKey) { out.pragma.dollar_in_identifier = true; - writeln!( - out, - "{} *cxxbridge1$box${}$alloc() noexcept;", - inner, instance, - ); - writeln!( - out, - "void cxxbridge1$box${}$dealloc({} *) noexcept;", - instance, inner, - ); - writeln!( - out, - "void cxxbridge1$box${}$drop(::rust::Box<{}> *ptr) noexcept;", - instance, inner, - ); + let export = format!("cxxbridge1$box${}$alloc", instance); + writeln!(out, "{inner} *{export}() noexcept;"); + out.add_export(export); + let export = format!("cxxbridge1$box${}$dealloc", instance); + writeln!(out, "void {export}({inner} *) noexcept;"); + out.add_export(export); + let export = format!("cxxbridge1$box${}$drop", instance); + writeln!(out, "void {export}(::rust::Box<{inner}> *ptr) noexcept;"); + out.add_export(export); } fn write_rust_vec_extern(out: &mut OutFile, key: &NamedImplKey) { @@ -1614,46 +1637,30 @@ fn write_rust_vec_extern(out: &mut OutFile, key: &NamedImplKey) { out.include.cstddef = true; out.pragma.dollar_in_identifier = true; - writeln!( - out, - "void cxxbridge1$rust_vec${}$new(::rust::Vec<{}> const *ptr) noexcept;", - instance, inner, - ); - writeln!( - out, - "void cxxbridge1$rust_vec${}$drop(::rust::Vec<{}> *ptr) noexcept;", - instance, inner, - ); - writeln!( - out, - "::std::size_t cxxbridge1$rust_vec${}$len(::rust::Vec<{}> const *ptr) noexcept;", - instance, inner, - ); - writeln!( - out, - "::std::size_t cxxbridge1$rust_vec${}$capacity(::rust::Vec<{}> const *ptr) noexcept;", - instance, inner, - ); - writeln!( - out, - "{} const *cxxbridge1$rust_vec${}$data(::rust::Vec<{0}> const *ptr) noexcept;", - inner, instance, - ); - writeln!( - out, - "void cxxbridge1$rust_vec${}$reserve_total(::rust::Vec<{}> *ptr, ::std::size_t new_cap) noexcept;", - instance, inner, - ); - writeln!( - out, - "void cxxbridge1$rust_vec${}$set_len(::rust::Vec<{}> *ptr, ::std::size_t len) noexcept;", - instance, inner, - ); - writeln!( - out, - "void cxxbridge1$rust_vec${}$truncate(::rust::Vec<{}> *ptr, ::std::size_t len) noexcept;", - instance, inner, - ); + let export = format!("cxxbridge1$rust_vec${}$new", instance); + writeln!(out, "void {export}(::rust::Vec<{inner}> const *ptr) noexcept;"); + out.add_export(export); + let export = format!("cxxbridge1$rust_vec${}$drop", instance); + writeln!(out, "void {export}(::rust::Vec<{inner}> *ptr) noexcept;"); + out.add_export(export); + let export = format!("cxxbridge1$rust_vec${}$len", instance); + writeln!(out, "::std::size_t {export}(::rust::Vec<{inner}> const *ptr) noexcept;"); + out.add_export(export); + let export = format!("cxxbridge1$rust_vec${}$capacity", instance); + writeln!(out, "::std::size_t {export}(::rust::Vec<{inner}> const *ptr) noexcept;"); + out.add_export(export); + let export = format!("cxxbridge1$rust_vec${}$data", instance); + writeln!(out, "{inner} const *{export}(::rust::Vec<{inner}> const *ptr) noexcept;"); + out.add_export(export); + let export = format!("cxxbridge1$rust_vec${}$reserve_total", instance); + writeln!(out, "void {export}(::rust::Vec<{inner}> *ptr, ::std::size_t new_cap) noexcept;"); + out.add_export(export); + let export = format!("cxxbridge1$rust_vec${}$set_len", instance); + writeln!(out, "void {export}(::rust::Vec<{inner}> *ptr, ::std::size_t len) noexcept;"); + out.add_export(export); + let export = format!("cxxbridge1$rust_vec${}$truncate", instance); + writeln!(out, "void {export}(::rust::Vec<{inner}> *ptr, ::std::size_t len) noexcept;"); + out.add_export(export); } fn write_rust_box_impl(out: &mut OutFile, key: &NamedImplKey) { @@ -1815,74 +1822,69 @@ fn write_unique_ptr_common(out: &mut OutFile, ty: &Type) { inner, ); + let symbol = format!("cxxbridge1$unique_ptr${}$null", instance); begin_function_definition(out); - writeln!( - out, - "void cxxbridge1$unique_ptr${}$null(::std::unique_ptr<{}> *ptr) noexcept {{", - instance, inner, - ); - writeln!(out, " ::new (ptr) ::std::unique_ptr<{}>();", inner); + writeln!(out, "void {symbol}(::std::unique_ptr<{inner}> *ptr) noexcept {{"); + writeln!(out, " ::new (ptr) ::std::unique_ptr<{inner}>();"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "void ", &format!("::std::unique_ptr<{inner}> *ptr"), " noexcept", "ptr"); if can_construct_from_value { out.builtin.maybe_uninit = true; out.pragma.mismatched_new_delete = true; + let symbol = format!("cxxbridge1$unique_ptr${}$uninit", instance); begin_function_definition(out); + writeln!(out, "{inner} *{symbol}(::std::unique_ptr<{inner}> *ptr) noexcept {{"); writeln!( out, - "{} *cxxbridge1$unique_ptr${}$uninit(::std::unique_ptr<{}> *ptr) noexcept {{", - inner, instance, inner, - ); - writeln!( - out, - " {} *uninit = reinterpret_cast<{} *>(new ::rust::MaybeUninit<{}>);", - inner, inner, inner, + " {inner} *uninit = reinterpret_cast<{inner} *>(new ::rust::MaybeUninit<{inner}>);", ); - writeln!(out, " ::new (ptr) ::std::unique_ptr<{}>(uninit);", inner); + writeln!(out, " ::new (ptr) ::std::unique_ptr<{inner}>(uninit);"); writeln!(out, " return uninit;"); writeln!(out, "}}"); + out.add_import(symbol.clone(), &format!("{inner} *"), &format!("::std::unique_ptr<{inner}> *ptr"), " noexcept", "ptr"); } + let symbol = format!("cxxbridge1$unique_ptr${}$raw", instance); begin_function_definition(out); writeln!( out, - "void cxxbridge1$unique_ptr${}$raw(::std::unique_ptr<{}> *ptr, ::std::unique_ptr<{}>::pointer raw) noexcept {{", - instance, inner, inner, + "void {symbol}(::std::unique_ptr<{inner}> *ptr, ::std::unique_ptr<{inner}>::pointer raw) noexcept {{", ); - writeln!(out, " ::new (ptr) ::std::unique_ptr<{}>(raw);", inner); + writeln!(out, " ::new (ptr) ::std::unique_ptr<{inner}>(raw);"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "void ", &format!("::std::unique_ptr<{inner}> *ptr, ::std::unique_ptr<{inner}>::pointer raw"), " noexcept", "ptr, raw"); + let symbol = format!("cxxbridge1$unique_ptr${}$get", instance); begin_function_definition(out); writeln!( out, - "::std::unique_ptr<{}>::element_type const *cxxbridge1$unique_ptr${}$get(::std::unique_ptr<{}> const &ptr) noexcept {{", - inner, instance, inner, + "::std::unique_ptr<{inner}>::element_type const *{symbol}(::std::unique_ptr<{inner}> const &ptr) noexcept {{", ); writeln!(out, " return ptr.get();"); writeln!(out, "}}"); + out.add_import(symbol.clone(), &format!("::std::unique_ptr<{inner}>::element_type const *"), &format!("::std::unique_ptr<{inner}> const &ptr"), " noexcept", "ptr"); + let symbol = format!("cxxbridge1$unique_ptr${}$release", instance); begin_function_definition(out); writeln!( out, - "::std::unique_ptr<{}>::pointer cxxbridge1$unique_ptr${}$release(::std::unique_ptr<{}> &ptr) noexcept {{", - inner, instance, inner, + "::std::unique_ptr<{inner}>::pointer {symbol}(::std::unique_ptr<{inner}> &ptr) noexcept {{", ); writeln!(out, " return ptr.release();"); writeln!(out, "}}"); + out.add_import(symbol.clone(), &format!("::std::unique_ptr<{inner}>::pointer "), &format!("::std::unique_ptr<{inner}> &ptr"), " noexcept", "ptr"); + let symbol = format!("cxxbridge1$unique_ptr${}$drop", instance); begin_function_definition(out); - writeln!( - out, - "void cxxbridge1$unique_ptr${}$drop(::std::unique_ptr<{}> *ptr) noexcept {{", - instance, inner, - ); + writeln!(out, "void {symbol}(::std::unique_ptr<{inner}> *ptr) noexcept {{"); out.builtin.deleter_if = true; writeln!( out, - " ::rust::deleter_if<::rust::detail::is_complete<{}>::value>{{}}(ptr);", - inner, + " ::rust::deleter_if<::rust::detail::is_complete<{inner}>::value>{{}}(ptr);", ); writeln!(out, "}}"); + out.add_import(symbol.clone(), "void ", &format!("::std::unique_ptr<{inner}> *ptr"), " noexcept", "ptr"); } fn write_shared_ptr(out: &mut OutFile, key: &NamedImplKey) { @@ -1911,75 +1913,70 @@ fn write_shared_ptr(out: &mut OutFile, key: &NamedImplKey) { inner, ); + let symbol = format!("cxxbridge1$shared_ptr${}$null", instance); begin_function_definition(out); - writeln!( - out, - "void cxxbridge1$shared_ptr${}$null(::std::shared_ptr<{}> *ptr) noexcept {{", - instance, inner, - ); - writeln!(out, " ::new (ptr) ::std::shared_ptr<{}>();", inner); + writeln!(out, "void {symbol}(::std::shared_ptr<{inner}> *ptr) noexcept {{"); + writeln!(out, " ::new (ptr) ::std::shared_ptr<{inner}>();"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "void ", &format!("::std::shared_ptr<{inner}> *ptr"), " noexcept", "ptr"); if can_construct_from_value { out.builtin.maybe_uninit = true; out.pragma.mismatched_new_delete = true; + let symbol = format!("cxxbridge1$shared_ptr${}$uninit", instance); begin_function_definition(out); + writeln!(out, "{inner} *{symbol}(::std::shared_ptr<{inner}> *ptr) noexcept {{"); writeln!( out, - "{} *cxxbridge1$shared_ptr${}$uninit(::std::shared_ptr<{}> *ptr) noexcept {{", - inner, instance, inner, + " {inner} *uninit = reinterpret_cast<{inner} *>(new ::rust::MaybeUninit<{inner}>);", ); - writeln!( - out, - " {} *uninit = reinterpret_cast<{} *>(new ::rust::MaybeUninit<{}>);", - inner, inner, inner, - ); - writeln!(out, " ::new (ptr) ::std::shared_ptr<{}>(uninit);", inner); + writeln!(out, " ::new (ptr) ::std::shared_ptr<{inner}>(uninit);"); writeln!(out, " return uninit;"); writeln!(out, "}}"); + out.add_import(symbol.clone(), &format!("{inner} *"), &format!("::std::shared_ptr<{inner}> *ptr"), " noexcept", "ptr"); } out.builtin.shared_ptr = true; + let symbol = format!("cxxbridge1$shared_ptr${}$raw", instance); begin_function_definition(out); writeln!( out, - "bool cxxbridge1$shared_ptr${}$raw(::std::shared_ptr<{}> *ptr, ::std::shared_ptr<{}>::element_type *raw) noexcept {{", - instance, inner, inner, + "bool {symbol}(::std::shared_ptr<{inner}> *ptr, ::std::shared_ptr<{inner}>::element_type *raw) noexcept {{", ); writeln!( out, - " ::new (ptr) ::rust::shared_ptr_if_destructible<{}>(raw);", - inner, + " ::new (ptr) ::rust::shared_ptr_if_destructible<{inner}>(raw);", ); - writeln!(out, " return ::rust::is_destructible<{}>::value;", inner); + writeln!(out, " return ::rust::is_destructible<{inner}>::value;"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "bool ", &format!("::std::shared_ptr<{inner}> *ptr, ::std::shared_ptr<{inner}>::element_type *raw"), " noexcept", "ptr, raw"); + let symbol = format!("cxxbridge1$shared_ptr${}$clone", instance); begin_function_definition(out); writeln!( out, - "void cxxbridge1$shared_ptr${}$clone(::std::shared_ptr<{}> const &self, ::std::shared_ptr<{}> *ptr) noexcept {{", - instance, inner, inner, + "void {symbol}(::std::shared_ptr<{inner}> const &self, ::std::shared_ptr<{inner}> *ptr) noexcept {{", ); - writeln!(out, " ::new (ptr) ::std::shared_ptr<{}>(self);", inner); + writeln!(out, " ::new (ptr) ::std::shared_ptr<{inner}>(self);"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "void ", &format!("::std::shared_ptr<{inner}> const &self, ::std::shared_ptr<{inner}> *ptr"), " noexcept", "self, ptr"); + let symbol = format!("cxxbridge1$shared_ptr${}$get", instance); begin_function_definition(out); writeln!( out, - "::std::shared_ptr<{}>::element_type const *cxxbridge1$shared_ptr${}$get(::std::shared_ptr<{}> const &self) noexcept {{", - inner, instance, inner, + "::std::shared_ptr<{inner}>::element_type const *{symbol}(::std::shared_ptr<{inner}> const &self) noexcept {{", ); writeln!(out, " return self.get();"); writeln!(out, "}}"); + out.add_import(symbol.clone(), &format!("::std::shared_ptr<{inner}>::element_type const *"), &format!("::std::shared_ptr<{inner}> const &self"), " noexcept", "self"); + let symbol = format!("cxxbridge1$shared_ptr${}$drop", instance); begin_function_definition(out); - writeln!( - out, - "void cxxbridge1$shared_ptr${}$drop(::std::shared_ptr<{}> *self) noexcept {{", - instance, inner, - ); + writeln!(out, "void {symbol}(::std::shared_ptr<{inner}> *self) noexcept {{"); writeln!(out, " self->~shared_ptr();"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "void ", &format!("::std::shared_ptr<{inner}> *self"), " noexcept", "self"); } fn write_weak_ptr(out: &mut OutFile, key: &NamedImplKey) { @@ -2002,54 +1999,49 @@ fn write_weak_ptr(out: &mut OutFile, key: &NamedImplKey) { inner, ); + let symbol = format!("cxxbridge1$weak_ptr${}$null", instance); begin_function_definition(out); - writeln!( - out, - "void cxxbridge1$weak_ptr${}$null(::std::weak_ptr<{}> *ptr) noexcept {{", - instance, inner, - ); - writeln!(out, " ::new (ptr) ::std::weak_ptr<{}>();", inner); + writeln!(out, "void {symbol}(::std::weak_ptr<{inner}> *ptr) noexcept {{"); + writeln!(out, " ::new (ptr) ::std::weak_ptr<{inner}>();"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "void ", &format!("::std::weak_ptr<{inner}> *ptr"), " noexcept", "ptr"); + let symbol = format!("cxxbridge1$weak_ptr${}$clone", instance); begin_function_definition(out); writeln!( out, - "void cxxbridge1$weak_ptr${}$clone(::std::weak_ptr<{}> const &self, ::std::weak_ptr<{}> *ptr) noexcept {{", - instance, inner, inner, + "void {symbol}(::std::weak_ptr<{inner}> const &self, ::std::weak_ptr<{inner}> *ptr) noexcept {{", ); - writeln!(out, " ::new (ptr) ::std::weak_ptr<{}>(self);", inner); + writeln!(out, " ::new (ptr) ::std::weak_ptr<{inner}>(self);"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "void ", &format!("::std::weak_ptr<{inner}> const &self, ::std::weak_ptr<{inner}> *ptr"), " noexcept", "self, ptr"); + let symbol = format!("cxxbridge1$weak_ptr${}$downgrade", instance); begin_function_definition(out); writeln!( out, - "void cxxbridge1$weak_ptr${}$downgrade(::std::shared_ptr<{}> const &shared, ::std::weak_ptr<{}> *weak) noexcept {{", - instance, inner, inner, + "void {symbol}(::std::shared_ptr<{inner}> const &shared, ::std::weak_ptr<{inner}> *weak) noexcept {{", ); - writeln!(out, " ::new (weak) ::std::weak_ptr<{}>(shared);", inner); + writeln!(out, " ::new (weak) ::std::weak_ptr<{inner}>(shared);"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "void ", &format!("::std::shared_ptr<{inner}> const &shared, ::std::weak_ptr<{inner}> *weak"), " noexcept", "shared, weak"); + let symbol = format!("cxxbridge1$weak_ptr${}$upgrade", instance); begin_function_definition(out); writeln!( out, - "void cxxbridge1$weak_ptr${}$upgrade(::std::weak_ptr<{}> const &weak, ::std::shared_ptr<{}> *shared) noexcept {{", - instance, inner, inner, - ); - writeln!( - out, - " ::new (shared) ::std::shared_ptr<{}>(weak.lock());", - inner, + "void {symbol}(::std::weak_ptr<{inner}> const &weak, ::std::shared_ptr<{inner}> *shared) noexcept {{", ); + writeln!(out, " ::new (shared) ::std::shared_ptr<{inner}>(weak.lock());"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "void ", &format!("::std::weak_ptr<{inner}> const &weak, ::std::shared_ptr<{inner}> *shared"), " noexcept", "weak, shared"); + let symbol = format!("cxxbridge1$weak_ptr${}$drop", instance); begin_function_definition(out); - writeln!( - out, - "void cxxbridge1$weak_ptr${}$drop(::std::weak_ptr<{}> *self) noexcept {{", - instance, inner, - ); + writeln!(out, "void {symbol}(::std::weak_ptr<{inner}> *self) noexcept {{"); writeln!(out, " self->~weak_ptr();"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "void ", &format!("::std::weak_ptr<{inner}> *self"), " noexcept", "self"); } fn write_cxx_vector(out: &mut OutFile, key: &NamedImplKey) { @@ -2063,75 +2055,57 @@ fn write_cxx_vector(out: &mut OutFile, key: &NamedImplKey) { out.pragma.dollar_in_identifier = true; out.pragma.missing_declarations = true; + let symbol = format!("cxxbridge1$std$vector${}$new", instance); begin_function_definition(out); - writeln!( - out, - "::std::vector<{}> *cxxbridge1$std$vector${}$new() noexcept {{", - inner, instance, - ); - writeln!(out, " return new ::std::vector<{}>();", inner); + writeln!(out, "::std::vector<{inner}> *{symbol}() noexcept {{"); + writeln!(out, " return new ::std::vector<{inner}>();"); writeln!(out, "}}"); + out.add_import(symbol.clone(), &format!("::std::vector<{inner}> *"), "", " noexcept", ""); + let symbol = format!("cxxbridge1$std$vector${}$size", instance); begin_function_definition(out); - writeln!( - out, - "::std::size_t cxxbridge1$std$vector${}$size(::std::vector<{}> const &s) noexcept {{", - instance, inner, - ); + writeln!(out, "::std::size_t {symbol}(::std::vector<{inner}> const &s) noexcept {{"); writeln!(out, " return s.size();"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "::std::size_t ", &format!("::std::vector<{inner}> const &s"), " noexcept", "s"); + let symbol = format!("cxxbridge1$std$vector${}$capacity", instance); begin_function_definition(out); - writeln!( - out, - "::std::size_t cxxbridge1$std$vector${}$capacity(::std::vector<{}> const &s) noexcept {{", - instance, inner, - ); + writeln!(out, "::std::size_t {symbol}(::std::vector<{inner}> const &s) noexcept {{"); writeln!(out, " return s.capacity();"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "::std::size_t ", &format!("::std::vector<{inner}> const &s"), " noexcept", "s"); + let symbol = format!("cxxbridge1$std$vector${}$get_unchecked", instance); begin_function_definition(out); - writeln!( - out, - "{} *cxxbridge1$std$vector${}$get_unchecked(::std::vector<{}> *s, ::std::size_t pos) noexcept {{", - inner, instance, inner, - ); + writeln!(out, "{inner} *{symbol}(::std::vector<{inner}> *s, ::std::size_t pos) noexcept {{"); writeln!(out, " return &(*s)[pos];"); writeln!(out, "}}"); + out.add_import(symbol.clone(), &format!("{inner} *"), &format!("::std::vector<{inner}> *s, ::std::size_t pos"), " noexcept", "s, pos"); + let symbol = format!("cxxbridge1$std$vector${}$reserve", instance); begin_function_definition(out); - writeln!( - out, - "bool cxxbridge1$std$vector${}$reserve(::std::vector<{}> *s, ::std::size_t new_cap) noexcept {{", - instance, inner, - ); - writeln!( - out, - " return ::rust::if_move_constructible<{}>::reserve(*s, new_cap);", - inner, - ); + writeln!(out, "bool {symbol}(::std::vector<{inner}> *s, ::std::size_t new_cap) noexcept {{"); + writeln!(out, " return ::rust::if_move_constructible<{inner}>::reserve(*s, new_cap);"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "bool ", &format!("::std::vector<{inner}> *s, ::std::size_t new_cap"), " noexcept", "s, new_cap"); if out.types.is_maybe_trivial(key.inner) { + let symbol = format!("cxxbridge1$std$vector${}$push_back", instance); begin_function_definition(out); - writeln!( - out, - "void cxxbridge1$std$vector${}$push_back(::std::vector<{}> *v, {} *value) noexcept {{", - instance, inner, inner, - ); + writeln!(out, "void {symbol}(::std::vector<{inner}> *v, {inner} *value) noexcept {{"); writeln!(out, " v->push_back(::std::move(*value));"); writeln!(out, " ::rust::destroy(value);"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "void ", &format!("::std::vector<{inner}> *v, {inner} *value"), " noexcept", "v, value"); + let symbol = format!("cxxbridge1$std$vector${}$pop_back", instance); begin_function_definition(out); - writeln!( - out, - "void cxxbridge1$std$vector${}$pop_back(::std::vector<{}> *v, {} *out) noexcept {{", - instance, inner, inner, - ); - writeln!(out, " ::new (out) {}(::std::move(v->back()));", inner); + writeln!(out, "void {symbol}(::std::vector<{inner}> *v, {inner} *out) noexcept {{"); + writeln!(out, " ::new (out) {inner}(::std::move(v->back()));"); writeln!(out, " v->pop_back();"); writeln!(out, "}}"); + out.add_import(symbol.clone(), "void ", &format!("::std::vector<{inner}> *v, {inner} *out"), " noexcept", "v, out"); } out.include.memory = true; diff --git a/src/cxx.cc b/src/cxx.cc index 6ce5d0bb3..2c35806df 100644 --- a/src/cxx.cc +++ b/src/cxx.cc @@ -1,4 +1,4 @@ -#include "../include/cxx.h" +#include "cxx.h" #include #include #include diff --git a/tests/shared_library/Cargo.toml b/tests/shared_library/Cargo.toml new file mode 100644 index 000000000..f02830b89 --- /dev/null +++ b/tests/shared_library/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cxx-test-shared-library" +version = "0.0.0" +edition = "2021" +publish = false + +[[test]] +name = "test_shared_library" +harness = true + +[dev-dependencies] +cc = "1.0" +cargo_metadata = "0.19" diff --git a/tests/shared_library/README.md b/tests/shared_library/README.md new file mode 100644 index 000000000..4ad96fd77 --- /dev/null +++ b/tests/shared_library/README.md @@ -0,0 +1,88 @@ +# Shared Library Test + +This test validates cxx-gen's ability to generate code for shared library/DLL scenarios with bidirectional function calls. + +## What This Tests + +This test specifically validates the cxx_gen functionality for generating: + +1. **export_symbols** - List of mangled symbols the shared library exports (Rust functions callable from C++) +2. **import_symbols** - List of mangled symbols the EXE must export (C++ functions callable from Rust) +3. **import_thunks** - C++ code compiled into the DLL to dynamically load EXE's exports via GetProcAddress (Windows only) + +### Platform-Specific Behavior + +**Windows:** +- Uses `.def` files to control symbol exports/imports +- Generates thunks that use `GetProcAddress` to dynamically resolve symbols from the executable +- Thunks are compiled into the DLL + +**Linux:** +- Uses a version script to control symbol visibility in the shared library +- Uses a dynamic list to export only the required symbols from the executable (more precise than `--export-dynamic`) +- The shared library resolves imported symbols directly (no thunks needed) +- Uses `rpath` with `$ORIGIN` for library loading + +**macOS:** +- Uses `-undefined dynamic_lookup` to allow undefined symbols +- The executable makes symbols available to the shared library at runtime + +## Structure + +- `library/lib.rs` - Defines the cxx bridge with exported Rust functions and imported C++ functions +- `library/build.rs` - Uses `cxx_gen::generate_header_and_cc()` to generate .def files and thunks +- `tests/main.cc` - Test executable that implements C++ functions and calls library functions +- `tests/exe_functions.{h,cc}` - Implementation of functions exported by the executable +- `tests/test_shared_library.rs` - Integration test that verifies the generated files and builds/runs test exe + +## How It Works + +1. **Library Build** (`library/build.rs`): + - Parses `lib.rs` using cxx_gen to extract bridge declarations + - Generates `library.def` with export_symbols (functions DLL exports) on Windows + - Generates `exe.def` with import_symbols (functions EXE must export) on Windows + - Generates `thunks.cc` with import_thunks (GetProcAddress-based loaders on Windows) + - On Linux, generates a version script to control symbol visibility + - On macOS, generates a response file with `-U` flags for each import symbol + - Compiles everything into `test_library.dll` (Windows) or `libtest_library.so` (Linux/macOS) + +2. **Test Execution** (`tests/test_shared_library.rs`): + - Detects the test's target platform and builds the library with the same target + - Builds the library with `cargo build --manifest-path library/Cargo.toml --target ` + - Verifies .def files contain correct mangled symbols (Windows only) + - Compiles a test executable linking main.cc + generated lib.cc + shared library + - **Windows**: Uses .def files, DLL import library, and MSVC linker + - **Linux**: Uses `--export-dynamic` to export symbols from executable, and `rpath` for library loading + - **macOS**: Uses `@loader_path` rpath and dynamic symbol lookup + - Runs the executable to validate bidirectional calling works + +3. **Test Executable** (`tests/main.cc`): + - Implements `exe_callback()` and `exe_get_constant()` in `tests/exe_functions.cc` + - Calls library's `get_magic_number()`, `multiply_values()`, `library_entry_point()` + - All calls go through cxx bridge (not direct mangled names) + +## Testing + +The tests verify that: +- The library builds successfully on Windows, Linux, and macOS +- Exported functions can be called and return correct values +- Imported functions are called correctly by the library +- The entry point demonstrates the full round-trip +- Platform-specific symbol resolution mechanisms work correctly + +Run with: +```bash +# Test for your current platform +cargo test + +# Test for a specific target +cargo test --target x86_64-unknown-linux-gnu +cargo test --target x86_64-pc-windows-msvc +cargo test --target x86_64-apple-darwin +``` + +## Expected Behavior + +- `get_magic_number()` calls `exe_get_constant()` (returns 1000) and adds 42, returning 1042 +- `multiply_values(3, 4)` computes 3*4=12, then calls `exe_callback(12)` which doubles it to 24 +- `library_entry_point()` calls both `exe_callback(100)` and `exe_get_constant()`, returning 200 + 1000 = 1200 diff --git a/tests/shared_library/build.rs b/tests/shared_library/build.rs new file mode 100644 index 000000000..7b3410eb8 --- /dev/null +++ b/tests/shared_library/build.rs @@ -0,0 +1,9 @@ +// build.rs + +fn main() { + // Read the target being used for the current build + if let Ok(target) = std::env::var("TARGET") { + // make env!("TARGET_TRIPLE") available + println!("cargo:rustc-env=TARGET_TRIPLE={}", target); + } +} diff --git a/tests/shared_library/library/Cargo.toml b/tests/shared_library/library/Cargo.toml new file mode 100644 index 000000000..12053f3bc --- /dev/null +++ b/tests/shared_library/library/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "test-library" +version = "0.0.0" +edition = "2021" +publish = false + +# Exclude from parent workspace - this is a standalone test library +[workspace] + +[lib] +crate-type = ["cdylib"] +path = "lib.rs" + +[dependencies] +cxx = { path = "../../.." } + +[build-dependencies] +cxx-gen = { path = "../../../gen/lib" } +cc = "1.0" +proc-macro2 = "1.0" +rustc_version = "0.4" diff --git a/tests/shared_library/library/build.rs b/tests/shared_library/library/build.rs new file mode 100644 index 000000000..f62ec3209 --- /dev/null +++ b/tests/shared_library/library/build.rs @@ -0,0 +1,140 @@ +use std::env; +use std::fs; +use std::io::Write; +use std::path::PathBuf; +use rustc_version::{version, Version}; + +fn main() { + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let profile = env::var("PROFILE").unwrap(); + let test_artifacts_dir = manifest_dir.join(format!("target/cxxbridge/{}", profile)); + fs::create_dir_all(&test_artifacts_dir).unwrap(); + + // generate the bridge using cxx_gen to get symbols and thunks + let lib_source = fs::read_to_string(manifest_dir.join("lib.rs")).unwrap(); + let lib_tokens: proc_macro2::TokenStream = lib_source.parse().unwrap(); + let generated = cxx_gen::generate_header_and_cc(lib_tokens, &cxx_gen::Opt::default()).unwrap(); + + // write header and implementation + fs::write(out_dir.join("lib.h"), &generated.header).unwrap(); + fs::write(out_dir.join("lib.cc"), &generated.implementation).unwrap(); + + // create EXE symbols file in OS-appropriate format + // Use TARGET env var to get the target OS, not the host OS + let target = env::var("TARGET").unwrap(); + let target_os = TargetOs::from(target.as_str()); + + let exe_symbols_content = cxx_gen::format_import_symbols_for_linker( + &generated.import_symbols(), + target_os.as_str(), + ); + + let exe_symbols_path = match target_os { + TargetOs::Windows => out_dir.join("exe.def"), + TargetOs::Macos => out_dir.join("exe_undefined.txt"), + TargetOs::Linux => out_dir.join("exe.dynamic"), + }; + fs::write(&exe_symbols_path, exe_symbols_content).unwrap(); + + // compile C++ code needed by the library + // On Windows: compile thunks (which call back to the exe via GetProcAddress) + // On Unix: no C++ code needed in the library (generate_import_thunks returns empty string) + let thunks = generated.generate_import_thunks(target_os.as_str()); + if !thunks.is_empty() { + // write thunks that will be compiled into the DLL + let thunks_path = out_dir.join("thunks.cc"); + fs::write(&thunks_path, &thunks).unwrap(); + + let mut build = cc::Build::new(); + build + .cpp(true) + .flag("/EHsc") + .file(&thunks_path) + .include(&out_dir) + .include(manifest_dir.parent().unwrap().join("tests")); // for exe_functions.h + build.compile("cxx-test-shared-library"); + } + + // on Windows, use the .def file for exports + if target_os == TargetOs::Windows { + // create DLL .def file with export symbols (functions the DLL exports) + let dll_def_content = cxx_gen::format_export_symbols_for_linker( + &generated.export_symbols(), + "windows", + ); + let dll_def_path = out_dir.join("library.def"); + fs::write(&dll_def_path, dll_def_content).unwrap(); + + println!("cargo:rustc-cdylib-link-arg=/DEF:{}", dll_def_path.display()); + + // copy for the test to use + fs::copy(&dll_def_path, test_artifacts_dir.join("library.def")).unwrap(); + } else if target_os == TargetOs::Macos { + // Per ld(1) man page: "-U symbol_name: Specified that it is ok for symbol_name to + // have no definition. With -two_levelnamespace, the resulting symbol will be marked + // dynamic_lookup which means dyld will search all loaded images." + // + // The Rust code in the library calls the cxxbridge wrapper functions (import_symbols), + // which are implemented in lib.cc that's compiled into the executable. + println!("cargo:rustc-cdylib-link-arg=-Wl,@{}", exe_symbols_path.display()); + } else { + // on Linux, create a version script to export symbols and allow undefined symbols + let mut version_script = Vec::new(); + writeln!(version_script, "{{").unwrap(); + writeln!(version_script, " global:").unwrap(); + for sym in &generated.export_symbols() { + writeln!(version_script, " {};", sym).unwrap(); + } + writeln!(version_script, " local: *;").unwrap(); + writeln!(version_script, "}};").unwrap(); + let version_script_path = out_dir.join("libtest_library.version"); + fs::write(&version_script_path, version_script).unwrap(); + + let curr = version().unwrap(); + let is_broken_version_script = curr < Version::parse("1.90.0").unwrap(); + if !is_broken_version_script { + println!("cargo:rustc-cdylib-link-arg=-Wl,--version-script={}", version_script_path.display()); + } + println!("cargo:rustc-cdylib-link-arg=-Wl,--allow-shlib-undefined"); + } + + // expose paths for the test to use + println!("cargo:rustc-env=EXE_SYMBOLS_PATH={}", exe_symbols_path.display()); + + // copy generated files to a predictable location for the test to find + fs::copy(&exe_symbols_path, test_artifacts_dir.join(exe_symbols_path.file_name().unwrap())).unwrap(); + fs::copy(out_dir.join("lib.h"), test_artifacts_dir.join("lib.h")).unwrap(); + fs::copy(out_dir.join("lib.cc"), test_artifacts_dir.join("lib.cc")).unwrap(); + + println!("cargo:rerun-if-changed=lib.rs"); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TargetOs { + Windows, + Macos, + Linux, +} + +impl From<&str> for TargetOs { + fn from(target: &str) -> Self { + if target.contains("windows") { + TargetOs::Windows + } else if target.contains("darwin") || target.contains("ios") { + TargetOs::Macos + } else { + TargetOs::Linux + } + } +} + +impl TargetOs { + fn as_str(self) -> &'static str { + match self { + TargetOs::Windows => "windows", + TargetOs::Macos => "macos", + TargetOs::Linux => "linux", + } + } +} diff --git a/tests/shared_library/library/lib.rs b/tests/shared_library/library/lib.rs new file mode 100644 index 000000000..1e3117502 --- /dev/null +++ b/tests/shared_library/library/lib.rs @@ -0,0 +1,34 @@ +#[cxx::bridge] +pub mod ffi { + extern "Rust" { + // functions exported by the cdylib, imported by test exe + fn get_magic_number() -> i32; + fn multiply_values(a: i32, b: i32) -> i32; + fn library_entry_point() -> i32; + } + + unsafe extern "C++" { + include!("exe_functions.h"); + + // functions exported by test exe, imported by cdylib + fn exe_callback(value: i32) -> i32; + fn exe_get_constant() -> i32; + } +} + +pub fn get_magic_number() -> i32 { + // call back to the exe to get a constant, then add our magic + let exe_value = ffi::exe_get_constant(); + exe_value + 42 +} + +pub fn multiply_values(a: i32, b: i32) -> i32 { + // use exe callback to process the result + let product = a * b; + ffi::exe_callback(product) +} + +pub fn library_entry_point() -> i32 { + // test that we can call exe functions from the library + ffi::exe_callback(100) + ffi::exe_get_constant() +} diff --git a/tests/shared_library/tests/exe_functions.cc b/tests/shared_library/tests/exe_functions.cc new file mode 100644 index 000000000..c97748c2b --- /dev/null +++ b/tests/shared_library/tests/exe_functions.cc @@ -0,0 +1,11 @@ +#include "exe_functions.h" + +// Implementation of exe callback functions +int exe_callback(int value) { + // doubles the value + return value * 2; +} + +int exe_get_constant() { + return 1000; +} diff --git a/tests/shared_library/tests/exe_functions.h b/tests/shared_library/tests/exe_functions.h new file mode 100644 index 000000000..e172db9ea --- /dev/null +++ b/tests/shared_library/tests/exe_functions.h @@ -0,0 +1,5 @@ +#pragma once + +// Functions exported by the executable that the library can call back to +int exe_callback(int value); +int exe_get_constant(); diff --git a/tests/shared_library/tests/main.cc b/tests/shared_library/tests/main.cc new file mode 100644 index 000000000..65a359cfe --- /dev/null +++ b/tests/shared_library/tests/main.cc @@ -0,0 +1,31 @@ +#include +#include +#include "lib.h" + +int main() { + std::cout << "Testing shared library interaction..." << std::endl; + + std::int32_t magic = get_magic_number(); + std::cout << "magic number: " << magic << std::endl; + if (magic != 1042) { + std::cerr << "ERROR: expected 1042, got " << magic << std::endl; + return 1; + } + + std::int32_t product = multiply_values(3, 4); + std::cout << "multiply result: " << product << std::endl; + if (product != 24) { + std::cerr << "ERROR: expected 24, got " << product << std::endl; + return 1; + } + + std::int32_t entry_result = library_entry_point(); + std::cout << "entry point result: " << entry_result << std::endl; + if (entry_result != 1200) { + std::cerr << "ERROR: expected 1200, got " << entry_result << std::endl; + return 1; + } + + std::cout << "All tests passed!" << std::endl; + return 0; +} diff --git a/tests/shared_library/tests/test_shared_library.rs b/tests/shared_library/tests/test_shared_library.rs new file mode 100644 index 000000000..a27607f44 --- /dev/null +++ b/tests/shared_library/tests/test_shared_library.rs @@ -0,0 +1,213 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +#[test] +fn test_shared_library_with_exe() { + // this test verifies the cxx_gen functionality for shared libraries: + // 1. export_symbols - mangled symbols the DLL exports + // 2. import_symbols - mangled symbols the EXE must export for the DLL + // 3. thunks - code compiled into DLL to dynamically load EXE's exports + + // build the library first (cargo won't build cdylib automatically for tests) + // match the build profile (debug vs release) to the test's profile + let profile = if cfg!(debug_assertions) { "debug" } else { "release" }; + let mut build_cmd = Command::new("cargo"); + build_cmd.args(&["build", "--manifest-path", "library/Cargo.toml"]); + if !cfg!(debug_assertions) { + build_cmd.arg("--release"); + } + // Build for the same target as this test binary + let target = env!("TARGET_TRIPLE"); + build_cmd.arg("--target").arg(target); + let status = build_cmd.status().expect("failed to execute cargo build"); + assert!(status.success(), "failed to build test-library"); + + // the library's build script copies artifacts to library/target/cxxbridge// + let artifacts_dir = PathBuf::from(format!("library/target/cxxbridge/{}", profile)); + + // verify the exe symbols file was created (platform-specific format) + let exe_symbols_filename = if cfg!(target_os = "windows") { + "exe.def" + } else if cfg!(target_os = "macos") { + "exe_undefined.txt" + } else { + "exe.dynamic" + }; + let exe_symbols_path = artifacts_dir.join(exe_symbols_filename); + + let exe_symbols = fs::read_to_string(&exe_symbols_path) + .unwrap_or_else(|e| panic!("failed to read {}: {}", exe_symbols_filename, e)); + + println!("{}:\n{}", exe_symbols_filename, exe_symbols); + + // verify exe symbols file contains the mangled import symbols + assert!(exe_symbols.contains("exe_callback"), "{} should list exe_callback", exe_symbols_filename); + assert!(exe_symbols.contains("exe_get_constant"), "{} should list exe_get_constant", exe_symbols_filename); + + // On Windows, also verify library.def contains the mangled export symbols + if cfg!(target_os = "windows") { + let dll_def_path = artifacts_dir.join("library.def"); + let dll_def = fs::read_to_string(&dll_def_path).expect("failed to read library.def"); + + println!("library.def:\n{}", dll_def); + + assert!(dll_def.contains("EXPORTS"), "library.def should have EXPORTS section"); + assert!(dll_def.contains("get_magic_number"), "library.def should list get_magic_number"); + assert!(dll_def.contains("multiply_values"), "library.def should list multiply_values"); + } + + // get the library's target directory for linking + let lib_metadata = cargo_metadata::MetadataCommand::new() + .manifest_path("library/Cargo.toml") + .exec() + .unwrap(); + let lib_target_dir = lib_metadata.target_directory.as_std_path(); + + // Determine the actual library output directory + // When using --target explicitly, cargo puts artifacts in target/// + let lib_output_dir = lib_target_dir.join(target).join(profile); + + // build a test executable that uses the exe symbols file to export symbols + build_and_run_test_exe(&artifacts_dir, &lib_output_dir, &exe_symbols_path); +} + +fn build_and_run_test_exe(artifacts_dir: &PathBuf, target_dir: &std::path::Path, exe_symbols_path: &PathBuf) { + let test_dir = artifacts_dir.join("test_exe"); + fs::create_dir_all(&test_dir).expect("failed to create test dir"); + + let target = env!("TARGET_TRIPLE"); + + // compile to object file + let mut cc_build = cc::Build::new(); + + // set required env vars that cc crate needs + if env::var("OUT_DIR").is_err() { + env::set_var("OUT_DIR", &test_dir); + } + if env::var("OPT_LEVEL").is_err() { + env::set_var("OPT_LEVEL", "0"); + } + if env::var("TARGET").is_err() { + env::set_var("TARGET", target); + } + if env::var("HOST").is_err() { + env::set_var("HOST", target); + } + + cc_build.cpp(true) + .file("tests/main.cc") + .file("tests/exe_functions.cc") + .file(artifacts_dir.join("lib.cc")) // include bridge implementation with exe wrappers + .include(&artifacts_dir) + .include("tests"); + + if cfg!(target_os = "windows") { + cc_build.flag("/EHsc"); + } else { + cc_build.flag("-std=c++17"); + } + + let objects = cc_build.compile_intermediates(); + let exe_path = test_dir.join(if cfg!(target_os = "windows") { "test_exe.exe" } else { "test_exe" }); + + // link with exe symbols file and DLL import library + if cfg!(target_os = "windows") { + let tool = cc::windows_registry::find_tool(target, "link.exe") + .expect("failed to find MSVC link.exe"); + + let mut cmd = tool.to_command(); + cmd.arg("/OUT:".to_string() + exe_path.to_str().unwrap()); + cmd.arg("/SUBSYSTEM:CONSOLE"); + cmd.arg("/DEF:".to_string() + exe_symbols_path.to_str().unwrap()); + cmd.arg("/LIBPATH:".to_string() + target_dir.to_str().unwrap()); + for obj in &objects { + cmd.arg(obj); + } + cmd.arg(target_dir.join("test_library.dll.lib")); + + println!("linking exe: {:?}", cmd); + let link_output = cmd.output().expect("failed to link test executable"); + + println!("link stdout: {}", String::from_utf8_lossy(&link_output.stdout)); + if !link_output.stderr.is_empty() { + println!("link stderr: {}", String::from_utf8_lossy(&link_output.stderr)); + } + + assert!(link_output.status.success(), "failed to link test executable"); + + // copy DLL to exe directory + let dll_path = target_dir.join("test_library.dll"); + let exe_dir_dll = test_dir.join("test_library.dll"); + fs::copy(&dll_path, &exe_dir_dll).expect("failed to copy DLL"); + } else { + // on Unix (Linux/macOS), link the executable with the shared library + let lib_name = if cfg!(target_os = "macos") { + "libtest_library.dylib" + } else { + "libtest_library.so" + }; + + let mut cmd = Command::new("c++"); + cmd.arg("-o").arg(&exe_path); + for obj in &objects { + cmd.arg(obj); + } + + // Link with the shared library + cmd.arg(target_dir.join(lib_name)); + + // Set rpath to allow the library to resolve symbols from the exe + if cfg!(target_os = "macos") { + cmd.arg("-Wl,-rpath,@loader_path"); + cmd.arg(format!("-Wl,-rpath,{}", target_dir.display())); + } else { + // Linux: use $ORIGIN and absolute path for rpath + cmd.arg("-Wl,-rpath=$ORIGIN"); + cmd.arg(format!("-Wl,-rpath,{}", target_dir.display())); + + // Use dynamic list to export only the required symbols (better than --export-dynamic) + cmd.arg(format!("-Wl,--dynamic-list={}", exe_symbols_path.display())); + } + + println!("linking exe: {:?}", cmd); + let link_output = cmd.output().expect("failed to link test executable"); + + if !link_output.stdout.is_empty() { + println!("link stdout: {}", String::from_utf8_lossy(&link_output.stdout)); + } + if !link_output.stderr.is_empty() { + println!("link stderr: {}", String::from_utf8_lossy(&link_output.stderr)); + } + + assert!(link_output.status.success(), "failed to link test executable"); + + // Copy the shared library to the test directory for $ORIGIN rpath + let lib_src = target_dir.join(lib_name); + let lib_dest = test_dir.join(lib_name); + fs::copy(&lib_src, &lib_dest).expect("failed to copy shared library"); + } + + println!("test executable created at: {}", exe_path.display()); + + // run the test executable + println!("\nRunning test executable...\n"); + // Convert to absolute path for Command to work properly on all platforms + let exe_abs_path = std::fs::canonicalize(&exe_path) + .unwrap_or_else(|e| panic!("failed to canonicalize exe path {:?}: {}", exe_path, e)); + let output = Command::new(&exe_abs_path) + .current_dir(&test_dir) + .output() + .unwrap_or_else(|e| panic!("failed to run test executable at {:?}: {}", exe_abs_path, e)); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + print!("{}", stdout); + if !stderr.is_empty() { + eprint!("{}", stderr); + } + + assert!(output.status.success(), "test executable failed with output:\n{}\n{}", stdout, stderr); +}