Проектиране на реентрантна библиотека в 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* указател)

Единственият недостатък е, че трябва да разпределите блок памет - но ако приемем, че вашата библиотека прави нещо, което не е тривиално (и ако прави I/O на файлове, това със сигурност е нетривиално), режийните разходи за един 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 структури и да връща нов всеки път, докато не се изчерпи. Когато програмата приключи със sporking своя 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