Как стать автором
Обновить

Комментарии 37

Хохо, эко Вы заморочились, но в целом очень прикольно.

Интересно, а в каких проектах промышленной разработки в принципе такой код может быть разрешен? Есть у меня подозрения, что почти нигде.

чтобы прямо ровно такой код - не знаю, но я вдохновлялся исходниками CPython

gtk тоже на си написан и там есть такое же ООП

И то верно, cpython, gtk... для pc проектов вполне возможно. Хотя наверное я очень критичен с некоторыми изменениями, возможно наверное и в эмбедед это все отлично вписать.

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

В rust есть trait’ы. В C мне вместо них не раз приходилось использовать либо функции, генерируемые макросами, либо .c.h файлы. Не знаю, как называется последняя техника, но уверен, что не я первый её придумал: идея в том, что у нас есть файл frob.c.h вида


// FROB_ACTION has default value.
#ifndef FROB_ACTION
# define _FROB_ACTION_DEFINED
# define FROB_ACTION(a, b) (a) += (b)
#endif

#define _FROB_FUNCNAME(suffix) FROB_PREFIX##suffix

static FROB_RETURN_TYPE _FROB_FUNCNAME(_frobnicate)(int arg1)
{
    FROB_RETURN_TYPE ret = 1;
    FROB_ACTION(ret, 1);
    return ret;
}

#undef _FROB_FUNCNAME

#ifdef _FROB_ACTION_DEFINED
# undef _FROB_ACTION_DEFINED
# undef FROB_ACTION
#endif

и он используется так:


#define FROB_PREFIX froba
#define FROB_RETURN_TYPE int
#include "frob.c.h"
#undef FROB_RETURN_TYPE
#undef FROB_PREFIX

#define FROB_PREFIX frobs
#define FROB_RETURN_TYPE int
#define FROB_ACTION(a, b) (a) <<= (b)
#include "frob.c.h"
#undef FROB_ACTION
#undef FROB_RETURN_TYPE
#undef FROB_PREFIX

int main(const int argc, const char *const *const argv)
{
    if (froba_frobnicate(argc) > 0) {
        return frobs_frobnicate(argc);
    } else {
        return 0;
    }
}

(#undef тут везде только чтобы не засорять пространство имён).


Такая вариация на тему generic’ов не слишком удобна, но она имеет несколько важных преимуществ перед определением функций в макросах:


  1. В отладчике функции теперь не в одну строку и вы можете нормально ставить точки останова.
  2. Подсветка синтаксиса работает лучше.
  3. Не нужно помнить про \ в конце строки.
  4. Можно сделать аргументы по‐умолчанию.

Из недостатков в первую очередь только бо́льший размер кода. Техника для случаев, когда вы хотите что‐то вроде HashMap<K, V> со всеми его функциями, но в C — т.е. когда кода достаточно много, чтобы вас волновало удобство работы с ним.

rt thread можете посмотреть. По моему реализация этой rtos для эмбедед очень напоминает методы, описанные в статье.

Можно, например если делать по книжке (близко к авторскому решению, но без самопальной vtable) Design Patterns for Embedded Systems in C, Bruce Powel Douglass, 2011. С практической точки зрения это имеет смысл где-ть на stm32Н7 или esp32 с тактовой частотой от 50МГц и памятью от 512КБ для модульного оборудования или расширяемого ПО. Ну или если не торопитесь и риалтайм не нужен :)

да я просто поторопился с комментарием, видимо malloc меня очень задел. Почти во всех проектах которых я работал, динамическое выделение памяти запрещено стандартами. Ну и еще пару мелочей, а так да.. еще раз повторюсь, выглядит все интересно :)

В далеком 2008 году я использовал похожий подход для программирования интерфейса в одном промышленном приборе.

Компилятор позволял писать только на Си, а все подходы организации подобного в процедурном стиле, которые я видел, были ещё ужаснее, на мой скромный взгляд.

При должной самодисциплине вполне себе работает, но мне повезло что команда состояла из одного меня и (потом) одного студента :-)

StrongSwan VPN - https://docs.strongswan.org/docs/5.9/devs/objectOrientedC.html, там ещё сильнее заморочились в плане синтаксического сахара на макросах. При этом общую идею авторы взяли из xine. Вполне живая и рабочая схема на самом деле.

В некоторых подсистемах ядра делают ООП-like (не обязательно прям вот такой, как описан здесь).

Есть книжка "Object-Oriented Programming With ANSI-C" автора Axel-Tobias Schreiner на эту тему

Сохраню коммент, вдруг, не дай бог, пригодится

К примеру, хочешь ты принтануть содержимое структуры несколько раз

Их есть у меня. Без ООП, абстракций, а исключительно печать структур, при этом не какой-нибудь там отдельной функцией в буфер, а возможность печатать структуру как один из спецификаторов форматной строки: https://github.com/Garrus007/advanced-fprintf

