Essence » Wiki » Tutorial - Getting Started

Introduction

This tutorial will guide you through the process of writing a program for the OS.

You will need a suitable 64-bit UNIX development environment, such as Arch Linux.

Building the OS

First, download the source.

1
git clone https://[email protected]/nakst/essence.git

Then start the build system.

1
./start.sh

Following on the on-screen instructions to build a cross-compiler. The build system will exit when it's done. It'll take approximately 20 minutes on a decent computer.

Then, making sure Qemu is installed, run the build system again, and try a test build on Qemu.

1
2
./start.sh
t2

If you have problems, complain in #essence on the Handmade Network Discord.

Adding a new program

In the build system, type the command

do mkdir "my_first_program"

(or equivalent.)

Then, in a text editor, create the file my_first_program/my_first_program.manifest.

Its contents should look something like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[program]
name = "My First Program";
shortName = "my_first_program";
systemNotificationCallback = ProcessNotification;

[build]
output = "My First Program.esx";
source = "my_first_program/main.cpp";
installationFolder = "/Programs/My First Program/";
Install;

Now create the file my_first_program/main.cpp with the following text:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#define OS_NO_CSTDLIB
#include <os.h>

#define OS_MANIFEST_DEFINITIONS
#include "../bin/Programs/My First Program/manifest.h"

