Tinkering around with kernel, Dart's intermediate format... And more.

Discuss on Hacker News: https://news.ycombinator.com/item?id=19844762

The continued development of Dart's unified frontend (shared across the VM, dev compiler, dart2js, etc.) has made it possible for changes to be made to the language much more quickly. The common IR that Dart now compiles to is called kernel.

There's not much documentation out at the moment about using kernel IR, but after messing around with a few projects, the following is what I know:

  • The VM no longer runs Dart source files; instead, they are compiled to kernel on-the-fly before execution.
  • You can directly run kernel files (*.dill) in the VM. This is what pub run, dart2js, and other top-level executables written in Dart do.
  • Kernel files given to the VM must include all code you want to run; therefore, all libraries your program uses must be linked into the file you give. The VM automatically links in dependencies from *.dart files you pass in.
  • You can create fully-linked .dill files by running dart --snapshot=<out.dill> <in.dart>.
  • To link kernel files together, you can simply concatenate them to one another [1].
  • Though kernel is a binary format, it is really more like a serialized Dart AST than an intermediate output like LLVM IR or JVM bytecode.
  • By producing kernel files, it is feasible for one to create a compiler frontend that targets the Dart VM (more on this later).
  • Dart kernel files can be compiled into machine code objects.
  • Versions of Dart kernel are not backwards compatible (this is important when using package:kernel).

[1]: https://github.com/dart-lang/sdk/issues/33335

AOT Compilation

I'm using the following version of Dart: 2.3.0-dev.0.3. I don't remember if these tools were included with Dart 2.2, but something makes me doubt it.

The process to compile *.dill into machine code is somewhat involved, and has a couple of steps.

First and foremost, you need to know the path to your Dart SDK. If you installed it via Homebrew, it should be /usr/local/opt/dart/libexec. On Linux, it should be /usr/lib/dart (if I remember correctly). On Windows, its location depends on what you used to install it, whether Chocolatey or the MSI installer.

Next, you'll want to create a kernel file. To create one specifically for AOT compilation, we need to find the gen_kernel tool in our Dart SDK, and run it. We'll also need to path to the precompiled Dart VM standard library, $DART_SDK/lib/_internal/vm_platform_strong.dill. Run the following to create the kernel file:

alias gen_kernel="dart $DART_SDK/bin/snapshots/gen_kernel.dart.snapshot"
gen_snapshot --platform="$DART_SDK/lib/_internal/vm_platform_strong.dill" \
-o myfile.dill myfile.dart

Next, we need to use the gen_snapshot tool to produce an assembly file:

alias gen_snapshot="$DART_SDK/bin/utils/gen_snapshot"
gen_snapshot \
  --snapshot_kind=app-aot-assembly \
  --assembly=myfile.S \
  myfile.dill

This assembly file can then be assembled into a shared object:

cc -shared -o myfile.o myfile.S

Using the dartaotruntime tool, we can then run our object file, by invoking dartaotruntime myfile.o. The startup is instant (though since there is no JIT, the peak performance will be different).

One thing I haven't yet figured out is how to produce a single executable. I tried to no avail disassembling dartaotruntime and re-linking it with my myfile.o, but as you can imagine, that's not typically possible with any executable. You could probably play around in the SDK repo and build an executable given some isolate snapshot data. (If I ever get around to this, I'll post a link here.).

In the Dart SDK repo, there is a bash script that streamlines this process: https://github.com/dart-lang/sdk/blob/master/pkg/vm/tool/precompiler2

Bullseye: A Functional Frontend for the Dart VM

You might recall that I mentioned that one can create a frontend for the Dart VM by compiling to kernel.

Bullseye is a recreational project I started about 5 months ago to do just that: https://github.com/thosakwe/bullseye

import "package:io/ansi.dart"

let main() =
  let msg = "Look! It's green text." in
  print (green.wrap msg)

It has a syntax nearly identical to that of OCaml, and is more or less what OCaml would look like if it targeted the Dart VM. It's not complete yet (mainly because I stopped touching it for months at a time), but you can play around with the test cases and see what results come of it.

While I won't say that writing the compiler has been outright easy, it definitely is a lot less difficult than directly targeting machine code, as the Bulllseye AST can be more or less directly translated into a Dart AST, which kernel files are.

If you think it's interesting, star it, and I'll know whether it's worth it to continue, or just drop it.

Hot Reloading VM Applications

One of the shinier features of Flutter is that applications can be hot reloaded while running in development mode, instead of having to stop and restart each time you make a minor (or major!) edit. When you consider the time it takes to compile code and then start a simulator, hot reloading saves a significant amount of time.

Hot reloading isn't just supported by Flutter, though - it's a feature of the Dart VM itself.

Angel hot reloading terminal screenshot

While working on Angel (https://angel-dart.dev - check it out!), a batteries-included server-side framework for Dart, I found it very easy to connect to what's known as the Dart VM service, and hot-reload the VM on filesystem changes. The cold startup time for Angel applications during development is quite noticeable, because the entire framework, along with whatever extension packages you are using, is compiled to kernel on each run. With hot reloading, the so-called "edit-refresh" cycle is greatly improved, and servers see changes in sub-second time. https://github.com/angel-dart/hot

To start the Dart VM service for an application, pass either the --enable-vm-service or --observe flag to the dart executable. Both flags can actually also take an option, representing the port number the service will listen on. The VM service is an RPC protocol that allows clients to interact with a running instance, and perform actions like hot reloading, pausing Isolates, getting stack traces, or even evaluating code on the fly. You simply connect to it via WebSocket - something the Dart Observatory automatically does. Just use package:vm_service_lib.

I have not had success trying to use the Dart VM service when running a .dill file, so perhaps that might not be a supported use case.

Dart Kernel Tools

There's a good amount of tooling within the Dart SDK, and in Pub packages, for manipulating kernel files:

  • $DART_SDK/bin/utils/gen_snapshot
  • $DART_SDK/bin/snapshots/gen_kernel.dart.snapshot
  • $DART_SDK/bin/snapshots/kernel-service.dart.snapshot
  • $DART_SDK/bin/snapshots/kernel_worker.dart.snapshot
  • package:build_modules
  • package:build_vm_compilers
  • package:front_end - seems to not be 100% stable yet, but is absolutely usable
  • package:kernel - Includes the AST, visitors, etc. Also includes binaries like pub run kernel:dump, which is probably the most useful.

Conclusion

Dart has always been very interesting to me, especially its VM. It took a lot of time to gather the information written above, so I sincerely hope that it might be of use to you. New versions of the SDK bring more functionality, and the community and ecosystem continue to grow, so I have no doubt that Dart will only really see positive changes from this point on.

If you'd like to continue the conversation: