Высокоуровневый С или пару слов о Cello

imageCello — это библиотека, которая сделала высокоуровневый C возможным! Обобщения (generics), параметрический полиморфизм, интерфейсы, конструкторы/деструкторы, сборщик мусора (по желанию), исключения и рефлекция. Да-да, ты не ослышался, все эти плюхи в одном флаконе. Так как Cello построен в пределах стандарта С, в сухом остатке ты получишь все, что нужно живому человеку на земле: высокую производительность, мощный инструментарий и гибкие библиотеки.

Talk is cheap, show me the code!

#include "Cello.h"

int main(int argc, char** argv) {

  /* Stack objects are created using "$" */
  var i0 = $(Int, 5);
  var i2 = $(Int, 3);
  var i2 = $(Int, 4);

  /* Heap objects are created using "new" */
  var items = new(Array, Int, i0, i1, i2);

  /* Collections can be looped over */
  foreach (item in items) {
    print("Object %$ is of type %$\n",
      item, type_of(item));
  }

  /* Heap objects destructed via Garbage Collection */
  return 0;
}


ШОК! Зачем же мне теперь все эти ваши Go/D/Nim/, если С на стероидах решает все проблемы рода человеческого?! Хочешь узнать о готовности Cello к продакшну и увидеть еще больше кода? Добро пожаловать подкат.

Вводная


Cello добавляет поверх С дополнительный слой рантайма. Это абсолютная необходимость, потому что иначе расширить язык было бы возможно только меняя компилятор, а такую роскошь мы себе позволить не можем. Пользователь определяет типопеременные (runtime type variables), которые содержат всю необходимую информацию для нового функционала, связывая их с обычными легитимными типами.

Оверхед у GC в Cello конечно же, есть. Указатели в Cello сопровождаются дополнительной мета-информацией, которая хранится прямо перед данными. Это говорит о том, что указатели в Cello полностью совместимы с рабоче-крестьянскими указателями из Стандарта и могут без труда кооперироваться.

Вся информация о типах в большей мере является всего лишь списком экземпляров типоклассов и интерфейсов. Эти ребята себя очень хорошо зарекомендовали, поэтому они используются везде: начиная от GC, заканчивая документацией. Они позволяют использовать объекты в контексте их поведения. Это прикольно, потому что можно писать алгоритмы в обобщенной манере, опираясь только на вводные данные, но не на реальную реализацию той или иной структуры.

Cello, в принципе, довольно умен и может автоматически выводить поведения (behaviours) в большинстве случаев. Объекты в Cello можно печатать, сравнивать, хэшировать, сериализировать, сортировать копировать и вот это все. Короче говоря, райское наслаждение.

Объекты


Объекты в Cello начинаются с обыкновенных рабоче-крестьянских структур, которые ты так хорошо знаешь из С. Чуть позже Cello добавит в них немного мета-информации. Тут есть одно требование: определять структуры нужно без typedef. Например, давайте напишем структуру для хранения какой-то картинки, да на стероидах! Для этого нужно определить обыкновенную сишную структуру и зарегистрировать новополученный тип при помощи макроса Cello:

struct Image {
  uint64_t width;
  uint64_t height;
  unsigned char *data;
};

var Image = Cello(Image);


Обрати внимание, у нас появились две штуки. Оригинальный сишный тип Image и переменная, которая представляет тип в рантайме. По воле случая, мы тоже ее назвали Image. Ты скорее всего обратил внимание на этого подозрительного товарища по имени var. На самом деле var это всего лишь void*, тоесть обобщенный указатель, но стоит использовать первый вариант, для удобства.

В контексте базовых типов, это все. Больше ничего не нужно писать, за тебя все сделает компилятор. Теперь можно создавать переменные типа Image: что на стэке, что на куче. Помимо всего прочего, их можно напечатать, сравнить, закинуть в коллекцию и вот это все:

/* Allocate on Stack or Heap */
struct Image* x = $(Image, 0, 0, NULL);
struct Image* y = new(Image);

/* Print */
print("This is an image: %$\n", x);

/* Compare */
print("Images %$ and %$ are equal? %s\n", 
  x, y, eq(x, y) ? $S("Yes") : $S("No"));

