Cross-compiling Dart apps

J-P Nurmi
Flutter Community
Published in
4 min readSep 20, 2020

Dart is a nice and modern programming language that not only compiles to JavaScript, but can also be AOT-compiled to native standalone executables.

When building the Dart SDK, one can easily select the desired target architecture(s), such as X64 and/or ARM. Even though one can that way basically cross-compile the entire SDK, the dart2native compiler that is part of the SDK, does unfortunately not support cross-compilation. In order to compile Dart code to a native ARM binary, one would have to run the compiler on the target, which is not the solution we’re looking for.

The question is, what would it take to make dart2native capable of cross-compiling ARM binaries for Raspberry Pi, on an X64 host? Well, let’s start by taking a look at what the compiler actually does. From the source code we can see that it:

  1. generates an AOT kernel,
  2. generates an AOT snapshot, and
  3. combines the result with a Dart AOT runtime.

There’s a nice wiki page on GitHub that summarizes the different types of snapshots in Dart. It explains that kernel snapshots are CPU architecture agnostic, and that AOT snapshots can be run with dartaotruntime.

When it comes to dart2native, the steps 1 and 2 listed above are simply calling external commands, more or less like this:

Generating AOT kernel (gen_kernel)
Generating AOT snapshot (gen_snapshot)

Step 3, on the other hand, is not executing an external command, but programmatically appending a) the snapshot that results from steps 1–2 and b) the Dart AOT runtime. For reference, here’s the final structure of an AOT-compiled Dart executable.

Dart AOT executable — the structure

It is worth noting that the last step, merging the final app snapshot with the AOT runtime, is optional. It is only required when compiling fully standalone executables. Alternatively, one can execute AOT snapshots directly with the AOT runtime, which is an executable by itself. That way one can share the same AOT runtime for multiple Dart apps, which may be preferred in order to save disk space on systems with limited resources. If you are curious how this works in practice, take a closer look at your Dart SDK’s bin-directory. ;)

Now that we know what dart2native does under the hood, we may start tinkering and see if we could somehow produce ARM binaries on X64. As pointed out earlier, the AOT kernel is CPU architecture agnostic, so it doesn’t really matter which Dart SDK’s gen_kernel tool we use. Let’s assume that we already have a Dart SDK built for X64:

# sdk (x64)
$ ./tools/build.py -a x64 -m product create_sdk

For the next step, we need a gen_snapshot tool that is able to execute on the X64 host architecture, and produce AOT snapshots for the ARM target architecture. This is not nearly as complicated as it sounds, because you can simply build the tool for a target architecture called SIMARM, to achieve exactly that.

# gen_snapshot (simarm)
$ ./tools/build.py -a simarm -m product copy_gen_snapshot

Notice that we don’t necessarily need to build the entire SDK, as we only need the gen_snapshot tool. Last but not least, we need a dartaotruntime binary for the ARM target architecture.

# dartaotruntime (arm)
$ ./tools/build.py -a arm -m product copy_dartaotruntime

Out of curiosity, let’s quickly examine what we’ve got so far:

$ file out/ProductX64/dart-sdk/bin/dart2native 
[...]/dart2native: Bourne-Again shell script, [...]
$ file out/ProductX64/dart-sdk/bin/dart
[...]/dart: ELF 64-bit LSB shared object, x86-64, [...]
$ file out/ProductSIMARM/dart-sdk/bin/utils/gen_snapshot
[...]/gen_snapshot: ELF 32-bit LSB executable, Intel 80386, [...]
$ file out/ProductXARM/dart-sdk/bin/dartaotruntime
[...]/dartaotruntime: ELF 32-bit LSB shared object, ARM, [...]

As you can see, dart2native is actually just a script that executes the respective snapshot. Nevertheless, it looks like we have 32-bit gen_snapshot that is supposed to be able to generate AOT snapshots for ARM, and we have a dartaotruntime for ARM as well.

At this point, we basically have all the necessary building blocks at hand. We could execute gen_kernel + gen_snapshot manually, read dartaotruntime, and finally, write the executable by hand, but it’s not that straight-forward as it’s not just a matter of appending two files, but needs to include the correct padding, snapshot offset, and a magic number. What currently prevents us from doing this with the dart2native tool is that it doesn’t allow us to specify the gen_snapshot and dartaotruntime we want to use, but looks them up relative to dart2native. Thus, I’ve prepared a simple dart2native patch that makes it possible to specify the two from the outside:

After rebuilding dart2native with the above patch, we are ready to start testing cross-compilation. For testing, we are going to use the following tiny piece of Dart code, that reads and prints the CPU model from /proc/cpuinfo:

Dart test snippet

And the results, finally!

It works! :)

If you’d like this to be made easier out of the box, please thumbs up the following issue on GitHub! 👍

--

--