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

Native Classes

Native classes are, for the most part, not anything special. They are just MiniD classes whose methods are defined as native functions. Pretty much any MiniD class you can come up with can be written just as simply as a native class, and will appear to MiniD code the same as a class defined in MiniD code.

There are, however, a couple important extra features that native classes and instances provide which "pure" MiniD classes do not. These features are hidden to MiniD code, and are usually used to deal with implementation details.

First we'll see how to make a simple class. Then we'll see how to create a subclass of it. After that, we'll see how to associate hidden data with class instances, and finally how to put finalizers on class instances to clean up resources.

Along the way, we'll be demonstrating how to perform the two other kinds of calls that were mentioned in the section on function calling - method calls and super calls.

A Simple Class (and method calls)

We'll make the equivalent of the following MiniD class and use it:

class Counter
{
	this(start: int = 0)
		:start = start
		
	function next()
	{
		local ret = :start
		:start++
		return ret
	}
}

First let's start with the "class definition."

	// Our "class definition"
	newClass(t, "Counter");
		newFunction(t, &constructor, "constructor");
		fielda(t, -2, "constructor");

		newFunction(t, &next, "next");
		fielda(t, -2, "next");
	newGlobal(t, "Counter");

The process for creating a class is kind of like that for creating a function - you create the object and then store it somewhere. Classes of course also have members, which we fill after we create it.

First we create a new class with, uh, "newClass". This class has no explicit base class, which means it inherits from Object.

Next we set up the two methods, the "constructor" and "next" functions.

Note that we're using the "fielda" function. This means "field-assign". What we're doing here is the equivalent of "Counter.constructor = <top of stack>". fielda takes the object into which the field should be set and the name of the field. It assigns the top of the stack into that field and pops the value off the stack. Remember, unlike Lua, MiniD separates field access and indexing for all types other than tables, so you can't use "idxa" (index-assign) to put fields into the class.

Once we've added the class members, we use "newGlobal" to put the class into a variable, similar to how we declared a function earlier.

Let's look at the definition of the class methods:

// Takes a single optional parameter, the index to start from.
uword constructor(MDThread* t, uword numParams)
{
	// start ?= 0
	auto start = optIntParam(t, 1, 0);

	// :start = start
	pushInt(t, start);
	fielda(t, 0, "start");

	return 0;
}

Remember, even if you declare a constructor using "this()" in MiniD, it's still just a function named "constructor".

Here we're using the "optIntParam" function from minid.ex to get an optional integer parameter. "optIntParam(t, 1, 0)" means "get any integer at slot 1 and return that, but if there isn't one, return 0". Then we push that value and set it as a field in 'this'. Remember that 'this' is always in slot 0, so "fielda(t, 0, "start")" means "this.start = <top of stack>". Remember that fielda pops the value off the stack. Finally we return 0 to indicate that we're not returning anything.

// Takes no parameters and returns the next value in the counter's sequence.
uword next(MDThread* t, uword numParams)
{
	// local ret = :start
	auto ret = field(t, 0, "start");

	// :start++
	pushInt(t, getInt(t, ret) + 1);
	fielda(t, 0, "start");

	// return ret
	return 1;
}

This is the "next" method, which gets the next number in the sequence. We get a field out of 'this' with the "field" function. Then we add one to it and assign that back into the "start" field (and since we used fielda, the int that was just pushed onto the stack is popped off). Finally we return 1 to indicate that we're returning one value.

Now we have a class, so let's instantiate it. How do you do that? Well, how do you do it in MiniD? By calling it, right? So just call it!

	// Instantiate the class.  How do you instantiate classes?  By calling them, of course!
	auto slot = pushGlobal(t, "Counter");
	pushNull(t);
	pushInt(t, 1); // start from 1
	rawCall(t, slot, 1); // 1 return for the instance itself

	// Now slot points to the instance.

We push the class, make room for 'this', push the params, and call it expecting one return value, which is the instance. Now that we have an instance, let's do something with it:

	// Let's call the next() method on it.

	dup(t, slot);
	pushNull(t); // make room for 'this', but this time it's different..
	methodCall(t, -2, "next", 1); // call "next" with 1 return value
	Stdout.formatln("{}", getInt(t, -1));
	pop(t);

