Getting Started Guide
  1. chevron_right  Installing Corto

    How to get Corto on your machine.

  2. chevron_right  The Basics

    Getting to know the corto framework.

  3. subject  A new Project
  4. subject  Using Bake
  5. subject  Debugging Corto
  6. subject  Using Packages
  7. subject  Running Tests
  8. subject  Writing Documentation
  9. chevron_right  The In-Memory Store

    The beating heart of corto.

  10. subject  Defining a Model
  11. subject  Creating Objects
  12. subject  Observing Events
  13. subject  Updating Objects
  14. subject  Serializing Objects
  15. subject  Forward Declarations
  16. subject  Lifecycle Hooks
  17. subject  Adding Methods
  18. subject  The Corto Web UI
  19. subject  Looking up Objects
  20. subject  Reflection
  21. subject  Persisting to disk
  22. subject  More Complex Models
  23. subject  Hierarchical Models
  24. subject  Models revisited
  25. subject  Dynamic Applications
  26. chevron_right  The Virtual Store

    TBD

  27. subject  Querying for Data
  28. subject  Subscribing for Data
  29. subject  Mounting Data
  30. subject  Receiving Events
  31. subject  Contenttypes
  32. subject  Subscriptions
  33. subject  Mounting Hierarchies
  34. subject  Batching
  35. chevron_right  Corto Extensions

    TBD

  36. subject  Serialization Formats
  37. subject  File Extensions
  38. subject  Code Generators
  39. subject  Tool Plugins
  40. chevron_right  The Corto Web Framework

    TBD

  41. subject  HTTP Servers
  42. subject  HTTP Services
  43. subject  The HTTP Router
  44. subject  The REST Protocol
  45. subject  The Websocket Protocol

Getting Started Guide


Installing Corto

How to get Corto on your machine.

To install a development version of corto, run this from a terminal:

curl https://corto.io/install-dev-src | sh
The installer will create a .corto directory in your $HOME which will contain all corto sources, binaries, header files and more. The installer will install two convenience scripts (corto and bake) to your /usr/local directory so you can run corto from anywhere!

Currently only Linux and OSX are supported.

Installing the scripts to `/usr/local` may ask for your password.
Development builds have lots of code for early detection of errors. All this
checking takes a lot of time, so if you want something that is fast, install a
release version by simply replacing "dev" with "release".

The Basics

Getting to know the corto framework.

A new Project


To create a new corto C project, run this command:

corto create Lemonade
You'll notice that a new directory called Lemonade has been created in the directory where you executed the command. Let's take a look at what's inside:
Lemonade
  |- bin
  |- include
  |- src
  |- test
  +- project.json
The bin/<platform-config> directory already contains a binary that was built when we created the project. Run it with this command:
Lemonade/bin/x64-linux-debug/Lemonade

The platform and operating system may be different on your system.

The program is still empty, so that didn't do much. Let's change this by going to the src/Lemonade.c file, and add this code to the cortomain function:

corto_log("#[magenta]Hello corto!\n");

To see the result of this, we need to build the project. Move to the next section to see how.

Using Bake


We built a new buildsystem called bake to build corto projects. We built bake because we felt that dependency management is too much of a hassle with existing buildtools. Here we will give a brief overview of basic bake features so you know how to build and specify dependencies between corto projects:

To bake the modified Lemonade project from the previous section, run:

bake Lemonade
If all is well, you should see something like this:

baking in Lemonade/
√ build application 'Lemonade' in 'Lemonade'
done!

Now run the program again with the same command we did earlier:

Lemonade/bin/x64-linux-debug/Lemonade
You should now see this (in magenta characters):

Hello Corto!

If you want to commit this project, you'll want to clean it first so that you're not checking in the binaries and object files. To clean the project, do:

bake clean Lemonade
Done! You can replace clean with other bake commands, like build (default) and rebake. By default, bake builds in development mode. You can also build in release mode (smaller, faster executables) by doing:

