Devo affermare le precondizioni delle funzioni in un'API pubblica?

3

Sto scrivendo una libreria per alcune strutture dati in C che verranno utilizzate nei sistemi embedded. Ho avuto problemi nel progettare e creare un solido piano di gestione degli errori. Questa API è solo soggetta ad errori logici ed è per questo che sono così conflittuale. Con questo voglio dire che le precondizioni potrebbero essere: "x! = NULL" o "indice < dimensione del contenitore".

Facendo molte ricerche sui forum qui, sembra che ci siano diversi approcci per la gestione degli errori in C:

  1. Usa errno. Credo che il sentimento generale sia errno dovrebbe essere evitato a tutti i costi, e la sua filosofia di design è superata, quindi questo è un no-go.
  2. Utilizza i codici di errore. Ogni funzione può restituire codici di errore oppure l'utente può passare un puntatore per i codici di errore da assegnare. Credo che restituire codici di errore sia molto più elegante di quest'ultimo, ma alcune funzioni risulteranno goffe perché l'utente dovrà passare un puntatore per ottenere l'output della funzione. Una delle funzioni è "get_index_of_object". Puoi vedere come sarebbe tutto meno che conveniente.
  3. Usa asserimenti che saranno disabilitati nella produzione di produzione. Questo è quello a cui mi sto proponendo fin d'ora, dal momento che queste strutture di dati saranno utilizzate così spesso, l'aumento delle prestazioni di avere i disertori disabilitati nella creazione di produzione potrebbe essere notevole (anche se non ho dati per supportare questo sin d'ora). Come accennato in precedenza, questa API può essere influenzata solo da errori logici (utenti che non rispettano le precondizioni delle funzioni). Per quanto ne so, si incoraggiano gli errori logici ai fini del debug, ma è buona norma usare gli asserti per convalidare gli argomenti delle funzioni in una API pubblica? Soprattutto quando sarà usato nei sistemi embedded?
  4. Verifica le condizioni preliminari e torna presto se vengono violate. Il mio più grande problema con questo approccio è il debugging potrebbe essere un incubo per qualcuno anni dopo. Alcune funzioni potrebbero tornare, diciamo "NULL", ma altre come "get_index_of_object" non possono restituire un valore che descriva l'errore.
  5. Utilizzare una combinazione dei quattro metodi di gestione degli errori di cui sopra. Vorrei evitare il gonfiarsi del codice e mantenere le cose il più comode possibile, ma questa è un'opzione.

Sono curioso di sapere cosa hai da dire.

    
posta Michael Joseph 21.12.2017 - 21:15
fonte

4 risposte

4

Parlando come sviluppatore per piccoli sistemi embedded, vorrei fare affermazioni su una qualsiasi delle altre opzioni.

Se lo spazio per il tuo codice è limitato, puoi facilmente entrare in una situazione in cui ogni byte conta. In tale situazione, afferma (in particolare la macro assert ) ha il vantaggio di poter disabilitare i controlli di integrità al punto che non contribuiscono alle dimensioni del codice.
Ovviamente, ciò dovrebbe essere fatto solo quando si è certi che il codice chiamante non infrange i contratti dell'API, ma che spesso può essere verificato in un ambiente più sfavorevole.

Nella maggior parte degli ambienti di sviluppo incorporati, la macro assert ha il vantaggio aggiuntivo di trasferirti immediatamente a un punto di interruzione nel debugger quando l'asserzione fallisce, così puoi immediatamente ispezionare il motivo dell'asserzione fallita.

Le linee guida generali per non utilizzare gli asserti nelle API pubbliche sono più orientate verso situazioni in cui l'input dell'utente non pubblicizzato può raggiungere l'API (ad esempio, un'API Web) o dove l'implementazione dell'API non è disponibile per il programmatore chiamandolo (ad esempio, una libreria closed-source, binary-only).
Nel mondo embedded, persino le librerie proprietarie sono generalmente fornite con il codice sorgente disponibile perché la variabilità nei processori e negli strumenti rende proibitivo distribuire i binari per tutti loro.

    
risposta data 22.12.2017 - 12:42
fonte
4

