Cogs and Levers A blog full of technical stuff

Loading dynamic libraries in Rust

Today’s post is going to be a quick demonstration of loading dynamic libraries at runtime in Rust.

In my earlier article, I showed how to use Glibc’s dlopen/dlsym/dlclose APIs from C to load a shared object off disk and call a function in it. Rust can do the same thing – with a bit more type safety – using:

This is not meant to be a full plugin framework, just a minimal “host loads a tiny library and calls one function” example, similar in spirit to the original C version.

A tiny library in Rust

We’ll start with a tiny dynamic library that exports one function, greet, which returns a C-style string:

cargo new --lib rust_greeter
cd rust_greeter

Edit Cargo.toml so that the library is built as a cdylib:

[package]
name = "rust_greeter"
version = "0.1.0"
edition = "2021"

[lib]
name = "test"                
crate-type = ["cdylib"]      

Now the library code in src/lib.rs:

use std::os::raw::c_char;

#[unsafe(no_mangle)]
pub extern "C" fn greet() -> *const c_char {
    static GREETING: &str = "Hello from Rust!\0";
    GREETING.as_ptr().cast()
}

The #[unsafe(no_mangle)] form marks the item (the function) as unsafe to call, and also forwards the nested no_mangle attribute exactly as written. This avoids needing unsafe fn syntax and keeps ABI-exported functions more visually consistent. It’s a small but nice modernisation that fits well when exposing C-compatible symbols from Rust.

Build:

cargo build --release

You’ll get:

target/release/libtest.so

Host program: loading the library with libloading

Create a new binary crate:

cargo new rust_host
cd rust_host

Add libloading to Cargo.toml:

[package]
name = "rust_host"
version = "0.1.0"
edition = "2021"

[dependencies]
libloading = "0.8"

And src/main.rs:

use std::error::Error;
use std::ffi::CStr;
use std::os::raw::c_char;

use libloading::{Library, Symbol};

type GreetFn = unsafe extern "C" fn() -> *const c_char;

fn main() -> Result<(), Box<dyn Error>> {
    unsafe {
        let lib = Library::new("./libtest.so")?;
        let greet: Symbol<GreetFn> = lib.get(b"greet\0")?;

        let raw = greet();
        let c_str = CStr::from_ptr(raw);
        let message = c_str.to_str()?;

        println!("{message}");
    }
    Ok(())
}

Before we can run any of this, we need to make sure the library is available to the host program. In order to do this, we simply copy over the library:

cp ../rust_greeter/target/release/libtest.so .

Just copy the so over to the host program folder.

Running cargo run prints:

$ cargo run                                     
   Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
    Running `target/debug/rust_host`
Hello from Rust!

Mapping back to the C version

  • Library::new(“./libtest.so”) ≈ dlopen()
  • lib.get(b”greet\0”) ≈ dlsym()
  • Dropping lib ≈ dlclose()

Platform notes

Linux: libtest.so
macOS: libtest.dylib
Windows: test.dll

cdylib produces the correct format automatically.

Conclusion

We:

  • built a tiny Rust cdylib exporting a C-ABI function,
  • loaded it at runtime with libloading,
  • looked up a symbol by name, and
  • invoked it through a typed function pointer.

I guess this was just a modern update to an existing article.

Just like in the C post, this is a deliberately minimal skeleton — but enough to grow into a proper plugin architecture once you define a stable API between host and library.