Early work experimenting with zig as a cross-compiler for OCaml

This is some early work using zig as a cross-compiler for building OCaml cross-compilation systems:

opam-cross-lambda

Status: It is currently severely untested but the aim is to be able to cross-compile to Linux from Windows/Mac/Linux for aarch64 and x86_64 CPU architectures simply by adding an opam repository, and without the need for nix.

Why: The novel aspect is zig, which allows you to cross-compile C code without needing to install or set up a cross-compilation sysroot i.e. glibc, gcc, binutils, kernel headers etc. as zig packages much of the needed headers and symbol information internally.

Next steps: start importing packages (including those with native binaries) into the opam repository overlay, validate them in CI/CD

This approach has led me down some rabbit-holes with a bunch of learning - some interesting points:

  • zig uses clang internally, so its effectively testing clang compatibility with OCamlā€™s autoconf + Makefile assumptions about the C compiler
  • targeting windows isnā€™t possible with this setup at this time, because flexdll hardcodes mingw binary names (e.g. x86_64-w64-mingw32-gcc) in its Makefile and the flexlink binary (it assumes these exist because targeting mingw32 is always a cross-compilation, even on Windows). It also depends on binutilsā€™ windres, which zig does not provide a wrapper for.
  • targeting macos x is untested
  • as you can see in the CI/CD scripts, setting the ZIG cache directory environment variables is crucial for MacOS because of opamā€™s sandboxing (zig builds its cache in the userā€™s home directory, which is outside the default sandbox)
  • although ocamlfind and dune have some cross-compilation support with ā€œtoolchainsā€, there are gaps and undocumented assumption
  • opam doesnā€™t really support cross-compilation environments well - packages often donā€™t require much change, but you do need to create a <package>-cross-<cross-name> version of every single package - this could be a lot more straightforward and less work with a more cohesive platform strategy for cross-compilation

Alternatives: Iā€™m aware of alternatives in the ecosystem (and indeed have benefitted from):

  • ocaml nix overlays - these offer a far better tested and reproducible cross-compilation environment, mostly for systems that can run nix
  • opam-cross-windows - lots of little nuggets of build-time information found in here
21 Likes

Iā€™m stuck in a frenzy of teaching at the moment due to it being the height of the academic term here, but I just wanted to stop by and say ā€˜wow!ā€™.

I hadnā€™t realised that Zig supports cross-compilation out of the box, but Iā€™ve been very impressed by using Ghostty recently that is written in it, and your work looks like a very promising direction of travel. I canā€™t wait to try this outā€¦

1 Like

zig supports cross compilation of not just itself, but C, out of the box, with binutils/gcc compatible wrappers for aspp, ar, ranlib, gcc, etc. targeting a number of platforms. Itā€™s quite incredible the backwards compatibility with C they built targeting numerous architectures and OSs they did

6 Likes

Maybe Makefile fixes by MisterDA Ā· Pull Request #153 Ā· ocaml/flexdll Ā· GitHub is a start that can help?

This could be great help, Iā€™ll have to try it out with a patched copy in my test repo when I next get round to it

1 Like

Ok, this gets very close, but the zig rc wrapper takes command line switches to be compatible with msvcā€™s rc tool instead of binutilsā€™ windres, which means that itā€™s not a simple drop-in replacement for windres.

I actually need to be able to override the whole call as this doesnā€™t work:

zig rc -D FLEXDLL_VS_VERSION_INFO=0,43,0,0 -D FLEXDLL_FULL_VERSION=\\\"0.43.0.0\\\" -i version.rc -o version_res.o

(the problem is the -o - to make things even more confusing, zig rc accepts /, - and all other manner of switch prefixes for this command in its latest version, but there is no -o compatibility equivalent to windres. This is probably because theyā€™re trying to be more windres compatible but havenā€™t quite got there yet)

Actually I take this back - I noticed I can just set which call to make with your patch. Thanks, that does help!

The problems Iā€™m hitting are now after that, where it tries to build the OCaml parts of flexlink with the bootstrap ocamlrun and ocamlc. I may be able to avoid those by building the flexlink sources separately, instead of relying on the builtin Makefile and building in the OCaml source tree.

Iā€™m not actively investigating this, but Iā€™ll keep it in mind if I decide I want to experiment with windowā€™s targets later.

This is very cool!

A couple of very quick notes on the Windows side - flexdll has always been updated ā€œas neededā€, so patches to facilitate other builds are very much welcomed (if occasionally only slowly reviewed :see_no_evil:). If youā€™re building via opam, note that flexdll can be pinned - you can either pin a branch directly before building the compiler and those sources will be used instead or you can obviously set a package in your overlay repo to do the same thing and then depend on that for any patched compiler, etc. Iā€™d recommend doing that rather than trying to circumvent the build system, just because itā€™ll be easier to share with others.

2 Likes

Thanks - I meant I need to run the OCaml build of flexdll separately, not necessarily patch the build system of flexdll before running it. OCaml 5.3.0ā€™s Makefile passes OCAMLC as ../boot/ocamlrun ../boot/ocamlc which is not what I want when cross-compiling.

The Makfile.cross in the current main branch has instructions closer to what I need to do - I will attempt those when I get the time.

My focus right now is working out how to generate the <package>-cross-<cross_name> for each package I want to cross-compile. Most of the changes are mechanistic updates to the dune package build name and adding the -x <toolchain> parameter, which I think I can automate

(again, I really wish opam had the concept of a ā€œsubswitchā€ where you could just run the build and install for each package and its dependencies again, but with the toolchain parameter specifed (or for those that donā€™t use dune, a {with-toolchain} filter). Once Iā€™ve figured out how to do this en-mass reliably and have a good idea of whatā€™s actually needed, I might write a proposal).

Iā€™ve gotten around that in the past by using opam pre-build-commands to create a dune-workspace file with the right cross-compile flags in each build directory. (EDIT: I think I needed a post-install-commands as well so that the extra cross-compiled output _build/default.TARGET/* gets into the switch.) Much of this would be much easier if dune supported an environment variable (DUNE_CROSSCOMPILE_TARGETS?) instead of just dune build -x ... or a custom dune-workspace. (I was going to do that with a follow-up to dune build -x accepts only one toolchain while dune-workspace accepts many Ā· Issue #10989 Ā· ocaml/dune Ā· GitHub but I have had little spare time to do it.)

For other build systems like ocamlbuild that do not easily support ocamlfind -toolchain cross-compiles, there is the GitHub - dune-universe/opam-overlays: An opam remote with the various Dune modified repositories in this org.

This is a really great idea. Cross compilation is a big deal for me, I am learning zig for exactly this reason, but ocaml is such a nice language. Most of my time is spent in the julia world and this is again their big problem. If you can just get a binary that works!

Thanks
David

Thatā€™s definitely a sneaky way of getting opam to compile for the host and the target at the same time!

My planned approach was to do something like what opam-cross-windows does, by duplicating packages and rewriting them. I was going to try and script this as much as possible so I could just point at a package I wanted, resolve the dependencies, and rewrite them to an overlay repository for the -cross-<name> I wanted.

Thank you for suggesting zig as a good choice for a C cross compiler!

I just checked that the cross-compiler support that was merged upstream (and that I backported to 5.3 if you want to give it a try on the stable release) is compatible with that and indeed it is! You can get a cross compiler with just:

$ ./configure --target=aarch64-linux-gnu \
    'CC=zig cc --target=aarch64-linux-gnu.2.34' \
    'PARTIALLD=zig cc --target=aarch64-linux-gnu.2.34 -r' \
    'AR=zig ar' STRIP=:
$ make crossopt -j

Here is the log for that configuration.

So I had to experiment with something more tricky, namely a Linux x86_64 to macOS aarch64 cross compiler. Building the cross compiler, using:

$ ./configure --target=aarch64-apple-darwin \
    'CC=zig cc --target=aarch64-macos' \
    'PARTIALLD=zig cc --target=aarch64-macos -r' \
    'AR=zig ar' STRIP=: --disable-shared

worked fine but then compiling a small program fails with a long list of linking errors:

error: symbol _caml_startup.code_end not attached to any (sub)section
    note: while parsing /tmp/camlstartup41b4bf.o
error: symbol _camlStd_exit.code_end not attached to any (sub)section
    note: while parsing /home/runner/cross/lib/ocaml/std_exit.o
error: symbol _camlExample.code_end not attached to any (sub)section
    note: while parsing example.o
[ā€¦]

You can see the full CI log for details. Do you have a clue about what is broken there?
(Iā€™ve found this similar error, maybe itā€™s much more general than my simple attempt; so I hope it might be fixed in the next zig release).

2 Likes

Not sure how your stuff works, but fwiw there is bazel support for zig, and you can use bazel to build opam packages. So for example you could probably eliminate the need to install zig locally by making an opam pkg for it. You might find it worth you while to look in to that.

1 Like

Iā€™m glad to see it works with the new crossopt Makefile - Iā€™ll look at using it once its in an official release.

I havenā€™t tried a target like MacOS X, but from what I understand, itā€™s diabolical to compile for, as anything useful needs system headers from FoundationKit and AppKit, and Apple protect against their use outside of their SDKs and platform.

That said, I donā€™t think disabling shared libraries is a good idea for a MacOS X target. Iā€™ve tried with shared libraries enabled, but when linking libasmrun_shared.so:

  1. I needed to remove the passing of the -Wl,-w flag, which doesnā€™t seem to be supported by zig
  2. After removing that, zig crashes with thread 10405 panic: unexpected augmentation string. This seems to be related to something in runtime/arm64.S

Iā€™m not likely to explore this one any further - there is definitely a limit on zigā€™s behaviour here, or something incompatible that weā€™re trying to compile. I think this is one to try again in the future.

A small update on this:

Iā€™ve been working on a way to automatically import packages from the existing opam-repository into a ā€œcross-compile repositoryā€ and rewriting them to pass toolchain parameters and the correct package names when building them.

You can see the start of my work in the packman tool Iā€™ve been putting together for this purpose.

It essentially starts with:

  • the name of a standard opam repository
  • the name of a ā€œcrossā€ opam repository that has the OCaml zig-based cross-compiler defined already (in this case, https://github.com/chris-armstrong/opam-cross-lambda)

and takes:

  • a set of package names
  • a ā€œtargetā€ opam repository where the rewritten package definitions are to be saved
  • the cross toolchain name (e.g. x86_64_al2023)

It then:

  • uses the opam solver to resolve the packages and their runtime dependencies (test, dev, doc are dropped; depopt should be added although this does not seem to be working)
  • generate a new package with name <package>-cross-<cross_toolchain>
  • rewrite the build commands to add the toolchain name and set the package name explicitly (this is started for dune and topkg builders)

I see this whole work as transitory - Iā€™d really love to get something like this working automatically with duneā€™s new package management support - but for me, this is both a demo of what is possible, a way to explore requirements, and a way to learn about dune and opam more deeply.

7 Likes