Skip to content

Commit edc80a3

Browse files
committed
WIP: loading TCL / Tk symbols dynamically
This is an attempt to load the symbols we need from the Tkinter.tkinter module at run time, rather than by linking at build time. It is one way of building a manylinux wheel that can build on a basic Manylinux docker image, and then run with the TCL / Tk library installed on the installing user's machine. It would also make the situation better on OSX, where we have to build against ActiveState TCL for compatibility with Python.org Python, but we would like to allow run-time TCL from e.g. homebrew. I have tested this on Debian Jessie Python 2.7 and 3.5, and on OSX 10.9 with Python 2.7. Questions: * Would y'all consider carrying something like this approach in the matplotlib source, but not enabled by default, to help building binary wheels? * Do you have any better suggestions about how to do this? * My C fu is weak; is there a way of collecting the typedefs I need from the TCL / Tk headers rather than copying them into the _tkagg.cpp source file (typdefs starting around line 52)? * My fu for Python C extension modules is also weak; did I configure exceptions and handle references correctly?
1 parent 7d66623 commit edc80a3

File tree

1 file changed

+141
-18
lines changed

1 file changed

+141
-18
lines changed

src/_tkagg.cpp

+141-18
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,38 @@ typedef struct
4444
Tcl_Interp *interp;
4545
} TkappObject;
4646

