Problemas de diseño de API en C [cerrado]

10

¿Cuáles son algunos defectos que te vuelven loco en las API de C (incluidas las bibliotecas estándar, las bibliotecas de terceros y los encabezados dentro de un proyecto)? El objetivo es identificar las dificultades de diseño de API en C, para que las personas que escriben nuevas bibliotecas en C puedan aprender de los errores del pasado.

Explique por qué el defecto es malo (preferiblemente con un ejemplo) e intente sugerir una mejora. Aunque es posible que su solución no sea práctica en la vida real (es demasiado tarde para solucionarla strncpy), debería avisar a futuros escritores de bibliotecas.

Aunque el foco de esta pregunta son las API de C, los problemas que afectan su capacidad de usarlos en otros lenguajes son bienvenidos.

Dé una falla por respuesta, para que la democracia pueda clasificar las respuestas.

Joey Adams
fuente
3
Joey, esta pregunta está a punto de no ser constructiva al pedir que construyas una lista de cosas que la gente odia. Hay potencial aquí para que la pregunta sea útil si las respuestas explican por qué las prácticas que señalan son malas y brindan información detallada sobre cómo mejorarlas. Con ese fin, mueva su ejemplo de la pregunta a una respuesta propia y explique por qué es un problema / cómo una malloccadena 'd lo solucionaría. Creo que dar un buen ejemplo con la primera respuesta realmente podría ayudar a que esta pregunta prospere. ¡Gracias!
Adam Lear
1
@ Anna Lear: Gracias por decirme por qué mi pregunta era problemática. Intenté mantenerlo constructivo pidiendo un ejemplo y una alternativa sugerida. Supongo que realmente necesitaba algunos ejemplos para indicar lo que tenía en mente.
Joey Adams
@Joey Adams Míralo de esta manera. Estás haciendo una pregunta que se supone que resuelve "automáticamente" los problemas de la API C de manera general. Donde los sitios como StackOverflow fueron diseñados para funcionar de manera tal que los problemas más comunes con la programación se encuentren y respondan fácilmente. StackOverflow naturalmente dará como resultado una lista de respuestas para su pregunta, pero de una manera más estructurada y fácil de buscar.
Andrew T Finnell
Voté para cerrar mi propia pregunta. Mi objetivo era tener una colección de respuestas que pudieran servir como una lista de verificación contra las nuevas bibliotecas de C. Hasta ahora, las tres respuestas usan palabras como "inconsistente", "ilógico" o "confuso". No se puede determinar objetivamente si una API viola o no alguna de estas respuestas.
Joey Adams

Respuestas:

5

Funciones con valores de retorno inconsistentes o ilógicos. Dos buenos ejemplos:

1) Algunas funciones de Windows que devuelven un HANDLE usan NULL / 0 para un error (CreateThread), algunas usan INVALID_HANDLE_VALUE / -1 para un error (CreateFile).

2) La función POSIX 'time' devuelve '(time_t) -1' en caso de error, lo cual es realmente ilógico ya que 'time_t' puede ser un tipo con signo o sin signo.

David Schwartz
fuente
2
En realidad, time_t está (generalmente) firmado. Sin embargo, llamar al 31 de diciembre de 1969 "inválido" es bastante ilógico. Supongo que los años 60 fueron difíciles :-) En serio, una solución sería devolver un código de error y pasar el resultado a través de un puntero, como en: int time(time_t *out);y BOOL CreateFile(LPCTSTR lpFileName, ..., HANDLE *out);.
Joey Adams
Exactamente. Es extraño si time_t no está firmado y si time_t está firmado, invalida una vez en medio de un océano de validos.
David Schwartz
4

Funciones o parámetros con nombres no descriptivos o afirmativamente confusos. Por ejemplo:

1) CreateFile, en la API de Windows, en realidad no crea un archivo, crea un identificador de archivo. Puede crear un archivo, al igual que la lata 'abierta', si se le solicita a través de un parámetro. Este parámetro tiene valores llamados 'CREATE_ALWAYS' y 'CREATE_NEW' cuyos nombres ni siquiera insinúan su semántica. (¿'CREATE_ALWAYS' significa que falla si el archivo existe? ¿O crea un nuevo archivo encima? ¿'CREATE_NEW' significa que siempre crea un nuevo archivo y falla si el archivo ya existe? ¿O crea uno nuevo? archivo encima de él?)

2) pthread_cond_wait en la API POSIX pthreads, que a pesar de su nombre, es una espera incondicional .

David Schwartz
fuente
1
El cond en pthread_cond_waitno significa "condicionalmente esperar". Se refiere al hecho de que está esperando una variable de condición .
Jonathon Reinhart
Correcto, es una espera incondicional para una condición.
David Schwartz
3

Tipos opacos que se pasan a través de la interfaz como identificadores de tipo eliminado. El problema es, por supuesto, que el compilador no puede verificar el código de usuario para los tipos de argumento correctos.

Esto viene en varias formas y sabores, que incluyen, entre otros:

  • void* abuso

  • utilizando intcomo un manejador de recursos (ejemplo: la biblioteca CDI)

  • argumentos escritos a máquina

Los tipos más distintos (= no se pueden usar de manera totalmente intercambiable) se asignan al mismo tipo de tipo eliminado, peor. Por supuesto, el remedio es simplemente proporcionar punteros opacos de tipo seguro a lo largo de las líneas de (ejemplo C):

typedef struct Foo Foo;
typedef struct Bar Bar;

Foo* createFoo();
Bar* createBar();

int doSomething(Foo* foo);
void somethingElse(Foo* foo, Bar* bar);

void destroyFoo(Foo* foo);
void destroyBar(Bar* bar);
cmaster - restablecer monica
fuente
2

Funciones con convenciones de retorno de cadena inconsistentes y a menudo engorrosas.

Por ejemplo, getcwd solicita un búfer proporcionado por el usuario y su tamaño. Esto significa que una aplicación tiene que establecer un límite arbitrario en la longitud actual del directorio o hacer algo como esto ( desde CCAN ):

 /* *This* is why people hate C. */
len = 32;
cwd = talloc_array(ctx, char, len);
while (!getcwd(cwd, len)) {
    if (errno != ERANGE) {
        talloc_free(cwd);
        return NULL;
    }
    cwd = talloc_realloc(ctx, cwd, char, len *= 2);
}

Mi solución: devolver una malloccadena ed. Es simple, robusto y no menos eficiente. Exceptuando plataformas integradas y sistemas más antiguos, en mallocrealidad es bastante rápido.

Joey Adams
fuente
44
No llamaría a esta mala práctica, llamaría a esta buena práctica. 1) Es tan común que ningún programador debería sorprenderse. 2) Deja la asignación a la persona que llama, lo que excluye numerosas posibilidades de errores de pérdida de memoria. 3) Es compatible con buffers asignados estáticamente. 4) Hace que la implementación de la función sea más limpia, una función que calcula alguna fórmula matemática no debería preocuparse por algo completamente no relacionado, como la asignación dinámica de memoria. Crees que main se vuelve más limpio pero la función se vuelve más desordenada. 5) malloc ni siquiera está permitido en muchos sistemas.
@Lundin: El problema es que lleva a los programadores a crear límites innecesarios codificados, y tienen que esforzarse mucho para no hacerlo (ver el ejemplo anterior). Está bien para cosas como snprintf(buf, 32, "%d", n), donde la longitud de salida es predecible (ciertamente no más de 30, a menos que intsea realmente enorme en su sistema). De hecho, malloc no está disponible en muchos sistemas, pero sí lo es para entornos de escritorio y servidor, y funciona realmente bien.
Joey Adams
Pero el problema es que la función en su ejemplo no establece límites codificados. Un código como este no es una práctica común. Aquí, main sabe cosas sobre la longitud del búfer que la función debería haber conocido. Todo sugiere un mal diseño del programa. Main no parece saber qué hace incluso la función getcwd, por lo que está utilizando alguna asignación de "fuerza bruta" para averiguarlo. En algún lugar, la interfaz entre el módulo en el que reside getcwd y la persona que llama está confusa. Eso no significa que esta forma de llamar a las funciones sea mala, por el contrario, la experiencia demuestra que es buena por las razones que ya mencioné.
1

Funciones que toman / devuelven tipos de datos compuestos por valor, o que usan devoluciones de llamada.

Peor aún si dicho tipo es una unión o contiene campos de bits.

Desde la perspectiva de una persona que llama en C, en realidad están bien, pero no escribo en C o C ++ a menos que sea necesario, por lo que generalmente llamo a través de una FFI. La mayoría de las FFI no admiten sindicatos o campos de bits, y algunos (como Haskell y MLton) no pueden admitir estructuras pasadas por valor. Para aquellos que pueden manejar estructuras por valor, al menos Common Lisp y LuaJIT se ven obligados a rutas lentas: la interfaz de función externa común de Lisp debe hacer una llamada lenta a través de libffi, y LuaJIT se niega a compilar JIT la ruta de código que contiene la llamada. Las funciones que pueden volver a llamar a los hosts también activan rutas lentas en LuaJIT, Java y Haskell, ya que LuaJIT no puede compilar dicha llamada.

Demi
fuente