Extending the runtime type information

D's built-in TypeInfo doesn't provide nearly as much information as we can get from the compile-time reflection tools. Here, we'll create an extended type info with custom data.

How to do it…

Let's execute the following steps to extend the runtime type information:

  1. Create an interface with methods exposing the information you want to make available at runtime.
  2. If your method works with the data, remember that RTTI is typically used with very little compile-time type information. Thus, methods should take a generic runtime type, such as Object or void* in the generic interface.
  3. Create an associative array of your interface keyed on TypeInfo.
  4. Write helper functions to retrieve the information from the associative array. Use typeid() to get the lookup key from a given variable or type.
  5. Write a template class that will implement your generic interface for each supported type. The std.traits Phobos module will help with our implementation.
  6. Write a mixin template that loops over all the types to which you wish to add support for extended type information. Alternatively (or additionally), you could provide a registerType function to be called explicitly or on request in the extended info lookup functions.
  7. On the user side, register the types you wish to support and use the information.

The code is as follows:

/// stores the information
immutable MoreInfo[TypeInfo] moreTypeInfo;

/// Gets extended info from a value
public immutable(MoreInfo) extTypeInfo(T)(T t) {
  if(auto ptr = typeid(t) in moreTypeInfo)
    return *ptr;
  return null;
}

/// Gets extended info from a type
public immutable(MoreInfo) extTypeInfo(T)() {
  if(auto ptr = typeid(T) in moreTypeInfo)
    return *ptr;
  return null;
}

/// The interface we provide to more info
public interface MoreInfo {
    immutable:
  /// is it an integral type?
  bool isIntegral();

  /// given a pointer to this type, return it as a string
  /// Be sure the pointer is actually the correct type!
  string getAsString(in void*);
}

/// The implementation of the interface for any type
private class MoreInfoImpl(T) : MoreInfo {
    immutable:
  // std.traits offers reflection helpers
  import trait = std.traits;

  bool isIntegral() {
    return trait.isIntegral!T;
  }

  string getAsString(in void* ptr) {
    import std.conv;
    auto t = cast(T*) ptr;
    return to!string(*t);
  }
}

/// This creates all the instances we want to enable
public mixin template EnableMoreInfo(types...) {
  /// A static constructor is run at thread startup
  shared static this() {
    foreach(type; types)
      moreTypeInfo[typeid(type)] = new immutable MoreInfoImpl!type();
  }
}

/* User code */

class A { } // a random custom class

// enable more info for built-in ints and our class
mixin EnableMoreInfo!(int, A);

void main() {
  import std.stdio;
  // test
  writeln(extTypeInfo(1).isIntegral()); // true
  writeln(extTypeInfo!(A).isIntegral()); // false

  int a = 34;
  writeln(extTypeInfo(a).getAsString(&a)); // prints 34
}

How it works…

We used three features to make information from compile-time reflection available at runtime: classes with interfaces, static module constructors, and associative arrays with the TypeInfo keys.

First, the interfaces were used to provide a consistent base type that always works at runtime. Sometimes, it is tempting to think of templates as being almost magical—if we could only write cast(runtime_type) it will all work. However, this is impossible because templates and types only exist at compile time. If you had enough information to successfully perform a downcast, you'd have enough information to use compile-time reflection and thus have no need for runtime information anyway!

So, while the class that implements the interface is a template, allowing us to use compile-time reflection to fill in the information, saving a lot of manual work. The interface itself is what must be used at runtime and it must all work with generic types. To get a type's value generically, we put getAsString in the interface. No matter what the underlying data is, we can convert it to one uniform type, string, and use that. Since the interface itself isn't a member of the type we're converting, getAsString must take a pointer to the data so it knows what to convert. Given the limitations of the generic MoreInfo interface, the data pointer must also be generic, so we use void*.

Other interface methods that operate on the data will need to work the same way; return a consistent type and take a generic pointer to the data.

Once we've defined and implemented the interface, we need some way to access it at runtime. That's where the associative array comes in. Using TypeInfo as a key, we can look up our extended information any time we are given a TypeInfo+void* pair elsewhere in the code, including generic classes. We can work with classes with a generic base type of Object instead of void* if you prefer, and we can always retrieve their TypeInfo with the typeid operator, but that only works on class objects. Remember, class objects are unique and they always carry their TypeInfo reference with the actual data.

However, how do we populate the associative array with data coming from several sources, possibly across modules and compilation units? That's where the static module constructor comes in.

A static module constructor is declared with one of two syntaxes: static this() or shared static this(), both in module scope. The static this() constructor declares a thread-local module constructor. Each new thread spawned will run its code to initialize thread variables. The shared static this() constructor declares a module constructor for the data shared across all threads, including the immutable data. A shared static constructor is run only once, automatically upon program start-up, before main is run.

Tip

You can also write static constructors for your classes and complex types by using the same syntax in the object's scope. It is important that you write the definition exactly as it is seen here; the literal keywords static this or shared static this must appear, in that order, for the compiler to recognize the constructor.

Since our extended type info never changes, we made it immutable. Immutable data normally cannot be modified, but the compiler makes an exception for constructors to allow them to be initialized. Since it is immutable, it can also be shared across threads, so we initialized it in a shared static constructor. Thus, the cost of initialization is minimized because it is done once and only once.

What happens if you have multiple calls to EnableTypeInfo in the same module? Try it, you'll find it works. The compiler will combine multiple module constructors together automatically.

Once the array is built, we can use it like any other collection of data. The extTypeInfo helper functions give us an easy interface to use a given type or variable, or we can access the array directly with a TypeInfo key. Since our extended type information is opt-in, these functions may return a null value, indicating that the extended information is not available for the given type. Here, we used a small manually curated list of types that will be enabled. We could have also used compile-time reflection to search the module for all types it defines.

Note

Why don't we construct the extended information lazily in the helper function? Immutability of the array notwithstanding, that will actually work for some types. The problem is lazy construction only works where compile-time reflection will work anyway, defeating the point of enabling runtime reflection. An example where it will not work correctly is when you give it an instance of a child class through a base class. The lazy function will construct information for the base class, unaware of the child class.

There's more…

The D runtime and compiler also includes a feature we could use to make extended type information for all user-defined types automatically. In the documentation of TypeInfo at dlang.org, you might have noticed a member rtInfo, which provides implementation-specific type extensions for the garbage collector.

Every time the compiler encounters a new type definition, it invokes a template called RTInfo on that type. The RTInfo template is found in the automatically imported core library file, object.d. The RTInfo template can return a pointer to the implementation-defined data that is accessible through TypeInfo, and it can also define new module constructors to build additional lists, exactly as we did here.

The same feature could, in theory, be used for user-defined type extensions too. However, since customizing it means customizing your core runtime library, it isn't very practical today. There's ongoing work in the D community to make this feature more accessible to end users.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.237.164