teek 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +139 -0
  5. data/Rakefile +316 -0
  6. data/ext/teek/extconf.rb +79 -0
  7. data/ext/teek/stubs.h +33 -0
  8. data/ext/teek/tcl9compat.h +211 -0
  9. data/ext/teek/tcltkbridge.c +1597 -0
  10. data/ext/teek/tcltkbridge.h +42 -0
  11. data/ext/teek/tkfont.c +218 -0
  12. data/ext/teek/tkphoto.c +477 -0
  13. data/ext/teek/tkwin.c +144 -0
  14. data/lib/teek/background_none.rb +158 -0
  15. data/lib/teek/background_ractor4x.rb +410 -0
  16. data/lib/teek/background_thread.rb +272 -0
  17. data/lib/teek/debugger.rb +742 -0
  18. data/lib/teek/demo_support.rb +150 -0
  19. data/lib/teek/ractor_support.rb +246 -0
  20. data/lib/teek/version.rb +5 -0
  21. data/lib/teek.rb +540 -0
  22. data/sample/calculator.rb +260 -0
  23. data/sample/debug_demo.rb +45 -0
  24. data/sample/goldberg.rb +1803 -0
  25. data/sample/goldberg_helpers.rb +170 -0
  26. data/sample/minesweeper/assets/MINESWEEPER_0.png +0 -0
  27. data/sample/minesweeper/assets/MINESWEEPER_1.png +0 -0
  28. data/sample/minesweeper/assets/MINESWEEPER_2.png +0 -0
  29. data/sample/minesweeper/assets/MINESWEEPER_3.png +0 -0
  30. data/sample/minesweeper/assets/MINESWEEPER_4.png +0 -0
  31. data/sample/minesweeper/assets/MINESWEEPER_5.png +0 -0
  32. data/sample/minesweeper/assets/MINESWEEPER_6.png +0 -0
  33. data/sample/minesweeper/assets/MINESWEEPER_7.png +0 -0
  34. data/sample/minesweeper/assets/MINESWEEPER_8.png +0 -0
  35. data/sample/minesweeper/assets/MINESWEEPER_F.png +0 -0
  36. data/sample/minesweeper/assets/MINESWEEPER_M.png +0 -0
  37. data/sample/minesweeper/assets/MINESWEEPER_X.png +0 -0
  38. data/sample/minesweeper/minesweeper.rb +452 -0
  39. data/sample/threading_demo.rb +499 -0
  40. data/teek.gemspec +32 -0
  41. metadata +179 -0
