Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make symbol resolution lazy #873

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion llvmlite/binding/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@
from .value import *
from .analysis import *
from .object_file import *
from .context import *
from .context import *


def __getattr__(name):
if name == 'llvm_version_info':
return initfini._version_info()
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
112 changes: 85 additions & 27 deletions llvmlite/binding/ffi.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,44 +77,118 @@ def __exit__(self, *exc_details):
self._lock.release()


class _suppress_cleanup_errors:
def __init__(self, context):
self._context = context

def __enter__(self):
return self._context.__enter__()

def __exit__(self, exc_type, exc_value, traceback):
try:
return self._context.__exit__(exc_type, exc_value, traceback)
except PermissionError:
pass # Resource dylibs can't be deleted on Windows.


class _lib_wrapper(object):
"""Wrap libllvmlite with a lock such that only one thread may access it at
a time.

This class duck-types a CDLL.
"""
__slots__ = ['_lib', '_fntab', '_lock']
__slots__ = ['_lib_handle', '_fntab', '_lock']

def __init__(self, lib):
self._lib = lib
def __init__(self):
self._lib_handle = None
self._fntab = {}
self._lock = _LLVMLock()

def _load_lib(self):
try:
with _suppress_cleanup_errors(importlib.resources.path(
__name__.rpartition(".")[0], get_library_name())) as lib_path:
self._lib_handle = ctypes.CDLL(str(lib_path))
# Check that we can look up expected symbols.
_ = self._lib_handle.LLVMPY_GetVersionInfo()
except (OSError, AttributeError) as e:
# OSError may be raised if the file cannot be opened, or is not
# a shared library.
# AttributeError is raised if LLVMPY_GetVersionInfo does not
# exist.
raise OSError("Could not find/load shared object file") from e

@property
def _lib(self):
# Not threadsafe.
if not self._lib_handle:
self._load_lib()
return self._lib_handle

def _resolve(self, lazy_wrapper):
"""Resolve a lazy wrapper, and store it as an attribute."""
with self._lock:
# If not already done, atomically replace a lazy wrapper
# with either a locking wrapper or a direct reference to
# the function (if marked as threadsafe).
wrapper = self._fntab[lazy_wrapper.symbol_name]
if isinstance(wrapper, _lazy_lib_fn_wrapper):
# Nothing else has resolved the lazy wrapper yet.
cfn = getattr(self._lib, lazy_wrapper.symbol_name)
if hasattr(lazy_wrapper, 'argtypes'):
cfn.argtypes = lazy_wrapper.argtypes
if hasattr(lazy_wrapper, 'restype'):
cfn.restype = lazy_wrapper.restype
if getattr(lazy_wrapper, 'threadsafe', False):
# If wrapper.threadsafe is True, then this function
# can safely be called directly without acquiring
# the library lock.
wrapper = cfn
else:
wrapper = _lib_fn_wrapper(self._lock, cfn)
self._fntab[lazy_wrapper.symbol_name] = wrapper
return wrapper

def __getattr__(self, name):
try:
return self._fntab[name]
except KeyError:
# Lazily wraps new functions as they are requested
cfn = getattr(self._lib, name)
wrapped = _lib_fn_wrapper(self._lock, cfn)
self._fntab[name] = wrapped
return wrapped
with self._lock:
# Double checking to avoid wrapper creation race.
try:
return self._fntab[name]
except KeyError:
wrapper = _lazy_lib_fn_wrapper(name)
self._fntab[name] = wrapper
return wrapper

@property
def _name(self):
"""The name of the library passed in the CDLL constructor.

For duck-typing a ctypes.CDLL
"""
return self._lib._name
with self._lock:
return self._lib._name

@property
def _handle(self):
"""The system handle used to access the library.

For duck-typing a ctypes.CDLL
"""
return self._lib._handle
with self._lock:
return self._lib._handle


class _lazy_lib_fn_wrapper(object):
"""A lazy wrapper for a ctypes.CFUNCTYPE that resolves on call."""

def __init__(self, symbol_name):
self.symbol_name = symbol_name

def __call__(self, *args, **kwargs):
return lib._resolve(self)(*args, **kwargs)


class _lib_fn_wrapper(object):
Expand Down Expand Up @@ -151,23 +225,7 @@ def __call__(self, *args, **kwargs):
return self._cfn(*args, **kwargs)


_lib_name = get_library_name()


pkgname = ".".join(__name__.split(".")[0:-1])
try:
_lib_handle = importlib.resources.path(pkgname, _lib_name)
lib = ctypes.CDLL(str(_lib_handle.__enter__()))
# on windows file handles to the dll file remain open after
# loading, therefore we can not exit the context manager
# which might delete the file
except OSError as e:
msg = f"""Could not find/load shared object file: {_lib_name}
Error was: {e}"""
raise OSError(msg)


lib = _lib_wrapper(lib)
lib = _lib_wrapper()


def register_lock_callback(acq_fn, rel_fn):
Expand Down
3 changes: 0 additions & 3 deletions llvmlite/binding/initfini.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,3 @@ def _version_info():
v.append(x & 0xff)
x >>= 8
return tuple(reversed(v))


llvm_version_info = _version_info()
2 changes: 2 additions & 0 deletions llvmlite/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import sys

import multiprocessing
import unittest
from unittest import TestCase

Expand Down Expand Up @@ -53,5 +54,6 @@ def run_tests(suite=None, xmloutput=None, verbosity=1):


def main():
multiprocessing.set_start_method('spawn')
res = run_tests()
sys.exit(0 if res.wasSuccessful() else 1)
65 changes: 65 additions & 0 deletions llvmlite/tests/test_loading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import importlib
import multiprocessing
import unittest
import unittest.mock

from llvmlite import binding as llvm


def _test_dylib_resource_loading(result):
try:
# We must not have loaded the llvmlite dylib yet.
assert llvm.ffi.lib._lib_handle is None
spec = importlib.util.find_spec(llvm.ffi.__name__.rpartition(".")[0])

true_dylib = spec.loader.get_resource_reader() \
.open_resource(llvm.ffi.get_library_name())

# A mock resource reader that does not support resource paths
class MockResourceReader(importlib.abc.ResourceReader):
def is_resource(self, name):
return True

def resource_path(self, name):
# Resource does not have a path, so it must be extracted to the
# filesystem.
raise FileNotFoundError

def open_resource(self, name):
# File-like object, from which the content of the resource
# is extracted.
return true_dylib

def contents(self):
return []

# Mock resource loader to force the dylib to be extracted into a
# temporary file.
with unittest.mock.patch.object(
spec.loader, 'get_resource_reader',
return_value=MockResourceReader()), \
unittest.mock.patch(
'llvmlite.binding.ffi.get_library_name',
return_value='notllvmlite.so'):
llvm.llvm_version_info # force library loading to occur.
except Exception as e:
result.put(e)
raise
result.put(None)


class TestModuleLoading(unittest.TestCase):
def test_dylib_resource_loading(self):
subproc_result = multiprocessing.Queue()
subproc = multiprocessing.Process(
target=_test_dylib_resource_loading,
args=(subproc_result,))
subproc.start()
result = subproc_result.get()
subproc.join()
if subproc.exitcode:
raise result


if __name__ == "__main__":
unittest.main()