Skip to content

CSteps bricklet documentation

anton-daynix edited this page Jun 5, 2012 · 6 revisions

Table of Contents

What is CSteps

CSteps is a lightweight library intended to help SW developers to solve the initialization and cleaning up (rollback) correctness issue, providing an ability to simulate and test all the failure scenarious.

CSteps is OS and space agnostic. It can be used in Linux, Windows and RTOSes, applications and drivers.

As SW developers, we deal a lot with pairs of init/de-init functions. ..._init/..._cleanup, ..._create/..._delete, ..._start/..._stop, ..._on/..._off, ..._register/..._unregister... Sounds familiar? ;-)

And as SW developers we know writing such functions in plain C is a headache.

Indeed, the logic is pretty simple here and can be generalized as:

  • an ..._init function is no more than a list of initialization steps one has to perform
  • the corresponding ..._cleanup function is just a rollback for initialization steps performed by ..._init
Although the logic one has to implement is pretty simple there's a lot of code to be written around this logic in order to make the code fail-safe.

The CSteps bricklet is intended to take all this headache off you and allow you to be focused on things that really matter - a product you're developing.

CSteps rationale

Problem Description

Let's imagine you're developing a module that has ..._init and ..._cleanup API that should initialize and de-initialize a few data members.

In C language, it would be something like:

struct my_module
{
  int       fd;     /* File */
  void     *buffer; /* Buffer */
  pthread_t thread; /* Thread */
};

int  my_module_init(struct my_module *m);
void my_module_cleanup(struct my_module *m);

Let's imagine how the my_module_init and my_module_cleanup functions can be implemented using standard approaches.

struct my_module
{
  FILE     *fd;     /* File */
  void     *buffer; /* Buffer */
  pthread_t thread; /* Thread */
  uint32_t  init_status; /* Init progress */
};

#define FILE_STEP_DONE   0x00000001
#define BUFFER_STEP_DONE 0x00000002
#define THREAD_STEP_DONE 0x00000003

...
int my_module_init(struct my_module *m)
{
  int res = 0;

  assert(m != NULL);
  assert(m->init_status == 0);

  m->fd = fopen(...);
  if (!m->fd)
  {
    res = errno;
    dbg_log("ERROR: cannot open file");
    goto FINISH;
  }
  m->init_status |= FILE_STEP_DONE;

  m->buffer = malloc(...);
  if (!m->buffer)
  {
    res = ENOMEM;
    dbg_log("ERROR: cannot allocate buffer");
    goto FINISH;
  }
  m->init_status |= BUFFER_STEP_DONE;

  res = pthread_create(&m->thread,...);
  if (!res)
  {
    dbg_log("ERROR: cannot create thread");
    goto FINISH;
  }
  m->init_status |= THREAD_STEP_DONE;


FINISH:
  if (res != 0)
  {
    my_module_cleanup(m);
  }

  return 0;
}

void my_module_cleanup(struct my_module *m)
{
  assert(m != NULL);

  if (m->init_status & THREAD_STEP_DONE)
  {
    pthread_kill(m->thread);
  }

  if (m->init_status & BUFFER_STEP_DONE)
  {
    free(m->buffer);
  }

  if (m->init_status & FILE_STEP_DONE)
  {
    fclose(m->fd);
  }

  m->init_status = 0;
}

Obviously, this code can be re-written using the goto stepX_error technique, nested ifs (with no goto) etc. - it's a matter of your taste and habits, but there are common weakness points in all of these approaches:

  • you have to write a lot of code around simple steps - all these checks, flags, etc.
  • you have to be very accurate writing it - once you forget to set a flag and it fails, at the very least, you have a leak
  • you cannot be sure the fallback (my_module_cleanup) is correct as usually all the init steps succeed and the rollback doesn't deal with partial cleanup cases.

Basic solution idea

The solution idea was taken from C++. Imagine, you have to create a C++ class with same data members:

class MyClass:
{
public:
  MyClass() : 
     fd(...), buffer(...), thread(...)
  {;}

protected:
  MyFile   fd;     /* File */
  MyPtr    buffer; /* Buffer */
  MyThread thread; /* Thread */

}

In this case, C++ takes care of all the initialization issues: once a data member initialization (constructor) throws an exception, all the objects created up to this point will be destroyed (their destructors will be called).

Thus, for instance, if fd(...) and buffer(...) succeeded and thread(...) fails - both buffer and fd destructors will be called.

CSteps in action

Now, let's re-write my_module_init and my_module_cleanup using the CSteps bricklet.

