Index ¦ Archives  ¦ Atom  ¦ RSS

Pebble Development with C++ on the mind

I've recently been working on Pebble smartwatch development, but I've spent the past 7 years in a C++ and Python based world, whether it was web development with Django, a C++ webserver, or a myriad of Python-based tools and frameworks. With C++ and Python, ignoring class hierarchies and many of the niceties of a higher-level language, a programmer at least has a few luxuries. While I can live without many, I'd prefer not to when they don't cost me anything, and there are a few useful ones and in this article I'll show how you can get them in C with minimal overhead.

Firstly, there's constructors and destructors tied to scopes. With this in C++ you normally get RAII, but that's not entirely necessary. In Pebble's 2.0 SDK, there are quite a few *_init and *_create functions, all of which have a corresponding *_deinit or *_destroy function, and you as the developer are expected to call those destructor functions after you're done using the constructed resource and only after. While Pebble seems to function correctly without everything *_deinit'd or *_destroy'd, you shouldn't rely on Pebble cleaning up after your watchface/app, especially since it can run for obscene amounts of time (weeks, even months).

Tracked Destructors

Each piece of your app will have an init and a deinit equivalent, whether it's at the top or bottom of main(), on window load/unload or appear/disappear, etc. Therefore, you have an obvious place to deinit anything init'd in init, but you don't want to keep having to change code in two places everytime you alter how many resources are used in that piece. Therefore, I'm introducing tracked destructors. call the *_init at the top of each piece, *_deinit when the piece is being deinit'd, and *_add when you want something destroyed. Here's an example:

#include "destructor.h"
// DESTRUCTOR'S MEMORY
static destructor_memory app_destructors;

static void init(void) {
  // DESTRUCTORS INIT
  destructors_init(&app_destructors);

  app_message_open(inbound_size, outbound_size);
  Tuplet initial_values[] = {TupletInteger(APP_KEY, (int16_t) 0)};
  app_sync_init(&sync_ctx,
      sync_buffer, sizeof(sync_buffer),
      initial_values, ARRAY_LENGTH(initial_values),
      sync_tuple_changed_callback, NULL, NULL);
  // DESTRUCTORS ADD
  destructors_add(&main_window_destructors,
      "sync", (destructor)app_sync_deinit, (void*)&sync_ctx);

  window = window_create();
  destructors_add(&app_destructors,
      "window", (destructor)window_destroy, (void*)window);
  window_set_window_handlers(window, (WindowHandlers) {
    .load = window_load, .unload = window_unload
  });
  window_stack_push(window, animated);
}

static void deinit(void) {
  // DESTRUCTORS DEINIT
  destructors_deinit(&app_destructors);
}

int main(void) {
  init();
  app_event_loop();
  deinit();
}

This is the minimum for an app that uses AppSync to send/receive messages from it's JS counterpart (or mobile app). This piece of the app is the main piece that initializes app_message and AppSync then pushes a window onto the stack, and it has init() and a deinit() functions. Here are the important parts:

  • At the top of init(), we create the destructor for this piece called app_destructors.
  • We call app_sync_init which has a app_sync_deinit counterpart, so instead of jumping to deinit and adding it there, we add it right after app_sync_init, which tightly links these two lines together. Now if we were to move when/where AppSync was setup, we would know to move its destructor with it. No memory leak, no use-after-free, and the memory is freed the moment its not used, much like in C++ with a stack-allocated object.
  • Next we create a window via window_create, which has window_destroy. We do the same as with app_sync, same benefits.
  • Lastly in deinit, we don't have to worry about what was created, in what order, we can have destructors handle it automatically. Destructors will destroy everything it was told to in the reverse order it was added, so you can even add dependent objects on later and they won't ever be deleted before the objects they have pointers to.

This isn't the only use case. In one of my apps, I have a list of text layers created and I add them to the destructor for that piece of the app so they're all destroyed in the right order and after that window is unloaded from the stack.

I'm a fair blogger, so now that we've seen the benefits, it's time for the costs. In Pebble, the costs are cycles and memory, as it's up to 120MHz STM32 with about 21kB of Heap available (according to the app_manager.c logging). If you're app gets large, bytes start to matter. The library I wrote uses a simple vector implementation, which can be tuned to meet your app's needs, by default each vector is of size 10 and doubles in size when that's reached. It's a little optimized for high-growth vectors while destructors tends to be a single-time growth vector, so there's some savings to be had there, though it's a vector of pointers so it's not too much of a cost.

  • A vector backs each destructor-set, with amortized linear cost for *_add. Those are dangerous words for an embedded app, so it all depends on how the vector library is tuned. I didn't care too much, as my app isn't heap-hungry, but adding a vector_resize function called by a new destructors_resize would make the memory cost only 1 pointer for each destructor in the vector. If the *_resize function calls were really trusted, the vector's int that tracks size becomes unnecessary (as it's only ever used for bounds checking), saving that int.
  • Each destructor added is backed by a struct of two pointers and a char* name, which can be removed via #ifdef lines.
  • Each destructor init creates a vector (a pointer and two ints), and adds the first destructor (the vector itself) to itself. Since it's added first, it gets freed last, so the vector is no longer referenced after it gets freed.
  • Lastly, when being deinit'd, Each destroying callback is called, then the tracking struct is freed.