47+
#define DYNAMIC_TKINTER
48+
49+
#ifdef DYNAMIC_TKINTER
50+
// Load TCL / Tk symbols from tkinter extension module at run-time.
51+
// Typedefs, global vars for TCL / Tk library functions.
52+
typedef Tcl_Command (*tcl_cc)(Tcl_Interp *interp,
53+
const char *cmdName, Tcl_CmdProc *proc,
54+
ClientData clientData,
55+
Tcl_CmdDeleteProc *deleteProc);
56+
static tcl_cc TCL_CREATE_COMMAND;
57+
typedef void (*tcl_app_res) (Tcl_Interp *interp, ...);
58+
static tcl_app_res TCL_APPEND_RESULT;
59+
typedef Tk_Window (*tk_mw) (Tcl_Interp *interp);
60+
static tk_mw TK_MAIN_WINDOW;
61+
typedef Tk_PhotoHandle (*tk_fp) (Tcl_Interp *interp, const char *imageName);
62+
static tk_fp TK_FIND_PHOTO;
63+
typedef void (*tk_ppb_nc) (Tk_PhotoHandle handle,
64+
Tk_PhotoImageBlock *blockPtr, int x, int y,
65+
int width, int height);
66+
static tk_ppb_nc TK_PHOTO_PUTBLOCK;
67+
typedef void (*tk_pb) (Tk_PhotoHandle handle);
68+
static tk_pb TK_PHOTO_BLANK;
69+
#else
70+
// Build-time linking against system TCL / Tk functions.
71+
#define TCL_CREATE_COMMAND Tcl_CreateCommand
72+
#define TCL_APPEND_RESULT Tcl_AppendResult
73+
#define TK_MAIN_WINDOW Tk_MainWindow
74+
#define TK_FIND_PHOTO Tk_FindPhoto
75+
#define TK_PHOTO_PUTBLOCK Tk_PhotoPutBlock
76+
#define TK_PHOTO_BLANK Tk_PhotoBlank
77+
#endif
78+
4779
static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int argc, char **argv)
4880
{
4981
Tk_PhotoHandle photo;
@@ -61,25 +93,25 @@ static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int argc,
6193

6294
long mode;
6395
long nval;
64-
if (Tk_MainWindow(interp) == NULL) {
96+
if (TK_MAIN_WINDOW(interp) == NULL) {
6597
// Will throw a _tkinter.TclError with "this isn't a Tk application"
6698
return TCL_ERROR;
6799
}
68100

69101
if (argc != 5) {
70-
Tcl_AppendResult(interp, "usage: ", argv[0], " destPhoto srcImage", (char *)NULL);
102+
TCL_APPEND_RESULT(interp, "usage: ", argv[0], " destPhoto srcImage", (char *)NULL);
71103
return TCL_ERROR;
72104
}
73105

74106
/* get Tcl PhotoImage handle */
75-
photo = Tk_FindPhoto(interp, argv[1]);
107+
photo = TK_FIND_PHOTO(interp, argv[1]);
76108
if (photo == NULL) {
77-
Tcl_AppendResult(interp, "destination photo must exist", (char *)NULL);
109+
TCL_APPEND_RESULT(interp, "destination photo must exist", (char *)NULL);
78110
return TCL_ERROR;
79111
}
80112
/* get array (or object that can be converted to array) pointer */
81113
if (sscanf(argv[2], SIZE_T_FORMAT, &aggl) != 1) {
82-
Tcl_AppendResult(interp, "error casting pointer", (char *)NULL);
114+
TCL_APPEND_RESULT(interp, "error casting pointer", (char *)NULL);
83115
return TCL_ERROR;
84116
}
85117
bufferobj = (PyObject *)aggl;
@@ -88,7 +120,7 @@ static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int argc,
88120
try {
89121
buffer = numpy::array_view<uint8_t, 3>(bufferobj);
90122
} catch (...) {
91-
Tcl_AppendResult(interp, "buffer is of wrong type", (char *)NULL);
123+
TCL_APPEND_RESULT(interp, "buffer is of wrong type", (char *)NULL);
92124
PyErr_Clear();
93125
return TCL_ERROR;
94126
}
@@ -99,13 +131,13 @@ static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int argc,
99131
/* get array mode (0=mono, 1=rgb, 2=rgba) */
100132
mode = atol(argv[3]);
101133
if ((mode != 0) && (mode != 1) && (mode != 2)) {
102-
Tcl_AppendResult(interp, "illegal image mode", (char *)NULL);
134+
TCL_APPEND_RESULT(interp, "illegal image mode", (char *)NULL);
103135
return TCL_ERROR;
104136
}
105137

106138
/* check for bbox/blitting */
107139
if (sscanf(argv[4], SIZE_T_FORMAT, &bboxl) != 1) {
108-
Tcl_AppendResult(interp, "error casting pointer", (char *)NULL);
140+
TCL_APPEND_RESULT(interp, "error casting pointer", (char *)NULL);
109141
return TCL_ERROR;
110142
}
111143
bboxo = (PyObject *)bboxl;
@@ -126,7 +158,7 @@ static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int argc,
126158

127159
destbuffer = new agg::int8u[deststride * destheight];
128160
if (destbuffer == NULL) {
129-
Tcl_AppendResult(interp, "could not allocate memory", (char *)NULL);
161+
TCL_APPEND_RESULT(interp, "could not allocate memory", (char *)NULL);
130162
return TCL_ERROR;
131163
}
132164

@@ -167,7 +199,7 @@ static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int argc,
167199
block.pitch = deststride;
168200
block.pixelPtr = destbuffer;
169201

170-
Tk_PhotoPutBlock(photo, &block, destx, desty, destwidth, destheight);
202+
TK_PHOTO_PUTBLOCK(photo, &block, destx, desty, destwidth, destheight);
171203
delete[] destbuffer;
172204

173205
} else {
@@ -177,9 +209,9 @@ static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int argc,
177209
block.pixelPtr = buffer.data();
178210

179211
/* Clear current contents */
180-
Tk_PhotoBlank(photo);
212+
TK_PHOTO_BLANK(photo);
181213
/* Copy opaque block to photo image, and leave the rest to TK */
182-
Tk_PhotoPutBlock(photo, &block, 0, 0, block.width, block.height);
214+
TK_PHOTO_PUTBLOCK(photo, &block, 0, 0, block.width, block.height);
183215
}
184216

185217
return TCL_OK;
@@ -216,11 +248,11 @@ static PyObject *_tkinit(PyObject *self, PyObject *args)
216248

217249
/* This will bomb if interp is invalid... */
218250

219-
Tcl_CreateCommand(interp,
220-
"PyAggImagePhoto",
221-
(Tcl_CmdProc *)PyAggImagePhoto,
222-
(ClientData)0,
223-
(Tcl_CmdDeleteProc *)NULL);
251+
TCL_CREATE_COMMAND(interp,
252+
"PyAggImagePhoto",
253+
(Tcl_CmdProc *)PyAggImagePhoto,
254+
(ClientData)0,
255+
(Tcl_CmdDeleteProc *)NULL);
224256

225257
Py_INCREF(Py_None);
226258
return Py_None;
@@ -232,6 +264,90 @@ static PyMethodDef functions[] = {
232264
{ NULL, NULL } /* sentinel */
233265
};
234266

267+
#ifdef DYNAMIC_TKINTER
268+
// Functions to fill global TCL / Tk function pointers from tkinter module.
269+
270+
#include <dlfcn.h>
271+
272+
#if PY3K
273+
#define TKINTER_PKG "tkinter"
274+
#define TKINTER_MOD "_tkinter"
275+
// From module __file__ attribute to char *string for dlopen.
276+
#define FNAME2CHAR(s) (PyBytes_AsString(PyUnicode_EncodeFSDefault(s)))
277+
#else
278+
#define TKINTER_PKG "Tkinter"
279+
#define TKINTER_MOD "tkinter"
280+
// From module __file__ attribute to char *string for dlopen
281+
#define FNAME2CHAR(s) (PyString_AsString(s))
282+
#endif
283+
284+
void *_dfunc(void *lib_handle, const char *func_name)
285+
{
286+
// Load function, unless there has been a previous error. If so, then
287+
// return NULL. If there is an error loading the function, return NULL
288+
// and set error flag.
289+
static int have_error = 0;
290+
void *func = NULL;
291+
if (have_error == 0) {
292+
// reset errors
293+
dlerror();
294+
func = dlsym(lib_handle, func_name);
295+
const char *error = dlerror();
296+
if (error != NULL) {
297+
PyErr_SetString(PyExc_RuntimeError, error);
298+
have_error = 1;
299+
}
300+
}
301+
return func;
302+
}
303+
304+
int _func_loader(void *tkinter_lib)
305+
{
306+
// Fill global function pointers from dynamic lib.
307+
// Return 0 fur success; 1 otherwise.
308+
TCL_CREATE_COMMAND = (tcl_cc) _dfunc(tkinter_lib, "Tcl_CreateCommand");
309+
TCL_APPEND_RESULT = (tcl_app_res) _dfunc(tkinter_lib, "Tcl_AppendResult");
310+
TK_MAIN_WINDOW = (tk_mw) _dfunc(tkinter_lib, "Tk_MainWindow");
311+
TK_FIND_PHOTO = (tk_fp) _dfunc(tkinter_lib, "Tk_FindPhoto");
312+
TK_PHOTO_PUTBLOCK = (tk_ppb_nc) _dfunc(tkinter_lib, "Tk_PhotoPutBlock_NoComposite");
313+
TK_PHOTO_BLANK = (tk_pb) _dfunc(tkinter_lib, "Tk_PhotoBlank");
314+
return (TK_PHOTO_BLANK == NULL);
315+
}
316+
317+
int load_tkinter_funcs(void)
318+
{
319+
// Load tkinter global funcs from tkinter compiled module.
320+
// Return 0 for success, non-zero for failure.
321+
int ret = -1;
322+
PyObject *pModule, *pSubmodule, *pString;
323+
324+
pModule = PyImport_ImportModule(TKINTER_PKG);
325+
if (pModule != NULL) {
326+
pSubmodule = PyObject_GetAttrString(pModule, TKINTER_MOD);
327+
if (pSubmodule != NULL) {
328+
pString = PyObject_GetAttrString(pSubmodule, "__file__");
329+
if (pString != NULL) {
330+
char *tkinter_libname = FNAME2CHAR(pString);
331+
void *tkinter_lib = dlopen(tkinter_libname, RTLD_LAZY);
332+
if (tkinter_lib == NULL) {
333+
PyErr_SetString(PyExc_RuntimeError,
334+
"Cannot dlopen tkinter module file");
335+
} else {
336+
ret = _func_loader(tkinter_lib);
337+
// dlclose probably safe because tkinter has been
338+
// imported.
339+
dlclose(tkinter_lib);
340+
}
341+
Py_DECREF(pString);
342+
}
343+
Py_DECREF(pSubmodule);
344+
}
345+
Py_DECREF(pModule);
346+
}
347+
return ret;
348+
}
349+
#endif
350+
235351
#if PY3K
236352
static PyModuleDef _tkagg_module = { PyModuleDef_HEAD_INIT, "_tkagg", "", -1, functions,
237353
NULL, NULL, NULL, NULL };
@@ -244,13 +360,20 @@ PyMODINIT_FUNC PyInit__tkagg(void)
244360

245361
import_array();
246362

247-
return m;
363+
#ifdef DYNAMIC_TKINTER
364+
return (load_tkinter_funcs() == 0) ? m : NULL;
365+
#else
366+
return m
367+
#endif
248368
}
249369
#else
250370
PyMODINIT_FUNC init_tkagg(void)
251371
{
252372
import_array();
253373

254374
Py_InitModule("_tkagg", functions);
375+
#ifdef DYNAMIC_TKINTER
376+
load_tkinter_funcs();
377+
#endif
255378
}
256379
#endif

0 commit comments

Comments
 (0)