Skip to content

Commit 7e6c915

Browse files
committed
On macOS, limit symbols exported by extension modules linking FreeType.
In my experience, mplcairo has only ever worked on macOS if imported prior to matplotlib (which means that in particular just setting the backend rcParam will not work, including via matplotlibrc), or (I have recently discovered) if using a non-local-freetype version build of ft2font. If none of these conditions are not satisfied, various bad things happen, such as segfaults at runtime. I realized recently the (one?) reason for this problem. Below, I only consider the case of local-freetype builds (the only problematic case). C-level cairo functions call FreeType for text rendering, and there are two different libfreetype libraries involved: the one statically linked into matplotlib's ft2font, and the OS-level one (which comes into play even for local-freetype builds because libcairo.so is always provided at the system level). Note that I am actually happy that libcairo can use the system-level freetype, because it is typically more recent and lets mplcairo offer e.g. better color font (emoji, etc.) support. On Linux, I find that the system-level libcairo.so indeed loads all the FreeType symbols it needs from the system-level libfreetype.so, so the libfreetype.a statically linked into ft2font is completely hidden to it; things are fine. On macOS, OTOH, I find that the libfreetype.a symbols "leak" out of the extension module, and libcairo.so picks up some symbols from it (one can check this via dladdr()), but (likely) not all, which means that two different versions of of libfreetype try to access the same FT_Face objects with (likely) incompatible ABIs, hence the segfaults. This difference in symbol priority likely comes down to difference in behavior in the macOS and Linux dynamic loaders, which I have not investigated more. However, one solution to fix is is to explicitly limit the symbols exported by ft2font to the only one needed as an extension module, i.e. PyInit_ft2font, hiding in particular all FreeType symbols. This also needs to be done for _backend_agg, which also links FreeType. To do so, I rely on the linker's -exported_symbol flag, as explained e.g. at https://stackoverflow.com/questions/2222162; also note that the symbol name is mangled with a leading underscore (https://news.ycombinator.com/item?id=20143037). I can confirm that with this fix, mplcairo can be correctly used even if imported after a matplotlib that uses a local-freetype build, and ft2font's FreeType symbols no longer leak-out. I am actually not very happy about limiting symbol visibility, because letting the linked library's symbols leak out allows various useful tricks: mplcairo itself actually gets its cairo symbols out of pycairo, and I have likewise previously written C-extensions that access FFTW by stealing the symbols from pyFFTW; in both cases the advantage is to not actually have to link the library against libcairo.so/libfftw3.so and instead push the responsibility of packaging these libraries for the Python ecosystem to pycairo/pyFFTW. I guess one key difference here, though, is that pycairo/pyFFTW exist exactly to provide bindings to these libraries, whereas ft2font isn't really there to provide bindings to FreeType (there's instead freetype-py, for example, for that). Still, if someone has a better understanding of the macOS dynamic loader and knows how to fix this better, I am all ears.
1 parent 6820797 commit 7e6c915

File tree

1 file changed

+4
-0
lines changed

1 file changed

+4
-0
lines changed

setupext.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,10 @@ def add_flags(cls, ext):
593593
0, str((src_path / 'objs/.libs/libfreetype').with_suffix(
594594
'.lib' if sys.platform == 'win32' else '.a')))
595595
ext.define_macros.append(('FREETYPE_BUILD_TYPE', 'local'))
596+
if sys.platform == 'darwin':
597+
name = ext.name.split('.')[-1]
598+
ext.extra_link_args.append(
599+
f'-Wl,-exported_symbol,_PyInit_{name}')
596600

597601
def do_custom_build(self, env):
598602
# We're using a system freetype

0 commit comments

Comments
 (0)