This is how you perform a method call. It's pretty much the same as a normal function call. First you push the object to call the method on, instead of the function (that's what "dup(t, slot)" is doing). Next we make room for 'this'. Then we call the method with "methodCall". methodCall is like rawCall but it has an extra parameter which is the name of the method. methodCall will fill in the 'this' parameter appropriately and call the given method on the object in the given slot. Just like rawCall, it'll pop the object and everything after it off the stack, leaving any return values on top. We print out the return value and then clean it up.

Putting it all together, we get the following:

module example;

import tango.io.Stdout;

import minid.api;

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

	// Our "class definition"
	newClass(t, "Counter");
		newFunction(t, &constructor, "constructor");
		fielda(t, -2, "constructor");

		newFunction(t, &next, "next");
		fielda(t, -2, "next");
	newGlobal(t, "Counter");

	// Instantiate the class.  How do you instantiate classes?  By calling them, of course!
	auto slot = pushGlobal(t, "Counter");
	pushNull(t);
	pushInt(t, 1); // start from 1
	rawCall(t, slot, 1); // 1 return for the instance itself

	// Now slot points to the instance.
	// Let's call the next() method on it.

	dup(t, slot);
	pushNull(t); // make room for 'this', but this time it's different..
	methodCall(t, -2, "next", 1); // call "next" with 1 return value
	Stdout.formatln("{}", getInt(t, -1));
	pop(t);

	// Again!
	dup(t, slot);
	pushNull(t);
	methodCall(t, -2, "next", 1);
	Stdout.formatln("{}", getInt(t, -1));
	pop(t);
}

// Takes a single optional parameter, the index to start from.
uword constructor(MDThread* t, uword numParams)
{
	// start ?= 0
	auto start = optIntParam(t, 1, 0);

	// :start = start
	pushInt(t, start);
	fielda(t, 0, "start");

	return 0;
}

// Takes no parameters and returns the next value in the counter's sequence.
uword next(MDThread* t, uword numParams)
{
	// local ret = :start
	auto ret = field(t, 0, "start");

	// :start++
	pushInt(t, getInt(t, ret) + 1);
	fielda(t, 0, "start");

	// return ret
	return 1;
}

When you run this program, the output is:

$ ./example
1
2
$

How about if we wanted to derive from our Counter class, and create a subclass which does something extra in the "next" method? Maybe something which just printed out some debugging info.

Creating a Subclass

Let's declare our new subclass:

	// The derived class; first we push the base class
	auto base = pushGlobal(t, "Counter");
	newClass(t, base, "DebugCounter"); // now we pass the base class stack index as a parameter to newClass
		newFunction(t, &debugNext, "next");
		fielda(t, -2, "next");
	newGlobal(t, "DebugCounter");
	pop(t); // pop the base class

First we have to get the base class, which is Counter. Then we create the new DebugCounter? class, using Counter as its base. We add the members and put it in a global, like before. Don't forget to pop the base class off the stack.

Let's look at the implementation of debugNext:

uword debugNext(MDThread* t, uword numParams)
{
	// print out some debugging info
	field(t, 0, "start");
	Stdout.formatln("calling next, start is {}", getInt(t, -1));
	pop(t);
	
	// call the base class implementation of "next" and return its values
	pushNull(t);
	pushNull(t);
	return superCall(t, -2, "next", -1); // -1 return values?
}

OK, it gets the "start" field out of 'this' and prints it out as some debugging info. But the interesting part is the call at the end. What we want to do is to call the base class's implementation of "next", so we use a supercall, like in MiniD. We want to do the equivalent of "return super.next()", but in native code.

First we push two nulls. Why are we pushing two nulls? Because that's how it works ;) It's like rawCall and methodCall, except instead of pushing a function or object, we push null. Why? Because a supercall is like a method call, except the object on which a supercall is performed is always 'this', so it would be redundant to make you do "dup(t, 0)" or something. The second null is just like for any other kind of call - room for the context object ('this').

Then we perform the supercall, which looks just like the methodCall function. But what does -1 return values mean? This is a special value which means "all return values." When you specify -1 return values, superCall will return how many values the function that was called returned. This works for rawCall and methodCall as well. Since superCall leaves all the return values of "super.next()" on top of the stack, we just return the number of values super.next() returned. This is the same as doing "return super.next()" in MiniD.

