d4rt 0.2.6
d4rt: ^0.2.6 copied to clipboard
An interpreter and runtime for the Dart language. It allows you to execute Dart code dynamically in your Dart/Flutter applications
d4rt #
d4rt (pronounced "dart") is an interpreter and runtime for the Dart language, written in Dart.
It allows you to execute Dart code dynamically, bridge native classes, and build advanced scripting or plugin systems in your Dart/Flutter applications.
Features #
- Dart interpreter: Run Dart code dynamically at runtime.
- Precompiled scripts & AST caching: Pre-parse and validate scripts with
compile()and execute repeatedly withexecuteCompiled()for maximum performance. - Execution limits & timeouts: Protect against infinite loops and runaway CPU usage with
timeoutandmaxStepssandbox limits. - Interactive CLI & REPL: Run scripts directly or launch an interactive shell with
dart run d4rt. - Full generics support: Complete support for generic classes, functions, and type constraints with runtime validation.
- Type bounds checking: Enforce generic type constraints (e.g.,
T extends num) with dynamic resolution. - Bridging system: Expose your own Dart/Flutter classes, enums, and methods to interpreted code.
- Async/await support: Handle asynchronous code and Futures.
- Class, enum, and extension support: Use most Dart language features, including classes, inheritance, mixins, enums, and extensions.
- Pattern matching: Support for Dart's pattern matching in switch/case and assignments.
- Runtime type validation: Validate generic type arguments and method parameters at runtime.
- Security sandboxing: Permission-based security system to restrict dangerous operations and prevent malicious code execution.
- Custom logging: Integrated, configurable logger for debugging interpreted code.
- Function argument passing: Pass positional and named arguments directly to functions via
execute(). - Code introspection: Analyze Dart code structure with the
analyze()method to extract metadata. - Dynamic code evaluation: Execute code dynamically with
eval()while preserving execution state. - Extensible: Add your own bridges for custom types and native APIs.
Installation #
Add to your pubspec.yaml:
dependencies:
d4rt: # latest version
Then run:
dart pub get
Usage Example #
import 'package:d4rt/d4rt.dart';
void main() {
final code = '''
int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
main() {
return fib(6);
}
''';
final interpreter = D4rt();
final result = interpreter.execute(source: code);
print('Result: $result'); // Result: 8
}
Advanced Features #
Function Argument Passing #
Pass positional and named arguments directly to functions:
final interpreter = D4rt();
final result = interpreter.execute(
source: '''
String greet(String name, {String greeting = "Hello"}) {
return "\$greeting, \$name!";
}
''',
name: 'greet',
positionalArgs: ['Alice'],
namedArgs: {'greeting': 'Hi'},
);
print(result); // Hi, Alice!
Interpreted function invocation #
Invoke interpreted functions from host dart code:
final interpreter = D4rt();
final interpretedFn = interpreter.execute(
source: '''
int sum(int a, int b) => a+b;
main() => sum;
'''
);
final result = d4rt.invokeInterpretedFunction(interpretedFn, [1,2]);
print(result); // 3
Promote interpreted functions to host functions to better integration:
final interpreter = D4rt();
final interpretedFn = interpreter.execute(
source: '''
int sum(int a, int b) => a+b;
main() => sum;
'''
);
T Function(A, B) bindFunction2<T, A, B>(D4rt d, InterpretedFunction fn) {
return (A a, B b) => d.invokeInterpretedFunction(fn, [a, b]);
}
final nativeFn = bindFunction2(interpreter,interpretedFn);
print( nativeFn(1,2) ); // 3
Note that it is possible to pass InterpretedFunction as argumento to invokeInterpretedFunction, but pass a native function it is not currently supported.
Code Introspection #
Analyze Dart code structure without execution:
final interpreter = D4rt();
final analysis = interpreter.analyze('''
class User {
String name;
int age;
User(this.name, this.age);
}
int getAge(User user) => user.age;
''');
// Access metadata about functions, classes, variables, etc.
print(analysis.functions['getAge']?.parameters);
print(analysis.classes['User']?.constructors);
Dynamic Code Evaluation #
Execute code dynamically while preserving execution state:
final interpreter = D4rt();
interpreter.grant(DangerousPermission.codeEvaluation);
interpreter.execute(source: 'int x = 10;');
final result = interpreter.eval('x + 5'); // Returns 15
Precompiled Scripts & AST Caching #
For high-performance scenarios where the same script is executed multiple times, precompile the AST once to avoid re-parsing overhead:
final d4rt = D4rt();
// Compile once
final script = d4rt.compile(source: '''
int multiply(int a, int b) => a * b;
''');
// Execute repeatedly with zero parsing overhead
final r1 = d4rt.executeCompiled(script, name: 'multiply', positionalArgs: [6, 7]); // 42
final r2 = d4rt.executeCompiled(script, name: 'multiply', positionalArgs: [10, 5]); // 50
You can also enable automatic AST caching for repeated source strings:
final d4rt = D4rt(enableAstCache: true);
// Subsequent execute() calls with identical source strings will reuse cached ASTs
Execution Limits & Timeout Protection #
Protect against infinite loops and runaway CPU usage by setting maximum step quotas or execution timeouts:
final d4rt = D4rt();
try {
d4rt.execute(
source: '''
void main() {
while (true) { /* infinite loop */ }
}
''',
maxSteps: 1000, // Maximum execution steps
timeout: Duration(milliseconds: 500), // Maximum execution duration
);
} on ExecutionLimitException catch (e) {
print('Step limit exceeded: $e');
} on ExecutionTimeoutException catch (e) {
print('Timed out: $e');
}
Command-Line Interface (CLI) & Interactive REPL #
d4rt includes a built-in CLI runner and an interactive REPL shell:
# Run a Dart script file directly
dart run d4rt path/to/script.dart [args...]
# Start an interactive REPL session
dart run d4rt repl
# or simply
dart run d4rt
Inside the REPL:
==============================================
d4rt v0.2.5 - Interactive Dart REPL
Type :help for help, :exit or Ctrl+C to quit
==============================================
d4rt> var x = 10;
d4rt> int double(int n) => n * 2;
d4rt> double(x)
=> 20
d4rt> :exit
Goodbye!
Security Sandboxing #
d4rt includes a comprehensive permission-based security system to prevent malicious code execution. By default, access to dangerous modules like dart:io and dart:isolate is blocked unless explicitly granted.
Granting Permissions #
import 'package:d4rt/d4rt.dart';
void main() {
final interpreter = D4rt();
// Grant filesystem access
interpreter.grant(FilesystemPermission.any);
// Grant network access
interpreter.grant(NetworkPermission.any);
// Grant process execution. Native processes are outside the D4rt sandbox,
// so the dangerous process-execution permission is also required.
interpreter.grant(ProcessRunPermission.any);
interpreter.grant(DangerousPermission.processExecution);
// Grant isolate operations
interpreter.grant(IsolatePermission.any);
// Now execute code that uses dangerous operations
final result = interpreter.execute(source: '''
import 'dart:io';
import 'dart:isolate';
void main() {
// This code can now access filesystem, network, etc.
print('Secure execution with granted permissions');
}
''');
}
Permission Types #
-
FilesystemPermission: Controls file and directory operationsFilesystemPermission.any- Allow all filesystem operationsFilesystemPermission.read- Allow read-only operationsFilesystemPermission.write- Allow write operationsFilesystemPermission.readPath('/specific/path')- Allow reads inside a pathFilesystemPermission.writePath('/specific/path')- Allow writes inside a pathFilesystemPermission.path('/specific/path')- Allow reads, writes and execution inside a path
-
NetworkPermission: Controls network operationsNetworkPermission.any- Allow all network operationsNetworkPermission.connect- Allow outbound connectionsNetworkPermission.listen- Allow listening on any portNetworkPermission.bind- Allow binding server socketsNetworkPermission.connectTo('api.example.com')- Allow connections to a specific hostNetworkPermission.connectToPort('api.example.com', 443)- Allow a host/port pairNetworkPermission.listenOn(8080)- Allow listening on a specific port
-
ProcessRunPermission: Controls process executionProcessRunPermission.any- Allow execution of any commandProcessRunPermission.command('git')- Allow a specific commandProcessRunPermission.commandWithArgs('git', ['status'])- Restrict arguments too- Process execution also requires
DangerousPermission.processExecutionbecause the child process is not confined by D4rt permissions.
-
IsolatePermission: Controls isolate creation and communicationIsolatePermission.spawn- Allow creating and controlling isolatesIsolatePermission.communicate- Allow sending and receiving messagesIsolatePermission.any- Allow all isolate operations
Isolate.spawnUrialso requiresDangerousPermission.codeEvaluation, because it runs native Dart code outside the interpreter sandbox. -
DangerousPermission: Controls operations that can escape the sandboxDangerousPermission.codeEvaluation- Required byD4rt.eval()andIsolate.spawnUriDangerousPermission.processExecution- Required to launch native processes or watch host process signalsDangerousPermission.developerTools- Required to importdart:developerDangerousPermission.any- Allow every dangerous operation
All scoped permissions fail closed: a host-only rule does not authorize a
connection without a host, and a port-specific rule requires the actual port.
Filesystem scopes are canonicalized, so symlinks cannot be used to escape a
readPath/writePath boundary, including through recursive directory listings,
filesystem watch events, temporary-directory prefixes and link resolution.
Creating or updating a symlink also requires write access to its target.
Scoped network permissions disable transparent HTTP redirects; follow redirects
manually so every destination is checked. HTTP proxy endpoints returned by
findProxy are also checked against the network permission scope. Watching
host process signals requires both ProcessRunPermission.any and
DangerousPermission.processExecution; a command-scoped process permission is
not sufficient. runInShell: true also requires ProcessRunPermission.any,
because shell parsing can execute commands outside a command/argument scope.
Running a child process is equivalent to host code execution: command and
argument scoping does not sandbox the child's filesystem or network access.
On POSIX systems (macOS, Linux, Android and iOS), file content reads and writes,
open streams, file copies, create/delete/rename operations and recursive
directory deletes use descriptor-based filesystem operations. Parent components
are opened with openat and O_NOFOLLOW, so a symlink substituted between the
permission check and the native operation is rejected instead of followed.
Temporary-directory creation and symbolic-link target reads are also anchored on
verified parent descriptors with mkdirat and readlinkat, with validated
prefixes and targets. File/Directory/Link stat operations,
FileStat.stat and file/directory existence checks open the target with
O_NOFOLLOW and read metadata from the descriptor, so a path substituted after
the permission check cannot redirect them. The remaining path-based operations,
including link existence, directory listings, watches, FileSystemEntity.type
and symbolic-link resolution, still have a narrow TOCTOU window; resolved paths
are revalidated before being returned. Windows falls back to path-based
operations for all filesystem bridges. For workloads that require a hard
guarantee against a concurrent local attacker with write access to the allowed
tree, run D4rt in an OS-level sandbox or a dedicated user account.
Permission Management #
final interpreter = D4rt();
// Grant permissions
interpreter.grant(FilesystemPermission.any);
// Check permissions
if (interpreter.hasPermission(FilesystemPermission.any)) {
print('Filesystem access granted');
}
// Revoke permissions
interpreter.revoke(FilesystemPermission.any);
Filesystem Module Imports #
Use basePath together with allowFileSystemImports: true when interpreted code imports local files from disk.
final interpreter = D4rt();
interpreter.grant(FilesystemPermission.readPath('/path/to/project/lib'));
final result = interpreter.execute(
source: '''
import './utils.dart';
String main() => greetFromUtils();
''',
basePath: '/path/to/project/lib',
allowFileSystemImports: true,
);
Rules:
- Relative filesystem imports require
basePath. - Filesystem modules are blocked unless
allowFileSystemImportsis enabled. - Reading modules from disk also requires a matching
FilesystemPermission. - Missing local files raise a
SourceCodeExceptionwith the resolved filesystem path. analyze()never evaluates top-level variable initializers, including those of imported modules.
CLI Sandbox #
The d4rt command is deny-by-default. Scripts can read files inside their own
directory, but network, process, isolate, broad filesystem and dangerous
operations must be enabled explicitly:
d4rt script.dart
d4rt --allow-network script.dart
d4rt --allow-fs --allow-process script.dart
d4rt --allow-all script.dart
--allow-process grants both ProcessRunPermission.any and
DangerousPermission.processExecution, because the flag explicitly opts into
host process execution. -e and the REPL require code evaluation and receive
DangerousPermission.codeEvaluation automatically; every other permission is
still denied unless the corresponding flag is passed.
Bridging Native Classes & Enums #
d4rt provides a powerful bridging system that allows you to automate the process of exposing your Dart and Flutter classes, enums, and functions to the interpreter using build_runner.
Automated Bridge Generation (Recommended) #
-
Add
build_runnerandd4rtdev dependencies:dev_dependencies: build_runner: ^2.4.0 -
Annotate your classes or enums with
@D4rtBridge():import 'package:d4rt/d4rt.dart'; part 'color.g.dart'; @D4rtBridge(libraryUri: 'package:app/color.dart') class Color { final int value; const Color(this.value); static const Color black = Color(0xFF000000); int get red => (value >> 16) & 0xFF; } -
Run the code generator:
dart run build_runner buildThis will generate a
.g.dartfile containing the bridge definitions and an automatic registration function. -
Register the bridges with the interpreter:
import 'package:d4rt/d4rt.dart'; import 'color.dart'; // Import the generated file void main() { final interpreter = D4rt(); // Register all bridges in the file with a single line: registerColorBridges(interpreter); // Now 'Color' is available in the interpreter under 'package:app/color.dart' }
Manual Bridge Definition #
If you prefer to define bridges manually (without code generation), you can use the BridgedClass and BridgedEnumDefinition classes:
Bridge a Dart Class (Manual)
import 'package:d4rt/d4rt.dart';
class MyClass {
int value;
MyClass(this.value);
int doubleValue() => value * 2;
}
final myClassBridge = BridgedClass(
nativeType: MyClass,
name: 'MyClass',
constructors: {
'': (InterpreterVisitor visitor, List<Object?> positionalArgs, Map<String, Object?> namedArgs) {
if (positionalArgs.length == 1 && positionalArgs[0] is int) {
return MyClass(positionalArgs[0] as int);
}
throw ArgumentError('MyClass constructor expects one integer argument.');
},
},
methods: {
'doubleValue': (InterpreterVisitor visitor, Object target, List<Object?> positionalArgs, Map<String, Object?> namedArgs) {
if (target is MyClass) {
return target.doubleValue();
}
throw TypeError();
},
},
getters: {
'value': (InterpreterVisitor? visitor, Object target) {
if (target is MyClass) {
return target.value;
}
throw TypeError();
},
},
);
void main() {
final interpreter = D4rt();
interpreter.registerBridgedClass(myClassBridge, 'package:example/my_class_library.dart');
final code = '''
import 'package:example/my_class_library.dart';
main() {
var obj = MyClass(21);
return obj.doubleValue();
}
''';
final result = interpreter.execute(source: code);
print(result); // 42
}
Bridge a Dart Enum (Manual)
import 'package:d4rt/d4rt.dart';
enum Color { red, green, blue }
final colorEnumBridge = BridgedEnumDefinition<Color>(
name: 'Color',
values: Color.values,
);
void main() {
final interpreter = D4rt();
interpreter.registerBridgedEnum(colorEnumBridge, 'package:example/my_enum_library.dart');
final code = '''
import 'package:example/my_enum_library.dart';
main() {
var favoriteColor = Color.green;
print('My favorite color is \${favoriteColor.name}');
return favoriteColor.index;
}
''';
final result = interpreter.execute(source: code);
print(result); // 1
}
Advanced Annotation Options #
The @D4rtBridge annotation provides several configuration parameters:
libraryUri: The namespace where the class will be registered (e.g.,'package:app/color.dart').includePrivate: Whether to generate bridges for private members (default:false).bridgeName: A custom variable name for the generated bridge (useful for avoiding name collisions).
Example:
@D4rtBridge(
libraryUri: 'package:app/models.dart',
includePrivate: true,
bridgeName: 'customUserBridge',
)
class User { ... }
Supported Features #
| Feature | Status / Notes |
|---|---|
| Classes & Inheritance | ✅ Full support (abstract, inheritance, mixins, interfaces, sealed, base, final) |
| Enums | ✅ Full support (fields, methods, static, named values, index, name) |
| Mixins | ✅ Supported (declaration, application, on clause, constraints) |
| Extensions | ✅ Supported (methods, getters, setters, operators) |
| Async/await | ✅ Supported (async functions, await, Futures) |
| Pattern matching | ✅ Supported (switch/case, if-case, destructuring, list/map/record patterns) |
| Collections (List, Map, Set) | ✅ Supported (literals, spread, if/for, nested, null-aware, records) |
| Top-level functions | ✅ Supported (named, anonymous, closures, nested) |
| Getters/Setters | ✅ Supported (instance, static, extension, bridge) |
| Static members | ✅ Supported (fields, methods, getters/setters, bridge) |
| Switch/case | ✅ Supported (classic, pattern, default, fallthrough, exhaustive checks) |
| Try/catch/finally | ✅ Supported (multiple catch, on/type, rethrow, stacktrace) |
| Imports | ✅ Supported (URIs, show/hide clauses for libraries defined in sources) |
| Generics | ✅ Full support (generic classes/functions, type constraints, runtime validation) |
| Extension types | ✅ Full support (representation fields, constructors, methods) |
| Precompiled scripts & AST Cache | ✅ Full support (compile, executeCompiled, AST caching) |
| Execution limits & Timeouts | ✅ Full support (timeout, maxSteps, sandbox exceptions) |
| Operator overloading | ✅ Full support |
| FFI | 🚫 Not supported |
Isolates (dart:isolate) |
✅ Full support |
Developer (dart:developer) |
✅ Full support, requires DangerousPermission.developerTools |
| Reflection/Mirrors | 🚫 Not supported |
| Records | ✅ Supported (positional, named, pattern matching) |
| String interpolation | ✅ Supported |
Cascade notation (..) |
✅ Supported |
| Null safety | ✅ Supported (null-aware ops, checks, patterns) |
Type tests (is, as) |
✅ Supported |
| Exception handling | ✅ Supported (throw, rethrow, custom errors) |
See the documentation for details and limitations.
Limitations #
- Operator overloading is partially supported (via extensions, not via class operator methods).
- Some advanced Dart features (FFI, mirrors) are not available.
- The interpreter is not a full Dart VM: some language features may behave differently.
Documentation #
Contributing #
Contributions are welcome!
Feel free to open issues, suggest features, or submit pull requests.
License #
MIT License. See LICENSE.
About the Name #
d4rt is a play on the word "dart", using "4" as a stylized "A".
It is pronounced exactly like "dart".