Note: This website is archived. For up-to-date information about D projects and development, please visit wiki.dlang.org.

Exchanging Values and Calling Functions

The Stack and Native Objects

MiniD has its own garbage collector and heap which is separate from the D heap. Each garbage collector only knows about its own heap. Suppose you got a pointer to a MiniD object in D, and stored it on the D heap. Suppose then that that object becomes garbage in the eyes of the MiniD GC and is collected. You now have a dangling pointer to a nonexistent object. The inverse is true - that is, if a pointer to the D heap were stored on the MiniD heap.

In order to combat this problem, the native API never gives you the chance to get a pointer to a MiniD object. Rather than getting a pointer and manipulating the object directly, you instead perform all operations indirectly through a virtual stack that is owned by the MiniD GC. This way, any objects on the stack will not be collected, but they will still be accessible from native code.

The other direction - MiniD holding pointers to D objects - is handled by the MiniD "native object" type. This is defined as something like an opaque pointer. Whenever you create a native object, though, the interpreter keeps track of its existence and puts the pointer in a list that is accessible to the D GC. This is why the MDVM object that we talked about in the last section has to be allocated on the D heap: so that the D GC will be able to see all the pointers that that VM holds to the D heap.

Stack Indices, Pushing, Popping, and Other Manipulations

So where is the stack? There's actually multiple stacks - each thread object has its own stack. Therefore all stack manipulations are performed on a thread object. The stack is not really a stack in the typical data structure sense, though, in the sense that most stack manipulations allow you to access arbitrary items and not just the top. The items on the stack are accessed using signed integral indices.

The stack always has at least one item on it, and that is at index 0. This is the 'this' parameter, and receives special treatment. The 'this' parameter may never be replaced or removed from the stack.

Positive stack indices access items from the bottom of the stack up. The stack grows upwards; that is, when you push things onto it, their indices will be increasing positive numbers. Negative stack indices access items from the top of the stack down. This is similar to how array indexing works in MiniD. An index of -1 accesses the top item on the stack; -2, the item below that; and so forth.

Positive stack indices are also sometimes called "absolute" indices, as they always refer to the same slot on the stack. Negative indices are always relative, and because of that, they might not be usable for some things.

You can get how many items there are on the stack with the stackSize function. This returns the number of items including the 'this' parameter (index 0). You can therefore access positive indices up to stackSize(t) - 1. You can also access negative indices down to -stackSize(t). One other function that is useful with stack indices is isValidIndex. This takes a stack index and returns true if accessing that index is OK, or false otherwise.

In order to get data onto the stack, there are a variety of methods which push individual values, as well as methods that access items from inside other values or from globals. The most basic methods involve pushing value types and strings.

pushNull(t);
pushBool(t, false);
pushInt(t, 4);
pushFloat(t, 2.718);
pushChar(t, 'x');
pushString(t, "hello");

All of these functions also return the corresponding stack index of the value that is pushed, but we don't need those indices right now.

A word on the pushString function. Even though MiniD pretends that strings are UTF-32, the implementation actually uses UTF-8 to store them. Hence, pushString accepts a char[] string value. The string that you push is never referenced by MiniD; a copy is created (if needed) on the MiniD heap and that is pushed, so it's perfectly fine to push slices of temporary buffers and such. You never need to duplicate strings that you give to MiniD.

Going the other way - getting those values off the stack - is just as easy. Supposing that we just executed the previous code, the following code would get the values other than null off the stack. Note the use of negative indices.

auto b = getBool(t, -5);
auto i = getInt(t, -4);
auto f = getFloat(t, -3);
auto c = getChar(t, -2);
auto s = getString(t, -1);

All the 'get' methods check that the value at the given index is of the correct type. None of them actually pop anything off the stack, they just get a value.

There are two things to note. One, getFloat does not implicitly convert integers to floating-point numbers. For that, you can use getNum, which expects the value at the given index to be an int or a float and will return the floating-point representation of it. Two, getString is a bit special. It is the only function in the API to break the rule of not returning pointers to MiniD objects. getString returns a string that points onto the MiniD heap. Needless to say, do not modify this string (a const(char)[] would come in handy right about now). If you're just printing out the resulting string, or copying its contents somewhere else, there's no need to do anything special. But if you plan on modifying it or storing the reference somewhere on the D heap, be sure to perform .dup on it first. This is because when the string is no longer referenced by anything on the MiniD heap, it will be collected and you will end up with a dangling string reference. (even a const(char)[] couldn't help us here.)

If you want to remove items from the stack, you of course have the pop function. It can pop any number of items, but it defaults to popping just one if you call it as "pop(t)". Continuing our example:

pop(t, 6); // pops six values

Now the stack is how it was before we started, no more or fewer items. It is good practice for your stack manipulations to be balanced like this. If you get into the habit now, you'll avoid problems with the stack growing uncontrollably later.

There's another way to put strings on the stack which allows you to use Tango-style formatting: the pushFormat function. This is a really useful operation as it allows you to put formatted content on the stack as a MiniD string without having to allocate any D heap memory. You can also use it to push UTF-16 or UTF-32 strings onto the stack. For example:

pushFormat(t, "I have {} things and {} places to put them.", numThings, numPlaces);

wchar[] str = "this is a wide string!";
pushFormat(t, "{}", str); // performs UTF-16 to UTF-8 conversion

There's another function, pushVFormat, which accepts a TypeInfo[] and a va_list to allow you to pass variadic arguments on to it. It does the same thing as pushFormat.

There are a few handy functions that you can use to move data around on the stack.

The first is dup. It duplicates an existing value and pushes it onto the stack. It doesn't actually perform a clone operation or anything, it just duplicates a stack slot. By default, it duplicates the value at the top of the stack, but you can duplicate any index.

// The comments just show what is on top of the stack.
               // []
pushInt(t, 5); // [5]
pushInt(t, 3); // [5 3]
dup(t);        // [5 3 3]
dup(t, -3);    // [5 3 3 5]
pop(t, 4);     // []

The next is swap, which swaps two items. By default it swaps the top two. If you call it with one index, it swaps the value at that index with the value at the top of the stack. If you call it with two indices, it swaps those two slots.

pushInt(t, 1);   // [1]
pushInt(t, 2);   // [1 2]
pushInt(t, 3);   // [1 2 3]
swap(t);         // [1 3 2] - swapped top 2 items
swap(t, -3);     // [2 3 1] - swapped top with index -3
swap(t, -3, -2); // [3 2 1] - swapped indices -3 and -2
pop(t, 3);       // []

Next up is insert, which inserts the value at the top of the stack into an arbitrary position, shifting everything after that position up a slot. The value at the top of the stack will be inserted at the index you give it, meaning the values from that index up will be shifted up. Performing "insert(t, -1)" is a no-operation, since you're moving the value at the top of the stack to the same location.

pushInt(t, 1); // [1]
pushInt(t, 2); // [1 2]
pushInt(t, 3); // [1 2 3]
insert(t, -3); // [3 1 2] - inserted '3' before '1' and '2', so they moved up
insert(t, -2); // [3 2 1] - inserted '2' before '1', so it moved up
pop(t, 3);     // []

A more generalized version of insert is rotate. Whereas insert only moves the top item into a given slot, rotate allows you to move several items from the top of the stack into a sequence of slots. rotate takes two parameters: the number of slots to be rotated, and how far they should rotate. So, if you tell it to rotate the top five slots by two, the top two slots will be moved before the next three below them, and those three will be moved up. It's a bit difficult to explain, so have a look:

                 // assume the top of the stack currently looks like this:
                 // [0 1 2 3 4 5]
rotate(t, 5, 2); // [0 4 5 1 2 3] - the top two items - 4 and 5 - were rotated within the top five
rotate(t, 5, 1); // [0 3 4 5 1 2] - the top item was rotated within the top five
insert(t, -5);   // [0 2 3 4 5 1] - hey, the top item was rotated within the top five!

As you can see, performing an insert operation is exactly the same as rotating one item.

Related to rotate, there is a rotateAll function which just takes a distance. It rotates all items on the stack (except 'this') by the given number of slots. This is just a shortcut for "rotate(t, stackSize(t) - 1, distance)".

Related to insert, there is a function called insertAndPop which does just that. It lets you insert an item into a stack location, and then pops all the values after that item. insertAndPop is more efficient than an insert followed by a pop.

                     // assume the top of the stack currently looks like this:
                     // [1 2 3 4 5]
insertAndPop(t, -3); // [1 2 5] - 5 was inserted before 3 and 4, and they were popped off

Finally we have setStackSize, which is the mutator equivalent of stackSize. It does just what it says: it sets the stack size to a given amount. Since stack size takes the 'this' parameter into account, and since it's illegal to remove 'this', the minimum stack size is then 1. If you make the stack smaller, it's like performing a pop. If you make the stack bigger, the new slots are filled with null.

                    // assume the entire stack looks like this:
                    // [this 1 2 3 4 5]
setStackSize(t, 3); // [this 1 2] - 3, 4, and 5 disappear
setStackSize(t, 6); // [this 1 2 null null null] - new slots filled with null

Our First Function

Now that we know how to manipulate basic values on the stack, we can write a D function that will be callable from MiniD.

A native function has the signature uword function(MDThread* t, uword numParams). (uword is just an alias to the platform's native unsigned word type, the same as size_t but with a nicer name.) The first parameter is the MiniD thread object in whose context this function is expected to execute. The second parameter is how many parameters were passed to this function. If numParams is nonzero, the parameters will be on the given thread's stack in slots 1 through numParams. Functions always receive 'this' in slot 0, but it is not counted by numParams. Finally, the function's return value is how many values this function is returning. We return values from a native function by just leaving them on top of the stack in order.

Now we can write a function that takes some parameters, does something to them, and then returns a result. For fun, let's make a function that returns two results. We'll write a function that takes a list of numbers (ints or floats) and returns both the minimum and maximum values.

uword minmax(MDThread* t, uword numParams)
{
	if(numParams < 1)
		throwException(t, "Must have at least 1 parameter to minmax");

	auto min = mdfloat.max;
	auto max = -mdfloat.max;

	for(word i = 1; i <= numParams; i++) // note the bound condition on this loop!
	{
		auto val = getNum(t, i);

		if(val < min)
			min = val;

		if(val > max)
			max = val;
	}

	pushFloat(t, min);
	pushFloat(t, max);
	return 2;
}

The first thing this function does is it checks to see if it has at least one parameter, and if not, it throws an exception. throwException is a simple function. It has two forms. The form we're using here takes a format string and any number of values, which are then passed on to pushVFormat. The resulting string is thrown as an exception. This is a really common way to throw exceptions. The other form of throwException takes no parameters other than the thread and simply throws the item on top of the stack as an exception.

The next thing to note about this function is the bound condition on the for loop. The upper bound is inclusive instead of exclusive. That is, numParams is a legal parameter index, and refers to the last parameter (if any).

We loop through all the parameters and get them as numbers with getNum. Remember, getNum accepts ints or floats and will return the value cast to a floating-point number (of type mdfloat, which is the MiniD float type). Also, if a given parameter is not a number, getNum will throw an exception. When the loop completes, we push the minimum and maximum values onto the stack and return 2 to indicate that we are returning values.

Now that we have the function, we have to actually create a closure and store it somewhere where MiniD code can access it. This is easy: we create a closure with newFunction, and can store it in a global variable with newGlobal. If we put this all together, we get the following:

module example;

import minid.api;

void main()
{
	MDVM vm;
	auto t = openVM(&vm);
	loadStdlibs(t);

	newFunction(t, &minmax, "minmax");
	newGlobal(t, "minmax");
	
	runString(t, `
	local min, max = minmax(2, 5, 8.6, -3, 12.4)
	writefln("min = {}, max = {}", min, max)`);
}

uword minmax(MDThread* t, uword numParams)
{
	if(numParams < 1)
		throwException(t, "Must have at least 1 parameter to minmax");

	auto min = mdfloat.max;
	auto max = -mdfloat.max;

	for(word i = 1; i <= numParams; i++)
	{
		auto val = getNum(t, i);

		if(val < min)
			min = val;

		if(val > max)
			max = val;
	}

	pushFloat(t, min);
	pushFloat(t, max);
	return 2;
}

Some of this - opening the VM, loading standard libraries into it, and running MiniD strings - you've already seen. The new stuff is the use of newFunction and newGlobal. newFunction takes a pointer to a native function and a name (which is only used for debugging), creates a closure, and pushes it onto the stack. Now we just need to put the function somewhere, which is what newGlobal does: it creates a new global variable of the given name, puts the value that is on top of the stack into it, and pops that value off the stack. newGlobal works just like a global variable declaration in MiniD. It creates the global in the currently-executing function's environment namespace. Since there is no currently-executing function, it defaults to the global namespace (_G).

After creating the global, we run some MiniD code which calls minmax and then prints out the values it gets back. The output of this entire program is:

$ ./example
min = -3.0, max = 12.4
$

Notice that the minimum is reported as a floating-point number, even though we passed in an integer; again this is because we used getNum which always returns a float.

(This function could be written in such a way that would not change the types of the values and really would return the integer -3. It would also be a bit less efficient, but if you're going for generality, know that it is possible.)

More Interesting Parameter Stuff

Most of the time when you write a function, it expects to be passed a certain number and types of arguments. The MiniD interpreter has a whole slew of functions for checking types of values: type returns the type of a value at a given index, and there are isXxx functions for every type in the language (isNull, isBool, isChar, etc.). Checking parameters with these kinds of functions gets really tedious. Thankfully the extended API (the minid.ex module) provides several functions to help with this.

There are two classes of functions in the extended API for checking parameters: the check functions and the opt functions. The check functions check that you have a parameter of the given type, and the opt functions allow for optional parameters (you provide a default value). I won't go into gory detail of how to use these functions here, but it suffices to say that they work similarly to the get functions described earlier. They also all give nice error messages saying how "parameter blah is supposed to be type X but is type Y instead" or "there are not enough parameters".

The isXxx type checking functions aren't always useless for checking the types of parameters. Since functions can't be overloaded, you instead have to write a function that decides what to do based on the types of its arguments.

module example;

import tango.io.Stdout;

import minid.api;

void main()
{
	MDVM vm;
	auto t = openVM(&vm);
	loadStdlibs(t);

	newFunction(t, &freep, "freep");
	newGlobal(t, "freep");
	
	runString(t, `freep(true); freep(5); freep("hi")`);
}

uword freep(MDThread* t, uword numParams)
{
	checkAnyParam(t, 1); // check that we have at least one parameter
	
	switch(type(t, 1))
	{
		case MDValue.Type.Bool:
			Stdout.formatln("Got a bool, its value is {}", getBool(t, 1));
			break;

		case MDValue.Type.Int:
			Stdout.formatln("Got an int, its value is {}", getInt(t, 1));
			break;

		default:
			pushTypeString(t, 1);
			Stdout.formatln("Got something else, its type is {}", getString(t, -1));
			break;
	}
	
	return 0;
}

This dumb function accepts any type for its one (and only) parameter. It prints out the value of bools and ints, and prints the type of other types of values. The pushTypeString function takes an index and pushes a string representation of the type of the value at that index. We then print out the string by using getString (it's safe here to not .dup since we're not storing the string anywhere). No, the stack is not balanced, but it's not really necessary, since we immediately return.

This program outputs the following:

Got a bool, its value is true
Got an int, its value is 5
Got something else, its type is string

OK, so now we can write simple functions and expose them to MiniD. In the next section, we'll be moving on to more ambitious things: calling functions from the native API (instead of just from MiniD) and creating native closures. So go there!