Problem Statement
It is currently impossible to use Zig to build shared (dynamic) libraries on macOS where some symbols are left undefined. For example, consider the program:
extern fn bar() void;
export fn foo() void {
bar();
}
On Linux we can build a shared library from this without error:
$ zig build-lib -dynamic test.zig
$ ls
libtest.so test.zig zig-cache
However the same program compiled with the same command line fails on macOS:
$ zig build-lib -dynamic test.zig
warning(link): unexpected LLD stderr:
Undefined symbol: zig-cache/o/c4e95092e3ee25bb54ad9a24fb2ee9a5/test.o: _bar
symbol(s) not found
error: FileNotFound
This has previously been reported in #3085 and #8016.
Motivation
This is a blocker for creating loadable extension modules for Python, Ruby, Lua, Postgres, etc. -- basically any program that loads modules via dlopen() and expects that the symbols will be resolved at load time. If you only need your module to work on Linux, then Zig will work. But if you need macOS support, there appears to be no way to make this work, and no workaround that I could find.
Background
Both macOS and Linux have command line options that control whether symbols may be undefined at link time. However the options are different and the two platforms have opposite defaults. By default, Linux is lax and macOS is strict.
Linux
Linux has two different options:
--no-undefined: causes the link to fail if unresolved symbols are present in the library currently being built. This is unset by default, so Linux allows undefined references by default, as we observed in the problem statement.
--no-allow-shlib-undefined: causes the link to fail if unresolved symbols are present in any shared libraries that we are linking against (eg. pre-existing shared libraries). This defaults to unset (ie. allowing undefined references) when you are building a shared library, but the default is the opposite (failing for undefined references) if you are building an executable. This is a somewhat odd option that I don't know a real use case for, and macOS doesn't have an analogue to this that I am aware of.
(There are also a few other flags such as --unresolved-symbols=method and -z defs, which are just different porcelain around the same underlying options, AFAICT).
More info in this StackOverflow thread
macOS
macOS has just one option for this:
-undefined treatment, where treatment is one of: error, warning, suppress, or dynamic_lookup. The default is error.
Existing practice for loadable modules on macOS is to prefer -undefined dynamic_lookup. For example, see: Building Python extension modules on OS X with cmake or autotools
Prior Art
zig cc has an existing option --allow-shlib-undefined. It appears to have the following semantics:
- This flag is only available for
zig cc, not zig build-lib.
- On macOS, it adds
-undefined dynamic_lookup to the link, which is just what we want. 👍👍👍
- On ELF targets, it adds
--allow-shlib-undefined to the link. This seems wrong and possibly confused, since it is not the analogue of the -undefined dynamic_lookup flag on macOS (see the "Linux" section above).
Proposed Solution
The only hard requirement is that we can somehow pass -undefined dynamic_lookup to the linker on macOS. Anything that makes this possible, even an escape hatch like -Wl,arbitrary_linker_flag would solve the problem.
That said, here is my proposed solution:
- Make Zig pass
-undefined dynamic_lookup to the macOS linker by default for zig build-lib -dynamic. That way, anybody porting their code from Linux to macOS will find that it "just works" and no platform-specific tweaks are needed.
- Do the same for
zig cc, and deprecate the zig cc --allow-shlib-undefined option (since this behavior is the default now).
- (If desired/requested): Add a
zig build-lib --no-undefined option for cases where users do truly want to force the shared library to define all symbols. This option would remove -undefined dynamic_lookup on macOS and add --no-undefined on Linux.
I think the use case for (3) is somewhat rare: I personally have never used ld --no-undefined or heard of anybody using it. I'm sure there are some users for it out there, but in terms of bang for the buck this seems rather niche.
Problem Statement
It is currently impossible to use Zig to build shared (dynamic) libraries on macOS where some symbols are left undefined. For example, consider the program:
On Linux we can build a shared library from this without error:
However the same program compiled with the same command line fails on macOS:
This has previously been reported in #3085 and #8016.
Motivation
This is a blocker for creating loadable extension modules for Python, Ruby, Lua, Postgres, etc. -- basically any program that loads modules via
dlopen()and expects that the symbols will be resolved at load time. If you only need your module to work on Linux, then Zig will work. But if you need macOS support, there appears to be no way to make this work, and no workaround that I could find.Background
Both macOS and Linux have command line options that control whether symbols may be undefined at link time. However the options are different and the two platforms have opposite defaults. By default, Linux is lax and macOS is strict.
Linux
Linux has two different options:
--no-undefined: causes the link to fail if unresolved symbols are present in the library currently being built. This is unset by default, so Linux allows undefined references by default, as we observed in the problem statement.--no-allow-shlib-undefined: causes the link to fail if unresolved symbols are present in any shared libraries that we are linking against (eg. pre-existing shared libraries). This defaults to unset (ie. allowing undefined references) when you are building a shared library, but the default is the opposite (failing for undefined references) if you are building an executable. This is a somewhat odd option that I don't know a real use case for, and macOS doesn't have an analogue to this that I am aware of.(There are also a few other flags such as
--unresolved-symbols=methodand-z defs, which are just different porcelain around the same underlying options, AFAICT).More info in this StackOverflow thread
macOS
macOS has just one option for this:
-undefined treatment, wheretreatmentis one of:error,warning,suppress, ordynamic_lookup. The default iserror.Existing practice for loadable modules on macOS is to prefer
-undefined dynamic_lookup. For example, see: Building Python extension modules on OS X with cmake or autotoolsPrior Art
zig cchas an existing option--allow-shlib-undefined. It appears to have the following semantics:zig cc, notzig build-lib.-undefined dynamic_lookupto the link, which is just what we want. 👍👍👍--allow-shlib-undefinedto the link. This seems wrong and possibly confused, since it is not the analogue of the-undefined dynamic_lookupflag on macOS (see the "Linux" section above).Proposed Solution
The only hard requirement is that we can somehow pass
-undefined dynamic_lookupto the linker on macOS. Anything that makes this possible, even an escape hatch like-Wl,arbitrary_linker_flagwould solve the problem.That said, here is my proposed solution:
-undefined dynamic_lookupto the macOS linker by default forzig build-lib -dynamic. That way, anybody porting their code from Linux to macOS will find that it "just works" and no platform-specific tweaks are needed.zig cc, and deprecate thezig cc --allow-shlib-undefinedoption (since this behavior is the default now).zig build-lib --no-undefinedoption for cases where users do truly want to force the shared library to define all symbols. This option would remove-undefined dynamic_lookupon macOS and add--no-undefinedon Linux.I think the use case for (3) is somewhat rare: I personally have never used
ld --no-undefinedor heard of anybody using it. I'm sure there are some users for it out there, but in terms of bang for the buck this seems rather niche.