Estou avaliando uma biblioteca cuja API pública se parece atualmente com esta:
libengine.h
/* Handle, used for all APIs */ typedef size_t enh; /* Create new engine instance; result returned in handle */ int en_open(int mode, enh *handle); /* Start an engine */ int en_start(enh handle); /* Add a new hook to the engine; hook handle returned in h2 */ int en_add_hook(enh handle, int hooknum, enh *h2);
Observe que enh
é um identificador genérico, usado como identificador para vários tipos de dados diferentes ( mecanismos e ganchos ).
Internamente, é claro que a maioria dessas APIs lança o "identificador" para uma estrutura interna que eles malloc
:
engine.c
struct engine { // ... implementation details ... }; int en_open(int mode, *enh handle) { struct engine *en; en = malloc(sizeof(*en)); if (!en) return -1; // ...initialization... *handle = (enh)en; return 0; } int en_start(enh handle) { struct engine *en = (struct engine*)handle; return en->start(en); }
Pessoalmente, odeio esconder as coisas por trás de typedef
s, especialmente quando isso compromete a segurança do tipo. (Dado um enh
, como sei o que realmente está se referindo?)
Então, enviei uma solicitação pull, sugerindo a seguinte alteração na API (depois de modificar a biblioteca inteira para estar em conformidade):
libengine.h
struct engine; /* Forward declaration */
typedef size_t hook_h; /* Still a handle, for other reasons */
/* Create new engine instance, result returned in en */
int en_open(int mode, struct engine **en);
/* Start an engine */
int en_start(struct engine *en);
/* Add a new hook to the engine; hook handle returned in hh */
int en_add_hook(struct engine *en, int hooknum, hook_h *hh);
Obviamente, isso faz com que as implementações internas da API pareçam muito melhores, eliminando conversões e mantendo a segurança do tipo de / para a perspectiva do consumidor.
libengine.c
struct engine
{
// ... implementation details ...
};
int en_open(int mode, struct engine **en)
{
struct engine *_e;
_e = malloc(sizeof(*_e));
if (!_e)
return -1;
// ...initialization...
*en = _e;
return 0;
}
int en_start(struct engine *en)
{
return en->start(en);
}
Eu prefiro isso pelos seguintes motivos:
- Adicionado tipo de segurança
- Maior clareza dos tipos e sua finalidade
- Eliminações removidas
typedef
es - Segue o padrão recomendado para tipos opacos em C
No entanto, o proprietário do projeto recusou a solicitação de recebimento (parafraseada):
Pessoalmente, não gosto da ideia de expor o
struct engine
. Ainda acho que a maneira atual é mais limpa e amigável.Inicialmente, usei outro tipo de dados para identificador de gancho, mas depois decidi mudar para usar
enh
, para que todos os tipos de identificadores compartilhem o mesmo tipo de dados para simplificar. Se isso é confuso, certamente podemos usar outro tipo de dados.Vamos ver o que os outros pensam sobre este PR.
Atualmente, essa biblioteca está em um estágio beta privado, portanto, ainda não há muito código do consumidor com o que se preocupar. Além disso, ofusquei os nomes um pouco.
Como um identificador opaco é melhor do que um struct opaco nomeado?
Nota: Eu fiz essa pergunta na Code Review , onde foi fechada.
fonte
Respostas:
O mantra "simples é melhor" tornou-se dogmático demais. Simples nem sempre é melhor se complica outras coisas. O assembly é simples - cada comando é muito mais simples que os comandos de linguagens de nível superior - e, no entanto, os programas de Assembly são mais complexos que os idiomas de nível superior que fazem a mesma coisa. No seu caso, o tipo de identificador uniforme
enh
simplifica os tipos ao custo de tornar as funções complexas. Como geralmente os tipos de projeto tendem a crescer em uma taxa sub-linear em comparação com suas funções, à medida que o projeto aumenta, você geralmente prefere tipos mais complexos se puder tornar as funções mais simples - portanto, nesse aspecto, sua abordagem parece ser a correta.O autor do projeto está preocupado com o fato de sua abordagem estar " expondo o
struct engine
". Eu teria explicado a eles que não está expondo a estrutura em si - apenas o fato de que existe uma estrutura chamadaengine
. O usuário da biblioteca já precisa estar ciente desse tipo - ele precisa saber, por exemplo, que o primeiro argumento deen_add_hook
é desse tipo e o primeiro argumento é de um tipo diferente. Portanto, na verdade, torna a API mais complexa, porque, em vez de ter a "assinatura" da função, documenta esses tipos, ela precisa ser documentada em outro lugar e porque o compilador não pode mais verificar os tipos para o programador.Uma coisa a ser observada - sua nova API torna o código do usuário um pouco mais complexo, pois em vez de escrever:
Agora eles precisam de uma sintaxe mais complexa para declarar sua manipulação:
A solução, no entanto, é bastante simples:
e agora você pode escrever diretamente:
fonte
struct
são expressamente desencorajadas.typedef struct engine engine;
e usarengine*
: menos um nome introduzido, e isso torna óbvio que é um identificadorFILE*
.Parece haver uma confusão de ambos os lados aqui:
struct
nome não expõe seus detalhes (apenas sua existência)Há vantagens em usar identificadores em vez de ponteiros simples, em uma linguagem como C, porque entregar o ponteiro permite a manipulação direta do apontador (incluindo chamadas para
free
) enquanto entregar um identificador requer que o cliente passe pela API para executar qualquer ação .No entanto, a abordagem de ter um único tipo de identificador, definido por meio de um
typedef
não é seguro, e pode causar muitas mágoas.Minha sugestão pessoal seria, portanto, avançar para digitar alças seguras, o que acho que satisfaria os dois. Isso é realizado de maneira simples:
Agora, não se pode passar acidentalmente
2
como uma alça nem passar uma alça acidentalmente para uma vassoura onde se espera uma alça para o motor.Esse é o seu erro: antes de se envolver em um trabalho significativo em uma biblioteca de código aberto, entre em contato com o (s) autor (es) / mantenedor (es) para discutir a alteração antecipadamente . Isso permitirá que vocês dois concordem com o que fazer (ou não), e evitem trabalhos desnecessários e a frustração resultante disso.
fonte
struct file
partir de umaint fd
. Isso certamente é um exagero para uma IMO da biblioteca em modo de usuário.3
) é liberado, um novo objeto é criado e, infelizmente, está sendo atribuído3
novamente o índice . Simplificando, é difícil ter um mecanismo seguro de vida útil do objeto em C, a menos que a contagem de referência (junto com convenções sobre propriedades compartilhadas de objetos) seja transformada em uma parte explícita do design da API.Aqui está uma situação em que é necessário um identificador opaco;
Quando a biblioteca possui dois ou mais tipos de estrutura que possuem a mesma parte do cabeçalho dos campos, como "tipo" acima, esses tipos de estrutura podem ser considerados como tendo uma estrutura pai comum (como uma classe base em C ++).
Você pode definir a parte do cabeçalho como "mecanismo de estrutura", assim;
Mas é uma decisão opcional, porque as conversões de tipo são necessárias, independentemente do uso do mecanismo struct.
Conclusão
Em alguns casos, há razões pelas quais alças opacas são usadas em vez de estruturas nomeadas opacas.
fonte
switch
uso inicial de "funções virtuais" é provavelmente o ideal e resolve todo o problema.O benefício mais óbvio da abordagem de identificadores é que você pode modificar as estruturas internas sem interromper a API externa. Concedido, você ainda precisa modificar o software cliente, mas pelo menos não está alterando a interface.
A outra coisa que ele faz é fornecer a capacidade de escolher entre vários tipos possíveis em tempo de execução, sem precisar fornecer uma interface API explícita para cada um. Algumas aplicações, como leituras de sensores de vários tipos diferentes de sensores, nas quais cada sensor é um pouco diferente e gera dados um pouco diferentes, respondem bem a essa abordagem.
Como você forneceria as estruturas para seus clientes de qualquer maneira, você sacrifica um pouco da segurança de tipo (que ainda pode ser verificada em tempo de execução) por uma API muito mais simples, embora uma que exija conversão.
fonte
Déjà vu
Encontrei exatamente o mesmo cenário, apenas com algumas diferenças sutis. Tivemos, em nosso SDK, muitas coisas assim:
Minha mera proposta era fazer com que correspondesse aos nossos tipos internos:
Para terceiros que usam o SDK, não deve fazer diferença alguma. É um tipo opaco. Quem se importa? Isso não afeta a ABI * ou a compatibilidade de origem, e o uso de novas versões do SDK exige que o plug-in seja recompilado de qualquer maneira.
* Observe que, como ressalta Gnasher, pode haver casos em que o tamanho de algo como ponteiro para estruturar e anular * pode realmente ter um tamanho diferente, caso em que afetaria a ABI. Como ele, nunca o encontrei na prática. Mas, desse ponto de vista, o segundo poderia realmente melhorar a portabilidade em algum contexto obscuro, de modo que esse é outro motivo para favorecer o segundo, embora provavelmente seja discutível para a maioria das pessoas.
Erros de terceiros
Além disso, eu tinha mais causas do que segurança de tipo para desenvolvimento / depuração interna. Já tínhamos vários desenvolvedores de plug-ins que tinham bugs em seu código porque dois identificadores semelhantes (
Panel
ePanelNew
, por exemplo) usavam umvoid*
typedef para seus identificadores e passavam acidentalmente os identificadores errados para os lugares errados como resultado do uso apenasvoid*
para tudo. Na verdade, estava causando erros do lado daqueles que usavamo SDK. Seus bugs também custam um tempo enorme para a equipe de desenvolvimento interno, já que eles enviavam relatórios de bugs reclamando de bugs em nosso SDK, e teríamos que depurar o plug-in e descobrir que ele foi realmente causado por um bug no plug-in que passava pelas alças erradas para os lugares errados (o que é facilmente permitido sem aviso quando qualquer identificador é um alias paravoid*
ousize_t
). Portanto, estávamos desperdiçando desnecessariamente nosso tempo fornecendo um serviço de depuração para terceiros por causa de erros causados por seu desejo de pureza conceitual em ocultar todas as informações internas, mesmo os meros nomes de nossos internosstructs
.Mantendo o Typedef
A diferença é que eu estava propondo que fazemos vara para a
typedef
gravação clientes ainda, para não terstruct SomeVertex
que iria afetar a compatibilidade fonte para futuros plugin de lançamentos. Embora eu pessoalmente goste da idéia de não digitar o códigostruct
em C, da perspectiva do SDK, issotypedef
pode ajudar, pois o ponto principal é a opacidade. Portanto, sugiro que relaxe esse padrão apenas para a API exposta publicamente. Para clientes que usam o SDK, não importa se um identificador é um ponteiro para uma estrutura, um número inteiro etc. A única coisa que importa para eles é que dois identificadores diferentes não usam o mesmo tipo de dados para que não passe incorretamente a alça errada para o lugar errado.Tipo Informação
Onde é mais importante evitar a transmissão é para você, os desenvolvedores internos. Esse tipo de estética de ocultar todos os nomes internos do SDK é uma estética conceitual que tem um custo significativo de perda de todas as informações de tipo e exige que borrifemos desnecessariamente os lançamentos em nossos depuradores para obter informações críticas. Embora um programador em C deva estar amplamente acostumado a isso em C, exigir isso desnecessariamente é apenas pedir problemas.
Ideais conceituais
Em geral, você deve estar atento aos tipos de desenvolvedores que colocam alguma idéia conceitual de pureza acima de todas as necessidades práticas e diárias. Isso levará a manutenção da sua base de código à busca de um ideal utópico, fazendo com que toda a equipe evite loção bronzeadora em um deserto por medo de que não seja natural e possa causar uma deficiência de vitamina D enquanto metade da equipe estiver morrendo de câncer de pele.
Preferência do usuário final
Mesmo do ponto de vista estrito do usuário para quem usa a API, eles preferem uma API de buggy ou uma API que funcione bem, mas exponha algum nome com o qual dificilmente poderiam se importar em troca? Porque essa é a troca prática. Perder informações de tipo desnecessariamente fora de um contexto genérico está aumentando o risco de bugs e, a partir de uma base de código em larga escala em um ambiente de toda a equipe ao longo de vários anos, a lei de Murphy tende a ser bastante aplicável. Se você aumentar exageradamente o risco de erros, é provável que você tenha pelo menos mais alguns erros. Em uma equipe grande, não demora muito tempo para descobrir que todo tipo de erro humano imaginável acabará por se transformar em potencial em realidade.
Talvez essa seja uma pergunta a ser feita aos usuários. "Você prefere um SDK de buggier ou um que exponha alguns nomes internos opacos com os quais você nunca se importará?" E se essa pergunta parece apresentar uma falsa dicotomia, eu diria que é necessária mais experiência em toda a equipe em um cenário de grande escala para apreciar o fato de que um maior risco de bugs acabará por manifestar bugs reais a longo prazo. Pouco importa o quanto o desenvolvedor esteja confiante em evitar bugs. Em um ambiente para toda a equipe, ajuda mais a pensar nos links mais fracos e, pelo menos, nas maneiras mais fáceis e rápidas de impedir que tropeçam.
Proposta
Portanto, sugiro um compromisso aqui que ainda lhe permita manter todos os benefícios da depuração:
... mesmo à custa da digitação
struct
, isso realmente nos matará? Provavelmente não, por isso recomendo um pouco de pragmatismo da sua parte, mas mais ainda ao desenvolvedor que preferiria tornar a depuração exponencialmente mais difícil usandosize_t
aqui e convertendo para / do número inteiro por nenhuma boa razão, exceto para ocultar mais as informações que já estão disponíveis. % oculto para o usuário e não pode causar mais mal quesize_t
.fonte
void*
ousize_t
e volta como outra razão para evitar vazamento redundante. Eu meio que o omiti, já que também nunca o vi na prática, dadas as plataformas que visamos (que sempre eram plataformas de desktop: linux, OSX, Windows).typedef struct uc_struct uc_engine;
Eu suspeito que o verdadeiro motivo é a inércia, é o que eles sempre fizeram e funciona. Por que mudar isso?
A principal razão que eu posso ver é que o identificador opaco permite que o designer coloque qualquer coisa atrás dele, não apenas uma estrutura. Se a API retornar e aceitar vários tipos opacos, todos terão a mesma aparência para o chamador e nunca haverá problemas de compilação ou recompilação se a cópia fina for alterada. Se en_NewFlidgetTwiddler (handle ** newTwiddler) mudar para retornar um ponteiro para o Twiddler em vez de um identificador, a API não será alterada e qualquer novo código silenciosamente estará usando um ponteiro onde antes estava usando um identificador. Além disso, não há perigo do sistema operacional ou de qualquer outra coisa "consertar" silenciosamente o ponteiro se ele passar através dos limites.
A desvantagem disso, é claro, é que o chamador pode alimentar tudo. Você tem uma coisa de 64 bits? Coloque-o no slot de 64 bits na chamada da API e veja o que acontece.
Ambos compilam, mas aposto que apenas um deles faz o que você deseja.
fonte
Acredito que a atitude deriva de uma filosofia de longa data de defender uma API da biblioteca C de abuso por iniciantes.
Em particular,
memcpy
os dados opacos ou incrementarão os bytes ou palavras dentro da estrutura. Vá hackear.A contramedida tradicional de longa data é:
void*
struct engine* peng = (struct engine*)((size_t)enh ^ enh_magic_number);
Isso é apenas para dizer que tem longas tradições; Eu não tinha opinião pessoal sobre se é certo ou errado.
fonte