Вот что получаем:

    struct foo foo = { .a=1, .b=2 };
    struct bar bar = { .a=3.14, .b=10, .c="hello world" };

    aprintf("Print: int: %d, foo: %Y, str: '%s', bar: %Y, bad one: %Y. The end!\n",
            123,
            FORMAT_FOO(&foo), 
            "some string", 
            FORMAT_BAR(&bar),
            FORMAT_FOO(NULL));
Подготовка за кулисами

Ну, тут придется немного пописать, чтобы предоставить структуре возможность печататься...

struct foo
{
    int a;
    int b;
};

// Function to print "struct foo" to the FILE*
void format_foo(FILE* f, void* data)
{
    if (data == NULL) {
        fprintf(f, "foo(nill)");
        return;
    }

    struct foo* foo = (struct foo*)data;
    fprintf(f, "foo{a=%d, b=%d}", foo->a, foo->b);
}

#define FORMAT_FOO(ptr)format_foo, (ptr)

Под капотом

А вот тут колдунство. Согласно стандарту, семейство *printf-функций не трогает "лишние" аргументы, после того, как форматная строка закончилась.

afprintf - это враппер над fprintf, который находит специальный спецификатор формата %Y, разделяет форматную строку по границе этого спецификатора, скармилвает подстроку обычному fprintf, потом печатает кастомную структуру, используая переданный указатель на функцию печати, а затем продолжает печатать обычным fprintf до следующего %Y или до конца форматной строки.

Плюсы:

  • можно печатать структуру, задавая ее в форматной строке

  • можно печатать сразу несколько структур (нет какого-то буфера, который затрется)

Минусы:

  • копирование форматной строки и аллокация

Не совсем портабельно, но

https://www.gnu.org/software/libc/manual/html_node/Printf-Extension-Example.html

Вот здесь:

    struct foo* foo = (struct foo*)data;

явного приведения типа не требуется.

А можно просто использовать С++...

Использовать какой-то другой язык вместо си? Да ну, бред какой-то.

Использовать какой-то другой язык вместо С++? Да ну, бред какой-то.

Мы заигрались.

НЛО прилетело и опубликовало эту надпись здесь

Подзабыл Си. ;-( Но есть возможность вспомнить!

Вы написали довольно понятный код. Не могли бы Вы пояснить пару моментов?

  1. Что такое PRIu32?

  2. В вашей реализации получается, что разные экземпляры одного и того же объекта (структуры) всё своё носят с собой (например, указатели методы). Не так ли?

  3. Как работает функция realloc? Она просто пытается расширить область памяти под объект без потери ранее построенных объектов?

НЛО прилетело и опубликовало эту надпись здесь

До таблицы виртуальных функций так и не дошли (да и вообще пошли в другом направлении) - т.е. чем больше функций тем больше объекты.

А какой компилятор Вы использовали? Хотелось бы проверит по шагам Ваше решение.

gcc и clang.

мне кажется, и msvc должен проканать, я старался не завязываться на особенностях компиляторов. но если вдруг не проканает, напишите

msvc для си лучше не использовать, так как microsoft его не обновляют. Я пытался скомпилировать код на c11 и обнаружил, что он поддерживает то ли c99, то ли ansi c89.

Как выстрелить себе в ногу на языке C

Использует буфферы... Я не спец, но может стоит использовать структуру на куче (в C ведь есть нормальный string)? Или хотя бы проверять длину строки, которая в буфер лезет?

да, лучше заморочиться с двойным вызовом snprintf. это выходит за пределы скоупа текущей задачи, поэтому я решил по-простому.

дописал UPD

А не проще generic юзать? Стандартный _Generic уже 11 лет с нами, а в gcc и clang generic через built-inы и того раньше появились.

Ой вей. Именно так и появился GTK.

Нужно ООП - пиши на плюсах.

Хочешь поупражняться в сверхуме - пиши на Аде.

На Си программа должна быть "палка-веревка". По другому - да, вы наркоман.

Но ведь, раз макросы уже применены, можно пойти "в лоб" и сделать в объекте указатель на структуру-класс, в которой и будут объявлены методы, а также указатель на родительский класс, ну и вызывать макросом, совсем избавившись от дублирования имени в вызове.

Как будто что-то плохое.


А ведь в препроцессоре еще и трюки с ## доступны...

Зачем привязываться к смещениям для вызова реализации из наследника? Можно объявить нужные "абстрактные" методы в базовом объекте, с реализацией по умолчанию (на всякий случай).

И, как выше упоминали, у вас получаются жирные объекты за счет того что они несут все функции в себе. Это прототипное ООП (как в JS), иногда имеет смысл конечно, но при более сложной модели стоит выделить такие функции в мета-объекты (классы) и держать единственную ссылку на класс в каждом объекте.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории