Skip to main content
Version: 0.9.0 (next)

Dynamic Loading

What you build​

A program that finds qar-streaming-c, the QAROS runtime library, loads it, initializes it and prints which file it loaded and which runtime version that file is. It then destroys the library again. Every other QAROS program in Rust starts with these lines.

What you will learn:

  • how Library::load_default finds the runtime, and when to use Library::load with a path instead
  • why loading is all or nothing: every function resolves at load, or nothing does
  • what to print so a user can tell which runtime your program runs against
  • what dropping a Library does, and why the runtime library stays loaded afterwards

Before you start​

  • Windows for the real runtime. QAROS installed, or the bin folder of a QAROS runtime package somewhere on disk. No Hub needs to run: this example only loads the library.
  • A runtime that exports the whole header. The binding needs a runtime that exports every function of the header it was generated from (the repository's qar-streaming-c/include/qar_streaming.h), and that is the runtime of the Hub you join. An older runtime fails to load and names the functions it lacks, as step 2 explains.
  • macOS or Linux: the example runs against qaros-stub, the in-memory stand-in the tests use.
  • The latest stable Rust.
  • Run the commands from the binding's folder, qar-streaming-rust/.

Run it​

With QAROS installed, the default lookup finds the installed runtime:

cargo run -p qaros-examples --bin dynamic_loading

With an unpacked QAROS package instead, name its bin folder, which holds qar-streaming-c.dll, with QAROS_RUNTIME_DIR:

$env:QAROS_RUNTIME_DIR = "C:\path\to\QAROS-package\bin"
cargo run -p qaros-examples --bin dynamic_loading

To load one library file and nothing else, name it. The example also takes the path from the QAROS_DLL variable, or from the first line of a qaros-library.txt beside the executable:

cargo run -p qaros-examples --bin dynamic_loading -- --qaros-library=C:\path\to\qar-streaming-c.dll

Without Windows, build the stub and name it. On macOS:

cargo build -p qaros-stub
cargo run -p qaros-examples --bin dynamic_loading -- --qaros-library=target/debug/libqaros_stub.dylib

It prints (with your own absolute path):

Loading qar-streaming-c from: target/debug/libqaros_stub.dylib
Loaded /.../target/debug/libqaros_stub.dylib
Library initialized with 199 symbols. Runtime binaries directory: (folder of the loaded library)
Runtime 0.9.0-stub (stub, C API v0)
Library destroyed; qar-streaming-c stays loaded until the process exits. Dynamic loading tutorial complete.

Against QAROS on Windows, the first line is Loading the QAROS runtime (default lookup) and the Loaded line names the step that found the runtime, for example Loaded C:\...\bin\qar-streaming-c.dll (the installed QAROS). The Runtime line shows the version of the runtime you loaded, with the flavor release or demo. The symbol count is the same on Windows; on Linux it is 197, since the Linux bindings have no D3D11 functions. The stub is qaros_stub.dll on Windows and libqaros_stub.so on Linux.

Walkthrough​

1. Decide where the runtime comes from​

A distributed application never names a file. It runs against the QAROS installed on the machine, so the Hub and the application use the same runtime (Authoring a Language Binding). A path is for a development build of QAROS, or for the stub.

The example's main hands the whole decision to a helper shared by all examples:

let library = load_library()?;
println!(
"Library initialized with {} symbols. Runtime binaries directory: (folder of the loaded library)",
qaros::qaros_sys::SYMBOL_NAMES.len(),
);

The helper first looks for an explicit file. A command-line flag wins, then an environment variable, then a file beside the executable. The last one exists for apps that QAROS starts itself: those get no arguments or environment of their own.

/// The explicit library file: `--qaros-library=PATH`, else `QAROS_DLL`, else the first line of
/// `qaros-library.txt` beside the executable. `None` loads the runtime with the default lookup.
pub fn explicit_library_path() -> Option<PathBuf> {
if let Some(path) = Arguments::parse().flag_value("--qaros-library") {
return Some(PathBuf::from(path));
}
if let Some(path) = std::env::var_os("QAROS_DLL") {
return Some(PathBuf::from(path));
}
library_path_from_file()
}

2. Load and initialize the library​

/// Loads and initializes the library, printing what it does like the C examples: the explicit
/// library file when one is given ([`explicit_library_path`]), else the runtime the default
/// lookup finds.
#[cfg(not(feature = "link"))]
pub fn load_library() -> ExampleResult<Library> {
install_stderr_logger();
let library = if let Some(path) = explicit_library_path() {
println!("Loading qar-streaming-c from: {}", path.display());
// SAFETY: the user named a qar-streaming-c build; a mismatching library is the user's
// configuration error, as in the C examples.
unsafe { Library::load(&path, &LibraryConfig::default()) }?
} else {
println!("Loading the QAROS runtime (default lookup)");
// SAFETY: the runtime found is the installed QAROS or one the user chose with
// QAROS_RUNTIME_DIR; a mismatching one is a configuration error, as in the C examples.
unsafe { Library::load_default(&LibraryConfig::default()) }.map_err(|error| match error {
qaros::Error::Load(qaros::qaros_sys::LoadError::NotFound { .. }) => format!(
"{error}\nInstall QAROS, set QAROS_RUNTIME_DIR to the folder of qar-streaming-c.dll, or pass --qaros-library=PATH (the qaros-stub library in target/debug/ runs without QAROS)."
),
other => other.to_string(),
})?
};
println!("Loaded {}", describe_loaded_runtime(&library));
Ok(library)
}

Both calls do two things: they open the library and resolve its functions, then they run qar_library_init with the settings in LibraryConfig (console logging on by default). You get a Library only when both succeeded.

Both calls are unsafe. Opening a library runs its initialization code, and the Rust bindings trust that every exported function has the signature the header declares. The compiler cannot check either, so you promise that the file is a qar-streaming-c build matching these bindings.

Library::load(path, &config) tries that one file and nothing else, from its absolute path. Its dependencies resolve from its own folder and System32, never from your executable's folder.

Library::load_default(&config) runs the default lookup every QAROS binding follows. The first match wins:

StepWhere it looksPrinted as
0a qar-streaming-c already loaded in this processalready loaded in this process
1beside the binding: the folder of the executable or library this crate is compiled intobeside the binding
2the folder QAROS_RUNTIME_DIR namesQAROS_RUNTIME_DIR
3the installed QAROS: InstallFolder under HKLM\SOFTWARE\Quaternar\QAROS, plus \bin (Windows only)the installed QAROS
4each absolute folder on PATH, in order; elsewhere the dynamic linker's searchPATH; elsewhere the dynamic linker's search

Two steps end the lookup when they fail. A set QAROS_RUNTIME_DIR is the only copy considered, so a missing or broken runtime there is an error even when PATH holds a good one. An installed QAROS whose runtime is missing or fails to load is an error too. The current directory is never searched, and the process's search path is never changed.

Library::lookup_step() returns the step that found the runtime, and Library::library_path() the file. The helper prints both, so Loaded ... (QAROS_RUNTIME_DIR) tells you at a glance which runtime you got. Print this in your own application too. A copy of the runtime beside the executable wins over the installation, and this line is how you notice.

All or nothing. The load resolves every qar_impl_* function the bindings know before it returns. If one is missing, it fails with a list of all missing names and keeps nothing. You never hold a half-bound API that fails at its first call.

What can go wrong. Every failure here is an Error::Load holding a qaros_sys::LoadError:

  • NotFound: the default lookup found nothing. Its message names every place it looked and what it found there. The helper adds what to do.
  • MissingSymbols: the file loaded but lacks functions. The runtime is older than the QAROS version the bindings were generated from, so it does not have the functions that version added; the error names each of them.
  • Open: the operating system refused the file, with its reason (missing file, missing dependency, wrong architecture).

A failing qar_library_init is an Error::Api naming qar_library_init, its status code and message. None of these is normal control flow: report the error and stop, or run without QAROS features.

3. Print what you loaded​

let runtime_info = library.runtime_info()?;
println!(
"Runtime {} ({}, C API {})",
runtime_info.version, runtime_info.flavor, runtime_info.c_api
);

Library::runtime_info() calls qar_runtime_info and returns the runtime's version, its flavor (release or demo) and the C API generation. The symbol count printed in step 1 is qaros_sys::SYMBOL_NAMES.len(): how many functions the bindings required, all of which resolved.

Log both in your application. A bug report with the runtime version and the file path saves a round of questions.

4. Tear down​

drop(library);
println!(
"Library destroyed; qar-streaming-c stays loaded until the process exits. Dynamic loading tutorial complete."
);

Dropping the last Library runs qar_library_destroy. The runtime library itself stays loaded until the process exits; the next section explains why.

Ownership and teardown​

  • Library is a shared handle. Cloning it shares the same library. Every runtime, session and other handle you create from it keeps it alive as well. qar_library_destroy runs when the last of them is gone, whatever order your values drop in. You cannot destroy the library under a live session.
  • One per process. qar_library_init is process-wide. While a Library is alive, a second load returns Error::LibraryAlreadyLoaded. After a successful destroy you may load again.
  • The runtime library is never unloaded. Freeing it after qar_library_destroy runs its detach code and unmaps code that its threads may still reach, which has crashed processes at exit. The wrapper therefore leaves it mapped. Only a load whose qar_library_init failed unloads the file again, since no handle of yours can refer to it.
  • A failed destroy is logged, not returned. Drop cannot return an error. The wrapper logs it through the log crate and keeps the process slot reserved, so every later load returns Error::LibraryAlreadyLoaded. Install a logger if you want to see it; the examples log to stderr.
  • Not Send. A Library and everything made from it stays on the thread that created it. The C API is designed for single-threaded use per runtime; its callbacks are the only cross-thread surface.
  • What the wrapper cannot guarantee: that the file you load matches the bindings. That is the promise behind unsafe.

Troubleshooting​

What you seeCauseWhat to do
no loadable QAROS runtime (qar-streaming-c.dll); looked at: followed by one line per place (libqar-streaming-c.dylib on macOS, libqar-streaming-c.so on Linux)no runtime in any place of the lookupinstall QAROS, or set QAROS_RUNTIME_DIR to the runtime's bin folder, or pass --qaros-library=PATH
QAROS_RUNTIME_DIR: ...\qar-streaming-c.dll does not exist (the macOS or Linux file name elsewhere), and the lookup stops therethe variable names a folder without the runtime; the lookup does not go on to PATHfix or unset the variable
... lacks N of M required symbols: ..., followed by the missing namesthe runtime is older than the header the bindings were generated from: it lacks the functions the header addeduse a runtime that exports every function of the repository's qar-streaming-c/include/qar_streaming.h, the runtime of the Hub you join; or pin the dependency (rev or tag) to a commit whose header that runtime exports in full
cannot open ...: with the operating system's reasonthe file or one of its dependencies cannot be loadedname the qar-streaming-c.dll inside the runtime's bin folder, where its dependencies are
Loaded ... (beside the binding) when you expected the installationa copy of the runtime sits beside your executable and winsremove it; do not ship the runtime with your application
a QAROS library is already initialized in this processa second Library while one is alive, or after a failed destroyload once and clone the Library

The Troubleshooting page covers what comes after loading.

From C​

The C tutorial Dynamic Loading shows the same lifecycle with explicit calls.

CRust
qar_library_load(NULL) then qar_library_init(&init)Library::load_default(&config), one call
qar_library_load(path) then qar_library_init(&init)Library::load(path, &config)
QarLibraryInit from qar_library_init_default()LibraryConfig::default()
qar_library_destroy() then qar_library_unload()drop the last Library; it destroys and never unloads
qar_installed_bin_folder, qar_runtime_env_folderqaros::installed_bin_folder(), qaros::runtime_dir_from_environment()
qar_runtime_infoLibrary::runtime_info()
no argument for the default lookup, --qaros-library=PATH for a filethe same, and a file from QAROS_DLL or qaros-library.txt beside the executable
exit codes 1 and 2exit code 1 on any error, with the message on stderr

With the link feature the bindings link the import library instead and Library::linked(&config) initializes it; the runtime package ships no import library, so this is for builds of QAROS itself.

Next​