Soundfonts are great for making music quickly. With no learning or configuration, you can play samples from a variety of instruments.

I wanted to make soundfont music on FL Studio Mac.

There was a nice soundfont plugin for FL Studio Windows, but the (third-party) source code was lost, and would not be ported.

So, I made my own plugin.

juicysfplugin - a soundfont VST for macOS

juicysfplugin is an AU/VST/VST3 audio plugin written in JUCE framework.
You can run it inside a plugin host (GarageBand, FL Studio, Sibelius, …), or it can self-host as a .app.

It’s my first C++ program.

I’d like to say I made a synthesizer, but really fluidsynth does the synthesis for me.
So, this is a story of software integration.

demo track (with Soundgoodizer compressor)

How juicysfplugin works

JUCE have good docs for making audio plugins like this one.

We have a responsibility to output (for example) 44.1 thousand samples of audio every second. We promise to deliver this in 512-sample blocks. To keep up with the demand, we have to render a block every 11.6ms. This also means we run at a latency of 11.6ms behind real-time.

Additionally, we’re given a buffer of MIDI messages each time this happens. In order:

  1. Audio plugin host invokes our processBlock() callback
    • input param: MidiBuffer
    • output param: AudioBuffer
    • must return within ~11.6ms
  2. We send the MidiBuffer to the JUCE Synthesiser (not fluidsynth)
    • updates the state of each ‘voice’ (for example: release the C key)
    • our voice implementation passes startNote(), stopNote() to fluidsynth
  3. We ask fluidsynth to output 512 samples of audio into AudioBuffer
    • fluidsynth has its own clock, so it knows this block starts where the previous block ended
    • fluidsynth has its own sample rate, which we keep updated

MIDI messages go in. We render the MIDI messages through the fluidsynth synthesiser. Then we output audio.

Integrating fluidsynth

I needed to dynamically link the fluidsynth library into my executable. Basic linker flags suffice:
-lfluidsynth -L/usr/local/lib (that’s the brew libraries directory).

But this creates a non-portable release:

Open juicysfplugin.app on another computer, and you get this error:

dyld: Library not loaded: /usr/local/lib/libfluidsynth.1.7.2.dylib
  Referenced from: ~/juicysfplugin.app/Contents/MacOS/juicysfplugin
  Reason: image not found

The fluidsynth library doesn’t exist on their system. They never brew-installed it.

Rather than tell users to prepare their environment, let’s bundle the library into our .app.
We copy libfluidsynth into juicysfplugin.app/Contents/lib during XCode’s “copy files” build phase.

Next, we must relink our binary to use the bundled libfluidsynth.

Relinking

Where does juicysfplugin.app/Contents/MacOS/juicysfplugin currently look for libfluidsynth?

otool -L ~/juicysfplugin.app/Contents/MacOS/juicysfplugin
juicysfplugin.app/Contents/MacOS/juicysfplugin:
  /usr/local/lib/libfluidsynth.1.7.2.dylib (compatibility version 1.0.0, current version 1.7.2)
  …

Let’s rewrite the /usr/local link, to search relative to @loader_path:

install_name_tool -change \
/usr/local/lib/libfluidsynth.1.7.2.dylib         `# rewrite this link` \
@loader_path/../lib/libfluidsynth.1.7.2.dylib    `# to this` \
~/juicysfplugin.app/Contents/MacOS/juicysfplugin `# in this obj file`

# @loader_path points to our binary's directory:
# juicysfplugin.app/Contents/MacOS

Our linkage now looks like this:

Let’s read the object file again to verify that we successfully relinked:

otool -L ~/juicysfplugin.app/Contents/MacOS/juicysfplugin
juicysfplugin.app/Contents/MacOS/juicysfplugin:
  @loader_path/../lib/libfluidsynth.1.7.2.dylib (compatibility version 1.0.0, current version 1.7.2)
  …

It goes deeper

We run our relinked .app on another computer. The first error is gone, but we’re onto a new error:

dyld: Library not loaded: /usr/local/opt/glib/lib/libglib-2.0.0.dylib
  Referenced from: ~/juicysfplugin.app/Contents/lib/libfluidsynth.1.7.2.dylib
  Reason: image not found

