LINUX.ORG.RU
ФорумTalks

Расширенный Си

 , , , ,


2

2

Не так давно я презентовал здесь свой велосипед - c-oop-gen: ООП в Си. А именно генератор сишного кода в ООП стиле из XML описания. Тогда мне объяснили, что XML не лучшая идея для представления программного кода. Так что встречайте мой новый велосипед - https://github.com/KivApple/c_ext.

Это что-то вроде препроцессора для Си, который добавляет в язык несколько новых конструкций.

Все эти конструкции связаны с определением структур. Во-первых, структуры теперь можно наследовать.

struct A {
    int a;
};

struct B: A {
    int b;
};

При этом очень немаловажен тот факт, что указатель на B совместим с указателем на A (но не наоборот). Под капотом там незаметно вставляется приведение типов, если оно необходимо.

Во-вторых, у структуры теперь могут быть методы:

struct A {
    int f();
};

int A::f() {
    return 100500;
}

Инлайнить (реализовывать методы прямо внутри структуры) нельзя, но я считаю, что это не очень нужно, ибо это таки С, а не С++ и программист должен явно определять, в каком исполняемом модуле окажется функция (а если очень хочется, может описать её как static в момент реализации и поместить в заголовочный файл).

Всем нестатическим методам автоматически передаётся указатель на структуру this, с которым можно свободно работать.

В-третьих, у структур теперь могут быть статические члены:

struct A {
    static int i;
    static void test();
};

int A::i;

void A::test() {
    ...
}

В-четвёртых, у структуры могут быть виртуальные методы. Правда, чтобы они работали, нужно обязательно объявить конструктор - нестатичный метод с именем construct, а также вызвать его до любого вызова виртуального метода.

В-пятых, можно явно обратиться к родительской реализации. Как-то так:

struct A {
    void f();
};
struct B: A {
    void f();
};

void B::f() {
    this->A::f();
}

Данный транслятор должен понимать большинство gcc-змов. Во всяком случае я проверил его на нескольких стандартных заголовочниках (а они полны gcc-змов) и он всё скушал, а на выходе получился валидный код. Однако крайне не рекомендуется применять всякие атрибуты к структурам и их членам, если вы используете новые возможности (если нет, то определения не будут изменены, а вот если транслятор в них уж полез, то он может съесть что-то неподдерживаемое).

Также транслятор сам по себе понимает директивы #line и генерирует свои после окончания обработки. То есть ему можно подавать на вход результат работы любого препроцессора (cpp, m4 и т. п.), а в сообщениях об ошибках компилятора будут адекватные указания, где произошла ошибка.

Несмотря на то, что транслятор имеет достаточно информации для того, чтобы отлавливать многие ошибки (например, неопределённые идентификаторы или несовместимость типов), он старается этого не делать и падать только на тех ошибках, которые специфичны именно для него (например, попытка унаследоваться не от структуры).

Логика простая: компиляторы обычно выдают очень хорошие сообщения об ошибках, а мои сообщения могут оказаться недостаточно информативными. К тому же мой транслятор выходит после первой же ошибки, а большинство компиляторов выводят все найденные. А ещё я могу воспринять за ошибку какой-то расширение компилятора. Так что я просто стараюсь оставить насколько это возможным неизменными все непонятные места.

В общем, вот такие дела. Жду ваших комментариев.

Особенно меня интересует вопрос: что делать с конструкторами, собственно? Можно вызывать их в C++ стиле - то есть в момент объявления переменной (или выделения памяти). Это хорошо, конечно, что никто не забудет вызвать конструкторы, но как-то недостаточно Plain C-way. А что если мы хотим выделить память, а конструктор вызывать лишь спустя какое-то время? От ответа на этот вопрос зависит и судьба деструкторов - если конструктор гарантированно вызывается, то мне не проблема автоматически вызвать деструктор. Но в текущем виде автоматическому вызову деструктора не место (а вдруг объект не был сконструирован?).

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

P. S.: Планы на будущее: поддержка лямбд и асинхронного программирования.

