Static library not being linked correctly in Rust dylib on macOS arm

Hello everyone, I'm having a compilation linking issue and I don't understand where the problem is. I hope someone can help me take a look.

It might sound a bit strange, but I want to build a Rust dynamic library (dylib, not cdylib) that depends on a static library, specifically raylib on macOS arm. I've specified it like this:

#[link(name = "raylib", kind = "static")]
unsafe extern "C" {
  // functions
}

However, this doesn't seem to work. The otool tool indicates that my dynamic library is not actually depending on the static library, but instead wants to depend on the dynamic version of the library.

dlopen(/Users/kevinstephen/.local/share/ksl/lib/libksl_raylib.dylib, 0x0005): Library not loaded: @rpath/libraylib.550.dylib
  Referenced from: <822DA870-8970-38FA-9772-20A658BA7C43> /Users/kevinstephen/projects/rswk/target/debug/libksl_raylib.dylib
  Reason: no LC_RPATH's found

The outputs of cargo build --verbose and otool -L <lib> are as follows:

/Users/kevinstephen/.local/sdk/rustup/toolchains/nightly-aarch64-apple-darwin/bin/rustc --crate-name ksl_raylib --edition=2024 ksl_raylib/src/lib.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --diagnostic-width=196 --crate-type lib --crate-type dylib --emit=dep-info,link -C embed-bitcode=no -C debuginfo=2 -C split-debuginfo=unpacked --check-cfg 'cfg(docsrs,test)' --check-cfg 'cfg(feature, values())' -C metadata=0022d810958125c8 --out-dir /Users/kevinstephen/projects/rswk/target/debug/deps -C incremental=/Users/kevinstephen/projects/rswk/target/debug/incremental -L dependency=/Users/kevinstephen/projects/rswk/target/debug/deps --extern ksl=/Users/kevinstephen/projects/rswk/target/debug/deps/libksl.rlib --extern ksl=/Users/kevinstephen/projects/rswk/target/debug/deps/libksl.dylib -L /Users/kevinstephen/projects/rswk/ksl_raylib/extern/raylib-5.5_macos/lib -L native=/usr/local/lib -l static=raylib -l framework=OpenGL -l framework=Cocoa -l framework=IOKit -l framework=CoreFoundation -l framework=CoreVideo
../target/debug/libksl_raylib.dylib:
        /Users/kevinstephen/projects/rswk/target/debug/deps/libksl_raylib.dylib (compatibility version 0.0.0, current version 0.0.0)
        @rpath/libraylib.550.dylib (compatibility version 550.0.0, current version 5.5.0)
        /System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL (compatibility version 1.0.0, current version 1.0.0)
        /System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa (compatibility version 1.0.0, current version 24.0.0)
        /System/Library/Frameworks/IOKit.framework/Versions/A/IOKit (compatibility version 1.0.0, current version 275.0.0)
        /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 3423.0.0)
        /System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo (compatibility version 1.2.0, current version 1.5.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)

The build.rs:

fn main() {
    let Ok(pkg_root) = std::env::var("CARGO_MANIFEST_DIR").map(|e| std::path::PathBuf::from(e)) else {
        unreachable!()
    };
    println!(
        "cargo::rustc-link-search={}/extern/raylib-5.5_{}/lib",
        pkg_root.display(),
        if cfg!(target_os = "windows") {
            "win64_mingw-w64"
        } else if cfg!(target_os = "macos") {
            "macos"
        } else {
            "linux_amd64"
        }
    );
    println!("cargo:rustc-link-lib=static=raylib");
    if cfg!(target_os = "windows") {
        println!("cargo:rustc-link-lib=dylib=winmm");
        println!("cargo:rustc-link-lib=dylib=gdi32");
        println!("cargo:rustc-link-lib=dylib=user32");
        println!("cargo:rustc-link-lib=dylib=shell32");
    } else if cfg!(target_os = "macos") {
        println!("cargo:rustc-link-search=native=/usr/local/lib");
        println!("cargo:rustc-link-lib=framework=OpenGL");
        println!("cargo:rustc-link-lib=framework=Cocoa");
        println!("cargo:rustc-link-lib=framework=IOKit");
        println!("cargo:rustc-link-lib=framework=CoreFoundation");
        println!("cargo:rustc-link-lib=framework=CoreVideo");
    } else {
        println!("cargo:rustc-link-search=/usr/local/lib");
        println!("cargo:rustc-link-lib=wayland-client");
        println!("cargo:rustc-link-lib=glfw");
    }
}

Can someone please tell me why this is happening and what the solution is?

The code in question is available in the rswk/ksl_raylib repository.

are you sure the dynamic library is the one you declared? typically #[link(name="raylib")] should direct the linker to link against libraylib, but you have this strange libksl_raylib: as its path is under .local/share/ksl, it looks like a transitive dependency introduced by ksl, whatever this ksl thingy is.