fluidsynth needs glib. glib doesn’t exist on their system. They never brew-installed it:

The bundle & relink dance must be done for all dependencies, recursively:

Something like this. Tedious.

Simplifying the build

We’ve established that we’ll want to have inside our .app: a copy of every .dylib. And we want to rewrite the load commands in every dylib, plus our juicysfplugin binary.

Our libraries don’t change between builds, so if we do the relinking dance just once, we can save those relinked libraries to $(PROJECT_DIR)/lib.
This is a project-local copy of the dylib, which can be shared via version control.

Next, we change our environment-specific -L/usr/local/lib library search path to -L$(PROJECT_DIR)/lib.

We attempt a build of juicysfplugin with our new linker flags: -lfluidsynth -L$(PROJECT_DIR)/lib.

The linker correctly finds our project-local libfluidsynth, and links to it. But what’s this?

otool -L ~/juicysfplugin.app/Contents/MacOS/juicysfplugin
juicysfplugin.app/Contents/MacOS/juicysfplugin:
  /usr/local/lib/libfluidsynth.1.7.2.dylib (compatibility version 1.0.0, current version 1.7.2)
  …

Why does juicysfplugin still load fluidsynth via the environment-specific /usr/local?

It’s because of what fluidsynth’s install_name was, at the time we linked to it.
We can view a dylib’s install name with otool -D:

otool -D $(PROJECT_DIR)/lib/libfluidsynth.dylib
libfluidsynth.dylib:
  /usr/local/lib/libfluidsynth.1.7.2.dylib (compatibility version 1.0.0, current version 1.7.2)
  …

The install_name recommends that consumers of libfluidsynth look for a dylib in /usr/local. We need to change that recommendation.

Let’s edit our project-local copy of libfluidsynth.
Give it a binary-relative install_name:

install_name_tool -id                            `# set install_name` \
@loader_path/../lib/libfluidsynth.1.7.2.dylib    `# to this` \
$(PROJECT_DIR)/lib/libfluidsynth.dylib           `# in this obj file`

Next time we build juicysfplugin, we see that the linker now writes the correct load command into our binary:

otool -L ~/juicysfplugin.app/Contents/MacOS/juicysfplugin
juicysfplugin.app/Contents/MacOS/juicysfplugin:
  @loader_path/../lib/libfluidsynth.1.7.2.dylib (compatibility version 1.0.0, current version 1.7.2)
  …

We no longer need to do any post-build relinking of juicysfplugin or its libraries.
juicysfplugin links to a project-local libfluidsynth, which has been configured to tell consumers to use a binary-relative link.

More project-agnostic convention

Our binary-relative link, @loader_path, is successful in making our binaries portable. We could even stop here.
But there’s an itch remaining.

It’s bad that our libraries are responsible for declaring “where can I be found at runtime”. This forced us to make a project-specific copy of each library, with baked-in assumptions about juicysfplugin.app’s directory layout.

It’s preferable to invert the control.
The binary, juicysfplugin, should be in charge of “where will libraries be found at runtime”.

Thankfully, there’s a mechanism to accomplish this: @rpath expansion.

Libraries may set an @rpath-relative install_name.
Binaries decide at runtime how to expand @rpath, and may even specify fallbacks.

Let’s make fluidsynth’s install_name @rpath-relative:

install_name_tool -id                            `# set install_name` \
@rpath/libfluidsynth.1.7.2.dylib                 `# to this` \
$(PROJECT_DIR)/lib/libfluidsynth.dylib           `# in this obj file`

Then we configure the juicysfplugin binary to use a “runtime search path” of @loader_path/../lib. This is an XCode build setting, equivalent to gcc’s -rpath option.

Now the libfluidsynth that we saved under $(PROJECT_DIR)/lib is environment-independent and project-independent. Other open-source developers may like to grab this portable library and use it in their own project.

To finish the job: replace all the @loader_path links we made earlier (i.e. fluidsynth to its brew dependencies) with @rpath.
And (optionally) declare @rpath install_names upon each dylib, to help anybody who links directly to the libraries you ship.

Generalizing the process

