Reflection

Introspection is the ability of a program to discover and use its own structure. Reflection is the ability of a program to use introspection to examine and modify the structure and behavior of the program at runtime. You can use reflection to dynamically create an instance of a type or get the type from an existing object and invoke its methods or access its fields and properties. This makes your code more dynamic and can be written against known interfaces so that the actual classes can be instantiated using reflection. Another purpose of reflection is to create development and debugging tools, and it is also used for meta-programming.

There are two different approaches to implementing reflection:

  • The first approach is that the information about reflection is tightly integrated with the language and exists as part of the program's structure. Access to program-based reflection is available by a property or method.
  • The second approach is based on the separation of reflection information and program structure. Reflection information is separated inside a distinct Mirror object that binds to the real program member.

Dart reflection follows the second approach with Mirrors. You can find more information about the concept of Mirrors in the original paper written by Gilad Bracha at of Mirrors:

  • Mirrors are separate from the main code and cannot be exploited for malicious purposes
  • As reflection is not part of the code, the resulting code is smaller
  • There are no method-naming conflicts between the reflection API and inspected classes
  • It is possible to implement many different Mirrors with different levels of reflection privileges
  • It is possible to use Mirrors in command-line and web applications

Let's try Mirrors and see what we can do with them. We will continue to create a library to run our tests.

Introspection in action

We will demonstrate the use of Mirrors with something simple such as introspection. We will need a universal code that can retrieve the information about any object or class in our program to discover its structure and possibly manipulate it with properties and call methods. For this, we've prepared the TypeInspector class. Let's take a look at the code. We've imported the dart:mirrors library here to add the introspection ability to our code:

library inspector;

import 'dart:mirrors';
import 'test.dart';

class TypeInspector {
  ClassMirror _classMirror;
  // Create type inspector for [type].
  TypeInspector(Type type) {
    _classMirror = reflectClass(type);
  }

The ClassMirror class contains all the information about the observing type. We perform the actual introspection with the reflectClass function of Mirrors and return a distinct Mirror object as the result. Then, we call the getAnnotatedMethods method and specify the name of the annotation that we are interested in. This will return a list of MethodMirror that will contain methods annotated with specified parameters. One by one, we step through all the instance members and call the private _isMethodAnnotated method. If the result of the execution of the _isMethodAnnotated method is successful, then we add the discovering method to the result list of found MethodMirror's, as shown in the following code:

  // Return list of method mirrors assigned by [annotation].
  List<MethodMirror> getAnnotatedMethods(String annotation) {
    List<MethodMirror> result = [];
    // Get all methods
    _classMirror.instanceMembers.forEach(
      (Symbol name, MethodMirror method) {
      if (_isMethodAnnotated(method, annotation)) {
        result.add(method);
      }
    });
    return result;
  }

The first argument of _isMethodAnnotated has the metadata property that keeps a list of annotations. The second argument of this method is the annotation name that we would like to find. The inst variable holds a reference to the original object in the reflectee property. We pass through all the method's metadata to exclude some of them annotated with the Test class and marked with include equals false. All other method's annotations should be compared to the annotation name, as follows:

  // Check is [method] annotated with [annotation].
 bool _isMethodAnnotated(MethodMirror method, String annotation) {
    return method.metadata.any(
      (InstanceMirror inst) {
      // For [Test] class we check include condition
      if (inst.reflectee is Test && 
        !(inst.reflectee as Test).include) {
        // Test must be exclude
        return false;
      }
      // Literal compare of reflectee and annotation 
      return inst.reflectee.toString() == annotation;
    });
  }
}

Dart Mirrors have the following three main functions for introspection:

  • reflect: This function is used to introspect an instance that is passed as a parameter and saves the result in InstanceMirror or ClosureMirror. For the first one, we can call methods, functions, or get and set fields of the reflectee property. For the second one, we can execute the closure.
  • reflectClass: This function reflects the class declaration and returns ClassMirror. It holds full information about the type passed as a parameter.
  • reflectType: This function returns TypeMirror and reflects a class, typedef, function type, or type variable.

Let's take a look at the main code:

library test.framework;

import 'type_inspector.dart';
import 'test_case.dart';

main() {
  TypeInspector inspector = new TypeInspector(TestCase);
  List methods = inspector.getAnnotatedMethods('test');
  print(methods);
}

Firstly, we created an instance of our TypeInspector class and passed the testable class, in our case, TestCase. Then, we called getAnnotatedMethods from inspector with the name of the annotation, test. Here is the result of the execution:

[MethodMirror on 'testStart', MethodMirror on 'testStop']

The inspector method found the methods testStart and testStop and ignored testWarmUp of the TestCase class as per our requirements.

Reflection in action

We have seen how introspection helps us find methods marked with annotations. Now we need to call each marked method to run the actual tests. We will do that using reflection. Let's make a MethodInvoker class to show reflection in action:

library executor;

import 'dart:mirrors';

class MethodInvoker implements Function {
  // Invoke the method
  call(MethodMirror method) {
    ClassMirror classMirror = method.owner as ClassMirror;
    // Create an instance of class
    InstanceMirror inst = 
      classMirror.newInstance(new Symbol(''), []);
    // Invoke method of instance
    inst.invoke(method.simpleName, []);
  }
}

As the MethodInvoker class implements the Function interface and has the call method, we can call instance it as if it was a function. In order to call the method, we must first instantiate a class. Each MethodMirror method has the owner property, which points to the owner object in the hierarchy. The owner of MethodMirror in our case is ClassMirror. In the preceding code, we created a new instance of the class with an empty constructor and then we invoked the method of inst by name. In both cases, the second parameter was an empty list of method parameters.

Now, we introduce MethodInvoker to the main code. In addition to TypeInspector, we create the instance of MethodInvoker. One by one, we step through the methods and send each of them to invoker. We print Success only if no exceptions occur. To prevent the program from terminating if any of the tests failed, we wrap invoker in the try-catch block, as shown in the following code:

library test.framework;

import 'type_inspector.dart';
import 'method_invoker.dart';
import 'engine_case.dart';

main() {
  TypeInspector inspector = new TypeInspector(TestCase);
  List methods = inspector.getAnnotatedMethods(test);
  MethodInvoker invoker = new MethodInvoker();
  methods.forEach((method) {
    try {
      invoker(method);
      print('Success ${method.simpleName}');
    } on Exception catch(ex) {
      print(ex);
    } on Error catch(ex) {
      print("$ex : ${ex.stackTrace}");
    }
  });
}

As a result, we will get the following code:

Success Symbol("testStart")
Success Symbol("testStop")

To prove that the program will not terminate in the case of an exception in the tests, we will change the code in TestCase to break it, as follows:

// Start engine
@test
testStart() {
  engine.start();
  // !!! Broken for reason
  if (engine.started)  throw new Exception("Engine must start");
}

When we run the program, the code for testStart fails, but the program continues executing until all the tests are finished, as shown in the following code:

Exception: Engine must start
Success Symbol("testStop")

And now our test library is ready for use. It uses introspection and reflection to observe and invoke marked methods of any class.