Writing Applications

Applications are what your code runs in. An application should run in its own namespace and respect the internal namespace of the server. Applications may be written in any language that has C/C++ bindings.

Structure

Applications are responsible for running any internal threads they might need, maintaining any connections to external services, and not bringing the server down when they fail. Applications will receive only two general callbacks from the server during their typical lifetime, and a possible third callback when running unit tests.

Initialization

The first callback to the application will be right after it’s loaded into memory: it will be told to initialize itself. During this time, the application should listen for all events it’s interested in, setup any internal structures, and run any threads necessary to do its job. Should the application fail to initialize, it should inform the server by returning FALSE, indicating it failed.

Note

Initialization is optional, but if you don’t implement it, you can’t really listen for events, and that’s rather pointless.

Exiting

The only other time the application will receive a general callback is when the server is exiting. At this point, the application MUST shut down its internal threads, close any external connections, and free any resources.

Registering Events

Since the server is built entirely around events, it might be useful to know to know how to register them. The call is incredibly simple:

// EV_PREFIX is usually defined as some namespace for your application, like: #define EV_PREFIX "/my-app-prefix"
struct event *myev = evs_add_handler(EV_PREFIX, "/my-event", _myev_handler, _myev_on, _myev_off, FALSE);

This function returns an struct event, which is used to broadcast events to all clients subscribed to the event.

_myev_handler
a function that will be called when the event is triggered from a client; if this is NULL, then it is assumed that no handler is wanted.
_myev_on
called when a client subscribes to an event. If this is NULL, it is assumed that any client will be allowed to subscribe. If it is a function, it may do any of the following:
  1. Return EVS_STATUS_OK, indicating that the client should be subscribed immediately.
  2. Return EVS_STATUS_HANDLED, indicating that no response should be sent back because verification of the event will be processed asynchronously, and a callback will be sent later.
  3. Return EVS_STATUS_ERR, indicating that the client is not allowed to subscribe.

Important

This function MUST execute as fast as possible as it runs in one of the server’s main threads. Any background operations, long processing, socket operations, or anything that could potentially block should be done in another thread. Return EVS_STATUS_HANDLED for these operations, and schedule them to run in another thread.

_myev_off
called when a client unsubscribes from an event. This is just a notification that the client left, and it cannot be stopped.
handle_children
a boolean value indicating if child events should also be handled by this event handler. If TRUE, the following events will all be handled by /my-app-prefix/my-event:
  • /my-app-prefix/my-event
  • /my-app-prefix/my-event/some/child
  • /my-app-prefix/my-event/another/child

It is completely up to the event handler if it wants to handle events with these path segments, and it may reject any event with segments it does not like. Extra event segments go to the handler and callbacks in a variable called ev_extra, or located inside struct evs_on_info as ev_extra.

Note

QuickIO reserves the /qio namespace for itself and its internal events. Applications MAY NEVER create events in this namespace.

Handling Events

When a client sends an event to the server, it is routed to the registered handler. The handler callback looks like:

typedef enum evs_status (*evs_handler_fn)(
        struct client *client,
        const gchar *ev_extra,
        const evs_cb_t client_cb,
        gchar *json);

This function is expected to return a status, as described above, and is given the following:

  1. client: The client that sent the event
  2. ev_extra: Any extra path segments sent from the client
  3. client_cb: Where any callback from the event should be sent
  4. json: A JSON string of data from the client. Check out qev_json for reading it.

Sending Callbacks

So what if you have an event subscription callback that does asynchronous verification of events, and you want to send the proper callbacks? There are a set of callback functions to help you out here.

evs_on_cb()
evs_on_info_copy()
evs_on_info_free()

Check them out.

Creating Server Callbacks

Client callbacks are simple enough to understand, but what about when you want to be able to have a callback on the server? The easiest way to see these is to look at some code:

// Whatever data needs to be passed to callback handler
struct callback_data {
        int id;
        gchar *name;
        float percent;
};

enum status _callback(
        struct client *client,
        const void *data,
        const evs_cb_t client_cb,
        gchar *json)
{
        return CLIENT_GOOD;
}

void _free(void *data)
{
        struct callback_data *d = data;
        g_free(d->name);
        g_slice_free1(sizeof(*d), d);
}

static enum evs_status _handler(
        struct client *client,
        const gchar *ev_extra G_GNUC_UNUSED,
        const evs_cb_t client_cb,
        gchar *json)
{
        struct callback_data *d = g_slice_alloc0(sizeof(*d));
        d->name = g_strdup("test");

        evs_cb_with_cb(client, client_cb, json, _callback, d, _free);

        return EVS_STATS_HANDLED;
}

... snip ...

evs_add_handler(EV_PREFIX, "/event", _hander, evs_no_on, NULL, FALSE);

... snip ...

The event flow will be as follows:

  1. An event comes in and is sent to _handler()
  2. Handler sends a callback to the client, passing it the function to be called, the data to be given to the function, and a free function. The ownership of the data passes to QuickIO, and it will free it as necessary.
  3. The client receives the event and triggers the server callback
  4. The _callback() function is called, and it responds exactly like _handler does
  5. Once _callback() is done, the data is free’d

