Let's add some context first: Windows XP went out of support at April 2014, Windows 7 went out of support at January 2020, and Windows 8.1 will go out of support at January 2023. Why would someone want to support Windows XP?
In my particular case: I have an arcade machine. No matter how strong Microsoft pushes to kill legacy hardware, my arcade will still have a 800Mhz FSB Pentium 4 Motherboard. Windows 7 runs slow on that hardware and newer versions run slower (not to mention that Microsoft stopped supporting 32-bit Windows).
Linux is a viable choice, I know that there's an ongoing effort to port some games from 16-bit DOS to Linux by hand-editing assembly in the binaries, and that effort has seen some success, but we're still not there yet.
ReactOS is starting to look viable too. I remember reading a person who resurrected a medical equipment thanks to ReactOS, but I couldn't find the link, I think it was an ECG.
Here's an excerpt from the ticket to drop XP support in Rust:
Removing official XP support would not stop anyone from compiling to XP (minus the std) [...]
What does this mean in practice? You can only use no_std
, which means you can only use the core
library and no_std crates.
I missed the second part of the excerpt on purpose:
[...] so long as LLVM and their linker still support XP targets.
So let's discuss about this particular point.
When you build a new project you usually pick the dependencies that will make it easier to port the project to other platforms. In the no_std world such project is the libc
crate, but do not confuse the Rust crate with the GNU library. This crate is maintained by the Rust team, and the support for XP depends on whether or not the maintainers decide to provide appropriate fallbacks whenever an update breaks XP support. Spoilers: They won't.
By not having the standard library of Rust, and by not having libc I am limiting myself to the Windows API (provided by the windows-sys
crate and maintained by Microsoft).
That will make an interesting experiment, because I can no longer use println!
or a libc equivalent like printf
to write a hello world.
An important detail about the Windows API is that you can find a handy table with details of the Windows version where a method was added.
Let's start by creating the hello world project:
cargo new hello_world
cd hello_world
cargo run
We get greeted by the familiar hello world:
Compiling hello_world v0.1.0 (C:\rust\hello_world)
Finished dev [unoptimized + debuginfo] target(s) in 0.75s
Running `target\debug\hello_world.exe`
Hello, world!
Looks unexciting, I know, but it will take some time until we see hello world working again, so let's start with this journey.
BTW, I am using the x86_64-pc-windows-msvc
toolchain.
First, I add an innocent attribute (#![no_std]
) at the start of main.rs
:
#![no_std]
fn main() {
println!("Hello, world!");
}
And I try to cargo run
it again:
Compiling hello_world v0.1.0 (C:\rust\hello_world)
error: cannot find macro `println` in this scope
--> src\main.rs:4:5
|
4 | println!("Hello, world!");
| ^^^^^^^
error: `#[panic_handler]` function required, but not found
error: language item required, but not found: `eh_personality`
|
= note: this can occur when a binary crate with `#![no_std]` is compiled for a target where `eh_personality` is defined in the standard library
= help: you may be able to compile for a target that doesn't need `eh_personality`, specify a target with `--target` or in `.cargo/config`
error: could not compile `hello_world` due to 3 previous errors
Oh boy, it sure broke. By removing the standard library I accepted to pay a hefty price: I don't have error handling anymore and I can't even panic!
.
Let's start by writing a super-simple panic handler, one which simply closes the program with error code 0xdeadbeef
.
use core::panic::PanicInfo;
use std::process::exit;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
exit(0xdeafbeef)
}
This should remove the first error, right? Let's cargo run
again:
Compiling hello_world v0.1.0 (C:\rust\hello_world)
error[E0433]: failed to resolve: use of undeclared crate or module `std`
--> src\main.rs:4:5
|
4 | use std::process::exit;
| ^^^ use of undeclared crate or module `std`
Oh, right! I forgot I'm not using std
anymore! Wait, how can I exit a program without std
then? That's right, I should check the Windows API (winapi for short), the answer is ExitProcess (introduced in Windows XP, how convenient). That means I have to add the windows-sys crate, but before doing that let's check the documentation on docs.rs.
There's this footnote: Required features: "Win32_System_Threading"
, I will need that feature name later so let's keep it at hand.
Let's add the windows-sys
crate to our Cargo.toml
, as well as the Win32_System_Threading
feature:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"
[dependencies]
windows-sys = { version = "0.42.0", features = ["Win32_System_Threading"] }
And let's replace our panic handler:
use core::panic::PanicInfo;
use windows_sys::Win32::System::Threading::ExitProcess;
#[panic_handler]
unsafe fn panic(_info: &PanicInfo) -> ! {
ExitProcess(0xdeafbeef)
}
See that unsafe fn
? Every method from winapi is considered unsafe, so keep that in mind because the Drop
trait will be helpful when writing destructors.
I run cargo run
again, and the panic_handler error is gone, yay! 🎉
I hope my phychologist doesn't believe I am crazy when I tell him that I spent a lot of time this weekend because of my Exception Handler's personality.
The good news is that you don't have to spend as much time as I did, we only need to add these lines to the Cargo.toml
:
[profile.dev]
panic = "abort"
[profile.release]
lto = true
panic = "abort"
I run cargo run again
, and I only have the println!
error:
Compiling windows_x86_64_msvc v0.42.0
Compiling windows-sys v0.42.0
Compiling hello_world v0.1.0 (C:\rust\hello_world)
error: cannot find macro `println` in this scope
--> src\main.rs:20:5
|
20 | println!("Hello, world!");
| ^^^^^^^
error: could not compile `hello_world` due to previous error
I will use the following winapi methods to write the in-house print method: WriteFile and GetStdHandle.
They bring the following features with them: Win32_Foundation
, Win32_Storage_FileSystem
, Win32_System_Console
and Win32_System_IO
.
At this point it is getting out of hand to add the features to our Cargo.toml
because rustfmt
collapses everything in a single line, so I'm going to give you a hint, you can separate a dependency into its own section (I'm not removing [dependencies]
yet, I'll be adding another crate later):
[dependencies]
[dependencies.windows-sys]
version = "0.42.0"
features = [
"Win32_Foundation",
"Win32_Storage_FileSystem",
"Win32_System_Console",
"Win32_System_IO",
"Win32_System_Threading",
]
This is my ahem unstable implementation of print:
use core::{ffi::c_void, ptr::null_mut};
use windows_sys::Win32::{
Storage::FileSystem::WriteFile,
System::Console::{GetStdHandle, STD_OUTPUT_HANDLE},
};
fn print(str: &str) -> usize {
let mut bytes_written = 0u32;
unsafe {
WriteFile(
GetStdHandle(STD_OUTPUT_HANDLE),
str.as_ptr() as *const c_void,
str.len() as u32,
&mut bytes_written,
null_mut(),
);
}
bytes_written as usize
}
I will talk about that unstability later, for now let's cargo run
this:
Compiling hello_world v0.1.0 (C:\rust\hello_world)
error: requires `start` lang_item
error: could not compile `hello_world` due to previous error
Wait, what? Are we playing wack-a-mole?
Long story short: I'm not using std, so I can't have a Rust-style main function, but I can have a C-style main function, so let's add a #![no_main]
attribute to the top and replace our main function:
use core::ffi::{c_char, c_int};
#[no_mangle]
pub extern "C" fn main(_argc: c_int, _argv: *const *const c_char) -> isize {
print("Hello, world!");
0
}
Let's try cargo run
again:
$ cargo run
Compiling hello_world v0.1.0 (C:\rust\hello_world)
error: linking with `link.exe` failed: exit code: 1120
|
= note: LINK : error LNK2001: unresolved external symbol mainCRTStartup
C:\rust\hello_world\target\debug\deps\hello_world.exe : fatal error LNK1120: 1 unresolved externals
Oh my, we are indeed playing wack-a-mole 🥲, this is again an issue with no_std
, I have to tell the linker to link against the visual studio runtime, so I add the following attributes to the main.rs
:
#[cfg(target_env = "msvc")]
#[link(name = "msvcrt")]
extern "C" {}
#[cfg(target_env = "msvc")]
#[link(name = "vcruntime")]
extern "C" {}
#[cfg(target_env = "msvc")]
#[link(name = "ucrt")]
extern "C" {}
I cargo run
again:
Compiling hello_world v0.1.0 (C:\rust\hello_world)
Finished dev [unoptimized + debuginfo] target(s) in 0.34s
Running `target\debug\hello_world.exe`
Hello, world!
It works! 🎉
I want to run on Windows XP, let's add the i686 target:
rustup target add i686-pc-windows-msvc
And cargo run --target i686-pc-windows-msvc
this time:
Compiling windows_i686_msvc v0.42.0
Compiling windows-sys v0.42.0
Compiling hello_world v0.1.0 (C:\rust\hello_world)
Finished dev [unoptimized + debuginfo] target(s) in 1.97s
Running `target\i686-pc-windows-msvc\debug\hello_world.exe`
Hello, world!
I copy the hello_world.exe
to Windows XP and I am greeted with hello_world.exe is not a valid Win32 application.
What's going on?
Long story short: You need to install the Visual Studio 2017 Build Tools for XP (aka v141_xp Platform Toolset), BUT... Rust doesn't yet have an equivalent to msbuild hello_world.vcxproj /p:PlatformToolset=v141_xp
, and the Rust team is still discussing how to target different Windows Subsystems. So the only way to continue is to edit ~/.cargo/config
to point to the correct linker.
I'm going to take a detour here and instead use the i686-pc-windows-gnu
toolchain:
rustup default stable-i686-pc-windows-gnu
cargo run
It works! 🎉
Compiling windows_i686_gnu v0.42.0
Compiling windows-sys v0.42.0
Compiling hello_world v0.1.0 (C:\rust\hello_world)
Finished dev [unoptimized + debuginfo] target(s) in 2.62s
Running `target\debug\hello_world.exe`
Hello, world!
I copy the hello_world.exe
to Windows XP and... It works! 🎉
Let's be honest here, the current setup is not attractive, at the very least we should be able to use familiar structs like String
, Vec
, or Box
, which is what we get if we enable the alloc
crate.
So let's add the following line to our (already crowded) main.rs
:
#[macro_use]
extern crate alloc;
I cargo run
and:
Compiling hello_world v0.1.0 (C:\rust\hello_world)
error: no global memory allocator found but one is required; link to std or add `#[global_allocator]` to a static item that implements the GlobalAlloc trait
error: `#[alloc_error_handler]` function required, but not found
note: use `#![feature(default_alloc_error_handler)]` for a default error handler
error: could not compile `hello_world` due to 2 previous errors
Back to playing wack-a-mole 🐭.
Let's add a basic error handler:
use core::alloc::Layout;
#[alloc_error_handler]
unsafe fn alloc_error_handler(_layout: Layout) -> ! {
ExitProcess(0xdeafbeef)
}
And...
error[E0658]: the `#[alloc_error_handler]` attribute is an experimental feature
--> src\main.rs:16:1
|
16 | #[alloc_error_handler]
| ^^^^^^^^^^^^^^^^^^^^^^
|
= note: see issue #51540 <https://github.com/rust-lang/rust/issues/51540> for more information
For more information about this error, try `rustc --explain E0658`.
Oh, wow, I can't use stable Rust today, the good news is that a PR is in progress, so hopefully this ticket will be fixed soon.
In the meantime let's move to the nightly channel, so create a rust-toolchain.toml
at the project root:
[toolchain]
channel = "nightly-i686-pc-windows-gnu"
Now that I moved to the nightly channel I can use the default error handling by adding #![feature(default_alloc_error_handler)]
to the top of main.rs
, so let's remove the error handler I added previously.
Now let's add the allocator (which requires adding the Win32_System_Memory
feature to Cargo.toml
):
#[derive(Default)]
struct Allocator;
unsafe impl GlobalAlloc for Allocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
HeapAlloc(GetProcessHeap(), 0, layout.size()) as *mut u8
}
unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
HeapFree(GetProcessHeap(), 0, ptr as *const c_void);
}
}
#[global_allocator]
static GLOBAL_ALLOCATOR: Allocator = Allocator;
I cargo run
again:
Compiling windows-sys v0.42.0
Compiling hello_world v0.1.0 (C:\rust\hello_world)
error: linking with `i686-w64-mingw32-gcc` failed: exit code: 1
|
= note: %USERPROFILE%\.rustup\toolchains\nightly-i686-pc-windows-gnu\lib\rustlib\i686-pc-windows-gnu\lib\liballoc-92c8714f80fbae62.rlib(alloc-92c8714f80fbae62.alloc.1911634b-cgu.0.rcgu.o):alloc.1911634b-cgu:(.eh_frame+0xbdb): undefined reference to `rust_eh_personality'
%USERPROFILE%\.rustup\toolchains\nightly-i686-pc-windows-gnu\lib\rustlib\i686-pc-windows-gnu\lib\libcore-e8cbede5689dc631.rlib(core-e8cbede5689dc631.core.13971860-cgu.0.rcgu.o):core.13971860-cgu.:(.eh_frame+0x5e7f): undefined reference to `rust_eh_personality'
We meet again, error handler personality.
Remember when I mentioned that I spent some time adding the personality?
#[cfg(target_env = "gnu")]
#[link(name = "stdc++")]
extern "C" {
fn __gxx_personality_v0(
version: c_int,
actions: c_int,
exception_class: *const c_char,
exception_object: *mut c_void,
context: *mut c_void,
) -> c_int;
}
#[cfg(target_env = "gnu")]
#[no_mangle]
unsafe fn rust_eh_personality(
version: c_int,
actions: c_int,
exception_class: *const c_char,
exception_object: *mut c_void,
context: *mut c_void,
) -> c_int {
__gxx_personality_v0(version, actions, exception_class, exception_object, context)
}
Long story short, this personality is provided by gcc (__gxx_personality_v0
), I looked for its signature and created a function that passes the arguments to it. I also link against lstdc++
.
I cargo run
again, and... We're back to business, the project works! 🎉 It works on XP too! 🎉🎉🎉
Remember when I mentioned that the print method was unstable?
Long story short: WriteFile expects a C string, it should be null-terminated.
Sorry I didn't tell you earlier, I didn't have any memory at all!
Why a singleton? Windows handles have a ritual to follow (ex. create, use, free), I don't want to do this ritual for every line I'm printing, and I don't want to pass references across the whole program either. I know singletons are cursed, but building for XP is cursed too. BTW, in the particular case of Std Handles cleaning up is not required.
Without further ado, here is my singleton:
#[macro_use]
extern crate lazy_static;
use alloc::{string::String, vec::Vec};
use core::{ffi::c_void, ptr::null_mut};
use windows_sys::Win32::{
Foundation::{HANDLE, INVALID_HANDLE_VALUE},
Storage::FileSystem::WriteFile,
System::Console::{GetStdHandle, STD_OUTPUT_HANDLE},
};
pub struct StdOut {
handle: HANDLE,
}
impl StdOut {
const NEWLINE_BYTES: &'static [u8] = &['\r' as u8, '\n' as u8, 0];
// Use the `STD_OUT` singleton instead
pub fn _new() -> Self {
let handle = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) };
StdOut { handle }
}
pub fn is_handle_valid(&self) -> bool {
self.handle != INVALID_HANDLE_VALUE
}
pub fn print_bytes(&self, bytes: Vec<u8>) -> usize {
let mut bytes_written = 0u32;
let buffer_size = bytes.len() as u32;
if self.is_handle_valid() {
unsafe {
WriteFile(
self.handle,
bytes.as_ptr() as *const c_void,
buffer_size,
&mut bytes_written,
null_mut(),
);
}
}
bytes_written as usize
}
pub fn print(&self, txt: String) -> usize {
let mut bytes = txt.into_bytes();
bytes.push(0);
self.print_bytes(bytes)
}
pub fn println(&self, txt: String) -> usize {
let mut bytes = txt.into_bytes();
bytes.extend_from_slice(Self::NEWLINE_BYTES);
self.print_bytes(bytes)
}
}
lazy_static! {
pub static ref STD_OUT: StdOut = StdOut::_new();
}
For this singleton to work, I need to add the lazy_static crate to Cargo.toml
:
[dependencies]
lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
I also removed the first iteration of the print
method, and rewrote the main function like this:
use core::ffi::{c_char, c_int};
#[no_mangle]
pub extern "C" fn main(_argc: c_int, _argv: *const *const c_char) -> isize {
STD_OUT.println(format!("Hello, {}!", "World"));
0
}
Notice that format!
? That's a gift from the alloc crate!
- Parse the arguments from main to receive the name via command line args. HINT: alloc::slice::from_raw_parts and core::ffi::CStr::to_str
- Create a singleton for STD_ERR
- Split the code in modules
- Fix compilation for MSVC
No, you don't need it.
It's enough to add subsystem version supported by WinXP to linker arguments:
/SUBSYSTEM:CONSOLE,5.01
.