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
, andhelpers.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.