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

Native Function Calls and Closures

Now you know how to write native functions, register them with the interpreter, and subsequently call them from MiniD code. Next you'll see how to call MiniD functions from the native API. When I say "MiniD functions" I don't mean just functions that were written in MiniD code, but rather any MiniD function value, including native functions. Also, you'll learn how to make native closures.

Calling a Function

I suppose an easy way to show this would be to just try calling the "minmax" function I explained in the last section. Here's the example, modified to call the function using the native API instead of MiniD code.

module example;

import tango.io.Stdout;

import minid.api;

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

	newFunction(t, &minmax, "minmax");
	newGlobal(t, "minmax");

	auto slot = pushGlobal(t, "minmax"); // get the function
	pushNull(t); // make room for 'this'
	pushInt(t, 2); // push all the params
	pushInt(t, 5);
	pushFloat(t, 8.6);
	pushInt(t, -3);
	pushFloat(t, 12.4);

	rawCall(t, slot, 2); // call it!
	Stdout.formatln("min = {}, max = {}", getNum(t, -2), getNum(t, -1));
	pop(t, 2); // clean up
}

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;
}

OK, all the code up to the "pushGlobal" and the "minmax" function itself are the same as before. What's new are the following lines:

	auto slot = pushGlobal(t, "minmax"); // get the function
	pushNull(t); // make room for 'this'
	pushInt(t, 2); // push all the params
	pushInt(t, 5);
	pushFloat(t, 8.6);
	pushInt(t, -3);
	pushFloat(t, 12.4);

	rawCall(t, slot, 2); // call it!
	Stdout.formatln("min = {}, max = {}", getNum(t, -2), getNum(t, -1));
	pop(t, 2); // clean up

Let's go through this step by step.