@@ -0,0 +1,1597 @@
1
+ /*
2
+ * tcltkbridge.c - Minimal Ruby/Tcl/Tk bridge
3
+ *
4
+ * Design goals:
5
+ * - Thin C layer, logic in Ruby
6
+ * - Clear method names (tcl_eval, tcl_invoke, etc.)
7
+ * - Modern Ruby (3.2+), Tcl/Tk (8.6+)
8
+ * - Always use stubs for version flexibility
9
+ */
10
+
11
+ /* Stubs are enabled via extconf.rb (-DUSE_TCL_STUBS -DUSE_TK_STUBS) */
12
+
13
+ #include "ruby.h"
14
+ #include "ruby/encoding.h"
15
+ #include "ruby/thread.h"
16
+ #include <tcl.h>
17
+ #include <tk.h>
18
+ #include <string.h>
19
+
20
+ #ifdef _WIN32
21
+ #include <windows.h>
22
+ #else
23
+ #include <dlfcn.h>
24
+ #endif
25
+
26
+ /* Tcl 8.x/9.x compatibility (Tcl_Size, etc.) */
27
+ #include "tcl9compat.h"
28
+
29
+ /* Shared types and function declarations */
30
+ #include "tcltkbridge.h"
31
+
32
+ /*
33
+ * Bootstrap helpers: call Tcl functions before stubs are initialized.
34
+ *
35
+ * Tcl 9.0 pre-initializes tclStubsPtr, but Tcl 8.6 does not.
36
+ * When tclStubsPtr is NULL, Tcl_CreateInterp() and Tcl_FindExecutable()
37
+ * crash because they're macros that dereference tclStubsPtr.
38
+ * We use dlsym/GetProcAddress to get the real function pointers and call them directly.
39
+ */
40
+
41
+ #ifdef _WIN32
42
+ /* Windows: Get function from loaded Tcl DLL */
43
+ static void *get_tcl_proc(const char *name)
44
+ {
45
+ /* Try common Tcl DLL names */
46
+ static const char *dll_names[] = {"tcl86.dll", "tcl90.dll", "tcl86t.dll", "tcl90t.dll", NULL};
47
+ HMODULE hmod;
48
+ void *proc;
49
+ int i;
50
+
51
+ for (i = 0; dll_names[i]; i++) {
52
+ hmod = GetModuleHandleA(dll_names[i]);
53
+ if (hmod) {
54
+ proc = (void *)GetProcAddress(hmod, name);
55
+ if (proc) return proc;
56
+ }
57
+ }
58
+ return NULL;
59
+ }
60
+ #endif
61
+
62
+ static void
63
+ find_executable_bootstrap(const char *argv0)
64
+ {
65
+ if (tclStubsPtr != NULL) {
66
+ Tcl_FindExecutable(argv0);
67
+ return;
68
+ }
69
+
70
+ void (*real_find_executable)(const char *);
71
+ #ifdef _WIN32
72
+ real_find_executable = (void (*)(const char *))get_tcl_proc("Tcl_FindExecutable");
73
+ #else
74
+ real_find_executable = dlsym(RTLD_DEFAULT, "Tcl_FindExecutable");
75
+ #endif
76
+ if (real_find_executable) {
77
+ real_find_executable(argv0);
78
+ }
79
+ }
80
+
81
+ static Tcl_Interp *
82
+ create_interp_bootstrap(void)
83
+ {
84
+ if (tclStubsPtr != NULL) {
85
+ return Tcl_CreateInterp();
86
+ }
87
+
88
+ Tcl_Interp *(*real_create_interp)(void);
89
+ #ifdef _WIN32
90
+ real_create_interp = (Tcl_Interp *(*)(void))get_tcl_proc("Tcl_CreateInterp");
91
+ #else
92
+ real_create_interp = dlsym(RTLD_DEFAULT, "Tcl_CreateInterp");
93
+ #endif
94
+ if (!real_create_interp) {
95
+ return NULL;
96
+ }
97
+ return real_create_interp();
98
+ }
99
+
100
+ /*
101
+ * Version strings for Tcl_InitStubs/Tk_InitStubs.
102
+ * Must match the major version we compiled against - Tcl's version
103
+ * satisfaction requires same major number (9.x won't satisfy "8.6").
104
+ * TCL_VERSION/TK_VERSION are defined in tcl.h/tk.h at compile time.
105
+ */
106
+
107
+ /* Module and class handles */
108
+ static VALUE mTeek;
109
+ static VALUE cInterp;
110
+ VALUE eTclError; /* Non-static: shared with tkphoto.c */
111
+
112
+ /* Track if stubs have been initialized (once per process) */
113
+ static int tcl_stubs_initialized = 0;
114
+
115
+ /* Lightweight Tcl interp for utility functions (split_list, make_list).
116
+ * Created at module load time to initialize stubs and provide error
117
+ * reporting without requiring the user to create a Teek::Interp first.
118
+ * No Tk loaded — just bare Tcl. */
119
+ static Tcl_Interp *utility_interp = NULL;
120
+
121
+ /* Track live interpreter instances for multi-interp safety checks */
122
+ static VALUE live_instances; /* Ruby Array of live Teek::Interp objects */
123
+
124
+ /* Forward declaration for Tcl callback command */
125
+ static int ruby_callback_proc(ClientData, Tcl_Interp *, int, Tcl_Obj *const *);
126
+ static int ruby_eval_proc(ClientData, Tcl_Interp *, int, Tcl_Obj *const *);
127
+ static void interp_deleted_callback(ClientData, Tcl_Interp *);
128
+
129
+ /* Default timer interval for thread-aware mainloop (ms) */
130
+ /* 16ms ≈ 60fps - balances UI responsiveness with scheduler contention */
131
+ #define DEFAULT_TIMER_INTERVAL_MS 16
132
+
133
+ /* Global timer interval for TclTkLib.mainloop (mutable) */
134
+ static int g_thread_timer_ms = DEFAULT_TIMER_INTERVAL_MS;
135
+
136
+ /* struct tcltk_interp is defined in tcltkbridge.h */
137
+
138
+ /* ---------------------------------------------------------
139
+ * Thread-safe event for cross-thread execution
140
+ *
141
+ * Background threads cannot safely call Tcl/Tk directly.
142
+ * Uses Tcl's native Tcl_ThreadQueueEvent mechanism.
143
+ *
144
+ * Design: Command data is stored in Ruby objects (GC-protected in
145
+ * thread_queue). The Tcl event just triggers execution.
146
+ * --------------------------------------------------------- */
147
+
148
+ struct ruby_thread_event {
149
+ Tcl_Event event; /* Must be first - Tcl casts to this */
150
+ struct tcltk_interp *tip; /* Interpreter context */
151
+ };
152
+
153
+ /* Ruby Queue class for thread synchronization */
154
+ static VALUE cQueue = Qundef;
155
+
156
+ /* Track callback depth for unsafe operation detection */
157
+ static int rbtk_callback_depth = 0;
158
+
159
+ /* Callback control flow exceptions - for signaling break/continue/return to Tcl */
160
+
161
+ /* ---------------------------------------------------------
162
+ * Memory management
163
+ * --------------------------------------------------------- */
164
+
165
+ static void
166
+ interp_mark(void *ptr)
167
+ {
168
+ struct tcltk_interp *tip = ptr;
169
+ rb_gc_mark(tip->callbacks); /* Mark callback procs so GC doesn't collect them */
170
+ rb_gc_mark(tip->thread_queue); /* Mark procs queued from other threads */
171
+ }
172
+
173
+ static void
174
+ interp_free(void *ptr)
175
+ {
176
+ struct tcltk_interp *tip = ptr;
177
+ if (tip->interp && !tip->deleted) {
178
+ Tcl_DeleteInterp(tip->interp);
179
+ }
180
+ xfree(tip);
181
+ }
182
+
183
+ /* ---------------------------------------------------------
184
+ * Callback invoked by Tcl when an interpreter is deleted
185
+ *
186
+ * This is registered via Tcl_CallWhenDeleted so that when Tcl
187
+ * internally deletes an interpreter (e.g., via `interp delete`),
188
+ * we update our Ruby-side state to reflect the deletion.
189
+ * Without this, the Ruby object would think the interp is still
190
+ * valid and using it would crash.
191
+ * --------------------------------------------------------- */
192
+ static void
193
+ interp_deleted_callback(ClientData clientData, Tcl_Interp *interp)
194
+ {
195
+ struct tcltk_interp *tip = (struct tcltk_interp *)clientData;
196
+ tip->deleted = 1;
197
+ tip->interp = NULL; /* Don't hold stale pointer */
198
+ }
199
+
200
+ static size_t
201
+ interp_memsize(const void *ptr)
202
+ {
203
+ return sizeof(struct tcltk_interp);
204
+ }
205
+
206
+ /* Non-static: shared with tkphoto.c */
207
+ const rb_data_type_t interp_type = {
208
+ .wrap_struct_name = "TclTkBridge::Interp",
209
+ .function = {
210
+ .dmark = interp_mark,
211
+ .dfree = interp_free,
212
+ .dsize = interp_memsize,
213
+ },
214
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
215
+ };
216
+
217
+ static VALUE
218
+ interp_alloc(VALUE klass)
219
+ {
220
+ struct tcltk_interp *tip;
221
+ VALUE obj = TypedData_Make_Struct(klass, struct tcltk_interp, &interp_type, tip);
222
+ tip->interp = NULL;
223
+ tip->deleted = 0;
224
+ tip->callbacks = rb_hash_new();
225
+ tip->thread_queue = rb_ary_new();
226
+ tip->next_id = 1;
227
+ tip->timer_interval_ms = DEFAULT_TIMER_INTERVAL_MS;
228
+ tip->main_thread_id = NULL;
229
+ return obj;
230
+ }
231
+
232
+ /* Non-static: shared with tkphoto.c */
233
+ struct tcltk_interp *
234
+ get_interp(VALUE self)
235
+ {
236
+ struct tcltk_interp *tip;
237
+ TypedData_Get_Struct(self, struct tcltk_interp, &interp_type, tip);
238
+ if (tip->deleted || tip->interp == NULL) {
239
+ rb_raise(eTclError, "interpreter has been deleted");
240
+ }
241
+ return tip;
242
+ }
243
+
244
+ /* ---------------------------------------------------------
245
+ * Interp#initialize(name=nil, opts={}) - Create Tcl interp and load Tk
246
+ *
247
+ * Arguments:
248
+ * name - Ignored (legacy compatibility)
249
+ * opts - Options hash
250
+ *
251
+ * Options:
252
+ * :thread_timer_ms - Timer interval for thread-aware mainloop (default: 5)
253
+ * Controls how often Ruby threads get a chance to run
254
+ * during Tk.mainloop.
255
+ *
256
+ * Tradeoffs:
257
+ * - 1ms: Very responsive threads, higher CPU when idle
258
+ * - 5ms: Good balance (default)
259
+ * - 10ms: Lower CPU, slight thread latency
260
+ * - 20ms: Minimal CPU, noticeable latency for threads
261
+ * - 0: Disable timer (threads won't run during mainloop)
262
+ *
263
+ * Initialization order (verified empirically on Tcl/Tk 9.0.3):
264
+ * 1. Tcl_FindExecutable - sets up internal paths (NOT stubbed)
265
+ * 2. Tcl_CreateInterp - create interpreter (NOT stubbed)
266
+ * 3. Tcl_InitStubs - bootstrap stubs table
267
+ * 4. Set argc/argv/argv0 - Tk_Init reads these
268
+ * 5. Tcl_Init - load Tcl runtime
269
+ * 6. Tk_Init - load Tk runtime (NOT stubbed - must come BEFORE Tk_InitStubs!)
270
+ * 7. Tk_InitStubs - bootstrap Tk stubs table (AFTER Tk_Init)
271
+ *
272
+ * CRITICAL: Tk_Init before Tk_InitStubs. Tk_InitStubs internally calls
273
+ * Tk_Init if not already done, causing "window already exists" error
274
+ * if you then call Tk_Init yourself.
275
+ * --------------------------------------------------------- */
276
+
277
+ static VALUE
278
+ interp_initialize(int argc, VALUE *argv, VALUE self)
279
+ {
280
+ struct tcltk_interp *tip;
281
+ const char *tcl_version;
282
+ const char *tk_version;
283
+ VALUE name, opts, val;
284
+
285
+ TypedData_Get_Struct(self, struct tcltk_interp, &interp_type, tip);
286
+
287
+ /* Parse legacy (name, opts) or new (opts) argument forms */
288
+ rb_scan_args(argc, argv, "02", &name, &opts);
289
+ /* name is ignored - kept for legacy compatibility */
290
+
291
+ /* Check for options in opts hash */
292
+ if (!NIL_P(opts) && TYPE(opts) == T_HASH) {
293
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("thread_timer_ms")));
294
+ if (!NIL_P(val)) {
295
+ int ms = NUM2INT(val);
296
+ if (ms < 0) {
297
+ rb_raise(rb_eArgError, "thread_timer_ms must be >= 0 (got %d)", ms);
298
+ }
299
+ tip->timer_interval_ms = ms;
300
+ }
301
+ }
302
+
303
+ /* 1. Tell Tcl where to find itself (once per process) */
304
+ if (!tcl_stubs_initialized) {
305
+ find_executable_bootstrap("ruby");
306
+ }
307
+
308
+ /* 2. Create Tcl interpreter (using bootstrap to handle Tcl 8.6) */
309
+ tip->interp = create_interp_bootstrap();
310
+ if (tip->interp == NULL) {
311
+ rb_raise(eTclError, "failed to create Tcl interpreter");
312
+ }
313
+
314
+ /* 3. Initialize Tcl stubs - MUST be before any other Tcl calls */
315
+ tcl_version = Tcl_InitStubs(tip->interp, TCL_VERSION, 0);
316
+ if (tcl_version == NULL) {
317
+ const char *err = Tcl_GetStringResult(tip->interp);
318
+ Tcl_DeleteInterp(tip->interp);
319
+ tip->interp = NULL;
320
+ rb_raise(eTclError, "Tcl_InitStubs failed: %s", err);
321
+ }
322
+
323
+ /* 4. Set up argc/argv/argv0 before Tcl_Init (required for proper init) */
324
+ Tcl_Eval(tip->interp, "set argc 0; set argv {}; set argv0 tcltkbridge");
325
+
326
+ /* 5. Initialize Tcl runtime */
327
+ if (Tcl_Init(tip->interp) != TCL_OK) {
328
+ const char *err = Tcl_GetStringResult(tip->interp);
329
+ Tcl_DeleteInterp(tip->interp);
330
+ tip->interp = NULL;
331
+ rb_raise(eTclError, "Tcl_Init failed: %s", err);
332
+ }
333
+
334
+ /* 6. Initialize Tk runtime - must come BEFORE Tk_InitStubs */
335
+ if (Tk_Init(tip->interp) != TCL_OK) {
336
+ const char *err = Tcl_GetStringResult(tip->interp);
337
+ Tcl_DeleteInterp(tip->interp);
338
+ tip->interp = NULL;
339
+ rb_raise(eTclError, "Tk_Init failed: %s", err);
340
+ }
341
+
342
+ /* 7. Initialize Tk stubs - after Tk_Init */
343
+ tk_version = Tk_InitStubs(tip->interp, TK_VERSION, 0);
344
+ if (tk_version == NULL) {
345
+ const char *err = Tcl_GetStringResult(tip->interp);
346
+ Tcl_DeleteInterp(tip->interp);
347
+ tip->interp = NULL;
348
+ rb_raise(eTclError, "Tk_InitStubs failed: %s", err);
349
+ }
350
+
351
+ tcl_stubs_initialized = 1;
352
+
353
+ /* 8. Register Tcl commands for Ruby integration */
354
+ Tcl_CreateObjCommand(tip->interp, "ruby_callback",
355
+ ruby_callback_proc, (ClientData)tip, NULL);
356
+ Tcl_CreateObjCommand(tip->interp, "ruby",
357
+ ruby_eval_proc, (ClientData)tip, NULL);
358
+ Tcl_CreateObjCommand(tip->interp, "ruby_eval",
359
+ ruby_eval_proc, (ClientData)tip, NULL);
360
+
361
+ /* 9. Register callback for when Tcl deletes this interpreter */
362
+ Tcl_CallWhenDeleted(tip->interp, interp_deleted_callback, (ClientData)tip);
363
+
364
+ /* 10. Track this instance for multi-interp safety checks */
365
+ rb_ary_push(live_instances, self);
366
+
367
+ /* 11. Store the main thread ID for cross-thread event queuing */
368
+ tip->main_thread_id = Tcl_GetCurrentThread();
369
+
370
+ return self;
371
+ }
372
+
373
+ /* ---------------------------------------------------------
374
+ * ruby_callback - Tcl command that invokes Ruby procs
375
+ *
376
+ * Called from Tcl as: ruby_callback <id> ?args...?
377
+ * Looks up proc by ID and calls it with args.
378
+ * --------------------------------------------------------- */
379
+
380
+ /* Helper struct for rb_protect call */
381
+ struct callback_args {
382
+ VALUE proc;
383
+ VALUE args;
384
+ };
385
+
386
+ static VALUE
387
+ callback_invoke(VALUE varg)
388
+ {
389
+ struct callback_args *cargs = (struct callback_args *)varg;
390
+ return rb_proc_call(cargs->proc, cargs->args);
391
+ }
392
+
393
+ static int
394
+ ruby_callback_proc(ClientData clientData, Tcl_Interp *interp,
395
+ int objc, Tcl_Obj *const objv[])
396
+ {
397
+ struct tcltk_interp *tip = (struct tcltk_interp *)clientData;
398
+ VALUE id_str, proc, args, result;
399
+ struct callback_args cargs;
400
+ int i, state;
401
+
402
+ if (objc < 2) {
403
+ Tcl_SetResult(interp, "wrong # args: should be \"ruby_callback id ?args?\"",
404
+ TCL_STATIC);
405
+ return TCL_ERROR;
406
+ }
407
+
408
+ /* Look up proc by ID */
409
+ id_str = rb_utf8_str_new_cstr(Tcl_GetString(objv[1]));
410
+ proc = rb_hash_aref(tip->callbacks, id_str);
411
+
412
+ if (NIL_P(proc)) {
413
+ Tcl_SetObjResult(interp, Tcl_ObjPrintf("unknown callback id: %s",
414
+ Tcl_GetString(objv[1])));
415
+ return TCL_ERROR;
416
+ }
417
+
418
+ /* Build args array */
419
+ args = rb_ary_new2(objc - 2);
420
+ for (i = 2; i < objc; i++) {
421
+ Tcl_Size len;
422
+ const char *str = Tcl_GetStringFromObj(objv[i], &len);
423
+ rb_ary_push(args, rb_utf8_str_new(str, len));
424
+ }
425
+
426
+ /* Call the proc with exception protection */
427
+ cargs.proc = proc;
428
+ cargs.args = args;
429
+ rbtk_callback_depth++;
430
+ result = rb_protect(callback_invoke, (VALUE)&cargs, &state);
431
+ rbtk_callback_depth--;
432
+
433
+ if (state) {
434
+ VALUE errinfo = rb_errinfo();
435
+ rb_set_errinfo(Qnil);
436
+
437
+ /* Let SystemExit and Interrupt propagate - don't swallow them */
438
+ if (rb_obj_is_kind_of(errinfo, rb_eSystemExit) ||
439
+ rb_obj_is_kind_of(errinfo, rb_eInterrupt)) {
440
+ rb_exc_raise(errinfo);
441
+ }
442
+
443
+ /* Other exceptions: convert to Tcl error */
444
+ VALUE msg = rb_funcall(errinfo, rb_intern("message"), 0);
445
+ Tcl_SetResult(interp, StringValueCStr(msg), TCL_VOLATILE);
446
+ return TCL_ERROR;
447
+ }
448
+
449
+ /* Check return value for Tcl control flow signals.
450
+ * Callbacks wrapped by Teek::App#register_callback use catch/throw
451
+ * and return these symbols when the user throws :teek_break etc. */
452
+ if (result == ID2SYM(rb_intern("break"))) return TCL_BREAK;
453
+ if (result == ID2SYM(rb_intern("continue"))) return TCL_CONTINUE;
454
+ if (result == ID2SYM(rb_intern("return"))) return TCL_RETURN;
455
+
456
+ /* Return result to Tcl */
457
+ if (!NIL_P(result)) {
458
+ VALUE str = rb_String(result);
459
+ Tcl_SetResult(interp, StringValueCStr(str), TCL_VOLATILE);
460
+ }
461
+
462
+ return TCL_OK;
463
+ }
464
+
465
+ /* ---------------------------------------------------------
466
+ * ruby_eval_proc - Tcl command that evaluates Ruby code strings
467
+ *
468
+ * Called from Tcl as: ruby <ruby_code_string>
469
+ * Used by tcltk.rb's callback mechanism.
470
+ * --------------------------------------------------------- */
471
+
472
+ /* Helper for rb_protect */
473
+ static VALUE
474
+ eval_ruby_string(VALUE arg)
475
+ {
476
+ return rb_eval_string(StringValueCStr(arg));
477
+ }
478
+
479
+ static int
480
+ ruby_eval_proc(ClientData clientData, Tcl_Interp *interp,
481
+ int objc, Tcl_Obj *const objv[])
482
+ {
483
+ VALUE code_str, result;
484
+ int state;
485
+ const char *code;
486
+
487
+ if (objc != 2) {
488
+ Tcl_SetResult(interp, (char *)"wrong # args: should be \"ruby code\"",
489
+ TCL_STATIC);
490
+ return TCL_ERROR;
491
+ }
492
+
493
+ code = Tcl_GetString(objv[1]);
494
+ code_str = rb_utf8_str_new_cstr(code);
495
+
496
+ result = rb_protect(eval_ruby_string, code_str, &state);
497
+
498
+ if (state) {
499
+ VALUE errinfo = rb_errinfo();
500
+ rb_set_errinfo(Qnil);
501
+
502
+ /* Let SystemExit and Interrupt propagate */
503
+ if (rb_obj_is_kind_of(errinfo, rb_eSystemExit) ||
504
+ rb_obj_is_kind_of(errinfo, rb_eInterrupt)) {
505
+ rb_exc_raise(errinfo);
506
+ }
507
+
508
+ VALUE msg = rb_funcall(errinfo, rb_intern("message"), 0);
509
+ Tcl_SetResult(interp, StringValueCStr(msg), TCL_VOLATILE);
510
+ return TCL_ERROR;
511
+ }
512
+
513
+ if (!NIL_P(result)) {
514
+ VALUE str = rb_String(result);
515
+ Tcl_SetResult(interp, StringValueCStr(str), TCL_VOLATILE);
516
+ }
517
+
518
+ return TCL_OK;
519
+ }
520
+
521
+ /* ---------------------------------------------------------
522
+ * Interp#register_callback(proc) - Store proc, return ID
523
+ * --------------------------------------------------------- */
524
+
525
+ static VALUE
526
+ interp_register_callback(VALUE self, VALUE proc)
527
+ {
528
+ struct tcltk_interp *tip = get_interp(self);
529
+ char id_buf[32];
530
+ VALUE id_str;
531
+
532
+ snprintf(id_buf, sizeof(id_buf), "cb%lu", tip->next_id++);
533
+ id_str = rb_utf8_str_new_cstr(id_buf);
534
+
535
+ rb_hash_aset(tip->callbacks, id_str, proc);
536
+ return id_str;
537
+ }
538
+
539
+ /* ---------------------------------------------------------
540
+ * Interp#unregister_callback(id) - Remove proc by ID
541
+ * --------------------------------------------------------- */
542
+
543
+ static VALUE
544
+ interp_unregister_callback(VALUE self, VALUE id)
545
+ {
546
+ struct tcltk_interp *tip = get_interp(self);
547
+ rb_hash_delete(tip->callbacks, id);
548
+ return Qnil;
549
+ }
550
+
551
+ /* ---------------------------------------------------------
552
+ * Thread-safe event queue: run Ruby proc on main Tcl thread
553
+ *
554
+ * Background threads cannot safely call Tcl/Tk directly.
555
+ * This mechanism queues a proc to execute on the main thread.
556
+ * --------------------------------------------------------- */
557
+
558
+ /* Symbol IDs for queued command hash keys */
559
+ static ID sym_type, sym_proc, sym_script, sym_args, sym_queue;
560
+ static VALUE sym_eval, sym_invoke, sym_proc_val;
561
+
562
+ /* Execute a Tcl eval on behalf of a queued request */
563
+ static VALUE
564
+ execute_queued_eval(VALUE arg)
565
+ {
566
+ VALUE *args = (VALUE *)arg;
567
+ struct tcltk_interp *tip = (struct tcltk_interp *)args[0];
568
+ VALUE script = args[1];
569
+ const char *script_cstr = StringValueCStr(script);
570
+ int result = Tcl_Eval(tip->interp, script_cstr);
571
+
572
+ if (result != TCL_OK) {
573
+ rb_raise(eTclError, "%s", Tcl_GetStringResult(tip->interp));
574
+ }
575
+ return rb_utf8_str_new_cstr(Tcl_GetStringResult(tip->interp));
576
+ }
577
+
578
+ /* Execute a Tcl invoke on behalf of a queued request */
579
+ static VALUE
580
+ execute_queued_invoke(VALUE arg)
581
+ {
582
+ VALUE *args = (VALUE *)arg;
583
+ struct tcltk_interp *tip = (struct tcltk_interp *)args[0];
584
+ VALUE argv_ary = args[1];
585
+ int argc = (int)RARRAY_LEN(argv_ary);
586
+ Tcl_Obj **objv;
587
+ int i, result;
588
+
589
+ objv = ALLOCA_N(Tcl_Obj *, argc);
590
+ for (i = 0; i < argc; i++) {
591
+ VALUE arg = rb_ary_entry(argv_ary, i);
592
+ const char *str;
593
+ Tcl_Size len;
594
+
595
+ if (NIL_P(arg)) {
596
+ str = "";
597
+ len = 0;
598
+ } else {
599
+ StringValue(arg);
600
+ str = RSTRING_PTR(arg);
601
+ len = RSTRING_LEN(arg);
602
+ }
603
+ objv[i] = Tcl_NewStringObj(str, len);
604
+ Tcl_IncrRefCount(objv[i]);
605
+ }
606
+
607
+ result = Tcl_EvalObjv(tip->interp, argc, objv, 0);
608
+
609
+ for (i = 0; i < argc; i++) {
610
+ Tcl_DecrRefCount(objv[i]);
611
+ }
612
+
613
+ if (result != TCL_OK) {
614
+ rb_raise(eTclError, "%s", Tcl_GetStringResult(tip->interp));
615
+ }
616
+ return rb_utf8_str_new_cstr(Tcl_GetStringResult(tip->interp));
617
+ }
618
+
619
+ /* Execute a Ruby proc */
620
+ static VALUE
621
+ execute_queued_proc(VALUE proc)
622
+ {
623
+ return rb_proc_call(proc, rb_ary_new());
624
+ }
625
+
626
+ /* Tcl event callback - runs on main thread when event is processed */
627
+ static int
628
+ ruby_thread_event_handler(Tcl_Event *evPtr, int flags)
629
+ {
630
+ struct ruby_thread_event *rte = (struct ruby_thread_event *)evPtr;
631
+ VALUE cmd, type, queue, result, exception;
632
+ int state;
633
+ VALUE exec_args[2];
634
+
635
+ /* Pop the command from the GC-protected queue */
636
+ cmd = rb_ary_shift(rte->tip->thread_queue);
637
+ if (NIL_P(cmd)) return 1;
638
+
639
+ type = rb_hash_aref(cmd, ID2SYM(sym_type));
640
+ queue = rb_hash_aref(cmd, ID2SYM(sym_queue));
641
+ result = Qnil;
642
+ exception = Qnil;
643
+
644
+ exec_args[0] = (VALUE)rte->tip;
645
+
646
+ if (type == sym_eval) {
647
+ exec_args[1] = rb_hash_aref(cmd, ID2SYM(sym_script));
648
+ result = rb_protect(execute_queued_eval, (VALUE)exec_args, &state);
649
+ } else if (type == sym_invoke) {
650
+ exec_args[1] = rb_hash_aref(cmd, ID2SYM(sym_args));
651
+ result = rb_protect(execute_queued_invoke, (VALUE)exec_args, &state);
652
+ } else if (type == sym_proc_val) {
653
+ VALUE proc = rb_hash_aref(cmd, ID2SYM(sym_proc));
654
+ result = rb_protect(execute_queued_proc, proc, &state);
655
+ } else {
656
+ state = 0;
657
+ }
658
+
659
+ if (state) {
660
+ exception = rb_errinfo();
661
+ rb_set_errinfo(Qnil);
662
+
663
+ /* Let SystemExit and Interrupt propagate immediately */
664
+ if (rb_obj_is_kind_of(exception, rb_eSystemExit) ||
665
+ rb_obj_is_kind_of(exception, rb_eInterrupt)) {
666
+ rb_exc_raise(exception);
667
+ }
668
+ }
669
+
670
+ /* Send result back through the queue if one was provided */
671
+ if (!NIL_P(queue)) {
672
+ VALUE response = rb_ary_new3(2, result, exception);
673
+ rb_funcall(queue, rb_intern("push"), 1, response);
674
+ }
675
+
676
+ return 1; /* Event handled, Tcl will free the event struct */
677
+ }
678
+
679
+ /* Internal: Queue a command and optionally wait for result */
680
+ static VALUE
681
+ queue_command_internal(struct tcltk_interp *tip, VALUE cmd_hash, int wait_for_result)
682
+ {
683
+ struct ruby_thread_event *rte;
684
+ Tcl_ThreadId current_thread;
685
+ VALUE result_queue = Qnil;
686
+ VALUE response, result, exception;
687
+
688
+ if (wait_for_result) {
689
+ /* Create a Queue for receiving the result */
690
+ result_queue = rb_funcall(cQueue, rb_intern("new"), 0);
691
+ rb_hash_aset(cmd_hash, ID2SYM(sym_queue), result_queue);
692
+ }
693
+
694
+ /* Store command in GC-protected queue */
695
+ rb_ary_push(tip->thread_queue, cmd_hash);
696
+
697
+ /* Allocate event - Tcl takes ownership and will free it */
698
+ rte = (struct ruby_thread_event *)ckalloc(sizeof(struct ruby_thread_event));
699
+ rte->event.proc = ruby_thread_event_handler;
700
+ rte->tip = tip;
701
+
702
+ /* Queue to main thread and wake it up */
703
+ current_thread = Tcl_GetCurrentThread();
704
+ Tcl_ThreadQueueEvent(tip->main_thread_id, (Tcl_Event *)rte, TCL_QUEUE_TAIL);
705
+
706
+ if (current_thread != tip->main_thread_id) {
707
+ Tcl_ThreadAlert(tip->main_thread_id);
708
+ }
709
+
710
+ if (!wait_for_result) {
711
+ return Qnil;
712
+ }
713
+
714
+ /* Wait for result - this blocks until main thread processes the command */
715
+ response = rb_funcall(result_queue, rb_intern("pop"), 0);
716
+ result = rb_ary_entry(response, 0);
717
+ exception = rb_ary_entry(response, 1);
718
+
719
+ if (!NIL_P(exception)) {
720
+ rb_exc_raise(exception);
721
+ }
722
+
723
+ return result;
724
+ }
725
+
726
+ /* Queue a proc to run on the main Tcl thread (fire-and-forget) */
727
+ static VALUE
728
+ interp_queue_for_main(VALUE self, VALUE proc)
729
+ {
730
+ struct tcltk_interp *tip;
731
+ VALUE cmd_hash;
732
+
733
+ TypedData_Get_Struct(self, struct tcltk_interp, &interp_type, tip);
734
+
735
+ if (tip->deleted || tip->interp == NULL) {
736
+ rb_raise(eTclError, "interpreter has been deleted");
737
+ }
738
+
739
+ cmd_hash = rb_hash_new();
740
+ rb_hash_aset(cmd_hash, ID2SYM(sym_type), sym_proc_val);
741
+ rb_hash_aset(cmd_hash, ID2SYM(sym_proc), proc);
742
+
743
+ return queue_command_internal(tip, cmd_hash, 0);
744
+ }
745
+
746
+ /* Check if current thread is the main Tcl thread */
747
+ static VALUE
748
+ interp_on_main_thread_p(VALUE self)
749
+ {
750
+ struct tcltk_interp *tip = get_interp(self);
751
+ Tcl_ThreadId current = Tcl_GetCurrentThread();
752
+ return (current == tip->main_thread_id) ? Qtrue : Qfalse;
753
+ }
754
+
755
+ /* ---------------------------------------------------------
756
+ * Interp#tcl_eval(script) - Evaluate Tcl script string
757
+ *
758
+ * Thread-safe: automatically routes through event queue if
759
+ * called from a background thread.
760
+ * --------------------------------------------------------- */
761
+
762
+ static VALUE
763
+ interp_tcl_eval(VALUE self, VALUE script)
764
+ {
765
+ struct tcltk_interp *tip = get_interp(self);
766
+ Tcl_ThreadId current = Tcl_GetCurrentThread();
767
+ const char *script_cstr;
768
+ int result;
769
+
770
+ StringValue(script);
771
+
772
+ /* If on background thread, queue to main thread and wait */
773
+ if (current != tip->main_thread_id) {
774
+ VALUE cmd_hash = rb_hash_new();
775
+ rb_hash_aset(cmd_hash, ID2SYM(sym_type), sym_eval);
776
+ rb_hash_aset(cmd_hash, ID2SYM(sym_script), script);
777
+ return queue_command_internal(tip, cmd_hash, 1);
778
+ }
779
+
780
+ /* On main thread - execute directly */
781
+ script_cstr = StringValueCStr(script);
782
+ result = Tcl_Eval(tip->interp, script_cstr);
783
+
784
+ if (result != TCL_OK) {
785
+ rb_raise(eTclError, "%s", Tcl_GetStringResult(tip->interp));
786
+ }
787
+
788
+ return rb_utf8_str_new_cstr(Tcl_GetStringResult(tip->interp));
789
+ }
790
+
791
+ /* ---------------------------------------------------------
792
+ * Interp#tcl_invoke(*args) - Invoke Tcl command with args
793
+ *
794
+ * This is the workhorse - creates widgets, configures them, etc.
795
+ * Thread-safe: automatically routes through event queue if
796
+ * called from a background thread.
797
+ * --------------------------------------------------------- */
798
+
799
+ static VALUE
800
+ interp_tcl_invoke(int argc, VALUE *argv, VALUE self)
801
+ {
802
+ struct tcltk_interp *tip = get_interp(self);
803
+ Tcl_ThreadId current = Tcl_GetCurrentThread();
804
+ Tcl_Obj **objv;
805
+ int i, result;
806
+ VALUE ret;
807
+
808
+ if (argc == 0) {
809
+ rb_raise(rb_eArgError, "wrong number of arguments (given 0, expected 1+)");
810
+ }
811
+
812
+ /* If on background thread, queue to main thread and wait */
813
+ if (current != tip->main_thread_id) {
814
+ VALUE cmd_hash = rb_hash_new();
815
+ VALUE args_ary = rb_ary_new4(argc, argv);
816
+ rb_hash_aset(cmd_hash, ID2SYM(sym_type), sym_invoke);
817
+ rb_hash_aset(cmd_hash, ID2SYM(sym_args), args_ary);
818
+ return queue_command_internal(tip, cmd_hash, 1);
819
+ }
820
+
821
+ /* On main thread - execute directly */
822
+ objv = ALLOCA_N(Tcl_Obj *, argc);
823
+ for (i = 0; i < argc; i++) {
824
+ VALUE arg = argv[i];
825
+ const char *str;
826
+ Tcl_Size len;
827
+
828
+ if (NIL_P(arg)) {
829
+ str = "";
830
+ len = 0;
831
+ } else {
832
+ StringValue(arg);
833
+ str = RSTRING_PTR(arg);
834
+ len = RSTRING_LEN(arg);
835
+ }
836
+
837
+ objv[i] = Tcl_NewStringObj(str, len);
838
+ Tcl_IncrRefCount(objv[i]);
839
+ }
840
+
841
+ /* Invoke the command */
842
+ result = Tcl_EvalObjv(tip->interp, argc, objv, 0);
843
+
844
+ /* Clean up Tcl objects */
845
+ for (i = 0; i < argc; i++) {
846
+ Tcl_DecrRefCount(objv[i]);
847
+ }
848
+
849
+ if (result != TCL_OK) {
850
+ rb_raise(eTclError, "%s", Tcl_GetStringResult(tip->interp));
851
+ }
852
+
853
+ ret = rb_utf8_str_new_cstr(Tcl_GetStringResult(tip->interp));
854
+ return ret;
855
+ }
856
+
857
+ /* ---------------------------------------------------------
858
+ * Interp#tcl_get_var(name) - Get Tcl variable value
859
+ * --------------------------------------------------------- */
860
+
861
+ static VALUE
862
+ interp_tcl_get_var(VALUE self, VALUE name)
863
+ {
864
+ struct tcltk_interp *tip = get_interp(self);
865
+ const char *name_cstr;
866
+ const char *value;
867
+
868
+ StringValue(name);
869
+ name_cstr = StringValueCStr(name);
870
+
871
+ value = Tcl_GetVar(tip->interp, name_cstr, TCL_GLOBAL_ONLY);
872
+ if (value == NULL) {
873
+ return Qnil;
874
+ }
875
+
876
+ return rb_utf8_str_new_cstr(value);
877
+ }
878
+
879
+ /* ---------------------------------------------------------
880
+ * Interp#tcl_set_var(name, value) - Set Tcl variable
881
+ * --------------------------------------------------------- */
882
+
883
+ static VALUE
884
+ interp_tcl_set_var(VALUE self, VALUE name, VALUE value)
885
+ {
886
+ struct tcltk_interp *tip = get_interp(self);
887
+ const char *name_cstr;
888
+ const char *value_cstr;
889
+ const char *result;
890
+
891
+ StringValue(name);
892
+ name_cstr = StringValueCStr(name);
893
+
894
+ if (NIL_P(value)) {
895
+ value_cstr = "";
896
+ } else {
897
+ StringValue(value);
898
+ value_cstr = StringValueCStr(value);
899
+ }
900
+
901
+ result = Tcl_SetVar(tip->interp, name_cstr, value_cstr, TCL_GLOBAL_ONLY);
902
+ if (result == NULL) {
903
+ rb_raise(eTclError, "failed to set variable '%s'", name_cstr);
904
+ }
905
+
906
+ return value;
907
+ }
908
+
909
+ /* ---------------------------------------------------------
910
+ * Interp#do_one_event(flags = ALL_EVENTS) - Process single event
911
+ *
912
+ * Returns true if event was processed, false if nothing to do.
913
+ * --------------------------------------------------------- */
914
+
915
+ static VALUE
916
+ interp_do_one_event(int argc, VALUE *argv, VALUE self)
917
+ {
918
+ int flags = TCL_ALL_EVENTS;
919
+ int result;
920
+
921
+ /* Optional flags argument */
922
+ if (argc > 0) {
923
+ flags = NUM2INT(argv[0]);
924
+ }
925
+
926
+ result = Tcl_DoOneEvent(flags);
927
+
928
+ return result ? Qtrue : Qfalse;
929
+ }
930
+
931
+ /* ---------------------------------------------------------
932
+ * Interp#deleted? - Check if interpreter was deleted
933
+ * --------------------------------------------------------- */
934
+
935
+ static VALUE
936
+ interp_deleted_p(VALUE self)
937
+ {
938
+ struct tcltk_interp *tip;
939
+ TypedData_Get_Struct(self, struct tcltk_interp, &interp_type, tip);
940
+ return (tip->deleted || tip->interp == NULL) ? Qtrue : Qfalse;
941
+ }
942
+
943
+ /* ---------------------------------------------------------
944
+ * Interp#safe? - Check if interpreter is running in safe mode
945
+ *
946
+ * Safe interpreters have restricted access to dangerous commands
947
+ * like file I/O, exec, socket, etc. Created via create_slave(name, true).
948
+ *
949
+ * See: https://www.tcl-lang.org/man/tcl/TclCmd/interp.html#M30
950
+ * --------------------------------------------------------- */
951
+
952
+ static VALUE
953
+ interp_safe_p(VALUE self)
954
+ {
955
+ struct tcltk_interp *tip = get_interp(self);
956
+ return Tcl_IsSafe(tip->interp) ? Qtrue : Qfalse;
957
+ }
958
+
959
+ /* ---------------------------------------------------------
960
+ * Interp#delete - Explicitly delete interpreter
961
+ * --------------------------------------------------------- */
962
+
963
+ static VALUE
964
+ interp_delete(VALUE self)
965
+ {
966
+ struct tcltk_interp *tip;
967
+ TypedData_Get_Struct(self, struct tcltk_interp, &interp_type, tip);
968
+
969
+ if (tip->interp && !tip->deleted) {
970
+ Tcl_DeleteInterp(tip->interp);
971
+ tip->deleted = 1;
972
+ /* Remove from live instances tracking */
973
+ rb_ary_delete(live_instances, self);
974
+ }
975
+
976
+ return Qnil;
977
+ }
978
+
979
+ /* ---------------------------------------------------------
980
+ * Interp#tcl_version / #tk_version - Get version strings
981
+ * --------------------------------------------------------- */
982
+
983
+ static VALUE
984
+ interp_tcl_version(VALUE self)
985
+ {
986
+ struct tcltk_interp *tip = get_interp(self);
987
+ const char *version = Tcl_GetVar(tip->interp, "tcl_patchLevel", TCL_GLOBAL_ONLY);
988
+ if (version == NULL) {
989
+ return Qnil;
990
+ }
991
+ return rb_utf8_str_new_cstr(version);
992
+ }
993
+
994
+ static VALUE
995
+ interp_tk_version(VALUE self)
996
+ {
997
+ struct tcltk_interp *tip = get_interp(self);
998
+ const char *version = Tcl_GetVar(tip->interp, "tk_patchLevel", TCL_GLOBAL_ONLY);
999
+ if (version == NULL) {
1000
+ return Qnil;
1001
+ }
1002
+ return rb_utf8_str_new_cstr(version);
1003
+ }
1004
+
1005
+ /* ---------------------------------------------------------
1006
+ * Interp#mainloop - Run Tk event loop until no windows remain
1007
+ *
1008
+ * This is a thread-aware event loop that yields to other Ruby threads.
1009
+ * A recurring Tcl timer ensures DoOneEvent returns periodically.
1010
+ * The timer interval is controlled by the :thread_timer_ms option
1011
+ * passed to initialize (default: 5ms).
1012
+ * --------------------------------------------------------- */
1013
+
1014
+ /* Quick no-op function for GVL release/reacquire */
1015
+ static void *
1016
+ thread_yield_func(void *arg)
1017
+ {
1018
+ return NULL;
1019
+ }
1020
+
1021
+ /* Timer handler - re-registers itself to keep event loop responsive */
1022
+ static void
1023
+ keepalive_timer_proc(ClientData clientData)
1024
+ {
1025
+ struct tcltk_interp *tip = (struct tcltk_interp *)clientData;
1026
+ if (tip && !tip->deleted && tip->timer_interval_ms > 0) {
1027
+ Tcl_CreateTimerHandler(tip->timer_interval_ms, keepalive_timer_proc, clientData);
1028
+ }
1029
+ }
1030
+
1031
+ static VALUE
1032
+ interp_mainloop(VALUE self)
1033
+ {
1034
+ struct tcltk_interp *tip = get_interp(self);
1035
+
1036
+ /* Start recurring timer if interval > 0 */
1037
+ if (tip->timer_interval_ms > 0) {
1038
+ Tcl_CreateTimerHandler(tip->timer_interval_ms, keepalive_timer_proc, (ClientData)tip);
1039
+ }
1040
+
1041
+ while (Tk_GetNumMainWindows() > 0) {
1042
+ /* Process one event (timer ensures this returns periodically) */
1043
+ Tcl_DoOneEvent(TCL_ALL_EVENTS);
1044
+
1045
+ /* Yield to other Ruby threads by releasing and reacquiring GVL */
1046
+ if (tip->timer_interval_ms > 0) {
1047
+ rb_thread_call_without_gvl(thread_yield_func, NULL, RUBY_UBF_IO, NULL);
1048
+ }
1049
+
1050
+ /* Check for Ruby interrupts (Ctrl-C, etc) */
1051
+ rb_thread_check_ints();
1052
+ }
1053
+
1054
+ return Qnil;
1055
+ }
1056
+
1057
+ /* ---------------------------------------------------------
1058
+ * TclTkLib.mainloop - Global event loop (no interpreter required)
1059
+ *
1060
+ * Runs the Tk event loop until all windows are closed.
1061
+ * Uses the global g_thread_timer_ms setting for thread yielding.
1062
+ *
1063
+ * IMPORTANT: GVL is released during Tcl_DoOneEvent so background
1064
+ * Ruby threads can run while waiting for Tk events. Without this,
1065
+ * background threads would be starved (only running during brief
1066
+ * yields between events).
1067
+ * --------------------------------------------------------- */
1068
+
1069
+ /* Global timer handler - re-registers itself using global interval */
1070
+ static void
1071
+ global_keepalive_timer_proc(ClientData clientData)
1072
+ {
1073
+ if (g_thread_timer_ms > 0) {
1074
+ Tcl_CreateTimerHandler(g_thread_timer_ms, global_keepalive_timer_proc, NULL);
1075
+ }
1076
+ }
1077
+
1078
+ static VALUE
1079
+ lib_mainloop(int argc, VALUE *argv, VALUE self)
1080
+ {
1081
+ int check_root = 1; /* default: exit when no windows remain */
1082
+ int event_flags = TCL_ALL_EVENTS;
1083
+
1084
+ /* Optional check_root argument:
1085
+ * true (default): exit when Tk_GetNumMainWindows() == 0
1086
+ * false: keep running even with no windows (for timers, traces, etc.)
1087
+ */
1088
+ if (argc > 0 && argv[0] != Qnil) {
1089
+ check_root = RTEST(argv[0]);
1090
+ }
1091
+
1092
+ for (;;) {
1093
+ /* Exit if check_root enabled and no windows remain */
1094
+ if (check_root && Tk_GetNumMainWindows() <= 0) {
1095
+ break;
1096
+ }
1097
+
1098
+ if (rb_thread_alone()) {
1099
+ /* No other threads - simple blocking wait */
1100
+ Tcl_DoOneEvent(event_flags);
1101
+ } else {
1102
+ /* Other threads exist - use polling with brief sleep.
1103
+ *
1104
+ * We tried rb_thread_call_without_gvl() with Tcl_ThreadAlert to
1105
+ * efficiently release GVL during blocking waits, but it proved
1106
+ * unstable - crashes in Digest and other C extensions, UI freezes,
1107
+ * and unreliable notifier wakeup across platforms.
1108
+ *
1109
+ * This polling approach is simple and stable:
1110
+ * - Process any pending events without blocking
1111
+ * - If no events, brief sleep to avoid spinning (uses ~1-3% CPU idle)
1112
+ * - rb_thread_schedule() lets background threads run during sleep
1113
+ */
1114
+ int had_event = Tcl_DoOneEvent(event_flags | TCL_DONT_WAIT);
1115
+ if (!had_event) {
1116
+ rb_thread_schedule();
1117
+ #ifdef _WIN32
1118
+ Sleep(5); /* 5ms */
1119
+ #else
1120
+ struct timespec ts = {0, 5000000}; /* 5ms */
1121
+ nanosleep(&ts, NULL);
1122
+ #endif
1123
+ }
1124
+ }
1125
+
1126
+ /* Check for Ruby interrupts (Ctrl-C, etc) */
1127
+ rb_thread_check_ints();
1128
+ }
1129
+
1130
+ return Qnil;
1131
+ }
1132
+
1133
+ static VALUE
1134
+ lib_get_thread_timer_ms(VALUE self)
1135
+ {
1136
+ return INT2NUM(g_thread_timer_ms);
1137
+ }
1138
+
1139
+ static VALUE
1140
+ lib_set_thread_timer_ms(VALUE self, VALUE val)
1141
+ {
1142
+ int ms = NUM2INT(val);
1143
+ if (ms < 0) {
1144
+ rb_raise(rb_eArgError, "thread_timer_ms must be >= 0 (got %d)", ms);
1145
+ }
1146
+ g_thread_timer_ms = ms;
1147
+ return val;
1148
+ }
1149
+
1150
+ /* ---------------------------------------------------------
1151
+ * TclTkLib.do_one_event(flags = ALL_EVENTS) - Process single event
1152
+ *
1153
+ * Global function - Tcl_DoOneEvent doesn't require an interpreter.
1154
+ * Returns true if event was processed, false if nothing to do.
1155
+ * --------------------------------------------------------- */
1156
+
1157
+ static VALUE
1158
+ lib_do_one_event(int argc, VALUE *argv, VALUE self)
1159
+ {
1160
+ int flags = TCL_ALL_EVENTS;
1161
+ int result;
1162
+
1163
+ if (argc > 0) {
1164
+ flags = NUM2INT(argv[0]);
1165
+ }
1166
+
1167
+ result = Tcl_DoOneEvent(flags);
1168
+
1169
+ return result ? Qtrue : Qfalse;
1170
+ }
1171
+
1172
+ /* ---------------------------------------------------------
1173
+ * Interp#thread_timer_ms / #thread_timer_ms= - Get/set timer interval
1174
+ * --------------------------------------------------------- */
1175
+
1176
+ static VALUE
1177
+ interp_get_thread_timer_ms(VALUE self)
1178
+ {
1179
+ struct tcltk_interp *tip = get_interp(self);
1180
+ return INT2NUM(tip->timer_interval_ms);
1181
+ }
1182
+
1183
+ static VALUE
1184
+ interp_set_thread_timer_ms(VALUE self, VALUE val)
1185
+ {
1186
+ struct tcltk_interp *tip = get_interp(self);
1187
+ int ms = NUM2INT(val);
1188
+ if (ms < 0) {
1189
+ rb_raise(rb_eArgError, "thread_timer_ms must be >= 0 (got %d)", ms);
1190
+ }
1191
+ tip->timer_interval_ms = ms;
1192
+ return val;
1193
+ }
1194
+
1195
+ /* ---------------------------------------------------------
1196
+ * Teek.tcl_to_bool(str) - Convert Tcl boolean string to Ruby true/false
1197
+ *
1198
+ * Uses Tcl_GetBooleanFromObj which recognizes:
1199
+ * true/false, yes/no, on/off, 1/0 (case-insensitive)
1200
+ * and any numeric value (0 = false, non-zero = true)
1201
+ * --------------------------------------------------------- */
1202
+
1203
+ static VALUE
1204
+ teek_tcl_to_bool(VALUE self, VALUE str)
1205
+ {
1206
+ Tcl_Obj *obj;
1207
+ int bval;
1208
+
1209
+ StringValue(str);
1210
+ obj = Tcl_NewStringObj(RSTRING_PTR(str), RSTRING_LEN(str));
1211
+ Tcl_IncrRefCount(obj);
1212
+
1213
+ if (Tcl_GetBooleanFromObj(utility_interp, obj, &bval) != TCL_OK) {
1214
+ const char *msg = Tcl_GetStringResult(utility_interp);
1215
+ Tcl_DecrRefCount(obj);
1216
+ rb_raise(eTclError, "%s", msg);
1217
+ }
1218
+
1219
+ Tcl_DecrRefCount(obj);
1220
+ return bval ? Qtrue : Qfalse;
1221
+ }
1222
+
1223
+ /* ---------------------------------------------------------
1224
+ * Teek.make_list(*args) - Merge strings into Tcl list
1225
+ *
1226
+ * Uses Tcl's quoting rules for proper escaping.
1227
+ * Module function — uses utility_interp (no user interp needed).
1228
+ * --------------------------------------------------------- */
1229
+
1230
+ /* rb_ensure cleanup: release Tcl list object on exception */
1231
+ struct make_list_state {
1232
+ Tcl_Obj *listobj;
1233
+ int argc;
1234
+ VALUE *argv;
1235
+ };
1236
+
1237
+ static VALUE
1238
+ make_list_body(VALUE arg)
1239
+ {
1240
+ struct make_list_state *st = (struct make_list_state *)arg;
1241
+ Tcl_Size len;
1242
+ const char *result;
1243
+ int i;
1244
+
1245
+ for (i = 0; i < st->argc; i++) {
1246
+ VALUE s = StringValue(st->argv[i]);
1247
+ Tcl_Obj *elem = Tcl_NewStringObj(RSTRING_PTR(s), RSTRING_LEN(s));
1248
+ Tcl_ListObjAppendElement(NULL, st->listobj, elem);
1249
+ }
1250
+
1251
+ result = Tcl_GetStringFromObj(st->listobj, &len);
1252
+ return rb_utf8_str_new(result, len);
1253
+ }
1254
+
1255
+ static VALUE
1256
+ make_list_cleanup(VALUE arg)
1257
+ {
1258
+ struct make_list_state *st = (struct make_list_state *)arg;
1259
+ Tcl_DecrRefCount(st->listobj);
1260
+ return Qnil;
1261
+ }
1262
+
1263
+ static VALUE
1264
+ teek_make_list(int argc, VALUE *argv, VALUE self)
1265
+ {
1266
+ struct make_list_state st;
1267
+
1268
+ if (argc == 0) return rb_utf8_str_new_cstr("");
1269
+
1270
+ st.listobj = Tcl_NewListObj(0, NULL);
1271
+ Tcl_IncrRefCount(st.listobj);
1272
+ st.argc = argc;
1273
+ st.argv = argv;
1274
+
1275
+ return rb_ensure(make_list_body, (VALUE)&st,
1276
+ make_list_cleanup, (VALUE)&st);
1277
+ }
1278
+
1279
+ /* ---------------------------------------------------------
1280
+ * Teek.split_list(str) - Parse Tcl list into Ruby array
1281
+ *
1282
+ * Module function — uses utility_interp for error reporting.
1283
+ * Single C call instead of N+1 eval round-trips.
1284
+ * Returns array of strings (does not recursively parse nested lists).
1285
+ * --------------------------------------------------------- */
1286
+
1287
+ static VALUE
1288
+ teek_split_list(VALUE self, VALUE list_str)
1289
+ {
1290
+ Tcl_Obj *listobj;
1291
+ Tcl_Size objc;
1292
+ Tcl_Obj **objv;
1293
+ VALUE ary;
1294
+ Tcl_Size i;
1295
+ int result;
1296
+
1297
+ if (NIL_P(list_str)) {
1298
+ return rb_ary_new();
1299
+ }
1300
+
1301
+ StringValue(list_str);
1302
+ if (RSTRING_LEN(list_str) == 0) {
1303
+ return rb_ary_new();
1304
+ }
1305
+
1306
+ /* Create Tcl object from Ruby string */
1307
+ listobj = Tcl_NewStringObj(RSTRING_PTR(list_str), RSTRING_LEN(list_str));
1308
+ Tcl_IncrRefCount(listobj);
1309
+
1310
+ /* Use utility_interp for error reporting */
1311
+ result = Tcl_ListObjGetElements(utility_interp, listobj, &objc, &objv);
1312
+ if (result != TCL_OK) {
1313
+ const char *msg = Tcl_GetStringResult(utility_interp);
1314
+ Tcl_DecrRefCount(listobj);
1315
+ rb_raise(eTclError, "invalid Tcl list: %s", msg);
1316
+ }
1317
+
1318
+ /* Convert to Ruby array of strings */
1319
+ ary = rb_ary_new2(objc);
1320
+ for (i = 0; i < objc; i++) {
1321
+ Tcl_Size len;
1322
+ const char *str = Tcl_GetStringFromObj(objv[i], &len);
1323
+ rb_ary_push(ary, rb_utf8_str_new(str, len));
1324
+ }
1325
+
1326
+ Tcl_DecrRefCount(listobj);
1327
+ return ary;
1328
+ }
1329
+
1330
+ /* ---------------------------------------------------------
1331
+ * Interp#create_slave(name, safe=false) - Create child interpreter
1332
+ *
1333
+ * Creates a Tcl slave interpreter with the given name.
1334
+ * If safe is true, the slave runs in safe mode (restricted commands).
1335
+ * --------------------------------------------------------- */
1336
+
1337
+ static VALUE
1338
+ interp_create_slave(int argc, VALUE *argv, VALUE self)
1339
+ {
1340
+ struct tcltk_interp *master = get_interp(self);
1341
+ struct tcltk_interp *slave;
1342
+ VALUE name, safemode, new_ip;
1343
+ int safe;
1344
+ Tcl_Interp *slave_interp;
1345
+
1346
+ rb_scan_args(argc, argv, "11", &name, &safemode);
1347
+ StringValue(name);
1348
+ safe = RTEST(safemode) ? 1 : 0;
1349
+
1350
+ /* Create the slave interpreter */
1351
+ slave_interp = Tcl_CreateSlave(master->interp, StringValueCStr(name), safe);
1352
+ if (slave_interp == NULL) {
1353
+ rb_raise(eTclError, "failed to create slave interpreter");
1354
+ }
1355
+
1356
+ /* Wrap in a new TclTkIp Ruby object */
1357
+ new_ip = TypedData_Make_Struct(cInterp, struct tcltk_interp,
1358
+ &interp_type, slave);
1359
+ slave->interp = slave_interp;
1360
+ slave->deleted = 0;
1361
+ slave->callbacks = rb_hash_new();
1362
+ slave->thread_queue = rb_ary_new();
1363
+ slave->next_id = 1;
1364
+ slave->timer_interval_ms = DEFAULT_TIMER_INTERVAL_MS;
1365
+ slave->main_thread_id = Tcl_GetCurrentThread();
1366
+
1367
+ /* Register Ruby integration commands in the slave */
1368
+ Tcl_CreateObjCommand(slave->interp, "ruby_callback",
1369
+ ruby_callback_proc, (ClientData)slave, NULL);
1370
+ Tcl_CreateObjCommand(slave->interp, "ruby",
1371
+ ruby_eval_proc, (ClientData)slave, NULL);
1372
+ Tcl_CreateObjCommand(slave->interp, "ruby_eval",
1373
+ ruby_eval_proc, (ClientData)slave, NULL);
1374
+
1375
+ /* Register callback for when Tcl deletes this interpreter */
1376
+ Tcl_CallWhenDeleted(slave->interp, interp_deleted_callback, (ClientData)slave);
1377
+
1378
+ /* Track this instance */
1379
+ rb_ary_push(live_instances, new_ip);
1380
+
1381
+ return new_ip;
1382
+ }
1383
+
1384
+ /* ---------------------------------------------------------
1385
+ * TclTkIp.instance_count - Number of live interpreter instances
1386
+ * --------------------------------------------------------- */
1387
+
1388
+ static VALUE
1389
+ tcltkip_instance_count(VALUE klass)
1390
+ {
1391
+ return LONG2NUM(RARRAY_LEN(live_instances));
1392
+ }
1393
+
1394
+ /* ---------------------------------------------------------
1395
+ * TclTkIp.instances - Array of live interpreter instances
1396
+ * --------------------------------------------------------- */
1397
+
1398
+ static VALUE
1399
+ tcltkip_instances(VALUE klass)
1400
+ {
1401
+ return rb_ary_dup(live_instances);
1402
+ }
1403
+
1404
+ /* ---------------------------------------------------------
1405
+ * TclTkLib.in_callback? - Check if currently inside a Tk callback
1406
+ *
1407
+ * Used to detect unsafe operations (exit/destroy from callback).
1408
+ * --------------------------------------------------------- */
1409
+
1410
+ static VALUE
1411
+ lib_in_callback_p(VALUE self)
1412
+ {
1413
+ return rbtk_callback_depth > 0 ? Qtrue : Qfalse;
1414
+ }
1415
+
1416
+ /* ---------------------------------------------------------
1417
+ * TclTkLib.get_version - Get Tcl version as [major, minor, type, patchlevel]
1418
+ *
1419
+ * WHY COMPILE-TIME MACROS INSTEAD OF Tcl_GetVersion()?
1420
+ *
1421
+ * With stubs enabled (-DUSE_TCL_STUBS), Tcl_GetVersion() becomes a macro
1422
+ * that dereferences tclStubsPtr->tcl_GetVersion. But tclStubsPtr is NULL
1423
+ * until Tcl_InitStubs() is called - which requires an interpreter.
1424
+ *
1425
+ * So the "proper" API to get the version needs an interpreter to exist
1426
+ * first. That's backwards - callers often want version info before
1427
+ * deciding whether to create an interpreter.
1428
+ *
1429
+ * The workaround: use the compile-time macros from tcl.h directly.
1430
+ * These are just #defines, no stubs table needed. The version reported
1431
+ * is what we compiled against, which must match the runtime major version
1432
+ * (stubs enforce this). Minor/patch may differ at runtime - use
1433
+ * TclTkIp#tcl_version for the exact runtime patchlevel.
1434
+ * --------------------------------------------------------- */
1435
+
1436
+ static VALUE
1437
+ lib_get_version(VALUE self)
1438
+ {
1439
+ return rb_ary_new3(4,
1440
+ INT2NUM(TCL_MAJOR_VERSION),
1441
+ INT2NUM(TCL_MINOR_VERSION),
1442
+ INT2NUM(TCL_RELEASE_LEVEL),
1443
+ INT2NUM(TCL_RELEASE_SERIAL));
1444
+ }
1445
+
1446
+ /* Photo image functions moved to tkphoto.c */
1447
+
1448
+ /* ---------------------------------------------------------
1449
+ * Interp#create_console - Create Tk console window
1450
+ *
1451
+ * Creates a console window for platforms without a real terminal.
1452
+ * See: https://www.tcl-lang.org/man/tcl8.6/TkLib/CrtConsoleChan.htm
1453
+ * --------------------------------------------------------- */
1454
+
1455
+ static VALUE
1456
+ interp_create_console(VALUE self)
1457
+ {
1458
+ struct tcltk_interp *tip = get_interp(self);
1459
+
1460
+ /*
1461
+ * tcl_interactive is normally set by tclsh/wish at startup.
1462
+ * When embedding Tcl in Ruby, we must set it ourselves.
1463
+ * console.tcl checks this to decide whether to show the console window:
1464
+ * if 0, the window starts hidden (wm withdraw); if 1, it's shown.
1465
+ * See: https://github.com/tcltk/tk/blob/main/library/console.tcl#L144
1466
+ */
1467
+ if (Tcl_GetVar(tip->interp, "tcl_interactive", TCL_GLOBAL_ONLY) == NULL) {
1468
+ Tcl_SetVar(tip->interp, "tcl_interactive", "0", TCL_GLOBAL_ONLY);
1469
+ }
1470
+
1471
+ Tk_InitConsoleChannels(tip->interp);
1472
+
1473
+ if (Tk_CreateConsoleWindow(tip->interp) != TCL_OK) {
1474
+ rb_raise(eTclError, "failed to create console window: %s",
1475
+ Tcl_GetStringResult(tip->interp));
1476
+ }
1477
+
1478
+ return Qtrue;
1479
+ }
1480
+
1481
+ /* ---------------------------------------------------------
1482
+ * Module initialization
1483
+ * --------------------------------------------------------- */
1484
+
1485
+ void
1486
+ Init_tcltklib(void)
1487
+ {
1488
+ /* Initialize live instances tracking array (must be before any interp creation) */
1489
+ live_instances = rb_ary_new();
1490
+ rb_gc_register_address(&live_instances);
1491
+
1492
+ /* Initialize symbols for thread queue command hashes */
1493
+ sym_type = rb_intern("type");
1494
+ sym_proc = rb_intern("proc");
1495
+ sym_script = rb_intern("script");
1496
+ sym_args = rb_intern("args");
1497
+ sym_queue = rb_intern("queue");
1498
+ sym_eval = ID2SYM(rb_intern("eval"));
1499
+ sym_invoke = ID2SYM(rb_intern("invoke"));
1500
+ sym_proc_val = ID2SYM(rb_intern("proc"));
1501
+
1502
+ /* Get Thread::Queue for cross-thread synchronization */
1503
+ cQueue = rb_path2class("Thread::Queue");
1504
+ rb_gc_register_address(&cQueue);
1505
+
1506
+ /* Bootstrap Tcl stubs via a lightweight utility interpreter.
1507
+ * This makes Teek.make_list / Teek.split_list work immediately
1508
+ * after require, without needing a Teek::Interp first.
1509
+ * No Tk loaded — just bare Tcl. */
1510
+ find_executable_bootstrap("ruby");
1511
+ utility_interp = create_interp_bootstrap();
1512
+ if (utility_interp) {
1513
+ if (Tcl_InitStubs(utility_interp, TCL_VERSION, 0)) {
1514
+ tcl_stubs_initialized = 1;
1515
+ }
1516
+ }
1517
+
1518
+ /* Teek module (may already exist from Ruby side) */
1519
+ mTeek = rb_define_module("Teek");
1520
+
1521
+ /* Event flags as constants */
1522
+ rb_define_const(mTeek, "WINDOW_EVENTS", INT2NUM(TCL_WINDOW_EVENTS));
1523
+ rb_define_const(mTeek, "FILE_EVENTS", INT2NUM(TCL_FILE_EVENTS));
1524
+ rb_define_const(mTeek, "TIMER_EVENTS", INT2NUM(TCL_TIMER_EVENTS));
1525
+ rb_define_const(mTeek, "IDLE_EVENTS", INT2NUM(TCL_IDLE_EVENTS));
1526
+ rb_define_const(mTeek, "ALL_EVENTS", INT2NUM(TCL_ALL_EVENTS));
1527
+ rb_define_const(mTeek, "DONT_WAIT", INT2NUM(TCL_DONT_WAIT));
1528
+
1529
+ /* Teek::TclError exception */
1530
+ eTclError = rb_define_class_under(mTeek, "TclError", rb_eRuntimeError);
1531
+
1532
+ /* Callback control flow symbols (used by Teek::App#register_callback catch/throw) */
1533
+ rb_define_const(mTeek, "CALLBACK_BREAK", ID2SYM(rb_intern("teek_break")));
1534
+ rb_define_const(mTeek, "CALLBACK_CONTINUE", ID2SYM(rb_intern("teek_continue")));
1535
+ rb_define_const(mTeek, "CALLBACK_RETURN", ID2SYM(rb_intern("teek_return")));
1536
+
1537
+ /* Module functions for Tcl value conversion (no interpreter needed) */
1538
+ rb_define_module_function(mTeek, "make_list", teek_make_list, -1);
1539
+ rb_define_module_function(mTeek, "split_list", teek_split_list, 1);
1540
+ rb_define_module_function(mTeek, "tcl_to_bool", teek_tcl_to_bool, 1);
1541
+
1542
+ /* Global thread timer - doesn't require an interpreter */
1543
+ rb_define_module_function(mTeek, "thread_timer_ms", lib_get_thread_timer_ms, 0);
1544
+ rb_define_module_function(mTeek, "thread_timer_ms=", lib_set_thread_timer_ms, 1);
1545
+
1546
+ /* Callback depth detection for unsafe operation warnings */
1547
+ rb_define_module_function(mTeek, "in_callback?", lib_in_callback_p, 0);
1548
+
1549
+ /* Version info - uses compile-time macros, no stubs needed */
1550
+ rb_define_module_function(mTeek, "get_version", lib_get_version, 0);
1551
+
1552
+ /* Teek::RELEASE_TYPE module with constants */
1553
+ {
1554
+ VALUE mReleaseType = rb_define_module_under(mTeek, "RELEASE_TYPE");
1555
+ rb_define_const(mReleaseType, "ALPHA", INT2NUM(TCL_ALPHA_RELEASE));
1556
+ rb_define_const(mReleaseType, "BETA", INT2NUM(TCL_BETA_RELEASE));
1557
+ rb_define_const(mReleaseType, "FINAL", INT2NUM(TCL_FINAL_RELEASE));
1558
+ }
1559
+
1560
+ /* Teek::Interp class */
1561
+ cInterp = rb_define_class_under(mTeek, "Interp", rb_cObject);
1562
+ rb_define_alloc_func(cInterp, interp_alloc);
1563
+
1564
+ rb_define_method(cInterp, "initialize", interp_initialize, -1);
1565
+ rb_define_method(cInterp, "tcl_eval", interp_tcl_eval, 1);
1566
+ rb_define_method(cInterp, "tcl_invoke", interp_tcl_invoke, -1);
1567
+ rb_define_method(cInterp, "tcl_get_var", interp_tcl_get_var, 1);
1568
+ rb_define_method(cInterp, "tcl_set_var", interp_tcl_set_var, 2);
1569
+ rb_define_method(cInterp, "do_one_event", interp_do_one_event, -1);
1570
+ rb_define_method(cInterp, "deleted?", interp_deleted_p, 0);
1571
+ rb_define_method(cInterp, "safe?", interp_safe_p, 0);
1572
+ rb_define_method(cInterp, "delete", interp_delete, 0);
1573
+ rb_define_method(cInterp, "tcl_version", interp_tcl_version, 0);
1574
+ rb_define_method(cInterp, "tk_version", interp_tk_version, 0);
1575
+ rb_define_method(cInterp, "mainloop", interp_mainloop, 0);
1576
+ rb_define_method(cInterp, "register_callback", interp_register_callback, 1);
1577
+ rb_define_method(cInterp, "unregister_callback", interp_unregister_callback, 1);
1578
+ rb_define_method(cInterp, "create_slave", interp_create_slave, -1);
1579
+ rb_define_method(cInterp, "thread_timer_ms", interp_get_thread_timer_ms, 0);
1580
+ rb_define_method(cInterp, "thread_timer_ms=", interp_set_thread_timer_ms, 1);
1581
+ rb_define_method(cInterp, "queue_for_main", interp_queue_for_main, 1);
1582
+ rb_define_method(cInterp, "on_main_thread?", interp_on_main_thread_p, 0);
1583
+ rb_define_method(cInterp, "create_console", interp_create_console, 0);
1584
+
1585
+ /* Photo image functions (tkphoto.c) */
1586
+ Init_tkphoto(cInterp);
1587
+
1588
+ /* Font functions (tkfont.c) */
1589
+ Init_tkfont(cInterp);
1590
+
1591
+ /* Tk window query functions (tkwin.c) */
1592
+ Init_tkwin(cInterp);
1593
+
1594
+ /* Class methods for instance tracking */
1595
+ rb_define_singleton_method(cInterp, "instance_count", tcltkip_instance_count, 0);
1596
+ rb_define_singleton_method(cInterp, "instances", tcltkip_instances, 0);
1597
+ }