Version 2.12.0-35.0.dev

Merge commit 'd03b7e31f52927de75d4495eeb033eed490af438' into 'dev'
diff --git a/pkg/test_runner/bin/test_runner.dart b/pkg/test_runner/bin/test_runner.dart
index 3f5e048..7cb77ae 100644
--- a/pkg/test_runner/bin/test_runner.dart
+++ b/pkg/test_runner/bin/test_runner.dart
@@ -23,22 +23,22 @@
 /// The default test directory layout is documented in "test_suite.dart", above
 /// `factory StandardTestSuite.forDirectory`.
 import "package:test_runner/src/options.dart";
+import "package:test_runner/src/build_configurations.dart";
 import "package:test_runner/src/test_configurations.dart";
 
 /// Runs all of the tests specified by the given command line [arguments].
-void main(List<String> arguments) {
+void main(List<String> arguments) async {
   // Parse the command line arguments to a configuration.
   var parser = OptionsParser();
+  var configurations = <TestConfiguration>[];
   try {
-    var configurations = parser.parse(arguments);
-    if (configurations == null || configurations.isEmpty) return;
-
-    // Run all of the configured tests.
-    // TODO(26372): Ensure that all tasks complete and return a future from this
-    // function.
-    testConfigurations(configurations);
+    configurations = parser.parse(arguments);
   } on OptionParseException catch (exception) {
     print(exception.message);
     exit(1);
   }
+  if (configurations.isEmpty) return;
+  await buildConfigurations(configurations);
+  // Run all of the configured tests.
+  await testConfigurations(configurations);
 }
diff --git a/pkg/test_runner/lib/src/build_configurations.dart b/pkg/test_runner/lib/src/build_configurations.dart
new file mode 100644
index 0000000..6179997
--- /dev/null
+++ b/pkg/test_runner/lib/src/build_configurations.dart
@@ -0,0 +1,92 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'configuration.dart';
+import 'utils.dart';
+
+Future buildConfigurations(List<TestConfiguration> configurations) async {
+  var startTime = DateTime.now();
+  if (!configurations.first.build) return;
+  final buildTargets = <String>{};
+  final modes = <Mode>{};
+  final architectures = <Architecture>{};
+  final systems = <System>{};
+  for (final configuration in configurations) {
+    final inner = configuration.configuration;
+    buildTargets.addAll(_selectBuildTargets(inner));
+    modes.add(inner.mode);
+    architectures.add(inner.architecture);
+    systems.add(inner.system);
+  }
+  if (buildTargets.isEmpty) return;
+  if (systems.length > 1) {
+    print('Unimplemented: building for multiple systems ${systems.join(',')}');
+    exit(1);
+  }
+  final system = systems.single;
+  final osFlags = <String>[];
+  if (system == System.android) {
+    osFlags.addAll(['--os', 'android']);
+  } else if (system == System.fuchsia) {
+    osFlags.addAll(['--os', 'fuchsia']);
+  } else {
+    final host = System.find(Platform.operatingSystem);
+    if (system != host) {
+      print('Unimplemented: running tests for $system on $host');
+      exit(1);
+    }
+  }
+  final command = [
+    'tools/build.py',
+    '-m',
+    modes.join(','),
+    '-a',
+    architectures.join(','),
+    ...osFlags,
+    ...buildTargets
+  ];
+  print('Running command: python ${command.join(' ')}');
+  final process = await Process.start('python', command);
+  stdout.nonBlocking.addStream(process.stdout);
+  stderr.nonBlocking.addStream(process.stderr);
+  final exitCode = await process.exitCode;
+  if (exitCode != 0) {
+    print('exit code: $exitCode');
+  }
+  var buildTime = niceTime(DateTime.now().difference(startTime));
+  print('--- Build time: $buildTime ---');
+}
+
+List<String> _selectBuildTargets(Configuration inner) {
+  final result = <String>[];
+  final compiler = inner.compiler;
+  const targetsForCompilers = {
+    Compiler.dartk: ['runtime'],
+    Compiler.dartkp: ['runtime', 'dart_precompiled_runtime'],
+    Compiler.appJitk: ['runtime'],
+    Compiler.fasta: ['create_sdk', 'dartdevc_test', 'kernel_platform_files'],
+    Compiler.dartdevk: ['dartdevc_test'],
+    Compiler.dart2js: ['create_sdk'],
+    Compiler.dart2analyzer: ['create_sdk'],
+    Compiler.specParser: <String>[],
+  };
+  result.addAll(targetsForCompilers[compiler]);
+
+  if (compiler == Compiler.dartkp &&
+      [Architecture.arm, Architecture.arm64, Architecture.arm_x64]
+          .contains(inner.architecture)) {
+    result.add('gen_snapshot');
+  }
+
+  if (compiler == Compiler.dartdevk && !inner.useSdk) {
+    result
+      ..remove('dartdevc_test')
+      ..add('dartdevc_test_local');
+  }
+
+  return result;
+}
diff --git a/pkg/test_runner/lib/src/configuration.dart b/pkg/test_runner/lib/src/configuration.dart
index a0bafae..12af614 100644
--- a/pkg/test_runner/lib/src/configuration.dart
+++ b/pkg/test_runner/lib/src/configuration.dart
@@ -27,6 +27,7 @@
       {this.configuration,
       this.progress,
       this.selectors,
+      this.build,
       this.testList,
       this.repeat,
       this.batch,
@@ -82,6 +83,7 @@
 
   final bool batch;
   final bool batchDart2JS;
+  final bool build;
   final bool copyCoreDumps;
   final bool rr;
   final bool fastTestsOnly;
diff --git a/pkg/test_runner/lib/src/options.dart b/pkg/test_runner/lib/src/options.dart
index 231dd81..b73f36c 100644
--- a/pkg/test_runner/lib/src/options.dart
+++ b/pkg/test_runner/lib/src/options.dart
@@ -164,6 +164,8 @@
 test options, specifying how tests should be run.''',
         abbr: 'n',
         hide: true),
+    _Option.bool(
+        'build', 'Build the necessary targets to test this configuration'),
     // TODO(sigmund): rename flag once we migrate all dart2js bots to the test
     // matrix.
     _Option.bool('host_checked', 'Run compiler with assertions enabled.',
@@ -355,6 +357,7 @@
   /// For printing out reproducing command lines, we don't want to add these
   /// options.
   static final _denylistedOptions = {
+    'build',
     'build_directory',
     'chrome',
     'clean_exit',
@@ -749,6 +752,7 @@
           configuration: innerConfiguration,
           progress: progress,
           selectors: selectors,
+          build: data["build"] as bool,
           testList: data["test_list_contents"] as List<String>,
           repeat: data["repeat"] as int,
           batch: !(data["noBatch"] as bool),
diff --git a/pkg/test_runner/lib/src/test_configurations.dart b/pkg/test_runner/lib/src/test_configurations.dart
index be34361..4d3925a 100644
--- a/pkg/test_runner/lib/src/test_configurations.dart
+++ b/pkg/test_runner/lib/src/test_configurations.dart
@@ -18,6 +18,8 @@
 import 'test_suite.dart';
 import 'utils.dart';
 
+export 'configuration.dart' show TestConfiguration;
+
 /// The directories that contain test suites which follow the conventions
 /// required by [StandardTestSuite]'s forDirectory constructor.
 ///
@@ -52,6 +54,7 @@
   Path('utils/tests/peg'),
 ];
 
+// TODO(26372): Ensure that the returned future awaits on all started tasks.
 Future testConfigurations(List<TestConfiguration> configurations) async {
   var startTime = DateTime.now();
 
diff --git a/runtime/bin/ffi_test/ffi_test_functions_vmspecific.cc b/runtime/bin/ffi_test/ffi_test_functions_vmspecific.cc
index ace3747..e642819 100644
--- a/runtime/bin/ffi_test/ffi_test_functions_vmspecific.cc
+++ b/runtime/bin/ffi_test/ffi_test_functions_vmspecific.cc
@@ -10,6 +10,7 @@
 #include <csignal>
 
 #include "platform/globals.h"
+#include "platform/memory_sanitizer.h"
 #if defined(HOST_OS_WINDOWS)
 #include <psapi.h>
 #include <windows.h>
@@ -47,6 +48,12 @@
 
 #define CHECK_EQ(X, Y) CHECK((X) == (Y))
 
+#define ENSURE(X)                                                              \
+  if (!(X)) {                                                                  \
+    fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, "Check failed: " #X);   \
+    exit(1);                                                                   \
+  }
+
 ////////////////////////////////////////////////////////////////////////////////
 // Functions for stress-testing.
 
@@ -271,6 +278,89 @@
 
 #endif  // defined(TARGET_OS_LINUX)
 
+DART_EXPORT void IGH_MsanUnpoison(void* start, intptr_t length) {
+  MSAN_UNPOISON(start, length);
+}
+
+DART_EXPORT Dart_Isolate IGH_CreateIsolate(const char* name, void* peer) {
+  struct Helper {
+    static void ShutdownCallback(void* ig_data, void* isolate_data) {
+      char* string = reinterpret_cast<char*>(isolate_data);
+      ENSURE(string[0] == 'a');
+      string[0] = 'x';
+    }
+    static void CleanupCallback(void* ig_data, void* isolate_data) {
+      char* string = reinterpret_cast<char*>(isolate_data);
+      ENSURE(string[2] == 'c');
+      string[2] = 'z';
+    }
+  };
+
+  Dart_Isolate parent = Dart_CurrentIsolate();
+  Dart_ExitIsolate();
+
+  char* error = nullptr;
+  Dart_Isolate child =
+      Dart_CreateIsolateInGroup(parent, name, &Helper::ShutdownCallback,
+                                &Helper::CleanupCallback, peer, &error);
+  if (child == nullptr) {
+    Dart_EnterIsolate(parent);
+    Dart_Handle error_obj = Dart_NewStringFromCString(error);
+    free(error);
+    Dart_ThrowException(error_obj);
+    return nullptr;
+  }
+  Dart_ExitIsolate();
+  Dart_EnterIsolate(parent);
+  return child;
+}
+
+DART_EXPORT void IGH_StartIsolate(Dart_Isolate child_isolate,
+                                  int64_t main_isolate_port,
+                                  const char* library_uri,
+                                  const char* function_name,
+                                  bool errors_are_fatal,
+                                  Dart_Port on_error_port,
+                                  Dart_Port on_exit_port) {
+  Dart_Isolate parent = Dart_CurrentIsolate();
+  Dart_ExitIsolate();
+  Dart_EnterIsolate(child_isolate);
+  {
+    Dart_EnterScope();
+
+    Dart_Handle library_name = Dart_NewStringFromCString(library_uri);
+    ENSURE(!Dart_IsError(library_name));
+
+    Dart_Handle library = Dart_LookupLibrary(library_name);
+    ENSURE(!Dart_IsError(library));
+
+    Dart_Handle fun = Dart_NewStringFromCString(function_name);
+    ENSURE(!Dart_IsError(fun));
+
+    Dart_Handle port = Dart_NewInteger(main_isolate_port);
+    ENSURE(!Dart_IsError(port));
+
+    Dart_Handle args[] = {
+        port,
+    };
+
+    Dart_Handle result = Dart_Invoke(library, fun, 1, args);
+    if (Dart_IsError(result)) {
+      fprintf(stderr, "Failed to invoke %s/%s in child isolate: %s\n",
+              library_uri, function_name, Dart_GetError(result));
+    }
+    ENSURE(!Dart_IsError(result));
+
+    Dart_ExitScope();
+  }
+
+  char* error = nullptr;
+  ENSURE(
+      Dart_RunLoopAsync(errors_are_fatal, on_error_port, on_exit_port, &error));
+
+  Dart_EnterIsolate(parent);
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // Initialize `dart_api_dl.h`
 DART_EXPORT intptr_t InitDartApiDL(void* data) {
diff --git a/runtime/bin/main.cc b/runtime/bin/main.cc
index 4b51532..a453899 100644
--- a/runtime/bin/main.cc
+++ b/runtime/bin/main.cc
@@ -245,12 +245,8 @@
     if (Dart_IsError(result)) goto failed;
   }
 
-  // Make the isolate runnable so that it is ready to handle messages.
   Dart_ExitScope();
-  Dart_ExitIsolate();
-  *error = Dart_IsolateMakeRunnable(isolate);
-  Dart_EnterIsolate(isolate);
-  return *error == nullptr;
+  return true;
 
 failed:
   *error = Utils::StrDup(Dart_GetError(result));
diff --git a/runtime/include/dart_api.h b/runtime/include/dart_api.h
index db0e630..189a6b7 100644
--- a/runtime/include/dart_api.h
+++ b/runtime/include/dart_api.h
@@ -1007,6 +1007,37 @@
                         void* isolate_group_data,
                         void* isolate_data,
                         char** error);
+/**
+ * Creates a new isolate inside the isolate group of [group_member].
+ *
+ * Requires there to be no current isolate.
+ *
+ * \param group_member An isolate from the same group into which the newly created
+ *   isolate should be born into. Other threads may not have entered / enter this
+ *   member isolate.
+ * \param name A short name for the isolate for debugging purposes.
+ * \param shutdown_callback A callback to be called when the isolate is being
+ *   shutdown (may be NULL).
+ * \param cleanup_callback A callback to be called when the isolate is being
+ *   cleaned up (may be NULL).
+ * \param isolate_data The embedder-specific data associated with this isolate.
+ * \param error Set to NULL if creation is successful, set to an error
+ *   message otherwise. The caller is responsible for calling free() on the
+ *   error message.
+ *
+ * \return The newly created isolate on success, or NULL if isolate creation
+ *   failed.
+ *
+ * If successful, the newly created isolate will become the current isolate.
+ */
+DART_EXPORT Dart_Isolate
+Dart_CreateIsolateInGroup(Dart_Isolate group_member,
+                          const char* name,
+                          Dart_IsolateShutdownCallback shutdown_callback,
+                          Dart_IsolateCleanupCallback cleanup_callback,
+                          void* child_isolate_data,
+                          char** error);
+
 /* TODO(turnidge): Document behavior when there is already a current
  * isolate. */
 
@@ -1483,6 +1514,31 @@
  *   error handle is returned.
  */
 DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_RunLoop();
+
+/**
+ * Lets the VM run message processing for the isolate.
+ *
+ * This function expects there to a current isolate and the current isolate
+ * must not have an active api scope. The VM will take care of making the
+ * isolate runnable (if not already), handles its message loop and will take
+ * care of shutting the isolate down once it's done.
+ *
+ * \param errors_are_fatal Whether uncaught errors should be fatal.
+ * \param on_error_port A port to notify on uncaught errors (or ILLEGAL_PORT).
+ * \param on_exit_port A port to notify on exit (or ILLEGAL_PORT).
+ * \param error A non-NULL pointer which will hold an error message if the call
+ *   fails. The error has to be free()ed by the caller.
+ *
+ * \return If successfull the VM takes owernship of the isolate and takes care
+ *   of its message loop. If not successful the caller retains owernship of the
+ *   isolate.
+ */
+DART_EXPORT DART_WARN_UNUSED_RESULT bool Dart_RunLoopAsync(
+    bool errors_are_fatal,
+    Dart_Port on_error_port,
+    Dart_Port on_exit_port,
+    char** error);
+
 /* TODO(turnidge): Should this be removed from the public api? */
 
 /**
diff --git a/runtime/lib/isolate.cc b/runtime/lib/isolate.cc
index 94de7aa..247adc3 100644
--- a/runtime/lib/isolate.cc
+++ b/runtime/lib/isolate.cc
@@ -352,6 +352,13 @@
       return;
     }
 
+    if (isolate->object_store()->root_library() == Library::null()) {
+      Dart_ShutdownIsolate();
+      FailedSpawn(
+          "The embedder has to ensure there is a root library (e.g. by calling "
+          "Dart_LoadScriptFromKernel).");
+    }
+
     Run(isolate);
   }
 
@@ -401,15 +408,19 @@
     state_->set_isolate(child);
 
     MutexLocker ml(child->mutex());
+
+    // We called out to the embedder to create/initialize a new isolate. The
+    // embedder callback sucessfully did so. It is now our responsibility to
+    // run the isolate.
+    // If the isolate was not marked as runnable, we'll do so here and run it
+    if (!child->is_runnable()) {
+      child->MakeRunnableLocked();
+    }
+    ASSERT(child->is_runnable());
+
     child->set_origin_id(state_->origin_id());
     child->set_spawn_state(std::move(state_));
-
-    // If the isolate is not marked as runnable, then the embedder might do so
-    // later on and the launch of the isolate will happen inside
-    // `Dart_IsolateMakeRunnable`.
-    if (child->is_runnable()) {
-      child->Run();
-    }
+    child->RunViaSpawnApi();
   }
 
   void FailedSpawn(const char* error) {
diff --git a/runtime/observatory/tests/service/service.status b/runtime/observatory/tests/service/service.status
index bc2bb42..f95c71d 100644
--- a/runtime/observatory/tests/service/service.status
+++ b/runtime/observatory/tests/service/service.status
@@ -11,6 +11,7 @@
 pause_on_start_and_exit_with_child_test: Pass, RuntimeError # Issue 33049
 reload_sources_test: Pass, Slow # Reload is slow on the bots
 valid_source_locations_test: Pass, Slow # Generally slow, even in release-x64.
+validate_timer_port_behavior_test: Skip  # Issue 44166
 
 [ $arch == arm ]
 process_service_test: Pass, Fail # Issue 24344
diff --git a/runtime/observatory_2/tests/service_2/service_2.status b/runtime/observatory_2/tests/service_2/service_2.status
index c16feab..6c5e1b4 100644
--- a/runtime/observatory_2/tests/service_2/service_2.status
+++ b/runtime/observatory_2/tests/service_2/service_2.status
@@ -11,6 +11,7 @@
 pause_on_start_and_exit_with_child_test: Pass, RuntimeError # Issue 33049
 reload_sources_test: Pass, Slow # Reload is slow on the bots
 valid_source_locations_test: Pass, Slow # Generally slow, even in release-x64.
+validate_timer_port_behavior_test: Skip  # Issue 44166
 
 [ $arch == arm ]
 process_service_test: Pass, Fail # Issue 24344
diff --git a/runtime/tests/vm/dart/isolates/dart_api_create_lightweight_isolate_test.dart b/runtime/tests/vm/dart/isolates/dart_api_create_lightweight_isolate_test.dart
new file mode 100644
index 0000000..9f9359d
--- /dev/null
+++ b/runtime/tests/vm/dart/isolates/dart_api_create_lightweight_isolate_test.dart
@@ -0,0 +1,225 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// SharedObjects=ffi_test_functions
+// VMOptions=--enable-isolate-groups --disable-heap-verification
+
+import 'dart:async';
+import 'dart:ffi';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:expect/expect.dart';
+import 'package:ffi/ffi.dart';
+
+import '../../../../../tests/ffi/dylib_utils.dart';
+
+final bool isAOT = Platform.executable.contains('dart_precompiled_runtime');
+final sdkRoot = Platform.script.resolve('../../../../../');
+
+class Isolate extends Struct {}
+
+abstract class FfiBindings {
+  static final ffiTestFunctions = dlopenPlatformSpecific("ffi_test_functions");
+
+  static final IGH_CreateIsolate = ffiTestFunctions.lookupFunction<
+      Pointer<Isolate> Function(Pointer<Utf8>, Pointer<Void>),
+      Pointer<Isolate> Function(
+          Pointer<Utf8>, Pointer<Void>)>('IGH_CreateIsolate');
+
+  static final IGH_StartIsolate = ffiTestFunctions.lookupFunction<
+      Pointer<Void> Function(Pointer<Isolate>, Int64, Pointer<Utf8>,
+          Pointer<Utf8>, IntPtr, Int64, Int64),
+      Pointer<Void> Function(Pointer<Isolate>, int, Pointer<Utf8>,
+          Pointer<Utf8>, int, int, int)>('IGH_StartIsolate');
+
+  static final Dart_CurrentIsolate = DynamicLibrary.executable()
+      .lookupFunction<Pointer<Isolate> Function(), Pointer<Isolate> Function()>(
+          "Dart_CurrentIsolate");
+
+  static final Dart_IsolateData = DynamicLibrary.executable().lookupFunction<
+      Pointer<Isolate> Function(Pointer<Isolate>),
+      Pointer<Isolate> Function(Pointer<Isolate>)>("Dart_IsolateData");
+
+  static final Dart_PostInteger = DynamicLibrary.executable()
+      .lookupFunction<IntPtr Function(Int64, Int64), int Function(int, int)>(
+          "Dart_PostInteger");
+
+  static Pointer<Isolate> createLightweightIsolate(
+      String name, Pointer<Void> peer) {
+    final cname = Utf8.toUtf8(name);
+    try {
+      final isolate = IGH_CreateIsolate(cname, peer);
+      Expect.isTrue(isolate.address != 0);
+      return isolate;
+    } finally {
+      free(cname);
+    }
+  }
+
+  static void invokeTopLevelAndRunLoopAsync(
+      Pointer<Isolate> isolate, SendPort sendPort, String name,
+      {bool? errorsAreFatal, SendPort? onError, SendPort? onExit}) {
+    final dartScript = sdkRoot.resolve(
+        'runtime/tests/vm/dart/isolates/dart_api_create_lightweight_isolate_test.dart');
+    final libraryUri = Utf8.toUtf8(dartScript.toString());
+    final functionName = Utf8.toUtf8(name);
+
+    IGH_StartIsolate(
+        isolate,
+        sendPort.nativePort,
+        libraryUri,
+        functionName,
+        errorsAreFatal == false ? 0 : 1,
+        onError != null ? onError.nativePort : 0,
+        onExit != null ? onExit.nativePort : 0);
+
+    free(libraryUri);
+    free(functionName);
+  }
+}
+
+void scheduleAsyncInvocation(void fun()) {
+  final rp = RawReceivePort();
+  rp.handler = (_) {
+    try {
+      fun();
+    } finally {
+      rp.close();
+    }
+  };
+  rp.sendPort.send(null);
+}
+
+Future withPeerPointer(fun(Pointer<Void> peer)) async {
+  final Pointer<Void> peer = Utf8.toUtf8('abc').cast();
+  try {
+    await fun(peer);
+  } catch (e, s) {
+    print('Exception: $e\nStack:$s');
+    rethrow;
+  } finally {
+    // The shutdown callback is called before the exit listeners are notified, so
+    // we can validate that a->x has been changed.
+    Expect.isTrue(Utf8.fromUtf8(peer.cast()).startsWith('xb'));
+
+    // The cleanup callback is called after after notifying exit listeners. So we
+    // wait a little here to ensure the write of the callback has arrived.
+    await Future.delayed(const Duration(milliseconds: 100));
+    Expect.equals('xbz', Utf8.fromUtf8(peer.cast()));
+    free(peer);
+  }
+}
+
+@pragma('vm:entry-point')
+void childTestIsolateData(int mainPort) {
+  final peerIsolateData =
+      FfiBindings.Dart_IsolateData(FfiBindings.Dart_CurrentIsolate());
+  FfiBindings.Dart_PostInteger(mainPort, peerIsolateData.address);
+}
+
+Future testIsolateData() async {
+  await withPeerPointer((Pointer<Void> peer) async {
+    final rp = ReceivePort();
+    final exit = ReceivePort();
+    final isolate = FfiBindings.createLightweightIsolate('debug-name', peer);
+    FfiBindings.invokeTopLevelAndRunLoopAsync(
+        isolate, rp.sendPort, 'childTestIsolateData',
+        onExit: exit.sendPort);
+
+    Expect.equals(peer.address, await rp.first);
+    await exit.first;
+
+    exit.close();
+    rp.close();
+  });
+}
+
+@pragma('vm:entry-point')
+void childTestMultipleErrors(int mainPort) {
+  scheduleAsyncInvocation(() {
+    for (int i = 0; i < 10; ++i) {
+      scheduleAsyncInvocation(() => throw 'error-$i');
+    }
+  });
+}
+
+Future testMultipleErrors() async {
+  await withPeerPointer((Pointer<Void> peer) async {
+    final rp = ReceivePort();
+    final accumulatedErrors = <dynamic>[];
+    final errors = ReceivePort()..listen(accumulatedErrors.add);
+    final exit = ReceivePort();
+    final isolate = FfiBindings.createLightweightIsolate('debug-name', peer);
+    FfiBindings.invokeTopLevelAndRunLoopAsync(
+        isolate, rp.sendPort, 'childTestMultipleErrors',
+        errorsAreFatal: false, onError: errors.sendPort, onExit: exit.sendPort);
+    await exit.first;
+    Expect.equals(10, accumulatedErrors.length);
+    for (int i = 0; i < 10; ++i) {
+      Expect.equals('error-$i', accumulatedErrors[i][0]);
+      Expect.isTrue(
+          accumulatedErrors[i][1].contains('childTestMultipleErrors'));
+    }
+
+    exit.close();
+    errors.close();
+    rp.close();
+  });
+}
+
+@pragma('vm:entry-point')
+void childTestFatalError(int mainPort) {
+  scheduleAsyncInvocation(() {
+    scheduleAsyncInvocation(() => throw 'error-0');
+    scheduleAsyncInvocation(() => throw 'error-1');
+  });
+}
+
+Future testFatalError() async {
+  await withPeerPointer((Pointer<Void> peer) async {
+    final rp = ReceivePort();
+    final accumulatedErrors = <dynamic>[];
+    final errors = ReceivePort()..listen(accumulatedErrors.add);
+    final exit = ReceivePort();
+    final isolate = FfiBindings.createLightweightIsolate('debug-name', peer);
+    FfiBindings.invokeTopLevelAndRunLoopAsync(
+        isolate, rp.sendPort, 'childTestFatalError',
+        errorsAreFatal: true, onError: errors.sendPort, onExit: exit.sendPort);
+    await exit.first;
+    Expect.equals(1, accumulatedErrors.length);
+    Expect.equals('error-0', accumulatedErrors[0][0]);
+    Expect.isTrue(accumulatedErrors[0][1].contains('childTestFatalError'));
+
+    exit.close();
+    errors.close();
+    rp.close();
+  });
+}
+
+Future testAot() async {
+  await testIsolateData();
+  await testMultipleErrors();
+  await testFatalError();
+}
+
+Future testJit() async {
+  dynamic exception;
+  try {
+    FfiBindings.createLightweightIsolate('debug-name', Pointer.fromAddress(0));
+  } catch (e) {
+    exception = e;
+  }
+  Expect.isTrue(exception
+      .toString()
+      .contains('Lightweight isolates are not yet ready in JIT mode'));
+}
+
+Future main(args) async {
+  if (isAOT) {
+    await testAot();
+  } else {
+    await testJit();
+  }
+}
diff --git a/runtime/tests/vm/dart_2/isolates/dart_api_create_lightweight_isolate_test.dart b/runtime/tests/vm/dart_2/isolates/dart_api_create_lightweight_isolate_test.dart
new file mode 100644
index 0000000..89045e8
--- /dev/null
+++ b/runtime/tests/vm/dart_2/isolates/dart_api_create_lightweight_isolate_test.dart
@@ -0,0 +1,234 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// SharedObjects=ffi_test_functions
+// VMOptions=--enable-isolate-groups --disable-heap-verification
+
+import 'dart:async';
+import 'dart:ffi';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:expect/expect.dart';
+import 'package:ffi/ffi.dart';
+
+import '../../../../../tests/ffi/dylib_utils.dart';
+
+final bool isAOT = Platform.executable.contains('dart_precompiled_runtime');
+final sdkRoot = Platform.script.resolve('../../../../../');
+
+class Isolate extends Struct {}
+
+abstract class FfiBindings {
+  static final ffiTestFunctions = dlopenPlatformSpecific("ffi_test_functions");
+
+  static final IGH_MsanUnpoison = ffiTestFunctions.lookupFunction<
+      Pointer<Isolate> Function(Pointer<Void>, IntPtr),
+      Pointer<Isolate> Function(Pointer<Void>, int)>('IGH_MsanUnpoison');
+
+  static final IGH_CreateIsolate = ffiTestFunctions.lookupFunction<
+      Pointer<Isolate> Function(Pointer<Utf8>, Pointer<Void>),
+      Pointer<Isolate> Function(
+          Pointer<Utf8>, Pointer<Void>)>('IGH_CreateIsolate');
+
+  static final IGH_StartIsolate = ffiTestFunctions.lookupFunction<
+      Pointer<Void> Function(Pointer<Isolate>, Int64, Pointer<Utf8>,
+          Pointer<Utf8>, IntPtr, Int64, Int64),
+      Pointer<Void> Function(Pointer<Isolate>, int, Pointer<Utf8>,
+          Pointer<Utf8>, int, int, int)>('IGH_StartIsolate');
+
+  static final Dart_CurrentIsolate = DynamicLibrary.executable()
+      .lookupFunction<Pointer<Isolate> Function(), Pointer<Isolate> Function()>(
+          "Dart_CurrentIsolate");
+
+  static final Dart_IsolateData = DynamicLibrary.executable().lookupFunction<
+      Pointer<Isolate> Function(Pointer<Isolate>),
+      Pointer<Isolate> Function(Pointer<Isolate>)>("Dart_IsolateData");
+
+  static final Dart_PostInteger = DynamicLibrary.executable()
+      .lookupFunction<IntPtr Function(Int64, Int64), int Function(int, int)>(
+          "Dart_PostInteger");
+
+  static Pointer<Isolate> createLightweightIsolate(
+      String name, Pointer<Void> peer) {
+    final cname = Utf8.toUtf8(name);
+    IGH_MsanUnpoison(cname.cast(), name.length + 10);
+    try {
+      final isolate = IGH_CreateIsolate(cname, peer);
+      Expect.isTrue(isolate.address != 0);
+      return isolate;
+    } finally {
+      free(cname);
+    }
+  }
+
+  static void invokeTopLevelAndRunLoopAsync(
+      Pointer<Isolate> isolate, SendPort sendPort, String name,
+      {bool errorsAreFatal, SendPort onError, SendPort onExit}) {
+    final dartScriptUri = sdkRoot.resolve(
+        'runtime/tests/vm/dart_2/isolates/dart_api_create_lightweight_isolate_test.dart');
+    final dartScript = dartScriptUri.toString();
+    final libraryUri = Utf8.toUtf8(dartScript);
+    IGH_MsanUnpoison(libraryUri.cast(), dartScript.length + 1);
+    final functionName = Utf8.toUtf8(name);
+    IGH_MsanUnpoison(functionName.cast(), name.length + 1);
+
+    IGH_StartIsolate(
+        isolate,
+        sendPort.nativePort,
+        libraryUri,
+        functionName,
+        errorsAreFatal == false ? 0 : 1,
+        onError != null ? onError.nativePort : 0,
+        onExit != null ? onExit.nativePort : 0);
+
+    free(libraryUri);
+    free(functionName);
+  }
+}
+
+void scheduleAsyncInvocation(void fun()) {
+  final rp = RawReceivePort();
+  rp.handler = (_) {
+    try {
+      fun();
+    } finally {
+      rp.close();
+    }
+  };
+  rp.sendPort.send(null);
+}
+
+Future withPeerPointer(fun(Pointer<Void> peer)) async {
+  final Pointer<Void> peer = Utf8.toUtf8('abc').cast();
+  FfiBindings.IGH_MsanUnpoison(peer.cast(), 'abc'.length + 1);
+  try {
+    await fun(peer);
+  } catch (e, s) {
+    print('Exception: $e\nStack:$s');
+    rethrow;
+  } finally {
+    // The shutdown callback is called before the exit listeners are notified, so
+    // we can validate that a->x has been changed.
+    Expect.isTrue(Utf8.fromUtf8(peer.cast()).startsWith('xb'));
+
+    // The cleanup callback is called after after notifying exit listeners. So we
+    // wait a little here to ensure the write of the callback has arrived.
+    await Future.delayed(const Duration(milliseconds: 100));
+    Expect.equals('xbz', Utf8.fromUtf8(peer.cast()));
+    free(peer);
+  }
+}
+
+@pragma('vm:entry-point')
+void childTestIsolateData(int mainPort) {
+  final peerIsolateData =
+      FfiBindings.Dart_IsolateData(FfiBindings.Dart_CurrentIsolate());
+  FfiBindings.Dart_PostInteger(mainPort, peerIsolateData.address);
+}
+
+Future testIsolateData() async {
+  await withPeerPointer((Pointer<Void> peer) async {
+    final rp = ReceivePort();
+    final exit = ReceivePort();
+    final isolate = FfiBindings.createLightweightIsolate('debug-name', peer);
+    FfiBindings.invokeTopLevelAndRunLoopAsync(
+        isolate, rp.sendPort, 'childTestIsolateData',
+        onExit: exit.sendPort);
+
+    Expect.equals(peer.address, await rp.first);
+    await exit.first;
+
+    exit.close();
+    rp.close();
+  });
+}
+
+@pragma('vm:entry-point')
+void childTestMultipleErrors(int mainPort) {
+  scheduleAsyncInvocation(() {
+    for (int i = 0; i < 10; ++i) {
+      scheduleAsyncInvocation(() => throw 'error-$i');
+    }
+  });
+}
+
+Future testMultipleErrors() async {
+  await withPeerPointer((Pointer<Void> peer) async {
+    final rp = ReceivePort();
+    final accumulatedErrors = <dynamic>[];
+    final errors = ReceivePort()..listen(accumulatedErrors.add);
+    final exit = ReceivePort();
+    final isolate = FfiBindings.createLightweightIsolate('debug-name', peer);
+    FfiBindings.invokeTopLevelAndRunLoopAsync(
+        isolate, rp.sendPort, 'childTestMultipleErrors',
+        errorsAreFatal: false, onError: errors.sendPort, onExit: exit.sendPort);
+    await exit.first;
+    Expect.equals(10, accumulatedErrors.length);
+    for (int i = 0; i < 10; ++i) {
+      Expect.equals('error-$i', accumulatedErrors[i][0]);
+      Expect.isTrue(
+          accumulatedErrors[i][1].contains('childTestMultipleErrors'));
+    }
+
+    exit.close();
+    errors.close();
+    rp.close();
+  });
+}
+
+@pragma('vm:entry-point')
+void childTestFatalError(int mainPort) {
+  scheduleAsyncInvocation(() {
+    scheduleAsyncInvocation(() => throw 'error-0');
+    scheduleAsyncInvocation(() => throw 'error-1');
+  });
+}
+
+Future testFatalError() async {
+  await withPeerPointer((Pointer<Void> peer) async {
+    final rp = ReceivePort();
+    final accumulatedErrors = <dynamic>[];
+    final errors = ReceivePort()..listen(accumulatedErrors.add);
+    final exit = ReceivePort();
+    final isolate = FfiBindings.createLightweightIsolate('debug-name', peer);
+    FfiBindings.invokeTopLevelAndRunLoopAsync(
+        isolate, rp.sendPort, 'childTestFatalError',
+        errorsAreFatal: true, onError: errors.sendPort, onExit: exit.sendPort);
+    await exit.first;
+    Expect.equals(1, accumulatedErrors.length);
+    Expect.equals('error-0', accumulatedErrors[0][0]);
+    Expect.isTrue(accumulatedErrors[0][1].contains('childTestFatalError'));
+
+    exit.close();
+    errors.close();
+    rp.close();
+  });
+}
+
+Future testAot() async {
+  await testIsolateData();
+  await testMultipleErrors();
+  await testFatalError();
+}
+
+Future testJit() async {
+  dynamic exception;
+  try {
+    FfiBindings.createLightweightIsolate('debug-name', Pointer.fromAddress(0));
+  } catch (e) {
+    exception = e;
+  }
+  Expect.isTrue(exception
+      .toString()
+      .contains('Lightweight isolates are not yet ready in JIT mode'));
+}
+
+Future main(args) async {
+  if (isAOT) {
+    await testAot();
+  } else {
+    await testJit();
+  }
+}
diff --git a/runtime/tests/vm/vm.status b/runtime/tests/vm/vm.status
index 16ee403..3091db9 100644
--- a/runtime/tests/vm/vm.status
+++ b/runtime/tests/vm/vm.status
@@ -327,12 +327,14 @@
 cc/Profiler_TrivialRecordAllocation: SkipByDesign
 cc/Profiler_TypedArrayAllocation: SkipByDesign
 cc/Service_Profile: SkipByDesign
+dart/isolates/dart_api_create_lightweight_isolate_test: SkipByDesign # Test uses dart:ffi which is not supported on simulators.
 dart/isolates/thread_pool_test: SkipByDesign # Test uses dart:ffi which is not supported on simulators.
 dart/regress_41971_test: SkipByDesign # dart:ffi is not supported on simulator
 dart/sdk_hash_test: SkipSlow # gen_kernel is slow to run on simarm
 dart/unboxed_param_args_descriptor_test: SkipByDesign # FFI helper not supported on simulator
 dart/unboxed_param_tear_off_test: SkipByDesign # FFI helper not supported on simulator
 dart/unboxed_param_test: SkipByDesign # FFI helper not supported on simulator
+dart_2/isolates/dart_api_create_lightweight_isolate_test: SkipByDesign # Test uses dart:ffi which is not supported on simulators.
 dart_2/isolates/thread_pool_test: SkipByDesign # Test uses dart:ffi which is not supported on simulators.
 dart_2/regress_41971_test: SkipByDesign # dart:ffi is not supported on simulator
 dart_2/sdk_hash_test: SkipSlow # gen_kernel is slow to run on simarm
diff --git a/runtime/vm/dart.cc b/runtime/vm/dart.cc
index 5d7112a..f570648 100644
--- a/runtime/vm/dart.cc
+++ b/runtime/vm/dart.cc
@@ -1068,7 +1068,7 @@
   Isolate* isolate = thread->isolate();
   void* isolate_group_data = isolate->group()->embedder_data();
   void* isolate_data = isolate->init_callback_data();
-  Dart_IsolateShutdownCallback callback = Isolate::ShutdownCallback();
+  Dart_IsolateShutdownCallback callback = isolate->on_shutdown_callback();
   if (callback != NULL) {
     TransitionVMToNative transition(thread);
     (callback)(isolate_group_data, isolate_data);
diff --git a/runtime/vm/dart_api_impl.cc b/runtime/vm/dart_api_impl.cc
index 7c1e68c..7209a5e 100644
--- a/runtime/vm/dart_api_impl.cc
+++ b/runtime/vm/dart_api_impl.cc
@@ -1563,6 +1563,39 @@
   return isolate;
 }
 
+DART_EXPORT Dart_Isolate
+Dart_CreateIsolateInGroup(Dart_Isolate group_member,
+                          const char* name,
+                          Dart_IsolateShutdownCallback shutdown_callback,
+                          Dart_IsolateCleanupCallback cleanup_callback,
+                          void* child_isolate_data,
+                          char** error) {
+  CHECK_NO_ISOLATE(Isolate::Current());
+  auto member = reinterpret_cast<Isolate*>(group_member);
+  if (member->IsScheduled()) {
+    FATAL("The given member isolate (%s) must not have been entered.",
+          member->name());
+  }
+
+  *error = nullptr;
+
+  Isolate* isolate;
+#if defined(DART_PRECOMPILED_RUNTIME)
+  isolate = CreateWithinExistingIsolateGroupAOT(member->group(), name, error);
+  if (isolate != nullptr) {
+    isolate->set_origin_id(member->origin_id());
+    isolate->set_init_callback_data(child_isolate_data);
+    isolate->set_on_shutdown_callback(shutdown_callback);
+    isolate->set_on_cleanup_callback(cleanup_callback);
+  }
+#else
+  *error = Utils::StrDup("Lightweight isolates are not yet ready in JIT mode.");
+  isolate = nullptr;
+#endif
+
+  return Api::CastIsolate(isolate);
+}
+
 DART_EXPORT void Dart_ShutdownIsolate() {
   Thread* T = Thread::Current();
   Isolate* I = T->isolate();
@@ -2001,18 +2034,11 @@
     FATAL1("%s expects argument 'isolate' to be non-null.", CURRENT_FUNC);
   }
   // TODO(16615): Validate isolate parameter.
-  Isolate* iso = reinterpret_cast<Isolate*>(isolate);
-  const char* error;
-  if (iso->object_store()->root_library() == Library::null()) {
-    // The embedder should have called Dart_LoadScriptFromKernel by now.
-    error = "Missing root library";
-  } else {
-    error = iso->MakeRunnable();
-  }
-  if (error != NULL) {
+  const char* error = reinterpret_cast<Isolate*>(isolate)->MakeRunnable();
+  if (error != nullptr) {
     return Utils::StrDup(error);
   }
-  return NULL;
+  return nullptr;
 }
 
 // --- Messages and Ports ---
@@ -2097,6 +2123,53 @@
   return Api::Success();
 }
 
+DART_EXPORT bool Dart_RunLoopAsync(bool errors_are_fatal,
+                                   Dart_Port on_error_port,
+                                   Dart_Port on_exit_port,
+                                   char** error) {
+  auto thread = Thread::Current();
+  auto isolate = thread->isolate();
+  CHECK_ISOLATE(isolate);
+  *error = nullptr;
+
+  if (thread->api_top_scope() != nullptr) {
+    *error = Utils::StrDup("There must not be an active api scope.");
+    return false;
+  }
+
+  if (!isolate->is_runnable()) {
+    const char* error_msg = isolate->MakeRunnable();
+    if (error_msg != nullptr) {
+      *error = Utils::StrDup(error_msg);
+      return false;
+    }
+  }
+
+  isolate->SetErrorsFatal(errors_are_fatal);
+
+  if (on_error_port != ILLEGAL_PORT || on_exit_port != ILLEGAL_PORT) {
+    auto thread = Thread::Current();
+    TransitionNativeToVM transition(thread);
+    StackZone zone(thread);
+    HANDLESCOPE(thread);
+
+    if (on_error_port != ILLEGAL_PORT) {
+      const auto& port =
+          SendPort::Handle(thread->zone(), SendPort::New(on_error_port));
+      isolate->AddErrorListener(port);
+    }
+    if (on_exit_port != ILLEGAL_PORT) {
+      const auto& port =
+          SendPort::Handle(thread->zone(), SendPort::New(on_exit_port));
+      isolate->AddExitListener(port, Instance::null_instance());
+    }
+  }
+
+  Dart_ExitIsolate();
+  isolate->RunViaEmbedder();
+  return true;
+}
+
 DART_EXPORT Dart_Handle Dart_HandleMessage() {
   Thread* T = Thread::Current();
   Isolate* I = T->isolate();
diff --git a/runtime/vm/dart_api_impl_test.cc b/runtime/vm/dart_api_impl_test.cc
index fea59eb..e65800f 100644
--- a/runtime/vm/dart_api_impl_test.cc
+++ b/runtime/vm/dart_api_impl_test.cc
@@ -7866,10 +7866,10 @@
       "}\n";
 
   // Create an isolate.
-  Dart_Isolate isolate = TestCase::CreateTestIsolate();
+  auto isolate = reinterpret_cast<Isolate*>(TestCase::CreateTestIsolate());
   EXPECT(isolate != NULL);
 
-  Isolate::SetShutdownCallback(IsolateShutdownRunDartCodeTestCallback);
+  isolate->set_on_shutdown_callback(IsolateShutdownRunDartCodeTestCallback);
 
   {
     Dart_EnterScope();
@@ -7887,8 +7887,6 @@
   // The shutdown callback has not been called.
   EXPECT_EQ(0, add_result);
 
-  EXPECT(isolate != NULL);
-
   // Shutdown the isolate.
   Dart_ShutdownIsolate();
 
diff --git a/runtime/vm/isolate.cc b/runtime/vm/isolate.cc
index 914eaa9..f85ce06 100644
--- a/runtime/vm/isolate.cc
+++ b/runtime/vm/isolate.cc
@@ -1644,6 +1644,8 @@
           reload_every_n_stack_overflow_checks_(FLAG_reload_every),
 #endif  // !defined(PRODUCT)
       start_time_micros_(OS::GetCurrentMonotonicMicros()),
+      on_shutdown_callback_(Isolate::ShutdownCallback()),
+      on_cleanup_callback_(Isolate::CleanupCallback()),
       random_(),
       mutex_(NOT_IN_PRODUCT("Isolate::mutex_")),
       constant_canonicalization_mutex_(
@@ -2048,16 +2050,31 @@
 #endif  // !defined(PRODUCT) && !defined(DART_PRECOMPILED_RUNTIME)
 
 const char* Isolate::MakeRunnable() {
-  ASSERT(Isolate::Current() == nullptr);
-
   MutexLocker ml(&mutex_);
   // Check if we are in a valid state to make the isolate runnable.
   if (is_runnable() == true) {
     return "Isolate is already runnable";
   }
+  if (spawn_state() != nullptr) {
+    return "The embedder has to make the isolate runnable during isolate "
+           "creation / initialization callback.";
+  }
+  if (object_store()->root_library() == Library::null()) {
+    return "The embedder has to ensure there is a root library (e.g. by "
+           "calling Dart_LoadScriptFromKernel ).";
+  }
+  MakeRunnableLocked();
+  return nullptr;
+}
+
+void Isolate::MakeRunnableLocked() {
+  ASSERT(mutex_.IsOwnedByCurrentThread());
+  ASSERT(!is_runnable());
+  ASSERT(spawn_state() == nullptr);
+  ASSERT(object_store()->root_library() != Library::null());
+
   // Set the isolate as runnable and if we are being spawned schedule
   // isolate on thread pool for execution.
-  ASSERT(object_store()->root_library() != Library::null());
   set_is_runnable(true);
 #ifndef PRODUCT
   if (!Isolate::IsSystemIsolate(this)) {
@@ -2066,16 +2083,6 @@
     }
   }
 #endif  // !PRODUCT
-  IsolateSpawnState* state = spawn_state();
-  if (state != nullptr) {
-    // If the embedder does not make the isolate runnable during the
-    // `create_isolate_group`/`initialize_isolate` embedder callbacks but rather
-    // some time in the future, we'll hit this case.
-    // WARNING: This is currently untested - we might consider changing our APIs
-    // to disallow two different flows.
-    ASSERT(this == state->isolate());
-    Run();
-  }
 #if defined(SUPPORT_TIMELINE)
   TimelineStream* stream = Timeline::GetIsolateStream();
   ASSERT(stream != nullptr);
@@ -2092,7 +2099,6 @@
   }
   GetRunnableLatencyMetric()->set_value(UptimeMicros());
 #endif  // !PRODUCT
-  return nullptr;
 }
 
 bool Isolate::VerifyPauseCapability(const Object& capability) const {
@@ -2411,11 +2417,18 @@
   sticky_error_ = sticky_error;
 }
 
-void Isolate::Run() {
+void Isolate::RunViaSpawnApi() {
+  ASSERT(spawn_state() != nullptr);
   message_handler()->Run(group()->thread_pool(), RunIsolate, ShutdownIsolate,
                          reinterpret_cast<uword>(this));
 }
 
+void Isolate::RunViaEmbedder() {
+  ASSERT(spawn_state() == nullptr);
+  message_handler()->Run(group()->thread_pool(), nullptr, ShutdownIsolate,
+                         reinterpret_cast<uword>(this));
+}
+
 void Isolate::AddClosureFunction(const Function& function) const {
   ASSERT(!Compiler::IsBackgroundCompilation());
   GrowableObjectArray& closures =
@@ -2628,7 +2641,7 @@
   // Cache these two fields, since they are no longer available after the
   // `delete this` further down.
   IsolateGroup* isolate_group = isolate->isolate_group_;
-  Dart_IsolateCleanupCallback cleanup = Isolate::CleanupCallback();
+  Dart_IsolateCleanupCallback cleanup = isolate->on_cleanup_callback();
   auto callback_data = isolate->init_callback_data_;
 
   // From this point on the isolate is no longer visited by GC (which is ok,
diff --git a/runtime/vm/isolate.h b/runtime/vm/isolate.h
index 0d3279d..9a8d26e 100644
--- a/runtime/vm/isolate.h
+++ b/runtime/vm/isolate.h
@@ -786,6 +786,8 @@
     return thread == nullptr ? nullptr : thread->isolate();
   }
 
+  bool IsScheduled() { return scheduled_mutator_thread_ != nullptr; }
+
   // Register a newly introduced class.
   void RegisterClass(const Class& cls);
 #if defined(DEBUG)
@@ -857,6 +859,19 @@
     message_notify_callback_ = value;
   }
 
+  void set_on_shutdown_callback(Dart_IsolateShutdownCallback value) {
+    on_shutdown_callback_ = value;
+  }
+  Dart_IsolateShutdownCallback on_shutdown_callback() {
+    return on_shutdown_callback_;
+  }
+  void set_on_cleanup_callback(Dart_IsolateCleanupCallback value) {
+    on_cleanup_callback_ = value;
+  }
+  Dart_IsolateCleanupCallback on_cleanup_callback() {
+    return on_cleanup_callback_;
+  }
+
   void bequeath(std::unique_ptr<Bequest> bequest) {
     bequest_ = std::move(bequest);
   }
@@ -923,7 +938,16 @@
   void ScheduleInterrupts(uword interrupt_bits);
 
   const char* MakeRunnable();
-  void Run();
+  void MakeRunnableLocked();
+
+  // Runs the isolate if it was created inside the VM as a response to
+  // invocation of Dart's `Isolate.spawn` api.
+  //
+  // It will wake up a potential await'er (e.g. `await Isolate.spawn()`).
+  void RunViaSpawnApi();
+
+  // Runs the isolate if it was created by the embedder.
+  void RunViaEmbedder();
 
   MessageHandler* message_handler() const { return message_handler_; }
   void set_message_handler(MessageHandler* value) { message_handler_ = value; }
@@ -1564,6 +1588,8 @@
   // All other fields go here.
   int64_t start_time_micros_;
   Dart_MessageNotifyCallback message_notify_callback_ = nullptr;
+  Dart_IsolateShutdownCallback on_shutdown_callback_ = nullptr;
+  Dart_IsolateCleanupCallback on_cleanup_callback_ = nullptr;
   char* name_ = nullptr;
   Dart_Port main_port_ = 0;
   // Isolates created by Isolate.spawn have the same origin id.
diff --git a/tools/VERSION b/tools/VERSION
index da3e7b1..5a2abd7 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 12
 PATCH 0
-PRERELEASE 34
+PRERELEASE 35
 PRERELEASE_PATCH 0
\ No newline at end of file