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
//! A Tk session in the product binary: what `tclrs --tk` opens, what `package
//! require Tk` does inside it, and the loop the application sits in.
//!
//! Everything here already existed as the `tk-host` probe binary
//! (`src/bin/tk_host.rs`), which builds the hosting table, `dlopen`s libtk,
//! calls `Tk_Init` and then evaluates whatever scripts it was given. This
//! module is that sequence taken apart so the *script* drives it: the binary
//! opens a session, the script says `package require Tk`, and the toolkit is
//! loaded at that point and not before.
//!
//! # Why the session is opened before the script is compiled
//!
//! tclrs resolves a command name while compiling
//! ([`crate::tk::dispatch`]), and it lowers an unknown name as a run-time
//! lookup only when a Tk interpreter already exists in the process
//! ([`crate::tk::dispatch::may_exist`]). A script is compiled whole and then
//! run, so a `package require Tk` on its first line happens long after
//! `button .b` on its second was lowered. If the host were built by `package
//! require`, that `button` would already be a deferred `invalid command name`
//! and no amount of loading afterwards could reach it.
//!
//! So [`open`] builds the host — the stub tables, the object types, the
//! interpreter pairing — and loads nothing. That is what makes `may_exist`
//! true for the compilation that follows. `dlopen` and `Tk_Init` wait for
//! [`load_tk`]. A `--tk` run of a script that never mentions Tk therefore
//! never opens the Tk dylib, and never reaches the window server.
//!
//! # Why the session's interpreter is the script's own
//!
//! [`crate::tk::interp::shared_for`] would otherwise make the host a fresh
//! interpreter of its own, and every callback Tk evaluated — `-command`,
//! `bind`, `after` — would run against variables and procedures the script
//! could not see. [`open`] hands it the interpreter the script is running in
//! instead, so a `-command` body is evaluated in the same interpreter that
//! registered it.
//!
//! # Why the main thread is a precondition
//!
//! `Tk_MacOSXSetupTkNotifier` installs the Aqua event source only when the
//! current run loop is the process's main run loop, and panics outright if
//! that holds on a thread AppKit does not consider the main one
//! (`tk9.0.4/macosx/tkMacOSXNotify.c:258-272`). `tclrs --tk` runs the
//! interpreter on the main thread for exactly that reason — see
//! `src/main_thread.rs` — and [`load_tk`] refuses rather than letting Tk
//! panic when something else has got here.
use c_void;
use ;
use Mutex;
use ;
/// Whether [`open`] has run.
///
/// `package require Tk` outside a session is `can't find package Tk`, which is
/// what `tclsh` says for a package it cannot locate. Loading anyway would put
/// Tk on whichever thread the interpreter happened to be spawned on.
static OPEN: AtomicBool = new;
/// Whether [`load_tk`] has already run to completion, so a second `package
/// require Tk` does not `dlopen` and initialise twice.
static LOADED: AtomicBool = new;
/// What `Tk_Init` returned, and what it left as the interpreter result.
///
/// Kept because the two are a measurement rather than a diagnostic: the host
/// carries `Tk_Init` past every stub slot it asks for, and whether the last
/// statement of `tkInit` also succeeds is a property of how much of the Tcl
/// language this frontend has, not of the ABI. Nothing prints them; the tests
/// read them.
static INIT_CODE: AtomicI32 = new;
static INIT_RESULT: = new;
/// Open a Tk session on this thread, against `interp`.
///
/// Builds the host and pairs it with the interpreter the script will run in.
/// Loads nothing: see the module documentation for why those are two steps.
///
/// `startup` is the script file this process was started with, if it was
/// started with one. `Tcl_MainEx` records the same thing from `argv`
/// (`generic/tclMain.c:336-338`), and Tk reads it back to decide whether it is
/// running under an interactive shell that wants a console window
/// (`tk9.0.4/macosx/tkMacOSXInit.c:585`).
/// Whether a session is open on this process.
/// `package require Tk`, from [`crate::cmd_package`].
///
/// `dlopen` the toolkit, hand it the session's interpreter and call `Tk_Init`.
/// Tk registers its commands into that interpreter as it goes
/// (`tk9.0.4/generic/tkWindow.c:1004-1096`) and provides itself as `Tk` and
/// `tk` (`:3461-3469`) — through this crate's `Tcl_PkgProvideEx`, into the
/// registry `package require` is asking about, which is what makes the answer
/// come out of the ordinary lookup rather than out of a special case here.
///
/// # What a `TCL_ERROR` from `Tk_Init` means, and why it is not the answer
///
/// `Tk_Init` returns the completion code of its *last statement*, which
/// evaluates `tkInit` (`tk9.0.4/generic/tkWindow.c:3508-3518`) — long after the
/// two provides. So the toolkit can be initialised, the main window created and
/// the commands registered, and the return code still be `TCL_ERROR` because
/// the trailing script used a piece of the Tcl language this frontend does not
/// have yet. What decides whether the package is there is whether it was
/// provided, and that is what this reports: an error only when `Tk_Init`
/// failed *and* left no `Tk` behind.
/// What `Tk_Init` returned and left behind, or `None` if it has not been
/// called in this process.
/// `Tcl_MainLoopProc *`, as `Tcl_SetMainLoop` takes one
/// (`generic/tcl.h:643`).
type MainLoopProc = unsafe extern "C" fn;
/// Sit in Tk's own main loop until the application's last window is gone.
///
/// This is not a loop written here. Tk registers `Tk_MainLoop` with
/// `Tcl_SetMainLoop` as it finishes initialising
/// (`tk9.0.4/generic/tkWindow.c:3477`), the host records the pointer
/// ([`notifier::main_loop_proc`]), and this calls it. `Tk_MainLoop` is
/// `while (Tk_GetNumMainWindows() > 0) Tcl_DoOneEvent(0);`
/// (`tk9.0.4/generic/tkEvent.c`), and the `Tcl_DoOneEvent` it calls is this
/// crate's ported notifier through the stub table — so the events it services
/// are the ones [`notifier`] queued.
///
/// Returns immediately when Tk was never loaded, which is what a `--tk` run of
/// a script that does not mention Tk does.