Source code for msl.loadlib.load_library

"""
Load a shared library.
"""
import ctypes
import ctypes.util
import os
import subprocess
import sys

from . import DEFAULT_EXTENSION
from . import IS_PYTHON2
from . import IS_WINDOWS
from . import utils

_encoding = sys.getfilesystemencoding()


[docs]class LoadLibrary(object): LIBTYPES = ['cdll', 'windll', 'oledll', 'net', 'clr', 'java', 'com', 'activex'] """The library types that are supported.""" def __init__(self, path, libtype=None, **kwargs): """Load a shared library. For example, a C/C++, FORTRAN, C#, Java, Delphi, LabVIEW, ActiveX, ... library. .. versionchanged:: 0.4 Added support for Java archives. .. versionchanged:: 0.5 Added support for COM_ libraries. .. versionchanged:: 0.9 Added support for ActiveX_ libraries. .. _Assembly: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly .. _comtypes.CreateObject: https://pythonhosted.org/comtypes/#creating-and-accessing-com-objects .. _COM: https://en.wikipedia.org/wiki/Component_Object_Model .. _ActiveX: https://en.wikipedia.org/wiki/ActiveX Parameters ---------- path : :class:`str` The path to the shared library. The search order for finding the shared library is: 1. assume that a full or a relative (to the current working directory) path is specified, 2. use :func:`ctypes.util.find_library` to find the shared library file, 3. search :data:`sys.path`, then 4. search :data:`os.environ['PATH'] <os.environ>` to find the shared library. If loading a COM_ library then `path` is either the `ProgID`, e.g. ``"InternetExplorer.Application"``, or the `CLSID`, e.g. ``"{2F7860A2-1473-4D75-827D-6C4E27600CAC}"``. libtype : :class:`str`, optional The library type. The following values are currently supported: * ``'cdll'`` -- for a library that uses the __cdecl calling convention * ``'windll'`` or ``'oledll'`` -- for a __stdcall calling convention * ``'net'`` or ``'clr'`` -- for Microsoft's .NET Framework (Common Language Runtime) * ``'java'`` -- for a Java archive, ``.jar``, or Java byte code, ``.class``, file * ``'com'`` -- for a COM_ library * ``'activex'`` -- for an ActiveX_ library Default is ``'cdll'``. .. tip:: Since the ``.jar`` or ``.class`` extension uniquely defines a Java library, the `libtype` will be automatically set to ``'java'`` if `path` ends with ``.jar`` or ``.class``. **kwargs All additional keyword arguments are passed to the object that loads the library. If `libtype` is * ``'cdll'`` then :class:`~ctypes.CDLL` * ``'windll'`` then :class:`~ctypes.WinDLL` * ``'oledll'`` then :class:`~ctypes.OleDLL` * ``'net'`` or ``'clr'`` then all keyword arguments are ignored * ``'java'`` then :class:`~.py4j.java_gateway.JavaGateway` * ``'com'`` then comtypes.CreateObject_ * ``'activex'`` then :meth:`Application.load <msl.loadlib.activex.Application.load>` Raises ------ OSError If the shared library cannot be loaded. ValueError If the value of `libtype` is not supported. """ # a reference to the shared library self._lib = None # a reference to the .NET Runtime Assembly self._assembly = None # a reference to the Py4J JavaGateway self._gateway = None if not path: raise ValueError('You must specify the path, got {!r}'.format(path)) # fixes Issue #8, if `path` is a <class 'pathlib.Path'> object if hasattr(path, 'as_posix'): path = path.as_posix() # try to automatically determine the libtype if libtype is None: if path.endswith('.jar') or path.endswith('.class'): libtype = 'java' else: libtype = 'cdll' else: libtype = libtype.lower() if libtype not in LoadLibrary.LIBTYPES: raise ValueError( 'Cannot load libtype={!r}.\n' 'Must be one of: {}'.format(libtype, ', '.join(LoadLibrary.LIBTYPES)) ) # create a new reference to `path` just in case the # DEFAULT_EXTENSION is appended below so that the # ctypes.util.find_library function call will use the # unmodified value of `path` _path = path # assume a default extension if no extension was provided ext = os.path.splitext(path)[1] if not ext and libtype not in ['java', 'com', 'activex']: _path += DEFAULT_EXTENSION if IS_PYTHON2: _path = _path.encode(_encoding) if libtype not in ['com', 'activex']: self._path = os.path.abspath(_path) if not os.path.isfile(self._path): # for find_library use the original 'path' value since it may be a library name # without any prefix like 'lib', suffix like '.so', '.dylib' or version number self._path = ctypes.util.find_library(path) if self._path is None: # then search sys.path and os.environ['PATH'] success = False search_dirs = sys.path + os.environ['PATH'].split(os.pathsep) for directory in search_dirs: p = os.path.join(directory, _path) if os.path.isfile(p): self._path = p success = True break if not success: raise OSError("Cannot find '{}' for libtype='{}'".format(path, libtype)) else: self._path = _path if libtype == 'cdll': self._lib = ctypes.CDLL(self._path, **kwargs) elif libtype == 'windll': self._lib = ctypes.WinDLL(self._path, **kwargs) elif libtype == 'oledll': self._lib = ctypes.OleDLL(self._path, **kwargs) elif libtype == 'com': if not utils.is_comtypes_installed(): raise OSError( 'Cannot load a COM library because comtypes is not installed.\n' 'Run: pip install comtypes' ) from comtypes import GUID from comtypes.client import CreateObject try: clsid = GUID.from_progid(self._path) except (TypeError, OSError): clsid = None if clsid is None: raise OSError("Cannot find '{}' for libtype='com'".format(path)) self._lib = CreateObject(clsid, **kwargs) elif libtype == 'activex': from .activex import Application self._lib = Application.load(self._path, **kwargs) elif libtype == 'java': if not utils.is_py4j_installed(): raise OSError( 'Cannot load a Java file because Py4J is not installed.\n' 'Run: pip install py4j' ) from py4j.version import __version__ from py4j.java_gateway import JavaGateway, GatewayParameters # the address and port to use to host the py4j.GatewayServer address = kwargs.pop('address', '127.0.0.1') port = kwargs.pop('port', utils.get_available_port()) # find the py4j*.jar file (needed to import the py4j.GatewayServer on the Java side) filename = 'py4j' + __version__ + '.jar' py4j_jar = os.environ.get('PY4J_JAR') if py4j_jar: if not os.path.isfile(py4j_jar) or os.path.basename(py4j_jar) != filename: raise OSError( 'A PY4J_JAR environment variable exists, ' 'but the full path to the {} file is invalid\n' 'PY4J_JAR={}'.format(filename, py4j_jar) ) else: root = os.path.dirname(sys.executable) for item in [root, os.path.dirname(root), os.path.join(os.path.expanduser('~'), '.local')]: py4j_jar = os.path.join(item, 'share', 'py4j', filename) if os.path.isfile(py4j_jar): break if not os.path.isfile(py4j_jar): raise OSError( 'Cannot find {0}\nCreate a PY4J_JAR environment ' 'variable to be equal to the full path to {0}'.format(filename) ) # build the java command wrapper = os.path.join(os.path.dirname(__file__), 'py4j-wrapper.jar') cmd = ['java', '-cp', py4j_jar + os.pathsep + wrapper, 'Py4JWrapper', str(port)] # from the URLClassLoader documentation: # Any URL that ends with a '/' is assumed to refer to a directory. Otherwise, the URL # is assumed to refer to a JAR file which will be downloaded and opened as needed. if ext == '.jar': cmd.append(self._path) else: # it is a .class file cmd.append(os.path.dirname(self._path) + '/') err = None try: # start the py4j.GatewayServer flags = 0x08000000 if IS_WINDOWS else 0 # fixes issue 31, CREATE_NO_WINDOW = 0x08000000 subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, creationflags=flags) except OSError as e: err = str(e).rstrip() err += '\nYou must have a Java Runtime Environment installed and available on PATH' if err: raise OSError(err) try: utils.wait_for_server(address, port, 10.0) except OSError as e: err = str(e).rstrip() err += '\nCould not start the Py4J GatewayServer' if err: raise OSError(err) self._gateway = JavaGateway( gateway_parameters=GatewayParameters(address=address, port=port, **kwargs) ) self._lib = self._gateway.jvm elif libtype == 'net' or libtype == 'clr': if not utils.is_pythonnet_installed(): raise OSError( 'Cannot load a .NET Assembly because pythonnet is not installed.\n' 'Run: pip install pythonnet' ) import clr import System dotnet = {'System': System} # the shared library must be available in sys.path head, tail = os.path.split(self._path) sys.path.insert(0, head) try: # don't include the library extension clr.AddReference(os.path.splitext(tail)[0]) except (System.IO.FileNotFoundException, System.IO.FileLoadException): # The file must exist since its existence is checked above. # There must be another reason why loading the DLL raises this # error. Calling LoadFile (below) provides more information # in the error message. pass try: # By default, pythonnet can only load libraries that are for .NET 4.0+ # # In order to allow pythonnet to load a library from .NET <4.0 the # useLegacyV2RuntimeActivationPolicy property needs to be enabled # in a <python-executable>.config file. If the following statement # raises a FileLoadException then attempt to create the configuration # file that has the property enabled and then notify the user why # loading the library failed and ask them to re-run their Python # script to load the .NET library. self._assembly = System.Reflection.Assembly.LoadFile(self._path) except System.IO.FileLoadException as err: # Example error message that can occur if the library is for .NET <4.0, # and the useLegacyV2RuntimeActivationPolicy is not enabled: # # " Mixed mode assembly is built against version 'v2.0.50727' of the # runtime and cannot be loaded in the 4.0 runtime without additional # configuration information. " if str(err).startswith('Mixed mode assembly is built against version'): status, msg = utils.check_dot_net_config(sys.executable) if not status == 0: raise OSError(msg) else: update_msg = 'Checking .NET config returned "{}" '.format(msg) update_msg += 'and still cannot load library.\n' update_msg += str(err) raise OSError(update_msg) raise OSError('The above "System.IO.FileLoadException" is not handled.\n') try: types = self._assembly.GetTypes() except Exception as e: utils.logger.error(e) utils.logger.error('The LoaderExceptions are:') for item in e.LoaderExceptions: utils.logger.error(' %s', item.Message) else: for t in types: try: if t.Namespace: obj = __import__(t.Namespace) else: obj = getattr(__import__('clr'), t.FullName) except: obj = t obj.__name__ = t.FullName if obj.__name__ not in dotnet: dotnet[obj.__name__] = obj self._lib = DotNet(dotnet, self._path) else: assert False, 'Should not get here -- contact developers' if IS_PYTHON2: self._path = self._path.decode(_encoding) utils.logger.debug('Loaded %s', self._path) def __del__(self): if hasattr(self, '_gateway'): self.cleanup() def __repr__(self): path = self._path.encode(_encoding) if IS_PYTHON2 else self._path return '<LoadLibrary libtype={} path={}>'.format(self._lib.__class__.__name__, path) def __enter__(self): return self def __exit__(self, *exc): self.cleanup()
[docs] def cleanup(self): """Clean up references to the library. .. versionadded:: 0.10.0 """ self._assembly = None self._lib = None if self._gateway: self._gateway.shutdown() self._gateway = None utils.logger.debug('shutdown Py4J.GatewayServer')
@property def assembly(self): """ Returns a reference to the `.NET Runtime Assembly <Assembly_>`_ object if the shared library is a .NET Framework otherwise returns :data:`None`. .. tip:: The `JetBrains dotPeek`_ program can be used to reliably decompile any .NET Assembly into the equivalent source code. .. _JetBrains dotPeek: https://www.jetbrains.com/decompiler/ """ return self._assembly @property def gateway(self): """ Returns the :class:`~py4j.java_gateway.JavaGateway` object, only if the shared library is a Java archive, otherwise returns :data:`None`. """ return self._gateway @property def lib(self): """Returns the reference to the loaded library object. For example, if `libtype` is * ``'cdll'`` then a :class:`~ctypes.CDLL` object * ``'windll'`` then a :class:`~ctypes.WinDLL` object * ``'oledll'`` then a :class:`~ctypes.OleDLL` object * ``'net'`` or ``'clr'`` then a :class:`~.load_library.DotNet` object * ``'java'`` then a :class:`~py4j.java_gateway.JVMView` object * ``'com'`` or ``'activex'`` then an interface pointer to the COM_ object """ return self._lib @property def path(self): """:class:`str`: The path to the shared library file.""" return self._path
[docs]class DotNet(object): def __init__(self, dot_net_dict, path): """Contains the namespace_ modules, classes and `System.Type`_ objects of a .NET Assembly. Do not instantiate this class directly. .. _namespace: https://msdn.microsoft.com/en-us/library/z2kcy19k.aspx .. _System.Type: https://docs.microsoft.com/en-us/dotnet/api/system.type """ self.__dict__.update(dot_net_dict) self._path = path def __repr__(self): return '<{} path={}>'.format(self.__class__.__name__, self._path)