Setting up the Host Application

MiniD is meant primarily to be an extension language -- a language used to extend the functionality of a larger program. To that end, the reference MiniD implementation is designed as a library meant to be used within another application. The controlling application is called the "host." Through the library, the host application sets up an environment in which MiniD code can execute, then loads code (either in source code form or from a precompiled binary file) and runs it.

As an example, consider MDCL, the MiniD command-line interpreter. When run in non-interactive mode, it's an extremely simple application. Basically it sets up a minimal environment, loads and runs a single script, then exits. We will create a similar application as an example.

The VM and Threads

There are two important things which make up the MiniD execution environment. First is the VM, represented by the MDVM struct. The VM holds global state, most importantly the global namespace, as well as the global metatables for the built-in types. Second are the threads, represented by the MDThread struct. Threads represent a thread of execution, including a call stack and stack of local variables. These also correspond to MiniD's coroutines. Each thread is associated upon creation with a single VM. In this way, you can think of the relationship between VMs and threads as very similar to that of processes and threads in most operating systems -- a process holds global state which is shared amongst the one or more threads which execute the program's code. One important difference is that in MiniD, multithreading is collaborative, meaning only one thread is executing per VM at any given time, making synchronization and all the headaches that go along with it unnecessary.

Your app can actually have as many VMs as it wants. Each one is completely independent of all others. They all have different global state and can be running different pieces of code. This situation probably isn't common, but is useful for things like having a sandbox to run untrusted code (into which a "safe" set of libraries has been loaded), or to have multiple native threads running different scripts (since the VMs are independent, no synchronization is necessary).

When you create a VM, it is automatically given one thread, the main thread. The main thread will never be collected during the lifetime of the VM. All code you execute in the VM will ultimately have to have been executed through the main thread.

Two parts of the VM -- the global namespace and the global metatables -- deserve mention.

The Global Namespace

MiniD's functions of course have local variables, but global variables are where all kinds of persistent data is kept. When you declare a global in a MiniD module, the variable is actually inserted into the module's namespace, after which it can be accessed from other modules through the module's name. Module (and package) namespaces form a tree, the root of which is the global namespace. This is a nameless, parentless namespace, one of which is created automatically for each VM you create. The global namespace does have a global in it, _G, which refers to itself. This is useful for getting a reference to the global namespace in your scripts.

When you create a VM, the global namespace is also populated with the base library and its sub-libraries, the "modules" and "thread" libraries. After that, which extra standard libraries are loaded are up to you.

The minid.api module contains the functions to open and close VMs, as well as the function to load standard libraries into an open VM.

The Global Metatables

The metamethods section of the spec shows how to define and use the various metamethods available to MiniD objects to customize their behavior. In fact all MiniD types have metamethods, but only tables and objects can be customized from within the language. Other types have metamethods defined for them by the host application. For example, tables, arrays, and namespaces all have opApply metamethods defined for them by the various standard libraries, without which you wouldn't be able to use foreach over them. The global metatables aren't just used for holding metamethods, either; they are more or less a general method table associated with a type. All the methods of the character type, which come from the character standard library, are held in the character type's global metatable.

A Simple Example

So we want to run some MiniD code. The first thing we have to do is create a VM in which to run it. You do this using the openVM function in minid.api. In fact, importing minid.api will give you access to most of the "normal" API functions in the library, as it publicly imports many other modules.

You are responsible for allocating an MDVM structure. This is because if the MDVM structure is not allocated in D's memory (or more specifically, if the D GC doesn't know about the MDVM structure), the references that the VM will keep to D-allocated memory will not be scanned and the D GC will accidentally collect them. The MDVM structure can be allocated anywhere, on the heap or on the stack.

openVM takes a pointer to a closed MDVM structure and initializes it. It creates all the global data structures, such as the global namespace, and loads the base library into it. It then returns the main thread of the VM.

Let's create a VM and load all the standard libraries into it:

module example;

import minid.api;

void main(char[][] args)
{
	MDVM vm;
	auto t = openVM(&vm);
	loadStdlibs(t);
}