bake Lemonade --cfg release

For a complete list of options, type bake --help.

The bake output is succinct and is useful when building lots of projects, as any warnings in your codebase will pop out. Sometimes though you need more information. In that case, you can enable tracing by doing:

bake Lemonade --trace
That should give you a good idea of the different steps in the build process. You can get even more information by specifying --debug, or less by specifying --ok.

Let's take a look the project.json file. This is the closest thing to a makefile, and contains all information bake needs to build your project:

{
    "id": "Lemonade",
    "type": "application",
    "value": {
        "description": "Making the world a better place",
        "author": "Arthur Dent",
        "version": "1.0.0",
        "language": "c"
    }
}

A project.json file always specifies a project id and a project type. The value member contains project configuration.

There's much more to bake than we'll cover here, from managing environments and configurations (check ~/.bake/config.json) to dependency management to code generation. We're currently writing the docs for bake so bear with us, but we'll link to them here when they're ready!

Debugging Corto


The unfortunate reality of writing code is that you're going to make errors, and as much as we'd like to pretend it won't happen, you are likely going to run into some pretty nasty problems with corto too. Whenever you find yourself in a situation like that, you'll remember this section, and march into the unknown with confidence.

Corto has an exception handling framework that is useful to learn as it will help you a lot when debugging applications. Let's take a moment to review the basic functions you'll be seeing a lot before trying them out:

Function Description
corto_throw Throw an exception or add information to an existing exception
corto_raise Raise a thrown exception
corto_catch Catch a thrown exception

Let's look at a code example to see how they are used in practice:

int16_t divide (int num, int by, int *out)
{
    if (by == 0) {
        corto_throw("cannot divide %d by zero!", num); // Throw exception
        goto error;
    }

    *out = num / by;

    return 0;
error:
    return -1;
}

int cortomain(int argc, char *argv[])
{
    int out;
    if (divide(10, 0, &out) != 0) {
        corto_throw(NULL); // Add stack frame to exception without error message
        goto error;
    }

    return 0;
error:
    corto_raise();
    return -1;
}

Copy this code into our application and see what it does. The output should look something like this:

exception src/Lemonade.c:5 (divide): cannot divide 10 by zero!
     from src/Lemonade.c:19 (cortomain)
     proc Lemonade [15501]

As you can see corto correctly tracks all information about the exception, and only displays it when it is raised. Now try to remove the corto_raise call, and rerun the code again:

exception src/Lemonade.c:5 (divide): cannot divide 10 by zero!
     from src/Lemonade.c:19 (cortomain)
     proc Lemonade [15501]

The exception is still raised! How? Well, when corto exits, it will perform a last check to ensure no exceptions occurred, and it will automatically raise any uncatched exceptions. To catch the exception, add corto_catch() where we did corto_raise before, and rerun the code:


Now no exception is shown on the command line, as we properly handled the exception. Corto employs a lot of trickery to raise exception that have not been catched at runtime as early as possible. You'll want to always check return codes and catch/raise exceptions appropriately, otherwise corto will do it for you.

Sometimes you need to stop an application when an exception occurs in order to debug it. This can be done by adding this line to your code:

corto_log_setExceptionAction(CORTO_LOG_ON_EXCEPTION_ABORT);

That way, whenever an exception occurs, corto will abort(), which is a signal you can catch with tools like gdb or valgrind.

Pro tip: Add a command-line argument to your program that allows turning this
setting on or off.

If you try to run the corto process in gdb or valgrind and you get errors like this: "/usr/local/bin/corto": not in executable format: File format not recognized or this: /bin/sh: 0: Can't open corto then that is because you are trying to load the corto script from /usr/local in the tools. This can simply be resolved by exporting the bake environment to your shell, by doing:

export `bake getenv`

