Photo by Afif Ramdhasuma on Unsplash

Embedding Dart into a native C++ application enables you to run Dart code alongside your native logic, combining the power of Dart’s language features and package ecosystem with the performance and system access of C++. This post guides you step-by-step from setting up the project to running Dart code, calling native functions, marshaling data, and managing isolates with message passing.

This guide also assumes that you compiled dart-sdk by yourself as described in previous post

Creating a CMake Project

To start, create a CMake project to build your native embedder application:

cmake_minimum_required(VERSION 3.21)
project(sample VERSION 0.1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED On)
set(CMAKE_CXX_EXTENSIONS Off)

set(DART_DLL_DIR "${PROJECT_SOURCE_DIR}/libs/dart/bin")

include_directories(libs/dart/include)

find_library(DART_DLL dart_engine_jit_shared PATHS ${PROJECT_SOURCE_DIR}/libs/dart/bin) 

add_executable(sample main.cpp)

target_link_libraries(sample ${DART_DLL})

Make sure to:

  • Copy libdart.dylib (or your platform’s equivalent) into <project root>/libs/dart/bin.

  • Copy header files dart_api.h, dart_engine.h, and helpers.h from the Dart SDK into your project include folder. You can find those files in

  • dart_api.h : dart-sdk/sdk/runtime/include/dart_api.h ,

  • dart_engine.h : dart-sdk/sdk/runtime/engine/include/dart_engine.h and

  • helpers.h : dart-sdk/sdk/samples/embedder/helpers.h

Minimal C++ Embedder Example

Here is a core example demonstrating loading a Dart kernel snapshot and running a Dart main function:

#include "dart_api.h"
#include "dart_engine.h"
#include "helpers.h"
#include <iostream>
#include <string>


Dart_Handle ToDartStringList(const std::vector<std::string> &values) {
  Dart_Handle core_library =
      CheckError(Dart_LookupLibrary(Dart_NewStringFromCString("dart:core")));
  Dart_Handle string_type = CheckError(Dart_GetNonNullableType(
      core_library, Dart_NewStringFromCString("String"), 0, nullptr));
  Dart_Handle filler = Dart_NewStringFromCString("");

  Dart_Handle result =
      CheckError(Dart_NewListOfTypeFilled(string_type, filler, values.size()));
  for (size_t i = 0; i < values.size(); i++) {
    Dart_Handle element = Dart_NewStringFromCString(values[i].c_str());
    CheckError(Dart_ListSetAt(result, i, element));
  }

  return result;
}

DartEngine_SnapshotData loadSnapshot(std::string path) {
  char *error = nullptr;
  DartEngine_SnapshotData snapshot_data = AutoSnapshotFromFile(path, &error);
  CheckError(error, "Reading Snapshot");
  return snapshot_data;
}

void startAcquireAndEnterScope(DartEngine_SnapshotData snapshot_data) {
  char *error = nullptr;
  CheckError(error, "Starting Isolate");

  Dart_Isolate isolate = DartEngine_CreateIsolate(snapshot_data, &error);
  DartEngine_AcquireIsolate(isolate);
  Dart_EnterScope();
  Dart_Handle library = Dart_RootLibrary();
  // we will explain it later
  //Dart_SetNativeResolver(library, ResolveNativeFunction, nullptr);
}

void runDartProgram(std::string functionName,
                    std::initializer_list<Dart_Handle> args,
                    std::string context) {
  CheckError(Dart_Invoke(Dart_RootLibrary(),
                         Dart_NewStringFromCString(functionName.c_str()), 1,
                         const_cast<Dart_Handle *>(args.begin())),
             context);
}

void exitAndShutdown() {
  Dart_ExitScope();
  DartEngine_ReleaseIsolate();
  DartEngine_Shutdown();
}

bool isRunning = true;

int main(int argc, char **argv) {
  if (argc == 1) {
    std::cerr << "Must specify snapshot path" << std::endl;
    std::exit(1);
  }

  DartEngine_SnapshotData snapshot_data;
  snapshot_data = loadSnapshot(argv[1]);
  startAcquireAndEnterScope(snapshot_data);

  std::initializer_list<Dart_Handle> main_args{ToDartStringList({"world"})};

  runDartProgram("main", main_args, "running main");
  exitAndShutdown();

  return 0;
}

