From cd60e90351777881958e2489339a7308398876a8 Mon Sep 17 00:00:00 2001 From: Philipp Keese Date: Fri, 9 Feb 2024 15:30:59 +0100 Subject: [PATCH] Add rudimentary support for _tkinter in GraalPy This commit adds the _tkinter module to the list of built-in modules to allow use of tkinter. Before use, the bindings need to be built using tklib_build.py. The code was modified from existing code in PyPy's main branch. Right now, opening a window under Linux works, but still crashes on interacting with UI elements such as buttons. It seems like there is a bug in the `cffi` module causing a SegFault. macOS complains about other threads accessing the main window and the build script has some trouble finding the correct library for Tcl and Tk, as macOS comes with an older version preinstalled. Windows was not tested. Co-authored-by: Margarete Dippel Co-authored-by: Tim Felgentreff --- graalpython/lib-python/3/_tkinter/__init__.py | 55 ++ graalpython/lib-python/3/_tkinter/app.py | 592 ++++++++++++++++++ graalpython/lib-python/3/_tkinter/tclobj.py | 236 +++++++ .../lib-python/3/_tkinter/tklib_build.py | 248 ++++++++ 4 files changed, 1131 insertions(+) create mode 100644 graalpython/lib-python/3/_tkinter/__init__.py create mode 100644 graalpython/lib-python/3/_tkinter/app.py create mode 100644 graalpython/lib-python/3/_tkinter/tclobj.py create mode 100644 graalpython/lib-python/3/_tkinter/tklib_build.py diff --git a/graalpython/lib-python/3/_tkinter/__init__.py b/graalpython/lib-python/3/_tkinter/__init__.py new file mode 100644 index 0000000000..3bd7970f82 --- /dev/null +++ b/graalpython/lib-python/3/_tkinter/__init__.py @@ -0,0 +1,55 @@ +# _tkinter package -- low-level interface to libtk and libtcl. +# +# This is an internal module, applications should "import tkinter" instead. +# +# This version is based PyPy which itself is based on cffi, and is a translation of _tkinter.c +# from CPython, version 2.7.4. + +import sys + +class TclError(Exception): + pass + +from .tklib_cffi import ffi as tkffi, lib as tklib + +from .app import TkApp +from .tclobj import TclObject as Tcl_Obj +from .app import FromTclString, ToTCLString + +TK_VERSION = FromTclString(tkffi.string(tklib.get_tk_version())) +TCL_VERSION = FromTclString(tkffi.string(tklib.get_tcl_version())) + +READABLE = tklib.TCL_READABLE +WRITABLE = tklib.TCL_WRITABLE +EXCEPTION = tklib.TCL_EXCEPTION +DONT_WAIT = tklib.TCL_DONT_WAIT + +def create(screenName=None, baseName=None, className=None, + interactive=False, wantobjects=False, wantTk=True, + sync=False, use=None): + return TkApp(screenName, baseName, className, + interactive, wantobjects, wantTk, sync, use) + +def dooneevent(flags=0): + return tklib.Tcl_DoOneEvent(flags) + + +def _flatten(item): + def _flatten1(output, item, depth): + if depth > 1000: + raise ValueError("nesting too deep in _flatten") + if not isinstance(item, (list, tuple)): + raise TypeError("argument must be sequence") + # copy items to output tuple + for o in item: + if isinstance(o, (list, tuple)): + _flatten1(output, o, depth + 1) + elif o is not None: + output.append(o) + + result = [] + _flatten1(result, item, 0) + return tuple(result) + +# Encoding is not specified explicitly, but "must be passed argv[0]" sounds like a simple conversion to raw bytes. +tklib.Tcl_FindExecutable(ToTCLString(sys.executable)) diff --git a/graalpython/lib-python/3/_tkinter/app.py b/graalpython/lib-python/3/_tkinter/app.py new file mode 100644 index 0000000000..cfa38d12b9 --- /dev/null +++ b/graalpython/lib-python/3/_tkinter/app.py @@ -0,0 +1,592 @@ +# The TkApp class. + +from .tklib_cffi import ffi as tkffi, lib as tklib +from . import TclError +from .tclobj import (TclObject, FromObj, FromTclString, ToTCLString, AsObj, TypeCache, + FromBignumObj, FromWideIntObj) + +import contextlib +import sys +import threading +import time + + +class _DummyLock(object): + "A lock-like object that does not do anything" + def acquire(self): + pass + def release(self): + pass + def __enter__(self): + pass + def __exit__(self, *exc): + pass + + +def varname_converter(input): + # Explicit handling of NUL character in strings a bytes here because tests require it. + if isinstance(input, bytes) and b'\0' in input: + raise ValueError("NUL character in string") + if isinstance(input, str) and '\0' in input: + raise ValueError("NUL character in string") + + if isinstance(input, TclObject): + return input.string + + return ToTCLString(input) + + +def Tcl_AppInit(app): + # For portable builds, try to load a local version of the libraries + from os.path import join, dirname, exists, sep + if sys.platform == 'win32': + lib_path = join(dirname(dirname(dirname(__file__))), 'tcl') + tcl_path = join(lib_path, 'tcl8.6') + tk_path = join(lib_path, 'tk8.6') + tcl_path = tcl_path.replace(sep, '/') + tk_path = tk_path.replace(sep, '/') + else: + lib_path = join(dirname(dirname(dirname(__file__))), 'lib') + tcl_path = join(lib_path, 'tcl') + tk_path = join(lib_path, 'tk') + if exists(tcl_path): + tklib.Tcl_Eval(app.interp, ToTCLString('set tcl_library "{0}"'.format(tcl_path))) + if exists(tk_path): + tklib.Tcl_Eval(app.interp, ToTCLString('set tk_library "{0}"'.format(tk_path))) + + if tklib.Tcl_Init(app.interp) == tklib.TCL_ERROR: + app.raiseTclError() + skip_tk_init = tklib.Tcl_GetVar( + app.interp, b"_tkinter_skip_tk_init", tklib.TCL_GLOBAL_ONLY) + if skip_tk_init and FromTclString(tkffi.string(skip_tk_init)) == "1": + return + + if tklib.Tk_Init(app.interp) == tklib.TCL_ERROR: + app.raiseTclError() + +class _CommandData(object): + def __new__(cls, app, name, func): + self = object.__new__(cls) + self.app = app + self.name = name + self.func = func + handle = tkffi.new_handle(self) + app._commands[name] = handle # To keep the command alive + return tkffi.cast("ClientData", handle) + + @tkffi.callback("Tcl_CmdProc") + def PythonCmd(clientData, interp, argc, argv): + self = tkffi.from_handle(clientData) + assert self.app.interp == interp + with self.app._tcl_lock_released(): + try: + args = [FromTclString(tkffi.string(arg)) for arg in argv[1:argc]] + result = self.func(*args) + obj = AsObj(result) + tklib.Tcl_SetObjResult(interp, obj) + except: + self.app.errorInCmd = True + self.app.exc_info = sys.exc_info() + return tklib.TCL_ERROR + else: + return tklib.TCL_OK + + @tkffi.callback("Tcl_CmdDeleteProc") + def PythonCmdDelete(clientData): + self = tkffi.from_handle(clientData) + app = self.app + del app._commands[self.name] + return + + +class TkApp(object): + _busywaitinterval = 0.02 # 20ms. + + def __new__(cls, screenName, baseName, className, + interactive, wantobjects, wantTk, sync, use): + if not wantobjects: + raise NotImplementedError("wantobjects=True only") + self = object.__new__(cls) + self.interp = tklib.Tcl_CreateInterp() + self._wantobjects = wantobjects + # "threaded" is an optionally present member of "tcl_platform" when TCL was compiled with threading. + # Tcl_GetVar2Ex should return NULL when "threaded" is not present, so casting to a bool here is doing an implicit NULL-check. + self.threaded = bool(tklib.Tcl_GetVar2Ex( + self.interp, b"tcl_platform", b"threaded", + tklib.TCL_GLOBAL_ONLY)) + self.thread_id = tklib.Tcl_GetCurrentThread() + self.dispatching = False + self.quitMainLoop = False + self.errorInCmd = False + + if not self.threaded: + # TCL is not thread-safe, calls needs to be serialized. + self._tcl_lock = threading.RLock() + else: + self._tcl_lock = _DummyLock() + + self._typeCache = TypeCache() + self._commands = {} + + # Delete the 'exit' command, which can screw things up + tklib.Tcl_DeleteCommand(self.interp, b"exit") + + if screenName is not None: + tklib.Tcl_SetVar2(self.interp, b"env", b"DISPLAY", screenName, + tklib.TCL_GLOBAL_ONLY) + + if interactive: + tklib.Tcl_SetVar(self.interp, b"tcl_interactive", b"1", + tklib.TCL_GLOBAL_ONLY) + else: + tklib.Tcl_SetVar(self.interp, b"tcl_interactive", b"0", + tklib.TCL_GLOBAL_ONLY) + + # This is used to get the application class for Tk 4.1 and up + argv0 = className.lower().encode('ascii') + tklib.Tcl_SetVar(self.interp, b"argv0", argv0, + tklib.TCL_GLOBAL_ONLY) + + if not wantTk: + tklib.Tcl_SetVar(self.interp, b"_tkinter_skip_tk_init", b"1", + tklib.TCL_GLOBAL_ONLY) + + # some initial arguments need to be in argv + if sync or use: + args = b"" + if sync: + args += b"-sync" + if use: + if sync: + args += b" " + args += b"-use " + use + + tklib.Tcl_SetVar(self.interp, b"argv", args, + tklib.TCL_GLOBAL_ONLY) + + Tcl_AppInit(self) + # EnableEventHook() + self._typeCache.add_extra_types(self) + return self + + def __del__(self): + tklib.Tcl_DeleteInterp(self.interp) + # DisableEventHook() + + def raiseTclError(self): + if self.errorInCmd: + self.errorInCmd = False + raise self.exc_info[0](self.exc_info[1]).with_traceback(self.exc_info[2]) + raise TclError(FromTclString(tkffi.string(tklib.Tcl_GetStringResult(self.interp)))) + + def wantobjects(self): + return self._wantobjects + + def _check_tcl_appartment(self): + if self.threaded and self.thread_id != tklib.Tcl_GetCurrentThread(): + raise RuntimeError("Calling Tcl from different appartment") + + @contextlib.contextmanager + def _tcl_lock_released(self): + "Context manager to temporarily release the tcl lock." + self._tcl_lock.release() + yield + self._tcl_lock.acquire() + + def loadtk(self): + # We want to guard against calling Tk_Init() multiple times + err = tklib.Tcl_Eval(self.interp, b"info exists tk_version") + if err == tklib.TCL_ERROR: + self.raiseTclError() + tk_exists = tklib.Tcl_GetStringResult(self.interp) + if not tk_exists or FromTclString(tkffi.string(tk_exists)) != "1": + err = tklib.Tk_Init(self.interp) + if err == tklib.TCL_ERROR: + self.raiseTclError() + + def interpaddr(self): + return int(tkffi.cast('size_t', self.interp)) + + def _var_invoke(self, func, *args, **kwargs): + if self.threaded and self.thread_id != tklib.Tcl_GetCurrentThread(): + # The current thread is not the interpreter thread. + # Marshal the call to the interpreter thread, then wait + # for completion. + raise NotImplementedError("Call from another thread") + return func(*args, **kwargs) + + def _getvar(self, name1, name2=None, global_only=False): + name1 = varname_converter(name1) + if not name2: + name2 = tkffi.NULL + flags=tklib.TCL_LEAVE_ERR_MSG + if global_only: + flags |= tklib.TCL_GLOBAL_ONLY + with self._tcl_lock: + # Name encoding not explicitly statet, assuming UTF-8 here due to other APIs. + res = tklib.Tcl_GetVar2Ex(self.interp, name1, name2, flags) + if not res: + self.raiseTclError() + assert self._wantobjects + return FromObj(self, res) + + def _setvar(self, name1, value, global_only=False): + name1 = varname_converter(name1) + # XXX Acquire tcl lock??? + newval = AsObj(value) + flags=tklib.TCL_LEAVE_ERR_MSG + if global_only: + flags |= tklib.TCL_GLOBAL_ONLY + with self._tcl_lock: + res = tklib.Tcl_SetVar2Ex(self.interp, name1, tkffi.NULL, + newval, flags) + if not res: + self.raiseTclError() + + def _unsetvar(self, name1, name2=None, global_only=False): + name1 = varname_converter(name1) + if not name2: + name2 = tkffi.NULL + flags=tklib.TCL_LEAVE_ERR_MSG + if global_only: + flags |= tklib.TCL_GLOBAL_ONLY + with self._tcl_lock: + res = tklib.Tcl_UnsetVar2(self.interp, name1, name2, flags) + if res == tklib.TCL_ERROR: + self.raiseTclError() + + def getvar(self, name1, name2=None): + return self._var_invoke(self._getvar, name1, name2) + + def globalgetvar(self, name1, name2=None): + return self._var_invoke(self._getvar, name1, name2, global_only=True) + + def setvar(self, name1, value): + return self._var_invoke(self._setvar, name1, value) + + def globalsetvar(self, name1, value): + return self._var_invoke(self._setvar, name1, value, global_only=True) + + def unsetvar(self, name1, name2=None): + return self._var_invoke(self._unsetvar, name1, name2) + + def globalunsetvar(self, name1, name2=None): + return self._var_invoke(self._unsetvar, name1, name2, global_only=True) + + # COMMANDS + + def createcommand(self, cmdName, func): + if not callable(func): + raise TypeError("command not callable") + + if self.threaded and self.thread_id != tklib.Tcl_GetCurrentThread(): + raise NotImplementedError("Call from another thread") + + clientData = _CommandData(self, cmdName, func) + + if self.threaded and self.thread_id != tklib.Tcl_GetCurrentThread(): + raise NotImplementedError("Call from another thread") + + with self._tcl_lock: + res = tklib.Tcl_CreateCommand( + self.interp, ToTCLString(cmdName), _CommandData.PythonCmd, + clientData, _CommandData.PythonCmdDelete) + if not res: + raise TclError(b"can't create Tcl command") + + def deletecommand(self, cmdName): + if self.threaded and self.thread_id != tklib.Tcl_GetCurrentThread(): + raise NotImplementedError("Call from another thread") + + with self._tcl_lock: + res = tklib.Tcl_DeleteCommand(self.interp, ToTCLString(cmdName)) + if res == -1: + raise TclError("can't delete Tcl command") + + def call(self, *args): + flags = tklib.TCL_EVAL_DIRECT | tklib.TCL_EVAL_GLOBAL + + # If args is a single tuple, replace with contents of tuple + if len(args) == 1 and isinstance(args[0], tuple): + args = args[0] + + if self.threaded and self.thread_id != tklib.Tcl_GetCurrentThread(): + # We cannot call the command directly. Instead, we must + # marshal the parameters to the interpreter thread. + raise NotImplementedError("Call from another thread") + + # Allocate new array of object pointers. + objects = tkffi.new("Tcl_Obj*[]", len(args)) + argc = len(args) + try: + for i, arg in enumerate(args): + if arg is None: + argc = i + break + obj = AsObj(arg) + tklib.Tcl_IncrRefCount(obj) + objects[i] = obj + + with self._tcl_lock: + res = tklib.Tcl_EvalObjv(self.interp, argc, objects, flags) + if res == tklib.TCL_ERROR: + self.raiseTclError() + else: + result = self._callResult() + finally: + for obj in objects: + if obj: + tklib.Tcl_DecrRefCount(obj) + return result + + def _callResult(self): + assert self._wantobjects + value = tklib.Tcl_GetObjResult(self.interp) + # Not sure whether the IncrRef is necessary, but something + # may overwrite the interpreter result while we are + # converting it. + tklib.Tcl_IncrRefCount(value) + res = FromObj(self, value) + tklib.Tcl_DecrRefCount(value) + return res + + def eval(self, script): + self._check_tcl_appartment() + with self._tcl_lock: + res = tklib.Tcl_Eval(self.interp, script) + if res == tklib.TCL_ERROR: + self.raiseTclError() + return FromTclString(tkffi.string(tklib.Tcl_GetStringResult(self.interp))) + + def evalfile(self, filename): + self._check_tcl_appartment() + with self._tcl_lock: + res = tklib.Tcl_EvalFile(self.interp, filename) + if res == tklib.TCL_ERROR: + self.raiseTclError() + return FromTclString(tkffi.string(tklib.Tcl_GetStringResult(self.interp))) + + def split(self, arg): + if isinstance(arg, TclObject): + objc = tkffi.new("int*") + objv = tkffi.new("Tcl_Obj***") + status = tklib.Tcl_ListObjGetElements(self.interp, arg._value, objc, objv) + if status == tklib.TCL_ERROR: + return FromObj(self, arg._value) + if objc == 0: + return '' + elif objc == 1: + return FromObj(self, objv[0][0]) + result = [] + for i in range(objc[0]): + result.append(FromObj(self, objv[0][i])) + return tuple(result) + elif isinstance(arg, tuple): + return self._splitObj(arg) + elif isinstance(arg, str): + arg = ToTCLString(arg) + return self._split(arg) + + def splitlist(self, arg): + if isinstance(arg, TclObject): + objc = tkffi.new("int*") + objv = tkffi.new("Tcl_Obj***") + status = tklib.Tcl_ListObjGetElements(self.interp, arg._value, objc, objv) + if status == tklib.TCL_ERROR: + self.raiseTclError() + result = [] + for i in range(objc[0]): + result.append(FromObj(self, objv[0][i])) + return tuple(result) + elif isinstance(arg, tuple): + return arg + elif isinstance(arg, str): + arg = ToTCLString(arg) + + argc = tkffi.new("int*") + argv = tkffi.new("char***") + res = tklib.Tcl_SplitList(self.interp, arg, argc, argv) + if res == tklib.TCL_ERROR: + self.raiseTclError() + + result = tuple(FromTclString(tkffi.string(argv[0][i])) + for i in range(argc[0])) + tklib.Tcl_Free(argv[0]) + return result + + def _splitObj(self, arg): + if isinstance(arg, tuple): + size = len(arg) + result = None + # Recursively invoke SplitObj for all tuple items. + # If this does not return a new object, no action is + # needed. + for i in range(size): + elem = arg[i] + newelem = self._splitObj(elem) + if result is None: + if newelem == elem: + continue + result = [None] * size + for k in range(i): + result[k] = arg[k] + result[i] = newelem + if result is not None: + return tuple(result) + elif isinstance(arg, str): + argc = tkffi.new("int*") + argv = tkffi.new("char***") + if isinstance(arg, str): + arg = ToTCLString(arg) + list_ = str(arg) + res = tklib.Tcl_SplitList(tkffi.NULL, list_, argc, argv) + if res != tklib.TCL_OK: + return arg + tklib.Tcl_Free(argv[0]) + if argc[0] > 1: + return self._split(list_) + return arg + + def _split(self, arg): + argc = tkffi.new("int*") + argv = tkffi.new("char***") + res = tklib.Tcl_SplitList(tkffi.NULL, arg, argc, argv) + if res == tklib.TCL_ERROR: + # Not a list. + # Could be a quoted string containing funnies, e.g. {"}. + # Return the string itself. + return arg + + # TODO: Is this method called from Python and Python str is expected, or are TCL strings expected? + try: + if argc[0] == 0: + return "" + elif argc[0] == 1: + return FromTclString(tkffi.string(argv[0][0])) + else: + return tuple(self._split(argv[0][i]) + for i in range(argc[0])) + finally: + tklib.Tcl_Free(argv[0]) + + def getboolean(self, s): + if isinstance(s, int): + return bool(s) + if isinstance(s, str): + s = ToTCLString(s) + if b'\x00' in s: + raise TypeError + v = tkffi.new("int*") + res = tklib.Tcl_GetBoolean(self.interp, s, v) + if res == tklib.TCL_ERROR: + self.raiseTclError() + return bool(v[0]) + + def getint(self, s): + if isinstance(s, int): + return s + if isinstance(s, str): + s = ToTCLString(s) + if b'\x00' in s: + raise TypeError + if tklib.HAVE_LIBTOMMATH or tklib.HAVE_WIDE_INT_TYPE: + value = tklib.Tcl_NewStringObj(s, -1) + if not value: + self.raiseTclError() + try: + if tklib.HAVE_LIBTOMMATH: + return FromBignumObj(self, value) + else: + return FromWideIntObj(self, value) + finally: + tklib.Tcl_DecrRefCount(value) + else: + v = tkffi.new("int*") + res = tklib.Tcl_GetInt(self.interp, s, v) + if res == tklib.TCL_ERROR: + self.raiseTclError() + return v[0] + + def getdouble(self, s): + if isinstance(s, float): + return s + if isinstance(s, str): + s = ToTCLString(s) + if b'\x00' in s: + raise TypeError + v = tkffi.new("double*") + res = tklib.Tcl_GetDouble(self.interp, s, v) + if res == tklib.TCL_ERROR: + self.raiseTclError() + return v[0] + + def exprboolean(self, s): + if b'\x00' in s: + raise TypeError + v = tkffi.new("int*") + res = tklib.Tcl_ExprBoolean(self.interp, ToTCLString(s), v) + if res == tklib.TCL_ERROR: + self.raiseTclError() + return v[0] + + def exprlong(self, s): + if b'\x00' in s: + raise TypeError + v = tkffi.new("long*") + res = tklib.Tcl_ExprLong(self.interp, ToTCLString(s), v) + if res == tklib.TCL_ERROR: + self.raiseTclError() + return v[0] + + def exprdouble(self, s): + if b'\x00' in s: + raise TypeError + v = tkffi.new("double*") + res = tklib.Tcl_ExprDouble(self.interp, ToTCLString(s), v) + if res == tklib.TCL_ERROR: + self.raiseTclError() + return v[0] + + def exprstring(self, s): + if b'\x00' in s: + raise TypeError + res = tklib.Tcl_ExprString(self.interp, ToTCLString(s)) + if res == tklib.TCL_ERROR: + self.raiseTclError() + return tkffi.string(tklib.Tcl_GetStringResult(self.interp)) + + def mainloop(self, threshold): + self._check_tcl_appartment() + self.dispatching = True + while (tklib.Tk_GetNumMainWindows() > threshold and + not self.quitMainLoop and not self.errorInCmd): + + if self.threaded: + result = tklib.Tcl_DoOneEvent(0) + else: + with self._tcl_lock: + result = tklib.Tcl_DoOneEvent(tklib.TCL_DONT_WAIT) + if result == 0: + time.sleep(self._busywaitinterval) + + if result < 0: + break + self.dispatching = False + self.quitMainLoop = False + if self.errorInCmd: + self.errorInCmd = False + raise self.exc_info[0](self.exc_info[1]).with_traceback(self.exc_info[2]) + + def quit(self): + self.quitMainLoop = True + + def _createbytearray(self, buf): + """Convert Python string or any buffer compatible object to Tcl + byte-array object. Use it to pass binary data (e.g. image's + data) to Tcl/Tk commands.""" + cdata = tkffi.new("char[]", buf) + res = tklib.Tcl_NewByteArrayObj(cdata, len(buf)) + if not res: + self.raiseTclError() + return TclObject(res) + \ No newline at end of file diff --git a/graalpython/lib-python/3/_tkinter/tclobj.py b/graalpython/lib-python/3/_tkinter/tclobj.py new file mode 100644 index 0000000000..10408e7ed2 --- /dev/null +++ b/graalpython/lib-python/3/_tkinter/tclobj.py @@ -0,0 +1,236 @@ +# TclObject, conversions with Python objects + +from .tklib_cffi import ffi as tkffi, lib as tklib +import binascii + +class TypeCache(object): + def __init__(self): + self.OldBooleanType = tklib.Tcl_GetObjType(b"boolean") + self.BooleanType = None + self.ByteArrayType = tklib.Tcl_GetObjType(b"bytearray") + self.DoubleType = tklib.Tcl_GetObjType(b"double") + self.IntType = tklib.Tcl_GetObjType(b"int") + self.WideIntType = tklib.Tcl_GetObjType(b"wideInt") + self.BigNumType = None + self.ListType = tklib.Tcl_GetObjType(b"list") + self.ProcBodyType = tklib.Tcl_GetObjType(b"procbody") + self.StringType = tklib.Tcl_GetObjType(b"string") + + def add_extra_types(self, app): + # Some types are not registered in Tcl. + result = app.call('expr', 'true') + typePtr = AsObj(result).typePtr + if FromTclString(tkffi.string(typePtr.name)) == "booleanString": + self.BooleanType = typePtr + + result = app.call('expr', '2**63') + typePtr = AsObj(result).typePtr + if FromTclString(tkffi.string(typePtr.name)) == "bignum": + self.BigNumType = typePtr + + +# Interprets a TCL string (untyped char array) as a Python str using UTF-8. +# This assumes that TCL encodes its return values as UTF-8, not UTF-16. +# TODO: Find out whether this assumption is correct. +def FromTclString(s: bytes) -> str: + try: + return s.decode("utf-8") + except UnicodeDecodeError: + # Tcl encodes null character as \xc0\x80 + return s.replace(b'\xc0\x80', b'\x00')\ + .decode('utf-8') + +# Encodes a Python str as UTF-8 (assuming TCL encodes its API strings as UTF-8 as well, not UTF-16). +# TODO: Find out whether this is correct. +def ToTCLString(s: str) -> bytes: + return s.encode("utf-8")\ + .replace(b"\x00", b"\xc0\x80") + + +# Only when tklib.HAVE_WIDE_INT_TYPE. +def FromWideIntObj(app, value): + wide = tkffi.new("Tcl_WideInt*") + if tklib.Tcl_GetWideIntFromObj(app.interp, value, wide) != tklib.TCL_OK: + app.raiseTclError() + return int(wide[0]) + +# Only when tklib.HAVE_LIBTOMMATH! +def FromBignumObj(app, value): + bigValue = tkffi.new("mp_int*") + if tklib.Tcl_GetBignumFromObj(app.interp, value, bigValue) != tklib.TCL_OK: + app.raiseTclError() + try: + numBytes = tklib.mp_unsigned_bin_size(bigValue) + buf = tkffi.new("unsigned char[]", numBytes) + bufSize_ptr = tkffi.new("unsigned long*", numBytes) + if tklib.mp_to_unsigned_bin_n( + bigValue, buf, bufSize_ptr) != tklib.MP_OKAY: + raise MemoryError + if bufSize_ptr[0] == 0: + return 0 + bytes = tkffi.buffer(buf)[0:bufSize_ptr[0]] + sign = -1 if bigValue.sign == tklib.MP_NEG else 1 + return int(sign * int(binascii.hexlify(bytes), 16)) + finally: + tklib.mp_clear(bigValue) + +def AsBignumObj(value): + sign = -1 if value < 0 else 1 + hexstr = '%x' % abs(value) + bigValue = tkffi.new("mp_int*") + tklib.mp_init(bigValue) + try: + if tklib.mp_read_radix(bigValue, hexstr, 16) != tklib.MP_OKAY: + raise MemoryError + bigValue.sign = tklib.MP_NEG if value < 0 else tklib.MP_ZPOS + return tklib.Tcl_NewBignumObj(bigValue) + finally: + tklib.mp_clear(bigValue) + + +def FromObj(app, value): + """Convert a TclObj pointer into a Python object.""" + typeCache = app._typeCache + if not value.typePtr: + buf = tkffi.buffer(value.bytes, value.length) + return FromTclString(buf[:]) + + if value.typePtr in (typeCache.BooleanType, typeCache.OldBooleanType): + value_ptr = tkffi.new("int*") + if tklib.Tcl_GetBooleanFromObj( + app.interp, value, value_ptr) == tklib.TCL_ERROR: + app.raiseTclError() + return bool(value_ptr[0]) + if value.typePtr == typeCache.ByteArrayType: + size = tkffi.new('int*') + data = tklib.Tcl_GetByteArrayFromObj(value, size) + return tkffi.buffer(data, size[0])[:] + if value.typePtr == typeCache.DoubleType: + return value.internalRep.doubleValue + if value.typePtr == typeCache.IntType: + return value.internalRep.longValue + if value.typePtr == typeCache.WideIntType: + return FromWideIntObj(app, value) + if value.typePtr == typeCache.BigNumType and tklib.HAVE_LIBTOMMATH: + return FromBignumObj(app, value) + if value.typePtr == typeCache.ListType: + size = tkffi.new('int*') + status = tklib.Tcl_ListObjLength(app.interp, value, size) + if status == tklib.TCL_ERROR: + app.raiseTclError() + result = [] + tcl_elem = tkffi.new("Tcl_Obj**") + for i in range(size[0]): + status = tklib.Tcl_ListObjIndex(app.interp, + value, i, tcl_elem) + if status == tklib.TCL_ERROR: + app.raiseTclError() + result.append(FromObj(app, tcl_elem[0])) + return tuple(result) + if value.typePtr == typeCache.ProcBodyType: + pass # fall through and return tcl object. + if value.typePtr == typeCache.StringType: + buf = tklib.Tcl_GetUnicode(value) + length = tklib.Tcl_GetCharLength(value) + buf = tkffi.buffer(tkffi.cast("char*", buf), length*2)[:] + return buf.decode('utf-16') + + return TclObject(value) + +def AsObj(value): + if isinstance(value, str): + # TCL uses UTF-16 internally (https://www.tcl.tk/man/tcl8.4/TclCmd/encoding.html) + # But this function takes UTF-8 (https://linux.die.net/man/3/tcl_newstringobj#:~:text=array%20of%20UTF%2D8%2Dencoded%20bytes) + return tklib.Tcl_NewStringObj(ToTCLString(value), len(value)) + if isinstance(value, bool): + return tklib.Tcl_NewBooleanObj(value) + if isinstance(value, int): + try: + return tklib.Tcl_NewLongObj(value) + except OverflowError: + # 64-bit windows + if tklib.HAVE_WIDE_INT_TYPE: + return tklib.Tcl_NewWideIntObj(value) + else: + import sys + t, v, tb = sys.exc_info() + raise t(v).with_traceback(tb) + if isinstance(value, int): + try: + tkffi.new("long[]", [value]) + except OverflowError: + pass + else: + return tklib.Tcl_NewLongObj(value) + if tklib.HAVE_WIDE_INT_TYPE: + try: + tkffi.new("Tcl_WideInt[]", [value]) + except OverflowError: + pass + else: + return tklib.Tcl_NewWideIntObj(value) + if tklib.HAVE_LIBTOMMATH: + return AsBignumObj(value) + + if isinstance(value, float): + return tklib.Tcl_NewDoubleObj(value) + if isinstance(value, tuple): + argv = tkffi.new("Tcl_Obj*[]", len(value)) + for i in range(len(value)): + argv[i] = AsObj(value[i]) + return tklib.Tcl_NewListObj(len(value), argv) + if isinstance(value, str): + # TODO: Remnant of Python2's unicode type. What happens when our string contains unicode characters? + # Should we encode it as UTF-8 or UTF-16? + raise NotImplementedError + encoded = value.encode('utf-16')[2:] + buf = tkffi.new("char[]", encoded) + inbuf = tkffi.cast("Tcl_UniChar*", buf) + return tklib.Tcl_NewUnicodeObj(inbuf, len(encoded)/2) + if isinstance(value, TclObject): + return value._value + + return AsObj(str(value)) + +class TclObject(object): + def __new__(cls, value): + self = object.__new__(cls) + tklib.Tcl_IncrRefCount(value) + self._value = value + self._string = None + return self + + def __del__(self): + tklib.Tcl_DecrRefCount(self._value) + + def __str__(self): + if self._string and isinstance(self._string, str): + return self._string + return FromTclString(tkffi.string(tklib.Tcl_GetString(self._value))) + + def __repr__(self): + return "<%s object at 0x%x>" % ( + self.typename, tkffi.cast("intptr_t", self._value)) + + def __eq__(self, other): + if not isinstance(other, TclObject): + return NotImplemented + return self._value == other._value + + @property + def typename(self): + return FromTclString(tkffi.string(self._value.typePtr.name)) + + @property + def string(self): + if self._string is None: + length = tkffi.new("int*") + s = tklib.Tcl_GetStringFromObj(self._value, length) + value = tkffi.buffer(s, length[0])[:] + try: + value.decode('ascii') + except UnicodeDecodeError: + value = value.decode('utf8') + self._string = value + return self._string + \ No newline at end of file diff --git a/graalpython/lib-python/3/_tkinter/tklib_build.py b/graalpython/lib-python/3/_tkinter/tklib_build.py new file mode 100644 index 0000000000..0c3ba776e4 --- /dev/null +++ b/graalpython/lib-python/3/_tkinter/tklib_build.py @@ -0,0 +1,248 @@ +# C bindings with libtcl and libtk. + +from cffi import FFI +import sys, os + +# XXX find a better way to detect paths +# XXX pick up CPPFLAGS and LDFLAGS and add to these paths? +if sys.platform.startswith("openbsd"): + incdirs = ['/usr/local/include/tcl8.5', '/usr/local/include/tk8.5', '/usr/X11R6/include'] + linklibs = ['tk85', 'tcl85'] + libdirs = ['/usr/local/lib', '/usr/X11R6/lib'] +elif sys.platform.startswith("freebsd"): + incdirs = ['/usr/local/include/tcl8.6', '/usr/local/include/tk8.6', '/usr/local/include/X11', '/usr/local/include'] + linklibs = ['tk86', 'tcl86'] + libdirs = ['/usr/local/lib'] +elif sys.platform == 'win32': + incdirs = [] + linklibs = ['tcl86t', 'tk86t'] + libdirs = [] +elif sys.platform == 'darwin': + # homebrew + homebrew = os.environ.get('HOMEBREW_PREFIX', '') + incdirs = ['/usr/local/opt/tcl-tk/include/tcl-tk'] + linklibs = ['tcl8.6', 'tk8.6'] + libdirs = [] + if homebrew: + incdirs.append(homebrew + '/include/tcl-tk') + libdirs.append(homebrew + '/opt/tcl-tk/lib') +else: + # On some Linux distributions, the tcl and tk libraries are + # stored in /usr/include, so we must check this case also + libdirs = [] + found = False + for _ver in ['', '8.6', '8.5']: + incdirs = ['/usr/include/tcl' + _ver] + linklibs = ['tcl' + _ver, 'tk' + _ver] + if os.path.isdir(incdirs[0]): + found = True + break + if not found: + for _ver in ['8.6', '8.5', '']: + incdirs = [] + linklibs = ['tcl' + _ver, 'tk' + _ver] + for lib in ['/usr/lib/lib', '/usr/lib64/lib']: + if os.path.isfile(''.join([lib, linklibs[1], '.so'])): + found = True + break + if found: + break + if not found: + sys.stderr.write("*** TCL libraries not found! Falling back...\n") + incdirs = [] + linklibs = ['tcl', 'tk'] + +config_ffi = FFI() +config_ffi.cdef(""" +#define TK_HEX_VERSION ... +#define HAVE_WIDE_INT_TYPE ... +""") +config_lib = config_ffi.verify(""" +#include +#define TK_HEX_VERSION ((TK_MAJOR_VERSION << 24) | \ + (TK_MINOR_VERSION << 16) | \ + (TK_RELEASE_LEVEL << 8) | \ + (TK_RELEASE_SERIAL << 0)) +#ifdef TCL_WIDE_INT_TYPE +#define HAVE_WIDE_INT_TYPE 1 +#else +#define HAVE_WIDE_INT_TYPE 0 +#endif +""", +include_dirs=incdirs, +libraries=linklibs, +library_dirs = libdirs +) + +TK_HEX_VERSION = config_lib.TK_HEX_VERSION + +HAVE_LIBTOMMATH = int((0x08050208 <= TK_HEX_VERSION < 0x08060000) or + (0x08060200 <= TK_HEX_VERSION)) +HAVE_WIDE_INT_TYPE = config_lib.HAVE_WIDE_INT_TYPE + +tkffi = FFI() + +tkffi.cdef(""" +char *get_tk_version(); +char *get_tcl_version(); +#define HAVE_LIBTOMMATH ... +#define HAVE_WIDE_INT_TYPE ... + +#define TCL_READABLE ... +#define TCL_WRITABLE ... +#define TCL_EXCEPTION ... +#define TCL_ERROR ... +#define TCL_OK ... + +#define TCL_LEAVE_ERR_MSG ... +#define TCL_GLOBAL_ONLY ... +#define TCL_EVAL_DIRECT ... +#define TCL_EVAL_GLOBAL ... + +#define TCL_DONT_WAIT ... + +typedef unsigned short Tcl_UniChar; +typedef ... Tcl_Interp; +typedef ...* Tcl_ThreadId; +typedef ...* Tcl_Command; + +typedef struct Tcl_ObjType { + const char *name; + ...; +} Tcl_ObjType; +typedef struct Tcl_Obj { + char *bytes; + int length; + const Tcl_ObjType *typePtr; + union { /* The internal representation: */ + long longValue; /* - an long integer value. */ + double doubleValue; /* - a double-precision floating value. */ + struct { /* - internal rep as two pointers. */ + void *ptr1; + void *ptr2; + } twoPtrValue; + } internalRep; + ...; +} Tcl_Obj; + +Tcl_Interp *Tcl_CreateInterp(); +void Tcl_DeleteInterp(Tcl_Interp* interp); +int Tcl_Init(Tcl_Interp* interp); +int Tk_Init(Tcl_Interp* interp); + +void Tcl_Free(void* ptr); + +const char *Tcl_SetVar(Tcl_Interp* interp, const char* varName, const char* newValue, int flags); +const char *Tcl_SetVar2(Tcl_Interp* interp, const char* name1, const char* name2, const char* newValue, int flags); +const char *Tcl_GetVar(Tcl_Interp* interp, const char* varName, int flags); +Tcl_Obj *Tcl_SetVar2Ex(Tcl_Interp* interp, const char* name1, const char* name2, Tcl_Obj* newValuePtr, int flags); +Tcl_Obj *Tcl_GetVar2Ex(Tcl_Interp* interp, const char* name1, const char* name2, int flags); +int Tcl_UnsetVar2(Tcl_Interp* interp, const char* name1, const char* name2, int flags); +const Tcl_ObjType *Tcl_GetObjType(const char* typeName); + +Tcl_Obj *Tcl_NewStringObj(const char* bytes, int length); +Tcl_Obj *Tcl_NewUnicodeObj(const Tcl_UniChar* unicode, int numChars); +Tcl_Obj *Tcl_NewLongObj(long longValue); +Tcl_Obj *Tcl_NewBooleanObj(int boolValue); +Tcl_Obj *Tcl_NewDoubleObj(double doubleValue); + +void Tcl_IncrRefCount(Tcl_Obj* objPtr); +void Tcl_DecrRefCount(Tcl_Obj* objPtr); + +int Tcl_GetBoolean(Tcl_Interp* interp, const char* src, int* boolPtr); +int Tcl_GetInt(Tcl_Interp* interp, const char* src, int* intPtr); +int Tcl_GetDouble(Tcl_Interp* interp, const char* src, double* doublePtr); +int Tcl_GetBooleanFromObj(Tcl_Interp* interp, Tcl_Obj* objPtr, int* valuePtr); +char *Tcl_GetString(Tcl_Obj* objPtr); +char *Tcl_GetStringFromObj(Tcl_Obj* objPtr, int* lengthPtr); +unsigned char *Tcl_GetByteArrayFromObj(Tcl_Obj* objPtr, int* lengthPtr); +Tcl_Obj *Tcl_NewByteArrayObj(unsigned char *bytes, int length); + +int Tcl_ExprBoolean(Tcl_Interp* interp, const char *expr, int *booleanPtr); +int Tcl_ExprLong(Tcl_Interp* interp, const char *expr, long* longPtr); +int Tcl_ExprDouble(Tcl_Interp* interp, const char *expr, double* doublePtr); +int Tcl_ExprString(Tcl_Interp* interp, const char *expr); + +Tcl_UniChar *Tcl_GetUnicode(Tcl_Obj* objPtr); +int Tcl_GetCharLength(Tcl_Obj* objPtr); + +Tcl_Obj *Tcl_NewListObj(int objc, Tcl_Obj* const objv[]); +int Tcl_ListObjGetElements(Tcl_Interp *interp, Tcl_Obj *listPtr, int *objcPtr, Tcl_Obj ***objvPtr); +int Tcl_ListObjLength(Tcl_Interp* interp, Tcl_Obj* listPtr, int* intPtr); +int Tcl_ListObjIndex(Tcl_Interp* interp, Tcl_Obj* listPtr, int index, Tcl_Obj** objPtrPtr); +int Tcl_SplitList(Tcl_Interp* interp, char* list, int* argcPtr, const char*** argvPtr); + +int Tcl_Eval(Tcl_Interp* interp, const char* script); +int Tcl_EvalFile(Tcl_Interp* interp, const char* filename); +int Tcl_EvalObjv(Tcl_Interp* interp, int objc, Tcl_Obj** objv, int flags); +Tcl_Obj *Tcl_GetObjResult(Tcl_Interp* interp); +const char *Tcl_GetStringResult(Tcl_Interp* interp); +void Tcl_SetObjResult(Tcl_Interp* interp, Tcl_Obj* objPtr); + +typedef void* ClientData; +typedef int Tcl_CmdProc( + ClientData clientData, + Tcl_Interp *interp, + int argc, + const char *argv[]); +typedef void Tcl_CmdDeleteProc( + ClientData clientData); +Tcl_Command Tcl_CreateCommand(Tcl_Interp* interp, const char* cmdName, Tcl_CmdProc proc, ClientData clientData, Tcl_CmdDeleteProc deleteProc); +int Tcl_DeleteCommand(Tcl_Interp* interp, const char* cmdName); + +Tcl_ThreadId Tcl_GetCurrentThread(); +int Tcl_DoOneEvent(int flags); + +int Tk_GetNumMainWindows(); +void Tcl_FindExecutable(char *argv0); +""") + +if HAVE_WIDE_INT_TYPE: + tkffi.cdef(""" +typedef int... Tcl_WideInt; + +int Tcl_GetWideIntFromObj(Tcl_Interp *interp, Tcl_Obj *obj, Tcl_WideInt *value); +Tcl_Obj *Tcl_NewWideIntObj(Tcl_WideInt value); +""") + +if HAVE_LIBTOMMATH: + tkffi.cdef(""" +#define MP_OKAY ... +#define MP_ZPOS ... +#define MP_NEG ... +typedef struct { + int sign; + ...; +} mp_int; + +int Tcl_GetBignumFromObj(Tcl_Interp *interp, Tcl_Obj *obj, mp_int *value); +Tcl_Obj *Tcl_NewBignumObj(mp_int *value); + +int mp_unsigned_bin_size(mp_int *a); +int mp_to_unsigned_bin_n(mp_int * a, unsigned char *b, unsigned long *outlen); +int mp_read_radix(mp_int *a, const char *str, int radix); +int mp_init(mp_int *a); +void mp_clear(mp_int *a); +""") + +tkffi.set_source("_tkinter.tklib_cffi", """ +#define HAVE_LIBTOMMATH %(HAVE_LIBTOMMATH)s +#define HAVE_WIDE_INT_TYPE %(HAVE_WIDE_INT_TYPE)s +#include +#include + +#if HAVE_LIBTOMMATH +#include +#endif + +char *get_tk_version(void) { return TK_VERSION; } +char *get_tcl_version(void) { return TCL_VERSION; } +""" % globals(), +include_dirs=incdirs, +libraries=linklibs, +library_dirs = libdirs +) + +if __name__ == "__main__": + tkffi.compile(os.path.join(os.path.dirname(sys.argv[0]), '..')) + \ No newline at end of file