Running arbitrary strings as Dart code
(NOTE: This only works in JIT mode. It’s a viable solution only if you’re building a Dart app that runs with Dart VM and which isn’t a AOT compiled. You can’t use this approach with Flutter. Or you can, but it stops working in release mode.)
In Javascript, it’s possible to evaluate a string as code:
const code = 'console.log("hey!")';
// Execute "code" as Javascript
eval(code);
Javascript has eval()
, which is a special function that takes in a string and evaluates it as Javascript code.
The above example prints a friendly “hey!” to the console.
“But Liro, why would you not just call
console.log()
directly?”
First, my name is Iiro. And to answer the question, you’re absolutely right. You would in fact do just that.
Here’s the same thing but with less code:
console.log("Hey!")
If the result of both examples is the same, why would we ever want to eval()
in the first place?
In this case, we wouldn’t.
That code snippet is just as an example - not an actual use case.
A real use case for using eval()
is quite hard to come up with.
So - when do I need to eval()
?
Most of the time, you should not use eval() at all. Chances are that you’re never going to need it in application development.
In very rare cases, you might actually need it. Most likely when building developer tools - after all, there’s no REPL without eval.
I found myself needing eval()
today when building something in Dart.
And guess what, it was in the context of building a developer tool.
final functionName = 'applyMagicSauce';
The functionName
here is hacked together during runtime.
It’s done by searching for implementers of a specific abstract class in a Dart project.
Those abstract classes are written by me, but the implementations come from third-party libraries.
There might be multiple functionName
s to call.
I needed to take that functionName
and do something like this:
final functionName = 'applyMagicSauce';
final input = 'some arbitrary string';
// Construct a call to "applyMagicSauce()"
// with "some arbitrary string" as input.
final result = eval('$functionName("$input")');
Calling applyMagicSauce(..)
and passing it a string returns a slightly different string.
I needed to find a way to call it.
Where did you hide your eval()
, Dart?
Dart does not have an eval()
function.
What a bummer.
If you’re running Dart in a web browser, you could kinda do it by calling Javascript from Dart like this:
void main() {
final result = js.context.callMethod("eval", /* ... */);
}
But that won’t work when running Dart on the VM.
I’m also pretty sure I wouldn’t get anything useful back if I tried to send some Dart to a Javascript eval()
function.
After some more investigative Googling, I stumpled upon this comment on a GitHub issue:
You can create a new library with a main function, encode the source code as a data: URI and spawn it as a new isolate, but that will not allow you to create new functions in the current isolate. You’ll have to communicate with the new code using isolate port communication.
I decided to try just that. There was a function called Isolate.spawnUri that seemed to be just what I needed.
It takes in an Uri
- which among other things, can be constructed from a string by calling Uri.dataFromString.
void main() async {
final uri = Uri.dataFromString(
'''
void main() {
print("Hellooooooo from the other side!");
}
''',
mimeType: 'application/dart',
);
await Isolate.spawnUri(uri, [], null);
}
Guess what? The damn thing worked.
ironman$ dart lib/eval.dart
Hellooooooo from the other side!
Yay for executing arbitrary strings as Dart code! What about sending something back?
void main() async {
final name = 'Eval Knievel';
final uri = Uri.dataFromString(
'''
import "dart:isolate";
void main(_, SendPort port) {
port.send("Nice to meet you, $name!");
}
''',
mimeType: 'application/dart',
);
final port = ReceivePort();
await Isolate.spawnUri(uri, [], port.sendPort);
final String? response = await (port.first as FutureOr<String?>);
print(response);
}
(Isolates in Dart are quite… isolated. They don’t share memory. In order to pass data around, we need to use ports - which might seem a little cumbersome. On the other hand, we don’t run into synchronization issues, so there’s that.)
That one worked too:
ironman$ dart lib/eval.dart
Nice to meet you, Eval Knievel!
Remember applyMagicSauce()
from earlier?
Let’s imagine I have the full package:
URI for the containing file in a variable called packageUri
.
Because I actually do.
I also know that the name of the function I want to call is applyMagicSauce
.
It takes in one argument which is a String.
It’s time to stitch some strings together.
void main() async {
// I filled out the content of these variables here
// for clarity. In a real scenario, you'd probably
// parse these from somewhere.
final packageUri = 'package:magic/magic_sauce.dart';
final functionName = 'applyMagicSauce';
final input = 'Hellooooooo from the other side!';
final uri = Uri.dataFromString(
'''
import "dart:isolate";
import '$packageUri';
void main(_, SendPort port) {
port.send($functionName("$input"));
}
''',
mimeType: 'application/dart',
);
final port = ReceivePort();
final isolate =
await Isolate.spawnUri(uri, [], port.sendPort);
final String? response = await (port.first as FutureOr<String?>);
port.close();
isolate.kill();
print(response);
}
After running this, the result of applyMagicSauce
will be in the response
variable.
If the magic sauce function was shuffling a string around, we would get something like this printed on the console:
ironman$ dart lib/eval.dart
lor meetei ooo sdel fthooh!oHoor
Final words
In Javascript, the first rule of using eval
is to not use it.
It applies for Dart too.
It’s usually hacky and there’s going to be a better way. If you’re just building Flutter apps, you’re probably never going to need it. In fact, if you’re building a Flutter app, this approach will not work for you.
The second rule is that eval can be evil. If you do run it with Dart VM, you need to seriously think about making sure that you’re not accidentally running something malicious on your end user’s computer.
3 comments
likes