Skip to main content

nsis_plugin/
rt.rs

1//! The runtime pieces a `no_std` plug-in DLL needs in order to link at all.
2//!
3//! [`nsis_plugin!`](crate::nsis_plugin) wires all of this up; you rarely name
4//! anything here directly. Everything is Windows-only, so a plug-in crate still
5//! `cargo check`s and `cargo test`s on macOS and Linux.
6
7#![allow(clippy::missing_safety_doc, reason = "these are C intrinsics")]
8
9use core::ffi::c_void;
10use core::sync::atomic::{AtomicUsize, Ordering};
11
12/// `HINSTANCE` of the loaded plug-in, stashed by `DllMain`.
13static HINSTANCE: AtomicUsize = AtomicUsize::new(0);
14
15/// Records the module handle. Called from the generated `DllMain`.
16pub fn set_hinstance(hinst: *mut c_void) {
17	HINSTANCE.store(hinst as usize, Ordering::Relaxed);
18}
19
20/// The plug-in's own module handle, as `RegisterPluginCallback` requires.
21///
22/// Null until `DllMain` has run, which cannot happen before an export is
23/// called.
24#[must_use]
25pub fn hinstance() -> *mut c_void {
26	HINSTANCE.load(Ordering::Relaxed) as *mut c_void
27}
28
29// -- Termination ------------------------------------------------------------
30
31// `#[link]` is what puts `kernel32.lib` on the link line. `std` normally
32// requests it; a `no_std` cdylib has to ask. On MSVC the request is the only
33// thing that survives `/NODEFAULTLIB`, which drops the CRT's own
34// `/defaultlib:kernel32.lib` directive along with the CRT.
35#[cfg(target_os = "windows")]
36#[link(name = "kernel32")]
37unsafe extern "system" {
38	fn GetCurrentProcess() -> *mut c_void;
39	fn TerminateProcess(process: *mut c_void, exit_code: u32) -> i32;
40}
41
42/// Ends the process.
43///
44/// Unwinding across the plug-in boundary into the exehead is undefined
45/// behaviour, so a panic in a plug-in has nowhere to go. Terminating is at
46/// least defined; the real answer is to return `Err` rather than panic.
47#[cfg(target_os = "windows")]
48pub fn abort() -> ! {
49	// SAFETY: `GetCurrentProcess` returns a pseudo-handle that is always valid
50	// for the calling process; neither call has further preconditions.
51	unsafe { TerminateProcess(GetCurrentProcess(), 0xC000_0409) };
52	// Unreachable in practice, but `TerminateProcess` is not `-> !`.
53	#[allow(clippy::empty_loop, reason = "process is already terminating")]
54	loop {}
55}
56
57/// Ends the process. Host builds defer to the standard abort.
58#[cfg(all(not(target_os = "windows"), feature = "std"))]
59pub fn abort() -> ! {
60	std::process::abort()
61}
62
63/// Ends the process. Host `no_std` builds have nothing better than a spin.
64#[cfg(all(not(target_os = "windows"), not(feature = "std")))]
65pub fn abort() -> ! {
66	#[allow(clippy::empty_loop, reason = "no_std host build has no abort")]
67	loop {}
68}
69
70// -- Unwinding stubs --------------------------------------------------------
71//
72// The precompiled `core` and `alloc` carry unwind tables that name
73// `rust_eh_personality`, even in a `panic = "abort"` build, and every
74// `extern "C"` export carries a guard that aborts if an unwind ever reaches
75// it — on the MSVC targets that guard names `__CxxFrameHandler3`. Normally
76// `std` and the CRT supply these; a `no_std` cdylib linked with
77// `/NODEFAULTLIB` has neither.
78//
79// The definitions themselves are emitted by `nsis_plugin!()`, into the
80// plug-in crate rather than into this one. They cannot live here: nothing
81// references them in the IR — the reference is attached to the exports by the
82// backend, after LTO has run — so fat LTO internalises them as unreachable
83// upstream symbols and drops them, and the link fails on the very symbol this
84// module was meant to provide. The memory intrinsics below are safe here
85// because IR calls them by name.
86//
87// None of the three can be reached: a panic goes to the `#[panic_handler]`,
88// which never returns.
89
90/// The body behind the unwinding stubs `nsis_plugin!()` emits.
91///
92/// Not meant to be called; it exists so the generated stubs stay one line
93/// each. Aborting is the honest answer — a plug-in has no unwinder.
94#[doc(hidden)]
95pub fn unwind_unreachable() -> ! {
96	abort()
97}
98
99// -- Global allocator -------------------------------------------------------
100
101#[cfg(target_os = "windows")]
102mod allocator {
103	use core::alloc::{GlobalAlloc, Layout};
104	use core::ffi::c_void;
105	use core::sync::atomic::{AtomicUsize, Ordering};
106
107	const HEAP_ZERO_MEMORY: u32 = 0x0000_0008;
108
109	/// `MEMORY_ALLOCATION_ALIGNMENT`: what `HeapAlloc` already guarantees.
110	const NATIVE_ALIGN: usize = if cfg!(target_pointer_width = "64") {
111		16
112	} else {
113		8
114	};
115
116	#[link(name = "kernel32")]
117	unsafe extern "system" {
118		fn GetProcessHeap() -> *mut c_void;
119		fn HeapAlloc(heap: *mut c_void, flags: u32, bytes: usize) -> *mut c_void;
120		fn HeapReAlloc(
121			heap: *mut c_void,
122			flags: u32,
123			mem: *mut c_void,
124			bytes: usize,
125		) -> *mut c_void;
126		fn HeapFree(heap: *mut c_void, flags: u32, mem: *mut c_void) -> i32;
127	}
128
129	static HEAP: AtomicUsize = AtomicUsize::new(0);
130
131	fn heap() -> *mut c_void {
132		let cached = HEAP.load(Ordering::Relaxed);
133		if cached != 0 {
134			return cached as *mut c_void;
135		}
136		// SAFETY: `GetProcessHeap` has no preconditions.
137		let h = unsafe { GetProcessHeap() };
138		HEAP.store(h as usize, Ordering::Relaxed);
139		h
140	}
141
142	/// A `GlobalAlloc` on the process heap.
143	///
144	/// The process heap is the smallest allocator available to a plug-in — it
145	/// needs no CRT and no initialisation, so the DLL links without a C
146	/// runtime.
147	pub struct NsisAllocator;
148
149	/// Header holding the real base pointer, for over-aligned allocations.
150	const HEADER: usize = core::mem::size_of::<usize>();
151
152	impl NsisAllocator {
153		unsafe fn alloc_flagged(layout: Layout, flags: u32) -> *mut u8 {
154			if layout.align() <= NATIVE_ALIGN {
155				// SAFETY: `heap()` is the process heap and `flags` is 0 or
156				// `HEAP_ZERO_MEMORY`; `HeapAlloc` reports failure as null.
157				return unsafe { HeapAlloc(heap(), flags, layout.size()) }.cast();
158			}
159			// Over-allocate, then place the aligned pointer and record the base
160			// just below it.
161			let total = layout.size() + layout.align() + HEADER;
162			// SAFETY: as above.
163			let base: *mut u8 = unsafe { HeapAlloc(heap(), flags, total) }.cast();
164			if base.is_null() {
165				return base;
166			}
167			let candidate = base as usize + HEADER;
168			let aligned = (candidate + layout.align() - 1) & !(layout.align() - 1);
169			let ptr = aligned as *mut u8;
170			// SAFETY: `base + HEADER <= aligned < base + HEADER + align`, and
171			// `total` reserves `align + HEADER` bytes beyond `size`, so both the
172			// header slot and the `size` bytes after `aligned` lie inside the
173			// block. `aligned` is a multiple of `align > NATIVE_ALIGN`, hence of
174			// `align_of::<usize>() == HEADER`, so the slot is aligned for `usize`.
175			unsafe { ptr.sub(HEADER).cast::<usize>().write(base as usize) };
176			ptr
177		}
178
179		unsafe fn base_of(ptr: *mut u8, layout: Layout) -> *mut u8 {
180			if layout.align() <= NATIVE_ALIGN {
181				ptr
182			} else {
183				// SAFETY: the caller passes a pointer `alloc_flagged` returned for
184				// this `layout`, which wrote the base into this slot.
185				unsafe { ptr.sub(HEADER).cast::<usize>().read() as *mut u8 }
186			}
187		}
188	}
189
190	// SAFETY: blocks up to `NATIVE_ALIGN` come straight from `HeapAlloc`, which
191	// guarantees that alignment; larger alignments go through the header path
192	// in `alloc_flagged`. `dealloc` and `realloc` recover the pointer `HeapAlloc`
193	// returned via `base_of`, so the heap only ever sees its own pointers.
194	unsafe impl GlobalAlloc for NsisAllocator {
195		unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
196			// SAFETY: forwards `GlobalAlloc::alloc`'s contract unchanged.
197			unsafe { Self::alloc_flagged(layout, 0) }
198		}
199
200		unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
201			// SAFETY: forwards `GlobalAlloc::alloc_zeroed`'s contract unchanged.
202			unsafe { Self::alloc_flagged(layout, HEAP_ZERO_MEMORY) }
203		}
204
205		unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
206			// SAFETY: `GlobalAlloc::dealloc` requires `ptr` to come from this
207			// allocator with this `layout`, which is what `base_of` needs.
208			let base = unsafe { Self::base_of(ptr, layout) };
209			// SAFETY: `base` is the pointer `HeapAlloc` returned on this heap.
210			unsafe { HeapFree(heap(), 0, base.cast()) };
211		}
212
213		unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
214			if layout.align() <= NATIVE_ALIGN {
215				// SAFETY: with no header, `ptr` is `HeapAlloc`'s own pointer on
216				// this heap.
217				return unsafe { HeapReAlloc(heap(), 0, ptr.cast(), new_size) }.cast();
218			}
219			// Over-aligned: the header makes in-place growth unsafe, so move.
220			let Ok(new_layout) = Layout::from_size_align(new_size, layout.align()) else {
221				return core::ptr::null_mut();
222			};
223			// SAFETY: `GlobalAlloc::realloc` guarantees `new_size` is non-zero.
224			let new_ptr = unsafe { self.alloc(new_layout) };
225			if !new_ptr.is_null() {
226				// SAFETY: `ptr` is valid for `layout.size()` bytes and `new_ptr`
227				// for `new_size`; they are distinct live blocks, so they do not
228				// overlap. `ptr` came from this allocator with `layout`.
229				unsafe {
230					core::ptr::copy_nonoverlapping(ptr, new_ptr, layout.size().min(new_size));
231					self.dealloc(ptr, layout);
232				}
233			}
234			new_ptr
235		}
236	}
237}
238
239#[cfg(target_os = "windows")]
240pub use allocator::NsisAllocator;
241
242// -- Memory intrinsics ------------------------------------------------------
243//
244// A plug-in links with `-nodefaultlibs` to keep the DLL small, which removes
245// the CRT's `memcpy` and friends. LLVM still emits calls to them, so provide
246// them here. Turn off the `mem-intrinsics` feature if you link a CRT that
247// already has them.
248
249#[cfg(all(target_os = "windows", feature = "mem-intrinsics"))]
250#[allow(
251	clippy::undocumented_unsafe_blocks,
252	reason = "every block rests on the same C contract: the caller guarantees \
253	          `n` valid bytes behind each pointer"
254)]
255#[allow(
256	suspicious_runtime_symbol_definitions,
257	reason = "`*mut u8` is ABI-identical to `*mut c_void` and keeps the byte \
258	          loops free of casts"
259)]
260mod mem {
261	#[unsafe(no_mangle)]
262	unsafe extern "C" fn memcpy(dest: *mut u8, src: *const u8, n: usize) -> *mut u8 {
263		let mut i = 0;
264		while i < n {
265			unsafe { *dest.add(i) = *src.add(i) };
266			i += 1;
267		}
268		dest
269	}
270
271	#[unsafe(no_mangle)]
272	unsafe extern "C" fn memmove(dest: *mut u8, src: *const u8, n: usize) -> *mut u8 {
273		if (dest as usize) < (src as usize) {
274			let mut i = 0;
275			while i < n {
276				unsafe { *dest.add(i) = *src.add(i) };
277				i += 1;
278			}
279		} else {
280			let mut i = n;
281			while i != 0 {
282				i -= 1;
283				unsafe { *dest.add(i) = *src.add(i) };
284			}
285		}
286		dest
287	}
288
289	#[unsafe(no_mangle)]
290	unsafe extern "C" fn memset(dest: *mut u8, c: i32, n: usize) -> *mut u8 {
291		let byte = c as u8;
292		let mut i = 0;
293		while i < n {
294			unsafe { *dest.add(i) = byte };
295			i += 1;
296		}
297		dest
298	}
299
300	#[unsafe(no_mangle)]
301	unsafe extern "C" fn memcmp(a: *const u8, b: *const u8, n: usize) -> i32 {
302		let mut i = 0;
303		while i < n {
304			let (x, y) = unsafe { (*a.add(i), *b.add(i)) };
305			if x != y {
306				return i32::from(x) - i32::from(y);
307			}
308			i += 1;
309		}
310		0
311	}
312
313	#[unsafe(no_mangle)]
314	unsafe extern "C" fn bcmp(a: *const u8, b: *const u8, n: usize) -> i32 {
315		unsafe { memcmp(a, b, n) }
316	}
317}