Bob Ippolito (@etrepum) on Haskell, Python, Erlang, JavaScript, etc.
«

Versioned Frameworks Considered Harmful?

»

It has recently [1] come to my attention that using versioned frameworks without binary compatible APIs simply doesn't work all too well. Essentially, you may have an older version installed, but without swapping symlinks around, you you won't be able to use the older version for anything except runtime compatibility with software that is already linked.

This isn't a problem with the dyld runtime, simply a problem with gcc's compiler and linker (and supposedly there may be runtime bugs with CFBundle and/or NSBundle in this arena as well).

Frameworks

Mac OS X frameworks are structured in such a way that allow for versioned code, data, and headers. A typical multi-versioned framework looks something like this:

Foo.framework/
    Foo -> Versions/Current/Foo
    Headers -> Versions/Current/Headers
    Resources -> Versions/Current/Resources
    Versions/
        Current -> 2.0
        2.0/
            Foo
            Headers/
            Resources/
        1.0/
            Foo
            Headers/
            Resources/

Mach-O, MH_DYLIB, and the dyld runtime

The MH_DYLIB object file, the part that you link to, is "Foo". If this were not a framework, it would have the following layout, and it would be application-specific to put the headers and resources in the right place:

libFoo.dylib -> libFoo.2.0.dylib
libFoo.1.0.dylib
libFoo.2.0.dylib

Due to the way that MH_DYLIB object files work, each of these dylibs know their own (supposed) location on the filesystem. This is called the "install name" or the "id" of the dylib. Technically, this is a LC_ID_DYLIB load command in the Mach-O header. When you create an Mach-O object file (executable, dylib, bundle, etc.) that depends on another, it will generate a load command (LC_LOAD_DYLIB) referencing the other dylib. The data associated with the LC_LOAD_DYLIB is exactly the same data that was provided by the linked-to MH_DYLIB in its LC_ID_DYLIB load command, unless explicitly overrided with the -dylib_file option to ld(1) or rewritten post-link with a tool such as install_name_tool(1). The otool(1) tool can be used to view these load commands in a Mach-O file, among other things. It's very important to have these values set correctly because they are used by the dyld runtime to locate the intended MH_DYLIB.

gcc, ld, and frameworks

Apple's GCC includes many changes to support Objective C/C++ and framework based development. Unfortunately, none of them have any explicit support for versioned frameworks. For example, gcc(1) states that it uses the following algorithm for finding frameworks in headers:

-Fdir
    In Apple's version of GCC only, add the directory dir to the head
    of the list of directories to be searched for frameworks.

    The framework search algorithm is, for an inclusion of
    <Fmwk/Header.h>, to look for files named path/Fmwk.framework/Head-
    ers/Header.h or path/Fmwk.framework/PrivateHeaders/Header.h where
    path includes /System/Library/Frameworks/ /Library/Frameworks/, and
    /Local/Library/Frameworks/, plus any additional paths specified by
    -F.

    All the -F options are also passed to the linker.

ld(1) has an similar option, -framework, which has a similar algorithm:

-framework name[,suffix]
       Specifies a framework to link against.  Frameworks  are  dynamic
       shared  libraries,  but  they are stored in different locations,
       and therefore must be searched for differently. When this option
       is  specified,  ld  searches for framework `name.framework/name'
       first in any directories specified with the -F option,  then  in
       the  standard  framework  directories /Library/Frameworks, /Net-
       work/Library/Frameworks,  and  /System/Library/Frameworks.   The
       placement  of the -framework option is significant, as it deter-
       mines when and how the framework is searched.  If  the  optional
       suffix is specified the framework is first searched for the name
       with the suffix and then without.

Note that neither of these options allow for any consideration for the Versions directory in a framework, and therefore only link to whichever version was installed last, because the installation process for a framework will create the symlinks that point to locations inside the Versions directly.

Workaround

Unfortunately, since there is no support or hook that will allow proper usage of versioned frameworks, the workarounds are all ugly. I think the following workaround is the most appropriate: Just Don't Use GCC's Search Algorithms.

  • Always use the compiler option -I/Path/To/Foo.framework/Versions/IntendedVersion/Headers
  • use #include "Foo.h" instead of #include <Foo/Foo.h> in your sources
  • Always use the direct path to the MH_DYLIB rather than any combination of -framework and -F.
  • Or, if you're building extension bundles that will be used by an executable that already has the correct version of Foo linked in, use the -undefined dynamic_lookup linker option

A minimal compiler/link line for an executable would look like the following:

env MACOSX_DEPLOYMENT_TARGET=10.3 cc
    -I/Path/To/Foo.framework/Versions/IntendedVersion/Headers
    -o usesFoo
    usesFoo.m /Path/To/Foo.framework/IntendedVersion/Foo

And a minimal compiler/link line for an extension bundle would look like:

env MACOSX_DEPLOYMENT_TARGET=10.3 cc
    -I/Path/To/Foo.framework/Versions/IntendedVersion/Headers
    -o fooUsingExtension.bundle
    fooUsingExtension.m
    -bundle -undefined dynamic_lookup

Quite ugly, eh? At least it works as intended. Unfortunately you'll need to emulate enough of GCC's search algorithms to find the framework, which is probably quite problematic from Xcode, but shouldn't be too hard from say, distutils, SCons, autoconf, etc. Note that you should also probably consider the NEXT_ROOT environment variable when building against an SDK.

[1]I wrote this a few months ago, before I had replaced PyDS, so "recently" means Novemberish.