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
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
//! The Tcl 9.0.4 C ABI, as far as Tk depends on it.
//!
//! Every layout here was taken from the Tcl 9.0.4 source that
//! `conformance/fetch-suite.sh` unpacks under `conformance/vendor/tcl9.0.4/`,
//! and every offset was confirmed by compiling `offsetof` against those same
//! headers rather than inferred from the field list. The constants that record
//! those measurements are asserted in `tests/tk_abi.rs`, so a wrong guess here
//! is a test failure and not a segfault at run time.
//!
//! Two things about this ABI are not visible in the header's function list and
//! shape everything else:
//!
//! 1. Tk reaches Tcl *only* through a table of function pointers. The dylib has
//! no undefined `Tcl_*` symbols at all; `Tcl_InitStubs` is statically linked
//! into Tk from `libtclstub.a` and pulls the table out of the interpreter it
//! is handed (`generic/tclStubLib.c:59-62`). Slot order is therefore the
//! entire contract.
//!
//! 2. Three of the operations Tk performs on a `Tcl_Obj` are not table calls at
//! all. `Tcl_IncrRefCount`, `Tcl_DecrRefCount` and `Tcl_IsShared` are macros
//! that read and write `objPtr->refCount` directly (`generic/tcl.h:2517-2534`),
//! so a value handed to Tk has to be writable memory in this exact shape.
//! See [`TclObj`].
use ;
/// A slot in a stub table, with its arguments and return value erased.
///
/// The tables are arrays of function pointers of many different signatures. A
/// single erased type is what lets them be built and patched uniformly; each
/// implementation is transmuted back to its real signature at the point of
/// installation, next to the header line that declares it.
pub type RawStub = unsafe extern "C" fn;
/// `TCL_STUB_MAGIC` for a 64-bit Tcl 9 (`generic/tcl.h:2309-2310`:
/// `((int) 0xFCA3BACB + (int) sizeof(void *))`).
///
/// `Tcl_InitStubs` rejects the interpreter outright if the table's `magic` is
/// anything else (`generic/tclStubLib.c:75`).
pub const TCL_STUB_MAGIC: c_int =
0xFCA3_BACB_u32 as c_int + as c_int;
/// `TCL_OK` (`generic/tcl.h`).
pub const TCL_OK: c_int = 0;
/// `TCL_ERROR` (`generic/tcl.h`).
pub const TCL_ERROR: c_int = 1;
/// Number of slots in `TclStubs` on this platform.
pub const TCL_STUBS_SLOTS: usize = TCL_NAMES.len;
/// Number of slots in `TclIntStubs`.
pub const TCL_INT_STUBS_SLOTS: usize = TCL_INT_NAMES.len;
/// Number of slots in `TclPlatStubs` on macOS.
pub const TCL_PLAT_STUBS_SLOTS: usize = TCL_PLAT_NAMES.len;
/// Number of slots in `TclIntPlatStubs` on macOS.
pub const TCL_INT_PLAT_STUBS_SLOTS: usize = TCL_INT_PLAT_NAMES.len;
/// `TclStubHooks` (`generic/tclDecls.h:1881-1885`): the three secondary tables,
/// which `Tcl_InitStubs` copies into the extension's globals
/// (`generic/tclStubLib.c:143-146`).
/// `TclStubs` (`generic/tclDecls.h:1887-2582`).
///
/// Measured: `sizeof` 5544, `magic` at 0, `hooks` at 8, slot 0 at 16, so
/// `(5544 - 16) / 8 = 691` slots.
/// `TclIntStubs` (`generic/tclIntDecls.h:580`), same `magic`/`hooks` prefix.
/// `TclPlatStubs` (`generic/tclPlatDecls.h:162`), the `MAC_OSX_TCL` arm.
/// `TclIntPlatStubs` (`generic/tclIntPlatDecls.h:589`), the `MAC_OSX_TCL` arm.
/// The head of Tcl's private `Interp` struct (`generic/tclInt.h:1990-2015`).
///
/// Tk itself never sees this: no Tk source file includes `tclInt.h`, and to Tk
/// a `Tcl_Interp *` is opaque. The one piece of code that does look inside is
/// `Tcl_InitStubs`, which Tk links statically out of `libtclstub.a` and which
/// starts with `Interp *iPtr = (Interp *)interp; ... iPtr->stubTable`
/// (`generic/tclStubLib.c:59-62`). It reads `stubTable`, and on the failure
/// path writes `legacyResult` and `legacyFreeProc`
/// (`generic/tclStubLib.c:95-96`) — offsets 24, 0 and 8.
///
/// The comment above the struct says outright that the position of `stubTable`
/// is frozen for exactly this reason (`generic/tclInt.h:1991-1999`), which is
/// what makes reproducing only the first 32 bytes safe rather than a bet on
/// the current release.
///
/// Measured against the 9.0.4 headers: `legacyResult` 0, `legacyFreeProc` 8,
/// `errorLine` 16, `stubTable` 24. (Real `Interp` is 1104 bytes; the rest is
/// Tcl's own state and is not reproduced.)
/// `Tcl_Obj` (`generic/tcl.h:744-765`).
///
/// Measured: `sizeof` 48; `refCount` 0, `bytes` 8, `length` 16, `typePtr` 24,
/// `internalRep` 32 (16 bytes, a union — `generic/tcl.h:716-736`).
///
/// This layout is not negotiable the way the stub table's is, because Tk does
/// not go through the table to touch it:
///
/// * `Tcl_IncrRefCount(objPtr)` expands to `((void)++(objPtr)->refCount)`
/// (`generic/tcl.h:2517-2519`) — a direct increment of offset 0.
/// * `Tcl_DecrRefCount(objPtr)` expands to a direct decrement, and only calls
/// the table (slot 30, `TclFreeObj`) once the count has already been written
/// back (`generic/tcl.h:2524-2531`).
/// * `Tcl_IsShared(objPtr)` expands to `((objPtr)->refCount > 1)`
/// (`generic/tcl.h:2532-2534`) — a direct read.
/// * Tk's own `Tcl_ObjType` implementations in `generic/tkObj.c` and ten other
/// files read and write `objPtr->typePtr` and `objPtr->internalRep` in place.
///
/// `Tcl_Size` is `ptrdiff_t` in Tcl 9 (`generic/tcl.h:332`), hence `isize`.
/// `Tcl_ObjInternalRep` (`generic/tcl.h:716-736`): a union of 16 bytes, the
/// widest arm being the two-pointer one. Tk stores its own reps here.
/// `Tcl_ObjType` (`generic/tcl.h:657-698`), the Tcl 9 shape with the abstract
/// list slots. Measured `sizeof` 112. Only ever handled by pointer here: Tk
/// registers ten of its own with `Tcl_RegisterObjType`
/// (`tk9.0.4/generic/tkObj.c:1223-1232`) and this side just has to keep them.
/// A `Tcl_ObjType` is a table of code pointers and a name, written once and
/// read for the life of the process — Tcl's own are `static const` for exactly
/// that reason (`generic/tclObj.c`). Nothing mutates one, so sharing it across
/// threads is sound; the raw pointers inside are what makes the compiler ask.
unsafe
/// `Tcl_CmdInfo` (`generic/tcl.h:847-870`). Measured `sizeof` 80;
/// `isNativeObjectProc` 0, `objProc` 8, `objClientData` 16, `proc` 24,
/// `clientData` 32, `deleteProc` 40, `deleteData` 48, `namespacePtr` 56,
/// `objProc2` 64, `objClientData2` 72.
///
/// Caller-allocated: Tk declares one on its stack and hands over a pointer for
/// `Tcl_GetCommandInfo` to fill (`tk9.0.4/generic/tkWindow.c:961`), then reads
/// four of the fields back.
/// `Tcl_Namespace` (`generic/tcl.h:774-790`), the public view of a namespace.
///
/// The header notes that its first five fields must match Tcl's private
/// `Namespace` exactly (`generic/tcl.h:769-771`), which is what makes the
/// public struct safe to hand out. Tk asks for `::tk` and `::tk::mac` during
/// initialisation and only ever checks the result against NULL
/// (`tk9.0.4/generic/tkWindow.c:904`, `tk9.0.4/macosx/tkMacOSXDraw.c:85`).
/// `Tcl_Time` (`generic/tcl.h:1320-1331`). Measured `sizeof` 16, `sec` 0
/// (`long long` in Tcl 9), `usec` 8 (`long`).
/// `Tcl_ChannelType` (`generic/tcl.h:1445-1494`), the third caller-supplied
/// struct after `Tcl_HashTable` and `Tcl_ObjType` — and the one that puts the
/// call in the other direction.
///
/// Measured against the 9.0.4 headers: `sizeof` 136, seventeen fields at
/// eight-byte strides — `typeName` 0, `version` 8, `closeProc` 16, `inputProc`
/// 24, `outputProc` 32, `seekProc` 40, `setOptionProc` 48, `getOptionProc` 56,
/// `watchProc` 64, `getHandleProc` 72, `close2Proc` 80, `blockModeProc` 88,
/// `flushProc` 96, `handlerProc` 104, `wideSeekProc` 112, `threadActionProc`
/// 120, `truncateProc` 128.
///
/// Everything else in this file describes memory Tk hands over for *this* side
/// to fill in or read. This one is the reverse: Tk defines one
/// (`tk9.0.4/generic/tkConsole.c:66-84`) and passes a pointer to
/// `Tcl_CreateChannel`, and the host then calls the procs inside it every time
/// the channel reads, writes or closes. Getting a field's position wrong here
/// is a call through whatever Tk happened to put at that offset.
///
/// Two of the fields are dead and must still be present, which is exactly why
/// the layout cannot be shortened to the useful ones: `closeProc` (16) and
/// `seekProc` (40) are `void *` marked "Not used any more"
/// (`generic/tcl.h:1451`, `:1456`) and Tk still stores a function pointer in
/// the first of them.
///
/// `version` is not a pointer despite its type: `TCL_CHANNEL_VERSION_5` is the
/// integer 5 cast to `Tcl_ChannelTypeVersion` (`generic/tcl.h:1387`), and
/// `Tcl_CreateChannel` panics on any other value (`generic/tclIO.c:1612-1614`).
/// `TCL_CHANNEL_VERSION_5` (`generic/tcl.h:1387`), the only version
/// `Tcl_CreateChannel` accepts (`generic/tclIO.c:1612-1614`).
pub const TCL_CHANNEL_VERSION_5: usize = 5;
/// `TCL_SMALL_HASH_TABLE` (`generic/tcl.h`), measured as 4.
pub const TCL_SMALL_HASH_TABLE: usize = 4;
/// `TCL_STRING_KEYS` (`generic/tcl.h`), measured as 0.
pub const TCL_STRING_KEYS: c_int = 0;
/// `TCL_ONE_WORD_KEYS` (`generic/tcl.h`), measured as 1.
pub const TCL_ONE_WORD_KEYS: c_int = 1;
/// `Tcl_HashEntry` (`generic/tcl.h:1088-1104`). Measured `sizeof` 40;
/// `nextPtr` 0, `tablePtr` 8, `hash` 16, `clientData` 24, `key` 32.
///
/// `key` is a union whose last arm is a flexible `char string[1]`, so a
/// string-keyed entry is allocated larger than `sizeof` — the header's own
/// comment is "MUST BE LAST FIELD IN RECORD".
///
/// The fields are not private in practice. `Tcl_GetHashValue` and
/// `Tcl_SetHashValue` are macros over `clientData` (`generic/tcl.h:2594-2595`)
/// and `Tcl_GetHashKey` reads `key` and the table's `keyType`
/// (`generic/tcl.h:2596-2600`), all without touching the stub table.
/// `Tcl_HashTable` (`generic/tcl.h:1182-1215`). Measured `sizeof` 104;
/// `buckets` 0, `staticBuckets` 8, `numBuckets` 40, `numEntries` 48,
/// `rebuildSize` 56, `mask` 64, `downShift` 72, `keyType` 76, `findProc` 80,
/// `createProc` 88, `typePtr` 96.
///
/// This one is the sharpest case of ABI that is not in the stub table.
/// `Tcl_FindHashEntry` and `Tcl_CreateHashEntry` are macros that call
/// `tablePtr->findProc` and `tablePtr->createProc` (`generic/tcl.h:2607-2610`),
/// which live *inside the caller's own memory*. Tk declares its tables inline
/// in its own structs — `Tcl_InitHashTable(&mainPtr->nameTable,
/// TCL_STRING_KEYS)` (`tk9.0.4/generic/tkWindow.c:887`) — so a host has to fill
/// in a struct of exactly this shape, including two working function pointers,
/// and every subsequent lookup bypasses the stub table entirely.
/// `Tcl_HashSearch` (`generic/tcl.h:1222-1228`). Measured `sizeof` 24.
/// `TCL_DSTRING_STATIC_SIZE` (`generic/tcl.h:879`).
pub const TCL_DSTRING_STATIC_SIZE: usize = 200;
/// `Tcl_DString` (`generic/tcl.h:880-890`). Measured `sizeof` 224; `string` 0,
/// `length` 8, `spaceAvl` 16, `staticSpace` 24.
///
/// Like `Tcl_Obj`, this one is partly bypassed: `Tcl_DStringValue` and
/// `Tcl_DStringLength` are macros over the fields (`generic/tcl.h:892-893`),
/// so the layout has to be right even though every mutation goes through the
/// table. Tk declares these on its own stack — `Tcl_DString nameDS;` in
/// `Initialize` (`tk9.0.4/generic/tkWindow.c:3352`) — and hands over a pointer,
/// so the memory is Tk's and only the shape is shared.
/// `Tcl_ArgvInfo` (`generic/tcl.h:2178-2189`), one entry of the option table a
/// caller hands `Tcl_ParseArgsObjv`.
///
/// The table is Tk's own storage — a `const Tcl_ArgvInfo table[]` on
/// `Initialize`'s stack (`tk9.0.4/generic/tkWindow.c:3196-3212`) — so only the
/// shape is shared, as with [`TclDString`]. It ends at the entry whose `type_`
/// is [`TCL_ARGV_END`]; there is no count.
/// `Tcl_ArgvFuncProc` (`generic/tcl.h:2211-2212`). A non-zero answer means the
/// handler consumed the object it was given.
pub type ArgvFuncProc = unsafe extern "C" fn ;
/// `Tcl_ArgvGenFuncProc` (`generic/tcl.h:2213-2214`). The answer is how many
/// arguments were consumed, or negative for a failure.
pub type ArgvGenFuncProc =
unsafe extern "C" fn ;
// The option types (`generic/tcl.h:2196-2204`).
pub const TCL_ARGV_CONSTANT: c_int = 15;
pub const TCL_ARGV_INT: c_int = 16;
pub const TCL_ARGV_STRING: c_int = 17;
pub const TCL_ARGV_REST: c_int = 18;
pub const TCL_ARGV_FLOAT: c_int = 19;
pub const TCL_ARGV_FUNC: c_int = 20;
pub const TCL_ARGV_GENFUNC: c_int = 21;
pub const TCL_ARGV_HELP: c_int = 22;
pub const TCL_ARGV_END: c_int = 23;