From 426ea9f985f0c18a5b16743f07f484bf65d153b0 Mon Sep 17 00:00:00 2001 From: Emery Conrad Date: Tue, 25 Aug 2026 12:40:52 -0500 Subject: [PATCH 1/2] Add a regression test for the load_library failure reason Cpp::LoadLibrary drops the loader message, so load_library asks the loader again with dlopen and reports its dlerror text. Co-developed-with-the-help-of: Claude Code (Opus 5, human in the loop) --- python/cppjit/__init__.py | 26 +++++++++++++++++++++++++- test/test_basic_api.py | 23 +++++++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/python/cppjit/__init__.py b/python/cppjit/__init__.py index 6280d6e..4b2613a 100644 --- a/python/cppjit/__init__.py +++ b/python/cppjit/__init__.py @@ -276,12 +276,36 @@ def macro(cppm): raise ValueError("Failed to evaluate macro %s", cppm) +def _dlopen_reason(name): + """Ask the loader why `name` will not load; Cpp::LoadLibrary drops the reason.""" + + # NOW, not LAZY: an unresolved symbol must fail here, before any global ctor runs + RTLD_NOW = 0x2 + try: + libc = ctypes.CDLL(None) + libc.dlopen.restype = ctypes.c_void_p + libc.dlopen.argtypes = [ctypes.c_char_p, ctypes.c_int] + libc.dlclose.argtypes = [ctypes.c_void_p] + libc.dlerror.restype = ctypes.c_char_p + libc.dlerror() # discard any stale message + handle = libc.dlopen(os.fsencode(name), RTLD_NOW) + if handle: + libc.dlclose(handle) + return "" + reason = libc.dlerror() + except Exception: + return "" + return reason.decode("utf-8", "replace") if reason else "" + + def load_library(name): """Explicitly load a shared library.""" with _stderr_capture() as err: result = gbl.Cpp.LoadLibrary(name, True) if result == False: # noqa: E712 - raise RuntimeError('Could not load library "%s": %s' % (name, err.err)) + raise RuntimeError( + 'Could not load library "%s": %s' % (name, err.err or _dlopen_reason(name)) + ) return True diff --git a/test/test_basic_api.py b/test/test_basic_api.py index 10e2e81..d62621f 100644 --- a/test/test_basic_api.py +++ b/test/test_basic_api.py @@ -1,9 +1,10 @@ +import os import shutil import tempfile import py -from pytest import raises -from support import setup_make +from pytest import mark, raises +from support import IS_LINUX, setup_make # reuse the example01 currpath = py.path.local(__file__).dirpath() @@ -57,6 +58,24 @@ def test03_add_library_path(self): shutil.copyfile(test_dct + ".so", tpath + "/test.so") cppjit.load_library("test.so") + @mark.skipif(IS_LINUX == 0, reason="checks Linux dlerror text") + def test03a_load_library_failure_reason(self): + """load_library reports the loader's failure reason""" + + import cppjit + + with tempfile.TemporaryDirectory() as tpath: + missing = os.path.join(tpath, "libdlerrmissing.so") + with raises(RuntimeError, match="No such file or directory"): + cppjit.load_library(missing) + + # a truncated ELF header: found on disk, rejected by the loader + invalid = os.path.join(tpath, "libdlerrinvalid.so") + with open(invalid, "wb") as out: + out.write(b"\x7fELF" + b"\0" * 12) + with raises(RuntimeError, match="file too short"): + cppjit.load_library(invalid) + def test04_add_include_path(self): import cppjit From dbabc27266198eb27fdae4b1a493881a469f531a Mon Sep 17 00:00:00 2001 From: Emery Conrad Date: Thu, 3 Sep 2026 12:41:07 -0500 Subject: [PATCH 2/2] Read the load_library failure reason from CppInterOp Cpp::LoadLibrary now hands back the loader's reason through an optional out-parameter (compiler-research/CppInterOp#1107), so the ctypes re-dlopen probe goes away. The pin bump to a CppInterOp commit that carries #1107 is folded in when it lands. Co-developed-with-the-help-of: Claude Code (Fable 5.1, human in the loop) --- python/cppjit/__init__.py | 30 +++--------------------------- test/test_basic_api.py | 6 +++--- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/python/cppjit/__init__.py b/python/cppjit/__init__.py index 4b2613a..6193fc1 100644 --- a/python/cppjit/__init__.py +++ b/python/cppjit/__init__.py @@ -276,36 +276,12 @@ def macro(cppm): raise ValueError("Failed to evaluate macro %s", cppm) -def _dlopen_reason(name): - """Ask the loader why `name` will not load; Cpp::LoadLibrary drops the reason.""" - - # NOW, not LAZY: an unresolved symbol must fail here, before any global ctor runs - RTLD_NOW = 0x2 - try: - libc = ctypes.CDLL(None) - libc.dlopen.restype = ctypes.c_void_p - libc.dlopen.argtypes = [ctypes.c_char_p, ctypes.c_int] - libc.dlclose.argtypes = [ctypes.c_void_p] - libc.dlerror.restype = ctypes.c_char_p - libc.dlerror() # discard any stale message - handle = libc.dlopen(os.fsencode(name), RTLD_NOW) - if handle: - libc.dlclose(handle) - return "" - reason = libc.dlerror() - except Exception: - return "" - return reason.decode("utf-8", "replace") if reason else "" - - def load_library(name): """Explicitly load a shared library.""" - with _stderr_capture() as err: - result = gbl.Cpp.LoadLibrary(name, True) + reason = gbl.std.string() + result = gbl.Cpp.LoadLibrary(name, True, reason) if result == False: # noqa: E712 - raise RuntimeError( - 'Could not load library "%s": %s' % (name, err.err or _dlopen_reason(name)) - ) + raise RuntimeError('Could not load library "%s": %s' % (name, reason)) return True diff --git a/test/test_basic_api.py b/test/test_basic_api.py index d62621f..c85d0fc 100644 --- a/test/test_basic_api.py +++ b/test/test_basic_api.py @@ -66,14 +66,14 @@ def test03a_load_library_failure_reason(self): with tempfile.TemporaryDirectory() as tpath: missing = os.path.join(tpath, "libdlerrmissing.so") - with raises(RuntimeError, match="No such file or directory"): + with raises(RuntimeError, match="libdlerrmissing.*library not found"): cppjit.load_library(missing) - # a truncated ELF header: found on disk, rejected by the loader + # a truncated ELF header: found on disk, rejected before dlopen invalid = os.path.join(tpath, "libdlerrinvalid.so") with open(invalid, "wb") as out: out.write(b"\x7fELF" + b"\0" * 12) - with raises(RuntimeError, match="file too short"): + with raises(RuntimeError, match="libdlerrinvalid"): cppjit.load_library(invalid) def test04_add_include_path(self):