There’s some relatable use-cases here:

  • You link to some brew-installed library (e.g. libfluidsynth), and want to make a relinked project-local copy of that library
  • You produce some binary (e.g. juicysfplugin.app/Contents/MacOS/juicysfplugin), and want to bundle+link libraries into the app, for portable distribution

I’ve automated both of these use-cases with this bash script.
Run ./make_portable.sh mycoolbinary or ./make_portable.sh libcool.dylib to make any mach-o object file portable.
It follows the dependencies, copies them into a nearby lib folder, and relinks everything to use those local libraries.

I am not the only one to automate this.

Further hints

Why not use @executable_path?

We output a variety of build targets. In the standalone juicysfplugin.app, @loader_path and @executable_path are the same thing.

The plugin targets (VST, VST3, AU), however, are designed to be hosted inside a different executable (e.g. Garageband, FL Studio).
Here @executable_path points to the plugin host (Garageband.app/Contents/MacOS), which is not what we want.

We want to load libraries relative to the binary which contains the load command. Hence @loader_path is necessary.

install_name_tool gotchas

When relinking a library with install_name_tool [-change old new] file, beware: you must match exactly.

To find the link matching /usr/local/Cellar/glib/2.56.1/lib/libglib-2.0.0.dylib and rewrite it…

  • You cannot match on leaf name libglib-2.0.0.dylib or library name, glib.
  • You cannot match on an equivalent symlink path /usr/local/opt/glib/lib/libglib-2.0.0.dylib

If your command matches nothing: there is no error message, and the exit code says success as usual.

If you are automating this in a parameterised way, don’t be tempted to re-use absolute paths; fluidsynth and gthread disagree on whether glib lives in /usr/local/opt or /usr/local/Cellar.

There’s no debugger

There used to be a helpful environment variable, LD_DEBUG, which let you watch runtime link resolution.
Unfortunately, Apple removed it. Probably removed a build option.

There is some tracing you can enable in the runtime linker. You can see how it expands the variable @rpath, and whether that succeeded.

DYLD_PRINT_RPATHS=1 …/juicysfplugin.app/Contents/MacOS/juicysfplugin
RPATH successful expansion of @rpath/lib/libfluidsynth.dylibDYLD_PRINT_LIBRARIES=1 …/juicysfplugin.app/Contents/MacOS/juicysfplugin
dyld: loaded: …/juicysfplugin.app/Contents/MacOS/juicysfplugin
dyld: loaded: …/juicysfplugin.app/Contents/MacOS/../lib/libfluidsynth.dylib

It will tell you which expansions of @rpath fail (here I deliberately wrote in a link to a non-existent file):

DYLD_PRINT_RPATHS=1 …/juicysfplugin.app/Contents/MacOS/juicysfplugin
RPATH failed to expanding     @rpath/lib/notlibfluidsynth.dylib
dyld: Library not loaded: @rpath/lib/notlibfluidsynth.dylib
  Referenced from: …/juicysfplugin.app/Contents/MacOS/juicysfplugin
  Reason: image not found

You can get some feedback regarding how it searches fallback locations.
I copied notlibfluidsynth.dylib into ~/tmp (a directory I specify as a fallback location), and it succeeds, and tells you which location it used:

DYLD_PRINT_LIBRARIES=1 \
DYLD_FALLBACK_LIBRARY_PATH="$HOME/tmp:$DYLD_FALLBACK_LIBRARY_PATH" \
…/juicysfplugin.app/Contents/MacOS/juicysfplugin
dyld: loaded: …/juicysfplugin.app/Contents/MacOS/juicysfplugin
dyld: loaded: ~/tmp/notlibfluidsynth.dylib

The linker provides other DYLD_PRINT_* variables, like DYLD_PRINT_STATISTICS_DETAILS, DYLD_PRINT_ENV, DYLD_PRINT_OPTS. I recommend you check them out in man dyld. You can see the environment and options with which your process is launched, or read statistics of how it spent its time before calling main().

Trawling the dependency list and relinking all non-system libraries with install_name_tool is manual and non-scalable.
For this small project, it was a local optimum of effort/reward.

But if you want to distribute your macOS application without using install_name_tool, there are some other routes you could try.

Provide an installer

Users could run an installer, to copy dependencies to /usr/local/Cellar, like brew does (or the installer could properly brew install them). No relinking required.