The way you call a MiniD function in the native API goes something like this: push the function, make room for 'this', push any params, call, retrieve results. The first step - pushing the function - is what the "pushGlobal" function does. This function just looks up the global of the given name and pushes it onto the stack (or throws an error if it doesn't exist). We save the return value, which is the stack index of the pushed function, in 'slot' for later.

The next step is to make room for 'this'. In the native API, you are required to explicitly pass 'this', unlike in MiniD. Fortunately, in most cases, you don't need to do anything more than to push null. When we get to method calls and supercalls, you'll see that you still don't have to do anything. Since you always explicitly push the 'this' value in the native API, it's like using "with" on every call in MiniD. That is, this function call is like "minmax(with null, ...)".

Next is pushing all the parameters. Hopefully by now you've got a hang of pushing numbers onto the stack.

The next step is where the fun happens (uh, I guess): you call the function. For a normal function call, you use the "rawCall" native API function. rawCall takes two parameters: the stack index of the function to call, and how many return values you want. The function stack index parameter is expected to be at least two slots down from the top; that is, there must be at least space for the 'this' parameter after the function. Parameters are expected to be on the stack after 'this'. rawCall calls the function, then pops all the values - including the function - and replaces them with any return values.

We asked for two return values, so after rawCall completes, 'slot' points to the first return value, and slot + 1 to the second. Here we just accessed the return values as relative indices from the top of the stack instead. We print out the numbers and then clean up the stack, leaving it the way it was before we called the function.

The overall result of running this program is:

$ ./example
min = -3.00, max = 12.40
$

Which is the same as before, save for some formatting differences.

This calling procedure is used to call other types of callable objects as well. These include threads (to start or resume them), classes (to instantiate them), or any instance or table which has an opCall metamethod defined.

Something that will probably come up a lot - how do you call a function that's inside a module? Well let's think it through. How do you do it in MiniD? Let's say it's "math.sin". So you write "math.sin(0)". If you break that up, you can see that we're getting the field "sin" from the global "math", then calling that value with "0" as a parameter. If you translate that to native API calls, it'd be something like:

pushGlobal(t, "math");
auto slot = field(t, -1, "sin");
pushNull(t);
pushInt(t, 0);
rawCall(t, slot, 1);
pop(t); // get the result off the top
pop(t); // remove the "math" global from the stack

However this "pushGlobal-field-field-pop" nonsense gets old after a while, so minid.ex provides a couple functions to make this process easier: lookup and lookupCT. lookup lets you pass a dotted name, and it will look up that name, leaving the value on the top of the stack (and none of the other intermediate values). lookupCT does the same, but differs in that it takes the name you're looking up as a compile-time parameter, trading a bit of code size for speed. Here's the same code as above, written using lookup:

auto slot = lookup(t, "math.sin");
pushNull(t);
pushInt(t, 0);
rawCall(t, slot, 1);
pop(t); // get the result off the top

Much nicer, and it extends to more deeply-nested names much more easily. In this case we could also have used "lookupCT!("math.sin")(t)".

This is how you call "normal" functions, but there are two more kinds of function calls - method calls and supercalls. All three kinds behave pretty much the same way, but I'll save the other two kinds for the section on classes, since they're pretty closely related. Let's now look at how you create native closures.

Closures

Remember that in MiniD, you can declare a nested function which accesses its enclosing function's stack frame, and those "upvalues" will still be accessible when the enclosing function returns. Well, we can do something that's kind of similar in the native API. You are able to associate a number of arbitrary values with a function when you create one with "newFunction". Nested functions can't actually access the stack frame of their enclosing functions, but you can still make "closures" which are self-contained functions with some kind of hidden state.

Consider the following MiniD function:

function makeCounter(start: int = 0)
{
	return function()
	{
		local ret = start
		start++
		return ret
	}
}

It returns a function which uses an upvalue to store its state. This can be translated into the following native code:

uword makeCounter(MDThread* t, uword numParams)
{
	static uword counter(MDThread* t, uword numParams)
	{
		getUpval(t, 0);
		pushInt(t, getInt(t, -1) + 1);
		setUpval(t, 0);
		return 1;
	}

	auto start = optIntParam(t, 1, 0);
	pushInt(t, start);
	newFunction(t, &counter, "counter", 1); // 1 upvalue
	return 1;
}

OK, so this function has a nested function (declared "static" so that it won't be a delegate). The nested function uses the "getUpval" and "setUpval" functions. You can imagine each closure as having an array of upvalues which only it can access. "getUpval(t, 0)" means "get the 0th upvalue and push it onto the stack." Then we add one, push that, and use "setUpval(t, 0)" to mean "pop the value off the top of the stack and set it as the 0th upvalue." Then we return the one value.

The makeCounter function itself checks its optional parameter, then pushes it. Notice when we use "newFunction" this time, we pass an extra parameter, 1, to indicate that this function has one upvalue. The upvalues must be on top of the stack and will be popped off and replaced with the function. Then we return the new function closure.

The whole program would look something like this:

module example;

import tango.io.Stdout;

import minid.api;

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

	newFunction(t, &makeCounter, "makeCounter");
	newGlobal(t, "makeCounter");

	runString(t, `
	local c = makeCounter(3)
	writeln(c())
	writeln(c())`);
}

uword makeCounter(MDThread* t, uword numParams)
{
	static uword counter(MDThread* t, uword numParams)
	{
		getUpval(t, 0);
		pushInt(t, getInt(t, -1) + 1);
		setUpval(t, 0);
		return 1;
	}

	auto start = optIntParam(t, 1, 0);
	pushInt(t, start);
	newFunction(t, &counter, "counter", 1); // 1 upvalue
	return 1;
}

We register the "makeCounter" function as usual, and then we test it out in some MiniD code. It works just like we'd expect. When we run the program, we get the following:

$ ./example
3
4
$

You might be wondering if you can use D's version of "upvalues", delegates. The answer is no. The problem is that the context pointer of a delegate can point onto the D heap, and all delegates would then have to be kept in some kind of list visible to the D GC. This is cumbersome and would introduce a performance hit, so for simplicity, all native functions must be static functions. You can, however, use the nativeobj type to have a pointer onto the D heap.

End

In the next section, we'll show how to handle errors with the native API.