UPD1: Добавил поддержку анонимных функций, в том числе с захватом переменных из текущего контекста по ссылке или по значению. См. README.

UPD2: Добавил начальную поддержку самого главного, ради чего всё затевалось. А именно - поддержку асинхронного программирования. Теперь можно вызвать функцию, возвращающую void и ожидающую делегата в качестве последнего аргумента (у меня делегат - это любая структура, у которой первое поле это указатель на функцию, которая первым аргументом принимает указатель на эту структуру), не указывая значение для этого самого аргумента. В результате случится магия и текущая функция станет асинхронной (функция, которая подтвергается данной метаморфозе, должна возвращать void и не принимать переменное число аргументов). Переменные, которые используются одновременно по разные стороны от асинхронного вызова будут сохранятся в специальную структуру состояния. Сама структура состояния будет создана с помощью malloc при входе в асинхронную функцию и освобождена при выходе (будь то естественное завершение или выход с помощью return). Пока в реализации есть косяки, но я их исправлю. Внутри асинхронной функции категорически не рекомендуется использовать goto, ибо это сломает алгоритм определения какие переменные нужно сохранять при асинхронном вызове.

★★★★★

Это делается просто из интереса, или есть причины реализовать такое вместо писания на плюсах в стиле «си с классами», если очень ООП охота?

evilface ★★ ()
Ответ на: комментарий от evilface

Это делается просто из интереса, или есть причины реализовать такое вместо писания на плюсах в стиле «си с классами», если очень ООП охота?

+1

intelfx ★★★★★ ()
Ответ на: комментарий от evilface

1) Интересно

2) Нормального асинхронного программирования в C++ нет. Нормального, в смысле когда нет лапши из функций обратного вызова, а просто пишешь обычный линейный код, а он превращается в лапшу уже под капотом (ну или в конечный автомат, тут разные подходы возможны). Есть protothreads, но они недостаточно функциональны (например, нельзя отправить функцию в ожидание из вызванной функции, только из самой функции, которая описана как protothread, не сохраняются локальные переменные). В новом стандарте планируют добавить данный функционал, но когда это ещё будет.

3) Бесплатные компиляторы C++ существуют не для всех платформ.

KivApple ★★★★★ ()

ой... ты изобрел C++? Здорово, молодец.

onon ★★★ ()

Т.е. генерацию кода всё-равно будет делать сторонний Си-компилятор?

P.S.Во времена Turbo Vision такое может быть и взлетело бы.

pacify ★★★★★ ()
Ответ на: комментарий от KivApple

непонятно, чего там плюсах «нет». там есть всё, что есть в С (ну, за исключением пары совсем мелких и несущественных фич последнего С11). плюс там есть ООП, который пользовать необязательно. плюс он по вызовам прекрасно совместим с сишными библиотеками и все сишные функции там доступны.

Iron_Bug ★★★★★ ()
Ответ на: комментарий от KivApple

3) Бесплатные компиляторы C++ существуют не для всех платформ.

Для какой нет GCC? Не троллинг. Просто интересно.

NULL ()
Ответ на: комментарий от Iron_Bug

Я не говорил, что в Си есть то, что мне нужно. Про то чего нет в C++ (и конечно же нет в Си) - я говорил про resumable-функции, которые вроде как планируют добавить в C++17. Добавление данного функционала в этот препроцессор как раз входит в мои планы.

KivApple ★★★★★ ()
Ответ на: комментарий от pacify

Компиляторы Си существуют под все хоть чуть-чуть актуальные платформы. Различается уровень поддержки ими новых стандартов, однако ничто не мешает мне преобразовывать новый синтаксический сахар в другой вид (возможно, в будущем добавлю опции типа «генерировать C89 код»). Даже LLVM не может похвастаться такой распространённостью.

Если бы мне был нужен просто «Си с классами», то было бы логично написать гору макросов для какого-нибудь M4 и препроцессить исходник, но я планирую добавить и другие фичи, в том числе те, которых нет и в C++ (в синтаксисе тех же структур я конечно же оглядываюсь на С++), а для них уже нужно побольше информации, чем у простых макросов.

