Diretriz de programação em C++¶
Introdução¶
Em termos de convenções de programação, o estilo presente num arquivo de código-fonte existente deve ser favorecido em relação aos padrões encontrados abaixo.
Quando um novo código-fonte está sendo criado, as seguintes convenções
de programação devem ser observadas ao criar um novo arquivo dentro do
núcleo do MAME (src/emu
e src/lib
). Caso o código-fonte esteja
fora do núcleo, pode-se dar deferência ao estilo preferido do
contribuidor, embora seja fortemente encorajado a programar entendendo
que o arquivo pode precisar ser compreensível por outras pessoas com o
passar do tempo.
Definições¶
Snake case
Tudo é escrito em minúsculas e os espaços são substituídos por sublinhados:
como_neste_exemplo
Screaming snake case
Tudo é escrito em minúsculas e os espaços são substituídos por sublinhados:
COMO_NESTE_EXEMPLO
Camel case
As palavras ou AS frases são escritas sem espaço onde o início de cada palavra começa com a letra em maiúsculas, menos a primeira:
comoNesteExemplo
Llama case
As palavras as ou frases são escritas sem espaço entre as palavras e onde o início de cada palavra começa com a primeira letra em maiúsculas:
ComoNesteExemplo
Formato do arquivo de código-fonte¶
Os arquivos C++ de código-fonte do MAME estão no formato texto UTF-8, assumindo os caracteres com largura fixa, com paradas de tabulação em intervalos com quatro espaços. Os arquivos de código-fonte devem terminar com um fim de linha. Qualquer texto "unicode" válido e imprimível é permitido nos comentários. Comentários e textos externos, são permitidos apenas o subconjunto "Unicode ASCII" imprimível.
A ferramenta srcclean
é usada para impor regras de formato no
arquivo do código-fonte antes de cada lançamento. É possível compilar
essa ferramenta e aplicá-la aos arquivos que você alterar antes de abrir
uma solicitação "pull" evitando posteriores conflitos ou alterações
inesperadas.
Convenção de nomenclatura¶
Macros do pré-processador
Os nomes das macros devem usar o screaming snake case. As macros são sempre globais e os nomes conflitantes podem causar erros, pense com cuidado sobre o que as macros precisam ser nos cabeçalhos e as nomeie de acordo.
Include guards
A inclusão das guard macros devem começar com
MAME_
e devem terminar com um uma versão em maiúsculas do nome do arquivo, com espaços sendo substituídos por sublinhados.
Constantes
As constantes devem usar o screaming snake case, sejam elas constantes globais, membros de dados constantes, enumeradores ou pré-processadores constantes.
Funções
Os nomes de funções livres devem usar o snake case. Existem alguns utilitários funções que foram implementadas anteriormente como macros dos pré-processadores que ainda usam o screaming snake case.
Classes
Os nomes das classes devem usar um snake case. Os nomes de classes abstratas devem terminar com
_base
. Os membro de funções públicas (incluindo funções de membro estático) devem usar o snake case.
As Classes dos dispositivos
Os nomes específicos da implementação do
driver_device
convencionalmente termina com_state
, enquanto a outra classe do nome do dispositivo específico terminar com_device
. Os nomes específico dodevice_interface
convencionalmente começam comdevice_
e terminam com_interface
.
Os tipos dos dispositivos
Os tipos dos dispositivos devem usar screaming snake case. Lembre-se que os tipos dos dispositivos são nomes dentro do namespace global, então escolha de forma explícita, nomes unívocos e diretos.
As enumerações
O nome da enumeração deve usar maiúsculas e minúsculas. Os enumeradores devem usar screaming snake case.
Os parâmetros usados como modelo
Os parâmetros usados como modelo devem usar maiúsculas e minúsculas (ambos os parâmetros de tipo e de valor).
Os identificadores que tenham dois sublinhados consecutivos ou que comece com um sublinhado seguido de uma letra maiúscula, estão sempre reservados e não podem ser usados.
Os nomes do tipo e dos outros identificadores com um sublinhado à
esquerda, devem ser evitados no espaço de nomes globais (namespace),
pois são reservados de forma explícita de acordo com o padrão C++. Além
disso, os identificadores sufixados com _t
devem ser evitados dentro
do espaço de nomes globais, pois eles também são reservados de acordo
com os padrões POSIX. Embora o MAME viole esta política ocasionalmente,
principalmente com device_t
, é considerado uma infeliz decisão
herdada que deve ser evitada em todo e qualquer novo código.
Variáveis e literais¶
O uso de literais octais é desencorajado fora de casos bem específicos. Eles não possuem os prefixos óbvios com base em letras encontrados nas literais hexadecimais e nos binários, portanto, podem ser difíceis de distinguir rapidamente de um literal decimal para codificadores que não estão familiarizados com a notação octal.
É preferido que seja utilizado os literais hexadecimais em minúsculas,
por exemplo, 0xbadc0de
em vez de 0xBADC0DE
. Para maior clareza,
tente não exceder a largura de bits da variável que será utilizada para
armazená-la.
Os literais binários raramente foram usados no código-fonte do MAME
devido ao prefixo 0b
não ser padronizado até o C++14, mas não há
nenhuma política para evitar a sua utilização.
A notação de sufixo inteiro deve ser usada ao especificar literais de
64 bits, mas não é estritamente necessária em outros casos. É possível,
no entanto, rapidamente deixar claro o uso pretendido de um determinado
literal. Os longos sufixos literais inteiros em maiúsculas devem ser
utilizados para evitar confusão com o dígito 1
, por exemplo 7LL
em vez de 7ll
.
O agrupamento dos dígitos deve ser usado para literais numéricos mais
longos, pois ajuda a reconhecer a ordem de magnitude ou as posições do
campo de bits mais rapidamente. Os literais decimais devem usar grupos
com três dígitos e os literais hexadecimais devem usar grupos com quatro
dígitos, excluindo situações específicas onde diferentes agrupamentos
seriam mais fáceis de entender, por exemplo 4'433'619
ou
0xfff8'1fff
.
Os tipos que não possuam um tamanho especificamente definido, devem ser
evitados caso sejam registrados no sistema "save-state" do MAME, pois
isso prejudica a portabilidade. Em geral, isso significa evitar o uso de
int
para estes membros.
É recomendável, porém não obrigatório, que os membros dos dados da
classe sejam prefixados com m_
nos membros com instância não
estáticos e s_
para membros estáticos. Isso não se aplica as classes
ou às estruturas aninhadas.
Contraventamento e indentação¶
As tabulações são usadas para o recuo inicial das linhas, com uma tabulação usada por nível do escopo agrupado. As declarações que forem divididas em várias linhas devem ser recuadas por duas tabulações. Os espaços são usados para alinhamento em outros lugares dentro de uma linha.
É preferível que a órtese seja no estilo K&R ou no estilo
Allman. Não há uma preferência específica para os colchetes nas
instruções com linha única, embora o colchete deva ser consistente num
determinado bloco if/else
, conforme é mostrado abaixo:
if (x == 0)
{
return;
}
else
{
call_some_function();
x--;
}
Ao utilizar uma série de blocos
if
/else
ou if
/else if
/else
com comentários no recuo
superior, evite novas linhas adicionais. O uso de novas linhas
adicionais pode levar à perda dos blocos else if
ou else
devido
às novas linhas empurrando os blocos para fora da altura visível do
editor:
// O início do seu contador hipotético acabou.
if (x == 0)
{
return;
}
// Devemos fazer algo se o contador estiver em execução.
else
{
call_some_function();
x--;
}
A indentação para as instruções case
dentro de um corpo switch
pode estar no mesmo nível que a instrução switch
ou para dentro um
nível. Não há um estilo específico que seja usado em todos os principais
arquivos, embora o recuo num nível pareça ser usado com mais frequência.
Espaçamento¶
O espaçamento simples e consistente entre os operadores binários, as variáveis e os literais é veementemente recomendado. Os exemplos a seguir exibem um espaçamento razoavelmente consistente:
uint8_t foo = (((bar + baz) + 3) & 7) << 1;
uint8_t foo = ((bar << 1) + baz) & 0x0e;
uint8_t foo = bar ? baz : 5;
Os exemplos a seguir exibem extremos em qualquer direção, embora ter espaços adicionais seja menos difícil de ler do que ter poucos:
uint8_t foo = ( ( ( bar + baz ) + 3 ) & 7 ) << 1;
uint8_t foo = ((bar<<1)+baz)&0x0e;
uint8_t foo = (bar?baz:5);
Um espaço deve ser usado entre uma instrução C++ fundamental e o seu parêntese de abertura, por exemplo:
switch (value) ...
if (a != b) ...
for (int i = 0; i < foo; i++) ...
Escopo¶
O escopo das variáveis devem ser o mais restrito possível. Existem muitas declarações das instâncias da variável local no estilo C89 na base do código do MAME, mas isso é em grande parte um resquício dos primeiros dias do MAME, que antecedem a especificação C99.
Os dois trechos a seguir mostram o estilo legado da declaração da variável local, seguido pelo estilo mais moderno e recomendado:
void dispositivo_exemplo::alguma_funcao()
{
int i;
uint8_t data;
for (i = 0; i < std::size(m_buffer); i++)
{
data = m_buffer[i];
if (data)
{
alguma_outra_funcao(data);
}
}
}
void dispositivo_exemplo::alguma_funcao()
{
for (int i = 0; i < std::size(m_buffer); i++)
{
const uint8_t data = m_buffer[i];
if (data)
{
alguma_outra_funcao(data);
}
}
}
Os valores enumerados, structs
e as classes usadas apenas por um
dispositivo específico, devem ser declarados dentro da própria classe do
dispositivo. Isso evita a poluição do "namespace" global e torna o
uso específico do dispositivo mais óbvio à primeira vista.
Const Correctness¶
A correção const não tem sido historicamente um requisito estrito do código que entra no MAME, mas há um valor crescente nisso à medida que a quantidade de refatoração do código aumenta e a dívida técnica diminui.
Ao escrever um novo código, vale a pena dedicar um tempo para determinar
se uma variável local pode ser declarada como const
. Da mesma forma,
é recomendável considerar quais as funções do membro de uma nova classe
podem ser qualificadas como const
.
Assim como, as matrizes das constantes devem ser declaradas como
constexpr
e devem usar o Screaming Snake Case, conforme é descrito
no início deste documento. Por fim, ambas as arrays das strings no
estilo C devem ser declarados como array const das strings const,
assim:
static const char *const NOMES_EXEMPLO[4] =
{
"1-bit",
"2-bit",
"4-bit",
"Invalid"
};
Comentários¶
Embora /* os comentários em ANSI C */
sejam frequentemente
encontrados na base do código, houve uma alteração gradual para
// comentários no estilo C++
nos casos de comentários com única
linha. Isso é basicamente uma diretriz e os programadores são
encorajados a usar o estilo que for mais confortável.
A menos que citem especificamente o conteúdo de uma máquina ou materiais auxiliares, os comentários devem ser em inglês para corresponder ao idioma predominante que a equipe do MAME compartilha com todos ao redor do mundo.
O código comentado normalmente deve ser removido antes de criar um
pull request, pois há uma tendência de ficar obsoleta devido à
natureza de rápida movimentação da API principal do MAME. Se houver um
desejo conhecido de antemão de que o código eventualmente seja incluído,
ele deve ser marcado em if (0)
ou if (false)
, pois o código
removido por meio de uma macro do pré-processador ficará obsoleta na
mesma velocidade.
Auxiliares específicos do MAME¶
Sempre que possível, use funções auxiliares e macros para operações de manipulação dos bits.
O auxiliar BIT(valor, bit)
pode ser usado para extrair o estado de
um bit numa determinada posição de um valor inteiro. O valor resultante
será alinhado à posição do bit de menor importância, ou seja, será 0
ou 1
.
Uma sobrecarga da mesma função, BIT(valor, bit, largura)
pode ser
usada para extrair um bit do campo de uma determinada largura de um
valor inteiro, começando na posição determinada do bit. O resultado
também será justificado à direita e será do mesmo tipo que o valor da
entrada.
Há, adicionalmente, uma série de auxiliares para funcionalidades como a contagem de zeros/uns à esquerda, para a contagem populada e para a multiplicação e a divisão dos números inteiros assinados/não assinados nos resultados de 32 bits e de 64 bits. Nem todos esses auxiliares têm amplo uso no código base do MAME, mas usá-los num novo código é altamente recomendável quando este código for crítico para questões de desempenho, pois eles utilizam montagem "inline" ou intrínsecos do compilador por plataforma, quando estiverem disponíveis.
count_leading_zeros_32/64(T value)
Aceita um valor não assinado com 32/64 bits e retorna um valor não assinado de 8 bits contendo a quantidade de zeros consecutivos a partir do bit mais importante.
count_leading_ones_32/64(T value)
Funcionalidade idêntica a da anterior, porém, examinando um bit consecutivo.
population_count_32/64(T value)
Aceita um valor com 32/64 bits não assinado e retorna a quantidade encontrada dos bits, ou seja, o peso Hamming do valor.
rotl_32/64(T value, int shift)
Executa um deslocamento circular/barril à esquerda de um valor não assinado com 32/64 bits usando um valor determinado de deslocamento. O valor do deslocamento será mascarado para o intervalo válido de bits para um valor com 32 ou com 64 bits.
rotr_32/64(T value, int shift)
Funcionalidade idêntica a da anterior, mas com o deslocamento à direita.
Para documentação sobre os auxiliares relacionados à multiplicação e
divisão, consulte src/osd/eminline.h
.
Registrando¶
O MAME possuí diversas funções de registro para diferentes propósitos.
Duas das funções de registro log utilizadas com mais frequência são o
logerror
e o osd_printf_verbose
:
Os dispositivos herdam uma função de membro
logerror
. Isso inclui automaticamente a tag totalmente qualificada do dispositivo que invoca as mensagens de registro. A saída é enviada para o registro log rotativo do buffer do depurador do MAME caso o depurador esteja ativado. Se a opção -log estiver ativada, ela também será registrada no arquivoerror.log
dentro do diretório de trabalho. Se a opção -oslog estiver ativada, ela também será enviada para a saída de diagnóstico do sistema operacional (o registro de diagnóstico do host do depurador do Windows, caso um host de depuração esteja conectado ou, caso contrário, usa o modo de erro padrão).A saída da função
osd_printf_verbose
é enviada para o modo de erro padrão caso a opção -verbose esteja ativada.
A função osd_printf_verbose
deve ser usada para fazer o registro que
é muito útil no diagnóstico de problemas do usuário, enquanto o
logerror
deve ser usado para mensagens mais relevantes aos
desenvolvedores (durante o desenvolvendo do próprio MAME ou
desenvolvendo programas para sistemas emulados usando o depurador do
próprio MAME).
Para o registro da depuração, existe um sistema de registro com base em
um canal através do cabeçalho logmacro.h
. Ele pode ser usado como um
sistema de registro genérico, sem a necessidade de usar a sua capacidade
de mascarar canais específicos da seguinte maneira:
// Todos os outros cabeçalhos no arquivo .cpp devem estar acima desta linha.
#define VERBOSE (1)
#include "logmacro.h"
...
void some_device::some_reg_write(u8 data)
{
LOG("%s: some_reg_write: %02x\n", machine().describe_context(), data);
}
O exemplo acima também faz uso de uma função auxiliar que está
disponível em todas que sejam derivadas de
device_t
: machine().describe_context()
. Esta função retornará
uma string que descreve o contexto da emulação onde a função está
sendo executada. Isso inclui a tag totalmente qualificada do
dispositivo que está atualmente em execução (se houver). Caso o
dispositivo relevante implemente um device_state_interface
, ele
também incluirá o valor do contador do programa atual relatado pelo
dispositivo.
Para um controle mais refinado, as máscaras dos bits específicos podem
ser definidos e usados através da macro LOGMASKED
:
// Todos os outros cabeçalhos no arquivo .cpp devem estar acima desta linha.
#define LOG_FOO (1 << 1U)
#define LOG_BAR (1 << 2U)
#define VERBOSE (LOG_FOO | LOG_BAR)
#include "logmacro.h"
...
void some_device::some_reg_write(u8 data)
{
LOGMASKED(LOG_FOO, "some_reg_write: %02x\n", data);
}
void some_device::another_reg_write(u8 data)
{
LOGMASKED(LOG_BAR, "another_reg_write: %02x\n", data);
}
Observe que a posição do bit menos importante para as máscaras
informadas pelo usuário é 1
, pois a posição do bit 0
é reservada
para o LOG_GENERAL
.
É predefinido que LOG
e o LOGMASKED
usarão a função logerror
fornecida pelo dispositivo. No entanto, isso pode ser redirecionado
conforme seja preciso. O caso de uso mais comum seria direcionar a saída
para a saída padrão, o que pode ser feito definindo explicitamente o
LOG_OUTPUT_FUNC
da seguinte maneira:
#define LOG_OUTPUT_FUNC osd_printf_info
Um desenvolvedor deve sempre garantir que a opção VERBOSE
esteja
definido como 0
e que qualquer definição de LOG_OUTPUT_FUNC
seja
comentada antes de abrir um "pull request".
Organização estrutural¶
Todos os arquivos de código-fonte C++ devem começar com dois comentários listando a licença de distribuição e os detentores dos direitos autorais num formato padronizado. As licenças são especificadas por seu identificador SPDX curto, caso esteja disponível. Abaixo um exemplo do formato padrão:
// license:BSD-3-Clause
// copyright-holders:David Haywood, Tomasz Slanina
Os cabeçalhos incluídos geralmente devem ser agrupados do mais dependente ao menos dependente e classificados alfabeticamente dentro dos seus referidos grupos:
O cabeçalho do prefixo do projeto,
emu.h
, deve ser a primeira coisa numa unidade de tradução.Cabeçalhos locais do projeto (cabeçalhos que estão junto com os arquivos de código-fonte por exemplo).
Para os cabeçalhos em
src/devices
.Para os cabeçalhos em
src/emu
.Para os cabeçalhos em
src/lib/util
.Para os cabeçalhos da camada OSD.
Para os cabeçalhos predefinidos da biblioteca C++.
Para os cabeçalhos específicos do sistema operacional.
Para os cabeçalhos layout.
Por fim, os cabeçalhos específicos da tarefa, como o logmacro.h
descritos na seção anterior, eles devem ser incluídos por último. Abaixo
segue um exemplo prático:
#include "emu.h"
#include "cpu/m68000/m68000.h"
#include "machine/mc68328.h"
#include "machine/ram.h"
#include "sound/dac.h"
#include "video/mc68328lcd.h"
#include "video/sed1375.h"
#include "emupal.h"
#include "screen.h"
#include "speaker.h"
#include "pilot1k.lh"
#define VERBOSE (0)
#include "logmacro.h"
Na maioria dos casos, a declaração da classe para um controlador do
sistema, deve estar junto no arquivo do código-fonte correspondente da
implementação. Nesses casos, a declaração da classe e todo o conteúdo do
arquivo de código-fonte, menos a macro GAME
, COMP
ou CONS
,
devem ser colocados num namespace anônimo (isso produz melhores
diagnósticos do compilador, permite uma otimização mais agressiva, reduz
a chance de símbolos duplicados e também reduz o tempo de lincagem).
Dentro de uma declaração da classe, deve haver uma seção para cada nível
de acesso do membro (public
, protected
e private
) quando for
possível. Isso pode não ser possível em casos onde as constantes e/ou os
tipos privados precisam ser declarados antes dos membros públicos. Os
membros devem usar o menor nível de acesso público necessário. As
funções do membro virtual substituídas, geralmente devem usar o mesmo
nível de acesso que a função do membro correspondente da classe base.
As declarações da classe dos membros devem ser agrupados para auxiliar na sua compreensão:
Dentro de uma seção de nível de acesso dos membros, constantes, tipos, membros de dados, funções do membro da instância e funções estáticas do membro devem ser agrupados.
Nas classes dos dispositivos, as funções de configuração do membro devem ser agrupadas separadamente das funções de sinal ativo do membro.
As funções virtuais do membro que forem substituídas, devem ser agrupadas de acordo com as classes base das quais elas forem herdadas.
Para as classes sobrecarregadas com diversos construtores, sempre que possível, a delegação do construtor deve ser usada visando evitar listas repetidas dos inicializadores dos membros.
As constantes que são usadas por um controlador (driver) de
dispositivo ou de uma máquina, devem estar na forma de valores
enumerados com tamanho explícito dentro da declaração da classe ou ser
relegados a macros #define
dentro do arquivo de origem. Isso ajuda a
evitar a poluição do pré-processador.