win32_round_trip/win32_round_trip.rs
1// Copyright (c) 2026 Mike Grier
2//! A wide (`*W`) Win32 round trip with no string conversion in either
3//! direction.
4//!
5//! Run it with:
6//!
7//! ```text
8//! cargo run --example win32_round_trip
9//! ```
10//!
11//! It exercises all three halves of the FFI surface against real kernel32
12//! entry points:
13//!
14//! * **terminated input** -- `as_terminated_ptr()` into `GetFullPathNameW`'s
15//! `LPCWSTR` parameter;
16//! * **buffer-fill output** -- `with_capacity` / `as_mut_ptr` /
17//! `set_len_from_ffi` to receive that call's result;
18//! * **counted input** -- `as_ptr()` + `len()` into `CompareStringOrdinal`,
19//! which takes explicit lengths and so works on borrowed slices that carry
20//! no terminator at all.
21//!
22//! The entry points are declared inline rather than pulled from `windows-sys`,
23//! purely to keep the crate dependency-free.
24
25#[cfg(windows)]
26fn main() {
27 windows::run();
28}
29
30#[cfg(not(windows))]
31fn main() {
32 eprintln!("win32_round_trip is Windows-only; nothing to demonstrate here.");
33}
34
35#[cfg(windows)]
36mod windows {
37 use wtf_string::{Wtf16Str, Wtf16String};
38
39 #[link(name = "kernel32")]
40 unsafe extern "system" {
41 /// Expands `lpfilename` to a full path. With `nbufferlength == 0` it
42 /// reports the required size *including* the terminator; on success it
43 /// returns the count written *excluding* it. Zero means failure.
44 fn GetFullPathNameW(
45 lpfilename: *const u16,
46 nbufferlength: u32,
47 lpbuffer: *mut u16,
48 lpfilepart: *mut *mut u16,
49 ) -> u32;
50
51 /// Ordinal comparison of two *counted* wide strings: neither pointer
52 /// needs to be NUL-terminated.
53 fn CompareStringOrdinal(
54 lpstring1: *const u16,
55 cchcount1: i32,
56 lpstring2: *const u16,
57 cchcount2: i32,
58 bignorecase: i32,
59 ) -> i32;
60 }
61
62 /// `CompareStringOrdinal` returns these rather than the usual -1/0/1.
63 const CSTR_LESS_THAN: i32 = 1;
64 const CSTR_EQUAL: i32 = 2;
65 const CSTR_GREATER_THAN: i32 = 3;
66
67 pub fn run() {
68 let input = Wtf16String::from(r"C:\Windows\System32\..\Temp");
69 println!("input : {input}");
70
71 match full_path(&input) {
72 Some(expanded) => println!("expanded: {expanded}"),
73 None => println!("expanded: <GetFullPathNameW failed>"),
74 }
75
76 compare(&Wtf16String::from("alpha"), &Wtf16String::from("beta"));
77 compare(&Wtf16String::from("beta"), &Wtf16String::from("alpha"));
78 compare(&Wtf16String::from("same"), &Wtf16String::from("same"));
79
80 // The counted pair works on a borrowed slice, which has no terminator
81 // of its own -- the length is what makes it well-defined.
82 let units: Vec<u16> = "borrowed".encode_utf16().collect();
83 let borrowed = Wtf16Str::from_units(&units);
84 println!(
85 "borrowed slice of {} units compares equal to itself: {}",
86 borrowed.len(),
87 ordinal(borrowed, borrowed) == CSTR_EQUAL
88 );
89 }
90
91 /// Terminated input, then buffer-fill output -- both without converting.
92 fn full_path(input: &Wtf16String) -> Option<Wtf16String> {
93 // Pass 1: ask for the size. The terminator is already in the buffer, so
94 // handing over an `LPCWSTR` costs nothing.
95 // SAFETY: `as_terminated_ptr` is NUL-terminated and valid while
96 // `input` is borrowed; a zero length asks for the required size only.
97 let needed = unsafe {
98 GetFullPathNameW(
99 input.as_terminated_ptr(),
100 0,
101 core::ptr::null_mut(),
102 core::ptr::null_mut(),
103 )
104 };
105 if needed == 0 {
106 return None;
107 }
108
109 // `needed` counts the terminator; our capacity is a *content* length,
110 // and `with_capacity` reserves the terminator slot itself.
111 let mut out = Wtf16String::with_capacity(needed as usize - 1);
112
113 // Pass 2: let the API write straight into our buffer.
114 // SAFETY: the buffer has room for `needed` units (content + the
115 // reserved terminator slot), which is exactly what pass 1 asked for.
116 let written = unsafe {
117 GetFullPathNameW(
118 input.as_terminated_ptr(),
119 needed,
120 out.as_mut_ptr(),
121 core::ptr::null_mut(),
122 )
123 };
124 if written == 0 || written >= needed {
125 // Failed, or raced a directory change and now wants more room.
126 // `out`'s invariant is still broken here, so republish an empty
127 // string before dropping it (see `as_mut_ptr`'s contract).
128 // SAFETY: publishing zero content units is always in bounds.
129 unsafe { out.set_len_from_ffi(0) };
130 return None;
131 }
132
133 // `written` excludes the terminator, which is precisely the content
134 // length `set_len_from_ffi` wants -- no guessing about conventions.
135 // SAFETY: the API initialized `written` units and `written < needed`,
136 // so the appended terminator still fits.
137 unsafe { out.set_len_from_ffi(written as usize) };
138 Some(out)
139 }
140
141 /// Counted input: pointer + length, no terminator required.
142 fn ordinal(a: &Wtf16Str, b: &Wtf16Str) -> i32 {
143 // SAFETY: each pointer is valid for exactly its own `len()` units while
144 // borrowed, which is the contract `CompareStringOrdinal` expects.
145 unsafe {
146 CompareStringOrdinal(
147 a.as_ptr(),
148 a.len() as i32,
149 b.as_ptr(),
150 b.len() as i32,
151 0, // case-sensitive
152 )
153 }
154 }
155
156 fn compare(a: &Wtf16String, b: &Wtf16String) {
157 let verdict = match ordinal(a, b) {
158 CSTR_LESS_THAN => "<",
159 CSTR_EQUAL => "==",
160 CSTR_GREATER_THAN => ">",
161 _ => "?(failed)",
162 };
163 println!("compare: {a} {verdict} {b}");
164 }
165}