Armadilhas de design de API em C [fechado]

10

Quais são algumas falhas que o deixam louco nas APIs C (incluindo bibliotecas padrão, bibliotecas de terceiros e cabeçalhos dentro de um projeto)? O objetivo é identificar as armadilhas do design da API em C, para que as pessoas que escrevem novas bibliotecas em C possam aprender com os erros do passado.

Explique por que a falha é ruim (de preferência com um exemplo) e tente sugerir uma melhoria. Embora sua solução possa não ser prática na vida real (é muito tarde para consertar strncpy), ela deve ser um alerta para futuros escritores de bibliotecas.

Embora o foco desta pergunta sejam as APIs C, problemas que afetam sua capacidade de usá-los em outros idiomas são bem-vindos.

Por favor, dê uma falha por resposta, para que a democracia possa classificar as respostas.

Joey Adams
fonte
3
Joey, essa questão está prestes a não ser construtiva, pedindo para criar uma lista de coisas que as pessoas odeiam. Aqui, existe potencial para a pergunta ser útil se as respostas explicarem por que as práticas que estão apontando são ruins e fornecerem informações detalhadas sobre como melhorá-las. Para esse fim, mova seu exemplo da pergunta para uma resposta própria e explique por que é um problema / como uma mallocstring seria corrigida. Eu acho que dar um bom exemplo com a primeira resposta poderia realmente ajudar essa questão a prosperar. Obrigado!
Adam Lear
11
@ Anna Lear: Obrigado por me dizer por que minha pergunta foi problemática. Eu estava tentando mantê-lo construtivo, pedindo um exemplo e sugerindo uma alternativa. Acho que realmente precisava de alguns exemplos para indicar o que tinha em mente.
Joey Adams
@ Joey Adams Veja desta maneira. Você está fazendo uma pergunta que deveria "automaticamente" resolver problemas de API C de uma maneira geral. Onde sites como o StackOverflow foram projetados para funcionar de modo que os problemas mais comuns com a programação sejam facilmente encontrados E respondidos. O StackOverflow resultará naturalmente em uma lista de respostas para sua pergunta, mas de uma maneira mais estruturada e facilmente pesquisável.
Andrew T Finnell
Votei para encerrar minha própria pergunta. Meu objetivo era ter uma coleção de respostas que pudessem servir como uma lista de verificação para novas bibliotecas C. Até agora, as três respostas usam palavras como "inconsistente", "ilógico" ou "confuso". Não se pode determinar objetivamente se uma API viola ou não alguma dessas respostas.
Joey Adams

Respostas:

5

Funções com valores de retorno inconsistentes ou ilógicos. Dois bons exemplos:

1) Algumas funções do Windows que retornam um HANDLE usam NULL / 0 para um erro (CreateThread), outras usam INVALID_HANDLE_VALUE / -1 para um erro (CreateFile).

2) A função POSIX 'time' retorna '(time_t) -1' em erro, o que é realmente ilógico, pois 'time_t' pode ser um tipo assinado ou não assinado.

David Schwartz
fonte
2
Na verdade, time_t é (geralmente) assinado. No entanto, chamar 31 de dezembro de 1969 de "inválido" é bastante ilógico. Eu acho que os anos 60 foram difíceis :-) Para seriedade, uma solução seria retornar um código de erro e passar o resultado por um ponteiro, como em: int time(time_t *out);e BOOL CreateFile(LPCTSTR lpFileName, ..., HANDLE *out);.
Joey Adams
Exatamente. É estranho que time_t não esteja assinado e se time_t for assinado, isso invalida uma vez no meio de um oceano de vales válidos.
David Schwartz
4

Funções ou parâmetros com nomes não descritivos ou afirmativamente confusos. Por exemplo:

1) CreateFile, na API do Windows, na verdade não cria um arquivo, ele cria um identificador de arquivo. Ele pode criar um arquivo, exatamente como o 'open' pode, se solicitado através de um parâmetro. Este parâmetro possui valores chamados 'CREATE_ALWAYS' e 'CREATE_NEW' cujos nomes nem sequer sugerem sua semântica. ('CREATE_ALWAYS' significa que falha se o arquivo existe? Ou cria um novo arquivo em cima dele? 'CREATE_NEW' significa que ele cria sempre um novo arquivo e falha se o arquivo já existe? arquivo em cima dele?)