struct my_module
{
  FILE     *fd;     /* File */
  void     *buffer; /* Buffer */
  pthread_t thread; /* Thread */
  DNX_CS_DECLARE_SET(my_module_init);
};

DNX_CS_SET_BEGIN(my_module_init)
  DNX_CS_STEP(my_module_init, FILE_STEP)
  DNX_CS_STEP(my_module_init, BUFFER_STEP)
  DNX_CS_STEP(my_module_init, THREAD_STEP)
DNX_CS_SET_LOCAL(my_module_init)
DNX_CS_SET_END(my_module_init);
...

int my_module_init(struct my_module *m)
{
  int res = 0;

  assert(m != NULL);

  DNX_CS_BUILD_BLOCK_BEGIN(m, my_module_init, int, 0, -1)
    DNX_CS_BUILD_STEP(TRUE,
                      m, my_module_init, FILE_STEP,
                      m->fd = fopen(...), m->fd != NULL, errno)
    DNX_CS_BUILD_STEP(TRUE,
                      m, my_module_init, BUFFER_STEP,
                      m->buffer = malloc(...), m->buffer != NULL, ENOMEM)
    DNX_CS_BUILD_STEP(TRUE,
                      m, my_module_init, THREAD_STEP,
                      res = pthread_create(&m->thread,...), res == 0, res)
  DNX_CS_BUILD_BLOCK_ALWAYS(m, my_module_init)
  DNX_CS_BUILD_BLOCK_END(m, my_module_init, my_module_cleanup())
}

void my_module_cleanup(struct my_module *m)
{
  assert(m != NULL);

  DNX_CS_RUIN_BLOCK_BEGIN(m, my_module_init)
    DNX_CS_RUIN_STEP(m, my_module_init, thread_step,
                     pthread_kill(m->thread))
    DNX_CS_RUIN_STEP(m, my_module_init, buffer_step,
                     free(m->buffer))
    DNX_CS_RUIN_STEP(m, my_module_init, file_step,
                     fclose(m->fd))
  DNX_CS_RUIN_BLOCK_END(m, my_module_init)
}

Voilà! :-)

CSteps Features

Here's the list of features the CSteps bricklet brings you *automatically* along with the fact it allows you to write less code. :-)

So, the CSteps:

  1. *Controls the Build and Ruin steps execution order*
    • For Build steps: the steps should be executed the same way they're declared in DNX_CS_SET_BEGIN/DNX_CS_SET_END section
    • For Ruin steps: the steps should be executed in the exactly opposite order
  2. *Verifies all the Build and Ruin steps are executed*
    • For Build steps: each step declared has a corresponding DNX_CS_BUILD_STEP call
    • For Ruin steps: each step declared has a corresponding DNX_CS_RUIN_STEP call
  3. *Verifies Build/Ruin steps are not mixed*
    • Build and Ruin steps of the same set (see the set_name parameter)
    • Build steps of different sets (see the set_name parameter)
    • Ruin steps of different sets (see the set_name parameter)
  4. *Supports more complex cases*
    • Conditional execution (when a step should not always be executed)
      • For Build steps: see exec_condition parameter of DNX_CS_BUILD_STEP and DNX_CS_BUILD_STEP_ITER
      • For Ruin steps: built in to the DNX_CS_RUIN_STEP/DNX_CS_RUIN_STEP_ITER macros
    • Initialization loops (when a step should be executed in a loop)
      • For Build steps: see DNX_CS_DECLARE_LOOP_STEP and DNX_CS_BUILD_STEP_ITER
      • For Ruin steps: see DNX_CS_LOOP_STEP_CNT and DNX_CS_RUIN_STEP_ITER
    • Temporary (local) steps only affect the ...init function itself (for instance, temporary buffer allocation, temporary file creation etc.) - see DNX_CS_SET_LOCAL section of DNX_CS_SET_BEGIN/DNX_CS_SET_END and DNX_CS_BUILD_BLOCK_ALWAYS section of DNX_CS_BUILD_BLOCK_BEGIN/DNX_CS_BUILD_BLOCK_END
  5. *Simulates failure of any step and checks the rollback sequence* - see DNX_CS_DECLARE_BUILD_STEP_COUNT/DNX_CS_SET_BUILD_STEP_TO_FAIL/DNX_CS_GET_BUILD_STEP_TO_FAIL (can be switched off by compilation)
  6. *Prints verbose info to allow easy debugging* of _..init/..cleanup related problems - see DNX_CS_LOG_E/DNX_CS_LOG_I/DNX_CS_LOG_X
  7. *Asserts if a problem is detected* - see DNX_ASSERT