/* Put in an Array */
struct Array* a = new(Array, Image, x, y);
print("Array of Images: %$\n", a);


В действительности, почти все основные типоклассы в Cello, по умолчанию, с реализацией. Но настоящая сила Cello проявляется тогда, когда мы начинаем расширять реализацию этих типоклассов.

Конструкторы и деструкторы


Наша сишная структура, Image, содержит указатель на какой-то участок памяти, который может быть выделен какой-то другой функцией. Если ты хочешь избежать утечек, надо убедиться, что мы вовремя эту память освобождаем. Теперь воспользуемся Cello, чтобы определить деструктор для Image:

void Image_Del(var self) {
  struct Image* i = self;
  free(i->data);
}


Ты можешь с легкостью привести аргумент self к сишному типу Image*. Это возможно, потому что указатели Cello (те, которые мы создаем с var) полностью совместимы с рабоче-крестьянскими указателями в С. Так как у тебя есть var-указатель из Cello, ты знаешь, что на нем висит опредленный сишный тип (прямо как здесь, в деструкторе), а значит, что можно абсолютно безопасно привести его к этому типу и разумеется, получить доступ к полям этого типа. В конкретно этом случае, мы вызываем free для указателя на данные из Image.

Чтобы зарегистрировать деструктор в Cello, ты захочешь передать его в макрос Cello, как экземпляр Instance нового типокласса New. Так как мы пока не хотим определять конструктор, то стоит просто передать NULL в соотв. поле:

var Image = Cello(Image, Instance(New, NULL, Image_Del));


Теперь, когда GC в Cello придет, чтобы разобраться с объектом Image, он вызовет наш деструктор. А чего, по-моему, круто!

Cахар, сахар, сахар


Daniel Holden написал Cello, чтобы местами упростить свою работу, так что тут хватает разнообразного сахара. Например, сокращенный синтаксис создания переменных или даже таблицы (sic!):

#include "Cello.h"

int main(int argc, char** argv) {

  /* Shorthand $ can be used for basic types */
  var prices = new(Table, String, Int);
  set(prices, $S("Apple"),  $I(12)); 
  set(prices, $S("Banana"), $I( 6)); 
  set(prices, $S("Pear"),   $I(55));

  /* Tables also support iteration */
  foreach (key in prices) {
    var val = get(prices, key);
    print("Price of %$ is %$\n", key, val);
  }

  return 0;
}


Или замысловатые range-циклы и прочие слайсы:

#include "Cello.h"

int main(int argc, char** argv) {

  var items = new(Array, Int, 
    $I( 8), $I( 5), $I(20), 
    $I(15), $I(16), $I(98));

  /* Iterate over indices using "range" */
  foreach (i in range($I(len(items)))) {
    print("Item Range %i is %i\n", i, get(items, i));
  }

  /* Iterate over every other item with "slice" */ 
  foreach (item in slice(items, _, _, $I(2))) {
    print("Item Slice %i\n", item);
  }

  return 0;
}

И это еще далеко не все…


На самом деле, возможности Cello не заканчиваются на приведенном мною в этой статье функционале, но это не беда, ведь с остальными штуками вы сможете ознакомиться с помощью документации. Кстати говоря, у Cello есть классный Quickstart, в котором автор покажет, как написать програму, которая интересным образом глитчует .tga-изображения. Настоятельно рекомендую ознакомиться!

Отвечая на вопрос, готов ли Cello к продакшну… тут нет однозначного ответа. Си, в основном, используется там, где нужна прямо таки maxxxимальная производительность — например, во встраиваемых системах. Захотят ли разработчики подобного софта тянуть за собой GC — это очень спорный вопрос и я склонен к отрицательному ответу. С другой стороны, над Cello довольно долгое время экспериментировали, так что в принципе, это штука рабочая. Я думаю, что фуллтайм С-программисты однозначно должны заценить.

Для тех, кому интересно, как эта шняшка устроена внутри, ссылочка на гитхаб. Помимо всего прочего, я также хотел бы сделать небольшой опрос по теме поста. Прошу отвечать на него только разработчиков, которые реально работают с языком С, остальных хочу попросить воздержаться.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

© Habrahabr.ru