Note

A server callback might never be called: a client has a limited number of server callbacks it can have registered simultaneously, so if it exceeds the number its allowed, then old callbacks will be culled to make room for the new. Chances are this will never happen, but it is a possibility.

Example

In this example, we (fakely) track the population of the world, sending out an updated population event every time a baby is born.

You’ll probably want to check out the Doxygen API docs just to see what is possible

Code

Enough talk, let’s see what we can do here!

  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
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// This file includes everything you need to write an app for QuickIO
#include "quickio.h"

/**
 * All events in this application are prefixed to /skeleton
 */
#define EV_PREFIX "/skeleton"

/**
 * The structure representing the events in the queue.
 */
struct pending {
	/**
	 * The number of babies born in this event.
	 */
	gint64 babies_born;

	/**
	 * A reference to the client, for the callback
	 */
	struct client *client;

	/**
	 * For if we want to send a callback
	 */
	evs_cb_t client_cb;
};

/**
 * The thread for running all async tasks; will be joined on exit.
 */
static GThread *_thread;

/**
 * Where all events are sent for processing; in this application, it's not
 * really necessary as we're just processing integer increments, but it's
 * a good way to show how to do things asynchronously.
 */
static GAsyncQueue *_events;

/**
 * The handler for population events
 */
struct event *_ev_population;

/**
 * The population of the earth, as is known right now. This value is only
 * ever updated inside the skeleton thread.
 */
static gint64 _population;

/**
 * If you need external configuration for an app, it's already provided for you.
 * Check out quick-event's config documentation for all options.
 */
static struct qev_cfg _cfg[] = {
	{	.name = "population",
		.description = "How many people there are on earth",
		.type = QEV_CFG_INT64,
		.val.i64 = &_population,
		.defval.i64 = 7000000000,
		.validate = NULL,
		.cb = NULL,
		.read_only = TRUE,
	},
};

static enum evs_status _ev_baby_born_handler(
	struct client *client,
	const gchar *ev_extra G_GNUC_UNUSED,
	const evs_cb_t client_cb,
	gchar *json)
{
	struct pending *p;
	gint64 babies_born;
	enum qev_json_status status;

	// This will modify the contents of `json`
	status = qev_json_unpack(json, NULL, "%d", &babies_born);
	if (status != QEV_JSON_OK) {
		evs_err_cb(client, client_cb, CODE_BAD, "could not parse JSON", NULL);
		goto out;
	}

	if (babies_born <= 0) {
		// Should probably give a better error message here
		evs_err_cb(client, client_cb, CODE_BAD, "invalid number of babies born", NULL);
		goto out;
	}

	p = g_slice_alloc(sizeof(*p));
	p->babies_born = babies_born;
	p->client = qev_ref(client);
	p->client_cb = client_cb;

	g_async_queue_push(_events, p);

out:
	return EVS_STATUS_HANDLED;
}

static void* _run(void *nothing G_GNUC_UNUSED)
{
	GString *buff = qev_buffer_get();

	while (TRUE) {
		struct pending *p = g_async_queue_pop(_events);

		// Once we get a NULL, assume we're exiting. The server finishes all
		// events before calling exit, so no more events will ever be in the
		// queue.
		if (p == NULL) {
			break;
		}

		_population += p->babies_born;

		// Send the client a callback asynchronously.
		evs_cb(p->client, p->client_cb, NULL);
		qev_unref(p->client);
		g_slice_free1(sizeof(*p), p);

		// Broadcast the new world population to everyone
		qev_buffer_clear(buff);
		qev_json_pack(buff, "%d", _population);
		evs_broadcast(_ev_population, NULL, buff->str);
	}

	qev_buffer_put(buff);

	return NULL;
}

static gboolean _app_init()
{
	// Read any configuration values set straight into our variables
	qev_cfg_add("skeleton", _cfg, G_N_ELEMENTS(_cfg));

	_events = g_async_queue_new();
	_thread = g_thread_new("skeleton", _run, NULL);

	// Will be automatically configured with the rest of quick-event's
	// configuration loading
	qev_cfg_add("skeleton", _cfg, G_N_ELEMENTS(_cfg));

	// No one may subscribe to this event
	evs_add_handler(EV_PREFIX, "/baby-born",
		_ev_baby_born_handler, evs_no_on, NULL, FALSE);

	// Anyone may subscribe to this event
	_ev_population = evs_add_handler(EV_PREFIX, "/population",
		NULL, NULL, NULL, FALSE);

	return TRUE;
}

static gboolean _app_exit()
{
	g_async_queue_push(_events, NULL);
	g_thread_join(_thread);
	_thread = NULL;

	g_async_queue_unref(_events);
	_events = NULL;

	return TRUE;
}

static gboolean _app_test()
{
	// Used to run test cases on the application
	return TRUE;
}

/**
 * Use this macro so that when the server loads the app, it knows what to call.
 */
QUICKIO_APP(
	_app_init,
	_app_exit);

/**
 * If you implement test cases, you're also going to need this macro.
 */
QUICKIO_APP_TEST(_app_test);

Table Of Contents

Previous topic

Client Architecture

Next topic

QuickIO Binaries

This Page