rock 0.9.6 is on the loose!

👋 This page was last updated ~12 years ago. Just so you know.

Just 8 days after the last release, rock 0.9.6 is out.

To update, run git pull && make rescue as usual. To install from scratch, clone the repo, cd into it, and run make rescue from there - it'll download the latest bootstrap, compile itself from C, then recompile itself from ooc.

Running rock -V should give you something like this:

rock 0.9.6 codename loki, built on Wed Feb 20 15:09:08 2013

The codename for this release comes from a boss you can encounter in the game The Binding of Isaac, by Edmund McMillen.

This release is mostly a bugfix and internal cleanup one, but we've seen two major features make their appearance: cover templates and versioned .use files.

You can read the full changelog for 0.9.6 on GitHub, or keep reading for a more human-friendly format.

Documentation

Our man in the field, @duckinator is still working on ooc docs, and as of right now, the working copy is accessible at http://docs.ooc-lang.org.

Operator overloads in types

One minor feature in this release is one of our oldest feature requests, issue #33 on GitHub (out of almost 600 now), that hadn't been touched in 3 years!

Previously, you could do something like this:

Vector2: class {
    x, y: Float

    init: func (=x, =y)

    add: func (other: This) -> This {
        new(x + other x, y + other y)
    }
}

operator + (v1, v2: Vector2) -> Vector2 {
    v1 add(v2)
}

That syntax still works, and it's handy: what if you want to add operator overloads to types you haven't defined yourself and you don't want to dig in the code of the library they're defined in? So there's still a use case for that.

However, when you're writing your own types, such declarations have two disadvantages:

  • They break DRY - inside the type you can write This to refer to "the type being declared", but outside from it, you can't.
  • Such operator overloads are only accessible if you explicitly import the module they're defined in. But if you get an object of type Vector2 from a method, and you don't import Vector2 from that module, you can still call methods, but you can't use operators. This has bitten many people with the List<T> type before.

A better alternative is to write this:

Vector2: class {
    x, y: Float

    init: func (=x, =y)

    add: func (other: This) -> This {
        new(x + other x, y + other y)
    }

    operator + (other: This) -> This {
        this add(other)
    }
}

Note that the this add here is overly pedantic - we could've simply written add instead, but I wanted to emphasize that the first parameter didn't disappear, it's just this in this context. The body of the operator overload is just like any other member method.

As you can see, the signature is very similar to add - which isn't too DRY in itself. Perhaps a shortcut syntax will make its appearance in a future version of ooc? Who knows.

Versioned .use files

Most of the changes I do to rock are driven by #1GAM.

Recently, for mobile-friendliness, I've switched from using glew for OpenGL extensions, to using the SDL_opengl.h and SDL_opengles2.h headers from SDL2.

However, for cross-platform purposes, this makes our life a bit hard. Whereas we could simply include glew.h and link with -lglew previously, now we have to do different things per platform:

  • On Windows, Linux and Mac OSX, we want to include SDL_opengl.h, because they don't have OpenGL ES 2 implementations onboard.
  • On the contrary, for mobile platforms such as Android (supported) and iOS (planned), we want to include SDL_opengles2.h.
  • Windows and Linux have different names for the OpenGL library: -lopengl32 and -lGL, respectively.
  • OSX is a whole other can of worms: it doesn't use a regular unixy lib, but a framework instead. Thankfully, .use files support that.

So for a while, I've used what I call the make + OOC_LIBS dirty trick. The trick is to have separate targets in your Makefile, that adjust OOC_LIBS depending on the platform.

Which means I used to have different .use files for different platforms - for sdl2-opengl.use for example. And the Makefile looked a little bit like this:

ROCK=rock
OOCFLAGS=-v

linux:
    $(ROCK) $(OOCFLAGS)

osx:
    OOC_LIBS=$OOC_LIBS/ooc-sdl2/uses/osx:$OOC_LIBS $(ROCK) $(OOCFLAGS)

