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:
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!
Here's another article just for you:
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".