Putting it all together, we get this:

module example;

import tango.io.Stdout;

import minid.api;

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

	newClass(t, "Counter");
		newFunction(t, &constructor, "constructor");
		fielda(t, -2, "constructor");

		newFunction(t, &next, "next");
		fielda(t, -2, "next");
	newGlobal(t, "Counter");

	// The derived class; first we push the base class
	auto base = pushGlobal(t, "Counter");
	newClass(t, base, "DebugCounter"); // now we pass the base class stack index as a parameter to newClass
		newFunction(t, &debugNext, "next");
		fielda(t, -2, "next");
	newGlobal(t, "DebugCounter");
	pop(t); // pop the base class

	auto slot = pushGlobal(t, "DebugCounter");
	pushNull(t);
	pushInt(t, 1);
	rawCall(t, slot, 1);

	dup(t, slot);
	pushNull(t);
	methodCall(t, -2, "next", 1);
	Stdout.formatln("{}", getInt(t, -1));
	pop(t);

	dup(t, slot);
	pushNull(t);
	methodCall(t, -2, "next", 1);
	Stdout.formatln("{}", getInt(t, -1));
	pop(t);
}

uword constructor(MDThread* t, uword numParams)
{
	auto start = optIntParam(t, 1, 0);
	pushInt(t, start);
	fielda(t, 0, "start");
	return 0;
}

uword next(MDThread* t, uword numParams)
{
	auto ret = field(t, 0, "start");
	pushInt(t, getInt(t, ret) + 1);
	fielda(t, 0, "start");
	return 1;
}

uword debugNext(MDThread* t, uword numParams)
{
	// print out some debugging info
	field(t, 0, "start");
	Stdout.formatln("calling next, start is {}", getInt(t, -1));
	pop(t);
	
	// call the base class implementation of "next" and return its values
	pushNull(t);
	pushNull(t);
	return superCall(t, -2, "next", -1); // -1 return values?
}

And the output:

$ ./example
calling next, start is 1
1
calling next, start is 2
2
$

Now you know how to create classes and subclasses, as well as how to perform method and super calls. With these few things, you can turn pretty much any MiniD class into a class defined in native code.

But the really interesting parts about native classes and instances are the features that aren't available to MiniD code. Often when implementing native classes, you'll be doing to in order to provide access to some library or resource that isn't normally accessible from MiniD. When you do this, often you'll want to prevent MiniD code from messing with the class internals, so you don't want to expose the fields that hold that data to the language. Lastly, you'll want to clean up the resources associated with a class instance. These are all features provided by the native API.

Class Allocators, Extra Bytes, and Finalizers

Suppose we want to make a class which represents an array of integers. MiniD has arrays, but they're untyped, and any operations on them have to check the type of each element. Furthermore MiniD's arrays have some space penalties, and they can't be accessed from native code directly. So we want to take advantage of the features of the host language to provide a lower-level integer array object.

There's one problem: where do we store the array? MiniD provides the nativeobj type for storing references to D class objects, but it seems awkward to have to wrap an array in a native object, as well as to be forced to allocate memory on the D heap (which may not be desirable). We somehow need to allocate the array somewhere else while still keeping it associated with an instance of our array class.

This can be solved using extra bytes. Each class instance that you create is allowed to have a number of extra bytes associated with it. These bytes are allocated right along with the class instance. The extra bytes are scanned by neither the D GC nor the MiniD GC, so you should not keep any pointers to garbage-collected objects in them unless you use some other protocol to signal that that pointer is being used. You can, however, store just about anything else there (including pointers to unmanaged memory). When the class instance is garbage collected, the extra bytes are deallocated along with it. So we'll just use the extra bytes to store the data.

Now that we know where to put the array, we encounter another issue: how do we allocate instances of our array class so that they have this extra data? The native API provides a function, newInstance, which creates a new instance of a class. newInstance differs from normal class instantiation in that it just creates an instance of a class and does no more. It doesn't call the constructor. newInstance also takes two extra parameters: the number of extra fields, and the number of extra bytes. (We'll get to the extra fields a little later.)

The problem is that normally you don't call newInstance at all; the interpreter does. When you call a class, it creates the instance and calls the constructor on it, without associating any extra bytes with the instances it creates. This is where class allocators come in.

