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