Easy enough! We allocated the VM on the stack here, but you could allocate it on the heap if you wanted to. We called openVM and got back the main thread of the VM.

When you call loadStdlibs, by default, all the standard libraries (except the debug library) are loaded into the context that owns the thread that you passed in. There is an optional second parameter that you can use to choose the libraries that are loaded.

Now we need to load some code and run it. Let's just assume the first parameter to our program is the name of the script to load, in the form "foo.bar.baz" (just like an import statement).

module example;

import minid.api;

void main(char[][] args)
{
	MDVM vm;
	auto t = openVM(&vm);
	loadStdlibs(t);
	importModule(t, args[1]);
}

That's it. importModule will search for a module (in either source code or binary form, picking whichever is most recently modified), load it, and then run it. It will also create an entry for the module in the global namespace hierarchy, into which any globals that the module declares are inserted.

What importModule won't do, however, is run any "main" function declared in the given module. The way you do that is to call the modules.runMain function on the newly-imported module. You'll learn how to call functions in native code in a later chapter.

If we compile this program and create a MiniD script like so:

module testscript
writeln("Hi, I'm a script!")

in a file "testscript.md", we can then run our example program with "example testscript" and we'll get the output:

Hi, I'm a script!

That's all there is to it.

A Simple Interactive Interpreter

Next we'll make an interpreter that you can use to enter code and have it executed immediately. It will be very basic, and will probably be kind of a pain to use, but it'll demonstrate some useful stuff.

module example;

import minid.api;

import tango.io.Console;
import tango.io.Stdout;
import tango.text.stream.LineIterator;

void main(char[][] args)
{
	MDVM vm;
	auto t = openVM(&vm);
	loadStdlibs(t);

	auto input = new LineIterator!(char)(Cin.stream);

	while(true)
	{
		// Output prompt
		Stdout("> ").flush;

		// Get next line of text
		char[] line = input.next();

		// Check for end-of-flow
		if(line.ptr is null)
			break;

		// Run the code and check for errors
		try
			runString(t, line);
		catch(MDException e)
		{
			catchException(t);
			pop(t);
			Stdout.formatln("Error: {}", e);
		}
	}
}

First we create a VM and load the libraries into it, like in the last example. Then we set up our input and go into a loop. We print the prompt ("> ") and get a line of text. We check for end-of-flow so that we can exit. The most interesting part is the call to runString. This function takes two parameters, the first of which is a state in which to execute the code, and the second is a string containing the code to compile and execute. This is a "statement string" which differs from a module in that there need not be a module declaration at the top. You can have any number of statements in the statement string.

We check for errors using a try-catch statement. This is certainly much easier than in, say, Lua, where you have to use "protected calls" and check error codes. If we wanted, we could skip the error checking altogether and the program would just quit if an error occurred. Note, however, that we have to inform the interpreter that we've caught the exception with the call to catchException, or else the interpreter will keep thinking that an exception is in flight, and weird things can happen. The subsequent call to pop removes the exception object from the stack.

This is a pretty crappy interactive interpreter. Thankfully, we don't actually have to write one ourselves. We have MDCL after all, but actually, all of the functionality of MDCL is contained within a library inside MiniD. MDCL itself is only a couple lines long. Have a look:

module mdcl;

import tango.io.Stdout;
import tango.io.Console;

import minid.api;
import minid.commandline;

void main(char[][] args)
{
	MDVM vm;
	auto t = openVM(&vm);
	loadStdlibs(t);

	CommandLine().run(t, args);
}

The minid.commandline module contains a struct, CommandLine, which allows you to add an interactive MiniD console to just about any place in your app. All you have to do is give it input and output streams and run it.

Closing the VM

Sometimes, you'll need to close a VM. Closing a VM will deallocate all memory associated with it. This is necessary for long-running programs that create VMs and need to get rid of those which it doesn't need anymore. Also, sometimes objects that are allocated by the VM have some cleanup to do, such as releasing system resources; closing a VM will allow those objects to release their resources.

Closing a VM is very simple; you just use the closeVM function.

closeVM(&vm);