This episode covers the Dart VM and native interop: working with the Dart VM and command-line tools, FFI for calling native libraries, building CLI utilities, and embedding Dart in a host application.

Behind the Dart language is a versatile engine: the Dart VM, which runs code with JIT, produces AOT binaries, and powers the command-line tools. Episode 16 dissects how to work directly with the Dart VM and connect it to the native world.
You'll learn to leverage VM tooling, call C libraries through FFI, build professional CLI utilities, and understand how Dart can be embedded in other host applications.
This interop capability is what lets Dart sit side by side with existing native ecosystems.
The Dart SDK provides a set of commands that run on top of the VM:
dart run bin/app.dart
dart compile exe bin/app.dart -o bin/app
dart analyze
dart testdart run executes code with JIT, dart compile exe produces an AOT binary, while dart analyze and dart test leverage the analyzer and test runner. This entire toolchain shares one SDK.
The VM supports snapshots — a captured state of the program for faster loading:
dart compile aot-snapshot bin/app.dart -o app.aot
dart run app.aotdart compile aot-snapshot bin/app.dart produces an AOT snapshot that loads faster than source code. Snapshots are often used for internal tools and services that need minimal startup time.
FFI (Foreign Function Interface) calls C functions from Dart without a wrapper:
import 'dart:ffi';
import 'dart:io';
final libc = DynamicLibrary.open('libc.so.6');
typedef StrlenNative = IntPtr Function(Pointer<Utf8>);
typedef StrlenDart = int Function(Pointer<Utf8>);
void main() {
final strlen = libc.lookupFunction<StrlenNative, StrlenDart>('strlen');
final teks = 'halo'.toNativeUtf8();
print('Panjang: ${strlen(teks)}');
calloc.free(teks);
}DynamicLibrary.open('libc.so.6') loads a C library, then lookupFunction binds the strlen function. Flutter uses FFI to call native code like SQLite and media players.
When using FFI, you're responsible for memory management: allocations with calloc must be freed, and pointers must not outlive the library's lifetime. Mistakes here cause crashes or memory corruption — test carefully and keep the interop surface as minimal as possible.
Dart is well suited for command-line tools. Accept arguments via dart:io:
import 'dart:io';
void main(List<String> args) {
if (args.isEmpty) {
stderr.writeln('Penggunaan: hello <nama>');
exitCode = 1;
return;
}
stdout.writeln('Halo, ${args.first}!');
}args.first takes the first argument from the terminal. Returning a non-zero exitCode signals failure — a standard pattern that scripts and pipelines understand.
For complex CLIs, use package:args, which handles flags, options, and automatic help:
dart pub add argsdart pub add args adds the official argument parser. With args, you get typed flags, validation, and --help output without writing it by hand.
Beyond calling native code from Dart, you can also embed Dart into a host application. The main approaches:
For a native application that wants to adopt Flutter gradually, use add-to-app: Flutter runs as a module inside an existing Android or iOS application. Two-way communication uses platform channels, allowing Flutter adoption without rewriting the entire app.
Key takeaways:
dart run, dart compile, and dart analyze are the core toolchain commands that run on the VM.DynamicLibrary and lookupFunction.dart:io reads arguments; package:args handles complex CLI parsing.In the next episode 17, we'll cover multiplatform and shared libraries — sharing code between mobile, web, server, and CLI, modular architecture and reusable packages, cross-platform testing, and integration with Flutter plugins.