OSResponse ProcessNotification(OSNotification *notification {
    if (notification->type == OS_MESSAGE_CREATE_INSTANCE) {
        OSPrint("Hello, world!\n");
        return OS_CALLBACK_HANDLED;
    }

    return OS_CALLBACK_NOT_HANDLED;
}

Restart the build system. It will automatically detect your new program.

Run the build system command first-program, and set it to My First Program. Then test the OS using the command t2.

In Qemu, switch to the serial output view (View->serial0; Ctrl+Alt+3). You should see your message, "Hello, world!", at the bottom of the output log.

Creating a window

Before we can create a window, we need to make an instance. Because our program will handle multiple instances in one process, we need to keep track of each instance separately. So let's make an Instance structure.

1
2
3
struct Instance {
    OSObject object, window, label;
};

When we receive the OS_MESSAGE_CREATE_INSTANCE system message, we should allocate an instance, and create the corresponding API object.

1
2
3
4
if (message->type == OS_MESSAGE_CREATE_INSTANCE) {
    Instance *instance = (Instance *) OSHeapAllocate(sizeof(Instance), true);
    instance->object = OSInstanceCreate(instance, message, nullptr);
}

We can now create a window!

In your manifest, first define the window template:

1
2
3
4
[window myFirstWindow]
width = 640;
height = 480;
title = "My First Program";

Then in the source file, create the window, a container grid, and add a label to it.

1
2
3
4
5
instance->window = OSWindowCreate(myFirstWindow, instance->object);
OSObject container = OSGridCreate(1, 1, OS_GRID_STYLE_CONTAINER);
OSWindowSetRootGrid(instance->window, container);
instance->label = OSLabelCreate(OSLiteral("Hello, world!"), false, false);
OSGridAddElement(container, 0, 0, instance->label, OS_FLAGS_DEFAULT);

Your source file should now look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define OS_NO_CSTDLIB
#include <os.h>

#define OS_MANIFEST_DEFINITIONS
#include "../bin/Programs/My First Program/manifest.h"

struct Instance {
    OSObject object, window, label;
};

OSResponse ProcessSystemMessage(OSObject, OSMessage *message) {
    if (message->type == OS_MESSAGE_CREATE_INSTANCE) {
        Instance *instance = (Instance *) OSHeapAllocate(sizeof(Instance), true);
        instance->object = OSInstanceCreate(instance, message, nullptr);

        instance->window = OSWindowCreate(myFirstWindow, instance->object);
        OSObject container = OSGridCreate(1, 1, OS_GRID_STYLE_CONTAINER);
        OSWindowSetRootGrid(instance->window, container);
        instance->label = OSLabelCreate(OSLiteral("Hello, world!"), false, false);
        OSGridAddElement(container, 0, 0, instance->label, OS_FLAGS_DEFAULT);

        return OS_CALLBACK_HANDLED;
    }

    return OS_CALLBACK_NOT_HANDLED;
}

Build the OS (t2), and you should be presented with a window.

The window that should appear.

Making a button

Before we can make a button, we have to define the command template for the operation it will perform. In a user interface, it's common to have multiple controls that invoke the same command (e.g. a menu item and a toolbar button), so the command system helps to coordinate all that.

In your manifest, define the following command template:

1
2
[command myFirstCommand]
label = "Do something";

The command template will be added to the osDefaultCommandGroup. When a new instance of a program created, we need to create this command group, and attach it to our instance object.

1
2
3
Instance *instance = (Instance *) OSHeapAllocate(sizeof(Instance), true);
OSCommand *commandGroup = OSCommandGroupCreate(osDefaultCommandGroup);
instance->object = OSInstanceCreate(instance, message, commandGroup);

The command group is just an array of commands. The name of the command template myFirstCommand can be used as an offset into this array. So we'd write commandGroup + myFirstCommand.

Now, let's increase the size of the container grid so there's room for a button.

1
OSObject container = OSGridCreate(1 /*columns*/, 2 /*rows*/, OS_GRID_STYLE_CONTAINER);

Then we can finally add the button, which will inherit the label of the command's template.

1
OSGridAddElement(container, 0, 1, OSButtonCreate(commandGroup + myFirstCommand, OS_BUTTON_STYLE_NORMAL), OS_FLAGS_DEFAULT);

Your program should now look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#define OS_NO_CSTDLIB
#include <os.h>

#define OS_MANIFEST_DEFINITIONS
#include "../bin/Programs/My First Program/manifest.h"

struct Instance {
    OSObject object, window, label;
};

OSResponse ProcessSystemMessage(OSObject, OSMessage *message) {
    if (message->type == OS_MESSAGE_CREATE_INSTANCE) {
        Instance *instance = (Instance *) OSHeapAllocate(sizeof(Instance), true);
        OSCommand *commandGroup = OSCommandGroupCreate(osDefaultCommandGroup);
        instance->object = OSInstanceCreate(instance, message, commandGroup);

        instance->window = OSWindowCreate(myFirstWindow, instance->object);
        OSObject container = OSGridCreate(1, 2, OS_GRID_STYLE_CONTAINER);
        OSWindowSetRootGrid(instance->window, container);
        instance->label = OSLabelCreate(OSLiteral("Hello, world!"), false, false);
        OSGridAddElement(container, 0, 0, instance->label, OS_FLAGS_DEFAULT);
        OSGridAddElement(container, 0, 1, OSButtonCreate(commandGroup + myFirstCommand, OS_BUTTON_STYLE_NORMAL), OS_FLAGS_DEFAULT);

        return OS_CALLBACK_HANDLED;
    }

    return OS_CALLBACK_NOT_HANDLED;
}

...and the manifest:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[program]
name = "My First Program";
shortName = "my_first_program";
systemMessageCallback = ProcessSystemMessage;

[build]
output = "My First Program.esx";
source = "my_first_program/main.cpp";
installationFolder = "/Programs/My First Program/";
Install;

[window myFirstWindow]
width = 640;
height = 480;
title = "My First Program";

[command myFirstCommand]
label = "Do something";

And when you run it, you'll get a window like this:

A window with a label and button.

Responding to command notifications

When the button is clicked, it will invoke the command to which it is attached - commandGroup[myFirstCommand]- by sending a OS_NOTIFICATION_COMMAND notification to its notification callback.

We can set the notification callback for the entire command group using OSCommandGroupSetNotificationCallback:

1
2
3
OSCommand *commandGroup = OSCommandGroupCreate(osDefaultCommandGroup);
instance->object = OSInstanceCreate(instance, message, commandGroup);
OSCommandGroupSetNotificationCallback(commandGroup, ProcessCommandNotification);

We will need to make the function ProcessCommandNotification.

1
2
3
OSResponse ProcessCommandNotification(OSNotification *notification) {
    return OS_CALLBACK_NOT_HANDLED;
}

OSCommandGroupSetNotificationCallback will make the notification->context field equal to the offset of the command into the group. Also, by passing our instance structure into OSInstanceCreate, notification->instanceContext will be equal to that instance structure.

We can now test for receiving a OS_NOTIFICATION_COMMAND from commandGroup[myFirstCommand]:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
OSResponse ProcessCommandNotification(OSNotification *notification) {
    Instance *instance = (Instance *) notification->instanceContext;

    if (notification->context == myFirstCommand) {
        if (notification->type == OS_NOTIFICATION_COMMAND) {
            // The button (or any control attached to this command) has been clicked.

            return OS_CALLBACK_HANDLED;
        }
    }

    return OS_CALLBACK_NOT_HANDLED;
}

When we get the notification, let's change the label's text. We stored the label in our instance structure.

1
OSControlSetText(instance->label, OSLiteral("The button was clicked!\n"), OS_RESIZE_MODE_EXACT);

Using this setup, if the user opens our program multiple times, the button in each window should modify the correct label.

Your program should now look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#define OS_NO_CSTDLIB
#include <os.h>

#define OS_MANIFEST_DEFINITIONS
#include "../bin/Programs/My First Program/manifest.h"

struct Instance {
    OSObject object, window, label;
};

OSResponse ProcessCommandNotification(OSNotification *notification) {
    Instance *instance = (Instance *) notification->instanceContext;

    if (notification->context == myFirstCommand) {
        if (notification->type == OS_NOTIFICATION_COMMAND) {
            OSControlSetText(instance->label, OSLiteral("The button was clicked!\n"), OS_RESIZE_MODE_EXACT);

            return OS_CALLBACK_HANDLED;
        }
    }

    return OS_CALLBACK_NOT_HANDLED;
}

OSResponse ProcessSystemMessage(OSObject, OSMessage *message) {
    if (message->type == OS_MESSAGE_CREATE_INSTANCE) {
        Instance *instance = (Instance *) OSHeapAllocate(sizeof(Instance), true);
        OSCommand *commandGroup = OSCommandGroupCreate(osDefaultCommandGroup);
        instance->object = OSInstanceCreate(instance, message, commandGroup);
        OSCommandGroupSetNotificationCallback(commandGroup, ProcessCommandNotification);

        instance->window = OSWindowCreate(myFirstWindow, instance->object);
        OSObject container = OSGridCreate(1, 2, OS_GRID_STYLE_CONTAINER);
        OSWindowSetRootGrid(instance->window, container);
        instance->label = OSLabelCreate(OSLiteral("Hello, world!"), false, false);
        OSGridAddElement(container, 0, 0, instance->label, OS_FLAGS_DEFAULT);
        OSGridAddElement(container, 0, 1, OSButtonCreate(commandGroup + myFirstCommand, OS_BUTTON_STYLE_NORMAL), OS_FLAGS_DEFAULT);

        return OS_CALLBACK_HANDLED;
    }

    return OS_CALLBACK_NOT_HANDLED;
}

Does your program work? :)