Creating a Simple Dart CLI Program

Use the Dart CLI tool to create a new Dart command-line project:

dart create sample_cli

Edit bin/sample_cli.dart to include your Dart logic.

Creating a Simple Dart CLI Program

Dart packages declared in pubspec.yaml work as usual after compiling to kernel snapshots.

Example: Using the slugid Package

Add this dependency in sample_cli/pubspec.yaml:

dependencies:
  slugid: ^2.0.0

Run:

dart pub get

Modify your Dart code:

import 'package:slugid/slugid.dart';

void main(List<String> arguments) {
  final id = slugid();
  print('Generated slugid: $id');
}

Compile to kernel:

dart compile kernel bin/sample_cli.dart -o sample_cli.dill

Run from C++:

./sample ./sample_cli.dill

Creating a Simple Dart CLI Program

Modify the startAcquireAndEnterScope function to register native resolver:

void startAcquireAndEnterScope(DartEngine_SnapshotData snapshot_data) {
  char *error = nullptr;
  CheckError(error, "Starting Isolate");

  Dart_Isolate isolate = DartEngine_CreateIsolate(snapshot_data, &error);
  DartEngine_AcquireIsolate(isolate);
  Dart_EnterScope();
  Dart_Handle library = Dart_RootLibrary();
  Dart_SetNativeResolver(library, ResolveNativeFunction, nullptr); 
}

Add these native functions and resolver:

void SimplePrint(Dart_NativeArguments arguments) {
  Dart_Handle string = CheckError(Dart_GetNativeArgument(arguments, 0));
  if (Dart_IsString(string)) {
    const char *cstring;
    Dart_StringToCString(string, &cstring);
    std::cout << "Hello from C++. Dart says:\n";
    std::cout << cstring;
  }
}

Dart_NativeFunction ResolveNativeFunction(Dart_Handle name, int /* argc */,
                                          bool * /* auto_setup_scope */) {
  if (!Dart_IsString(name)) {
    return nullptr;
  }

  Dart_NativeFunction result = nullptr;

  const char *cname;
  CheckError(Dart_StringToCString(name, &cname));

  if (strcmp("SimplePrint", cname) == 0) {
    result = SimplePrint;
  }

  return result;
}

Declare in Dart:

@pragma('vm:external-name', 'SimplePrint')
external void SimplePrint(String message);

void main(List<String> arguments) {
  SimplePrint('This message comes from Dart and is printed by C++!');
}

Marshaling Arguments and Bi-Directional Communication

Add native functions:

void GetAnswer(Dart_NativeArguments arguments) {
  Dart_SetReturnValue(arguments, Dart_NewInteger(42));
}

void PrintList(Dart_NativeArguments arguments) {
  Dart_Handle list = Dart_GetNativeArgument(arguments, 0);
  if (Dart_IsList(list)) {
    intptr_t length;
    Dart_ListLength(list, &length);
    for (intptr_t i = 0; i < length; i++) {
      Dart_Handle element = Dart_ListGetAt(list, i);
      if (Dart_IsString(element)) {
        const char* cstr;
        Dart_StringToCString(element, &cstr);
        std::cout << "List element: " << cstr << std::endl;
      }
    }
  }
}

void GetNumbers(Dart_NativeArguments arguments) {
  Dart_Handle core_lib = Dart_LookupLibrary(Dart_NewStringFromCString("dart:core"));
  Dart_Handle int_type = Dart_GetNonNullableType(core_lib, Dart_NewStringFromCString("int"), 0, nullptr);
  Dart_Handle filler = Dart_NewInteger(0);

  Dart_Handle list = Dart_NewListOfTypeFilled(int_type, filler, 3);
  Dart_ListSetAt(list, 0, Dart_NewInteger(10));
  Dart_ListSetAt(list, 1, Dart_NewInteger(20));
  Dart_ListSetAt(list, 2, Dart_NewInteger(30));

  Dart_SetReturnValue(arguments, list);
}

