feat: re-enable std in uv-trampoline (#4722)

## Summary

Partially closes #1917

This PR picks up on some of the great work from #1864 and opted to keep
`panic_immediate_abort` (for size reasons). I split the PR in different
isolated commits in case we want to separate/cherry-pick them out.

1. The first commit ports mostly all std changes from that PR into this
PR. Binary sizes stayed the same ~16kb.
2. The second commit migrates our existing usage of windows-sys to
windows for a safer ffi calls with Results!. It also changes all large
unsafe blocks to be isolated to the actual unsafe calls, and switches
some areas to use std such as getenv port ( which seemed buggy! ) from
launcher.c. In addition, this also adds more error checking in order to
match some missing assertions from distlib's launcher.c. Note, due to
the additional .text data, the binary sizes increased to ~20.5kb, but we
can cut back on some of the added error msgs as needed.
3. The third commit switches to using xwin for building on all 3
supported trampoline targets for sanity, and adds a CI bloat check for
core::fmt and panic as a precaution. Sadly, this will invalidate the
xwin cache on the first run.

## Test Plan

Most changes were tested on a couple of local GUI apps and console apps,
also tested some of the error states manually by using SetLastError at
different points in the code and/or passing in invalid handles.

I'm not sure how far we can get with migrating some of the other calls
without increasing binary size substantially. An initial attempt at
using std::path didn't seem so bad size wise when I tried it (~1k). On
other cases, such as std::process::exit added ~10k to the total binary
size.

---------

Co-authored-by: konstin <konstin@mailbox.org>
This commit is contained in:
samypr100 2024-07-06 16:38:45 -04:00 committed by GitHub
parent 6c8ce1d013
commit eee90a340c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 482 additions and 494 deletions

View file

@ -88,7 +88,7 @@ jobs:
uses: actions/cache@v4
with:
path: "${{ github.workspace}}/.xwin"
key: cargo-xwin
key: cargo-xwin-x86_64
- name: Load rust cache
uses: Swatinem/rust-cache@v2
with:
@ -274,57 +274,65 @@ jobs:
windows-trampoline:
needs: determine_changes
if: ${{ github.repository == 'astral-sh/uv' && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
runs-on: windows-latest
name: "check windows trampoline"
runs-on: ubuntu-latest
name: "check windows trampoline | ${{ matrix.target-arch }}"
strategy:
fail-fast: false
matrix:
target-arch: ["x86_64", "i686", "aarch64"]
steps:
- name: Create Dev Drive using ReFS
run: |
$Volume = New-VHD -Path C:/uv_dev_drive.vhdx -SizeBytes 10GB |
Mount-VHD -Passthru |
Initialize-Disk -Passthru |
New-Partition -AssignDriveLetter -UseMaximumSize |
Format-Volume -FileSystem ReFS -Confirm:$false -Force
Write-Output $Volume
Write-Output "DEV_DRIVE=$($Volume.DriveLetter):" >> $env:GITHUB_ENV
- uses: actions/checkout@v4
# actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone...
- name: Copy Git Repo to Dev Drive
run: |
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.DEV_DRIVE }}/uv" -Recurse
- name: "Install Rust toolchain"
working-directory: ${{ env.DEV_DRIVE }}/uv/crates/uv-trampoline
env:
CARGO_HOME: ${{ env.DEV_DRIVE }}/.cargo
RUSTUP_HOME: ${{ env.DEV_DRIVE }}/.rustup
working-directory: ${{ github.workspace }}/crates/uv-trampoline
run: |
rustup target add x86_64-pc-windows-msvc
rustup component add clippy rust-src --toolchain nightly-2024-05-27-x86_64-pc-windows-msvc
rustup target add ${{ matrix.target-arch }}-pc-windows-msvc
rustup component add rust-src --target ${{ matrix.target-arch }}-pc-windows-msvc
- name: Load xwin cache
uses: actions/cache@v4
with:
path: "${{ github.workspace }}/.xwin"
key: cargo-xwin-${{ matrix.target-arch }}
- uses: rui314/setup-mold@v1
- uses: Swatinem/rust-cache@v2
with:
workspaces: ${{ env.DEV_DRIVE }}/uv/crates/uv-trampoline
env:
CARGO_HOME: ${{ env.DEV_DRIVE }}/.cargo
RUSTUP_HOME: ${{ env.DEV_DRIVE }}/.rustup
workspaces: ${{ github.workspace }}/crates/uv-trampoline
- name: "Install cargo-xwin and cargo-bloat"
uses: taiki-e/install-action@v2
with:
tool: cargo-xwin,cargo-bloat
- name: "Install xwin dependencies"
run: sudo apt-get install --no-install-recommends -y lld llvm clang cmake ninja-build
- name: "Clippy"
working-directory: ${{ env.DEV_DRIVE }}/uv/crates/uv-trampoline
working-directory: ${{ github.workspace }}/crates/uv-trampoline
if: matrix.target-arch == 'x86_64'
run: cargo xwin clippy --all-features --locked --target x86_64-pc-windows-msvc -- -D warnings
env:
CARGO_HOME: ${{ env.DEV_DRIVE }}/.cargo
RUSTUP_HOME: ${{ env.DEV_DRIVE }}/.rustup
run: cargo clippy --all-features --locked --target x86_64-pc-windows-msvc -- -D warnings
XWIN_ARCH: "x86_64"
XWIN_CACHE_DIR: "${{ github.workspace }}/.xwin"
- name: "Bloat Check"
working-directory: ${{ github.workspace }}/crates/uv-trampoline
if: matrix.target-arch == 'x86_64'
run: |
cargo xwin bloat --release --target x86_64-pc-windows-msvc | \
grep -q -E 'core::fmt|std::panicking|std::backtrace_rs' && exit 1 || exit 0
env:
XWIN_ARCH: "x86_64"
XWIN_CACHE_DIR: "${{ github.workspace }}/.xwin"
- name: "Build"
working-directory: ${{ env.DEV_DRIVE }}/uv/crates/uv-trampoline
working-directory: ${{ github.workspace }}/crates/uv-trampoline
run: cargo xwin build --release --target ${{ matrix.target-arch }}-pc-windows-msvc
env:
CARGO_HOME: ${{ env.DEV_DRIVE }}/.cargo
RUSTUP_HOME: ${{ env.DEV_DRIVE }}/.rustup
run: cargo build --release --target x86_64-pc-windows-msvc
XWIN_ARCH: "${{ matrix.target-arch == 'i686' && 'x86' || matrix.target-arch }}"
XWIN_CACHE_DIR: "${{ github.workspace }}/.xwin"
typos:
runs-on: ubuntu-latest

View file

@ -913,17 +913,33 @@ mod test {
Ok(())
}
#[test]
#[cfg(all(windows, target_arch = "x86"))]
fn test_launchers_are_small() {
// At time of writing, they are 17408 bytes.
assert!(
super::LAUNCHER_I686_GUI.len() < 25 * 1024,
"GUI launcher: {}",
super::LAUNCHER_I686_GUI.len()
);
assert!(
super::LAUNCHER_I686_CONSOLE.len() < 25 * 1024,
"CLI launcher: {}",
super::LAUNCHER_I686_CONSOLE.len()
);
}
#[test]
#[cfg(all(windows, target_arch = "x86_64"))]
fn test_launchers_are_small() {
// At time of writing, they are 15872 bytes.
// At time of writing, they are 21504 and 20480 bytes.
assert!(
super::LAUNCHER_X86_64_GUI.len() < 20 * 1024,
super::LAUNCHER_X86_64_GUI.len() < 25 * 1024,
"GUI launcher: {}",
super::LAUNCHER_X86_64_GUI.len()
);
assert!(
super::LAUNCHER_X86_64_CONSOLE.len() < 20 * 1024,
super::LAUNCHER_X86_64_CONSOLE.len() < 25 * 1024,
"CLI launcher: {}",
super::LAUNCHER_X86_64_CONSOLE.len()
);
@ -932,16 +948,16 @@ mod test {
#[test]
#[cfg(all(windows, target_arch = "aarch64"))]
fn test_launchers_are_small() {
// At time of writing, they are 14848 and 14336 bytes.
// At time of writing, they are 20480 and 19456 bytes.
assert!(
super::LAUNCHER_AArch64_GUI.len() < 20 * 1024,
super::LAUNCHER_AARCH64_GUI.len() < 25 * 1024,
"GUI launcher: {}",
super::LAUNCHER_AArch64_GUI.len()
super::LAUNCHER_AARCH64_GUI.len()
);
assert!(
super::LAUNCHER_AArch64_CONSOLE.len() < 20 * 1024,
super::LAUNCHER_AARCH64_CONSOLE.len() < 25 * 1024,
"CLI launcher: {}",
super::LAUNCHER_AArch64_CONSOLE.len()
super::LAUNCHER_AARCH64_CONSOLE.len()
);
}

View file

@ -1,3 +1,3 @@
[unstable]
build-std = ["core", "panic_abort", "alloc", "std"]
build-std-features = ["compiler-builtins-mem"]
build-std = ["std", "panic_abort"]
build-std-features = ["compiler-builtins-mem", "panic_immediate_abort"]

View file

@ -37,6 +37,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "ufmt"
version = "0.2.0"
@ -55,7 +66,7 @@ checksum = "d337d3be617449165cb4633c8dece429afd83f84051024079f97ad32a9663716"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 1.0.109",
]
[[package]]
@ -77,27 +88,72 @@ dependencies = [
"embed-manifest",
"ufmt",
"ufmt-write",
"windows-sys",
"windows",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
name = "windows"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core",
"windows-targets",
]
[[package]]
name = "windows-core"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result",
"windows-targets",
]
[[package]]
name = "windows-implement"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.58",
]
[[package]]
name = "windows-interface"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.58",
]
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.0"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
@ -106,42 +162,48 @@ dependencies = [
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"

View file

@ -6,28 +6,30 @@ license = "MIT OR Apache-2.0"
edition = "2021"
autotests = false
# Need to optimize etc. or else build fails
[profile.dev]
lto = true
codegen-units = 1
opt-level = 1
panic = "abort"
debug-assertions = false
overflow-checks = false
debug = true
[profile.release]
# Enable Link Time Optimization.
lto = true
# Reduce number of codegen units to increase optimizations.
codegen-units = 1
# Optimize for size.
opt-level = "z"
# Abort on panic.
panic = "abort"
# Automatically strip symbols from the binary.
strip = true
debug = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
windows-sys = { version = "0.52.0", features = [
windows = { version = "0.57.0", features = [
"Win32_Foundation",
"Win32_Security",
"Win32_Storage_FileSystem",
@ -45,7 +47,7 @@ windows-sys = { version = "0.52.0", features = [
] }
ufmt-write = "0.1.0"
ufmt = "0.2.0"
ufmt = { version = "0.2.0", features = ["std"] }
[build-dependencies]
embed-manifest = "1.4.0"

View file

@ -19,9 +19,9 @@ rustup target add aarch64-pc-windows-msvc
Then, build the trampolines for both supported architectures:
```shell
cargo +nightly-2024-05-27 xwin build --xwin-arch x86 --release --target i686-pc-windows-msvc
cargo +nightly-2024-05-27 xwin build --release --target x86_64-pc-windows-msvc
cargo +nightly-2024-05-27 xwin build --release --target aarch64-pc-windows-msvc
cargo +nightly-2024-06-08 xwin build --xwin-arch x86 --release --target i686-pc-windows-msvc
cargo +nightly-2024-06-08 xwin build --release --target x86_64-pc-windows-msvc
cargo +nightly-2024-06-08 xwin build --release --target aarch64-pc-windows-msvc
```
### Cross-compiling from macOS
@ -39,9 +39,9 @@ rustup target add aarch64-pc-windows-msvc
Then, build the trampolines for both supported architectures:
```shell
cargo +nightly-2024-05-27 xwin build --release --target i686-pc-windows-msvc
cargo +nightly-2024-05-27 xwin build --release --target x86_64-pc-windows-msvc
cargo +nightly-2024-05-27 xwin build --release --target aarch64-pc-windows-msvc
cargo +nightly-2024-06-08 xwin build --release --target i686-pc-windows-msvc
cargo +nightly-2024-06-08 xwin build --release --target x86_64-pc-windows-msvc
cargo +nightly-2024-06-08 xwin build --release --target aarch64-pc-windows-msvc
```
### Updating the prebuilt executables
@ -84,9 +84,8 @@ That's what this does: it's a generic "trampoline" that lets us generate custom
### How do you use it?
Basically, this looks up `python.exe` (for console programs) or
`pythonw.exe` (for GUI programs) in the adjacent directory, and invokes
`python[w].exe path\to\the\<the .exe>`.
Basically, this looks up `python.exe` (for console programs)
and invokes `python.exe path\to\the\<the .exe>`.
The intended use is:
@ -124,25 +123,21 @@ is copied more-or-less directly.
### Anything I should know for hacking on this?
In order to minimize binary size, this uses `#![no_std]`, `panic="abort"`, and
carefully avoids using `core::fmt`. This removes a bunch of runtime overhead: by
In order to minimize binary size, this uses, `panic="abort"`, and carefully
avoids using `core::fmt`. This removes a bunch of runtime overhead: by
default, Rust "hello world" on Windows is ~150 KB! So these binaries are ~10x
smaller.
Of course the tradeoff is that `#![no_std]` is an awkward super-limited
environment. No C runtime, no platform APIs, very few features... you don't even
get `Vec` or memory allocation or panicking support by default. To work around
this:
Of course the tradeoff is that this is an awkward super-limited
environment. No C runtime and limited platform APIs... you don't
even panicking support by default. To work around this:
- We use `windows-sys` to access Win32 APIs directly. Who needs a C runtime?
- We use `windows` to access Win32 APIs directly. Who needs a C runtime?
Though uh, this does mean that literally all of our code is `unsafe`. Sorry!
- `runtime.rs` has the core glue to get panicking, heap allocation, and linking
working.
- `diagnostics.rs` uses `ufmt` and some cute Windows tricks to get a convenient
version of `eprintln!` that works without `std`, and automatically prints to
either the console if available or pops up a message box if not.
version of `eprintln!` that works without `core::fmt`, and automatically prints
to either the console if available or pops up a message box if not.
- All the meat is in `bounce.rs`.
@ -164,20 +159,15 @@ Miscellaneous tips:
Building this can be frustrating, because the low-level compiler/runtime
machinery have a bunch of implicit assumptions about the environment they'll run
in, and the facilities it provides for things like `memcpy`, unwinding, etc.
With `#![no_std]` most of this machinery is missing. So we need to replace the
bits that we actually need, and which bits we need can change depending on stuff
like optimization options. For example: we use `panic="abort"`, so we don't
actually need unwinding support, but at lower optimization levels the compiler
might not realize that, and still emit references to the unwinding helper
`__CxxFrameHandler3`. And then the linker blows up because that symbol doesn't
exist.
So we need to replace the bits that we actually need, and which bits we need
can change depending on stuff like optimization options.
For example: we use `panic="abort"`, so we don't actually need unwinding support,
but at lower optimization levels the compiler might not realize that, and still
emit references to the unwinding helper`__CxxFrameHandler3`. And then the linker
blows up because that symbol doesn't exist.
```
cargo build --release --target i686-pc-windows-msvc
cargo build --release --target x86_64-pc-windows-msvc
cargo build --release --target aarch64-pc-windows-msvc
```
Hopefully in the future as `#![no_std]` develops, this will get smoother.
Also, sometimes it helps to fiddle with optimization levels.

View file

@ -1,2 +1,2 @@
[toolchain]
channel = "nightly-2024-05-27"
channel = "nightly-2024-06-08"

View file

@ -1,4 +1,3 @@
#![no_std]
#![no_main]
#![windows_subsystem = "console"]

View file

@ -1,4 +1,3 @@
#![no_std]
#![no_main]
#![windows_subsystem = "windows"]

View file

@ -1,52 +1,53 @@
use alloc::string::String;
use alloc::{ffi::CString, vec, vec::Vec};
use core::mem::MaybeUninit;
use core::{
ffi::CStr,
mem,
ptr::{addr_of, addr_of_mut, null, null_mut},
};
use std::ffi::{CStr, CString};
use std::mem::size_of;
use std::mem::MaybeUninit;
use std::ptr::addr_of;
use std::vec::Vec;
use windows_sys::Win32::Storage::FileSystem::{
CreateFileA, GetFileSizeEx, ReadFile, SetFilePointerEx, FILE_ATTRIBUTE_NORMAL, FILE_BEGIN,
FILE_SHARE_READ, OPEN_EXISTING,
};
use windows_sys::Win32::{
Foundation::*,
System::{
Console::*,
Environment::{GetCommandLineA, GetEnvironmentVariableA, SetCurrentDirectoryA},
JobObjects::*,
LibraryLoader::GetModuleFileNameA,
Threading::*,
use windows::core::{s, PCSTR, PSTR};
use windows::Win32::{
Foundation::{
CloseHandle, GetLastError, SetHandleInformation, SetLastError, BOOL,
ERROR_INSUFFICIENT_BUFFER, ERROR_SUCCESS, HANDLE, HANDLE_FLAG_INHERIT,
INVALID_HANDLE_VALUE, MAX_PATH, TRUE,
},
Storage::FileSystem::{
CreateFileA, GetFileSizeEx, ReadFile, SetFilePointerEx, FILE_ATTRIBUTE_NORMAL, FILE_BEGIN,
FILE_GENERIC_READ, FILE_SHARE_READ, OPEN_EXISTING,
},
System::Console::{
GetStdHandle, SetConsoleCtrlHandler, SetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE,
STD_OUTPUT_HANDLE,
},
System::Diagnostics::Debug::{
FormatMessageA, FORMAT_MESSAGE_ALLOCATE_BUFFER, FORMAT_MESSAGE_FROM_SYSTEM,
FORMAT_MESSAGE_IGNORE_INSERTS,
},
System::Environment::GetCommandLineA,
System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
QueryInformationJobObject, SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK,
},
System::LibraryLoader::GetModuleFileNameA,
System::Threading::{
CreateProcessA, ExitProcess, GetExitCodeProcess, GetStartupInfoA, WaitForInputIdle,
WaitForSingleObject, INFINITE, PROCESS_CREATION_FLAGS, PROCESS_INFORMATION,
STARTF_USESTDHANDLES, STARTUPINFOA,
},
UI::WindowsAndMessaging::{
CreateWindowExA, DestroyWindow, GetMessageA, PeekMessageA, PostMessageA, HWND_MESSAGE, MSG,
PEEK_MESSAGE_REMOVE_TYPE, WINDOW_EX_STYLE, WINDOW_STYLE,
},
UI::WindowsAndMessaging::*,
};
use crate::helpers::SizeOf;
use crate::{eprintln, format};
const MAGIC_NUMBER: [u8; 4] = [b'U', b'V', b'U', b'V'];
const PATH_LEN_SIZE: usize = mem::size_of::<u32>();
const PATH_LEN_SIZE: usize = size_of::<u32>();
const MAX_PATH_LEN: u32 = 32 * 1024;
fn getenv(name: &CStr) -> Option<CString> {
unsafe {
let count = GetEnvironmentVariableA(name.as_ptr() as _, null_mut(), 0);
if count == 0 {
return None;
}
let mut value = Vec::<u8>::with_capacity(count as usize);
GetEnvironmentVariableA(
name.as_ptr() as _,
value.as_mut_ptr(),
value.capacity() as u32,
);
value.set_len(count as usize);
Some(CString::from_vec_with_nul_unchecked(value))
}
}
/// Transform `<command> <arguments>` to `python <command> <arguments>`.
fn make_child_cmdline() -> CString {
let executable_name: CString = executable_filename();
@ -69,8 +70,8 @@ fn make_child_cmdline() -> CString {
// Helpful when debugging trampoline issues
// eprintln!(
// "executable_name: '{}'\nnew_cmdline: {}",
// core::str::from_utf8(executable_name.to_bytes()).unwrap(),
// core::str::from_utf8(child_cmdline.as_slice()).unwrap()
// std::str::from_utf8(executable_name.to_bytes()).unwrap(),
// std::str::from_utf8(child_cmdline.as_slice()).unwrap()
// );
CString::from_vec_with_nul(child_cmdline).unwrap_or_else(|_| {
@ -98,11 +99,11 @@ fn push_quoted_path(path: &CStr, command: &mut Vec<u8>) {
fn executable_filename() -> CString {
// MAX_PATH is a lie, Windows paths can be longer.
// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation
// But it's a good first guess, usually paths are short and we should only need a single attempt.
// But it's a good first guess, usually paths are short, and we should only need a single attempt.
let mut buffer: Vec<u8> = vec![0; MAX_PATH as usize];
loop {
// Call the Windows API function to get the module file name
let len = unsafe { GetModuleFileNameA(0, buffer.as_mut_ptr(), buffer.len() as u32) };
let len = unsafe { GetModuleFileNameA(None, &mut buffer) };
// That's the error condition because len doesn't include the trailing null byte
if len as usize == buffer.len() {
@ -116,7 +117,7 @@ fn executable_filename() -> CString {
err => {
print_last_error_and_exit(&format!(
"Failed to get executable name (code: {})",
err
err.0
));
}
}
@ -143,40 +144,33 @@ fn executable_filename() -> CString {
/// # Panics
/// If there's any IO error, or the file does not conform to the specified format.
fn find_python_exe(executable_name: &CStr) -> CString {
let file_handle = expect_result(
unsafe {
let file_handle = unsafe {
CreateFileA(
executable_name.as_ptr() as _,
GENERIC_READ,
PCSTR::from_raw(executable_name.as_ptr() as *const _),
FILE_GENERIC_READ.0,
FILE_SHARE_READ,
null(),
None,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
0,
None,
)
},
INVALID_HANDLE_VALUE,
|| {
format!(
}
.unwrap_or_else(|_| {
print_last_error_and_exit(&format!(
"Failed to open executable '{}'",
&*executable_name.to_string_lossy(),
)
},
);
))
});
let mut file_size: i64 = 0;
// `SetFilePointerEx` supports setting the file pointer from the back, but pointing it past the file's start
// results in an error. That's why we need to know the file size to avoid ever seeking past the start of the file.
expect_result(
unsafe { GetFileSizeEx(file_handle, &mut file_size) },
0,
|| {
format!(
if unsafe { GetFileSizeEx(file_handle, &mut file_size) }.is_err() {
print_last_error_and_exit(&format!(
"Failed to get the size of the executable '{}'",
&*executable_name.to_string_lossy(),
)
},
);
));
}
// Start with a size of 1024 bytes which should be enough for most paths but avoids reading the
// entire file.
@ -187,34 +181,24 @@ fn find_python_exe(executable_name: &CStr) -> CString {
// SAFETY: Casting to usize is safe because we only support 64bit systems where usize is guaranteed to be larger than u32.
buffer.resize(bytes_to_read as usize, 0);
expect_result(
unsafe {
if unsafe {
SetFilePointerEx(
file_handle,
file_size - i64::from(bytes_to_read),
null_mut(),
None,
FILE_BEGIN,
)
},
0,
|| String::from("Failed to set the file pointer to the end of the file."),
);
}
.is_err()
{
print_last_error_and_exit("Failed to set the file pointer to the end of the file.");
}
let mut read_bytes = 0u32;
expect_result(
unsafe {
ReadFile(
file_handle,
buffer.as_mut_ptr() as *mut _,
bytes_to_read,
&mut read_bytes,
null_mut(),
)
},
0,
|| String::from("Failed to read the executable file"),
);
let mut read_bytes = bytes_to_read;
if unsafe { ReadFile(file_handle, Some(&mut buffer), Some(&mut read_bytes), None) }.is_err()
{
print_last_error_and_exit("Failed to read the executable file.");
}
// Truncate the buffer to the actual number of bytes read.
buffer.truncate(read_bytes as usize);
@ -271,9 +255,9 @@ fn find_python_exe(executable_name: &CStr) -> CString {
}
};
expect_result(unsafe { CloseHandle(file_handle) }, 0, || {
String::from("Failed to close file handle")
});
if unsafe { CloseHandle(file_handle) }.is_err() {
print_last_error_and_exit("Failed to close file handle.");
}
if is_absolute(&path) {
path
@ -307,13 +291,12 @@ fn is_absolute(path: &CStr) -> bool {
}
fn push_arguments(output: &mut Vec<u8>) {
let arguments_as_str = unsafe {
// SAFETY: We rely on `GetCommandLineA` to return a valid pointer to a null terminated string.
CStr::from_ptr(GetCommandLineA() as _)
};
let arguments_as_str = unsafe { GetCommandLineA() };
let arguments_as_bytes = unsafe { arguments_as_str.as_bytes() };
// Skip over the executable name and then push the rest of the arguments
let after_executable = skip_one_argument(arguments_as_str.to_bytes());
let after_executable = skip_one_argument(arguments_as_bytes);
output.extend_from_slice(after_executable)
}
@ -350,70 +333,78 @@ fn skip_one_argument(arguments: &[u8]) -> &[u8] {
}
fn make_job_object() -> HANDLE {
unsafe {
let job = CreateJobObjectW(null(), null());
let job = unsafe { CreateJobObjectW(None, None) }
.unwrap_or_else(|_| print_last_error_and_exit("Job creation failed."));
let mut job_info = MaybeUninit::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>::uninit();
let mut retlen = 0u32;
expect_result(
if unsafe {
QueryInformationJobObject(
job,
JobObjectExtendedLimitInformation,
job_info.as_mut_ptr() as *mut _,
job_info.size_of(),
&mut retlen as *mut _,
),
0,
|| String::from("Error from QueryInformationJobObject"),
);
let mut job_info = job_info.assume_init();
Some(&mut retlen),
)
}
.is_err()
{
print_last_error_and_exit("Job information querying failed.");
}
let mut job_info = unsafe { job_info.assume_init() };
job_info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
job_info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
expect_result(
if unsafe {
SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
addr_of!(job_info) as *const _,
job_info.size_of(),
),
0,
|| String::from("Error from SetInformationJobObject"),
);
job
)
}
.is_err()
{
print_last_error_and_exit("Job information setting failed.");
}
job
}
fn spawn_child(si: &STARTUPINFOA, child_cmdline: CString) -> HANDLE {
unsafe {
if si.dwFlags & STARTF_USESTDHANDLES != 0 {
// ignore errors from these -- if the handle's not inheritable/not valid, then nothing
// we can do
SetHandleInformation(si.hStdInput, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
SetHandleInformation(si.hStdOutput, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
SetHandleInformation(si.hStdError, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
// See distlib/PC/launcher.c::run_child
if (si.dwFlags & STARTF_USESTDHANDLES).0 != 0 {
// ignore errors, if the handles are not inheritable/valid, then nothing we can do
unsafe { SetHandleInformation(si.hStdInput, HANDLE_FLAG_INHERIT.0, HANDLE_FLAG_INHERIT) }
.unwrap_or_else(|_| eprintln!("Making stdin inheritable failed."));
unsafe { SetHandleInformation(si.hStdOutput, HANDLE_FLAG_INHERIT.0, HANDLE_FLAG_INHERIT) }
.unwrap_or_else(|_| eprintln!("Making stdout inheritable failed."));
unsafe { SetHandleInformation(si.hStdError, HANDLE_FLAG_INHERIT.0, HANDLE_FLAG_INHERIT) }
.unwrap_or_else(|_| eprintln!("Making stderr inheritable failed."));
}
let mut child_process_info = MaybeUninit::<PROCESS_INFORMATION>::uninit();
expect_result(
unsafe {
CreateProcessA(
null(),
None,
// Why does this have to be mutable? Who knows. But it's not a mistake --
// MS explicitly documents that this buffer might be mutated by CreateProcess.
child_cmdline.as_ptr().cast_mut() as _,
null(),
null(),
1,
0,
null(),
null(),
addr_of!(*si),
PSTR::from_raw(child_cmdline.as_ptr() as *mut _),
None,
None,
true,
PROCESS_CREATION_FLAGS(0),
None,
None,
si,
child_process_info.as_mut_ptr(),
),
0,
|| String::from("Failed to spawn the python child process"),
);
let child_process_info = child_process_info.assume_init();
CloseHandle(child_process_info.hThread);
child_process_info.hProcess
)
}
.unwrap_or_else(|_| {
print_last_error_and_exit("Failed to spawn the python child process.");
});
let child_process_info = unsafe { child_process_info.assume_init() };
unsafe { CloseHandle(child_process_info.hThread) }.unwrap_or_else(|_| {
print_last_error_and_exit("Failed to close handle to python child process main thread.");
});
// Return handle to child process.
child_process_info.hProcess
}
// Apparently, the Windows C runtime has a secret way to pass file descriptors into child
@ -421,21 +412,31 @@ fn spawn_child(si: &STARTUPINFOA, child_cmdline: CString) -> HANDLE {
// The UCRT source code has details on the memory layout (see also initialize_inherited_file_handles_nolock):
// https://github.com/huangqinjin/ucrt/blob/10.0.19041.0/lowio/ioinit.cpp#L190-L223
fn close_handles(si: &STARTUPINFOA) {
unsafe {
for handle in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE] {
CloseHandle(GetStdHandle(handle));
SetStdHandle(handle, INVALID_HANDLE_VALUE);
// See distlib/PC/launcher.c::cleanup_standard_io()
for std_handle in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
if let Ok(handle) = unsafe { GetStdHandle(std_handle) } {
unsafe { CloseHandle(handle) }.unwrap_or_else(|_| {
eprintln!("Failed to close standard device handle {}.", handle.0);
});
unsafe { SetStdHandle(std_handle, INVALID_HANDLE_VALUE) }.unwrap_or_else(|_| {
eprintln!("Failed to modify standard device handle {}.", std_handle.0);
});
}
}
// See distlib/PC/launcher.c::cleanup_fds()
if si.cbReserved2 == 0 || si.lpReserved2.is_null() {
return;
}
let crt_magic = si.lpReserved2 as *const u32;
let handle_count = crt_magic.read_unaligned() as isize;
let handle_start = crt_magic.offset(1 + handle_count);
let handle_count = unsafe { crt_magic.read_unaligned() } as isize;
let handle_start = unsafe { crt_magic.offset(1 + handle_count) };
for i in 0..handle_count {
CloseHandle(handle_start.offset(i).read_unaligned() as HANDLE);
}
let handle_ptr = unsafe { handle_start.offset(i).read_unaligned() } as *const HANDLE;
// Close all fds inherited from the parent, except for the standard I/O fds.
unsafe { CloseHandle(*handle_ptr) }.unwrap_or_else(|_| {
eprintln!("Failed to close child file descriptors at {}.", i);
});
}
}
@ -459,139 +460,123 @@ fn close_handles(si: &STARTUPINFOA) {
Is creating a window and calling PeekMessage the best way to do this? idk.
*/
fn clear_app_starting_state(child_handle: HANDLE) {
unsafe {
PostMessageA(0, 0, 0, 0);
let mut msg = MaybeUninit::<MSG>::uninit();
GetMessageA(msg.as_mut_ptr(), 0, 0, 0);
WaitForInputIdle(child_handle, INFINITE);
unsafe {
// End the launcher's "app starting" cursor state.
PostMessageA(None, 0, None, None).unwrap_or_else(|_| {
eprintln!("Failed to post a message to specified window.");
});
if GetMessageA(msg.as_mut_ptr(), None, 0, 0) != TRUE {
eprintln!("Failed to retrieve posted window message.");
}
// Proxy the child's input idle event.
if WaitForInputIdle(child_handle, INFINITE) != 0 {
eprintln!("Failed to wait for input from window.");
}
// Signal the process input idle event by creating a window and pumping
// sent messages. The window class isn't important, so just use the
// system "STATIC" class.
let hwnd = CreateWindowExA(
0,
c"STATIC".as_ptr() as *const _,
c"uv Python Trampoline".as_ptr() as *const _,
0,
WINDOW_EX_STYLE(0),
s!("STATIC"),
s!("uv Python Trampoline"),
WINDOW_STYLE(0),
0,
0,
0,
0,
HWND_MESSAGE,
0,
0,
null(),
None,
None,
None,
);
PeekMessageA(msg.as_mut_ptr(), hwnd, 0, 0, 0);
DestroyWindow(hwnd);
// Process all sent messages and signal input idle.
_ = PeekMessageA(msg.as_mut_ptr(), hwnd, 0, 0, PEEK_MESSAGE_REMOVE_TYPE(0));
DestroyWindow(hwnd).unwrap_or_else(|_| {
print_last_error_and_exit("Failed to destroy temporary window.");
});
}
}
pub fn bounce(is_gui: bool) -> ! {
unsafe {
let child_cmdline = make_child_cmdline();
let mut si = MaybeUninit::<STARTUPINFOA>::uninit();
GetStartupInfoA(si.as_mut_ptr());
let si = si.assume_init();
unsafe { GetStartupInfoA(si.as_mut_ptr()) }
let si = unsafe { si.assume_init() };
let child_handle = spawn_child(&si, child_cmdline);
let job = make_job_object();
expect_result(AssignProcessToJobObject(job, child_handle), 0, || {
String::from("Error from AssignProcessToJobObject")
});
if unsafe { AssignProcessToJobObject(job, child_handle) }.is_err() {
print_last_error_and_exit("Failed to assign child process to the job.")
}
// (best effort) Close all the handles that we can
close_handles(&si);
// (best effort) Switch to some innocuous directory so we don't hold the original
// cwd open.
if let Some(tmp) = getenv(c"TEMP") {
SetCurrentDirectoryA(tmp.as_ptr() as *const _);
} else {
SetCurrentDirectoryA(c"c:\\".as_ptr() as *const _);
// (best effort) Switch to some innocuous directory, so we don't hold the original cwd open.
// See distlib/PC/launcher.c::switch_working_directory
if std::env::set_current_dir(std::env::temp_dir()).is_err() {
eprintln!("Failed to set cwd to temp dir.");
}
// We want to ignore control-C/control-Break/logout/etc.; the same event will
// be delivered to the child, so we let them decide whether to exit or not.
unsafe extern "system" fn control_key_handler(_: u32) -> BOOL {
1
TRUE
}
SetConsoleCtrlHandler(Some(control_key_handler), 1);
// See distlib/PC/launcher.c::control_key_handler
unsafe { SetConsoleCtrlHandler(Some(control_key_handler), true) }.unwrap_or_else(|_| {
print_last_error_and_exit("Control handler setting failed.");
});
if is_gui {
clear_app_starting_state(child_handle);
}
WaitForSingleObject(child_handle, INFINITE);
_ = unsafe { WaitForSingleObject(child_handle, INFINITE) };
let mut exit_code = 0u32;
expect_result(
GetExitCodeProcess(child_handle, addr_of_mut!(exit_code)),
0,
|| String::from("Error from GetExitCodeProcess"),
);
if unsafe { GetExitCodeProcess(child_handle, &mut exit_code) }.is_err() {
print_last_error_and_exit("Failed to get exit code of child process.");
}
exit_with_status(exit_code);
}
}
/// Unwraps the result of the C call by asserting that it doesn't match the `error_code`.
///
/// Prints the passed error message if the `actual_result` is equal to `error_code` and exits the process with status 1.
#[inline]
fn expect_result<T, F>(actual_result: T, error_code: T, error_message: F) -> T
where
T: Eq,
F: FnOnce() -> String,
{
if actual_result == error_code {
print_last_error_and_exit(&error_message());
}
actual_result
}
#[cold]
fn print_last_error_and_exit(message: &str) -> ! {
use windows_sys::Win32::{
Foundation::*,
System::Diagnostics::Debug::{
FormatMessageA, FORMAT_MESSAGE_ALLOCATE_BUFFER, FORMAT_MESSAGE_FROM_SYSTEM,
FORMAT_MESSAGE_IGNORE_INSERTS,
},
};
let err = unsafe { GetLastError() };
eprintln!("Received error code: {}", err);
let mut msg_ptr: *mut u8 = core::ptr::null_mut();
eprintln!("Received error code: {}", err.0);
let mut msg_ptr = PSTR::null();
let size = unsafe {
FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER
| FORMAT_MESSAGE_FROM_SYSTEM
| FORMAT_MESSAGE_IGNORE_INSERTS,
null(),
err,
None,
err.0,
0,
// Weird calling convention: this argument is typed as *mut u16,
// Weird calling convention: this argument is typed as *mut u8,
// but if you pass FORMAT_MESSAGE_ALLOCATE_BUFFER then you have to
// *actually* pass in a *mut *mut u16 and just lie about the type.
// *actually* pass in a *mut *mut u8 and just lie about the type.
// Getting Rust to do this requires some convincing.
core::ptr::addr_of_mut!(msg_ptr) as *mut _ as _,
PSTR(&mut msg_ptr.0 as *mut _ as *mut _),
0,
core::ptr::null(),
None,
)
};
if size == 0 {
if size == 0 || msg_ptr.0.is_null() {
eprintln!(
"{}: with code {} (failed to get error message)",
message, err
message, err.0
);
} else {
let reason = unsafe {
let reason = core::slice::from_raw_parts(msg_ptr, size as usize + 1);
CStr::from_bytes_with_nul_unchecked(reason)
let reason = std::slice::from_raw_parts(msg_ptr.0, size as usize);
std::str::from_utf8_unchecked(reason)
};
eprintln!(
"(uv internal error) {}: {}",
message,
&*reason.to_string_lossy()
);
eprintln!("(uv internal error) {}: {}", message, reason);
}
// Note: We don't need to free the buffer here because we're going to exit anyway.

View file

@ -1,16 +1,14 @@
extern crate alloc;
use alloc::{ffi::CString, string::String};
use core::{
convert::Infallible,
ptr::{addr_of_mut, null, null_mut},
};
use std::convert::Infallible;
use std::ffi::CString;
use std::string::String;
use ufmt_write::uWrite;
use windows_sys::Win32::{
use windows::core::PCSTR;
use windows::Win32::{
Foundation::INVALID_HANDLE_VALUE,
Storage::FileSystem::WriteFile,
System::Console::{GetStdHandle, STD_ERROR_HANDLE},
UI::WindowsAndMessaging::MessageBoxA,
UI::WindowsAndMessaging::{MessageBoxA, MESSAGEBOX_STYLE},
};
#[macro_export]
@ -43,24 +41,22 @@ impl uWrite for StringBuffer {
#[cold]
pub(crate) fn write_diagnostic(message: &str) {
unsafe {
let handle = GetStdHandle(STD_ERROR_HANDLE);
let handle = unsafe { GetStdHandle(STD_ERROR_HANDLE) }.unwrap_or(INVALID_HANDLE_VALUE);
let mut written: u32 = 0;
let mut remaining = message;
while !remaining.is_empty() {
let ok = WriteFile(
handle,
remaining.as_ptr(),
remaining.len() as u32,
addr_of_mut!(written),
null_mut(),
);
if ok == 0 {
let nul_terminated = CString::new(message.as_bytes()).unwrap_unchecked();
MessageBoxA(0, nul_terminated.as_ptr() as *const _, null(), 0);
// If we get an error, it means we tried to write to an invalid handle (GUI Application)
// and we should try to write to a window instead
if unsafe { WriteFile(handle, Some(remaining.as_bytes()), Some(&mut written), None) }
.is_err()
{
let nul_terminated = unsafe { CString::new(message.as_bytes()).unwrap_unchecked() };
let pcstr_message = PCSTR::from_raw(nul_terminated.as_ptr() as *const _);
unsafe { MessageBoxA(None, pcstr_message, None, MESSAGEBOX_STYLE(0)) };
return;
}
remaining = remaining.get_unchecked(written as usize..);
if let Some(out) = remaining.get(written as usize..) {
remaining = out
}
}
}

View file

@ -1,4 +1,4 @@
use core::mem::size_of;
use std::mem::size_of;
pub trait SizeOf {
fn size_of(&self) -> u32;

View file

@ -1,9 +1,3 @@
#![feature(panic_info_message)]
#![no_std]
extern crate alloc;
pub mod bounce;
mod diagnostics;
mod helpers;
mod runtime;

View file

@ -1,63 +0,0 @@
// Nothing in this file is directly imported anywhere else; it just fills in
// some of the no_std gaps.
extern crate alloc;
use alloc::alloc::{GlobalAlloc, Layout};
use core::ffi::c_void;
use windows_sys::Win32::System::{
Memory::{GetProcessHeap, HeapAlloc, HeapFree, HeapReAlloc, HEAP_ZERO_MEMORY},
Threading::ExitProcess,
};
use crate::eprintln;
// Windows wants this symbol. It has something to do with floating point usage?
// idk, defining it gets rid of link errors.
#[no_mangle]
#[used]
static _fltused: i32 = 0;
struct SystemAlloc;
#[global_allocator]
static SYSTEM_ALLOC: SystemAlloc = SystemAlloc;
unsafe impl Sync for SystemAlloc {}
unsafe impl GlobalAlloc for SystemAlloc {
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);
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, layout.size()) as *mut u8
}
unsafe fn realloc(&self, ptr: *mut u8, _layout: Layout, new_size: usize) -> *mut u8 {
HeapReAlloc(GetProcessHeap(), 0, ptr as *const c_void, new_size) as *mut u8
}
}
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
if let Some(location) = info.location() {
let mut msg = "(couldn't format message)";
if let Some(msg_args) = info.message() {
if let Some(msg_str) = msg_args.as_str() {
msg = msg_str;
}
}
eprintln!(
"panic at {}:{} (column {}): {}",
location.file(),
location.line(),
location.column(),
msg,
);
}
unsafe {
ExitProcess(128);
}
}