1"""DLL wrapper""" 2import os 3import sys 4import warnings 5from ctypes import CDLL, POINTER, Structure, c_uint8, cast, addressof 6from ctypes.util import find_library 7 8# Prints warning without stack or line info 9def prettywarn(msg, warntype): 10 """Prints a suppressable warning without stack or line info.""" 11 original = warnings.formatwarning 12 def _pretty_fmt(message, category, filename, lineno, line=None): 13 return "{0}: {1}\n".format(category.__name__, message) 14 warnings.formatwarning = _pretty_fmt 15 warnings.warn(msg, warntype) 16 warnings.formatwarning = original 17 18# Use DLLs from pysdl2-dll, if installed and DLL path not explicitly set 19try: 20 prepath = os.getenv('PYSDL2_DLL_PATH') 21 import sdl2dll 22 postpath = os.getenv('PYSDL2_DLL_PATH') 23 if prepath != postpath: 24 msg = "Using SDL2 binaries from pysdl2-dll {0}" 25 prettywarn(msg.format(sdl2dll.__version__), UserWarning) 26except ImportError: 27 pass 28 29__all__ = ["DLL", "nullfunc"] 30 31 32# Gets a usable pointer from an SDL2 ctypes object 33def get_pointer(ctypes_obj): 34 pointer_type = POINTER(type(ctypes_obj)) 35 return cast(addressof(ctypes_obj), pointer_type) 36 37 38# For determining DLL version on load 39class SDL_version(Structure): 40 _fields_ = [("major", c_uint8), 41 ("minor", c_uint8), 42 ("patch", c_uint8), 43 ] 44 45 46def _using_ms_store_python(): 47 """Checks if the Python interpreter was installed from the Microsoft Store.""" 48 return 'WindowsApps\\PythonSoftwareFoundation.' in sys.executable 49 50 51def _preload_deps(libname, dllpath): 52 """Preloads all DLLs that SDL2 and its extensions link to (e.g. libFLAC). 53 54 This is required for Python installed from the Microsoft Store, which has 55 strict DLL loading rules but will allow loading of DLLs that have already 56 been loaded by the current process. 57 """ 58 deps = { 59 "SDL2": [], 60 "SDL2_ttf": ["freetype"], 61 "SDL2_image": ["zlib", "jpeg", "png16", "tiff", "webp"], 62 "SDL2_mixer": ["modplug", "mpg123", "ogg", "vorbis", "vorbisfile", 63 "opus", "opusfile", "FLAC"], 64 "SDL2_gfx": [] 65 } 66 preloaded = {} 67 dlldir = os.path.abspath(os.path.join(dllpath, os.pardir)) 68 all_dlls = [f for f in os.listdir(dlldir) if f.split(".")[-1] == "dll"] 69 for name in deps[libname]: 70 dllname = name if name == "zlib" else "lib{0}-".format(name) 71 for dll in all_dlls: 72 if dll.startswith(dllname): 73 try: 74 filepath = os.path.join(dlldir, dll) 75 preloaded[name] = CDLL(filepath) 76 except OSError: 77 pass 78 break 79 80 if len(preloaded) < len(deps[libname]): 81 e = ("Unable to preload all dependencies for {0}. This module may not " 82 "work correctly.") 83 prettywarn(e.format(libname), RuntimeWarning) 84 85 return preloaded 86 87 88def _findlib(libnames, path=None): 89 """Finds SDL2 libraries and returns them in a list, with libraries found in the directory 90 optionally specified by 'path' being first (taking precedence) and libraries found in system 91 search paths following. 92 """ 93 94 platform = sys.platform 95 if platform == "win32": 96 patterns = ["{0}.dll"] 97 elif platform == "darwin": 98 patterns = ["lib{0}.dylib", "{0}.framework/{0}", "{0}.framework/Versions/A/{0}"] 99 else: 100 patterns = ["lib{0}.so"] 101 102 # Adding the potential 'd' suffix that is present on the library 103 # when built in debug configuration 104 searchfor = libnames + [libname + 'd' for libname in libnames] 105 results = [] 106 if path and path.lower() != "system": 107 # First, find any libraries matching pattern exactly within given path 108 for libname in searchfor: 109 for subpath in str.split(path, os.pathsep): 110 for pattern in patterns: 111 dllfile = os.path.join(subpath, pattern.format(libname)) 112 if os.path.exists(dllfile): 113 results.append(dllfile) 114 115 # Next, on Linux and similar, find any libraries with version suffixes matching pattern 116 # (e.g. libSDL2.so.2) at path and add them in descending version order (i.e. newest first) 117 if platform not in ("win32", "darwin"): 118 versioned = [] 119 files = os.listdir(path) 120 for f in files: 121 for libname in searchfor: 122 dllname = "lib{0}.so".format(libname) 123 if dllname in f and not (dllname == f or f.startswith(".")): 124 versioned.append(os.path.join(path, f)) 125 versioned.sort(key = _so_version_num, reverse = True) 126 results = results + versioned 127 128 # Finally, search for library in system library search paths 129 for libname in searchfor: 130 dllfile = find_library(libname) 131 if dllfile: 132 # For Python 3.8+ on Windows, need to specify relative or full path 133 if os.name == "nt" and not ("/" in dllfile or "\\" in dllfile): 134 dllfile = "./" + dllfile 135 results.append(dllfile) 136 137 return results 138 139 140class DLLWarning(Warning): 141 pass 142 143 144class DLL(object): 145 """Function wrapper around the different DLL functions. Do not use or 146 instantiate this one directly from your user code. 147 """ 148 def __init__(self, libinfo, libnames, path=None): 149 self._dll = None 150 self._deps = None 151 self._libname = libinfo 152 self._version = None 153 minversions = { 154 "SDL2": 2005, 155 "SDL2_mixer": 2001, 156 "SDL2_ttf": 2014, 157 "SDL2_image": 2001, 158 "SDL2_gfx": 1003 159 } 160 foundlibs = _findlib(libnames, path) 161 dllmsg = "PYSDL2_DLL_PATH: %s" % (os.getenv("PYSDL2_DLL_PATH") or "unset") 162 if len(foundlibs) == 0: 163 raise RuntimeError("could not find any library for %s (%s)" % 164 (libinfo, dllmsg)) 165 for libfile in foundlibs: 166 try: 167 self._dll = CDLL(libfile) 168 self._libfile = libfile 169 self._version = self._get_version(libinfo, self._dll) 170 if self._version < minversions[libinfo]: 171 versionstr = self._version_int_to_str(self._version) 172 minimumstr = self._version_int_to_str(minversions[libinfo]) 173 err = "{0} (v{1}) is too old to be used by py-sdl2" 174 err += " (minimum v{0})".format(minimumstr) 175 raise RuntimeError(err.format(libfile, versionstr)) 176 break 177 except Exception as exc: 178 # Could not load the DLL, move to the next, but inform the user 179 # about something weird going on - this may become noisy, but 180 # is better than confusing the users with the RuntimeError below 181 self._dll = None 182 warnings.warn(repr(exc), DLLWarning) 183 if self._dll is None: 184 raise RuntimeError("found %s, but it's not usable for the library %s" % 185 (foundlibs, libinfo)) 186 if _using_ms_store_python(): 187 self._deps = _preload_deps(libinfo, self._libfile) 188 if path is not None and sys.platform in ("win32",) and \ 189 path in self._libfile: 190 os.environ["PATH"] = "%s;%s" % (path, os.environ["PATH"]) 191 192 def bind_function(self, funcname, args=None, returns=None, added=None): 193 """Binds the passed argument and return value types to the specified 194 function. If the version of the loaded library is older than the 195 version where the function was added, an informative exception will 196 be raised if the bound function is called. 197 198 Args: 199 funcname (str): The name of the function to bind. 200 args (List or None, optional): The data types of the C function's 201 arguments. Should be 'None' if function takes no arguments. 202 returns (optional): The return type of the bound C function. Should 203 be 'None' if function returns 'void'. 204 added (str, optional): The version of the library in which the 205 function was added, in the format '2.x.x'. 206 """ 207 func = getattr(self._dll, funcname, None) 208 min_version = self._version_str_to_int(added) if added else None 209 if not func: 210 versionstr = self._version_int_to_str(self._version) 211 if min_version and min_version > self._version: 212 e = "'{0}' requires {1} <= {2}, but the loaded version is {3}." 213 errmsg = e.format(funcname, self._libname, added, versionstr) 214 return _unavailable(errmsg) 215 else: 216 e = "Could not find function '%s' in %s (%s)" 217 libver = self._libname + ' ' + versionstr 218 raise ValueError(e % (funcname, self._libfile, libver)) 219 func.argtypes = args 220 func.restype = returns 221 return func 222 223 def _version_str_to_int(self, s): 224 v = [int(n) for n in s.split('.')] 225 return v[0] * 1000 + v[1] * 100 + v[2] 226 227 def _version_int_to_str(self, i): 228 v = str(i) 229 v = [v[0], v[1], str(int(v[2:4]))] 230 return ".".join(v) 231 232 def _get_version(self, libname, dll): 233 """Gets the version of the linked SDL library""" 234 if libname == "SDL2": 235 dll.SDL_GetVersion.argtypes = [POINTER(SDL_version)] 236 v = SDL_version() 237 dll.SDL_GetVersion(v) 238 else: 239 if libname == "SDL2_mixer": 240 func = dll.Mix_Linked_Version 241 elif libname == "SDL2_ttf": 242 func = dll.TTF_Linked_Version 243 elif libname == "SDL2_image": 244 func = dll.IMG_Linked_Version 245 elif libname == "SDL2_gfx": 246 return 1004 # not supported in SDL2_gfx, so just assume latest 247 func.argtypes = None 248 func.restype = POINTER(SDL_version) 249 v = func().contents 250 return v.major * 1000 + v.minor * 100 + v.patch 251 252 @property 253 def libfile(self): 254 """str: The filename of the loaded library.""" 255 return self._libfile 256 257 @property 258 def version(self): 259 """int: The version of the loaded library in the form of a 4-digit 260 integer (e.g. '2008' for SDL 2.0.8, '2010' for SDL 2.0.10). 261 """ 262 return self._version 263 264 265def _unavailable(err): 266 """A wrapper that raises a RuntimeError if a function is not supported.""" 267 def wrapper(*fargs, **kw): 268 raise RuntimeError(err) 269 return wrapper 270 271def _nonexistent(funcname, func): 272 """A simple wrapper to mark functions and methods as nonexistent.""" 273 def wrapper(*fargs, **kw): 274 warnings.warn("%s does not exist" % funcname, 275 category=RuntimeWarning, stacklevel=2) 276 return func(*fargs, **kw) 277 wrapper.__name__ = func.__name__ 278 return wrapper 279 280 281def _so_version_num(libname): 282 """Extracts the version number from an .so filename as a list of ints.""" 283 return list(map(int, libname.split('.so.')[1].split('.'))) 284 285 286def nullfunc(*args): 287 """A simple no-op function to be used as dll replacement.""" 288 return 289 290try: 291 dll = DLL("SDL2", ["SDL2", "SDL2-2.0", "SDL2-2.0.0"], os.getenv("PYSDL2_DLL_PATH")) 292except RuntimeError as exc: 293 raise ImportError(exc) 294 295def get_dll_file(): 296 """Gets the file name of the loaded SDL2 library.""" 297 return dll.libfile 298 299_bind = dll.bind_function 300version = dll.version 301