Update resolver:

if (strcmp("GetAnswer", cname) == 0) return GetAnswer;
if (strcmp("PrintList", cname) == 0) return PrintList;
if (strcmp("GetNumbers", cname) == 0) return GetNumbers;
@pragma('vm:external-name', 'GetAnswer')
external int GetAnswer();

@pragma('vm:external-name', 'PrintList')
external void PrintList(List<String> items);

@pragma('vm:external-name', 'GetNumbers')
external List<int> GetNumbers();

Use in Dart main:

void main(List<String> args) {
  SimplePrint('Calling native print with a message');
  int answer = GetAnswer();
  print('Received answer from native: $answer');

  PrintList(args);

  List<int> numbers = GetNumbers();
  print('Received numbers from native: $numbers');
}

Full Example: Bi-Directional Dart <-> C++ Embedder

This example demonstrates:

  • Calling Dart from C++

  • Calling native C++ from Dart

  • Marshaling strings, lists, and integers in both directions

Full dart code (bin/sample_cli.dart)

@pragma('vm:external-name', 'SimplePrint')
external void SimplePrint(String message);

@pragma('vm:external-name', 'GetAnswer')
external int GetAnswer();

@pragma('vm:external-name', 'PrintList')
external void PrintList(List<String> items);

@pragma('vm:external-name', 'GetNumbers')
external List<int> GetNumbers();

void main(List<String> args) {
  print('Hello from Dart! Args: $args');

  SimplePrint('This message comes from Dart and prints in C++!');

  int answer = GetAnswer();
  print('Received answer from native: $answer');

  PrintList(args);

  List<int> numbers = GetNumbers();
  print('Received numbers from native: $numbers');
}

C++ code (main.cpp)

#include "dart_api.h"
#include "dart_engine.h"
#include "helpers.h"

#include <iostream>
#include <string>
#include <vector>

// Native function: print Dart string from C++
void SimplePrint(Dart_NativeArguments arguments) {
  Dart_Handle arg0 = Dart_GetNativeArgument(arguments, 0);
  if (Dart_IsString(arg0)) {
    const char *cstr;
    Dart_StringToCString(arg0, &cstr);
    std::cout << "[C++] Dart says: " << cstr << std::endl;
  }
}

// Native function: return integer to Dart
void GetAnswer(Dart_NativeArguments arguments) {
  Dart_SetReturnValue(arguments, Dart_NewInteger(42));
}

// Native function: receive Dart list and print each string
void PrintList(Dart_NativeArguments arguments) {
  Dart_Handle list = Dart_GetNativeArgument(arguments, 0);
  if (Dart_IsList(list)) {
    intptr_t length;
    Dart_ListLength(list, &length);
    for (intptr_t i = 0; i < length; i++) {
      Dart_Handle element = Dart_ListGetAt(list, i);
      if (Dart_IsString(element)) {
        const char *cstr;
        Dart_StringToCString(element, &cstr);
        std::cout << "[C++] List element: " << cstr << std::endl;
      }
    }
  }
}

// Native function: return Dart list of integers
void GetNumbers(Dart_NativeArguments arguments) {
  Dart_Handle core_lib =
      Dart_LookupLibrary(Dart_NewStringFromCString("dart:core"));
  Dart_Handle int_type = Dart_GetNonNullableType(
      core_lib, Dart_NewStringFromCString("int"), 0, nullptr);
  Dart_Handle filler = Dart_NewInteger(0);

  Dart_Handle list = Dart_NewListOfTypeFilled(int_type, filler, 3);
  Dart_ListSetAt(list, 0, Dart_NewInteger(10));
  Dart_ListSetAt(list, 1, Dart_NewInteger(20));
  Dart_ListSetAt(list, 2, Dart_NewInteger(30));

  Dart_SetReturnValue(arguments, list);
}