A class allocator is a special function associated with a class which you can use to replace the default behavior of calling a class. Normally, when you call a class, a new instance of it is created, and if it has any constructor, the constructor is called with any parameters that were passed to the class. Then the new instance is returned. A class allocator is a function which takes any number of parameters and returns an instance. It doesn't matter what you do in that function; as long as you return an instance, the interpreter will be happy.

So what we want to do, when creating our array class, is associate a custom class allocator with it which will create an instance of the class with some extra bytes, and then call the constructor.

A First Attempt

So let's make this class. First let's look at the class definition:

		newClass(t, "IntArray");
			newFunction(t, &allocator, "IntArray.allocator");
			setAllocator(t, -2); // set this function as the class's allocator.

			newFunction(t, &constructor, "IntArray.constructor");
			fielda(t, -2, "constructor");
			
			newFunction(t, &opIndex, "IntArray.opIndex");
			fielda(t, -2, "opIndex");
			
			newFunction(t, &opIndexAssign, "IntArray.opIndexAssign");
			fielda(t, -2, "opIndexAssign");
			
			newFunction(t, &opLength, "IntArray.opLength");
			fielda(t, -2, "opLength");
		newGlobal(t, "IntArray");

Pretty standard, though it's getting a bit bigger than the classes we've dealt with so far. The interesting lines here are where we set the allocator function with the setAllocator API. This sets the class's allocator to the function on top of the stack (or unsets it, if the value on top of the stack is null). Let's see what the allocator looks like:

	// The class's allocator, which creates a new instance and calls its constructor.
	uword allocator(MDThread* t, uword numParams)
	{
		// Since we get all the parameters that were passed to the class, we can
		// do some checking of their validity here.  We're also using the length
		// to determine how many bytes to allocate.
		auto len = checkIntParam(t, 1);

		if(len < 0)
			throwException(t, "length should be at least 0, not {}", len);

		// New instance from 'this' (the class), with 0 extra fields and enough
		// extra bytes to hold our integers.
		// The extra bytes are initialized to 0, so the data will be all 0s.
		newInstance(t, 0, 0, len * int.sizeof);

		// Call the constructor.  Notice that we duplicate the instance and leave
		// one reference to it on the stack, in slot 1, so that we can return it.
		dup(t);
		pushNull(t);
		rotateAll(t, 3);
		methodCall(t, 2, "constructor", 0);

		// Return the instance.
		return 1;
	}

It looks big but there's just a lot of comments in there. As the comments say, we can check the parameters that were passed to the class. So when we write "IntArray(5)", this function gets passed "IntArray" as the 'this' parameter and the integer 5 as its first parameter. We check that the first param is an integer in the valid range, and then we create the class instance. Notice the parameters: the first 0 means "use 'this' as the base class", which is right since 'this' is "IntArray". The second 0 means "0 extra fields", which we haven't talked about yet. And the last parameter is how many extra bytes to allocate in this instance. After we create the instance, we do some stack shuffling, call the constructor, and return the new instance, as we should.

Now we have an instance with some extra data. If we want to get that data, we use the getExtraBytes API. This returns a void[] which you then cast into whatever type you want. We want it to be an int[]. So here's a little helper function that will allow us to get the array associated with the instance in 'this':

	// Function to get the array data associated with this instance.
	int[] getArr(MDThread* t)
	{
		return cast(int[])getExtraBytes(t, 0);
	}

The constructor of this class does nothing, but it's there to make the allocator easier to write (otherwise we'd have to check if the instance had a constructor and only call it if it did). We have three more methods - opIndex, opIndexAssign, and opLength - which do exactly what you might expect. In the interest of space, here's the whole program, and I'll explain after:

module example;

import minid.api;

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

	runString(t, `
	local a = IntArray(5)
	for(i: 0 .. #a)
		a[i] = i + 1

	for(i: 0 .. #a)
		writeln(a[i])`);
}

