Можно ли написать обработку исключений с нулевой стоимостью в C?

Компилятор

g++ имеет функцию обработки исключений с нулевой стоимостью. Насколько я понимаю, try ничего не делает, но когда генерируется исключение, выполняется подпрограмма для обработчика исключений. Вот так:

void foo() {
    try {
        bar(); // throws.
    } catch (Type exc) {
        baz();
    }
}

В псевдокоде (c-стильный) будет выглядеть так:

void foo() {
    bar();
    return;
catch1_Type:
    baz();
}

bar() выбрасывает. Процедура исключения выполняет следующие действия:

Ah, обратный адрес находится в функции foo()! И обратный адрес находится в первом блоке try-catch, и мы бросаем тип Type, поэтому процедура обработчика исключений находится по адресу foo + catch1_Type. Поэтому очистите стек, чтобы мы оказались там!

Теперь мой вопрос: есть ли способ реализовать его в C? (Может быть C99 или новее, хотя меня интересует диалект C, поддерживаемый gcc). Я знаю, что я могу использовать, например, libunwind для проверки стека и обхода, хотя я понятия не имею, как получить адрес метки catch1_Type. Это может быть невозможно.

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

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

И чтобы быть понятным - цель этого вопроса - избежать подхода setjmp/longjmp.

EDIT: я нашел довольно крутую идею, но не работает полностью:

Вложенные функции в gcc. Что они могут сделать?

  • имеют доступ к локальным переменным,
  • есть возможность получить локальные метки в родительской функции!
  • может быть вызван вызовами нашей функции при условии, что мы передаем указатель на вложенную функцию, поэтому она доступна указателем в вызываемом абоненте.

Даунсайд, который мешает мне делать что-то нулевое:

  • они оптимизированы даже на уровне -O0, если они не используются. Могу я что-нибудь сделать? Если бы я мог, я мог бы получить адрес по имени символа, когда будет выбрано исключение, и он просто выполнит выполнение исключений, которые стоили бы nohing, когда они не были выбраны...

Ответ 1

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

  • gcc позволяет вложенные функции, которые имеют доступ к локальным переменным родительской функции и могут получить метки в родительской функции!
  • gcc не будет генерировать код для внутренней функции, если он определен, но не указан. Вы можете определить встроенную функцию no-op, которая получает указатель на локальную функцию и вызывает ее в родительской функции. Это заставит генерировать код для внутренней функции, и это будет нулевая стоимость (при более высоких уровнях оптимизации будет отключен встроенный вызов no-op). Возможно, что в более поздней gcc-оптимизации встроенный вызов предотвратит создание кода для внутренней функции, хотя..
  • Плохая вещь, я не вижу способа заставить вложенные (внутренние) функции быть глобальными символами. Они всегда локальны, поэтому нет возможности получить адрес с помощью dlsym.
  • другие плохие новости, если программа использует такие вложенные функции, valgrind сбой;) Мне удалось проверить мою простую тестовую программу, но я не смог использовать valgrind, чтобы проверить, нет ли нарушений памяти.

Код, который я использовал для проверки, имеет очевидный недостаток: это не "нулевая стоимость", поскольку глобальный указатель на процедуру обработки исключений должен быть установлен во время выполнения функции. Если бы мы могли сделать это время компиляции!

Ну, в конце концов, если кто-то хочет правильно использовать внутреннюю функцию, например, передать указатель на него в вызовы, чтобы они могли вызвать его в случае сброшенных исключений, мы, вероятно, могли бы очень быстро обрабатывать исключительные ситуации, намного, МНОГО быстрее чем setjmp/longjmp...

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

#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>

typedef void (*catch_routine)(void*);

catch_routine g_r = NULL;

void tostr_internal(char* str, int a)
{
    int result = a + 'a';
    if (result < 'a' || result > 'z')
    {
        // handle exception
        if(g_r)
        {
            g_r(&a);
        }
        else
        {
            fprintf(stderr, "Exception not caught!");
            abort();
        }
    }
    else
    {
        str[0] = result;
        str[1] = '\0';
    }
}

char* tostring(int a)
{
    __label__ exhandler;
    char* string = (char*)malloc(2*sizeof(char));

    void personality(void* exid) {
        fprintf(stderr, "Number %d is not a character!\n", *(int*)(exid));
        free(string);
        goto exhandler;
    }
    g_r = personality;

    tostr_internal(string, a);
    return string;

exhandler:
    return NULL;
}

int main(int a, char** b)
{
    int i = 0;

    for(i = 0; i < 10000; i++)
    {
        int trythisbastard = i % 95;
        char* result = tostring(trythisbastard);
        if (result)
        {
            fprintf(stderr, "Number %d is %s\n", trythisbastard, result);
            free(result);
        }
    }

    return 0;
}

Ответ 2

Я использовал эту статью, когда мне понадобилась функция try/catch как functionnality в C.

http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

Как уже заметили некоторые, это клей вокруг setjmp longjmp