Во-первых, это поддержка асинхронного программирования. В этом случае функция будет прозрачно дробиться на кучу отдельных функций, принимающих указатель на данные (а для этого как раз нужно знать все используемые внутри функции символы и в каких частях какие используются). Что-то вроде:

void f() {
    char buffer[100];
    file.read(buffer, sizeof(buffer));
    printf("%s\n", buffer);
}

Которое превращается в:

void FileStream_read(FileStream *stream, void *buffer, size_t count, void (*callback)(void*), void *callbackArg);
...
struct State {
    char buffer[100];
};

void f() {
    struct State *state = malloc(sizeof(State));
    f1(state);
}

void f1(struct State *state) {
    FileStream_read(&file, state->buffer, sizeof(state->buffer), f2, state);
}

void f2(struct State *state) {
    printf("%s\n", state->buffer);
}

Ну это я схематично обрисовал. Я ещё продумываю механизм как всё это будет работать.

Помимо этого ещё хочу сделать что-то типа C++ шаблонов, но с большими возможностями. В смысле, что должно быть возможно налету генерировать имя функции (не MyClass<int, int>::myFunc, а произвольное в том числе) и некоторые другие мелочи.

KivApple ★★★★★ ()
Последнее исправление: KivApple (всего исправлений: 1)
Ответ на: комментарий от andreyu

c-front не поддерживается и как следствие нельзя ожидать от него появление поддержки новых стандартов Си. Так что нет лямбд и там точно не появятся resumable-функции.

KivApple ★★★★★ ()
Ответ на: комментарий от ls-h

Его судьба весьма туманна. Вроде как тут проскакивали сообщения о том, что сами гномеры сомневаются в его нужности.

Хотя сама по себе задумка хороша. Однако он слишком сильно привязан к GObject. Конечно, там можно отключить это, но тогда пропадает куча плюшек. Если посмотреть как сделан порт для AVR, то можно заметить, что там по факту просто захардкодили всякие специфичные опции. Мой же подход (расширять Си) позволяет особо об этом не думать (главное не съедать всякие __attribute__ и не падать на них) - все платформоспецифичные вещи люди просто будут делать как раньше, я лишь добавил универсальный синтаксический сахар.

Вообще мой принцип - транслятор не должен добавлять зависимостей от каких-либо библиотек. Подобно тому как работает сам С++, у которого линковать какие-то библиотеки требуется только для работы исключений (но их весьма проблематично сделать без этого), а все остальные плюшки языка работают сами по себе. Vala же изначально разрабатывался как генератор GObject-style кода.

KivApple ★★★★★ ()
Последнее исправление: KivApple (всего исправлений: 1)
Ответ на: комментарий от der_looser

Почему гномерам можно разрабатывать Vala для своей мега-библиотеки GObject, а мне расширение Си для своего фреймворка для микроконтроллеров нельзя?

KivApple ★★★★★ ()
Ответ на: комментарий от KivApple

c-front не поддерживается и как следствие нельзя ожидать от него появление поддержки новых стандартов Си. Так что нет лямбд и там точно не появятся resumable-функции.

С выходом с++11 это уже и не актуально.

andreyu ★★★★★ ()
Ответ на: комментарий от Stil

Проблема в том, что он парсит только нормальный C++. Я не смогу добавить туда какие-то свои плюшки - clang просто не распарсит исходник.

KivApple ★★★★★ ()
Ответ на: комментарий от Stil

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

KivApple ★★★★★ ()
Ответ на: комментарий от KivApple

как раз асинхронность и является моей главной целью

Почему бы тебе не прикрутить асинхронность сразу к плюсам? Пользуясь, например, <ucontext.h>?

intelfx ★★★★★ ()
Ответ на: комментарий от KivApple

Проблема в том, что как раз асинхронность и является моей главной целью, ради чего всё это затевалось.