I'm quite certain that when dlopen is called, it's looking for @rpath/libraylib.550.dylib, which is the dynamic library for raylib, not the static library I want. Meanwhile, ksl_raylib is the name of the crate I'm trying to compile as a dynamic library. The path .local/share/ksl is where the program ksl will look for the libksl_raylib.dylib library. As for what ksl is, you can check out the GitHub repository link I provided for more information. Nonetheless, thank you for your reply.

For reference, the ksl_raylib directory structure is:

> tree -L 2 ksl_raylib

ksl_raylib
├── build.rs
├── Cargo.toml
├── example.ksl
├── extern
│   ├── raylib-5.5_macos
│   └── ZhuqueFangsong-Regular.ttf
├── ksl_raylib.ksl
├── README.md
└── src
    ├── lib.rs
    └── raylib

5 directories, 7 files

Note that the raylib library includes both dynamic and static libraries, and I'm trying to link against the static library.

> ls ksl_raylib/extern/raylib-5.5_macos/lib

libraylib.5.5.0.dylib libraylib.a
libraylib.550.dylib   libraylib.dylib

maybe that's the issue. when specify -lfoo, both libfoo.a and libfoo.so may be searched by the linker, and how to choose one over another is linker specific, for instance, gcc have -static flag.

what's the command line flags when rustc invoking the linker? e.g. what's the output when set RUSTFLAGS='--print link-args' and build?

The linker flags can be seen in the output of cargo build --verbose, which is:

-L /Users/kevinstephen/projects/rswk/ksl_raylib/extern/raylib-5.5_macos/lib -L native=/usr/local/lib -l static=raylib -l framework=OpenGL -l framework=Cocoa -l framework=IOKit -l framework=CoreFoundation -l framework=CoreVideo

Just in case, I'll also show the output using RUSTFLAGS='--print link-args': output for cargo build

It seems that for cc, the linker flags are these:

"-lraylib" "-framework" "OpenGL" "-framework" "Cocoa" "-framework" "IOKit" "-framework" "CoreFoundation" "-framework" "CoreVideo"

that's the command line flags cargo passed to rustc, not what rustc passed to the system linker (cc in this case).

I don't know how the linker on macos works, but it doesn't seem right, if the linker works similar to a linux linker, which I assume it is.

on linux, static libraries should be prefixed with a -Bstatic (or equivalently, -Wl,-Bstatic when the linker is invoked through the C compiler), and shared libraries should be prefixed with -Bdynamic, so a typical linker invocation by rustc looks like this:

cc
/path/to/some/object.o
...
/path/to/some/dependency-crate.rlib
...
-Wl,-Bstatic # begin a section of list of static libraries
-lfoo
-lbar
-Wl,-Bdynamic # begin a section of dynamic libraries
-lbaz
-Wl,-Bstatic # can be interleaved to preserve `-l` order
-lqux
...
-Wl,-Bdynamic # libc is dynamically linked by default for gnu, unless `+crt-static` is enabled
-lm
-ldl
-lc
...
-L
/library/search/dir/
...

btw, the gnu linker supports a special syntax for the -l flag, which allows the user to specify the full file name of the library to link, for example, if you specify -l:foo, the linker will search a file named foo instead of libfoo.a or libfoo.so. it searches the file in the same way as "regular" library files, you cannot give it an absolute path, so you still need the -L flag.

last time I checked, this also works in rust if you use #[link] attributes, at least on linux, e.g. #[link(name = ":libfoo.a")] will force the linker to use libfoo.a, while libfoo.so is preferred when only foo is specified. I don't know if this works on macos though, but at least worth a try.

another workaround is to specify the path to the static library as a direct input file to the linker instead of relying on the -l flag, in other words:

cc .. /my/lib/dir/libfoo.a ...

instead of:

cc ... -L /my/lib/dir -lfoo ...

Thank you very much for your help. Although the methods you provided later didn't work for me, your previous suggestion that the linker might consider libraylib.550.dylib as a valid link for the raylib library was very helpful. I removed the unnecessary dynamic libraries and recompiled, and since there were no dynamic libraries to interfere, the linker correctly linked the static library. Now it finally works as expected. Thank you again for your help in analyzing the issue!

But I'm still wondering why macOS didn't correctly locate the static library. I've tested this on Windows and Linux machines as well, and even without removing the unnecessary dynamic libraries, the linker still managed to link the static library correctly. This could be due to some peculiarity of macOS or Apple Clang?

I don't have a macos system to test for sure, but as I said above, it seems rustc didn't generate the -Bstatic/-Bdynamic linker flags before the library names. what's the root cause? I don't know.

1 Like

I found other people's complaints about the same issue on reddit, and although it was in 2021, it doesn't seem to be resolved

-Bstatic and -Bdynamic are not passed on macOS because it their linked doesn't support them.

2 Likes

Thanks for your patience and for walking me through the solution. I've learned a lot from this experience.