Dart is both a lightweight weapon and the name of a programming language. The Dart VM of this language is built into the Flutter framework and is widely used in mobile development. So, can we break away from Flutter and embed the Dart VM solely for native projects such as a game engine or high-performance graphics application? This is the scenario that this article focuses on, namely, embedded integration of virtual machines.

Dart already provides a Node-like Dart runtime for direct use in terminals. However, this is a single application, not easy to integrate into other programs. How do you separate the Dart VM from this single application, or from the Flutter, for use alone? To solve this problem, you need to roughly understand the following three parts:

  • Basic concepts and how the Dart VM works.
  • How to compile the embeddable Dart VM static library for iOS.
  • How to embed the Dart VM on iOS and perform the simplest Hello World.

This article uses the iOS platform as an example to demonstrate how to get rid of Flutter and use the Dart VM programmatically in your own native project.

How the Dart VM works

The Dart VM executes code in a very different way than a familiar scripting engine like V8. The biggest difference is that the Dart VM does not support direct interpretation of the execution string source code. If you want to think of it as a convenient first scripting subsystem, this is definitely a disadvantage. This sacrifice in flexibility, however, has resulted in major breakthroughs in the engine’s engineering capabilities, including three different modes of operation:

  1. JIT execution based on AST (so-called Kernel Binary) files.
  2. JIT execution based on preheated snapshot (so called AppJIT).
  3. AOT execution based on AOT snapshot (so-called AppAOT).

The Dart VM can actually be configured to work in many other ways, such as by turning off JIT interpretation execution. Here are just a few of the most critical ones.

With the exception of the first, which is closest to the current JS engine, the Dart VM works in two modes that are more difficult to implement on the JS stack (although V8 also partially supports snapshots). Not only that, but the real-world Dart VM does even more. During the deep support of Flutter, the Dart VM’s execution mode on the mobile side is further different from the original desktop side. These distinctions are very confusing and need to be sorted out first.

Briefly, the Dart VM works differently in each of the scenarios listed below:

  • Perform standard on the desktopdartCommand, the engine will pass through the internal CFE Common Front End component, will.dartThe formatted source code is parsed into a binary AST syntax tree and then JIT executed on the main Isolate (similar to the main thread).Corresponding Mode 1.
  • Run this command on the desktopdart --snapshot-kind=app-jitCommand, the engine will parse.dartAfter the source code, JIT it with the training data and save the virtual machine state as.snapshotFormat snapshot file. This snapshot can be re-read by the VM to restore the JIT scene in one step, optimizing startup performance. As a typical example, Flutterflutter runThat’s what the command doesflutter_tools.snapshotSnapshot,Corresponding Mode 2.
  • Run this command on the desktopdart2nativeCommand, the Dart source code is compiled into platform machine code, obtained.aotFormat artifacts. This product is similar to the native ELF executable format and can be precompileddart_precompiled_runtimeDynamically load execution at runtime,Corresponding Mode 3.
  • Dart source code is compiled on the developer’s desktop with Mobile Flutter Debug mode.dillKernel Binary format, and then these.dillFiles are dynamically updated to mobile devices via RPC services. This is the basis of Flutter’s support for dark technologies like incremental compilation and hot overloading,Variant corresponding to pattern 1.
  • Dart source code is cross-compiled into ARM machine code on the developer’s desktop under mobile Flutter Release mode, linked to the pre-compiled runtime, corresponding to mode 3 variants.

Sounds complicated? In practice, just remember these simple rules:

  • The simplest Kernel Binary format is.dillPlatform universal.
  • The snapshot generated by AppJIT preheating is.snapshotFormat, platform is not universal.
  • The AOT compilation command generates.aotFormat file, platform is not common.

The Dart VM is based on the standard operating mode of Kernel Binary

Note the architectural separation between the CFE build front end and THE VM: although this is not noticeable when executing the.dart file directly, the situation is different in the mobile scenario of Flutter. Flutter encapsulates CFE directly into the desktop Flutter Tool command line project (a pure Dart implementation), so that the Flutter Engine (a hybrid of C++ and Dart implementation) on the mobile end contains only the VM part. As follows:

Dart VM running mode on Flutter

In Flutter, the compilation details of Dart VM are encapsulated by the framework. However, this is not difficult to understand in detail with VSCode breakpoint debugging of the Flutter Tool, which is no longer expanded.