struct IntArray
{
static:
	// Initialize the class by putting it into the global namespace.
	void init(MDThread* t)
	{
		newClass(t, "IntArray");
			newFunction(t, &allocator, "IntArray.allocator");
			setAllocator(t, -2); // set this function as the class's allocator.

			newFunction(t, &constructor, "IntArray.constructor");
			fielda(t, -2, "constructor");
			
			newFunction(t, &opIndex, "IntArray.opIndex");
			fielda(t, -2, "opIndex");
			
			newFunction(t, &opIndexAssign, "IntArray.opIndexAssign");
			fielda(t, -2, "opIndexAssign");
			
			newFunction(t, &opLength, "IntArray.opLength");
			fielda(t, -2, "opLength");
		newGlobal(t, "IntArray");
	}
	
	// Function to get the array data associated with this instance.
	int[] getArr(MDThread* t)
	{
		return cast(int[])getExtraBytes(t, 0);
	}

	// The class's allocator, which creates a new instance and calls its constructor.
	uword allocator(MDThread* t, uword numParams)
	{
		// Since we get all the parameters that were passed to the class, we can
		// do some checking of their validity here.  We're also using the length
		// to determine how many bytes to allocate.
		auto len = checkIntParam(t, 1);

		if(len < 0)
			throwException(t, "length should be at least 0, not {}", len);

		// New instance from 'this' (the class), with 0 extra fields and enough
		// extra bytes to hold our integers.
		// The extra bytes are initialized to 0, so the data will be all 0s.
		newInstance(t, 0, 0, len * int.sizeof);

		// Call the constructor.  Notice that we duplicate the instance and leave
		// one reference to it on the stack, in slot 1, so that we can return it.
		dup(t);
		pushNull(t);
		rotateAll(t, 3);
		methodCall(t, 2, "constructor", 0);

		// Return the instance.
		return 1;
	}

	// Does nothing, but could be overriden in a derived class.
	uword constructor(MDThread* t, uword numParams)
	{
		checkInstParam(t, 0, "IntArray"); // like "this : instance IntArray"
		return 0;
	}

	// Get a value.
	uword opIndex(MDThread* t, uword numParams)
	{
		checkInstParam(t, 0, "IntArray");
		auto arr = getArr(t);
		
		auto idx = checkIntParam(t, 1);
		
		if(idx < 0 || idx >= arr.length)
			throwException(t, "Invalid index: {}", idx);
			
		pushInt(t, arr[idx]);
		return 1;
	}

	// Set a value.
	uword opIndexAssign(MDThread* t, uword numParams)
	{
		checkInstParam(t, 0, "IntArray");
		auto arr = getArr(t);

		auto idx = checkIntParam(t, 1);

		if(idx < 0 || idx >= arr.length)
			throwException(t, "Invalid index: {}", idx);

		arr[idx] = cast(int)checkIntParam(t, 2);
		return 0;
	}
	
	// Find out how big it is.
	uword opLength(MDThread* t, uword numParams)
	{
		checkInstParam(t, 0, "IntArray");
		pushInt(t, getArr(t).length);
		return 1;
	}
}

I put everything in the IntArray struct to make it a little cleaner. IntArray.init holds the class definition and is called when we want to load the IntArray class into a thread. opIndex, opIndexAssign, and opLength work as you might expect. One thing to note are these lines:

checkInstParam(t, 0, "IntArray");

What this does is makes sure that the 'this' parameter is an instance of IntArray. If you're using extra bytes or values, you should put a check like this on each method of the class. Why? Well what if someone called that method as a free function (with a null 'this'), or using the 'with' statement to pass something bogus as the 'this' parameter? Then your extra bytes or values would be different or invalid and you could end up with a crash. These checks ensure that the given 'this' parameter is legitimate.

What if someone derives from this class? Class allocators are inherited, and can't be set from within MiniD. So if someone makes a "SuperIntArray" which derives from it, the extra bytes will still be allocated whenever SuperIntArray is instantiated. Therefore, these methods only have to check that the given instance derives from IntArray, since any class derived from IntArray will also have valid extra bytes.

If you run the program above, you'll get output like this:

$ ./example
1
2
3
4
5
$ 

as expected.

