Реентерабельный дизайн библиотеки на C

Допустим, я создаю библиотеку для запуска quux на C.

Для успешного запуска Quux нужны две переменные состояния:

static int quux_state;
static char* quux_address;

/* function to spork quuxes found in a file, 
   reads a line from the file each time it's called. */
void spork_quux(FILE*);

Если я сохраню эти данные как глобальные переменные, только один клиент сможет запускать quux за один раз, иначе переменные состояния будут искажены вторым вызывающим абонентом, и может произойти катастрофа.

Вопрос в том, как лучше всего разработать реентерабельную библиотеку на C?

Я рассмотрел следующие случаи, но не пришел к удовлетворительному выводу.

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

/* library handles all state data allocation */
static int* quux_state; 
static char** quux_address;

В следующем случае клиент может возиться с состоянием, что очень нежелательно.

/* let each client store state */
typedef struct { int state; char* address; } QuuxState; 
QuuxState spork_quux(FILE*);

Итак, как это сделать правильно?


person Vinko Vrsalovic    schedule 13.07.2010    source источник


Ответы (3)


Используйте структуру, но не раскрывайте определение клиенту.

т.е. в заголовочном файле .h поместите:

typedef struct QuuxState QuuxState;

QuuxState *spork_quux(FILE*);

и в файле реализации .c:

struct QuuxState
{
    int state;
    char* address;
};

QuuxState *spork_quuxFILE *f)
{
    QuuxState *quuxState = calloc(1, sizeof(*quuxState));
    if (!quuxState)
        return NULL;

    quuxState->state = ....;

    ........

    return quuxState;
}

Преимущества такого подхода в том, что:

  1. вы можете изменить содержимое структуры без перекомпиляции всего клиентского кода
  2. клиентский код не может получить доступ к членам, т.е. quuxState->state выдаст ошибку компилятора
  3. структура QuuxState по-прежнему будет полностью видна отладчику, так что вы сможете тривиально видеть значения и устанавливать точки наблюдения и т. д.
  4. литье не требуется
  5. тип, который вы возвращаете, является определенным типом, поэтому вы получите некоторый компилятор, проверяющий, что передается правильная вещь (по сравнению с указателем void *)

Единственным недостатком является то, что вам нужно выделить блок памяти, однако, если ваша библиотека делает что-то нетривиальное (и если она выполняет файловый ввод-вывод, это, безусловно, нетривиально), накладные расходы на один malloc незначительны.

Возможно, вы захотите переименовать приведенную выше функцию во что-то вроде «QuuxSpork_create» и добавить дополнительные функции для выполнения построчной работы и уничтожения состояния после того, как вы закончите.

void QuuxSpork_readLine(QuuxState *state)
{
    ....
}

void QuuxSpork_destroy(QuuxState *state)
{
    free(state);
}

Случайным примером библиотеки, которая работает примерно так, может быть библиотека потоков POSIX, pthreads.

person JosephH    schedule 13.07.2010
comment
Re: «недостаток»: код может статически выделить массив структур struct QuuxState и каждый раз возвращать новый, пока он не закончится. Когда программа завершает работу со своим quux, она вызывает unspork_quux() или что-то еще, чтобы вернуть значение. Это устанавливает конечное ограничение на количество quux, которые могут быть запущены одновременно. Это все еще проблема, тогда вы можете динамически выделять после исчерпания статического распределения, подвергая (номинальному, а не практическому) риску неопределенного поведения при проверке того, находится ли возвращаемый указатель в пределах статического массива. - person Jonathan Leffler; 13.07.2010
comment
Спасибо! Это определенно способ, который будет работать — конечно, будут применяться обычные правила о предотвращении преждевременной оптимизации и сохранении простоты кода, но если вы используете этот шаблон в тесном цикле или иным образом очень критичен к производительности (или во встроенной системе), предварительно распределенный подход то, что вы описываете, определенно было бы разумным и, вероятно, необходимым. - person JosephH; 13.07.2010
comment
+1 Это почти стандартный шаблон, используемый в C для инкапсуляции объектов. Вы увидите, что его используют повсеместно. Тоже неплохо объяснил. - person JeremyP; 13.07.2010
comment
Кстати, если вы обнаружите, что выделяете и освобождаете много Quux, вы можете добавить указатель на Quux в определение структуры и вместо того, чтобы освобождать их, добавить их в список неиспользуемых Quux. Когда вам нужен новый, просто удалите его из верхней части списка, если он не пуст, в этом случае просто выделите новый. - person JeremyP; 13.07.2010

Используйте структуру, но не говорите клиенту, что это структура. Передайте непрозрачный указатель — void*, или еще лучше указатель на пустую фиктивную структуру — и приведите его обратно, когда это необходимо.

person Seva Alekseyev    schedule 13.07.2010
comment
Или указатель на неполный тип, т.е. поставить struct quux; в свой заголовочный файл без определения, а потом вернуть struct quux *. Определение может оставаться в частном заголовочном файле, используемом/видимом только для кода вашей библиотеки. - person R.. GitHub STOP HELPING ICE; 13.07.2010

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

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

void spork_quux(FILE*, QuuxState **);
person Rannick    schedule 13.07.2010