Carey Evans
Posted on August 21, 2022
If you’re distributing a standalone program for Windows, you should include an XML application manifest, but Cargo and Rust don’t provide an easy way to do this. In this post I’ll show you how you can do this yourself.
I’m using the manifest below, which unlocks compatibility with Windows versions 7 to 11, so that Windows doesn’t lie to your program that it’s running on Vista; sets the code page to UTF-8 so that you can pass Rust strings directly to API’s like MessageBoxA
; and enables long path names if you configure them in the registry, letting your current directory be very long.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" manifestVersion="1.0">
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
<asmv3:application>
<asmv3:windowsSettings>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</asmv3:windowsSettings>
</asmv3:application>
<asmv3:trustInfo>
<asmv3:security>
<asmv3:requestedPrivileges>
<asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</asmv3:requestedPrivileges>
</asmv3:security>
</asmv3:trustInfo>
</assembly>
The Visual Studio Build Tools way
If you only ever compile programs for Windows, on Windows, using the Visual Studio Build Tools, you can use a build script to add your Windows manifest by passing a couple of extra arguments to LINK.EXE
:
use std::{env, path::Path};
fn main() {
if env::var("TARGET").expect("target").ends_with("windows-msvc") {
let manifest = Path::new("hello.exe.manifest").canonicalize().unwrap();
println!("cargo:rustc-link-arg-bins=/MANIFEST:EMBED");
println!("cargo:rustc-link-arg-bins=/MANIFESTINPUT:{}", manifest.display());
println!("cargo:rerun-if-changed=hello.exe.manifest");
}
println!("cargo:rerun-if-changed=build.rs");
}
This is what rustc
does in release 1.63.
The “hold my beer” way
If you are compiling or cross-compiling with the MinGW tools, you can embed the manifest resource using inline assembly:
#![windows_subsystem = "windows"]
use std::ffi::c_void;
use std::ptr::null;
#[link(name = "kernel32")]
extern "system" {
fn GetVersion() -> u32;
}
#[link(name = "user32")]
extern "system" {
fn MessageBoxA(hwnd: *const c_void, lpText: *const u8, lpCaption: *const u8, uType: u32) -> i32;
}
fn main() {
let version = unsafe { GetVersion() };
let major_version = version & 0x0f;
let minor_version = (version & 0xf0) >> 8;
let build = if version < 0x80000000 { version >> 16 } else { 0 };
let message = format!(
"It’s raining 🐈s and 🐕s.\n\nI think this is Windows {}.{} ({}).\0",
major_version, minor_version, build
);
unsafe {
MessageBoxA(null(), message.as_ptr(), "Manifest Test\0".as_ptr(), 0x40);
}
}
#[cfg(all(windows, not(target_env = "msvc")))]
mod manifest {
use std::arch::global_asm;
global_asm!(
r#".section .rsrc$01,"dw""#,
".p2align 2",
"2:",
".zero 14",
".short 1",
".long 24",
".long (3f - 2b) | 0x80000000",
"3:",
".zero 14",
".short 1",
".long 1",
".long (4f - 2b) | 0x80000000",
"4:",
".zero 14",
".short 1",
".long 1033",
".long 5f - 2b",
"5:",
".long MANIFEST@imgrel",
".long 1207",
".zero 8",
".p2align 2",
);
#[no_mangle]
#[link_section = ".rsrc$02"]
static mut MANIFEST: [u8; 1207] = *br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" manifestVersion="1.0">
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
<asmv3:application>
<asmv3:windowsSettings>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</asmv3:windowsSettings>
</asmv3:application>
<asmv3:trustInfo>
<asmv3:security>
<asmv3:requestedPrivileges>
<asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</asmv3:requestedPrivileges>
</asmv3:security>
</asmv3:trustInfo>
</assembly>"#;
}
The inline assembly goes in a COFF section named .rsrc$01
, and the manifest string goes in a section named .rsrc$02
. The linker combines these into a single .rsrc
section in the executable, with .long MANIFEST@imgrel
in the header being turned into a relocatable reference to the manifest string.
You can combine these two approaches to work with any of the msvc
, gnu
and gnullvm
target environments.
The easy way
Self-promotion moment: I’ve written a crate called embed-manifest that will use the MSVC approach, or generate an object file containing the manifest data in a .rsrc
section and link against it, without needing to know as much about what you’re doing:
use embed_manifest::{embed_manifest, new_manifest};
fn main() {
if std::env::var_os("CARGO_CFG_WINDOWS").is_some() {
embed_manifest(new_manifest("Contoso.Sample"))
.expect("unable to embed manifest file");
}
println!("cargo:rerun-if-changed=build.rs");
}
It has no external dependencies so it won’t slow your build down much at all.
Posted on August 21, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 17, 2024