android:
    # copy our asset folder where it'll get packaged
    cp -rfv assets android/assets/
    OOC_LIBS=$OOC_LIBS/ooc-sdl2/uses/mobile:$OOC_LIBS/deadlogger/uses/mobile:$OOC_LIBS $(ROCK) $(OOCFLAGS)

Since the libs search path was adjusted, rock found the mobile-specific or osx-specific .use files first. However, as you can see, that is hard to read and not very friendly to maintain.

Instead, I implemented a feature that allows you to have version blocks in your .use file. In light of that, here's the latest sdl2-opengl.use:

Name: SDL 2.0 OpenGL support
Description: OpenGL headers + libraries for SDL2
Requires: sdl2

# Desktop platforms
version (linux || windows || (apple && !ios)) {
  Includes: SDL_opengl.h

  version (linux) {
    Libs: -lGL
  }

  version (windows) {
    Libs: -lopengl32
  }

  version (apple) {
    Frameworks: OpenGL, Carbon
  }
}

# Mobile platforms
version (android || ios) {
  Includes: SDL_opengles2.h

  version (android) {
    Libs: -lGLESv2
  }

  version (ios) {
    Frameworks: OpenGLES, QuartzCore
  }
}

And then we can get rid of the Makefile altogether - except for the copying android assets part - I guess rock doesn't know how to do that yet!

Much better.

Cover templates

This is a complex subject, and I'm only going to touch the surface here. I wanted to have much more to show, especially regarding arrays, but that'll have to wait for 0.9.7 because I really want to finish February 1GAM on time. However, here's what I got done.

What we've had since the very early ooc compilers are generics. The most basic form of a generic function would be this:

identity: func <T> (value: T) -> T {
    value
}

This is a generic function with type parameter T, that does.. nothing except returning what you pass to it. When calling identity(42), it'll return an Int. When calling identity("loki"), it'll return a String.

However, when compiled to C, only one version of this function exists, and it looks something like this:

C code
void identity(uint8_t* value, Class T, uint8_t* returnValue) {
    if (returnValue) {
        memcpy(value, T->size, returnValue);
    }
}

// and here's our call:
int a = 42;
int b;
identity(&a, Int_class(), &b);

That's the gist of how ooc generics work. Using memcpy is less than ideal for performance, however, that's how all collections work. Since we can't know the size of the type T at runtime, and since types like Int, Double, etc. are base types, not classes, we can't do any assumptions. Hence, passing parameters, returning values, assigning variables, etc. all rely on calling memcpy.

One possible course of action to make generics faster (ie. not rely on memcpy) is to do generic specialization - which was the subject of my semester's project at EPFL - you can read the slides here.

However, I'm not going to discuss that here. On top of generic functions, you can also have generic classes. For example, ArrayList is a generic class, that looks something like that:

ArrayList: class <T> {
    get: func (index: Int) -> T { /* ... */ }
    set: func (index: Int, value: T) { /* ... */ }
}

Which is why you can instanciate ArrayLists of any type, for instance ArrayList<Int>, or even ArrayList<Loki> if you want.

However the same logic applies here: generic classes exist only in one version in the generated C code, and they rely on memcpy, which has a performance impact.

Now fast-forward to ooc arrays. Currently, you can make ooc arrays of anything, e.g. Int[], or Loki[], and it'll work just fine. However, arrays aren't generic. And they're not even classes? How is that possible? Thanks to a pretty smart, but dirty, combination between an Array.h file shoved in the sdk, along with a corresponding cover in lang/types.

However, that limits us greatly. The only things you can do so far with ooc arrays are the following:

a := [1, 2, 3] // literals
b: String[3] // array of 3 null strings

b[0] = "loki" // direct write
b[0] println() // direct read

// accessing 'data' (C pointer) and 'length' attributes.
someExternFunction(b data, b length)