That will set PATH and LD_LIBRARY_PATH accordingly, so the shell can find the actual corto executable. Note that any changes you now make to your bake configuration file afterwards will have to be re-exported by running this command again.

There are many more logging functions. In particular, check out corto_log_push and corto_log_pop. These two functions add nesting to your traces. For example, this code:

corto_info("dividing %d by %d", 10, 2);
corto_log_push("divide");
corto_info("result = %d", 5);
corto_log_pop();

Has the following output:

dividing 10 by 2
divide
|  result = 5
+  

Sometimes you need to know more about what's going on beneath the covers of corto's public APIs. Fortunately, corto comes with lots of tracing built in, and this can be enabled by setting the log verbosity:

corto_log_verbositySet(CORTO_DEBUG); // set to lowest level

That will output a lot of information to the console. If CORTO_DEBUG is a bit too much, you can alternatively try CORTO_TRACE or CORTO_OK.

Sometimes you'll find yourself in a position where you need to debug something and you can't (or don't want to) access the code. In that case, you can also set an environment variable to control verbosity:

export CORTO_VERBOSITY=DEBUG

To reset to default verbosity, do:

export CORTO_VERBOSITY=

If you want to use the corto logging API to add debugging or tracing information to your application, you can easily do that by using one of the following functions:

corto_debug("setting this variable"); // information for developer debugging
corto_trace("the app is doing this"); // tracing reveals what the app is doing
corto_ok("this ended pretty well");   // a task completed successfully
corto_info("Hello World");            // General information (default loglevel)
corto_warning("watch out!");          // display warnings
corto_error("this is not right");     // log error directly to console
corto_critical("boom!");              // display error, stacktrace and abort
corto_assert(result == 0, "boom!");   // check condition, abort if failse.

Using Packages


Our current project is an application, which means the resulting binary is an executable file. Sometimes we'll want to use libraries, so we can share functionality between our projects. In corto we can do this by creating a package. To create a package, do:

corto create package weather

We now have a new folder called weather. When we look inside, it looks similar to our application, save for a file called model.cx.

You might also notice that when you look in project.json, the type now says package instead of application. When you look in the bin folder, you'll see a libweather.so.

Add this line to the model.cx file:

bool is_sunny()

and then do:

bake weather

You'll likely see something like:

√ build package 'weather/c' in 'c'
src/weather.c: In function ‘_weather_is_sunny’:
src/weather.c:14:1: warning: control reaches end of non-void function [-Wreturn-type]
 }
 ^
√ build package 'weather' in 'weather'
done!

What happened? If you open src/weather.c you'll see that we have a new function! It look like this:

bool weather_is_sunny(void)
{
    /* Insert implementation */
}

As you might've guessed, the model.cx file contains the definitions for our package, from which code is generated. We'll explore the different things we can do with models later. For now, let's assume this package is always run in Southern California:

return true;

Now hit bake weather again to build the package.

Our lemonade stand could use weather information to determine if it should be open today. Open up the project.json file of the Lemonade project, and change it so it looks like this:

{
    "id": "Lemonade",
    "type": "application",
    "value": {
        "description": "Making the world a better place",
        "author": "Arthur Dent",
        "version": "1.0.0",
        "language": "c",
        "use": ["weather"]
    }
}

Note that we added an extra field use which refers to the weather package. Now go into our cortomain function (you can throw away any old code you may have) and enter this code:

if (weather_is_sunny()) {
  corto_info("I'm gonna be open today!");
} else {
  corto_info("Looks like I'll have be closing soon ...");
}

Now rebuild the lemonade stand, and run the application. You should see:

I'm gonna be open today!

Under the hood, packages are normal native shared libraries (.so files in Linux/Darwin) that are built to a special location on the filesystem. That way, corto always knows how to find the corresponding library ($BAKE_TARGET/lib/corto/2.0/weather/libweather.so) from a logical package identifier (weather). Also note how corto automatically generates the correct #include statements for the packages that you include, so all you need to do when you want to include a package, is adding its identifier to the use array in the project.json file of your project.

Packages are extensively used for extending corto capabilities. These packages are called drivers, and can be found in the /driver scope in corto. Drivers are essentially packages that conform to a predefined interface to, for example, add support for a serialization format (/driver/fmt) or file extension (/driver/ext). More on drivers and their interfaces later.

Running Tests


Corto ships with a simple but powerful test framework that leverages corto code generation from model files to quickly create testsuites. By default, every project is created with a default example testsuite in the test directory. Let's look at the test directory in the weather project created in the previous section. You'll find these files:

test
  |- bin
  |- include
  |- src
  |- model.cx
  +- project.json

As you can see, this looks a lot like the structure of the weather package. The tests for weather are located in another regular corto project. When inspecting the model.cx file, we find these contents:

in package test

test/Suite MySuite:/
    void testSomething()

What this means is that we have a package called test. In this package, we find a test suite called MySuite, which contains a testcase testSomething.

Let's also review the project.json, as this will reveal more about how this project is structured:

{
    "id": "test",
    "type": "package",
    "value": {
        "language": "c",
        "public": false,
        "use": [
            "corto/test",
            "weather"
        ]
    }
}

A couple of things are different from the project.json of the weather package. The false value for the public member means that this package will not be built to the package repository ($BAKE_TARGET). This means that it cannot be used as a dependency for other projects, and that it won't pollute our package repository.

In the use member, we find a dependency on the weather project, which is the package we want to test, and a dependency on the corto/test project, which is the package that contains the corto test framework.

We can run the testsuite by running this command from the project directory:

corto test

This will yield output that looks similar to:

EMPTY: MySuite/testSomething: missing implementation
 Use this command to debug the testcase:
  corto -l /home/vagrant/cortoproject/guide/weather/test/bin/x64-linux-debug/libtest.so MySuite/testSomething

weather: PASS:0, FAIL:0, EMPTY:1 

Because we did not test anything in the testSomething testcase, corto flags this testcase correctly as empty.

Let's change the model.cx file to something a bit more appropriately named for our package. Replace the old contents with:

in package test

test/Suite WeatherSuite:/
    void testIsSunny()

Now run this command again:

corto test

You should now see a similar message, but instead of MySuite/testSomething you should see WeatherSuite/testIsSunny.

Notice that the `MySuite.c` file has been renamed to `MySuite.c.old`. You
can safely delete this file.

Let's go into the WeatherSuite.c file. Here we find the following function:

void test_WeatherSuite_testIsSunny(
    test_WeatherSuite this)
{
    /* Insert implementation */
}

This is the body of the testIsSunny testcase. In here we can put any code that verifies if the is_sunny function works correctly. Add the following implementation:

test_assert(weather_is_sunny() == true);

If all is well, running corto test should now show:

weather: PASS:1, FAIL:0, EMPTY:0

Try changing the assert to == false to see what happens when the testcase fails. After running corto test again, you should see:

FAIL: WeatherSuite/testIsSunny:8: assert(weather_is_sunny() == false)           
 Use this command to debug the testcase:
  corto -l /home/vagrant/cortoproject/guide/weather/test/bin/x64-linux-debug/libtest.so WeatherSuite/testIsSunny

weather: PASS:0, FAIL:1, EMPTY:0
The testframework shows the linenumber of the assert that failed. For ease of 
debugging, put `test_assert` in the bodies of testcases instead of encapsulating 
them in other "utility" functions.

You can use the corto -l command to run only the failing testcase instead of all the test suites.

Instead of test_assert you can also use these functions, which can show more information about why a test fails:

test_assertint(int64_t expected, int64_t actual);
test_assertflt(double expected, double actual);
test_assertstr(char *expected, char *actual);

That should get you started with testing corto projects! Try adding more testcases and see how code is automatically regenerated when a new testcase is added, while the implementation of the existing testcase is preserved.