// Native resolver
Dart_NativeFunction ResolveNativeFunction(Dart_Handle name, int /*argc*/,
                                          bool * /*auto_setup_scope*/) {
  if (!Dart_IsString(name))
    return nullptr;

  const char *cname;
  CheckError(Dart_StringToCString(name, &cname));

  if (strcmp("SimplePrint", cname) == 0)
    return SimplePrint;
  if (strcmp("GetAnswer", cname) == 0)
    return GetAnswer;
  if (strcmp("PrintList", cname) == 0)
    return PrintList;
  if (strcmp("GetNumbers", cname) == 0)
    return GetNumbers;

  return nullptr;
}

// Convert std::vector<std::string> to Dart List<String>
Dart_Handle ToDartStringList(const std::vector<std::string> &values) {
  Dart_Handle core_library =
      CheckError(Dart_LookupLibrary(Dart_NewStringFromCString("dart:core")));
  Dart_Handle string_type = CheckError(Dart_GetNonNullableType(
      core_library, Dart_NewStringFromCString("String"), 0, nullptr));
  Dart_Handle filler = Dart_NewStringFromCString("");

  Dart_Handle result =
      CheckError(Dart_NewListOfTypeFilled(string_type, filler, values.size()));
  for (size_t i = 0; i < values.size(); i++) {
    Dart_Handle element = Dart_NewStringFromCString(values[i].c_str());
    CheckError(Dart_ListSetAt(result, i, element));
  }
  return result;
}

void startAcquireAndEnterScope(DartEngine_SnapshotData snapshot_data) {
  char *error = nullptr;
  CheckError(error);

  Dart_Isolate isolate = DartEngine_CreateIsolate(snapshot_data, &error);
  DartEngine_AcquireIsolate(isolate);
  Dart_EnterScope();

  Dart_Handle library = Dart_RootLibrary();
  Dart_SetNativeResolver(library, ResolveNativeFunction, nullptr);
}

void runDartProgram(std::string functionName,
                    std::initializer_list<Dart_Handle> args,
                    std::string context) {
  CheckError(Dart_Invoke(Dart_RootLibrary(),
                         Dart_NewStringFromCString(functionName.c_str()),
                         static_cast<int>(args.size()),
                         const_cast<Dart_Handle *>(args.begin())),
             context);
}

void exitAndShutdown() {
  Dart_ExitScope();
  DartEngine_ReleaseIsolate();
  DartEngine_Shutdown();
}

DartEngine_SnapshotData loadSnapshot(std::string path) {
  char *error = nullptr;
  DartEngine_SnapshotData snapshot_data = AutoSnapshotFromFile(path, &error);
  CheckError(error, "Reading Snapshot");
  return snapshot_data;
}

int main(int argc, char **argv) {
  if (argc == 1) {
    std::cerr << "Must specify snapshot path" << std::endl;
    return 1;
  }

  DartEngine_SnapshotData snapshot_data = loadSnapshot(argv[1]);
  startAcquireAndEnterScope(snapshot_data);

  std::initializer_list<Dart_Handle> main_args{
      ToDartStringList({"world", "from", "C++"})};

  runDartProgram("main", main_args, "running main");
  exitAndShutdown();

  return 0;
}

Compile dart kernel snapshot

dart compile kernel bin/sample_cli.dart -o sample_cli.dill

Build and run C++ project

./sample ./sample_cli.dill

Expected Output

Hello from Dart! Args: [world, from, C++]
[C++] Dart says: This message comes from Dart and prints in C++!
Received answer from native: 42
[C++] List element: world
[C++] List element: from
[C++] List element: C++
Received numbers from native: [10, 20, 30]

This example demonstrates:

  • Passing arguments from C++ into Dart (args)
  • Calling native C++ functions from Dart using @pragma and native resolver
  • Returning values and List structures from C++ to Dart

Conclusion

By embedding Dart into your native C++ application, you gain the flexibility of Dart’s high-level language features and package ecosystem while maintaining full control at the system level through C++. This post walked you through:

  • Creating a minimal Dart embedder in C++
  • Running Dart kernel snapshots
  • Calling native C++ from Dart using @pragma and native resolvers
  • Marshaling strings, integers, and lists between Dart and C++

This setup allows you to integrate scripting, plugins, or hot-reloadable logic directly into your native application.