LTW: /DELAYLOAD
I'd like to start a new convention on this blog: learned this week (LTW) posts. These will be small posts documenting something I learned this week. Here's the first such post about a MSVC linker feature.
The MSVC linker has an interesting feature called Delay Load
Import.
It's purpose is to support runtime loading of shared libraries without having to
manually call LoadLibrary() and GetProcAddress().
For example, imagine you are writing a program on Windows which does something
with Windows Security Center (wscapi.dll):
Cargo.toml:
[package]
name = "wsctest"
version = "0.1.0"
edition = "2024"
[dependencies]
windows = { version = "0.62.2", default-features = false, features = ["Win32_System_SecurityCenter"] }
src/main.rs:
use windows::Win32::System::SecurityCenter;
fn main() {
println!("Start");
unsafe {
let _ = SecurityCenter::WscGetAntiMalwareUri();
}
println!("End");
}
Further, imagine that you want this program to run on both client Windows (e.g. Windows 11) and server Windows (e.g. Windows Server 2025). Maybe you even just want to run your tests in CI on a Windows Server runner. An important thing to know is that Windows Security Center is not supported on Windos Server 2025.
As written, this program will compile and run on client Windows. If you try to run this program on server Windows, it will fail with the following error:
error: process didn't exit successfully: `target/debug/wsctest.exe` (exit code: 0xc0000135, STATUS_DLL_NOT_FOUND)
The problem is that wscapi.dll is added as a shared library dependency which
is not found when we launch the program. If you run ldd on the output binary,
you see:
$ ldd target/debug/wsctest.exe
...
wscapi.dll => not found
One solution for this problem is to use MSVC's delay load feature. Add the following build script to pass linker arguments:
build.rs
fn main() {
println!("cargo:rustc-link-lib=delayimp");
println!("cargo:rustc-link-arg=/DELAYLOAD:wscapi.dll");
}
You are required to link delayimp when delay-loading a DLL.
For good measure, let's modify the program so that it knows whether it's running on Windows Server or not. We can use a CLI argument as a stand-in.
src/main.rs
// println!("Start");
let os_kind = std::env::args().nth(1).unwrap();
if os_kind != "server" {
unsafe {
let _ = SecurityCenter::WscGetAntiMalwareUri();
}
}
// println!("End");
Now when we run:
$ cargo run -- server
Start
End
Our program does not crash. On client windows, the DLL will be loaded when the first call to any function exported by the library is called.