Distribute application via brew

Brew already provides a distribution mechanism and semantics for expressing dependencies. You could take advantage of that if users are comfortable with command-line installation.

Static linking burns a library into your executable. This means there’s no possibility for the library to be in the wrong place or missing.

That said, static linking is fiddly. You would compile the source of libfluidsynth, libsndfile and so on into object files. Then you would collect them into one big archive, libfluidsynth.a. Then you would compile the source of juicysfplugin, and statically link its object code to libfluidsynth.a.

The problem is the “and so on”. Eventually there’s a dependency in the tree which cannot be statically linked. macOS does not provide static versions of libSystem.dylib. GNU libc is not designed to be statically linked.

Mercifully, the user is guaranteed to have system libraries, so we can link to those dynamically. But for anything else: we would have to change the way we build objects in libfluidsynth.a (i.e. omit unused dependencies, use alternative libraries which can be statically linked, or use a mixture of static/dynamic linking).

Supposing you succeed, you still have a new problem: licensing. By statically linking, you’ve created a derivative work in your name, instead of distributing the original artefact.
If you statically link to a GPL library, you must release under GPL license: the source for both your work and the library. Statically linking to LGPL: you may instead release just the compiled object code for your work.
Note: I am not a lawyer, and I inferred the above from this discussion.

Reconfigure the runtime linker

Don’t actually do this, but you can abuse the dynamic linker, dyld.

The dynamic linker uses the following environment variables. They affect any program that uses the dynamic linker.

DYLD_FALLBACK_LIBRARY_PATH
  It is used as the default location for libraries not found in their install path. By default, it is set to $(HOME)/lib:/usr/local/lib:/lib:/usr/lib.

When resolution fails for /usr/local/Cellar/glib/2.56.1/lib/libglib-2.0.0.dylib, dyld will search for the leaf name libglib-2.0.0.dylib under each of those fallback library paths.

So, you could provide a folder of libs and instruct the user to add that folder to their DYLD_FALLBACK_LIBRARY_PATH.

# add to your .profile or similar
export DYLD_FALLBACK_LIBRARY_PATH="$HOME/Downloads/juicysfplugin/lib:$DYLD_FALLBACK_LIBRARY_PATH"

This isn’t very deterministic though. Our bundled libraries would only be used as a last resort.

More prescriptive is to use DYLD_INSERT_LIBRARIES. This has higher precedence; the bundled libraries are checked as a first resort.

Reflection

It was fun to deep-dive into dynamic linking. The non-intuitive bits were:

  • install_name_tool has a bad API for rewriting links
    • To rewrite a link: you must state the current link’s fully-qualified path
    • I would rather specify the library’s leaf name
  • The linker writes load commands based on a library’s install_name
    • My intuition was that it’d use the current filesystem path of the library
  • otool -L has a weird output format
    • various load commands are displayed, but their ‘type’ is hidden
    • otool -l is more specific, but hard to parse
  • @rpath expansion seems far more sane than distributing project-specific libraries, but it is underused

I’m surprised by how fiddly this was. I’d thought software bundling would be a really solved problem on macOS. The .app format is a nice attempt at making applications portable, but the dream falls flat if dynamic linking is difficult.

The Windows version of juicysfplugin was far easier to link. If the library is not found in the primary location, the runtime linker will search for it in a few fallback directories. These are pretty convenient; you can place the .dll alongside your application, or into the system folder (an installer can help with this).

It would be nice if Brew libraries were built with @rpath-relative install_names. For my application, this would’ve removed any need for relinking. But redistributability of libraries requires solving more problems than just linking. And if you distribute your own software via the brew ecosystem, then library paths are well-known anyway.

Still, the journey was educational. The concepts are transferable (it’s helped me resolve linking problems on Linux), but I can see that there are ecosystems (JVM, NodeJS) that have a totally different approach to libraries. A developer could build a career on JVM/JS and never have to battle with native code linking. Docker is another way to solve the portability problem for native code, since it gives you a reproducible environment.

I feel like native code linking is in danger of becoming a lost art, but at the same time I have confidence that WebAssembly (which enables native code to target the browser) and GraalVM (which enables LLVM-compatible source code to target the JVM) will generate new interest.