Не понимаю, кому захочется иметь с этим дело добровольно. Приходилось работать с async/await, так я до сих пор плююсь при виде одних только этих слов. У них есть существенная проблема: классы обладают инвариантами, которые выполняются только до и после вызовов их методов. await посередине всё ломает, так как управление может (и это естественно происходит) вернутся в объект с поломанным внутренним состоянием, после чего всё летит к чертям; аналогично управление уходит в другое место до восстановления инвариантов. Итого, эта штука заставляет оглядываться на каждой строчке кода, вместо начала/конца методов. Оно просто несовместимо с объектами с изменяемым состоянием и создаёт проблемы на пустом месте. Я уже не говорю о том, что отлаживать эту неявную лапшу — сущий ад. А ещё и на микроконтроллерах...

xaizek ★★★★★ ()
Последнее исправление: xaizek (всего исправлений: 1)
Ответ на: комментарий от KivApple

Подобно тому как работает сам С++, у которого линковать какие-то библиотеки требуется только для работы исключений

А как же libstdc++?

CYB3R ★★★★★ ()
Ответ на: комментарий от CYB3R

STL - это стандартная библиотека. По факту это просто обычный набор исходников, который можно откуда-нибудь скачать или даже написать своё и скомпилировать. Я имею ввиду, что конструкции самого языка (а не то, что объявлено в каких-нибудь инклюдах) не должны полагаться на какие-то внешние библиотеки.

KivApple ★★★★★ ()
Ответ на: комментарий от xaizek

Можно пример проблемного кода?

И чем это всё отличается от, например, лапши из callback-ов? А такой стиль программирования вполне имеет место быть. В том же NodeJS это вообще дефолтный стиль. Я имею некоторый опыт с разработкой на сервером JS с подобными проблемами не встречался. Да, были неприятные моменты, но не то, что ты описал.

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

KivApple ★★★★★ ()
Ответ на: комментарий от KivApple

Тогда я чего-то не понимаю. Конструкции Vala полагаются на GObject? Если да, то не является ли GObject обычным набором исходников, который можно откуда-нибудь скачать или даже написать своё и скомпилировать.

CYB3R ★★★★★ ()
Ответ на: комментарий от CYB3R

Если при написании программы на C++ я не буду использовать STL и исключения, то моя программа будет отлично работать где-угодно, где есть только libc. Если не использовать libc, то моя программа будет работать вообще где-угодно. Если писать программу на Vala, то по дефолту она не запустится без GObject, даже если я явно не использовал ничего из него. Если немного постараюсь, то я таки смогу заставить работать Vala без GObject, однако при этом отвалится половина плюшек языка. Чтобы эти плюшки заработали мне придётся писать плагин для компилятора Vala, что требует совсем других знаний, нежели просто программирование на C++ без STL.

KivApple ★★★★★ ()
Ответ на: комментарий от KivApple

Да ладно?
Не требуют полноценный GObject:

  • Vala и Genie
  • Импорт VAPI
  • Compact classes
  • String processing
  • List и SList
  • Exceptions
  • Delegates
  • Inheritance
  • Structs
  • Lambdas
  • Closures

Требуют полноценный GObject:

  • Abstract Classes
  • Interfaces
  • Async methods
  • Non-compact classes
  • Regular expressions
CYB3R ★★★★★ ()
Ответ на: комментарий от CYB3R

Признаю, был не прав.

Но у Vala есть ещё пара фатальных недостатков:

1) Неоптимальность полученного сишного кода. А именно - каждое промежуточное вычисление внутри выражения помещается в отдельную переменную.

Ладно, допустим, компилятор Си это соптимизирует (поместит все эти переменные в регистры и по факту получится то же самое, что и без них). Но в случае асинхронного программирования это становится проблемой, потому что Vala помещает все эти временные переменные в структуру состояния асинхронной функции. Скомпилировал тестовый пример про асинхронный ввод-вывод и получил в сишном исходнике структуру с десятком всяких разных _temp_X_. А такое уже ни один компилятор не сможет соптизировать - ведь нельзя же выкидывать поля из структуры, как и запись значений в них (то есть мы будем зря хранить в памяти лишние данные, а ещё зря писать в эти данные всякие промежуточные значения вычислений, хотя они не должны были бы уйти дальше регистров процессора).