Writing Documentation


To make it easy to document your projects, corto comes with a simple framework that generates HTML from markdown documents.

To add documentation to your project is simple. Lets start by adding a markdown document to our weather package. First, add a doc directory to our project:

mkdir doc

The doc directory is where the documentation framework will look for markdown documents. In this folder, create a document called weather.md.

Your markdown documents can have any name, as long as they end in .md

Now lets add some content to the markdown document. Add the following lines:

# Weather package documentation
This package contains the documentation for the weather package.

## API
All of the API functions of the weather package.

### is_sunny
Tells the application if it is currently sunny.

#### Signature
\`\`\`
bool is_sunny()
\`\`\`

#### Returns
`true` if it is sunny, `false` if it isn't.

Now run the following command from the project directory:

corto doc

You should now have an HTML directory in your project with a file called weather.html which contains the generated HTML from your markdown file. If you doubleclick on it, you will see a webpage that looks similar to this guide.

The documentation framework, in addition to plain markdown, offers a few features that allow you to annotate your documentation with notes, warnings, code examples and other verbatim snippets. All of these features can be enabled by adding a keyword after the first three backticks of a codeblock. Here is an example of what the different keywords do:

This is three backticks followed by nothing
void foo() { // Three backticks followed by a programming language ('c') }
Three backticks followed by 'note'
Three backticks followed by 'warning'
Three backticks followed by 'demo'

Using CORTO_DOCROOT

The documentation framework contains a feature that allows all of your documentation to be generated to a single location. If you specify a path in an environment variable called CORTO_DOCROOT, all documentation will be generated to this path. For example, when doing:

export CORTO_DOCROOT=~/docs

all HTML will be subsequently generated to ~/docs.

Protip: Add CORTO_DOCROOT to the environment in your ~/.bake/config.json file so
you don't have to manually set the environment variable every time.

Using Doxygen (beta)

The corto documentation framework can use doxygen to automatically extract doxygen comments from source files. This feature is currently in beta, so you might encounter some issues as you're using it.

Before trying out this feature, make sure that doxygen is installed and that the
doxygen command is available from the current shell.

First, for every header that you want to generate documentation for, you need to add the following header:

/** @file
 * @section weather The Weather API
 * @brief API for checking the current weather.
 */

The section id (weather) needs to be unique across your project. It is recommended to use the header name for this, so if you have a header called weather.h, the section name is weather.

You can then add doxygen comments to your functions as you would normally do, like:

/** Check if it is sunny in a specified location.
 * This function checks whether it is sunny in a specific location.
 *
 * @param lattitude Lattitude of the location
 * @param longitude Longitude of the location
 * @return `true` if sunny, `false` if not.
 * @see is_cloudy
 */
bool is_sunny(uint64_t lattitude, uint64_t longitude);
The empty line between description and parameters is necessary. Without that line
the doxygen parser will think that the parameters are part of the description.

For the annotated source corto will invoke doxygen and request XML output. We now need to add a little bit of information to your project.json that tells corto which doxygen output it should include in which document. Add this to the value member in project.json:

    "doxygen": [{
        "title": "Lemonadestand project",
        "chapters": [ "weather" ]
    }]

The title will be used as the tier-1 markdown header. The chapters list includes all the include files (without extension) for which to generate the documentation. Now run this command again:

corto doc

You should see some output from doxygen. If you now check your HTML folder, you should see a file called Lemonadestand_project.html, which contains the documentation from your header file.

The In-Memory Store

The beating heart of corto.

The in-memory object store is to your application what a filesystem is to an operating system. It stores your application state, model information (reflection), configuration data and a whole lot more. Just like a filesystem, the object store is organized hierarchically.

Applications built with corto are incredibly versatile. Their state can be easily persisted, shared or visualized with any of the dozens of corto-aware packages. The object store is the key enabler for this. Once data is in the store, it is discoverable by anything connected to the store.

Data is represented as objects. Objects are organized in a hierarchy, where any object can act as a container for other objects. When object A is a child of object B, we say that A is in the scope of B.

Objects have a type that cannot change for as long as the object is alive. The type information is also stored in the object store, and can be used to dynamically inspect the values of objects. Object types are typically defined in a model (like in the model.cx file we saw earlier). Let's start by looking at how models are defined.

Defining a Model


A model describes what the state of your application looks like. A model is a cross-over between something like a database schema, and a programming language class definition. This may sound confusing, so let's look at an example model:

in application Lemonade

class Shop:/
  inventory: int32
  balance: int32
This model is described in the native corto modeling language. Models can be
described in any language, as long as corto recognizes the file extension.

This class describes a simple datatype that represents a store. We have two members, inventory and balance that are both of type int32. As you can see we have not yet added any methods (we will later on). We do this to demonstrate that in corto, updates on data are what drives application logic, not methods. Lets see how this works in practice.

If you had not yet created the Lemonade project, run this command:

corto create Lemonade

Now create a new file model.cx in the project directory of the Lemonade project. Paste the model that contains the Shop class to the model.cx file. Then, to build the project, run bake from the project directory:

bake

If all went well, corto generated the types and function that enable you to use objects of type Store in your application. The next section shows you how.

Creating Objects


Now that we have a model, we can start populating the object store with objects that instantiate the model. Let's look at a simple code example:

Lemonade_Shop MyShop = corto_create(root_o, "data/MyShop", Lemonade_Shop_o);

This line of code created an object in the store of the Lemonade/Shop type. There are a number of things that are interesting about this statement. First, we are using a native C type called Lemonade_Shop for the MyStore variable.

Secondly, this object is created in the root of the store, in the scope of data. Putting application state in the data scope is good practice, as it is guaranteed that it won't be used at some point for corto builtin mechanisms.

Thirdly, we provide a Lemonade_Shop_o variable to the function, which is the type object. This variable contains a pointer to the Lemonade/Shop type in the object store.

Now that we have an object of type Lemonade/Shop in the store, lets see what we can do with it!

Observing Events


A key feature of objects in the store is that they are observable. This means that whenever an object changes, anyone observing the object will be notified, and can execute some application logic.

Let's create an observer that listens for DEFINE events in the data scope. A DEFINE event is generated whenever an object is created. Add the following code to Lemonade.c:

void cb(corto_observerEvent *event)
{
  corto_info("event received for %s!", corto_idof(event->data));
}

int cortomain(int argc, char *argv[])
{
  // Create Lemonade/Shop object
  Lemonade_Shop MyShop = corto_create(root_o, "data/MyShop", Lemonade_Shop_o);

  // Create observer in 'data'
  corto_object data = corto_lookup(root_o, "data");
  corto_observer observer = 
    corto_observe(CORTO_DEFINE|CORTO_ON_SCOPE, data).callback(cb);

  return 0;
}

First we use the corto_lookup function to lookup the data object. We need a reference to the data object, so that on the next line we can provide it to our observer in the corto_observe call.

Notice how we create the observer after we create the object? What do you expect to see? Let's run the above code and see what happens. Do (from the project directory):

bake
Lemonade/bin/x64-linux-debug/Lemonade

You should see this output:

event received for MyShop!

How did that happen? The event occurred before we created the observer, and yet we still got the event! This is because corto aligns DEFINE events when an observer is created, so that the observer has a full picture of the store. This makes corto operations order independent, an important property of building robust data-driven applications.

Try printing the address of the object with printf("%p\n", event->data); in the
observer callback function, and also in the cortomain function with printf("%p\n", MyShop);
You will see that they are the same address!

Updating Objects


Let's update the value of our MyShop object by adding the following code to the cortomain function, after creating the observer:

corto_update_begin(MyShop);
MyShop->inventory = 100;
MyShop->balance = 50;
corto_update_end(MyShop);

Because an UPDATE event is different from a DEFINE event, we must add the CORTO_UPDATE constant to the corto_observe call so it will also trigger on updates. Change the call to:

  corto_observer observer = 
    corto_observe(CORTO_DEFINE|CORTO_UPDATE|CORTO_ON_SCOPE, data).callback(cb);

Now build & run the application again with:

bake
Lemonade/bin/x64-linux-debug/Lemonade

The output should look like this:

event received for MyShop!
event received for MyShop!

We can make the output of the observer callback a bit more interesting by showing which kind of event caused it to trigger:

void cb(corto_observerEvent *event)
{
  const char *event_str = "?";
  if (event->event == CORTO_DEFINE) {
    event_str = "DEFINE";
  } else if (event->event == CORTO_UPDATE) {
    event_str = "UPDATE";
  }
  corto_info("%s received for %s!", event_str, corto_idof(event->data));
}

After building & running the application again, you should now see:

DEFINE received for MyShop!
UPDATE received for MyShop!

In the next section we will look at how we can see changes in the object value.

Serializing Objects


As mentioned earlier, in corto, every object is annotated with a type which allows corto to do intelligent things with the value of objects. One of those things is serializing the object value to a format, like JSON.

In our application, lets serialize the value of objects to JSON in the observer callback. Add the following code to the cb function:

char *json = corto_serialize_value(event->data, "text/json");
corto_info("%s received for %s, value = %s",
    event_str, corto_idof(event->data), json);
free(json);

After building & running the application, you should now see:

DEFINE received for MyShop, value = {"inventory":0,"balance":0}
UPDATE received for MyShop, value = {"inventory":100,"balance":50}

This correctly shows the value of the object changing after we updated it in our cortomain function. Also notice that objects are created with all members initialized to 0. As we will see later on, DEFINE events can be synchronized to other storages or applications, and sometimes we will want to override this initial value. The next section shows how to do that.

Forward Declarations


In corto, objects are created in two steps. The first step is declaring the object, the second step is defining the object. When you call corto_create, you are actually calling two functions under the hood, corto_declare and corto_define. By calling these functions manually, we can control what the initial value of an object is going to be. Change the corto_create line in the cortomain function to:

Lemonade_Shop MyShop = corto_declare(root_o, "data/MyShop", Lemonade_Shop_o);
MyShop->inventory = 10;
MyShop->balance = 20;
corto_define(MyShop);

If you rebuild and run again, the output should now look like this:

DEFINE received for MyShop, value = {"inventory":10,"balance":20}
UPDATE received for MyShop, value = {"inventory":100,"balance":50}

Notice how the value of the object is now no longer set to 0 in the CORTO_DEFINE event. As we will see later, the corto_declare + corto_define combination plays an important role in both thread-safe object creation as well as persisting data to another store.

Lifecycle Hooks


Observers are one way to run application logic when something changes about an object. Another mechanism is lifecycle hooks. With lifecycle hooks objects can take more control over their own lifecycles.

We add lifecycle hooks by adding methods to our model. Open the model.cx file, and change the class definition to this:

class Shop:/
  inventory: int32
  balance: int32

  int16 construct()
  void define()
  int16 validate()
  void update()

These four methods are just a few of the lifecycle hooks recognized by corto. To see what they do, first build the project with:

bake

You should now see a new file called Shop.c in your project. This file contains the implementation of all the methods of the Shop class. At the moment you will find the four methods that we just added to the model with empty function bodies.

Change their implementations to this:

int16_t Lemonade_Shop_construct(
    Lemonade_Shop this)
{
    corto_info("construct %s", corto_idof(this));
    return 0;
}

void Lemonade_Shop_define(
    Lemonade_Shop this)
{
    corto_info("define %s", corto_idof(this));
}

void Lemonade_Shop_update(
    Lemonade_Shop this)
{
    corto_info("update %s", corto_idof(this));
}

int16_t Lemonade_Shop_validate(
    Lemonade_Shop this)
{
    corto_info("validate %s", corto_idof(this));
    return 0;
}

Now build & run the project. You should see the following output:

construct MyShop
DEFINE received for MyShop, value = {"inventory":10,"balance":20}
define MyShop
validate MyShop
UPDATE received for MyShop, value = {"inventory":100,"balance":50}
update MyShop

Notice in particular how some methods are called before the event, and some methods are called after the event. In corto, we refer to these as pre-hooks and post-hooks. The construct and validate are both pre-hooks. They are invoked before the DEFINE and UPDATE event. The define and update methods are post-hooks. They are called after the DEFINE and UPDATE events.

Never update objects inside a lifecycle hook! This may trigger a stack
overflow due to recursion.

In corto, pre-hooks can prevent an event from happening by returning a non-zero value in their method implementation. This allows objects to for example verify if the new value is valid before observers are notified.

Let's change the invalidate hook so that it returns -1 for invalid values, in which case corto should not send the UPDATE event:

int16_t Lemonade_Shop_validate(
    Lemonade_Shop this)
{
    corto_info("validate %s", corto_idof(this));

    if (this->inventory < 0) {
      corto_throw("invalid inventory value %d for %s", this->inventory, corto_idof(this));
      return -1; //indicates failure
    } else {
      return 0;
    }
}
It is good practice to call corto_throw in a pre-hook when it fails.

Now change the code that updates the object to:

  corto_update_begin(MyShop);
  MyShop->inventory = -10; // Invalid value
  MyShop->balance = 50;
  if (corto_update_end(MyShop)) {
    corto_throw("failed to update '%s'", corto_idof(MyShop));
  }

If you now build and run the application again, you will see:

construct MyShop
DEFINE received for MyShop, value = {"inventory":10,"balance":20}
define MyShop
validate MyShop
exception src/Shop.c:30 (_Lemonade_Shop_validate): invalid inventory value -10 for MyShop
     from src/Lemonade.c:33 (cortomain): failed to update 'MyShop'
     proc Lemonade [5350]

Notice that the UPDATE event was not sent, and the update lifecycle hook was not called.

After a pre-hook fails, the object is left in an INVALID state. The application will need to retry to update the object to re-validate the object. The application can know that an update failed by checking the returncode of corto_update_end.

Add the following lines to the application's cortomain function to re-validate the object:

  corto_update_begin(MyShop);
  MyShop->inventory = 100;
  MyShop->balance = 50;
  if (corto_update_end(MyShop)) {
    corto_throw("failed to update '%s'", corto_idof(MyShop));
  }

After building & running, the full output should now look like this:

construct MyShop
DEFINE received for MyShop, value = {"inventory":10,"balance":20}
define MyShop
validate MyShop
exception src/Shop.c:30 (_Lemonade_Shop_validate): invalid inventory value -10 for MyShop
     from src/Lemonade.c:33 (cortomain): failed to update 'MyShop'
     proc Lemonade [5358]

validate MyShop
UPDATE received for MyShop, value = {"inventory":100,"balance":50}
update MyShop

After providing a correct value for inventory, the object is revalidated, and the UPDATE event along with the pre- and post-hooks are again correctly executed.

Adding Methods


The Corto Web UI


Looking up Objects


Reflection


Persisting to disk


More Complex Models


Hierarchical Models


Models revisited


Dynamic Applications


The Virtual Store

TBD

Querying for Data


Subscribing for Data


Mounting Data


Receiving Events


Contenttypes


Subscriptions


Mounting Hierarchies


Batching


Corto Extensions

TBD

Serialization Formats


File Extensions


Code Generators


Tool Plugins


The Corto Web Framework

TBD

HTTP Servers


HTTP Services


The HTTP Router


The REST Protocol


The Websocket Protocol