Before we get into the hands-on phase below, let’s finish with some common questions:

  • Binary AST is not bytecode, but rather a binary representation of the syntax tree JSON structure compiled by Babel. See my popular science on the TC39 Binary AST proposal.
  • High-level languages can also be compiled directly into machine code, simply by linking to a native runtime that supports basic capabilities such as garbage collection and platform IO. Things like Go and Static TypeScript are implemented this way. What about the special dynamic parts like JS? Script interpreters can also be compiled to machine code, which in principle can be reversed to explain execution (so compiling to machine code is not necessarily fast; sometimes technology is based on shell-switching).
  • Dart does OT compilation not because AOT is necessarily better than JIT. In contrast, high-level languages such as Java tend to have higher JIT performance ceilings than AOT. The primary motivation for the Dart VM was to meet the long-standing iOS policy of JIT prohibition and match the characteristics of mobile scenarios such as short page dwell times, need for quick peak performance, and sensitivity to code volume.

Compile the Dart VM static library

Now that we’re fully familiar with the essentials of the Amway Dart VM on PPT, it’s time to get to work.

First, suppose we have a C++ project, how do we plug in the Dart VM for use as a scripting engine? As with any other C++ library, this requires headers and library files from third-party libraries. Common C++ libraries include header files in their include directory for external use and compile various. A library files for reuse by default. Troublingly, the Dart VM does not do this in the way the convention colloquially calls it, and there are no such examples available in the source tree as with Skia. Fortunately, Vyacheslav Egorov, who runs the Dart team and is the guy who optimizes JS performance beyond Rust, recently gave the unofficial Embedder Example. By putting the patch directly into the Dart source code, you can compile a sample C++ project embedded in the Dart VM based on the existing Dart VM build system. The C++ part of the specific code is a bit lengthy, can be summarized in the following steps:

  • indart::embedder::InitOnceafterDart_Initialize.
  • withDart_CreateIsolateGroupFromKernelLoad the Kernel Binary and create the corresponding Isolate.
  • Start theDart_RunLoopTo formally execute the Dart code.

The corresponding GN construction configuration is as follows (this is relatively rare, but the construction system of Google project is still very good after I am familiar with it, and I may make a systematic introduction later) :

Embed the executable file entry for the Dart VM
executable("embedder_example_1") {
  The executable depends on the static library defined below
  deps = [ ":libdartvm_for_embedding_nosnapshot_jit" ]
  sources = [ "embedder_example_1.cc" ]
  include_dirs = [ ".."]}A minimum static library containing the Dart VM
static_library("libdartvm_for_embedding_nosnapshot_jit") {
  deps = [
    ":standalone_dart_io".".. :libdart_jit".".. /platform:libdart_platform_jit"."//third_party/boringssl"."//third_party/zlib",
  ]

  sources = [
    "builtin.cc"."dart_embedder_api_impl.cc"]},Copy the code

This example is compiled like this:

# build system based on Dart to compile C++ artifacts
$ ninja -C xcodebuild/ReleaseX64/ embedder_example_1

Dart to hello.dill based on the Dart infrastructure
$ dart pkg/vm/bin/gen_kernel.dart \
  --platform xcodebuild/ReleaseX64/vm_platform_strong.dill \
  -o /tmp/hello.dill \
  /tmp/hello.dart

# Hello.dill with the compiled executable
$ ./xcodebuild/ReleaseX64/embedder_example_1 \
  out/ReleaseX64/vm_platform_strong.dill \
  /tmp/hello.dill
Copy the code

Based on the build system that comes with the Dart VM, this process can be done smoothly. But if you want to compile the C++ logic above in a third-party project, in addition to manually picking out the libdartvm_for_embedding_nosnapshot_jit.a static library from the Dart VM build product, you need to copy these header files to link properly:

  • dart/runtime/includeAll header files in the directory.
  • dart/runtime/platformThese header files in the directory:
    • assert.hMay cause Xcode conflicts, can be renamed todart_assert.h)
    • floating_point.h
    • globals.h
    • hashmap.h
    • memory_sanitizer.h

Once you’ve taken the first step, it’s natural to try cross-compiling static libraries for iOS. Here’s a weird problem: The Dart VM doesn’t provide an ios-oriented build configuration item (is_ios configuration, to be exact, but setting it will only cause the build to fail), nor does it provide documentation. This problem bothered me for so long that I read a lot about GN and Ninja build systems, and even tried to modify the Xcode build configurations they generated, without success. Later, Vyacheslav Egorov showed me a way to build Dart by relying on the Build environment of Flutter.

Personally, I still think it’s unreasonable, because you say if I want to build V8, why do I need to rely on The Chromium build environment? But for now, that’s all we can do. Here’s what it looks like:

Enter the dart directory in the third-party dependencies of Flutter Engine and add the following BUILD configurations to the Runtime /bin/ build.gn file:

static_library("libdartvm_with_utils") {
  complete_static_lib = true # XXX
  deps = [
    ":standalone_dart_io".".. :libdart_jit".".. /platform:libdart_platform_jit"."//third_party/boringssl"."//third_party/zlib",
  ]
  defines = [ "DEBUG" ]
  sources = [
    "builtin.cc"."dartutils.cc"."dart_embedder_api_impl.cc"]},Copy the code

Then use the build configuration of the Flutter Engine to perform the build:

Build in the working directory of the Flutter Engine
$ ninja -C out/ios_debug_sim_unopt libdartvm_with_utils
Copy the code

This gives us the libdartVM_with_utils. a file from the Build of the Flutter Engine. This is the static library of the Dart VM that can be accessed on iOS (this has all dependencies added through the violence configuration and is therefore very large). But it is not difficult to manually configure the rules later to optimize).

Embed Hello World running the Dart version

With static libraries, header files, and C++ entry, we can run the Dart VM independently on iOS. However, you also need to obtain a Kernel Binary file in.dill format for iOS. How do you do that?

If gen_kernel.dart is followed, the platform code will also be packaged into a.dill file, making even the simplest Hello World require several megabytes of volume. The more “extreme” approach here is to use Flutter. When the Flutter Run command is started, it compiles the.dill file and obtains all static resources before performing Xcode’s iOS app build. Here the Flutter Tool starts the CFE build service to which it is connected. The corresponding build products are placed in a temporary directory of the system. Their path is passed as interprocess communication messages, which can be found by searching.dill in the Flutter run -v log.

Therefore, the entire experimental process of independent access to the Dart VM on iOS generally includes the following steps:

  • Create a new Flutter empty project.
  • Change the entry to the Flutter project to an empty Dart version of Hello World, intercept the build directory of the Flutter Tool, and extract itapp.dillFile.
  • Build the Flutter Engine into the productvm_platform_strong.dillFile out.
  • With the OCpathForResourceMethod, take the top two.dillFormat file open aschar *Type them into the C++ Demo that integrated the Dart VM earlier.
  • The same Embedding Example C++ entry function is implemented.

Then, the first attempt failed spectacularly. The.dill file extracted from the flutter run command did not run on the iOS emulator.

Bypassed the twists and turns of the process, the final result was surprisingly simple: when building a Flutter environment, the two repositories of the Flutter Engine and the Flutter Tool must use exactly the same revision, otherwise they will not be compatible. If you build the Flutter Engine yourself, there are two versions of Dart that can run on your machine, one in the Host build of the Flutter Engine and one that comes with the Flutter Tool out of the box. We only need to verify that their versions are the same with dart –version.

Once the versioning problem is resolved, the.dill file corresponding to Hello World can be successfully executed. The result of the successful embedding is very simple, as shown in the figure below:

Unlike QuickJS, where the engine body has no IO capabilities at all, asynchronous IO and other capabilities are available once the Dart VM is successfully integrated. So by taking this step, the example is at least somewhat practical. However, since this article is only a feasibility test, there is no ready “out of the box” sample project source code. It is suggested that if we are interested in Embedding Examples, it is easy to use the upstream patch as the starting point.

conclusion

Because of the lack of documentation and Dart’s niche, the experiment was a bit of a detour, and a taste of how Google can be an “open source oligopoly” : there’s a lot of stuff on its own, but a lot of integration with its own products. If Google had operated Dart in a more community-like way, the current typical use of Dart might not have been almost exclusive to Flutter as it is today.

But we all know that Ryan Dahl’s attempt to take V8 out of Chromium led to node.js today. So if you take the Dart VM out of Flutter like this, does it open up new possibilities for the community? For embedded systems like Hongmeng LiteOS, for example, can Dart VM be a better application development solution than an embedded JS interpreter? Of course, not every innovation is going to be the next Node.js, but it’s worth trying.

This article is just the beginning of integrating the Dart VM. The Dart virtual machine has a lot of capabilities, but incremental compilation, hot reload, snapshot warm-up, AOT compilation, remote debugging, and more remain to be explored. If you are interested, please pay attention.

The resources

  • Dart VM Introduction
  • Snapshots – Dart Wiki
  • dart2native | Dart
  • Building – Dart Wiki
  • Compiling Flutter Engine
  • Embedding Dart – Reddit
  • Dart Embedding Examples
  • Build Embeddable VM Runtime on iOS
  • Flutter Tool Wiki