Our IntArray class works, but it has a shortcoming: once the array is created, we can't resize it. You can't change how many extra bytes are associated with a class instance. What if we want to implement opLengthAssign? Well what we can do instead is keep a pointer to a resizable array in the extra bytes (or, since we're using D, an array reference, which is nicer than a raw pointer). Then, to resize the array, we just have to resize the extra memory we allocated.

But there's an issue that arises. Before, since we used the extra bytes to hold the data, we didn't have to worry about deallocating the array's data - the MiniD GC took care of that, since extra bytes are part of the class instance and are deallocated along with it. Now we have an array that's allocated somewhere else, out of reach of the GC (since, remember, we don't want to keep pointers to GC data in the extra bytes). If we don't deallocate it when the instance is collected, we'll end up with a memory leak. We need some way to deallocate the array data when the instance is collected.

This is what finalizers are for. A finalizer is a function associated with an instance which is called on the instance when it is about to be collected. This is perfect for cleaning up resources associated with the instance.

A Second Attempt

For the most part, the class remains the same. However, we'll be making a couple changes: the allocator will now just allocate enough extra bytes to hold an array reference. The constructor will check the parameters and allocate the memory by calling the new opLengthAssign method. And we'll have a finalizer function which frees the memory associated with the instance.

Here's the new allocator:

	uword allocator(MDThread* t, uword numParams)
	{
		// We're not checking the params this time, since the ctor now does that.
		// Allocate enough extra bytes to hold an int[] reference.
		newInstance(t, 0, 0, (int[]).sizeof);

		dup(t);
		pushNull(t);
		rotateAll(t, 3);
		methodCall(t, 2, "constructor", 0);
		return 1;
	}

Not too different.

The updated constructor:

	uword constructor(MDThread* t, uword numParams)
	{
		checkInstParam(t, 0, "IntArray");
		checkIntParam(t, 1);

		// Just call opLengthAssign on it
		dup(t, 0);
		pushNull(t);
		dup(t, 1);
		methodCall(t, -3, "opLengthAssign", 0);

		return 0;
	}

It checks the parameters and then just calls opLengthAssign to resize itself.

The getArr helper has changed too:

	int[]* getArr(MDThread* t)
	{
		return cast(int[]*)getExtraBytes(t, 0).ptr;
	}

So it now gets a pointer to an array reference (it's a pointer so that we can change it).

opLengthAssign looks like this:

	// Change how big it is.
	uword opLengthAssign(MDThread* t, uword numParams)
	{
		checkInstParam(t, 0, "IntArray");
		auto len = checkIntParam(t, 1);

		if(len < 0)
			throwException(t, "length should be at least 0, not {}", len);

		// Resize the array.  Resizing an empty array is perfectly fine.
		auto arr = getArr(t);
		resizeArray(t, *arr, len);
		
		return 0;
	}

The interesting thing here is the call to resizeArray. This is defined in minid.ex and is templated to allow any type of array. Be sure to only call it with array references which have been allocated with resizeArray, dupArray, or newArray (all are in minid.ex).

Last is the finalizer. This is the function which just deallocates the array memory.

	uword finalizer(MDThread* t, uword numParams)
	{
		// Don't have to check if 'this' is an instance of IntArray, since finalizers
		// are only ever called on instances of IntArray.
		auto arr = getArr(t);
		
		// Freeing an empty array is just fine too.
		freeArray(t, *arr);
		return 0;
	}

freeArray goes along with resizeArray, dupArray, and newArray.

Put together, we get this:

module example;

import minid.api;

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

	IntArray.init(t);

	runString(t, `
	local a = IntArray(5)
	for(i: 0 .. #a)
		a[i] = i + 1

	for(i: 0 .. #a)
		writeln(a[i])

	writeln()
	#a = 3

	for(i: 0 .. #a)
		writeln(a[i])`);
}

struct IntArray
{
static:
	void init(MDThread* t)
	{
		newClass(t, "IntArray");
			newFunction(t, &allocator, "IntArray.allocator");
			setAllocator(t, -2);
			
			// Similar to setAllocator
			newFunction(t, &finalizer, "IntArray.finalizer");
			setFinalizer(t, -2);

			newFunction(t, &constructor, "IntArray.constructor");
			fielda(t, -2, "constructor");

			newFunction(t, &opIndex, "IntArray.opIndex");
			fielda(t, -2, "opIndex");

			newFunction(t, &opIndexAssign, "IntArray.opIndexAssign");
			fielda(t, -2, "opIndexAssign");

			newFunction(t, &opLength, "IntArray.opLength");
			fielda(t, -2, "opLength");
			
			newFunction(t, &opLengthAssign, "IntArray.opLengthAssign");
			fielda(t, -2, "opLengthAssign");
		newGlobal(t, "IntArray");
	}

	int[]* getArr(MDThread* t)
	{
		return cast(int[]*)getExtraBytes(t, 0).ptr;
	}

	uword allocator(MDThread* t, uword numParams)
	{
		// We're not checking the params this time, since the ctor now does that.
		// Allocate enough extra bytes to hold an int[] reference.
		newInstance(t, 0, 0, (int[]).sizeof);

		dup(t);
		pushNull(t);
		rotateAll(t, 3);
		methodCall(t, 2, "constructor", 0);
		return 1;
	}
	
	uword finalizer(MDThread* t, uword numParams)
	{
		// Don't have to check if 'this' is an instance of IntArray, since finalizers
		// are only ever called on instances of IntArray.
		auto arr = getArr(t);
		
		// Freeing an empty array is just fine too.
		freeArray(t, *arr);
		return 0;
	}

	uword constructor(MDThread* t, uword numParams)
	{
		checkInstParam(t, 0, "IntArray");
		checkIntParam(t, 1);

		// Just call opLengthAssign on it
		dup(t, 0);
		pushNull(t);
		dup(t, 1);
		methodCall(t, -3, "opLengthAssign", 0);

		return 0;
	}

	// Get a value.
	uword opIndex(MDThread* t, uword numParams)
	{
		checkInstParam(t, 0, "IntArray");
		auto arr = *getArr(t);

		auto idx = checkIntParam(t, 1);

		if(idx < 0 || idx >= arr.length)
			throwException(t, "Invalid index: {}", idx);

		pushInt(t, arr[idx]);
		return 1;
	}

	// Set a value.
	uword opIndexAssign(MDThread* t, uword numParams)
	{
		checkInstParam(t, 0, "IntArray");
		auto arr = *getArr(t);

		auto idx = checkIntParam(t, 1);

		if(idx < 0 || idx >= arr.length)
			throwException(t, "Invalid index: {}", idx);

		arr[idx] = cast(int)checkIntParam(t, 2);
		return 0;
	}

	// Find out how big it is.
	uword opLength(MDThread* t, uword numParams)
	{
		checkInstParam(t, 0, "IntArray");
		pushInt(t, getArr(t).length);
		return 1;
	}
	
	// Change how big it is.
	uword opLengthAssign(MDThread* t, uword numParams)
	{
		checkInstParam(t, 0, "IntArray");
		auto len = checkIntParam(t, 1);

		if(len < 0)
			throwException(t, "length should be at least 0, not {}", len);

		// Resize the array.  Resizing an empty array is perfectly fine.
		auto arr = getArr(t);
		resizeArray(t, *arr, len);
		
		return 0;
	}
}

When run, this program outputs:

$ ./example
1
2
3
4
5

1
2
3
$

You actually know just about everything you need to know to write something like the Vector class declared in the base library. The Vector class is just bigger.

Extra Fields

There's just one more aspect of native classes which I've hinted at until now. Extra bytes are nice for holding data that is invisible to MiniD or which MiniD just doesn't understand (like array references, or pointers). But sometimes you'll want to have a class instance hold some data which does point onto either the D or the MiniD heap. This is what the extra fields are for.

Extra fields work a bit like upvalues in native function closures. When you create an instance with newInstance, you can tell it to associate a number of extra fields with the instance. These are like a fixed-size array of MiniD values which are associated with the instance. These extra fields are scanned by the MiniD GC and can therefore hold references to MiniD objects, and through the nativeobj type, can hold references to D objects as well.

I won't go into too much detail on these, since this page is already incredibly long and they're actually really easy to use. Just like when using extra bytes, you'll have to set up a custom allocator which allocates some extra fields (by passing a nonzero value as the third parameter to newInstance). Then you can get and set extra fields with getExtraVal and setExtraVal, whose use is identical to getUpval and setUpval. You can also find out how many extra fields there are with numExtraVals. When the instance is collected, you don't have to do anything with these extra fields since they all point to garbage-collected objects which will be collected automatically.

The End

of a very long, but hopefully informative, section.

The next section? is about coroutines.