Комментарии 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’ов не слишком удобна, но она имеет несколько важных преимуществ перед определением функций в макросах:
- В отладчике функции теперь не в одну строку и вы можете нормально ставить точки останова.
- Подсветка синтаксиса работает лучше.
- Не нужно помнить про
\
в конце строки. - Можно сделать аргументы по‐умолчанию.
Из недостатков в первую очередь только бо́льший размер кода. Техника для случаев, когда вы хотите что‐то вроде HashMap<K, V>
со всеми его функциями, но в C — т.е. когда кода достаточно много, чтобы вас волновало удобство работы с ним.
rt thread можете посмотреть. По моему реализация этой rtos для эмбедед очень напоминает методы, описанные в статье.
Можно, например если делать по книжке (близко к авторскому решению, но без самопальной vtable) Design Patterns for Embedded Systems in C, Bruce Powel Douglass, 2011. С практической точки зрения это имеет смысл где-ть на stm32Н7 или esp32 с тактовой частотой от 50МГц и памятью от 512КБ для модульного оборудования или расширяемого ПО. Ну или если не торопитесь и риалтайм не нужен :)
В далеком 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 или до конца форматной строки.
Плюсы:
можно печатать структуру, задавая ее в форматной строке
можно печатать сразу несколько структур (нет какого-то буфера, который затрется)
Минусы:
копирование форматной строки и аллокация
А можно просто использовать С++...
Подзабыл Си. ;-( Но есть возможность вспомнить!
Вы написали довольно понятный код. Не могли бы Вы пояснить пару моментов?
Что такое PRIu32?
В вашей реализации получается, что разные экземпляры одного и того же объекта (структуры) всё своё носят с собой (например, указатели методы). Не так ли?
Как работает функция realloc? Она просто пытается расширить область памяти под объект без потери ранее построенных объектов?
До таблицы виртуальных функций так и не дошли (да и вообще пошли в другом направлении) - т.е. чем больше функций тем больше объекты.
А какой компилятор Вы использовали? Хотелось бы проверит по шагам Ваше решение.
gcc и clang.
мне кажется, и msvc должен проканать, я старался не завязываться на особенностях компиляторов. но если вдруг не проканает, напишите
Как выстрелить себе в ногу на языке C
Использует буфферы... Я не спец, но может стоит использовать структуру на куче (в C ведь есть нормальный string)? Или хотя бы проверять длину строки, которая в буфер лезет?
А не проще generic юзать? Стандартный _Generic уже 11 лет с нами, а в gcc и clang generic через built-inы и того раньше появились.
Ой вей. Именно так и появился GTK.
Нужно ООП - пиши на плюсах.
Хочешь поупражняться в сверхуме - пиши на Аде.
На Си программа должна быть "палка-веревка". По другому - да, вы наркоман.
Но ведь, раз макросы уже применены, можно пойти "в лоб" и сделать в объекте указатель на структуру-класс, в которой и будут объявлены методы, а также указатель на родительский класс, ну и вызывать макросом, совсем избавившись от дублирования имени в вызове.
Зачем привязываться к смещениям для вызова реализации из наследника? Можно объявить нужные "абстрактные" методы в базовом объекте, с реализацией по умолчанию (на всякий случай).
И, как выше упоминали, у вас получаются жирные объекты за счет того что они несут все функции в себе. Это прототипное ООП (как в JS), иногда имеет смысл конечно, но при более сложной модели стоит выделить такие функции в мета-объекты (классы) и держать единственную ссылку на класс в каждом объекте.
Абстракции и наследование в Си — стреляем по ногам красиво