Reflection in Dart with Mirrors: An Introduction
Written by Gilad Bracha
November 2012 (Updated May 2013)
Reflection in Dart is based on the concept of mirrors,
which are simply objects that reflect other objects.
In a mirror-based API,
whenever one wants to reflect on an entity,
one must obtain a separate object called a mirror.
Mirror-based reflective APIs have substantial advantages
with respect to security, distribution, and deployment.
On the other hand,
using them is sometimes more verbose than older approaches.
For a thorough introduction to the rationale for mirror-based reflection,
see the references at the end of this document.
However, you don’t need to delve into all that if you don’t want to;
all you really need to know about Dart’s mirror API will be covered here.
At this time, only part of the planned API has been realized.
The part that exists deals with introspection,
the ability of a program to discover and use its own structure.
The introspection API has been largely implemented on the Dart VM.
The dart2js version is in progress and will be ready soon.
There is also a closely related source mirror API
that is designed for compile-time reflection,
and is being used to support DartDoc.
Given this state of affairs,
we will concentrate on introspection.
The introspection API is declared in a library named mirrors
and is for the most part synchronous.
If you wish to experiment with reflection,
you can import dart:mirrors.
import 'dart:mirrors';
For the sake of illustration,
we’ll assume you’ve defined the following class:
class MyClass {
int i, j;
int sum() => i + j;
MyClass(this.i, this.j);
static noise() => 42;
static var s;
}
The easiest way to get a mirror is to call the top-level function
reflect().
The reflect() method takes an object and returns an
InstanceMirror
on it.
InstanceMirror myClassInstanceMirror = reflect(new MyClass(3, 4));
InstanceMirror is a subclass of
Mirror,
the root of the mirror hierarchy.
An InstanceMirror allows one to invoke dynamically chosen code on an object.
InstanceMirror res = myClassInstanceMirror.invoke(const Symbol('sum'), []);
// Returns an InstanceMirror on 7.
The invoke() method takes the method name,
a list of positional arguments,
and (optionally) a map describing named arguments.
The method name must be encoded as a Symbol object.
Symbol has a constructor that takes a string. Calling the constructor with
const rather than new allows dart2js to minify all identifiers in the program,
keeping the
generated JavaScript small. A fuller explanation of why we use Symbol appears
toward the end of this article.
Suppose you want to print out all the members of a class.
You’ll need a
ClassMirror,
which as you’d expect reflects a class.
One way to get a class mirror is from an instance mirror.
ClassMirror MyClassMirror = myClassInstanceMirror.type; // Reflects MyClass
Now we can print out the names of all members of the class
reflected by MyClassMirror.
for (var m in MyClassMirror.members.values) print(MirrorSystem.getName(m.simpleName));
ClassMirror has a getter
members
that returns a map from the names of the reflected class’ members
to mirrors on those members.
We extract the values from the map;
each of these will be a mirror on one of
the members of MyClass. Each such mirror has a getter simpleName
that returns the name of the corresponding member.
Since names are represented by symbols, we use the utility function
MirrorSystem.getName() which converts symbols back into strings.
Obviously, we know what the members of MyClass are in this case;
the point is that the for loop above works for any class mirror,
and therefore we can use it to print the members of any class.
printAllMembersOf(ClassMirror cm) {
for (var m in cm.members.values) print(MirrorSystem.getName(m.simpleName));
}
A number of methods in the mirror API
return maps in a similar fashion.
The maps allow you to look up members by name,
to iterate over all the names, or to iterate over all the members.
In fact, there is a simpler way to accomplish what we just did.
printAllMembersOf(ClassMirror cm) {
for (var k in cm.members.keys) print(MirrorSystem.getName(k));
}
What if we want to invoke static code reflectively?
We can call invoke() on a ClassMirror as well.
InstanceMirror v = MyClassMirror.invoke(const Symbol('noise'), []);
// Returns an InstanceMirror on 42.
In fact, invoke() is defined in class
ObjectMirror,
a common superclass for mirror classes
that reflect Dart entities that have state and executable code
such as regular instances, classes, libraries, and so on.
Here is a complete example incorporating what we’ve done so far:
import 'dart:mirrors';
class MyClass {
int i, j;
void my_method() { }
int sum() => i + j;
MyClass(this.i, this.j);
static noise() => 42;
static var s;
}
main() {
InstanceMirror myClassInstanceMirror = reflect(new MyClass(3, 4));
ClassMirror MyClassMirror = myClassInstanceMirror.type; // Reflects MyClass
InstanceMirror res = myClassInstanceMirror.invoke(const Symbol('sum'), []);
// Returns an InstanceMirror on 7.
print('sum = ${res.reflectee}');
InstanceMirror v = MyClassMirror.invoke(const Symbol('noise'), []);
// Returns an InstanceMirror on 42.
print('noise = ${v.reflectee}');
print('\nmethods:');
Map<Symbol, MethodMirror> map = MyClassMirror.methods;
map.values.forEach((MethodMirror mm) {print(MirrorSystem.getName(mm.simpleName));});
print('\nmembers:');
for (var k in MyClassMirror.members.keys) print(MirrorSystem.getName(k));
MyClassMirror.setField(const Symbol('s'), 91);
print(MyClass.s);
}
And here’s the output:
sum = 7
noise = 42
methods:
my_method
noise
sum
members:
noise
my_method
i
j
s
sum
91
At this point we’ve shown you enough to get started.
Some more things you should be aware of follow.
Because the size of web applications needs to be kept down,
deployed Dart applications may be subject to tree shaking and minification.
Tree shaking refers to the elimination of source code that isn’t called;
minification refers to the compaction of the code,
which may include replacing symbols used in the original source.
Such optimizations are a fact of life in Dart,
because of the need to deploy to JavaScript.
We need to avoid downloading the entire Dart platform
with every web page written in Dart.
Tree shaking does this by detecting
what method names are actually invoked in the source code.
However, code that is invoked based on dynamically computed symbols
cannot be detected this way, and is therefore subject to elimination.
The above means that the actual code that exists at runtime
may differ from the code you had during development.
Code you only used reflectively may not be deployed.
Runtime reflection is only aware of what actually exists at
runtime in the running program.
This can lead to surprises.
For example, one may attempt to reflectively invoke a method
that exists in the source code,
but has been optimized away because no non-reflective invocations exist.
Such an invocation will result in a call to
noSuchMethod().
Tree shaking has implications for structural introspection as well.
Again, what members a library or type has at runtime
may be at variance with what the source code states.
In the presence of mirrors, one could choose to be more conservative.
Unfortunately, since one can obtain mirrors for any object in an application,
all code in the application would have to be preserved,
including the Dart platform itself.
Instead, we may choose to treat such invocations
as if the method never existed in the source.
We intend provide a reliable, portable, and straightforward mechanism
for programmers to specify that certain code
may not be eliminated by tree shaking.
The planned approach is to use metadata.
Minification also poses a challenge, which Dart’s mirror system addresses
by using instances of Symbol to describe names used in the program. If we
simply used strings to refer to, say, the members of a class, the minified
code would no longer use those same names, and reflection would fail to find
the members when used in minified code. However, class Symbol encodes strings
using the same minification scheme used by dart2js for program identifiers,
so reflection works under minification. To help dart2js optimize away the
strings, we recommend using constant Symbols as much as possible, as shown
in the code above.
The above should be enough to get you started using mirrors.
There is a good deal more to the introspection API;
you can
explore the API
to see what else is there.
We’d like to support more powerful reflective features in the future.
These would include mirror builders,
designed to allow programs to extend and modify themselves,
and a mirror-based debugging API as well.
References
Gilad Bracha and David Ungar.
Mirrors: Design Principles for Meta-level Facilities of Object-Oriented
Programming Languages.
In Proc. of the ACM Conf. on Object-Oriented Programming,
Systems, Languages and Applications, October 2004.
Gilad Bracha.
Linguistic Reflection via Mirrors.
Screencast of a lecture at HPI Potsdam in January 2010. 57 minutes.
These blog posts on mirrors may also prove useful
(and less time consuming to digest):