That all means that the minimal memory overhead is 1 pointer and 1 int on the stack plus 3 pointers on the heap per destructor, or 8 (S) + 12*N (H) bytes. The minimal CPU overhead is a single malloc per destructor with *_init and *_add being O(1) and *_deinit being O(N) with small constants.

And finally, here's the code:

vector.h

#ifndef VECTOR_H__
#define VECTOR_H__

typedef struct vector_ {
    void** data;
    int size;
    int count;
} vector;

void vector_init(vector*);
int vector_count(vector*);
void vector_add(vector*, void*);
void vector_set(vector*, int, void*);
void *vector_get(vector*, int);
void vector_delete(vector*, int);
void vector_free(vector*);

#endif

vector.c

#include <pebble.h>
#include "vector.h"

void vector_init(vector *v)
{
  v->data = NULL;
  v->size = 0;
  v->count = 0;
}

int vector_count(vector *v)
{
  return v->count;
}

void vector_add(vector *v, void *e)
{
  if (v->size == 0) {
    v->size = 10;
    v->data = malloc(sizeof(void*) * v->size);
    memset(v->data, '\0', sizeof(void*) * v->size);
  }

  // condition to increase v->data:
  // last slot exhausted
  if (v->size == v->count) {
    // No realloc, so reimplement it as a poor-man's malloc/free combo.
    // v->size *= 2;
    // v->data = realloc(v->data, sizeof(void*) * v->size);

    int newsize = v->size * 2;
    void** newdata = malloc(sizeof(void*) * newsize);
    memcpy(newdata, v->data, v->size);

    free(v->data);
    v->data = newdata;
    v->size = newsize;
  }

  v->data[v->count] = e;
  v->count++;
}

void vector_set(vector *v, int index, void *e)
{
  if (index >= v->count) {
    return;
  }

  v->data[index] = e;
}

void *vector_get(vector *v, int index)
{
  if (index >= v->count) {
    return NULL;
  }

  return v->data[index];
}

void vector_delete(vector *v, int index)
{
  if (index >= v->count) {
    return;
  }

  v->data[index] = NULL;

  int i, j;
  void **newarr = (void**)malloc(sizeof(void*) * v->count * 2);
  for (i = 0, j = 0; i < v->count; i++) {
    if (v->data[i] != NULL) {
      newarr[j] = v->data[i];
      j++;
    }
  }

  free(v->data);

  v->size = v->count * 2;
  v->data = newarr;
  v->count--;
}

void vector_free(vector *v)
{
  free(v->data);
}

destructor.h

:::c #include "vector.h"

typedef void (*destructor)(void *target);
typedef vector destructor_memory;

extern void destructors_init(destructor_memory* destructor_m);
extern void destructors_add(destructor_memory* destructor_m, const char* const name, destructor func, void* target);
extern void destructors_deinit(destructor_memory* destructor_m);

destructor.c

#include <pebble.h>
#include "destructor.h"

typedef void (*destructor)(void *target);
typedef struct destructor_cb {
  const char* name;
  destructor callback;
  void *target;
} destructor_cb;

void destructors_init(destructor_memory* destructor_m) {
  vector_init(destructor_m);
  destructors_add(destructor_m,
      "destructor_vector", (destructor)vector_free,
      (void*)destructor_m);
}

void destructors_add(destructor_memory* destructor_m,
    const char* const name, destructor func, void* target) {
  destructor_cb *new_destructor = (destructor_cb*)malloc(sizeof(destructor_cb));
  new_destructor->name = name;
  new_destructor->callback = func;
  new_destructor->target = target;
  vector_add(destructor_m, (void*)new_destructor);
}

void destructors_deinit(destructor_memory* destructor_m) {
  // Destruct objects backwards.
  for (int i = vector_count(destructor_m) - 1; i >= 0; i--) {
    destructor_cb* cb = vector_get(destructor_m, i);
    cb->callback(cb->target);
    free(cb);
  }
}

On the next edition of Pebble Development with C++ on the mind, I'll cover getting actual C++ running on your smartwatch.

© Fahrzin Hemmati. Built using Pelican. Theme by Giulio Fidente on github.