Shipping a library for ctypes¶
If your package is a thin ctypes (or cffi) wrapper around a CMake-built
shared library, rather than a compiled Python extension, the goal is to install
the library alongside your Python code and then locate it at runtime without
hard-coding a path. This has the nice property that a single wheel works on
every Python version, since the library does not touch the Python ABI. The
tradeoff is that you have to find and load the library yourself.
Install the library next to your Python code¶
Point the install destination at your package directory so the library lands in
site-packages/mypackage/ next to __init__.py:
install(TARGETS mylib DESTINATION mypackage)
The destination is relative to the platlib (${SKBUILD_PLATLIB_DIR}) by
default; you can name any of the install trees
explicitly if you need to. If you set wheel.install-dir = "mypackage", then
the destination is relative to that instead, and a bare DESTINATION . works.
Find it at runtime with importlib.resources¶
Do not compute the path relative to __file__ — that assumes the package
lives on a real filesystem, which is not guaranteed (it could be in a zip, and
in an editable install the Python source and the compiled library live in
different directories). Use importlib.resources instead, which
scikit-build-core’s editable installs fully support:
import ctypes
import sys
from importlib.resources import files
# Pick the right suffix for the platform.
_suffix = {"win32": ".dll", "darwin": ".dylib"}.get(sys.platform, ".so")
_lib = files("mypackage") / f"libmylib{_suffix}"
lib = ctypes.CDLL(str(_lib))
For the general (zip-safe) case, wrap the traversable in
importlib.resources.as_file, which extracts the resource to a real path if
necessary. Because ctypes needs the file to remain on disk for the lifetime of
the process, keep the context manager open — for example with an
contextlib.ExitStack closed at interpreter exit:
import atexit, ctypes
from contextlib import ExitStack
from importlib.resources import files, as_file
_files = ExitStack()
atexit.register(_files.close)
_lib = _files.enter_context(as_file(files("mypackage") / f"libmylib{_suffix}"))
lib = ctypes.CDLL(str(_lib))
Editable installs and rebuilds¶
In redirect-mode editable installs (the default), importlib.resources finds
the compiled library through the redirecting finder, so the code above works
unchanged. Note that accessing a resource does not trigger a rebuild — plain
libraries are not importable modules, so the automatic editable.rebuild
on-import hook does not fire for them. To pick up C/C++ changes, either request
a rebuild explicitly (this works whenever a persistent build-dir is set, with
or without editable.rebuild)…
import mypackage
mypackage.__loader__.rebuild()
…or import a real extension module from the same project first, which does fire
the on-import hook when editable.rebuild is enabled. See
Triggering a rebuild manually for the details and the build-dir
requirement.
Runtime search paths¶
If your shipped library links against other shared libraries, you still need
to make those discoverable at load time (RPATH on Linux/macOS,
os.add_dll_directory on Windows). See Differences Between Unix and Windows for the full set of
options, including wheel-repair tools.