1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
//! Running the interpreter on the main thread, on a stack big enough for it.
//!
//! # The conflict
//!
//! Two requirements meet head-on in a Tk session, and neither of them bends.
//!
//! **Tk needs the main thread.** `Tk_MacOSXSetupTkNotifier` installs the Aqua
//! event source — the thing that turns a mouse click into a `Tcl_Event` — only
//! when the current run loop is the process's main run loop, and calls
//! `Tcl_Panic("first [load] of TkAqua has to occur in the main thread!")` if
//! that is true on a thread AppKit does not consider the main one
//! (`tk9.0.4/macosx/tkMacOSXNotify.c:258-272`). The two
//! `Tcl_MacOSXNotifierAddRunLoopMode` calls that keep events flowing during a
//! menu drag and a modal dialog are inside the same branch (`:270-271`). On any
//! other thread Tk initialises without complaint and no window event ever
//! arrives.
//!
//! **The interpreter needs a stack.** A nested `eval` runs a VM of its own, so
//! nesting costs native stack, and the interpreter's answer to a runaway script
//! is to refuse at the depth `tclsh` refuses at rather than overflow — because
//! an overflow is a signal, not an error a script can be blamed for. Measured
//! on this tree at the default limit of 1000 levels: **99 MiB** unoptimized (98
//! fails, 99 passes) and **14 MiB** optimized (12 fails, 14 passes).
//! `runtime::RECOMMENDED_STACK` is 256 MiB.
//!
//! macOS gives the main thread 8.0 MiB
//! (`pthread_get_stacksize_np(pthread_self())`, measured). That is not enough
//! for either profile.
//!
//! # What was rejected, and what it measured
//!
//! * **Raise `RLIMIT_STACK` and re-exec.** The soft limit is 8176 KiB and the
//! hard limit is 65520 KiB (`ulimit -s`, `ulimit -Hs`); raising the soft
//! limit to the hard one and `execv`-ing the binary gives the child a main
//! thread of exactly 64.0 MiB (measured). That covers an optimized build with
//! four times the room and misses an unoptimized one by 35 MiB, so the Tk
//! path would work in release and die on a signal under `cargo test`. The
//! 64 MiB is a ceiling, not a setting: it is the hard limit.
//! * **`-Wl,-stack_size` at link time.** This does work — a binary linked with
//! `-Wl,-stack_size,0x10000000` reports a 256.0 MiB main stack, and costs
//! nothing resident (1328 KiB either way, measured with `task_info`). But the
//! linker rejects the option for anything that is not a main executable
//! (`ld: -stack_size option can only be used when linking a main
//! executable`), so it cannot be a blanket `rustflags`: this tree links 13
//! proc-macro crates, each of which is a dylib. Scoping it to binaries is
//! `cargo::rustc-link-arg-bins` in `build.rs`, and `build.rs` belongs to
//! another part of this work.
//! * **Keep the interpreter on the worker and drive the run loop from the main
//! thread.** Tk's C would then run on the main thread while the interpreter's
//! state lived on another, and Tk calls back into the interpreter from inside
//! its own event dispatch. That is two threads sharing one interpreter, which
//! Tcl does not do and this host would have to invent.
//!
//! # What this does instead
//!
//! Keeps the thread and changes its stack. The main thread runs the
//! interpreter on a region this module maps, by switching the stack pointer for
//! the duration of the call and switching it back afterwards. No thread is
//! created, so `pthread_self`, `[NSThread isMainThread]` and
//! `CFRunLoopGetMain() == CFRunLoopGetCurrent()` are all exactly as they were —
//! every one of them is a property of the thread, not of the stack it is
//! standing on.
//!
//! # What it costs
//!
//! * Two short pieces of architecture-specific assembly, one per calling
//! convention this crate targets. On any other architecture there is no
//! switch and [`run`] says so and exits rather than running the interpreter
//! on a stack that cannot hold its own recursion limit.
//! * Rust's stack-overflow handler recognizes a fault in the guard page of a
//! thread *it* created, and this stack is not one of those. A runaway on the
//! borrowed stack still stops at a guard page — [`Stack`] maps one — but it
//! is reported as a segmentation fault rather than as `fatal runtime error:
//! stack overflow`. The recursion limit is what makes that unreachable, and
//! is exactly why it may not be lowered.
//! * A panic may not unwind across the switch, so the payload runs inside
//! `catch_unwind` and the failure is turned into an exit status on the
//! original stack.
//! * 256 MiB of address space, and no resident memory until it is used.
//!
//! None of it applies without `--tk`: that path is the same `std::thread`
//! spawn it has always been.
use c_void;
use ;
use ExitCode;
/// A mapped region to run on, with a guard page under it.
///
/// The guard is what turns a runaway recursion into a fault at a known address
/// instead of a silent write into whatever the allocator put below the stack.
/// It is the same protection `std::thread` gives a thread it creates, arranged
/// by hand because this stack has no thread of its own.
/// Call `f(arg)` with the stack pointer set to `top`, then restore it.
///
/// Naked because the whole body is the switch: any prologue the compiler
/// generated would address locals through a stack pointer this changes
/// underneath it.
///
/// The frame pointer is saved on the *old* stack and used to find the way back,
/// so the callee's frames chain into the caller's and a backtrace taken inside
/// `f` still walks out through `main`.
///
/// # Safety
/// `top` must be 16-byte aligned, must be the high end of a writable region
/// large enough for everything `f` will do, and that region must outlive the
/// call.
unsafe extern "C"
/// The x86-64 half of [`call_on_stack`]. See that function for the contract.
///
/// # Safety
/// As [`call_on_stack`].
unsafe extern "C"
/// What crosses the switch: the function to run and the status it produced.
/// The far side of the switch. Runs on the mapped stack.
///
/// A panic may not unwind past this frame — the unwinder would be walking a
/// stack the runtime does not know about — so it is caught here and reported as
/// a failing exit status once control is back on the original stack.
unsafe extern "C"
/// Run `f` on this thread, on a stack of [`tclrs::runtime::RECOMMENDED_STACK`]
/// bytes.
///
/// The thread is the caller's — which for a `--tk` run is the main thread, and
/// that is the point. See the module documentation for why the stack has to be
/// borrowed rather than the thread replaced.