That's it. No each, no map, no filter, no append, no concatenation operators, nothing. And we rely on some C header somewhere in the SDK, along with tons of magic in rock itself. Heck, the GitHub issue for that is even called 'Make arrays less magical'! Which goes to show that however cool, magic is not always a good thing.

Enter template covers. What I implemented makes it possible to write an array class like this:

MyArray: cover template <T> {
    data: T*
    length: SizeT

    init: func@ ~alloc (=length) {
        data = gc_malloc(T size * length)
    }

    init: func@ ~raw (=data, =length)

    get: func (index: Int) -> T {
        _checkIndex(index)
        data[index]
    }

    set: func@ (index: Int, value: T) {
        _checkIndex(index)
        data[index] = value
    }

    _checkIndex: func (index: Int) {
        if (index < 0 || index >= length) {
            Exception new("Out of bounds array access: %d should be in %d..%d" \
                format(index, 0, length)) throw()
        }
    }
}

That's pretty modest so far, but bear with me for a second. We can use it like this:

// we can set the elements ourselves
arr := MyArray<Int> new(3)
arr set(0, 1)
arr set(1, 2)
arr set(2, 3)

// or initialize from a raw C array
raw := [1, 2, 3] as Int*
arr2 := MyArray<Int> new(raw, 3)

// and then print both!
for (i in 0..arr length) {
    "%d / %d" printfln(arr get(i), arr2 get(i))
}

So far, so good. What about operators. Can we get those?

Sure! Just add a few overloads in the cover:

MyArray: cover template <T> {
    // previous stuff

    operator [] (i: Int) -> T {
        get(i)
    }

    operator@ []= (i: Int, v: T) {
        set(i, v)
    }
}

Now we can replace all those sets and gets with nice brackest:

arr := MyArray<Int> new(3)
arr[0] = 1
arr[1] = 2
arr[2] = 3

for (i in 0..arr length) {
    "%d" printfln(arr[i])
}

What about each? What about map? And what about an append method with a + operator? We can have all those:

MyArray: cover template <T> {
    // previous stuff

    operator + (other: This<T>) -> This<T> {
        append(other)
    }

    append: func (other: This<T>) -> This<T> {
        result := This<T> new(length + other length)

        i := 0
        doAppend := func (v: T) {
            result[i] = v
            i += 1
        }

        each(|v| doAppend(v))
        other each(|v| doAppend(v))
        result
    }

    each: func (f: Func (T)) {
        for (i in 0..length) {
            f(this[i])
        }
    }

    map: func <U> (f: Func (T) -> U) -> MyArray<U> {
        other := MyArray<U> new(length)
        for (i in 0..length) {
            other set(i, f(this[i]))
        }
        other
    }
}

And then we can do stuff like:

// arr is our MyArray<Int> from earlier
arr each(|v|
    "%d" printfln(v)
)

floats := arr map(|v| 0.1 * v)
// floats is now a MyArray<Float>

And there you have it. Fast arrays, pure ooc.

All this has a cost, not in performance, but in code size (which can also impact performance but that's besides the point). For every different T in MyArray<T> when compiling code, a different 'instance' is created, with all T replaced with whatever you used, Int, Loki, etc.

But as a result, it is as fast as if you had written specific code by hand.

Note that covers and classes are different beasts, but all of that is documented on http://docs.ooc-lang.org.

Conclusion

I'm glad to have that release out, so I can focus on gamedev for a bit now. I'd like to remind every fellow gamedev out there that they are welcome on irc.nevargames.com/6667, channel #nevargames.

Until next time, over and out!

Comment on /r/fasterthanlime

(JavaScript is required to see this. Or maybe my stuff broke)

Here's another article just for you:

Proc macro support in rust-analyzer for nightly rustc versions

I don't mean to complain. Doing software engineering for a living is a situation of extreme privilege. But there's something to be said about how alienating it can be at times.

Once, just once, I want to be able to answer someone's "what are you working on?" question with "see that house? it wasn't there last year. I built that".