Course contentsShow
← Rust
Lesson 1483 of 1,91012. Unsafe Rust and Low-Level AccessFree lesson

Symbol Versioning Concepts

You will handle library symbol versioning to avoid linking conflicts at runtime.

In this lesson, you will learn how to use symbol versioning to prevent "dependency hell" when your Rust library needs to export multiple, binary-incompatible versions of the same function.

The Challenge of Symbol Collisions

When you build a dynamic library (cdylib) in Rust, the linker exports symbols based on their names. If a host application loads two different versions of your library—or if your library evolves to change a function signature while maintaining the same symbol name—the dynamic linker may resolve calls to the wrong implementation. This often leads to runtime crashes or silent memory corruption because the binary layout of arguments or return types no longer matches.

Versioning via Linker Scripts

To solve this, we use symbol versioning. This technique attaches a version string (like MYLIB_1.0) to each exported symbol within the object files. At runtime, the dynamic linker ensures that a caller requesting my_function@MYLIB_1.0 is mapped strictly to that specific implementation, even if my_function@MYLIB_2.0 is also present in the memory space.

In Rust, because the compiler manages exports automatically, we define these versions using a linker script (usually a .map or .lds file). You instruct the linker to use this script via cargo:rustc-link-arg=-Wl,--version-script=mapfile.map.

An example of a version script looks like this:

MYLIB_1.0 {
    global:
        process_data;
    local: *;
};

MYLIB_2.0 {
    global:
        process_data_extended;
        process_data; # New signature bound to 2.0
} MYLIB_1.0;

How it Works

When you export a symbol, you are essentially creating a namespace for the linker. By wrapping your FFI-exposed functions in these version blocks, you force the linker to resolve symbols by both their name and their version tag. If a legacy C application expects process_data, you can keep an old version tagged as 1.0 while introducing a safer, refactored process_data tagged as 2.0. This provides the ABI stability required for long-lived systems where you cannot recompile every dependency simultaneously.

Key Takeaway: Symbol versioning allows you to coexist multiple ABI-incompatible versions of the same function within a single process by using linker-level version tags to disambiguate symbol resolution.