nsis_plugin/macros.rs
1//! The three macros that make up the plug-in surface.
2
3/// Declares the crate to be an NSIS plug-in DLL. Invoke exactly once.
4///
5/// Generates the `DllMain` that stashes the module handle, a `#[panic_handler]`
6/// that terminates rather than unwinding into the exehead, and a
7/// `#[global_allocator]` on the process heap so the DLL links without a C
8/// runtime.
9///
10/// Everything it emits is gated on `target_os = "windows"`, so a plug-in crate
11/// still `cargo check`s and `cargo clippy`s on macOS and Linux. Pair it with
12/// `#![cfg_attr(target_os = "windows", no_std)]`.
13///
14/// ```ignore
15/// nsis_plugin!(); // no_std: panic handler and allocator included
16/// nsis_plugin!(std); // std build: only DllMain
17/// ```
18///
19/// A `std` build links the C runtime, which owns the PE entry point
20/// (`DllMainCRTStartup`), initialises itself and then calls `DllMain`. So the
21/// `std` form defines `DllMain` instead; defining the entry point too is a
22/// duplicate symbol.
23#[macro_export]
24macro_rules! nsis_plugin {
25 () => {
26 $crate::__nsis_dllmain!(DllMainCRTStartup);
27
28 #[cfg(target_os = "windows")]
29 #[panic_handler]
30 fn __nsis_panic(_info: &::core::panic::PanicInfo) -> ! {
31 $crate::rt::abort()
32 }
33
34 #[cfg(target_os = "windows")]
35 #[global_allocator]
36 static __NSIS_ALLOCATOR: $crate::rt::NsisAllocator = $crate::rt::NsisAllocator;
37
38 $crate::__nsis_unwind_stubs!();
39 };
40 (std) => {
41 $crate::__nsis_dllmain!(DllMain);
42
43 /// Named by the landing pads in the precompiled `std`. Rust's i686
44 /// target expects a DWARF-unwinding mingw, but many i686 mingw builds
45 /// (Homebrew's among them) unwind with SJLJ, and their `libgcc_eh`
46 /// only has `_Unwind_SjLj_Resume`. With `panic = "abort"` nothing
47 /// unwinds, so the landing pads are dead code. On a DWARF toolchain
48 /// this definition keeps the archive member out, and nothing else in a
49 /// `panic = "abort"` DLL asks for it.
50 #[cfg(all(target_os = "windows", target_env = "gnu", target_arch = "x86"))]
51 #[unsafe(no_mangle)]
52 extern "C" fn _Unwind_Resume() -> ! {
53 $crate::rt::unwind_unreachable()
54 }
55 };
56}
57
58/// The symbols a `no_std` plug-in needs in place of `std` and the CRT.
59///
60/// These are expanded into the plug-in crate, not defined in `nsis-plugin`,
61/// and that placement is load-bearing — see the note in `rt.rs`. Nothing
62/// references them in the IR, so as upstream symbols fat LTO would internalise
63/// and drop them, and the DLL would fail to link on exactly these names.
64#[doc(hidden)]
65#[macro_export]
66macro_rules! __nsis_unwind_stubs {
67 () => {
68 /// Named by the unwind tables in the precompiled `core` and `alloc`.
69 #[cfg(target_os = "windows")]
70 #[unsafe(no_mangle)]
71 extern "C" fn rust_eh_personality() {}
72
73 /// The MSVC personality, named by the abort-on-unwind guard the
74 /// compiler attaches to every `extern "C"` export. `vcruntime` defines
75 /// it; `/NODEFAULTLIB` removes `vcruntime`.
76 #[cfg(all(target_os = "windows", target_env = "msvc"))]
77 #[unsafe(no_mangle)]
78 extern "C" fn __CxxFrameHandler3() -> ! {
79 $crate::rt::unwind_unreachable()
80 }
81
82 /// Named by mingw's unwinder on the GNU targets.
83 #[cfg(all(target_os = "windows", target_env = "gnu"))]
84 #[unsafe(no_mangle)]
85 extern "C" fn _Unwind_Resume() -> ! {
86 $crate::rt::unwind_unreachable()
87 }
88 };
89}
90
91#[doc(hidden)]
92#[macro_export]
93macro_rules! __nsis_dllmain {
94 ($entry:ident) => {
95 /// PE entry point, or the `DllMain` the CRT's entry point calls.
96 /// Returning 0 would make `LoadLibrary` fail.
97 #[cfg(target_os = "windows")]
98 #[unsafe(no_mangle)]
99 pub extern "system" fn $entry(
100 hinst: *mut ::core::ffi::c_void,
101 reason: u32,
102 _reserved: *mut ::core::ffi::c_void,
103 ) -> i32 {
104 const DLL_PROCESS_ATTACH: u32 = 1;
105 if reason == DLL_PROCESS_ATTACH {
106 $crate::rt::set_hinstance(hinst);
107 }
108 1
109 }
110 };
111}
112
113/// Defines one or more plug-in exports.
114///
115/// Each body gets a `&mut Nsis` and returns [`Result<()>`](crate::Result).
116/// Returning `Err` sets `exec_flags->exec_error`, so `IfErrors` works in the
117/// calling script; `Ok` leaves the flag untouched.
118///
119/// The export name is the function name, verbatim and undecorated — that is
120/// what `plugin::Name` resolves to in an `.nsi` script.
121///
122/// ```ignore
123/// nsis_fn! {
124/// fn Add(nsis: &mut Nsis) -> Result<()> {
125/// let b = nsis.stack.pop_int()?;
126/// let a = nsis.stack.pop_int()?;
127/// nsis.stack.push_int(a + b)?;
128/// Ok(())
129/// }
130///
131/// fn Reverse(nsis: &mut Nsis) -> Result<()> {
132/// let s = nsis.stack.pop()?;
133/// nsis.stack.push(&s.chars().rev().collect::<alloc::string::String>())
134/// }
135/// }
136/// ```
137///
138/// Arguments arrive on the stack, not in the signature: the five-pointer
139/// boundary is fixed by NSIS and the same for every export.
140#[macro_export]
141macro_rules! nsis_fn {
142 ($(
143 $(#[$attr:meta])*
144 fn $name:ident($nsis:ident: &mut Nsis) -> Result<()> $body:block
145 )*) => {$(
146 $(#[$attr])*
147 #[unsafe(no_mangle)]
148 pub unsafe extern "C" fn $name(
149 hwnd: $crate::Hwnd,
150 string_size: ::core::ffi::c_int,
151 variables: *mut $crate::Tchar,
152 stacktop: *mut *mut $crate::StackNode,
153 extra: *mut $crate::ExtraParameters,
154 ) {
155 // A plain nested fn, not a closure: nothing from the environment
156 // can leak in, and `?` cannot escape past `__nsis_body`. The
157 // signature is written with the caller's own `Nsis` and `Result`,
158 // so the source reads as the real thing rather than as a pattern.
159 fn __nsis_body($nsis: &mut Nsis) -> Result<()> $body
160
161 // SAFETY: these are the installer's own arguments, by construction.
162 let mut nsis = unsafe {
163 $crate::Nsis::from_raw(hwnd, string_size, variables, stacktop, extra)
164 };
165 let outcome = __nsis_body(&mut nsis);
166 if outcome.is_err() {
167 nsis.set_error(true);
168 }
169 }
170 )*};
171}
172
173/// Defines an `NSPIM_UNLOAD` callback, to be handed to
174/// [`Nsis::register_callback`](crate::Nsis::register_callback).
175///
176/// This is where cleanup belongs: `/NOUNLOAD` and `SetPluginsUnload` were
177/// deprecated in NSIS 3, and plug-ins now stay loaded for the life of the
178/// installer. `api.h` documents `NSPIM_UNLOAD` as the last message a plug-in
179/// receives.
180///
181/// ```ignore
182/// nsis_unload! {
183/// fn cleanup() {
184/// // release anything held across calls
185/// }
186/// }
187///
188/// nsis_fn! {
189/// fn Init(nsis: &mut Nsis) -> Result<()> {
190/// nsis.register_callback(cleanup)?;
191/// Ok(())
192/// }
193/// }
194/// ```
195#[macro_export]
196macro_rules! nsis_unload {
197 ($(
198 $(#[$attr:meta])*
199 fn $name:ident() $body:block
200 )*) => {$(
201 $(#[$attr])*
202 ///
203 /// Registered with `RegisterPluginCallback`; not a DLL export.
204 pub unsafe extern "C" fn $name(message: ::core::ffi::c_int) -> usize {
205 if message == $crate::NSPIM_UNLOAD {
206 fn __nsis_unload_body() $body
207 __nsis_unload_body();
208 }
209 // Unknown messages must return 0.
210 0
211 }
212 )*};
213}