Questo è un malinteso comune:

To my understanding, asserting logic errors for debugging purposes is encouraged,...

Nonostante molti sviluppatori li utilizzino a tale scopo, le affermazioni assolutamente non sono intese per il controllo logico degli errori. Se un'affermazione si attiva, significa indicare un problema con il programmatore, non con il programma. Le asserzioni sono utilizzate per convalidare le ipotesi del programmatore sulla struttura e il design del programma.

Ad esempio, supponiamo tu abbia un'API pubblica:

int get_value(void *p) {
    if (p) {
        return getValueImpl(p);
    }
    return APP_ERROR_NULL_POINTER;
}

con l'implementazione interna associata:

static int getValueImpl(void *p) {
    assert(p); /* this should have been checked already */
    return 3;
}

L'asserzione viene utilizzata per convalidare l'ipotesi del programmatore che tutti gli input delle API pubbliche siano disinfettati prima di essere passati al codice interno. Questa affermazione dovrebbe MAI, MAI FUOCO . Se lo fa, significa che qualcuno ha incasinato la scrittura di un'API pubblica che non ha disinfettato l'input dell'utente. Questo è un problema strutturale, non un errore logico.

Nel tuo caso particolare, anche gli assert non funzioneranno, anche se sono sicuro che il loro ingombro minimo è attraente in un ambiente embedded.

3.Use asserts that will be disabled in production build.

Gli errori logici si verificano in fase di esecuzione. Se la tua biblioteca non ha alcun controllo logico degli errori in atto oltre alle asserzioni che sono omesse nel codice di produzione, cosa succede se il cliente viola un contratto API allora? C'è un modo per il cliente di sapere che hanno violato il contratto in fase di runtime?

Supponiamo che tu rilasci la tua libreria come un binario collegabile con asserzioni abilitate. Se il codice client viola un contratto API, il meccanismo di asserzione bloccherà il processo del client. È il comportamento voluto ?

La cosa che è facile interpretare erroneamente sulla progettazione per contratto è il fatto che i contratti di codice sono solo applicabili all'interno della stessa base di codice. Non è possibile forzare il codice client per rispettare un contratto di codice in un'API pubblica. Tutto quello che puoi fare è pubblicare il contratto e rispondere in modo ben definito a qualsiasi violazione di quel contratto. Il modo più comune per farlo in C è restituire un codice di errore che è richiesto dal contratto (per contratto) per verificare. Se non lo controllano, questo è il loro problema, non il tuo.

Per quanto riguarda le API che "non possono" restituire un codice di errore perché già restituiscono un valore intero, il modo comunemente accettato e capito di farlo in C è di restituire il valore intero in un puntatore e il codice di errore nel valore di ritorno della funzione. Non è una questione di "convenienza" dal punto di vista degli sviluppatori C esperti, è semplicemente come è normalmente fatto.

    
risposta data 23.12.2017 - 17:03
fonte
3

Preferisco usare gli asserzioni e lasciarli attivi per il codice di produzione. Il loro costo è più che compensato dai benefici che forniscono.

La macro assert è un modello di riferimento di semplicità ed eleganza. Si riduce essenzialmente a:

define assert(e) ((e) ? (void)0 : abort()) 

tranne che stamperà un messaggio di errore su stderr se l'asserzione fallisce. L'asserzione

assert( size <= LIMIT );

interromperà il programma e stamperà un messaggio di errore simile a questo:

Assertion violation: file tripe.c, line 34: size <= LIMIT

Questo è il risultato migliore che puoi aspettarti in un linguaggio di programmazione che non ha eccezioni. Non è richiesto alcun ulteriore sforzo di gestione da parte del chiamante (come il controllo dei codici di ritorno).