CSteps Implementation notes

Well, just like everybody else, we don't like macros :-(. And we would be more than happy to implement the same functionality with no macros.

Unfortunately, it's the only way C provides us at the moment...

CSteps Usage Examples

Conditional initialization steps

Sometimes there're initialization steps that should only be executed in some specific cases.

For example, you may want to have a thread initialized only if the code is running in server mode.

Here's how it can be written in plain C:

struct my_module
{
  ...
  bool_t     server_mode; /* Execution Mode */
  pthread_t *thread;      /* Thread */
  ...
  uint32_t   init_status; /* Init progress */
};

#define THREADS_STEP_DONE   0x00000001
...
int my_module_init(struct my_module *m, bool_t server_mode)
{
  int res = 0;
  uint32_t i;

  assert(m != NULL);
  assert(m->init_status == 0);

  ...
  /* Allocate m->threads here */
  ...
  m->server_mode = server_mode;
  if (server_mode)
  {
    res = pthread_create(&m->threads[i],...);
    if (!res)
    {
      dbg_log("ERROR: cannot create thread");
      goto FINISH;
    }
  }
  m->init_status |= THREADS_STEP_DONE;
  ...
}

void my_module_cleanup(struct my_module *m)
{
  ...
  if ((m->init_status & THREADS_STEP_DONE) && m->server_mode)
  {
    pthread_kill(m->threads[i]);
  }
  ...
}

The same functionality implemented using CSteps macros:

struct my_module
{
  ...
  pthread_t thread; /* Thread */
  ...
  DNX_CS_DECLARE_SET(my_module_init);
};

DNX_CS_SET_BEGIN(my_module_init)
  ...
  DNX_CS_STEP(my_module_init, THREAD_STEP)
  ...
DNX_CS_SET_LOCAL(my_module_init)
DNX_CS_SET_END(my_module_init);
...
int my_module_init(struct my_module *m, bool_t server_mode)
{
  int res = 0;

  DNX_CS_BUILD_BLOCK_BEGIN(m, my_module_init, int, 0, -1)
    ...
    DNX_CS_BUILD_STEP(server_mode == TRUE,
                      m, my_module_init, THREADS_STEP,
                      res = pthread_create(&m->thread,...), res == 0, res)
    ...
  DNX_CS_BUILD_BLOCK_ALWAYS(m, my_module_init)
  DNX_CS_BUILD_BLOCK_END(m, my_module_init, my_module_cleanup())
}

void my_module_cleanup(struct my_module *m)
{
  DNX_CS_RUIN_BLOCK_BEGIN(m, my_module_init)
    ...
    DNX_CS_RUIN_STEP_ITER(m, my_module_init, THREADS_STEP,
                          pthread_kill(m->thread));
    ...
  DNX_CS_RUIN_BLOCK_END(m, module1_test1)
}

Initialization loops

Let's say you have to make an initialization in a loop.

For example, you may want to have an array of threads initialized.

Here's how it looks usually:

struct my_module
{
  ...
  uint32_t   nof_threads; /* Number of threads */
  pthread_t *threads; /* Thread */
  ...
  uint32_t   init_status; /* Init progress */
};

#define THREADS_STEP_DONE   0x00000001
...
int my_module_init(struct my_module *m, uint32_t nof_threads)
{
  int res = 0;
  uint32_t i;

  assert(m != NULL);
  assert(m->init_status == 0);

  ...
  /* Allocate m->threads here */
  ...
  for (i = 0; i < nof_threads; i++)
  {
    res = pthread_create(&m->threads[i],...);
    if (!res)
    {
      dbg_log("ERROR: cannot create thread");
      goto FINISH;
    }
    ++m->nof_threads;
  }
  m->init_status |= THREADS_STEP_DONE;
  ...
}

void my_module_cleanup(struct my_module *m)
{
  uint32_t i;

  ...
  for (i = 0; i < m->nof_threads; i++)
  {
    pthread_kill(m->threads[i]);
  }
  ...
  /* Free m->threads here */
  ...

  m->init_status = 0;
}

The same code written with CSteps:

struct my_module
{
  ...
  pthread_t *threads; /* Thread */
  ...
  DNX_CS_DECLARE_LOOP_STEP(my_module_init, THREADS_STEP);
  DNX_CS_DECLARE_SET(my_module_init);
};

DNX_CS_SET_BEGIN(my_module_init)
  ...
  DNX_CS_STEP(my_module_init, THREADS_STEP)
  ...
