Skip to content

Commit e577eac

Browse files
rgommersdnicolodi
authored andcommitted
DOC: add documentation about using shared libraries
1 parent 75a89c3 commit e577eac

File tree

2 files changed

+251
-0
lines changed

2 files changed

+251
-0
lines changed
+250
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
.. SPDX-FileCopyrightText: 2024 The meson-python developers
2+
..
3+
.. SPDX-License-Identifier: MIT
4+
5+
.. _shared-libraries:
6+
7+
**********************
8+
Using shared libraries
9+
**********************
10+
11+
Python projects may build shared libraries as part of their project, or link
12+
with shared libraries from a dependency. This tends to be a common source of
13+
issues, hence this page aims to explain how to include shared libraries in
14+
wheels, any limitations and gotchas, and how support is implemented in
15+
``meson-python`` under the hood.
16+
17+
We distinguish between *internal* shared libraries that are built as part of
18+
the project, and *external* shared libraries that are provided by project
19+
dependencies and that are linked with the project build artifacts.
20+
For internal shared libraries, we also distinguish whether the shared library
21+
is being installed to its default system location (typically
22+
``/usr/local/lib`` on Unix-like systems, and ``C:\\lib`` on Windows - we call
23+
this ``libdir`` in this guide) or to a location in ``site-packages`` within the
24+
Python package install tree. All these scenarios are (or will be) supported,
25+
with some caveats:
26+
27+
+-----------------------+------------------+---------+-------+-------+
28+
| shared library source | install location | Windows | macOS | Linux |
29+
+=======================+==================+=========+=======+=======+
30+
| internal | libdir | no (1) |||
31+
+-----------------------+------------------+---------+-------+-------+
32+
| internal | site-packages ||||
33+
+-----------------------+------------------+---------+-------+-------+
34+
| external | n/a | ✓ (2) |||
35+
+-----------------------+------------------+---------+-------+-------+
36+
37+
.. TODO: add subproject as a source
38+
39+
1: Internal shared libraries on Windows cannot be automatically handled
40+
correctly, and currently ``meson-python`` therefore raises an error for them.
41+
`PR meson-python#551 <https://github.com/mesonbuild/meson-python/pull/551>`__
42+
may improve that situation in the near future.
43+
44+
2: External shared libraries require ``delvewheel`` usage on Windows (or
45+
some equivalent way, like amending the DLL search path to include the directory
46+
in which the external shared library is located). Due to the lack of RPATH
47+
support on Windows, there is no good way around this.
48+
49+
.. _internal-shared-libraries:
50+
51+
Internal shared libraries
52+
=========================
53+
54+
A shared library produced by ``library()`` or ``shared_library()`` built like this
55+
56+
.. code-block:: meson
57+
58+
example_lib = shared_library(
59+
'example',
60+
'examplelib.c',
61+
install: true,
62+
)
63+
64+
is installed to ``libdir`` by default. If the only reason the shared library exists
65+
is to be used inside the Python package being built, then it is best to modify
66+
the install location to be within the Python package itself:
67+
68+
.. code-block:: python
69+
70+
install_path: py.get_install_dir() / 'mypkg/subdir'
71+
72+
Then an extension module in the same install directory can link against the
73+
shared library in a portable manner by using ``install_rpath``:
74+
75+
.. code-block:: meson
76+
77+
py3.extension_module('_extmodule',
78+
'_extmodule.c',
79+
link_with: example_lib,
80+
install: true,
81+
subdir: 'mypkg/subdir',
82+
install_rpath: '$ORIGIN'
83+
)
84+
85+
The above method will work as advertised on macOS and Linux; ``meson-python`` does
86+
nothing special for this case. Windows needs some special handling though, due to
87+
the lack of RPATH support:
88+
89+
.. literalinclude:: ../../tests/packages/sharedlib-in-package/mypkg/__init__.py
90+
:start-after: start-literalinclude
91+
:end-before: end-literalinclude
92+
93+
If an internal shared library is not only used as part of a Python package, but
94+
for example also as a regular shared library in a C/C++ project or as a
95+
standalone library, then the method shown above won't work. The library is`
96+
then marked for installation into the system default ``libdir`` location.
97+
Actually installing into ``libdir`` isn't possible with wheels, hence
98+
``meson-python`` will instead do the following *on platforms other than
99+
Windows*:
100+
101+
1. Install the shared library to ``<project-name>.mesonpy.libs`` (i.e., a
102+
top-level directory in the wheel, which on install will end up in
103+
``site-packages``).
104+
2. Rewrite RPATH entries for install targets that depend on the shared library
105+
to point to that new install location instead.
106+
107+
This will make the shared library work automatically, with no other action needed
108+
from the package author. *However*, currently an error is raised for this situation
109+
on Windows. This is documented also in :ref:`reference-limitations`.
110+
111+
112+
External shared libraries
113+
=========================
114+
115+
External shared libraries are installed somewhere on the build machine, and
116+
usually detected by a ``dependency()`` or ``compiler.find_library()`` call in a
117+
``meson.build`` file. When a Python extension module or executable uses the
118+
dependency, the shared library will be linked against at build time.
119+
120+
If the shared library is located in a directory on the loader search path,
121+
the wheel created by ``meson-python`` will work locally when installed.
122+
If it's in a non-standard location however, the shared library will go
123+
missing at runtime. The Python extension module linked against it needs an
124+
RPATH entry - and Meson will not automatically manage RPATH entries for you.
125+
Hence you'll need to add the needed RPATH yourself, for example by adding
126+
``-Wl,rpath=/path/to/dir/sharedlib/is/in`` to ``LDFLAGS`` before starting
127+
the build. In case you run into this problem after a wheel is built and
128+
installed, adding that same path to ``LD_LIBRARY_PATH`` is a quick way of
129+
checking if that is indeed the problem.
130+
131+
On Windows, the solution is similar - the shared library can either be
132+
preloaded, or the directory that the library is in added to ``PATH`` or with
133+
``os.add_dll_directory``, or vendored into the wheel with ``delvewheel`` in
134+
order to make the built Python package usable locally.
135+
136+
Publishing wheels which depend on external shared libraries
137+
-----------------------------------------------------------
138+
139+
On all platforms, wheels which depend on external shared libraries usually need
140+
post-processing to make them usable on machines other than the one on which
141+
they were built. This is because the RPATH entry for an external shared library
142+
contains a path specific to the build machine. This post-processing is done by
143+
tools like ``auditwheel`` (Linux), ``delvewheel`` (Windows), ``delocate``
144+
(macOS) or ``repair-wheel`` (any platform, wraps the other tools).
145+
146+
Running any of those tools on a wheel produced by ``meson-python`` will vendor
147+
the external shared library into the wheel and rewrite the RPATH entries (it
148+
may also do some other things, like symbol mangling).
149+
150+
On Windows, the package author may also have to add the preloading like shown
151+
above with ``_enable_sharedlib_loading()`` to the main ``__init__.py`` of the
152+
package, ``delvewheel`` may or may not take care of this (please check its
153+
documentation if your shared library goes missing at runtime).
154+
155+
Note that we don't cover using shared libraries contained in another wheel
156+
and depending on such a wheel at runtime in this guide. This is inherently
157+
complex and not recommended (you need to be in control of both packages, or
158+
upgrades may be impossible/breaking).
159+
160+
161+
Using libraries from a Meson subproject
162+
=======================================
163+
164+
It can often be useful to build a shared library in a
165+
`Meson subproject <https://mesonbuild.com/Subprojects.html>`__, for example as
166+
a fallback in case an external dependency isn't detected. There are two main
167+
strategies for folding a library built in a subproject into a wheel built with
168+
``meson-python``:
169+
170+
1. Build the library as a static library instead of a shared library, and
171+
link it into a Python extension module that needs it.
172+
2. Build the library as a shared library, and either change its install path
173+
to be within the Python package's tree, or rely on ``meson-python`` to fold
174+
it into the wheel when it'd otherwise be installed to ``libdir``.
175+
176+
Option (1) tends to be easier, so unless the library of interest cannot be
177+
built as a static library or it would inflate the wheel size too much because
178+
it's needed by multiple Python extension modules, we recommend trying option
179+
(1) first.
180+
181+
A typical C or C++ project providing a library to link against tends to provide
182+
(a) one or more ``library()`` targets, which can be built as shared, static, or both,
183+
and (b) headers, pkg-config files, tests and perhaps other development targets
184+
that are needed to use the ``library()`` target(s). One of the challenges to use
185+
such projects as a subproject is that the headers and other installable targets
186+
are targeting system locations (e.g., ``<prefix>/include/``) which isn't supported
187+
by wheels and hence ``meson-python`` errors out when it encounters such an install
188+
target. This is perhaps the main issue one encounters with subproject usage,
189+
and the following two sections discuss how options (1) and (2) can work around
190+
that.
191+
192+
Static library from subproject
193+
------------------------------
194+
195+
The major advantage of building a library target as static and folding it directly
196+
into an extension module is that no targets from the subproject need to be installed.
197+
To configure the subproject for this use case, add the following to the
198+
``pyproject.toml`` file of your package:
199+
200+
.. code-block:: toml
201+
202+
[tool.meson-python.args]
203+
setup = ['--default-library=static']
204+
install = ['--skip-subprojects']
205+
206+
This ensures that ``library`` targets are built as static, and nothing gets installed.
207+
208+
To then link against the static library in the subproject, say for a subproject
209+
named ``bar`` with the main library target contained in a ``bar_dep`` dependency,
210+
add this to your ``meson.build`` file:
211+
212+
.. code-block:: meson
213+
214+
bar_proj = subproject('bar')
215+
bar_dep = bar_proj.get_variable('bar_dep')
216+
217+
py.extension_module(
218+
'_example',
219+
'_examplemod.c',
220+
dependencies: bar_dep,
221+
install: true,
222+
)
223+
224+
That is all!
225+
226+
Shared library from subproject
227+
------------------------------
228+
229+
If we can't use the static library approach from the section above and we need
230+
a shared library, then we must have ``install: true`` for that shared library
231+
target. This can only work if we can pass some build option to the subproject
232+
that tells it to *only* install the shared library and not headers or other
233+
targets that we don't need. Install tags don't work per subproject, so
234+
this will look something like:
235+
236+
.. code-block:: meson
237+
238+
foo_subproj = subproject('foo',
239+
default_options: {
240+
# This is a custom option - if it doesn't exist, can you add it
241+
# upstream or in WrapDB?
242+
'only_install_main_lib': true,
243+
})
244+
foo_dep = foo_subproj.get_variable('foo_dep')
245+
246+
Now we can use ``foo_dep`` like a normal dependency, ``meson-python`` will
247+
include it into the wheel in ``<project-name>.mesonpy.libs`` just like an
248+
internal shared library that targets ``libdir`` (see
249+
:ref:`internal-shared-libraries`).
250+
*Remember: this method doesn't support Windows (yet)!*

docs/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ the use of ``meson-python`` and Meson for Python packaging.
8282
how-to-guides/config-settings
8383
how-to-guides/meson-args
8484
how-to-guides/debug-builds
85+
how-to-guides/shared-libraries
8586
reference/limitations
8687
projects-using-meson-python
8788

0 commit comments

Comments
 (0)