Problems on OCaml z3 binding
It’s frustrating that there have been subtle problems to use OCaml z3 binding: libc++abi.dylib terminating with uncaught exception
(spoiler: I don’t solve this but bypass it) dyld: Library not loaded: libz3.dylib
(spoiler: I SOLVED this) etc. Many OCaml users including me suffer from this at least for more than two years after z3 4.8.1 (z3#2305). @ivg once fixed it with an excellent explanation at z3#4468 (and the related post here). However, libc++abi
error comes back to me again (z3#5578) when I use it on macOS.
So, there are two problems now:
-
The staticlib-by-default doesn’t work for macOS users. It’s set so after z3.4.8.1. Z3 uses the exception handle internally in some use-case, however, ABI mismatch breaks those code. That’s why some people are still using z3.4.8.1 on macOS.
-
z3 binding with the shared lib doesn’t work perfectly since users still need to set
LD_LIBRARY_PATH
manually or even worse on macos.
For problem 1, with some discussion in z3#5578, I made two opam files for package z3
(it uses the shared library by default) and z3-static
that can be used for the next z3 release. Problem 1 can be solved by choosing the one you can use.
Problem 2 is also solved with z3#5616 and z3#5618. In my local testing, OCaml z3
binding works correctly under any combination of {ocamlopt, ocamlc, ocamlc-with-custom} * {z3 shared lib, z3 static lib} * {ubuntu, macOS}
, without any change on LD_WHATSOEVER_LIBRARY_PATH
. Bravo!
I would like to share my ideas (and sufferings) here and I am guessing it may help people facing similar problems.
A glimpse of z3ml’s building, installing, and using
source gist: OCaml z3 linking · GitHub (live editor: https://mermaid.live/ which can load gist)
In the building of z3’s ml binding (it occurs in <z3_source>/build
), we see these generated rules for z3 ml stublibs.
ocamlmklib -o api/ml/z3ml -I api/ml -L. api/ml/z3native_stubs$(OBJ_EXT) api/ml/z3enums.cmo api/ml/z3native.cmo api/ml/z3.cmo -lz3 -lstdc++
ocamlmklib
will generate libz3ml.a
dllz3ml.so
and z3ml.cma
. These files together with META
and others are installed later via ocamlfind install z3 <all_these_files>
into <opam-sitelib>/stublibs
and <opam-sitelib>/z3
. The opam-sitelib
is usually ~/.opam/<your_switch>/lib
.
When the user requires the z3 library, its entries in META
will expand to the files in the package automatically.
ocamlfind ocamlc -o z1.bc -verbose -thread -package z3 -linkpkg z1.ml
ocamlfind ocamlopt -o z1.exe -verbose -thread -package z3 -linkpkg z1.ml
It works nicely if the original z3 is a static archive library. In ocamlmklib
, the flags -lz3
and -L.
locates <z3_source>/build/libz3.a
and makes large files libz3ml.a
dllz3ml.so
. There is no problem when using the static library. libz3ml.a
and libz3.a
is installed into <opam-sitelib>/z3
and -package z3
will expands to -I <opam-sitelib>/z3
for ocamlc
and -L<opam-sitelib>/z3
for ocamlopt
.
However, it does have problems if using z3 shared library a.k.a. using libz3.so
(or libz3.dylib
). Both linkers and loaders are fragile and error-prone.
Linker and loader
(I don’t look up a textbook. Correct me if I am wrong.)
During the compilation, the linker finds and stores the dependent libraries in the compiled object.
During the execution of native-code or the interpretation of bytecode, the loader finds and loads the dependent libraries.
Assuming the OCaml binding is built with z3 shared library, when the user requires the z3 library
ocamlfind ocamlc -o z2.bc -verbose -thread -package z3 -linkpkg z1.ml
./z2.bc
It can build but running ./z2.bc
fails because ocamlrun
can load dllz3ml.so
but it cannot find libz3.so
required by dllz3ml.so
.
We can provide the search path a.k.a where to find libz3.so
for the loader to ocamlmklib
via -dllpath <path/to/libz3.so>
. This path is stored in dllz3ml.so
and later the loader can search libz3.so
in that path.
ocamlmklib -o api/ml/z3ml -I api/ml -L. api/ml/z3native_stubs$(OBJ_EXT) api/ml/z3enums.cmo api/ml/z3native.cmo api/ml/z3.cmo -lz3-static -lstdc++ -dllpath $(ocamlfind printconf path)/stublibs
If we are using this, the above ocamlc ...
and ./z2.bc
work. However, the following command complains in the building:
ocamlfind ocamlopt -o z2.exe -verbose -thread -package z3 -linkpkg z1.ml
ocamlopt
(actually the cc) linker cannot find libz3.so
required by dllz3ml.so
.
We can also provide search path for the linker to ocamlmklib
via -ccopt -L<path/to/libz3.so>
. This path is stored in z3ml.cma
and later the linker can search libz3.so
in that path.
ocamlmklib -o api/ml/z3ml -I api/ml -L. api/ml/z3native_stubs$(OBJ_EXT) api/ml/z3enums.cmo api/ml/z3native.cmo api/ml/z3.cmo -lz3-static -lstdc++ -ccopt -L$(ocamlfind printconf path)/stublibs -dllpath $(ocamlfind printconf path)/stublibs
What if we have have -ccopt -L<path/to/libz3.so>
but no -dllpath <path/to/libz3.so>
? ocamlc
can build but the bytecode cannot run. ocamlopt
can build but the nativecode cannot run.
With both -ccopt -L
and -dllpath
, everyone is satisfied … except for the mac users!
What’s wrong with macOS!?
Even with the previous fix, this error still occurs:
dyld: Library not loaded: libz3.dylib
Referenced from: blabla
Reason: image not found
make: *** [test] Abort trap: 6
Some workaround solutions include
- Modify
LD_LIBRARY_PATH
. However, it may not work now due to System Integrity Protection (SIP, wiki). - Copy libz3.dylib to your working path.
Answer: the problem is inside libz3.dylib
. Unlike the ELF and the loader convention used in Linux, Mach-O also stores the library id inside the library file under section LC_ID_DYLIB
. What is not loaded is not the file libz3.dylib
but the id libz3.dylib
in the search path.
According to the apple document on dynamic libraries, using a filename without paths or qualifiers means it will be searched only in ~/lib
, /usr/local/lib
, /usr/lib
and the working path.
The id libz3.dylib
in the LC_ID_DYLIB
section of file libz3.dylib
will propagate to any other shared or executable objects who uses it, stored in the LC_LOAD_DYLIB
section. Therefore, the stublib dllz3ml.so
and executables made with ocamlopt will all have this.
The fix is to change library id libz3.dylib
inside dllz3ml.so
(via install_name_tool -change
) and libz3.dylib
(via install_name_tool -id
) to the absolute installed path <path/to/libz3.dylib>
. We can do a similar game by just fixing one and check how the compiler, linker, loader, and interpreter work. It is exactly what I was doing when trying fixing this problem.
Now everyone is satisfied … including the mac users!
In hindsight, brew install --build-from-source --keep-tmp --debug z3
have this output, showing the platform experts also solve the problem by changing dylib ID.
==> Changing dylib ID of /usr/local/Cellar/z3/4.8.12/lib/libz3.dylib
from libz3.dylib
to /usr/local/opt/z3/lib/libz3.dylib
==> Changing dylib ID of /usr/local/Cellar/z3/4.8.12/lib/python3.9/site-packages/z3/lib/libz3.dylib
from libz3.dylib
to /usr/local/opt/z3/lib/python3.9/site-packages/z3/lib/libz3.dylib
Discussion
There are several glitches in my debugging process, and I don’t even have an idea on which is the best solution.
- On macos,
ocamlmklib
generateslib<foo>.a
anddll<foo>.so
. Woulddll<foo>.dylib
be more consistent with the system convention? - On macos,
ocamlfind install <foo>.dylib
will be in the<opam-sitelib>/foo
rather than<opam-sitelib>/stublibs
by default. Would going to<opam-sitelib>/stublibs
better for*.dylib
? - Who is the best guardian to set the installed path of a library?
ocamlmklib
(via passing itgcc -dynamiclib -install_name
),ocamlfind install
,opam
? -
ocamlmklib -dllpath <path/to/foo>
may not work on macos. It can be checked byotool -l <shared_library> | grep LC_RPATH -A2
. What’s more, if the library’s name is not like@rpath/foo.dylib
, even setting rpath successful may be useless. - I saw
ocamlfind: [WARNING] You have installed DLLs but the directory <opam-sitelib>/stublibs is not mentioned in ld.conf
several times. I am thinking whyocamlopt
orocamlc
don’t take<opam-sitelib>/stublibs
in the default search path for the linker and loader? Or shallopam switch
be in charge of this?
Reference
OCaml manuals on ocamlc, ocamlopt, ocamlrun, etc.
many github issues and discourse posts on z3