2) pthread_cond_wait na API POSIX pthreads, que apesar do nome, é uma espera incondicional .

David Schwartz
fonte
11
O cond in pthread_cond_waitnão significa "espera condicional". Refere-se ao fato de que você está aguardando uma variável de condição .
Jonathon Reinhart
Certo, é uma espera incondicional por uma condição.
David Schwartz
3

Tipos opacos que são transmitidos pela interface como identificadores excluídos do tipo. O problema é, obviamente, que o compilador não pode verificar o código do usuário para tipos de argumentos corretos.

Isso ocorre de várias formas e sabores, incluindo, entre outros:

  • void* Abuso

  • usando intcomo um identificador de recurso (exemplo: a biblioteca CDI)

  • argumentos digitados em sequência

Os tipos mais distintos (= não podem ser usados ​​de forma totalmente intercambiável) são mapeados para o mesmo tipo excluído, o pior. Obviamente, o remédio é simplesmente fornecer ponteiros opacos tipicamente seguros ao longo das linhas de (exemplo 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 - restabelece monica
fonte
2

Funções com convenções de retorno de string inconsistentes e muitas vezes complicadas.

Por exemplo, getcwd solicita um buffer fornecido pelo usuário e seu tamanho. Isso significa que um aplicativo precisa definir um limite arbitrário no tamanho do diretório atual ou fazer algo assim ( do 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);
}

Minha solução: retornar uma mallocstring ed. É simples, robusto e não menos eficiente. Exceto plataformas embarcadas e sistemas mais antigos, mallocé realmente muito rápido.

Joey Adams
fonte
4
Eu não chamaria isso de má prática, chamaria isso de boa prática. 1) É tão comum que nenhum programador se surpreenda com isso. 2) Deixa a alocação para o chamador, o que exclui inúmeras possibilidades de erros de vazamento de memória. 3) É compatível com buffers alocados estaticamente. 4) Torna a implementação da função mais limpa, uma função que calcula alguma fórmula matemática não deve se preocupar com algo totalmente não relacionado, como alocação dinâmica de memória. Você acha que main fica mais limpo, mas a função fica mais bagunçada. 5) o malloc nem é permitido em muitos sistemas.
@Lundin: O problema é que isso leva os programadores a criar limites desnecessários codificados, e eles precisam se esforçar muito para não (veja o exemplo acima). É bom para coisas como snprintf(buf, 32, "%d", n), onde o comprimento da saída é previsível (certamente não superior a 30, a menos que intseja realmente grande no seu sistema). De fato, o malloc não está disponível em muitos sistemas, mas para ambientes de desktop e servidor, é, e funciona muito bem.
Joey Adams
Mas o problema é que a função no seu exemplo não define limites codificados. Código como este não é uma prática comum. Aqui, main sabe coisas sobre o comprimento do buffer que a função deveria saber. Tudo isso sugere um design inadequado do programa. O Main não parece saber o que a função getcwd faz, por isso está usando alguma alocação de "força bruta" para descobrir. Em algum lugar a interface entre o módulo no qual o getcwd reside e o chamador está confusa. Isso não significa que esse modo de chamar funções seja ruim, pelo contrário, a experiência mostra que é bom pelos motivos que eu já listei.
1

Funções que recebem / retornam tipos de dados compostos por valor ou que usam retornos de chamada.

Pior ainda, se esse tipo for uma união ou contiver campos de bits.

Do ponto de vista de um chamador em C, eles são realmente bons, mas eu não escrevo em C ou C ++, a menos que seja necessário, por isso costumo ligar através de um FFI. A maioria das FFIs não suporta uniões ou campos de bits, e algumas (como Haskell e MLton) não podem suportar estruturas passadas por valor. Para aqueles que podem lidar com estruturas de valor, pelo menos Common Lisp e LuaJIT são forçados a caminhos lentos - a Common Foreign Function Interface da Lisp deve fazer uma chamada lenta via libffi, e LuaJIT se recusa a JIT-compilar o caminho de código que contém a chamada. As funções que podem retornar aos hosts também acionam caminhos lentos no LuaJIT, Java e Haskell, com o LuaJIT não sendo capaz de compilar essa chamada.

Demi
fonte