Pull to refresh

Comments 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 году я использовал похожий подход для программирования интерфейса в одном промышленном приборе.

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

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

В некоторых подсистемах ядра делают ООП-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;

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

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

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

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

UFO just landed and posted this here

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

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

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

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

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

UFO just landed and posted this here

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

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

gcc и clang.

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

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

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

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

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

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

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

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

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

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

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

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

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

Sign up to leave a comment.

Articles