Wednesday 22 March 2023

Libadalang, Alire, and macOS

Background

This exercise was prompted by the need for Scripted Testing to be supported by – as far as possible – code generation. The need is for the public or interfacing view of a supporting part (domain) of a system to be able to log calls made to it and to provide values on calls to it, using a scripting mechanism.

A previous project, ColdFrame, made extensive provision for this. An example of the scripting support code generated for

function Get (S : Input_Signal) return Boolean;

was

function Get
  (S : Input_Signal)
  return Boolean is
   Lock : ColdFrame.Stubs.Lock (ColdFrame.Stubs.Mutex'Access);
   pragma Unreferenced (Lock);
   Call : constant Positive := ColdFrame.Stubs.Note_Entry
     ("Digital_IO.Get");
begin
   Input_Signal'Output
     (ColdFrame.Stubs.Get_Input_Value_Stream
        ("Digital_IO.Get", "S", Call, S'Size),
      S);
   ColdFrame.Stubs.Check_For_Exception
     ("Digital_IO.Get", Call);
   return Boolean'Input
     (ColdFrame.Stubs.Get_Output_Value_Stream
        ("Digital_IO.Get", "return", Call));
end Get;

In that case, the framework was based on code generation from UML models, so generating this code wasn’t a stretch. In more typical circumstances, you’d want to generate these bodies from package specs.

Back in the day, one could have extracted the Ada spec’s structure using ASIS (e.g. ASIS2XML), but now that FSF GCC later than 10.3 doesn’t support ASIS some other approach is needed.

The obvious solution is to use Libadalang.

Libadalang

Libadalang is available in Alire, and the documentation includes code to traverse the parsed structure (that’s the Ada interface; there’s a Python interface too).

This traversal appears to flatten the tree, which of course makes it hard to see where one construct ends and the next begins; for example, the return type of a function is distinguished by appearing after all the parameters. Also, it wasn’t easy to work out from the documentation which subprograms were useful for what (note, the same was true for ASIS!) So, the alternative approach I adopted was an explict tree navigation, dumping the node types (and text content, where relevant); see libadalang2xml.

At this point, some of the issues with the actual build process became apparent.

An important thing to note is that most of the trouble came from my (dogged? foolish? pigheaded?) insistance on using Macs (a Macbook Pro with Intel silicon, x86-apple-darwin, and a Mac Mini with Apple silicon, aarch64-apple-darwin). Linux? no worries.

Building

I initiated an Alire binary crate,

alr init --bin libadalang2xml
alr with libadalang

using the Alire-provided x86_64 compiler, gnat_native 12.2.1. There were several stumbling blocks along the way, mainly to do with gnatcoll_gmp and libgmp.

  • gnatcoll_gmp needs gmp’s header files. One could of course go off and build gmp, but that seems rather contrary to the spirit of Alire, so get it via Homebrew.
    • On Intel silicon, Homebrew installs in /usr/local, while on Apple it’s in /opt/homebrew (don’t ask me why), and in any case the way we’ve set up GCC to accept either of the possible SDK options has had the unfortunate side-effect of removing /usr/local/include, /usr/local/lib from the compiler’s standard paths, so we have to set CFLAGS and LDFLAGS appropriately.
      • Some day, Alire may understand pkg-config.
  • Still on Intel silicon, all appears to go well until the final link, at which point the Libadalang and GNATCOLL libraries get a lot of missing symbols to do with tasking, protected types and semaphores.
    • This is because of gprbuild issue 97; gprbuild has the concept of a “static stand-alone library”, which it implements using a kludgetechnique which isn’t possible on Darwin.
  • The only reasonable solution seen for this was to build the libraries as relocatables (LIBRARY_TYPE=relocatable). Unfortunately, although gnatcoll_gmp builds just fine, langkit_support doesn’t, because it can’t find libgmp; this turns out to be because langkit_support.gpr doesn’t read LDFLAGS (nor, for that matter, does libadalang).
    • If I build an executable against a dynamic library, gprbuild includes the library’s Library_Options in the link; why not if it’s building one dynamic library against another? but in this case we have libadalang > langkit_support > gnatcoll_gmp, so perhaps it’s expecting a bit much).

This saga means that the only way to get a build is to use a full compiler suite, like that obtainable for GCC 12.2.0 (x86_64) or GCC 12.2.0 (aarch64).

Apple silicon

Let’s try building with the full x86_64 compiler under Rosetta on Apple silicon. Fails, because even though we have an x86_64 libgmp.dylib with the compiler, the linker sees the aarch64 (a.k.a. arm64) libgmp.dylib provisioned via Homebrew:

ld: warning: ignoring file /opt/homebrew/lib/libgmp.dylib, building for macOS-x86_64 but attempting to link with file built for macOS-arm64

Nothing for it by to use the full compiler appropriate for the architecture.

At least once the tool’s been built, you only have to use it!

No comments:

Post a Comment