2) Vala не создаёт #line директивы. А это в свою очередь делает затруднительной отладку. Мой транслятор привязывает каждую строчку сгенерированного исхода к строчке исходного файла. В результате при отладке можно будет нормально ориентироваться по исходному файлу. Я вообще себе не представляю как должен отлаживаться код на Vala, потому что не вижу никакой возможности понять какая строчка сишного исходника соответствует какой строчке файла на языке Vala. Разве что выполнять что-то вроде декомпиляции, но это очень сложно и неоднозначно. У меня разве что в случае асинхронности придётся написать какой-нибудь плагин для GDB, тут же никакие плагины не помогут, причём для всего кода, а не для отдельных фич.

KivApple ★★★★★ ()
Последнее исправление: KivApple (всего исправлений: 1)
Ответ на: комментарий от KivApple

1) Конечно, иногда генерируется неоптимальный код, но в большинстве случаев компилятор всё оптимизирует. Да, программы на Vala будут немного больше, чем на C, будут потреблять больше памяти, но на скорость работы это влияет незначительно. Есть C++ и, если сравнивать с ним, Vala выигрывает по всем параметрам.
2) Создаёт, отладка — одно удовольствие. Ключ -g добавь.

CYB3R ★★★★★ ()

На что только не идут люди, чтобы не использовать Rust.

tailgunner ★★★★★ ()
Ответ на: комментарий от KivApple

Можно пример проблемного кода?

Любой класс с полями и методом, который вызывается извне два раза подряд. Но второй вызов выполняется до того как первый закончился. Структурно как-то так:

class Class
{
...
public:
    void doIt()
    {
        busy = true;
        await Something();
        // busy is true at this point, right?
        busy = false;
    }

private:
    bool busy = false;
};

Class c;
c.doIt();
c.doIt();

И чем это всё отличается от, например, лапши из callback-ов?

Неявностью. Для каждого callback-а каждый объект надо захватывать отдельно и думать о том, а всё ли будет хорошо. Это всё видно и прослеживается в коде. await втихаря это всё делает. Всеобщая асинхронщина в целом странная идея и проблема скорее в ней, а не в конкретной реализации будь то callback или async (но async только усугубляет). Оно недостаточно структурировано и программа становится сложной в управлении/понимании, так как управление скачет между абсолютно несвязанными функциями.

xaizek ★★★★★ ()
Ответ на: комментарий от KivApple

Про то чего нет в C++ (и конечно же нет в Си)

Настоятельно рекомендую ознакомиться с

  1. man setjmp, longjmp
  2. man makecontext, swapcontext
  3. man sigaltstack (впрочем, в libcoro это уже упоминается)
  4. http://software.schmorp.de/pkg/libcoro.html

В общем-то никакой магии нет: на каждой платформе есть вполне себе описанный ABI, соблюдая который можно добиться «resumable-функций» на самом низком уровне. С POSIX-овыми ucontext вообще на раз делается.

kawaii_neko ★★★★ ()

Добавил поддержку анонимных функций, в том числе с захватом переменных из текущего контекста по ссылке или по значению. См. README.

KivApple ★★★★★ ()
Ответ на: комментарий от xaizek

Пока вижу только, что при использовании асинхронности нужно думать примерно о том же, что и при многопоточном программировании. Если убрать await, но зато запустить два потока, которые будут вызывать doIt, то результат будет таким же (первая doIt может быть прервана в середине другой doIt, которая сбросит флаг busy). И да, в большом приложении тоже может быть не очевидно с ходу, что к методам класса может быть одновременное обращение из разных потоков. И там, и там решение - использовать что-то типа семафора. То есть если busy == true на входе в doIt, то нужно уснуть, пока он не станет равен false (разумеется, эту логику - захват с ожиданием, освобождение лучше вынести в отдельный класс).

Отсюда делаю вывод, что асинхронное программирование не более неочевидное, чем просто многопоточное.

При этом в отличии от многопоточного:

1) Во многих ситуациях я таки могут быть уверен, что между двумя await меня никто не прервёт. Поэтому не нужны всякие атомарные операции и т. д. Да и блокировок меньше становится. Например, добавление в связанный список, которое является по сути несколькими присваиваниями простых переменных не требуется защищать блокировкой.

2) Жрёт меньше памяти, потому что не надо держать отдельные стеки для каждого потока.

KivApple ★★★★★ ()
Последнее исправление: KivApple (всего исправлений: 1)
Ответ на: комментарий от kawaii_neko

Насколько я понял, всё это требует создавать отдельный стек для каждой сопрограммы. Мне же хотелось бы, чтобы в этом не было нужды. Асинхронная функция должна хранить своё состояние в специальной структуре (либо сохранять его туда прямо перед своим прерыванием и соответственно восстанавливать при возобновлении).

В терминах C++ это должно выглядеть как-то так (это просто пример, написал по-быстрому, он может быть кривым, но должен передавать суть):

void f(std::function<void(int)> done) {
    readNumber([done](int n) {
        int m = convert_number(n);
        done(m);
    });
}

То есть все функции вместо ожидания сразу же выходят, но зато вызывают callback'и, когда операция по-настоящему завершается. Идея в том, чтобы callback'ом было всё, что идёт в функции дальше.

KivApple ★★★★★ ()
Ответ на: комментарий от KivApple

О каких либах идёт речь? gcc прекрасно собирает С++ бинари которые зависят только от libc.

RazrFalcon ★★★★★ ()

Посмотрел примеры - типичный С++11. Польза сомнительная, если не считать мк.

RazrFalcon ★★★★★ ()
Ответ на: комментарий от KivApple

А теперь ознакомься с ABI x86 и x86_64 и осознай, что хранить весьма ограниченное число регистров будет намного быстрее. Не говоря уже о том, что в колбечной лапше стек трейс всегда будет абсоютно бесполезным.

kawaii_neko ★★★★ ()
Ответ на: комментарий от kawaii_neko

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

KivApple ★★★★★ ()

Снова натолкнулся на реддите на очередной расширенный Си http://libdill.org/index.html На этот раз выглядит основательнее, чем те ссылки которые удалось нагуглить по ключевикам )

foror ★★★★ ()

Добавил начальную поддержку самого главного, ради чего всё затевалось. А именно - поддержку асинхронного программирования. Теперь можно вызвать функцию, возвращающую void и ожидающую делегата в качестве последнего аргумента (у меня делегат - это любая структура, у которой первое поле это указатель на функцию, которая первым аргументом принимает указатель на эту структуру), не указывая значение для этого самого аргумента. В результате случится магия и текущая функция станет асинхронной (функция, которая подтвергается данной метаморфозе, должна возвращать void и не принимать переменное число аргументов). Переменные, которые используются одновременно по разные стороны от асинхронного вызова будут сохранятся в специальную структуру состояния. Сама структура состояния будет создана с помощью malloc при входе в асинхронную функцию и освобождена при выходе (будь то естественное завершение или выход с помощью return). Пока в реализации есть косяки, но я их исправлю. Внутри асинхронной функции категорически не рекомендуется использовать goto, ибо это сломает алгоритм определения какие переменные нужно сохранять при асинхронном вызове.

KivApple ★★★★★ ()

Прости, но ТрупСтрауса сделал всё за тебя.

PanZagloba ()
Ответ на: комментарий от Stil

Я тут придумал, как оформить асинхронность так, чтобы исходник остался корректным с точки зрения C++.

Для начала нам нужно определить некоторый класс, используемый в качестве обёртки для функции обратного вызова. Что-то вроде:

struct Callback {
    void (*func)(Callback&, bool);
    void *data;

    Callback(void (*func)(Callback&, bool), void *data): func(func), data(data) {}

    template<typename T> Callback(T&& callable) {
        struct LambdaContainer {
            T lambda;
            LambdaContainer(T& lambda): lambda(lambda) {}
            void operator()() {
                lambda();
            }
        }
        data = new LambdaContainer(callable);
        func = [](Callback& c, bool release) {
            if (release) {
                delete (T*)c.data;
            } else {
                (*(T*)c.data)();
            }
        } 
    }

