From 569dc4b9f697cb47e8dd76a6db078d13a9fcc701 Mon Sep 17 00:00:00 2001 From: geisserml Date: Thu, 28 Sep 2023 19:10:59 +0200 Subject: [PATCH] Major cleanup (new library loader, rm multilib, rm stdcall) This removes support for multiple different libraries in one file, which IOHO was an inherent design mistake messing up the code - pypdfium2 ctypesgen is now single-lib. We also replace the old, bloated library loader with a new lean one. In the future, we may want an option to plug in a custom library loader so we can import config from pypdfium2. This also removes support for the windows-only stdcall convention because it's not needed for pypdfium2 and probably an edge case. It would cause some additional complexity and we're not sure what would be the cleanest way to integrate it, so this can wait. Fun fact: It seems like ctypesgen prior to this change added the complie_libdirs to the runtime library loader. Further, relative paths (I think) were never correctly interpreted relative to the file's directory, but to the user's CWD, which is useless and could even cause unexpected behavior. So runtime_libdirs = ["."] didn't work, but the fallback below "then we search the directory where the generated python interface is stored" captured. --- ctypesgen/__main__.py | 19 +- ctypesgen/libraryloader.py | 442 +++------------------------- ctypesgen/printer_json/printer.py | 9 +- ctypesgen/printer_python/printer.py | 238 +++++++-------- ctypesgen/processor/operations.py | 51 ++-- tests/ctypesgentest.py | 2 +- 6 files changed, 171 insertions(+), 590 deletions(-) diff --git a/ctypesgen/__main__.py b/ctypesgen/__main__.py index b576d8a9..f92f0f1f 100644 --- a/ctypesgen/__main__.py +++ b/ctypesgen/__main__.py @@ -45,20 +45,20 @@ def main(givenargs=None): nargs="+", help="Sequence of header files", ) - parser.add_argument( - "-o", - "--output", - metavar="FILE", - help="write wrapper to FILE [default stdout]", - ) parser.add_argument( "-l", - "--libraries", - nargs="+", + "--library", + required=True, default=[], metavar="LIBRARY", help="link to LIBRARY", ) + parser.add_argument( + "-o", + "--output", + metavar="FILE", + help="write wrapper to FILE [default stdout]", + ) parser.add_argument( "--other-headers", nargs="+", @@ -322,8 +322,7 @@ def main(givenargs=None): # Figure out what names will be defined by imported Python modules args.other_known_names = find_names_in_modules(args.modules) - if len(args.libraries) == 0: - msgs.warning_message("No libraries specified", cls="usage") + assert args.library # Fetch printer for the requested output language if args.output_language == "py": diff --git a/ctypesgen/libraryloader.py b/ctypesgen/libraryloader.py index 19073d26..c77e1a1b 100644 --- a/ctypesgen/libraryloader.py +++ b/ctypesgen/libraryloader.py @@ -1,410 +1,36 @@ -""" -Load libraries - appropriately for all our supported platforms -""" -# ---------------------------------------------------------------------------- -# Copyright (c) 2008 David James -# Copyright (c) 2006-2008 Alex Holkner -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in -# the documentation and/or other materials provided with the -# distribution. -# * Neither the name of pyglet nor the names of its -# contributors may be used to endorse or promote products -# derived from this software without specific prior written -# permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# ---------------------------------------------------------------------------- - +import sys import ctypes import ctypes.util -import glob -import os.path -import platform -import re -import sys - - -def _environ_path(name): - """Split an environment variable into a path-like list elements""" - if name in os.environ: - return os.environ[name].split(":") - return [] - - -class LibraryLoader: - """ - A base class For loading of libraries ;-) - Subclasses load libraries for specific platforms. - """ - - # library names formatted specifically for platforms - name_formats = ["%s"] - - class Lookup: - """Looking up calling conventions for a platform""" - - mode = ctypes.DEFAULT_MODE - - def __init__(self, path): - super(LibraryLoader.Lookup, self).__init__() - self.access = dict(cdecl=ctypes.CDLL(path, self.mode)) - - def get(self, name, calling_convention="cdecl"): - """Return the given name according to the selected calling convention""" - if calling_convention not in self.access: - raise LookupError( - "Unknown calling convention '{}' for function '{}'".format( - calling_convention, name - ) - ) - return getattr(self.access[calling_convention], name) - - def has(self, name, calling_convention="cdecl"): - """Return True if this given calling convention finds the given 'name'""" - if calling_convention not in self.access: - return False - return hasattr(self.access[calling_convention], name) - - def __getattr__(self, name): - return getattr(self.access["cdecl"], name) - - def __init__(self): - self.other_dirs = [] - - def __call__(self, libname): - """Given the name of a library, load it.""" - paths = self.getpaths(libname) - - for path in paths: - # noinspection PyBroadException - try: - return self.Lookup(path) - except Exception: # pylint: disable=broad-except - pass - - raise ImportError("Could not load %s." % libname) - - def getpaths(self, libname): - """Return a list of paths where the library might be found.""" - if os.path.isabs(libname): - yield libname - else: - # search through a prioritized series of locations for the library - - # we first search any specific directories identified by user - for dir_i in self.other_dirs: - for fmt in self.name_formats: - # dir_i should be absolute already - yield os.path.join(dir_i, fmt % libname) - - # check if this code is even stored in a physical file - try: - this_file = __file__ - except NameError: - this_file = None - - # then we search the directory where the generated python interface is stored - if this_file is not None: - for fmt in self.name_formats: - yield os.path.abspath(os.path.join(os.path.dirname(__file__), fmt % libname)) - - # now, use the ctypes tools to try to find the library - for fmt in self.name_formats: - path = ctypes.util.find_library(fmt % libname) - if path: - yield path - - # then we search all paths identified as platform-specific lib paths - for path in self.getplatformpaths(libname): - yield path - - # Finally, we'll try the users current working directory - for fmt in self.name_formats: - yield os.path.abspath(os.path.join(os.path.curdir, fmt % libname)) - - def getplatformpaths(self, _libname): # pylint: disable=no-self-use - """Return all the library paths available in this platform""" - return [] - - -# Darwin (Mac OS X) - - -class DarwinLibraryLoader(LibraryLoader): - """Library loader for MacOS""" - - name_formats = [ - "lib%s.dylib", - "lib%s.so", - "lib%s.bundle", - "%s.dylib", - "%s.so", - "%s.bundle", - "%s", - ] - - class Lookup(LibraryLoader.Lookup): - """ - Looking up library files for this platform (Darwin aka MacOS) - """ - - # Darwin requires dlopen to be called with mode RTLD_GLOBAL instead - # of the default RTLD_LOCAL. Without this, you end up with - # libraries not being loadable, resulting in "Symbol not found" - # errors - mode = ctypes.RTLD_GLOBAL - - def getplatformpaths(self, libname): - if os.path.pathsep in libname: - names = [libname] - else: - names = [fmt % libname for fmt in self.name_formats] - - for directory in self.getdirs(libname): - for name in names: - yield os.path.join(directory, name) - - @staticmethod - def getdirs(libname): - """Implements the dylib search as specified in Apple documentation: - - http://developer.apple.com/documentation/DeveloperTools/Conceptual/ - DynamicLibraries/Articles/DynamicLibraryUsageGuidelines.html - - Before commencing the standard search, the method first checks - the bundle's ``Frameworks`` directory if the application is running - within a bundle (OS X .app). - """ - - dyld_fallback_library_path = _environ_path("DYLD_FALLBACK_LIBRARY_PATH") - if not dyld_fallback_library_path: - dyld_fallback_library_path = [ - os.path.expanduser("~/lib"), - "/usr/local/lib", - "/usr/lib", - ] - - dirs = [] - - if "/" in libname: - dirs.extend(_environ_path("DYLD_LIBRARY_PATH")) - else: - dirs.extend(_environ_path("LD_LIBRARY_PATH")) - dirs.extend(_environ_path("DYLD_LIBRARY_PATH")) - dirs.extend(_environ_path("LD_RUN_PATH")) - - if hasattr(sys, "frozen") and getattr(sys, "frozen") == "macosx_app": - dirs.append(os.path.join(os.environ["RESOURCEPATH"], "..", "Frameworks")) - - dirs.extend(dyld_fallback_library_path) - - return dirs - - -# Posix - - -class PosixLibraryLoader(LibraryLoader): - """Library loader for POSIX-like systems (including Linux)""" - - _ld_so_cache = None - - _include = re.compile(r"^\s*include\s+(?P.*)") - - name_formats = ["lib%s.so", "%s.so", "%s"] - - class _Directories(dict): - """Deal with directories""" - - def __init__(self): - dict.__init__(self) - self.order = 0 - - def add(self, directory): - """Add a directory to our current set of directories""" - if len(directory) > 1: - directory = directory.rstrip(os.path.sep) - # only adds and updates order if exists and not already in set - if not os.path.exists(directory): - return - order = self.setdefault(directory, self.order) - if order == self.order: - self.order += 1 - - def extend(self, directories): - """Add a list of directories to our set""" - for a_dir in directories: - self.add(a_dir) - - def ordered(self): - """Sort the list of directories""" - return (i[0] for i in sorted(self.items(), key=lambda d: d[1])) - - def _get_ld_so_conf_dirs(self, conf, dirs): - """ - Recursive function to help parse all ld.so.conf files, including proper - handling of the `include` directive. - """ - - try: - with open(conf) as fileobj: - for dirname in fileobj: - dirname = dirname.strip() - if not dirname: - continue - - match = self._include.match(dirname) - if not match: - dirs.add(dirname) - else: - for dir2 in glob.glob(match.group("pattern")): - self._get_ld_so_conf_dirs(dir2, dirs) - except IOError: - pass - - def _create_ld_so_cache(self): - # Recreate search path followed by ld.so. This is going to be - # slow to build, and incorrect (ld.so uses ld.so.cache, which may - # not be up-to-date). Used only as fallback for distros without - # /sbin/ldconfig. - # - # We assume the DT_RPATH and DT_RUNPATH binary sections are omitted. - - directories = self._Directories() - for name in ( - "LD_LIBRARY_PATH", - "SHLIB_PATH", # HP-UX - "LIBPATH", # OS/2, AIX - "LIBRARY_PATH", # BE/OS - ): - if name in os.environ: - directories.extend(os.environ[name].split(os.pathsep)) - - self._get_ld_so_conf_dirs("/etc/ld.so.conf", directories) - - bitage = platform.architecture()[0] - - unix_lib_dirs_list = [] - if bitage.startswith("64"): - # prefer 64 bit if that is our arch - unix_lib_dirs_list += ["/lib64", "/usr/lib64"] - - # must include standard libs, since those paths are also used by 64 bit - # installs - unix_lib_dirs_list += ["/lib", "/usr/lib"] - if sys.platform.startswith("linux"): - # Try and support multiarch work in Ubuntu - # https://wiki.ubuntu.com/MultiarchSpec - if bitage.startswith("32"): - # Assume Intel/AMD x86 compat - unix_lib_dirs_list += ["/lib/i386-linux-gnu", "/usr/lib/i386-linux-gnu"] - elif bitage.startswith("64"): - # Assume Intel/AMD x86 compatible - unix_lib_dirs_list += [ - "/lib/x86_64-linux-gnu", - "/usr/lib/x86_64-linux-gnu", - ] - else: - # guess... - unix_lib_dirs_list += glob.glob("/lib/*linux-gnu") - directories.extend(unix_lib_dirs_list) - - cache = {} - lib_re = re.compile(r"lib(.*)\.s[ol]") - # ext_re = re.compile(r"\.s[ol]$") - for our_dir in directories.ordered(): - try: - for path in glob.glob("%s/*.s[ol]*" % our_dir): - file = os.path.basename(path) - - # Index by filename - cache_i = cache.setdefault(file, set()) - cache_i.add(path) - - # Index by library name - match = lib_re.match(file) - if match: - library = match.group(1) - cache_i = cache.setdefault(library, set()) - cache_i.add(path) - except OSError: - pass - - self._ld_so_cache = cache - - def getplatformpaths(self, libname): - if self._ld_so_cache is None: - self._create_ld_so_cache() - - result = self._ld_so_cache.get(libname, set()) - for i in result: - # we iterate through all found paths for library, since we may have - # actually found multiple architectures or other library types that - # may not load - yield i - - -# Windows - - -class WindowsLibraryLoader(LibraryLoader): - """Library loader for Microsoft Windows""" - - name_formats = ["%s.dll", "lib%s.dll", "%slib.dll", "%s"] - - class Lookup(LibraryLoader.Lookup): - """Lookup class for Windows libraries...""" - - def __init__(self, path): - super(WindowsLibraryLoader.Lookup, self).__init__(path) - self.access["stdcall"] = ctypes.windll.LoadLibrary(path) - - -# Platform switching - -# If your value of sys.platform does not appear in this dict, please contact -# the Ctypesgen maintainers. - -loaderclass = { - "darwin": DarwinLibraryLoader, - "cygwin": WindowsLibraryLoader, - "win32": WindowsLibraryLoader, - "msys": WindowsLibraryLoader, -} - -load_library = loaderclass.get(sys.platform, PosixLibraryLoader)() - - -def add_library_search_dirs(other_dirs): - """ - Add libraries to search paths. - If library paths are relative, convert them to absolute with respect to this - file's directory - """ - for path in other_dirs: - if not os.path.isabs(path): - path = os.path.abspath(path) - load_library.other_dirs.append(path) - - -del loaderclass +from pathlib import Path + + +def _find_library(libname, libdirs): + + if not libdirs: + return ctypes.util.find_library(libname) + + if sys.platform in ("win32", "cygwin", "msys"): + patterns = ["{}.dll", "lib{}.dll", "{}"] + elif sys.platform == "darwin": + patterns = ["lib{}.dylib", "{}.dylib", "lib{}.so", "{}.so", "{}"] + else: # assume unix pattern or plain libname + patterns = ["lib{}.so", "{}.so", "{}"] + + RELDIR = Path(__file__).parent + + for dir in libdirs: + # joining an absolute path silently discardy the path before + dir = (RELDIR / dir).resolve(strict=False) + for pat in patterns: + test_path = dir / pat.format(libname) + if test_path.is_file(): + return test_path + + +def load_library(libname, libdirs): + + libpath = _find_library(libname, libdirs) + if not libpath: + raise RuntimeError(f"Library '{libname}' could not be found in {libdirs if libdirs else 'system'}.") + + return ctypes.CDLL(libpath) diff --git a/ctypesgen/printer_json/printer.py b/ctypesgen/printer_json/printer.py index 5f89fe2c..2949868c 100755 --- a/ctypesgen/printer_json/printer.py +++ b/ctypesgen/printer_json/printer.py @@ -44,7 +44,7 @@ def __init__(self, outpath, options, data): if self.options.strip_build_path and self.options.strip_build_path[-1] != os.path.sep: self.options.strip_build_path += os.path.sep - self.print_group(self.options.libraries, "libraries", self.print_library) + self.print_library(self.options.library) method_table = { "function": self.print_function, @@ -114,15 +114,12 @@ def print_function(self, function): "args": todict(function.argtypes), "return": todict(function.restype), "attrib": function.attrib, + "source": self.options.library, } - if function.source_library: - res["source"] = function.source_library return res def print_variable(self, variable): - res = {"type": "variable", "ctype": todict(variable.ctype), "name": variable.c_name()} - if variable.source_library: - res["source"] = variable.source_library + res = {"type": "variable", "ctype": todict(variable.ctype), "name": variable.c_name(), "source": self.options.library} return res def print_macro(self, macro): diff --git a/ctypesgen/printer_python/printer.py b/ctypesgen/printer_python/printer.py index 4c09964f..50f77ccd 100755 --- a/ctypesgen/printer_python/printer.py +++ b/ctypesgen/printer_python/printer.py @@ -23,49 +23,67 @@ class WrapperPrinter: def __init__(self, outpath, options, data): status_message("Writing to %s." % (outpath or "stdout")) - self.file = open(outpath, "w") if outpath else sys.stdout - self.options = options - - if self.options.strip_build_path and self.options.strip_build_path[-1] != os.path.sep: - self.options.strip_build_path += os.path.sep - - if not self.options.embed_preamble and outpath: - self._copy_preamble_loader_files(outpath) - - self.print_header() - self.file.write("\n") - - self.print_preamble() - self.file.write("\n") - - self.print_loader() - self.file.write("\n") - - self.print_group(self.options.libraries, "libraries", self.print_library) - self.print_group(self.options.modules, "modules", self.print_module) - - method_table = { - "function": self.print_function, - "macro": self.print_macro, - "struct": self.print_struct, - "typedef": self.print_typedef, - "variable": self.print_variable, - "enum": self.print_enum, - "constant": self.print_constant, - "undef": self.print_undef, - } - - for kind, desc in data.output_order: - if desc.included: - method_table[kind](desc) - self.file.write("\n") - - self.print_group(self.options.inserted_files, "inserted files", self.insert_file) - - def __del__(self): - self.file.close() + + try: + self.options = options + + # FIXME(geisserml) see below + if self.options.strip_build_path and self.options.strip_build_path[-1] != os.path.sep: + self.options.strip_build_path += os.path.sep + + if not self.options.embed_preamble and outpath: + self._copy_preamble_loader_files(outpath) + + self.print_header() + self.file.write("\n") + + self.print_preamble() + self.file.write("\n") + + self.print_loader() + self.file.write("\n") + + self.print_library(self.options.library) + self.print_group(self.options.modules, "modules", self.print_module) + + method_table = { + "function": self.print_function, + "macro": self.print_macro, + "struct": self.print_struct, + "typedef": self.print_typedef, + "variable": self.print_variable, + "enum": self.print_enum, + "constant": self.print_constant, + "undef": self.print_undef, + } + + for kind, desc in data.output_order: + if desc.included: + method_table[kind](desc) + self.file.write("\n") + + self.print_group(self.options.inserted_files, "inserted files", self.insert_file) + + finally: + self.file.close() + + + def print_loader(self): + self.file.write("# Begin loader\n\n") + if self.options.embed_preamble: + with open(LIBRARYLOADER_PATH, "r") as loader_file: + self.file.write(loader_file.read()) + else: + self.file.write("from .ctypes_loader import *\n") + self.file.write("\n# End loader\n\n") + def print_library(self, library): + self.file.write( + f'_libdirs = {self.options.runtime_libdirs}\n' + f'_lib = load_library("{library}", _libdirs)\n' + ) + def print_group(self, list, name, function): if list: self.file.write("# Begin %s\n" % name) @@ -193,25 +211,6 @@ def from_param(cls, x): shutil.copyfile(LIBRARYLOADER_PATH, join(dst, "ctypes_loader.py")) - def print_loader(self): - self.file.write("_libs = {}\n") - self.file.write("_libdirs = %s\n\n" % self.options.compile_libdirs) - self.file.write("# Begin loader\n\n") - if self.options.embed_preamble: - with open(LIBRARYLOADER_PATH, "r") as loader_file: - self.file.write(loader_file.read()) - else: - self.file.write("from .ctypes_loader import *\n") - self.file.write("\n# End loader\n\n") - self.file.write( - "add_library_search_dirs([%s])" - % ", ".join([repr(d) for d in self.options.runtime_libdirs]) - ) - self.file.write("\n") - - def print_library(self, library): - self.file.write('_libs["%s"] = load_library("%s")\n' % (library, library)) - def print_module(self, module): self.file.write("from %s import *\n" % module) @@ -311,26 +310,19 @@ def print_function(self, function): def print_fixed_function(self, function): self.srcinfo(function.src) - CC = "stdcall" if function.attrib.get("stdcall", False) else "cdecl" - use_sourcelib = function.source_library or len(self.options.libraries) == 1 - - if use_sourcelib: - lib = function.source_library if function.source_library else self.options.libraries[0] - self.file.write( - 'if _libs["{L}"].has("{CN}", "{CC}"):\n' - ' {PN} = _libs["{L}"].get("{CN}", "{CC}")\n'.format( - L=lib, CN=function.c_name(), PN=function.py_name(), CC=CC - ) - ) - else: - self.file.write( - "for _lib in _libs.values():\n" - ' if not _lib.has("{CN}", "{CC}"):\n' - " continue\n" - ' {PN} = _lib.get("{CN}", "{CC}")\n'.format( - CN=function.c_name(), PN=function.py_name(), CC=CC - ) + # NOTE pypdfium2-ctypesgen currently does not support the windows-only stdcall convention + # this could theoretically be done by adding a second library handle _lib_stdcall = ctypes.WinDLL(...) on windows and using that for stdcall functions + # since this would cause additional complexity and/or produce an unnecessary/invalid handle for a non-stdcall library, it's not implemented ATM + + assert not function.attrib.get("stdcall", False) + + # TODO add option to skip hasattr() guard + self.file.write( + 'if hasattr(_lib, "{CN}"):\n' + ' {PN} = _lib.{CN}\n'.format( + L=self.options.library, CN=function.c_name(), PN=function.py_name(), ) + ) # Argument types self.file.write( @@ -346,74 +338,42 @@ def print_fixed_function(self, function): self.file.write( "\n %s.errcheck = %s" % (function.py_name(), function.errcheck.py_string()) ) - - if not use_sourcelib: - self.file.write("\n break") - + def print_variadic_function(self, function): - CC = "stdcall" if function.attrib.get("stdcall", False) else "cdecl" + # TODO see if we can remove the _variadic_function wrapper and use just plain ctypes + + assert not function.attrib.get("stdcall", False) self.srcinfo(function.src) - if function.source_library: - self.file.write( - 'if _libs["{L}"].has("{CN}", "{CC}"):\n' - ' _func = _libs["{L}"].get("{CN}", "{CC}")\n' - " _restype = {RT}\n" - " _errcheck = {E}\n" - " _argtypes = [{t0}]\n" - " {PN} = _variadic_function(_func,_restype,_argtypes,_errcheck)\n".format( - L=function.source_library, - CN=function.c_name(), - RT=function.restype.py_string(), - E=function.errcheck.py_string(), - t0=", ".join([a.py_string() for a in function.argtypes]), - PN=function.py_name(), - CC=CC, - ) - ) - else: - self.file.write( - "for _lib in _libs.values():\n" - ' if _lib.has("{CN}", "{CC}"):\n' - ' _func = _lib.get("{CN}", "{CC}")\n' - " _restype = {RT}\n" - " _errcheck = {E}\n" - " _argtypes = [{t0}]\n" - " {PN} = _variadic_function(_func,_restype,_argtypes,_errcheck)\n".format( - CN=function.c_name(), - RT=function.restype.py_string(), - E=function.errcheck.py_string(), - t0=", ".join([a.py_string() for a in function.argtypes]), - PN=function.py_name(), - CC=CC, - ) + self.file.write( + 'if hasattr(_lib, {CN}):\n' + ' _func = _lib.{CN}\n' + " _restype = {RT}\n" + " _errcheck = {E}\n" + " _argtypes = [{t0}]\n" + " {PN} = _variadic_function(_func,_restype,_argtypes,_errcheck)\n".format( + L=self.options.library, + CN=function.c_name(), + RT=function.restype.py_string(), + E=function.errcheck.py_string(), + t0=", ".join([a.py_string() for a in function.argtypes]), + PN=function.py_name(), ) + ) def print_variable(self, variable): self.srcinfo(variable.src) - if variable.source_library: - self.file.write( - "try:\n" - ' {PN} = ({PS}).in_dll(_libs["{L}"], "{CN}")\n' - "except:\n" - " pass\n".format( - PN=variable.py_name(), - PS=variable.ctype.py_string(), - L=variable.source_library, - CN=variable.c_name(), - ) - ) - else: - self.file.write( - "for _lib in _libs.values():\n" - " try:\n" - ' {PN} = ({PS}).in_dll(_lib, "{CN}")\n' - " break\n" - " except:\n" - " pass\n".format( - PN=variable.py_name(), PS=variable.ctype.py_string(), CN=variable.c_name() - ) + self.file.write( + "try:\n" + ' {PN} = ({PS}).in_dll(_lib, "{CN}")\n' + "except:\n" + " pass\n".format( + PN=variable.py_name(), + PS=variable.ctype.py_string(), + L=self.options.library, + CN=variable.c_name(), ) + ) def print_macro(self, macro): if macro.params: diff --git a/ctypesgen/processor/operations.py b/ctypesgen/processor/operations.py index 156bec29..58f6c0c7 100644 --- a/ctypesgen/processor/operations.py +++ b/ctypesgen/processor/operations.py @@ -249,30 +249,29 @@ def fix_conflicting_names(data, opts): def find_source_libraries(data, opts): - """find_source_libraries() determines which library contains each function - and variable.""" - - all_symbols = data.functions + data.variables + + # NOTE is_available is not currently used throughout ctypesgen because it's not clear what we should do with the info - we already have hasattr() if-guards anyway. And what is more, in practice, binary and headers should always match, otherwise the caller has probably made a mistake. + + all_symbols = set(data.functions + data.variables) + # default assumption: all symbols available + for symbol in all_symbols: + symbol.is_available = True + + if opts.no_load_library: + status_message(f"Bypass load_library '{opts.library}'.") + return + + try: + library = libraryloader.load_library(opts.library, opts.compile_libdirs) + except RuntimeError: + warning_message(f"Could not load library '{opts.library}'. Okay, I'll try to load it at runtime instead.", cls="missing-library") + return + + missing_symbols = set() for symbol in all_symbols: - symbol.source_library = None # FIXME probably unnecessary? - - libraryloader.add_library_search_dirs(opts.compile_libdirs) - - for library_name in opts.libraries: - if opts.no_load_library: - status_message("Bypass load_library %s." % library_name) - continue - - try: - library = libraryloader.load_library(library_name) - except ImportError: - warning_message( - f"Could not load library '{library_name}'. Okay, I'll try to load it at runtime instead.", - cls="missing-library", - ) - continue - for symbol in all_symbols: - # TODO warn if the same symbol is provided by multiple different libs? - if symbol.source_library is None: - if hasattr(library, symbol.c_name()): - symbol.source_library = library_name + symbol.is_available = hasattr(library, symbol.c_name()) + if not symbol.is_available: + missing_symbols.add(symbol) + + if missing_symbols: + warning_message(f"Some symbols could not be found - binary/headers mismatch suspected. {missing_symbols}", cls="other") diff --git a/tests/ctypesgentest.py b/tests/ctypesgentest.py index a567e2b4..7a7e398c 100644 --- a/tests/ctypesgentest.py +++ b/tests/ctypesgentest.py @@ -224,7 +224,7 @@ def _generate_common(file_name, common_lib, embed_preamble=True): test_options = options.get_default_options() test_options.headers = [f"{COMMON_DIR}/{file_name}.h"] test_options.include_search_paths = [COMMON_DIR] - test_options.libraries = [common_lib] + test_options.library = common_lib test_options.compile_libdirs = [COMMON_DIR] test_options.embed_preamble = embed_preamble if embed_preamble: