Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Programmieren allgemein (https://www.delphipraxis.net/40-programmieren-allgemein/)
-   -   DLL/Lib: Parameter Validierung (https://www.delphipraxis.net/195818-dll-lib-parameter-validierung.html)

Zacherl 28. Mär 2018 16:43


DLL/Lib: Parameter Validierung
 
Hallo zusammen,

mal ein etwas kontroverses Thema: Ich schreibe eine Library (in C; also keine Exceptions) mit einigen Funktionen, die exportiert werden (also als API für Entwickler; nicht als User-Interface). Wie gehe ich vor?
  1. Parameter durch Assertions validieren
  2. Parameter validieren und ggfls. mit Error-Code returnen
  3. Parameter zuerst mit Assertion validieren und als Fallback mit Error-Code returnen

Zu 1.:
Pro:
  • (Programmier-)Fehler während der Entwicklung schmeißen unmittelbar eine Exception
  • Kein Runtime Overhead im Release Modus
Neutral:
  • Validierung von (Benutzer-)Eingaben obliegt hier vollständig dem Nutzer der Library (müsste beim Fuzzen dann beispielsweise manuell vorgenommen werden)
Contra:
  • Assertions lassen sich abschalten
  • Fehlerhafte Parameter könnten zum Crash/Data-Corruption im Produktivbetrieb führen, wenn sie zur Entwicklungszeit z.B. einfach nicht aufgetreten sind
  • Für manche Entwickler bedeutet ein Crash/Assertion innerhalb einer Library = Bug

Zu 2.:
Pro:
  • Fehler, die während der Entwicklung nicht aufgetreten sind, könnten im Produktivbetrieb keinen großen Schaden anrichten
  • Fehler können im Produktivbetrieb sehr einfach geloggt werden
Contra:
  • Error-Codes können ignoriert werden, was dazu führt, dass (Programmier-)Fehler während der Entwicklung ggfls. nicht auffallen und behoben werden
  • Runtime Overhead durch - teilweise etliche redundante - Checks

Momentan fahre ich mit Runtime-Checks für die exportierten Funktionen und verwende Assertions nur intern. Funktioniert für mich so eigentlich, aber auf der anderen Seite machen sich die Checks zur Laufzeit bei Zeitkritischen Operationen teilweise doch bemerkbar. Außerdem wird der Code hierdurch unnötig komplex und wenn man den Gedanken mal weiterspinnt: Eine weitere Library verwendet intern meine Library, validiert aber in ihrem Public Interface auch erst noch Parameter, etc. dann werden sich die redundanten Checks irgendwann ziemlich häufen.

Eine weitere Überlegung zu diesem Thema sind Zeiger. Prüft man die explizit auf
Delphi-Quellcode:
NULL
/
Delphi-Quellcode:
nil
? Oder lässt man es gleich sein, da ein Null-Pointer nicht schlimmer ist als irgendein anderer ungültiger Zeiger (auf den man eh nicht gescheit testen kann)?

Mich würde mal interessieren, wie ihr da vorgeht. Insbesondere bei professioneller (nicht Endanwender-Software).

Viele Grüße
Zacherl

Der schöne Günther 28. Mär 2018 16:50

AW: Library: Parameter Validierung
 
Das hängt doch auch stark von der Art und Weise ab wie deine Sachen konsumiert werden. Über REST? Mit Dingen wie "nil" und Assertions hört sich das so an als gehe es direkt um eine DLL.

Wenn Geschwindigkeit dir so wichtig ist dann biete doch einfach zwei Varianten an: Eine mit Validierung, eine ohne. So etwas sieht man ja sogar in der Delphi-RTL, beispielsweise TBitConverter.From<T> vs. TBitConverter.UnsafeFrom<T> (DocWiki).

Dann kann jeder nehmen was er für richtig hält und sich selbst braucht man auch keine Vorwürfe machen dass man die Eingaben nicht vernünftig geprüft hätte.

Zacherl 28. Mär 2018 17:07

AW: Library: Parameter Validierung
 
Zitat:

Zitat von Der schöne Günther (Beitrag 1397466)
Mit Dingen wie "nil" und Assertions hört sich das so an als gehe es direkt um eine DLL.

Ja, bzw. statische Libraries (primär in C).

Zitat:

Zitat von Der schöne Günther (Beitrag 1397466)
Wenn Geschwindigkeit dir so wichtig ist dann biete doch einfach zwei Varianten an: Eine mit Validierung, eine ohne

Das ist leider nicht so einfach bei komplexen Funktionen, bei denen die Parameter teilweise erst konditional ausgewertet werden (müssen). Ohne den Code dann praktisch zu duplizieren, ist das nicht machbar - außer man regelt das über einen Compiler-Switch global für die ganze Lib.

Der schöne Günther 28. Mär 2018 17:16

AW: DLL/Lib: Parameter Validierung
 
Kannst du denn ein abstraktes Beispiel geben? Mir fehlt dazu die Vorstellungskraft. Holt man sich bei deiner APIs denn auch Bezeichner wie z.B. Handles ab und übergibt die später anderen Routinen?

himitsu 28. Mär 2018 17:33

AW: DLL/Lib: Parameter Validierung
 
Zitat:

1. Parameter durch Assertions validieren
2. Parameter validieren und ggfls. mit Error-Code returnen
3. Parameter zuerst mit Assertion validieren und als Fallback mit Error-Code returnen
Ich baue grundsätzlich immer nur eine Art der Fehlerprüfung ein und mische nicht. (also fast wie 2)

Maximal wird intern zusätzlich nochmal ein Fehlerflag (ErrorCode/-Text) gespeichert, welchen man nachträglich nochmal prüfen kann, so ala GetLastError.
Dafür sind grade Assertions da, dass man sie deakivieren kann, damit es schneller geht. Aber wenn man sie deaktiviert, dann hat man auch mit Fehlerverhalten zu rechnen, wenn die Eingaben doch ma nicht stimmen.
Also ist es IMHO sinnlos und kontraproduktiv, wenn zusätzlich noch eine zweite Eingangsprüfung vorhanden wäre. (3)

Und meistens nehme ich inzwischen Exceptions, zur Fehlerbehandlung, anstatt einem Fehlercode-Result.
Dazu definiere ich mir meistens mindestens eine eigene Exceptionklasse, je Library.
Diese Klassen können dann auch Zusatzinfos enthalten, siehe ErrorCode in EOSError oder EInOutError.

Zitat:

Contra:
* Assertions lassen sich abschalten
Das ist doch eher ein Pro, für den Entwickler. :zwinker:


Gerade bei Komponenten sollte man nie Fehler dierekt anzeigen, also bleiben da nur
* Exceptions
* Boolean-Result und interner Statusspeicher ala GetLastError
* oder Fehlercodes

Und Fehlercodes, also quasi MagicNumbers findet ich etwas blöd, darum habe ich mich für Exceptions entschieden.
Sind die Exceptions aber lokalisierbar (ResourceStrings oder sonstwie), dann sollte die Exceptionklasse auch statische Fehlerinfos beinhalten (z.B. immer englischer Text oder Fehlercode).

Zacherl 28. Mär 2018 17:40

AW: DLL/Lib: Parameter Validierung
 
Zitat:

Zitat von Der schöne Günther (Beitrag 1397470)
Kannst du denn ein abstraktes Beispiel geben? Mir fehlt dazu die Vorstellungskraft. Holt man sich bei deiner APIs denn auch Bezeichner wie z.B. Handles ab und übergibt die später anderen Routinen?

Ja, im Grunde genau so.

Wobei das auch nicht wirklich einen Unterschied macht, da die Fragestellung z.B. auch auf eine einfache Wurzel-Funktion anwendbar ist. Sagen wir ich habe einen Algorithmus, der die Wurzel einer Zahl berechnet und der auch für negative Zahlen ein (sinnloses) Ergebnis liefert. Prüfe ich jetzt per Assertion auf Zahlen < 0 oder implementiere ich einen "richtigen" Check, der ggfls. einen Error-Code zurückgibt? Oder kombiniere ich sogar beide Varianten?

Im Falle der Assertion kann es sein, dass der Programmierer meine Funktion immer nur mit positiven Zahlen füttert, der Endanwender aber etwas Negatives übergibt und somit ein unerwartetes Ergebnis bekommen würde (wenn der Entwickler nicht manuell die Benutzereingabe vorher validiert, bevor er sie meiner Funktion übergibt).

Mit Runtime-Checks würde die - zur Entwicklungszeit nicht getestete - Eingabe dazu führen, dass eine Fehlermeldung angezeigt wird (vorrausgesetzt der Entwickler prüft den Error-Code).

Zitat:

Zitat von himitsu (Beitrag 1397474)
Dafür sind grade Assertions da, dass man sie deakivieren kann, damit es schneller geht. Aber wenn man sie deaktiviert, dann hat man auch mit Fehlerverhalten zu rechnen, wenn die Eingaben doch ma nicht stimmen.
Also ist es IMHO sinnlos und kontraproduktiv, wenn zusätzlich noch eine zweite Eingangsprüfung vorhanden wäre.

Mhh, aber dass Assertions im Release Modus aktiviert bleiben, ist (aus gutem Grunde denke ich) in praktisch keiner Standardkonfiguration gegeben. Die zusätzlichen Checks (3) wären ausschließlich hierfür gedacht, um undefiniertes Verhalten im Produktivbetrieb zu vermeiden, auch wenn mal ein Fehler während der Entwicklung nicht erkannt wurde (siehe z.B. das obige Beispiel der Wurzelfunktion).

Zitat:

Zitat von himitsu (Beitrag 1397474)
Und meistens nehme ich inzwischen Exceptions, zur Fehlerbehandlung, anstatt einem Fehlercode-Result.

Ah, ja guter Punkt. Ich hätte vielleicht im Anfangspost erwähnen sollen, dass es sich im Speziellen grade um eine C-Library handelt (also keine Exceptions existieren).

Zitat:

Zitat von himitsu (Beitrag 1397474)
Zitat:

Contra:
* Assertions lassen sich abschalten
Das ist doch eher ein Pro, für den Entwickler. :zwinker:

:-D

Der schöne Günther 28. Mär 2018 17:45

AW: DLL/Lib: Parameter Validierung
 
Was ich immer noch nicht richtig zuordnen kann ist deine Sorge um Return values die der Nutzer vielleicht nicht auswertet und glaubt ein richtiges Ergebnis zu haben. Und glauben die Library sei fehlerhaft und "nicht gut". So etwas ist mir persönlich völlig fremd.

Ich würde, beispielsweise bei einem negativen Handle einen ordentlich dokumentierten Fehlercode zurückgeben. Wer eine Routine nimmt, ohne Doku zu lesen glaubt zu wissen was sie tut und sich dann freut dass es immerhin kompiliert - Damit hätte ich kein Mitleid. Wer dann undefinierte Werte in Ausgabeparametern hat und damit weiterarbeitet ist klar selber schuld.

himitsu 28. Mär 2018 17:56

AW: DLL/Lib: Parameter Validierung
 
Exceptions gibt es im C auch, nur kennt C natürlich die Delphi-Exceptionklassen nicht und kann da dann nichtmal den im unbekannten Message-Text auswerten.

Nja, du kannst natürlich auch LongBool-Results nehmen (in C++ ist der BOOL gern 32 Bit groß),
definierst dir eine Reihe Fehlercodes, nach dem Muster der WinAPI, wie z.B. HRESULT,
und nutzt fleißig MSDN-Library durchsuchenSetLastError. (natürlich nur wenn es auch einen Fehler gab, also nur bei Result=False)

FACILITY ist ein Code für deine Komponente/Library.

Delphi-Quellcode:
{------------------------------}
{     OLE Error Codes         }
{------------------------------}

(*
  The return value of OLE APIs and methods is an HRESULT.
  This is not a handle to anything, but is merely a 32-bit value
  with several fields encoded in the value. The parts of an
  HRESULT are shown below.

  HRESULTs are 32 bit values layed out as follows:

   3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
   1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
  +-+-+-+-+-+---------------------+-------------------------------+
  |S|R|C|N|r|    Facility        |               Code           |
  +-+-+-+-+-+---------------------+-------------------------------+

  where

      S - Severity - indicates success/fail
          0 - Success
          1 - Fail (COERROR)

      R - reserved portion of the facility code, corresponds to NT's
              second severity bit.

      C - reserved portion of the facility code, corresponds to NT's
              C field.

      N - reserved portion of the facility code. Used to indicate a
              mapped NT status value.

      r - reserved portion of the facility code. Reserved for internal
              use. Used to indicate HRESULT values that are not status
              values, but are instead message ids for display strings.

      Facility - is the facility code

      Code - is the facility's status code
*)

const
  SEVERITY_SUCCESS = 0;
  SEVERITY_ERROR = 1;

function Succeeded(Status: HRESULT): BOOL; inline;
function Failed(Status: HRESULT): BOOL; inline;
function IsError(Status: HRESULT): BOOL; inline;
function HResultCode(hr: HRESULT): Integer; inline;
function HResultFacility(hr: HRESULT): Integer; inline;
function HResultSeverity(hr: HRESULT): Integer; inline;
function MakeResult(sev, fac, code: Integer): HResult; inline;

const
  { HRESULT value definitions }
  { Codes $4000-$40ff are reserved for OLE }

  S_OK   = $00000000;
  S_FALSE = $00000001;

  NOERROR = 0;

  E_UNEXPECTED = HRESULT($8000FFFF);
  E_NOTIMPL = HRESULT($80004001);
  E_OUTOFMEMORY = HRESULT($8007000E);
  E_INVALIDARG = HRESULT($80070057);
  E_NOINTERFACE = HRESULT($80004002);
  ...
oder
Delphi-Quellcode:
{ DOS and OS/2 Compatible Error Code definitions returned by the Win32 Base
  API functions. }


{ Translated from WINERROR.H }
{ Error code definitions for the Win32 API functions }

(*
  Values are 32 bit values layed out as follows:
   3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
   1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
  +---+-+-+-----------------------+-------------------------------+
  |Sev|C|R|     Facility         |               Code           |
  +---+-+-+-----------------------+-------------------------------+

  where
      Sev - is the severity code
          00 - Success
          01 - Informational
          10 - Warning
          11 - Error

      C - is the Customer code flag
      R - is a reserved bit
      Facility - is the facility code
      Code - is the facility's status code
*)

{ Define the facility codes }

const
  FACILITY_WINDOWS                    = 8;
  {$EXTERNALSYM FACILITY_WINDOWS}
  FACILITY_STORAGE                    = 3;
  {$EXTERNALSYM FACILITY_STORAGE}
  FACILITY_RPC                        = 1;
  {$EXTERNALSYM FACILITY_RPC}
  FACILITY_SSPI                       = 9;
  {$EXTERNALSYM FACILITY_SSPI}
  FACILITY_WIN32                       = 7;
  {$EXTERNALSYM FACILITY_WIN32}
  FACILITY_CONTROL                    = 10;
  {$EXTERNALSYM FACILITY_CONTROL}
  FACILITY_NULL                       = 0;
  {$EXTERNALSYM FACILITY_NULL}
  FACILITY_INTERNET                   = 12;
  {$EXTERNALSYM FACILITY_INTERNET}
  FACILITY_ITF                        = 4;
  {$EXTERNALSYM FACILITY_ITF}
  FACILITY_DISPATCH                   = 2;
  {$EXTERNALSYM FACILITY_DISPATCH}
  FACILITY_CERT                       = 11;
  {$EXTERNALSYM FACILITY_CERT}

{ Define the severity codes }

  ERROR_SUCCESS = 0;
  NO_ERROR = 0;

  ERROR_INVALID_FUNCTION = 1;
  ERROR_FILE_NOT_FOUND = 2;
  ERROR_PATH_NOT_FOUND = 3;
  ERROR_TOO_MANY_OPEN_FILES = 4;
  ...

Zacherl 28. Mär 2018 18:28

AW: DLL/Lib: Parameter Validierung
 
Zitat:

Zitat von Der schöne Günther (Beitrag 1397477)
Was ich immer noch nicht richtig zuordnen kann ist deine Sorge um Return values die der Nutzer vielleicht nicht auswertet und glaubt ein richtiges Ergebnis zu haben. Und glauben die Library sei fehlerhaft und "nicht gut". So etwas ist mir persönlich völlig fremd.

Das nicht-Auswerten von Fehlercodes ist leider ziemlich häufig - wobei dieser Punkt auch eher nur ein kleines Contra ist. Der zweite Teil bezieht sich auf Assertions. Hier würde es ja dann innerhalb der Library crashen und da gehen manche Entwickler hin und behaupten einfach, dass alles was nicht in ihrem eigenen Code crasht, ein Bug in der Library sein muss. Absolut dämlich, ich weiß.

Zitat:

Zitat von Der schöne Günther (Beitrag 1397477)
Ich würde, beispielsweise bei einem negativen Handle einen ordentlich dokumentierten Fehlercode zurückgeben.

Okay danke, das entspricht ja dann meiner Variante (2), die ich auch momentan verwende.

Zitat:

Zitat von himitsu (Beitrag 1397479)
Exceptions gibt es im C auch, nur kennt C natürlich die Delphi-Exceptionklassen nicht und kann da dann nichtmal den im unbekannten Message-Text auswerten.

Leider nein. Der C Standard sieht keine Exception-Behandlung vor. CPU-Exceptions existieren natürlich weiterhin, aber man kann sie nicht Plattform-/Architektur-unabhängig fangen. Im Falle von C++ hast du Recht (ich verwende allerdings tatsächlich pures C im konkreten Falle).

Zitat:

Zitat von himitsu (Beitrag 1397479)
Nja, du kannst natürlich auch LongBool-Results nehmen (in C++ ist der BOOL gern 32 Bit groß),
definierst dir eine Reihe Fehlercodes, nach dem Muster der WinAPI, wie z.B. HRESULT,
und nutzt fleißig MSDN-Library durchsuchenSetLastError. (natürlich nur wenn es auch einen Fehler gab, also nur bei Result=False)

Danke dir, das entspricht ja auch meiner Variante (2). Die konkrete Umsetzung ist mir noch gar nicht so wichtig im Moment. Mir geht es eher um die Grundsatzdiskussion Assertions vs. "richtigen" Checks und den damit verbundenen (theoretischen) Vor- und Nachteilen.


Alle Zeitangaben in WEZ +1. Es ist jetzt 08:37 Uhr.

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz