Bindgen having trouble with global variable in DLL?

Scroll to the bottom for most recent progress

I am trying to link the Julia C library to my project using the jlrs and jl-sys crates. Here is a test I wrote in C++ to diagnose the problem I was having:

// Compile with clang -ljulia -LC:/path/to/julia/lib/ test.cpp -IC:/path/to/julia/include/julia/

extern "C" __decltype(dllimport) void* jl_method_type;

int main() {
    auto local = jl_method_type;
    return 0;
}

This code compiles fine. From my research making this, it looks like the __decltype(dllimport) annotation is required, removing it produces a linker error. I then found that the code will also link correctly if the annotation is removed and the variable name is changed to __impl_jl_method_type. This lead to making the following Rust test:

extern "C" {
    pub static mut __imp_jl_method_type: *mut ();
}

fn main() {
    println!("{:?}", unsafe { __imp_jl_method_type });
}

This code fails to compile with a linker error if the __impl_ prefix is removed. The ultimate problem with this is that the code generated by bindgen for the jl-sys crate looks like this:

// ...
extern "C" {
    pub static mut jl_method_type: *mut ();
}
// ...

This causes linker errors identical to my test. I do not think it would be practical to add a build step to manually add the __impl_ prefix to all the variables as the jlrs crate is directly using these variables to provide its functionality. Is there something I could configure in bindgen to resolve this issue?

Edit: It looks like the link_name attribute can be used to change the linked symbol without changing the name used by Rust code. Is there a way to tell bindgen to automatically add code like #[link_name = "__impl_var_name"] to all the variable bindings it generates?

For now I have found a workaround by adding this to the build.rs file which generates the bindings:

let mut code = bindings.to_string();
if (cfg!(target_os = "windows")) {
    const BEFORE_GLOBAL: &'static str = "extern \"C\" {\r\n    pub static mut ";
    let mut prev_index = 0;
    while let Some(index) = code[prev_index..].find(BEFORE_GLOBAL) {
        let index = index + prev_index;
        let name_start = BEFORE_GLOBAL.len();
        let colon = code[index..].find(":").expect("Invalid syntax.");
        let name = &code[index..][name_start..colon];
        let annotation = format!("#[link_name = \"__imp_{}\"]\r\n    ", name);
        code.insert_str(index + "extern \"C\" {\r\n    ".len(), &annotation[..]);
        prev_index = index;
    }
}

It modifies the generated code so that global variables look like this:

extern "C" {
    #[link_name = "__imp_jl_vararg_type"]
    pub static mut jl_vararg_type: *mut jl_unionall_t;
}

I think the underlying problem is that you are linking to an import library and not directly against the DLL. That's why you've got the leading __imp_.

What linker args are you giving to rustc inside your build.rs?

Does explicitly asking to link to julia dynamically work?

#[link(name = "julia", kind="dylib")]
extern "C" {
  ...
}
1 Like

Wow, that did it... I feel bad for how much time I spent trying other things now : P It was my understanding that linking against the import lib was the standard practice, the link you provided even mentions that. Previously the build script was printing some commands to do the linking, using the annotation instead worked great.

Yeah normally I've found that adding a println!("cargo:rustc-link-lib=dynamic=julia") in build.rs is enough to make sure everything links together.

That annotation comes from RFC 1717 - dllimport, which seems to have been implemented largely due to the presence of import libraries on Windows.

https://rust-lang.github.io/rfcs/1717-dllimport.html

It seems to be standard practice in C and C++, but I found rust-lang/rust#27438 where the very first sentence is:

Currently the compiler makes basically no attempt to correctly use dllimport.

After some more investigation, it looks like just adding a line to link Julia dynamically isn't enough, as dynamic is already the default. The annotation specifically needed to be added above every extern "C" { block that bindgen created for all the various global variables exported by the DLL. According to the RFC you linked (and the progress made on it according to the Github issue), this acts as the equivalent of adding __decltype(dllimport) in the C++ code.. It also looks like the import library is still passed to the linker even when using kind="dylib", so I added its location to the link search path. But after that, everything works perfectly!

For any future googlers, I added this code to build.rs to add the annotation to the generated bindings:

    let mut code = bindings.to_string();
    if cfg!(target_os = "windows") {
        code = code.replace(
            "extern \"C\" {",
            "#[link(name = \"julia\", kind = \"dylib\")]\r\nextern \"C\" {",
        );
    }

    // Write the bindings to the $OUT_DIR/bindings.rs file.
    let mut file = std::fs::File::create(&out_path).unwrap();
    use std::io::Write;
    file.write_all(code.as_bytes()).unwrap();
1 Like

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.