DNX_CS_SET_LOCAL(my_module_init)
DNX_CS_SET_END(my_module_init);
...
int my_module_init(struct my_module *m, uint32_t nof_threads)
{
  int res = 0;
  uint32_t i;

  DNX_CS_BUILD_BLOCK_BEGIN(m, my_module_init, int, 0, -1)
    ...
    /* Allocate m->threads here */
    ...
    for (i = 0; i < nof_threads; i++)
    {
      DNX_CS_BUILD_STEP_ITER(TRUE,
                             m, my_module_init, THREADS_STEP,
                             res = pthread_create(&m->threads[i],...), res == 0, res)
    }
    ...
  DNX_CS_BUILD_BLOCK_ALWAYS(m, my_module_init)
  DNX_CS_BUILD_BLOCK_END(m, my_module_init, my_module_cleanup())
}

void my_module_cleanup(struct my_module *m)
{
  uint32_t i = 0;

  DNX_CS_RUIN_BLOCK_BEGIN(m, my_module_init)
    ...
    while (DNX_CS_LOOP_STEP_CNT(m, my_module_init, THREADS_STEP))
    {
      DNX_CS_RUIN_STEP_ITER(m, my_module_init, THREADS_STEP,
                            pthread_kill(m->threads[i++]));
    }
    ...
    /* Free m->threads here */
    ...
  DNX_CS_RUIN_BLOCK_END(m, module1_test1)
}

Temporary (local) initialization steps

In some cases, you have to initialize a temporary object (allocate an intermediate buffer, open a file etc.) necessary to proceed with the initialization sequence, but should be uninitialized once the ..._init function exits - whether it succeeds or fails.

Let's say, for example, you have to initialize an XMLReader object to read configuration parameters on init.

Here's how it can be written in plain C:

struct my_module
{
  ...
  uint32_t   init_status; /* Init progress */
};

#define THREADS_STEP_DONE   0x00000001
...
int my_module_init(struct my_module *m, bool_t server_mode)
{
  int          res = 0;
  bool_t       xml_reader_inited = 0;
  xml_reader_t xml_reader;

  assert(m != NULL);
  assert(m->init_status == 0);

  ...
  res = xml_reader_init(&xml_reader,...);
  if (!res)
  {
    dbg_log("ERROR: cannot initialize xml_reader");
    goto FINISH;
  }
  xml_reader_inited = 1;
  ...
  /* Use the xml_reader object here */
  ...

FINISH:
  ...
  if (xml_reader_inited)
  {
    xml_reader_cleanup(&xml_reader);
  }
  ...
}

void my_module_cleanup(struct my_module *m)
{
  ...
  /* No mention of xml_reader here obviously */
  ...
}

The same functionality implemented using CSteps macros:

struct my_module
{
  ...
  DNX_CS_DECLARE_SET(my_module_init);
};

DNX_CS_SET_BEGIN(my_module_init)
  ...
DNX_CS_SET_LOCAL(my_module_init)
  ...
  DNX_CS_STEP(my_module_init, XML_READER_STEP)
  ...
DNX_CS_SET_END(my_module_init);
...
int my_module_init(struct my_module *m, bool_t server_mode)
{
  int res = 0;

  DNX_CS_BUILD_BLOCK_BEGIN(m, my_module_init, int, 0, -1)
    ...
    DNX_CS_BUILD_STEP(TRUE,
                      m, my_module_init, XML_READER_STEP,
                      res = xml_reader_init(&xml_reader,...), res == 0, res)
    ...
    /* Use the xml_reader object here */
    ...
  DNX_CS_BUILD_BLOCK_ALWAYS(m, my_module_init)
    ...
    DNX_CS_RUIN_STEP_ITER(m, my_module_init, XML_READER_STEP,
                          xml_reader_cleanup(&xml_reader))
    ...
  DNX_CS_BUILD_BLOCK_END(m, my_module_init, my_module_cleanup())
}

void my_module_cleanup(struct my_module *m)
{
  DNX_CS_RUIN_BLOCK_BEGIN(m, my_module_init)
    ...
    /* No mention of xml_reader here obviously */
    ...
  DNX_CS_RUIN_BLOCK_END(m, module1_test1)
}

Mixed cases

Indeed, you can use the CSteps in even more complex cases. Any combination is possible. You can implement a loop of conditional local steps using the same basic syntax.

Final thoughts

CSteps isn't a magic bullet. It won't and can't solve all the problems in the world. :-)

However, it's a foolproof remedy for all the initialization/rollback correctness issues.

It helps us a lot and hopefully will help you as well.

Something went wrong with that request. Please try again.