A picture of the program working.

Closing the window

Note: window closing and instance destruction isn't fully implemented yet.

When the window is closed, a OS_NOTIFICATION_WINDOW_CLOSE notification is sent to the window. When this happens, we can destroy the instance object we created, and deallocate the Instance structure.

First, let's set the window's notification callback.

1
OSElementSetNotificationCallback(instance->window, OS_MAKE_NOTIFICATION_CALLBACK(ProcessWindowNotification, nullptr));

Then in the notification callback...

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
OSResponse ProcessWindowNotification(OSNotification *notification) {
    Instance *instance = (Instance *) notification->instanceContext;

    if (notification->type == OS_NOTIFICATION_WINDOW_CLOSE) {
        OSCommandGroupDestroy(instance->commandGroup);
        OSInstanceDestroy(notification->instance);
        OSHeapFree(instance);
        return OS_CALLBACK_HANDLED;
    }

    return OS_CALLBACK_NOT_HANDLED;
}

We the destroy the command group (don't forget to add this to your Instance structure!), destroy the instance, then deallocate the instance.

So our final program should look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#define OS_NO_CSTDLIB
#include <os.h>

#define OS_MANIFEST_DEFINITIONS
#include "../bin/Programs/My First Program/manifest.h"

struct Instance {
    OSObject object, window, label;
    OSCommand *commandGroup;
};

OSResponse ProcessCommandNotification(OSNotification *notification) {
    Instance *instance = (Instance *) notification->instanceContext;

    if (notification->context == myFirstCommand) {
        if (notification->type == OS_NOTIFICATION_COMMAND) {
            OSControlSetText(instance->label, OSLiteral("The button was clicked!\n"), OS_RESIZE_MODE_EXACT);

            return OS_CALLBACK_HANDLED;
        }
    }

    return OS_CALLBACK_NOT_HANDLED;
}

OSResponse ProcessWindowNotification(OSNotification *notification) {
    Instance *instance = (Instance *) notification->instanceContext;

    if (notification->type == OS_NOTIFICATION_WINDOW_CLOSE) {
        OSCommandGroupDestroy(instance->commandGroup);
        OSInstanceDestroy(notification->instance);
        OSHeapFree(instance);
        return OS_CALLBACK_HANDLED;
    }

    return OS_CALLBACK_NOT_HANDLED;
}

OSResponse ProcessSystemMessage(OSObject, OSMessage *message) {
    if (message->type == OS_MESSAGE_CREATE_INSTANCE) {
        Instance *instance = (Instance *) OSHeapAllocate(sizeof(Instance), true);
        OSCommand *commandGroup = instance->commandGroup = OSCommandGroupCreate(osDefaultCommandGroup);
        instance->object = OSInstanceCreate(instance, message, commandGroup);
        OSCommandGroupSetNotificationCallback(commandGroup, ProcessCommandNotification);

        instance->window = OSWindowCreate(myFirstWindow, instance->object);
        OSElementSetNotificationCallback(instance->window, OS_MAKE_NOTIFICATION_CALLBACK(ProcessWindowNotification, nullptr));
        OSObject container = OSGridCreate(1, 2, OS_GRID_STYLE_CONTAINER);
        OSWindowSetRootGrid(instance->window, container);
        instance->label = OSLabelCreate(OSLiteral("Hello, world!"), false, false);
        OSGridAddElement(container, 0, 0, instance->label, OS_FLAGS_DEFAULT);
        OSGridAddElement(container, 0, 1, OSButtonCreate(commandGroup + myFirstCommand, OS_BUTTON_STYLE_NORMAL), OS_FLAGS_DEFAULT);

        return OS_CALLBACK_HANDLED;
    }

    return OS_CALLBACK_NOT_HANDLED;
}

Linking with the C standard library

To link with the C standard library, add:

1
additionalLinkerFlags = "-lc";

...to your manifest, below the [build] line.