Communicating with a dynamic scripting language

D's features also lead to easy integration with dynamic languages. It can host a dynamic type as well as dynamic objects.

Getting ready

To begin, download bindings to a scripting language. Here, we'll use the one I wrote whose syntax is inspired by both D and JavaScript. It was written with the goal to blur the line between the D code and the script code—the scripting language's dynamic type can also be used in D itself. Download jsvar.d and script.d from the following website:

Build your program with all three files on the command line, as follows:

dmd yourfile.d jsvar.d script.d

How to do it…

Let's execute the following steps to communicate with JavaScript:

  1. Create an object to wrap the script engine with a friendlier API.
  2. Prepare the script engine by passing data and function references to it. It may be necessary to wrap D functions in transforming functions to match the script engine's layout.
  3. Use opDispatch and variadic arguments to forward the D function call syntax to the script to use user functions.
  4. Evaluate the user script.
  5. Call a user-defined script function.

The code is as follows:

struct ScriptEngine { // step 1
    import arsd.script;

    this(string scriptSource) { // step 2
        scriptGlobals = var.emptyObject;

        // add a function, transforming functions and returns
        import std.stdio;
        scriptGlobals.write = delegate var(var _this, var[] args) {
            writeln(args[0]);
            return var(null);
        };

        // run the user's script code (step 4)
        interpret(scriptSource, scriptGlobals);
    }

    var scriptGlobals;

    auto opDispatch(string func, T...)(T t) { // step 3
        if(func !in scriptGlobals)
            throw new Exception("method "" ~ func ~ "" not found in script");
        var[] args;
        foreach(arg; t)
            args ~= var(arg);
        return scriptGlobals[func].apply(scriptGlobals, args);
    }

}
void main() {
    // step 5
    auto scriptContext = ScriptEngine(q{
         // this is script source code!
        function hello(a) {
            // call the D function with the argument
            write("Hello, " ~ a ~ "!");

            // and return a value too
            return "We successfully said hello.";
        }
    });

    // call a script function and print out the result
    import std.stdio;
    writeln("The script returned: ", scriptContext.hello("user"));
}

How it works…

If a method isn't found in an object, and the object implements a member called opDispatch, D will rewrite the missing method as follows:

obj.name(args…); // if obj doesn't have a method called nameobj.opDispatch!"name"(args…); // it is rewritten into this

The method name is passed as a compile-time argument. The opDispatch implementation may thus be able to handle this without a runtime cost. If the lookup must happen at runtime, as is the case here, we can use the compile-time argument in the same way that we'd use a runtime argument, including forwarding it to a runtime lookup function.

The opDispatch function, like any other function, can also take both additional compile-time and runtime arguments. Here, we defined it as a variadic template with T…, which is an arbitrary length list of types. The exact list is determined automatically when you call the function. In short, this function can accept any number of arguments of any types. This is perfect for forwarding to a dynamic function, like a user-defined script method.

Note

The writeln function of std.stdio is also a variadic template in this form. That is why you can pass it a mixed list of strings, integers, or any other types and it knows how to handle them all, even with a generic format string.

In a (T…)(T t) template argument list, T is the list of types and t is the list of values. Both T and t can be looped over with foreach, similar to an array.

Inside the loop, we will build an array of dynamic variables to pass to the script engine. The constructor of var automatically handles conversions from arbitrary types by using std.traits to categorize them into basic categories, and keeps a flag internally that says which category it is currently holding. The following code is an excerpt:

this(T)(T t) {
    static if(isIntegral!T) {
          this._type = Type.Integral;
          this._payload._integral = to!long(t);
    } else static if(isSomeString!T) {
           this._type = Type.String;
          this._payload._string = to!string(t);
   } else static assert(0, "Unsupported type: " ~ T.stringof);
}

The constructor takes an argument of any type T. Using static if, it checks whether the type is a type of integer or a type of string. For these types, the code stores the internal flag and the value, which is converted to one uniform type that can represent the whole category. This lets us automatically handle as many D types as possible with the least amount of script glue code. The full implementation in jsvar checks every supported category it can handle, including objects, floating point values, functions, and arrays. They all follow a pattern similar to this.

Tip

The script engine has already provided an implementation of various operators, including calling functions. You can simply write scriptGlobals["hello"]("user") using either the opIndex function written in jsvar or scriptGlobals[name](t) written in our opDispatch—forwarding the variadic argument list—and it would build the args array for us. The var type—a user-defined dynamic type from the jsvar module you downloaded—also implements opDispatch, though calling functions with it currently requires a second set of parenthesis to disambiguate function calling from property assignment.

We also return a value from this function. In some cases, you'd have to write a converter for the script engine's return value, checking its type flag type and offering an opCast, to, or get function, which works in the reverse of the preceding constructor example, or perhaps convert it to a string with toString. The var type used here implements a get function using the type family check like the constructor as well as a toString function, which makes it just work in writeln.

We also made the writeln function available to the script by wrapping it in a helper function that accepts script variables, calls the D function, and then returns an appropriate variable back to the script. Like with calling functions, this can be (and is, in jsvar) automated so that nothing is necessary beyond writing scriptGlobals.write = &writeln!var;, but we did it the long way here to show how it is done. The automatic generator implementation works in the same way using a bit of compile-time reflection to get the details right for each function. We'll cover the reflection features later in the book.

Note

Why would we have to write &writeln!var instead of just &writeln? As writeln has compile-time arguments—though they are typically deduced automatically while calling writeln (they are always there)—we have to specify which ones we want at compile time before it is possible to get a runtime function pointer. Different compile-time arguments may mean a different function pointer. If the function didn't have compile-time arguments, then &func would work.

Lastly, we used a peculiar type of a string literal for the source, that is, a q{ string literal }. This works the same as any other string literal, with one difference; the q{} literal needs to superficially look like the valid D code (technically, it must pass the D tokenizer). It must not have unterminated quotes and unmatched braces. As far as the consuming function is concerned though, it is just a string, like any other. We could have also loaded the script from a file. We used q{} here because the script source looks like D source. Most syntax-highlighting editors continue to highlight q{} strings like regular code, giving us colorization for the script as well. However, which kind of string literal you use comes down to the circumstances and personal preference.

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

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