D's features also lead to easy integration with dynamic languages. It can host a dynamic type as well as dynamic objects.
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
Let's execute the following steps to communicate with JavaScript:
opDispatch
and variadic
arguments to forward the D function call syntax to the script to use user functions.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")); }
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.
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.
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.
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.
3.14.246.148