    Callback(Callback& c) = delete;
    
    Callback(Callback&& c): func(c.func), data(c.data) {
        c.func = nullptr;
    }
    
    ~Callback() {
        if (func) {
            func(data, true);
        }
    }

    void operator()() {
        if (func) {
            func(*this, false);
        }
    }

    Callback& operator=(Callback& c) {
        if (func) {
            func(*this, true);
        }
        func = c.func;
        data = c.data;
        c.func = nullptr;
    }
};

Это просто набросок. Разумеется, сюда ещё можно добавить всякие конструкторы копирования и т. д. Шаблонный конструктор нужен, чтобы можно было заворачивать в эту обёртку лямбда-функции (в том числе неявно - компилятор догадается вызвать этот конструктор, если передать лямбда-функцию в качестве аргумента типа Callback), для моих целей его наличие не обязательно.

Теперь все асинхронные операции должны принимать этот объект в качестве одного из своих аргументов. В принципе это то же самое, что и традиционная пара void (*)(void*) и void*, только упакованная в одну структуру и с управлением памятью.

Для моего кода принципиален только один факт - этот объект должен иметь конструктор, принимающий указатель на функцию с данной сигнатурой и указатель на данные.

Теперь самая главная магия - делаем вот такое определение функции (обязательно без тела):

Callback yield(bool *aborted = nullptr) __attribute__((annotate("yield_func")));

Вот и всё. Теперь можно сделать так:

FileStream::readAsync(void *buffer, size_t count, Callback done);
...
void myAsyncFunc() {
    char buffer[100];
    bool fail = false;
    myFile.readAsync(buffer, sizeof(buffer), yield(&fail));
    if (!fail) {
        ...
    }
}

Это опять же просто набросок, чтобы продемонстировать саму концепцию. С точки зрения парсера и IDE всё абсолютно валидно. Более того, семантика использования корректна (выбор перегруженной функции или инстантация шаблона произойдут исходя из того, что yield возвращает Callback).

Однако на самом деле транслятор в этом месте устроит магию. А именно, сохранит все переменные используемые по разные стороны асинхронного вызова в структуру состояния, вместо реального вызова yield случится ручное создание Callback, которому будет передана текущая функция, а в качестве данных - её структура состояния.

Вроде решение выглядит достаточно элегантным. Во-первых, я не привязываюсь ни к каким конкретным именам (можно назвать Callback и yield как-угодно, поместить в любое пространство имён, требования предъявляются лишь к сигнатуре конструктора Callback и yield). Во-вторых, код будет абсолютно адекватно воспринимать Clang и любая IDE для C++.

KivApple ★★★★★ ()
Ответ на: комментарий от Stil

И ещё вопрос: что мне следует выбрать? Clang C API или Clang C++ API. Первый обеспечивает обратную совместимость. То что там нет RAII я как-нибудь переживу. Однако помимо этого ещё говорят, что Clang C API не предоставляет доступ к некоторым вещам. К каким?

В смысле, что информации, которая доступна через Clang C API достаточно чтобы сгенерировать эквивалентный сишный исходник (то есть там будет инстанцация шаблонов, лямбда-функций и т. д.) или же мне подойдёт только Clang C++ API?

KivApple ★★★★★ ()
Ответ на: комментарий от KivApple

И ещё вопрос: что мне следует выбрать? Clang C API или Clang C++ API. Первый обеспечивает обратную совместимость. То что там нет RAII я как-нибудь переживу. Однако помимо этого ещё говорят, что Clang C API не предоставляет доступ к некоторым вещам. К каким?

Я без понятий, я с ним не работал, только бложики в интернетах читал :)

Stil ★★★★★ ()
Ответ на: комментарий от Stil

Уже понял, что только Clang C++ API мне подходит. Clang C API даже на простой вызов шаблонной функции выдаёт «неподдерживаемая нода».

KivApple ★★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.