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.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +139 -0
- data/Rakefile +316 -0
- data/ext/teek/extconf.rb +79 -0
- data/ext/teek/stubs.h +33 -0
- data/ext/teek/tcl9compat.h +211 -0
- data/ext/teek/tcltkbridge.c +1597 -0
- data/ext/teek/tcltkbridge.h +42 -0
- data/ext/teek/tkfont.c +218 -0
- data/ext/teek/tkphoto.c +477 -0
- data/ext/teek/tkwin.c +144 -0
- data/lib/teek/background_none.rb +158 -0
- data/lib/teek/background_ractor4x.rb +410 -0
- data/lib/teek/background_thread.rb +272 -0
- data/lib/teek/debugger.rb +742 -0
- data/lib/teek/demo_support.rb +150 -0
- data/lib/teek/ractor_support.rb +246 -0
- data/lib/teek/version.rb +5 -0
- data/lib/teek.rb +540 -0
- data/sample/calculator.rb +260 -0
- data/sample/debug_demo.rb +45 -0
- data/sample/goldberg.rb +1803 -0
- data/sample/goldberg_helpers.rb +170 -0
- data/sample/minesweeper/assets/MINESWEEPER_0.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_1.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_2.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_3.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_4.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_5.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_6.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_7.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_8.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_F.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_M.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_X.png +0 -0
- data/sample/minesweeper/minesweeper.rb +452 -0
- data/sample/threading_demo.rb +499 -0
- data/teek.gemspec +32 -0
- 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
|
+
}
|