Questo articolo spiega come usare gli asserti per fornire le condizioni preliminari e le postcondizioni per le tue funzioni C , essenzialmente implementando code contracts (non è un'impresa da poco poiché C non ha alcun built-in intrinseco di gestione degli errori).

    
risposta data 21.12.2017 - 21:27
fonte
1

Non utilizzare mai asserzioni per verificare le precondizioni API pubbliche , soprattutto se sviluppi una libreria.

Non controlli come le altre persone chiamano la tua biblioteca. Ogni crash nella tua libreria è un bug in quella libreria . Una libreria non dovrebbe bloccarsi solo perché ha ricevuto input errati, dovrebbe invece restituire un errore. Pensa a test fuzz : genera un input non valido per verificare che il tuo software possa recuperare da esso, il che è il comportamento normale.

Vorrei fare un esempio: una volta ho avuto un arresto anomalo in Microsoft Word durante l'apertura di un documento specifico. OpenOffice in quel momento potrebbe aprirlo e mi ha aiutato a recuperare i miei dati. Anche se avessi un'affermazione in Word che diceva che il file era mal formato, quale programma pensi abbia fatto un lavoro migliore? Questo non è specifico per il mondo desktop. Ho lavorato su sistemi embedded (Set Top Boxes) in cui disponevi di file multimediali da elaborare e visualizzare: non ti è consentito arrestarti in modo anomalo perché un file multimediale è corrotto, in peggio devi solo attivare un errore e non riprodurre il file multimediale.

Pensi che affermare aiuta a raggiungere il punto in cui si è verificato l'errore? Usa la registrazione e otterrai lo stesso effetto, ma senza arresti forzati. L'affermazione non distingue tra errori recuperabili e non recuperabili: si interrompe sempre. Tuttavia, la registrazione ti consente di sapere dove e perché non ha funzionato e ti consente di decidere se puoi continuare o meno.

Pensa al tuo cliente che vuole che la sua app sia in esecuzione e ogni volta che si verifica una determinata condizione, si blocca. Penso che il cliente potrebbe preferire avere qualche registro o errore e il resto della domanda riprendere se l'errore non è fatale. Non vuoi alcun incidente nel codice della produzione e un asserito è un crasher intenzionale. Se le asserzioni sono disabilitate in produzione, allora peggiora: la tua intera logica di gestione degli errori è scomparsa e invece di bloccarsi con un messaggio di asserzione che stai andando in crash (forse molto più tardi) senza messaggi, solo perché non hai rilevato male input e ha continuato a elaborarlo.

Se stai sviluppando la tua applicazione, potresti voler usare assert per controllare i tuoi bug, tuttavia, nelle tue chiamate interne, ma per una libreria è un no-no.

Come per le tue opzioni:

  1. errno: Sono d'accordo che errno non si adatta alle attuali pratiche di programmazione, specialmente con il multithreading
  2. Il codice di ritorno o il codice di ritorno + puntatore all'errore è buono. Questo è ciò che fa GLib . Il puntatore all'errore è facoltativo ma può essere utilizzato per ottenere un messaggio pertinente e restituire l'errore nei livelli precedenti. Nel cas del tuo "get_index_of_object", non è nemmeno necessario: un indice non è mai negativo, quindi è sufficiente restituire -1 per dire che il tuo oggetto non è stato trovato.
  3. Disabilitazione degli asserti in produzione: questo interrompe la logica di registrazione, poiché le persone non eseguono il logging + assert, penseranno che l'asserzione sia sufficiente. Di nuovo, è necessario accedere alla produzione ed evitare arresti anomali a tutti i costi, quindi non si può fare affidamento su asserzioni.
  4. Restituire in anticipo è ciò che fa il GLib. Guardate g_return_if_fail o g_return_val_if_fail . Si desidera avere una traccia di errori di runtime: ancora una volta utilizzare accedendo , con diversi livelli ( debug, avviso, informazioni, errore). E se questo errore è considerato fatale e non può essere recuperato, allora sì, ferma .
  5. Sii pragmatico. A volte il codice di errore è sufficiente, a volte no e avrai bisogno di una posizione per un codice di errore e un messaggio di errore più specifici . A volte usi funzioni che restituiscono errori a errno e dovranno affrontarlo internamente, ma possono racchiudere gli errori prima di riportarli al livello precedente.

Tutto sommato, ti consiglio di dare un'occhiata alle librerie GLib e GTK +. Ho imparato molto guardando il loro codice sorgente.

    
risposta data 15.01.2018 - 16:32
fonte