Creating a command-line function caller

One useful application of reflection is that it builds dynamic interfaces automatically. Reflection can be used to help the code that interacts with a scripting language, a network protocol, and more. Here, we'll write a command-line program that calls functions and displays information about them when help is requested. We'll need to be able to add new functions without needing any boilerplate code to be added along with it.

How to do it…

We can create a command-line function caller by performing the following steps:

  1. Loop over all members of the module.
  2. Find functions with a protection level of export (alternatively, you may look for the presence of a user-defined attribute).
  3. If the user requested help, list the possible functions or details about a particular function, by performing the following steps:
    1. Use a user-defined attribute for documentation.
    2. Use std.traits' Parameter*Tuple family of functions to get details about the function.
  4. If the user wants to run a function, prepare a function call as follows:
    1. Use std.traits.ParameterTypeTuple to build the argument list.
    2. Check std.traits.ReturnType to see if the function has a return value (in other words, if it doesn't return void). If it does, store the return value as a string. Otherwise, call the function without trying to store the return value.
  5. Format the result or the error message for presentation to the user, as shown in the following code:
    // The functions we want to make available
    export {
      @doc("Say hello to the user", ["the user's name to greet"])
        string hello(string name = "user") {
          return "Hello, " ~ name ~ "!";
        }
    
      @doc("Adds two numbers together.")
        int sum(int a, int b) {
          return a+b;
        }
    }
    
    // alias helper for reflection
    alias helper(alias T) = T;
    
    // Our user-defined attribute for documentation
    struct doc {
      string value;
      string[] argDocs;
    }
    
    // Helper function to find a generic value UDA
    auto getAttribute(alias mem, T)() {
      foreach(attr; __traits(getAttributes, mem))
        static if(is(typeof(attr) == T))
        return attr;
      return T.init;
    }
    // Function to do the reflection and call
    int runWithArgs()(string[] args) {
      import std.traits, std.conv, std.stdio;
    
      string name;
      if(args.length) {
        name = args[0];
        args = args[1 .. $];
      }
    
      // look over the whole module for callable functions
      alias mod = helper!(mixin(__MODULE__));
      foreach(memberName; __traits(allMembers, mod)) {
        alias member = helper!(__traits(getMember, mod, memberName));
        static if(
            // Is it a function marked with export?
            is(typeof(member) == function) &&
            __traits(getProtection, member) == "export")
        {
          if(name == "--help") {
          // user requested help
            if(args.length) {
              if(memberName == args[0]) {
                // print the details of this function
                writef("Usage: %s", memberName);
                foreach(argName; ParameterIdentifierTuple!member)
                  writef(" %s", argName);
                writefln("
    	%s", getAttribute!(member, doc).value);
    
                if(ParameterTypeTuple!member.length)
                  writeln("Arguments:");
    
                auto argDocs = getAttribute!(member, doc).argDocs;
    
                foreach(idx, argName; ParameterIdentifierTuple!member) {
                  string defaultValue;
                  bool hasDefaultValue;
                  static if(!is(ParameterDefaultValueTuple!member[idx] == void)) {
                    defaultValue = to!string(ParameterDefaultValueTuple!member[idx]);
                    hasDefaultValue = true;
                  }
                  string argDoc = "?";
                  if(idx < argDocs.length)
                    argDoc = argDocs[idx];
    
                  writefln("	%s (%s): %s %s",
                      argName,
                      ParameterTypeTuple!member[idx].stringof,
                      argDoc,
                      hasDefaultValue ? "[default=" ~ defaultValue ~ "]" : "");
                }
              }
            } else {
              // no details requested, just show the full listing
              writefln("%16s -- %s", memberName, getAttribute!(member, doc).value);
            }
         // the user did NOT ask for help, call the function if
         // we have the correct name
          } else if(memberName == name) {
            // Prepare arguments
            ParameterTypeTuple!member arguments;
            alias argumentNames = ParameterIdentifierTuple!member;
            alias defaultArguments = ParameterDefaultValueTuple!member;
    
            try {
              foreach(idx, ref arg; arguments) {
                // populate arguments, with user data if available,
                // default if not, and throw if no argument provided.
                if(idx < args.length)
                  arg = to!(typeof(arg))(args[idx]);
                else static if(!is(defaultArguments[idx] == void))
                  arg = defaultArguments[idx];
                else
                  throw new Exception("Required argument " ~ argumentNames[idx] ~ " is missing.");
              }
    
              string result;
    
              // We have to check the return type for void
              // since it is impossible to convert a void return
              // to a printable string.
              static if(is(ReturnType!member == void))
                member(arguments);
              else
                result = to!string(member(arguments));
    
              writeln(result); // show the result to the user
              return 0;
            } catch(Exception e) {
              // print out the error succinctly
              stderr.writefln("%s: %s", typeid(e).name, e.msg);
              return 1;
            }
          }
        }
      }
      return 0;
    }
    
    int main(string[] args) {
      return runWithArgs(args[1 .. $]);
    }
  6. Run it with the following options to look at the results:
    $ ./command --help
               hello -- Say hello to the user
                 sum -- Adds two numbers together.
    $ ./command --help hello
    Usage: hello name
            Say hello to the user
    Arguments:
            name (string): the user's name to greet [default=user]
    $ ./command hello "D fan"
    Hello, D fan!
    

Also, try adding new functions and see how they work with no additional boilerplate code.

Note

Since Ddoc comments are not currently available through compile-time reflection, we had to define our own doc structure to use as an attribute instead.

How it works…

We brought together the following techniques we learned earlier in the chapter to form a complete program:

  • Looping over the module using __traits(allMembers) and finding functions with the is expression
  • Locating the user-defined attributes with a helper function and then using the value like a regular data type
  • Getting function details with the std.traits module from Phobos and __traits(getProtection), which returns a string with the protection level (for example, private, public, protected, export, or package)
  • Using runtime type information to get the dynamic name of the Exception subclass used to print errors

The new part is using the reflection data to actually locate and call the function's given information at runtime.

The first thing to notice is that the compile-time reflection is done the same way, as if the name of the function we want to call doesn't even exist. The reason is that at compile time, the name indeed doesn't exist yet—it comes from the user at runtime! All the reflection information needs to be there just in case the user wants to call it.

Another consequence of this is that we need to make sure that all the possible function calls compile. If name != memberName, the code won't run, but it will be still compiled just in case it does run the next time.

The next new thing is actually calling the function. We learned about ParameterTypeTuple previously and saw how to get information from it. Here, we're using ParameterTypeTuple to actually declare a variable that holds the function arguments. We loop over it, by reference, filling in those arguments from our runtime strings. Using typeof in the loop, we can determine what type of value the function expects and automatically convert our strings appropriately.

Notice that we checked the length of the runtime args argument against the index of the parameter loop instead of looping over the runtime arguments and assigning them to the parameters. It is important to always loop over compile-time constructs when available, since that maximizes the potential for compile-time reflection.

If we try to write foreach(idx, arg; args) arguments = to!(typeof(arguments[idx]))(arg), the compiler will complain about not being able to index a compile-time construct with a runtime variable—it won't even know the limits until the user provides an arguments' string! Remember, compile-time data can always be used at runtime, but runtime data can never be used at compile time.

Once the arguments are prepared, we pass them to the function. As we saw in the discussion of .tupleof previously, compile-time tuples or template argument lists have the special ability to expand to fill argument lists.

The last notable aspect of the function call is checking the return value. We want to assign it to a string so that we can print it to the user. Almost any type can be converted to a string, but there is one major exception: void. When a function doesn't have a return value, we cannot do the conversion! If we want to store the return value in a variable, we will have to perform this check too, since variables cannot have no type or void type.

A simple call to ReturnType inside static if lets us handle the special case without breaking the build.

There's more…

The generated code for these loops looks like the following:

if(name == "hello")
    hello(to!string(args[0]);
if(name == "add")
    add(to!int(args[0]), to!int(args[1]);

It is a linear list of function names. If you had a very large number of functions, searching this list for the right function to call may be slow. A potential optimization for this program will be to put each function call into a helper function or a switch case statement, and then generate the code to perform a hash table lookup with a mixin expression. We'll learn about code generation in the next chapter.

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

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