You'll want our private fork of Dart: https://github.com/shorebirdtech/dart-sdk/tree/shorebird/dev
- Setting up your build environment
- Getting the sources
- Setting up Visual Studio Code
- Building
- Iteration
- Tips on debugging
- FFI tests
You'll need XCode installed, and the command line tools for XCode.
xcode-select --install
might help you with that?
I installed XCode from the App Store and then I think it prompted me when I launched it to install the command line tools?
Flutter has some instructions for C++ development, which are provided here for reference (you'll eventually need them, but may not for just Dart).
Dart has some public docs which may help: https://github.com/dart-lang/sdk/wiki/Building
It's much faster to use Google's servers to get Dart first, and then switch to our fork. GitHub is very slow at cloning the Dart repo.
Still this whole process may take 20+ minutes to run.
mkdir dart-sdk
cd dart-sdk
fetch dart
cd sdk
git remote rename origin upstream
git remote add origin https://github.com/shorebirdtech/dart-sdk.git
git fetch
git checkout origin/shorebird/dev
gclient sync -D
The -D isn't necessary in the gclient sync
it's just there to clean up
the empty directories from dependencies that main
dart has but our
fork of stable
(3.0.6 at time of writing) doesn't yet.
At least two extensions provide C++ completion, navigation, highlighting, etc. for Visual Studio Code (VS Code). There is the Microsoft C/C++ extension, and LLVM's clangd extension. The clangd extension seems to do a better job with most things and we've been using that one.
It is absolutely essential to get compile_commands.json
set up for your C++
extension. Without it you will not have any code completion or
error highlighting. You will also find that code guarded by #ifdef
is
incorrectly dimmed and hard to read.
Flutter has some instructions. Dart is similar.
Ninja can produce a compile_commands.json
in your build directory.
Build according to the instructions below and additionally pass the flag
--export-compile-commands
to build.py
. The JSON file will be put in the
build directory (e.g., dart-sdk/sdk/xcodebuild/DebugSIMARM64
on Mac and
dart-sdk/sdk/out/DebugSIMARM64
on other platforms).
The clangd extension will look for this file in parent directories of the
source code (and subdirectories named build
, but that doesn't match the Dart
directory structure). You can copy or symlink the generated file into
dart-sdk/sdk
or dart-sdk/sdk/runtime
.
(Note: the Microsoft C++ extension does not find compile_commands.json
in
the same way. It has a configuration option that contains an absolute path to
the file, so it's not necessary to move it. Use the VS Code command pallet to
find the C++ configuration UI, expand the "Advanced" section, and provide a
full path to the JSON file.)
You also likely want to use the sdk.code-workspace
workspace in
the root of the Dart SDK. Tell VS Code "File > Open Workspace from File...".
This will allow you to open the whole `sdk`` directory while ignoring
enough directories to not make the analyzer crash itself and to silence Dart
code errors and warnings that we don't care about.
We use two build directories. One houses the native arm64 AOT compiler and the other houses the simulator AOT runtime.
This mimics the Shorebird setup where we AOT compile with native tools and run in the simulator on the device. It's also slower than necessary to use the simulator, especially a debug build, to compile.
Build targets needed for AOT compilation, in your Dart SDK checkout directory
(i.e., dart-sdk/sdk
if you followed the instructions above):
./tools/build.py -m debug -a arm64 --no-goma --gn-args='dart_simulator_ffi=true' \
--gn-args='dart_debug_optimization_level=0' \
gen_snapshot vm_platform_strong.dill analyze_snapshot
The dart_debug_optimization_level
argument overrides the default -O2. It's
not needed for release
or product
mode bulds.
You might see a warning directory not found for option -L/usr/local/lib
.
This is Dart issue 52411
and nothing to worry about.
Build targets needed for the AOT runtime:
./tools/build.py -m debug -a simarm64 --no-goma --gn-args='dart_force_simulator=true' \
--gn-args='dart_debug_optimization_level=0' \
dart_precompiled_runtime_product
Edit ffi.dart
to change the function you want to call.
Edit hello.cc
to change the C/C++ side.
debug.sh
is a pre-made script to compile and start lldb
with the AOT snapshot binary.
By hand you can, build C:
clang++ hello.cc -shared -o libhello.so
Compile the Dart code to AOT:
DART_CONFIGURATION=DebugARM64 ../dart-sdk/sdk/pkg/vm/tool/precompiler2 ffi.dart ffi.aot
Run the AOT snapshot in lldb:
lldb ../dart-sdk/sdk/xcodebuild/DebugSIMARM64/dart_precompiled_runtime_product ffi.aot
Both the compiler (gen_snapshot
) and AOT runtime can dump disassembled
code. Pass the flags --disassemble --disassemble-stubs
. Disassembled code
is written to stderr
(redirect to a file with 2> /tmp/whatever.asm
).
Assembly code from the compiler has code comments embedded in it by default,
except in product
builds (pass --code-comments
to enable). Assembly code
in the snapshot does not have comments. If wanted you can find them by:
- Dump the code from the compiler, with comments
- Dump the code from the AOT runtime, without comments
The code objects will have different addresses. Serializing and deserializing them effectively relocates them. So you can find an address in the snapshot code, figure out what code object it is in, and then find the corresponding code object from the precompiler.
You can trace the simulator. Pass the flag --trace-sim-after
. It takes an
integer value which is the simulated instruction number to begin tracing at.
So for instance, --trace-sim-after=0
will trace right from the beginning.
If you know that something goes wrong later, for example by running in lldb
,
you can begin tracing at a later instruction number. I usually just dump the
entire trace to a file. Traces are also written to stderr
so they can be
redirected with 2>
.
You can trace and disassemble at the same time.
Sometimes you will want to change Log::ShouldLogForIsolateGroup to return true. The default impl is fine if your failure i son the main isolate, but if it's before that you will want to return true to catch failures during startup.
It's often useful to dump registers during Simulator::InstructionDecode right around where it calls the Disassembler.
In addition to the compiler and runtime, you'll need to build the supporting libraries for the FFI tests.
./tools/build.py -m debug -a simarm64 --no-goma --gn-args='dart_force_simulator=true' \
runtime/bin:ffi_test_functions runtime/bin:ffi_test_dynamic_library
Then you can run the tests.
We couldn't figure out how to make the Dart test harness use different configurations for the AOT compiler and runtime, so we wrote a simple harness ourselves.
The test harness has no concept of "expected failure" tests, some of the tests are "compile failure" tests so they will fail. We could just remove them from the list of tests to run, but we haven't yet.
dart run bin/run_ffi_tests.dart
Saving results as a baseline in ffi_tests_output.txt
:
dart run run_ffi_tests.dart > ffi_tests_output.txt
And go get some coffee. It takes a while. You can run individual tests or
a subset of the tests by passing a string argument to the script. You can pass
the flag -v
or --verbose
to the test harness to see what commands it is
running.
To build your own Dart for local testing with Flutter, we have some (needlessly private) instructions here: https://github.com/shorebirdtech/_shorebird/wiki/Hacking-on-the-engine-and-updater
First you have to determine which version of Flutter you're going to use
as your base. Since you have a specific Dart revision base you're trying to
build into Flutter, you need to figure out what version of Flutter shipped
with that exact Dart revision (or close enough). In the case of
shorebird/mixed
it's based on Dart 3.1.0, which was in Flutter 3.13.0.
You can figure out what version of Flutter/Dart your branch is based on by
walking back through git log
until you find a tag from the flutter team.
We keep Shorebird branches for each Flutter version we ship, so you can
find the branch for the version you're looking for. In this case it's
flutter_release/3.13.0
across all of our forks. You only need to check out
the "engine" revision since that contains the DEPS file and gclient sync
will
check out the other repos for you.
You'll need a checkout of the Flutter engine (documented above), including
making sure you run gclient sync
with the version of Flutter you're trying to
use. After the gclient sync, you can switch to your Dart branch.
e.g for mixed-mode:
cd third_party/dart
git checkout shorebird/mixed-mode
I've already done the above as part of a mixed
branch on the engine
repo.
It may still be necessary to update the DEPS of that branch to point to the
latest commit in the shorebird/mixed
branch of the Dart repo and then
gclient sync
again.
Flutter requires a host build to support device builds. You can build the host build with your custom Dart, but you'll need to build it with the same version of Dart that you're trying to use for iOS.
./flutter/tools/gn --runtime-mode=release --no-goma
ninja -C out/host_release flutter/build/archives:archive_gen_snapshot
I'm not 100% certain which targets are needed to build. Trying to build all of the default targets seems to take an hour or more on a modern machine.
archive_gen_snapshot is based on what the host_release builder seems to do: https://github.com/flutter/engine/blob/main/ci/builders/mac_host_engine.json#L168
Make sure the App you're running is configured to use the Release scheme: https://github.com/flutter/flutter/wiki/Debugging-the-engine#debugging-ios-builds-with-xcode
To build the engine for iOS with your changes:
./flutter/tools/gn --no-goma --runtime-mode=release --no-enable-unittests --ios --gn-arg='dart_force_simulator=true' && \
ninja -C out/ios_release
You can try running directly from the command line:
flutter run -d iphone --release --local-engine-src-path=$HOME/Documents/GitHub/engine/src --local-engine ios_release -v
However you're more likely to want to run inside XCode, where you have easy access to the debugger:
flutter build ios --config-only --local-engine-src-path=$HOME/Documents/GitHub/engine/src --local-engine ios_release
open ios/Runner.xcworkspace
However sometimes XCode builds will fail with "Unhandled Exception:" when
there is a failure in one of the Dart scripts which XCode calls out to.
The only way I know to see that error is to use the flutter run
flavor
of the command with -v
and then scroll up (the error will occur long before
the actual command execution stops).
"Unexpected Kernel Format Version XXX (expected YYY) when reading file:///...flutter_patched_sdk/platform_strong.dill"
This most often happens when you haven't built host_release
with the same
version of Dart as you're trying to use for ios_release
. Make sure you
gclient sync
with the same version of Flutter you're trying to use for iOS.
Linking is an experimental feature.
You will need analyze_snapshot (built above).
Compile the Dart code to AOT. We've written a compile.dart
helper script
to support compiling multiple files at once with the correct flags.
Take two dart files, e.g. before.dart
and after.dart
:
void main() {
print("Hello before!");
}
void main() {
print("Hello after!");
print("And another thing"); // a new call makes the snapshot diff more different.
}
Compile them with compile.dart
:
dart compile.dart before.dart after.dart
Then run the shorebird linker to link them:
dart ../dart-sdk/sdk/pkg/aot_tools/bin/aot_tools.dart link \
--analyze-snapshot ../dart-sdk/sdk/xcodebuild/DebugARM64/analyze_snapshot \
--base=before.aot --patch=after.aot
It will write out an after.vmcode
file which contains the two linker tables
one mapping from CPU -> Simulator offsets and the other in reverse with the
after.aot file appended to it.
dart ../dart-sdk/sdk/pkg/aot_tools/bin/shorebird_linker.dart before.aot after.aot
Found 3375 base codes and 3375 patch codes
Base size: 819000, patch size: 819016
Hash match found 2648 sharable functions with size 604796
Hash match found 730 sharable leaf functions with size 43180
Subgraph match found 1143 sharable functions with size 72292
No sim offset for cpu offset 0x1cc with hash 8957670f
...
../dart-sdk/sdk/xcodebuild/DebugSIMARM64/dart_precompiled_runtime_product --base_snapshot=before.aot after.vmcode
Loading failed: Not a 64-bit Mach-O file.
cpu_to_sim[0] = 68 68
sim_to_cpu[0] = 68 68
elf_file_offset = 1c000
Loading failed: Not a 64-bit Mach-O file.
base_snapshot: 0x103c78a40 0x103d57800
base_snapshot: 0x103c78a40 0x103d57800
Hello after!
Right now it just prints the table header and first entry, eventually we'll make it load into the isolate and do the actual linking.
This doesn't really belong here, but it's a convenient place to put it.
git checkout 3.1.5
gclient sync
tools/build.py -m debug -a arm64 runtime dart_precompiled_runtime gen_snapshot --no-goma --gn-args='dart_force_simulator=true'
tools/test.py -m debug -c dartkp -r dart_precompiled -a arm64 -p line corelib > mixed_mode_results.txt
See mixed_mode_results.txt
If you're changing the size of objects in the Dart VM, you might hit an error like this:
Code::ArrayTraits::elements_start_offset() got 176, Code_elements_start_offset expected 184
../../third_party/dart/runtime/vm/dart.cc: 248: error: CheckOffsets failed. Try updating offsets by running ./tools/run_offsets_extractor.sh
You will need a Linux machine to update them:
bash -x tools/run_offsets_extractor.sh
The -x is just so you can see what commands its running in case one fails.
For Shorebird employees we have a Linux machine in our cloud you can use:
% gcloud compute ssh kevin-linux
[...login...]
% cd dart-sdk/sdk
% bash -x tools/run_offsets_extractor.sh